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 asDevice
and takes a device ID as argumentinit
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
, andfinish
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) exceptionfinish
is always called from within the PendSV exceptionstart
andfinish
can never interrupt each other: when one of them runs, the other waits- because of this, the code in
start
andfinish
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’sfinish
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.