从定时器到电机驱动:基于STM32F103C8T6标准外设库的PWM实战解析

张开发
2026/4/16 12:28:37 15 分钟阅读

分享文章

从定时器到电机驱动:基于STM32F103C8T6标准外设库的PWM实战解析
1. STM32定时器基础与PWM原理第一次接触STM32的定时器时我完全被那些专业术语搞晕了。后来才发现定时器本质上就是个电子秒表只不过这个秒表功能强大到离谱。以STM32F103C8T6为例它内置了11个定时器就像你手机里可以同时运行多个计时器APP一样。最常用的通用定时器TIM2-TIM5有三个关键部件预分频器PSC、计数器CNT和自动重装载寄存器ARR。想象你在玩音乐节拍器PSC决定节拍速度72MHz时钟分频后得到1MHzCNT就是当前拍数从0数到65535ARR则是总拍数比如设为999就是数到1000拍循环。这三个部件配合就能精确控制时间。PWM原理更像是个智能开关通过快速开关来控制平均功率。比如用50%占空比的PWM驱动电机相当于每秒开关各占一半时间电机就以半速运行。STM32的捕获比较寄存器CCR就是这个开关的分界线当CNTCCR输出高电平CNTCCR输出低电平。通过调整CCR值就能像调光台灯一样无级调节电机转速。2. 硬件连接与工程搭建我的实验设备很简单一块蓝色pill开发板STM32F103C8T6核心、L298N电机驱动模块、小风扇电机。连线时特别注意PWM输出引脚必须用带定时器通道功能的比如PA0TIM2_CH1。有次我错接到PA1调试半天才发现引脚不对。使用标准外设库时建议用STM32CubeMX生成基础代码框架。我习惯这样组织工程文件Project/ ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── Src/ │ ├── main.c │ ├── pwm.c │ └── motor.c └── Inc/ ├── pwm.h └── motor.h在pwm.h中定义关键参数#define PWM_FREQ 1000 // PWM频率1kHz #define MAX_DUTY 100 // 最大占空比100%3. 定时器配置实战配置TIM2生成PWM需要五步走我把它总结为时钟树之旅开启时钟就像给水管注水RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);GPIO初始化设置PA0为复用推挽输出GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);时基配置设定计时器的心跳节奏TIM_TimeBaseInitStructure.TIM_Period 100 - 1; // ARR值 TIM_TimeBaseInitStructure.TIM_Prescaler 720 - 1; // 72MHz/720100kHz TIM_TimeBaseInit(TIM2, TIM_TimeBaseInitStructure);PWM输出配置选择PWM1模式TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_Pulse 50; // 初始占空比50% TIM_OC1Init(TIM2, TIM_OCInitStructure);启动定时器打开总闸门TIM_Cmd(TIM2, ENABLE);实测时发现PSC和ARR的值会影响PWM分辨率。比如ARR100时占空比调节精度是1%而ARR1000时精度达到0.1%但频率会降低。我的经验公式PWM频率 72MHz / (PSC1) / (ARR1)4. 电机驱动实现技巧电机控制最怕遇到癫痫式启停。我的解决方案是软启动像汽车缓踩油门void Motor_SoftStart(uint8_t targetSpeed) { for(uint8_t i0; itargetSpeed; i) { PWM_SetCompare1(TIM2, i); Delay_ms(10); } }转向控制用两个GPIO组成H桥控制// 正转 GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); // 反转 GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1);抗干扰在电机电源端并联100uF电解电容0.1uF陶瓷电容能有效消除电火花干扰。遇到电机抖动时检查PWM频率是否过低建议1-20kHz电源功率是否充足电机驱动模块是否过热5. 高级控制按键与呼吸灯效果通过按键控制电机就像给玩具车装遥控器。我设计了三档调速void Motor_ControlByKey(uint8_t key) { static uint8_t speedLevel 0; switch(key) { case KEY1: // 加速 speedLevel (speedLevel 1) % 3; break; case KEY2: // 停止 speedLevel 0; break; } uint16_t speedMap[] {0, 50, 100}; Motor_SetSpeed(speedMap[speedLevel]); }呼吸灯效果更酷炫原理是让CCR值像正弦波变化void Motor_Breathing(void) { for(uint16_t i0; i100; i) { TIM_SetCompare1(TIM2, i); Delay_ms(20); } for(uint16_t i100; i0; i--) { TIM_SetCompare1(TIM2, i); Delay_ms(20); } }调试时发现Delay_ms时间太短会导致电机响应迟钝太长又会出现卡顿。最终测试20ms间隔最平滑这个值跟PWM周期和电机惯性都有关。6. 常见问题排查指南踩过无数坑后我整理了这个求生手册无PWM输出检查GPIO是否配置为复用功能确认定时器时钟已开启用示波器测量引脚别相信LED测试电机不转测量驱动模块供电电压检查使能引脚是否拉高尝试直接给电机供电排除驱动问题转速不稳定检查电源功率是否足够尝试增加PWM频率在电机两端并联续流二极管代码卡死检查ARR是否为0确认没有在中断里死循环查看堆栈是否溢出有个特别隐蔽的bug当PSC设置过大导致计数器频率低于1Hz时电机完全没反应。后来用这个公式验证计数器频率 72MHz / (PSC1)7. 性能优化与扩展思路想让电机控制更专业试试这些进阶技巧闭环控制通过编码器反馈实现精准调速// 伪代码示例 void TIM3_IRQHandler() { if(检测到编码器脉冲) { 计算实际转速; 调整CCR值补偿误差; } }PID算法像自动驾驶一样平滑控制typedef struct { float Kp, Ki, Kd; float error, lastError, integral; } PID; float PID_Calculate(PID* pid, float target, float actual) { pid-error target - actual; pid-integral pid-error; return pid-Kp * pid-error pid-Ki * pid-integral pid-Kd * (pid-error - pid-lastError); }多电机同步使用主从定时器模式// 配置TIM2为主模式 TIM_SelectMasterSlaveMode(TIM2, TIM_MasterSlaveMode_Enable); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 配置TIM3为从模式 TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Trigger);最后分享一个实用技巧用SysTick定时器做微秒级延时比Delay_ms精准得多void Delay_us(uint32_t us) { SysTick-LOAD 72 * us; // 72MHz时钟 SysTick-VAL 0; SysTick-CTRL SysTick_CTRL_ENABLE_Msk; while(!(SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk)); SysTick-CTRL 0; }

更多文章