Writing an Intelligent Serial Card Driver
It started out innocently enough. I had been looking for an upgrade for my home system, a decrepit Unix workstation with only 8MB RAM and 40MB disk. I had been looking at 386BSD, but was a bit put off by the AT&T lawsuit and such. Suddenly last September I noticed this other system, Linux, and saw articles pointing to distribution sets. I copied a set to some floppies (on a Sun system, ugh) and borrowed a partition on a client's system that normally runs MS-DOS.
When I discovered how easily Linux loaded and how well it functioned, I knew I had the answer. I spent much of my monthly billing of that client to order a hefty 486 through their purchasing agent. I was a little concerned about assembling the pieces, but the guys in the manufacturing area were interested to see how it worked. I couldn't have kept them from putting it together if I had wanted to.
All this was very timely. I had to get back to my studies and finish my dissertation if I were ever going to graduate. Although I had access to plenty of systems over the Internet, it would have been messy analyzing the data from afar. With all the X tools now available on my new system, my work from home was greatly improved. For all this, I owed the Linux community a lot for the tools upon which I so thoroughly depended.
Then came my chance to pay back this debt (and benefit as well). Phil Hughes posted the following on a local Linux mail list:
... anyone out there want to write a driver for the Cyclades serial board? This is an ISA-bus card with a Cirrus Logic RISC processor on it.... Why do you want to do this? 1. Linux needs it :-) 2. You will get a free 8-port board for your effort What you have to do: 1. Write a driver that really works and make it available on the net. 2. Write an article on this for Linux Journal
That sounded like a bargain! I immediately sent off a note saying I would be up to the task. I had done a lot of OS internals in the '70s, hacked in the BSD Unix kernel in the '80s, and had done some PC drivers in the last few years. That seemed to persuade Phil I could do the job. He wrote that his “big fear was that people who had no idea what a device driver is would decide to try to write a driver so they could get a free board”.
Things then went on hold while I finished up that last of the notes for my defense. (Actually I was hoping my advisor wouldn't ask what I was up to.) Fortunately it took a while for us to work out the details and for the card to arrive, so I didn't look bad by the delay. The developer's kit that finally arrived had a wealth of information: a 130-page data sheet on the Cirrus chip and lots of code fragments showing how the board is accessed. With that stuff in hand, I finally got to work inside the kernel.
The character tty-like interface is implemented across several files. Some look at the high-level part of the data flow; implementing the canonical character processing, newline to carriage return conversions, command line echoing, escape character processing, and such. The others are involved with controlling the devices; the console, the keyboard, and the serial lines. I started my driver by copying serial.c and changing all the names of the form rs_something to cy_something. I then had to hook this new low-level driver to the the system. There are two routines that are called to do this: cy_init and cy_open. Once these are slipped into tty_io.c, the Cyclades driver development is limited to the new file, cyclades.c.
The very first part was recognizing the boards and initializing the data structures. There was suitable code from Cyclades to do the former and the serial.c code took care of the latter. One major difference appeared. The serial.c code was based on the design where each port had its own IRQ which was to be deactivated when the port was closed. I had to move the IRQ setup code into the board initialization. The inital success was to boot the system and get the message reporting that the board and its ports were present.
The next phase was to fix up the cy_open routine. Once I stripped out the IRQ stuff, the rest was mostly to establish the link between the low-level driver and the high-level driver. This didn't need much of a change at all. I just added a subroutine call to initialize the port on the card, setting character size, baud rate, modem control signals, etc. That seemed to work, so I went on to the next part.
Then it got scary; actually sending characters. Again, using code from serial.c as an outline (hinting strongly where something has to to be done to the hardware), I changed cy_write and cy_interrupt.
cy_write is called whenever the high-level driver has queued characters to be sent. The serial.c version actually stuffed some of the initial characters into a hardware register, which starts sending characters out. I changed this so that I only enabled interrupts; the interrupt service routine would be the only code that put characters on the wire.
The interrupt service was even more scary. For the first time, code would be executed outside of the context of the calling program. If things went astray, it would be really hard to figure out where. Fortunately the code fragments from Cyclades and from serial.c merged without too much difficulty. Whenever an interrupt occurs, it indicates that something on the board needs tending to. It's a simple matter of programming ( :-)) to poll each of the chips to see if any of their four ports need attention, and if so, whether it is for transmit, receive, or modem conditions. With some trepidation I compiled these changes and rebuilt and rebooted the kernel. A simple test: date > /dev/ttyC0 worked!
I was glad I had made this much progress because about this time I got a query from Cyclades. They wanted to know how things were going. With some relief I replied I was actively testing “increasingly feature-full versions of the driver.”
This was well received. They had issued a small advertisement regarding a Linux driver and were starting to get responses. To me this was also good news. There would be a pool of folks to test my code.
I continued my efforts, working up my courage to try the receive side, as well as addressing a mysterious (to me) kernel crash. As part of my tests, I was actually issuing the command “sync” just prior to the test transmission, so the kernel crashes hadn't hurt anything yet.
I traced the crash to a pointer that was being cleared prematurely. By comparing the serial.c code with my code, I discovered I had moved too many things around. Some minor checks in cy_close fixed that. (I also explored how I might get some kind of display out of the console showing what's going on. I looked around and found that although printk() isn't always appropriate for messages from within interrupt service routines (if ever), console_print() can be called anytime if interrupts are turned off. A little effort allowed me to sprinkle single character messages showing how far a routine had progressed. More calls to console_print() allowed me to zero in on what went wrong.) Finally, after making the best of the Memorial Day weekend, I reported the following: