From 67d19cfccd838f3ce1ffd46ece72642c5dbe8946 Mon Sep 17 00:00:00 2001 From: Levi Pearson Date: Wed, 9 Mar 2022 02:03:12 -0700 Subject: [PATCH] Working RTIC + UI w/separate board module --- .vscode/settings.json | 13 ++ cross/Cargo.lock | 17 ++ cross/Cargo.toml | 2 + cross/src/bin/ui_example.rs | 132 ++++++++-------- cross/src/board.rs | 298 ++++++++++++++++++++++++++++++++++++ cross/src/lib.rs | 12 ++ 6 files changed, 408 insertions(+), 66 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 cross/src/board.rs create mode 100644 cross/src/lib.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4311dd7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + // override the default setting (`cargo check --all-targets`) which produces the following error + // "can't find crate for `test`" when the default compilation target is a no_std target + // with these changes RA will call `cargo check --bins` on save + "rust-analyzer.checkOnSave.allTargets": false, + "rust-analyzer.checkOnSave.extraArgs": [ + "--bins" + ], + "rust-analyzer.linkedProjects": [ + "Cargo.toml", + "cross/Cargo.toml", + ] +} diff --git a/cross/Cargo.lock b/cross/Cargo.lock index 0ca6c72..76be8ae 100644 --- a/cross/Cargo.lock +++ b/cross/Cargo.lock @@ -54,6 +54,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fe8f5a8a398345e52358e18ff07cc17a568fbca5c6f73873d3a62056309603" +[[package]] +name = "bbqueue" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3baa8859d1a4c7411039a75c0599a4640ef1c9a8fc811e4325b00e6cfe0a55" + [[package]] name = "bit_field" version = "0.10.1" @@ -76,11 +82,13 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" name = "blue_pill_ui" version = "0.1.0" dependencies = [ + "bbqueue", "cortex-m-rtic", "panic-rtt-target", "rtt-target", "ssd1306", "stm32f1xx-hal", + "switch-hal", "ui", ] @@ -580,6 +588,15 @@ dependencies = [ "void", ] +[[package]] +name = "switch-hal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90a4adc8cbd1726249b161898e48e0f3f1ce74d34dc784cbbc98fba4ed283fbf" +dependencies = [ + "embedded-hal", +] + [[package]] name = "syn" version = "1.0.86" diff --git a/cross/Cargo.toml b/cross/Cargo.toml index b21a3b1..409abbc 100644 --- a/cross/Cargo.toml +++ b/cross/Cargo.toml @@ -12,6 +12,8 @@ panic-rtt-target = { version = "0.1.2", features = ["cortex-m"] } rtt-target = { version = "0.3.1", features = ["cortex-m"] } stm32f1xx-hal = { version = "0.8.0", features = ["rt", "stm32f103", "medium"] } ssd1306 = "0.7.0" +switch-hal = "0.4.0" +bbqueue = "0.5.1" ui = { path = "../ui" } [[bin]] diff --git a/cross/src/bin/ui_example.rs b/cross/src/bin/ui_example.rs index 69b72a2..e99713c 100644 --- a/cross/src/bin/ui_example.rs +++ b/cross/src/bin/ui_example.rs @@ -1,16 +1,16 @@ -//! Blinks an LED +//! Blinks an LED and updates a UI based on an OLED display and rotary encoder //! -//! This assumes that a LED is connected to pc13 as is the case on the blue pill board. -//! -//! Note: Without additional hardware, PC13 should not be used to drive an LED, see page 5.1.2 of -//! the reference manual for an explanation. This is not an issue on the blue pill. +//! This demonstrates decomposing an RTIC-based project into communicating +//! tasks and breaking out lower-level hardware details into separate modules. +//! +//! The UI model itself is a completely separate crate, and can also be built +//! for the `embedded-graphics-simulator` on the host, allowing for rapid UI +//! development without having to re-flash. #![deny(unsafe_code)] #![no_std] #![no_main] -use panic_rtt_target as _; - // RTIC requires that unused interrupts are declared in "dispatchers" when // using software tasks; these free interrupts will be used to dispatch the // software tasks. @@ -19,17 +19,11 @@ use panic_rtt_target as _; // https://docs.rs/stm32f1xx-hal/0.6.1/stm32f1xx_hal/stm32/enum.Interrupt.html #[rtic::app(device = stm32f1xx_hal::stm32, peripherals = true, dispatchers = [TAMPER])] mod app { - - use core::sync::atomic::{self, Ordering}; use rtt_target::{rprintln, rtt_init_print}; - use stm32f1xx_hal::{ - prelude::*, - stm32, - timer::{Event, Timer}, - }; - - use stm32f1xx_hal as hal; + use stm32f1xx_hal::prelude::*; + use blue_pill_ui::board::{self, Board, CountDownTimer, TIM2, TIM3, Event}; + // Defining this struct makes shared resources available to tasks; // they will be initialized by the values returned from `init` and // will be wrapped in a `Mutex` and must be accessed via a closure @@ -37,16 +31,23 @@ mod app { // If you annotate a field with #[lock_free] you can opt-out of the // mutex but it may only be shared by tasks at the same priority. #[shared] - struct Shared {} + struct Shared { + /// This will be used to communicate control updates from the + /// control polling task to the idle thread, which manages the + /// UI model and display drawing + update: Option<(i32, bool)>, + } // This struct defines local resources (accessed by only one task); // they will be initialized by the values returned from `init` and // can be accessed directly. #[local] struct Local { - led1: hal::gpio::gpioc::PC13>, - tmr2: hal::timer::CountDownTimer, - tmr3: hal::timer::CountDownTimer, + led: board::UserLed, + encoder: board::Encoder, + display: board::Display, + poll_timer: CountDownTimer, + blink_timer: CountDownTimer, } // This task does startup config; the peripherals are passed in thanks to @@ -57,72 +58,71 @@ mod app { rtt_init_print!(); rprintln!("init begin"); - // Set everything to 8MHz using the external clock - let mut flash = cx.device.FLASH.constrain(); - let rcc = cx.device.RCC.constrain(); - let clocks = rcc - .cfgr - .use_hse(8.mhz()) - .sysclk(8.mhz()) - .hclk(8.mhz()) - .pclk1(8.mhz()) - .pclk2(8.mhz()) - .adcclk(8.mhz()) - .freeze(&mut flash.acr); + let Board { encoder, display, led, mut poll_timer, mut blink_timer } = Board::init(cx.device); + poll_timer.listen(Event::Update); + blink_timer.listen(Event::Update); - // LED is on pin C13, configure it for output - let mut gpioc = cx.device.GPIOC.split(); - let led1 = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); - - // Use TIM2 for the beat counter task - let mut tmr2 = Timer::tim2(cx.device.TIM2, &clocks).start_count_down(1.hz()); - tmr2.listen(Event::Update); - - // Use TIM3 for the LED blinker task - let mut tmr3 = Timer::tim3(cx.device.TIM3, &clocks).start_count_down(2.hz()); - tmr3.listen(Event::Update); + let delta = 0; + let button_up = false; + let update = Some((delta, button_up)); rprintln!("init end"); - (Shared {}, Local { led1, tmr2, tmr3 }, init::Monotonics()) + (Shared { update }, Local { led, encoder, display, poll_timer, blink_timer }, init::Monotonics()) } - #[idle] - fn idle(_: idle::Context) -> ! { + /// The idle task never stops running, so it can hold `!Send` state like + /// our UI state. It busy-waits for updates via the shared `update` + /// resource. + #[idle(local = [display], shared = [update])] + fn idle(cx: idle::Context) -> ! { + let mut ui: ui::HelloDisplay<128,64> = ui::HelloDisplay::new(); + let mut update = cx.shared.update; loop { - // The compiler may omit this loop without the following - atomic::compiler_fence(Ordering::SeqCst); + if let Some((delta, button_up)) = update.lock(|upd| upd.take()) { + if delta != 0 { + ui.event(ui::HelloEvent::Knob(delta)); + } + if button_up { + ui.event(ui::HelloEvent::Button); + } + ui.event(ui::HelloEvent::Tick); + cx.local.display.draw(&mut ui); + } } } - // Update the beat counter and periodically display the current count - // on the RTT channel - // Since `beat` is a local, we can have it initialized. - #[task(local = [beat: u32 = 0])] - fn beat_update(cx: beat_update::Context) { - if *cx.local.beat % 10 == 0 { - rprintln!("TIM2 beat = {}", *cx.local.beat); + // Poll the encoder and send its state to the idle task via the shared + // `update` resource. Print out the raw encoder count when the button is + // pressed. + // + // Since `count` is a local, we can have it initialized with a const expr. + #[task(local = [count: u16 = 0, encoder], shared = [update])] + fn count_update(mut cx: count_update::Context) { + let delta = cx.local.encoder.poll_count_delta(); + let button_up = cx.local.encoder.poll_button_up(); + if button_up { + rprintln!("Button pressed: encoder count is {}", cx.local.encoder.count()); } - - *cx.local.beat += 1; + cx.shared.update.lock(|upd| upd.replace((delta, button_up))); } - // Interrupt task for TIM2, the beat counter timer - #[task(binds = TIM2, priority = 2, local = [tmr2])] + // Interrupt task for TIM2, the control polling timer + #[task(binds = TIM2, priority = 2, local = [poll_timer])] fn tim2(cx: tim2::Context) { // Delegate the state update to a software task - beat_update::spawn().unwrap(); + count_update::spawn().unwrap(); // Restart the timer and clear the interrupt flag - cx.local.tmr2.start(1.hz()); - cx.local.tmr2.clear_update_interrupt_flag(); + cx.local.poll_timer.start(60.hz()); + cx.local.poll_timer.clear_update_interrupt_flag(); } // Interrupt task for TIM3, the LED blink timer - #[task(binds = TIM3, priority = 1, local = [led1, tmr3])] + #[task(binds = TIM3, priority = 1, local = [led, blink_timer])] fn tim3(cx: tim3::Context) { - cx.local.led1.toggle(); - cx.local.tmr3.start(2.hz()); - cx.local.tmr3.clear_update_interrupt_flag(); + cx.local.led.toggle(); + cx.local.blink_timer.start(2.hz()); + cx.local.blink_timer.clear_update_interrupt_flag(); } } diff --git a/cross/src/board.rs b/cross/src/board.rs new file mode 100644 index 0000000..030d067 --- /dev/null +++ b/cross/src/board.rs @@ -0,0 +1,298 @@ +//! Hardware initialization and control wrappers + +use stm32f1xx_hal as hal; +use hal::{ + prelude::*, + stm32::{self, Peripherals}, + timer::{Timer, Tim4NoRemap}, + i2c::{BlockingI2c, DutyCycle, Mode}, + qei::{Qei, QeiOptions}, +}; + +// re-export some of the types from the HAL we're not wrapping +pub use hal::timer::{Event, CountDownTimer}; +pub use stm32::{TIM2, TIM3}; + +use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306, mode::BufferedGraphicsMode}; + +// Imports for specifying the types of pins +// The Pin type takes 4 parameters; config, low/high register, bank name, and number +// E.g. Pin, CRL, 'A', 0> for PA0 in input mode with no pull resistor. +use hal::gpio::{Pin, Input, Output, Floating, PushPull, CRL, CRH, Alternate, OpenDrain}; + +// +// The Board peripheral collection +// + +/// The managed peripherals for this Blue Pill project board +pub struct Board { + pub encoder: Encoder, + pub display: Display, + pub led: UserLed, + pub poll_timer: CountDownTimer, + pub blink_timer: CountDownTimer, +} + +impl Board { + /// Set up clocks and pin mappings for peripherals on the board + /// + /// This returns a plain struct containing all the managed + /// peripherals so that it can be destructured. + pub fn init(periphs: Peripherals) -> Self { + // Set up board clocks + let mut flash = periphs.FLASH.constrain(); + let rcc = periphs.RCC.constrain(); + let clocks = rcc + .cfgr + .use_hse(8.mhz()) + .sysclk(72.mhz()) + .hclk(72.mhz()) + .pclk1(36.mhz()) + .pclk2(72.mhz()) + .adcclk(12.mhz()) + .freeze(&mut flash.acr); + + // LED is on pin C13, configure it for output + let mut gpioc = periphs.GPIOC.split(); + let led_pin = gpioc.pc13; + let led = UserLed::new(led_pin, &mut gpioc.crh); + + let mut gpiob = periphs.GPIOB.split(); + + // Rotary encoder uses TIM4 on B6 and B7, and B8 as the encoder button. + let enc_clk = gpiob.pb6; + let enc_dt = gpiob.pb7; + let enc_button = gpiob.pb8; + let mut afio = periphs.AFIO.constrain(); + let encoder = Encoder::new( + periphs.TIM4, + enc_clk, + enc_dt, + enc_button, + &mut afio.mapr, + &clocks, + ); + + // Use TIM2 for the control polling task + let poll_timer = Timer::tim2(periphs.TIM2, &clocks).start_count_down(1.hz()); + + // Use TIM3 for the LED blinker task + let blink_timer = Timer::tim3(periphs.TIM3, &clocks).start_count_down(2.hz()); + // I2C for display is on gpiob B10 and B11 + let scl = gpiob.pb10.into_alternate_open_drain(&mut gpiob.crh); + let sda = gpiob.pb11.into_alternate_open_drain(&mut gpiob.crh); + // Configure the I2C peripheral itself + let i2c = BlockingI2c::i2c2( + periphs.I2C2, + (scl, sda), + Mode::Fast { + frequency: 400_000.hz(), + duty_cycle: DutyCycle::Ratio2to1, + }, + clocks, + 1000, + 10, + 1000, + 1000, + ); + let display = Display::new(i2c); + + Self { + encoder, + display, + led, + poll_timer, + blink_timer, + } + } +} + +/// The user-controllable green LED +pub struct UserLed(Pin, CRH, 'C', 13>); + +impl UserLed { + /// Create the LED controller from the pin it is attached to + /// + /// Requires both the pin and a mutable reference to its control register + pub fn new(pin: Pin, CRH, 'C', 13>, cr: &mut hal::gpio::Cr) -> Self { + let led = pin.into_push_pull_output(cr); + Self(led) + } + + /// Toggle the state of the LED + /// + /// The state is managed in the hardware; the HAL api this calls will read, + /// modify, and write the control register to change the state. + pub fn toggle(&mut self) { + self.0.toggle(); + } +} + +// +// Rotary Encoder +// + +use switch_hal::{Switch, ActiveLow, InputSwitch, IntoSwitch}; +type EncoderQei = Qei< + stm32::TIM4, + Tim4NoRemap, + (Pin, CRL, 'B', 6>, + Pin, CRL, 'B', 7>) +>; +/// A rotary encoder peripheral with a push-button shaft +/// +/// This can return the current readings of the shaft count and buttons and +/// also statefully poll for rotation deltas and "button up" events. +/// +/// Monitoring the position is handled by the TIM4 timer in quadrature encoder +/// mode, which provides clean and accurate positioning with no software +/// overhead. +pub struct Encoder { + qei: EncoderQei, + button: Switch, CRH, 'B', 8>, ActiveLow>, + last_count: u16, + last_active: bool, +} + +impl Encoder { + /// Create a new encoder manager + /// + /// You must supply the following: + /// + The timer control register + /// + 2 pins attached to the timer for monitoring rotation + /// + 1 gpio pin for monitoring the button (active low) + /// + Mutable reference to the `MAPR` register + /// + Shared reference to `Clocks` + pub fn new( + timer: stm32::TIM4, + clk_pin: Pin, CRL, 'B', 6>, + dt_pin: Pin, CRL, 'B', 7>, + button_pin: Pin, CRH, 'B', 8>, + mapr: &mut hal::afio::MAPR, + &clocks: &hal::rcc::Clocks, + ) -> Self { + let qei = Timer::tim4(timer, &clocks) + .qei((clk_pin, dt_pin), mapr, QeiOptions::default()); + let button = button_pin.into_active_low_switch(); + + Self { + qei, + button, + last_count: 0, + last_active: false, + } + } + + /// Get the current count from the encoder + /// + /// Each tick of the counter represents one of the tactile detents, which + /// occur once every 4 of the raw counts. + pub fn count(&self) -> u16 { + self.qei.count() / 4 + } + + /// Get the current depressed state of the shaft button of the encoder + /// + /// This will be `true` while the shaft is being pressed down and `false` + /// when it is not being depressed. + pub fn is_pressed(&self) -> bool { + self.button.is_active().unwrap() + } + + /// Statefully get the number of ticks the shaft has turned since last poll + /// + /// Each poll stores the current position and returns the delta between the + /// current position and the last one. + pub fn poll_count_delta(&mut self) -> i32 { + let prev = self.last_count; + let current = self.count(); + self.last_count = current; + + i32::from(current) - i32::from(prev) + } + + /// Statefully poll for "button up" events + /// + /// Each poll stores the current button state and returns whether the + /// button returned to the inactive state from the active state since + /// the previous poll. + pub fn poll_button_up(&mut self) -> bool { + let prev = self.last_active; + let current = self.button.is_active().unwrap(); + self.last_active = current; + + !current && prev + } +} + +// +// OLED Display +// + +type DisplayI2c = BlockingI2c< + stm32::I2C2, + (Pin, CRH, 'B', 10>, + Pin, CRH, 'B', 11>) +>; +type DisplayController = Ssd1306< + I2CInterface, + DisplaySize128x64, + BufferedGraphicsMode +>; +/// An I2C-attached SSD1306 128x64 OLED display +/// +/// This manages the I2C communication with the controller, providing `draw` +/// and `flush` methods for interacting with the `embedded-graphics` crate. +pub struct Display { + controller: DisplayController, +} + +impl Display { + /// Create the OLED display manager + /// + /// This requires a pre-configred `BlockingI2c` bus manager. It initializes + /// the controller upon construction but doesn't flush it. + /// + /// The underlying `Ssd1306` controller is configured for buffered graphics + /// mode. + pub fn new(i2c: DisplayI2c) -> Self { + let interface = I2CDisplayInterface::new(i2c); + let mut controller = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0) + .into_buffered_graphics_mode(); + + controller.init().unwrap(); + + Self { + controller, + } + } + + /// Flush the buffered graphics operations to the display + pub fn flush(&mut self) { + self.controller.flush().ok(); + } + + /// Draw the current state of the UI model to the display + /// + /// This includes a flush operation. + pub fn draw(&mut self, model: &mut ui::HelloDisplay<128,64>) { + model.draw(&mut self.controller).ok(); + self.controller.flush().ok(); + } +} + +// Implement Deref and DerefMut so we can treat Display as a DisplayController +use core::ops::{Deref, DerefMut}; + +impl Deref for Display { + type Target = DisplayController; + fn deref(&self) -> &Self::Target { + &self.controller + } +} + +impl DerefMut for Display { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.controller + } +} \ No newline at end of file diff --git a/cross/src/lib.rs b/cross/src/lib.rs new file mode 100644 index 0000000..79064f8 --- /dev/null +++ b/cross/src/lib.rs @@ -0,0 +1,12 @@ +//! Blue Pill Rust project with UI and RTIC +//! +//! This is the core library of the firmware that runs on the Blue Pill itself + +#![no_std] +#![no_main] + +use panic_rtt_target as _; + +use stm32f1xx_hal as _; // memory layout + +pub mod board; \ No newline at end of file