Benedikt a13def7df6
RMT: Type-erase channels (#3980)
* RMT: add HIL test that exercises all channels

to ensure that there are no issues with channel/register indexing; see
also the code comments.

no user-facing changes

* RMT: store ch_idx instead of channel number

`ConstChannelAccess` and `DynChannelAccess` used to store the channel number
and have a `Direction` generic parameter.
However, there are invalid combinations of `Direction` and the channel
number on some chips (e.g. on esp32c3, there is no channel with (Tx, 2),
because channel 2 and 3 only support Rx).

In constrast, the tuple `(Dir, ch_idx)` also uniquely identifies a
channel and its configuration, and any combination of it is valid as long as the
channel index is in bounds.

This makes it somewhat easier to work with; in particular, it will allow
removing bounds checks on ch_idx in a later commit by replacing
`ch_idx: u8` with an enum type.

This also refactors the `async_interrupt_handler` accordingly.

This is user-visible via the `ConstChannelAccess` type, however a later
commit in this PR will remove that entirely.

* RMT: elide bounds checks via ChannelIndex enum

If the channel index is not known at compile time in a given function,
the compiler will insert bounds checks on

- indexing the STATE global
- register access via the PAC

The compiler seems to be able to deduplicate them since low-level
functions are mostly inlined, but each function will retain at least one
bounds check.

This change helps the compiler to elide these checks by using a custom
type for channel indices that can only take values for which a channel
exists.
Specifically, this uses an enum to serve as a refinement of u8 to help
the compiler restrict value ranges. Due to inlining, this restricted
value range is propagated by the compiler until the potential bounds checks,
even if there's intermediate cast to u8,
cf. https://github.com/rust-lang/rust/issues/109958

There are no user-facing changes. (`ChannelIndex` is `pub` because it
appears in a `[doc(hidden)]` trait member.)

* RMT: always type-erase channels

- remove the `Raw` generic on `Channel`: This simplifies the types,
  should have negligible performance impact (helped by elided bounds
  checks via the recently introduced `ChannelIndex` enum), and greatly
  reduce code size if several channels are used (by avoiding
  monomorphization per channel)

- this also changed the arguments to configure_(tx,rx)_channel such that
  they don't contain generics, again avoiding code bloat due to
  monomorphization

This requires user code to change type annotations for channels and
transactions, and remove any manual type erasure (via `channel.degrade()`).

* RMT: impl blocking tx/rx methods directly on Channel

There's now only a single type that should implement these methods due
to erasing the channel index const generic, so the indirection via a
trait serves no purpose. Implementing methods directly on the channel
probably also helps to make the docs more discoverable.

This requires user code to remove the `TxChannel` and `RxChannel`
imports.

* RMT: impl async tx/rx methods directly on Channel

There's now only a single type that should implement these methods due
to erasing the channel index const generic, so the indirection via a
trait serves no purpose. Implementing methods directly on the channel
probably also helps to make the docs more discoverable.

This requires user code to remove the `TxChannelAsync` and `RxChannelAsync`
imports.

* RMT: impl *ChannelInternal traits only on DynChannelAccess

rather than as a blanket impl for RawChannelAccess, which includes
ConstChannelAccess: We don't intend to perform any accesses via
ConstChannelAccess anymore, so enforce that.
We could also get rid of the traits entirely and directly impl their
methods for DynChannelAccess, but it seems somewhat useful to keep them
around to guarantee that the interfaces in both chip_specific modules
are consistent.

There are no user-facing changes here.

* RMT: remove `pub` on various internal types

that used to be visible somewhere in the API's trait bounds, but are not
part of the API themselves

This is user-visible, but user code should not have used these types in
the first place.

* RMT: rm ConstChannelAccess, RawChannelAccess, private DynChannelAccess

now that Channel has no `Raw: RawChannelAccess` type parameter:

- we don't need low-level channel methods to be implemented for
  ConstChannelAccess, since all accesses go through DynChannelAccess.
  Thus, remove the *ChannelInternal traits and implement their methods directly
  on DynChannelAccess
- none of these types need to be public (although one might consider
  actually making them public with an unsafe constructor to provide an
  unsafe low-level interface to the hardware)

This is user-visible (imports need to be removed; they're not used
anymore since the previous commit that always type-erased the `Raw`
parameter of `Channel`)

* RMT: changelog and migration guide for type-erased channels

* RMT: (review) remove stray comment

* RMT: (review) remove is_tx() from Direction trait

we can simply use the const item directly
2025-09-03 12:15:51 +00:00

374 lines
12 KiB
Rust

//! RMT Loopback Test
//% CHIPS: esp32 esp32c3 esp32c6 esp32h2 esp32s2 esp32s3
//% FEATURES: unstable
#![no_std]
#![no_main]
use esp_hal::{
Blocking,
DriverMode,
gpio::{InputPin, Level, NoPin, OutputPin},
rmt::{
Channel,
Error,
PulseCode,
Rmt,
Rx,
RxChannelConfig,
RxChannelCreator,
Tx,
TxChannelConfig,
TxChannelCreator,
},
time::Rate,
};
use hil_test as _;
cfg_if::cfg_if! {
if #[cfg(esp32h2)] {
const FREQ: Rate = Rate::from_mhz(32);
const DIV: u8 = 64;
} else {
const FREQ: Rate = Rate::from_mhz(80);
const DIV: u8 = 160;
}
}
fn setup<Dm: DriverMode>(
rmt: Rmt<'static, Dm>,
rx: impl InputPin,
tx: impl OutputPin,
tx_config: TxChannelConfig,
rx_config: RxChannelConfig,
) -> (Channel<Dm, Tx>, Channel<Dm, Rx>) {
let tx_channel = rmt
.channel0
.configure_tx(tx, tx_config.with_clk_divider(DIV))
.unwrap();
cfg_if::cfg_if! {
if #[cfg(any(esp32, esp32s3))] {
let rx_channel_creator = rmt.channel4;
} else {
let rx_channel_creator = rmt.channel2;
}
};
let rx_channel = rx_channel_creator
.configure_rx(rx, rx_config.with_clk_divider(DIV))
.unwrap();
(tx_channel, rx_channel)
}
fn generate_tx_data<const TX_LEN: usize>(write_end_marker: bool) -> [PulseCode; TX_LEN] {
let mut tx_data: [_; TX_LEN] = core::array::from_fn(|i| {
PulseCode::new(Level::High, (100 + (i * 10) % 200) as u16, Level::Low, 50)
});
if write_end_marker {
tx_data[TX_LEN - 2] = PulseCode::new(Level::High, 3000, Level::Low, 500);
tx_data[TX_LEN - 1] = PulseCode::end_marker();
}
tx_data
}
fn do_rmt_loopback_inner<const TX_LEN: usize>(
tx_channel: Channel<Blocking, Tx>,
rx_channel: Channel<Blocking, Rx>,
) {
let tx_data: [_; TX_LEN] = generate_tx_data(true);
let mut rcv_data: [PulseCode; TX_LEN] = [PulseCode::default(); TX_LEN];
let mut rx_transaction = rx_channel.receive(&mut rcv_data).unwrap();
let mut tx_transaction = tx_channel.transmit(&tx_data).unwrap();
loop {
let tx_done = tx_transaction.poll();
let rx_done = rx_transaction.poll();
if tx_done && rx_done {
break;
}
}
tx_transaction.wait().unwrap();
rx_transaction.wait().unwrap();
// the last two pulse-codes are the ones which wait for the timeout so
// they can't be equal
assert_eq!(&tx_data[..TX_LEN - 2], &rcv_data[..TX_LEN - 2]);
}
// Run a test where some data is sent from one channel and looped back to
// another one for receive, and verify that the data matches.
fn do_rmt_loopback<const TX_LEN: usize>(tx_memsize: u8, rx_memsize: u8) {
let peripherals = esp_hal::init(esp_hal::Config::default());
let (rx, tx) = hil_test::common_test_pins!(peripherals);
let rmt = Rmt::new(peripherals.RMT, FREQ).unwrap();
let tx_config = TxChannelConfig::default().with_memsize(tx_memsize);
let rx_config = RxChannelConfig::default()
.with_idle_threshold(1000)
.with_memsize(rx_memsize);
let (tx_channel, rx_channel) = setup(rmt, rx, tx, tx_config, rx_config);
do_rmt_loopback_inner::<TX_LEN>(tx_channel, rx_channel);
}
// Run a test where some data is sent from one channel and looped back to
// another one for receive, and verify that the data matches.
async fn do_rmt_loopback_async<const TX_LEN: usize>(tx_memsize: u8, rx_memsize: u8) {
let peripherals = esp_hal::init(esp_hal::Config::default());
let (rx, tx) = hil_test::common_test_pins!(peripherals);
let rmt = Rmt::new(peripherals.RMT, FREQ).unwrap().into_async();
let tx_config = TxChannelConfig::default().with_memsize(tx_memsize);
let rx_config = RxChannelConfig::default()
.with_idle_threshold(1000)
.with_memsize(rx_memsize);
let (mut tx_channel, mut rx_channel) = setup(rmt, rx, tx, tx_config, rx_config);
let tx_data: [_; TX_LEN] = generate_tx_data(true);
let mut rcv_data: [PulseCode; TX_LEN] = [PulseCode::default(); TX_LEN];
let (rx_res, tx_res) = embassy_futures::join::join(
rx_channel.receive(&mut rcv_data),
tx_channel.transmit(&tx_data),
)
.await;
assert!(tx_res.is_ok());
assert!(rx_res.is_ok());
// the last two pulse-codes are the ones which wait for the timeout so
// they can't be equal
assert_eq!(&tx_data[..TX_LEN - 2], &rcv_data[..TX_LEN - 2]);
}
// Run a test that just sends some data, without trying to recive it.
#[must_use = "Tests should fail on errors"]
fn do_rmt_single_shot<const TX_LEN: usize>(
tx_memsize: u8,
write_end_marker: bool,
) -> Result<(), Error> {
let peripherals = esp_hal::init(esp_hal::Config::default());
let (rx, tx) = hil_test::common_test_pins!(peripherals);
let rmt = Rmt::new(peripherals.RMT, FREQ).unwrap();
let tx_config = TxChannelConfig::default()
.with_clk_divider(DIV)
.with_memsize(tx_memsize);
let (tx_channel, _) = setup(rmt, rx, tx, tx_config, Default::default());
let tx_data: [_; TX_LEN] = generate_tx_data(write_end_marker);
tx_channel.transmit(&tx_data)?.wait().map_err(|(e, _)| e)?;
Ok(())
}
#[cfg(test)]
#[embedded_test::tests(default_timeout = 1, executor = hil_test::Executor::new())]
mod tests {
use super::*;
#[init]
fn init() {}
#[test]
fn rmt_loopback_simple() {
// 20 codes fit a single RAM block
do_rmt_loopback::<20>(1, 1);
}
#[test]
async fn rmt_loopback_simple_async() {
// 20 codes fit a single RAM block
do_rmt_loopback_async::<20>(1, 1).await;
}
#[test]
fn rmt_loopback_extended_ram() {
// 80 codes require two RAM blocks
do_rmt_loopback::<80>(2, 2);
}
// FIXME: This test currently fails on esp32 with an rmt::Error::ReceiverError,
// which should imply a receiver overrun, which is unexpected (the buffer
// should hold 2 * 64 codes, which is sufficient).
// However, the problem already exists exists in the original code that added
// support for extended channel RAM, so we can't bisect to find a
// regression. Skip the test for now.
#[cfg(not(esp32))]
#[test]
fn rmt_loopback_tx_wrap() {
// 80 codes require two RAM blocks; thus a tx channel with only 1 block requires
// wrapping.
do_rmt_loopback::<80>(1, 2);
}
// FIXME: This test can't work right now, because wrapping rx is not
// implemented.
//
// #[test]
// fn rmt_loopback_rx_wrap() {
// // 80 codes require two RAM blocks; thus an rx channel with only 1 block
// // requires wrapping
// do_rmt_loopback<80>(2, 1);
// }
#[test]
fn rmt_single_shot_wrap() {
// Single RAM block (48 or 64 codes), requires wrapping
do_rmt_single_shot::<80>(1, true).unwrap();
}
#[test]
fn rmt_single_shot_extended() {
// Two RAM blocks (96 or 128 codes), no wrapping
do_rmt_single_shot::<80>(2, true).unwrap();
}
#[test]
fn rmt_single_shot_extended_wrap() {
// Two RAM blocks (96 or 128 codes), requires wrapping
do_rmt_single_shot::<150>(2, true).unwrap();
}
#[test]
fn rmt_single_shot_fails_without_end_marker() {
let result = do_rmt_single_shot::<20>(1, false);
assert!(matches!(result, Err(Error::EndMarkerMissing)));
}
#[test]
fn rmt_overlapping_ram_fails() {
let peripherals = esp_hal::init(esp_hal::Config::default());
let rmt = Rmt::new(peripherals.RMT, FREQ).unwrap();
let _ch0 = rmt
.channel0
.configure_tx(NoPin, TxChannelConfig::default().with_memsize(2))
.unwrap();
// Configuring channel 1 should fail, since channel 0 already uses its memory.
let ch1 = rmt.channel1.configure_tx(NoPin, TxChannelConfig::default());
assert!(matches!(ch1, Err(Error::MemoryBlockNotAvailable)));
}
#[test]
fn rmt_overlapping_ram_release() {
use esp_hal::rmt::TxChannelCreator;
let peripherals = esp_hal::init(esp_hal::Config::default());
let rmt = Rmt::new(peripherals.RMT, FREQ).unwrap();
let ch0 = rmt
.channel0
.configure_tx(NoPin, TxChannelConfig::default().with_memsize(2))
.unwrap();
// After dropping channel 0, the memory that it reserved should become available
// again such that channel 1 configuration succeeds.
core::mem::drop(ch0);
rmt.channel1
.configure_tx(NoPin, TxChannelConfig::default())
.unwrap();
}
macro_rules! test_channel_pair {
(
$peripherals:ident,
$tx_pin:ident,
$rx_pin:ident,
$tx_channel:ident,
$rx_channel:ident
) => {
// It's currently not possible to reborrow ChannelCreators and reconfigure channels, so
// we reconfigure the whole peripheral for each sub-test.
let rmt = Rmt::new($peripherals.RMT.reborrow(), FREQ).unwrap();
let tx_config = TxChannelConfig::default();
let rx_config = RxChannelConfig::default().with_idle_threshold(1000);
let tx_channel = rmt
.$tx_channel
.configure_tx($tx_pin.reborrow(), tx_config)
.unwrap();
let rx_channel = rmt
.$rx_channel
.configure_rx($rx_pin.reborrow(), rx_config)
.unwrap();
do_rmt_loopback_inner::<20>(tx_channel, rx_channel);
};
}
// A simple test that uses all channels: This is useful to verify that all channel/register
// indexing in the driver is correct. The other tests are prone to hide related issues due to
// using low channel numbers only (in particular 0 for the tx channel).
#[test]
fn rmt_use_all_channels() {
let mut p = esp_hal::init(esp_hal::Config::default());
let (mut rx, mut tx) = hil_test::common_test_pins!(p);
// FIXME: Find a way to implement these with less boilerplate and without a macro.
// Maybe use ChannelCreator::steal() (doesn't help right now because it uses a const
// generic channel number)?
// Chips with combined RxTx channels
#[cfg(esp32)]
{
test_channel_pair!(p, tx, rx, channel0, channel1);
test_channel_pair!(p, tx, rx, channel0, channel2);
test_channel_pair!(p, tx, rx, channel0, channel3);
test_channel_pair!(p, tx, rx, channel0, channel4);
test_channel_pair!(p, tx, rx, channel0, channel5);
test_channel_pair!(p, tx, rx, channel0, channel6);
test_channel_pair!(p, tx, rx, channel0, channel7);
test_channel_pair!(p, tx, rx, channel1, channel0);
test_channel_pair!(p, tx, rx, channel2, channel0);
test_channel_pair!(p, tx, rx, channel3, channel0);
test_channel_pair!(p, tx, rx, channel4, channel0);
test_channel_pair!(p, tx, rx, channel5, channel0);
test_channel_pair!(p, tx, rx, channel6, channel0);
test_channel_pair!(p, tx, rx, channel7, channel0);
}
#[cfg(esp32s2)]
{
test_channel_pair!(p, tx, rx, channel0, channel1);
test_channel_pair!(p, tx, rx, channel0, channel2);
test_channel_pair!(p, tx, rx, channel0, channel3);
test_channel_pair!(p, tx, rx, channel1, channel0);
test_channel_pair!(p, tx, rx, channel2, channel0);
test_channel_pair!(p, tx, rx, channel3, channel0);
}
// Chips with separate Rx and Tx channels
#[cfg(esp32s3)]
{
test_channel_pair!(p, tx, rx, channel0, channel4);
test_channel_pair!(p, tx, rx, channel1, channel5);
test_channel_pair!(p, tx, rx, channel2, channel6);
test_channel_pair!(p, tx, rx, channel3, channel7);
}
#[cfg(not(any(esp32, esp32s2, esp32s3)))]
{
test_channel_pair!(p, tx, rx, channel0, channel2);
test_channel_pair!(p, tx, rx, channel1, channel3);
}
}
}