USB serial drivers, Part 2
By artem on Nov 20, 2005
The generic serial driver (GSD) hides a great deal of termio(7I) complexity from the USB serial driver writers. Another major benefit is that it ensures compliance with UNIX standards, such as Single UNIX specification (see chapter 11, General Terminal Interfaces). VSX-PCTS test suite includes terminal interface tests that all USB serial drivers should pass. Today I am going to discuss some aspects of GSD implementation.
Open(2) implementation for serial devices is quite complicated, there a many rules to follow.
Recall that there are two types of device nodes per serial port: /dev/term (aka tty lines) and /dev/cua (aka dial-out lines). When an application attempts to open a tty line, the open(2) system call should block until Carrier Detect signal is asserted. Dial-out opens do not block and succeed immediately.
Now recall that there can be multiple applications attempting to open the same device simultaneously. For example, while one application is blocked in open(2) waiting for Carrier Detect, another opens the corresponding dial-out line; in this case, the dial-out open should succeed and the first application's open(2) should unblock and fail.
Open behavior also varies depending on the O_NONBLOCK/O_NDELAY flags, soft carrier setting and "ignore-cd" device properties.
usbser_open_setup() function accounts for all possible scenarios through the used of a state machine.
Quite a few things should happen before a serial device can be closed:
- Remaining data must be drained, first from the local software buffer, then from the hardware FIFO.
- Any outstanding break and delay requests must be cancelled.
- The line must be hung up by dropping RTS and DTR lines.
This is done in usbser_close().
The driver uses two threads, one each for read and write message processing. Strictly speaking, separate threads are not needed in Solaris 9 and up since the STREAMS scheduler was improved with dynamic task queues. However, GSD was first written for Solaris 8, and at that time is was problematic using blocking USB requests in the STREAMS context.
Due to asynchronous nature of USB requests, operations that usually happen atomically, take several steps. To prevent multiple operations from stepping on each other, some of them are serialized using usbser_serialize_port_act() and usbser_release_port_act().
The core of write-side processing is usbser_wmsg: it dequeues one message at a time and dispatches it depending on type, such as M_STOP, M_DATA or M_IOCTL.
usbser_ioctl() handles various terminal ioctls, passed via M_IOCTL messages. Setting port parameters, sending break, draining and flushing data, enabling internal loopback mode (great for testing), getting and setting port signals - all this is done here by calling down to DSD. Not all ioctls are handled by GSD, however, some of them are passed to the ttycommon_ioctl() function, which is a generic terminal ioctl implementation used by all Solaris serial drivers.
When the DSD detects modem status line changes, such as CTS or CD, it notifies GSD with the status callback. usbser_status_cb(), the callback handler, does not process this request immediately. Instead, it sets a special flag, USBSER_FL_STATUS_CB, and wakes up the write thread. usbser_wq_thread() checks the flag and if it's set, calls the actual handler, usbser_status_proc_cb().
Because the status callback is called from DSD, and the callback handler may need to call other DSD functions, we avoid recursive calls into DSD by delegating status handling to the write thread. Back in Solaris 8, the DSD callback was called in the interrupt context, so a recursive call into DSD could lead to a deadlock.
usbser_data() simply takes an M_DATA message and passes it on to the DSD by calling USBSER_DS_TX() for transmission. The DSD can't refuse the message, if it needs to postpone transmission, it has to maintain its own queue.
When DSD is done transmitting the data, is would call back into GSD. usbser_tx_cb() callback handler simply wakes up the write thread to check for any new data to transfer.
When DSD receives new data, it lets GSD know via RX callback. See usbser_rx_cb() how it retrieves the data from DSD's queue by calling USBSER_DS_RX(), processes it and sends upstream.
Data processing is the interesting part. If data was received without errors, then it's just a linked list of message blocks (mblk_t), which will be put on the stream's read queue after being processed by usbser_rx_massage_data(). This extra step is required for standards compliance: under certain conditions, a received character '\\377' (0xFF) should be read by application as two such characters, i.e. '\\377' followed by another '\\377'.
If a character was received with a framing or parity error, the DSD must pass it it to GSD as an M_BREAK message. The first byte should contain the error code, with the character in the second byte. The GSD then does the right thing for termio in usbser_rx_massage_mbreak().
Flow control is needed when a receive buffer on either end of communication becomes full and it signals the transmitting end to suspend transmission temporarily, until signalled to resume.
There are two types of signalling:
- Hardware flow control is enabled by the receiving end by deasserting, i.e. setting to logical false, the RTS (Request to Send) line. At the transmitter's end, RTS is seen as CTS (Clear to Send). When the transmitter sees CTS deasserted, it should stop sending any new data. When CTS is asserted again, it can start sending data.
- Software flow control is enabled by sending a XOFF character, which can be simulated done by pressing Ctrl-S on the terminal. To resume data flow, a XON character (Ctrl-Q) is sent.
Both types of flow control can be implemented in hardware. However, not all devices support flow control, and because GSD should work with any device, it implements both types in software.
Inbound flow control happens when our local queue becomes full (reaches the high watermark) and we want to ask the device to stop transmission. Both hardware and software type of flow control are done in usbser_inbound_flow_ctl().
Outbound flow control happens when the other end asks us to stop transmission. For the hardware type,the GSD detects CTS transition through the status callback, see usbser_status_proc_cb(). When CTS turns 0, an M_STOP message is put on GSD's local write queue; when CTS turns 1, an M_START message is put. usbser_wmsg() then calls usbser_stop() and usbser_start() respectively, to suspend or resume data transmittion.
Next time - DSDI.