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

RMT: an arbitrary pulse generator

Sequences of {level, duration} symbols in hardware — IR remotes, WS2812 LED strings, 1-Wire, and any timing you would otherwise bit-bang badly.

Symbols, not bits

RMT transmits and receives sequences of pulses. The unit is a symbol: two consecutive {level, duration} pairs packed into one 32-bit word, laid out to match the hardware exactly:

type Tick_Count is range 0 .. 32_767;          --  15-bit duration

type RMT_Symbol is record
   Level0    : Boolean    := False;
   Duration0 : Tick_Count := 0;
   Level1    : Boolean    := False;
   Duration1 : Tick_Count := 0;
end record;

for RMT_Symbol use record
   Duration0 at 0 range  0 .. 14;
   Level0    at 0 range 15 .. 15;
   Duration1 at 0 range 16 .. 30;
   Level1    at 0 range 31 .. 31;
end record;
for RMT_Symbol'Size use 32;

The representation clause is the point: you write ordinary Ada record fields and the compiler lays them out bit-exactly as the symbol RAM expects, so there is no shifting or masking anywhere in your code. Durations are in channel ticks, and a tick is 1 / Resolution_Hz — set the resolution to 1_000_000 and a tick is one microsecond, which is how IR protocol timings are usually written down.

Eight channels, split by direction

type TX_Index is range 0 .. 3;
type RX_Index is range 0 .. 3;

type TX_Channel is limited private;
type RX_Channel is limited private;

Channels 0 .. 3 transmit and 4 .. 7 receive, each with a 48-symbol RAM block. They are claimed handles — limited, controlled, released on scope exit — and TX and RX are distinct types, so the two cannot be confused at a call site.

Borrowing RAM for longer bursts

Configure takes a Blocks parameter of 1 .. 4, giving the channel that many consecutive 48-symbol RAM blocks.

Blocks > 1 borrows the RAM of the higher-numbered TX channels. Claiming two blocks on channel 0 consumes channel 1's memory, so channel 1 is no longer usable. That is a real constraint on how many independent pulse trains you can run at once, and it is invisible unless you know to look for it.

Beyond the symbol RAM, a longer burst is streamed by refilling the RAM in halves as it drains, so Transmit is not limited to what fits — it just costs CPU attention during the burst.

procedure Transmit (C : TX_Channel; Symbols : Symbol_Array);   --  blocking

Receiving

procedure Start   (C : RX_Channel);                                        --  arm
procedure Receive (C : RX_Channel; Into : out Symbol_Array; Count : out Natural);

Start arms the receiver and should be called just before the incoming burst; Receive blocks until reception ends and reports how many symbols were captured. Reception ends on an idle threshold, so a protocol whose inter-frame gap is shorter than your threshold will run two bursts together.

./x run esp32s3_rmt_loopback exercises both directions with a TX channel driving an RX channel through one pad — no IR LED, no receiver.

RMT: an arbitrary pulse generator · 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 21 of 56

RMT: an arbitrary pulse generator

Sequences of {level, duration} symbols in hardware — IR remotes, WS2812 LED strings, 1-Wire, and any timing you would otherwise bit-bang badly.

Symbols, not bits

RMT transmits and receives sequences of pulses. The unit is a symbol: two consecutive {level, duration} pairs packed into one 32-bit word, laid out to match the hardware exactly:

type Tick_Count is range 0 .. 32_767;          --  15-bit duration

type RMT_Symbol is record
   Level0    : Boolean    := False;
   Duration0 : Tick_Count := 0;
   Level1    : Boolean    := False;
   Duration1 : Tick_Count := 0;
end record;

for RMT_Symbol use record
   Duration0 at 0 range  0 .. 14;
   Level0    at 0 range 15 .. 15;
   Duration1 at 0 range 16 .. 30;
   Level1    at 0 range 31 .. 31;
end record;
for RMT_Symbol'Size use 32;

The representation clause is the point: you write ordinary Ada record fields and the compiler lays them out bit-exactly as the symbol RAM expects, so there is no shifting or masking anywhere in your code. Durations are in channel ticks, and a tick is 1 / Resolution_Hz — set the resolution to 1_000_000 and a tick is one microsecond, which is how IR protocol timings are usually written down.

Eight channels, split by direction

type TX_Index is range 0 .. 3;
type RX_Index is range 0 .. 3;

type TX_Channel is limited private;
type RX_Channel is limited private;

Channels 0 .. 3 transmit and 4 .. 7 receive, each with a 48-symbol RAM block. They are claimed handles — limited, controlled, released on scope exit — and TX and RX are distinct types, so the two cannot be confused at a call site.

Borrowing RAM for longer bursts

Configure takes a Blocks parameter of 1 .. 4, giving the channel that many consecutive 48-symbol RAM blocks.

