Chapter 18. How to Write a Driver
Table of Contents
A device driver is nothing more than a named entity that supports the basic I/O functions - read, write, get config, and set config. Typically a device driver also uses and manages interrupts from the device. While the interface is generic and device driver independent, the actual driver implementation is completely up to the device driver designer.
That said, the reason for using a device driver is to provide access to a device from application code in as general purpose a fashion as reasonable. Most driver writers are also concerned with making this access as simple as possible while being as efficient as possible.
Most device drivers are concerned with the movement of information, for example data bytes along a serial interface, or packets in a network. In order to make the most efficient use of system resources, interrupts are used. This will allow other application processing to take place while the data transfers are under way, with interrupts used to indicate when various events have occurred. For example, a serial port typically generates an interrupt after a character has been sent “down the wire” and the interface is ready for another. It makes sense to allow further application processing while the data is being sent since this can take quite a long time. The interrupt can be used to allow the driver to send a character as soon as the current one is complete, without any active participation by the application code.
The main building blocks for device drivers are found in the
include file: <cyg/io/devtab.h>
All device drivers in eCos are described
by a device table entry, using the cyg_devtab_entry_t type.
The entry should be created using the DEVTAB_ENTRY()
macro,
like this:
DEVTAB_ENTRY
(l, name, dep_name, handlers, init, lookup, priv)
Arguments
-
l
- The "C" label for this device table entry.
-
name
- The "C" string name for the device.
-
dep_name
- For a layered device, the "C" string name of the device this device is built upon.
-
handlers
- A pointer to the I/O function "handlers" (see below).
-
init
- A function called when eCos is initialized. This function can query the device, setup hardware, etc.
-
lookup
-
A function called when
cyg_io_lookup()
is called for this device. -
priv
- A placeholder for any device specific data required by the driver.
The interface to the driver is through the handlers
field. This is a pointer to
a set of functions which implement the various cyg_io_XXX()
routines. This table is defined by the macro:
DEVIO_TABLE(l, write, read, get_config, set_config)
Arguments
-
l
- The "C" label for this table of handlers.
- write
-
The function called as a result of
cyg_io_write()
. - read
-
The function called as a result of
cyg_io_read()
. - get_config
-
The function called as a result of
cyg_io_get_config()
. - set_config
-
The function called as a result of
cyg_io_set_config()
.
When eCos is initialized (sometimes called
“boot” time), the init()
function is called
for all devices in the system. The init()
function is
allowed to return an error in which case the device will be placed
“off line” and all I/O requests to that device will be
considered in error.
The lookup()
function is called whenever
the cyg_io_lookup()
function
is called with this device name. The lookup function may cause the device
to come “on line” which would then allow I/O
operations to proceed. Future versions of the I/O system
will allow for other states, including power saving modes,
etc.
18.1. How to Write a Serial Hardware Interface Driver
The standard serial driver supplied with eCos is structured as a hardware independent portion and a hardware dependent interface module. To add support for a new serial port, the user should be able to use the existing hardware independent portion and just add their own interface driver which handles the details of the actual device. The user should have no need to change the hardware independent portion.
The interfaces used by the serial driver and serial implementation
modules are contained in the file <cyg/io/serial.h>
Note | |
---|---|
In the sections below we use the notation <<xx>> to mean a module specific value, referred to as “xx” below. |
18.1.1. DevTab Entry
The interface module contains the devtab entry (or entries if a single module supports more than one interface). This entry should have the form:
DEVTAB_ENTRY(<<module_name>>, <<device_name>>, 0, &serial_devio, <<module_init>>, <<module_lookup>>, &<<serial_channel>> );
Arguments
-
module_name
- The "C" label for this devtab entry
-
device_name
-
The "C" string for the
device. E.g.
/dev/serial0
. -
serial_devio
- The table of I/O functions. This set is defined in the hardware independent serial driver and should be used.
-
module_init
- The module initialization function.
-
module_lookup
- The device lookup function. This function typically sets up the device for actual use, turning on interrupts, configuring the port, etc.
-
serial_channel
- This table (defined below) contains the interface between the interface module and the serial driver proper.
18.1.2. Serial Channel Structure
Each serial device must have a “serial channel”. This is a set of data which describes all operations on the device. It also contains buffers, etc., if the device is to be buffered. The serial channel is created by the macro:
SERIAL_CHANNEL_USING_INTERRUPTS(l, funs, dev_priv, baud,stop, parity, word_length, flags, out_buf, out_buflen, in_buf, in_buflen)
Arguments
-
l
- The "C" label for this structure.
-
funs
- The set of interface functions (see below).
-
dev_priv
- A placeholder for any device specific data for this channel.
-
baud
- The initial baud rate value (cyg_serial_baud_t).
-
stop
- The initial stop bits value (cyg_serial_stop_bits_t).
-
parity
- The initial parity mode value (cyg_serial_parity_t).
-
word_length
- The initial word length value (cyg_serial_word_length_t).
-
flags
- The initial driver flags value.
-
out_buf
-
Pointer to the output
buffer.
NULL
if none required. -
out_buflen
- The length of the output buffer.
-
in_buf
-
pointer to the input
buffer.
NULL
if none required. -
in_buflen
- The length of the input buffer.
If either buffer length is zero, no buffering will take place in that direction and only polled mode functions will be used.
The interface from the hardware independent driver into the
hardware interface module is contained in the funs
table.
This is defined by the macro:
18.1.3. Serial Functions Structure
SERIAL_FUNS(l, putc, getc, set_config, start_xmit, stop_xmit)
Arguments
-
l
- The "C" label for this structure.
-
putc
bool (*putc)(serial_channel *priv, unsigned char c)
This function sends one character to the interface. It should return
true
if the character is actually consumed. It should returnfalse
if there is no space in the interface-
getc
unsigned char (*getc)(serial_channel *priv)
This function fetches one character from the interface. It will be only called in a non-interrupt driven mode, thus it should wait for a character by polling the device until ready.
-
set_config
bool (*set_config)(serial_channel *priv,cyg_serial_info_t *config)
This function is used to configure the port. It should return
true
if the hardware is updated to match the desired configuration. It should returnfalse
if the port cannot support some parameter specified by the given configuration. E.g. selecting 1.5 stop bits and 8 data bits is invalid for most serial devices and should not be allowed.-
start_xmit
void (*start_xmit)(serial_channel *priv)
In interrupt mode, turn on the transmitter and allow for transmit interrupts.
-
stop_xmit
void (*stop_xmit)(serial_channel *priv)
In interrupt mode, turn off the transmitter.
18.1.4. Callbacks
The device interface module can execute functions in the
hardware independent driver via chan->callbacks
.
These functions are available:
void (*serial_init)( serial_channel *chan )
This function is used to initialize the serial channel. It is only required if the channel is being used in interrupt mode.
void (*xmt_char)( serial_channel *chan )
This function would be called from an interrupt handler after a
transmit interrupt indicating that additional characters may be
sent. The upper driver will call the putc
function as appropriate to send more data to the device.
void (*rcv_char)( serial_channel *chan, unsigned char c )
This function is used to tell the driver that a character has arrived at the interface. This function is typically called from the interrupt handler.
Furthermore, if the device has a FIFO it should require the hardware independent driver to provide block transfer functionality (driver CDL should include "implements CYGINT_IO_SERIAL_BLOCK_TRANSFER"). In that case, the following functions are available as well:
bool (*data_xmt_req)( serial_channel *chan, int space, int* chars_avail, unsigned char** chars) void (*data_xmt_done)(serial_channel *chan)
Instead of calling xmt_char()
to get a single
character for transmission at a time, the driver should call
data_xmt_req()
in a loop, requesting character
blocks for transfer. Call with a space
argument of how much space
there is available in the FIFO.
If the call returns true
, the driver can read
chars_avail
characters from
chars
and copy them into the FIFO.
If the call returns false
, there are
no more buffered characters and the driver should continue without
filling up the FIFO.
When all data has been unloaded, the
driver must call data_xmt_done()
.
bool (*data_rcv_req)( serial_channel *chan, int avail, int *space_avail, unsigned char** space) void (*data_rcv_done)(serial_channel *chan)
Instead of calling rcv_char()
with a single
character at a time, the driver should call
data_rcv_req()
in a loop, requesting space to
unload the FIFO to. avail
is the number of
characters the driver wishes to unload.
If the call returns true
, the driver can copy
space_avail
characters to
space
.
If the call returns false
, the input buffer is
full. It is up to the driver to decide what to do in that case
(callback functions for registering overflow are being planned for
later versions of the serial driver).
When all data has been unloaded, the driver must call
data_rcv_done()
.
18.2. Serial testing with ser_filter
18.2.1. Rationale
Since some targets only have one serial connection, a serial testing harness needs to be able to share the connection with GDB (however, the test and GDB can also run on separate lines).
The serial filter (ser_filter) sits between the serial port and GDB and monitors the exchange of data between GDB and the target. Normally, no changes are made to the data.
When a test request packet is sent from the test on the target, it is intercepted by the filter.
The filter and target then enter a loop, exchanging protocol data between them which GDB never sees.
In the event of a timeout, or a crash on the target, the filter falls back into its pass-through mode. If this happens due to a crash it should be possible to start regular debugging with GDB. The filter will stay in the pass-though mode until GDB disconnects.
18.2.2. The Protocol
The protocol commands are prefixed with an "@"
character which the serial filter is looking for. The protocol
commands include:
-
PING
-
Allows the test on the target to probe for the filter. The
filter responds with
OK
, while GDB would just ignore the command. This allows the tests to do nothing if they require the filter and it is not present. -
CONFIG
- Requests a change of serial line configuration. Arguments to the command specify baud rate, data bits, stop bits, and parity. [This command is not fully implemented yet - there is no attempt made to recover if the new configuration turns out to cause loss of data.]
-
BINARY
Requests data to be sent from the filter to the target. The data is checksummed, allowing errors in the transfer to be detected. Sub-options of this command control how the data transfer is made:
-
NO_ECHO
- (serial driver receive test) Just send data from the filter to the target. The test verifies the checksum and PASS/FAIL depending on the result.
-
EOP_ECHO
-
(serial driver half-duplex receive and send test) As
NO_ECHO
but the test echoes back the data to the filter. The filter does a checksum on the received data and sends the result to the target. The test PASS/FAIL depending on the result of both checksum verifications. -
DUPLEX_ECHO
- (serial driver duplex receive and send test) Smaller packets of data are sent back and forth in a pattern that ensures that the serial driver will be both sending and receiving at the same time. Again, checksums are computed and verified resulting in PASS/FAIL.
-
-
TEXT
- This is a test of the text translations in the TTY layer. Requests a transfer of text data from the target to the filter and possibly back again. The filter treats this as a binary transfer, while the target ma be doing translations on the data. The target provides the filter with checksums for what it should expect to see. This test is not implemented yet.
The above commands may be extended, and new commands added, as required to test (new) parts of the serial drivers in eCos.
18.2.3. The Serial Tests
The serial tests are built as any other eCos test. After running the
make tests command, the tests can be found in
install/tests/io_serial/
-
serial1
- A simple API test.
-
serial2
- A simple serial send test. It writes out two strings, one raw and one encoded as a GDB O-packet
serial3
[ requires the serial filter ]- This tests the half-duplex send and receive capabilities of the serial driver.
serial4
[ requires the serial filter ]- This test attempts to use a few different serial configurations, testing the driver's configuration/setup functionality.
serial5
[ requires the serial filter ]- This tests the duplex send and receive capabilities of the serial driver.
All tests should complete in less than 30 seconds.
18.2.4. Serial Filter Usage
Running the ser_filter program with no (or wrong) arguments results in the following output:
Usage: ser_filter [-t -S] TcpIPport SerialPort BaudRate or: ser_filter -n [-t -S] SerialPort BaudRate -t: Enable tracing. -S: Output data read from serial line. -c: Output data on console instead of via GDB. -n: No GDB.
The normal way to use it with GDB is to start the filter:
$ ser_filter -t 9000 com1 38400
In this case, the filter will be listening on port 9000 and connect to the
target via the serial port COM1
at 38400 baud. On a UNIX
host, replace "COM1
" with a device such as
"/dev/ttyS0
".
The -t
option enables tracing which will cause the
filter to describe its actions on the console.
Now start GDB with one of the tests as an argument:
$ mips-tx39-elf-gdb -nw install/tests/io_serial/serial3
Then connect to the filter:
(gdb) target remote localhost:9000
This should result in a connection in exactly the same way as if you had connected directly to the target on the serial line.
(gdb) c
Which should result in output similar to the below:
Continuing. INFO:<BINARY:16:1!> PASS:<Binary test completed> INFO:<BINARY:128:1!> PASS:<Binary test completed> INFO:<BINARY:256:1!> PASS:<Binary test completed> INFO:<BINARY:1024:1!> PASS:<Binary test completed> INFO:<BINARY:512:0!> PASS:<Binary test completed> … PASS:<Binary test completed> INFO:<BINARY:16384:0!> PASS:<Binary test completed> PASS:<serial13 test OK> EXIT:<done>
If any of the individual tests fail the testing will terminate with a
FAIL
.
With tracing enabled, you would also see the filter's status output:
The PING
command sent from the target to determine the
presence of the filter:
[400 11:35:16] Dispatching command PING [400 11:35:16] Responding with status OK
Each of the binary commands result in output similar to:
[400 11:35:16] Dispatching command BINARY [400 11:35:16] Binary data (Size:16, Flags:1). [400 11:35:16] Sending CRC: '170231!', len: 7. [400 11:35:16] Reading 16 bytes from target. [400 11:35:16] Done. in_crc 170231, out_crc 170231. [400 11:35:16] Responding with status OK [400 11:35:16] Received DONE from target.
This tracing output is normally sent as O-packets to GDB which will display the tracing text. By using the
-c
option, the tracing text can be redirected to the
console from which ser_filter was started.
18.2.5. A Note on Failures
A serial connection (especially when driven at a high baud rate) can garble the transmitted data because of noise from the environment. It is not the job of the serial driver to ensure data integrity - that is the job of protocols layering on top of the serial driver.
In the current implementation the serial tests and the serial filter are
not resilient to such data errors. This means that the test may crash or hang
(possibly without reporting a FAIL
). It also
means that you should be aware of random errors - a FAIL
is not necessarily caused by a bug in the serial driver.
Ideally, the serial testing infrastructure should be able to distinguish random errors from consistent errors - the former are most likely due to noise in the transfer medium, while the latter are more likely to be caused by faulty drivers. The current implementation of the infrastructure does not have this capability.
18.2.6. Debugging
If a test fails, the serial filter's output may provide some hints about
what the problem is. If the option -S
is used when starting
the filter, data received from the target is printed out:
[400 11:35:16] 0000 50 41 53 53 3a 3c 42 69 'PASS:<Bi' [400 11:35:16] 0008 6e 61 72 79 20 74 65 73 'nary.tes' [400 11:35:16] 0010 74 20 63 6f 6d 70 6c 65 't.comple' [400 11:35:16] 0018 74 65 64 3e 0d 0a 49 4e 'ted>..IN' [400 11:35:16] 0020 46 4f 3a 3c 42 49 4e 41 'FO:<BINA' [400 11:35:16] 0028 52 59 3a 31 32 38 3a 31 'RY:128:1' [400 11:35:16] 0030 21 3e 0d 0a 40 42 49 4e '!..@BIN' [400 11:35:16] 0038 41 52 59 3a 31 32 38 3a 'ARY:128:' [400 11:35:16] 0040 31 21 .. .. .. .. .. .. '1!'
In the case of an error during a testing command the data received by the filter will be printed out, as will the data that was expected. This allows the two data sets to be compared which may give some idea of what the problem is.
2024-12-10 | Open Publication License |