简介:这份STM32F103蜘蛛机器人控制程序源码面向嵌入式开发初学者与机器人控制爱好者,用于演示如何用经典Cortex-M3内核芯片驱动多足机器人。源码围绕硬件接口、驱动程序、RTOS任务调度、PID与步进电机控制算法、通信协议、传感器融合、电源管理及故障诊断等模块展开,可帮助读者理解从GPIO、ADC到电机驱动的完整控制链路。压缩包共188个文件,约1.6MB,以31个c源文件与34个h头文件为核心,配合31个crf、33个o等编译中间文件,以及uv2、axf、map等工程与链接产物,目录结构保留了Keil工程原貌,便于直接编译与二次开发。目前已有1384人学习下载,适合作为嵌入式控制入门与多足机器人运动控制的实践参考。1. 蜘蛛机器人STM32F103控制程序:从舵机抖动到步态稳定的完整工程如果你手头正好有一块 STM32F103 的开发板,又对多足机器人感兴趣,那这份蜘蛛机器人控制程序源码大概率能让你少走不少弯路。它不是那种只点亮一个 LED 就结束的玩具 Demo,而是一套围绕 18 路舵机协同、三角步态规划、逆运动学解算展开的完整嵌入式工程。我第一次拿到类似结构的代码时,最直观的感受是:原来六足机器人的“走路”不是靠延时硬凑出来的,而是靠定时器分时复用加坐标变换算出来的。这份源码适合已经学过 STM32 基础外设、想往机器人控制方向进阶的开发者,也适合做毕业设计或课程项目的同学直接拿来二次开发。下面我就按“能干什么 → 怎么跑 → 怎么改”的顺序,把它拆开讲清楚。2. 工程架构与硬件资源映射:18 路舵机怎么挂到 STM32F103 上2.1 整体软件分层这套程序的结构并不复杂,但分层比较清晰。最底层是 STM32F103 的标准外设库或 HAL 库封装,负责 GPIO、定时器、串口和系统时钟;中间层是舵机驱动层,把 18 路 PWM 信号抽象成统一的Servo_SetAngle()接口;再往上是步态控制层,包含三角步态相位表和逆运动学解算;最顶层是主循环和指令解析,负责接收串口指令或按预设动作序列执行。常见做法是把舵机按腿分组,每条腿 3 个关节:髋关节、膝关节、踝关节。六条腿就是 18 个自由度。源码里一般会定义一个结构体数组来管理这些舵机的当前角度、目标角度和运动速度,这样后续做插值过渡时不会出现舵机“跳变”导致机械结构咔咔响。typedef struct { uint8_t id; // 舵机编号 0~17 uint16_t current_angle; // 当前角度,单位 0.1 度 uint16_t target_angle; // 目标角度 uint16_t speed; // 每周期变化量 TIM_TypeDef *tim; // 所属定时器 uint8_t channel; // 定时器通道 } Servo_t; Servo_t g_servos[18]; // 全局舵机数组这段结构体是整个控制程序的基石。current_angle和target_angle分开存,是为了让主循环里做一个简单的线性插值,避免直接赋值造成电流冲击。speed字段决定了舵机从当前角度转到目标角度需要多少个控制周期,数值越大转得越快,但太大就会抖。我一般会把速度控制在每 20ms 周期变化 5~15 个单位之间,具体要看舵机型号和负载。2.2 定时器资源分配与 PWM 生成STM32F103 最多有 4 个通用定时器加 2 个高级定时器,每个定时器能输出 4 路 PWM,理论上 6 个定时器就能凑出 24 路。但实际工程里还要留出串口、系统滴答等资源,所以常见方案是用 3 个定时器的 12 路 PWM 配合 74HC595 或 PCA9685 这类外扩芯片来驱动 18 路舵机。如果源码里直接用了 5 个定时器各出 4 路,那说明作者对引脚复用做了比较细的规划。void Servo_PWM_Init(void) { // 以 TIM3 为例,配置 4 路 PWM 输出,周期 20ms TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // PB0~PB3 复用为 TIM3 通道 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 72MHz / 72 = 1MHz 计数频率,20000 计数 = 20ms TIM_TimeBaseStructure.TIM_Period = 20000 - 1; TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 初始占空比 1.5ms,对应舵机中位 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 1500; 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); TIM_Cmd(TIM3, ENABLE); }这里的关键参数是TIM_Prescaler和TIM_Period。72MHz 主频下,预分频 72 得到 1MHz 计数频率,周期设 20000 就是 20ms,正好是舵机标准控制周期。TIM_Pulse设 1500 对应 1.5ms 高电平,舵机停在中位。如果你用的舵机是 180 度版本,脉宽范围一般是 500~2500,对应 0~180 度;如果是 270 度版本,范围会宽一些,这个要在Servo_SetAngle()里做映射。2.3 舵机角度映射与限幅保护角度映射看起来简单,但实际写的时候很容易翻车。比如机械结构装好后发现某个关节只能转 30~150 度,硬写 0~180 就会撞限位,舵机堵转发热甚至烧掉。所以源码里一般会有一个校准表,把每个舵机的安全范围单独存起来。// 每个舵机的最小/最大脉宽,单位 us const uint16_t servo_min_pulse[18] = {500, 600, 550, ...}; const uint16_t servo_max_pulse[18] = {2400, 2350, 2450, ...}; void Servo_SetAngle(uint8_t id, uint16_t angle) { uint16_t pulse; if (angle > 1800) angle = 1800; // 限幅到 180.0 度 pulse = servo_min_pulse[id] + (uint32_t)(servo_max_pulse[id] - servo_min_pulse[id]) * angle / 1800; switch (g_servos[id].tim) { case TIM3: TIM_SetCompare1(TIM3, pulse); break; // ... 其他定时器分支 } }angle用 0.1 度为单位是为了避免浮点运算,在 M3 内核上整数运算快得多。servo_min_pulse和servo_max_pulse这两个数组就是你的“后悔药”,装好机械结构后第一件事就是逐个舵机测安全范围,把值填进去。不填的话,后面调步态时一个参数写错就可能烧舵机。3. 三角步态与逆运动学:让六足机器人走得像那么回事3.1 三角步态相位表设计六足机器人最常用的步态就是三角步态:把六条腿分成两组,每组三条腿构成一个三角形支撑面,交替抬起和落下。这样任何时候至少有三条腿着地,稳定性好,控制逻辑也比波动步态简单。源码里一般用一个相位表来定义每个时刻哪些腿是摆动相、哪些是支撑相。// 三角步态相位表,1 表示摆动相(抬腿),0 表示支撑相 // 每组 6 个元素对应腿 0~5,周期分 8 个阶段 const uint8_t tripod_gait[8][6] = { {1, 0, 1, 0, 1, 0}, // 阶段 0:腿 0/2/4 抬起 {1, 0, 1, 0, 1, 0}, {0, 1, 0, 1, 0, 1}, // 阶段 2:腿 1/3/5 抬起 {0, 1, 0, 1, 0, 1}, {1, 0, 1, 0, 1, 0}, {1, 0, 1, 0, 1, 0}, {0, 1, 0, 1, 0, 1}, {0, 1, 0, 1, 0, 1} };这个表看起来有点“笨”,但实际跑起来很稳。每个阶段持续 20~40ms,整个周期 160~320ms,机器人就能以比较自然的速度前进。如果你想让机器人走得更快,可以减少每阶段延时,但要注意舵机响应速度跟不跟得上。我试过把周期压到 100ms 以内,结果舵机还没转到目标位置就切下一阶段了,机器人直接趴窝。3.2 单腿逆运动学解算要让足端走到指定坐标,得靠逆运动学算三个关节的角度。六足机器人的单腿一般简化为平面二连杆加一个髋关节旋转,所以解算分两步:先在腿平面内算膝关节和踝关节,再算髋关节的旋转角。#include <math.h> #define L1 50.0f // 大腿长度 mm #define L2 60.0f // 小腿长度 mm typedef struct { float x, y, z; // 足端目标坐标 float hip_angle; // 髋关节角度 float knee_angle; // 膝关节角度 float ankle_angle; // 踝关节角度 } LegIK_t; void Leg_InverseKinematics(LegIK_t *leg) { float dist, cos_knee, temp; // 髋关节旋转角:足端在水平面的投影 leg->hip_angle = atan2f(leg->y, leg->x) * 180.0f / M_PI; // 腿平面内的水平距离 dist = sqrtf(leg->x * leg->x + leg->y * leg->y) - L1; temp = sqrtf(dist * dist + leg->z * leg->z); // 余弦定理求膝关节 cos_knee = (L1 * L1 + L2 * L2 - temp * temp) / (2 * L1 * L2); if (cos_knee > 1.0f) cos_knee = 1.0f; if (cos_knee < -1.0f) cos_knee = -1.0f; leg->knee_angle = acosf(cos_knee) * 180.0f / M_PI; // 踝关节角度由几何关系得出 leg->ankle_angle = atan2f(leg->z, dist) * 180.0f / M_PI; }这段代码里L1和L2必须和你的机械结构实际尺寸一致,差几毫米就会导致足端落点偏移。atan2f和acosf是标准库函数,在 M3 上跑一次大概几十微秒,18 个舵机轮流算一遍也就几毫秒,完全来得及。注意cos_knee要做限幅,否则浮点误差可能让它超出 [-1,1] 导致acosf返回 NaN,机器人就会突然抽一下。3.3 步态调度与主循环主循环里要做的事情就是按相位表更新每条腿的足端坐标,然后调用逆运动学算出关节角度,再通过插值慢慢把舵机转过去。整个过程不能有阻塞延时,否则步态节奏就乱了。void Main_Loop(void) { static uint8_t phase = 0; static uint32_t last_tick = 0; LegIK_t leg; uint8_t i; while (1) { if (SysTick_GetTick() - last_tick >= 30) { // 每 30ms 走一个阶段 last_tick = SysTick_GetTick(); for (i = 0; i < 6; i++) { if (tripod_gait[phase][i]) { // 摆动相:足端抬高并前移 leg.x = gait_x[i] + stride; leg.y = gait_y[i]; leg.z = gait_z[i] + lift_height; } else { // 支撑相:足端后移,推动身体前进 leg.x = gait_x[i] - stride; leg.y = gait_y[i]; leg.z = gait_z[i]; } Leg_InverseKinematics(&leg); Servo_SetAngle(i * 3 + 0, (uint16_t)(leg.hip_angle * 10)); Servo_SetAngle(i * 3 + 1, (uint16_t)(leg.knee_angle * 10)); Servo_SetAngle(i * 3 + 2, (uint16_t)(leg.ankle_angle * 10)); } phase = (phase + 1) % 8; } // 这里可以处理串口指令、传感器数据等 } }stride是步幅,lift_height是抬腿高度,这两个参数直接决定机器人走得多快、多稳。步幅太大容易打滑,抬腿太低容易绊到地面。我一般会从stride=20mm、lift_height=15mm开始调,观察机器人实际运动再微调。SysTick_GetTick()提供毫秒级时间戳,保证每个阶段间隔稳定。4. 编译烧录与联调:从源码到机器人动起来4.1 开发环境搭建这套源码一般是用 Keil MDK 或者 STM32CubeIDE 打开的。如果你拿到的是 Keil 工程,需要确认安装了 STM32F1 的器件支持包;如果是 CubeIDE 工程,直接导入就能编译。源码里通常会包含Startup文件、外设库文件和main.c,缺少库文件的话编译会报一堆undefined symbol。# 以命令行方式用 arm-none-eabi 编译为例 arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \ -I./Inc -I./Drivers/STM32F1xx_HAL_Driver/Inc \ -c ./Src/main.c -o ./build/main.o arm-none-eabi-gcc -T ./STM32F103C8Tx_FLASH.ld \ ./build/*.o -o ./build/firmware.elf arm-none-eabi-objcopy -O binary ./build/firmware.elf ./build/firmware.bin编译通过后,用 ST-Link 或串口 ISP 把firmware.bin烧进去。烧录前记得把 BOOT0 跳线设对,否则芯片不进入下载模式。烧录成功后复位,如果舵机开始按中位角度锁住,说明 PWM 初始化正常。4.2 舵机中位校准第一次上电不要急着让机器人走,先做中位校准。把所有舵机转到机械结构的中间位置,然后调整servo_min_pulse和servo_max_pulse让Servo_SetAngle(id, 900)时舵机正好在中间。这一步偷懒的话,后面步态调出来的姿势会歪七扭八。提示:校准的时候最好用外部电源给舵机供电,不要从 STM32 板子上取电。18 个舵机同时动起来电流能到好几安培,USB 供电根本扛不住,还会导致单片机复位。4.3 串口指令调试源码里一般会留一个串口接口,用来在线修改步幅、抬腿高度、周期这些参数。常见协议是简单的 ASCII 指令,比如S20设置步幅为 20mm,L15设置抬腿高度 15mm。这样你不用每次改参数都重新编译烧录,调步态效率高很多。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { char ch = USART_ReceiveData(USART1); if (ch == '\n') { Parse_Command(cmd_buffer); // 解析并执行指令 cmd_index = 0; } else { cmd_buffer[cmd_index++] = ch; } } }中断里只做接收和缓冲,解析放到主循环里做,避免在中断里执行耗时操作。cmd_buffer要留够长度并做溢出保护,不然收到长字符串会踩内存。5. 避坑与常见问题:那些让我熬夜的瞬间5.1 舵机抖动或嗡嗡响现象是舵机停在某个角度时不停抖动,发出嗡嗡声。原因通常是 PWM 信号抖动或者电源纹波太大。解决方法是检查定时器配置有没有被其他中断打断,同时在舵机电源端并一个大电容(比如 1000uF)吸收电流突变。如果是个别舵机抖,可能是该路 PWM 占空比计算时除了零或者整数溢出。5.2 机器人走几步就复位现象是机器人动几下单片机就重启。原因是舵机启动电流把电源电压拉低,导致单片机欠压复位。解决方法是舵机和单片机分开供电,共地但不共电源。如果必须共电源,要选内阻小、电流余量大的电池,并在电源入口加 LC 滤波。5.3 逆运动学算出 NaN现象是机器人突然某条腿抽一下或者完全不动。原因是acosf的参数超出了 [-1,1] 范围。解决方法是像前面代码那样做限幅,同时检查L1、L2和足端坐标是否合理。如果目标点离髋关节太远,超过了大腿加小腿的总长,那这个点本身就是不可达的,应该在解算前先判断距离并截断。5.4 步态节奏忽快忽慢现象是机器人走路时快时慢,像喝醉了。原因是主循环里做了阻塞操作,比如用了delay_ms()而不是时间戳判断。解决方法是把所有延时改成非阻塞的 tick 判断,确保每个控制周期的时间偏差在几毫秒以内。另外串口打印也会拖慢节奏,调试信息要精简或者用 DMA 发送。5.5 烧录后没有任何反应现象是程序烧进去了但舵机不动。先检查 BOOT0 和 BOOT1 跳线是不是在正确位置,再确认晶振有没有起振。如果用的是外部晶振,而板子上没焊或者焊错了,系统时钟初始化就会卡住。可以在SystemInit()里加个 LED 翻转来确认程序有没有跑到主循环。6. 二次开发方向:从能走到走得好看这份源码跑通之后,最值得折腾的方向是让步态更自然。三角步态虽然稳,但看起来比较机械。你可以试着加入波动步态,让六条腿按顺序依次抬起,身体重心跟着前后移动,走起来会像真的虫子。实现思路是把相位表从 8 阶段扩展到 24 阶段,每条腿的摆动窗口错开,同时根据支撑腿的位置动态调整身体高度。另一个方向是加传感器反馈。比如在足端加微动开关或者力传感器,检测哪条腿真正着地,然后动态调整抬腿高度和步幅。这样机器人走到不平地面时不会因为某条腿悬空而失去平衡。代码上就是在主循环里读 GPIO 或者 ADC,把结果映射到lift_height和stride上。还有一个很实用的改造是加蓝牙或无线串口模块,用手机 App 遥控。源码里已经有串口指令解析的框架,你只需要把接收源从 USART1 换成蓝牙模块对应的串口,再定义几个动作指令比如前进、后退、转向,就能用手机操控了。我自己的习惯是每次改完步态参数,都会先用串口把关键数据打印出来,确认逆运动学算出的角度在安全范围内,再让机器人实际跑。有一次偷懒没打印,结果一个步幅参数写大了,机器人直接劈叉,烧了一个舵机。从那以后我每次调步态都强制走一遍“打印角度 → 空载测试 → 带载测试”的流程,再急也不跳过。希望这份拆解能帮你把蜘蛛机器人顺利跑起来,少烧几个舵机。本文还有配套的精品资源,点击获取
简介:这份STM32F103蜘蛛机器人控制程序源码面向嵌入式开发初学者与机器人控制爱好者,用于演示如何用经典Cortex-M3内核芯片驱动多足机器人。源码围绕硬件接口、驱动程序、RTOS任务调度、PID与步进电机控制算法、通信协议、传感器融合、电源管理及故障诊断等模块展开,可帮助读者理解从GPIO、ADC到电机驱动的完整控制链路。压缩包共188个文件,约1.6MB,以31个c源文件与34个h头文件为核心,配合31个crf、33个o等编译中间文件,以及uv2、axf、map等工程与链接产物,目录结构保留了Keil工程原貌,便于直接编译与二次开发。目前已有1384人学习下载,适合作为嵌入式控制入门与多足机器人运动控制的实践参考。1. 蜘蛛机器人STM32F103控制程序:从舵机抖动到步态稳定的完整工程如果你手头正好有一块 STM32F103 的开发板,又对多足机器人感兴趣,那这份蜘蛛机器人控制程序源码大概率能让你少走不少弯路。它不是那种只点亮一个 LED 就结束的玩具 Demo,而是一套围绕 18 路舵机协同、三角步态规划、逆运动学解算展开的完整嵌入式工程。我第一次拿到类似结构的代码时,最直观的感受是:原来六足机器人的“走路”不是靠延时硬凑出来的,而是靠定时器分时复用加坐标变换算出来的。这份源码适合已经学过 STM32 基础外设、想往机器人控制方向进阶的开发者,也适合做毕业设计或课程项目的同学直接拿来二次开发。下面我就按“能干什么 → 怎么跑 → 怎么改”的顺序,把它拆开讲清楚。2. 工程架构与硬件资源映射:18 路舵机怎么挂到 STM32F103 上2.1 整体软件分层这套程序的结构并不复杂,但分层比较清晰。最底层是 STM32F103 的标准外设库或 HAL 库封装,负责 GPIO、定时器、串口和系统时钟;中间层是舵机驱动层,把 18 路 PWM 信号抽象成统一的Servo_SetAngle()接口;再往上是步态控制层,包含三角步态相位表和逆运动学解算;最顶层是主循环和指令解析,负责接收串口指令或按预设动作序列执行。常见做法是把舵机按腿分组,每条腿 3 个关节:髋关节、膝关节、踝关节。六条腿就是 18 个自由度。源码里一般会定义一个结构体数组来管理这些舵机的当前角度、目标角度和运动速度,这样后续做插值过渡时不会出现舵机“跳变”导致机械结构咔咔响。typedef struct { uint8_t id; // 舵机编号 0~17 uint16_t current_angle; // 当前角度,单位 0.1 度 uint16_t target_angle; // 目标角度 uint16_t speed; // 每周期变化量 TIM_TypeDef *tim; // 所属定时器 uint8_t channel; // 定时器通道 } Servo_t; Servo_t g_servos[18]; // 全局舵机数组这段结构体是整个控制程序的基石。current_angle和target_angle分开存,是为了让主循环里做一个简单的线性插值,避免直接赋值造成电流冲击。speed字段决定了舵机从当前角度转到目标角度需要多少个控制周期,数值越大转得越快,但太大就会抖。我一般会把速度控制在每 20ms 周期变化 5~15 个单位之间,具体要看舵机型号和负载。2.2 定时器资源分配与 PWM 生成STM32F103 最多有 4 个通用定时器加 2 个高级定时器,每个定时器能输出 4 路 PWM,理论上 6 个定时器就能凑出 24 路。但实际工程里还要留出串口、系统滴答等资源,所以常见方案是用 3 个定时器的 12 路 PWM 配合 74HC595 或 PCA9685 这类外扩芯片来驱动 18 路舵机。如果源码里直接用了 5 个定时器各出 4 路,那说明作者对引脚复用做了比较细的规划。void Servo_PWM_Init(void) { // 以 TIM3 为例,配置 4 路 PWM 输出,周期 20ms TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // PB0~PB3 复用为 TIM3 通道 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 72MHz / 72 = 1MHz 计数频率,20000 计数 = 20ms TIM_TimeBaseStructure.TIM_Period = 20000 - 1; TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 初始占空比 1.5ms,对应舵机中位 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 1500; 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); TIM_Cmd(TIM3, ENABLE); }这里的关键参数是TIM_Prescaler和TIM_Period。72MHz 主频下,预分频 72 得到 1MHz 计数频率,周期设 20000 就是 20ms,正好是舵机标准控制周期。TIM_Pulse设 1500 对应 1.5ms 高电平,舵机停在中位。如果你用的舵机是 180 度版本,脉宽范围一般是 500~2500,对应 0~180 度;如果是 270 度版本,范围会宽一些,这个要在Servo_SetAngle()里做映射。2.3 舵机角度映射与限幅保护角度映射看起来简单,但实际写的时候很容易翻车。比如机械结构装好后发现某个关节只能转 30~150 度,硬写 0~180 就会撞限位,舵机堵转发热甚至烧掉。所以源码里一般会有一个校准表,把每个舵机的安全范围单独存起来。// 每个舵机的最小/最大脉宽,单位 us const uint16_t servo_min_pulse[18] = {500, 600, 550, ...}; const uint16_t servo_max_pulse[18] = {2400, 2350, 2450, ...}; void Servo_SetAngle(uint8_t id, uint16_t angle) { uint16_t pulse; if (angle > 1800) angle = 1800; // 限幅到 180.0 度 pulse = servo_min_pulse[id] + (uint32_t)(servo_max_pulse[id] - servo_min_pulse[id]) * angle / 1800; switch (g_servos[id].tim) { case TIM3: TIM_SetCompare1(TIM3, pulse); break; // ... 其他定时器分支 } }angle用 0.1 度为单位是为了避免浮点运算,在 M3 内核上整数运算快得多。servo_min_pulse和servo_max_pulse这两个数组就是你的“后悔药”,装好机械结构后第一件事就是逐个舵机测安全范围,把值填进去。不填的话,后面调步态时一个参数写错就可能烧舵机。3. 三角步态与逆运动学:让六足机器人走得像那么回事3.1 三角步态相位表设计六足机器人最常用的步态就是三角步态:把六条腿分成两组,每组三条腿构成一个三角形支撑面,交替抬起和落下。这样任何时候至少有三条腿着地,稳定性好,控制逻辑也比波动步态简单。源码里一般用一个相位表来定义每个时刻哪些腿是摆动相、哪些是支撑相。// 三角步态相位表,1 表示摆动相(抬腿),0 表示支撑相 // 每组 6 个元素对应腿 0~5,周期分 8 个阶段 const uint8_t tripod_gait[8][6] = { {1, 0, 1, 0, 1, 0}, // 阶段 0:腿 0/2/4 抬起 {1, 0, 1, 0, 1, 0}, {0, 1, 0, 1, 0, 1}, // 阶段 2:腿 1/3/5 抬起 {0, 1, 0, 1, 0, 1}, {1, 0, 1, 0, 1, 0}, {1, 0, 1, 0, 1, 0}, {0, 1, 0, 1, 0, 1}, {0, 1, 0, 1, 0, 1} };这个表看起来有点“笨”,但实际跑起来很稳。每个阶段持续 20~40ms,整个周期 160~320ms,机器人就能以比较自然的速度前进。如果你想让机器人走得更快,可以减少每阶段延时,但要注意舵机响应速度跟不跟得上。我试过把周期压到 100ms 以内,结果舵机还没转到目标位置就切下一阶段了,机器人直接趴窝。3.2 单腿逆运动学解算要让足端走到指定坐标,得靠逆运动学算三个关节的角度。六足机器人的单腿一般简化为平面二连杆加一个髋关节旋转,所以解算分两步:先在腿平面内算膝关节和踝关节,再算髋关节的旋转角。#include <math.h> #define L1 50.0f // 大腿长度 mm #define L2 60.0f // 小腿长度 mm typedef struct { float x, y, z; // 足端目标坐标 float hip_angle; // 髋关节角度 float knee_angle; // 膝关节角度 float ankle_angle; // 踝关节角度 } LegIK_t; void Leg_InverseKinematics(LegIK_t *leg) { float dist, cos_knee, temp; // 髋关节旋转角:足端在水平面的投影 leg->hip_angle = atan2f(leg->y, leg->x) * 180.0f / M_PI; // 腿平面内的水平距离 dist = sqrtf(leg->x * leg->x + leg->y * leg->y) - L1; temp = sqrtf(dist * dist + leg->z * leg->z); // 余弦定理求膝关节 cos_knee = (L1 * L1 + L2 * L2 - temp * temp) / (2 * L1 * L2); if (cos_knee > 1.0f) cos_knee = 1.0f; if (cos_knee < -1.0f) cos_knee = -1.0f; leg->knee_angle = acosf(cos_knee) * 180.0f / M_PI; // 踝关节角度由几何关系得出 leg->ankle_angle = atan2f(leg->z, dist) * 180.0f / M_PI; }这段代码里L1和L2必须和你的机械结构实际尺寸一致,差几毫米就会导致足端落点偏移。atan2f和acosf是标准库函数,在 M3 上跑一次大概几十微秒,18 个舵机轮流算一遍也就几毫秒,完全来得及。注意cos_knee要做限幅,否则浮点误差可能让它超出 [-1,1] 导致acosf返回 NaN,机器人就会突然抽一下。3.3 步态调度与主循环主循环里要做的事情就是按相位表更新每条腿的足端坐标,然后调用逆运动学算出关节角度,再通过插值慢慢把舵机转过去。整个过程不能有阻塞延时,否则步态节奏就乱了。void Main_Loop(void) { static uint8_t phase = 0; static uint32_t last_tick = 0; LegIK_t leg; uint8_t i; while (1) { if (SysTick_GetTick() - last_tick >= 30) { // 每 30ms 走一个阶段 last_tick = SysTick_GetTick(); for (i = 0; i < 6; i++) { if (tripod_gait[phase][i]) { // 摆动相:足端抬高并前移 leg.x = gait_x[i] + stride; leg.y = gait_y[i]; leg.z = gait_z[i] + lift_height; } else { // 支撑相:足端后移,推动身体前进 leg.x = gait_x[i] - stride; leg.y = gait_y[i]; leg.z = gait_z[i]; } Leg_InverseKinematics(&leg); Servo_SetAngle(i * 3 + 0, (uint16_t)(leg.hip_angle * 10)); Servo_SetAngle(i * 3 + 1, (uint16_t)(leg.knee_angle * 10)); Servo_SetAngle(i * 3 + 2, (uint16_t)(leg.ankle_angle * 10)); } phase = (phase + 1) % 8; } // 这里可以处理串口指令、传感器数据等 } }stride是步幅,lift_height是抬腿高度,这两个参数直接决定机器人走得多快、多稳。步幅太大容易打滑,抬腿太低容易绊到地面。我一般会从stride=20mm、lift_height=15mm开始调,观察机器人实际运动再微调。SysTick_GetTick()提供毫秒级时间戳,保证每个阶段间隔稳定。4. 编译烧录与联调:从源码到机器人动起来4.1 开发环境搭建这套源码一般是用 Keil MDK 或者 STM32CubeIDE 打开的。如果你拿到的是 Keil 工程,需要确认安装了 STM32F1 的器件支持包;如果是 CubeIDE 工程,直接导入就能编译。源码里通常会包含Startup文件、外设库文件和main.c,缺少库文件的话编译会报一堆undefined symbol。# 以命令行方式用 arm-none-eabi 编译为例 arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \ -I./Inc -I./Drivers/STM32F1xx_HAL_Driver/Inc \ -c ./Src/main.c -o ./build/main.o arm-none-eabi-gcc -T ./STM32F103C8Tx_FLASH.ld \ ./build/*.o -o ./build/firmware.elf arm-none-eabi-objcopy -O binary ./build/firmware.elf ./build/firmware.bin编译通过后,用 ST-Link 或串口 ISP 把firmware.bin烧进去。烧录前记得把 BOOT0 跳线设对,否则芯片不进入下载模式。烧录成功后复位,如果舵机开始按中位角度锁住,说明 PWM 初始化正常。4.2 舵机中位校准第一次上电不要急着让机器人走,先做中位校准。把所有舵机转到机械结构的中间位置,然后调整servo_min_pulse和servo_max_pulse让Servo_SetAngle(id, 900)时舵机正好在中间。这一步偷懒的话,后面步态调出来的姿势会歪七扭八。提示:校准的时候最好用外部电源给舵机供电,不要从 STM32 板子上取电。18 个舵机同时动起来电流能到好几安培,USB 供电根本扛不住,还会导致单片机复位。4.3 串口指令调试源码里一般会留一个串口接口,用来在线修改步幅、抬腿高度、周期这些参数。常见协议是简单的 ASCII 指令,比如S20设置步幅为 20mm,L15设置抬腿高度 15mm。这样你不用每次改参数都重新编译烧录,调步态效率高很多。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { char ch = USART_ReceiveData(USART1); if (ch == '\n') { Parse_Command(cmd_buffer); // 解析并执行指令 cmd_index = 0; } else { cmd_buffer[cmd_index++] = ch; } } }中断里只做接收和缓冲,解析放到主循环里做,避免在中断里执行耗时操作。cmd_buffer要留够长度并做溢出保护,不然收到长字符串会踩内存。5. 避坑与常见问题:那些让我熬夜的瞬间5.1 舵机抖动或嗡嗡响现象是舵机停在某个角度时不停抖动,发出嗡嗡声。原因通常是 PWM 信号抖动或者电源纹波太大。解决方法是检查定时器配置有没有被其他中断打断,同时在舵机电源端并一个大电容(比如 1000uF)吸收电流突变。如果是个别舵机抖,可能是该路 PWM 占空比计算时除了零或者整数溢出。5.2 机器人走几步就复位现象是机器人动几下单片机就重启。原因是舵机启动电流把电源电压拉低,导致单片机欠压复位。解决方法是舵机和单片机分开供电,共地但不共电源。如果必须共电源,要选内阻小、电流余量大的电池,并在电源入口加 LC 滤波。5.3 逆运动学算出 NaN现象是机器人突然某条腿抽一下或者完全不动。原因是acosf的参数超出了 [-1,1] 范围。解决方法是像前面代码那样做限幅,同时检查L1、L2和足端坐标是否合理。如果目标点离髋关节太远,超过了大腿加小腿的总长,那这个点本身就是不可达的,应该在解算前先判断距离并截断。5.4 步态节奏忽快忽慢现象是机器人走路时快时慢,像喝醉了。原因是主循环里做了阻塞操作,比如用了delay_ms()而不是时间戳判断。解决方法是把所有延时改成非阻塞的 tick 判断,确保每个控制周期的时间偏差在几毫秒以内。另外串口打印也会拖慢节奏,调试信息要精简或者用 DMA 发送。5.5 烧录后没有任何反应现象是程序烧进去了但舵机不动。先检查 BOOT0 和 BOOT1 跳线是不是在正确位置,再确认晶振有没有起振。如果用的是外部晶振,而板子上没焊或者焊错了,系统时钟初始化就会卡住。可以在SystemInit()里加个 LED 翻转来确认程序有没有跑到主循环。6. 二次开发方向:从能走到走得好看这份源码跑通之后,最值得折腾的方向是让步态更自然。三角步态虽然稳,但看起来比较机械。你可以试着加入波动步态,让六条腿按顺序依次抬起,身体重心跟着前后移动,走起来会像真的虫子。实现思路是把相位表从 8 阶段扩展到 24 阶段,每条腿的摆动窗口错开,同时根据支撑腿的位置动态调整身体高度。另一个方向是加传感器反馈。比如在足端加微动开关或者力传感器,检测哪条腿真正着地,然后动态调整抬腿高度和步幅。这样机器人走到不平地面时不会因为某条腿悬空而失去平衡。代码上就是在主循环里读 GPIO 或者 ADC,把结果映射到lift_height和stride上。还有一个很实用的改造是加蓝牙或无线串口模块,用手机 App 遥控。源码里已经有串口指令解析的框架,你只需要把接收源从 USART1 换成蓝牙模块对应的串口,再定义几个动作指令比如前进、后退、转向,就能用手机操控了。我自己的习惯是每次改完步态参数,都会先用串口把关键数据打印出来,确认逆运动学算出的角度在安全范围内,再让机器人实际跑。有一次偷懒没打印,结果一个步幅参数写大了,机器人直接劈叉,烧了一个舵机。从那以后我每次调步态都强制走一遍“打印角度 → 空载测试 → 带载测试”的流程,再急也不跳过。希望这份拆解能帮你把蜘蛛机器人顺利跑起来,少烧几个舵机。本文还有配套的精品资源,点击获取