简介:《工业机器人机械臂控制系统设计》是一份聚焦机器人控制领域的技术文献,面向自动化、机电一体化专业学生及从事机械臂控制的工程人员,帮助读者理解低成本机械臂控制方案的完整设计思路与实现路径。内容以STM32微控制器为核心搭建机械臂控制体系,借助PID控制理论优化运行路径、提升鲁棒性与稳定性,并通过力矩传感与电流检测模块判断力矩变化、补偿自重影响;硬件层面讨论直流有刷电机选型、基于增量型编码器的位置方案与H桥驱动电路设计,软件层面梳理主程序与中断程序结构及PID算法实现,最终以仿真曲线验证方案是否科学有效。压缩包为单个PDF文件,约1.15MB,阅读与检索都比较方便,适合打印存档或作为参考文献随时查阅。目前已有306人学习下载,可结合文中知识点用于课程设计、毕业设计选题以及控制算法入门参考。
1. STM32 机械臂控制系统的成本与实时性权衡
一台 5 自由度机械臂的关节控制板,如果选用 DSP 方案,光是主控加外围就要吃掉大半预算;换成 STM32F103 或 F407 级别的芯片,省下的钱可以买到分辨率更高的增量式编码器和更低温漂的电流采样电阻。这个选题来自工业机器人机械臂控制系统设计里的一个判断:在关节转角有限、负载中等的场景下,控制器算力过剩并不一定带来更好的轨迹跟踪,反倒是采样周期、编码器分辨率和电流环响应决定最终手感。很多人第一次做机械臂,电机能转就以为成了,真正上电才发现低速爬行、停不准、一碰就抖。适合已经能跑通单关节转动、但卡在“一动就抖、一停就过冲”的开发者。关键词里的工业机器人、机械臂、控制系统、STM32、PID、编码器,接下来都会落到具体寄存器和参数上。
2. 硬件驱动链路拆解:有刷电机、H 桥与增量式编码器
2.1 有刷直流电机在关节驱动里的选型边界
机械臂关节驱动常见两条路:有刷直流电机和无刷直流电机。无刷电机寿命长、效率高,但驱动要三相逆变加换相逻辑,低速大扭矩时还得做电流环,成本和调试量都上一个台阶。有刷电机在启动和调速方面灵敏度更高,控制方便,成本投入更低,配合 H 桥就能做到正反转和 PWM 调速。机械臂关节转角通常不超过 ±180°,频繁启停、负载中等,有刷电机的碳刷磨损在这个工况下并不致命。
| 对比项 | 有刷直流电机 | 无刷直流电机 |
|---|---|---|
| 驱动电路成本 | 低,单 H 桥即可 | 高,三相逆变加换相 |
| 启动转矩 | 大,低速线性好 | 需霍尔或无感算法 |
| 调速线性度 | 电压-转速近似线性 | 依赖换相与电流环 |
| 维护 | 碳刷定期更换 | 寿命长 |
| 适用场景 | 小负载、频繁启停 | 高速、长时连续运转 |
选型时还要看额定电压和堵转电流。24V 有刷电机配 12 位编码器,在 5 自由度桌面机械臂上已经能跑到 0.1° 左右的定位分辨率。如果追求具身智能机械臂那种柔顺控制,有刷电机加力矩传感反而比无刷方案更容易做重力补偿算法,因为电流和力矩的线性关系更直接。
2.2 H 桥、IR2106S 与 74ACT244 的驱动链路
电机驱动用 4 个相同功率的 MOSFET 搭 H 桥,电机接在桥臂中间。IR2106S 是半桥栅极驱动芯片,负责把逻辑电平抬到 MOSFET 栅极需要的电压。问题在于 STM32 的 PWM 引脚输出电流只有几毫安,直接推 IR2106S 的输入端会边沿变缓、发热增加,所以中间加一级 74ACT244 做功率增强。常见做法是:TIM1 输出两路互补 PWM,经 74ACT244 缓冲后送入 IR2106S 的 HIN 和 LIN,IR2106S 再驱动上下桥 MOSFET。
// TIM1 互补 PWM 初始化:20kHz 载波,驱动 H 桥左/右半桥 // 系统时钟 72MHz,预分频 0,自动重装载 3599 static void Motor_PWM_Init(void) { GPIO_InitTypeDef gpio = {0}; TIM_TimeBaseInitTypeDef tb = {0}; TIM_OCInitTypeDef oc = {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); tb.TIM_Prescaler = 0; tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_Period = 3599; // 72MHz / 3600 = 20kHz tb.TIM_ClockDivision = TIM_CKD_DIV1; tb.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM1, &tb); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_OutputNState = TIM_OutputNState_Enable; // 互补输出给 IR2106S oc.TIM_Pulse = 0; oc.TIM_OCPolarity = TIM_OCPolarity_High; oc.TIM_OCNPolarity = TIM_OCNPolarity_High; oc.TIM_OCIdleState = TIM_OCIdleState_Reset; TIM_OC1Init(TIM1, &oc); // CH1 左半桥 TIM_OC2Init(TIM1, &oc); // CH2 右半桥 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }20kHz 载波避开人耳可听范围,减少电机啸叫。死区时间要在 TIM1 的 BDTR 寄存器里设,典型 500ns 到 1μs,防止上下桥直通。TIM_Pulse 对应 CCR1/CCR2,改这两个值就能改占空比。74ACT244 的使能脚记得拉低有效,别悬空。
2.3 增量式编码器的 TIM3 编码器模式配置
编码器分增量型和绝对型。增量型以前一时刻为基准测角度改变量,小角度精度高,角度增大后误差会累积;绝对型以运行后时刻为基准,大角度不累积误差,但小角度分辨率差。机械臂转角范围有限,上电回零后不会累积大误差,所以选增量型更划算。
// TIM3 编码器接口模式:PA6/PA7 接 A/B 相,四倍频计数 void Encoder_Init(void) { GPIO_InitTypeDef gpio = {0}; TIM_TimeBaseInitTypeDef tb = {0}; TIM_ICInitTypeDef ic = {0}; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 编码器开漏输出时需外接上拉 GPIO_Init(GPIOA, &gpio); tb.TIM_Prescaler = 0; tb.TIM_Period = 65535; // 16 位满量程,靠软件扩展高位 tb.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &tb); ic.TIM_Channel = TIM_Channel_1; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; ic.TIM_ICSelection = TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0x0F; // 最大滤波,抑制电机换向毛刺 TIM_ICInit(TIM3, &ic); ic.TIM_Channel = TIM_Channel_2; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInit(TIM3, &ic); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM3, 0); TIM_Cmd(TIM3, ENABLE); }TIM_EncoderMode_TI12 表示 A/B 相都参与计数,实现四倍频。CNT 是 16 位,溢出后在 TIM3 中断里累加高 16 位,得到 32 位绝对位置。ICFilter 设 0x0F 是最大滤波,适合长线、电机干扰大的场合;信号干净可以降到 0x03,否则会吃掉高速段的脉冲。
3. PID 闭环与 TIM2 10ms 位置环调度
3.1 位置式与增量式 PID 在关节控制里的取舍
位置式 PID 输出的是绝对控制量,直接映射成 PWM 比较值;增量式 PID 输出的是控制量增量,适合带积分分离的速度环。机械臂关节要的是“转到某个编码器计数”,位置式更直观,也更容易做输出限幅。但位置式有一个坑:积分项会一直累加,电机堵转时输出饱和,松开后要很久才退饱和,表现就是过冲后回不来。所以积分限幅和 PWM 限幅必须同时做。
3.2 带积分限幅和 PWM 限幅的 PID 实现
typedef struct { float Kp, Ki, Kd; float target; // 目标编码器计数 float integral; // 积分累加 float last_err; float out_min, out_max; float integral_max; } PID_t; // TIM2 中断周期 10ms 调用 float PID_Compute(PID_t *pid, float feedback) { float err = pid->target - feedback; float out; pid->integral += err; // 积分限幅,防止长时间偏差导致积分饱和 if (pid->integral > pid->integral_max) pid->integral = pid->integral_max; if (pid->integral < -pid->integral_max) pid->integral = -pid->integral_max; out = pid->Kp * err + pid->Ki * pid->integral + pid->Kd * (err - pid->last_err); pid->last_err = err; // 输出限幅,映射到 PWM 比较值 if (out > pid->out_max) out = pid->out_max; if (out < pid->out_min) out = pid->out_min; return out; }Kp 决定响应速度,Ki 消除稳态误差,Kd 抑制超调。integral_max 一般取 out_max / Ki 的 0.6 到 0.8,太小会导致稳态误差消不掉,太大等于没限。10ms 周期下 Kd 对编码器噪声很敏感,差分速度建议做一阶低通,截止频率取 50Hz 左右。输出 out 再线性映射到 CCR 值,注意死区和方向。
3.3 中断优先级与调度顺序
| 中断源 | 优先级 | 周期/触发 | 职责 |
|---|---|---|---|
| TIM2 | 1 | 10ms | PID 计算、PWM 更新 |
| CAN RX | 2 | 事件 | 接收目标位置与使能指令 |
| TIM3 | 3 | 溢出 | 编码器高 16 位扩展 |
| USART1 | 4 | 事件 | 上报位置、调试数据 |
PID 中断里不要放浮点打印,串口上报放到主循环,否则 10ms 节拍会被打断。CAN 接收中断只做解析和置标志,目标位置更新放在 PID 里统一生效。如果做机械臂轨迹规划,目标位置由上位机按插补周期下发,PID 只负责跟踪,不要把插补算法塞进 10ms 中断。
4. 串口整定与仿真曲线拟合:从阶跃响应到 PID 参数
4.1 逐参数整定流程
整定顺序是先比例、再积分、最后微分。每次只改一个参数,用串口发阶跃指令,记录编码器位置序列。常见做法是用上位机画曲线,肉眼判断超调量和调节时间,比盯着数字快得多。
| 步骤 | 操作 | 观察现象 | 调整方向 |
|---|---|---|---|
| 1 | Kp 从 0 逐步增大,Ki=Kd=0 | 响应变快,出现等幅振荡 | 取振荡前 60% 的 Kp |
| 2 | 加 Ki,从小往大 | 稳态误差下降,出现过冲 | 减小 Ki 或加积分限幅 |
| 3 | 加 Kd | 超调减小,噪声放大 | 配合编码器滤波 |
| 4 | 缩采样周期 | 响应更快,抖动增加 | 10ms 到 5ms 逐步试 |
一旦曲线超调量超过 10%,振荡会明显加剧。这时候先降 Kp,再补一点 Kd。如果 PI 控制后稳态误差还在,不要一味加 Ki,先看编码器分辨率是不是不够,或者电机死区有没有补偿。
4.2 上位机拟合脚本
import numpy as np import serial import matplotlib.pyplot as plt from scipy.optimize import curve_fit # 二阶欠阻尼阶跃响应模型 def step_model(t, K, wn, zeta): wd = wn * np.sqrt(1 - zeta**2) return K * (1 - np.exp(-zeta * wn * t) * (np.cos(wd * t) + (zeta / np.sqrt(1 - zeta**2)) * np.sin(wd * t))) ser = serial.Serial('COM3', 115200, timeout=0.1) raw = [] while len(raw) < 300: line = ser.readline().decode().strip() if line: raw.append(float(line)) y = np.array(raw) t = np.arange(len(y)) * 0.01 # 10ms 采样周期 p0 = [y[-1], 10.0, 0.5] popt, _ = curve_fit(step_model, t, y, p0=p0, maxfev=10000) print('K=%.3f wn=%.3f zeta=%.3f' % tuple(popt)) plt.plot(t, y, label='measured') plt.plot(t, step_model(t, *popt), '--', label='fit') plt.legend(); plt.xlabel('time/s'); plt.ylabel('encoder count') plt.show()脚本从串口读 300 个 10ms 间隔的位置样本,用二阶欠阻尼模型拟合,得到自然频率 wn 和阻尼比 zeta。zeta 小于 0.6 说明超调偏大,加 Kd;wn 太小说明响应慢,加 Kp。K 是稳态增益,用来判断有没有静差。实际调试时把 measured 和 fit 画在同一张图,曲线贴合度比单个数字更有说服力。
4.3 判据与采样周期
| 指标 | 合格范围 | 不达标时先动谁 |
|---|---|---|
| 超调量 | < 10% | 加 Kd 或降 Kp |
| 调节时间 | < 0.3s | 加 Kp 或缩采样周期 |
| 稳态误差 | < 2 个编码器计数 | 加 Ki 或检查死区 |
| 振荡次数 | < 2 次 | 降 Kp,加微分滤波 |
采样周期从 10ms 缩到 5ms,实时性更好,但 PWM 噪声更容易进微分项。如果缩周期后抖动变大,先把 Kd 减半,再观察。机械臂重力补偿算法如果需要更快的力矩响应,可以把电流环单独放到 1ms 定时器里,位置环保持 10ms。
5. 力矩保护与 CAN 总线指令接入的工程细节
5.1 电流采样与力矩估算
有刷电机的输出力矩近似正比于电枢电流,用采样电阻加运放加 ADC 注入通道就能估力矩。常见做法是在 H 桥下桥臂串 5mΩ 采样电阻,运放增益 20 倍,STM32 的 ADC 注入通道在 PWM 周期中点采样,避开开关噪声。把电流采样放在 1ms 定时器里,独立于 10ms 位置环,急停路径更短。力矩超过额定值 1.5 倍时直接封 PWM,而不是等位置环下一拍再处理。这就是工业机器人里常说的力矩参数传感模块和电流参数检测模块的最小实现。
5.2 CAN 帧格式与超限停机
| CAN ID | 数据长度 | 字段 | 含义 |
|---|---|---|---|
| 0x120 | 4 | Data[0..3] | 目标编码器计数,小端 |
| 0x121 | 1 | bit0 使能,bit1 急停 | 关节使能与急停 |
| 0x122 | 2 | Data[0..1] | 电流上限,单位 10mA |
// CAN 接收回调:0x120 为目标位置,0x121 为使能/急停 void CAN_RxHandler(CanRxMsg *msg) { switch (msg->StdId) { case 0x120: // 4 字节小端目标编码器计数 g_target = (int32_t)(msg->Data[0] | (msg->Data[1] << 8) | (msg->Data[2] << 16) | (msg->Data[3] << 24)); break; case 0x121: g_enable = msg->Data[0] & 0x01; if (msg->Data[0] & 0x02) { // 急停位 Motor_PWM_Set(0, 0); g_fault = FAULT_ESTOP; } break; default: break; } }急停位用 CAN 帧的 bit1,而不是单独发一帧停止指令,这样从节点可以在同一个接收中断里立即封 PWM。接收中断里不要做浮点运算,只置标志和写寄存器。如果做多关节协同,主机按 10ms 广播一次目标位置,从节点在 TIM2 中断里统一更新,避免 CAN 到达时刻不同导致关节不同步。
本文还有配套的精品资源,点击获取