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 06 of 13

What happens between reset and Main

Your blink worked. Here is every layer it went through, from the chip's mask ROM to the first line of your Ada — and what an RTOS would normally be doing that nothing here does.

The layers, top to bottom

LayerWhat it is
Your application Ada packages and a Main procedure.
The HAL libs/esp32s3_hal: hand-written driver packages (ESP32S3.*) over a generated register layer (ESP32S3_Registers.*). Optional — a program may poke registers itself — but it is where the device knowledge lives.
The binder output gnatbind generates b__main, which elaborates every unit in dependency order and then calls Main.
The Ada runtime The System.* and Ada.* bodies: secondary stack, exceptions, text I/O over the USB-serial console, and the tasking kernel — tasks, protected objects, delays, scheduling.
Board support The silicon-specific bottom of the runtime: the Xtensa windowed context switch, interrupt entry/exit vectors, the clock tick, SMP scheduling and the inter-core interrupt. Ada plus a little assembly (start.S, highint5.S, context_switch.S). This is the layer an off-the-shelf RTOS would otherwise provide.
2nd-stage bootloader A minimal loader of our own that maps the flash cache/MMU, brings up the octal PSRAM, and jumps to the application image. It replaces the vendor's stock second-stage bootloader.
Mask ROM On-chip, immutable. Loads the 2nd-stage bootloader from flash at reset.

The boot path

ROM ─> 2nd-stage bootloader ─> start.S ─> adainit ─> Main
        cache/MMU, PSRAM      PLL, vectors,  elaboration
                              release core 1

start.S selects the 240 MHz PLL, sets up the stack and the interrupt vectors, and on the SMP examples releases the second core. Then the runtime's startup calls adainit, which runs every package's elaboration — and that is where library-level tasks like the blinker come alive, before Main is entered. Finally Main runs.

There is no vendor startup shim and no third-party scheduler in that chain. The Ada runtime is the only scheduler and it is linked from source.

Why the blink example's Main does nothing

Look at examples/esp32s3_gpio0_blink/src/main.adb and you find this:

with Ada.Real_Time; use Ada.Real_Time;

--  The GPIO0 blink driver + its task live in package GPIO; withing it pulls the
--  task into the program so it elaborates and runs.
with GPIO;
pragma Unreferenced (GPIO);

procedure Main is
begin
   loop
      delay until Clock + Seconds (3600);
   end loop;
end Main;

The work is done by a library-level task inside package GPIO, which starts at elaboration. Main becomes the environment task, which here just parks forever. Withing a package you never call is how you pull its tasks into the link closure; pragma Unreferenced tells the compiler that is deliberate.

A minimal application

If you would rather do the work in Main itself, this is a complete program:

with Ada.Real_Time; use Ada.Real_Time;
with ESP32S3.GPIO;

procedure Main is
   Led : constant ESP32S3.GPIO.Pin_Id := 2;
begin
   ESP32S3.GPIO.Configure (Led, Mode => ESP32S3.GPIO.Output);
   loop
      ESP32S3.GPIO.Toggle (Led);
      delay until Clock + Milliseconds (250);
   end loop;
end Main;

delay until is served by the board-support tick; ESP32S3.GPIO is the HAL; everything links against the runtime crate.

What happens between reset and Main · 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 06 of 13

What happens between reset and Main

Your blink worked. Here is every layer it went through, from the chip's mask ROM to the first line of your Ada — and what an RTOS would normally be doing that nothing here does.

The layers, top to bottom

