☰
STM32F103六足机器人控制:舵机PWM、逆运动学与三角步态
2026/10/26 10:52:59 网站建设 项目流程

简介:面向嵌入式与机器人控制学习者的蜘蛛机器人STM32F103控制程序源码,是一套可直接上手研读的完整工程实例,适合已掌握C语言与单片机基础、希望贯通芯片外设、驱动程序与控制算法链路的中级入门开发者。压缩包共188个文件、约1.6MB,以h头文件、c源文件、o目标文件、d依赖文件与crf编译索引为主;还保留了uv2工程文件、sct分散加载脚本、map映射表与axf可执行文件,源码与编译产物一一对应,便于对照分析工程组织方式。目前已有1386人学习。工程内容围绕GPIO驱动电机、ADC采集陀螺仪与加速度计数据、定时器输出PWM调速展开,配合中断处理、PID闭环调节与步进电机步序计算,并涉及实时操作系统的多任务调度、无线通信协议栈、传感器融合避障、电池电量监测与故障自恢复等模块,可作为蜘蛛机器人步态调试、控制参数整定与嵌入式项目复现的参考底稿。

1. 六足平台从“能动”到“走得稳”,差的是这套控制层

第一次给六足平台通电,十八个舵机同时回中位,桌面上那台蜘蛛机器人猛地抽了一下就趴着不动了,电源灯还亮着,单片机却已经悄悄复位。这类翻车在 stm32f 系列的多足项目里太常见了:机械和电源先被怀疑,控制程序反倒最后才查。这份基于 STM32F103 的蜘蛛机器人控制程序源码,把控制层该干的四件事拆得比较清楚:定时器输出多路 50Hz 舵机 PWM、三关节腿的逆运动学解算、三角步态相位调度、串口在线调参。适合已经会点灯但一上多舵机就一头雾水的开发者,也适合拿它当多足算法练手底座的熟手。

2. 工程结构与硬件拓扑:先看清 18 路 PWM 到底从哪来

拿到一份控制程序,别急着编译下载。六足项目最常见的返工点是“代码没问题,但引脚和定时器对不上”。先花十分钟把目录和硬件拓扑理清,后面调参能省掉一多半的来回。

2.1 目录树里每个文件夹负责什么

标准库风格的工程结构大体如下,不同人的命名习惯会有出入,但分层逻辑基本一致:

SpiderBot_F103/ ├── CMSIS/ # 内核头文件、system_stm32f10x.c、启动汇编 ├── FWLIB/ # 标准外设库 src/inc,按需裁剪 ├── USER/ │ ├── main.c # 外设初始化顺序 + 主循环调度 │ ├── stm32f10x_it.c # 5ms 节拍中断入口 │ └── stm32f10x_conf.h # 库裁剪开关 ├── HARDWARE/ │ ├── pwm.c / pwm.h # TIM2/TIM3/TIM4 多通道 PWM │ ├── pca9685.c / .h # I2C 扩展 16 路,补齐到 18 路 │ ├── usart.c / .h # 串口收发与环形缓冲 │ └── delay.c / .h # SysTick 延时 ├── ALGORITHM/ │ ├── ik.c / ik.h # 单腿逆运动学 │ └── gait.c / gait.h # 步态相位生成 ├── APP/ │ ├── servo.c / .h # 角度限幅、零点校准、平滑插值 │ └── cmd.c / .h # 串口命令行解析 └── DOC/ # 接线表、舵机编号图

这个分层的价值在于:ALGORITHM目录里全是纯数学,不碰任何寄存器,可以直接拿到 PC 上用 gcc 编译单测;HARDWARE只负责把角度值变成脉宽,不关心步态;APP是黏合层。改算法的时候不用动硬件代码,换硬件的时候算法也不用重写,这在后面加 IMU 闭环时特别省事。我一般建议新手先只编译ALGORITHM+APP两层,用 printf 打印关节角,确认数学对了再上电,能避开大量“到底是算法错还是接线错”的玄学排查。

2.2 定时器与舵机通道的分配表

