恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32舵机控制机械臂实战:从PWM配置到逆运动学避坑指南
首页
资讯中心
/
STM32舵机控制机械臂实战:从PWM配置到逆运动学避坑指南
STM32舵机控制机械臂实战:从PWM配置到逆运动学避坑指南
发布时间:2026/10/15 15:36:21
简介这份资源面向嵌入式开发初学者与机器人爱好者提供一套以STM32微控制器驱动舵机、并通过数据手套实现手势操控机械臂的完整工程。项目将嵌入式控制、舵机PWM角度调节与传感器人机交互结合可用于课程设计、毕业设计或机器人入门实践。压缩包为rar格式共206个文件约4.88MB以C源码与头文件为核心包含STM32外设驱动、工程配置、编译中间文件及可执行输出覆盖从源码到固件的完整开发链路。目前已有6568人学习下载热度较高。读者可从中获取STM32工程搭建思路、舵机控制固件实现、数据手套信号解析与系统集成方法并借助现成工程结构快速复现手势控制机械臂方案理解嵌入式系统与交互设计的结合方式。1. 从抖舵到稳如磐石STM32舵机控制机械臂的工程真相很多人第一次用 STM32 驱动机械臂都会遇到同一个场景代码烧进去舵机开始疯狂抖动机械臂像得了帕金森要么原地抽搐要么直接冲到限位角把齿轮扫平。你以为是舵机坏了换一个新的还是抖。你以为是电源不够换个大功率的依然抖。最后发现问题根本不在舵机也不在电源而在 STM32 输出的 PWM 信号上——周期不对、占空比分辨率不够、多路定时器不同步任何一个细节都能让机械臂变成一台昂贵的震动棒。STM32 舵机控制机械臂这件事核心就一句话用 STM32 的定时器外设产生多路稳定、可独立调节的 50Hz PWM 信号每路对应一个舵机通过改变脉宽控制舵机转角再配合逆运动学解算让末端执行器走到指定坐标。听起来简单但真正落地时你会碰到定时器资源分配、多路 PWM 相位对齐、电源地弹、舵机死区补偿、机械臂坐标系标定这一连串问题。这套方案适合谁适合已经会用 STM32 点灯、串口收发想从单舵机玩具跨到多自由度机械臂的嵌入式开发者也适合做毕设、做小型自动化装置、做教学演示的工程师。它不需要昂贵的总线舵机普通 PWM 舵机就能跑成本可控但前提是你得把下面这些坑一个个填平。2. 定时器选型与多路PWM配置为什么你的四路舵机总在打架2.1 先搞清楚50Hz PWM 对 STM32 定时器意味着什么舵机的控制协议非常朴素周期 20ms50Hz脉宽 0.5ms 到 2.5ms 对应 0 到 180 度中间 1.5ms 是 90 度。STM32 的通用定时器 TIM2、TIM3、TIM4 都是 16 位APB1 总线频率常见 72MHz以 F103 为例预分频器 PSC 和自动重装载寄存器 ARR 决定了 PWM 周期。计算方式PWM 频率 72MHz / ((PSC1) * (ARR1))。要得到 50Hz可以设 PSC71ARR19999。这样计数频率是 1MHz每个计数 1usARR19999 意味着 20000 个计数正好 20ms。此时 CCR 寄存器的值直接就是脉宽微秒数CCR500 对应 0.5msCCR1500 对应 1.5msCCR2500 对应 2.5ms。这个配置的好处是直观坏处是分辨率只有 1us对应舵机角度约 0.09 度对大多数机械臂够用但对高精度场景偏粗。如果你想要更高分辨率可以设 PSC0ARR1439999计数频率 72MHz每个计数约 13.9nsCCR 值需要换算但分辨率大幅提升。代价是 16 位定时器最大 ARR 是 65535根本装不下 1439999所以必须用 32 位定时器或者降低计数频率。常见做法是 PSC7ARR179999计数频率 9MHz每个计数约 0.111us分辨率约 0.01 度已经足够。但注意ARR 超过 65535 时只有 TIM2 和 TIM5 在 F4 系列上是 32 位的F103 全是 16 位所以 F103 上你只能用 1us 分辨率方案。2.2 四路舵机怎么分配定时器一个定时器四通道 vs 四个定时器机械臂通常 4 到 6 个自由度对应 4 到 6 个舵机。STM32 的通用定时器每个有 4 个独立通道所以一个 TIM3 就能出 4 路 PWM。但这里有个隐藏问题同一个定时器的四个通道共享 ARR 和 PSC所以周期完全同步这其实是好事因为四路舵机的 20ms 周期严格对齐不会出现某一路周期偏移导致舵机抖动。但如果你用四个不同的定时器每个定时器的时钟源、预分频器、ARR 都独立即使你设置相同的值实际周期也会有微小差异示波器上看就是几微秒的偏差。对舵机来说周期偏差几微秒通常没事但如果偏差超过 100us舵机内部的控制环路就会开始抽搐。我一般会这样分配TIM3 出四路负责底座旋转、大臂、小臂、腕部俯仰TIM4 出两路负责腕部旋转和夹爪。这样两个定时器都是 APB1 上的时钟同源周期一致性有保障。配置时注意 GPIO 复用功能要开F103 上 TIM3 的通道 1 到 4 对应 PA6、PA7、PB0、PB1TIM4 对应 PB6 到 PB9。如果你用 F4 系列引脚映射不同查数据手册的复用功能表。// STM32F103 标准库配置 TIM3 四路 PWM50Hz1us 分辨率 void Servo_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能时钟TIM3 和 GPIOA、GPIOB RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // PA6、PA7 复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PB0、PB1 复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_Init(GPIOB, GPIO_InitStructure); // 定时器基础配置PSC71ARR19999计数频率 1MHz周期 20ms TIM_TimeBaseStructure.TIM_Period 19999; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 四路 PWM 模式 1 配置 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 1500; // 初始 1.5ms90 度 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_OC3Init(TIM3, TIM_OCInitStructure); TIM_OC4Init(TIM3, TIM_OCInitStructure); // 使能预装载防止更新 CCR 时产生毛刺 TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }这段代码的关键点有三个。第一PSC 和 ARR 的取值决定了 1us 分辨率CCR 直接写微秒数调试时心里有数。第二TIM_OCPreload_Enable必须开否则你在运行中修改 CCR 时计数器可能正好在比较点附近输出会出现一个异常窄的脉冲舵机就会抖一下。第三四路通道的初始脉宽都设成 1500上电时机械臂保持中位不会突然甩出去。2.3 占空比更新为什么直接写 CCR 会抖怎么做到无毛刺即使开了预装载直接写TIM_SetCompare1(TIM3, 1500)也不是绝对安全。预装载的意思是 CCR 的写入先进入影子寄存器等下一个更新事件UEV才真正生效。但如果你在中断里连续写多个通道或者写入时机正好在更新事件前后仍然可能出现某一路延迟一个周期生效。对舵机来说一个周期的延迟就是 20ms肉眼看不出来但机械臂多关节联动时这种微小的不同步会累积成轨迹偏差。更稳妥的做法是把所有通道的目标 CCR 值先算好放在一个数组里然后在定时器的更新中断里一次性写入。这样所有通道在同一个更新事件后同时生效相位严格对齐。更新中断频率是 50Hz中断里做六路写入耗时不到 2us对 CPU 负担可以忽略。// 在 TIM3 更新中断中同步更新四路 CCR volatile uint16_t servo_target[4] {1500, 1500, 1500, 1500}; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 一次性写入四路保证同周期生效 TIM3-CCR1 servo_target[0]; TIM3-CCR2 servo_target[1]; TIM3-CCR3 servo_target[2]; TIM3-CCR4 servo_target[3]; } }这里直接操作寄存器而不是调库函数是因为库函数内部有参数检查耗时略长而且在中断里调库函数不是好习惯。注意servo_target要加volatile防止编译器优化掉。中断优先级不要设太高否则会影响串口接收等其他任务一般设成 1 或 2 即可。3. 逆运动学解算与舵机角度映射从坐标到脉宽的完整链路3.1 二连杆平面机械臂的逆解为什么你的末端总是差几毫米假设机械臂底座旋转一个舵机大臂和小臂各一个舵机构成一个平面二连杆结构。大臂长度 L1小臂长度 L2末端目标坐标 (x, y) 是在底座旋转后的平面内。逆运动学要解出两个角度大臂与水平面的夹角 θ1小臂与大臂延长线的夹角 θ2。标准解法用余弦定理先算末端到原点的距离 r sqrt(x² y²)然后 cos(θ2) (r² - L1² - L2²) / (2 * L1 * L2)。注意 θ2 是关节角不是小臂与水平面的夹角。解出 θ2 后θ1 atan2(y, x) - atan2(L2 * sin(θ2), L1 L2 * cos(θ2))。这里有个易错点atan2 的参数顺序是 (y, x)不是 (x, y)写反了角度会差 90 度。解出来的 θ1 和 θ2 是弧度需要转成舵机角度。但舵机的 0 到 180 度对应机械结构的实际角度范围中间有机械安装偏移。比如大臂舵机在 90 度时大臂水平那么 θ10 对应舵机 90 度θ145 度对应舵机 135 度。这个映射关系每个关节都不一样必须单独标定。import math def inverse_kinematics(x, y, L1, L2): 二连杆逆运动学返回大臂和小臂的关节角弧度 x, y: 末端目标坐标平面内 L1, L2: 大臂、小臂长度 r_sq x*x y*y r math.sqrt(r_sq) # 检查是否超出工作空间 if r L1 L2 or r abs(L1 - L2): raise ValueError(目标点超出工作空间) # 余弦定理求肘关节角 cos_theta2 (r_sq - L1*L1 - L2*L2) / (2 * L1 * L2) cos_theta2 max(-1.0, min(1.0, cos_theta2)) # 防止浮点误差越界 theta2 math.acos(cos_theta2) # 肘部弯曲角0 到 pi # 求肩关节角 alpha math.atan2(y, x) beta math.atan2(L2 * math.sin(theta2), L1 L2 * math.cos(theta2)) theta1 alpha - beta return theta1, theta2这段代码里cos_theta2的钳位很重要。实际计算中由于浮点误差cos 值可能变成 1.0000001math.acos 会抛异常。钳位到 [-1, 1] 是标准做法。另外theta2 取正值表示肘部向上弯曲取负值表示向下机械臂通常用向上方案避免与底座干涉。3.2 舵机角度到 CCR 值的映射一个线性公式背后的三个参数舵机的控制脉宽 500us 到 2500us 对应 0 到 180 度这是理想情况。实际舵机存在三个偏差零点偏差、行程偏差、死区。零点偏差是舵机在 1500us 时不一定正好 90 度可能偏 2 到 3 度。行程偏差是 500us 和 2500us 对应的实际角度可能只有 170 度或超过 180 度。死区是舵机对微小脉宽变化不响应通常在 5us 以内。工程上的做法是不直接用 500 到 2500 的线性映射而是为每个舵机标定两个端点。比如让舵机转到机械限位的最小角度记录此时的 CCR 值 ccr_min转到最大角度记录 ccr_max。然后角度到 CCR 的映射就是线性插值ccr ccr_min (ccr_max - ccr_min) * angle / 180。// 舵机标定参数结构体 typedef struct { uint16_t ccr_min; // 最小角度对应的 CCR uint16_t ccr_max; // 最大角度对应的 CCR int16_t offset; // 零点补偿单位 us uint8_t reversed; // 是否反向安装 } ServoCalib; ServoCalib servo_calib[6] { {500, 2500, 0, 0}, // 底座 {600, 2400, -20, 0}, // 大臂标定后范围偏窄 {550, 2450, 10, 1}, // 小臂反向安装 {500, 2500, 0, 0}, // 腕部俯仰 {500, 2500, 0, 0}, // 腕部旋转 {700, 2300, 0, 0}, // 夹爪行程小 }; uint16_t servo_angle_to_ccr(uint8_t id, float angle) { ServoCalib *cal servo_calib[id]; if (cal-reversed) { angle 180.0f - angle; } float ccr cal-ccr_min (cal-ccr_max - cal-ccr_min) * angle / 180.0f; ccr cal-offset; // 钳位到安全范围 if (ccr cal-ccr_min) ccr cal-ccr_min; if (ccr cal-ccr_max) ccr cal-ccr_max; return (uint16_t)ccr; }标定过程我一般这样操作先写一个简单的串口命令解析能手动设置某个舵机的 CCR 值。然后逐步增加 CCR观察机械臂实际角度记录两个极限位置的 CCR。这个过程血泪经验是一定要先拆掉负载或者让机械臂悬空否则标定过程中舵机堵转电流飙升容易烧驱动板。3.3 多关节联动时的速度规划为什么直接给目标角度会过冲如果你直接把逆解算出的角度赋给舵机舵机会以最大速度冲过去大臂和小臂同时猛甩末端轨迹完全不可控机械臂会剧烈晃动甚至因为惯性导致舵机齿轮打滑。正确的做法是加一个速度规划让每个关节的角度随时间平滑变化。最简单的是梯形速度规划设定最大角速度 ω_max 和最大角加速度 α_max每个控制周期比如 20ms更新一次目标角度直到到达目标。更简单的是线性插值把当前角度到目标角度分成 N 步每步走固定增量步数由最大速度决定。// 每个关节的插值状态 typedef struct { float current; // 当前角度 float target; // 目标角度 float step; // 每周期步进角度 } JointState; JointState joints[6]; void joint_update_target(uint8_t id, float target_angle, float max_speed_deg_per_sec) { joints[id].target target_angle; // 控制周期 20ms计算每周期最大步进 float max_step max_speed_deg_per_sec * 0.02f; float diff target_angle - joints[id].current; if (diff max_step) { joints[id].step max_step; } else if (diff -max_step) { joints[id].step -max_step; } else { joints[id].step diff; } } void joint_tick(void) // 每 20ms 调用一次 { for (int i 0; i 6; i) { if (joints[i].step ! 0) { joints[i].current joints[i].step; // 检查是否到达 if ((joints[i].step 0 joints[i].current joints[i].target) || (joints[i].step 0 joints[i].current joints[i].target)) { joints[i].current joints[i].target; joints[i].step 0; } uint16_t ccr servo_angle_to_ccr(i, joints[i].current); servo_target[i] ccr; } } }这个逻辑放在 20ms 的定时任务里和 PWM 更新中断分开。PWM 中断只负责把servo_target写入 CCR角度插值在主循环或另一个定时器里做。这样解耦的好处是即使插值计算耗时波动PWM 输出依然稳定。4. 电源与地弹机械臂抖动的头号元凶往往不在代码4.1 为什么换了三块驱动板还是抖共地问题的排查方法舵机堵转电流能到 1A 甚至 2A六个舵机同时动作瞬间电流可能超过 5A。如果 STM32 和舵机共用一根细长的杜邦线供电线路压降会导致 STM32 的 3.3V 稳压器输入跌落MCU 复位舵机失去信号然后 MCU 重启又输出信号舵机又动形成周期性抖动。这种抖动看起来像软件问题实际上是电源问题。排查方法很简单用示波器看 STM32 的 3.3V 电源轨同时让机械臂做动作。如果看到 3.3V 上有超过 200mV 的跌落基本可以确定是电源问题。没有示波器的话用万用表直流档看平均电压如果动作时电压从 3.3V 掉到 3.0V 以下也是同样结论。解决方案是电源分离舵机用独立的 5V 或 6V 电源STM32 用另一路 5V 或 3.3V 电源两路电源的地线在一点汇合。注意是单点共地不是多点接地。多点接地会形成地环路引入更多噪声。我一般会在舵机电源正负极之间并一个 1000uF 的低 ESR 电解电容再并一个 0.1uF 陶瓷电容吸收瞬态电流。4.2 信号线上的小电阻一个 100 欧姆电阻解决 80% 的随机复位即使电源分离了舵机信号线仍然可能把噪声耦合回 STM32。舵机内部有电机和电位器电机换向时会产生高频噪声通过信号线传回 GPIO。STM32 的 GPIO 输入阻抗高噪声电压容易超过阈值导致定时器通道误触发或者 GPIO 状态异常。在信号线上串一个 100 欧姆电阻靠近 STM32 一端能有效抑制反射和噪声。这个电阻对 PWM 信号本身影响很小因为舵机信号输入阻抗通常在 10k 以上100 欧姆分压不到 1%。如果噪声特别严重还可以在信号线和地之间并一个 100pF 电容构成低通滤波截止频率约 16MHz对 50Hz PWM 毫无影响但能把几十 MHz 的噪声旁路掉。注意串阻和电容要放在 STM32 端不要放在舵机端。放在舵机端的话信号线本身成了天线噪声在到达电阻之前就已经耦合进来了。4.3 舵机死区与机械回差为什么你的机械臂重复定位精度只有几毫米舵机内部是电位器反馈齿轮组有回差通常 1 到 2 度。加上舵机死区你给同样的 CCR 值舵机正转到位和反转到位的位置可能差 2 到 3 度。对 20cm 长的臂3 度对应末端偏差约 10mm。这就是为什么很多人的机械臂重复定位精度只有几毫米。改善方法有两个。一是单向逼近无论目标角度在哪都让舵机从同一个方向转过去。比如始终从小于目标角度的方向逼近这样回差始终在同一侧重复性会好很多。二是软件补偿记录每个关节正转和反转的到位 CCR 差值在换向时加上补偿。第一种方法简单可靠我一般优先用。// 单向逼近始终从负方向逼近目标 void joint_move_approach(uint8_t id, float target_angle) { float current joints[id].current; if (current target_angle) { // 先退后一点再正向逼近 joints[id].current target_angle - 3.0f; // 退 3 度 // 然后正常插值到目标 } joint_update_target(id, target_angle, 60.0f); }这个 3 度是经验值具体取决于舵机回差大小。标定方法是让舵机从 0 度转到 90 度记录 CCR再从 180 度转到 90 度记录 CCR两个 CCR 的差值对应回差角度。5. 避坑与排查六个让机械臂从抽搐到顺滑的关键检查点5.1 现象上电瞬间所有舵机同时猛转到底原因STM32 复位期间 GPIO 是浮空输入定时器没有输出舵机失去信号。但舵机上电后如果没有 PWM 信号内部电路可能把电机驱动到某个默认位置通常是极限位置。解决在 GPIO 初始化之前先让定时器通道输出正确的初始 PWM。具体做法是在main函数最开头就调用Servo_PWM_Init()并且在初始化 GPIO 之前先把 CCR 设成 1500再使能定时器。另外舵机电源和 STM32 电源的上电顺序也有影响最好用同一个开关同时上电或者先给 STM32 上电再给舵机上电。5.2 现象串口发送目标坐标后机械臂动一下就停然后复位原因串口接收中断和 PWM 更新中断优先级冲突或者串口接收缓冲区溢出导致 HardFault。排查先看串口波特率是否匹配再看中断优先级分组。STM32 默认优先级分组是 0所有中断都是抢占优先级如果串口中断优先级高于 PWM 更新中断串口数据量大时会阻塞 PWM 更新舵机信号周期被拉长舵机抖动。解决把 PWM 更新中断优先级设为最高串口接收中断设为次高主循环处理协议解析。另外串口接收用 DMA 加空闲中断不要用单字节中断减少中断频率。5.3 现象机械臂动作时OLED 屏幕闪烁或者 I2C 传感器读数跳变原因舵机电流突变导致电源波动I2C 总线的上拉电阻供电不稳通信误码。解决I2C 设备单独用 LDO 供电或者在 I2C 上拉电阻的电源脚加 10 欧姆电阻和 10uF 电容构成 RC 滤波。另外I2C 速率降到 100kHz不要用 400kHz给通信留更多裕量。如果 OLED 和舵机共用 5V 电源中间加一个磁珠或者电感隔离。5.4 现象逆解算出的角度正确但机械臂末端总是偏向一侧原因底座舵机的零点没有标定或者底座旋转中心与机械臂平面原点不重合。排查先单独给底座舵机发 1500看机械臂是否指向正前方。如果偏了调整底座舵机的 offset 参数。然后检查大臂和小臂的安装平面是否与底座旋转轴垂直如果不垂直逆解算的平面假设就不成立需要加补偿角。解决用激光笔固定在末端在地面画网格手动示教几个点反算补偿角。5.5 现象舵机在某个角度附近发出嗡嗡声但不动作原因舵机死区导致在这个角度附近CCR 的微小变化不足以驱动电机但电机有保持力矩产生高频振荡。解决在这个角度附近给 CCR 加一个小的抖动比如 ±2us 的三角波让舵机持续微调避免死区。或者降低这个关节的负载检查机械结构是否卡涩。如果是金属齿轮舵机死区通常比塑料齿轮小但噪声更明显。6. 进阶技巧用定时器DMA和查表法把六轴联动做到毫秒级响应当你把上面这些坑都填平机械臂能稳定运行了下一步就是提升响应速度和轨迹精度。我常用的两个技巧是定时器 DMA 更新 CCR 和逆解算查表法。定时器 DMA 更新 CCR 的思路是把六个通道的 CCR 值放在一个数组里配置 DMA 在定时器更新事件时自动把数组搬运到 CCR 寄存器。这样 CPU 完全不用管 PWM 更新中断里只做角度插值。STM32 的 TIM2、TIM3、TIM4 都支持 DMA 请求具体是哪个 DMA 通道查参考手册。配置时注意 DMA 模式用循环模式外设地址是TIM3-CCR1内存地址是servo_target数组数据宽度 16 位每次传输 4 个数据TIM3 四通道。这样每个更新事件DMA 自动把四个 CCR 写一遍CPU 零开销。// TIM3 DMA 配置自动更新四路 CCR void Servo_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel2); // TIM3_CH1 对应 DMA1_Channel2查手册确认 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)TIM3-CCR1; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)servo_target; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 4; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel2, DMA_InitStructure); TIM_DMACmd(TIM3, TIM_DMA_Update, ENABLE); DMA_Cmd(DMA1_Channel2, ENABLE); }注意 DMA 传输的源地址是servo_target数组但 DMA 要求外设地址固定内存地址递增。这里外设是 CCR1固定不变内存是数组递增。但 CCR1 到 CCR4 在寄存器映射上是连续的所以 DMA 从 CCR1 开始写连续写四个半字正好覆盖 CCR1 到 CCR4。这个技巧在 F1 和 F4 上都适用但 DMA 通道号不同务必查手册。逆解算查表法是针对计算资源紧张的场景。如果你的机械臂工作空间不大可以把工作空间离散成网格比如 1mm 一格预先在 PC 上算好每个网格点对应的六个舵机角度存成二维数组烧到 STM32 的 Flash 里。运行时根据目标坐标查表双线性插值计算量从三角函数变成几次乘加速度快一个数量级。代价是 Flash 占用假设工作空间 200mm x 200mm1mm 一格就是 40000 个点每个点 6 个角度每个角度 2 字节总共 480KBF103 的 Flash 不够F4 可以。如果精度要求不高2mm 一格Flash 占用降到 120KBF103 也能放下。我自己的习惯是先用逆解算跑通确认机械结构和标定都正确再根据实际响应速度决定要不要上查表。很多时候STM32F4 跑浮点逆解算六轴联动周期 20msCPU 占用不到 30%根本不需要查表。只有用 F0 或者 F1 且主频低于 48MHz 时才考虑查表法。最后说一个我踩过的坑DMA 更新 CCR 时如果servo_target数组在 DMA 传输过程中被修改可能出现某一路写入一半旧值一半新值。解决方法是双缓冲准备两个数组DMA 传输 A 数组时CPU 更新 B 数组传输完成后切换。STM32 的 DMA 支持双缓冲模式配置稍复杂但能彻底避免撕裂。如果不想用双缓冲至少保证在更新事件中断里修改数组因为 DMA 传输是在更新事件后立即触发中断里修改数组时 DMA 已经传输完毕不会冲突。希望帮到你。本文还有配套的精品资源点击获取