LayerWhat it is
Your application Ada packages and a Main procedure.
The HAL libs/esp32s3_hal: hand-written driver packages (ESP32S3.*) over a generated register layer (ESP32S3_Registers.*). Optional — a program may poke registers itself — but it is where the device knowledge lives.
The binder output gnatbind generates b__main, which elaborates every unit in dependency order and then calls Main.
The Ada runtime The System.* and Ada.* bodies: secondary stack, exceptions, text I/O over the USB-serial console, and the tasking kernel — tasks, protected objects, delays, scheduling.
Board support The silicon-specific bottom of the runtime: the Xtensa windowed context switch, interrupt entry/exit vectors, the clock tick, SMP scheduling and the inter-core interrupt. Ada plus a little assembly (start.S, highint5.S, context_switch.S). This is the layer an off-the-shelf RTOS would otherwise provide.
2nd-stage bootloader A minimal loader of our own that maps the flash cache/MMU, brings up the octal PSRAM, and jumps to the application image. It replaces the vendor's stock second-stage bootloader.
Mask ROM On-chip, immutable. Loads the 2nd-stage bootloader from flash at reset.

The boot path

ROM ─> 2nd-stage bootloader ─> start.S ─> adainit ─> Main
        cache/MMU, PSRAM      PLL, vectors,  elaboration
                              release core 1

start.S selects the 240 MHz PLL, sets up the stack and the interrupt vectors, and on the SMP examples releases the second core. Then the runtime's startup calls adainit, which runs every package's elaboration — and that is where library-level tasks like the blinker come alive, before Main is entered. Finally Main runs.

There is no vendor startup shim and no third-party scheduler in that chain. The Ada runtime is the only scheduler and it is linked from source.

Why the blink example's Main does nothing

Look at examples/esp32s3_gpio0_blink/src/main.adb and you find this:

with Ada.Real_Time; use Ada.Real_Time;

--  The GPIO0 blink driver + its task live in package GPIO; withing it pulls the
--  task into the program so it elaborates and runs.
with GPIO;
pragma Unreferenced (GPIO);

procedure Main is
begin
   loop
      delay until Clock + Seconds (3600);
   end loop;
end Main;

The work is done by a library-level task inside package GPIO, which starts at elaboration. Main becomes the environment task, which here just parks forever. Withing a package you never call is how you pull its tasks into the link closure; pragma Unreferenced tells the compiler that is deliberate.

A minimal application

If you would rather do the work in Main itself, this is a complete program:

with Ada.Real_Time; use Ada.Real_Time;
with ESP32S3.GPIO;

procedure Main is
   Led : constant ESP32S3.GPIO.Pin_Id := 2;
begin
   ESP32S3.GPIO.Configure (Led, Mode => ESP32S3.GPIO.Output);
   loop
      ESP32S3.GPIO.Toggle (Led);
      delay until Clock + Milliseconds (250);
   end loop;
end Main;

delay until is served by the board-support tick; ESP32S3.GPIO is the HAL; everything links against the runtime crate.

What happens between reset and Main · 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 06 of 13

What happens between reset and Main

Your blink worked. Here is every layer it went through, from the chip's mask ROM to the first line of your Ada — and what an RTOS would normally be doing that nothing here does.

The layers, top to bottom

LayerWhat it is
Your application Ada packages and a Main procedure.
The HAL libs/esp32s3_hal: hand-written driver packages (ESP32S3.*) over a generated register layer (ESP32S3_Registers.*). Optional — a program may poke registers itself — but it is where the device knowledge lives.
The binder output gnatbind generates b__main, which elaborates every unit in dependency order and then calls Main.
The Ada runtime The System.* and Ada.* bodies: secondary stack, exceptions, text I/O over the USB-serial console, and the tasking kernel — tasks, protected objects, delays, scheduling.
Board support The silicon-specific bottom of the runtime: the Xtensa windowed context switch, interrupt entry/exit vectors, the clock tick, SMP scheduling and the inter-core interrupt. Ada plus a little assembly (start.S, highint5.S, context_switch.S). This is the layer an off-the-shelf RTOS would otherwise provide.
2nd-stage bootloader A minimal loader of our own that maps the flash cache/MMU, brings up the octal PSRAM, and jumps to the application image. It replaces the vendor's stock second-stage bootloader.
Mask ROM On-chip, immutable. Loads the 2nd-stage bootloader from flash at reset.