STM32F103 的通用定时器 TIM2/3/4 各有 4 个通道,高级定时器 TIM1 也有 4 个,加起来 16 路硬件 PWM。六足每腿 3 关节正好 18 路,差 2 路,所以常见的做法是 12 路走片内定时器、剩下 6 路走一片 I2C 的 PCA9685,或者干脆全部走 PCA9685 把引脚省下来。源码里两种都留了接口,按需开关。

定时器总线通道引脚驱动的舵机编号
TIM2APB1CH1~CH4PA0~PA3右前腿 髋/股/胫 + 右中腿髋
TIM3APB1CH1~CH4PA6/PA7/PB0/PB1右中腿 股/胫 + 右后腿 髋/股
TIM4APB1CH1~CH4PB6~PB9右后腿胫 + 左后腿 髋/股/胫
PCA9685I2C1CH0~CH5PB10/PB11左中腿 3 路 + 左前腿 3 路

这里有个容易忽略的细节:TIM2/3/4 挂在 APB1 上,APB1 的时钟是 36MHz,但定时器时钟有倍频规则,实际计数时钟仍然是 72MHz。很多人按 36MHz 去算分频,结果输出频率差一倍,舵机就在那儿嗡嗡叫却不转,这类“看起来代码没错”的问题最耗时间。

2.3 上电初始化顺序:为什么不能一次全开

初始化顺序直接决定第一次上电是稳稳站住还是抽一下趴下。十八个舵机同时从自由状态拧到中位,瞬间电流能到十几安,如果电源余量不够,MCU 的 3.3V 会被拉塌,表现就是复位。

int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); USART1_Init(115200); /* 先开串口,方便输出启动日志 */ Servo_Init(); /* 只清角度数组和校准表,不输出 PWM */ PCA9685_Init(0x40, 50); /* 片外扩展芯片,50Hz */ Gait_Init(); /* 相位计数器归零 */ Delay_ms(800); /* 等舵机电源建立、电容充电完成 */ Servo_EnableAll(); /* 统一把目标角写进比较寄存器 */ TIM_Cmd(TIM2, ENABLE); TIM_Cmd(TIM3, ENABLE); TIM_Cmd(TIM4, ENABLE); while (1) { App_Loop(); /* 命令解析、状态机、安全限幅 */ } }

这段的关键在Delay_ms(800)和Servo_EnableAll()的先后。先把目标角全部算好写进影子数组,再一次性使能,舵机是“同时缓慢靠近”而不是“逐个跳变”。如果逐路使能,先动的那几个会把电流峰值叠加到还没动的路上,电源更容易塌。PCA9685_Init的第二个参数是输出频率,单位 Hz,写 50 就是标准模拟舵机的 20ms 周期。

3. 舵机驱动层:50Hz PWM、脉宽映射与角度校准

驱动层看起来最简单,实际上舵机抖、行程不够、角度反向这些“机械味”的问题,八成出在这一层。

3.1 定时器算出 50Hz 的完整过程

以 TIM2 为例,目标周期 20ms,计数时钟 72MHz。分频 72 得到 1MHz,也就是 1 个计数等于 1 微秒,那么周期寄存器写 20000 就是 20ms。

void Servo_PWM_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); /* PA0~PA3 复用推挽输出,接 TIM2_CH1~CH4 */ gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); /* 72MHz / 72 = 1MHz,1 计数 = 1us */ tb.TIM_Period = 19999; /* 20000us = 20ms */ tb.TIM_Prescaler = 71; tb.TIM_ClockDivision = TIM_CKD_DIV1; tb.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &tb); TIM_OCStructInit(&oc); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 1500; /* 中位 1.5ms,先给安全值 */ oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &oc); TIM_OC2Init(TIM2, &oc); TIM_OC3Init(TIM2, &oc); TIM_OC4Init(TIM2, &oc); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); }

TIM_Prescaler = 71是“预分频值”,实际分频系数是 72,别写成 72。TIM_Pulse就是比较值 CCR,单位是计数值,在这里等于微秒数。Preload_Enable让新写入的 CCR 在下一个周期生效,避免在一个 PWM 周期中途改值把波形撕裂,舵机对这种半截脉冲很敏感。

