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 typeIoReg<>
, 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 peripheralUSART1[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)
andUSART1.byte(0x12) = ...
for the byte at 0x12 (which can also be accessed asUSART1[0x10](16,8)
)... = USART1(38,2)
andUSART1(38,2) = ...
for 2 bits starting at bit 38 of the USART1 peripheral (this is equivalent toUSART1[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).
-
There’s a lot more to PIO: see the documentation. I’ve been using it for many years. ↩︎
-
Up to a point: there is no way to guard against nested
logf
calls from IRQs. ↩︎