The boot path

ROM ─> 2nd-stage bootloader ─> start.S ─> adainit ─> Main
        cache/MMU, PSRAM      PLL, vectors,  elaboration
                              release core 1

start.S selects the 240 MHz PLL, sets up the stack and the interrupt vectors, and on the SMP examples releases the second core. Then the runtime's startup calls adainit, which runs every package's elaboration — and that is where library-level tasks like the blinker come alive, before Main is entered. Finally Main runs.

There is no vendor startup shim and no third-party scheduler in that chain. The Ada runtime is the only scheduler and it is linked from source.

Why the blink example's Main does nothing

Look at examples/esp32s3_gpio0_blink/src/main.adb and you find this:

with Ada.Real_Time; use Ada.Real_Time;

--  The GPIO0 blink driver + its task live in package GPIO; withing it pulls the
--  task into the program so it elaborates and runs.
with GPIO;
pragma Unreferenced (GPIO);

procedure Main is
begin
   loop
      delay until Clock + Seconds (3600);
   end loop;
end Main;

The work is done by a library-level task inside package GPIO, which starts at elaboration. Main becomes the environment task, which here just parks forever. Withing a package you never call is how you pull its tasks into the link closure; pragma Unreferenced tells the compiler that is deliberate.

A minimal application

If you would rather do the work in Main itself, this is a complete program:

with Ada.Real_Time; use Ada.Real_Time;
with ESP32S3.GPIO;

procedure Main is
   Led : constant ESP32S3.GPIO.Pin_Id := 2;
begin
   ESP32S3.GPIO.Configure (Led, Mode => ESP32S3.GPIO.Output);
   loop
      ESP32S3.GPIO.Toggle (Led);
      delay until Clock + Milliseconds (250);
   end loop;
end Main;

delay until is served by the board-support tick; ESP32S3.GPIO is the HAL; everything links against the runtime crate.

What happens between reset and Main · 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 06 of 13

What happens between reset and Main

Your blink worked. Here is every layer it went through, from the chip's mask ROM to the first line of your Ada — and what an RTOS would normally be doing that nothing here does.

The layers, top to bottom

LayerWhat it is
Your application Ada packages and a Main procedure.
The HAL libs/esp32s3_hal: hand-written driver packages (ESP32S3.*) over a generated register layer (ESP32S3_Registers.*). Optional — a program may poke registers itself — but it is where the device knowledge lives.
The binder output gnatbind generates b__main, which elaborates every unit in dependency order and then calls Main.
The Ada runtime The System.* and Ada.* bodies: secondary stack, exceptions, text I/O over the USB-serial console, and the tasking kernel — tasks, protected objects, delays, scheduling.
Board support The silicon-specific bottom of the runtime: the Xtensa windowed context switch, interrupt entry/exit vectors, the clock tick, SMP scheduling and the inter-core interrupt. Ada plus a little assembly (start.S, highint5.S, context_switch.S). This is the layer an off-the-shelf RTOS would otherwise provide.
2nd-stage bootloader A minimal loader of our own that maps the flash cache/MMU, brings up the octal PSRAM, and jumps to the application image. It replaces the vendor's stock second-stage bootloader.
Mask ROM On-chip, immutable. Loads the 2nd-stage bootloader from flash at reset.

The boot path

ROM ─> 2nd-stage bootloader ─> start.S ─> adainit ─> Main
        cache/MMU, PSRAM      PLL, vectors,  elaboration
                              release core 1

start.S selects the 240 MHz PLL, sets up the stack and the interrupt vectors, and on the SMP examples releases the second core. Then the runtime's startup calls adainit, which runs every package's elaboration — and that is where library-level tasks like the blinker come alive, before Main is entered. Finally Main runs.

There is no vendor startup shim and no third-party scheduler in that chain. The Ada runtime is the only scheduler and it is linked from source.