3.2 角度到脉宽的映射与零点校准

标准模拟舵机的行程是 0.5ms 到 2.5ms 对应 0 到 180 度。但每颗舵机的机械零点和端点都有偏差,尤其是廉价数字舵机,左右能差出 3 到 5 度。硬编码 500~2500 会让整机站着歪,校准表就是必须的。

/* 单颗舵机的校准参数:机械零点偏移 + 两端限幅 */ typedef struct { int16_t offset; /* 装配误差补偿,单位 0.1 度 */ int16_t minAngle; /* 该关节允许的最小角 */ int16_t maxAngle; /* 该关节允许的最大角 */ uint8_t reverse; /* 左右腿镜像安装时的方向翻转 */ } ServoCalib; static ServoCalib s_calib[SERVO_NUM]; /* 角度 -> 比较值(us),带限幅和方向处理 */ uint16_t AngleToPulse(uint8_t id, float angle) { const ServoCalib *c = &s_calib[id]; int16_t a; if (c->reverse) angle = 180.0f - angle; a = (int16_t)(angle * 10.0f) + c->offset; /* 折算到 0.1 度 */ if (a < c->minAngle * 10) a = c->minAngle * 10; if (a > c->maxAngle * 10) a = c->maxAngle * 10; /* 0.1 度 -> 微秒:0 度 = 500us,180 度 = 2500us */ return (uint16_t)(500 + (int32_t)a * 2000 / 1800); }

offset用 0.1 度为单位存整数,是为了避免在中断里做浮点。Cortex-M3 没有硬件 FPU,一次 float 乘除是几百个周期的软浮点调用,放在 5ms 节拍里很容易把周期拖长。reverse处理的是左右腿镜像安装的问题:同一侧腿的髋关节方向相反,不翻转的话左腿会往右摆。minAngle/maxAngle是保护,写在代码里比贴在机架上靠谱,我见过有人调试时忘了限幅,一帧错误指令把胫节舵机顶到机械死点,齿轮直接扫齿。

校准的做法是:上电后通过串口把某一路设成 90 度,肉眼观察腿是否垂直,不垂直就改offset,正负都试,直到角度表现正常。这一步用不了二十分钟,但能省掉后面“步态看起来怪但不知道怪在哪”的大量时间。

3.3 平滑插值:别让目标角一步跳到位

逆运动学算出 30 度,上一帧还是 120 度,如果直接写进 CCR,舵机会以最大速度冲过去,机械结构跟着抖,脚底还会打滑。中间加一层斜率限制就能压住。

/* 每 5ms 调用一次,把 current 向 target 逼近,步长受 maxStep 约束 */ void Servo_Slew(uint8_t id, float target, float maxStep) { float cur = s_current[id]; float d = target - cur; if (d > maxStep) d = maxStep; if (d < -maxStep) d = -maxStep; s_current[id] = cur + d; } /* 节拍中断里统一刷新 */ void Servo_UpdateAll(void) { uint8_t i; for (i = 0; i < SERVO_NUM; i++) { Servo_Slew(i, s_target[i], 2.0f); /* 每 5ms 最多 2 度 */ Servo_SetPulse(i, AngleToPulse(i, s_current[i])); } }

maxStep取 2.0 度时,180 度满行程大约 450ms 走完,肉眼看起来是“匀速转过去”而不是“弹过去”。这个值跟舵机型号、负载、腿长都有关系,重载可以降到 1.2,轻载可以提到 3.5。注意插值要放在节拍中断或主循环里按固定周期调,跟着while(1)跑得快慢不定的话,插值速度会随代码量变化,表现出来就是“程序改一行,机器人走路速度变了”,这种问题查起来非常难受。

4. 逆运动学与三角步态:一条腿怎么走,六条腿怎么配合

这一层是整个项目的价值所在。硬件层谁都能抄,步态和逆解才是决定这台蜘蛛机器人是“能爬”还是“走得稳”的地方。

