1. PWM与电机控制基础
第一次用STM32控制直流电机时,我盯着L298N模块上密密麻麻的接线端子直发懵。直到把PWM信号接上电机突然转起来那一刻,才真正理解什么叫"用数字信号模拟模拟量"。这里说的PWM(脉冲宽度调制),本质上就是通过快速开关电路来控制平均电压。
举个生活例子,就像用自来水龙头给水桶加水。如果一直开着小水流(低占空比),接满一桶水要很久;要是快速开关龙头(高占空比),在相同时间内就能注入更多水。PWM控制电机也是这个原理,只不过这个"开关"动作每秒要重复上千次。
在STM32上实现PWM需要用到定时器外设。以常见的TIM1为例,关键寄存器就两个:
- ARR(自动重装载寄存器):决定PWM周期,好比设定水龙头开关的总时间
- PSC(预分频器):调整计时基准频率,相当于控制每次开关动作的精细程度
实际调试时发现,当PWM频率低于1kHz时,电机运转会有明显抖动和噪音。这是因为人类能听到20Hz-20kHz的声音,低频PWM会落入可听范围。经过多次测试,我把电机控制频率定在16kHz,既避开人耳敏感区,又不会让MOS管过热。
2. L298N模块实战接线
拆开手头的L298N模块,可以看到双H桥电路的核心设计。这个蓝色小板子最让人困惑的就是那六个接线端子:
- 电源端:12V输入接电机电源,5V输出可给STM32供电(但建议独立供电)
- 输出端:OUT1/OUT2和OUT3/OUT4分别控制两个电机
- 控制端:IN1-IN4接STM32的GPIO,ENA/ENB接PWM信号
我踩过的坑是忘记接使能端跳线帽。有次调试半天电机不转,后来发现模块默认需要短接ENA/ENB到5V才能工作。更稳妥的做法是用STM32的PWM引脚直接控制使能端,这样既能调速又能软启动。
接线示范(以TIM3_CH1控制电机A为例):
// CubeMX配置 GPIO_InitStruct.Pin = GPIO_PIN_6; // TIM3_CH1对应PA6 GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);实测中发现,当PWM占空比低于5%时,电机可能出现启动困难。这是因为静摩擦力需要更大启动力矩,解决方法有两种:
- 软件上设置最小启动占空比(如10%)
- 硬件上在电机两端并联104电容消除火花干扰
3. 定时器参数精细调校
调PWM就像调相机光圈,参数组合直接影响最终效果。以TIM1为例,其时钟源通常为72MHz,经过PSC分频和ARR计数后,最终频率计算公式为:
PWM频率 = 时钟频率 / ((PSC + 1) * (ARR + 1))在智能小车项目中,我发现一个实用技巧:先确定目标频率,再反推参数。比如要16kHz的PWM:
- 设PSC=0,得ARR = 72M/16k -1 = 4499
- 但ARR值太大会降低分辨率,改为PSC=4, ARR=899
这里有个容易忽略的细节:计数器模式。STM32的定时器有向上、向下、中央对齐三种计数模式。控制电机推荐用中央对齐模式,能减少电磁噪声。CubeMX里对应选项是"Center-aligned mode 1"。
转速线性化是另一个重点。实测某减速电机在10%占空比时转速为30RPM,90%时280RPM,但中间并非完美线性。我在代码里做了分段补偿:
// 转速-占空比补偿表 const uint16_t speed_comp[] = {0, 5, 15, 30, 50, 75, 100, 130, 165, 200}; void set_motor_speed(uint8_t percent) { if(percent > 100) percent = 100; uint16_t real_duty = speed_comp[percent / 10]; __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, real_duty); }4. 抗干扰与保护机制
实验室里运行良好的代码,到了现场可能问题百出。有次在金属加工车间测试,电机时不时就失控。后来用示波器抓取信号,发现是电火花干扰导致PWM信号毛刺。解决方案是三重防护:
- 硬件滤波:在L298N的输入脚对地加100nF电容
- 软件消抖:检测到异常信号后延迟5ms再响应
- 物理隔离:用磁环套住电机电源线
过流保护也不容忽视。L298N的规格书上标称最大2A电流,但持续工作建议不超过1.5A。我的做法是在电机回路串联0.1Ω采样电阻,通过ADC监测电压:
// 过流检测代码片段 if(HAL_ADC_GetValue(&hadc1) > 1500) { // 1.5A对应150mV HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1); error_handler(ERR_OVER_CURRENT); }遇到最棘手的问题是电机堵转。有次机械臂卡死导致电流骤增,虽然触发了保护,但MOS管已经烧毁。后来改进的方案是加入加速度限制:
// 渐进式加速 void smooth_accel(uint8_t target_speed) { static uint8_t current = 0; while(current != target_speed) { current += (current < target_speed) ? 1 : -1; set_motor_speed(current); HAL_Delay(20); // 每20ms变化1% } }5. 闭环控制进阶实现
开环控制就像蒙眼开车,要想精准还得上编码器。我用的是200线的增量式编码器,通过STM32的TIM2编码器接口模式读取转速。关键配置如下:
// 编码器接口模式初始化 TIM_Encoder_InitTypeDef encoder_config = { .EncoderMode = TIM_ENCODERMODE_TI12, .IC1Polarity = TIM_ICPOLARITY_RISING, .IC2Polarity = TIM_ICPOLARITY_RISING }; HAL_TIM_Encoder_Init(&htim2, &encoder_config);PID算法调参是个经验活。开始用经典Ziegler-Nichols方法整定参数,但实际效果不理想。后来改用试凑法,先调P直到出现振荡,再逐步加入I和D:
// 简化版PID实现 float pid_update(float setpoint, float actual) { static float integral = 0, last_error = 0; float error = setpoint - actual; integral += error * 0.001; // 采样时间1ms float derivative = (error - last_error) / 0.001; last_error = error; return Kp*error + Ki*integral + Kd*derivative; }有个反直觉的现象:降低PWM频率反而能提升低速稳定性。在调试微型机械臂时,将PWM从16kHz降到8kHz后,0-5RPM的低速控制明显更平滑。这是因为开关损耗减少,MOS管能更彻底地导通。