All drivers, which are basically leaf glue code between an IO
multiplexor and a device, have three aspects to them, which
can/should be distinct code:

Code that models and drives the state
machine of the device.

Code that accepts requests from the kernel.

Code that uses the resources of the kernel to implement
the previous two parts.

Some of the kernel is not directly part of the IO
multiplexor: it's helper functions for drivers.

The most important kernel resource used by a driver is
memory, either for keeping track of state, or for buffering
transfers. The second most important, if used, is threads (or
similar).

It is easy to write a simple driver; it is very easy to
write a bad driver; it is difficult to write a good one; it is
difficult and time consuming to figure out devices; it is time
consuming to track kernel internal evolution.

The invocation usually is triggered by some user program
action, but sometimes not voluntarily.

Examples: the user program wants to read a character from a
serial port is voluntary invocation; a disk driver is used to
write some cached file blocks to disk because a user program
is filling the cache is involuntary invocation.

In all cases, a well defined table of function
pointers must be defines by the driver as its main invocation
interface.

Sometimes the user program directly invokes the
driver's routines; sometimes the user program directly invokes
a kernel subsystem, and this invokes the driver.

The specific table of function pointers the driver has to
provide depends on the type of direct interface, or the type
of subsystem that invokes the driver.

Take a driver of the same ilk and modify it; alternatively
some drivers are very simple and can be used as starting
points.

Never forget to write the man page (section 4) and
documentation, both of the device and of the internal
logic of the driver. Make both terse, readers
of driver documentation should be assumed to be knowledgeable.