4.1 单腿三关节的几何模型

每条腿三个关节:髋关节负责水平面摆动,股节(femur)和胫节(tibia)在竖直平面内构成两连杆。给定足端相对髋关节的坐标 (x, y, z),求三个关节角。

符号含义典型取值
L1股节长度60mm
L2胫节长度90mm
x, y足端在水平面的投影随步态变化
z足端相对髋关节的竖直高度(向下为负)-70mm 左右
q1髋关节水平摆角由 atan2(y, x) 得到
q2股节相对水平面的角度逆解得到
q3胫节相对股节的夹角逆解得到

髋关节的解是纯平面几何,q1 = atan2(y, x)。剩下两连杆在“髋到足端”的斜面上解,先算斜距 d,再用余弦定理求膝关节内角,最后换算成相对水平面的角度。常见做法是先把 d 和 L1、L2 的关系判掉,超出可达域就直接返回错误,而不是算出一堆 NaN 塞给舵机。

4.2 逆解代码与可达域判断

#define L1 60.0f #define L2 90.0f typedef enum { IK_OK = 0, IK_OUT_OF_REACH, /* 目标点太远 */ IK_TOO_CLOSE, /* 目标点太近,膝关节收死 */ IK_DEGENERATE /* 足端落在髋轴正下方 */ } IkResult; IkResult Leg_IK(float x, float y, float z, float *q1, float *q2, float *q3) { float r = sqrtf(x * x + y * y); float d, cosK, knee, alpha, beta; if (r < 1e-3f) return IK_DEGENERATE; *q1 = atan2f(y, x); /* 水平面摆角 */ d = sqrtf(r * r + z * z); /* 髋到足端直线距离 */ if (d > (L1 + L2) * 0.98f) return IK_OUT_OF_REACH; if (d < fabsf(L1 - L2) + 1.0f) return IK_TOO_CLOSE; /* 余弦定理:膝关节内角 */ cosK = (L1 * L1 + L2 * L2 - d * d) / (2.0f * L1 * L2); if (cosK > 1.0f) cosK = 1.0f; if (cosK < -1.0f) cosK = -1.0f; knee = acosf(cosK); /* 股节与髋-足连线的夹角 */ cosK = (L1 * L1 + d * d - L2 * L2) / (2.0f * L1 * d); if (cosK > 1.0f) cosK = 1.0f; if (cosK < -1.0f) cosK = -1.0f; alpha = acosf(cosK); beta = atan2f(-z, r); /* 连线相对水平面 */ *q2 = beta + alpha; /* 股节角 */ *q3 = knee; /* 膝关节夹角 */ return IK_OK; }

几个必须讲清的点。d > (L1 + L2) * 0.98f里的 0.98 是留余量,正好等于腿长时限位会让舵机顶死,留 2% 安全余量是血泪经验。beta = atan2f(-z, r)取负号是因为 z 向下为负,而几何角以水平向上为正。q2 = beta + alpha和q3 = knee的符号约定跟你的舵机安装方向强相关,第一版一定是错的,别怀疑代码,先把一条腿拆下来单独测:给 q2 加 10 度看腿是抬起还是落下,反了就把符号翻过来,然后把这个约定记在servo.c的注释里。

IK_TOO_CLOSE这个返回值很多人不写,结果足端贴近身体时acosf的参数超出 [-1,1],出来的是 NaN,NaN 一路传到 CCR 变成 0,舵机直接打死到一端。加上判断和钳位之后,这类底层问题就绝迹了。

4.3 三角步态的相位表

六足最常用的是三角步态:把六条腿分成两组,每组三条构成一个稳定三角形,一组支撑、另一组摆动,交替进行。腿编号按“右前=0、右中=1、右后=2、左后=3、左中=4、左前=5”顺时针排。

