从零到一:基于STM32与ULN2003A的PWM直流电机调速系统实战

从零到一:基于STM32与ULN2003A的PWM直流电机调速系统实战 1. PWM基础与STM32实现原理第一次接触PWM调速时我也被那些专业术语搞得一头雾水。直到把直流电机想象成水龙头才恍然大悟——PWM其实就是快速开关水龙头来控制水流大小。具体来说PWM脉冲宽度调制通过调节高电平持续时间脉宽与整个周期时间的比例占空比来控制平均电压。比如50%占空比相当于半开水龙头电机转速就是全速的一半。STM32的通用定时器天生就是为PWM设计的。以TIM3为例它像是个智能秒表TIMx_ARR寄存器决定计数上限相当于水龙头的开关周期TIMx_CCRx寄存器则像开关阀门的手控制着高电平的持续时间。配置时要注意三个关键点时钟树配置APB1总线时钟经过预分频器TIMx_PSC后才是定时器的实际工作频率计数模式向上计数时计数器从0涨到ARR值的过程中会与CCRx值比较输出高低电平极性设置TIMx_CCER寄存器的CCxP位决定有效电平是高还是低实测发现电机控制最好用PWM模式2TIM_OCMode_PWM2配合高电平有效。这样当CNTCCRx时输出高电平更符合常规逻辑。记得开启预装载功能TIM_OCPreload_Enable否则修改CCRx时会立即生效导致脉冲紊乱。2. ULN2003A驱动模块的实战细节很多新手拿到ULN2003A第一反应就是这芯片怎么有16个引脚。其实它内部是7组达林顿管每组都像是一个电流放大器。我拆解过它的工作原理当输入端给高电平时对应输出端会导通到地注意是拉低不是拉高此时若电机正极接电源负极接芯片输出就形成了回路。接线时踩过两个坑必须提醒电机电源一定要独立供电。我曾试图用STM32的3.3V直接驱动结果电机纹波导致单片机不断重启续流二极管必须接好。有次没接二极管电机停转时产生的反向电动势直接烧毁了芯片推荐这样连接ULN2003A的COM脚接电机电源正极5-12V输入脚IN1接STM32的PWM输出如PC7输出脚OUT1接电机负极电机正极直接接电源在COM脚与电机电源间并联100uF电容用万用表测量时会发现个有趣现象当PWM占空比50%时电机两端电压其实是电源电压的一半。这就是PWM调速的本质——用数字信号模拟出模拟电压的效果。3. 完整工程代码剖析下面这个经过实战检验的代码框架已经优化掉了初学者常犯的五个错误// timer.c void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct; TIM_OCInitTypeDef TIM_OCInitStruct; // 时钟使能要放最前面 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_AFIO, ENABLE); // 完全重映射配置必须在GPIO初始化前 GPIO_PinRemapConfig(GPIO_FullRemap_TIM3, ENABLE); // 推挽复用输出才是正确模式 GPIO_InitStruct.GPIO_Pin GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStruct); // 时基配置决定PWM频率 TIM_TimeBaseStruct.TIM_Period arr; // 自动重装载值 TIM_TimeBaseStruct.TIM_Prescaler psc; // 预分频 TIM_TimeBaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStruct); // PWM模式2高电平有效是最佳组合 TIM_OCInitStruct.TIM_OCMode TIM_OCMode_PWM2; TIM_OCInitStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC2Init(TIM3, TIM_OCInitStruct); // 这两行缺一不可 TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }主函数里我设计了渐进式调速方案比简单固定值更实用// main.c #define PWM_MAX 900 // 对应ARR值 #define PWM_MIN 50 // 低于这个值电机可能不启动 void Motor_Speed(uint16_t speed) { if(speed PWM_MAX) speed PWM_MAX; if(speed PWM_MIN speed ! 0) speed PWM_MIN; TIM_SetCompare2(TIM3, PWM_MAX - speed); // 注意这里是PWM_MAX-speed } int main(void) { // 初始化代码... TIM3_PWM_Init(899, 0); // 80kHz PWM频率 while(1) { if(KEY0_Pressed) { // 低速 for(int i0; i300; i10) { Motor_Speed(i); delay_ms(30); } } if(KEY1_Pressed) { // 高速 Motor_Speed(600); } if(KEY_UP_Pressed) { // 急停 Motor_Speed(0); } } }4. 调试过程中的五个经典问题问题1电机发出刺耳噪音但不转动检查项PWM频率是否在10-20kHz之间用示波器看波形解决方法调整TIMx_PSC和TIMx_ARR我常用72MHz/(8991)80kHz问题2调速时电机转速非线性检查项电机负载是否变化轻载时PWM占空比与转速不成正比解决方法在程序里做PWM-转速的映射表实测数据如下占空比实测转速(RPM)补偿值30%12005%50%25002%70%3800-3%问题3ULN2003A发热严重检查项电机电流是否超过500mA单个达林顿管极限解决方法并联使用多个输出通道我在OUT2也接相同电机线问题4按键控制响应迟钝检查项是否在按键检测中用了阻塞式延时解决方法改用状态机模式检测示例代码typedef enum {IDLE, PRESSED, HOLD} KeyState; KeyState keyState IDLE; void Key_Handler(void) { static uint32_t holdTime; switch(keyState) { case IDLE: if(KEY0_Read()0) { keyState PRESSED; holdTime 0; } break; case PRESSED: if(holdTime 10) { // 消抖 keyState HOLD; Motor_Speed(300); } break; case HOLD: if(KEY0_Read()1) { keyState IDLE; } break; } }问题5电机干扰单片机复位检查项电源滤波是否足够解决方法在电机供电端加π型滤波100uF100Ω0.1uFSTM32的复位脚加0.1uF电容到地