Drivers come in many forms as does the hardware they control. Perhaps one of the simplest types of drivers to write for the NT kernel is an NDIS driver for network cards, simplest being in the context of kernel development. That simplicity comes from how the framework used for NIC drivers abstracts away some of the manual setup needed for drivers, eliminating a lot of boilerplate code and making it easier to get a handle on the actual hardware operations being performed. As such we will be introducing driver development using the RTL8139 driver developed for ReactOS.
The RTL8139 is a fairly ubiquitous controller with readily available documentation. It is also the emulated network card in QEMU, meaning it is easy to test. The ReactOS driver written by Cameron Gutman is also simple, clean, and adheres to enough good practices that it can be readily used as a reference. It is also however missing just enough functionality and required components that it serves as a good exercise for people reading this guide. Note also that this walkthrough was written against revision 70387 and any file names or line numbers given may not reflect that of later revisions, especially if some intrepid reader decides to submit patches to fill in the aforementioned holes.
As mentioned previously, when dealing with device drivers there are two pieces to the equation. The first is the actual piece of hardware one is trying to write a driver for. Information about the hardware usually comes in the form of datasheets describing things like how to initialize the device, how to get status information out of it, and how to carry out specific operations. For the RTL8139, that information can be found on this OSDev page (http://wiki.osdev.org/RTL8139). Not only does it describe the various steps to configure the card, it also has links to datasheets with more detailed information and an example driver written for a UNIX-based operating system. That example is primarily useful when the datasheets are ambiguous or incorrect (datasheets often being written by hardware engineers because companies were too cheap to hire proper technical writers), since code is code and will tell you exactly what steps are being taken.
The other piece is the actual API for writing a driver. For Windows, NIC drivers are written against the NDIS interface. Several different types of drivers exist for NDIS, but the simplest is a miniport driver that directly controls the network card. Even then there are additional subcategories for miniport, of which the one this walkthrough deals with is a connectionless miniport driver since the RTL8139 is an Ethernet device. Writing an NDIS miniport driver entails basically plugging in a series of holes to create a bridge between the operating system and the NIC. This is where the datasheets come into play, as they tell one how to fill the holes for the specific device in question.
To begin this exploration, we will examine the entrypoint into an NDIS driver. Much like how user mode applications have a main function as an entrypoint, drivers have a DriverEntry function that serves the same purpose. ReactOS' driver has the entrypoint at line 483 in ndis.c and it is primarily responsible for registering the various callback functions the operating system will use to perform operations on the network card via the NDIS_MINIPORT_CHARACTERISTICS struct. The first thing DriverEntry does however is notify NDIS that it is initializing a driver by calling the NdisMInitializeWrapper function. Assuming NdisMInitializeWrapper succeeds, DriverEntry receives a handle that represents NDIS that it then uses to register all of the callbacks mentioned above. It is these callback functions that do most of the work and their examination will reveal the most about how to write drivers. Briefly, the callbacks are ultimately responsible for initializing the actual hardware, handling interrupts that may occur, and providing the operating system with general information such as numbers of successful and failed transfers.
Before the network card can actually be used, it must be turned on and configured. This is the responsibility of the MiniportInitialize callback function located in ndis.c at line 199. In addition, MiniportInitialize will initialize a data structure that holds all of the device specific information needed by the other callback functions. For this driver, the various device specific information and resources are stuffed into the RTL_ADAPTER data structure defined in nic.h at line 36. The contents of the struct will make sense as more of the driver is examined.
Once more the two halves of a driver, the interaction with NDIS and the interaction with the hardware, come into play. The first piece of information NDIS needs to know is what type of device is this driver for. This is provided by the NdisMSetAttributesEx call in ndis.c at line 251, along with a few other configuration options. Here the previously allocated RTL_ADAPTER struct is provided to NDIS so that it can pass it around to the other callbacks functions. As a generic driver for the RTL8139, Cameron kept things simple by only claiming the device is a bus-master DMA device with NDIS_ATTRIBUTE_BUS_MASTER. Finally, as the RTL8139 is a PCI network card, the type is set with NdisInterfacePci. The next step is to get all of the resources the operating system has allocated for this device.
Before continuing, a brief aside is needed to provide some background for the types of resources provided. At the most simplistic level, communication with devices is done by reading and writing to specific ranges of memory addresses. These ranges are assigned to a device by the operating system from the virtual address space and the need to provide these ranges is one reason why on a non-PAE 32bit operating system even if one has 4GB of physical memory installed, not all of it is usable. Reading from and writing to offsets within the assigned range will then result in certain operations being performed. For example, resetting a device might entail writing a certain value to a specific offset. Getting the current status of the device would be reading from another offset. In most data sheets these offsets are often referred to as registers and NDIS provides access to them via what it terms ports. All of these operations are of course abstracted to a certain extent by helper functions. In the case of NDIS drivers, there are a variety of read and write functions that take in an address and either read or write data at that address. Once this concept is grasped, understanding the rest of the driver code becomes much, much easier.
Getting the resources entails a call to NdisMQueryAdapterResources. What follows is a pretty standard pattern in Win32 development. When unsure of how much space to allocate to receive data, first make a call with a buffer size of zero. The function will respond with the actual size needed and from there one can allocate an actual buffer to receive the data. The two types of resources the driver is interested in are the I/O ports for register access and the interrupt vectors. Once these are identified, their respective addresses and values are added to the RTL_ADAPTER struct. Before the ports can be accessed however the driver must ask NDIS to map them to virtual addresses with NdisMRegisterIoPortRange and store the base memory address in its RTL_ADAPTER struct.
Again, another aside to talk about device communication. While the registers and functions mentioned above are enough to transfer a few bytes of data at a time between the NIC and the operating system, they are wholly inadequate for transmitting the amount of bulk data that often crosses networks. Theoretically the register access method could be used, but performance would be abysmal and the CPU would also be constantly pegged mediating the operations. The solution to this is direct memory access (DMA) wherein the hardware is given direct access to system memory to perform reads and writes without intervention from the CPU.
For the RTL8139 driver, the NdisMInitializeScatterGatherDma call at line 341 in ndis.c is used to ask the operating system to automatically set up the resources needed to perform DMA operations. As the previous statement implies, there is also a way to manually set up the resources, but needless to say it is easier to have the operating system do it for you. Once NDIS is made aware of how the driver wishes to perform DMA, memory must be actually allocated that will be accessible to both the network card and the host system. This is done using NdisMAllocateSharedMemory at lines 351 and 363 of the same file for the receive and transmit buffers respectively.
Once the various operating system resources have been allocated and initialized, it is time to actually configure and perform operations on the network card itself. The functions related to that are in the hardware.c file and the first one of interest is NICPowerOn at line 21. Conveniently, the OSDev page describes all the necessary steps to initialize and configure the RTL8139, sparing the need to dig through a datasheet. This is where one can see just how simple, relatively speaking, communicating with the hardware is. For the RTL8139, setting configuration register one to 0x00 will effectively turn on the device. Here one can see the offset being added to the base memory address to determine the final address for configuration register one. The definitions for all of the port offsets are in the rtlhw.h header file. Next the driver has to be reset to clear away any garbage values in its buffers and status registers. Reset is done by writing the value 0x10 to offset 0x37. Note however that the same register must be read back and its reset bit checked to make sure the device has finished coming out of reset, otherwise attempts to perform operations on the card will result in unexpected behavior.
The remaining operations on the network card are fairly straightforward, simply a matter of writing to a few more registers to point it to the buffer for storing received data and actually enabling transmitting and receiving. There is however one other thing that needs to be done on the NDIS side, specifically identifying the interrupt the device will use and claiming the associated interrupt level. These resources are part of the group assigned to this device by the operating system, so it is simply a matter of reporting the values stored in the RTL_ADAPTER struct.
With configuration complete, it is now time to examine the actual operation of the driver. For this, we will actually examine the NDIS_MINIPORT_CHARACTERISTICS instance back in the DriverEntry function. Simply looking at which callbacks were registered and which were not is telling. For example, there is no function to explicitly check for whether the network card is hung, so the operating system must rely on its own heuristics to do so. There is also no way to disable the interrupt handler, presumably because the developer saw no need for it. Looking at the data sheet, it is actually implied that interrupts can be disabled by clearing the bits in the interrupt mask register so it might make for an interesting exercise for the reader. A few other functions are also ostensibly missing and a strict reading of the MSDN documentation would actually indicate a few required functions are not implemented. Completing them would serve as another good exercise if a reader is so inclined.
Of the remaining implemented functions, we will look at three more groups in this walkthrough. The first relates to interrupt handling and are located in the appropriately named interrupt.c file. They also come in a pair, MiniportISR starting at line 29 and MiniportHandleInterrupt at line 76. At the most basic level, interrupts are basically notifications that an event has happened. When an interrupt fires, the hardware that registered interest in an interrupt will be notified and the driver must respond appropriately. It is entirely possible for multiple hardware devices to share the same interrupt levels and vectors on a system. That being the case, it becomes necessary to be able to identify for whom an interrupt is for. That is the purpose of MiniportISR. To perform the check, the interrupt status register on the NIC is checked to see if the hardware actually generated an interrupt. Actually handling the interrupt is the responsibility of the MiniportHandleInterrupt function. Here, the interrupt is identified and the necessary registers are hit on the NIC to perform whatever reaction is warranted.
The second set of functions is located in info.c and is the pair MiniportQueryInformation at line 69 and MiniportSetInformation at line 276. The MiniportQueryInformation function allows for accessing a variety of statistics such as number of succeeded and failed transfers, the MAC address, and other tidbits like driver developer, several of which are stored in the RTL_ADAPTER struct that gets passed around. MiniportSetInformation on the other hand actually allows for changing a few configuration options for the driver. Examining this pair effectively explains how the various members of the RTL_ADAPTER struct were decided.
The last function we will examine is back in ndis.c at line 42, MiniportSend, because it illustrates how to perform Scatter/Gather DMA operations within a network driver. The nice thing about Scatter/Gather is that NDIS conveniently creates a list of all the packets that need to be transmitted and hands it to the driver. Each element within that list represents a chunk of contiguous bytes to be transmitted. To send them, one simply needs to tell the network card the starting memory address and the length of bytes to read via register operations and the NIC will handle the rest by using DMA to copy out the data.
At this point one should have a sufficient grasp of the two halves of network drivers to be able to look at the RTL8139 driver source code and follow what is going on, even the parts not explained in this walkthrough. One point this walkthrough did not talk about however is some of the numbers scattered through the driver such as the size of buffers. Many of these will come from the driver datasheets while others are from the Ethernet standard itself. Now that their sources have been mentioned, you the reader should have the point of reference needed to look them up. Happy hunting.