ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 HAL库控制SG-5010舵机:PWM原理与工程实践

STM32 HAL库控制SG-5010舵机:PWM原理与工程实践 1. 项目拆解与核心思路玩嵌入式到一定阶段几乎都会碰到舵机。舵机是个非常讨喜的执行器——不用编码器、不用驱动板给对脉冲就转到对应角度接上电源就能转属于“性价比极高”的入门级运动控制方案。这篇内容围绕“STM32控制SG-5010舵机转动”这个项目展开使用HAL库配合CubeMX图形化配置从原理到代码再到排坑尽量一次性讲透。先说清楚这个项目能做什么、适合谁。如果你正在做机械臂、小车转向、云台、鱼缸喂食器或者毕业设计里需要“让某个东西转到指定角度”那么用STM32输出PWM控制舵机几乎是必经之路。SG-5010是标准的模拟舵机体积比常见的SG90大一圈扭矩也大很多非常适合作为学习舵机控制的载体。这篇内容的读者可以是刚学完GPIO和定时器的初学者也可以是已经会用标准库、想切到HAL库的开发者只要你在“用定时器输出PWM”这个环节上有需求这篇文章的配置思路和代码封装都可以直接抄。我当时做这个项目的时候起因是想给一个云台加舵机驱动但网上搜到的教程大多只贴了一段代码舵机能转就完了根本没讲清楚为什么定时器要配成72分频、为什么周期是20000、为什么比较值500对应0度。真换一个定时器、换一块板子代码就“失灵”了。所以这篇内容我打算把思路拆开讲SG-5010到底要什么样的信号STM32怎么用HAL库生成这个信号以及你拿到代码之后怎么改参数适配自己的板子。1.1 SG-5010舵机到底是怎样一个器件舵机的全称是“舵机伺服系统”内部集成了直流电机、减速齿轮组、电位器位置传感器和控制电路。它对外只有三根线电源、地、信号。控制电路根据信号线上输入的PWM脉宽和电位器反馈的实际位置做比较然后驱动电机往目标位置转直到误差为零。SG-5010的详细参数这里给个速查表参数数值说明工作电压4.8V ~ 6V推荐5V或6V电压越高扭矩越大扭矩5.5kg·cm4.8V/ 6.5kg·cm6V标准舵机里算比较有劲的工作频率50Hz即PWM周期20ms脉宽范围0.5ms ~ 2.5ms对应0° ~ 180°死区≤5us脉宽误差在此范围内舵机不响应重量约55g比SG90重得多接线棕/GND、红/VCC、橙/信号具体颜色以实物为准这里最核心的信息是“0.5ms ~ 2.5ms对应0° ~ 180°”。也就是说你给舵机信号线上送一个周期为20ms、高电平持续1.5ms的PWM波舵机就会转到90度位置。脉宽每增加约11us舵机就大约转1度。这个线性关系是后面所有代码计算的基础。另外一个容易被忽略的参数是“死区”。舵机内部电路对脉宽的分辨率是有限的当输入脉宽和目标位置对应的脉宽差值小于死区时舵机不会动作。这意味着你不需要把PWM做到纳秒级精确1us级别的精度已经绰绰有余。这个特性对后面定时器参数的选择非常友好。还有一个重要认知模拟舵机需要持续的PWM信号来维持位置。如果PWM信号停止舵机虽然会因为齿轮自锁而停在当前角度但外力一推就可能松动。所以代码里启动PWM之后就不要随便停掉它。1.2 为什么用HAL库定时器PWM而不是IO口模拟有两种常见方式可以让STM32输出PWM信号一种是用普通GPIO配合定时器中断在中断里翻转电平另一种是用定时器的PWM输出模式硬件自动产生波形。GPIO模拟的好处是灵活想输出什么波形都行但代价是CPU被占死。假设用1us一个tick的节奏去翻转IO口中断频率就是1MHzCPU几乎什么都干不了。同时中断响应本身存在抖动波形精度也不稳定舵机容易出现“咔咔”的抖动声。定时器PWM模式则完全不同。定时器在硬件层面自动完成计数、比较、翻转电平的动作CPU只负责在需要改变角度时修改一个寄存器值。波形由硬件产生稳定且精度高CPU可以腾出来跑逻辑、跑通信、跑算法。这也是每次有人问“舵机控制用什么方式好”时标准答案基本都是“定时器PWM”的原因。至于为什么用HAL库而不是标准库我的考虑很简单CubeMX图形化配置定时器、引脚、时钟生成代码后直接填逻辑开发效率高很多。而且HAL库对寄存器的封装更规范换芯片平台时只要CubeMX重新生成一下底层上层业务代码几乎不用改。LL库虽然性能上更接近寄存器操作但对新手理解门槛更高而且舵机是50Hz的低频信号HAL库的函数调用开销完全是零压力的存在。所以这个项目选HAL库纯粹是“用最合适的工具做最合适的事”。2. 工程搭建与CubeMX配置2.1 时钟树和引脚规划先想清楚再点鼠标很多人拿到板子直接打开CubeMX赶紧把定时器配置出来结果卡在时钟树上一脸懵。我的习惯是先从系统层面想一遍需求需要几个通道输出PWM本案例只需要1个但我一般会预留几个备用引脚定时器时钟源从哪里来STM32F103系列定时器时钟默认走APB1总线系统主频跑多少一般是72MHzCubeMX会根据你选的晶振自动配好。以最常见的STM32F103C8T6为例板载8MHz晶振CubeMX时钟树里HSE选Crystal/Ceramic Resonator系统时钟选PLL倍频到72MHz。APB1总线频率配置为36MHz这里有个关键点APB1预分频系数如果不等于1那么挂在该总线上的定时器时钟是APB1频率的两倍也就是72MHz。这一点很多人容易搞错导致定时器频率算出来只有实际的一半。CubeMX里你可以直接在时钟树页面看到TIM2/3/4的时钟输入把它记下来后面计算要用。引脚选择上我习惯用TIM2的CH1也就是PA0。原因很简单PA0在绝大多数开发板上都引出来了而且不占用串口和I2C引脚。如果你的板子PA0被其他功能占了可以换TIM3_CH1PA6、TIM4_CH1PB6配置流程完全一样。引脚的工作模式CubeMX会自动帮你设成复用推挽输出AF_PP不需要手动配置。有人会纠结要不要开上拉电阻PWM输出模式下不需要那是输入模式的选项。2.2 定时器PWM参数计算为什么是71和19999在CubeMX里配置TIM2的PWM核心参数就四个Prescaler预分频、Counter Mode计数模式、Counter Period自动重装值、Pulse初始比较值。每一项的值都需要算不能瞎填。首先是预分频。前文说到TIM2的时钟是72MHz而我们希望计数器的计数频率是1MHz也就是1us计数一次。公式是定时器时钟 /预分频 1 计数频率72MHz /PSC 1 1MHz所以 PSC 71。Counter Mode选向上计数UP即可。然后是自动重装值。舵机要求的PWM周期是20ms即20000us。计数器从0数到自动重装值需要的时间正好是20000us自动重装值 1× 1us 20000us所以自动重装值 19999。Pulse是初始比较值也就是初始脉宽。为了让舵机上电后先停在中间位置初始脉宽设成1500us对应90度所以在CubeMX的Pulse里填1500。算完之后你在CubeMX里会看到生成的代码中TIM2的初始化结构体长这样static void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; TIM_OC_InitTypeDef sConfigOC {0}; htim2.Instance TIM2; htim2.Init.Prescaler 71; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 19999; htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_PWM_Init(htim2) ! HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(htim2, sClockSourceConfig) ! HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(htim2, sMasterConfig) ! HAL_OK) { Error_Handler(); } sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1500; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; if (HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1) ! HAL_OK) { Error_Handler(); } }注意AutoReloadPreload被使能了意思是自动重装值在更新事件时才生效。这是CubeMX默认配置通常保持默认就好。OCMode是PWM1含义是计数器值小于比较值时输出高电平大于比较值时输出低电平这正好是我们需要的脉宽波形。配置完成后记得在CubeMX的Project Manager里勾选“Generate peripheral initialization as a pair of .c/.h files”然后生成代码。这样工程里会自动出现tim.c和tim.h写代码时更方便。3. 核心代码实现与角度控制3.1 让舵机动起来启动PWM与设置脉宽CubeMX生成的代码只是完成了初始化PWM通道默认是没有启动的。要在main函数里显式调用启动函数HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);这行代码执行后PA0引脚就开始输出周期20ms、初始脉宽1.5ms的PWM波形了。如果此时舵机已经接好电源和信号线它会立刻转到90度位置。启动之后改变角度就是修改比较寄存器。HAL库对此提供了宏定义__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 500); // 转到0度 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 1500); // 转到90度 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 2500); // 转到180度这个宏的本质是设置TIM2的捕获比较寄存器CCR1的值。定时器每个计数周期都会拿CCR1和当前计数值做比较超过计数值时拉低电平从而改变脉宽。修改这个寄存器的操作是原子的执行一条指令就完成了不需要担心在PWM周期中途修改会产生毛刺——因为比较是在硬件层面实时进行的下一个脉宽立刻就会变化。一个值得注意的点__HAL_TIM_SET_COMPARE接收的单位是“计数个数”因为我们前面配的是1us计数一次所以500就是500us脉宽、1500就是1500us脉宽。如果你的预分频不是71单位就不是us换算关系也会变。这也是我反复强调要理解参数计算的原因——不懂原理换个配置就废。3.2 封装角度换算函数直接用角度说话直接填500、1500、2500这种“魔法数字”虽然能用但代码可读性太差。更优雅的做法是写一个角度换算函数把0到180度映射到500到2500的脉宽值。映射关系是线性的pulse 500 angle × 2000 / 180用整数运算实现就是void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t angle) { if (angle 180) { angle 180; } uint16_t pulse 500 (uint16_t)angle * 2000 / 180; __HAL_TIM_SET_COMPARE(htim, channel, pulse); }这个函数把角度限制在180度以内防止误操作把非法数据写进寄存器。2000乘以角度再除以180先乘后除能避免整数除法截断导致的精度损失。有了这个封装主循环里的逻辑就非常清爽了int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); while (1) { Servo_SetAngle(htim2, TIM_CHANNEL_1, 0); HAL_Delay(1000); Servo_SetAngle(htim2, TIM_CHANNEL_1, 90); HAL_Delay(1000); Servo_SetAngle(htim2, TIM_CHANNEL_1, 180); HAL_Delay(1000); Servo_SetAngle(htim2, TIM_CHANNEL_1, 90); HAL_Delay(1000); } }跑起来的效果就是舵机在0度、90度、180度之间循环摆动。如果你的SG-5010实测结果略有偏差比如0度时舵机还差5度才到机械限位可以在函数里加一个偏移量做修正比如脉冲值整体加上20us这在实际项目中非常常见。3.3 平滑转动与多目标控制直接改比较值时舵机会以最快速度冲过去这在很多场景下看起来不够自然也会带来机械冲击。更高级的玩法是让舵机以可控速度平滑过渡到目标角度。最简单的实现是步进式逼近每次只修改一小段脉宽间隔几毫秒再修改下一段直到到达目标值。void Servo_SetAngleSmooth(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t angle, uint16_t step, uint16_t delay_ms) { uint16_t current_pulse __HAL_TIM_GET_COMPARE(htim, channel); uint16_t target_pulse 500 (uint16_t)angle * 2000 / 180; if (current_pulse target_pulse) { for (uint16_t pulse current_pulse; pulse target_pulse; pulse step) { __HAL_TIM_SET_COMPARE(htim, channel, pulse); HAL_Delay(delay_ms); } } else { for (uint16_t pulse current_pulse; pulse target_pulse; pulse - step) { __HAL_TIM_SET_COMPARE(htim, channel, pulse); HAL_Delay(delay_ms); } } }step参数控制步长step越大速度越快delay_ms控制步间延时延时越大越平滑。这个函数虽然简单但已经完全能满足云台预置位、机械臂分度动作这类需求。如果想控制多个舵机每个舵机对应一条PWM通道把设备句柄和通道号作为参数传进去上面封装天然支持多实例。比如你想让PA0上的舵机转到30度、PA1上的舵机转到120度只需要Servo_SetAngle(htim2, TIM_CHANNEL_1, 30); Servo_SetAngle(htim2, TIM_CHANNEL_2, 120);但要注意同一时间多个舵机同时大角度快速转动时瞬时电流会是单个舵机的数倍电源余量一定要留足否则电压跌落会导致所有舵机都出现抖动。4. 电源、调试与常见问题排查4.1 舵机供电最容易翻车的环节舵机项目里十有八九的问题最终都能归结到电源上而不是代码上。SG-5010正常工作电流在几百毫安启动瞬间和堵转瞬间电流可以冲到1A以上。如果用开发板的USB口直接供电USB口的500mA限流根本扛不住表现就是舵机一开始转动板子上的LED闪一下系统重启串口掉线什么异常都来了。正确做法是单独给舵机供电用一个5V、至少2A输出的电源适配器或者锂电池降压模块。接线方式为舵机电源正极接5V电源正极舵机电源负极接5V电源负极同时舵机负极要和STM32的GND连在一起也就是“共地”。信号线接STM32的PA0。共地这一条特别重要。信号线上传输的是STM32的3.3V电平参考地是STM32的GND如果舵机电源地不和STM32共地信号电平就没有统一的参考点舵机接收到的脉宽是乱码表现就是疯狂抖动或者完全不动。另外如果舵机电源线比较长或者线径太细线上的压降会导致舵机输入电压不足。扭转力矩不够、转不到位、抖舵都可能是电源线太细导致的。建议舵机电源线用至少0.5mm²的线尽量短一些。我实测过同一套系统换一根粗短的电源线抖动问题直接消失。4.2 常见问题排查表下面把实际项目中遇到的高频问题整理成表格方便遇到问题时对照排查现象可能原因解决思路舵机完全不动没启动PWM确认执行了HAL_TIM_PWM_Start舵机完全不动信号线接错确认信号线接对引脚颜色区分舵机完全不动没共地把舵机电源负极与STM32 GND短接舵机抖动并有滋滋声电源电流不足换大电流电源或单独给舵机供电舵机抖动并有滋滋声PWM频率不对确认周期是20ms不是2ms舵机只转一个方向脉宽超出限定范围检查angle是否限制在180以内舵机转到尽头一直嗡嗡响脉宽超出机械行程用示波器或串口检查实际脉宽板子一转动就重启舵机电流导致电压跌落独立供电共地不用USB直接供电改比较值后无反应修改了错误定时器的CCR检查句柄和通道是否与启动时一致这里特别说一下“嗡嗡响”这种情况。当脉宽对应角度超过机械限位时舵机会一直往限位方向使劲电机堵转电流迅速上升齿轮箱发出高频噪音。短时间内问题不大但长时间堵转会明显缩短舵机寿命甚至烧毁电机驱动管。代码里要做角度限幅就是上面那个if (angle 180) angle 180;这个小细节能救舵机一命。4.3 实战排查用示波器还是用逻辑分析仪排查PWM信号问题时有条件的话优先用逻辑分析仪。一根杜邦线夹在PA0上能看到完整的PWM波形周期、脉宽一目了然。没有也没关系可以用串口打印辅助判断在修改比较值后把脉宽值通过串口发出来确认软件层面算出的脉宽是否正确。还有一个我个人踩过的小技巧如果舵机在某个角度附近来回抖动排除电源问题后可以用手指轻触舵机臂感受力度——如果感觉舵机在“硬顶”说明信号已经给了只是机械负载太大需要降低目标角速度如果舵机“松垮”说明信号根本没进去问题在信号路径上。5. 扩展玩法与进阶思考5.1 从单舵机到多舵机一体化的控制方案实际项目中很少只用一个舵机。比如机械臂要同时控制肩、肘、腕三个关节云台要同时控制水平和俯仰两个轴。多舵机的实现方案有三种常见选择第一种是直接用STM32的多个定时器通道。STM32的定时器资源很多TIM2、TIM3、TIM4各有4个通道理论上可以同时产生12路PWM对绝大多数舵机应用绰绰有余。缺点是每路占一个引脚走线会多一些。第二种是用I2C接口的PCA9685舵机驱动板。它用两线I2C扩展出16路PWM输出每路都可以独立设置周期和脉宽而且板载电源端子可以直接接大电流电源。这种方案适合舵机数量多、主控引脚紧张的项目。HAL库对I2C有现成支持移植官方驱动也不算难。第三种是用串行总线舵机比如LX-16A、总线舵机这类通过UART指令控制和读取角度还能回传温度和电压。这已经超出SG-5010模拟舵机的范畴了但当舵机数量多、实时性要求高的时候总线舵机是更合适的方向。考虑到本项目是单舵机打基础前面封装的Servo_SetAngle函数天然支持多通道扩展这一点在后续项目复用时会发现很省心。5.2 舵机与上位机联动串口控制角度的实现一个非常实用的扩展是让PC或者手机通过串口发送指令控制舵机转到任意角度。这本质上是把串口接收到的字符串解析成一个数值然后调用封装函数。用HAL库的中断接收方式代码也不复杂。在main中启动串口中断接收uint8_t rx_buffer[16]; uint8_t rx_index 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_buffer[rx_index] rx_data; if (rx_data \n) { int angle atoi((char *)rx_buffer); Servo_SetAngle(htim2, TIM_CHANNEL_1, angle); rx_index 0; } HAL_UART_Receive_IT(huart1, rx_data, 1); } }上位机发一个“120\n”舵机就转到120度。串口控制的好处是调试效率极高不用重新编译烧录就能测试不同角度下的机械动作。把这条链路打通之后后续接蓝牙模块、ESP8266、WiFi透传原理都一样只是把串口对端从PC换成了无线模块。这些热词里提到的ESP8266通信实际上就是在这个串口联动基础上加一步透传而已。5.3 死区校准与位置闭环的思路前面提到SG-5010有死区参数不同舵机的死区略微不同。实际使用时如果发现舵机对微小角度变化不敏感可以增大角度步长来避免死区问题。比如脉宽每次至少变化20us也就是角度步长约1.8度这样舵机每一次动作都在死区之外响应更干脆。如果项目需要精确的位置反馈光靠PWM开环还不够。因为舵机内部电位器反馈的位置信号没有引出你无法得知舵机是否真正抵达目标。进阶做法是在输出轴上加外部电位器或磁编码器把角度采样值通过STM32的ADC读取再用PID去修正脉宽。这也是很多低成本机械臂项目做闭环的标准路线。热词里提到的“stm32 lqr”、数字电源的反馈思路本质上都是一样的检测、计算、修正只是被控对象不同。最后分享一点个人体会这个项目做下来我最大的收获不是学会了调PWM而是建立了一个非常实用的认知嵌入式开发的很多所谓“难点”剥开之后都是一层窗户纸核心在于参数背后的物理意义。定时器的预分频为什么是71自动重装值为什么是19999这些数字不是随便填的每一处都对应一个时间单位。把时间单位理解透了换芯片、换定时器、换舵机型号你都能从容应对。做舵机控制先慢后快先在本子上把计算公式写明白再动手改代码这个顺序能帮你避开绝大多数新手会踩的坑。
返回列表