diff --git a/examples/loopback.rs b/examples/loopback.rs index 48872eec184a19e51a3cdab2e1c5743cd7d73412..e820129a7e3c772568aaa8a36f5e3fda35deed90 100644 --- a/examples/loopback.rs +++ b/examples/loopback.rs @@ -115,7 +115,7 @@ fn usart2_handler(_t: &mut Threshold, r: USART2::Resources) { match r.SEND.dequeue() { Some(b) => { // we (still) have something in the queue to send - let _ = serial.write(b); + let _ = serial.write(b); // will clear Txe bit } _ => { // the que was empty so the last item was already sent @@ -152,12 +152,12 @@ fn command( _ => {} } - // here we assume that + // re-enable the USART2 Txe to trigger the USART2 handler USART2.claim(t, |usart, _| { Serial(&**usart).listen(Event::Txe); }); // trigger the usart to send queued data - rtfm::set_pending(f4::stm32f40x::Interrupt::USART2); + // rtfm::set_pending(f4::stm32f40x::Interrupt::USART2); } fn error_receive_buffer_full() { @@ -172,30 +172,251 @@ fn rxne_not_set() { ip!("-"); } -#[derive(Debug)] -enum Command { - Start, - Stop, - Freq(u32), -} +// loopback example +// NOTICE, this example is made to show that a seemingly trivial +// function, like an interrutp driven loopback faces you with +// non-trivial concurrency/real-time challenges +// +// the setup is the following +// SEND/RECEIVE buffers are circular, each holding both +// producer index and a consumer index +// +// only when a full frame (ending with a Carrige return or Newline) +// has been received, the command handler is notified (from the USART) +// interrupt handler. +// +// to stress the problems the RECEIVE buffer is larger than the +// SEND buffer, hence we can run in the problem that +// receiving (at full speed), and trying to loop that back +// will overflow the SEND buffer +// +// initially we enqueque "abc", and trigger the USART2 interrupt +// +// let _ = r.SEND.enqueue('a' as u8); +// let _ = r.SEND.enqueue('b' as u8); +// let _ = r.SEND.enqueue('c' as u8); +// rtfm::set_pending(f4::stm32f40x::Interrupt::USART2); // trigger USART2 task +// +// the hardware provides only a single point entry for a USART2 interrupt +// initially enabled by +// +// serial.listen(Event::Rxne); // data received interrupt +// serial.listen(Event::Txe); // data transmitted interrupt +// +// in the USART2 handler we have to determine by reading the RXNE if data has arrived +// +// if sr.rxne().bit_is_set() +// +// in this case not, so we check if we have data to send +// +// match r.SEND.dequeue() +// +// in this case we have (the 'a' of "abc", so a write will occur) +// +// serial.write(b); // will clear Txe bit +// +// +// the interrupt handler will exit (with the Txe bit cleared), +// and the processor will be put to sleep (in idle) +// +// when the 'a' has been transmitted, we get another USART2 interrupt (Txe bit set) +// the above procedure is repeated, and we will write a 'b' +// +// again we get an interrupt and this time write the 'c' +// +// and when 'c' has been written we get a new USART2 interrupt (Txe bit set). +// +// here we have the case of the SEND buffer being empty and match +// +// serial.unlisten(Event::Txe); +// +// this is needed in order to avoid that the USART2 interrupt handler is +// re-entered. (Still the Txe bit will be Set at this point) +// the only way to clear the Txe is by writing and we have nothing to write +// so the only option here is to "unlisten" to it.... +// already here we see that we need to fully understand the HARDWARE in order +// to implement correct software, so Read the Manual +// +// you should now see "abc" in the moserial console +// in the swo trace you should have something like +// +// [2018-02-23T10:28:30.735Z] init +// [2018-02-23T10:28:30.735Z] -- +// +// the two (2) dashes `-` are due to the triggering of the EXTI4 +// why not three (3), we have triggered it three (3), times when writing +// `a`, `b` and `c`. Well the hardware has a single buffer for each interrupt +// so if the interrupt was already pended (but not yet executed) +// the events are *coalesced* (if we need to handle an event without coalescing +// we need to implement this by a sofware overlay/buffer) +// +// and this was the easy part.... +// +// now lets give an input from `moserial`. Set it to generate CR end. +// enter `1234567<return>` +// +// in the trace output you should get something like... +// +// [2018-02-23T10:41:54.644Z] command +// [2018-02-23T10:41:54.645Z] SEND BufferFullError +// [2018-02-23T10:41:54.646Z] error_receive_buffer_full +// [2018-02-23T10:41:54.652Z] error_receive_buffer_full +// +// and in `moserial` you should get +// +// abc123 +// +// there is much in play, let's try to break it down +// a USART2 Rxne interrupt was initally generated, and we enter the +// interrupt handler, and detect this +// +// if sr.rxne().bit_is_set() { +// // interrupt due to data received +// match serial.read() { +// Ok(b) => { +// if b == '\n' as u8 || b == '\r' as u8 { +// // we have received a `return`, trigger command to process it +// rtfm::set_pending(f4::stm32f40x::Interrupt::EXTI1); +// } else { +// match r.RECEIVE.enqueue(b) { +// Err(_) => { +// // trigger error_handler on buffer overflow +// rtfm::set_pending(f4::stm32f40x::Interrupt::EXTI2); +// } +// _ => {} +// } +// } +// } +// +// here the serial read was successful (Some('1')), its not a Carrige return so we enque on the BUFFER +// as a side effect of serial.read(), the Rxne bit was cleared and we exit the interrupt handler +// and sleep until the next byte arrives (`2`). +// +// this is repeated (wake, enque, sleep) until the buffer is full (that happens before we +// receive the carrige return) +// +// when the buffer is full we trigger EXTI2 (that makes a trace output of the error) +// the buffer is of lenght 6, but the implementation allows only N-1 elements +// so both the `6` and `7` are dropped, and we produce two error events. +// in this case they were not coalesced (both printed) +// +// we will recieve the carrige return, and and pend the `command` (EXTI1), i.e., our listener. +// +// the `command` interrupt handler +// { +// ipln!("command"); +// match SEND.claim_mut(t, |send, t1| { +// RECEIVE.claim_mut(t1, |receive, _| { +// let mut err = Ok(()); +// while let Some(e) = receive.dequeue() { +// err = send.enqueue(e); +// } +// err +// }) +// }) { +// Err(err) => { +// // error handling outside the critical section +// ipln!("SEND {:?}", err); +// } +// _ => {} +// } +// +// // re-enable Txe +// USART2.claim(t, |usart, _| { +// Serial(&**usart).listen(Event::Txe); +// }); +// // trigger the usart to send queued data +// rtfm::set_pending(f4::stm32f40x::Interrupt::USART2); +// } +// we trace `command` and claim both SEND and RECEIVE +// in the critical section we copy the RECIEVE buffer elements to the SEND buffer +// (this is actually the *loopback* behavior, we could also copy to another local buffer +// to do further parsing or simiar) +// +// here its important that we keep the crical section as short as possible +// so we just copy nothing else... since we hold a shared resource we will block +// the USART2 from executing, and may loose incoming data +// +// we also do error handling here if the send buffer is full we will return from +// the claim(s) withe an error (else with an Ok(())), +// result is matched +// { +// Err(err) => { +// // error handling outside the critical section +// ipln!("SEND {:?}", err); +// } +// _ => {} +// rendering a `SEND BufferFullError` (or nonthig) +// +// finally we need to re-enable the USART2 Txe, so that interrupts due to USART2 writes +// will cause USART2 interrupts +// +// // re-enable Txe +// USART2.claim(t, |usart, _| { +// Serial(&**usart).listen(Event::Txe); +// }); +// +// recall that at an earlier point, we left a Txe interrupt condition hanging, +// and filtered it out by *unlisten* +// +// alternative we could add a triggering event +// rtfm::set_pending(f4::stm32f40x::Interrupt::USART2); +// +// but it is not needed for this use case +// +// so what DO this implementatin promise +// +// 1. graceful handling of RECIEVE buffer overflow +// 2. graceful handling of SEND buffer overflow +// 3. no data loss, (e.g., we never miss detecting the carrige return) +// (you can stress this by entering a long input) +// oahuesthueoasnthueonthueoantueoaueoasnthueoasnthueoasnueoasnthueoasnthueoa +// the output should be some errors, in the trace log but the `moserial` should receive +// `oah`, i.e. the firs three bytes +// +// what it DOES NOT promise +// handling of hardware level errors (framing, etc.), this could be added if needed +// +// of course you need to adjust the SEND/RECEIVE buffer lenghts to the requirements +// of the application +// +// so lessons learned +// 1. seemingly trivial problems may be harder than expected +// 2. Reading the Manual(s) are required to write correct low level code +// 3. RTFM provides a means to implment correct low level code +// +// On a side note, it took me quite some time to implment, debug and comment +// the example, so be prepared, writing correct low level code is HARD +// +// On the possitive side, the safety enforced by Rust RTFM +// makes our lifes much easier, as we don't need to worry about +// - race conditions, on the shared data structures +// - deadlocks, for the reasource managenent +// +// As a comparison, in RTFM we write the low level code INSIDE the model of computation +// hence we get the luxyry of letting RTFM manage the resource management +// In a threaded environment (e.g. FreeRTOS), the driver (interrupt handling etc.) would +// be largely OUTSIDE the threaded model of computation, so correctness would +// be up to you.... +// +// In the arduino world there is no protection mechanisms at all, what you write is what you get: +// hence rubustness can never be argued else than for the specic application at hand. +// +// The outsets are very different, in the FreeRTOS/arduino word we are typically happy to see things +// working, without concerns of roubustness (simly put robustness can never be argued, unless you +// somehow make a proof over the complete code base). +// +// Using RTFM, we can establish critera for correctness, and from there argue robustness etc. +// (The theorecical task/resource underpinning, gives us a solid fundament for reasoning) +// +// If one would want to prove correctness for the above implementation the way forward is +// 1 Derive the possible sates of the system. *regarding both HW and software* +// 2 Formulate invariants for each state. +// 3 Show that each possible state transition upholds the invariants. +// +// We have not gone the length of doing so for this application.. but its indeed possible but +// takes a lot of effort. + + -fn parse(s: &str) -> Result<Command, &str> { - let mut iter = s.split(' ').filter(|c| !(c == &"")); - match iter.next() { - Some("Stop") => Ok(Command::Stop), - Some("Start") => Ok(Command::Start), - Some("Freq") => match iter.next() { - Some(fs) => { - if let Ok(f) = fs.parse::<u32>() { - Ok(Command::Freq(f)) - } else { - Err("Invalid frequency") - } - } - None => Err("No frequency"), - }, - Some(_) => Err("Invalid command"), - None => Err("No input"), - } -}