esp32_rmt
RMT: the remote-control peripheral, used here as a precise pulse generator.
Eight channels that emit a programmed sequence of high/low durations, clocked off APB through a per-channel divider. It exists for infrared remote protocols, and it is also the only sane way to drive an addressable LED — WS2812 and friends need sub-microsecond pulse widths held to a few hundred nanoseconds, which is not something a task competing with a 1 ms tick can do by toggling a pin.
What this does and does not do
Section titled “What this does and does not do”Two modes.
[Rmt::transmit] is one shot: write the entries, start, done. It is
bounded by the channel’s 64-entry block — 63 usable, and a WS2812 bit is
one entry, so two LEDs. Simple, and enough for a board with a single
onboard LED.
[Rmt::start_stream] is for anything longer. The channel wraps around its
block while an interrupt refills the half just played, so frame length is
bounded by nothing but the caller’s buffer. A 5×5 panel is 600 entries and
needs this: MEM_SIZE maxed gives one channel all eight blocks, 512
entries, at the cost of every other channel, and 512 < 600.
Streaming buys that at the price of a deadline. A refill has one half-block
— about 40 µs at WS2812 rates — before the transmitter reaches what it is
writing, and there is no underrun flag, so being late is silent and looks
like corrupt pixels. [Refill] holds the bookkeeping and is pure, so the
ping-pong can be tested on a host rather than by watching an LED.
Rejecting an over-long sequence is deliberate. Silently truncating it would light some LEDs and leave others at whatever they held before, which looks like a wiring fault rather than a programming error.
Register facts, all checked against esp-idf headers
Section titled “Register facts, all checked against esp-idf headers”Every constant here was read out of soc/rmt_reg.h, soc/soc.h,
soc/dport_reg.h and soc/gpio_sig_map.h rather than recalled. Four of
them would have been wrong from memory, which is why:
- the channel FIFO register is at offset 0x0000, not 0x800
IDLE_OUT_ENandIDLE_OUT_LVlive in CONF1, not CONF0ETS_RMT_INTR_SOURCEis 47, not 46RMT_SIG_OUT0_IDXis 87
Two ways in, and only one of them works twice
Section titled “Two ways in, and only one of them works twice”There are two paths to a channel’s entries, and the first version of this
module took the wrong one. Writing RMT_CHnDATA_REG pushes through an APB
FIFO; setting RMT_APB_FIFO_MASK instead maps the 64-entry block into
memory at RMT_BASE + 0x800 and you write it by index.
The FIFO path transmits correctly once. Its write pointer is rewound by
APB_MEM_RST, which is a different bit from the MEM_RD_RST that rewinds
the read pointer, so a driver that resets only the read pointer replays its
first frame forever – an LED that lights the right colour and then ignores
every later one. That is what happened, and the symptom is quiet enough to
be mistaken for the encoder.
This uses the memory path, because esp-idf does: rmt_ll_tx_reset_pointer
touches mem_rd_rst alone, which is only sufficient when the writes went
to RMTMEM rather than through the FIFO. mem_wr_rst, despite the name,
rewinds the receiver’s pointer and has nothing to do with transmitting.
RMTMEM = 0x3ff56800 comes from esp-idf’s esp32.peripherals.ld, and the
rest from rmt_reg.h read literally rather than summarised.
Structs
Section titled “Structs”struct Chunk { pub dest: usize, pub src: usize, pub len: usize, pub terminator: bool,}One refill: copy len entries from src in the source into the block at
dest, and append a terminator if this is the last of them.
Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq
struct Entry(u32);One RMT entry: two consecutive pulses, each a level and a 15-bit duration in divided clock ticks.
A duration of zero is the end marker, which is why [Rmt::transmit] appends
one rather than trusting the caller to.
Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq
Refill
Section titled “Refill”struct Refill { /* private fields */ }Bookkeeping for a frame longer than the block it is played from.
Deliberately pure: no registers, no channel, nothing target-specific. The half-block ping-pong is the part with the off-by-ones in it, and keeping it separate is what lets it be tested on a host instead of by watching an LED.
Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq
struct Rmt { /* private fields */ }One RMT channel, configured for transmit.
Functions
Section titled “Functions”divider_for_ns
Section titled “divider_for_ns”const fn divider_for_ns(ns: u32) -> (u8, u32)Divider giving approximately ns per tick, clamped to the 8-bit field.
Returns the divider and the resulting nanoseconds-per-tick, because the caller needs the latter to convert its own pulse widths and recomputing it from the divider is how the two drift apart.
frame_ns
Section titled “frame_ns”const fn frame_ns(total_ticks: u32, ns_per_tick: u32) -> u32How long a frame of entries takes, in nanoseconds, at ns_per_tick.
Each entry holds two pulses; total_ticks is their summed durations. The
caller has that number already from building the entries, and recomputing it
from the packed words here would only be a second chance to get it wrong.
Constants
Section titled “Constants”APB_HZ
Section titled “APB_HZ”const APB_HZ: u32;APB clock feeding the RMT divider.
ENTRIES_PER_BLOCK
Section titled “ENTRIES_PER_BLOCK”const ENTRIES_PER_BLOCK: usize;Entries in one channel’s memory block.
Each entry encodes two pulses, so a block is 128 pulses — and a WS2812 bit is one entry, giving 64 bits, or two LEDs and change.
HALF_BLOCK
Section titled “HALF_BLOCK”const HALF_BLOCK: usize;Entries in half a block. The transmitter plays one half while the other is being refilled, which is what bounds the memory a stream needs to 64 entries however long the frame is.
IRQ_SOURCE
Section titled “IRQ_SOURCE”const IRQ_SOURCE: u8;The chip’s RMT peripheral interrupt source, for interrupt::connect.
One source serves all eight channels (ETS_RMT_INTR_SOURCE); the handler
reads the per-channel status to tell them apart. Re-exported so a caller can
wire the channel to a CPU interrupt without naming the SoC’s address map.