STM32F407 实战排错从定时器到 PWM 电机调速
PWM 调速是一条完整链路
PWM 调速不是只改一个 CCR 寄存器就结束了。它至少包含定时器时钟、计数周期、输出比较、GPIO 复用、外部中断、软件状态和电机驱动板七个环节。任何一处对象不一致,都会出现“编译正常、运行无效”。
按键 PE2/PE3
↓ 下降沿
EXTI2/EXTI3 中断
↓ 修改 count
TIM_SetCompare1(TIM3, count)
↓ 更新 TIM3_CH1 的 CCR1
PC6 输出 PWM → TB6612FNG 的 PWMA → 电机转速变化
1. 定时器与 PWM:先把三个数算明白
定时器本质上是由时钟驱动的计数器。PSC 决定计数速度,ARR 决定一个周期包含多少个计数,CCR 决定比较点。对于向上计数的 PWM1 模式和高电平有效极性,通常可近似理解为 CNT < CCR 时输出有效电平。
|
核心公式 计数频率 = TIMxCLK / (PSC + 1);PWM 频率 = TIMxCLK / [(PSC + 1) × (ARR + 1)];占空比约为 CCR / (ARR + 1)。 |
本工程使用 STM32F407 的 TIM3。若 APB1 定时器时钟为 84 MHz,PSC = 84 - 1,ARR = 100 - 1,则:
计数频率 = 84 MHz / 84 = 1 MHz
PWM 频率 = 1 MHz / 100 = 10 kHz
CCR = 50 时,占空比 = 50 / 100 = 50%
这也是为什么 PSC 和 ARR 的公式里都有“+1”:寄存器保存的是从 0 开始计数的终值。忽略这个细节,会造成频率或边界差一拍。
2. 工程里的硬件与软件对应关系
|
功能 |
工程配置 |
必须保持一致的对象 |
|
PWM 输出 |
TIM3_CH1 / PC6 / AF2 |
TIM3 时钟、TIM3 初始化、CH1、PC6 复用 |
|
加速按键 |
PE3 / EXTI3 / 下降沿 |
GPIOE 上拉、EXTI Line3、EXTI3_IRQn |
|
减速按键 |
PE2 / EXTI2 / 下降沿 |
GPIOE 上拉、EXTI Line2、EXTI2_IRQn |
|
电机方向 |
PB3、PB5 |
GPIOB 时钟、GPIOB 初始化、中断里操作 GPIOB |
|
电机驱动 |
TB6612FNG |
PWMA 有 PWM、STBY 为高、VM/VCC 正确、与 MCU 共地 |
3. 这次实际遇到的错误
|
现象 |
表现 |
根因 |
修复原则 |
|
链接失败 |
Exti_Init_key_* 未定义 |
声明/调用使用 key,实现使用 Key |
统一函数名大小写 |
|
PWM 无输出 |
TIM3 已配置但无波形 |
开启了 TIM2 时钟却操作 TIM3 |
时钟、实例、通道保持一致 |
|
方向控制无效 |
PB3/PB5 不动作 |
开启 GPIOB 时钟,却初始化 GPIOF |
时钟端口与 GPIO_Init 端口一致 |
|
按键无效 |
EXTI 没有有效边沿 |
Key_Init 未调用或 key.c 未加入工程 |
先配置 GPIO 上拉,再配置 EXTI |
|
首次按键无效 |
PWM 从 50% 启动但 count 为 0 |
软件状态与 CCR 初值不同步 |
count 与 TIM_Pulse 使用同一初值 |
|
到边界后卡住 |
100 不能减、0 不能加 |
外层条件提前排除了边界值 |
减速判断 >=10,加速判断 <=90 |
3.1 链接错误:函数名大小写不一致
最开始的 Keil 链接器提示 Undefined symbol Exti_Init_key_0、Exti_Init_key_2、Exti_Init_key_3。原因不是 exti.c 没有编译,而是 C 语言标识符区分大小写:调用的是 key,实现的是 Key。
// 错误:声明和调用
Exti_Init_key_0();
// 实现
void Exti_Init_Key_0(void)
这类问题发生在链接阶段:编译器看到头文件声明后允许 main.c 通过,但链接器找不到完全同名的实现。排查时应同时搜索声明、调用和定义,不要只看文件是否加入工程。
3.2 定时器对象错位:开 TIM2,却配置 TIM3
PWM 初始化曾开启 RCC_APB1Periph_TIM2,但后续调用 TIM_TimeBaseInit(TIM3)、TIM_OC1Init(TIM3)。外设时钟没有打开时,寄存器配置不会产生预期效果。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
TIM_Cmd(TIM3, ENABLE);
|
排错习惯 看到 TIMx 无输出时,把 RCC 时钟、初始化函数、比较函数、中断函数和 GPIO 复用中的 x 全部圈出来,逐个确认是不是同一个定时器。 |
3.3 GPIO 时钟与端口错位
方向控制代码操作 PB3/PB5,但 LED_Init 曾开启 GPIOB 时钟后调用 GPIO_Init(GPIOF, ...)。这会让 PB3/PB5 保持复位状态。GPIO 的时钟端口、初始化端口和后续读写端口必须是同一个 GPIOx。
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_5;
GPIO_Init(GPIOB, &GPIO_InitStructure);
3.4 按键中断配置了,但 GPIO 没准备好
EXTI 只负责把某个 GPIO 线映射为中断源,它不会替代 GPIO 输入初始化。PE2/PE3/PE4 需要 GPIOE 时钟和内部上拉;PA0 需要与实际按键电路匹配的上下拉。Key_Init 必须被调用,key.c 也必须加入 Keil 工程。
PE2/PE3 使用下降沿触发意味着按键按下时应把引脚从高电平拉到低电平。每次再次触发前必须先松开按键,让引脚回到高电平。
3.5 PWM 初值与 count 不同步
PWM 初始化把 TIM_Pulse 设为 50,但 count 一度初始化为 0。此时示波器看到的是 50% PWM,软件却认为当前占空比是 0。后续所有边界判断都会基于错误状态。
#define PWM_MIN 0U
#define PWM_MAX 100U
#define PWM_STEP 10U
#define PWM_START 50U
volatile uint32_t count = PWM_START;
TIM_OCInitStructure.TIM_Pulse = PWM_START;
由中断修改、主程序或调试器可能读取的共享变量建议声明为 volatile,避免编译器把读取结果长期缓存。当前主循环没有读取 count 时它不是故障主因,但这是更稳妥的接口约定。
3.6 最隐蔽的问题:边界值被外层条件排除
为了处理 0 和 100,代码加入了嵌套判断,但外层条件 count >= 10 && count <= 90 已经先把两个边界排除了。于是 count == 100 时不能减速,count == 0 时不能加速。内层的 count == 100 和 count == 0 永远不可达。
// 错误示例:count == 100 永远进不来
if (count <= 90 && count >= 10) {
if (count == 100) {
count -= 10;
}
}
// 正确:只判断本次操作会不会越界
if (count >= PWM_STEP) {
count -= PWM_STEP;
TIM_SetCompare1(TIM3, count);
}
if (count <= PWM_MAX - PWM_STEP) {
count += PWM_STEP;
TIM_SetCompare1(TIM3, count);
}
|
边界设计原则 不要判断“当前值是不是普通值”,而要判断“执行这次加减后会不会越界”。这样 100 可以减到 90,0 可以加到 10,同时不会发生无符号下溢或超过上限。 |
4. 推荐的按键调速实现
下面的写法让软件状态、硬件 CCR 和边界规则保持一致。若 TIM_Period = 99,则一个周期有 100 个计数,CCR 在 0~100 之间可表达 0%~100% 的有效时间;具体在 CCR = ARR + 1 时的输出行为仍应结合芯片参考手册和实际波形确认。
#define PWM_MIN 0U
#define PWM_MAX 100U
#define PWM_STEP 10U
#define PWM_START 50U
volatile uint32_t count = PWM_START;
void EXTI2_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line2) != RESET)
{
if (count >= PWM_STEP)
{
count -= PWM_STEP;
TIM_SetCompare1(TIM3, count);
}
EXTI_ClearITPendingBit(EXTI_Line2);
}
}
void EXTI3_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line3) != RESET)
{
if (count <= PWM_MAX - PWM_STEP)
{
count += PWM_STEP;
TIM_SetCompare1(TIM3, count);
}
EXTI_ClearITPendingBit(EXTI_Line3);
}
}
5. 中断里还容易踩的坑
5.1 中断标志应在对应分支内清除
先确认 EXTI_GetITStatus 返回 SET,再处理并清除对应挂起位。不要误清其他 EXTI Line,也不要漏清,否则可能重复进入中断。
5.2 机械按键需要消抖
一次按下可能产生多次快速边沿,表现为速度一下跳多个档位。简单实验可以在中断中做很短的屏蔽或用定时器延后确认;更推荐中断只记录事件,主循环按时间戳完成消抖,避免在 ISR 中长时间 Delay 或等待松手。
5.3 不要在 ISR 中做阻塞操作
中断服务函数应尽量短。循环等待按键释放、长延时或复杂打印都会阻塞其他中断,使系统出现抖动、丢事件或响应异常。
5.4 NVIC 优先级要与系统整体匹配
工程使用 NVIC_PriorityGroup_2。配置抢占优先级和响应优先级时,应确保取值范围符合当前分组,并避免多个实时性不同的中断全部使用完全相同的优先级。
6. 电机不转时,别只盯着软件
TB6612FNG 负责把 MCU 的逻辑信号放大为可驱动电机的电压和电流。即使 PC6 已经输出正确 PWM,下面任何一个条件不满足,电机仍可能不转:
- STBY 必须为高电平,否则驱动器处于待机状态。
- VM 是电机电源,VCC 是逻辑电源,两者不能混淆。
- 驱动板 GND 必须与 STM32 GND 共地,否则 PWM 没有共同参考电位。
- AIN1/AIN2 决定方向和制动状态,不能处于禁止输出的组合。
- PWMA 必须连接到实际输出 PWM 的 PC6/TIM3_CH1,而不是只连接方向引脚。
- 电机是感性大电流负载,不能直接由 MCU GPIO 驱动;电源也必须具备足够电流能力。
- 占空比很低时,电机可能因静摩擦和负载而无法启动,这不一定是 PWM 没有变化。
7. 排错顺序
- 先编译并查看链接结果。Undefined symbol 优先检查函数名、声明、定义和工程文件列表。
- 用示波器或逻辑分析仪测 PC6。没有波形时先查 TIM3 时钟、AF2、通道和 GPIO 模式。
- 在 EXTI2/EXTI3 中断入口切换一个空闲 GPIO,确认按键是否真的产生中断。
- 在调试器 Watch 窗口观察 count,并同时查看 TIM3->CCR1,确认软件值是否写入硬件。
- 从 50 开始依次测试 60、70、80、90、100,再反向测试 90;从 0 测试能否回到 10。
- 最后连接驱动板和电机,核对 STBY、方向、电源、共地与负载。先证明 PWM,再证明驱动链。
结语
这次问题最有价值的地方,不是某一个 if 条件写错,而是它揭示了嵌入式调试的基本方法:把系统拆成信号链,逐级确认“时钟是否到达、寄存器是否更新、引脚是否输出、外设是否响应”。只要对象命名一致、软件状态与硬件状态同步、边界条件覆盖完整,再复杂的 PWM 调速问题也能快速收敛。
更多推荐


所有评论(0)