
简介这是一份蜘蛛机器人STM32F103控制程序源码示例项目主要面向正在学习嵌入式系统、STM32开发或机器人控制的开发者。源码围绕GPIO、ADC、SPI、I2C、UART等硬件接口展开涵盖电机驱动、传感器数据采集、FreeRTOS等实时操作系统任务调度、PID与步进电机控制算法并涉及蓝牙/Wi-Fi通信、传感器融合、电源管理及故障诊断机制可帮助使用者系统理解一只完整蜘蛛机器人从底层驱动到上层控制的实现路径。资源包为1.6MB的RAR压缩包共188个文件以C源码、编译工程文件、中间输出及调试文件为主目录结构清晰便于对照代码和工程配置进行学习目前已有1378人学习/下载。这份源码的看点在于它并非零散例程而是一套可运行的工程骨架。读者既能从中提取外设初始化、中断处理和RTOS任务管理的通用模板也能结合蜘蛛机器人特有的步态计算与传感器融合逻辑加深对嵌入式机器人控制架构的整体认识适合具备一定C语言与STM32基础、希望进阶到实际项目层级的开发者参考。 做了两年多足式机器人我的感受是真正能跑起来的蜘蛛机器人难点压根不在舵机驱动而在“18路PWM怎么管、每条腿三个关节怎么解算、六条腿怎么排步态周期”这一整条链路。STM32F103恰好是这条链路里性价比和资料丰富度都很均衡的控制器72MHz主频没有浮点单元但算三连杆逆解绰绰有余。这篇我会把蜘蛛机器人控制程序的核心思路、模块拆解和代码骨架摊开讲重点是让你拿到源码后能看懂、能改、能自己加功能而不是只会烧录进去看它爬两下。1. 为什么是F10318个舵机背后的引脚与选型逻辑1.1 先算一笔账STM32F103的PWM资源到底够不够蜘蛛机器人常规构型是六条腿每条腿三个舵机根部水平旋转一个控制整条腿的朝向、大腿一个控制抬腿、小腿一个控制蹬地。算下来是18路PWM输出这个数字直接决定了控制器选型。STM32F103的定时器资源大概是这样的TIM1是高级定时器4路PWM通道TIM2、TIM3、TIM4都是通用定时器每个也是4路TIM6、TIM7是基本定时器没有输出比较通道只能做定时和内部触发。所以满打满算F103的PWM输出能力是12到16路——注意这里还不能全算因为TIM1的部分通道在某些封装脚位上会跟SWD调试口或晶振引脚冲突。我之前做过一版只依赖MCU原生PWM的设计全部堆在TIM1、TIM2、TIM3、TIM4上实际可用的稳定通道数是14路离18路还差4路。如果你追求“MCU直驱全部舵机”F103直接驱动是不现实的。1.2 外扩方案PCA9685和“162”的实战组合既然原生PWM不够常规解法是外扩PCA9685这是一颗I2C接口的16路PWM驱动芯片舵机控制频率可以通过寄存器配置为50Hz脉宽精度到12位对蜘蛛机器人完全够用。但我不建议把18路舵机全部接到PCA9685上原因有两个一是PCA9685和MCU之间靠I2C通信虽然400kHz速率下刷新20ms周期的18路信号没问题但如果你同时还要跑运动学解算和步态规划I2C总线上的连续写入会占用不少CPU时间二是SPI/I2C引脚的电气噪声在舵机频繁动作时容易被干扰处理起来比原生PWM麻烦。我实测下来最好用的组合是“MCU原生PWM出14到16路PCA9685补剩余2到4路”。原生PWM给大腿和小腿这种需要频繁高精度控制的关节PCA9685给根部水平旋转这种动作幅度大、对时序并不敏感的关节。这样即使PCA9685偶尔被干扰导致某一路抖动也不会影响整机稳定性。1.3 引脚分配的具体建议分配引脚时记住三条铁律避开SWDPA13/PA14和晶振脚PD0/PD1避开串口下载用的BOOT引脚给调试串口预留至少一组USART。我的推荐分配是这样的定时器通道映射引脚用途TIM1CH1-CH4PA8/PA9/PA10/PA11左前腿大腿/小腿、左中腿大腿/小腿TIM2CH1-CH4PA0-PA3左后腿大腿/小腿、右前腿大腿/小腿TIM3CH1-CH4PB0-PB3右中腿大腿/小腿、右后腿大腿/小腿TIM4CH1-CH2PB6/PB7预留备用PWM或调试信号PCA9685通道0-5SCLPB8, SDAPB9六条腿根部旋转舵机注意TIM1的CH1在PA8PA8和PA9刚好又是USART1的TX引脚所以USART1没法同时用我一般把调试串口放到USART2PA2/PA3或者把USART1复用重映射到PB6/PB7——但PB6/PB7又被TIM4占用了所以干脆用USART2最省事。2. 20ms控制周期PWM解算与定时器分频的参数体系2.1 舵机信号的本质是脉宽不是频率几乎所有模拟舵机和数字舵机标准控制信号都是50Hz20ms一个周期而角度信息藏在脉宽里通常0.5ms脉宽对应0度2.5ms对应180度1.5ms对应中点。也就是说你不需要额外控制频率只需要精确地输出一个宽度在0.5ms到2.5ms之间的高电平并且每20ms重复一次。这意味着PWM配置的核心不是“多高频率”而是“一个周期内的高电平时间”。我在第一次调这块时犯过一个错把ARR设得特别大想提高脉宽分辨率结果周期变成了50ms舵机开始发抖——因为舵机内部的比较器在等待下一个脉冲时发现周期不对以为信号丢失回中力就开始振荡。2.2 F103定时器的分频计算从72MHz推到20msF103的系统时钟是72MHz。以挂在APB2上的TIM1为例它的时钟源就是72MHz。我们要做到20ms周期、1µs级别的脉宽分辨率计算如下预分频器PSC 71这样计数脉冲频率 72MHz / (711) 1MHz即每个计数单位1微秒。自动重装载值ARR 19999因为周期 (199991) * 1µs 20ms。脉宽 CCR寄存器的值单位是微秒。0.5ms对应CCR5002.5ms对应CCR2500。如果要把角度映射到CCR做一个线性转换uint16_t angle_to_ccr(float angle) { // angle: 0.0f ~ 180.0f return (uint16_t)(500.0f angle * (2000.0f / 180.0f)); }上面这个公式的前提是舵机严格按0.5ms到2.5ms对应0到180度。实际会有偏差我在后面调试章节专门讲标定问题。2.3 控制周期的分层设计20ms只是一个PWM刷新周期。整个机器人的控制逻辑还需要更细致的时基。我的代码里分了三个时间层1ms时基用SysTick或TIM6中断维护一个tick计数器用来做非阻塞延时和状态轮询。5ms时基做运动学解算生成每条腿足端的目标坐标。20ms时基把解算结果写入舵机PWM寄存器并且执行步态相位更新。为什么要分开因为运动学解算如果放到20ms周期里做一个步态周期只有5次更新走出来的动作明显卡顿而如果放到1ms里做CPU占用又偏高F103没有FPUatan2f和cosf加起来单次近20微秒18条腿算一轮要几百微秒但1ms周期内中仍有大量富余——其实也没有那么吃紧只是没必要。5ms的解算间隔配合20ms的输出刷新既能保证动作平滑又留足余量给串口指令处理和传感器读取。3. 三连杆逆解把“足端坐标”变成“三个舵机角度”3.1 把一条腿拆成“根部旋转 平面两连杆”蜘蛛机器人的单腿运动学可以拆成两步理解。第一步是根部旋转舵机它决定了整条腿在水平面上的朝向第二步是大腿和小腿在竖直平面内组成的两连杆结构。两步解耦之后逆解就变得非常清晰。我用的坐标系定义如下以身体几何中心为原点x轴指向机器人前方y轴指向左侧z轴竖直向上。腿根部旋转舵机的输出轴垂直于身体平面所以它控制的其实是足端在以根部为圆心的水平投影方向。先解根部角theta0 atan2f(target_y, target_x);这里target_x和target_y是足端在身体坐标系中的坐标。注意腿部安装方向不同atan2f可能需要加一个偏移角度我用结构体里存了每只脚的安装偏角初始化时根据机械装配统一写入。3.2 大腿和小腿的解算余弦定理是主角根部旋转解出来之后把坐标系绕z轴旋转-theta0问题就变成了在一个竖直平面内从髋关节点出发用两根分别长L1大腿和L2小腿的连杆去抵达平面内的目标点 (px, pz)。这个平面内的距离float r sqrtf(px * px pz * pz); // 髋关节到足端的直线距离如果 r 大于 L1 L2说明目标点超出机械臂可达范围需要做钳位或者返回错误标志。我是在代码里先做可达性检查不可达就直接保留上一周期的角度避免舵机瞬间跳变。接下来用余弦定理求小腿角度float cos_theta2 (L1 * L1 L2 * L2 - r * r) / (2.0f * L1 * L2); float theta2 acosf(cos_theta2); // 小腿相对大腿的夹角然后求大腿角度。大腿相对竖直轴的夹角等于髋关节到目标点连线与竖直轴的夹角减去大腿与这条连线之间的夹角float alpha atan2f(px, pz); // 注意这里atan2f(px, pz)是先传x再传z float beta acosf((L1 * L1 r * r - L2 * L2) / (2.0f * L1 * r)); float theta1 alpha - beta;最后把theta0、theta1、theta2分别乘上舵机安装方向系数再映射到CCR值写入PWM寄存器。这里有个很容易搞错的地方每条腿因为装配镜像大腿和小腿的旋转方向可能相反所以需要预留一个direction参数取值1或-1乘到最终输出上。3.3 完整函数实现参考typedef struct { float x; float y; float z; } Vec3f; typedef struct { float l1; // 大腿长度 float l2; // 小腿长度 float mount_angle; // 根部安装偏角 float dir[3]; // 三个舵机的方向系数 } LegParam; void leg_inverse_kinematics(LegParam *leg, Vec3f target, float *angles) { // 1. 根部角 float theta0 atan2f(target.y, target.x) - leg-mount_angle; // 2. 绕z轴旋转后的平面坐标 float c cosf(theta0 leg-mount_angle); float s sinf(theta0 leg-mount_angle); float px target.x * c target.y * s; // 旋转到腿部平面 float pz target.z; // z方向不变 float r sqrtf(px * px pz * pz); // 3. 可达性检查 float d_max leg-l1 leg-l2 - 0.01f; if (r d_max) { return; // 上一周期角度保持不变 } // 4. 余弦定理求解 float cos_theta2 (leg-l1 * leg-l1 leg-l2 * leg-l2 - r * r) / (2.0f * leg-l1 * leg-l2); float theta2 acosf(fmaxf(-1.0f, fminf(1.0f, cos_theta2))); float alpha atan2f(px, pz); float beta acosf((leg-l1 * leg-l1 r * r - leg-l2 * leg-l2) / (2.0f * leg-l1 * r)); float theta1 alpha - beta; // 5. 方向修正 angles[0] theta0 * leg-dir[0]; angles[1] theta1 * leg-dir[1]; angles[2] -theta2 * leg-dir[2]; // 小腿角度的符号取决于安装方式 }F103没有硬件浮点单元但atan2f、cosf、acosf这些函数在20ms周期内算18个点是没问题的实测大约每只脚0.3ms左右全腿一轮不到2ms远低于20ms刷新周期。4. 三角步态的实现方式分组、抬腿与重心4.1 为什么是“三脚着地”而不是“四脚着地”蜘蛛机器人做静态行走最关键的是重心投影要落在支撑多边形内部。六条腿最稳定的支撑状态有两个选择三腿支撑和四腿支撑。四腿支撑虽然更稳但四腿着地时另外两条腿抬起来步态周期会变得不连续而且四腿支撑容易和着地的另外两条腿打架。三角步态是最经典的方案六条腿分成两组每组三条腿两组交替支撑和摆动。因为任意时刻必有一组的三条腿着地而且这三条腿形成了一个三角形稳定区域重心只要落在这个区域内机器人就不会倾倒。分组方式我用了最常见的“对角三角”结构分组腿号A组左前(LF)、右中(RM)、左后(LH)B组右前(RF)、左中(LM)、右后(RH)A组摆动时B组着地B组摆动时A组着地。注意每组里的三条腿不能都在同一侧或同一前后位置否则支撑多边形会很窄容易侧翻。4.2 相位管理用一对方波生成“抬腿-落地”时序我实现步态的方式很简单维护一个gait_phase变量从0到1循环0到0.5是A组摆动、B组支撑0.5到1是B组摆动、A组支撑。这样每组腿的摆动和支撑正好各占半个步态周期。对每一条腿在摆动相内足端轨迹是“抬起来-往前伸-落下去”。我用半正弦曲线生成抬腿高度这样摆动腿起点和终点的垂直速度为零落地冲击小float t_swing phase_in_gait; // 0~1摆动相内部归一化 // 摆动相抬腿高度起点和终点都是0 float z_lift swing_height * sinf(M_PI * t_swing); // 水平方向位移从起点线性插值到终点也可以加S形 float x_swing start_x (end_x - start_x) * t_swing;支撑相的腿则相对身体向后滑动提供前进推力。支撑相的目标点也要随身体移动不断更新否则会出现“腿追不上身体”的拖拽感。4.3 转向和横移在足端轨迹上叠加偏移蜘蛛机器人转向和横移不需要改运动学模型只需要在生成足端目标坐标时对每组腿的摆动轨迹叠加一个旋转偏移或侧向偏移。我是在gait_generate_target()函数里先计算基础步态轨迹然后根据外部遥控指令前进速度、转向速度叠加偏移量Vec3f gait_generate_target(int leg_index, float phase) { // 基础步态目标点以腿根部为原点 Vec3f pos base_trajectory(leg_index, phase); // 转向每只腿绕身体中心旋转 if (turn_rate ! 0.0f) { float theta turn_rate * step_distance; rotate_around_body_center(pos, leg_index, theta); } // 横移 pos.x strafe_offset * phase_amp; return pos; }这里的核心经验是转向量和横移量都要归一化到单步步距不要直接用速度值去叠加否则不同步态周期会越走越偏。5. 代码架构状态机调度与模块划分5.1 五个核心模块各管一摊事拿到一份蜘蛛机器人源码不要急着通读全部先按模块归类。我做这套程序时模块划分是这样的main.c初始化硬件、注册中断回调、进入主循环调度。servo.c/servo.hPWM输出层负责把“角度”写入对应定时器的CCR寄存器同时管理PCA9685的外部PWM输出。kinematics.c/kinematics.h正逆解输入足端坐标输出关节角。gait.c/gait.h步态规划输入控制指令前进、后退、转向、横移输出每条腿的足端目标点。protocol.c/protocol.h串口指令解析接收上位机或遥控模块发来的命令帧翻译成控制指令。这种分层的好处很明显你想调步态参数根本不用关心舵机PWM细节你想换舵机型号改servo层就行kinematics和gait逻辑都复用。5.2 主循环别用阻塞延时用状态标志很多初学者写的STM32控制程序是while(1)里跑一大串延时舵机一抖动就发现是延时阻塞导致刷新周期不稳定。正确做法是主循环做非阻塞调度由定时器中断置标志位主循环查标志位再干活。我搭的骨架大概是这样的int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); MX_USART2_UART_Init(); pca9685_init(); servo_center_all(); gait_init(); while (1) { protocol_process(); // 收到遥控指令则更新 gait 参数 if (flag_5ms) { // 由TIM6中断置位 flag_5ms 0; gait_update(); for (int i 0; i 6; i) { leg_inverse_kinematics(legs[i], target[i], angles[i]); } } if (flag_20ms) { // 由TIM6或SysTick置位 flag_20ms 0; for (int i 0; i 6; i) { servo_set_angle(i * 3 0, angles[i][0]); servo_set_angle(i * 3 1, angles[i][1]); servo_set_angle(i * 3 2, angles[i][2]); } } } }5.3 状态机待机、行走、转向、停止整个控制主状态我设计成一个简单的枚举状态机typedef enum { ROBOT_STANDBY, ROBOT_WALK, ROBOT_TURN, ROBOT_STRAFE, ROBOT_STOP } RobotState;状态切换发生在一个动作周期结束后不要在动作执行中途切换——否则舵机目标点会跳变轻则卡顿重则撞机。我在gait.c里维护一个“当前步态是否执行完毕”的标志位只有在这个标志位置位时才允许切换状态。6. 调试中绕不开的坑位抖动、电流、掉电6.1 上电瞬间的“舵机乱舞”这个问题几乎每个做多舵机机器人的都会遇到。现象是单片机一上电所有舵机猛转一下然后停在某个随机位置过一会儿才恢复正常。根因是单片机在GPIO初始化和定时器启动之间有一段“无信号”窗口舵机检测不到有效PWM波形时就跑到一个不安全的默认位置。我的处理方式是在servo_init()里先做三件事void servo_init(void) { // 1. 先把所有PWM脚配置为推挽输出低电平 // 2. 再初始化定时器但先不输出比较 // 3. 等主函数走到这里时再把PWM使能并输出中点脉宽 }这样上电后舵机会先收到一个稳定的低电平不会误转。另外控制板给舵机的电源如果比逻辑信号晚到也会出现乱舞我加了硬件上的“信号保持电路”——串一个10k电阻到GND让GPIO未初始化时信号线默认拉低。6.2 抖动与供电万恶的电不够蜘蛛机器人18个舵机同时动的时候瞬间电流是惊人的。MG996R这种金属齿轮舵机堵转电流能到1A以上18个就是18A哪怕不是同时堵转起步瞬间的浪涌也够普通5V电源喝一壶的。我一开始用2A的USB电源供电爬了两步就开始抖动测了一下电压纹波已经到400mV了。解决办法是给舵机独立供电用6V/5A以上的BEC或是锂电池降压模块并且在舵机电源端并联一颗470µF~1000µF的电解电容加若干104陶瓷电容。MCU逻辑电路和舵机电源之间要共地但供电走线要分开避免舵机的电流脉冲在地线上压出噪声。有一个不太直观但很重要的细节舵机的PWM信号电平一般是5V而F103是3.3V逻辑直接连问题也不大因为大多数舵机对高电平的识别阈值比较宽松。但如果你用的是那种对电平很挑剔的舵机最好加一个电平转换模块。6.3 掉电保存F103内部Flash写数据的两个坑如果需要存“舵机中点标定值”这类掉电不能丢的参数F103内部Flash可以省掉外接EEPROM。但有两个坑必须知道第一Flash写入前必须擦除整个扇区不能只擦一个字节。F103小容量每扇区1KB中容量是2KB擦除操作是按扇区为单位整块擦的。所以你不能频繁修改同一个字节否则Flash会快速磨损。第二写入Flash时程序会被挂起如果此时发生中断会导致写入异常。正确写法是在写入前关闭总中断写完后恢复。我用的方案是在Flash里划出两个扇区写新数据前先检查当前扇区是否快写满快满了就擦除备份扇区再切换这样即使写入中途掉电至少有一份旧数据是完整的。7. 还能往哪里扩展蜘蛛机器人的基础平台搭好之后扩展方向非常多。只说几个我试过并且觉得性价比高的手机遥控在protocol.c的指令解析里加一条蓝牙串口协议将手机陀螺仪角度映射成机器人的速度方向和转向量改动量很小但体验提升非常大。姿态反馈加MPU6050惯性传感器把姿态角作为闭环反馈实时纠正四条支撑腿的伸缩量在斜坡和软地面上稳定性明显提高。这类扩展需要把运动学输出从“纯几何解算”升级成“闭环控制”但代码结构不用大改只要在5ms周期里加入姿态修正量即可。上位机可视化用Python写一个简单的3D可视化脚本通过串口实时读取每条腿的关节角度和足端轨迹这对调参太有用了——你可以直接看到足端轨迹有没有严重抖动、有没有穿过身体平面省得蹲在地上盯半天。我个人的经验是千万不要一上来就想着给蜘蛛机器人加视觉、加导航先把步态调顺把舵机标定做好这台机器人才算真正“活”了。再往后加任何传感器都是在这个稳定骨架上锦上添花。本文还有配套的精品资源点击获取