Embassy-based STM32 motor speed controller using:
- Quadrature encoders (timer QEI)
- H-bridge PWM (two PWM channels per motor: forward/reverse)
- PID speed loop (counts-per-second setpoint)
- UART telemetry + simple command interface (newline-terminated ASCII)
These are the pins/timers currently wired in src/main.rs:
- Left encoder:
TIM3withPA6(CH1) andPC7(CH2) - Right encoder:
TIM1withPE9(CH1) andPE11(CH2)
Each motor uses 2 PWM channels:
- CH1 = forward, CH2 = reverse
Configured as:
- Left motor:
TIM4withPD12(CH1 forward) andPD13(CH2 reverse) - Right motor:
TIM23withPF0(CH1 forward) andPF1(CH2 reverse)
- USART1:
PB7= RX,PB6= TX,115200baud
src/main.rs: hardware init + control loop + UART command parsing + telemetry formatting.src/modules/encoder.rs:EncoderQeiwrapper around EmbassyQei, plusdelta_counts()sampling.src/modules/motor_control.rs:HBridgePwm: appliesMotorCommandtoSimplePwmCH1/CH2 (forward/reverse).MotorPid: PID speed controller that outputsMotorCommandfrom measured counts/s.
src/modules/serial.rs:SerialInterfaceusing blocking UART writes + nonblocking byte reads.
The main loop runs at a fixed period (CONTROL_PERIOD_MS, currently 10ms).
Per tick:
- Read any available UART bytes and parse newline-terminated commands.
- Sample encoder deltas via
EncoderQei::delta_counts()and convert to counts per second. - Run PID for each motor to generate a signed output in [-1, 1].
- Map the PID output to:
- drive direction (
Forward/Reverse/Coast) - duty cycle between
MIN_DUTYand the timer’smax_duty.
- drive direction (
- Apply PWM to each H-bridge.
Telemetry is emitted every TELEMETRY_PERIOD_MS (currently 100ms) over:
defmtlogging (RTT)- UART as an ASCII line:
L:<left_cps> R:<right_cps>\r\n
Commands are ASCII, newline-terminated (\n). \r is ignored.
T <int>: set target CPS for both motorsTL <int>: set target CPS for left motorTR <int>: set target CPS for right motor
Examples:
T 4000
TL 2500
TR -3000
Values are interpreted as counts-per-second setpoints. Negative values command reverse.
This project targets thumbv7em-none-eabihf and uses a runner from .cargo/config.toml:
[target.thumbv7em-none-eabihf]
runner = 'probe-rs run --chip STM32H723ZGTx'Common commands:
cargo build
cargo runDEFMT_LOG is set in .cargo/config.toml (currently trace).
These are practical smoke tests you can run without changing code.
Goal: confirm UART wiring, baud, and command parsing.
- Connect a USB-UART adapter to USART1:
- Adapter TX ->
PB7(MCU RX) - Adapter RX ->
PB6(MCU TX) - Common GND
- Adapter TX ->
- Open a serial terminal at
115200 8N1. - Reset the board and observe periodic telemetry lines:
L:<...> R:<...>
- Send a command, e.g.:
T 0\nto coast both motorsT 2000\nto command forward
If telemetry prints but commands do nothing:
- Verify you are sending a newline (
\n). - Verify RX/TX are not swapped.
Goal: confirm encoder counts move and direction is correct.
- Run firmware with motors disabled or setpoint
0(T 0\n). - Spin the left wheel/encoder by hand.
- Observe
L:change; repeat for the right wheel/encoder andR:.
Notes:
EncoderQei::delta_counts()uses wrapping subtraction and assumes you do not exceed ~32k counts per sample.- If you see wildly incorrect readings, reduce speed or reduce
CONTROL_PERIOD_MS. - If your encoder outputs are open-collector (common on NPN optical encoders), A/B require pull-ups. This firmware enables pull-ups on the QEI pins via a local patch to
embassy-stm32and usesPull::Upwhen configuring QEI pins.
Expected scaling (Taiss 600 P/R encoder):
- With x4 quadrature decoding, counts/rev =
600 * 4 = 2400. - Telemetry reports counts/s (CPS). With 2400 counts/rev:
RPM = CPS / 40.
Goal: confirm the MCU is generating PWM on the expected pins.
- With the motor driver enabled but wheels off the ground, send:
TL 1000\nandTR 1000\n
- Use a scope/logic analyzer on:
- Left:
PD12/PD13 - Right:
PF0/PF1
- Left:
- Expect:
- Forward: CH1 toggling with duty, CH2 low (0% duty)
- Reverse: CH2 toggling with duty, CH1 low
Goal: confirm closed-loop behavior and basic stability.
- Start with a small setpoint:
T 500\n
- Increase gradually:
T 1000\n,T 2000\n, ...
- Watch telemetry counts/s and check it tracks the setpoint.
If the loop oscillates or saturates:
- Tune PID gains in
src/modules/motor_control.rs(MotorPid::new). - Adjust
MIN_DUTY(static friction compensation) insrc/main.rs. - Consider increasing PWM frequency if your motor driver prefers it (currently
khz(1)to match prior code).
- Test with wheels off the ground first.
- Be ready to stop the motors quickly (
T 0\n). - If direction is inverted, swap encoder channels or swap the motor outputs (or invert the sign of the setpoint).