Skip to content
Snippets Groups Projects
Commit 6196b9f1 authored by Per Lindgren's avatar Per Lindgren
Browse files

updated exercises

parent 801391e1
No related branches found
No related tags found
No related merge requests found
# 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.
......
......@@ -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.
---
......
......@@ -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.
......
//! 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.)
//! 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.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment