SchOS/docs/guides/adding-a-driver.md
Nexoniarz 1334f446cc config/drivers.conf: which drivers are compiled in
y or n per optional driver (virtio-gpu, stdvga, e1000, rtl8139, native r8169, AC'97, AHCI, NVMe), all y except the native
r8169, overridable with make DRV_X=n. Only drivers nothing else calls can be optional; the registry #ifdefs them and the
Makefile leaves their objects out. Built with every one off (links, none of their symbols present), with r8169 on, and
by default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 08:59:55 +01:00

6.5 KiB

Adding a driver

The device model itself is described in ../architecture/devices.md. This is the procedure.


Where to get one from

The best source is the specification, not another kernel. xHCI, AHCI, virtio, NVMe and the Intel datasheets are public documents with no licence on what you write from them. kernel/drivers/bus/xhci/xhci.c came from the spec, and so did net/e1000/ and gpu/virtio_gpu/.

Three sources, in the order to try them:

  1. A native driver, written from the specification. The default, and what every driver in the kernel is.
  2. Linux's own driver, unmodified, through LDLS (ldls.md), when the device is one Linux supports and SchOS cannot afford to write a driver for — a NIC with a PHY behind it, a GPU, a Wi-Fi card. It runs in a process, so a crash costs the interface and not the machine. This is how the laptop's Realtek NIC is driven.
  3. BSD or Linux code taken into the tree. Licensing no longer forbids either: SchOS is GPL-2.0-or-later, so GPL-2.0-only code combines with it, and BSD-2/3 code can be used directly with a NOTICE entry — see ../licensing.md. The reason to prefer 1 and 2 is maintenance, not law: a driver copied in is a driver that has to be tracked.

Reading another kernel to understand hardware is fine and normal.

One caveat about "port it from BSD" as a plan, learned by looking: FreeBSD's sys/dev/usb is roughly 100k lines welded to newbus, devclass, bus_dma, callouts, taskqueues and FreeBSD's locking. Porting it means porting FreeBSD's device framework first and finding the USB stack somewhere underneath.

BSD is far more valuable as the reference you check yourself against than as the code you paste. Read it when the spec is ambiguous — which it is, often — and write your own.

Where a table of constants is genuinely faster to transcribe than to retype from a PDF — device IDs, register offsets — taking it from a BSD tree is fine after the licence check, and goes in NOTICE. That is data; the logic stays ours.


The procedure

  1. Write kernel/drivers/<class>/<name>/<name>.c exporting a struct pci_driver or struct usb_driver. One directory per driver, so a driver that grows a register header, a second source file or a firmware blob has somewhere to put it:

    kernel/drivers/
      registry.c
      gpu/vga/          gpu/virtio_gpu/
      net/e1000/  net/rtl8139/  net/r8169/
      storage/ahci/  storage/nvme/
      audio/ac97.c
      bus/xhci/
      usb/core/  usb/hid/  usb/storage/  usb/hub/
    
  2. Declare it in kernel/drivers/registry.c and add it to the array.

  3. Add the object to KERN_DRIVERS in the Makefile. 3½. If nothing else in the kernel calls it, make it optional: a DRV_<NAME> line in config/drivers.conf, the name in DRV_LIST in the Makefile, the object behind $(filter y,$(DRV_<NAME>)), and an #ifdef CONFIG_DRV_<NAME> around its line in registry.c. A driver other code calls (vga_fb, the USB stack) cannot be optional.

  4. Fill in the class vtable for what it is, and point .ops at it. That is the whole of what the rest of the kernel sees:

    class vtable consumer
    DEVCLASS_DISPLAY struct display_ops (display.h) none today — Xorg draws through /dev/fb0
    DEVCLASS_NET struct netdev_ops (netdev.h) the TCP/IP stack
    DEVCLASS_AUDIO struct audio_ops /dev/dsp

    A display driver that owns a linear framebuffer also publishes it with gfx_attach_scanout() (gfx.h). That is what the text console and the crash screen draw on, and it is separate from the display class on purpose: the class is about frames — acquire, present, vblank — and the crash screen has to work when nothing else in the system does.

    A driver that fills these in never mentions its consumer, and its consumer never mentions it. Getting this wrong has a characteristic shape: the e1000 shipped with .ops = 0 and the network stack called e1000_transmit() directly, so kernel/net/ was full of one driver's name and the second NIC driver would have had to edit the protocol stack to add itself.

Private headers go next to the driver, not in include/. The build adds the driver's own directory to the include path, so e1000.h — a register map that exactly one file has any use for — lives in kernel/drivers/net/e1000/. include/ is for headers the rest of the kernel needs.

Registration in the device model is automatic — pci_bind() and usb_bind_all() publish whatever they claim, so a driver cannot forget to. Set devclass on a USB driver, or the device lands in other.

A driver that can drive more than one card at once keeps its instances in an array and implements .attached, which the bus calls once the struct device exists; hang the instance on dev->priv there and read it back in every op. It cannot be done inside probe(), because the device is published afterwards. Skipping this is how a driver ends up refusing the second card, as the e1000 did until it was asked to drive two.

The best source for a new driver is the specification, not another kernel: xHCI, AHCI, virtio, NVMe and the Intel datasheets are public documents with no licence on what you write from them. kernel/drivers/bus/xhci/xhci.c came from the spec, and so did net/e1000/ and gpu/virtio_gpu/. Where a table of constants is genuinely faster to transcribe than to retype from a PDF -- device IDs, register offsets -- taking it from a BSD tree is fine after the licence check, and goes in NOTICE. That is data; the logic stays ours.


Checklist

  • One directory under kernel/drivers/<class>/<name>/
  • Exports a struct pci_driver or struct usb_driver
  • Declared in kernel/drivers/registry.c and added to its array
  • Object added to KERN_DRIVERS in the Makefile, and an option in config/drivers.conf if it can be left out
  • Class vtable filled in and .ops pointed at it
  • .attached implemented if it can drive more than one card
  • devclass set, if it is a USB driver
  • Private headers next to the driver, not in include/
  • A NOTICE entry, if anything was copied — before the commit, not after
  • A selftest reachable from /proc/sysrq, if the class has one (netdev_selftest is the model)