Bug description
TxChannelDriver::send_and_wait unconditionally wraps its encoder argument in EncoderWrapper via into_raw(). For built-in RawEncoder types like BytesEncoder, this double-wraps: EncoderWrapper allocates a new Rust-side rmt_encoder_t around an encoder that already holds a C-allocated rmt_encoder_t from ESP-IDF (e.g. rmt_new_bytes_encoder). When this double-wrapped handle reaches rmt_transmit, ESP-IDF doesn't recognize it and aborts in lock_acquire_generic.
The same issue affects CopyEncoder and SimpleEncoder — any type that implements RawEncoder.
Call chain:
send_and_wait accepts E: Encoder, forwards to send_iter
send_iter calls into_raw(encoder) on every encoder
into_raw unconditionally wraps in EncoderWrapper
BytesEncoder satisfies E: Encoder via the blanket impl impl<E: RawEncoder> Encoder for E, so into_raw wraps it in a second rmt_encoder_t.
Workaround: Use start_send + wait_all_done instead, which passes the encoder's C handle directly:
unsafe {
channel.start_send(&mut encoder, &data, &TransmitConfig::default())?;
}
channel.wait_all_done(Some(Duration::from_millis(1000)))?;
- Would you like to work on a fix? [n]
To Reproduce
use esp_idf_svc::hal::rmt::config::{TransmitConfig, TxChannelConfig};
use esp_idf_svc::hal::rmt::encoder::{BytesEncoder, BytesEncoderConfig};
use esp_idf_svc::hal::rmt::{PinState, Symbol, TxChannelDriver};
use esp_idf_svc::hal::units::FromValueType;
use core::time::Duration;
fn main() -> anyhow::Result<()> {
let peripherals = esp_idf_svc::hal::peripherals::Peripherals::take()?;
let tx_config = TxChannelConfig {
resolution: 10_000_000u32.Hz(),
..Default::default()
};
let mut channel = TxChannelDriver::new(peripherals.pins.gpio2, &tx_config)?;
let resolution = 10_000_000u32.Hz();
let bit0 = Symbol::new_with(resolution, PinState::High, Duration::from_nanos(400), PinState::Low, Duration::from_nanos(850))?;
let bit1 = Symbol::new_with(resolution, PinState::High, Duration::from_nanos(800), PinState::Low, Duration::from_nanos(450))?;
let config = BytesEncoderConfig { bit0, bit1, msb_first: true, ..Default::default() };
let mut encoder = BytesEncoder::with_config(&config)?;
let data: Vec<u8> = vec![0xFF; 384];
// Aborts with panic in lock_acquire_generic:
channel.send_and_wait(encoder, &data, &TransmitConfig::default())?;
Ok(())
}
Reproduced on master (b8820cfa) — the same into_raw wrapping logic is present.
Expected behavior
send_and_wait should transmit the data successfully, the same way start_send + wait_all_done does when called directly with a BytesEncoder.
Environment
- Crate (
esp-idf-hal) version: 0.46.2 (also reproduced on master b8820cfa)
- ESP-IDF branch or tag: v5.5.3
- Target device (MCU): esp32s3
- OS: macOS (Apple Silicon)
Bug description
TxChannelDriver::send_and_waitunconditionally wraps its encoder argument inEncoderWrapperviainto_raw(). For built-inRawEncodertypes likeBytesEncoder, this double-wraps:EncoderWrapperallocates a new Rust-sidermt_encoder_taround an encoder that already holds a C-allocatedrmt_encoder_tfrom ESP-IDF (e.g.rmt_new_bytes_encoder). When this double-wrapped handle reachesrmt_transmit, ESP-IDF doesn't recognize it and aborts inlock_acquire_generic.The same issue affects
CopyEncoderandSimpleEncoder— any type that implementsRawEncoder.Call chain:
send_and_waitacceptsE: Encoder, forwards tosend_itersend_itercallsinto_raw(encoder)on every encoderinto_rawunconditionally wraps inEncoderWrapperBytesEncodersatisfiesE: Encodervia the blanket implimpl<E: RawEncoder> Encoder for E, sointo_rawwraps it in a secondrmt_encoder_t.Workaround: Use
start_send+wait_all_doneinstead, which passes the encoder's C handle directly:To Reproduce
Reproduced on
master(b8820cfa) — the sameinto_rawwrapping logic is present.Expected behavior
send_and_waitshould transmit the data successfully, the same waystart_send+wait_all_donedoes when called directly with aBytesEncoder.Environment
esp-idf-hal) version: 0.46.2 (also reproduced on masterb8820cfa)