ARTICLE DETAIL

资讯详情

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

STM32F103六路舵机PWM与串口调试解析

STM32F103六路舵机PWM与串口调试解析 简介这是面向STM32F103单片机的6自由度机械手舵机控制源码包适合正在学习STM32、PWM输出或机械臂控制的嵌入式开发者与机器人爱好者。代码以串口调试为核心提供6路PWM输出驱动6个舵机包含硬件初始化、系统时钟配置、延时、串口初始化、USMART调试组件及舵机控制初始化等完整底层逻辑可直接在Keil工程中编译烧录也可作为二次开发基础。包内含86个文件以C源文件36个和头文件38个为主另有启动文件、Hex固件、Keil工程配置、USMART调试相关文件及说明文档压缩包大小约695KB结构清晰便于按模块查看。目前已有987人学习阅读源码中同时给出基于标准外设库的初始化思路对理解单片机上电初始化流程、PWM通道配置和串口调试机制有实际参考价值。1. 6路PWM与串口调试STM32F103机械手项目的核心组合桌面级六自由度机械手从散件到能按指令抓取中间隔着两条路一是把6路舵机信号按正确时序从芯片引脚送出去二是让上位机调整角度时不用反复拔线烧固件。STM32F103在这两个任务上恰好都有先天优势——定时器通道能同时输出多路PWM串口又能接管在线调试两个功能组合起来就是这个源码包背后最常见的实现思路。很多初次接触这类项目的工程师会把注意力放在PWM占空比的计算上实际调试里最容易卡住的却是串口协议帧格式没设计好、校验不严、更新时序不对机械手就会表现出迟滞、抖动、偶发抽搐看起来像PWM配置错了其实是协议层的问题。这篇文章从定时器分频开始把6路PWM的生成、串口命令帧的解析、多路运动协调以及常见串口调试故障逐层讲透适合正在做机械手课设、自制桌面机械臂或者想基于F103二次开发控制板的工程师参考。2. STM32F103输出6路舵机PWM定时器通道分配、分频系数与脉宽计算2.1 舵机要的到底是什么信号20ms周期与0.5~2.5ms高电平绝大多数模拟舵机和数字舵机的兼容模式下控制信号都是周期20ms、高电平0.5~2.5ms的PWM。舵机内部的比较电路只关心高电平持续了多长时间周期的要求反而宽松一些20ms只是最常见的约定。所以真正要控制的量是脉宽不是占空比占空比只是脉宽除以周期之后的计算结果。这个区别决定了F103上定时器的配置思路先保证一个稳定的20ms周期然后把CCR比较寄存器的值直接映射成微秒级脉宽。角度与脉宽的对应关系如下表。目标角度高电平脉宽占空比20ms周期F103比较寄存器值1MHz计数0度500us2.5%50045度1000us5%100090度1500us7.5%1500135度2000us10%2000180度2500us12.5%2500有部分舵机手册会写周期可在10~30ms范围内工作但机械手场景里6路舵机最好统一到20ms这样改一路的脉宽不会因为周期抖动影响其他舵机的占空比换算。顺序舵机串行总线舵机另说PWM阶段先用这个标准。2.2 为什么F103的定时器刚好凑出6路STM32F103的通用定时器每个有4个捕获比较通道每个通道都能独立输出PWM。以最常见的F103C8T6为例它带TIM1高级定时器加TIM2、TIM3、TIM4三个通用定时器取TIM2的四路加TIM3的两路正好6路还多出两路做备用。引脚上不用做任何重映射PA0~PA3默认复用为TIM2_CH1~CH4PA6~PA7默认复用为TIM3_CH1~CH2。如果PCB设计时把PA2、PA3占给了USART2或其他功能可以改用TIM3的另外两个通道或TIM4但需要先查AFIO重映射表确认引脚不冲突。PA11、PA12尽量留给USB别把PWM或串口往这两个脚上放省得后续扩展USB时跟复用功能打架。这里再说一个容易踩的坑PB4、PB5默认是JTAG引脚复位后不当作普通IO用。如果为了布线把PWM引到PB4或PB5光配置GPIO不够还要在AFIO的SWJ_CFG里关闭JTAG否则电平根本出不来。桌面机械手的6路PWM用PA0~PA7就够没必要碰这个麻烦。2.3 寄存器方式初始化6路PWM的完整代码标准外设库和HAL库的底层逻辑都是配置分频、重载、比较模式这几个寄存器。用寄存器写虽然啰嗦但每一步都看得见出问题时对照手册查也最直接。下面这段代码基于72MHz主频在系统默认的SystemInit之后运行即可。void Servo_6ch_PWM_Init(void) { // 1. 使能GPIOA、TIM2、TIM3时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // PA0~PA7所在GPIOA RCC-APB1ENR | RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN; // 2. 配置PA0~PA3为TIM2_CH1~CH4PA6~PA7为TIM3_CH1~CH2 // 每个引脚4位0xB1011复用推挽输出50MHz uint32_t crl GPIOA-CRL; crl ~((0xF 0) | (0xF 4) | (0xF 8) | (0xF 12) | (0xF 24) | (0xF 28)); crl | ((0xB 0) | (0xB 4) | (0xB 8) | (0xB 12) | (0xB 24) | (0xB 28)); GPIOA-CRL crl; // 3. TIM2PSC71 - 计数时钟1MHzARR19999 - 周期20ms TIM2-PSC 71; TIM2-ARR 19999; // 4. 四个通道都设为PWM模式1OCxM110 TIM2-CCMR1 ~0x0070; TIM2-CCMR1 | 0x0060; // CH1 TIM2-CCMR1 ~0x7000; TIM2-CCMR1 | 0x6000; // CH2 TIM2-CCMR2 ~0x0070; TIM2-CCMR2 | 0x0060; // CH3 TIM2-CCMR2 ~0x7000; TIM2-CCMR2 | 0x6000; // CH4 // 5. 使能四路比较输出极性默认高有效 TIM2-CCER | 0x1111; // 6. 初始脉宽1.5ms对应约90度中位 TIM2-CCR1 1500; TIM2-CCR2 1500; TIM2-CCR3 1500; TIM2-CCR4 1500; TIM2-CR1 | 0x0001; // 启动计数 // 7. TIM3只配CH1和CH2参数同上 TIM3-PSC 71; TIM3-ARR 19999; TIM3-CCMR1 ~0x0070; TIM3-CCMR1 | 0x0060; // CH1 TIM3-CCMR1 ~0x7000; TIM3-CCMR1 | 0x6000; // CH2 TIM3-CCER | 0x0011; TIM3-CCR1 1500; TIM3-CCR2 1500; TIM3-CR1 | 0x0001; }代码的核心在第三步到第六步。PSC71让72MHz变成1MHz计数频率一个计数值正好1usARR19999让计数器从0数到19999后溢出周期就是20000us即20ms。PWM模式1的含义是向上计数过程中CNT小于CCR时输出有效电平所以CCR写500就是500us高电平CCR值直接等于微秒数上位机传下来的脉宽不用做任何换算就能写进寄存器。运行时修改CCR不需要重新初始化定时器赋值即刻生效。这是串口能在线调角度的前提。CCMR里OCxM那三位如果漏配CCER即使使能了输出也不会有波形这是寄存器写法里最常见的手误。2.4 分频系数、重载值与脉宽的换算关系换到其他主频或借用其他定时器时三个数要重新算。计数频率等于系统主频除以PSC加1周期等于ARR加1除以计数频率实际脉宽等于CCR除以计数频率单位是秒。PSC71、ARR19999、CCR500这个组合只在72MHz主频下成立换成48MHz主频的芯片同样的寄存器值周期就变成了30ms舵机偏转角度会跟着错。修改参数有个口诀改周期改ARR改分频改PSC改角度改CCR三者互不影响。但有一条红线CCR不能大于ARR否则输出会一直保持有效电平舵机直接冲到物理极限位。很多机械手上电猛抽一下就是这么来的所以后面串口解析层做500到2500的软件限幅比依赖任何硬件保护都可靠。3. 串口命令帧设计从字节流到6路PWM更新的解析实现3.1 帧格式帧头、命令、长度、数据、校验各归其位串口在机械手项目里承担的是在线调试和控制链路双重角色。六自由度机械手调整动作时经常要6路角度同时到位靠按键逐路调不现实一帧串口数据携带全部目标值才是合理做法。F103的USART1挂在PA9和PA10TTL电平和USB转串口模块交叉连接、共地就能通信这也是市面上串口调试助手配合烧写下载最常见的连接方式。帧格式按帧头、命令、长度、数据、校验五段设计具体如下表。字段长度说明帧头2字节固定0xAA 0x55避免单字节误同步命令1字节0x01单路设置0x02六路设置0x03状态查询0x04复位长度1字节后续数据字节数不含校验数据N字节角度脉宽按大端两字节存放校验1字节帧头到数据末尾的和校验取低8位角度用两字节是为了直接传输0~2500us的脉宽整数两字节上限65535余量充足。上位机发90度时可以直接换算成1500发下来也可以让F103自己换算取决于协议约定。和校验在115200波特率下误码率不高偶发一帧错误最多导致一次抖动对机械手来说可以接受如果传的是关键标定参数升级成CRC8也只改校验那一个分支。举一个完整帧的例子AA 55 02 0C 01 F4 01 F4 01 F4 01 F4 01 F4 01 F4 CS命令0x02表示设置六路长度0x0C表示后面12字节每路两字节大端这里六路都是0x01F4即500us对应0度。3.2 中断接收与状态机解析不在中断里等待任何东西串口解析最忌讳在中断里做循环等待。F103的USART1只有一个接收中断如果用阻塞方式等完整帧主循环稍一卡顿就会丢字节。正确做法是中断里只收单个字节喂给状态机状态机攒够一整帧后再派发。下面的实现逐字节处理任何时刻收到半个帧下一字节到达还能继续不会因为拆包乱序而锁死。typedef enum { ST_IDLE, ST_HEAD1, ST_HEAD2, ST_CMD, ST_LEN, ST_DATA, ST_CHK } FrameState_Type; static FrameState_Type st ST_IDLE; static uint8_t rx_buf[32]; static uint8_t rx_idx 0; static uint8_t rx_len 0; static uint8_t sum 0; void ParseFrameByte(uint8_t b) { switch (st) { case ST_IDLE: if (b 0xAA) st ST_HEAD1; break; case ST_HEAD1: st (b 0x55) ? ST_CMD : ST_IDLE; break; case ST_CMD: rx_buf[0] b; sum 0xAA 0x55 b; // 从帧头开始累计和校验 st ST_LEN; break; case ST_LEN: rx_buf[1] b; rx_len b; rx_idx 0; sum b; st (rx_len 0) ? ST_CHK : ST_DATA; break; case ST_DATA: rx_buf[2 rx_idx] b; sum b; rx_idx; if (rx_idx rx_len) st ST_CHK; break; case ST_CHK: if (sum b) DispatchFrame(rx_buf); // 校验通过才派发 st ST_IDLE; break; } } void USART1_IRQHandler(void) { if (USART1-SR USART_SR_RXNE) { uint8_t b (uint8_t)USART1-DR; // 读DR自动清RXNE标志 ParseFrameByte(b); } }在这个实现里状态迁移只依赖当前字节不依赖时间天然免疫帧间隙抖动。校验失败的处理是静默丢弃并回到IDLE不向对方报错。机械手控制场景里一轮20ms的PWM周期很快就能覆盖一次丢帧优先保证不误动比反馈错误更重要。想看调试信息的场合可以把校验失败计数累计起来用查询命令读走。rx_buf开32字节已经富余当前最长帧是帧头2加命令1加长度1加数据12加校验1共17字节留出的空间可以容纳将来扩展的标定命令。3.3 命令派发与回传一次性原子更新6路CCRDispatchFrame拿到的是校验通过、内容完整的帧接下来的任务是把数据段解析成6路脉宽并写入各自的CCR。这里有个关键顺序必须先把帧内所有角度都解析并限幅完再统一写寄存器不能边解析边写。否则上位机发六路数据时如果帧被截断会出现前几路已经更新、后几路还是旧值的撕裂状态机械手会摆出一个从未下发过的诡异姿态。void SetServoPulse(uint8_t ch, uint16_t pulse) { if (pulse 500) pulse 500; // 软限幅下限 if (pulse 2500) pulse 2500; // 软限幅上限 switch (ch) { case 0: TIM2-CCR1 pulse; break; case 1: TIM2-CCR2 pulse; break; case 2: TIM2-CCR3 pulse; break; case 3: TIM2-CCR4 pulse; break; case 4: TIM3-CCR1 pulse; break; case 5: TIM3-CCR2 pulse; break; } } static void DispatchFrame(uint8_t *buf) { uint8_t cmd buf[0]; uint8_t len buf[1]; if (cmd 0x02 len 12) { for (uint8_t i 0; i 6; i) { uint16_t pulse (uint16_t)((buf[2 i * 2] 8) | buf[3 i * 2]); SetServoPulse(i, pulse); } SendAck(0x82, 0); // 高位置1表示应答帧 } else if (cmd 0x03) { SendStatusReport(); // 回传当前6路CCR值 } }命令字节0x02的应答帧设计成0x82高位置1表示方向是下位机到上位机。这样上下行命令在同一个解析器里不会混淆上位机判断应答时看bit7即可。SendStatusReport回传的是当前CCR寄存器的实际值不是上位机最后一次下发的目标值两者在校验失败丢帧时会不一样这正好是调试时判断链路是否可靠的一个信号。4. 六自由度舵机协调角度映射、多路平滑与掉线保护4.1 角度换脉宽标定表与整数运算的取舍6路PWM都能输出后机械手还未必能好好动。每个舵机的实际行程有差异装到机械臂上之后又叠加了安装误差这就要引入标定表。标定表的每一项存三个量min_pulse对应0度、max_pulse对应180度、offset对应机械安装零位偏移。前两个解决舵机本身行程不一致的问题第三个解决舵机臂装歪的问题三者分开管理标定才不乱。角度换算在F103上不建议用浮点整数运算就够。下面的函数把0~180度换算成脉宽用一个uint32_t中间变量避免乘法溢出typedef struct { uint16_t min_pulse; // 0度对应脉宽默认500 uint16_t max_pulse; // 180度对应脉宽默认2500 int16_t offset; // 机械安装零位偏移单位us } ServoCal; static ServoCal cal[6] { {500, 2500, 0}, {500, 2500, 0}, {500, 2500, 0}, {500, 2500, 0}, {500, 2500, 0}, {500, 2500, 0} }; uint16_t AngleToPulse(uint8_t ch, int16_t deg) { uint16_t range cal[ch].max_pulse - cal[ch].min_pulse; uint32_t temp (uint32_t)deg * range; return cal[ch].min_pulse (uint16_t)(temp / 180) cal[ch].offset; }offset的正负取决于安装偏差方向标定时先把min_pulse和max_pulse对准舵机真实行程再单独调offset让机械臂零位对齐。上位机传角度还是传脉宽都可以底层建议统一用脉宽us做内部单位因为串口协议里两字节直接传脉宽最省事标定后的角度映射放在下位机做。4.2 用20ms定时中断做多路插值避免瞬间跳变直接改CCR等于让舵机瞬间跳变到目标角度。舵机本身跟得上但机械臂是大惯量结构跳变会引发整臂抖动齿轮也容易磨损。常见的做法是在20ms的PWM周期里做线性插值把一次大角度运动拆成几十个小步进。typedef struct { int32_t current; // 当前实际CCR值 int32_t target; // 目标CCR值 int16_t step; // 每20ms的增量 uint16_t remain; // 剩余步数 uint8_t active; } Motion; static Motion motion[6]; void Servo_StartMove(uint8_t ch, uint16_t target, uint16_t move_ms) { uint16_t steps move_ms / 20; if (steps 0) steps 1; motion[ch].current (int32_t)Servo_GetPulse(ch); motion[ch].target target; motion[ch].step (int16_t)((int32_t)target - motion[ch].current) / steps; motion[ch].remain steps; motion[ch].active 1; } void Motion_Tick(void) // 由20ms定时中断调用 { for (uint8_t i 0; i 6; i) { if (!motion[i].active) continue; if (motion[i].remain 1) { motion[i].current motion[i].step; motion[i].remain--; } else { motion[i].current motion[i].target; // 最后一步强制对准 motion[i].active 0; } SetServoPulse(i, (uint16_t)motion[i].current); } }这里有两个细节值得注意。一是整除误差如果目标减当前的差值不能被步数整除每步取整后的累加会不断偏离目标最后一步直接赋值为target可以消除累积误差。二是同步性六路运动在同一个Motion_Tick里更新所有通道从同一个tick起步同一拍结束不会出现某一路先到、其他路还在半路的拧巴姿态。上位机下发0x02六路命令时连续调用6次Servo_StartMove即可。move_ms必须取20的倍数否则steps的小数部分被截断实际运动时间会比预期偏短。4.3 掉线保护与软限位机械手最需要兜底的三个场景串口在机械手项目里是控制链路上位机崩溃、USB线被拔、串口模块松动都意味着链路中断。没有兜底策略的话舵机会保持在最后一个角度机械臂以不确定姿态悬停一旦有人碰到就可能受伤。最简单的保护是链路存活计数每收到一帧有效数据就重置计数器1ms系统节拍里递减减到0就执行安全动作。static volatile uint16_t link_alive; void OnFrameReceived(void) { link_alive 500; // 500ms内没收到帧就算掉线 } void SysTick_1ms(void) { if (link_alive 0) { link_alive--; } else { Servo_GoSafe(); } }Servo_GoSafe的行为应该做成可配置的回中位还是保持当前位置取决于机械臂当前处在安全姿势还是工作姿势。回中位的优点是姿态确定缺点是中位姿势本身可能压到周围物体保持当前位置更保守但掉线后机械臂会一直悬着。我的习惯是默认保持当前位置同时把回中位做成一条独立命令由上位机在正常结束时主动下发。至于PWM故障保护很多初学者会照搬电机驱动的思路去配置F103高级定时器的刹车功能。舵机场景用不上这个舵机内部的比较电路只认脉宽范围真正危险的是脉宽超限和掉线前者靠4.1的软限幅后者靠链路超时。刹车输入是给H桥功率级用的舵机不是功率级硬往里套只会增加配置复杂度。5. 串口调试避坑BOOT0烧写失败、ch340乱码与PWM波形验证5.1 串口烧写失败的排查顺序BOOT0、BOOT1与设备管理器F103串口ISP烧写的硬件前提是BOOT0拉高、BOOT1拉低上电或复位后进入系统存储器里的bootloader这时候PA9、PA10变成下载口。很多源码包里附带的是ISP下载说明但实际操作失败率最高的恰恰是这一步原因往往不在软件而在跳线帽。下表是常见的失败现象和检查路径。现象常见原因检查顺序芯片无响应、握手失败BOOT0没拉高或没复位确认BOOT0接3.3VBOOT1接地上电后再点连接握手成功但擦除/校验失败波特率偏差或线材过长把115200降到38400或9600重试提示芯片读保护Option Byte里设置了RDP执行解除读保护注意会整片擦除Flash下载成功但程序不运行BOOT0没拉回低下载完成后BOOT0接GND重新上电DAP/SWD下载失败供电不足或SWDIO/SWCLK虚焊先量3.3V对地再查两颗线到调试器的通断还有一个经常被忽略的场景用户程序把PA9或PA10复用成了其他外设。程序烧进去之后ISP再连接时bootloader虽然不依赖用户程序但PA9上的外部电路可能会影响串口电平导致握手不稳定。处理办法是点连接的同时给目标板重新上电让bootloader在引脚状态干净的情况下完成握手。5.2 ch340驱动与乱码先分清是枚举失败还是数据损坏USB转串口模块在Windows下最常见的两件事插上去设备管理器不认或者认了但收到一堆乱码。前者是枚举失败查ch340驱动版本和USB线质量后者才是通信参数或接线问题。在Linux下的Ubuntu环境里插上ch340后先用dmesg | grep -i ch34x看有没有ttyUSB节点没有就检查内核模块ch34x是否加载有节点但打不开时多半是权限用sudo usermod -aG dialout $USER把用户加进dialout组。判断是模块问题还是接线问题有个很快的办法把USB转串口模块的TXD和RXD直接短接用串口调试助手自发自收。回显正常说明模块本身没坏之后再去查STM32一侧。STM32侧的验证可以先用最简协议例如收到0xA5回0x5A确认PA9和PA10的TX/RX交叉正确、共地良好再上完整帧协议。5V和3.3V的电平不匹配也会造成乱码F103的串口是3.3V TTLUSB转串口模块要选3.3V档位接RS232电平转换器那种老式用法在这里不适用。5.3 用逻辑分析仪验证6路PWM波形软件打印脉宽值是间接证据波形才是直接证据。桌面开发用八通道逻辑分析仪采样率选24MHz测量脉宽精度完全够。验证步骤如下逻辑分析仪CH0夹PA7TIM3_CH2CH1夹PA6TIM3_CH1GND接目标板地线。打开上位机软件采样率设为24MHz采样时长设1秒。观察周期相邻两个上升沿的间隔应该在20ms左右偏差超过±2%要查主频配置。观察高电平宽度用测量功能读出来对比当前CCR值例如CCR1500时高电平应为1500us。通过串口助手发一帧0x02命令把六路角度全改掉观察所有通道的脉宽是否同步变化。如果波形显示脉宽恒为0或者恒为高电平先查CCR是否超过了ARR再查GPIO是否误配成了普通推挽输出而不是复用推挽。如果波形正常但舵机不动那问题在舵机供电或信号线上别急着改固件。这个判断顺序能省下大量反复烧写的时间。6. 从PWM舵机升级到总线舵机保留串口协议的最小改动方案6.1 为什么推荐总线舵机作为F103机械手的下一步PWM方案到了6自由度这个规模痛点会越来越明显每路一根信号线线束杂乱没有状态反馈舵机堵转只能靠电流间接判断改舵机ID和行程参数需要专用编程器。总线舵机用一根半双工串口线把所有舵机串起来角度、电压、温度都能回读ID和参数也能在线设置这就是总线舵机机械臂逐渐成为主流的原因。F103在这个升级里仍然够用。USART1继续接PC做调试USART3转做总线舵机口原来的串口帧解析层和标定表层完全保留只把最底层的写CCR替换成发送总线指令。上位机的命令格式、六路同步逻辑、掉线保护策略都不用变改动被隔离在一个函数里。6.2 底层替换SetServoPulse换一种实现原方案里所有角度最终都走SetServoPulse这个函数就是天然替换点。PWM版本直接写CCR寄存器总线版本则把脉宽换算成舵机位置值经USART3半双工发出去两种实现的上层接口保持一致。void SetServoPulse(uint8_t ch, uint16_t pulse) { // PWM方案写CCR寄存器 // 总线方案换算成位置值后发到对应ID uint16_t pos (uint16_t)((uint32_t)(pulse - 500) * 1000 / 2000); BusServo_SendCmd(ch, 0x01, (uint8_t *)pos, 2); }总线舵机的位置范围因型号差异很大有的是0~1000有的是0~4096换算公式必须按舵机手册调整。BusServo_SendCmd的发送要注意半双工方向切换发之前关接收发完最后一个字节等TC标志置位后再恢复接收否则自己发出的指令会从总线上收回来被误判成舵机应答。void BusServo_SendCmd(uint8_t id, uint8_t cmd, uint8_t *data, uint8_t len) { uint8_t frame[16]; uint8_t i; uint8_t sum 0; frame[0] id; // 不同品牌的帧格式差异较大此处仅为示意 frame[1] len 2; frame[2] cmd; for (i 0; i len; i) { frame[3 i] data[i]; sum data[i]; } frame[3 len] sum; USART3-CR1 ~USART_CR1_RE; // 关闭接收转向发送 for (i 0; i len 4; i) { while (!(USART3-SR USART_SR_TXE)); USART3-DR frame[i]; } while (!(USART3-SR USART_SR_TC)); // 等最后一个bit移出移位寄存器 USART3-CR1 | USART_CR1_RE; // 恢复接收 }20ms的插值周期对总线舵机来说非常从容一条指令的发送和应答在1ms内完成剩下的时间总线完全空闲。F103在两次指令之间做什么都不影响总线时序这是总线方案比PWM方案在时序上更宽松的地方。6.3 串口资源分配与控制周期对齐F103的USART1挂在APB2上时钟72MHzUSART3挂在APB1上默认36MHz。总线舵机跑1M波特率时USART3需要更高的分频精度误差积累会比USART1明显。遇到总线舵机误码率高的情况先查波特率分频误差再考虑把总线口挪到USART1或USART2上PC调试口改用USART3。如果确定要在USART3上跑1M检查一下APB1的实际时钟和BRR整定值别直接照搬USART1的参数。控制周期的对齐问题比波特率更隐蔽。上位机把插补周期从20ms改成10ms后总线舵机往往不会立刻报错但动作会明显变得肉、像是滞后一拍。原因是舵机内部有指令缓存10ms下发的指令在缓存区里排队舵机仍然按自己固件里的周期逐条执行。这时候不是舵机坏了也不是F103没发出去而是下位机的指令产生速率和舵机的指令消费速率没有对齐。排查思路是先看总线空闲时间用逻辑分析仪抓USART3的TX脚如果连续两帧指令间隙小于舵机数据手册里的最小应答间隔就是节拍太快导致的积压把插补周期调回20ms或在上位机侧对指令做节流即可解决。本文还有配套的精品资源点击获取
返回列表