/* 三角步态分组:A 组 = 右前、右后、左中;B 组 = 右中、左后、左前 */ static const uint8_t kGroup[6] = { 0, 1, 0, 1, 0, 1 }; #define GAIT_PERIOD 200 /* 一个完整步态周期 = 200 * 5ms = 1s */ static uint16_t s_phase; /* 每 5ms 调用一次 */ void Gait_Tick(void) { uint8_t i; float t; s_phase = (s_phase + 1) % GAIT_PERIOD; for (i = 0; i < 6; i++) { uint16_t ph = kGroup[i] == 0 ? s_phase : (s_phase + GAIT_PERIOD / 2) % GAIT_PERIOD; t = (float)ph / GAIT_PERIOD; /* 0~1 的相位 */ if (t < 0.5f) { /* 支撑相:足端向后平移,速度与机身前进一致 */ Gait_StancePhase(i, t * 2.0f); } else { /* 摆动相:足端抬起、前移、落下 */ Gait_SwingPhase(i, (t - 0.5f) * 2.0f); } } Leg_ApplyAll(); /* IK 解算 + 写目标角 */ }

kGroup这个表定死了哪三条腿同相。写成{0,1,0,1,0,1}是 A 组三腿交替,如果你按物理位置装反了编号,走起来就会像抽搐。判断方法很简单:切到调试模式,把s_phase停在 0 附近,看抬起来的腿是不是恰好构成一个三角形,如果不是,那就说明编号表错了。

支撑相和摆动相的比例是 50:50,这个比例决定了速度上限。想要更稳,可以改成 60:40,支撑时间更长、摆动更快,机身晃动明显减少,代价是最大速度下降。这个参数在源码里是宏,改一个数就能对比,不用改结构。

4.4 节拍中断里的调度顺序

时序错乱是步态调试里最隐蔽的问题。正确的顺序是:先产生相位,再算足端坐标,再逆解,再写 PWM;而不是先写 PWM 再算。

void TIM3_IRQHandler(void) /* 5ms 节拍 */ { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); Cmd_Poll(); /* 先吃掉串口命令,调参立即生效 */ Gait_Tick(); /* 相位推进 + 足端坐标 + 逆解 */ Servo_UpdateAll(); /* 插值 + 写比较寄存器 */ s_tickCount++; if (s_tickCount % 100 == 0) { /* 每 500ms 报一次状态 */ Telemetry_Send(); } } }

Cmd_Poll()放在最前面,是为了让串口调参的响应落在一帧之内。Telemetry_Send()每 500ms 发一次遥测,不要每个节拍都发,115200 波特率下一帧十几字节还能承受,但接收端解析跟不上时缓冲区会溢出,表现出来就是“调参调着调着程序就不响应了”。

5. 避坑与排查:上电复位、舵机抖动、步态一瘸一拐

这一章全是踩过的坑,按“现象 → 原因 → 解决”记,遇到同类问题可以直接对着查。

5.1 电气与电源相关的三条

现象:一上电 MCU 反复复位,串口打印到一半就重启。原因:舵机电源和 MCU 共用一路 5V,或者虽然分开供电但没有共地。十八个舵机静止时也要吃几百毫安,启动瞬间十几个安培的浪涌把 3.3V 稳压拉塌,触发欠压复位。 解决:舵机单独一路降压,线径至少 1.5 平方;两路电源的负极必须接在一起,不共地的话 PWM 信号没有参考电平,舵机会乱动;MCU 侧并 1000uF 电解加 0.1uF 陶瓷,靠近芯片引脚放。改完先只让三个舵机动,逐个加上去,看电压跌到多少。

现象:舵机不转,只有持续的高频嗡嗡声。原因:PWM 频率不是 50Hz。最常见是 APB1 定时器时钟按 36MHz 算分频,导致实际输出 25Hz 或 100Hz。 解决:用示波器或逻辑分析仪量 PA0,看周期是不是 20ms。没有仪器就把分频按 72MHz 改回来,即Prescaler = 71、Period = 19999,两个数一起看。

现象:每颗舵机单独测都正常,装到机器人上就有几个发热严重。原因:机械死点顶住了。腿的行程在装配后比舵机标称行程小,角度限幅写的是 0~180,实际机械只允许 15~160。 解决:把minAngle/maxAngle收窄到机械允许范围,宁可牺牲行程也别顶死;同时把电流限制或者保险丝加上,保护一次能省一个舵机。

