Step 11 of 56
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.
Step 12 catalogues all 96 examples — which one to run for each peripheral, and under which profile. The steps after it go through the peripherals one at a time. The four you will reach for first come first — GPIO, I2C, SPI and UART — then the engine they and everything else are built on, GDMA:
| Step | Peripheral | Why it has its own page |
|---|---|---|
| 13 | GPIO | The pin type, what is atomic in silicon, interrupts and the trampoline rule |
| 14 | I2C | Session ownership, repeated START, unbounded transfers |
| 15 | SPI | Per-device clock and mode, chip select three ways, DMA preconditions |
| 16 | UART | No setup call, interrupt-driven RX, a pin-routing trap |
| 17 | GDMA | Channels as a claimed resource, and the buffer rules PSRAM's cache imposes |
| 18 | I2S | No CPU FIFO at all, gapless looping, capture under playback |
| 19 | LCD | Command-driven i8080 and continuously-refreshed RGB |
| 20 | TWAI/CAN | Identifier widths kept apart by type, and the bus-off trap |
| 21 | RMT | Arbitrary pulse trains; IR, WS2812, 1-Wire |
| 22 | LEDC & SDM | PWM dimming, and density modulation that filters to analog |
| 23 | MCPWM | Dead-time and hardware fault shutdown |
| 24 | Timers & PCNT | A 54-bit timer with an alarm, and edge counters that wrap |
| 25 | ADC & touch | Fixed-pin channels, attenuation, and relative touch detection |
| 26 | RTC & deep sleep | Waking is a reset; retained memory and pad hold |
| 27 | Crypto & RNG | SHA/AES/RSA, MD5's specific job, and the RNG caveat |
| 28 | SD cards | Two hosts, one block API, different profile requirements |
| 29 | Temperature & MAC | Die temperature, and the four factory addresses |
Steps 29 to 38 then cover the external devices the SDK ships drivers for — parts on your board rather than inside the chip, each built on one of the buses above:
| Step | Device | What it is |
|---|---|---|
| 30 | ST7789 & GT911 | SPI display and capacitive touch controller |
| 31 | ES8311 | Mono audio codec: I2C control, I2S audio |
| 32 | QMI8658C & SHT41 | 6-axis IMU, and temperature/humidity |
| 33 | PCF85063A | Real-time clock with an alarm |
| 34 | TCA9555, CH422G, HC595 | Port expanders and a shift register |
| 35 | TX1812 | Addressable RGB LEDs |
| 36 | W25Q, 24C, FRAM | NOR flash, EEPROM catalogue, FRAM |
| 37 | TLV2556 | External 12-bit SPI ADC |
| 38 | GPS | NMEA receiver as a background service |
| 39 | W5500 | Ethernet with a hardwired TCP/IP stack |
Steps 39 to 43 are the networking stack above those interfaces — chip-neutral, so the same application code runs over Ethernet, Wi-Fi or anything else registered as a NIC:
| Step | Layer | What it gives you |
|---|---|---|
| 40 | Sockets & routing | One socket API over several NICs, longest-prefix routing, failover |
| 41 | DNS & NTP | Name resolution and time, portable between host and board |
| 42 | TLS 1.3 | A full client handshake and chain validation, no C library |
| 43 | Wi-Fi | Pure Ada around the fetched radio blobs, WPA2 handshake included |
| 44 | Modbus TCP | Industrial master and slave over the facade |
| 45 | FTP | Streamed client, and a server over your filesystems |
Steps 45 to 51 are the rest of the SDK — storage, filesystems, text and the standalone tools:
| Step | Component | What it gives you |
|---|---|---|
| 46 | Block devices & wear levelling | The vtable the filesystems sit on, and an FTL that spreads flash wear |
| 47 | ext4 | Read/write ext2/3/4 with JBD2 replay and on-device mkfs |
| 48 | FAT16 | The filesystem a PC can mount, read-only by design |
| 49 | Console, text & fonts | Formatted output with no hosted runtime; panel-independent glyphs |
| 50 | Esp_Loader | Program another ESP32 from the board |
| 51 | SIMD (PIE) | 128-bit vector kernels in inline assembly |
| 52 | Stack measurement | Stack painting, to catch what static analysis cannot see |
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.