一句话 :直流有刷电机的转速只由电枢端电压 决定,所以"调速"就是"调平均电压 "。 单片机没法直接调电压,于是用 PWM 斩波 + H 桥把电池的固定电压切成可调的平均电压。
0. 速查表
| # | 环节 | 干什么 | 关键量 | 代码位置 |
|---|---|---|---|---|
| 1 | 电机本体 | 电压 → 转速 | e = Ke·ω,空载 ω = Ua/Ke |
--- |
| 2 | H 桥 | 换向(电压极性可正可负) | IN1/IN2 两根 GPIO |
App_PWM_Set_L/R() |
| 3 | PWM | 把固定 Vbat 切成可调平均电压 |
Ua = D·Vbat,72kHz、1000 级 |
TIM1/TIM4 初始化 |
| 4 | 占空比 | 期望电压 → 占空比 | D = Va/Vbat×100,单位 % |
App_PWM_Set_L/R() 入参 |
信号链(自上而下):
期望的平均电压 Ua (V)
└─► [÷ 电池电压 Vbat] ──► Duty (%, ±100)
└─► [两根方向 GPIO + PWM 占空比] ──► TB6612 内部 H 桥 ──► 电机 ──► 转速 ω
1. 调速到底在调什么
1.1 电机的电学方程
电机拆开看是"一半电路、一半机械",电学方程把电和转连起来(3.1 章)。
电学方程:
Ua = Ra·Ia + La·(dIa/dt) + e e = Ke·ω
| 符号 | 含义 | 符号 | 含义 |
|---|---|---|---|
| Ua | 加在电枢两端的电压 | Ia | 电枢电流 |
| Ra | 电枢电阻 | La | 电枢电感 |
| e | 反电动势(自己发出来顶住电源的电压) | Ke | 反电动势常数 V/(rad/s) |
| ω | 转子角速度 | Kt | 扭矩常数 N·m/A |
(F = B·L·I)就是这个链路的物理源头: 电压 → 电流 → 电磁力 → 转子转起来 → 转起来以后又产生反电动势 e = Ke·ω, 转速越高反电动势越大,把电流顶回去,最后停在某个平衡点上。这个"自平衡"就是转速由电压唯一确定的原因。
1.2 稳态:转速由端电压决定
电机稳速旋转时 dIa/dt → 0,电学方程退化成:
Ua = Ra·Ia + Ke·ω
空载时 Ia 很小、Ra·Ia 这一项可以忽略,于是:
ω ≈ Ua / Ke
也就是说,转速最终是 Ua 的函数 :电压越高、空载转速越大;电流越大、输出扭矩越大(这两条就是 3.1 章第一页的结论)。想改转速,就去改 Ua。
带载时电流增大、Ra·Ia 压降变大,转速会比空载低一截;而且负载一变电流就变、转速跟着变。这正是 1.3 节说的"机械特性变软"的由来。
1.3 为什么不能"串个电阻"调速
既然转速由电压决定,串电阻分压不就行了?不行,三个原因:
-
电阻上白烧功率 ,
P = I²R全变成热,电池续航和驱动板都扛不住; -
机械特性变软:串了 R 以后,负载一变电流就变,电阻上的压降跟着变,转速跟着飘;
-
没法反向:串电阻只能降速,不能换向,而平衡车必须要能反转。
1.4 结论
调速 = 调加在电机上的平均电压。 实现手段:用开关管快速通断(PWM)切出可调的平均电压;用电桥(H 桥)切换极性实现正反转。
2. 怎么产生可调的平均电压:PWM + H 桥
2.1 PWM 调压:用"时间占比"代替"阻值"
管子只有"通"和"断"两个状态,通的时候管子内阻极小、几乎不发热。于是让它在一个周期 T 内导通 D·T 的时间:
Vbat ────────┐ ┌────────────┐
│ │ │
0 └──────────────┘ └────
←──── D·T ────→←─ (1-D)·T ──→
←───────────── T ─────────────→
平均电压 Ua = D · Vbat (D = 占空比,0 ~ 1)
Vbat+ / Vbat- 表示 H 桥给电机端加的是正接还是反接,所以 D 取正负号就能表达"正转多快 / 反转多快"。
2.2 H 桥:让电压能正能负
4 个开关排成"H"形,电机横在中间(3.1 章 slide 4/5):
Vbat+
│
┌────┴────┐
Q1 Q2
│ │
├───[ M ]──┤ M = 电机
│ │
Q3 Q4
└────┬────┘
│
Vbat-
| 想要 | 导通的管子 | 电机端电压 |
|---|---|---|
| 正转 | Q1 + Q4 | +Vbat |
| 反转 | Q2 + Q3 | −Vbat |
| 停(刹车/短路) | Q1+Q2 或 Q3+Q4 | 0(绕组被短路) |
| 停(滑行/高阻) | 全断 | 悬空 |
注意 Q1 和 Q3 不能同时导通 (会把 Vbat 直接短路,叫"直通"), 所以真正的 H 桥驱动都需要死区时间。本工程用的是集成芯片,这部分由芯片内部处理。
2.3 本工程用的是 TB6612FNG
板上用的是 TB6612FNG : 芯片内部就是两路完整的 H 桥,还带欠压保护(UVLO)、过热保护(TSD)和待机引脚(STBY)。
芯片的输入引脚分成两组:
| 引脚 | 作用 |
|---|---|
PWMA / PWMB |
该路的速度(PWM 占空比) |
AIN1/AIN2、BIN1/BIN2 |
该路的方向(哪两个管子导通) |
| STBY | 总开关。拉低 → 整片待机,输出全关 |
AO1/AO2、BO1/BO2 |
接电机的两根线 |
所以单片机只需要给:一路 PWM + 两根方向 GPIO + 一根 STBY,H 桥的四管时序和死区都不用管。
2.4 频率怎么选:电感 La 决定了电流纹波
PWM 加在电机上,电机不是纯电阻------La 会把电流"稳住",所以电流并不会像电压那样方波跳动。 用三张图讲这件事:

