简介:这份课程设计报告面向自动化、机器人及嵌入式方向的学生与开发者,围绕基于STM32的两连杆机械臂运动控制展开,解决从运动学建模到控制系统落地的完整设计问题。报告以四自由度理论建立运动模型,借助齐次坐标与4×4齐次变换矩阵推导手爪位姿方程,并给出D-H参数表;控制系统部分对比单CPU集中控制、多CPU分布式控制与二级主从式控制三种方案,最终选用STM32作为下位机,通过PWM驱动舵机实现关节角度控制,同时涵盖机械结构刚度、导向性与偏重力矩等设计要点。资源包为1个docx文档,约735KB,内容含摘要、运动模型建立、控制系统总体方案设计及硬件电路选型等章节,结构完整、公式与图表齐全。目前已有152人学习,适合作为机器人测控技术大作业或嵌入式控制入门的参考范例,便于读者理解运动学建模思路与STM32控制系统的实现流程。
1. 从一份大作业文档说起:STM32 怎么把两连杆机械臂管起来
如果你手头正躺着一份《基于 STM32 的机械臂运动控制分析设计》的课程设计文档,翻了两页发现全是矩阵和公式,却不知道从哪下手把它变成能跑的代码,那这篇笔记就是写给你的。这份资源本质上是一套完整的机械臂运动控制设计报告,核心是用 STM32 微控制器驱动舵机,让一个两连杆(扩展到四自由度)机械臂按预定轨迹完成搬运任务。它解决的不是“从零造机器人”的问题,而是“给定机械结构和控制芯片,怎么把运动学模型、硬件选型、PWM 驱动和电源设计串成一条能落地的链路”。适合正在做机器人测控类课程设计的学生、需要快速搭建机械臂控制原型的嵌入式开发者,以及想搞明白 D-H 参数怎么变成实际舵机转角的人。我见过太多人卡在“公式推完了,板子不会动”这一步,这份文档的价值就在于它把从数学到脉冲的路径画了出来,虽然画得不算细,但骨架是完整的。
2. 运动学模型建立:D-H 参数怎么落到两连杆上
2.1 为什么先用齐次坐标把问题“抬高一维”
机械臂运动学最怕的就是坐标系绕来绕去绕晕。这份设计里用的是齐次坐标加 4×4 齐次变换矩阵,思路很直接:把三维空间里的旋转和平移统一塞进一个矩阵里,这样连杆之间的变换就能用连乘表示。对于两连杆机械臂,你只需要关心两个旋转关节的角度 θ1 和 θ2,以及两根杆的长度 L1 和 L2。D-H 参数表的作用就是把这四个量标准化成四个参数:连杆长度 a、连杆扭转角 α、关节距离 d、关节转角 θ。文档里给出的变换矩阵形式是经典的:
import numpy as np def dh_transform(a, alpha, d, theta): """ 标准 D-H 参数生成齐次变换矩阵 a: 连杆长度 alpha: 连杆扭转角(弧度) d: 关节距离 theta: 关节转角(弧度) """ T = np.array([ [np.cos(theta), -np.sin(theta)*np.cos(alpha), np.sin(theta)*np.sin(alpha), a*np.cos(theta)], [np.sin(theta), np.cos(theta)*np.cos(alpha), -np.cos(theta)*np.sin(alpha), a*np.sin(theta)], [0, np.sin(alpha), np.cos(alpha), d], [0, 0, 0, 1] ]) return T # 两连杆示例:L1=100mm, L2=80mm,关节转角待定 L1, L2 = 100, 80 theta1, theta2 = np.radians(30), np.radians(45) T1 = dh_transform(L1, 0, 0, theta1) T2 = dh_transform(L2, 0, 0, theta2) T_total = T1 @ T2 print("手爪位置:", T_total[:3, 3]) print("手爪姿态矩阵:\n", T_total[:3, :3])这段代码的逻辑是:每个连杆用一个 4×4 矩阵描述它相对于前一个坐标系的位置和姿态,连乘之后得到手爪相对于基座的总变换。参数说明里,a 和 d 决定平移,alpha 和 theta 决定旋转。对于平面两连杆,alpha 和 d 通常为 0,你只需要调 theta 和杆长。文档里提到的“手爪姿态方程”和“位置方程”其实就是从 T_total 里拆出来的:前三行第四列是位置,前三行前三列是姿态。常见做法是先用这个模型算出目标位置对应的 θ1 和 θ2,再把这个角度换算成舵机的 PWM 占空比。
2.2 逆运动学:从目标坐标反推关节角
正运动学是“给角度算位置”,逆运动学是“给位置算角度”,后者才是控制里真正要用的。两连杆平面臂的逆解有解析公式,不需要迭代:
def inverse_kinematics(x, y, L1, L2): """ 两连杆平面机械臂逆运动学解析解 x, y: 目标位置(相对于基座) L1, L2: 连杆长度 返回:theta1, theta2(弧度) """ # 先算第二个关节角的余弦值 cos_theta2 = (x**2 + y**2 - L1**2 - L2**2) / (2 * L1 * L2) # 检查是否超出工作空间 if abs(cos_theta2) > 1: raise ValueError("目标点超出机械臂可达范围") # 两个解:肘部向上或向下 sin_theta2 = np.sqrt(1 - cos_theta2**2) theta2 = np.arctan2(sin_theta2, cos_theta2) # 肘部向上 # 再算第一个关节角 k1 = L1 + L2 * cos_theta2 k2 = L2 * sin_theta2 theta1 = np.arctan2(y, x) - np.arctan2(k2, k1) return theta1, theta2 # 测试:目标点 (120, 60) th1, th2 = inverse_kinematics(120, 60, L1, L2) print(f"关节角1: {np.degrees(th1):.2f}°, 关节角2: {np.degrees(th2):.2f}°")这里的关键参数是 cos_theta2 的检查,它决定了目标点是否在机械臂的工作空间内。如果绝对值大于 1,说明够不着,这时候要么移动基座,要么报错。theta2 取正还是取负对应“肘部向上”和“肘部向下”两种构型,实际控制里要根据机械结构避免干涉来选。文档里没有展开逆解,但这是让机械臂真正动起来必须补上的一环。我一般会先把逆解算出来的角度转成舵机脉冲,再在代码里加一个限幅,防止舵机撞到机械限位。
2.3 从关节角到 PWM:占空比换算的细节
舵机的控制信号是周期 20ms 的 PWM,脉宽 0.5ms 到 2.5ms 对应 0 到 180 度。STM32 的定时器输出 PWM 时,你需要根据系统时钟算出 ARR 和 CCR 的值。假设定时器时钟 72MHz,预分频 72-1,则计数频率 1MHz,周期 20ms 对应 ARR=20000-1。脉宽 0.5ms 对应 CCR=500,2.5ms 对应 CCR=2500。换算公式是:
// STM32 HAL 库示例:设置舵机角度 void set_servo_angle(TIM_HandleTypeDef *htim, uint32_t channel, float angle) { // 限制角度范围 if (angle < 0) angle = 0; if (angle > 180) angle = 180; // 脉宽 = 0.5ms + (angle/180)*2ms // CCR = 脉宽(us) * 1 (因为1MHz计数) uint32_t ccr = 500 + (uint32_t)((angle / 180.0f) * 2000); __HAL_TIM_SET_COMPARE(htim, channel, ccr); }这段代码里,500 和 2500 是舵机厂商给的典型值,但不同舵机有偏差,血泪经验是先用示波器或者逻辑分析仪测一下实际脉宽,再微调这两个端点。文档里提到“STM32 根据指令输出 PWM 波,从而使机械臂的各个关节转过指定的角度”,说的就是这个过程。参数说明:htim 是定时器句柄,channel 是通道号,angle 是目标角度。注意 ARR 的值要和预分频匹配,否则周期就不是 20ms,舵机会抖动甚至不转。
3. 控制系统总体方案:二级主从式怎么搭
3.1 三种控制方式的选型理由
文档里列了单 CPU 集中控制、多 CPU 分布式控制和二级主从式控制三种方案。单 CPU 集中控制适合早期 Hero-Ⅰ 那种简单机器人,所有计算和 IO 都压在一个处理器上,速度慢,一旦任务复杂就卡死。多 CPU 分布式控制是每个关节一个 CPU,再加一个通信接口,可靠性高但成本也高,走线复杂。二级主从式是上位机负责轨迹规划和运动学计算,下位机 STM32 负责实时输出 PWM 和读传感器。这个方案的好处是分工明确:上位机算力强,可以跑复杂的路径规划;STM32 实时性好,不会被操作系统调度打断。对于课程设计级别的两连杆机械臂,二级主从式是性价比最高的选择,既不会像单 CPU 那样捉襟见肘,也不会像分布式那样把预算烧光。
3.2 硬件链路:从 J-Link 到舵机
整个系统的信号流是这样的:计算机通过 J-Link 仿真器把程序下载到 STM32,同时也可以通过串口发送目标位置指令。STM32 收到指令后,先做逆运动学解算,得到两个关节的目标角度,然后调用定时器输出对应占空比的 PWM。PWM 信号进入舵机的控制电路板,控制板把它和位置反馈电位计的电压比较,驱动电机正反转,直到误差为零。电源部分用了双电源供电:STM32 和外围电路走 AMS1117-3.3V 稳压,舵机走 7.2V 电池经 LM2596 降压到 6V 左右。这个设计的好处是舵机的大电流不会拉垮单片机的电源,避免复位或死机。
# 典型的编译和下载流程(以 Makefile + OpenOCD 为例) make clean make all openocd -f interface/jlink.cfg -f target/stm32f1x.cfg -c "program build/firmware.elf verify reset exit"这段命令的逻辑是:先清理旧编译产物,再重新编译,最后通过 OpenOCD 把固件烧进 STM32。参数说明:interface/jlink.cfg 指定调试器,target/stm32f1x.cfg 指定芯片型号,program 后面跟固件路径。如果你用的是 Keil 或 IAR,直接点下载按钮就行,但命令行方式更适合批量复现。
3.3 电源模块:LM2596 和 AMS1117 的分工
文档里特别强调了电源设计的重要性,我完全同意。LM2596 是降压型开关稳压器,输入最高 40V,输出可调 1.2V 到 37V,最大 3A 电流,效率高,发热小。它负责把 7.2V 电池降到舵机需要的 6V。AMS1117-3.3V 是线性稳压器,负责给 STM32 和 TFT 彩屏供 3.3V。注意 AMS1117 的压差要求:输入至少比输出高 1V 以上,所以如果电池电压掉到 4.2V 以下,3.3V 可能就不稳了。常见做法是在电池和 AMS1117 之间加一个升压或者直接用两节锂电池串联。文档里提到“地线要足够粗,单点和多点相结合,同时分离模拟地和数字地”,这是为了避免舵机启停时的大电流在地线上产生压降,干扰单片机的模拟部分。我一般会在舵机电源输入端并一个 470uF 以上的电解电容,再并一个 0.1uF 陶瓷电容,效果立竿见影。
4. 避坑与排查:舵机抖动、逆解无解和电源复位
4.1 舵机抖动或不动:先查共地,再查脉宽
现象:STM32 输出 PWM 后,舵机要么纹丝不动,要么来回抖动。原因通常有三个:第一,STM32 和舵机没有共地,信号电平没有参考点;第二,PWM 周期不是 20ms,比如定时器配置错了预分频;第三,脉宽范围超出舵机实际接受范围。解决方法是先用万用表测 STM32 的 GND 和舵机电源的 GND 是否连通,再用逻辑分析仪抓一下 PWM 波形,确认周期和脉宽。如果周期是 20ms 但舵机还是抖,试着把脉宽范围缩到 1ms 到 2ms,很多舵机实际只认这个区间。
4.2 逆运动学报“超出工作空间”:检查杆长和目标点
现象:调用逆解函数时抛出异常,提示目标点不可达。原因是你给的目标坐标距离基座超过了 L1+L2,或者小于 |L1-L2|。解决方法是先算一下目标点的模长,和 L1+L2 比较。如果确实够不着,要么调整目标点,要么在代码里加一个“最近可达点”的降级策略。另外注意单位统一:如果杆长是毫米,目标点也必须是毫米,别一个用米一个用毫米。
4.3 电源一接舵机就复位:电流不够或地线太细
现象:STM32 单独跑正常,一接上舵机就重启,或者 TFT 彩屏闪一下。原因是舵机启动瞬间电流可能超过 1A,而 AMS1117 或者电池的放电能力不够,导致 3.3V 跌落。解决方法是给舵机单独供电,或者加大电池容量和稳压芯片的余量。地线也要加粗,最好用星型接地,把大电流回路和小信号回路分开。我一般会在舵机电源正负极之间直接焊一个 1000uF 的低阻电解电容,能吸收大部分启动冲击。
4.4 J-Link 能下载但程序不跑:检查 BOOT 引脚和复位电路
现象:J-Link 提示下载成功,但舵机没反应,或者 STM32 根本不启动。原因是 BOOT0 和 BOOT1 引脚的电平不对,芯片从系统存储器启动而不是 Flash。解决方法是确认 BOOT0 接地,BOOT1 任意。另外检查复位按钮是不是卡住了,或者复位电路的上拉电阻和电容值不对。常见做法是复位引脚接 10k 上拉到 3.3V,再并一个 100nF 电容到地。
4.5 舵机角度线性度差:端点校准和死区补偿
现象:给 90 度,舵机停在 85 度;给 180 度,舵机只能到 170 度。原因是舵机的机械限位和电位计非线性,尤其是廉价舵机。解决方法是做两点校准:先给 0 度和 180 度对应的脉宽,记录实际角度,然后线性插值修正。如果要求更高,可以分段校准,每 30 度一个点。注意别把舵机往机械限位死推,容易烧齿轮。
5. 进阶技巧:用定时器中断做轨迹插补
5.1 为什么需要插补
直接给目标角度,舵机会全速冲过去,冲击大,轨迹也不平滑。实际搬运任务里,你希望机械臂走直线或者圆弧,这就需要把路径拆成很多小段,每段算一次逆解,然后按固定时间间隔更新 PWM。STM32 的定时器中断可以做到 1ms 一次,足够让运动看起来连续。
// 定时器中断回调:每 1ms 更新一次舵机角度 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { static uint32_t step = 0; if (step < trajectory_len) { float angle1 = trajectory[step].theta1; float angle2 = trajectory[step].theta2; set_servo_angle(&htim3, TIM_CHANNEL_1, angle1); set_servo_angle(&htim3, TIM_CHANNEL_2, angle2); step++; } else { // 轨迹结束,停止更新 HAL_TIM_Base_Stop_IT(&htim2); } } }这段代码的逻辑是:预先算好一条轨迹上每个时间点的关节角,存在数组里,然后定时器每 1ms 取一个点更新 PWM。参数说明:trajectory_len 是轨迹点总数,trajectory 是结构体数组,每个元素包含两个角度。注意中断里不要做浮点运算太重的活,逆解最好在主循环里算完。
5.2 验证方法:用串口打印实际角度
光看舵机转不够,你还需要知道它是不是真的到了目标位置。常见做法是给舵机加一个电位计反馈,或者用编码器,然后通过串口把实际角度打印出来。如果不想加传感器,至少把目标角度和逆解结果打印出来,确认数学部分没错。
# 上位机 Python 脚本:通过串口发送目标点并接收状态 import serial import time ser = serial.Serial('COM3', 115200, timeout=1) time.sleep(2) # 等待 STM32 复位 def send_target(x, y): cmd = f"{x},{y}\n" ser.write(cmd.encode()) response = ser.readline().decode().strip() print(f"目标 ({x},{y}) -> 关节角: {response}") send_target(120, 60) send_target(80, 100) ser.close()这个脚本的逻辑是:通过串口把目标坐标发给 STM32,STM32 算完逆解后把角度回传。参数说明:COM3 是串口号,115200 是波特率,要和 STM32 端一致。注意发送的字符串要以换行结尾,方便 STM32 用中断接收解析。
5.3 一个我常犯的错误
早期我做这类设计时,总想把所有代码塞进 main 函数里,结果中断和主循环抢资源,舵机偶尔抽风。后来我强制自己把运动学计算、PWM 输出、串口通信分成三个独立的模块,每个模块只暴露一个接口,问题就少多了。从那以后我每次拿到新的机械臂项目,都先把 D-H 参数表在纸上画一遍,再用 Python 验证正逆解,最后才写 STM32 代码。这个习惯帮我省下了大量调试时间。希望帮到你。
本文还有配套的精品资源,点击获取