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 26 of 56

RTC, pad hold and deep sleep

Deep sleep is not a pause — the chip resets and re-runs from the start. What survives is RTC memory, and the pads you explicitly told to hold their level.

The mental model that matters

Waking from deep sleep is a reset, not a resume. The digital core — CPU and main RAM — is powered down; only the RTC domain stays alive. On wake the chip restarts from the beginning: your Main runs again from the top, every variable re-elaborated. Nothing in ordinary RAM survives. Code written as though sleep were a blocking delay will be wrong in a way that looks like a spontaneous reboot.

Two things do survive: data you deliberately put in RTC memory, and RTC pads you told to hold.

Retained memory

subtype Word_Index is Natural range 0 .. Slow_Memory_Size / 4 - 1;

function  Read  (Index : Word_Index) return Interfaces.Unsigned_32;
procedure Write (Index : Word_Index; Value : Interfaces.Unsigned_32);

There is also a generic typed retained object: instantiate it once per stored item and it gives you a distinct slot with a real type, rather than making you hand-manage word indices and remember which one held what. Prefer that for anything beyond a single counter.

Knowing why you are running

type Wake_Cause is ...;                       --  power-on, timer, RTC-GPIO, ...
function Last_Wake       return Wake_Cause;
function Raw_Reset_Cause return Natural;      --  5 = deep-sleep wake
function Raw_Wake_Cause  return Natural;

Because every wake re-runs your program, the first thing it usually has to do is ask why: a cold power-on initialises state, a timer wake continues a duty cycle, a pin wake handles an event. That branch is the shape of nearly every low-power application.

Entering sleep

procedure Deep_Sleep_For (Wake_After : Duration);   --  timer wake
--  plus a variant that sleeps until an RTC-capable pin reaches a level (EXT1)

These do not return. If one does, the sleep FSM rejected the request — and Raw_Reject_Cause tells you why. Treat a return as an error path, not as normal control flow, because that is exactly what it is.

Pad hold: keeping a line asserted through the reset

GPIO0 .. GPIO21 are RTC-capable. The headline feature of ESP32S3.RTC_IO is hold: latch a pad at its current output level so it keeps driving while the rest of the chip changes — including through deep sleep and across the reset that waking causes.

subtype RTC_Pin is ESP32S3.GPIO.Pin_Id range 0 .. 21;

procedure Hold    (Pin : RTC_Pin);
procedure Release (Pin : RTC_Pin);
function  Is_Held (Pin : RTC_Pin) return Boolean;
procedure Set_Pull (Pin : RTC_Pin; Mode : Pull_Mode);   --  No_Pull | Up | Down

This is how you keep a load enabled or a peripheral's reset line asserted while you sleep — without it, every pad returns to a default input at the moment of sleep and your board resets its own sensors.

A held pad ignores ordinary GPIO writes until you Release it. On the run after a wake, a pin that refuses to change is almost always one you held before sleeping and have not released.

Neither package needs a tasking runtime — both are register pokes — and RTC-IO works under every runtime profile.

RTC, pad hold and deep sleep · 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 26 of 56

RTC, pad hold and deep sleep

Deep sleep is not a pause — the chip resets and re-runs from the start. What survives is RTC memory, and the pads you explicitly told to hold their level.

The mental model that matters

Waking from deep sleep is a reset, not a resume. The digital core — CPU and main RAM — is powered down; only the RTC domain stays alive. On wake the chip restarts from the beginning: your Main runs again from the top, every variable re-elaborated. Nothing in ordinary RAM survives. Code written as though sleep were a blocking delay will be wrong in a way that looks like a spontaneous reboot.

Two things do survive: data you deliberately put in RTC memory, and RTC pads you told to hold.

Retained memory

subtype Word_Index is Natural range 0 .. Slow_Memory_Size / 4 - 1;

function  Read  (Index : Word_Index) return Interfaces.Unsigned_32;
procedure Write (Index : Word_Index; Value : Interfaces.Unsigned_32);

There is also a generic typed retained object: instantiate it once per stored item and it gives you a distinct slot with a real type, rather than making you hand-manage word indices and remember which one held what. Prefer that for anything beyond a single counter.

Knowing why you are running

type Wake_Cause is ...;                       --  power-on, timer, RTC-GPIO, ...
function Last_Wake       return Wake_Cause;
function Raw_Reset_Cause return Natural;      --  5 = deep-sleep wake
function Raw_Wake_Cause  return Natural;

Because every wake re-runs your program, the first thing it usually has to do is ask why: a cold power-on initialises state, a timer wake continues a duty cycle, a pin wake handles an event. That branch is the shape of nearly every low-power application.

Entering sleep

procedure Deep_Sleep_For (Wake_After : Duration);   --  timer wake
--  plus a variant that sleeps until an RTC-capable pin reaches a level (EXT1)

These do not return. If one does, the sleep FSM rejected the request — and Raw_Reject_Cause tells you why. Treat a return as an error path, not as normal control flow, because that is exactly what it is.

Pad hold: keeping a line asserted through the reset

