Most devices have a “driver” - a piece of code that runs in the Kernel - for communicating with devices. These drivers typically use MMIO to control the device and get status and use whatever kernel subsystem is relevant to provide access to userspace. For example, network cards interface to the networking subsystem to provide network adapters. The biggest reason for this is code reuse and interface sharing - the common code is shared between all the drivers and the API presented to userspace is uniform.

Another advantage is layering - for example, you can write an SPI device driver that hooks into the SPI subsystem. All you really need to implement are three functions: probe, transfer message, and remove. Probe and remove are about handling device attaching and removal. The transfer message callback is where the work happens. The kernel passes you a structure that describes the message (clock mode, bits per word, the data buffers, etc) and it’s up to your driver to poke the hardware in a way that gets it to transmit the message.

Here is one such driver that I wrote for a former employer.

kp_spi_probe() is the callback used when this device is added to the system. It informs the kernel that there is a new SPI controller to use. It also tells the kernel about SPI devices attached to this controller - more on that in a minute.

kp_spi_remove() is the callback used when the device is removed from the system.

kp_spi_transfer_one_message() is the callback used when the kernel needs to send a SPI transaction across the bus. It’s mostly checking and setup code. It eventually calls kp_spi_txrx_pio() which is what actually tells the hardware to transfer data to the SPI devices.

You’ll notice at this point that nothing has really said what that data going across the bus is or what is on the other side of the link. This is because this is a generic SPI device driver - it doesn’t need to know what device is out there or how to talk to it! That’s all handled by higher layer code.

This driver does know what is on the other side of the SPI bus - that’s what the spi_new_device() calls do. They tell the kernel that attached to this SPI bus is a “n25q256a11” NOR flash device. It also sets up the partition tables for that device. Note that this part doesn’t need to be done by this driver - it could be done by another driver (like the kpc2000 driver, which is the one instantiating these kpc2000_spi devices). I just chose to do it here because we didn’t need the flexibility and it was easiest to do it here.

Now, once the kernel hears about those SPI NOR devices, it brings in another driver - the “spi-nor” driver to be precise. That driver is the one that knows the bytes to send across the link to talk to SPI NOR flash chips. It doesn’t talk to the SPI controller (again, that’s this driver’s job) - but this makes it able to work with any SPI controller driver. The ability to mix and match drivers like this is stupidly powerful.

It goes even farther than that though - on top of the SPI NOR driver is another called the “MTD” driver (Memory Technology Device). This one is even more high-level than the spi-nor driver - it’s responsible for things like bad block scanning/tracking and block read/write/erase.

On top of that you could stack a filesystem driver like ext3 or UBIFS to get a real filesystem. And all I had to write was that little driver that talked to the hardware to get it to do generic SPI transactions.