FreeBSD Parallel Port Driver Demo

For my Operating Systems class, I like to give a device driver demo, where we control a lamp using a parallel port. The device driver is really simple, and just writes either 0 or 0xff to the parallel port's data pins. But when putting the demo together, I noticed a distinct lack of extra-simple FreeBSD parallel port driver examples. So here's mine! It works on my laptop (Lenovo R61) with FreeBSD 13.2. I don't know if it works on anything else.

SRCS+= ppbus_if.h 

 * FreeBSD Driver for Lamp in MLH 310
 * (Parallel Port driver to trigger PowerTail)
 * Seth Long, Spring 2024
 * This is a *very* simple device driver.  The PowerTail has a solid state 
 * amplification circuit followed by a relay, and can switch mains current.
 * I use this as a device driver demo for CS430 Operating Systems.  This 
 * driver is not compatible with other uses of the parallel port at the same
 * time, simply sets all data pins high for on, all low for off.  The powertail
 * can be connected to any of the data pins.  The driver makes no attempt to 
 * determine if there's anything else on the port, and assumes exclusive use.

#include "ppbus_if.h" /* Will be generated by make, include in the Makefile */

int status = 0;
struct cdev *lamp_dev;
/* Parallel Port Controller.  NOT ppbus or ppi, although these could look
 * like the right thing.  
 * Note that a device_t is "struct _device *" by typedef, but this constitutes
 * an incomplete type in this code.  The definition is in kern/subr_bus.c as 
 * of FreeBSD 13.2, but it looks (I'm no authority here) like kernel module
 * authors should avoid using structure members directly, and instead rely on
 * functions like device_get_name whenever access is needed. */
device_t ppc;

/* Note:  This does not actually check to see if the user buffer is large 
 * enough, or if there's an offset less than 3 or 4.  Therefore, the read
 * must be accomplished in a single call to read.  This is what will happen
 * if the user runs something like "cat /dev/lamp" */
static int lamp_read(struct cdev *dev, struct uio *uio, int ioflag){
		return 0;
		uiomove("ON\n", 3, uio);
		uiomove("OFF\n", 4, uio);
	return 0;

/* Note:  Expected usage from userspace is something like this:
 * echo on > /dev/lamp
 * Or from code:
 * write(lamp_fd, "on", 2);
 * NOT this:
 * write(lamp_fd, "o", 1);
 * write(lamp_fd, "n", 1);
 * Writing data to the port:
 * PPBUS_IO type 14 will set the data register.  There might be a constant I 
 * could have used instead.  Setting the data register sets the state of the 
 * pins, so 0xff will set all pins high.  The two arguments which are 0 are
 * the address of a data area, and the size of the data area, but for a type
 * 14, the last parameter is the value of the register.  Despite the naming
 * of PPBUS_IO, it needs the ppc, not the ppbus.
static int lamp_write(struct cdev *dev, struct uio *uio, int ioflag){
	char cmd[16];
	size_t len = MIN(uio->uio_resid, 16);
	uiomove(cmd, len, uio);

	if(!strncmp(cmd, "on", 2) || !strncmp(cmd, "ON", 2)){
		status = 1;
		uprintf("Turning light ON\n");
		printf("IO Result:  %d\n", PPBUS_IO(ppc, 14, 0, 0, 0xff));
	} else { /* Anything that isn't "on" or "ON" means off */
		status = 0;
		uprintf("Turning light OFF\n"); 
		printf("IO Result:  %d\n", PPBUS_IO(ppc, 14, 0, 0, 0x00));
	return 0;

static struct cdevsw lamp_dev_description = {
	.d_version = D_VERSION,
	.d_write = lamp_write,
	.d_read = lamp_read

/* print_all_devices is recursive, and prints the entire device tree.  But it
 * also sets the value of the global device_t ppc, so without calling this 
 * function, the driver won't work.  Could be converted to search silently,
 * and terminate once the correct device is found.  It would be better to 
 * use an iterative solution here to avoid excessive stack use, but I was in
 * a hurry when I wrote this. */
void print_all_devices(device_t, char*);
void print_all_devices(device_t startpoint, char *prefix){
       	device_t *devlist;
       	int devcount;
	const char* name = device_get_name(startpoint);
	if(name && !strcmp(name, "ppc"))
		ppc = startpoint;
       	device_get_children(startpoint, &devlist, &devcount);
	char new_prefix[32];
	snprintf(new_prefix, 32, "%s- ", prefix);
       	printf("%s Device count:  %d\n", prefix, devcount);
       	for(int i = 0; i < devcount; i++) {
       		printf("%s %s : %s\n", prefix, device_get_name(devlist[i]), device_get_desc(devlist[i]));
		print_all_devices(devlist[i], new_prefix);
       	free(devlist, M_TEMP); 

static int lamp_loader(struct module *m, int what, void *arg) {
		case MOD_LOAD: 
			ppc = 0;
			print_all_devices(root_bus, "");
				printf("ppbus was found\n");			
			printf("Loading Lamp Module\n");
			if(make_dev_p(0, &lamp_dev, &lamp_dev_description, 0, UID_ROOT, GID_WHEEL, 0666, "lamp"))
				printf("Lamp Device Failed to load!\n");

		case MOD_UNLOAD:
			printf("Unloading Lamp Module\n");

	return 0;

static moduledata_t lamp_mod = {

/* There is a DECLARE_DRIVER macro that may be more appropriate here */