JeeH

JeeH #

JeeH is a Hardware Abstraction Layer and multitasker library for STM’s 32-bit ARM Cortex µCs. This code is used by the other projects on this site.

This page describes some of my choices for this library, many of which also carry through to the rest of the projects. There tend to be many unavoidable - but also highly personal - choices, because ya gotta pick somethin’ when creating stuff. Least I can do is describe the main ones.

PlatformIO: µC development #

PlatformIO (“PIO”) helps manage embedded software development for just about any µC architecture and takes care of all dependencies, libraries, and toolchains. PIO is written in Python and uses SCons to manage the build process. It integrates nicely with the VSCode IDE, but is also very easy to use from the command line with any other source code editor. There is extensive documentation and getting started with either the IDE or CLI version is trivial.

What makes PIO stand out for me is its convenience1. A simple platformio.ini text file fully defines a project’s build / upload / debug steps, as well as its library dependencies and variants. PIO will download and manage everything, keeping it all in one place and up to date. There is no need to manually install any package or tool. And PIO is 100% open source and cross-platform.

The projects on this site all use PlatformIO. Here is a project which blinks an LED on a NUCLEO-F103RB development board, using the JeeH library. It needs just these two files:

platformio.ini

[env:blink]
platform = ststm32
framework = cmsis
board = nucleo_f103rb
build_flags = -std=c++17 -Wno-format -Wall
lib_deps = jcw/JeeH

src/main.cpp

#include <jee.h>
using namespace jeeh;

int main () {
    Pin led ("A5"); // GPIO pin PA5 has a LED attached to it
    led.mode("P");  // set the pin to push-pull output mode
    while (true) {
        led.toggle();
        msIdle(500);
    }
}

To build and upload this code from the command line, type:

pio run -t upload

Since I’m a vim+cli type of guy, I’ve defined some shell aliases to reduce typing even further:

alias p='pio run'
alias pu='pio run -t upload'

A complete build + upload takes me three keystrokes: p + u + RETURN - and for even more automation, I tend to switch to a Continuous TDD approach, which runs PIO on every file save.

SVD: System View Descriptions #

SVD files are used to describe different µC chips at the CPU, peripheral, and register level. JeeH uses them to automatically generate C++ headers with uniform API conventions across a wide range of ARM µCs.

See CMSIS-SVD on GitHub for a collection of data files and software tools. They’re also available directly in PlatformIO (e.g. ~/.platformio/platforms/ststm32/misc/svd/), which is where JeeH gets them.

There’s an svdgen.py script which parses the XML file for the current build target and generates two header files from it:

  • jee-def.h has a few macro definitions, for use in #if and #ifdef preprocessor statements in the rest of the code.
  • jee-svd.h defines all hardware registers as constexpr of type IoReg<>, all IRQ vector positions, key RCC register offsets, and the bit offsets used to enable/disable individual built-in peripherals in the chip.

Everything except the preprocessor macros is defined in the jeeh namespace.

IoReg: I/O register definitions #

The two generated header files help implement the IoReg<> template class, which offers convenient notations to access all peripheral registers as words, bytes, bits, and bit ranges. This relies on a bit of C++ trickery - here are some examples:

  • ... = USART1[0x14] reads the 32-bit register at offset 0x14 of the USART1 peripheral
  • USART1[0x14] = ... writes that same 32-bit register peripheral
  • ... = USART1[0x14](20) reads bit 20 of that register (0 or 1)
  • USART1[0x14](20) = ... writes bit 20 of that register (using the lower bit)
  • ... = USART1[0x14](20,4) reads bits 20..23 of that register (0..15)
  • USART1[0x14](20,4) = ... writes bit 20..23 of that register (from the lower 4 bits)

With a few more variations to top it all off:

  • ... = USART1.byte(0x12) and USART1.byte(0x12) = ... for the byte at 0x12 (which can also be accessed as USART1[0x10](16,8))
  • ... = USART1(38,2) and USART1(38,2) = ... for 2 bits starting at bit 38 of the USART1 peripheral (this is equivalent to USART1[0x04](6,2) since 4x8+6 is 38)

In a way, USART1[0x14] is like indexing the USART1 peripheral with a byte offset of 0x14, but note that the access mode then ends up using 32-bit words (this is why these offsets must always be multiples of 4). This choice was made to avoid confusion when using offsets from the data sheet or reference manual.

There is no overhead in using this notation, the C++ compiler will optimise it all.

Continuous Test Driven Design #

The thing about TDD, is that it takes more effort, but what you gain is confidence. When adding new code and making changes, there are lots of things which can unexpectedly break. Nobody is perfect, no-one can predict 100% what the consequence of a change will be in other parts of the design. With TDD, re-running tests verifies and validates the change to not break anything.

TDD actually goes much further than “running tests”: the idea is to first write a test (which will fail) and then write the code to make it pass. That way, the test itself will be known to work (and exist) for any new code.

I’m not super-good at always writing tests up front. But I do try to at least debug each mistake and problem by adding one or more small new test. That way, this bug will never re-appear in the same way again. As more and more tests get added, the confidence you get while coding new features grows and grows forever. A far cry from “why does feature X no longer work?”.

Since JeeH-based apps build very quickly, and since I tend to develop new functionality in small self-contained units, the barrier to running tests becomes so low that I’ve adopted a continuous TDD style, using some scripting to automatically re-run a test whenever a source files is saved (in Vim, I’ve set this up to happen whenever I switch from one window to the next).

T.B.D.

Logging via ITM / SWO #

There’s a logf function in JeeH to send printf-formatted output to the Instrumentation Trace Macrocell (ITM) built into all ARM Cortex chips. This can be set up to generate a fast UART stream on an external pin (usually PB3, often tied to the on-board ST-Link of Nucleo and Discovery boards). The maximum rate with an ST-Link v2 is 2 Mbps. The ITM protocol adds some overhead, but it’s an easy way of emitting development log output. The API is similar to printf, except that logf forces a newline ending and truncates output over 80 characters:

void logf (char const* fmt ...);

One nice feature of ITM logging, is that it’s only active when enabled from a debugger. In “normal” use, logf calls will simply be ignored, and can be left in, even for production code.

Note that printf calls use Sys::outf and cannot be used inside driver code (they’ll trigger a hard-fault exception). Since logf avoids outf, it can be used in driver code and even in IRQ handlers 2 (but it’ll use more stack space and the extra CPU cycles may affect processing).

As mentioned, logf calls are ignored when running an application without debugger. For development, you have to jump through a few hoops to see the log output. These steps are illustrated in examples/f439n144/README.md, based on some simple Makefile entries.

The PIO setup was modeled after a weblog post by Phil Greenland (Dec 2022).


  1. There’s a lot more to PIO: see the documentation. I’ve been using it for many years. ↩︎

  2. Up to a point: there is no way to guard against nested logf calls from IRQs. ↩︎