GPIO0 .. GPIO21 are RTC-capable. The headline feature of ESP32S3.RTC_IO is hold: latch a pad at its current output level so it keeps driving while the rest of the chip changes — including through deep sleep and across the reset that waking causes.

subtype RTC_Pin is ESP32S3.GPIO.Pin_Id range 0 .. 21;

procedure Hold    (Pin : RTC_Pin);
procedure Release (Pin : RTC_Pin);
function  Is_Held (Pin : RTC_Pin) return Boolean;
procedure Set_Pull (Pin : RTC_Pin; Mode : Pull_Mode);   --  No_Pull | Up | Down

This is how you keep a load enabled or a peripheral's reset line asserted while you sleep — without it, every pad returns to a default input at the moment of sleep and your board resets its own sensors.

A held pad ignores ordinary GPIO writes until you Release it. On the run after a wake, a pin that refuses to change is almost always one you held before sleeping and have not released.

Neither package needs a tasking runtime — both are register pokes — and RTC-IO works under every runtime profile.

RTC, pad hold and deep sleep · 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 26 of 56

RTC, pad hold and deep sleep

Deep sleep is not a pause — the chip resets and re-runs from the start. What survives is RTC memory, and the pads you explicitly told to hold their level.

The mental model that matters

Waking from deep sleep is a reset, not a resume. The digital core — CPU and main RAM — is powered down; only the RTC domain stays alive. On wake the chip restarts from the beginning: your Main runs again from the top, every variable re-elaborated. Nothing in ordinary RAM survives. Code written as though sleep were a blocking delay will be wrong in a way that looks like a spontaneous reboot.

Two things do survive: data you deliberately put in RTC memory, and RTC pads you told to hold.

Retained memory

subtype Word_Index is Natural range 0 .. Slow_Memory_Size / 4 - 1;

function  Read  (Index : Word_Index) return Interfaces.Unsigned_32;
procedure Write (Index : Word_Index; Value : Interfaces.Unsigned_32);

There is also a generic typed retained object: instantiate it once per stored item and it gives you a distinct slot with a real type, rather than making you hand-manage word indices and remember which one held what. Prefer that for anything beyond a single counter.

Knowing why you are running

type Wake_Cause is ...;                       --  power-on, timer, RTC-GPIO, ...
function Last_Wake       return Wake_Cause;
function Raw_Reset_Cause return Natural;      --  5 = deep-sleep wake
function Raw_Wake_Cause  return Natural;

Because every wake re-runs your program, the first thing it usually has to do is ask why: a cold power-on initialises state, a timer wake continues a duty cycle, a pin wake handles an event. That branch is the shape of nearly every low-power application.

Entering sleep

procedure Deep_Sleep_For (Wake_After : Duration);   --  timer wake
--  plus a variant that sleeps until an RTC-capable pin reaches a level (EXT1)

These do not return. If one does, the sleep FSM rejected the request — and Raw_Reject_Cause tells you why. Treat a return as an error path, not as normal control flow, because that is exactly what it is.

Pad hold: keeping a line asserted through the reset

GPIO0 .. GPIO21 are RTC-capable. The headline feature of ESP32S3.RTC_IO is hold: latch a pad at its current output level so it keeps driving while the rest of the chip changes — including through deep sleep and across the reset that waking causes.

subtype RTC_Pin is ESP32S3.GPIO.Pin_Id range 0 .. 21;

procedure Hold    (Pin : RTC_Pin);
procedure Release (Pin : RTC_Pin);
function  Is_Held (Pin : RTC_Pin) return Boolean;
procedure Set_Pull (Pin : RTC_Pin; Mode : Pull_Mode);   --  No_Pull | Up | Down

This is how you keep a load enabled or a peripheral's reset line asserted while you sleep — without it, every pad returns to a default input at the moment of sleep and your board resets its own sensors.

A held pad ignores ordinary GPIO writes until you Release it. On the run after a wake, a pin that refuses to change is almost always one you held before sleeping and have not released.

Neither package needs a tasking runtime — both are register pokes — and RTC-IO works under every runtime profile.

RTC, pad hold and deep sleep · 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 26 of 56

RTC, pad hold and deep sleep

Deep sleep is not a pause — the chip resets and re-runs from the start. What survives is RTC memory, and the pads you explicitly told to hold their level.

The mental model that matters

Waking from deep sleep is a reset, not a resume. The digital core — CPU and main RAM — is powered down; only the RTC domain stays alive. On wake the chip restarts from the beginning: your Main runs again from the top, every variable re-elaborated. Nothing in ordinary RAM survives. Code written as though sleep were a blocking delay will be wrong in a way that looks like a spontaneous reboot.

Two things do survive: data you deliberately put in RTC memory, and RTC pads you told to hold.

Retained memory

subtype Word_Index is Natural range 0 .. Slow_Memory_Size / 4 - 1;

function  Read  (Index : Word_Index) return Interfaces.Unsigned_32;
procedure Write (Index : Word_Index; Value : Interfaces.Unsigned_32);

