简介面向嵌入式初学者、课程设计学生与机电控制实践者的PS2手柄四电机全向轮小车项目源码包基于STM32微控制器实现重点解决游戏手柄输入解析、PWM调速以及四轮全向底盘协同驱动等问题。压缩包共233个文件、约8.28MB包含C源文件与H头文件、O和CRF编译中间文件以及HEX、AXF可执行产物和工程配置能够呈现从程序源码、编译链接到烧录验证的完整组织。已有747人学习。资源中既有底层外设驱动也有用户应用层代码便于定位手柄解析、电机控制和系统初始化之间的关系读者可据此理解PS2通信时序、按键与摇杆数据处理、电机正反转及占空比调节、全向轮平移与旋转运动解算并参考BSP、USER分层和构建脚本安排自己的项目结构。内容适合嵌入式入门、课程设计、竞赛调试及小车平台二次开发时对照学习。1. 一个手柄、一块 STM32、四个轮子这套遥控底盘能干什么桌上放着一台四个直流电机的小车一个 PS2 手柄一块 STM32 最小系统板没有手机 App、没有配网、没有上位机上电拨一下摇杆车就走了。这就是整个项目的全部形态用手柄的左摇杆做前后和转向右摇杆做原地旋转或者云台L1/L2 做速度档位。它适合两类人——刚学完 GPIO 和定时器、想找一个“能动起来”的完整项目练手的以及手上已经有底盘、想找一个稳定遥控方案做二次开发底座的。代码量不大但串行时序、PWM、差速解算、电源干扰这几块能一次性踩全含金量比点灯高得多。2. 硬件链路拆解从 9 针接收器到 STM32 引脚的完整对接这套方案的链路很短手柄2.4G→ 接收器9 针排针→ STM32GPIO 模拟时序→ 驱动芯片 → 四个电机。链路越短出问题时定位越快但也意味着任何一环接错都直接表现为“车不动”所以先把硬件定死。2.1 为什么是 PS2 手柄加 STM32 这套组合做遥控底盘常见的三条路手机蓝牙、WiFi 路由器、专用遥控器。蓝牙要跑协议栈手机端还得写个 App调试的时候一手拿手机一手拿车很别扭WiFi 依赖路由场地一变 IP 就变血泪经验是户外根本连不上。PS2 接收器走的是 2.4G 私有协议接收器直接输出数字时序STM32 只要按位把 9 个字节读上来就行不需要协议栈、不需要配对、不需要知道射频细节上电就能用。代价是这套协议本身很“裸”没有 CRC、没有重传、没有断连通知。接收器被遮挡或者电池电压掉下去STM32 收到的就是一串无效数据或者上一次的旧数据它不会告诉你“我掉线了”。所以软件层面必须自己补一个超时机制这个后面会讲。整个协议占用的 MCU 资源非常小两根推挽输出CMD、CLOCK、一根推挽输出ATT 片选、一根上拉输入DATA再加一个 10ms 的轮询任务。72MHz 的 F103 跑四路 PWM 加 100Hz 手柄轮询CPU 占用连一成都不到这就是它至今还在被大量使用的原因——够用、便宜、不挑板子。2.2 接收器 9 针怎么落到 STM32 引脚上接收器是标准 2.54 排针9 个引脚正反面容易看反接线前先用万用表确认一遍。下面是这套项目里我用得最顺手的一组映射避开了 JTAG 的 PA13/PA14/PA15 和串口方便后期挂调试口。接收器引脚信号名方向接到 STM32说明1DATA输入PA6上拉输入手柄发给 MCU 的数据2CMD输出PA5推挽输出MCU 发给手柄的命令37.5V电源电池正极只给震动马达供电不接也能用4GND地GND必须和主控共地53.3V电源3.3V逻辑供电别接到 5V6ATT输出PA4片选低电平有效一帧的开始和结束7CLOCK输出PA7推挽输出时钟由 MCU 产生8——悬空部分接收器内部保留9ACK输入悬空或 PA3震动反馈用普通遥控可以不接供电这块有个容易被忽略的点接收器有两种供电3.3V 是逻辑电平7.5V 是震动马达。只接 3.3V 时手柄能正常通信但震动功能基本没劲接了 7.5V 之后 DATA/CMD 仍然是 3.3V 电平可以直连 MCU不用电平转换。如果不确定手上模块的内部结构中间串一个 1k 电阻最稳妥即使电平不匹配也不会烧 IO。2.3 软件分层别把时序和业务写在一个文件里新手最容易把所有东西塞进 main.c结果调电机的时候手柄读不出来查半天不知道是谁阻塞了谁。这套项目我按四层拆每层只干一件事出问题时能直接定位到层。Project/ ├── Core/ │ ├── Src/ │ │ ├── main.c # 初始化 主循环调度 │ │ ├── tim.c # TIM1 四路 PWM、TIM3 1ms 时基 │ │ └── gpio.c │ └── Inc/ ├── BSP/ │ ├── bsp_ps2.c # 位操作时序只负责收发 9 字节 │ ├── bsp_ps2.h │ ├── bsp_motor.c # 方向 GPIO CCR 写入 │ └── bsp_motor.h └── APP/ ├── app_remote.c # 报文字节 → 摇杆值 按键位域 ├── app_chassis.c # 差速解算 → 四路目标速度 └── app_config.h # 死区、限幅、最大速度等参数分层的边界很清楚bsp_ps2 完全不知道“摇杆”是什么它只知道“收 9 个字节”app_remote 不知道电机存在它只输出 -1000 到 1000 的摇杆量app_chassis 不知道 PWM 寄存器在哪它只输出四个目标速度。这样换驱动芯片只改 bsp_motor换控制方式只改 app_chassis上层业务一行都不用动。调度上1ms 的定时器中断里跑电机斜坡和超时计数主循环里每 10ms 发一次手柄请求100Hz。注意这里千万别在主循环里加 HAL_Delay一旦哪次延时超过 50ms手柄这边就会丢包表现出来就是“偶尔一顿一顿的”这是最常见的翻车点之一。3. PS2 协议驱动9 字节报文怎么一位一位收上来协议本身不复杂但时序敏感用硬件 SPI 反而不好控制因为 ATT 的时序和 ACK 的处理硬件 SPI 处理不了所以老老实实拿 GPIO 位操作是最稳的做法。3.1 一次完整的 poll 长什么样每次通信固定 9 个字节MCU 先发命令同时把接收器返回的字节读回来。最常用的三条命令是轮询读按键和摇杆、进入配置模式、设置模拟模式。命令字是固定死的抄下来就行用途9 字节命令说明轮询0x01 0x42 0x00 0x00 0x00 0x00 0x00 0x00 0x00回复第 2 字节为 0x41 表示手柄在线进入配置0x01 0x43 0x00 0x01 0x00 0x00 0x00 0x00 0x00必须在设置模式前发设置模拟模式0x01 0x44 0x00 0x01 0x03 0x00 0x00 0x00 0x00开启双摇杆模拟量开启震动0x01 0x4D 0x00 0x00 0x01 0xFF 0xFF 0xFF 0xFF可选的马达映射退出配置0x01 0x43 0x00 0x00 0x5A 0x5A 0x5A 0x5A 0x5A固定 0x5A 结尾回复报文的字节含义也固定第 3 字节是按键低字节Select、L3、R3、Start、上、右、下、左第 4 字节是按键高字节L2、R2、L1、R1、三角、圆圈、叉、方块第 5 到第 8 字节依次是右摇杆 X、右摇杆 Y、左摇杆 X、左摇杆 Y取值范围 0 到 255中值约 128。注意一个细节只有成功进入模拟模式之后第 5 到第 8 字节才有意义。如果初始化序列漏了“进入配置”直接发“设置模拟模式”多数接收器会静默忽略摇杆值一直是 0x00 或者 0xFF表现出来的现象就是“按键能用但摇杆不动”很多人会误以为是手柄坏了。3.2 位操作收发函数逐段拆解核心就三个宏加一个字节收发函数理解了这一段整个驱动就没有黑匣子了。/* bsp_ps2.c —— 引脚操作按实际接线改宏即可 */ #define PS2_ATT_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define PS2_ATT_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define PS2_CLK_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET) #define PS2_CLK_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET) #define PS2_CMD_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET) #define PS2_CMD_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) #define PS2_DAT_RD() HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) /* 收发一个字节低位先出下降沿后采样 * 协议是 LSB First和标准 SPI 的 MSB First 正好相反 * 这一点写错就会得到完全对不上的数据且不会报错 */ static uint8_t ps2_xfer_byte(uint8_t out) { uint8_t in 0; for (uint8_t i 0; i 8; i) { if (out 0x01) PS2_CMD_H(); else PS2_CMD_L(); /* 准备这一位 */ out 1; PS2_CLK_L(); /* 时钟拉低 */ __NOP(); __NOP(); __NOP(); /* 约 1us 建立时间 */ if (PS2_DAT_RD()) in | (uint8_t)(1U i); /* 低电平期间采样 */ PS2_CLK_H(); /* 时钟拉高 */ __NOP(); __NOP(); __NOP(); } return in; } /* 一整帧9 字节ATT 包住 */ void PS2_Transfer(const uint8_t *tx, uint8_t *rx) { PS2_ATT_L(); for (volatile uint8_t d 0; d 30; d) __NOP(); /* 拉低后至少等 10us */ for (uint8_t i 0; i 9; i) rx[i] ps2_xfer_byte(tx[i]); for (volatile uint8_t d 0; d 30; d) __NOP(); PS2_ATT_H(); }这段代码里有三个参数决定了能不能跑通。第一个是时钟频率__NOP()的个数决定了半周期长度F103 上 3 个 NOP 大约是 1us 出头对应 250kHz 左右正好在接收器能接受的范围内NOP 太少比如一个都不加会超过 500kHz接收器直接不响应。第二个是采样边沿必须放在时钟低电平期间读 DATA写在高电平期间读到的会是下一位的数据表现是数据整体错位一位摇杆值永远差一截。第三个是 ATT 拉低后的等待这个 10us 不是可选的接收器内部要在这段时间里准备应答省掉它的话第一次读到的经常是 0xFF。帧与帧之间还要留间隔。PS2_Transfer返回之后到下一次调用中间至少隔 500us太密集接收器会丢帧。主循环 10ms 一次天然满足但如果你在初始化里连发五条命令中间一定要加HAL_Delay(1)。3.3 初始化序列与摇杆值归一化初始化不是发一次就完事要按顺序来而且每一步都要检查返回。/* app_remote.c —— 上电初始化 */ static const uint8_t cmd_poll[9] {0x01,0x42,0,0,0,0,0,0,0}; static const uint8_t cmd_cfg[9] {0x01,0x43,0x00,0x01,0,0,0,0,0}; static const uint8_t cmd_analog[9] {0x01,0x44,0x00,0x01,0x03,0,0,0,0}; static const uint8_t cmd_exit[9] {0x01,0x43,0x00,0x00,0x5A,0x5A,0x5A,0x5A,0x5A}; uint8_t PS2_Init(void) { uint8_t rx[9] {0}; HAL_Delay(200); /* 等接收器上电稳定 */ PS2_Transfer(cmd_poll, rx); if (rx[1] ! 0x41) return 0; /* 第 2 字节不是 0x41说明没找到手柄 */ PS2_Transfer(cmd_cfg, rx); HAL_Delay(2); PS2_Transfer(cmd_analog, rx); HAL_Delay(2); PS2_Transfer(cmd_exit, rx); HAL_Delay(2); PS2_Transfer(cmd_poll, rx); /* 再轮询一次确认模式切换成功 */ return (rx[1] 0x41) ? 1 : 2; }返回值设计成三态是有用的0 表示完全没通信查线2 表示通信正常但模式没切过去查初始化顺序或者手柄是不是原装的1 才是正常。实际调试时这两个失败码能省下大量时间。摇杆归一化要在初始化之后做一次校准。理论中值是 128但手柄用久了会有漂移实测经常在 118 到 138 之间。上电后让手柄静置连续读 20 帧取平均把结果当中值存起来后面所有解算都用这个实测值。/* 读取一帧并归一化输出范围 -1000 ~ 1000 */ int16_t PS2_ReadAxis(uint8_t raw, uint8_t center) { int16_t v (int16_t)raw - (int16_t)center; if (v -12 v 12) return 0; /* 死区防止车自己爬行 */ if (v 0) v (v * 1000) / (255 - center); /* 正向按剩余行程缩放 */ else v (v * 1000) / center; /* 反向按中值行程缩放 */ if (v 1000) v 1000; if (v -1000) v -1000; return v; }这里的缩放分母必须分开算。如果统一除以 127正向只能到 1000 左右反向却会溢出摇杆往一边推到底车快往另一边推到底车慢手感非常怪。死区取 ±12 是实测出来的经验值太小会在中位附近抖动太大转向会有明显滞后。4. 电机与运动学四路 PWM 怎么把摇杆值变成轮速硬件链路走通了接下来是让四个轮子听话。这一块最容易出的问题不是代码而是电源和共地。4.1 驱动芯片选型与四路 PWM 配置四个直流电机常见的两种方案一片 L298N 双路模块需要两片压降大、发热明显TB6612FNG 一片两路同样需要两片但效率高、体积小。这套项目用的是后者每片负责一侧的两个轮子。每片 TB6612 需要五个控制信号PWMA、AIN1、AIN2、PWMB、BIN1、BIN2外加一个 STBY。STBY 是使能脚拉低时芯片完全关断很多新手把这个脚悬空结果 PWM 打得好好的电机就是不转查了半天以为是 PWM 没配好。PWM 用 TIM1 的四路通道CH1 到 CH4两片驱动各占两路。配置上72MHz 主频预分频 PSC 0自动重装 ARR 7199得到 10kHz 的 PWM 频率。这个频率是折中低于 5kHz 电机会有可闻的啸叫高于 20kHz 又会让驱动芯片开关损耗上升、发热加剧。/* bsp_motor.c —— 电机抽象四个实例对应四个轮子 */ typedef struct { GPIO_TypeDef *port; uint16_t pin_a; /* 方向脚 1 */ uint16_t pin_b; /* 方向脚 2 */ TIM_HandleTypeDef *htim; uint32_t ch; /* TIM_CHANNEL_x */ int8_t invert; /* 电机装反了置 -1 省得改线 */ } Motor_t; /* speed 范围 -1000 ~ 1000正为前进 */ void Motor_Set(const Motor_t *m, int16_t speed) { if (speed 1000) speed 1000; if (speed -1000) speed -1000; speed * m-invert; if (speed 0) { HAL_GPIO_WritePin(m-port, m-pin_a, GPIO_PIN_SET); HAL_GPIO_WritePin(m-port, m-pin_b, GPIO_PIN_RESET); } else if (speed 0) { HAL_GPIO_WritePin(m-port, m-pin_a, GPIO_PIN_RESET); HAL_GPIO_WritePin(m-port, m-pin_b, GPIO_PIN_SET); } else { /* 刹车两脚同时拉高比滑行更稳适合坡道停车 */ HAL_GPIO_WritePin(m-port, m-pin_a, GPIO_PIN_SET); HAL_GPIO_WritePin(m-port, m-pin_b, GPIO_PIN_SET); } /* 把 -1000~1000 映射到 0~ARR */ uint32_t ccr (uint32_t)((int32_t)abs(speed) * 7199 / 1000); __HAL_TIM_SET_COMPARE(m-htim, m-ch, ccr); }invert这个字段看着不起眼实际能救命。四个电机装到车架上左右两侧是对称的左边正转对应前进右边正转可能是后退如果靠改接线来纠正拆装几次端子就松了。用软件取反一行搞定。零速时让两个方向脚同时拉高是刹车模式比同时拉低滑行更稳尤其是在有坡的地方松手不会溜车。4.2 差速解算一个摇杆值拆成四个轮子四驱底盘本质上是个滑移转向skid steer结构左右两侧速度差产生转向。解算就两行左轮速度 前进量 转向量右轮速度 前进量 - 转向量。复杂的地方在于两个量各自的范围和限幅。/* app_chassis.c —— 输入归一化后的摇杆值输出四轮目标速度 */ typedef struct { int16_t lf, lr, rf, rr; } Wheel_Target_t; void Chassis_Mix(int16_t fwd, int16_t turn, uint16_t max_speed, Wheel_Target_t *out) { int32_t left (int32_t)fwd turn; int32_t right (int32_t)fwd - turn; /* 等比压缩保证最快的那个轮子不超上限同时保留比例关系 * 直接单边限幅会导致转向时内轮先饱和车会走歪 */ int32_t peak 1000; int32_t m (abs(left) abs(right)) ? abs(left) : abs(right); if (m peak) { left left * peak / m; right right * peak / m; } /* 映射到实际最大速度档位 */ left left * max_speed / 1000; right right * max_speed / 1000; out-lf out-lr (int16_t)left; out-rf out-rr (int16_t)right; }max_speed是速度档位由 L1/L2 按键在 300 到 1000 之间切换这样低速时操控精细高速时能跑起来。等比压缩那一段是整个解算里最关键的三行——如果简单地写成“left 超过 1000 就截断到 1000”前进和转向同时打满的时候内轮先饱和、外轮还在涨车会突然往一边偏手感极差。等比压缩牺牲一点绝对速度换来了转向过程中左右轮速比始终正确。如果底盘是麦克纳姆轮这一段要换成四轮独立解算X 轴、Y 轴、旋转三个量分别投影到四个轮子上公式变成 (fwd ± strafe ± rotate) 的组合。普通四驱不需要别为了看起来高级硬上麦轮麦轮对地面平整度要求高稍微有点坡就打滑。4.3 限幅、死区和斜坡起步三层保护叠在一起才能让车开起来不像是“开关控制”。第一层是死区前面PS2_ReadAxis里已经做了摇杆中位附近输出 0。第二层是电机死区补偿PWM 占空比低于 15% 左右时电机根本不转只会嗡嗡响发热所以非零速度要映射到 150 到 1000 这个区间而不是 0 到 1000。第三层是斜坡目标是让速度指令按固定斜率逼近避免瞬间满占空比导致电流冲击轻则驱动芯片过流保护重则电池电压瞬间跌落把 MCU 拉复位。/* 1ms 定时器中断里调用把当前速度平滑推到目标速度 */ void Motor_Slew(Motor_t *m, int16_t target, int16_t step) { static int16_t cur[4] {0}; /* cur 按索引区分四个电机这里省略索引只写逻辑 */ int16_t c cur[0]; if (c target) { c step; if (c target) c target; } else if (c target) { c - step; if (c target) c target; } cur[0] c; Motor_Set(m, c); }step取 20 意味着 1ms 变化 20从 0 到满速需要 50ms起步有明显缓冲但不拖沓。这个值调太小比如 5会感觉车“肉”调太大比如 100就退化成阶跃等于没做。另外要注意cur数组必须用静态变量或者全局变量放在函数局部非 static 里每帧都会清零斜坡就永远爬不上去这个坑我在别的项目里踩过一次。5. 常见问题排查调了三天小车还在原地打转的 5 个坑这一节是按“现象 → 原因 → 解决”整理的实战记录基本上新手能遇到的都在里面了。5.1 接收器灯亮着但代码读回来全是 0xFF现象接收器上电后指示灯常亮或者闪烁说明供电没问题但PS2_Init一直返回 0rx[1]永远是 0xFF 或者 0x00。原因通常有三个。一是 DATA 引脚没配上拉接收器输出是开漏结构MCU 端不加上拉读到的永远是高电平。二是 CMD 和 DATA 接反了这两根线在排针上相邻插错很方便。三是时钟太快NOP 个数不够接收器跟不上直接不响应。解决先示波器或者逻辑分析仪看 CLOCK 和 ATT确认有波形再把 PA6 配成GPIO_PULLUP最后把ps2_xfer_byte里的 NOP 加到 5 个以上降速验证。如果降到 100kHz 能通说明是时序太快慢慢往上加找到临界点再留一倍余量。5.2 按键能读摇杆一直是 0 或者一直是 255现象按叉、圆圈都有反应但摇杆四个字节纹丝不动。原因初始化序列里漏了“设置模拟模式”或者顺序错了。手柄默认是数字模式只上传按键状态模拟量字段填的是固定值。另外还有一种可能是买到了只支持数字模式的兼容手柄。解决严格按“轮询 → 进入配置 → 设置模拟模式 → 退出配置 → 再轮询”的顺序走每一步之间加 2ms 延时。如果还是不行的把rx[1]和rx[2]打出来看正常情况下rx[1]应该是 0x41、rx[2]应该是 0x5A不是的话说明命令根本没被接受。5.3 PWM 有输出电机就是不转或者一抖一抖现象用示波器量 PWMA 有正常方波占空比也在变但电机静止或者发出“哒哒”声轻微抖动。原因最常见的是 TB6612 的 STBY 脚没拉高芯片处于待机状态PWM 进得去出不来。其次是电机和主控没共地或者驱动芯片的逻辑电源和电机电源接反。第三种是电池功率不够四个电机同时启动瞬间电流能到 5A 以上用 9V 层叠电池或者小容量锂电池会直接跌压。解决先量 STBY 是不是 3.3V再把万用表打到通断档确认主控 GND 和驱动 GND 是通的最后换一个能持续输出 2A 以上的电源同时在电机两端各并一个 0.1uF 陶瓷电容抑制火花干扰。这个电容很关键不加的话电机一转 MCU 就复位看起来像程序跑飞。5.4 车能走但走不直松手还会缓慢爬行现象摇杆回中车还在动全速前进时车往一边偏。原因摇杆中值不是理论上的 128实测偏差能到 ±10四个电机的内阻和减速箱阻力也不一致同样占空比转速不同。解决上电时做一次静置校准把 20 帧的平均值存成 center这个方法前面代码里已经写了。电机差异靠软件补偿在app_config.h里给每个轮子加一个 0.9 到 1.1 的系数实测哪个轮子快就往下调。另外确认死区没被注释掉很多人调试的时候为了看效果把死区去了忘了加回来。5.5 跑着跑着突然一顿或者干脆失控现象正常行驶中偶尔卡顿严重时车会保持上一次的动作一直冲。原因手柄数据没有超时保护。接收器被遮挡、电池电量低、或者主循环里被某个阻塞操作拖住都会导致读上来的数据和上一次一样程序以为你还在推摇杆。另外如果主循环里有HAL_Delay(100)之类的操作100Hz 的轮询就变成了 10Hz手感立刻变差。解决加一个看门狗计数器每成功收到一帧就清零主循环里超过 200ms 没清就强制把四路速度设成 0。这个后悔药必须提前吃不能等撞了墙再加。同时把主循环里所有超过 5ms 的延时都去掉需要延时的活挪到定时器里去。6. 二次开发把遥控玩具改成能跑闭环的底盘平台遥控跑通只是起点这套代码真正的价值在于分层之后每一层都能单独替换。想把遥控车升级成能跑算法的底盘按下面的顺序改最省事。第一步是给四个轮子加编码器把Motor_Set换成Motor_SetWithFeedback用定时器的编码器模式读脉冲算出实际转速。这一步做完app_chassis输出的就不再是占空比而是目标转速中间插一个 PI 控制器。参数上先只加 P从 0.5 开始往上试看到速度有轻微振荡就回退一半I 用来消稳态误差取 P 的十分之一左右太大一定会积分饱和。第二步是把遥控输入的来源抽象掉。现在app_chassis的输入是摇杆值改成从一个Control_Input_t结构读遥控、串口上位机、自动巡航任务都往这个结构里写底盘层完全不用改。这个抽象做早了没意义做晚了到处是 if-else我一般会在第二次接新输入源的时候就抽出来。第三步是加状态机。手动遥控、原地旋转、定速巡航、低速跟线这几种模式用一个enum管起来切换时统一走一个Chassis_Stop()清速度避免模式切换瞬间速度突变。改造项改动位置工作量注意点编码器闭环bsp_motor 新增app_chassis 改输出半天编码器线要远离电机线走上位机调试新增 app_uart10Hz 上报2 小时别在中断里 printf电池电压监测ADC 低压降速1 小时分压电阻别用太大阻值手柄断连保护app_remote 加超时计数20 分钟超时后必须清速度验证方法很简单把车架起来让四个轮子悬空全速跑十分钟摸驱动芯片温度、看电流表读数、观察有没有偶发复位。落地测试之前先做这一步能提前发现九成的电源问题。我现在的习惯是每加一个新模块先不接电机让Motor_Set里只打印不写寄存器用串口把四个轮子的目标速度打出来看波形对不对确认解算没问题再上电。这套流程走下来调车时间从原来的两三天压缩到半天。希朝这套拆解能帮你少走几趟弯路。本文还有配套的精品资源点击获取