Writing an Etherboot Driver

Preliminaries

So Etherboot does not have a driver for your network adapter and you want to write one. You should have a good grasp of C, especially with respect to bit operations. You should also understand hardware interfacing concepts, such as the fact that the x86 architecture has a separate I/O space and that peripherals are commanded with `out' instructions and their status read with `in' instructions. A microprocessor course such as those taught in engineering or computer science curricula would have given you the fundamentals. (Note to educators and students in computer engineering: An Etherboot driver should be feasible as a term project for a final year undergraduate student. I estimate about 40 hours of work is required. I am willing to be a source of technical advice.)

Next you need a development machine. This can be your normal Linux machine. You need another test machine, networked to the development machine. This should be a machine you will not feel upset rebooting very often. So the reset button should be in working condition. :-) It should have a floppy drive on it but does not need a hard disk, and in fact a hard disk will slow down rebooting. Alternatively, it should have another network adapter which can netboot; see discussion further down. Needless to say, you need a unit of the adapter you are trying to write a driver for. You should gather all the documentation you can find for the hardware, from the manufacturer and other sources.

Background information

There are several types of network adapter architecture. The simplest to understand is probably programmed I/O. This where the controller reads incoming packets into memory that resides on the adapter and the driver uses `in' instructions to extract the packet data, word by word, or sometimes byte by byte. Similarly, packets are readied for transmission by writing the data into the adapter's memory using `out' instructions. This architecture is used on the NE2000 and 3C509. The disadvantage of this architecture is the load on the CPU imposed by the I/O. However this is of no import to Etherboot (who cares how loaded the CPU is during booting), but will be to Linux. Next in the sophistication scale are shared memory adapters such as the Western Digital or SMC series, of which the WD8013 is a good example. Here the adapter's memory is also accessible in the memory space of the main CPU. Transferring data between the driver and the adapter is done with memory copy instructions. Load on the CPU is light. Adapters in this category are some of the best performers for the ISA bus. Finally there are bus mastering cards such as the Lance series for the ISA bus and practically all good PCI adapters (but not the NE2000 PCI). Here the data is transferred between the main memory and the adapter controller using Direct Memory Access. Setting up the transfers usually involves a sequence of operations with the registers of the controller.

Structure of the code

Examine the file skel.c, in the src directory, which is a template for a driver. You may also want to examine a working driver. You will see that an Etherboot driver requires 4 functions to be provided:

No routine needs to be public, all routines should be static and private to the driver module. Similarly all global data in the driver should be static and private.

If the NIC is a PCI adapter, create a struct pci_driver (as in skel.c) and an array of struct pci_id:

static struct pci_id skel_nics[] = {
PCI_ROM(0x0000, 0x0000, "skel-pci", "Skeleton PCI Adaptor"),
};
Fill the pci_id array with one entry for each combination of pci vendor id and pci device id that your driver can handle. PCI_ROM is a a macro defined in include/pci.h. The arguments have the following meaning: vendor id, device id, rom name and short description. Since this information is also used to build the Makefile rules, you must use the PCI_ROM macro and can't fill in the values directly. Both the pci vendor and device id must be given in hex form, no define is allowed. Additionally PCI_ROM must occur only once in a line and one macro call must not span more than one line. You can obtain the vendor and device ids from the file /usr/include/linux/pci.h. It is also displayed by PCI BIOSes on bootup, or you can use the lspci program from the pciutils package to discover the ids.

If the NIC is an ISA adapter, create a struct isa_driver (as in skel.c) and one line like the following:

ISA_ROM("skel-isa", "Skeleton ISA driver")
in your driver source. The ISA_ROM macro is like the PCI_ROM without the vendor/device ids. The same rules about formatting as in the PCI case apply.

Only for special cases where the automatic generation of build rules via the PCI_ROM and ISA_ROM entries does not work, add an entry to the here document in genrules.pl so that the build process will create Makefile rules for it in the file bin/Roms.

The above mentioned structs and macros hold all information that etherboot needs about your driver. In case you wonder how this works at all although everything is declared static: The special build process that is used by Etherboot (including linker scripts and some Perl magic) packs the necessary information into public segments.

The Etherboot build process places a few restrictions on your driver: If you need more than one .c file, the main file (that will contain the PCI_ROM or ISA_ROM macro call) must contain #include directives for the other files. They must not contain a PCI_ROM or ISA_ROM call. See drivers/net/prism* for an example.

Rom naming rules

Currently there is no official rom naming convention in etherboot. Use some descriptive name, but note that two ore more consecutive hyphens (like in "my--rom") are not allowed, since "--" is the delimiter sign for multiple-driver-roms. Sometimes it is difficult to find a sensible name, for example for "NICs" that are built in motherboard chipsets or if you don't know the model name. In this case we choose to name the corresponding roms "driver-deviceid", like "eepro100-1035". Of course you have to make sure that your rom name is unique in etherboot.

