Writing Drivers for 2.11BSD
I am not a good programmer. I spent the past 6 years of my life writing lousy Lua, messy MATLAB and shameful shell scripts in an academic setting. There will be things in this post which are completely wrong. Feel free to send me an email and let me know if this is the case.
I recently purchased an IBV11-A controller off eBay for the bargain price of £20. This is a GPIB (General Purpose Interface Bus, or more officially, an IEEE-488) controller card, allowing a PDP-11 to interface with equipment such as older digital oscilloscopes, laboratory power supplies, printers and even floppy drives. The bus has some nice features, such as having multiple devices hanging off the same controller, either "daisy chained" or in some kind of a star topology.
My PDP-11 runs 2.11BSD. This release of the second BSD incorporates some features from 4.3BSD into 2.10BSD, notably shadow password files, the TMSCP tape driver, and longer filename support. For a while now, I've been wanting to get to grips with how this early kernel is structured. The IBV11 controller seemed like a good candidate for this learning, since there is no support for it within the kernel at present. Also I'm not entirely sure why, but I really want to control my oscilloscope and power supply from my PDP-11. I have plans for some other drivers in future, such as the Dowty SBD video frame buffer card and a random 64 bit digital I/O board, but the IBV11 seemed line an ideal first kernel project.
Lacking experience in kernel development, I found it quite difficult to pull together all the necessary knowledge for writing a driver. The kernel source isn't particularly well commented (bytes were expensive back then) and the man pages are generally quite short and lacking in detail. I spent most of my time on this project reading other drivers and jumping between random files in the kernel. Hopefully this post will help me retain some of this information, but also help others get started quicker if they decide they want to add support for a card in 2.11BSD.
The General Purpose Interface Bus
GPIB bus a parallel communications standard consisting of 8 bidirectional data lines, along with a number of bidirectional signal lines. Each device on the bus has an address and any which is sent at the beginning of each data frame. It is commonly used on test equipment, such as power supplies and oscilloscopes, but also more typical computing peripherals, such as floppy drives, scanners and printers. Since test equipment often has a very long shelf life, GPIB is still commonly used in laboratory settings, even though modern devices are no longer sold with GPIB support. One of the novelties of GPIB is the double sided plug has both male and female connectors. This allows devices to be conveniently daisy chained or arranged in a star configuration.
The address of a GPIB device is usually specified using physical switches on the rear of the device, although this can also be handled in software just as easily. Each message sent on the bus is prefixed with a address of the device it is intended for.
The IBV11 Controller
How we talk to the IBV11 controller is described in detail in the user guide. Fundamentally however, the controller provides two addressable registers. These are both one word (2 bytes) long. The first is the Instrument Bus Status (IBS) register, and the second is the Instrument Bus Data (IBD) register. Both are shown below, expanded to see name of each bit. The first row for each register denotes whether the bit is read-only or read-write. The exception to this is the IBS SRQ bit, which is only read-write if the IBV11 is the only GPIB controller (which requires a switch on the board to be enabled).
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
IBS | * | R | R | R | R | R | RW | RW | RW | RW | RW | RW | RW | RW | ||
SRQ |
ER1 |
ER2 |
CMD |
TKR |
LNR |
ACC |
IE |
TON |
LON |
IBC |
REM |
EOP |
TCS |
|||
IBD | R | R | R | R | R | R | R | R | RW | RW | RW | RW | RW | RW | RW | RW |
EOI |
ATN |
IFC |
REN |
SRQ |
RFD |
DAV |
DAC |
D7 |
D6 |
D5 |
D4 |
D3 |
D2 |
D1 |
D0 |
Most of these bits follow the standard GPIB signal names, except for
DAV
and DAC
which are the inverted versions of
NDAV
and NDAC
. IBS is located (by default) at
1601508 and IBD is in
available at the next word in memory. The card also has four vector
addresses, the first of which defaults to 4208. On the PDP-11 (probably
different platforms too), each vector is two words long. The first the
interrupt handler address, and the second is the processor status word,
which includes an interrupt priority. These interrupt vectors are as
follows,
- Error - This interrupt is triggered if an error
occurs on the instrument bus. If this happens,
ER1
orER2
will be set in the status register. - Service Request is triggered when an instrument on
the bus is requiring service, e.g. it may have data for the controller.
When this interrupt is triggered, the
SRQ
bit of the status register will be set. - Talker and Command interrupts are triggered when the card is ready to send out another byte of data.
- Listener will trigger when a new byte of data has
been received which needs to be pushed to the PDP-11 bus. When this is
the case, the
LNR
bit will be set in the status register.
The 2.11BSD kernel is quite picky about interrupt priorities. The IBV11 user guide isn't particularly kind when it comes to telling you what priority level these interrupts are raised. I will write more about interrupts and priorities later in this post, but for now, we can figure out the interrupt priorities from the assembly source listed on page 51. The top reads,
ADDRESS CODE
000430 001024 ; Command/Talker Interrupt address
000432 000200 ; PSW
000434 001056 ; Listener Interrupt address
000436 000200 ; PSW
As mentioned earlier, interrupt vectors consist of two words, the first being the address of the interrupt handler, and the second being the Processor Status Word (PSW). We can break up the value of this PSW to figure out the interrupt priority. If we split up the least significant digits into binary, we get:
010 000 000
^^ ^
Bits 5 to 7 (zero indexed) are the interrupt priority level, telling us that in this case, the priorities are level 410. The page of assembler, in the user guide, also gives us a good idea about how we should interact with the card. There are a few things we should take note of:
- Every write to the data register involves waiting for an interrupt before continuing. At address 10408, a byte is pushed to the IBD register, followed by jumping back up to the branch to self loop at address 10228. This is then interrupted by the command/talker interrupt at 4308. The same also happens after clearing the data register while receiving bytes, as seen at address 10708.
- Before we write data to the instrument bus, we have to "take control
synchronously". This involves setting the
IE
andIBC
bits of the status register (1108). - Once we have done talking, we have to switch to listening mode. This
involves setting
ACC
,IE
andLON
bits of the status register (3208). - After each byte received, we have to clear the data register to notify the controller that we have read the data. As mentioned earlier, this also means we need to wait for an interrupt.
Now that we have learnt about how GPIB commands are formed and studied the controller's example assembler, we have a decent idea of how we can use the controller to talk to an interface. Next we need to understand a bit about how the 2.11BSD kernel is structured so we can put all three pieces together.
2.11BSD Structure
The kernel source is stored in /usr/src/sys/
, but the
directory structure of 2.11BSD does not exactly match up with the
original intentions. MASSBUS drivers should be placed in the
pdpmba
directory, while UNIBUS drivers should be placed in
pdpuba
. Unfortunately, the pdpmba
folder is
empty and all the MASSBUS drivers are in the pdpuba
directory. This also doesn't separate drivers for UNIBUS and QBUS
systems, but this might be because so much of a given driver can be
shared regardless the bus being used for communication - The same is not
true for MASSBUS drivers though. Network drivers are located in
pdpif
. Machine specific, such as memory mapping, goes in
pdp
. Some drivers, mainly for disk controllers, go in both
pdpuba
and pdpstand
, although in most cases
the drivers in pdpstand
seem to be smaller, supporting on a
subset of the functionality.
The autoconfig
directory contains the source for a
programme called by init, just before entering the single user mode
shell during boot. It checks that drivers provide both a probe and
attach routine, followed by checking if the device is actually present.
Some cards support dynamic interrupt vector addresses, such as MSCP disk
controllers. In these cases, autoconfig is responsible for setting those
vector addresses to a free memory location. Information about which
devices should be present is defined in /etc/dtab
.
We can actually get a driver working without having to touch anything in
autoconfig or dtab, but it isn't really the right way to build a
driver.
In order to hook the driver into the kernel, we also need to modify
pdp/conf.c
. This file checks whether a driver is enabled at
compile time, and provides the function prototypes for calls such as
open
, close
, read
, and
write
. If a driver isn't present, these are mapped to a
nodev
routine instead. conf.c
also contains a
table of all these calls, for all drivers.
Writing the Driver
Character and Block based devices
Devices under Unix tend to fall in one of two categories: block devices or character devices. The fundamental difference between the two is that block devices have buffering. This means sending a single byte to the block device will not result in a single write. A character device does not buffer bytes, and instead handles each byte as they arrive. Character device read and write functions are synchronous - they do not return until the byte has been written or read. Block devices do not even have read and write functions, instead they implement what is called (at least in the BSD world) a strategy function. The strategy function handles both reading and writing, but from buffers, and is called periodically, usually when the buffers are full.
Which is better for a GPIB interface? In some ways, GPIB is a lot like a serial port - when talking to, for example, an oscilloscope, you type a command, press return, and the remote device responds. Under Unix, serial devices are implemented as character devices, so this might lead us to believe that our driver should also implement a character device. However, GPIB is a G eneral P urpose bus, and many GPIB devices existed which would typically be considered block devices, such as floppy drives.
Realistically though, neither block or character devices are generic
enough to support all the possibly functionality of GPIB. I'm not sure
we could even do it if we started using ioctl
. This leaves
us with two options. We either make some assumptions about what kind of
devices we support, or we provide a library within the kernel, allowing
'higher level' drivers to interact with devices across GPIB.
Unfortunately, given that this is my first kernel project, I chose the
former option. I will be working under the assumption that all the
devices I talk to are like oscilloscopes, where we can just
send text based commands, terminated with line breaks.
Device Nodes
Earlier I mentioned that in order to provide a hook into the kernel,
we must modify pdp/conf.c
to define what a device driver
supports (e.g. read, write, etc.). However, we also have to define a
major number to our device. The major number ties a file system node
(e.g. /dev/ttyS0
) to a specific driver. It is a unique
numeric identifier for that driver. For our IBV11 driver, we will use
27
, because it's the next available number (26
is used by the file descriptor device). Some controllers may have
multiple devices attached to them, and for those we also have a minor
number. Creating a device node is quite simple - we just use the
mknod
program which takes a path, the type of device
(character or block), the major number and the minor number.
$ mknod /dev/ibv0 c 27 1
For our IBV11 driver, we can use the minor number to indicate the
address of the device we are talking to. This means we could, for
example, create a device called /dev/ibv/scope
which points
to address 3 on the GPIB bus.
A Dummy Driver
To figure out how to pass bytes from user space to kernel space, and
back, I decided I would write a dummy driver. The purpose of this driver
is to spit out a string when we read from it (I chose "Hello"), and to
write any character written to the device to syslogd
.
Let's start with pdpuba/dummy.c
. This first chunk pulls
in the syslog
and uio
headers, defines a
buffer and creates the open function. The open function should return 0
if successful, and a non-zero value if there was an error. The man page
for open
describes some of the common error codes you might
want to return if this fails.
#include "../h/syslog.h"
#include "../h/uio.h"
#define DUMMY_BUFF_LEN 128
dummyopen(dev, flag)
dev_t dev;
short flag;
{
log(LOG_INFO, "dummy open\n");
return 0;
}
The close method is very similar - it should also return 0 if successful. For our dummy driver, we are not actually opening a device, so we don't need to worry about this. Still, it is a good opportunity to add a log call because it will help us understand when these functions are called.
dummyclose(dev, flag)
dev_t dev;
short flag;
{
log(LOG_INFO, "dummy close\n");
return 0;
}
Next, the read method. This makes use of the ureadc
function, which conveniently passes a single character into the
uio
object.
dummyread(dev, uio, flag)
dev_t dev;
struct uio *uio;
int flag;
{
log(LOG_INFO, "dummy read\n");
ureadc('H', uio);
ureadc('e', uio);
ureadc('l', uio);
ureadc('l', uio);
ureadc('o', uio);
return 0;
}
Finally, the write function. The majority of this was copied from the
lp
driver. For our IBV11 driver, this will handle the
actual pushing of bytes out to GPIB.
dummywrite(dev, uio, flag)
dev_t dev;
struct uio *uio;
int flag;
{
register int n;
register char *cp;
char inbuf[DUMMY_BUFF_LEN];
int error;
while (n = MIN(DUMMY_BUFF_LEN, uio->uio_resid)) {
cp = inbuf;
error = uiomove(cp, (int)n, uio);
if (error)
return error;
do
log(LOG_NOTICE, "dummy write: c=%c\n", *cp++);
while (--n);
}
return 0;
}
While there are a few other files which need to be modified (e.g.
scb.s
and conf.c
), this is pretty
much all that is required for a simple character device driver. I
built this into the kernel as device number 27, so below is a quick
example of how we can use it.
$ mknod /dev/dummy c 27 0
$ cat /dev/dummy
HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello
HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello
^C
$ echo Hello > /dev/dummy
Taking a look at /usr/adm/messages
will reveal a dummy
open, several dummy reads, and a dummy close. The echo
line
(i.e. writing to the device) will also produce a dummy open and close,
but also a bunch of dummy writes, one for each character.
Addressing
One of the things I always appreciated about C (which is also a thing many people hate about C due to security concerns etc), is the ability to take address in memory and just cast it to an object. We learnt earlier than our IBV card has two registers, the status register and the data register. These registers are contiguous in memory, occupying a single word of memory each (16 bits, because it's a PDP-11). So, what do you think if we define the following structure and cast a pointer to it?
struct ibvdevice {
short ibs; /* instrument bus status */
short ibd; /* instrument bus data */
};
struct ibvdevice *ibv = (struct ibvdevice *)0160150;
Well, we can easily address the data register at
ibv->ibd
and the status register at
ibv->ibs
. However, we learnt earlier that we can only
actually write to the lower half of the data register. We are also going
to need to query specifically that byte of memory quite frequently. Of
course, we could just and it with something (lame), or we could make our
structure above more useful.
struct ibvdevice {
union {
short ibs; /* Control status register */
struct {
char ibsl; /* low byte for r/w */
char ibsh; /* high byte for ro (except for b15) */
} s;
} un1;
#define ibvcsr un1.ibs
#define ibvcsrl un1.s.ibsl
#define ibvcsrh un1.s.ibsh
union {
short ibd; /* Data register */
struct {
char ibd_io; /* i/o data (low byte) */
char ibd_status; /* data bus status (high byte) */
} s;
} un2;
#define ibvd un2.ibd
#define ibvio un2.s.ibd_io
#define ibvds un2.s.ibd_status
};
This is what my struct actually ended up looking like. I know, sort of disgusting, but allows us to very easily access every part of the status and data registers with ease.
Interrupt Handling and Priorities
PDP-11 interrupts are defined as vector addresses which consist of 2 words. The first word contains the address of interrupt handler, and the second word contains the processor status word (PSW). Going into details of all the bits within the PSW isn't really relevant to the our goal here. However, what we do need to know is that three bits of the PSW define the priority of an interrupt handler. This allows more important interrupts to be processed immediately, deferring less important interrupts until the processor isn't thinking so hard. The actual hardware which drives these interrupts is based around three interrupt lines on the physical backplane. In order for the CPU to handle the interrupt, the interrupt priorities on the bus and the PSW must match.
Interrupt handlers are defined in two places scb.s
.
First, towards the top of the file, where we use DEVTRAP
macros to specify the vector address, handler and priority level. For
our IBV11 driver, this looks like:
#if NIBV > 0 /* IBV11 */
DEVTRAP(420, ibvinterr, br4) /* Error */
DEVTRAP(424, ibvintsr, br4) /* Service Request */
DEVTRAP(430, ibvintcmd, br4) /* Command and talker */
DEVTRAP(434, ibvintlis, br4) /* Listener
#endif
If we try to build this as is, the compiler will complain, telling us that these functions aren't defined. We have to define them towards the bottom of this
#if NIBV > 0 /* IBV11 */
HANDLER(ibvinterr)
HANDLER(ibvintsr)
HANDLER(ibvintcmd)
HANDLER(ibvintlis)
#endif
It seems simple when I write this here, but it took me quite a while to figure this bit out 😅.
Compiling
Once all the pieces have been put in the right place, we can modify
our GENERIC
configuration file to include a flag which will
enable or disable the IBV11 driver at compile time.
NIBV 1 # IBV-11
We also need to modify the config
script to produce a
header file for the IBV11. There are lots of examples within this
script, so not worth explaining in any further detail.
The general procedure for compiling a custom 2.11BSD kernel is roughly as follows:
- Copy
GENERIC
kernel configuration file to a name of your choosing. For this I usually use hostname, unless I'm testing some weird stuff. - Modify your kernel configuration to enable the devices specific to your machine.
- Run the
config
script with your configuration file as an argument. This will produce a build directory one which you cancd
into. - Run
make
- this will almost certainly fail with an error about overlays being too big. - Rearrange the kernel objects so they fit together. These overlays
correspond to memory ranges which can be accessed at a given time. The
memory management unit manages this for us, but we need to be sensible
about the order of our objects. The most used objects need to be first,
and related objects should be close together to prevent excessive page
switching. The base can be no more than 56KB, and each overlay can be no
more than 8KB. You can use the
size
command to find your chonkiest kernel libraries.
Once you have finally got it to compile, you should take a backup of your current kernel and then install your new one before rebooting.
$ cp /unix /unixold
$ make install
With any luck, things should boot up fine, but if they don't you
might need to specify unixold
at boot time to figure out
what went wrong.
Testing
Originally I constructed a fairly ugly looking cable to go from the 20 pin IDC header, to the 24 pin GPIB Centronix plug. However, while searching for "digital dec" (as you do) on eBay one night, I accidentally stumbled across the correct cable for ~£28, so snapped it up.
I'm still working on some bugs with this driver, so I'll include some more stuff here once it's working fully. Just wanted to get this blog post out of draft mode for now. :-)
Related posts:
Wanting to leave a comment?
Comments and feedback are welcome by email (aaron@nospam-aaronsplace.co.uk).