1. 为什么需要双通道PWM控制直流电机
直流电机控制是嵌入式开发中的常见需求,无论是智能小车、机械臂还是工业设备,精准的电机调速都至关重要。传统单路PWM控制虽然简单,但在需要正反转或双电机协同的场景就显得力不从心。这时候,高级定时器的双通道PWM功能就能大显身手。
我做过一个智能窗帘项目,需要电机既能正转打开窗帘,又能反转关闭窗帘。最初尝试用继电器切换极性,结果不仅电路复杂,切换时还有明显的机械冲击声。后来改用双路PWM方案,通过调节两路PWM的占空比差值来实现无级变速和方向控制,效果立竿见影——电机运行平稳安静,控制精度也大幅提升。
STM32的高级定时器(如TIM1/TIM8)相比通用定时器有个独特优势:它们可以生成严格同步的两路PWM,确保频率完全一致,避免因时钟偏差导致的控制误差。实测用TIM1同时驱动两个直流电机时,即使占空比分别调到10%和90%,两路PWM的上升沿仍然完美对齐,这在需要双电机同步的场合特别有用。
2. 硬件设计要点与连接方式
2.1 电机驱动电路选型
直接拿STM32的IO口驱动电机绝对是新手最容易踩的坑。我最早做平衡小车时,曾天真地用IO口直连电机,结果不仅电机纹丝不动,单片机还烫得能煎鸡蛋。后来才明白,单片机引脚驱动能力通常只有几十毫安,而哪怕小型直流电机启动电流都可能达到安培级。
现在常用的方案有三种:
- 分立元件搭建H桥:用MOS管如IR2104配合N沟道/P沟道管,成本低但布线复杂
- 集成驱动芯片:如L298N(最大2A)、DRV8871(3.6A),自带过流保护
- 智能驱动器:如TB6612FNG,集成待机模式和故障检测
以TB6612FNG为例,其典型接线如下:
// STM32引脚连接 PWMA -> TIM1_CH1 PWMB -> TIM1_CH2 AIN1 -> GPIO控制方向 AIN2 -> GPIO控制方向 BIN1 -> GPIO控制方向 BIN2 -> GPIO控制方向 // 电机端连接 VM -> 12V电源 GND -> 共地 AO1/AO2 -> 电机A BO1/BO2 -> 电机B2.2 死区时间设置要点
第一次调试带死区的PWM时,我遇到过MOS管莫名发烫的问题。用示波器抓波形才发现,由于没设置死区,上下管出现了纳秒级的共通现象。高级定时器的死区发生器(Break and Dead-Time)单元就是专门解决这个问题的。
计算死区时间的公式为:
死区时间(ns) = (DTG[7:0] + 1) × Tdts 其中Tdts = 2 × TIMx时钟周期例如72MHz主频下,设置DTG=0x17时:
Tdts = 2 × (1/72MHz) ≈ 27.78ns 死区时间 = (23+1)×27.78 ≈ 666ns在CubeMX中配置时,建议:
- 先测量所用MOS管的开启/关断时间
- 取最大延迟时间加20%余量
- 通过在线计算器生成DTG值
3. CubeMX配置全流程详解
3.1 时钟树配置技巧
很多新手会忽略时钟配置对PWM精度的影响。我曾调试过一个案例,预期生成20kHz PWM,实际测量却是19.23kHz,问题就出在APB2分频设置上。关键点在于:高级定时器挂在APB2总线,其时钟可能经过倍频。
正确步骤应该是:
- 在Clock Configuration页面
- 确认APB2 Timer clocks显示为72MHz(无括号)
- 如果显示36MHz(x2),需要调整分频系数
- 保证最终定时器时钟=72MHz
3.2 定时器参数设置
配置TIM1生成两路20kHz PWM的典型参数:
- Prescaler: 0 (不分频)
- Counter Mode: Up
- Counter Period: 3599 (72000000/20000 - 1)
- AutoReload Preload: Enable
- CH Polarity: High (根据驱动芯片需求调整)
通道配置特别注意:
- PWM Generation CH1/CH2
- Mode: PWM mode 1
- Pulse: 初始占空比,如1800(50%)
- Fast Mode: Disable (除非需要ns级响应)
- CH Output: Enable
有个容易遗漏的设置:在Parameter Settings页最下方,需要勾选"Advanced configuration"才能看到Break and Dead-Time配置选项。
4. 代码实战与调试技巧
4.1 HAL库电机控制函数封装
CubeMX生成的代码虽然能用,但直接操作寄存器效率更高。这是我常用的控制函数:
// 设置PWM占空比 void Motor_SetDuty(TIM_HandleTypeDef *htim, uint32_t Channel, float duty) { uint32_t pulse = (htim->Instance->ARR + 1) * duty / 100; __HAL_TIM_SET_COMPARE(htim, Channel, pulse); } // 电机停止(刹车) void Motor_Brake(TIM_HandleTypeDef *htim1, TIM_HandleTypeDef *htim2) { HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_2); // 将驱动芯片IN引脚置为相同电平 HAL_GPIO_WritePin(IN1_GPIO_Port, IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(IN2_GPIO_Port, IN2_Pin, GPIO_PIN_SET); } // 电机正转(示例占空比30%) void Motor_Forward(TIM_HandleTypeDef *htim) { Motor_SetDuty(htim, TIM_CHANNEL_1, 30); Motor_SetDuty(htim, TIM_CHANNEL_2, 0); }4.2 示波器调试实战
没有示波器调试PWM就像闭着眼睛开车。分享几个实测技巧:
- 测量频率:将时基调到50μs/div,观察10个周期是否占5大格(20kHz时)
- 检查死区:触发模式设为Normal,边沿触发,放大观察上升/下降沿交界处
- 动态调整:运行中旋转编码器,观察占空比变化是否线性
常见问题排查:
- 无输出:检查TIMx_CR1寄存器的CEN位是否置1
- 频率偏差:重新计算ARR值,确认时钟源正确
- 占空比异常:检查CCRx寄存器是否被意外修改
5. 进阶应用:PID闭环控制
开环控制总会有速度波动,特别是负载变化时。给电机加装编码器后,可以用PID算法实现精准调速。这里分享一个经过实测的简化PID实现:
typedef struct { float Kp, Ki, Kd; float integral; float prev_error; } PID_Controller; float PID_Update(PID_Controller *pid, float setpoint, float measurement) { float error = setpoint - measurement; pid->integral += error; if(pid->integral > 1000) pid->integral = 1000; if(pid->integral < -1000) pid->integral = -1000; float derivative = error - pid->prev_error; pid->prev_error = error; return pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; } // 在定时器中断中调用 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim == &htim3) { // 假设编码器使用TIM3 int16_t cnt = __HAL_TIM_GET_COUNTER(&htim3); __HAL_TIM_SET_COUNTER(&htim3, 0); float speed = cnt * 60.0 / (ENCODER_PPR * 4); // 转/分 float duty = PID_Update(&pid, target_speed, speed); Motor_SetDuty(&htim1, TIM_CHANNEL_1, duty); } }调试PID参数时,建议先用Ziegler-Nichols方法初步确定参数范围,然后:
- 先调Kp直到出现小幅振荡
- 加入Kd抑制振荡
- 最后加Ki消除静差
- 实际测试时,建议从较小目标速度(如30RPM)开始调参