Blocks > 1 borrows the RAM of the higher-numbered TX channels. Claiming two blocks on channel 0 consumes channel 1's memory, so channel 1 is no longer usable. That is a real constraint on how many independent pulse trains you can run at once, and it is invisible unless you know to look for it.

Beyond the symbol RAM, a longer burst is streamed by refilling the RAM in halves as it drains, so Transmit is not limited to what fits — it just costs CPU attention during the burst.

procedure Transmit (C : TX_Channel; Symbols : Symbol_Array);   --  blocking

Receiving

procedure Start   (C : RX_Channel);                                        --  arm
procedure Receive (C : RX_Channel; Into : out Symbol_Array; Count : out Natural);

Start arms the receiver and should be called just before the incoming burst; Receive blocks until reception ends and reports how many symbols were captured. Reception ends on an idle threshold, so a protocol whose inter-frame gap is shorter than your threshold will run two bursts together.

./x run esp32s3_rmt_loopback exercises both directions with a TX channel driving an RX channel through one pad — no IR LED, no receiver.

RMT: an arbitrary pulse generator · 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 21 of 56

RMT: an arbitrary pulse generator

Sequences of {level, duration} symbols in hardware — IR remotes, WS2812 LED strings, 1-Wire, and any timing you would otherwise bit-bang badly.

Symbols, not bits

RMT transmits and receives sequences of pulses. The unit is a symbol: two consecutive {level, duration} pairs packed into one 32-bit word, laid out to match the hardware exactly:

type Tick_Count is range 0 .. 32_767;          --  15-bit duration

type RMT_Symbol is record
   Level0    : Boolean    := False;
   Duration0 : Tick_Count := 0;
   Level1    : Boolean    := False;
   Duration1 : Tick_Count := 0;
end record;

for RMT_Symbol use record
   Duration0 at 0 range  0 .. 14;
   Level0    at 0 range 15 .. 15;
   Duration1 at 0 range 16 .. 30;
   Level1    at 0 range 31 .. 31;
end record;
for RMT_Symbol'Size use 32;

The representation clause is the point: you write ordinary Ada record fields and the compiler lays them out bit-exactly as the symbol RAM expects, so there is no shifting or masking anywhere in your code. Durations are in channel ticks, and a tick is 1 / Resolution_Hz — set the resolution to 1_000_000 and a tick is one microsecond, which is how IR protocol timings are usually written down.

Eight channels, split by direction

type TX_Index is range 0 .. 3;
type RX_Index is range 0 .. 3;

type TX_Channel is limited private;
type RX_Channel is limited private;

Channels 0 .. 3 transmit and 4 .. 7 receive, each with a 48-symbol RAM block. They are claimed handles — limited, controlled, released on scope exit — and TX and RX are distinct types, so the two cannot be confused at a call site.

Borrowing RAM for longer bursts

Configure takes a Blocks parameter of 1 .. 4, giving the channel that many consecutive 48-symbol RAM blocks.

Blocks > 1 borrows the RAM of the higher-numbered TX channels. Claiming two blocks on channel 0 consumes channel 1's memory, so channel 1 is no longer usable. That is a real constraint on how many independent pulse trains you can run at once, and it is invisible unless you know to look for it.

Beyond the symbol RAM, a longer burst is streamed by refilling the RAM in halves as it drains, so Transmit is not limited to what fits — it just costs CPU attention during the burst.

procedure Transmit (C : TX_Channel; Symbols : Symbol_Array);   --  blocking

Receiving

procedure Start   (C : RX_Channel);                                        --  arm
procedure Receive (C : RX_Channel; Into : out Symbol_Array; Count : out Natural);

Start arms the receiver and should be called just before the incoming burst; Receive blocks until reception ends and reports how many symbols were captured. Reception ends on an idle threshold, so a protocol whose inter-frame gap is shorter than your threshold will run two bursts together.

./x run esp32s3_rmt_loopback exercises both directions with a TX channel driving an RX channel through one pad — no IR LED, no receiver.

RMT: an arbitrary pulse generator · 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 21 of 56

RMT: an arbitrary pulse generator

Sequences of {level, duration} symbols in hardware — IR remotes, WS2812 LED strings, 1-Wire, and any timing you would otherwise bit-bang badly.

Symbols, not bits

RMT transmits and receives sequences of pulses. The unit is a symbol: two consecutive {level, duration} pairs packed into one 32-bit word, laid out to match the hardware exactly:

type Tick_Count is range 0 .. 32_767;          --  15-bit duration

type RMT_Symbol is record
   Level0    : Boolean    := False;
   Duration0 : Tick_Count := 0;
   Level1    : Boolean    := False;
   Duration1 : Tick_Count := 0;
end record;

for RMT_Symbol use record
   Duration0 at 0 range  0 .. 14;
   Level0    at 0 range 15 .. 15;
   Duration1 at 0 range 16 .. 30;
   Level1    at 0 range 31 .. 31;
end record;
for RMT_Symbol'Size use 32;

