ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F103C8T6舵机控制:PWM配置到角度映射全攻略

2026/9/17 2:18:12 拓冰建站 浏览量
STM32F103C8T6舵机控制:PWM配置到角度映射全攻略 简介面向STM32嵌入式初学者、机器人及智能车爱好者这套资料以STM32F103C8T6为主控围绕定时器输出PWM驱动舵机这一典型场景给出了可编译运行的完整Keil工程。工程不仅演示了PWM频率与脉宽的设置方法还覆盖了GPIO复用推挽输出、定时器预分频器/自动重载寄存器/比较寄存器的配置以及舵机角度与脉冲宽度的对应关系。资源总量86个文件包括37个C源文件、35个头文件以及启动文件、工程文件、调试配置和清理脚本压缩包仅317KB轻量且结构清晰便于逐个模块对照学习。目前已有424人学习下载实用性得到初步验证。借助板载OLED显示、按键输入、PWM输出与舵机驱动等模块读者可以实时调节占空比并观察舵机转角变化既能巩固STM32定时器与PWM外设的编程方法也能为后续多舵机同步控制、闭环位置反馈等功能开发打下基础。1. 让舵机“听话”之前先搞懂 STM32F103C8T6 的 PWM 输出很多第一次接触 stm32f103c8t6 最小系统板的人把舵机信号线直接接到某个普通 GPIO然后用HAL_Delay翻转引脚电平结果舵机只在两个极端位置来回抖动中间角度完全停不住。问题出在舵机要的并不是简单的高/低电平而是一串周期稳定在 20ms、高电平宽度 0.5ms 到 2.5ms 的 PWM 脉冲软件延时生成的方波频率和占空比都不稳定舵机自然乱走。标题把 STM32F103C8T6、PWM 和舵机放在一起本质是解决定时器资源分配、占空比计算公式和 50Hz 信号生成这三件事顺带把供电与干扰这些容易翻车的点讲清楚。下面按 CubeMX 配置、HAL 库代码、角度映射到排错验证的顺序展开适合手头有一块最小系统板和一颗常见模拟舵机的开发场景。2. 舵机与 PWM 的绑定关系为什么是 50Hz而不是 1kHz2.1 舵机控制板读取的是高电平宽度不是占空比本身模拟舵机内部结构可以简化成直流电机、减速齿轮、电位器和控制板四部分。电位器和输出轴同步旋转控制板读取输入信号的脉宽和电位器反馈的当前角度位置比较差值经过电路驱动电机正转或反转。也就是说真正决定目标角度的是高电平持续的时间而不是一个抽象的百分比。所有标准模拟舵机都工作在 20ms 周期左右也就是 50Hz 刷新率在这个周期内高电平宽度在 0.5ms 到 2.5ms 之间变化。如果把 PWM 频率提到 1kHz一个 1.5ms 的高电平算下来占空比已经超过 100%实际输出会一直保持高电平舵机控制板无法识别刷新间隔舵机会冲到限位位置并且堵转。这个反直觉点导致很多初学者以为是代码里的 Bug其实只要回到舵机数据结构就会明白50Hz 不是随便选的它是给控制板留出的刷新窗口。数字舵机可以支持 200Hz 到 400Hz 的更高刷新率但在没有确认规格之前模拟舵机项目一律从 50Hz 起步最稳妥。这也是为什么舵机资料里几乎不会写“占空比 5%”这样的描述而是直接写“脉宽 1.5ms”。2.2 STM32 定时器如何生成 PWMPSC、ARR、CCR 的配合STM32F103C8T6 内部有 TIM1、TIM2、TIM3、TIM4 四个定时器可以输出 PWM。通用定时器里有一个 16 位计数器从 0 递增到 ARR 后归零然后继续下一轮CCR 是比较寄存器的值。如果配置成 PWM 模式 1计数器值小于 CCR 时输出高电平大于等于 CCR 时输出低电平这样一轮计数就生成一个完整的脉冲。PWM 频率公式是定时器时钟 / ((PSC1) * (ARR1))占空比可以近似看成CCR / (ARR1)。所以调整频率有两个旋钮PSC 影响计数步长ARR 影响整个周期长度。很多人只改 ARR 不改 PSC导致 PWM 频率变化的同时占空比分辨率也被拉低。正确的做法是先根据需要的频率和分辨率定下 PSC再根据周期算 ARR。常见的组合是先选 PSC 让计数频率变成 1MHz也就是每个计数对应 1us这样 CCR 直接填脉宽微秒数代码写起来最直观。如果计数频率选得太高比如 72MHz一个计数只有 13.9nsARR 就必须设到 359999 才能凑出 20ms 周期但 F103 的定时器计数器只有 16 位上限 65535所以这样做根本不可行。2.3 给 F103C8T6 算一组 50Hz 参数在 stm32f103c8t6 最小系统板的原理图上外部 8MHz 晶振经过 PLL 倍频到 72MHz。APB1 分频为 2 时挂载在 APB1 总线上的 TIM2、TIM3、TIM4 定时器时钟会自动变成 72MHz这一点在用 STM32CubeMX 时要核对 APB1 Timer Clocks 是不是 72MHz。按照公式取 PSC71 后计数频率变成 1MHzARR19999 后周期等于 20ms得到 50Hz。配置PSCARR计数频率PWM 频率单个计数对应时间高分辨率71199991MHz50Hz1us低分辨率14399990.5MHz50Hz2us这两种配置都能输出 50Hz区别是分辨率。1MHz 计数下 CCR 每加 1脉宽变化 1us对 180 度舵机大约对应 0.09 度0.5MHz 计数下 CCR 每加 1脉宽变化 2us。舵机本身存在机械死区2us 分辨率已经够用但 1MHz 的计算更简单后面代码直接拿脉宽微秒数当 CCR 值所以我通常用第一组参数。TIM_HandleTypeDef htim2; 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;这段代码是 HAL 库中定时器基础配置的一部分。Prescaler 填 71内部预分频后计数频率是 1MHzPeriod 填 19999计数器从 0 数到 19999 需要 20000 个计数也就是 20msCounterMode 选择向上计数生成的是标准锯齿波AutoReloadPreload 必须设为 ENABLE这样修改 ARR 或 CCR 后会在更新事件统一加载避免运行中输出瞬间乱跳。提示验证配置对不对可以手动代公式。CCR1500、ARR19999 时1500 / (199991) 7.5%占空比乘 20ms 刚好是 1.5ms如果算出来不是这个值说明某个参数填错了。3. 用 STM32CubeMX 给 F103C8T6 配置定时器 PWM 通道3.1 先把引脚选对定时器通道与串口、系统引脚的冲突F103C8T6 虽然引脚不多但每个定时器通道都有默认复用引脚选择前最好对着 stm32f103c8t6 原理图确认硬件上哪些引脚已经接了按钮、LED 或者串口芯片。常用的 PWM 输出引脚有 PA0、PA1、PA6、PA7、PB6、PB7其中 PA0 和 PA1 也是 ADC 输入如果最小系统板把 PA1 接进了板载按键就不适合做舵机信号线。定时器/通道引脚典型冲突TIM2_CH1PA0部分板子按钮/WKUPTIM2_CH2PA1ADC 输入TIM3_CH1PA6复用推挽输出TIM3_CH2PA7复用推挽输出TIM4_CH1PB6I2C1_SCLTIM4_CH2PB7I2C1_SDAGPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);这段 GPIO 配置是 CubeMX 自动生成的把 PA6 设置为复用推挽输出。注意不能选GPIO_MODE_OUTPUT_PP否则定时器的事件无法输出到引脚舵机信号线上只会是一个固定电平。Speed 选 LOW 或 HIGH 都可以PWM 频率只有 50Hz低频信号用 LOW 即可反而能减少边沿过冲。3.2 时钟树APB1 分频怎么影响定时器时钟打开 CubeMX 后在 System Core 里选 RCC把 HSE 设为 Crystal/Ceramic Resonator然后在 Clock Configuration 中将 HCLK 配到 72MHz。F103C8T6 的 APB1 默认是 36MHz而定时器的时钟源在 APB1 分频不为 1 时会自动翻倍所以 APB1 Timer Clocks 一栏通常显示 72MHz。如果你把 APB1 分频设成 1定时器时钟反而只有系统时钟的一半PWM 频率会随之变化。所以在计算 PSC 和 ARR 前先看 CubeMX 里这一项不要凭默认值心算。提示如果 APB1 Timer Clocks 不是 72MHz把前面公式里的定时器时钟改掉再重新计算 PSC 和 ARR否则舵机周期不对会出现高频率啸叫或抖不停。F103C8T6 的定时器时钟上限是 72MHz用内部 HSI 或者外部 HSE 都不影响这个上限但 APB1 预分频值会直接影响最终计数频率。3.3 在 CubeMX 参数页填入的数值以 TIM3_CH1 输出到 PA6 为例。先在 Timers 里勾选 TIM3 的 Internal Clock再选 Channel1 为 PWM Generation CH1。参数页按下面这张表填参数值含义Prescaler7172MHz 分频为 1MHzCounter ModeUp向上计数Counter Period19999周期 20msAutoReloadPreloadEnable影子寄存器使能Pulse1500初始 CCR对应 1.5ms生成代码后主循环开始之前要启动 PWM 输出。CubeMX 会在htim3初始化完成后等待用户添加启动代码写成下面这样HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); while (1) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 1000); HAL_Delay(1000); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 2000); HAL_Delay(1000); }逻辑说明HAL_TIM_PWM_Start启动定时器通道让 PWM 波形真正从 PA6 输出。第一次调用后舵机先停在 CubeMX 中 Pulse 配置的 1.5ms 对应角度也就是中位。随后循环里用__HAL_TIM_SET_COMPARE直接改写 CCR让舵机在 1.0ms 和 2.0ms 两个位置来回摆动用来快速验证接线和电源。参数说明两个HAL_Delay只是让舵机有足够时间跟上天真正做项目时这里应该换成非阻塞的状态机或 RTOS 延时避免影响其他逻辑。如果循环跑起来之后 PA6 始终没有波形可以先量引脚静态电压0V 说明复用没配好3.3V 固定高电平多半是定时器没启动抖动的小电压才说明计数器在工作。4. 舵机角度映射与 HAL 库驱动代码4.1 从角度到脉宽的线性公式标准模拟舵机的输入脉宽和输出角度近似线性0.5ms 对应 0 度1.5ms 对应 90 度2.5ms 对应 180 度。按线性关系角度到脉宽的计算公式是pulse_us 500 angle * 2000 / 180。这里 2000us 是 0.5ms 到 2.5ms 的脉宽跨度180 度是舵机总行程。 这个公式写进单片机时要注意运算顺序如果都是整数类型最好先算angle * 2000再除 180避免中间结果被截断。不同品牌舵机的实际范围不一定完全一样最好预留两个宏定义作为脉宽上下限这样在机械臂项目中可以快速校准不必每次改公式。4.2 set_angle 函数下面是基于 HAL 库的完整角度设置函数。假设定时器计数频率已经是 1MHz此时 CCR 数值等于脉宽微秒数。#define PULSE_MIN_US 500 #define PULSE_MAX_US 2500 void set_angle(TIM_HandleTypeDef *htim, uint32_t channel, float angle) { uint32_t pulse_us; if (angle 0.0f) angle 0.0f; if (angle 180.0f) angle 180.0f; pulse_us (uint32_t)(PULSE_MIN_US angle * (PULSE_MAX_US - PULSE_MIN_US) / 180.0f); __HAL_TIM_SET_COMPARE(htim, channel, pulse_us); }逻辑说明函数先做角度边界处理然后按线性公式算出脉宽最后用__HAL_TIM_SET_COMPARE把 CCR 写入定时器通道。参数中htim是定时器句柄channel是通道号这两个参数决定哪个引脚输出。如果在 CubeMX 中选的是 TIM3_CH1调用时传htim3和TIM_CHANNEL_1。这里把脉宽限制在 500us 到 2500us防止因角度换算越界给舵机发送危险信号。使用的时候先启动通道再把角度放进去HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_Delay(200); set_angle(htim3, TIM_CHANNEL_1, 90.0f);这段代码在启动 PWM 之后延时 200ms 再给舵机发 90 度指令是为了避开上电瞬间电源不稳定。参数说明启动 PWM 后如果 Pulse 初始值是 1500舵机已经在中位再调用set_angle不会有突变抖动。有人喜欢先写 0 度再延时这反而会让舵机在一侧冲击限位不建议这么写。4.3 多路舵机怎么做单个定时器的多个通道可以共用同一个周期但每个通道的 CCR 独立。比如机械臂底座和肩部都用 TIM3CH1 接 PA6CH2 接 PA7初始化时启动两个通道之后分别设置角度即可。这样刷新率一致舵机群动作更协调。代码示例如下HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); set_angle(htim3, TIM_CHANNEL_1, 0.0f); set_angle(htim3, TIM_CHANNEL_2, 90.0f);如果用两路不同定时器也能工作但要注意两路 PWM 周期不一定同相对要求同步的场景尽量把舵机通道集中在同一个定时器上减少误差。下面的表格给出常用角度对应的脉宽和 CCR 值方便在无法接示波器时直接对照角度脉宽CCR1MHz 计数占空比0 度500us5002.5%45 度1000us10005.0%90 度1500us15007.5%135 度2000us200010.0%180 度2500us250012.5%值得注意的是这些值是理论值。实际舵机安装连杆时如果到了限位还继续给更大角度电机会堵转电流明显上升轻则舵机发烫重则烧坏控制板。因此在函数范围内建议加更小的保护范围比如软件里限制在 10 到 170 度给机械结构留出缓冲。如果要求动作平滑不要把角度一下子从 0 跳到 180可以在定时器更新中断里按每 20ms 改变 1 度的步进逼近目标角度这样能降低机械冲击。5. 供电、波形验证与 PWM 故障保护让舵机驱动更稳5.1 先解决供电再怀疑代码舵机启动和加减载时电流波动远高于普通外设如果直接从 stm32f103c8t6 最小系统板的 3.3V 引脚取电电压很容易被拉低。标准模拟舵机建议用 4.8V 到 6V 电源独立供电电源负极和 STM32 的 GND、舵机信号参考地连在一起。如果不共地PWM 信号在舵机端没有参考电平舵机不会动作或乱抖。单个舵机调试时可以在舵机电源两侧并联 100uF 电解电容和 0.1uF 陶瓷电容电容尽量靠近舵机电源引脚多舵机或做机械臂最好直接从电池供电模块取电不要依赖 USB 口那几百毫安限流。5.2 用逻辑分析仪确认 PWM 波形将逻辑分析仪的通道接到舵机信号线引脚 PA6参考地接 STM32 的 GND采样率设置成 10MHz 左右触发方式选下降沿。在代码里让舵机停在 90 度抓取一个完整周期应该能看到高电平宽度约 1.5ms周期约 19.98ms 到 20.02ms。如果测量的高电平宽度是 500us 或 2500us说明 CCR 设置被其他代码覆盖如果周期明显不对重点检查 APB1 Timer Clocks 和 PSC。没有逻辑分析仪时可以用舵机仿真器这类工具对静态 PWM 做解析或者临时把pulse_us通过串口打印出来先确认计算值在 500 到 2500 之间。这样做能快速区分软件换算错误和硬件供电问题不用靠耳朵听舵机啸叫来猜。5.3 上电抖动与 PWM 故障保护CubeMX 中每个 GPIO 都有初始电平设置。对于 PWM 输出引脚建议初始电平设为 Low避免主频初始化完成前输出高电平让舵机在系统启动阶段先跑到一个极端位置。程序中还可以用窗口看门狗监控主循环一旦代码跑飞让定时器通道停止输出或把引脚拉低舵机失去信号后会保持在当前扭矩状态比盲目乱转安全。但注意窗口看门狗只解决程序异常不能防止舵机堵转造成的供电跌落实际应用中还是要靠供电方案兜底。如果舵机在某个角度一直嗡嗡响用手触摸外壳明显发烫应该立刻停止输出检查角度上限并把类似的堵转判断逻辑写进代码。需要控制多路舵机并希望具备真正的 PWM 故障保护时常见做法是外接 PCA9685 这类 PWM 驱动板用它的 OE 引脚做硬件使能异常时直接高阻输出F103C8T6 只要用 I2C 发控制命令CPU 负担比直接操作大量定时器通道小得多。本文还有配套的精品资源点击获取