1. 项目整体设计与思路拆解
1.1 为什么用 STM32 来做机械臂主控
先说结论:STM32 是在“性能够用、外设丰富、资料成熟、成本可控”这四个维度上,最适合中小型工业机械臂项目的一颗芯片,没有之一。
很多人一开始会想,机械臂这种看起来“高大上”的设备,是不是得上 PID 运动控制卡、PLC、或者什么专用 DSP?实际做下来你会发现,对于 3~6 自由度的教学型、桌面级、或轻载工业机械臂,一颗 STM32F103 或者 F407 就能把关节控制、运动学解算、通信管理全部跑起来。为什么不直接上 PLC?因为 PLC 在运动学算法、轨迹插补、自定义协议这些环节里开发效率实在太低,而且对于关节细粒度速度规划来说,PLC 的指令周期和开放性都不如直接用 MCU 来得顺手。
STM32 的优势在哪?首先是性价比,几十块钱一颗芯片,整机 BOM 成本可以压得很低;其次是库函数和参考设计极其丰富,HAL 库加 CubeMX 基本一周内就能把外设初始化全部搞定;再就是生态,伺服驱动器、编码器、通信模块、触摸屏都有大量现成的对接方案。工业机械臂的核心需求其实就是“实时性”和“确定性”,STM32 的硬件定时器、高级定时器的互补 PWM 输出、正交编码器接口、CAN 外设,正好把这些需求全覆盖了。对比过 Raspberry Pi 接扩展板方案的人应该都懂,Linux 下的实时性和中断响应,和裸机或 RTOS 下的 STM32 相比,差别是本质性的。
1.2 整机架构:闭环控制的核心链路
把机械臂系统拆开看,本质上是一条“指令输入 → 运动学解算 → 关节规划 → 电机驱动 → 位置反馈 → 闭环修正”的循环链路。STM32 在这条链路里扮演的不仅仅是“大脑”,它同时还是“小脑”。什么意思?上位机或者示教器负责“决定要去哪”,而 STM32 负责“怎么去”和“稳定到达”。每一毫秒或者五毫秒,控制器就要读一次所有关节的编码器位置,计算当前偏差,输出 PWM 给驱动器,再去规划下一小段轨迹。
这套系统的合理架构是这样的:
- 上层:上位机或示教器,负责轨迹输入、状态显示,通过串口、CAN 或以太网和主控通信;
- 中层:STM32 主控板,负责逆运动学解算、关节角度规划、PID 闭环、I/O 管理、故障保护;
- 底层:关节执行机构,包括直流伺服电机、编码器、驱动器,以及末端夹爪/工具;
- 附加层:限位开关、急停按钮、LED/蜂鸣器报警、电源监控。
我特别想强调一点:机械臂项目的复杂度不在电路板本身,而是“每一个关节都是独立的小闭环系统,而所有关节又要协同工作”。整条链路上,STM32 的定时器中断是节拍器,控制周期一到,所有关节必须同时采样、同时计算、同时更新 PWM。这就是为什么要用硬件定时器触发 ADC 采样、为什么要用 DMA 来搬运数据,否则中断一乱,某个关节稍有延迟,整个手臂的动作就会抖。
1.3 需求分解:从自由度到控制周期
做机械臂硬件设计,最容易犯的错就是一上来就想做六轴,结果五个电机、三块驱动板、传感器没接完就把自己绕晕了。我的建议是从需求出发倒推。如果你是做教学演示或桌面级轻载应用,3 自由度就够用了;如果是模拟工业场景,4~5 轴比较合适;真正要上产线的重型六轴,就不建议用 STM32 直接驱动大功率伺服了,那属于 PLC + 高速总线 + 专用伺服控制的领域。
我们以一套常见的 4 轴轻载机械臂来拆解:
| 关节 | 执行方式 | 反馈方式 | 负载场景 |
|---|---|---|---|
| 腰关节 J1 | 直流减速电机 + 蜗轮蜗杆 | 增量式编码器 | 整臂旋转,惯量大 |
| 肩关节 J2 | 直流减速电机 + 谐波/行星减速 | 增量式编码器 | 大臂摆动,重力负载 |
| 肘关节 J3 | 直流减速电机 + 行星减速 | 增量式编码器 | 小臂摆动,中等负载 |
| 腕关节 J4 | 舵机/小型直流电机 | 电位器/编码器 | 末端旋转/夹爪 |
确定好关节数量后,核心控制参数就是控制周期和通信周期。控制周期取决于机械系统的响应速度。轻载桌面级机械臂,位置环控制在 10ms 就能工作,速度环最好做到 5ms 以内,电流环那是驱动器的事,不在 STM32 里做。为什么不能把控制周期做到 1ms?不是 STM32 跑不动,而是机械结构本身的固有频率和减速比决定了,太快的控制周期反而会引入高频抖动和噪声放大。我的经验是:位置环 10ms、速度规划 10ms、通信刷新 20~50ms,这套参数在 F103 上稳跑,完全不需要上 F407。
2. 核心硬件选型与关键接口设计
2.1 主控选型:F103、F407、H750 怎么选
STM32 家族很庞大,但做机械臂项目基本就三个选择:STM32F103C8T6、STM32F407VET6、STM32H750VBT6。
F103C8T6 是入门性价比之王,72MHz 主频,64KB RAM 的型号跑轻量算法完全够用。做 3~4 关节机械臂,逆运动学用浮点算的话比较吃力,毕竟 F103 没有 FPU,浮点全靠软件模拟,矩阵运算一多,CPU 就吃力。所以如果主要做直线轨迹插补和关节空间点到点运动,F103 完全够;但如果要做笛卡尔空间连续轨迹,建议直接上 F407。
F407 主频 168MHz,带硬件 FPU 和 DSP 指令,运行逆运动学矩阵运算比 F103 快了一个数量级。更重要的是,F407 有 2 个 CAN 接口、多个高级定时器、DCMI 摄像头接口(想加视觉识别也留了路)。如果你预算不紧张,F407 是当前最稳的选择。
H750 主频 400MHz+,带双精度 FPU 和硬件加密,性能强很多,但功耗和 Layout 复杂度也上来了,一般只有做高精度高速机械臂才用。做毕业设计或一般项目,F407 完全够用,没必要为了“性能”去折腾 H750 的复杂启动配置。
2.2 电机驱动与反馈检测的实战选择
机械臂关节的驱动方案,我梳理了一下有四种常见搭配:
- 舵机方案:直接买大扭力串行舵机,用 PWM 控制角度。优点是简单、便宜;缺点是精度和重复定位能力差,负载一大就抖动,无法获取真实位置反馈(部分总线舵机除外),只适合玩具级或教学演示。
- 步进电机方案:开环控制,成本可控,力矩足。问题是丢步后系统不自知,没有反馈闭环,做轻载实验可以,工业场景不安全。
- 直流减速电机 + 增量编码器方案:这是目前桌面级轻载机械臂最主流的方案。用 STM32 的高级定时器输出 PWM,配上 TB6612、DRV8874 或者更专业的伺服驱动板,再通过编码器接口读反馈,构成“几乎工业级”的闭环。
- 交流伺服/空心杯伺服方案:这就是工业标准方案了,驱动器通过脉冲/方向或者 CAN 总线接受 STM32 的速度或位置指令。这个方案最稳,但成本也最高,一般用于集成度更高的样机。
编码器的选择上,增量式编码器(如 500 线的光电编码器)配合 STM32 的定时器编码器模式,直接接两路正交信号就能读出位置和方向。需要注意,STM32 的定时器编码器模式只支持每个定时器一个编码器,并且 A、B 相要接在同一个定时器的两个通道上。而绝对式编码器(如 SSI 接口的 17 位/23 位编码器)需要用 SPI 或软件模拟时序来读取,好处是断电不丢位置,上电不用回零,工业上更推荐,但 STM32 直接支持的少,一般配驱动器用。
2.3 电源系统与通信接口的几个大坑
电源是机械臂项目里最容易被忽视、又最容易出问题的地方。机械臂里有电机、有控制板、有编码器、有通信模块,它们的电源需求完全不同。电机会在启停、加减速瞬间拉低电压,如果控制板和电机共用一路电源,单片机就很容易复位。我的做法是:24V 或者 12V 电机电源单独一路,DC-DC 降压到 5V 给逻辑电路,再通过 LDO 降到 3.3V 给 MCU,电源地单点连接,电机驱动部分的地和控制部分的地用 0 欧电阻或磁珠做隔离。
通信接口方面,STMs32 的串口是 TTLL 电平,跟 RS232、RS485、CAN 都不直接兼容。工业机械臂至少需要预留这几个口:
- 调试串口:USART1,接 USB 转串口工具,输出调试日志;
- 上位机通信:USART2 或 CAN,工业现场首选 CAN,抗干扰强,距离远;
- 触摸屏接口:如果配了 HMI,可以用 RS485 + Modbus 协议;
- 扩展接口:预留一些空闲 GPIO、SPI、I2C。
这里重点说一下 CAN 和 RS485 的区别。CAN 是差分信号,自带仲裁和错误检测机制,适合多主多从、实时性要求高的场景。RS485 是差分串口,适合一主多从的轮询方式,工业触摸屏常用的就是这种。STM32F103 没有硬件 CAN 需要外接 MCP2515 之类的控制器,F407 自带两个 CAN 外设,用起来就方便很多。
3. 软件架构与核心算法实现细节
3.1 状态机与任务调度:MCU 的“项目管理”
机械臂软件最忌讳的就是把所有的功能堆在一个 while(1) 循环里。你想想,如果机械臂正在高速移动,突然上位机发来一个查询指令,你临时处理通信,PWM 更新就延迟了,关节位置就会抖动。所以从一开始就要建立清晰的软件架构。
我推荐两种方式:裸机定时器调度,或者 FreeRTOS。如果是入门项目,建议裸机加定时器中断,结构更直观;如果关节多、任务多,直接用 FreeRTOS 管理任务。
先给出一套基础的“系统状态机”设计:
| 状态 | 含义 | 进入条件 | 动作 |
|---|---|---|---|
| INIT | 初始化 | 上电 | 外设初始化、读取参数、回零检查 |
| IDLE | 空闲 | 初始化完成/暂停 | 等待指令、响应查询、保持当前位置 |
| HOME | 回零 | 收到回零指令 | 低速寻找限位开关,记录机械原点 |
| TELEOP | 手动示教 | 收到手动模式 | 按关节运动,常用于示教 |
| AUTO | 自动运行 | 收到运行指令 | 执行预编轨迹或上位机实时轨迹 |
| FAULT | 故障保护 | 触发急停/超限/通信超时 | 停止所有电机,报告故障码 |
状态机的好处是,不管上位机发什么指令,主控永远按照当前状态去判断“能不能执行这个动作”,比如正在 AUTO 状态下,除了暂停和急停,其他指令都只缓存不执行,这就避免了误操作导致的机械碰撞。
3.2 正逆运动学:让机械臂“知道”自己的姿态
机械臂控制的第一个核心算法就是运动学解算。正运动学是“知道各个关节角度,求末端位置和姿态”;逆运动学正好相反,“知道末端目标位置和姿态,求各个关节应该转到多少度”。
以 3R 平面机械臂为例,最简单的正运动学模型可以用几何法推导。假设三个关节角度分别是 θ1、θ2、θ3,臂长分别是 L1、L2、L3,那么末端位置就能通过每段关节的余弦、正弦叠加计算出来。
逆运动学求解则要麻烦得多。解析法需要对几何关系进行消元推导,适合结构简单、轴数少的机械臂;数值迭代法(如雅可比迭代、阻尼最小二乘法)适合各种复杂结构,但计算量大。这里给出一个 2R 平面机械臂逆运动学解析思路,大家感受一下:
假设末端目标位置是 (x, y):
- 先求末端到肩关节的距离 d = sqrt(x² + y²);
- 用余弦定理求肘关节角 θ2 的两组解(同一位置下弯肘和伸肘): cos(θ2) = (x² + y² - L1² - L2²) / (2 * L1 * L2);
- 由 θ2 和几何关系求出肩关节角 θ1;
- 正负号的处理决定了机械臂取“右侧构型”还是“左侧构型”。
这部分代码写起来不复杂,但需要特别注意三角函数的象限判断,否则机械臂会走“绕远路”或者出现奇异点抖动。实际项目中我开始用 double 类型存储角度,但 F103 的软浮点计算太慢,后来换成了 float,精度差一点但对控制影响不大,速度提升非常明显。
3.3 轨迹规划与梯形加减速:防止机械臂“甩尾”
很多刚做机械臂的人会忽略轨迹规划这一步,直接让每个关节从当前角度一口气跑到目标角度。这样做的问题很严重:机械臂启动瞬间加速度无穷大,电机会因为瞬间电流过大而堵转,机械结构会因为惯性冲击而抖动甚至损坏。
正确的做法是给每个动作做“速度规划”。最简单实用的是梯形加减速:加速段、匀速段、减速段三段式。规划的目标是在指定时间内让末端或关节平滑地从当前状态过渡到目标状态。
举个例子,某个关节要从 0°运动到 90°,规划总时长 2 秒,最大速度 Vmax,加速度 A。那么:
- 第一阶段:从 0 加速到 Vmax,耗时 Tacc = Vmax / A,走过的角度是 0.5 * A * Tacc²;
- 第二阶段:以 Vmax 匀速运动,走完剩余角度;
- 第三阶段:对称减速到 0。
如果计算出的匀速段时长为负数,说明给的加速度和最大速度无法在目标时间内完成运动,就要自动降速或延长运动时间。这就是轨迹规划代码里的核心判断逻辑,写起来不复杂,但一定要处理好边界条件。
梯形加减速虽然好用,但如果对机械臂轨迹平滑度要求更高,可以用 S 形曲线(加加速度 S 形限制),让速度和加速度都连续变化,机械臂的振动和噪音都会小很多。代价是计算量大了不少,F103 会有点吃力,F407 的话没问题。
3.4 PID 闭环控制代码与参数整定
闭环控制是机械臂稳定性的核心。每个关节都需要一个位置 PID(或 PID 加前馈),STM32 的定时器中断每隔固定周期(比如 10ms)执行一次:
// 关节位置PID控制(增量式实现) typedef struct { float target; // 目标位置 float current; // 当前位置 float last_error; // 上一次偏差 float integral; // 误差积分(带限幅) float kp, ki, kd; // PID参数 float out_limit; // 输出限幅 } JointPID; float JointPID_Update(JointPID *pid, float target, float current, float dt) { float error = target - current; float output; // 位置式PID,带积分限幅 pid->integral += error * dt; if (pid->integral > pid->out_limit) pid->integral = pid->out_limit; if (pid->integral < -pid->out_limit) pid->integral = -pid->out_limit; float derivative = (error - pid->last_error) / dt; output = pid->kp * error + pid->ki * pid->integral + pid->kd * derivative; // 输出限幅 if (output > pid->out_limit) output = pid->out_limit; if (output < -pid->out_limit) output = -pid->out_limit; pid->last_error = error; return output; }这段代码是纯位置式 PID,需要特别注意的就是两点:一个是积分限幅,不限制的话积分饱和会让机械臂到达目标位置后还继续“冲”;另一个是微分项对噪声非常敏感,编码器反馈有毛刺时,微分项会剧烈波动,通常 PD 输出前先做一阶低通滤波。
参数整定一般先用临界比例度法。先把 Ki 和 Kd 设为 0,只调 Kp,让机械臂某个关节处于“微振但不发散”的临界状态,记下这个 Kp 值 Ku;然后根据经验公式计算 P、I、D 参数,再微调。实际经验是,位置环 Kp 从 1.0 左右起步慢慢加,如果出现高频振荡就降回来一点。Ki 的作用是消除稳态误差,但过大的 Ki 会导致超调和振荡,Kd 能抑制超调,但对噪声敏感,要配合滤波。
3.5 通信协议设计:上位机和主控的“对话语言”
机械臂的控制系统至少有两个通信方向:向下是给关节驱动器发指令,向上是接收上位机指令和状态上报。通信协议的设计决定了系统扩展性和调试效率。
我个人常用的协议格式是帧头+命令字+数据长度+数据体+校验码。STM32 串口接收用中断/DMA,将一帧数据完整接收后解析,再根据命令字分发到对应处理函数。
这里给一个简洁的帧格式定义:
#define FRAME_HEADER_1 0xAA #define FRAME_HEADER_2 0x55 #pragma pack(1) typedef struct { uint8_t header[2]; uint8_t cmd; // 命令字 uint8_t len; // 数据长度 uint8_t data[32]; // 数据区 uint8_t checksum; // 校验和 } FramePacket; #pragma pack()校验和就是对除了帧头之外的字节求和取低八位。在实际项目中,我还遇到过一个比较隐蔽的问题:上位机发送频率高时,串口中断和主循环解析不同步会导致半包或粘包。解决方法是串口中断只做“存数据”,w解析放在主循环或 RTOS 任务里做,保证一次只解析完整的一帧。
4. 实操过程、调试记录与问题排查实录
4.1 从零搭建第一版控制系统的实际过程记录
我当时做第一版系统大概花了三天时间把硬件搭起来:一块 STM32F103C8T6 核心板、三个 TB6612 驱动模块、三个带霍尔编码器的直流减速电机、一个 12V 电源模块、若干限位开关。CubeMX 初始化完时钟和 GPIO 后,最先调试的就是 PWM 输出。
用 CubeMX 把定时器 TIM1 的 CH1、CH2、CH3 配成 PWM 输出,频率设 20kHz。注意 TIM1 属于高级定时器,输出 PWM 之前默认要手动拉高 MOE(主输出使能),否则示波器上看不到波形。这个坑很多人查半天查不出来,实际上就是一行代码的事:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 普通定时器这样就行 __HAL_TIM_MOE_ENABLE(&htim1); // 高级定时器还要额外使能主输出然后调编码器读取。STM32 的编码器模式是把定时器配置成外部计数模式,CH1 和 CH2 分别接编码器 A、B 相,计数方向会自动跟随旋转方向变化。初始化代码里关键是设置编码器模式、极性和计数范围:
TIM_Encoder_InitTypeDef encoder_config = {0}; encoder_config.EncoderMode = TIM_ENCODERMODE_TI1; // 或者 TI12,双沿计数 encoder_config.IC1Polarity = TIM_ICPOLARITY_RISING; encoder_config.IC2Polarity = TIM_ICPOLARITY_RISING; encoder_config.IC1Selection = TIM_ICSELECTION_DIRECTTI; encoder_config.IC2Selection = TIM_ICSELECTION_DIRECTTI; encoder_config.IC1Prescaler = TIM_ICPSC_DIV1; encoder_config.IC2Prescaler = TIM_ICPSC_DIV1;电机转动方向可以通过比较 TIMx->CNT 寄存器在正转和反转时的变化方向来判断,一般不需要额外传感器。实际操作中,编码器信号线最好用双绞屏蔽线,并且屏蔽层在控制板端单点接地,不然电机一启动,编码器读数就会被噪声干扰。
4.2 上电调试中遇到的几个典型故障与排查方法
故障一:PWM 输出正常,但电机不转。
排查过程是先量 PWM 波形,波形正常的情况下再测驱动器输入端的使能引脚。TB6612 这类驱动模块通常需要把 STBY(待机)引脚拉高,如果这个引脚悬空,电机自然不会转。另外 AIN1/AIN2 的电平组合决定正反转,如果两者都是低电平,驱动器同样不输出。
故障二:编码器读数跳变、数值异常。
这个问题的最大嫌疑是电源共地不良,或者编码器供电电压不足(编码器需要 5V 时,用 3.3V 供必然工作不稳定)。还有一点,STM32 的编码器计数寄存器只有 16 位(部分定时器 32 位),溢出之后会“跳变”,需要在代码里做溢出检测,累加到全局变量中保证位置连续。
故障三:机械臂启动时抖动,PID 参数无论怎么调都振。
这个问题通常是控制周期和速度规划不匹配导致的。如果位置环每 10ms 收一次“瞬间大步长目标”,PID 的输出就会因为误差太大而饱和,电动机猛烈加速,一旦到位又猛减速,从而产生“锤击效应”。解决办法是在控制周期内做多段小步长插补,也就是上一节说的轨迹规划,把每一步目标位置细分,让 PID 追寻的不是跳跃目标而是一条平滑曲线。
故障四:串口数据丢包或 CRC 错误。
最常见的诱因有几种:一是波特率太高(机械臂这种环境 115200 就上限了);二是主循环中用了 HAL_Delay(),导致长时间不处理串口缓存;三是地线干扰。注意 STM32 的串口 RX 要加上拉电阻,提高抗干扰能力;485 通信的话,在总线的 A、B 两端各加 120 欧终端电阻匹配。
4.3 让机械臂“稳”下来的三个工程细节
这部分算是实战经验中比较有含金量的部分。机械臂跑起来不难,但让它“稳如磐石”就需要在工程细节上下功夫。
第一是控制周期的确定性。不要在定时器中断中调用任何可能阻塞的函数,比如 printf、HAL_Delay、浮点除法最好用查表或定点代替。中断服务函数只做三件事:读编码器、算 PID、更新 PWM。把日志输出、UI 刷新这些活全部挪到主循环或低优先级任务里。
第二是软件限位和硬件限位双重保护。每个关节除了机械限位外,都要在软件里设定软件限位,一旦编码器反馈的位置超出设定范围,立即停车并进入 FAULT 状态。硬件限位开关则直接接入 STM32 的外部中断引脚,优先级设为最高,即使跑飞了也能触发急停。这两个保护措施一个都不能省,否则一次误操作就可能让机械臂撞坏夹具或者扫到操作人员。
第三是电机 PWM 的“死去区”处理。直流减速电机由于减速箱摩擦和死区电压的存在,PID 输出很小时电机根本不动,误差会一直存在,PID 积分会一直积累,最后输出大了又过冲。解决方法是设置一个 PWM 死区阈值,当输出小于该阈值时直接不给 PWM,同时冻结积分,在代码里这叫 Bang-Bang 带死区。这个细节对定位精度影响极大,指数级提升手感,加了它之后机械臂才真正“定得住”。
4.4 PID 参数整定经验汇总
| 现象 | 原因 | 调整方向 |
|---|---|---|
| 到位后持续低频振荡 | Kp 偏大,惯量大 | 降低 Kp,适当增加 Kd |
| 静态误差大,回不到目标点 | 积分不足或摩擦太大 | 增加 Ki,或加死区补偿前馈 |
| 高频“振铃”声 | Kd 过大或反馈噪声 | 降低 Kd,对微分加低通滤波 |
| 启动瞬间猛冲 | 积分饱和 | 限制积分上限,做积分清零 |
| 不同负载下性能变化大 | 固定 PID 无法适应 | 做模糊 PID,或按负载区间切换参数 |
4.5 升级方向与扩展思路
把一个基础版本的 STM32 机械臂做稳定后,接下来有很多升级方向。最容易见效的是加一个无线遥控手柄或手机 APP 控制,通过蓝牙模块或 Wi-Fi 模块接 STM32 串口,把控制指令变成“手柄摇杆值 → 关节速度值”的映射。再往上走,就是加视觉定位。F407 带 DCMI 接口可以直接接 OV2640 摄像头,配合简单的 OPENMV 或 K210 做图像坐标识别,把识别结果通过串口发给 STM32 去抓取物体。这个方向也是目前毕业设计比较热门的方向。
我自己在实际操作中的体会是,机械臂系统真正的难点不是某个单独模块,而是把所有模块串在一条实时控制链路上,任何一个环节的延迟或抖动都会在末端放大器一样放大。STM32 的每一个定时器、每一个中断优先级、每一行影响中断退出时间的代码,都会最终反映在机械臂的运动质量上。所以初学的时候没必要急着追求复杂的算法,先把“定时器中断里跑 PID、主循环里跑通信”这套简单骨架跑通,观察运动曲线,就会发现大量值得优化的细节。