next up previous contents
suivant: À propos de ce monter: Annexes précédent: Annexes   Table des matières

Sous-sections

RTLinux device driver for Computer Boards CIO-PDISO16 digital I/O board

Introduction

The CIO- PDISO16 is a digital I/O board that has 2*8 relay channels and 2*8 digital inputs. It is designed for control and sensing applications where a few points of high voltage need to be sensed or controlled. The 16 outputs are electromechanical relays. The contacts are rated at 6A @ 120V A. C. or 28V D. C., resistive load. The relays are controlled by writing to two 8 -bit ports. The state of the relays may be determined by a read from the control port address. There are 16 individual, optically isolated (500V) inputs that may be read back as two 8- bit bytes. The inputs are not polarity sensitive and may be driven by either A. C. (50 - 1000 Hz) or D. C. in the range 5V - 24V. Programming is accomplished by writes and reads to the 8- bit ports. Each bit indicates the state of an input or controls an output. Pdiso16 is the RTLinux device driver written by Integrated Real Time Systems (IRTS). The driver has been implemented as a Linux loadable module for RTLinux 2.2a (2.2.14 Linux kernel). This document explains the functionalities of the pdiso16 device driver and the programmer's C-interface library for it.

http://www.computerboards.com www.computerboards.com

The pdiso16 device driver main features are :

A RTLinux thread can use the pdiso16 device driver through the posixio API. Thus, calling well known open(), close(), read(), write() and ioctl() functions from a rt-thread, the user can access all the resources of the pdiso16 board.

A Linux user space process can access the pdiso16 device driver using 2 RT-FIFO: one to send instructions, the other to receive a response or acknowledgment. It just have to use the communication structure described in the com.h header file: Message_struct. The response or acknowledgment to its answers are also sent in such a structure.

Note that the process have to use specialized command codes and ioctl() command constants to communicate with the device driver.

User can set/read any relay state using the read() write() functions. The port number is specified by a call to ioctl() function with the CHANGE_PORT command and the PORT1/PORT2 argument. A precise channel can be accessed through logical opérations.

Note that the LSB of the byte is always associated with the lowest channel of the group.

User can set/read all 16 channels' state using the read() write() functions. This mode can be specified by a call to ioctl() function with the BOTH_PORTS command.

Note that the LSB of the byte is always associated with the lowest channel of the group, the lowest byte is associated with PORT1 and the higher with PORT2 (!).

User can read any differential digital input channel's state using an ioctl() call with the DIGITAL_INPUT command. Result is returned in the arg field of the ioctl() function. User can ask a single port or a word-wise reading using the same ioctl() commands : CHANGE_PORT or BOTH_PORTS.

Note that the LSB of the byte is always associated with the lowest channel of the group, the lowest byte is associated with PORT1 and the higher with PORT2 (!).

pdiso16 kernel module

Loading (init_module)

Mechanism

Linux kernel modules are specially made to be pieces of kernel that can be loaded and unloaded dynamically, while the kernel is running. These appear as object files (modname.o) and are loaded with the command insmod modname.o [arguments]. This operation runs the initialization of the device(s) and gives a major number to the device driver. This one can be found in the /proc/devices file. The command can receive many arguments specific to the module.

After being loaded, the device driver module must be associated with devices files which will be used by user programs. This is made with the command : mknod devname c major minor. This action can be avoided, if this file is already created with its proper major and minor numbers.

Parameters

An optional parameters can be specified when you load the pdiso16 device driver with the insmod command. The loading command will looks like (run being root):

insmod pdiso16.o [PDISO16_base_adr=adr | PDISO16_major=major]