Why the blink example's Main does nothing

Look at examples/esp32s3_gpio0_blink/src/main.adb and you find this:

with Ada.Real_Time; use Ada.Real_Time;

--  The GPIO0 blink driver + its task live in package GPIO; withing it pulls the
--  task into the program so it elaborates and runs.
with GPIO;
pragma Unreferenced (GPIO);

procedure Main is
begin
   loop
      delay until Clock + Seconds (3600);
   end loop;
end Main;

The work is done by a library-level task inside package GPIO, which starts at elaboration. Main becomes the environment task, which here just parks forever. Withing a package you never call is how you pull its tasks into the link closure; pragma Unreferenced tells the compiler that is deliberate.

A minimal application

If you would rather do the work in Main itself, this is a complete program:

with Ada.Real_Time; use Ada.Real_Time;
with ESP32S3.GPIO;

procedure Main is
   Led : constant ESP32S3.GPIO.Pin_Id := 2;
begin
   ESP32S3.GPIO.Configure (Led, Mode => ESP32S3.GPIO.Output);
   loop
      ESP32S3.GPIO.Toggle (Led);
      delay until Clock + Milliseconds (250);
   end loop;
end Main;

delay until is served by the board-support tick; ESP32S3.GPIO is the HAL; everything links against the runtime crate.

What happens between reset and Main · 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 06 of 13

What happens between reset and Main

Your blink worked. Here is every layer it went through, from the chip's mask ROM to the first line of your Ada — and what an RTOS would normally be doing that nothing here does.

The layers, top to bottom

LayerWhat it is
Your application Ada packages and a Main procedure.
The HAL libs/esp32s3_hal: hand-written driver packages (ESP32S3.*) over a generated register layer (ESP32S3_Registers.*). Optional — a program may poke registers itself — but it is where the device knowledge lives.
The binder output gnatbind generates b__main, which elaborates every unit in dependency order and then calls Main.
The Ada runtime The System.* and Ada.* bodies: secondary stack, exceptions, text I/O over the USB-serial console, and the tasking kernel — tasks, protected objects, delays, scheduling.
Board support The silicon-specific bottom of the runtime: the Xtensa windowed context switch, interrupt entry/exit vectors, the clock tick, SMP scheduling and the inter-core interrupt. Ada plus a little assembly (start.S, highint5.S, context_switch.S). This is the layer an off-the-shelf RTOS would otherwise provide.
2nd-stage bootloader A minimal loader of our own that maps the flash cache/MMU, brings up the octal PSRAM, and jumps to the application image. It replaces the vendor's stock second-stage bootloader.
Mask ROM On-chip, immutable. Loads the 2nd-stage bootloader from flash at reset.

The boot path

ROM ─> 2nd-stage bootloader ─> start.S ─> adainit ─> Main
        cache/MMU, PSRAM      PLL, vectors,  elaboration
                              release core 1

start.S selects the 240 MHz PLL, sets up the stack and the interrupt vectors, and on the SMP examples releases the second core. Then the runtime's startup calls adainit, which runs every package's elaboration — and that is where library-level tasks like the blinker come alive, before Main is entered. Finally Main runs.

There is no vendor startup shim and no third-party scheduler in that chain. The Ada runtime is the only scheduler and it is linked from source.

Why the blink example's Main does nothing

Look at examples/esp32s3_gpio0_blink/src/main.adb and you find this:

with Ada.Real_Time; use Ada.Real_Time;

--  The GPIO0 blink driver + its task live in package GPIO; withing it pulls the
--  task into the program so it elaborates and runs.
with GPIO;
pragma Unreferenced (GPIO);

procedure Main is
begin
   loop
      delay until Clock + Seconds (3600);
   end loop;
end Main;

The work is done by a library-level task inside package GPIO, which starts at elaboration. Main becomes the environment task, which here just parks forever. Withing a package you never call is how you pull its tasks into the link closure; pragma Unreferenced tells the compiler that is deliberate.

