The ESP32 H2 from Espressif, as provided by The Pi Hut, has a RISC-V 32-bit single-core processor with a clock speed of up to 96MHz and 4MB of built-in Flash, an extensive set of on-chip pripherals, and also integrated wireless connectivity.
The Espressif development environment provides a considerable range of software packages to take advantage of the MCU’s features.
It’s possible to implement Ada code in the Espressif environment, but I was more interested in extending Cortex GNAT RTS (which is built over FreeRTOS) to use this new-to-me hardware. This wouldn’t have been possible had FreeRTOS not already supported RISC-V.
Building the Ada runtime
The first thing needed is of course a compiler. I was able to pick up the GCC 4.1.3 riscv64-elf
compiler via Alire. This is an x86_64
suite, but it runs happily on an M1 Mac under Rosetta.
Next, copy the arduino-due/
tree in Cortex GNAT RTS to a new esp32h2/
tree. With any luck, the only changes needed will be to some scripts there, and in adainclude/
and adalib/
. Also, we need an SVD file (from Espressif’s Github site here), converted to Ada using svd2ada
from Alire.
Top-level scripts
The “scripts” required, noted above, are - aside from the Makefile
-
build_runtime.gpr
runtime.xml
By the way, AdaCore normally call those files runtime_build.gpr
and runtime.xml
, which aren’t tab-completion-friendly!
The GNAT Project File build_runtime.gpr
tells gprbuild how to build and install the RTS.
Notable points are
for Target use external ("TARGET", "riscv64-elf");
— by default, look for tools prefixed by riscv64-elf-
, e.g. riscv64-elf-gcc
for Runtime ("ada") use Build_Runtime'Project_Dir;
— that is, this directory, whose structure is already that of a runtime, viz. directories adainclude/
for source, adalib/
for objects.
Some files are excluded: the ARM hard fault handling, and an SVD file not properly generated by svd2ada.
The runtime description file runtime.xml
describes how the RTS is to be built, and, after installation, how it is to be used while compiling applications (compiler and linker switches). It includes the architectural compiler switches:
- the machine architecture,
-march=rv32imac_zicsr_zifencei
- Risc-V 32-bit core with base Integer, Multiplication/division, Atomic and Compressed standard extensions; also standard Configuration and Status Registers, and fence functionality; and CLINT (Core Local Interrupts).
- the C datatypes and calling convention,
-mabi=ilp32
.
adainclude/
This directory contains machine-dependent files: startup, interrupt handling, and the package System
. The last is mainly copied from the arduino-due
version, with some modifications from AdaCore’s light-tasking-polarfiresoc
RTS.
adalib/
This directory contains linker scripts, and also the runtime’s .a
and .ali
files (I think, with hindsight, the linker scripts should be moved to a directory of their own).
This is the point at which things got interesting!
The program entry point has to be written in assembler, as a naked function (the stack pointer isn’t valid yet), and also it needs to be at the start of flash. There’s very helpful discussion here.
The spec:
procedure Program_Entry
with
Export,
Convention => Ada,
External_Name => "program_entry",
Linker_Section => ".program_entry",
No_Return;
pragma Machine_Attribute (Program_Entry, "naked");
and the body:
procedure Program_Entry is
begin
System.Machine_Code.Asm
(
".option push;" & ASCII.LF &
".option norelax;" & ASCII.LF &
"la gp, __global_pointer$;" & ASCII.LF &
".option pop;" & ASCII.LF &
"la sp, __freertos_irq_stack_top;" & ASCII.LF &
"jal zero, startup__program_initialization"
,
Volatile => True);
end Program_Entry;
(__freertos_irq_stack_top
is the top of usable RAM, set up by the linker script).
Running on the hardware
The debugger
The first thing is to get a null main program running (it’s best if it loops endlessly; the RTS runs it as a task, and Ravenscar tasks aren’t allowed to exit). This means using a debugger, riscv64-elf-gdb
, and a JTAG adaptor.
The ESP32-H2 contains a JTAG adaptor, accessible via a USB link (you have to cannibalise a spare USB cable, because the onboard USB-C socket doesn’t connect to this). You can then use the Espressif fork of OpenOCD to provide the gdb server.
I’m used to Cortex chips, which tend to allow the debugger to load software into flash. Not so with ESP32-H2 (maybe the ESP32 range? all Espressif chips?). I suspect this is because the loading code is actually implemented on-chip, using reserved flash, with minimal external assistance.
Anyway, it turned out much easier to run tests in ram. Of course, this involved a change to the linker script, but at least the whole thing could be controlled from GDB. I had to restrain myself from using monitor reset
, or pressing the board’s RESET button, both of which stopped OpenOCD working. Almost always, a fresh load
did the trick.
The clock
FreeRTOS will use the Risc-V MTIME
and MTIMECMP
registers to manage time. For the ESP32-H2, these registers are described in the Technical Reference Manual section 1.7.5-6.
Of course, in order to use this you have to start the clock; this will involve interrupts.
Interrupts
The ESP32-H2 supports 32 traps/interrupts. FreeRTOS provides a trap handler, which distinguishes between application and clock interrupts and error traps. These are set up in interrupt_vectors.S
. The machine is told about them using the mtvec
CSR.
Starting the clock
We have to set the MTIMECTL.MTIE
bit (TRM register 1.26), and then enable interrupts using the MIE
CSR to set bit 7 (MTIE) (TRM register 1.8), as here. (It would have been useful to have an SVD document for these machine registers!)
Adjustment
I was convinced that I’d seen at least one document saying that the default clock interrupt rate was 24 MHz. It turned out that it’s actually 32 MHz; no need at this stage to investigate tweaking it up to the full 96 MHz.
Testbed
My standard testbed is here (the referenced packages are in common).
The full set of tests make a program too big to fit in ram, so when the program didn’t run from flash (as evidenced by the heartbeat
stopping), I had to cut back the number of tests so as to be able to use the debugger. It turned out that the problem was insufficient stack space (not uncommon with FreeRTOS), so I had to double the default; some other tasks which weren’t using the default needed similar adjustment.
Two slightly-alarming but, I think, ignorable warnings came up during development:
- a security-related warning that the link contans an executable segment with write permission. This isn’t surprising, since the main segment contains text, read-only data, and writable data to be transferred into ram before execution. Suppressed by telling the linker
--no-warn-rwx-segments
.
- a warning triggered by existential or universal quantification on containers:
iteration.o: requires executable stack (because the .note.GNU-stack section is executable)
. This is to do with trampolines, which GNAT for other targets doesn’t use.
Interrupts
Most work with interrupts will be down to application developers (you have to tell a GPIO pin that you want an interrupt on the rising edge, for instance, but there’s one interrupt for all the GPIOs, and you have to work out which pins have caused this interrupt, and acknowledge them).
The added interest with the ESP32 H2 is that there are 65 peripherals that can generate interrupts (TRM table 9.1; these would correspond to Ada interrupt names), but only 28 machine interrupts (TRM section 1.6). This makes for a complicated mapping in System.Interrupts
. Install_Restricted_Handlers
is called during elaboration when the compiler sees a protected object with an interrupt handler, e.g.
protected Handler is
entry Wait;
private
Triggered : Boolean := False;
procedure Handler
with Attach_Handler => Ada.Interrupts.Names.GPIO_Interrupt;
end Handler;
protected body Handler is
entry Wait when Triggered is
begin
Triggered := False;
end Wait;
procedure Handler is
begin
-- At this point we should at least clear the interrupt!
-- This is development code, aimed only at driving
-- Install_Restricted_Handlers for test.
Triggered := True;
end Handler;
end Handler;
At present, interrupt handling is very much a work in progress.
Embedded Ada/SPARK (AdaCore blog)
ESP32-H2 Datasheet
ESP-IDF Programming Guide
… including the Hardware Reference
… including the Technical Reference Manual
Startup code in C
Bare metal examples
The Direct Boot feature
Jeremy Grosser’s proof of concept, Ada on ESP32-C3