From 6196b9f10fdc3d10853980ed18af0baf173b6719 Mon Sep 17 00:00:00 2001 From: Per Lindgren <per.lindgren@ltu.se> Date: Sun, 7 Mar 2021 21:52:30 +0100 Subject: [PATCH] updated exercises --- CHANGELOG.md | 6 ++ README.md | 3 + examples/rtic_bare7.rs | 4 +- examples/rtic_bare8.rs | 185 ++++++++++++++++++++++++++++++++++ examples/rtic_bare9.rs | 222 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 examples/rtic_bare8.rs create mode 100644 examples/rtic_bare9.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c9738..fc5b8f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2021-03-07 + +- examples/rtic_bare7.rs, using embedded HAL. +- examples/rtic_bare8.rs, serial communication, bad design. +- examples/rtic_bare9.rs, serial communication, good design. + ## 2021-03-05 - examples/rtic_bare6.rs, setup and validate the clock tree. diff --git a/README.md b/README.md index 1e6f487..201bf3d 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ Bare metal programming: - `examples/rtic_bare4.rs`, in this exercise you will encounter a simple bare metal peripheral access API. The API is very unsafe and easy to misuse. - `examples/rtic_bare5.rs`, here you will write your own C-like peripheral access API. This API is much safer as you get control over bit-fields in a well defined way, thus less error prone. - `examples/rtic_bare6.rs`, in this exercise you learn about clock tree generation and validation. +- `examples/rtic_bare7.rs`, here you learn more on using embedded HAL abstractions and the use of generics. +- `examples/rtic_bare8.rs`, in this exercise you will setup serial communication to receive and send data. You will also see that polling may lead to data loss in a bad design. +- `examples/rtic_bare9.rs`, here you revisit serial communication and implement a good design in RTIC leveraging preemptive scheduling to ensure lossless communication. --- diff --git a/examples/rtic_bare7.rs b/examples/rtic_bare7.rs index cc7327c..0f2dea1 100644 --- a/examples/rtic_bare7.rs +++ b/examples/rtic_bare7.rs @@ -231,9 +231,9 @@ fn _toggleable_generic<E>(led: &mut dyn ToggleableOutputPin<Error = E>) { // way to implement it more efficiently. Remember, zero-cost is not without cost // just that it is as good as it possibly gets (you can't make it better by hand). // -// (You can also force the compiler to deduce the type at compile time, by using +// You can also force the compiler to deduce the type at compile time, by using // `impl` instead of `dyn`, if you are sure you don't want the compiler to -// "fallback" to dynamic dispatch.) +// "fallback" to dynamic dispatch. // // You might find Rust to have long compile times. Yes you are right, // and this type of deep analysis done in release mode is part of the story. diff --git a/examples/rtic_bare8.rs b/examples/rtic_bare8.rs new file mode 100644 index 0000000..b4b18ee --- /dev/null +++ b/examples/rtic_bare8.rs @@ -0,0 +1,185 @@ +//! bare8.rs +//! +//! Serial +//! +//! What it covers: +//! - serial communication +//! - bad design + +#![no_main] +#![no_std] + +use panic_rtt_target as _; + +use nb::block; + +use stm32f4xx_hal::{ + gpio::{gpioa::PA, Output, PushPull}, + prelude::*, + serial::{config::Config, Rx, Serial, Tx}, + stm32::USART2, +}; + +use rtic::app; +use rtt_target::{rprintln, rtt_init_print}; + +#[app(device = stm32f4xx_hal::stm32, peripherals = true)] +const APP: () = { + struct Resources { + // Late resources + TX: Tx<USART2>, + RX: Rx<USART2>, + } + + // init runs in an interrupt free section + #[init] + fn init(cx: init::Context) -> init::LateResources { + rtt_init_print!(); + rprintln!("init"); + + let device = cx.device; + + let rcc = device.RCC.constrain(); + + // 16 MHz (default, all clocks) + let clocks = rcc.cfgr.freeze(); + + let gpioa = device.GPIOA.split(); + + let tx = gpioa.pa2.into_alternate_af7(); + let rx = gpioa.pa3.into_alternate_af7(); + + let serial = Serial::usart2( + device.USART2, + (tx, rx), + Config::default().baudrate(115_200.bps()), + clocks, + ) + .unwrap(); + + // Separate out the sender and receiver of the serial port + let (tx, rx) = serial.split(); + + // Late resources + init::LateResources { TX: tx, RX: rx } + } + + // idle may be interrupted by other interrupts/tasks in the system + #[idle(resources = [RX, TX])] + fn idle(cx: idle::Context) -> ! { + let rx = cx.resources.RX; + let tx = cx.resources.TX; + + loop { + match block!(rx.read()) { + Ok(byte) => { + rprintln!("Ok {:?}", byte); + tx.write(byte).unwrap(); + } + Err(err) => { + rprintln!("Error {:?}", err); + } + } + } + } +}; + +// 0. Background +// +// The Nucleo st-link programmer provides a Virtual Com Port (VCP). +// It is connected to the PA2(TX)/PA3(RX) pins of the stm32f401/411. +// On the host, the VCP is presented under `/dev/ttyACMx`, where +// `x` is an enumerated number (ff 0 is busy it will pick 1, etc.) +// +// 1. In this example we use RTT. +// +// > cargo run --example rtic_bare8 +// +// Start a terminal program, e.g., `moserial`. +// Connect to the port +// +// Device /dev/ttyACM0 +// Baude Rate 115200 +// Data Bits 8 +// Stop Bits 1 +// Parity None +// +// This setting is typically abbreviated as 115200 8N1. +// +// Send a single character (byte), (set the option `No end` in `moserial`). +// Verify that sent bytes are echoed back, and that RTT tracing is working. +// +// Try sending "a", don't send the quotation marks, just a. +// +// What do you receive in `moserial`? +// +// ** your answer here ** +// +// What do you receive in the RTT terminal? +// +// ** your answer here ** +// +// Try sending: "abcd" as a single sequence, don't send the quotation marks, just abcd. +// +// What did you receive in `moserial`? +// +// ** your answer here ** +// +// What do you receive in the RTT terminal? +// +// ** your answer here ** +// +// What do you believe to be the problem? +// +// Hint: Look at the code in `idle` what does it do? +// +// ** your answer here ** +// +// Experiment a bit, what is the max length sequence you can receive without errors? +// +// ** your answer here ** +// +// Commit your answers (bare8_1) +// +// 2. Add a local variable `received` that counts the number of bytes received. +// Add a local variable `errors` that counts the number of errors. +// +// Adjust the RTT trace to print the added information inside the loop. +// +// Compile/run reconnect, and verify that it works as intended. +// +// Commit your development (bare8_2) +// +// 3. Experiment a bit, what is the max length sequence you can receive without errors? +// +// ** your answer here ** +// +// How did the added tracing/instrumentation affect the behavior? +// +// ** your answer here ** +// +// Commit your answer (bare8_3) +// +// 4. Now try compile and run the same experiment 3 but in --release mode. +// +// > cargo run --example rtic_bare8 --release +// +// Reconnect your `moserial` terminal. +// +// Experiment a bit, what is the max length sequence you can receive without errors? +// +// ** your answer here ** +// +// Commit your answer (bare8_4) +// +// 5. Discussion +// +// (If you ever used Arduino, you might feel at home with the `loop` and poll design.) +// +// Typically, this is what you can expect from a polling approach, if you +// are not very careful what you are doing. This exemplifies a bad design. +// +// Loss of data might be Ok for some applications but this typically NOT what we want. +// +// (With that said, Arduino gets away with some simple examples as their drivers do +// internal magic - buffering data etc.) diff --git a/examples/rtic_bare9.rs b/examples/rtic_bare9.rs new file mode 100644 index 0000000..a1fc61c --- /dev/null +++ b/examples/rtic_bare9.rs @@ -0,0 +1,222 @@ +//! bare8.rs +//! +//! Serial +//! +//! What it covers: + +#![no_main] +#![no_std] + +use panic_rtt_target as _; + +use stm32f4xx_hal::{ + prelude::*, + serial::{config::Config, Event, Rx, Serial, Tx}, + stm32::USART2, +}; + +use rtic::app; +use rtt_target::{rprintln, rtt_init_print}; + +#[app(device = stm32f4xx_hal::stm32, peripherals = true)] +const APP: () = { + struct Resources { + // Late resources + TX: Tx<USART2>, + RX: Rx<USART2>, + } + + // init runs in an interrupt free section + #[init] + fn init(cx: init::Context) -> init::LateResources { + rtt_init_print!(); + rprintln!("init"); + + let device = cx.device; + + let rcc = device.RCC.constrain(); + + // 16 MHz (default, all clocks) + let clocks = rcc.cfgr.freeze(); + + let gpioa = device.GPIOA.split(); + + let tx = gpioa.pa2.into_alternate_af7(); + let rx = gpioa.pa3.into_alternate_af7(); + + let mut serial = Serial::usart2( + device.USART2, + (tx, rx), + Config::default().baudrate(115_200.bps()), + clocks, + ) + .unwrap(); + + // generate interrupt on Rxne + serial.listen(Event::Rxne); + + // Separate out the sender and receiver of the serial port + let (tx, rx) = serial.split(); + + // Late resources + init::LateResources { TX: tx, RX: rx } + } + + // idle may be interrupted by other interrupts/tasks in the system + #[idle()] + fn idle(_cx: idle::Context) -> ! { + loop { + continue; + } + } + + // capacity sets the size of the input buffer (# outstanding messages) + #[task(resources = [TX], priority = 1, capacity = 128)] + fn rx(cx: rx::Context, data: u8) { + let tx = cx.resources.TX; + tx.write(data).unwrap(); + rprintln!("data {}", data); + } + + // Task bound to the USART2 interrupt. + #[task(binds = USART2, priority = 2, resources = [RX], spawn = [rx])] + fn usart2(cx: usart2::Context) { + let rx = cx.resources.RX; + let data = rx.read().unwrap(); + cx.spawn.rx(data).unwrap(); + } + + extern "C" { + fn EXTI0(); + } +}; + +// 0. Background +// +// As seen in the prior example, you may loose data unless polling frequently enough. +// Let's try an interrupt driven approach instead. +// +// In init we just add: +// +// // generate interrupt on Rxne +// serial.listen(Event::Rxne); +// +// This causes the USART hardware to generate an interrupt when data is available. +// +// // Task bound to the USART2 interrupt. +// #[task(binds = USART2, priority = 2, resources = [RX], spawn = [rx])] +// fn usart2(cx: usart2::Context) { +// let rx = cx.resources.RX; +// let data = rx.read().unwrap(); +// cx.spawn.rx(data).unwrap(); +// } +// +// The `usart2` task will be triggered, and we read one byte from the internal +// buffer in the USART2 hardware. (panic if something goes bad) +// +// We send the read byte to the `rx` task (by `cx.spawn.rx(data).unwrap();`) +// (We panic if the capacity of the message queue is reached) +// +// // capacity sets the size of the input buffer (# outstanding messages) +// #[task(resources = [TX], priority = 1, capacity = 128)] +// fn rx(cx: rx::Context, data: u8) { +// let tx = cx.resources.TX; +// tx.write(data).unwrap(); +// rprintln!("data {}", data); +// } +// +// Here we echo the data back, `tx.write(data).unwrap();` (panic if usart is busy) +// We then trace the received data `rprintln!("data {}", data);` +// +// The `priority = 2` gives the `usart2` task the highest priority +// (to ensure that we don't miss data). +// +// The `priority = 1` gives the `rx` task a lower priority. +// Here we can take our time and process the data. +// +// `idle` runs at priority 0, lowest priority in the system. +// Here we can do some background job, when nothing urgent is happening. +// +// This is an example of a good design! +// +// 1. In this example we use RTT. +// +// > cargo run --example rtic_bare9 +// +// Try breaking it!!!! +// Throw any data at it, and see if you could make it panic! +// +// Were you able to crash it? +// +// ** your answer here ** +// +// Notice, the input tracing in `moserial` seems broken, and may loose data. +// So don't be alarmed if data is missing, its a GUI tool after all. +// +// If you want to sniff the `ttyACM0`, install e.g., `interceptty` and run +// > interceptty /dev/ttyACM0 +// +// In another terminal, you can do: +// > cat examples/rtic_bare9.rs > /dev/ttyACM0 +// +// Incoming data will be intercepted/displayed by `interceptty`. +// (In the RTT trace you will see that data is indeed arriving to the target.) +// +// Commit your answer (bare9_1) +// +// 2. Now, re-implement the received and error counters from previous exercise. +// +// Good design: +// - Defer any tracing to lower priority task/tasks +// (you may introduce an error task at low priority). +// +// - State variables can be introduced either locally (static mut), or +// by using a resource. +// +// If a resource is shared among tasks of different priorities: +// The highest priority task will have direct access to the data, +// the lower priority task(s) will need to lock the resource first. +// +// Check the RTIC book, https://rtic.rs/0.5/book/en/by-example +// regarding resources, software tasks, error handling etc. +// +// Test that your implementation works and traces number of +// bytes received and errors encountered. +// +// If implemented correctly, it should be very hard (or impossible) +// to get an error. +// +// You can force an error by doing some "stupid delay" (faking workload), +// e.g., burning clock cycles using `cortex_m::asm::delay` in the +// `rx` task. Still you need to saturate the capacity (128 bytes). +// +// To make errors easier to produce, reduce the capacity. +// +// Once finished, comment your code. +// +// Commit your code (bare9_2) +// +// 3. Discussion +// +// Here you have used RTIC to implement a highly efficient and good design. +// +// Tasks in RTIC are run-to-end, with non-blocking access to resources. +// (Even `lock` is non-blocking, isn't that sweet?) +// +// Tasks in RTIC are scheduled according to priorities. +// (A higher priority task `H` always preempts lower priority task `L` running, +// unless `L` holds a resource with higher or equal ceiling as `H`.) +// +// Tasks in RTIC can spawn other tasks. +// (`capacity` sets the message queue size.) +// +// By design RTIC guarantees race- and deadlock-free execution. +// +// It also comes with theoretical underpinning for static analysis. +// - task response time +// - overall schedulability +// - stack memory analysis +// - etc. +// +// RTIC leverages on the zero-cost abstractions in Rust, +// and the implementation offers best in class performance. -- GitLab