A minimal application

If you would rather do the work in Main itself, this is a complete program:

with Ada.Real_Time; use Ada.Real_Time;
with ESP32S3.GPIO;

procedure Main is
   Led : constant ESP32S3.GPIO.Pin_Id := 2;
begin
   ESP32S3.GPIO.Configure (Led, Mode => ESP32S3.GPIO.Output);
   loop
      ESP32S3.GPIO.Toggle (Led);
      delay until Clock + Milliseconds (250);
   end loop;
end Main;

delay until is served by the board-support tick; ESP32S3.GPIO is the HAL; everything links against the runtime crate.

What happens between reset and Main · 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 06 of 13

What happens between reset and Main

Your blink worked. Here is every layer it went through, from the chip's mask ROM to the first line of your Ada — and what an RTOS would normally be doing that nothing here does.

The layers, top to bottom

LayerWhat it is
Your application Ada packages and a Main procedure.
The HAL libs/esp32s3_hal: hand-written driver packages (ESP32S3.*) over a generated register layer (ESP32S3_Registers.*). Optional — a program may poke registers itself — but it is where the device knowledge lives.
The binder output gnatbind generates b__main, which elaborates every unit in dependency order and then calls Main.
The Ada runtime The System.* and Ada.* bodies: secondary stack, exceptions, text I/O over the USB-serial console, and the tasking kernel — tasks, protected objects, delays, scheduling.
Board support The silicon-specific bottom of the runtime: the Xtensa windowed context switch, interrupt entry/exit vectors, the clock tick, SMP scheduling and the inter-core interrupt. Ada plus a little assembly (start.S, highint5.S, context_switch.S). This is the layer an off-the-shelf RTOS would otherwise provide.
2nd-stage bootloader A minimal loader of our own that maps the flash cache/MMU, brings up the octal PSRAM, and jumps to the application image. It replaces the vendor's stock second-stage bootloader.
Mask ROM On-chip, immutable. Loads the 2nd-stage bootloader from flash at reset.

The boot path

ROM ─> 2nd-stage bootloader ─> start.S ─> adainit ─> Main
        cache/MMU, PSRAM      PLL, vectors,  elaboration
                              release core 1

start.S selects the 240 MHz PLL, sets up the stack and the interrupt vectors, and on the SMP examples releases the second core. Then the runtime's startup calls adainit, which runs every package's elaboration — and that is where library-level tasks like the blinker come alive, before Main is entered. Finally Main runs.

There is no vendor startup shim and no third-party scheduler in that chain. The Ada runtime is the only scheduler and it is linked from source.

Why the blink example's Main does nothing

Look at examples/esp32s3_gpio0_blink/src/main.adb and you find this:

with Ada.Real_Time; use Ada.Real_Time;

--  The GPIO0 blink driver + its task live in package GPIO; withing it pulls the
--  task into the program so it elaborates and runs.
with GPIO;
pragma Unreferenced (GPIO);

procedure Main is
begin
   loop
      delay until Clock + Seconds (3600);
   end loop;
end Main;

The work is done by a library-level task inside package GPIO, which starts at elaboration. Main becomes the environment task, which here just parks forever. Withing a package you never call is how you pull its tasks into the link closure; pragma Unreferenced tells the compiler that is deliberate.

A minimal application

If you would rather do the work in Main itself, this is a complete program:

with Ada.Real_Time; use Ada.Real_Time;
with ESP32S3.GPIO;

procedure Main is
   Led : constant ESP32S3.GPIO.Pin_Id := 2;
begin
   ESP32S3.GPIO.Configure (Led, Mode => ESP32S3.GPIO.Output);
   loop
      ESP32S3.GPIO.Toggle (Led);
      delay until Clock + Milliseconds (250);
   end loop;
end Main;

delay until is served by the board-support tick; ESP32S3.GPIO is the HAL; everything links against the runtime crate.