Thursday 18 July 2024

Ada & FreeRTOS on ESP32 H2

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).

Startup

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.


Useful information

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

Sunday 11 February 2024

SDK 15 issues

This note covers some problems we’ve had with Xcode/the Command Line Tools (CLTs) at version 15.

Wednesday 7 June 2023

Alire on macOS, revisited

This note covers some of the considerations that’ll apply when running Alire on macOS.

Wednesday 22 March 2023

Libadalang, Alire, and macOS

Background

This exercise was prompted by the need for Scripted Testing to be supported by – as far as possible – code generation. The need is for the public or interfacing view of a supporting part (domain) of a system to be able to log calls made to it and to provide values on calls to it, using a scripting mechanism.

Friday 10 February 2023

ColdFrame and the micro:bit revisited

This article discusses various issues rebuilding a demonstrator intended for the BBC micro:bit (version 1.3b) after an interval of several years.

Sunday 20 November 2022

Building GCC 12.2.0 on Ventura for aarch64

These are notes on building GCC 12.2.0 and GNAT tools for Apple silicon.

There were two main problems:

  • the base package was built on an Intel machine (lockheed - named after Shadowcat’s companion dragon), running Monterey (macOS 12).
  • the build machine, an M1 mac Mini (temeraire - named after Naomi Novik’s dragon) was by this time running Ventura (macOS 13), but I wanted to make sure that users could continue to run on Monterey.