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.

Makefile
RCS=lamp_driver.c
KMOD=lamp_driver
SRCS+= ppbus_if.h 
.include 

lamp_driver.c
/*
 * FreeBSD Driver for Lamp in MLH 310
 * (Parallel Port driver to trigger PowerTail)
 * Seth Long, Spring 2024
 * http://isoptera.lcsc.edu/seth
 *
 * 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<sys/param.h>
#include<sys/uio.h>
#include<sys/types.h>
#include<sys/systm.h>
#include<sys/conf.h>
#include<sys/module.h>
#include<sys/kernel.h>
#include<sys/bus.h>
#include<sys/malloc.h>
#include<sys/pcpu.h>
#include<sys/sysctl.h>
#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){
	if(uio->uio_offset)
		return 0;
	if(status)
		uiomove("ON\n", 3, uio);
	else
		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);
	if(!devcount)
		return;
	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) {
	switch(what){
		case MOD_LOAD: 
			ppc = 0;
			print_all_devices(root_bus, "");
			if(ppc)
				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");
			break;

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

	}
	return 0;
}

static moduledata_t lamp_mod = {
	"lamp_driver",
	lamp_loader,
	0
};

/* There is a DECLARE_DRIVER macro that may be more appropriate here */
DECLARE_MODULE(lamp_driver, lamp_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);