个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A two-wheel self-balancing robot on the STM32F103C8T6
Angle + rate loops · inverse dynamics · velocity & turn loops · Web Bluetooth remote · pure HAL
Features · Control · Hardware · Getting Started · Remote · Layout · Lessons Learned · License
main().flowchart LR
RC["Bluetooth remote<br/>move <turn> <speed>"] -->|speed setpoint| V
RC -->|turn setpoint| T
subgraph CTRL["App_Control_Process · 5 ms"]
V["Velocity PI<br/>Kp=10 Ki=1"] -->|"θ_ref = atan(ẍ/g)"| A["Angle P<br/>Kp=4"]
A -->|θ̇_ref| R["Rate PI<br/>Kp=10 Ki=10"]
R -->|θ̈_ref| ID["Inverse dynamics<br/>ẍ = (g·sinθ − lp·θ̈)/cosθ"]
ID --> INT["Integrator 1/(rw·s)<br/>clamp ±40 rad/s"]
T["Turn P<br/>Kp=1"]
end
INT -->|ω_ref| SUM(("±"))
T -->|differential| SUM
SUM --> M["Motor PI × 2<br/>Kp=0.5 Ki=7 · 1 ms"]
M --> PWM["TB6612 PWM"]
IMU["MPU6500<br/>complementary filter"] -.θ / θ̇.-> A & R
IMU -.gyro Z.-> T
ENC["Encoders<br/>T-method speed"] -.ω.-> M & V
| Loop | Feedback | Type | Gains | Output limit | Period |
|---|---|---|---|---|---|
| Motor (innermost) | wheel speed ω (encoder) | PI | Kp 0.5 / Ki 7 | duty cycle | 1 ms |
| Rate | pitch rate θ̇ (gyro X) | PI | Kp 10 / Ki 10 | ±125.7 rad/s² | 5 ms |
| Angle | pitch θ (complementary filter) | P | Kp 4 | ±12.57 rad/s | 5 ms |
| Velocity (outermost) | ω·rw + θ̇·(lp+rw) | PI | Kp 10 / Ki 1 | ±0.5 g | 5 ms |
| Turn | yaw rate (gyro Z) | P | Kp 1 | ±10 rad/s | 5 ms |
[!NOTE] The
θ̇·(lp+rw)term in the velocity feedback cancels the wheel motion caused by the body swinging. Without it, the wheels "chasing" a forward lean would look like forward travel to the velocity loop and create positive feedback.
The motor model comes from measured parameters, Gm(s) = 5.591 / ((s/14.2+1)(s/1962.2+1)). The motor PI is designed by pole-zero cancellation, giving a closed loop of roughly 1/(s/40+1) (τ ≈ 25 ms).
| Part | Model | Notes |
|---|---|---|
| MCU | STM32F103C8T6 | Cortex-M3 72 MHz, 64 KB Flash / 20 KB RAM |
| IMU | MPU6500 module | hardware I2C1 @ 400 kHz, ±2000 dps / ±2 g, DLPF 92 Hz |
| Motor driver | TB6612FNG | dual H-bridge |
| Motors | geared DC motors with AB-phase encoders × 2 | gear ratio ≈ 20.4; phase A on EXTI for timing, phase B for direction |
| Bluetooth | ECB02 BLE transparent module | 9600 baud, works with iOS |
| Battery | 2S LiPo (6.0 – 8.4 V) | voltage sampled through a divider |
| Pin | Function |
|---|---|
| PB0 | ADC1_IN8 battery voltage |
| PA4 / PA5 / PA6 | battery LEDs (low → high) |
| PA11 | start/stop button (pull-up, active low) |
| PA1 | TB6612 STBY (1 = enabled) |
| PA8 (TIM1_CH1) | left motor PWM |
| PA9 / PA10 | left motor AIN1 / AIN2 |
| PB6 (TIM4_CH1) | right motor PWM |
| PB5 / PB7 | right motor BIN1 / BIN2 |
| PB14 / PB15 | left encoder phase A (EXTI) / phase B |
| PB3 / PB4 | right encoder phase A (EXTI) / phase B |
| PB8 / PB9 | I2C1 remapped SCL / SDA → MPU6500 (0x68) |
| PB10 / PB11 | USART3 TX / RX → ECB02 |
| USART2 | VOFA+ telemetry (test programs only) |
blance_car.ioc)Same hardware and no Keil? Download blance_car.hex from the latest release and flash it with STM32CubeProgrammer, ST-Link Utility or OpenOCD (command below).
git clone https://github.com/tthc8t6/blance_car.git
MDK-ARM/blance_car.uvprojx in KeilF7 to build — expect 0 Error(s), 0 Warning(s)F8 to downloadWith OpenOCD:
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
-c "program MDK-ARM/blance_car/blance_car.hex verify reset exit"
[!WARNING] Never halt the CPU with a debugger while the motors are enabled. The timers keep outputting PWM at the last duty cycle during a halt, and the motors run uncontrolled.
Open the web remote (source in tools/rc_web/):
| Platform | Browser | Controls |
|---|---|---|
| Windows / macOS / Android | Chrome, Edge | W A S D, space = stop |
| iPhone / iPad | Bluefy | on-screen stick (Safari has no Web Bluetooth) |
Protocol: write text to characteristic FFF2 of BLE service FFF0, one line every 50 ms:
move <turn> <speed>\n # turn, speed ∈ [-100, 100]
| Argument | Mapping | Max |
|---|---|---|
speed | velocity setpoint = −speed × 0.01 × 0.7 m/s | ±0.7 m/s |
turn | turn setpoint = −turn × 0.01 × 15 rad/s | ±15 rad/s |
The text box can't send \n, so write FFF2 in Hex format:
| Command | Hex |
|---|---|
move 0 50\n | 6D6F766520302035300A |
move 0 0\n | 6D6F7665203020300A |
blance_car/
├── Core/
│ ├── Inc/
│ │ └── task.h # PERIODIC(ms) non-blocking periodic task macro
│ └── Src/
│ ├── main.c # init order + main loop
│ ├── app_control.c # ⭐ balance control: velocity/angle/rate/turn loops + inverse dynamics + fall guard
│ ├── app_motor.c # per-wheel speed PI, voltage compensation duty = U / Vbat
│ ├── app_mpu6500.c # MPU6500 driver + complementary filter + config self-repair
│ ├── app_encoder.c # quadrature decoding + T-method speed + µs timebase
│ ├── app_rc.c # USART3 interrupt RX + move command parser
│ ├── app_pwm.c # duty → H-bridge direction pins + compare registers
│ ├── app_button.c # button debounce + start/stop toggle
│ ├── app_bat.c # battery voltage scaling + LEDs
│ ├── pid.c # generic PID (trapezoidal integral + output clamp)
│ └── quick_math.c # lookup-table sin/cos/atan
├── test/ # per-module bring-up programs (encoder, PWM, IMU, motor PID, math)
├── tools/rc_web/index.html # Web Bluetooth remote
├── skills/ # Claude Code skills used by this project (toolchain, note-taking)
├── CLAUDE.md # project notes for Claude Code
├── MDK-ARM/ # Keil project
└── blance_car.ioc # STM32CubeMX config
Main-loop scheduling:
while (1) {
App_Button_Process(); // button: start/stop + controller reset
App_MPU6500_Process(); // 5 ms: read IMU → complementary filter
App_Control_Process(); // 5 ms: balance control → wheel speed setpoints
App_Motor_Process(); // 1 ms: measure speed → motor PI → PWM
App_RC_Process(); // parse a remote command once a full line arrives
}
Each test function in test/ contains its own infinite loop. Uncomment its call in main.c to debug that module on its own.
Problems hit and solved during development, for anyone building something similar:
Symptom: right after power-on the body vibrates at about 24 Hz (it tingles to the touch), and the battery LEDs drop.
Cause: the MPU6500 may need up to 100 ms after power-on before it accepts writes. The original driver returned on the first failed write, leaving the chip at its default ±250 dps range while the firmware still scaled for ±2000 dps. Every angular rate read 8× too large, the turn loop's effective gain went up 8×, and the system went unstable.
Fix: init no longer returns early; after the 100 ms delay it writes every register. On top of that, GYRO_CONFIG is read back every 100 ms and the chip is reconfigured if it doesn't match.
Diagnostic tip: if the gyro bias at rest is about 8× its usual value, the MPU has lost its config.
HAL_UART_Receive_IT is one-shot — call it again inside every HAL_UART_RxCpltCallback. On an overrun error (ORE), HAL aborts reception and calls HAL_UART_ErrorCallback instead; re-arm there too, or the UART stops receiving for good.
The Cortex-M3 has no FPU. 0.005 is a double and pulls in the software double-precision library. Write every float literal as 0.005f.
The control loop keeps running while the motors are in standby, so with the wheels stopped the integral keeps growing. Two fixes: clamp ω_ref to ±40 rad/s, and call App_Control_Reset() on every button press to clear all PID states and motor setpoints (note that pid_reset does not clear the setpoint).
A HAL_Delay(1000) in the control path freezes the 1 ms motor loop, so the PWM stays at the saturated duty from the moment of the fall and the wheels spin on the ground. Record a timestamp instead and check that the car has stayed upright for 0.5 s.
Bluefy requires the full 128-bit UUID string (0000fff0-0000-1000-8000-00805f9b34fb). The 16-bit form 0xfff0 works in Chrome but fails in Bluefy.
The code written for this project is released under the MIT License. STM32 HAL and CMSIS under Drivers/ keep their own licenses from STMicroelectronics and Arm. The course material credited above belongs to its author and is not included in this repository.
If this project helped you, a ⭐ star is appreciated