Skip to content

Instantly share code, notes, and snippets.

@leehambley
Last active January 3, 2025 21:01
Show Gist options
  • Save leehambley/f7a68b2e807bcf72ba966df998cb8b0a to your computer and use it in GitHub Desktop.
Save leehambley/f7a68b2e807bcf72ba966df998cb8b0a to your computer and use it in GitHub Desktop.

Backstory

"host" for a lathe ELS controller (lead screw). this little embedded module is used as part of a test harness for an arduino based lathe screw controller.

Host has a rotary encoder for stop/start (click) and for RPM (and direction change) with ++ and -- turns.

The "host" project should take the internal RPM target and output a quadrature-encoder alike pair of pulses offset by one another.

There's a compensation between target rpm and current rpm to simulate the physical properties of teh machine (acceleration, intertia, etc)

Problem

after "fiddling" with teh encoder inputs for a while the system seems to just stop. the probe-rs "cargo run" uart logging stops, the pwm outputs to the scope stop, adn it stops responding.

connecting to the thing to debug shows that it's still running, despite not logging anything.

>> halt
Core stopped at address 0x08004dd0
0x8004dd0: add sp, #0x10
0x8004dd2: pop {r4, pc}
0x8004dd4: push {r7, lr}
0x8004dd6: mov r7, sp
0x8004dd8: sub sp, #0x18
0x8004dda: mov ip, r0
0x8004ddc: add r0, sp, #8
0x8004dde: strd r2, r3, [sp]
0x8004de2: mov r2, ip
0x8004de4: mov r3, r1
0x8004de6: bl #0x8004e04
0x8004dea: ldr.w ip, [r7, #8]
>> bt
No debug information present!
>> halt
Core stopped at address 0x08004dd0
0x8004dd0: add sp, #0x10
0x8004dd2: pop {r4, pc}
0x8004dd4: push {r7, lr}
0x8004dd6: mov r7, sp
0x8004dd8: sub sp, #0x18
0x8004dda: mov ip, r0
0x8004ddc: add r0, sp, #8
0x8004dde: strd r2, r3, [sp]
0x8004de2: mov r2, ip
0x8004de4: mov r3, r1
0x8004de6: bl #0x8004e04
0x8004dea: ldr.w ip, [r7, #8]
>> run
Core is running.
>> halt
Core stopped at address 0x08001338
0x8001338: b #0x8001330
0x800133a: push {r4, r6, r7, lr}
0x800133c: add r7, sp, #8
0x800133e: sub sp, #8
0x8001340: mov r4, r0
0x8001342: movw r0, #3
0x8001346: movt r0, #0
0x800134a: strh r0, [r7, #-0xa]
0x800134e: sub.w r0, r7, #0xa
0x8001352: bl #0x80033c8
0x8001356: ldr r0, [r4]
>> bt
No debug information present!
>>

Enabling debuggig by tweaking the proifile causes the linker to fail as the code is too big for the Flash then

#![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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment