Working RTIC + UI w/separate board module
parent
7088f63c25
commit
67d19cfccd
|
@ -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",
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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.
|
||||
//! This demonstrates decomposing an RTIC-based project into communicating
|
||||
//! tasks and breaking out lower-level hardware details into separate modules.
|
||||
//!
|
||||
//! 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.
|
||||
//! 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,16 +19,10 @@ 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::prelude::*;
|
||||
|
||||
use stm32f1xx_hal as hal;
|
||||
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
|
||||
|
@ -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<hal::gpio::Output<hal::gpio::PushPull>>,
|
||||
tmr2: hal::timer::CountDownTimer<stm32::TIM2>,
|
||||
tmr3: hal::timer::CountDownTimer<stm32::TIM3>,
|
||||
led: board::UserLed,
|
||||
encoder: board::Encoder,
|
||||
display: board::Display,
|
||||
poll_timer: CountDownTimer<TIM2>,
|
||||
blink_timer: CountDownTimer<TIM3>,
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Input<Floating>, 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<TIM2>,
|
||||
pub blink_timer: CountDownTimer<TIM3>,
|
||||
}
|
||||
|
||||
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<Output<PushPull>, 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<Input<Floating>, CRH, 'C', 13>, cr: &mut hal::gpio::Cr<CRH, 'C'>) -> 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<Input<Floating>, CRL, 'B', 6>,
|
||||
Pin<Input<Floating>, 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<Pin<Input<Floating>, 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<Input<Floating>, CRL, 'B', 6>,
|
||||
dt_pin: Pin<Input<Floating>, CRL, 'B', 7>,
|
||||
button_pin: Pin<Input<Floating>, 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<Alternate<OpenDrain>, CRH, 'B', 10>,
|
||||
Pin<Alternate<OpenDrain>, CRH, 'B', 11>)
|
||||
>;
|
||||
type DisplayController = Ssd1306<
|
||||
I2CInterface<DisplayI2c>,
|
||||
DisplaySize128x64,
|
||||
BufferedGraphicsMode<DisplaySize128x64>
|
||||
>;
|
||||
/// 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
|
||||
}
|
||||
}
|
|
@ -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;
|
Loading…
Reference in New Issue