Skip to content

Commit

Permalink
add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
kelko committed Oct 2, 2024
1 parent 350c87e commit 56a69d2
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 39 deletions.
4 changes: 4 additions & 0 deletions src/keret-controller-transmit/src/postcard_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::fmt::{Debug, Display, Formatter};

/// compatibility wrapper until core::error is used everywhere
#[repr(transparent)]
pub struct PostcardError(postcard::Error);

impl PostcardError {
Expand All @@ -9,16 +10,19 @@ impl PostcardError {
}
}

// Debug trait is used on errors to generate developer targeted information. Required by snafu::Error
impl Debug for PostcardError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
Debug::fmt(&self.0, f)
}
}

// Display trait is used on errors to generate the error message itself. Required by snafu::Error
impl Display for PostcardError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
Debug::fmt(&self.0, f)
}
}

// mark PostcardError as compatible to snafu::Error trait
impl snafu::Error for PostcardError {}
12 changes: 7 additions & 5 deletions src/keret-controller/src/controls.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use microbit::{board::Buttons, hal::gpiote::Gpiote, pac};

/// enum to indicate the users desired interaction
/// which is calculated by which button was pressed
#[derive(Debug, Copy, Clone, Default)]
pub(crate) enum InteractionRequest {
#[default]
Expand All @@ -8,12 +10,14 @@ pub(crate) enum InteractionRequest {
Reset,
}

/// reading and interpreting the button presses to calculate requested interaction
pub(crate) struct InputControls {
gpiote: Gpiote,
request: InteractionRequest,
}

impl InputControls {
/// create a new instance, configured to handle both buttons
pub(crate) fn new(board_gpiote: pac::GPIOTE, board_buttons: Buttons) -> Self {
let gpiote = Gpiote::new(board_gpiote);

Expand All @@ -31,17 +35,13 @@ impl InputControls {
.enable_interrupt();
channel1.reset_events();

unsafe {
pac::NVIC::unmask(pac::Interrupt::GPIOTE);
}
pac::NVIC::unpend(pac::Interrupt::GPIOTE);

Self {
gpiote,
request: InteractionRequest::None,
}
}

/// return the last requested interaction and set it next to `None`
#[inline(always)]
pub(crate) fn get_requested_interaction(&mut self) -> InteractionRequest {
let current = self.request;
Expand All @@ -50,6 +50,8 @@ impl InputControls {
current
}

/// check the button channels to see which button was pressed and
/// calculate the next interaction request, reset the buttons afterward
pub(crate) fn check_input(&mut self) {
let a_pressed = self.gpiote.channel0().is_event_triggered();
let b_pressed = self.gpiote.channel1().is_event_triggered();
Expand Down
6 changes: 3 additions & 3 deletions src/keret-controller/src/display.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
use microbit::{
display::nonblocking::Display as NonblockDisplay, gpio::DisplayPins, hal::timer::Instance, pac,
display::nonblocking::Display as NonblockDisplay, gpio::DisplayPins, hal::timer::Instance,
};
use tiny_led_matrix::Render;

/// convenience abstraction of the BSP display module
#[repr(transparent)]
pub(crate) struct Display<T: Instance> {
inner: NonblockDisplay<T>,
}

impl<T: Instance> Display<T> {
/// create a new instance, configuring the underlying Display element and unlocking the timer
pub(crate) fn new(board_timer: T, board_display: DisplayPins) -> Self {
let display = NonblockDisplay::new(board_timer, board_display);

unsafe { pac::NVIC::unmask(pac::Interrupt::TIMER1) }

Self { inner: display }
}

Expand Down
7 changes: 6 additions & 1 deletion src/keret-controller/src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use microbit::hal::uarte::Instance;

type Instant = u64;

/// current state of the application logic (the "domain")
#[derive(Debug, Copy, Clone, Default, PartialEq)]
pub(crate) enum AppMode {
#[default]
Expand All @@ -17,6 +18,7 @@ pub(crate) enum AppMode {
}

impl AppMode {
/// check what interaction the user requested to perform and calculate next state from that
pub(crate) fn handle_interaction_request<T: Instance>(
&self,
request: InteractionRequest,
Expand All @@ -36,8 +38,10 @@ impl AppMode {
}
}

/// user hit right button -> toggle between idle & running if possible
/// sending the report over the serial bus if necessary
#[inline(always)]
pub(crate) fn toggle_mode<T: Instance>(
fn toggle_mode<T: Instance>(
&self,
serial_bus: &mut SerialBus<T>,
timestamp: u64,
Expand All @@ -49,6 +53,7 @@ impl AppMode {
}
}

/// user ended the timer, calculate duration and send it over the wire
fn finish_report<T: Instance>(
&self,
serial_bus: &mut SerialBus<T>,
Expand Down
8 changes: 8 additions & 0 deletions src/keret-controller/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use rtt_target::rprintln;
use snafu::{Error as _, Snafu};

/// compatibility wrapper until core::error is used everywhere
#[repr(transparent)]
pub struct UarteError(microbit::hal::uarte::Error);

impl UarteError {
Expand All @@ -11,20 +12,26 @@ impl UarteError {
}
}

// Debug trait is used on errors to generate developer targeted information. Required by snafu::Error
impl Debug for UarteError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
Debug::fmt(&self.0, f)
}
}

// Display trait is used on errors to generate the error message itself. Required by snafu::Error
impl Display for UarteError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
Debug::fmt(&self.0, f)
}
}

// mark UarteError as compatible to snafu::Error trait
impl snafu::Error for UarteError {}

/// all errors which are generated inside this crate
/// as enum, so handling code can easily match over it (if necessary)
/// using snafu crate to auto-generate user-facing message which are sent over rtt
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub(crate) enum Error {
Expand All @@ -47,6 +54,7 @@ pub(crate) enum Error {
NoControls,
}

/// send details of a top-level error over the rtt
pub(crate) fn report_error(err: Error) {
rprintln!("[ERROR] {}", err);
let mut source = err.source();
Expand Down
97 changes: 74 additions & 23 deletions src/keret-controller/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#![no_main]
#![no_std]
//⬆️ this code runs directly (bare-metal) on a microcontroller
// not in a typical OS situation with a kernel
// so the "main" method is not indicator for code entry, but below you will find #[entry]
// also as there is no OS the Rust std lib can't be used, as it depends on libc/musl/something similar

// the "modules" of this app (think "package"/"namespace") in other languages
mod controls;
mod display;
mod domain;
Expand All @@ -9,15 +14,14 @@ mod render;
mod serialize;
mod time;

use rtt_target::rtt_init_print;

use crate::error::report_error;
use crate::render::FATAL_SPRITE;
// importing elements (modules, structs, traits, ...) from other modules to be used in this file
use crate::{
controls::{InputControls, InteractionRequest},
display::Display,
domain::AppMode,
error::report_error,
error::{Error, NoControlsSnafu},
render::FATAL_SPRITE,
serialize::SerialBus,
time::RunningTimer,
};
Expand All @@ -32,24 +36,43 @@ use microbit::{
hal::timer::{Instance as TimerInstance, Periodic},
hal::uarte::Instance as UarteInstance,
hal::Timer,
pac::{interrupt, RTC1, TIMER0, TIMER1, UARTE0},
pac::{interrupt, Interrupt, NVIC, RTC1, TIMER0, TIMER1, UARTE0},
};
use panic_rtt_target as _;
use rtt_target::rtt_init_print;

// the following three variables are static, as they need to be accessed
// by the main running code but also by the interrupts
// as both could happen concurrently they are wrapped in a `Mutex` allowing only one
// concurrent access at a time (using Cortex hardware features)
// and in a `RefCell` so we can call mutable methods on it (so-called "inner mutability")

/// the primary timer used to calculate the running time
static RUNNING_TIMER: Mutex<RefCell<Option<RunningTimer<RTC1>>>> = Mutex::new(RefCell::new(None));

/// the display to show something on the LED matrix
static DISPLAY: Mutex<RefCell<Option<Display<TIMER1>>>> = Mutex::new(RefCell::new(None));

/// wrapper for input controls handling
static CONTROLS: Mutex<RefCell<Option<InputControls>>> = Mutex::new(RefCell::new(None));

/// entry point for the application. Could have any name, `main` used to follow convention from C
/// Initializes the controller as well as go into the execution loop. This method should never return
/// as it drives the whole microcontroller
#[entry]
fn main() -> ! {
// rtt -> send debug information via USB to attached debugging tool
rtt_init_print!();

// get the main "Board" instance from the HAL to access the hardware features
let Some(board) = Board::take() else {
panic!("Could not initialize board. Nothing left to do.");
};

let mut mode = AppMode::Idle;
let (mut serial_bus, mut main_loop_timer) = initialize_board(board);

// main execution loop, should never end
loop {
mode = next_cycle(&mode, &mut serial_bus).unwrap_or_else(handle_runtime_error);
show_mode(&mode);
Expand All @@ -58,17 +81,7 @@ fn main() -> ! {
}
}

fn show_mode(mode: &AppMode) {
free(|cs| {
let mut display = DISPLAY.borrow(cs).borrow_mut();
let display = display
.as_mut()
.expect("Display must be set at this point. Need restart");

display.display_image(mode);
});
}

/// initialize the board, creating all helper objects and put those necessary in the Mutexes
fn initialize_board(board: Board) -> (SerialBus<UARTE0>, Timer<TIMER0, Periodic>) {
let mut display = Display::new(board.TIMER1, board.display_pins);
display.display_image(&AppMode::Idle);
Expand All @@ -82,6 +95,16 @@ fn initialize_board(board: Board) -> (SerialBus<UARTE0>, Timer<TIMER0, Periodic>
Err(e) => handle_init_error(display, e),
};

// unmaking interrupts necessary for them to fire
// needs to be done here to keep Display and RunningTimer flexible
// as to which RTCs & Timers are actually used
unsafe {
NVIC::unmask(Interrupt::TIMER1);
NVIC::unmask(Interrupt::RTC1);
NVIC::unmask(Interrupt::GPIOTE);
}
NVIC::unpend(Interrupt::GPIOTE);

free(|cs| {
*DISPLAY.borrow(cs).borrow_mut() = Some(display);
*CONTROLS.borrow(cs).borrow_mut() = Some(controls);
Expand All @@ -91,6 +114,9 @@ fn initialize_board(board: Board) -> (SerialBus<UARTE0>, Timer<TIMER0, Periodic>
(serial_bus, main_loop_timer)
}

/// calculate the next state in the next processing cycle:
/// check what the user requested to do (by clicking on buttons) and
/// let domain layer calculate the next state based on this input
fn next_cycle<T: UarteInstance>(
mode: &AppMode,
serial_bus: &mut SerialBus<T>,
Expand All @@ -99,6 +125,19 @@ fn next_cycle<T: UarteInstance>(
mode.handle_interaction_request(request, now(), serial_bus)
}

/// convenience method to read the current "running time" from the static timer object
#[inline(always)]
fn now() -> Option<u64> {
free(|cs| {
RUNNING_TIMER
.borrow(cs)
.borrow_mut()
.as_mut()
.map(|timer| timer.now())
})
}

/// convenience wrapper to read the user interaction from the static controls object
fn get_requested_interaction() -> Result<InteractionRequest, Error> {
free(|cs| {
if let Some(controls) = CONTROLS.borrow(cs).borrow_mut().as_mut() {
Expand All @@ -109,22 +148,26 @@ fn get_requested_interaction() -> Result<InteractionRequest, Error> {
})
}

#[inline(always)]
fn now() -> Option<u64> {
/// convenience method to show the correct sprite for current mode on the display
fn show_mode(mode: &AppMode) {
free(|cs| {
RUNNING_TIMER
.borrow(cs)
.borrow_mut()
let mut display = DISPLAY.borrow(cs).borrow_mut();
let display = display
.as_mut()
.map(|timer| timer.now())
})
.expect("Display must be set at this point. Need restart");

display.display_image(mode);
});
}

/// report an error that happened while executing the main loop
/// and switch the AppMode appropriately to indicate it's in a failure state
fn handle_runtime_error(err: Error) -> AppMode {
report_error(err);
AppMode::Error
}

/// report an error that happened during initialization, don't even go into the main loop
fn handle_init_error<T: TimerInstance>(mut display: Display<T>, err: Error) -> ! {
display.display_image(&FATAL_SPRITE);
report_error(err);
Expand All @@ -135,6 +178,12 @@ fn handle_init_error<T: TimerInstance>(mut display: Display<T>, err: Error) -> !
}
}

// below here are the interrupt handlers
// the name of the method is the interrupt (from an enum) it handles
// in all cases we just forward the call to handle such an interrupt to the
// static object

/// tick the running timer
#[interrupt]
fn RTC1() {
free(|cs| {
Expand All @@ -144,6 +193,7 @@ fn RTC1() {
})
}

/// refresh the display
#[interrupt]
fn TIMER1() {
free(|cs| {
Expand All @@ -153,6 +203,7 @@ fn TIMER1() {
})
}

/// check user inputs
#[interrupt]
fn GPIOTE() {
free(|cs| {
Expand Down
Loading

0 comments on commit 56a69d2

Please sign in to comment.