Introduction

I put this little document together mostly to educate myself, but I presume it may be useful to others who are interested in understanding or meddling with a NetBSD device driver. A couple of publications with related information are worth mentioning. The first is The Design and Implementation of the 4.4BSD Operating System by McKusick et. al. Chapter 14 is of particular interest, as it discusses system startup and the related issues of kernel configuration, autoconfiguration, device probing, and attaching. Chapter 6 is the other place to look, but it is amazingly thin on real driver issues. What you really want is something like TCP/IP Illustrated, volume 2 by Wright and Stevens, but to my knowledge it does not exist. A distantly related document that is still quite useful is the "Writing Device Drivers" section of the SunOS manuals.

Building a kernel with config

As anyone who has ever built a NetBSD kernel knows, the place where the game starts is /usr/src/sys/arch/MACHINE/conf, where MACHINE is i386 or sun3 or some such. This is where the input file to the config program ( /usr/sbin/config ) lives, and any device will need to be named here, or you won't get far at all. There are actually two parts to the config process, the part that goes on when a kernel is built, and the autoconfiguration part that happens when the kernel actually runs and initializes itself.

You type "config TARGET" where TARGET is the name of the config file, and config creates a directory ../compile/TARGET and generates a number of files in that directory. The files generated are a Makefile, ioconf.c, swapnetbsd.c, and a bunch of include files. The file ioconf.c is of particular interest, as it holds data structures that govern autoconfiguration.

The include files are named xxx.h, where xxx is a device name and consist of a single line of the form:

#define NXXX 1

Autoconfiguration

When a netbsd kernel boots up, one of the most visible aspects of the process is the probing of devices done during what is known as autoconfiguration. Basically, the config file is just a list of devices that are potentially present in the system. What devices are actually present is worked out during autoconfiguration when the kernel probes each device.

NetBSD useds an interesting machine independent autoconfiguration scheme. This scheme (sometimes refereed to as the new config) was developed at Lawrence Berkeley Labs for the sparc port, but now is used for (all?) of the NetBSD ports.

Here is how the game is played: by some means (infinitely interesting, but not to be described in detail here and now), the kernel gets loaded somewhere in memory and is set running. We can traverse the following sequence of entry points, ignoring a lot of details along the way:

start in sys/arch/MACHINE/MACHINE/locore.s
main() in sys/kern/init_main.c
config_init() in sys/kern/subr_autoconf.c --returns to main()
cpu_startup() in sys/arch/MACHINE/MACHINE/machdep.c
configure() in sys/arch/MACHINE/MACHINE/autoconf.c

Perhaps the most important thing to notice in all of this is the dance back and forth from machine independent to machine dependent code, this is not the last we will see of this.

The cfdata structure

The heart of ioconf.c is an array of cfdata structures, one per device configured in the system. Some of these devices are buses, others are controllers, others are things attached to buses or controllers. Everything is attached to something in a tree-like structure, anchored at the root (which is typically mainbus0 or some such, which is itself anchored to the root, but there is no entry for root in the cfdata array, and there may be multiple roots). The cfdata structure is defined in /usr/src/sys/sys/device.h and looks like this:
struct cfdata {
	struct	cfattach *cf_attach;	/* config attachment */
	struct	cfdriver *cf_driver;	/* config driver */
	short	cf_unit;		/* unit number */
	short	cf_fstate;		/* finding state (below) */
	int	*cf_loc;		/* locators (machine dependent) */
	int	cf_flags;		/* flags from config */
	short	*cf_parents;		/* potential parents */
	void	(**cf_ivstubs)		/* config-generated vectors, if any */
			__P((void));
};

The first 2 entries point to structures that are within the specific device driver itself. Note that even buses must have device drivers with the entry points that support the autoconfiguration process. The unit number is the digit part of the name (i.e. 0 for sd0). State is either NORM (for fully specified devices like sd0) or STAR (for wildcard specifield devices like sd?). The loc field gives hardware address information. Flags is usually zero, by may be set by a flags entry in the config file. The parents field is almost universally zero. The ivstubs field point to an array of values, terminated by a -1 entry.

The cfattach and cfdriver structures

Each device driver must have an initialized structure of each of these present in it for the autoconfiguration code to latch onto.
struct cfattach {
	size_t	  ca_devsize;		/* size of dev data (for malloc) */
	cfmatch_t ca_match;		/* returns a match level */
	void	(*ca_attach) __P((struct device *, struct device *, void *));
	/* XXX should have detach */
};
struct cfdriver {
	void	**cd_devs;		/* devices found */
	char	*cd_name;		/* device name */
	enum	devclass cd_class;	/* device classification */
	int	cd_indirect;		/* indirectly configure subdevices */
	int	cd_ndevs;		/* size of cd_devs array */
};

Root devices

The show really starts in configure() in the file: sys/arch/MACHINE/MACHINE/autoconf.c For each root device, this should call config_rootfound("mainbus",NULL) The config_rootfound() routine (in sys/kern/subr_autoconf.c) searches for the requested root device in the cfdata array, and once it finds it, attaches it using config_attach(). The config_attach() routine will ultimately call the xx_attach() entry point in the device driver. For things at the root level (like busses), this will launch a loop thru all devices attached to the bus. For some busses (like pci) it is possible to walk the bus, finding out what is there and then discovering if it matches some configured device. For other busses (like vme, isa) this is impossible, and the thing to do is to use the cfdata array to then probe all devices attached to the bus currently being attached.

Probing, matching, attaching

Each device configured into the system is probed during system boot. Probing is done by calling the match function (typically xx_match()) during the autoconfig process. These used to be called probe functions, but things have changed a bit. The match function may or may not actually touch hardware. Often for a controller, some kind of poking at the hardware is done. In some cases just verifying that a device register can be read or written is considered adequate to say that the probe has succeeded. Some devices are nice enough to have some kind of readable signature that can be verified by software. Ugly cases exist when the same address may contain any one of several devices. A probe for one type of device may severely annoy a device of a different type. (A case in point are certain software configurable wd8013 ethernet cards which can be probed into a state of non-functionality).

Once the probe for a device has succeeded, it is attached. This is basically an opportunity for the driver for the device to initalize itself, allocate resources, and perhaps to launch a new sequence of probing for devices that are attached to it (significantly true for buses and controllers).