There is also a generic typed retained object: instantiate it once per stored item and it gives you a distinct slot with a real type, rather than making you hand-manage word indices and remember which one held what. Prefer that for anything beyond a single counter.

Knowing why you are running

type Wake_Cause is ...;                       --  power-on, timer, RTC-GPIO, ...
function Last_Wake       return Wake_Cause;
function Raw_Reset_Cause return Natural;      --  5 = deep-sleep wake
function Raw_Wake_Cause  return Natural;

Because every wake re-runs your program, the first thing it usually has to do is ask why: a cold power-on initialises state, a timer wake continues a duty cycle, a pin wake handles an event. That branch is the shape of nearly every low-power application.

Entering sleep

procedure Deep_Sleep_For (Wake_After : Duration);   --  timer wake
--  plus a variant that sleeps until an RTC-capable pin reaches a level (EXT1)

These do not return. If one does, the sleep FSM rejected the request — and Raw_Reject_Cause tells you why. Treat a return as an error path, not as normal control flow, because that is exactly what it is.

Pad hold: keeping a line asserted through the reset

GPIO0 .. GPIO21 are RTC-capable. The headline feature of ESP32S3.RTC_IO is hold: latch a pad at its current output level so it keeps driving while the rest of the chip changes — including through deep sleep and across the reset that waking causes.

subtype RTC_Pin is ESP32S3.GPIO.Pin_Id range 0 .. 21;

procedure Hold    (Pin : RTC_Pin);
procedure Release (Pin : RTC_Pin);
function  Is_Held (Pin : RTC_Pin) return Boolean;
procedure Set_Pull (Pin : RTC_Pin; Mode : Pull_Mode);   --  No_Pull | Up | Down

This is how you keep a load enabled or a peripheral's reset line asserted while you sleep — without it, every pad returns to a default input at the moment of sleep and your board resets its own sensors.

A held pad ignores ordinary GPIO writes until you Release it. On the run after a wake, a pin that refuses to change is almost always one you held before sleeping and have not released.

Neither package needs a tasking runtime — both are register pokes — and RTC-IO works under every runtime profile.

RTC, pad hold and deep sleep · 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 26 of 56

RTC, pad hold and deep sleep

Deep sleep is not a pause — the chip resets and re-runs from the start. What survives is RTC memory, and the pads you explicitly told to hold their level.

The mental model that matters

Waking from deep sleep is a reset, not a resume. The digital core — CPU and main RAM — is powered down; only the RTC domain stays alive. On wake the chip restarts from the beginning: your Main runs again from the top, every variable re-elaborated. Nothing in ordinary RAM survives. Code written as though sleep were a blocking delay will be wrong in a way that looks like a spontaneous reboot.

Two things do survive: data you deliberately put in RTC memory, and RTC pads you told to hold.

Retained memory

subtype Word_Index is Natural range 0 .. Slow_Memory_Size / 4 - 1;

function  Read  (Index : Word_Index) return Interfaces.Unsigned_32;
procedure Write (Index : Word_Index; Value : Interfaces.Unsigned_32);

There is also a generic typed retained object: instantiate it once per stored item and it gives you a distinct slot with a real type, rather than making you hand-manage word indices and remember which one held what. Prefer that for anything beyond a single counter.

Knowing why you are running

type Wake_Cause is ...;                       --  power-on, timer, RTC-GPIO, ...
function Last_Wake       return Wake_Cause;
function Raw_Reset_Cause return Natural;      --  5 = deep-sleep wake
function Raw_Wake_Cause  return Natural;

Because every wake re-runs your program, the first thing it usually has to do is ask why: a cold power-on initialises state, a timer wake continues a duty cycle, a pin wake handles an event. That branch is the shape of nearly every low-power application.

Entering sleep

procedure Deep_Sleep_For (Wake_After : Duration);   --  timer wake
--  plus a variant that sleeps until an RTC-capable pin reaches a level (EXT1)

These do not return. If one does, the sleep FSM rejected the request — and Raw_Reject_Cause tells you why. Treat a return as an error path, not as normal control flow, because that is exactly what it is.

Pad hold: keeping a line asserted through the reset

GPIO0 .. GPIO21 are RTC-capable. The headline feature of ESP32S3.RTC_IO is hold: latch a pad at its current output level so it keeps driving while the rest of the chip changes — including through deep sleep and across the reset that waking causes.

subtype RTC_Pin is ESP32S3.GPIO.Pin_Id range 0 .. 21;

procedure Hold    (Pin : RTC_Pin);
procedure Release (Pin : RTC_Pin);
function  Is_Held (Pin : RTC_Pin) return Boolean;
procedure Set_Pull (Pin : RTC_Pin; Mode : Pull_Mode);   --  No_Pull | Up | Down

This is how you keep a load enabled or a peripheral's reset line asserted while you sleep — without it, every pad returns to a default input at the moment of sleep and your board resets its own sensors.

A held pad ignores ordinary GPIO writes until you Release it. On the run after a wake, a pin that refuses to change is almost always one you held before sleeping and have not released.

Neither package needs a tasking runtime — both are register pokes — and RTC-IO works under every runtime profile.