|
#![no_std] |
|
#![no_main] |
|
#![feature(type_alias_impl_trait)] // Required for Embassy's async support |
|
|
|
use core::cell::RefCell; |
|
use {defmt_rtt as _, panic_probe as _}; |
|
// use embassy_stm32::timer::simple_pwm::SimplePwm; |
|
use core::f32; |
|
use core::sync::atomic::{AtomicBool, Ordering}; |
|
use defmt::*; |
|
use embassy_executor::Spawner; |
|
use embassy_stm32::exti::ExtiInput; |
|
use embassy_stm32::{ |
|
exti::{AnyChannel, Channel}, |
|
gpio::{AnyPin, Input, Level, Output, Pin, Pull, Speed}, |
|
peripherals::TIM3, |
|
}; |
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; |
|
use embassy_sync::blocking_mutex::Mutex; |
|
use embassy_time::{Duration, Timer}; |
|
|
|
// use embassy_rp::Peripheral as _; |
|
// use embassy_stm32::i2c::{I2c, Config}; |
|
// use embassy_stm32::interrupt::Interrupt; |
|
// use ssd1306::{prelude::*, Builder}; |
|
|
|
pub struct DirectionAtomic { |
|
inner: AtomicBool, |
|
} |
|
|
|
impl DirectionAtomic { |
|
pub const fn new(initial: bool) -> Self { |
|
Self { |
|
inner: AtomicBool::new(initial), |
|
} |
|
} |
|
|
|
pub fn store(&self, cw: bool) { |
|
self.inner.store(cw, Ordering::Relaxed); |
|
} |
|
|
|
pub fn is_forward(&self) -> bool { |
|
!self.inner.load(Ordering::Relaxed) |
|
} |
|
} |
|
|
|
impl defmt::Format for DirectionAtomic { |
|
fn format(&self, f: defmt::Formatter) { |
|
let val = self.inner.load(Ordering::Relaxed); |
|
defmt::write!(f, "{}", if val { "FORWARD" } else { "REVERSE" }); |
|
} |
|
} |
|
|
|
// Shared state using Mutex for thread safety |
|
static TARGET_RPM: Mutex<CriticalSectionRawMutex, RefCell<f32>> = Mutex::new(RefCell::new(500.0)); |
|
static CURRENT_RPM: Mutex<CriticalSectionRawMutex, RefCell<f32>> = Mutex::new(RefCell::new(250.0)); |
|
static RUNNING: AtomicBool = AtomicBool::new(true); |
|
static DIRECTION: DirectionAtomic = DirectionAtomic::new(false); |
|
|
|
#[embassy_executor::task] |
|
async fn rpm_logger_task() { |
|
loop { |
|
Timer::after(Duration::from_secs(1)).await; |
|
|
|
// Lock and copy out both RPMs |
|
let target = TARGET_RPM.lock(|cell| *cell.borrow()); |
|
let current = CURRENT_RPM.lock(|cell| *cell.borrow()); |
|
|
|
info!("RPMs: target = {}, current = {}", target, current); |
|
} |
|
} |
|
|
|
#[embassy_executor::main] |
|
async fn main(spawner: Spawner) { |
|
let p: embassy_stm32::Peripherals = embassy_stm32::init(Default::default()); |
|
|
|
// Spawn tasks |
|
spawner |
|
.spawn(encoder_input_task( |
|
p.PA0.degrade(), |
|
p.PA1.degrade(), |
|
&TARGET_RPM, |
|
&DIRECTION, |
|
)) |
|
.unwrap(); |
|
spawner |
|
.spawn(button_task(p.PA2.degrade(), p.EXTI2.degrade())) |
|
.unwrap(); |
|
spawner.spawn(rpm_logger_task()).unwrap(); |
|
spawner |
|
.spawn(rpm_control_task(&TARGET_RPM, &CURRENT_RPM, &RUNNING)) |
|
.unwrap(); |
|
spawner |
|
.spawn(quadrature_output_task( |
|
p.TIM3, |
|
&CURRENT_RPM, |
|
&DIRECTION, |
|
&RUNNING, |
|
p.PB0.degrade(), |
|
p.PB1.degrade(), |
|
)) |
|
.unwrap(); |
|
} |
|
|
|
// Task to handle EC11 encoder rotation |
|
#[embassy_executor::task] |
|
async fn encoder_input_task( |
|
chan_a: AnyPin, |
|
chan_b: AnyPin, |
|
target_rpm: &'static Mutex<CriticalSectionRawMutex, RefCell<f32>>, |
|
direction: &'static DirectionAtomic, |
|
) { |
|
let a = Input::new(chan_a, Pull::Up); |
|
let b = Input::new(chan_b, Pull::Up); |
|
|
|
let mut last_state = (a.is_high(), b.is_high()); |
|
|
|
loop { |
|
Timer::after(Duration::from_millis(10)).await; |
|
|
|
let current_a = a.is_high(); |
|
let current_b = b.is_high(); |
|
|
|
// Detect any change |
|
if current_a != last_state.0 || current_b != last_state.1 { |
|
let delta = match (last_state.0, last_state.1, current_a, current_b) { |
|
(false, false, true, false) => 1, |
|
(true, false, true, true) => 1, |
|
(true, true, false, true) => 1, |
|
(false, true, false, false) => 1, |
|
|
|
(false, false, false, true) => -1, |
|
(false, true, true, true) => -1, |
|
(true, true, true, false) => -1, |
|
(true, false, false, false) => -1, |
|
|
|
_ => 0, |
|
}; |
|
|
|
if delta != 0 { |
|
// Lock the mutex once, do all reads/writes inside |
|
target_rpm.lock(|cell| { |
|
let mut rpm = cell.borrow_mut(); // => MutexGuard<'_, RefCell<f32>> |
|
*rpm += delta as f32 * 50.0; |
|
warn!("rpm changed: {} ({})", rpm.abs(), direction); |
|
// Clamp |
|
if *rpm > 3000.0 { |
|
*rpm = 3000.0; |
|
} else if *rpm < -3000.0 { |
|
*rpm = -3000.0; |
|
} |
|
// Direction: true => CW, false => CCW |
|
if *rpm < 0.0 { |
|
// warn!("switching direction"); |
|
direction.store(true); |
|
} else { |
|
// warn!("switching direction"); |
|
direction.store(false); |
|
} |
|
}); |
|
} |
|
|
|
// Update state |
|
last_state = (current_a, current_b); |
|
} |
|
} |
|
} |
|
|
|
// Task to handle encoder button press |
|
#[embassy_executor::task] |
|
async fn button_task(pin: AnyPin, channel: AnyChannel) { |
|
let mut button = ExtiInput::new(pin, channel, Pull::Up); |
|
|
|
loop { |
|
button.wait_for_falling_edge().await; |
|
embassy_time::Timer::after(Duration::from_millis(50)).await; |
|
if button.is_low() { |
|
// Toggle RUNNING |
|
let new = !RUNNING.load(Ordering::Relaxed); |
|
RUNNING.store(new, Ordering::Relaxed); |
|
defmt::info!("button pressed, now RUNNING={}", new); |
|
} |
|
// TODO - ensure RPM handler task drops the RPM to zero after a reasonable time. |
|
} |
|
} |
|
|
|
#[embassy_executor::task] |
|
async fn rpm_control_task( |
|
target_rpm: &'static Mutex<CriticalSectionRawMutex, RefCell<f32>>, |
|
current_rpm: &'static Mutex<CriticalSectionRawMutex, RefCell<f32>>, |
|
running: &'static AtomicBool, |
|
) { |
|
info!("rpm_control_task started"); |
|
let accel = 100.0; // RPM per second |
|
loop { |
|
// Sleep briefly between updates |
|
Timer::after(Duration::from_millis(1000)).await; |
|
info!("rpm control task"); |
|
|
|
if running.load(Ordering::Relaxed) { |
|
// Copy out target |
|
let target = target_rpm.lock(|cell| *cell.borrow()); // f32 is Copy |
|
|
|
// Mutate current |
|
current_rpm.lock(|cell| { |
|
let mut cur = cell.borrow_mut(); |
|
// Accelerate or decelerate toward target |
|
if *cur < target { |
|
*cur += accel * 0.1; |
|
if *cur > target { |
|
*cur = target; |
|
} |
|
} else if *cur > target { |
|
*cur -= accel * 0.1; |
|
if *cur < target { |
|
*cur = target; |
|
} |
|
} |
|
}); |
|
} else { |
|
// Decelerate to zero |
|
current_rpm.lock(|cell| { |
|
let mut cur = cell.borrow_mut(); |
|
if *cur > 0.0 { |
|
*cur -= accel * 0.1; |
|
if *cur < 0.0 { |
|
*cur = 0.0; |
|
} |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
// Task to generate quadrature encoder signals using Timer3 (Software-based) |
|
#[embassy_executor::task] |
|
async fn quadrature_output_task( |
|
_tim3: TIM3, // Not used in software-based pulses |
|
current_rpm: &'static Mutex<CriticalSectionRawMutex, RefCell<f32>>, |
|
_direction: &'static DirectionAtomic, |
|
running: &'static AtomicBool, |
|
channel_a: AnyPin, |
|
channel_b: AnyPin, |
|
) { |
|
let mut a = Output::new(channel_a, Level::Low, Speed::VeryHigh); |
|
let mut b = Output::new(channel_b, Level::Low, Speed::VeryHigh); |
|
loop { |
|
if running.load(Ordering::Relaxed) { |
|
let mut rpm: f32 = 0.0; |
|
current_rpm.lock(|current_rpm| { |
|
rpm = *current_rpm.borrow(); |
|
}); |
|
let pulses_per_sec = (rpm / 60.0) * 1000.0 * 4.0; // 1000 PPR * 4 for quadrature |
|
if pulses_per_sec > 0.0 { |
|
let period_us = (1_000_000.0 / pulses_per_sec) as u32; |
|
// Generate A Pulse |
|
a.set_high(); |
|
// info!("high"); |
|
Timer::after(Duration::from_micros((period_us / 2).into())).await; |
|
a.set_low(); |
|
// info!("low"); |
|
|
|
// Generate B Pulse with Phase Shift based on direction |
|
b.set_high(); |
|
Timer::after(Duration::from_micros((period_us / 2).into())).await; |
|
b.set_low(); |
|
} |
|
} else { |
|
a.set_low(); |
|
b.set_low(); |
|
Timer::after(Duration::from_millis(100)).await; |
|
} |
|
} |
|
} |