一句话:直流有刷电机的转速只由电枢端电压决定,所以"调速"就是"调平均电压"。 单片机没法直接调电压,于是用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) = 72kHz72kHz 比 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 设置占空比
voidApp_PWM_Set_L(floatDuty){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里没有"转速"这个量的任何反馈。