I'm trying to write a driver for an infrared receiver to be used by an STM32[H7], and I'm using embassy to do so.
It's my first real foray into embedded rust, and I've managed to write a couple of implementations directly using an InputCapture. So far, I've just been using plain interrupts to retrieve the timestamps for the waveform edges (i.e. using wait_for_{x}_edge async methods), but I'd like to try going one step further and making using of DMA.
InputCapture does provide a receive_waveform method, which is almost what I want, but not quite.
The specific algorithm I want to implement is:
- wait for a falling edge (the start of a waveform) and get the timestamp
- immediately start a DMA read to capture the timestamps of subsequent edges
- end receipt of the waveform when either:
- a timeout completes
- the DMA buffer is filled up
The waveform I'm capturing is the Philips RC-5 protocol and is always 14 bits long, where each bit is (according to the spec) 1.778ms in duration. Due to the way the bits are encoded, different signals can have different numbers of edges, but they will always complete in the given time frame. In a different project on a different microcontroller, I implemented a level-based, rather than this edge-based, algorithm, but I have found different remote controls to be way out of spec, and timestamping edges is much more forgiving.
My very gnarly code looks something like this:
pub struct IRReceiver<'d, T: GeneralInstance4Channel> {
ic: InputCapture<'d, T>,
dma_req: u8,
dma_channel: dma::Channel<'d>,
timer_addr: *mut u16,
}
impl<'d, T: GeneralInstance4Channel> IRReceiver<'d, T> {
pub fn new<D>(
mut tim: Peri<'d, T>,
pin: CaptureInput<'d, T, Ch1>,
dma: Peri<'d, D>,
irq: impl Binding<T::CaptureCompareInterrupt, CaptureCompareInterruptHandler<T>>
+ Binding<D::Interrupt, dma::InterruptHandler<D>>
+ 'd,
) -> Self
where
D: Dma<T, Ch1>,
{
let timer_addr = {
let timer = low_level::Timer::new(tim.reborrow());
// to start the DMA in `Self::capture_one`, I need to do these things with
// the actual `Timer` before losing it when I pass it to `InputCapture`.
// I just hope that `InputCapture` doesn't clobber these values...
timer.set_cc_dma_enable_state(Channel::Ch1, true);
timer.regs_gp16().ccr(Channel::Ch1.index()).as_ptr() as *mut u16
};
let mut ic = InputCapture::new(
tim,
Some(pin),
None,
None,
None,
irq,
time::mhz(1),
Default::default(),
);
// in the following scope, I do a bit of set up of the input capture
// channel, which I've hoisted from `InputCapture::receive_waveform()`
{
let mut ch = ic.ch1();
ch.enable();
ch.set_input_capture_selection(InputCaptureSelection::Normal);
ch.set_input_capture_mode(InputCaptureMode::BothEdges);
}
// again, need to save this information for when we do the DMA read
let dma_req = dma.request();
// create the DMA Channel here... mostly because it requires `irq`.
// Unlike `InputCapture::request_waveform``, I want
// `Self::capture_one()` to _not_ need to take a `Dma` and
// `irq: impl Binding...` every time it's called; I only want the
// caller to have to pass those things once to `Self::new`
let dma_channel = dma::Channel::new(dma, irq);
Self {
ic,
dma_req,
dma_channel,
timer_addr,
}
}
pub async fn capture_one(&mut self) {
// create a buffer big enough to hold the worst-case number of edge timestamps
let mut buf = heapless::Vec::<u16, MAX_EDGES>::from_array([0; MAX_EDGES]);
// the first thing to do is wait for a falling edge, which signals the start
// of a waveform (well, it's possible we are halfway through a waveform
// already, but it will get parsed as invalid which isn't a problem)
let first_timestamp = self.ic.ch1().wait_for_falling_edge().await;
// create the DMA transfer (I have yet to receive my STM32 eval board, so I
// haven't tested this, but I _think_ the DMA transfer begins immediately in
// the background without needing to .await it. Notably, I want to access
// the `Transfer<'_>` _after_ the timeout to find out how many edges we actually
// received).
let mut tx: dma::Transfer<'_> = unsafe {
self.dma_channel.read(
self.dma_req,
self.timer_addr,
&mut buf,
dma::TransferOptions::default(),
)
};
// wait a bit longer than the spec says, to allow for remote controls
// which are slower than the spec
embassy_time::Timer::after(embassy_time::Duration::from_millis(28)).await;
// the above feels a bit race condition-y. The actual behaviour I want is
// to start the DMA, and for the timeout to be triggered to start after
// the first edge is received.
// I need to get the number of edge timestamps so that:
// - I can work out if too many edges were received, and thus the
// waveform was not correct
// - I know how many entries in the buffer are valid timestamps
// because I can't guarantee that I can discriminate between a
// placeholder value (0, in this case) and a real timestamp
tx.request_reset();
let n_timestamps = MAX_EDGES as u32 - tx.get_remaining_transfers();
info!("n_timestamps: {}", n_timestamps);
// at this point I can parse the edge timestamps to return e.g. Result<
// IRData { toggle_bit: u8, address: u8, command: u8},
// CaptureError // enum { InvalidStartBits, InsufficientEdges, TooManyEdges}
// >
}
}
I've added comments with my thoughts... it all seems quite messy. I would be grateful if anyone with more experience of embassy could offer some thoughts on how I can tidy this up.