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.