5.2 控制与算法相关的两条

现象:走路时腿一瘸一拐,某一条腿明显抬得低或者落得慢。原因:舵机零点没校准,或者逆解用的长度参数跟实际机械不符。另外要检查这条腿的reverse是不是跟其他腿一致。 解决:先做单腿标定,把足端固定在 (0, 60, -70),六条腿统一解一次,看是否所有腿末端都落在大致相同的相对位置;不一致就改offset和 L1/L2。别一上来就调步态参数,机械没对齐的时候调参数只会越调越乱。

现象:串口发一条命令,机器人有时候响应,有时候像没收到。原因:没有帧头帧尾也没有校验,命令和遥测数据混在同一个接收缓冲里,解析错位。 解决:命令加$开头、#结尾,中间带一个异或校验;接收用环形缓冲 + 状态机,中断里只搬运字节,解析放主循环。这个改动不到五十行,但能把“调参调到怀疑人生”的情况基本消灭。

提示:每次上电前先量一遍电池电压和 3.3V 轨,两个数字正常再按电源开关。这个动作看起来多余,但能挡掉一半的“代码明明没改为什么今天不行”。

6. 二次开发:把固定步态改成可调参数表

这套代码最值得动手改的地方,是把写死在宏里的步态参数抽成一张运行时可改的表。原版的GAIT_PERIOD、抬腿高度、支撑比例都是编译期常量,改一次要重新烧录,调参效率极低。

typedef struct { uint16_t period; /* 步态周期,单位 5ms 拍 */ float liftHeight; /* 摆动相抬腿高度,mm */ float stride; /* 单步前后行程,mm */ float stanceRatio; /* 支撑相占比,0.5~0.7 */ float maxStep; /* 每拍最大角度变化,度 */ } GaitParam; GaitParam g_gait = { 200, 25.0f, 40.0f, 0.55f, 2.0f }; /* 串口命令:$GAIT,period,lift,stride,ratio,maxstep# */ void Cmd_SetGait(const char *args) { GaitParam p; if (sscanf(args, "%hu,%f,%f,%f,%f", &p.period, &p.liftHeight, &p.stride, &p.stanceRatio, &p.maxStep) == 5) { if (p.period < 80 || p.period > 600) return; /* 防呆 */ if (p.stanceRatio < 0.45f || p.stanceRatio > 0.75f) return; g_gait = p; } }

改完之后,电脑上开个串口助手就能实时对比不同步态,不用反复拔线烧录。注意sscanf里%hu对应uint16_t,写成%d在部分编译器上会写坏相邻内存,这类问题查起来特别费时间。

第二个值得加的是姿态反馈。板子上挂一颗 MPU6050,读俯仰和横滚角,机身向前倾就整体降低前腿的 z、抬高后腿的 z,形成最简单的平衡补偿。接入点在Leg_ApplyAll()之前,把六条腿的足端 z 加上一个与倾角成正比的偏移,代码不到二十行,但走斜坡时的稳度提升非常明显。做法上建议先用串口把倾角打出来,确认滤波后的数据不抖再加进控制环,直接闭环很容易因为噪声把机身带得来回晃。

第三个方向是换成查表逆解。Cortex-M3 跑浮点三角函数开销不小,六条腿每拍六次acosf,在 5ms 节拍里占比不低。可以把 q2、q3 按 (d, beta) 做成 64×64 的查找表,上电时算一次放进 Flash,运行时双线性插值,精度损失零点几度,但节拍余量一下子就出来了。我一般会先用示波器量节拍中断的执行时间,超过 2ms 才考虑换查表,不然纯属提前优化。

从那以后我每次拿到多足类的新代码,都会先只使能三条腿、把电源另接一路、拿串口把每个关节的实际角度打出来对一遍,确认机械和数学都对齐了再上整机。这个习惯让我少烧过好几个舵机,也少走过很多“改代码改到天亮其实是接线松了”的弯路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询