Booting the code from a floppy

Use the rule for bin/driver.fd0 to write another instance of the driver to the floppy for testing. Use lots of printf statements to track where execution has reached and to display the status of various variables and registers in the code. You should expect to do this dance with the development machine, floppy disk and target machine many many times.

Booting the test code with another Etherboot ROM

There is another method of testing ROM images that does not involve walking a floppy disk between the machines and is much nicer. Set up a supported NIC with a boot ROM. Put the target NIC on the same machine but at a non-conflicting I/O location. That is to say, your test machine has two NICs and two connections to the LAN. Then when you are ready to test a boot image, use the utility mknbi-rom to create a network bootable image from the ROM image, and set up bootpd/DHCPD and tftpd to send this over the when the machine netboots. Using Etherboot to boot another version of itself is rather mind-boggling I know.

Writing the code

First set up the various required services, i.e. BOOTP/DHCP, tftp, etc. on the development machine. You should go through the setup process with a supported adapter card on a test machine so that you know that the network services are working and what to expect to see on the test machine.

If you are starting from a Linux driver, usually the hardest part is filtering out all the things you do not need from the Linux driver. Here is a non-exhaustive list: You do not use interrupts. You do not need more than one transmit buffer. You do not need to use the most efficient method of data transfer. You do not need to implement statistics counting. In general it is a good idea to use the latest Linux driver as base, because it usually supports newer cards and has more bugs fixed than older versions. If the driver is written by Donald Becker, it is probably best to start from the version available on this page, since the driver in the official kernel may be behind. See also the Section called Keeping your driver in sync with the Linux version.

Generally speaking, the probe routine is relatively easy to translate from the Linux driver. The exception is when you need to handle media and speed switching. The transmit is usually straightforward, and the receive a bit more difficult. The main problem is that in the Linux driver, the work is split between routines called from the kernel and routines triggered by hardware interrupts. As mentioned before, Etherboot does not use interrupts so you have to bring the work of transmitting and receiving back into the main routines. The disable routine is straightforward if you have the hardware commands.

When coding, first get the probe routine working. You will need to refer to the programmer's guide to the adapter when you do this. You can also get some information by reading a Linux or FreeBSD driver. You may also need to get the disable routine working at this time.

Next, get the transmit routine working. To check that packets are going out on the wire, you can use tcpdump or ethereal on the development machine to snoop on the Ethernet. The first packet to be sent out by Etherboot will be a broadcast query packet, on UDP port 67. Note that you do not need interrupts at all. You should ensure the packet is fully transmitted before returning from this routine. You may also wish to implement a timeout to make sure the driver doesn't get stuck inside transmit if it fails to complete. A couple of timer routines are available for implementing the timeout, see timer.h. You use them like this (in pseudo-code):

	for (load_timer2(TIMEOUT_VALUE);
		transmitter_busy && (status = timer2_running()); )
		;
	if (status == 0)
		transmitter_timed_out;
The timeout value should be 1193 per millisecond of wait. The maximum value is 65535, which is about 54 milliseconds of timeout. If you just need to delay a short time without needing to do other checks during the timeout, you can call waiton_timer2(TIMEOUT_VALUE) which will load, then poll the timer, and return control on timeout.

Please do not use counting loops to implement time-sensitive delays. Such loops are CPU speed dependent and can fail to give the right delay period when run on a faster machine.

Next, get the receive routine working. If you already have the transmit routine working correctly you should be getting a reply from the BOOTP/DHCP server. Again, you do not need interrupts, unlike drivers from Linux and other operating systems. This means you just have to read the right register on the adapter to see if a packet has arrived. Note that you should NOT loop in the receive routine until a packet is received. Etherboot needs to do other things so if you loop in the poll routine, it will not get a chance to do those tasks. Just return 0 if there is no packet awaiting and the main line will call the poll routine again later.

Finally, get the disable routine working. This may simply be a matter of turning off something in the adapter.

Things to watch out for

Things that may complicate your coding are constraints imposed by the hardware. Some adapters require buffers to be on word or doubleword boundaries. See rtl8139.c for an example of this. Some adapters need a wait after certain operations.

Tidying up

When you get something more or less working, release early. People on the mailing lists can help you find problems or improve the code. Besides you don't want to get run over by a bus and then the world never gets to see your masterpiece, do you? :-)

Your opus should be released under GPL, BSD or a similar Open Source license, otherwise people will have problems using your code, as most of the rest of Etherboot is GPLed.

Keeping your driver in sync with the Linux version

Most Etherboot drivers are derived from their Linux counterparts. Sooner or later the Linux driver where you started from will get updated. This may include bugfixes and support for new NICs. Unfortunately there is no way to keep the Etherboot driver automatically up to date, but if you keep the following rules in mind while porting it can be made easier: