Step 11 of 13
Talking to the hardware: the HAL
Twenty-five-plus drivers, each a private register engine hidden behind a task-safe gateway. Here is what using one looks like, and why they are shaped the way they are.
Using it
The HAL is a plain GPR library project —
libs/esp32s3_hal/esp32s3_hal.gpr — not an Alire
crate. It has no alire.toml, and nothing reaches it through
Alire's dependency graph; the runtime (crates/esp32s3_rts) is the
only crate here, path-pinned by each example. What you get instead is one line
in your own project file, resolved either of two ways:
-- Standalone project: by name, via GPR_PROJECT_PATH (which export.sh sets)
with "esp32s3_hal.gpr";
-- In-repo example: by relative path, so the Ada Language Server resolves it
-- with no environment set at all
with "../../libs/esp32s3_hal/esp32s3_hal.gpr";
export.sh puts crates/esp32s3_rts and every
directory under libs/ on GPR_PROJECT_PATH, so the
by-name form resolves for gprbuild and for the Ada Language
Server. Adding a library to the SDK needs no edit anywhere: dropping
libs/<name>/<name>.gpr in is enough. The HAL's units
compile against the same runtime and the same profile as whatever consumes them
— the project reads ESP32S3_RTS_PROFILE itself and keys its
object directory by it.
Then with the driver you want. This is the whole body of the
blink example's GPIO package — a real, complete driver client:
with System;
with Ada.Real_Time; use Ada.Real_Time;
with ESP32S3.GPIO;
with ESP32S3.Log; use ESP32S3.Log;
package body GPIO is
Pin : constant ESP32S3.GPIO.Pin_Id := 0;
-- Library-level task: toggle GPIO0 every 250 ms (2 Hz square wave) on core 0,
-- logging each transition over the USB-Serial-JTAG console.
task Blinker
with Priority => System.Priority'Last - 1, CPU => 1;
task body Blinker is
Period : constant Time_Span := Milliseconds (250);
Next : Time;
High : Boolean := False;
begin
ESP32S3.GPIO.Configure (Pin, ESP32S3.GPIO.Output,
Drive => ESP32S3.GPIO.Drive_Strong);
Next := Clock + Period;
loop
delay until Next;
High := not High;
ESP32S3.GPIO.Write (Pin, High);
Put_Line ("[gpio0] " & (if High then "HIGH" else "low "));
Next := Next + Period;
end loop;
end Blinker;
end GPIO;
Three things in there are worth noticing.
CPU => 1pins the task to a core. This is a genuinely dual-core SMP runtime; tasks can be pinned per core and protected-object entries work across cores.Next := Next + Period, notClock + Period. Absolute deadlines do not accumulate drift. The interrupt-backeddelay untilgives exact, stable periods.- No register pokes. Everything goes through
ESP32S3.GPIO, which is where the device knowledge lives.
How the drivers are shaped
Each driver is a thin private register "engine" hidden behind a task-safe gateway — either a protected object or a limited-controlled RAII handle. Concurrent access from several tasks is therefore safe by construction rather than by convention, and a driver handle releases its peripheral when it goes out of scope.
The profile decides what the HAL even contains.
The RAII-handle drivers — SPI, I2C, UART, GDMA, MCPWM — are built on
controlled types, and light-tasking forbids those
(No_Finalization), so the HAL project excludes those sources
under that profile; so are the ext4 and FAT16 filesystems and the ESP
serial-bootloader client. What remains under light-tasking is the
lock-free subset: GPIO, RNG, temperature. The drivers target
embedded, where full exception
propagation lets their -gnata contracts — the GPIO valid-pin
predicate, for one — raise something you can catch.
Under the drivers sits a generated register layer,
ESP32S3_Registers.*, produced by svd2ada from the vendor's SVD
description — typed record fields with representation clauses, not
volatile uint32_t* arithmetic.
What is available
GPIO, SPI, I2C, UART, GDMA, I2S, LEDC, RMT, PCNT, SDM, MCPWM, general-purpose timers, ADC, capacitive touch, RTC and RTC-IO, LCD (i80), TWAI/CAN, hardware crypto (SHA/AES), RNG, and SD over both SPI and the native SDHOST. Alongside them, a pure-Ada ext2/3/4 filesystem with a JBD2 journal, and a pure-Ada FAT16 reader and formatter for media a PC has to be able to mount.
Most drivers ship with a self-test under examples/ that needs no
wiring — internal loopback or GPIO sampling. Running the one for the
peripheral you are about to use is the fastest way to confirm your board before
you write any code against it.
Verify on your own board. The drivers were exercised on an ESP32-S3 during development, but nothing has been re-verified as it ships. A few components — the SD drivers, the temperature sensor, the filesystems' on-device paths, the ESP serial-bootloader client — are explicitly host-verified or smoke-tested only. The repository's Testing status table says which is which; treat every driver as needing confirmation on your hardware before you rely on it.
Console output
ESP32S3.Log is the formatted-output path the examples use
(Put, Put_Line, Put_Hex,
Put_Fixed…) over the USB-Serial-JTAG console. On the
embedded and full profiles
Ada.Text_IO is available too, routed to the same console by the
runtime.