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

📅 2026/8/2 1:06:01 👁️ 阅读次数 📝 编程学习
从零到一:基于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模式2(TIM_OCMode_PWM2)配合高电平有效。这样当CNT<CCRx时输出高电平,更符合常规逻辑。记得开启预装载功能(TIM_OCPreload_Enable),否则修改CCRx时会立即生效导致脉冲紊乱。

2. ULN2003A驱动模块的实战细节

很多新手拿到ULN2003A第一反应就是"这芯片怎么有16个引脚?"。其实它内部是7组达林顿管,每组都像是一个电流放大器。我拆解过它的工作原理:当输入端给高电平时,对应输出端会导通到地(注意是拉低不是拉高!),此时若电机正极接电源,负极接芯片输出,就形成了回路。

接线时踩过两个坑必须提醒:

  1. 电机电源一定要独立供电。我曾试图用STM32的3.3V直接驱动,结果电机纹波导致单片机不断重启
  2. 续流二极管必须接好。有次没接二极管,电机停转时产生的反向电动势直接烧毁了芯片

推荐这样连接:

  • 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 i=0; i<300; i+=10) { 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/(899+1)=80kHz

问题2:调速时电机转速非线性

  • 检查项:电机负载是否变化,轻载时PWM占空比与转速不成正比
  • 解决方法:在程序里做PWM-转速的映射表,实测数据如下:
占空比实测转速(RPM)补偿值
30%1200+5%
50%2500+2%
70%3800-3%

问题3:ULN2003A发热严重

  • 检查项:电机电流是否超过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:电机干扰单片机复位

  • 检查项:电源滤波是否足够
  • 解决方法:在电机供电端加π型滤波(100uF+100Ω+0.1uF),STM32的复位脚加0.1uF电容到地