Device drivers

Device drivers #

JeeH is based on messages, with device drivers handling all the interaction with hardware peripherals when it comes to interrupts. By providing a very specific design for drivers, most (all?) the complexity of interrupts, atomicity, and race conditions is essentially gone. The price of this is having to adhere (and understand) the model provided in JeeH, and (maybe) some minor overhead due to the use of message-passing between apps and device drivers.

This design is not new, it’s essentially modeled after an old BCPL-based system called TRIPOS.

The Driver API #

In JeeH’s C++ world, each driver is implemented as a subclass of jeeh::Device:

struct MyDevice : Device {
    using Device::Device;

    void init (...) { ... }
    void deinit () { ... }

    void start (Message& msg) override { ... }
    bool interrupt (int irq) override { ... }
    void finish () override { ... }
  • using Device::Device tells the compiler that the constructor is the same as Device and takes a device ID as argument
  • init is not called by JeeH, it’s there for the app to set up things and specify whatever information the driver needs to get going
  • likewise for deinit, it’s used to close the driver and turn off its hardware
  • start, interrupt, and finish are virtual functions and must be implemented in each driver

Device IDs must be a single uppercase character and must be unique. If there are multiple instances (e.g. multiple UARTs), they must each be given a unique ID.

The start() method is called when sending a message to a device, with its target set to that device’s ID. This can be either a non-blocking sys::send or a blocking sys::call. It needs to queue the message until a reply can be sent back and start h/w activity if nothing is ongoing.

The interrupt() method is hooked into the ARM’s interrupt system and gets called when any hardware interrupt associated with this driver fires. This code is not allowed to alter message queues, since it might trigger while the driver is also making such changes in start or finish.

The finish() method is called by JeeH, after interrupt() returns true. It deals with message replies and starting up the next task in the queue, if any.

Interrupts and stacks #

For interrupts to work properly, they have to be set up in init by calling irqInstall.

Everything takes place without ever disabling interrupts globally (except on ARM Cortex M0+, which needs very brief lock-outs for bitwise atomic test-and-set). All time-critical interrupts will run as soon as possible (taking into account IRQ priorities and nesting).

Once multi-tasking is enabled, i.e. after sys::init has been called, all driver calls and IRQs use a common separate “MSP” stack. Each thread only needs to reserve stack space for its own needs plus one copy of all CPU state. Otherwise, the main application stack is used for everything.

Atomic execution #

It is up to the device driver to properly manage interrupts, since these tend to, eh, interrupt the flow of an app (and its device drivers!) at the most unexpected moments in time. Even a seemingly simple statement such as “++i;” can be a source of very hard-to-find and intermittent problems.

To help manage this in JeeH, driver methods are called under very specific circumstances:

  • start is always called from within an SVC (supervisor call) exception
  • finish is always called from within the PendSV exception
  • start and finish can never interrupt each other: when one of them runs, the other waits
  • because of this, the code in start and finish do not have to take special precautions to avoid interfering with each other (just like normal app function calls)
  • driver code is “thread-safe”: only one thread can be running inside a driver

As a result, driver code is not very different from application code: deal with messages, process them as needed, and queue them if the work is not done and a reply can not yet be sent. Interrupts from other device drivers wil have no impact, other than briefly stealing some CPU cycles.

The only exception (ha!) is with a driver’s own interrupt() method. This is bound to access some of the same hardware registers and variables as its start and finish code. But there are several tricks to stay out of trouble:

  • do as little as possible in interrupt(): identify the precise interrupt source (e.g. RX or TX), do whatever is needed to clear or mask the interrupt and save whatever state is needed to avoid data loss (e.g. adding incoming data to a buffer)
  • if this interrupt needs to do more, such as sending a reply, return true, else return false
  • when interrupt returns true, JeeH will call the driver’s finish code as soon as possible (using ARM’s PendSV mechanism) - which is usually: right away

The key point here is that interrupt might run at the most inconvenient time imaginable, but finish will only run when no other driver activity is going on anywhere. Both start and finish are guaranteed to not interfere with anything related to drivers or message passing.