PDISO16_base_adr:
As The cio-pdiso16 board can be installed at several address location, the base address switch sets the starting I/O location where the CPU can access the registers of the board. The factory default is 300 hex (768 decimal). If you already have a board installed at address 300 hex, choose a new address from those available on your computer and set the switches. User can specify board address by adding PDISO16_base_adr=adr at the end of the loading command. It must be presented in a hex format (e.g. : PDISO16_base_adr=0x300. In case of no PDISO16_base_adr parameter, a board with the default address IO_BOARD_ADR (specified in the pdiso16.h header file) will try to be loaded. To see which I/O space is already use by devices you can look at the /proc/ioports file.
PDISO16_major:
The major number for the pdiso16 device driver can be set to another value. In case of no PDISO16_major parameter, PDISO16_MAJOR is taken from the pdiso16.h header file.

Initialization

The device driver has to register itself with Linux and RTLinux. The loading process begins with a call to pdiso16_init() function dedicated to general purpose initialization (Linux I/O region checking, register device...). Then, it has to deal with RTLinux stuff: register using posixio API, set up fifo's and initialize its base address and communication state (PORT1 byte-wise behavior)

Errors

Error can appear during the module loading. This error may be caused by invalid loading parameters or by the fact that module is already running on system.

In case of error, you should check if the module is not already running and if all the required resources are free (see the Special files section). You can also use the dmesg command to see debug or error messages (see the Debug options section).

Unloading (cleanup_module)

Mechanism

As you can dynamically load your kernel module, you can also unload it when you want using the command rmmod modname. You can check its name calling the useful lsmod command.

Errors

Nevertheless, the module will be unloaded only if all the processes/threads have been closed before. This is done with the driver's release() function call which is called by the generic close() function. Finally, the call may look like close(FD). If some processes/threads are still using the device driver when you try to unload it, the kernel will display a 'busy device or resource' message on console.

Ioctl() calls

Description

Before making an ioctl() call to a special file (device driver description file in our case), the device must have been opened by the RTLinux task, using the driver's open() function call which may look like fd = open("/dev/pdiso", O_NONBLOCK).

Then to make any ioctl() call user has to indicate the file descriptor (int FD) that has been returned by the open() function, a command parameter (unsigned char cmd) and, if required, an argument parameter. The call then may look like err=ioctl(FD, cmd, arg) where err is an integer returned by the function. In the pdiso16 device driver, arg is required while reading digital inputs thus, the ioctl() function waits for well sized buffer . User must then provide a buffer address to the function. Used with CHANGE_PORT command, arg takes two values: PORT1 or PORT2 constants.

This section explains the specifications of cmd and arg parameters and the returned values of the ioctl() function.

Command parameter (cmd)

This unsigned char parameter is used to indicate to the driver which port(s) (CHANGE_PORT, BOTH_PORTS) you want to read or to write on the pdiso16 board. You can also indicate that you want to read digital inputs of your board (DIGITAL_INPUT).

cmd=CHANGE_PORT:
read()/write() functions applied on relay port given in arg field (byte-wise bit manipulation)
cmd=BOTH_PORTS:
read()/write() functions applied on both relay ports (word-wise bit manipulation)
cmd=DIGITAL_INPUT:
read differential digital inputs, result stored at address indicated by the arg parameter.
CHANGE_PORT, BOTH_PORTS and DIGITAL_INPUT are unsigned char (u8) values declared on com.h:

/*com.h*/

/*ioctl() cmd constants*/ 

#define CHANGE_PORT 0xA1 

#define BOTH_PORTS 0xA2 

#define DIGITAL_INPUT 0xA3

Argument parameters (arg)

The arg parameter is used, on the one hand, to select a port when theCHANGE_PORT command is used, on the other hand, to get back the value of the digital input channels.

Selecting port constants

In com.h header file:

/*ioctl() arg constants*/

#define PORT1 0

#define PORT2 1

Example: want to read port2 relay state ?

u8 buffer;   /* u8: byte-wise variable ( = unsigned char ) */ 
int err, n;  
... 
err=ioctl(fd, CHANGE_PORT, PORT2); 
if (err == 0) { 
    n = read (fd, &buffer, sizeof(buffer)); 
    if (n > 0) rtl_printf("port2 0x%x\n", buffer ); 
}

Reading the digital input channels

I decided to use an ioctl() call to read these I/O ports instead of adding another device. Although it can be done easily... The command used is DIGITAL_INPUT to signal the driver it has to read its digital input channels and to get back the value in the result buffer.

Note that the result buffer must be sized according to the number of port you want to read (byte-wise for an only port reading, word-wise for a dual port reading).

Example: want to read port1 then both ports digital input state ?

u8    read_buffer_single_port; 
u16  read_buffer_double_port; 
... 
ioctl(fd, CHANGE_PORT, PORT1);  
if (ioctl(fd, DIGITAL_INPUT, &read_buffer_single_port)==0) 
    rtl_printf("Digital input PORT1 0x%x\n", read_buffer_single_port); 
 
ioctl(fd, BOTH_PORTS); 
if (ioctl(fd, DIGITAL_INPUT, &read_buffer_double_port)==0)  
    rtl_printf("Digital input PORT1 0x%x\n", read_buffer_double_port);

Returned value

The ioctl() call returns 0 on success or -1 on fail. In case of fail, errno values are standardized by the include file <asm/errno.h> so that you can know what kind of problem has occurred.

If the driver has the required debug level, you can also use the command dmesg to see in details where and why the ioctl() call has failed.

Write() call

Description

Writing the pdiso16 digital I/O board means to change the state of the relay channels, ie: to change the connection between pins of the selected relay.

Before making a write() call to a special file (device driver description file in our case), the device must have been opened by the RTLinux task, using the driver's open() function call which may look like fd = open("/dev/pdiso", O_NONBLOCK).

Then to make any write() call user has to indicate the file descriptor (int fd) that has been returned by the open() function, a command buffer and, its size. The call then may look like nbwr=write(fd, &buffer, sizeof(buffer)) where nbwr is an integer returned by the function: the number of bytes written. In the pdiso16 device driver, user can give a byte-wise or word-wise buffer according to his choice of a single port reading or a dual one.

Controlling relay channels

When a 1 is written to the output, the common and the NO ( Normally Open ) pins of the relay, are in contact. User can switch a relay state setting a 1 or 0 to the right place within the byte or word wise buffer. Then the buffer is written down to the board's register by the device driver's write() call...

#define CHANNEL0 0x01 
#define CHANNEL1 0x02 
... 
#define CHANNEL7 0x80 
... 
int n; 
u8 buffer = 0x00; 
... 
ioctl(fd, CHANGE_PORT, PORT1);  
buffer |= CHANNEL0;     /*Turn on channel 0 ( Less Significant Bit of the byte)*/ 
buffer &=  CHANNEL1;    /*Turn off channel1*/ 
... 
n = write(fd, &buffer, sizeof(buffer));
Of course, direct write is the simplest way to access the board registers:

int n; 
u8 buffer = 0xA3;  /*channel 7: on , off, on, off, off, off, on, channel 0: on*/ 
ioctl(fd, CHANGE_PORT, PORT1);  
 n = write(fd, &buffer, sizeof(buffer));
If the user wish to control only one channel state, without changing the others, he has to use the read() function. This example shows how to open channel 3 relay ( turn off ):

ioctl(fd, CHANGE_PORT, PORT1);  
read(fd, &buffer, sizeof(buffer)); 
buffer &=  CHANNEL3;    /*Turn off channel3*/ 
n = write(fd, &buffer, sizeof(buffer));

Read() call

Description

Reading the pdiso16 digital I/O board means to control the status of the relay channels.

As for an ioctl() or a write() call, before making a read() call, the device must have been opened.

Then to make any read() call user has to indicate the file descriptor (int fd) that has been returned by the open() function, a result buffer and, its size. The call then may look like nbrd=read(fd, &buffer, sizeof(buffer)) where nbrd is the number of bytes read. In the pdiso16 device driver, user can give a byte-wise or word-wise buffer according to his choice of a single port reading or a dual one.

Reading relay channels status

int n; 
u8 buffer; 
 ... 
ioctl(fd, CHANGE_PORT, PORT2); 
n = read(fd, &buffer, sizeof(buffer)); 
if (n>0) { 
    /*Channel 0 is the LSB (Less Significant Bit) of the byte*/ 
    u8 channel_0 = buffer & 0x01; 
    u8 channel_1 = buffer & 0x02; 
    ... 
    u8 channel_6 = buffer & 0x40; 
    u8 channel_7 = buffer & 0x80; 
    /*Channel 7 is the MSB (Most Significant Bit)*/ 
    ... 
    rtl_printf(``channel_0 relay: %s, ( (channel_0 == 0) ? "off" : "on") ); 
    ... 
    rtl_printf(``channel_7 relay: %s, ( (channel_7 == 0) ? "off" : "on") ); 
}
int n; 
u16 buffer; 
 ... 
ioctl(fd, BOTH_PORTS); 
n = read(fd, &buffer, sizeof(buffer)); 
if (n>0) { 
    u8 port_one = buffer & 0x00FF;             /*PORT1: The lowest byte of the word*/ 
    u8 port_two = (buffer & 0xFF00) >> 8;      /*PORT2: The highest one*/ 
    rtl_printf(``port_one=0x%x, port_two=0x%x\n'', port_one, port_two); 
}

Calls from Linux user space : using RT-FIFOs

RT-FIFO implements message passing through rt-kernel space... This kind of communication makes me think of QNX's, except that it is non blocking ( which is a pretty big difference ! ). Another difference is that rt-fifos are uni-directional. User must exchange data through a pair of rt-fifos for bi-directional communication. My device driver requires 2 rt-fifos...

if ((Fd_Rd = open("/dev/rtf0", O_RDONLY)) < 0) { 
    fprintf(stderr, " Error opening /dev/rtf0 for reading.\n");  
    fprintf(stderr, " RT-Linux module not active??\n\n"); 
    exit(-1); 
}
Same thing for the other direction: ...Fd_Wr = open("/dev/rtf1", O_WRONLY)...

/* Messages sent to rtlinux module */ 

typedef struct { 

    u8 what; 

    u16 value; 

} Message_struct;

#define MESSAGE_SIZE sizeof(Message_struct) 

Example: command the relay channels

Message_struct Send_Cmd; 
... 
Send_Cmd.what = WRITE_RELAY; 
Send_Cmd.value = 0xAA;
if (write(Fd_Wr, &Send_Cmd, MESSAGE_SIZE) < 0) {  
    fprintf(stderr, "Can't send a command to RT-task\n"); 
    exit(1); 

Send request messages

/* command codes for the message types (what tag) */

#define WRITE_RELAY 0x1 /*= read() function */

#define READ_STATE 0x2  /*= write() function */

#define TAKE_DATA 0x3   /*= ioctl(fd, DIGITAL_INPUT, ...) function */

AND the ioctl() commands CHANGE_PORT(PORT1/PORT2 in value tag) and BOTH_PORTS.

Receive messages

To receive a message, the Linux user space process must listen to the RTLinux pdiso16 device driver. User can simply implement it with a read() call or use read() with a select() call on the rt-fifo. Example could be fount in the app.c source file. A Message_struct data structure must be used while calling the read() function.

Acknowledgment messages

The device driver passes on an acknowledgment message to the Linux process when no result is required ( write or ioctl commands ). Thus, the device driver says that it has received the command ant that it has succeeded ; the Message_struct's what member contains: RECV_DONE.

/* OK code in com.h*/

#define RECV_DONE 0x10 

Error codes are sent to the Linux process the same way.

/* Error codes in com.h */ 

#define RECV_ERR1 -1

#define RECV_ERR2 -2

#define CMD_ERR -3

#define SEND_ERR -4 

Result messages

The result message's what member is always TAKE_DATA and its value member is filled with the result of the request previously passed on to the device driver. If a single port access is done, this 16 bits value only contains its lowest byte valid. I recommend to give a 0 filled value member to the read() function. In case of a double port access, the entire value field is OK and the lowest byte corresponds with port1.

Miscellaneous

Debug options

At the compilation of the driver, user can specify which level of debug he wants to be displayed on the kernel log. This is done by uncommenting #define DEBUG for debug level 1 or both #define DEBUG and #define DEBUG2 for debug level 2 in the pdiso16.c file. In real-time In general, debug level 1 displays actions and probable causes of command faults and debug level 2 add the state of important global and local variables at that time so that you can determine what was wrong.

You can also display all the kernel messages by using dmesg command.

Note that debug options slow down the device driver.

Special files

Kernel uses special files to save all the systems parameters. Some of those can be very useful to get informations about the device driver :

/proc/devices:
this file indexes all the devices drivers installed on the system with their major number and their type (char or block).
/proc/interrupts:
this file indexes all the interrupts that have already appear on the system with their interrupt vector, their frequency and the name of the device driver which owns them.
/proc/ioports:
this file indexes all the I/O regions that have been taken by devices drivers. The name of the device driver that owns the region is also displayed.
/proc/ksyms:
this file indexes all the kernel entry points with their address and the name of the function. You can display these informations with the ksyms command.
/proc/modules:
this file registers all the loaded modules with their memory occupation and the number of processes/threads that have opened it. You can display these informations with the lsmod command.
/proc/version:
this file contains the current running kernel version. It is useful to see if your module version is compatible with the current kernel but you can force the module even if the versions are incompatible with insmod -f.
/dev/pdiso:
this file is the devices files associated with the board using the pdiso16 device driver. You can see major and minor number of this file with ls -l command.
/var/log/messages:
these file contains all the messages sent by kernel with printk() calls. You can display these messages with the dmesg command.
/etc/devinfo:
this file indexes all the different device drivers type that can exist with their major and minor number.

next up previous contents
suivant: À propos de ce monter: Annexes précédent: Annexes   Table des matières
Nicolas Ferre 2000-12-08