资讯动态

STM32 PWM实战:从CubeMX配置到驱动舵机和步进电机

发布时间:2026/9/9 12:12:16 来源:尧图企业网站定制
第一次把舵机信号线插到开发板上时我以为随便找一个IO口用延时翻转电平就能模拟出PWM结果舵机要么抖要么完全不动查了半天才发现STM32的PWM和51里用定时器中断翻转IO的“软PWM”完全是两回事。F103C8T6的普通GPIO不具备PWM能力PWM波形本质上来自定时器通道IO口只是被配置成复用推挽输出把定时器内部比较器产生的那路信号引到引脚上而已。这篇学习笔记就用SG90舵机和步进电机两个典型负载把“IO口PWM波的开启”这件事从引脚选型、CubeMX配置、算PSC和ARR、写HAL代码到示波器实测完整过一遍最后会讲几个我实际踩过、修了一晚上的坑。适合已经会点灯、准备接触PWM和电机控制的初学者参考。1. 先搞懂PWM是怎么从IO口出来的F103C8T6引脚分配1.1 PWM的硬件本质和普通IO的差别STM32的PWM输出不是靠软件不断翻转引脚电平而是由定时器内部的计数器、比较器、输出控制电路配合完成的。以最常用的向上计数PWM模式1为例计数器CNT一直从0数到自动重装载值ARR每数到一次就重新归零形成周期同时有一个比较寄存器CCR硬件比较CNT和CCR的关系CNT小于CCR时输出有效电平CNT大于等于CCR时输出无效电平于是引脚上就出现了稳定的方波。这个过程中CPU只负责把参数写进寄存器之后完全不用管方波会一直自动输出。这和普通GPIO加delay的软PWM有本质区别软PWM在输出高电平期间CPU被完全占住中途任何一个中断都会让脉冲变宽变窄频率稍微高一点系统就卡死。舵机要求20ms周期里给出0.5ms到2.5ms的精准脉宽步进电机驱动器要求稳定连续的脉冲频率这些用软PWM基本都做不到稳定。所以“IO口PWM波的开启”这句话准确理解应该是先把某个定时器的某个通道配置成PWM模式再把该通道默认映射到的那只IO引脚配置成复用推挽输出。很多人直接把GPIO配置成普通推挽输出然后对着代码喊为什么没波形——因为引脚没有连接到定时器内部信号上。1.2 C8T6上PWM引脚速查与冲突STM32F103C8T6一共有4个定时器可输出PWM高级定时器TIM1通用定时器TIM2、TIM3、TIM4。每个定时器有4个通道每个通道固定对应一个引脚引脚默认映射关系如下定时器通道1通道2通道3通道4备注TIM1PA8PA9PA10PA11高级定时器带互补输出/刹车功能TIM2PA0PA1PA2PA332位定时器ARR可到0xFFFFFFFFTIM3PA6PA7PB0PB116位定时器最常用TIM4PB6PB7PB8PB916位定时器和4线SPI无冲突重点提两个细节。第一个是TIM2是32位定时器ARR上限非常高适合产生极低频率的脉冲比如1Hz的步进脉冲ARR可以写到999999而TIM3、TIM4是16位定时器ARR最大65535在1MHz计数频率下最低只能到15Hz左右。第二个是引脚复用冲突PA9和PA10接了USART1PA11和PA12接了USBPA2和PA3接了USART2。如果你串口调试占用了PA9/PA10又想用TIM1的通道2输出PWM它们就会打架。1.3 选择PWM引脚的三个实用原则选引脚我建议按这个顺序考虑。第一是兼容现有外设串口占了PA9/PA10就优先考虑TIM3的PA6/PA7或TIM4的PB6/PB7第二是避开JTAG和BOOT引脚PB3、PB4默认是JTAG复用PB2是BOOT1直接当普通IO用很容易踩坑后面会专门讲第三是优先选不需要重映射的默认引脚F103C8T6这个48脚封装的芯片IO资源本来就紧张默认映射能少一个AFIO重映射操作就少一次翻车机会。我自己的习惯是舵机放在TIM3_CH1的PA6上步进电机STEP脉冲放在TIM2_CH1的PA0上这两个引脚在48脚封装里都是直接引出来的干扰少逻辑分析仪夹子也好夹。方向引脚、使能引脚这些不要求PWM的功能随便找个空闲GPIO输出口即可。2. CubeMX从零配置PSC和ARR的计算不是背公式2.1 时钟树先设对APB1定时器时钟为什么是72MHz很多人在这里被绕晕。SYSCLK设为72MHz后AHB总线是72MHzAPB1总线为了满足36MHz上限被二分频变成36MHz但APB1上的定时器时钟有一个隐藏的2倍频规则只要APB1分频系数不等于1定时器时钟就是APB1的两倍。所以TIM2、TIM3、TIM4的实际输入时钟都是72MHz。这一点直接决定了PSC和ARR的计算基准。PWM频率公式是PWM_freq 定时器时钟 / ((PSC 1) * (ARR 1))我习惯先把PSC设为71让计数频率变成72MHz/721MHz也就是计数器每加1正好耗时1微秒。这样CCR的值可以直接当微秒用CCR1500就是1.5ms脉宽省去很多换算。常见配置直接查表目标波形PSCARR实际频率用途50Hz脉宽0.5~2.5ms711999950HzSG90舵机1kHz方波719991kHzA4988步进脉冲、呼吸灯10kHz方波719910kHz高速步进2.2 配置TIM3输出50HzPWM打开CubeMX芯片选STM32F103C8Tx时钟树配好HSE外部8M晶振后PLL倍频到72MHz然后按下面几步操作左侧Pinout面板搜索TIM3把Channel1的PWM Generation CH1勾上PA6会自动变成TIM3_CH1复用模式。在Parameter Settings里填Prescaler71Counter Period19999Counter Mode选UpAuto-Reload Preload选Enable。打开Project Manager生成代码。生成的初始化函数里会自动把PA6配置成复用推挽输出不需要手动再折腾GPIO。生成的MX_TIM3_Init核心部分长这样htim3.Instance TIM3; htim3.Init.Prescaler 71; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 19999; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1500; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1);注意sConfigOC.Pulse就是CCR初始值这里先填1500对应1.5ms也就是舵机的90度位置算是安全的起始姿态。2.3 让PWM真正跑起来HAL启动流程CubeMX只做了初始化配置PWM默认并没有输出。必须在main函数里主动调用启动函数并且最好先设置好CCR再启动int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 1500); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); while (1) { } }HAL_TIM_PWM_Start内部会打开对应通道的输出开关。如果你是TIM1这种高级定时器它还会帮你把主输出MOE位打开这个位很多人手动操作寄存器时会漏导致明明CCR有值但引脚上就是没波形。TIM3虽然没这个问题但养成用HAL启动函数的习惯总没错。2.4 先用LED验证一下PWM是否真的出来了不要一上来就接舵机先花两分钟验证一下IO口PWM波是否正常。在PA6和GND之间串一个1k电阻加LED把PWM频率临时改成1kHzARR999然后在while里不断改变CCR值让LED呼吸while (1) { for (uint32_t ccr 0; ccr 999; ccr 5) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); HAL_Delay(5); } for (uint32_t ccr 999; ccr 0; ccr - 5) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); HAL_Delay(5); } }如果LED亮度平滑变化说明PWM链路已经通了。如果LED不亮或者恒定亮度问题大概率出在引脚没有被正确配置成复用功能或者PWM根本没启动。3. SG90舵机把占空比换算成角度再解决上电抖动3.1 角度映射公式SG90是模拟舵机控制信号是周期20ms50Hz的脉冲脉冲宽度决定了输出轴角度标准关系是0.5ms对应0度1.5ms对应90度2.5ms对应180度。也就是说脉宽范围2000us对应180度平均每度约11.11us。在计数频率1MHz的配置下CCR值直接等于微秒数所以角度转CCR的代码可以这样写void servo_set_angle(uint8_t angle) { if (angle 180) angle 180; uint16_t ccr 500 (uint32_t)angle * 2000 / 180; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, ccr); }注意这里用整数运算优先乘后除避免浮点。如果想要0~180度之外的更精细角度比如0.1度级别那就把CCR差值放大10倍再处理。3.2 为什么舵机会在上电瞬间抽搐这个问题几乎每个人都会遇到程序刚烧进去舵机猛地抽动一下然后才稳定。原因通常是HAL_TIM_PWM_Start启动时CCR还停在0或某个随机值舵机在极短时间内收到一个异常脉宽当然会乱动。解决办法就是我在前面说的启动PWM之前先把CCR写到中位值1500。还是不够稳的话可以在启动后再延迟200ms再操作舵机给它一个稳定供电和稳定信号的时间。还有一个细节便宜SG90的脉宽范围和标称值有偏差有些舵机0.5ms不到底、2.5ms已经顶死强行写满范围会听到齿轮憋死的声音。稳妥做法是用示波器或者逻辑分析仪测一下自己手上那颗舵机的真实死区脉宽通常是在0.544ms到2.4ms附近。我做法是在软件层把脉宽限在500~2400两端各留一点余量宁可少转5度也不闷住电机。3.3 平滑旋转和供电注意事项如果直接让舵机从0度瞬间跳到180度舵机内部会全力猛冲电流峰值很高齿轮也容易打齿。做机械臂或者云台时一般需要平滑控制最简单的做法是逐步逼近目标角度void servo_smooth_move(uint8_t target, uint8_t step, uint16_t interval_ms) { static uint8_t current 90; while (current ! target) { if (current target) { current step; if (current target) current target; } else { current - step; if (current target) current target; } servo_set_angle(current); HAL_Delay(interval_ms); } }供电问题必须单独说。SG90堵转电流可以到500mA以上正常转动也有100~300mA如果直接从开发板的3.3V稳压芯片取电电压一掉MCU就复位。正确做法是舵机电源单独接5V信号线和地线跟开发板共地。我习惯在舵机电源两端并一个220uF电解电容效果立竿见影。4. 步进电机两种驱动思路步进脉冲和四相节拍4.1 脉冲当量先算清楚不同电机一圈多少步步进电机不像直流电机给个电压就转它是靠脉冲一步一步走的。控制步进电机本质是“给多少个脉冲走多少步”所以第一步必须知道一圈对应多少个脉冲专业点叫脉冲当量。NEMA17这类42步进电机步距角通常是1.8度电机转子转一圈要200个整步。如果接了A4988并设置为16细分驱动一个整步需要16个脉冲所以一圈就是200乘以16等于3200个脉冲。转速和脉冲频率的关系是转速(rpm) 脉冲频率(Hz) / 每圈脉冲数 * 60比如目标60rpm16细分下脉冲频率就是3200Hz。28BYJ-48这种5线四相电机带1比64减速箱不同教程里步数有写2048有写4096其实区别在驱动方式是4拍还是8拍。4拍整步按2048步/圈算8拍半步按4096步/圈算。我的建议是用8拍驱动然后以4096作为参考值真正要精确角度时做个自校准测试转固定步数量一下实际角度再反推。4.2 方案AA4988驱动42步进电机A4988是很多人入门用的步进驱动板只需要STEP脉冲、DIR方向、ENABLE使能三个控制信号。硬件接线模块引脚接到C8T6VDD5VGNDGNDSTEPPA0TIM2_CH1PWM输出DIRPB1普通GPIOENABLEPB0普通GPIO低电平使能MS1/MS2/MS3全接3.3V时16细分VMOT电机独立电源12V1A/1B/2A/2B42步进电机对应线圈A4988模块上有电流限制电位器VREF常见42电机把VREF调到0.6V左右起步电机发热严重就往回调。VREF和电流的关系是I VREF / (8 * Rs)Rs是板子上采样电阻看丝印是R050还是R100对应的计算公式要相应调整。STEP脉冲用TIM2的PWM输出最方便。把TIM2_CH1的PSC设为71计数频率就是1MHz改ARR就能改脉冲频率CCR设为ARR的一半就有稳定50%占空比void stepper_set_speed(uint32_t freq) { if (freq 100) freq 100; if (freq 80000) freq 80000; uint32_t arr 1000000 / freq; HAL_TIM_PWM_Stop(htim2, TIM_CHANNEL_1); __HAL_TIM_SET_AUTORELOAD(htim2, arr - 1); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, arr / 2); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); }方向控制就是写GPIO高低电平。使能注意A4988的ENABLE是低有效要让它工作得把PB0拉低HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); // DIR1正转 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // EN0使能用PWM连续输出时做的是速度控制如果要做定位必须在TIM2更新中断里数脉冲数到目标就停volatile uint32_t pulse_count 0; volatile uint32_t target_count 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { pulse_count; if (pulse_count target_count) { HAL_TIM_PWM_Stop(htim2, TIM_CHANNEL_1); } } }4.3 方案BULN2003直驱28BYJ-4828BYJ-48自带减速箱扭矩大但速度慢配合ULN2003驱动板非常便宜。控制方式是把四根相线IN1到IN4依次通电8拍半步的节拍表是1000, 1100, 0100, 0110, 0010, 0011, 0001, 1001把IN1~IN4接到PA0~PA3初始化四个引脚为推挽输出。很多人会想这地方为什么不用PWM了因为四相电机的本质是IO口按顺序输出电平组合PWM的占空比在这里没有意义我们真正需要的是一个精确的时基来切换节拍。所以挂一个定时器更新中断让系统每N微秒切一次节拍volatile uint8_t step_index 0; volatile uint32_t step_remain 0; volatile uint8_t step_dir 1; const uint8_t phase_table[8] { 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09, 0x08 }; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM4) { if (step_remain 0) { if (step_dir 1) step_index (step_index 1) 0x07; else step_index (step_index - 1) 0x07; GPIOA-ODR (GPIOA-ODR ~0x0F) | (phase_table[step_index] 0x0F); step_remain--; } } }TIM4配置成74预分频、周期999就是1kHz更新中断每秒走1000个半步按4096步/圈算大概14.6rpm。28BYJ-48电流不大但也要从5V取电别把全部电流压在板载稳压上否则同样会复位。4.4 梯形加减速的简单实现思路步进电机最典型的翻车是启动频率太高脉冲发太快电机跟不上直接丢步体现在运行上就是一顿一顿甚至嗡嗡响不转。解决方法是启动时频率从低到高加速停止时从高到低减速。因为PWM频率就是ARR加减速就可以写得很简单for (int level 0; level accel_steps; level) { uint32_t freq start_freq (target_freq - start_freq) * level / accel_steps; stepper_set_speed(freq); HAL_Delay(5); } HAL_Delay(run_ms); for (int level accel_steps; level 0; level--) { uint32_t freq start_freq (target_freq - start_freq) * level / accel_steps; stepper_set_speed(freq); HAL_Delay(5); }初学阶段不用把S形曲线搞得太复杂梯形已经能解决80%的丢步问题。关键是加速步数不要太少我的经验是至少留10个档每档延时5到10ms让电机有明显的“慢慢起来”的过程。5. 实测波形记录和五个翻车现场5.1 实测波形频率和脉宽一目了然软件写得再漂亮最终都要落实在引脚波形上。买一个几十块钱的8通道逻辑分析仪配合Sigrok PULSEview软件把信号线夹在PA6或PA0上能直接读出频率、周期和脉宽。舵机的50Hz波形看起来是20ms一个大周期、1.5ms的高电平、其余时间低电平占空比约7.5%。步进脉冲的波形则是一条均匀的方波频率改一下方波密度立刻变化。没有示波器的话也可以用计数法粗略验证写一个软件计数器测GPIO翻转次数但真心建议买逻辑分析仪这个钱是调电机项目里回报率最高的投资。5.2 坑1改了ARR之后转速乱跳用stepper_set_speed函数中途改频率时会发现第一个脉冲周期特别长或者突然多了一个高电平电机速度肉眼可见地乱一下。这是因为定时器有自动重装载预装载功能ARPE置1ARR的新值要等到下一个更新事件才会真正生效。如果当前计数器已经超过了新的ARR值定时器就得先跑完一个大周期才能同步于是产生一个异常脉冲。我用的笨办法就是先停PWM、改ARR和CCR、再启动PWM期间电机会短暂停一下对于非高实时性场景完全够用。如果要做连续不断流的加减速需要精细化处理预装载和更新事件先写个小实验摸清规律再上真机。5.3 坑2舵机接上就嗡嗡响舵机频率和脉宽都对但0度和180度附近总是嗡嗡响用手摸能感觉到输出轴在里面较劲。这是舵机的死区问题脉冲宽度稍微差一点点舵机内部电位器测不到角度变化但电机还在持续加力。解决方法是把脉宽范围收窄一点或者在做角度闭环时给一个死区窗口当实际角度和目标角度差小于某个阈值就不再更新脉宽。另外供电不稳也会引起舵机嗡嗡响尤其是把舵机接到3.3V时电压被拉低控制信号高电平可能不够高舵机逻辑混乱。确认信号线接的是IO供电是5V地线共地三个条件缺一不可。5.4 坑3PB4/PB3当普通IO用不了我有一次想把方向引脚放在PB4结果写GPIO电平怎么都不对量电压始终不变。查了引脚说明才想起PB3默认是JTDO、PB4默认是NJTRST这俩引脚在上电后由JTAG功能占用普通GPIO输出控制根本轮不上。解决办法是在CubeMX的SYS里把Debug改成Serial Wire模式JTAG被释放后PB3、PB4才能当GPIO用。如果你只接了SWD下载器改成Serial Wire不影响调试。这个坑我建议直接绕开能用PA、PB0、PB1这类无特殊功能的引脚就别碰PB3、PB4、PA15这些半个JTAG腿省得还要记重映射规则。5.5 坑4频率调高后步进电机丢步28BYJ-48直驱时把节拍中断频率从1kHz调到2kHz甚至更高电机会突然嗡嗡叫不转或者转着转着角度对不上了。这不是代码问题是步进电机本身存在最大启动频率和扭矩特性。28BYJ-48带1比64减速箱输出轴转速本来就不快强行提高脉冲频率只会让转子跟不上磁场切换失去同步。处理办法我说了无数次了就是加减速。还有一个容易被忽视的原因中断里如果还做了HAL_Delay或者串口打印节拍切换会被打断等效于频率不稳定步进电机对这种抖动极其敏感。中断回调里只做最轻量级的操作打印调试信息放到主循环去做。5.6 坑5电机一转MCU就复位这是所有电机项目里最经典的坑。步进电机启动瞬间电流很大如果开发板和电机共用一个电源电压会被瞬间拉低STM32检测到供电跌落到复位阈值就直接重启。现象就是电机刚要转串口重新打印启动信息程序回到main入口。排查时先用万用表测电机运转瞬间的电源电压如果掉到4.5V以下基本就是供电问题。正确思路是电机独立供电开发和电机之间只共地不要把电机电源引脚插到开发板的5V排针上。我经历过一次因为贪图方便把28BYJ-48接到开发板5V上板子反复重启到怀疑人生。另外一个相关的问题是信号地没连电机独立电源和开发板之间没有共地的话STEP脉冲参考地不一致步进驱动板会时不时误触发。整体学下来PWM配置本身并不难半小时就能跑通真正花时间的是排查引脚复用、供电、预装载这些边界问题。如果你也卡在电机不动我建议先看电源再看波形最后怀疑代码按这个顺序排查能省下大量时间。这套PWM和电机驱动的组合打通之后后面做小车底盘、机械臂关节、云台稳像基本就是复制粘贴加改参数的事了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价