Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

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

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:

StepPeripheralWhy it has its own page
13GPIOThe pin type, what is atomic in silicon, interrupts and the trampoline rule
14I2CSession ownership, repeated START, unbounded transfers
15SPIPer-device clock and mode, chip select three ways, DMA preconditions
16UARTNo setup call, interrupt-driven RX, a pin-routing trap
17GDMAChannels as a claimed resource, and the buffer rules PSRAM's cache imposes
18I2SNo CPU FIFO at all, gapless looping, capture under playback
19LCDCommand-driven i8080 and continuously-refreshed RGB
20TWAI/CANIdentifier widths kept apart by type, and the bus-off trap
21RMTArbitrary pulse trains; IR, WS2812, 1-Wire
22LEDC & SDMPWM dimming, and density modulation that filters to analog
23MCPWMDead-time and hardware fault shutdown
24Timers & PCNTA 54-bit timer with an alarm, and edge counters that wrap
25ADC & touchFixed-pin channels, attenuation, and relative touch detection
26RTC & deep sleepWaking is a reset; retained memory and pad hold
27Crypto & RNGSHA/AES/RSA, MD5's specific job, and the RNG caveat
28SD cardsTwo hosts, one block API, different profile requirements
29Temperature & MACDie 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:

StepDeviceWhat it is
30ST7789 & GT911SPI display and capacitive touch controller
31ES8311Mono audio codec: I2C control, I2S audio
32QMI8658C & SHT416-axis IMU, and temperature/humidity
33PCF85063AReal-time clock with an alarm
34TCA9555, CH422G, HC595Port expanders and a shift register
35TX1812Addressable RGB LEDs
36W25Q, 24C, FRAMNOR flash, EEPROM catalogue, FRAM
37TLV2556External 12-bit SPI ADC
38GPSNMEA receiver as a background service
39W5500Ethernet 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:

StepLayerWhat it gives you
40Sockets & routingOne socket API over several NICs, longest-prefix routing, failover
41DNS & NTPName resolution and time, portable between host and board
42TLS 1.3A full client handshake and chain validation, no C library
43Wi-FiPure Ada around the fetched radio blobs, WPA2 handshake included
44Modbus TCPIndustrial master and slave over the facade
45FTPStreamed client, and a server over your filesystems

Steps 45 to 51 are the rest of the SDK — storage, filesystems, text and the standalone tools:

StepComponentWhat it gives you
46Block devices & wear levellingThe vtable the filesystems sit on, and an FTL that spreads flash wear
47ext4Read/write ext2/3/4 with JBD2 replay and on-device mkfs
48FAT16The filesystem a PC can mount, read-only by design
49Console, text & fontsFormatted output with no hosted runtime; panel-independent glyphs
50Esp_LoaderProgram another ESP32 from the board
51SIMD (PIE)128-bit vector kernels in inline assembly
52Stack measurementStack 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.