Closed-loop DC motor controller for the STM32F103RCTx (Cortex-M3, 72 MHz), with a companion web-based GUI for real-time tuning and monitoring.
The firmware implements a cascade PID control scheme — an optional outer position loop feeds a velocity setpoint into an inner velocity loop — and communicates with the GUI over UART. Built with STM32 HAL and Python/FastAPI.
DC_Motor_Controller/
├── FW/ ← STM32 firmware (C, STM32CubeIDE)
├── SW/ ← Web GUI (Python backend + HTML/JS frontend)
└── docs/ ← Hardware photos, specs
Encoder (PA0 / PA1)
│
▼
TIM2 [Quadrature counter — TI12 ×4]
│ delta counts / 10 ms
▼
Motor2_Speed_Update()
└── 8-sample moving average → RPM
│
├─────────────────────────────────────────────┐
│ [Velocity Mode] │ [Position Mode]
│ │
│ setpoint_rpm ──► PID Velocity ◄── rpm │ setpoint_counts ──► PID Position
│ │ │ │
│ │ duty (−100…+100) │ vel_sp (RPM) ◄─────┘
│ │ │ │
│ └───────────────────┼───► PID Velocity
│ │ │
└────────────────────────────────────────────┘ │
duty (−100…+100)
│
▼
Motor2_SetOutput(signed_duty)
│
┌───────────────────┴──────────────────┐
│ │
TIM3_CH3 (forward) TIM3_CH4 (reverse)
│ │
└──────────────┬───────────────────────┘
│
H-Bridge
│
DC Motor
Control tick: SysTick ISR (1 ms) counts to 10 → sets g_control_tick flag → main loop runs one full control cycle.
| Parameter | Value |
|---|---|
| MCU | STM32F103RCTx — Cortex-M3 @ 72 MHz |
| Control loop period | 10 ms (100 Hz) |
| Encoder | Quadrature TI12 mode — 2023 PPR → 8092 CPR |
| Speed filter | 8-sample circular moving average |
| PWM | Dual-channel — TIM3_CH3 (forward) / TIM3_CH4 (reverse) |
| PID output range | −100 % … +100 % duty cycle |
| Velocity PID | Kp = 1.8, Ki = 0.05, Kd = 0.01, N = 0.2 |
| Position PID | Kp = 2.0, Ki = 0.005, Kd = 0.08, N = 0.2 |
Discrete PID with integrator anti-windup (clamp to output limits) and a first-order derivative low-pass filter controlled by coefficient N. Gains and sample period are set at initialisation; internal state can be reset independently of gains via PID_Reset().
PID_Init(&pid_vel, Kp, Ki, Kd, N, dt, out_min, out_max);
float duty = PID_Update(&pid_vel, setpoint_rpm, measured_rpm);Reads the TIM2 hardware quadrature encoder counter at each 10 ms tick, computes RPM from the signed 16-bit delta (wrap-safe), and applies an 8-sample circular moving average with O(1) running sum. Exposes both Motor2_Speed_GetRPM() and Motor2_Speed_GetRadS().
Controls an H-bridge via two complementary PWM channels. Supports FORWARD, REVERSE, BRAKE, and STOP modes. High-level entry point:
Motor2_SetOutput(int32_t signed_duty); /* −100 = full reverse, +100 = full forward */| Signal | Pin | Peripheral |
|---|---|---|
| Encoder A | PA0 | TIM2_CH1 |
| Encoder B | PA1 | TIM2_CH2 |
| PWM Forward | PB8 | TIM3_CH3 |
| PWM Reverse | PB9 | TIM3_CH4 |
| Driver Enable 1 | PB5 | GPIO output |
| Driver Enable 2 | PB4 | GPIO output |
- Open STM32CubeIDE
- File → Import → Existing Projects into Workspace → select repo root → Finish
- Select Debug or Release build configuration → Ctrl+B
- Flash via ST-Link: Run → Debug, or use STM32CubeProgrammer with
Release/DRIVER_RYM_V1.hex