| 图 | 情况 | 电流长什么样 |
|---|---|---|
| 图 1 | 忽略 La | 电流和电压同形状,方波 |
| 图 2 | 考虑 La,PWM 频率低 | 电流上冲下掉,纹波大,甚至断续(CCM → DCM) |
| 图 3 | 考虑 La,PWM 频率高 | 电流被电感滤得很平,近似直流 |
时间常数 τe = La/Ra 是分界线:频率远高于 1/(2π·τe) 时电流才够平。 本工程电机 La ≈ 1.5mH, Ra ≈ 3.9Ω → τe ≈ 384µs → 转折频率约 400Hz。
本工程选的 PWM 频率:
f = 72MHz / ((PSC+1) × (ARR+1)) = 72MHz / (1 × 1000) = 72kHz
-
72kHz 比 400Hz 高两个数量级 → 电流纹波很小,近似"平滑直流"(图 3 那种);
-
72kHz 远高于人耳听觉上限 20kHz → 没有啸叫(低于 20kHz 的 PWM 电机是会有"吱吱"声的);
-
代价是开关损耗,但 TB6612 允许到 100kHz,72kHz 留有余量。
⚠️ 坑 :TIM4 挂在 APB1 上,APB1 只有 36MHz。但 STM32 的规则是"APB 分频系数 ≠ 1 时定时器时钟翻倍", 所以 TIM4 实际也是 72MHz。TIM1 挂 APB2(72MHz)也是 72MHz。两路 PWM 频率相同、不用分开算。
2.5 分辨率、死区与量纲
| 项 | 值 | 说明 |
|---|---|---|
| 计数范围 | CNT: 0 ~ ARR = 999 | 共 1000 级 |
| 分辨率 | 1/1000 = 0.1% | 电压分辨率 = 0.1% × Vbat(满电 8.2V 时约 8.2mV) |
| 周期 | 1/72kHz = 13.89µs | |
| 最小非零占空比 | CCR=1 → 0.1% |
低占空比死区 :占空比小于某个值(几 %)时,平均电压太低,产生的电磁转矩 不足以克服静摩擦和 H 桥压降,电机根本不转。调参和标定"占空比 → 转速"曲线时, 这一段是要单独留意的(曲线不是从原点起的直线)。
3. 代码实现:app_pwm.c
3.1 引脚映射
| 引脚 | 接哪里 | 配置 | 代码 |
|---|---|---|---|
| PA8 | 左电机 PWMA | AF_PP(TIM1_CH1) |
PWM_L_Init() |
PA9 / PA10 |
左电机 AIN1 / AIN2 | Out_PP | App_PWM_Set_L() |
| PA1 | STBY | Out_PP | StbyPin_Init() |
| PB6 | 右电机 PWMB | AF_PP(TIM4_CH1) |
PWM_R_Init() |
PB5 / PB7 |
右电机 BIN1 / BIN2 | Out_PP | App_PWM_Set_R() |
3.2 初始化:6 步
以左电机(PWM_L_Init())为例,右电机完全对称,只是 TIM1 → TIM4、APB2 → APB1:
// #1. 初始化IO引脚:PA9、PA10 普通推挽输出(方向)
// #2. 使能定时器1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
// #3. 配置定时器的基本参数
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseInitStruct.TIM_Period = PERIOD; // ARR = 999 ← 决定频率和分辨率
TIM_TimeBaseInitStruct.TIM_Prescaler = 0; // PSC = 0 ← 不分频
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
// #4. 配置输出比较:PWM1 模式,高电平有效
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStruct.TIM_OutputState = ENABLE;
TIM_OCInitStruct.TIM_Pulse = 0; // 初始 CCR = 0 → 0%
TIM_OC1Init(TIM1, &TIM_OCInitStruct);
// 然后 PA8 配成 AF_PP(复用推挽),作为定时器输出脚
// #5. 开启CCR的预加载 ← ⚠️ 见 3.5 节的坑
TIM_CCPreloadControl(TIM1, ENABLE);
// #6. 闭合总开关
TIM_Cmd(TIM1, ENABLE);
TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须开 MOE,否则没有输出
PWM1 模式(3.2 章 slide 11 那张模式表)的含义:
CNT < CCR → OCxRef = 高
CNT ≥ CCR → OCxRef = 低
所以 CCR 就是高电平持续的计数个数,占空比 D = CCR/1000。
3.3 设置占空比
c
void App_PWM_Set_L(float Duty)
{
if(Duty > 100) Duty = 100; // ① 限幅
if(Duty < -100) Duty = -100;
if(Duty >= 0) // ② 方向:两根 GPIO
{
GPIO_WriteBit(GPIOA, GPIO_Pin_10, Bit_SET);
GPIO_WriteBit(GPIOA, GPIO_Pin_9, Bit_RESET);
}
else
{
GPIO_WriteBit(GPIOA, GPIO_Pin_10, Bit_RESET);
GPIO_WriteBit(GPIOA, GPIO_Pin_9, Bit_SET);
}
// ③ 大小:百分比 → 计数值
TIM_SetCompare1(TIM1, (uint16_t)(fabsf(Duty) / 100.0f * (PERIOD + 1)));
}
三个要点:
-
符号拆成两件事:符号 → 方向 IO,绝对值 → 占空比。芯片替你完成"哪两个管子导通"。
-
PERIOD + 1而不是PERIOD:Duty = 100时CCR = 1000 > ARR = 999,
保证 CNT < CCR 恒成立 → 输出恒高 、真正 100%。若写成 ×999,CCR = 999 时 CNT = 999 那一拍会掉低,实际只有 99.9%。
- 限幅放在函数内部:入参单位是百分比,调用方把"想给的电压"换算成占空比时要除以电池电压。
电池电压掉下来时(比如想给 8.2V 而电池只剩 6.6V),算出来的占空比会到 124%, 必须在这里兜住------限幅的责任在 PWM 这一层,不在调用方。
3.4 开环实验
**① 最原始的验证,直接给固定占空比:
App_PWM_Init();
App_PWM_Cmd(1); // 拉高 STBY,唤醒
App_PWM_Set_L(30); App_PWM_Set_R(30); Delay(2000); // 30% 转 2 秒
App_PWM_Set_L(60); App_PWM_Set_R(60); Delay(2000); // 60% 转 2 秒
App_PWM_Set_L(90); App_PWM_Set_R(90); Delay(2000); // 90% 转 2 秒
App_PWM_Cmd(0); // 拉低 STBY,休眠
现象:占空比越大,轮子转得越快 ------ 这就是"调平均电压"的直观验证。
②改成了"给固定电压**":
// 正转3s:给 3V
App_PWM_Set_L( 3 / bat * 100.0f); // ← 占空比 = 3V / 电池电压
App_PWM_Set_R( 3 / bat * 100.0f);
// 反转3s:给 -3V(只是把符号取反)
App_PWM_Set_L(-3 / bat * 100.0f);
这一步演进很关键 :从"给固定占空比"变成"给固定电压"。 电池从 8.2V 掉到 6.6V,同样 80% 的占空比电压就从 6.56V 掉到 5.28V; 改成 3/bat*100 之后,无论电池多少伏,电机端都是 3V。 这就是 Ua = D·Vbat 这个式子的直接应用 :把想给的 Ua 反解成 D。
两个开环实验的共同结论:转速能调,但调不准 。 换一块电池、或者用手捏一下轮子,同样的占空比转速就变了------因为开环的 D 里没有"转速"这个量的任何反馈。