Skip to content

JeeH version three

The JeeH library serves three main purposes:

  1. provide a concise notation for using µC hardware from C++
  2. provide a uniform API for use across a wide range of µC families
  3. provide a lean-and-mean message-based multitasking foundation

I’ve been exploring all of the above in JeeH for a few years now. Version one created a nice notation for using GPIO pins and several basic hardware peripherals, including UART, SPI, and I2C. Version two added some scripting to auto-generate headers for all STM32 µCs from their System View Description (SVD) files.

But as they say: “the third time is the charm”. I wasn’t quite satisfied with how the whole JeeH library fitted together, how its test suites were structured, and how it was intended to be used from PlatformIO. Now there’s a third revision on the way, and this balances all the - sometimes conflicting - needs much better. It’s still all “pre-alpha” for now. Tons of unexplored directions, but also a much better development setup.

Development takes place along several different paths:

  • a Makefile is used for super-fast native tests, i.e. those parts which are not µC-specific
  • it can also be used for quick one-line regression checks: make clean full remote
  • there are mini-apps for specific features, e.g. uart, pool, ticks (all using PlatformIO)

The version-two builds, including its exploration of modular builds and the Simulated MMU (sMMU) design, are driving the design. I’m keeping all those ideas in reach, while gradually building up the code and its API. The entire system needs to be usable in a range of scenarios:

  • a “bare”, non-multi-tasking setup, with direct access to raw hardware and I/O registers
  • a multitasker with a small set of system calls to interact with the kernel and device drivers
  • a clean API for adding device drivers, usable in bare as well as in multi-tasking contexts
  • optional protected mode, where tasks can’t access I/O registers or some parts of memory

Protected mode is where sMMU tricks can be used to create a big-iron’ish operating system environment. This is still mostly vapourware, but a first protected-mode task with DMA-UART console I/O is already working.

I’m particularly pleased with the lean-and-mean design of JeeH’s core system calls:

  1. void Sys::send (Message&))
  2. Message& Sys::recv ())
  3. void Sys::drop (Message&, int))
  4. Message& Sys::fork (uint32_t*, uint16_t, void(*)(Message&), void*))
  5. void Sys::quit (int8_t))
  6. uint32_t* Sys::pool (uint32_t, uint32_t*))
  7. uint32_t Sys::outf (char const*, void*, Message*))

Everything else is built on top of this, including the Sys::call and Sys::wait utilities.

All system calls and drivers run in “privileged mode”, and have full access to the underlying hardware. The structure of an app using JeeH is as follows (each step is optional):

  1. configure and instantiate all required hardware drivers
  2. start multi-tasking by creating an instance of Sys (optional: run tasks in privileged mode)
  3. launch tasks, perform driver requests, and do whatever else the app needs …

Embedded apps tend to run forever, and so that third step is usually an infinite loop. Note that everything interesting is implemented using drivers and all interaction is based on messages:

struct Message {
    int8_t mDst, mTag; uint16_t mLen; void* mPtr; Message* mLnk;
};

As an example: timers and timeouts are handled by the Ticker driver. The way to send out messages is by calling Sys::send. Receiving is done though Sys::recv, which is blocking. But here’s a trick to avoid blocking: request a zero millisecond timeout, then wait for the next reply: if there is no reply yet, the timed-out message will arrive instead (without any delay).

Messages can be cancelled with Sys::drop, even if they are already being processed.

New tasks are started with Sys::fork, and they can terminate themselves with Sys::quit. Once a task ends, its creating task is sent a message with the exit code. Tasks can’t be “killed”, they can only quit themselves. All inter-task communication is also based on messages.

Memory can be allocated, resized, and released using Sys::pool, which is thread-safe. This is not meant for fine-grained memory allocation, but only to obtain large blocks of memory - to be used for self-management inside each task.

The last system call is Sys::outf, for formatting and reporting messages on a debug console.

The main “gotcha” with this message-centric design, is that there’s no way to control the order in which messages are received. If multiple requests have been sent out, their replies can come back in any order. In particular, a Sys::call or a Sys::wait (which are both a send + recv) may not get the replies they expect. Blocking as it all may seem, things do tend to get more complicated with more than one request “in transit”. One way to avoid this, is to only use Sys::call, i.e. always block until its reply arrives (very similar to Linux). The advantage of this approach is that tasks never see any interrupts - they might merely suspend & resume a lot …

Time will tell whether this scales. The good news: Go’s “go-routines” behave in the same way.

I fully intend to give this a serious try. The entire system is under 8 kB of code (and half of that is printf, pfff …). The RAM memory requirements are equally minimal: a few hundred bytes, plus whatever is needed for stacks. One aspect which I find appealing, is the ability to explicitly juggle blocking and non-blocking behaviour.