
简介这是一份基于STM32C8T6微控制器的二自由度云台舵机调速控制完整工程面向嵌入式单片机开发者和机电控制爱好者解决二轴云台的坐标输入、舵机角度转换与速度精确控制问题。程序整合了UART串口通信、定时器PWM输出和PID闭环控制能根据上位机下发的X、Y坐标动态调整舵机脉宽实现平稳、可调速的云台转动适合用于机器人视觉、安防监控等场景。资源包共243个文件压缩后约7.18MB核心内容包括stm32f10x系列标准外设库源码、应用层C程序、Keil工程配置文件及编译后的axf/hex/bin固件同时包含编译中间文件与工程备份可直接导入工程二次修改也可用于分析编译流程或验证烧录结果。目前已有3458人学习下载是学习STM32运动控制与PID算法的实践参考资料。 最近在折腾一个基于STM32F103C8T6的二自由度云台项目。所谓二自由度就是水平旋转和上下俯仰两个轴各由一颗舵机带动。项目本身不复杂但真正把程序跑顺、让云台不抖不跳卡了不少时间。这里把我从硬件接线到软件调速的完整过程整理出来顺带把几个典型坑也一起记录。如果你是做课设、毕业设计或者想给摄像头云台做基础控制这篇应该能帮你少走不少弯路。先说结论这颗芯片驱动两个舵机绰绰有余难点不在输出PWM而在“调速”二字。舵机内部其实自带位置闭环我们给一个脉宽信号它就会拼命转到对应角度。如果你直接把0度到180度的目标值一次性丢过去舵机会以最大速度猛甩过去云台带着摄像头就像被抽了一鞭子转轴上的齿轮也容易打齿。所以这个项目真正要做的是在“当前角度”和“目标角度”之间搭一个平滑逼近的阶梯。1. 项目整体拆解1.1 核心需求是什么先把需求拆开才能决定后面怎么设计。我这个项目一共有四件事要做需求项具体要求备注两路舵机PWM输出频率稳定在50Hz脉宽500到2500分别控制水平轴和俯仰轴平滑调速舵机从A点转到B点不能直接跳变需要加减速规划两种输入来源摇杆ADC或串口指令调试和演示都方便状态可观测能实时看到当前角度和目标角度用串口回传四件事里第二件是核心也是这篇博客的重点。因为PWM输出是STM32定时器的基础能力网上例程一大堆输入解析也不难。真正影响体验的是舵机运动过程中的“手感”——说起来就一句话代码里却要处理好几个细节包括步进量设计、死区设置、边界限制。1.2 为什么选STM32C8T6而不是别的方案刚开始做的时候我也犹豫过要不要用51或者ESP32。51简单但它的定时器资源紧张要同时出两路稳定PWM还得靠软件模拟再做加减速就比较吃力。ESP32性能强但是双核架构、WiFi协议栈这些对纯舵机控制项目来说属于“用不上还要兼顾复杂度”的东西。STM32F103C8T6在这里算是恰到好处72MHz主频多个定时器ADC和UART齐全64KB Flash装一个云台控制程序绰绰有余。更重要的是它的资料在网络上非常多遇到问题搜起来方便。对课设和毕业设计来说用这颗芯片也比较好讲故事外围电路简单不容易在答辩时被抓着硬件问题问住。2. 舵机控制原理与调速本质2.1 PWM角度映射的底层计算舵机控制说到底就是“脉宽决定角度”。标准模拟舵机工作在50Hz也就是周期20ms在这个周期内高电平时间从0.5ms到2.5ms变化时舵机角度从0度转到180度。我用的STM32F103C8T6主频72MHz定时器选择TIM2的CH1和CH2。预分频器PSC设为71计数频率变成72MHz/(711)1MHz即每个计数值代表1微秒。自动重载值ARR设为19999于是PWM频率为1MHz/(199991)50Hz正好对得上。角度转CCR值的公式是uint16_t angle_to_ccr(float angle) { if (angle 0) angle 0; if (angle 180) angle 180; return (uint16_t)(angle * 2000.0f / 180.0f 500); }这里的500和2500就是0.5ms和2.5ms对应的比较值1.5ms对应1500也就是90度中位。有一点需要提醒不同品牌、不同型号的舵机对脉宽范围的解析不是完全一致的比如MG996R的实际行程可能在600到2400之间所以正式使用前最好用示波器或者串口回读实测一下把角度校准到实际机械零位。2.2 为什么不能直接给目标角度我第一次上电时直接把目标CCR从1500改到2400结果云台“咔”一下甩过去声音非常响。这个现象背后其实很直观舵机内部有位置闭环芯片给的脉宽变了它就会以最大速度驱动电机去追目标。舵机的减速齿轮组放大了电机扭矩也放大了机械冲击直接跳变轻则导致云台姿态瞬间失稳重则会打齿、堵转、拉低电源电压。用一个生活类比来讲这就像你开车从静止直接加速到60码然后到路口再一脚刹死乘客肯定受不了。舵机比人还脆弱因为它不允许任何过冲一过冲就会来回震荡。所谓“调速”实际就是限制目标角度变化的速率让舵机内部的闭环一直处于“可追赶”的状态。2.3 从档位调速到梯形加减速最简单的调速是分档误差大于1000就用大步进误差在300到1000之间用中步进小于300用小步进。这种办法代码量少也能缓解猛甩但速度切换时仍然有阶梯感仔细听能听到舵机声音“呜—呜—呜”分段变化。更接近真实运动控制的方案是梯形加减速。思路很简单启动时步进量从0开始每个控制周期增加一点直到达到设定的最大步进接近目标时根据剩余距离提前减速让舵机平稳停下。这样做的好处是运动过程连续不会出现档位切换时的顿挫感。我在项目里把最大步进设为80也就是每10ms移动80个脉宽单位加速度步进设为8。如果从0度转到180度总距离约2000理论最快250ms走完但加上加减速过程会到300ms左右视觉上观感非常舒服。3. 软件实现与关键代码3.1 PWM定时器初始化软件部分我用的标准库因为习惯了而且这个项目的逻辑不依赖HAL的分层标准库直接操作寄存器也更直观。TIM2的CH1对应PA0CH2对应PA1初始化代码大概如下void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 20000 - 1; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 1500; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC2Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }这里有个新手容易踩的坑TIM2挂在APB1上但72MHz主频下APB1分频系数为2时定时器时钟实际还是72MHz所以PSC直接按72算没问题。如果你把PSC写成了72没减1实际就是73分频计数频率就不是整数了舵机会出现细微的周期性抖动。3.2 加减速状态机实现调速逻辑我用一个10ms的定时器中断驱动每个周期更新一次两个舵机的目标值。这里定义一个结构体来管理每个轴的当前位置和目标位置typedef struct { int16_t current; int16_t target; int16_t speed; int16_t max_step; int16_t accel; } ServoAxis_t; ServoAxis_t servo[2] { {1500, 1500, 0, 80, 8}, {1500, 1500, 0, 80, 8} }; void Servo_Update(ServoAxis_t *s, uint8_t axis) { int16_t delta s-target - s-current; if (delta 0) { s-speed 0; return; } int16_t dist delta 0 ? delta : -delta; if (s-speed * s-speed / (2 * s-accel) dist) { s-speed - s-accel; // 剩余距离不够刹车了减速 } else if (s-speed s-max_step) { s-speed s-accel; // 加速段 } if (s-speed 0) s-speed 0; if (s-speed dist) s-speed dist; if (delta 0) { s-current s-speed; } else { s-current - s-speed; } if (axis 0) { TIM_SetCompare1(TIM2, s-current); } else { TIM_SetCompare2(TIM2, s-current); } }这段代码里的核心是刹车距离判断公式speed * speed / (2 * accel)来自运动学当前速度从speed减到0需要多长的距离。每次更新时先看“如果现在开始减速能不能在到达目标前停下来”能就继续加速不能就减速这样就能自然形成梯形速度曲线不需要额外维护加速段和减速段的标志位。3.3 摇杆与串口两种输入方式输入部分我做了两套并行的方式。摇杆用ADC读取两个通道分别对应水平和俯仰。ADC我开了多次采样取平均避免单次读取抖动导致舵机小范围乱动uint16_t adc_value Read_ADC_Average(ADC_CH0, 8); servo[0].target 500 (uint32_t)adc_value * 2000 / 4095;注意摇杆电位器是线性变化的但舵机角度对脉宽也是线性所以直接映射就行不需要关心具体电压值。串口协议则简单定义成一行文本“P 1500 1500”空格分隔两个目标值解析后直接写到servo[0].target和servo[1].target。调试的时候我还会把current和target通过串口打印出来观察速度曲线是否符合预期这个习惯帮我发现过好几次参数设置错误。4. 硬件接线、供电与机械细节4.1 接线与共地要点硬件接线看起来简单实际上坑比软件多。我的接线是这样的连接项接线说明水平舵机信号线PA0TIM2_CH1俯仰舵机信号线PA1TIM2_CH2舵机电源正极外接5V电源正极舵机电源负极外接电源GND同时接到STM32的GND电源滤波在舵机电源两端并联一个470uF电解电容共地这一步很多人会漏。STM32的GND如果不和舵机电源的GND连在一起PWM信号的高电平没有参考地舵机收到的是乱七八糟的差分电平轻则乱转重则不转。电容的作用则是吸收舵机启动瞬间的电流尖峰能明显减少电源电压跌落。4.2 供电电流和舵机选型心得舵机选型直接决定供电方案。SG90空载电流只有100多毫安堵转约700mA5V 1A电源就能带一个MG996R空载就要500mA左右堵转能到2.5A二自由度云台装两个MG996R峰值电流经常超过3A。如果还用USB给板子供电舵机一动作USB口的电压直接掉到4V以下板子就会重启这是最常见的故障之一。我实测下来的可靠组合是C8T6的3.3V由USB转串口模块提供舵机单独用一个5V 3A开关电源。开关电源和线性稳压模块相比对峰值电流的耐受能力好很多不会像稳压芯片那样一过流就进入保护。如果条件有限至少也要用一个输出能力2A以上的降压模块并且不要把舵机电源和主控电源做成一路。4.3 机械重心对调试的影响程序调到差不多了替换云台支架上的摄像头后俯仰轴开始出现奇怪的抖动而且是朝下坠的方向抖。排查下来发现是重心问题摄像头加固定架之后整个负载重心偏到了舵机转轴前方舵机为了保持角度静态时就要持续输出扭矩动态时更是吃力。处理办法是给云台后端加配重让重心回到转轴附近。配重不需要很精确让舵机静态保持角度时不发烫就算达标。另外云台支架的螺丝一定要拧紧螺丝松的时候舵机在运动过程中会带动支架产生微小的来回晃动听起来就像控制有问题其实是机械回差。5. 常见问题与排查实录5.1 舵机抖动和吱吱叫这个现象最典型。先听声音再摸发热基本能判断原因如果抖动频率很高且声音尖锐大概率是电源不稳或信号线过长如果舵机在固定角度持续有轻微“吱吱”声可能是CCR值在目标点附近反复跳动。前者的处理是加强供电、缩短信号线后者的处理是在程序里加死区容差比如current和target相差小于5时不更新输出。模拟舵机的PWM频率必须保持在50Hz左右不要为了追求“平滑”把频率调到100Hz以上。这个方向性错误我犯过调完后舵机反而发热严重因为模拟舵机内部电路对高频率PWM信号会持续触发位置比较一直处于紧张状态。5.2 下载失败与no stm32 target found用ST-LINK下载时最常遇到的报错就是“no stm32 target found”。第一次遇到先查三件事BOOT0是否接地、SWDIO和SWCLK两根线是否接对、目标板有没有供电。还有一个隐蔽原因代码里把PA13和PA14配置成了复用功能把SWD引脚占用了导致芯片再也没法通过ST-LINK连接。解决占用的标准操作是按住板子复位键在IDE里点击下载同时松开复位键。ST-LINK会在复位释放后的极短时间内尝试握手抢在用户代码重新配置GPIO之前连上芯片然后擦除Flash。如果这个方法也失效就只能用串口ISP方式先擦除整个芯片。5.3 上电瞬间舵机猛甩一下这个问题排查了很多天。STM32在上电初始化过程中GPIO默认浮空输入舵机信号线上的电平不受控如果刚好出现一个接近2.0ms的高电平脉冲舵机就会在上电瞬间猛转一下。我的解决办法是在初始化代码最开头先把PA0和PA1配置成推挽输出并输出低电平然后再初始化定时器PWMGPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1);这样在上电到PWM生效之间的短暂窗口里信号线被强制拉低舵机收到的是一个接近0ms脉宽不会触发转动。另外一种做法是给舵机电源加MOS管延迟上电等程序跑到正常状态再通电效果也更彻底。5.4 串口乱码与ADC抖动串口打印出现乱码九成是波特率不一致或者GND没共地先检查这两项。另外STM32的UART如果使用外部晶振晶振实际频率偏差也会导致波特率轻微偏移长时间传输偶尔出错这时可以减小波特率到9600或者换用内部时钟修正。ADC抖动也很常见摇杆静止时读数还能上下跳几十个数。我在代码里做了两件事一是连续采集8次求和后取平均等于一个简单滑动平均二是对摇杆设置中立死区比如中心值上下50以内当0处理这样舵机不会因为摇杆的微小漂移而发出“嗡嗡”的修正声。6. 写在最后把整个项目跑顺之后回头看最花时间的其实不是定时器配置也不是PWM输出而是“让舵机像人操作一样运动”这个看似不起眼的要求。做这类控制项目我的建议是别急着堆功能先把一个舵机从转动到停止的整个过程调到满意再加第二个轴最后再接入摇杆或者视觉模块。最后再分享一个小技巧调试时把加减速参数通过串口打印出来观察速度曲线是不是梯形比靠耳朵听声音可靠得多。我现在还留了一个后面扩展的口子等接了OpenMV做视觉追踪这套加减速代码可以原样保留只需要把target的来源从串口改成视觉模块的坐标解算结果一个基础的自动追踪云台就有了雏形。本文还有配套的精品资源点击获取