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

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.

Talking to the hardware: the HAL · Bare-Metal Ada on the ESP32-S3
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 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.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.

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.

Talking to the hardware: the HAL · Bare-Metal Ada on the ESP32-S3
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 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.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.

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.

Talking to the hardware: the HAL · Bare-Metal Ada on the ESP32-S3
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 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.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.

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.

Talking to the hardware: the HAL · Bare-Metal Ada on the ESP32-S3
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 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.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.

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.

Talking to the hardware: the HAL · Bare-Metal Ada on the ESP32-S3
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 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.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.

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.