The representation clause is the point: you write ordinary Ada record fields and the compiler lays them out bit-exactly as the symbol RAM expects, so there is no shifting or masking anywhere in your code. Durations are in channel ticks, and a tick is 1 / Resolution_Hz — set the resolution to 1_000_000 and a tick is one microsecond, which is how IR protocol timings are usually written down.

Eight channels, split by direction

type TX_Index is range 0 .. 3;
type RX_Index is range 0 .. 3;

type TX_Channel is limited private;
type RX_Channel is limited private;

Channels 0 .. 3 transmit and 4 .. 7 receive, each with a 48-symbol RAM block. They are claimed handles — limited, controlled, released on scope exit — and TX and RX are distinct types, so the two cannot be confused at a call site.

Borrowing RAM for longer bursts

Configure takes a Blocks parameter of 1 .. 4, giving the channel that many consecutive 48-symbol RAM blocks.

Blocks > 1 borrows the RAM of the higher-numbered TX channels. Claiming two blocks on channel 0 consumes channel 1's memory, so channel 1 is no longer usable. That is a real constraint on how many independent pulse trains you can run at once, and it is invisible unless you know to look for it.

Beyond the symbol RAM, a longer burst is streamed by refilling the RAM in halves as it drains, so Transmit is not limited to what fits — it just costs CPU attention during the burst.

procedure Transmit (C : TX_Channel; Symbols : Symbol_Array);   --  blocking

Receiving

procedure Start   (C : RX_Channel);                                        --  arm
procedure Receive (C : RX_Channel; Into : out Symbol_Array; Count : out Natural);

Start arms the receiver and should be called just before the incoming burst; Receive blocks until reception ends and reports how many symbols were captured. Reception ends on an idle threshold, so a protocol whose inter-frame gap is shorter than your threshold will run two bursts together.

./x run esp32s3_rmt_loopback exercises both directions with a TX channel driving an RX channel through one pad — no IR LED, no receiver.

RMT: an arbitrary pulse generator · 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 21 of 56

RMT: an arbitrary pulse generator

Sequences of {level, duration} symbols in hardware — IR remotes, WS2812 LED strings, 1-Wire, and any timing you would otherwise bit-bang badly.

Symbols, not bits

RMT transmits and receives sequences of pulses. The unit is a symbol: two consecutive {level, duration} pairs packed into one 32-bit word, laid out to match the hardware exactly:

type Tick_Count is range 0 .. 32_767;          --  15-bit duration

type RMT_Symbol is record
   Level0    : Boolean    := False;
   Duration0 : Tick_Count := 0;
   Level1    : Boolean    := False;
   Duration1 : Tick_Count := 0;
end record;

for RMT_Symbol use record
   Duration0 at 0 range  0 .. 14;
   Level0    at 0 range 15 .. 15;
   Duration1 at 0 range 16 .. 30;
   Level1    at 0 range 31 .. 31;
end record;
for RMT_Symbol'Size use 32;

The representation clause is the point: you write ordinary Ada record fields and the compiler lays them out bit-exactly as the symbol RAM expects, so there is no shifting or masking anywhere in your code. Durations are in channel ticks, and a tick is 1 / Resolution_Hz — set the resolution to 1_000_000 and a tick is one microsecond, which is how IR protocol timings are usually written down.

Eight channels, split by direction

type TX_Index is range 0 .. 3;
type RX_Index is range 0 .. 3;

type TX_Channel is limited private;
type RX_Channel is limited private;

Channels 0 .. 3 transmit and 4 .. 7 receive, each with a 48-symbol RAM block. They are claimed handles — limited, controlled, released on scope exit — and TX and RX are distinct types, so the two cannot be confused at a call site.

Borrowing RAM for longer bursts

Configure takes a Blocks parameter of 1 .. 4, giving the channel that many consecutive 48-symbol RAM blocks.

Blocks > 1 borrows the RAM of the higher-numbered TX channels. Claiming two blocks on channel 0 consumes channel 1's memory, so channel 1 is no longer usable. That is a real constraint on how many independent pulse trains you can run at once, and it is invisible unless you know to look for it.

Beyond the symbol RAM, a longer burst is streamed by refilling the RAM in halves as it drains, so Transmit is not limited to what fits — it just costs CPU attention during the burst.

procedure Transmit (C : TX_Channel; Symbols : Symbol_Array);   --  blocking

Receiving

procedure Start   (C : RX_Channel);                                        --  arm
procedure Receive (C : RX_Channel; Into : out Symbol_Array; Count : out Natural);

Start arms the receiver and should be called just before the incoming burst; Receive blocks until reception ends and reports how many symbols were captured. Reception ends on an idle threshold, so a protocol whose inter-frame gap is shorter than your threshold will run two bursts together.

./x run esp32s3_rmt_loopback exercises both directions with a TX channel driving an RX channel through one pad — no IR LED, no receiver.