ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PS2手柄+STM32四电机麦克纳姆轮小车:从接线到PID调试全解析

PS2手柄+STM32四电机麦克纳姆轮小车:从接线到PID调试全解析 简介这是一套基于STM32RCT6微控制器、利用PS2手柄控制四轮全向轮小车的完整工程资源。项目从手柄信号解析、电机PWM调速到全向轮运动逻辑均有详细代码实现适合学习STM32库函数开发、嵌入式电机控制及无线遥控小车的开发者参考。压缩包共233个文件包含大量C源文件、H头文件及编译生成的O、HEX、AXF等文件同时提供BSP板级支持包、USER应用层代码、Keil工程构建脚本与README说明便于直接查阅工程结构和二次开发。包体约8.28MB小巧完整。目前已有744人学习下载项目涉及PS2通信协议、全向轮运动学、定时器PWM输出等关键知识点代码注释清晰目录组织规范可作为课程设计、毕业设计或机器人入门实战的良好范例。 做四电机小车这个项目最难的不是“让电机转起来”而是“让四个电机配合得像一个整体”。我早期调试这么一套底盘时每次改完参数都要弯腰去按车上的复位键再跑一遍流程观察动作效率低到让人抓狂。后来把 PS2 手柄接上去人坐在电脑前遥控小车跑几圈很多运动学问题和 PID 参数问题一下就暴露出来了调试效率翻倍。这也是到今天为止PS2 手柄加 4 电机小车依然是电赛、工创赛、毕设和各类课程设计里最常用组合的原因。这篇文章就把整个链路拆开来讲从“4 电机到底该怎么理解”开始到硬件接线、PS2 协议解析、麦克纳姆轮运动学解算、PID 速度闭环最后落到联调阶段的真实排错过程。无论你用的是 STM32F103C8T6 还是 ZET6是直接做全向底盘还是普通四驱差速内容都能直接参考。1. 选型之前先想清楚你的“4电机”到底是哪种布局很多朋友一看到“4 电机小车”就直接进入硬件采购环节结果轮子买回来才发现控制策略完全搞错了方向。实际上“4 电机”至少对应三种完全不同的底盘结构代码实现难度也是天差地别。第一种是普通四驱车四个轮子各带一个电机但左右两侧的电机成组控制。这种底盘本质上是两路独立驱动转向靠左右轮的转速差完成。控制代码只需要两路 PWM、两路方向引脚运动学上没有任何需要解算的东西。第二种是履带式底盘看起来是四个电机实际上左侧两个并联、右侧两个并联同样退化成两路控制。第三种就是麦克纳姆轮全向底盘四个轮子必须独立控制转速和方向通过四个轮子速度矢量的组合让整车实现前后、横移、斜向和原地旋转。这才是真正意义上“需要 4 路独立控制”的 4 电机小车。我的建议是如果你做的是搬运、循迹、避障这类场景普通四驱加差速转向完全够用没必要给自己增加运动学解算的负担。但如果场地狭窄、要求全向移动比如物流小车、竞赛中需要侧向入位、原地掉头麦克纳姆轮就是更合适的选择。这篇文章后面的内容都基于麦克纳姆轮方案展开普通四驱的读者可以跳过第 4 章其他章节基本相同。整套系统的选型我给出一个经过验证的组合主控用 STM32F103 系列手柄用 PS2 无线手柄加接收模块电机选带霍尔编码器的直流减速电机比如 JGB37-520、MG310 这类驱动芯片用两片 TB6612每片带两个电机。这套组合成本不高、资料多、稳定性也足够是当前 DIY 和竞赛场景里最不容易踩坑的一套配置。2. 硬件接线与供电设计PS2接收模块、TB6612和4路电机的连接细节2.1 PS2 接收模块的六根线PS2 接收模块是整套系统里很多人第一次见到就懵的东西模块上引出的引脚一般是 VCC、GND、DAT、CMD、CS、CLK。它本质上是 PS2 手柄的扩展接收器把无线手柄的数据用类似 SPI 的协议和单片机通信而不是直接输出模拟电压。接线分两组来记忆DAT、CMD、CS、CLK 这四根是通信线对应软件模拟 SPI 的 MISO、MOSI、NSS、SCKVCC 和 GND 是电源。我用 F103C8T6 时的接法是 DAT 接 PA6、CMD 接 PA7、CS 接 PA4、CLK 接 PA5也就是把 PS2 接收模块挂到 SPI1 的默认引脚上。如果你想让通信线更灵活完全可以用软件模拟时序接任意 GPIO这样不会和其他外设冲突。模块供电这里必须单独强调PS2 接收模块多数是 3.3V 供电接 5V 很容易烧模块。上电之前先看模块丝印是 3.3V 就老老实实接 3.3V。同时一定要和单片机共地也就是模块的 GND 和主板 GND 连在一起否则通信电平根本没有参考基准表现就是数据时好时坏。2.2 TB6612 和 4 路电机的接线TB6612 比第一代常用的 L298N 好用太多体积小、效率高、发热低。一片 TB6612 有两个通道每个通道控制一个电机所以 4 个电机正好用两片。关于 TB6612 的引脚重点记这几个VM 接电池正极VCC 接 3.3V 或 5V 逻辑电源STBY 必须接高电平否则芯片完全不工作。每个通道有 AIN1、AIN2、PWMA 三个控制脚AIN1/AIN2 决定方向PWMA 决定速度。比如 AIN11、AIN20 时电机正转反过来就是反转PWMA 上的 PWM 占空比决定有效电压的大小。输出端 A01、A02 直接接电机两根线如果发现电机转向和预期相反把这两根线对调即可。PWM 频率我建议设置在 10kHz 到 20kHz 之间这个范围既避开了听觉敏感区也适合 TB6612 这类芯片的开关特性。4 路 PWM 在 F103 上可以用定时器的四个通道输出比如 TIM2 的 CH1 到 CH4方向引脚随便找四个 GPIO 就行。如果你用的是不带编码器的电机到此接线就结束了如果带编码器每个电机还要接 A、B 两相到单片机的定时器输入引脚。2.3 电源分配是硬件里最容易埋雷的地方四电机小车的电源问题比想象中严重得多。四个电机同时启动的瞬间电流可以轻松达到 2A 甚至更高如果电池电压直接被压到单片机复位电压以下就会出现一推摇杆就重启的诡异现象。我的做法是分三路供电电池正极直接进 TB6612 的 VM负责电机供电电池同时经过 5V 稳压模块给逻辑电路供电3.3V 再由 5V 通过 LDO 得到给 STM32 和 PS2 接收模块。注意 STM32 和 PS2 模块不要从 TB6612 板载的 5V 引脚取电那个引脚的电流能力有限驱动负载稍大会把电压拉垮。电池和所有低压模块的 GND 必须全部共地共地是一切正常工作的前提。如果实测发现电压跌落严重在电源输入端并联一个大电解电容比如 470μF 以上再并一个 100nF 陶瓷电容分别吸收低频纹波和高频噪声。这个习惯能解决非常多“莫名其妙复位”的疑难杂症。3. PS2手柄通信协议从0x01配置到9字节数据帧的完整解析3.1 为什么选择 PS2 手柄而不是自制遥控器市场上很多 2.4G 遥控器套件只有几个按键和两个摇杆买回来发现协议不公开、信号不稳定甚至两套设备会互相干扰。PS2 手柄在老玩家手里积累了大量开源的通信代码按键、模拟摇杆、震动等功能都有现成的解析方案搭配几十块钱的接收模块稳定性和可定制性远超市面上大部分玩具遥控器。更关键的是 PS2 手柄有两个模拟摇杆也就是四个模拟量输入这对麦克纳姆轮底盘来说非常合适一个摇杆控制前后和横移另一个摇杆控制旋转操作起来极其自然。3.2 通信时序和两个关键前提PS2 接收模块和单片机之间用类似 SPI 的时序通信主机单片机拉低 CS然后通过 CMD 线发送命令同时从 DAT 线读取数据。两件事是同时进行的每发送一个字节的命令就会从 DAT 上读回一个字节的数据。时钟频率不要太高一般 250kHz 左右就足够也就是每一位约 4 微秒一帧 9 个字节的全部传输时间不到 1 毫秒对小车控制来说完全够用。这里有两个特别容易翻车的前提。第一手柄必须处于模拟模式手柄中央的 ANALOG 键按下后指示灯从红色变成绿色摇杆才输出模拟量。红灯状态下摇杆虽然也能动但读回来的值会退化成方向键的开关量或者始终是中间值。第二代码里必须先在通信时发送 0x01 让手柄进入配置模式再发送 0x42 读取帧数据。跳过配置直接读很多模块只能拿到按键数据摇杆模拟量是无效的。3.3 数据帧布局和按键映射完整读回来的一帧数据是 10 个字节习惯上用一个 10 字节数组保存每个字节的含义如下。uint8_t buf[10]; // buf[0] : 状态字节0x00 表示正常0x79 表示模拟模式生效 // buf[1] : 按键组1 // buf[2] : 按键组2 // buf[3] : 按键组3通常忽略 // buf[4] : 保留字节 0x00 // buf[5] : 保留字节 0xFF // buf[6] : 右摇杆 X 轴 // buf[7] : 右摇杆 Y 轴 // buf[8] : 左摇杆 X 轴 // buf[9] : 左摇杆 Y 轴按键组的位定义要记住一个反直觉的规则按键按下时对应位是 0松开时是 1。比如 buf[1] 的 0x10 位代表方向键上的 UP判断 UP 按下就要判断(buf[1] 0x10) 0。buf[2] 里常用的有 0x10 代表三角键、0x20 代表圆圈键、0x40 代表叉键、0x80 代表方块键。摇杆的原始值是 0 到 255静止时在 128 附近。为了方便控制代码里通常先减 128得到 -128 到 127 的有符号数再配合死区处理效果就很直观。下面是我实际用过的软件模拟 SPI 读帧代码不依赖硬件 SPI任何 GPIO 都能跑#define PS2_DAT HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) #define PS2_CMD_H HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET) #define PS2_CMD_L HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET) #define PS2_CLK_H HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) #define PS2_CLK_L HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET) #define PS2_CS_H HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define PS2_CS_L HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) uint8_t PS2_Xfer(uint8_t tx) { uint8_t rx 0; for (uint8_t i 0; i 8; i) { if (tx 0x80) PS2_CMD_H; else PS2_CMD_L; tx 1; PS2_CLK_H; delay_us(4); rx 1; if (PS2_DAT) rx | 0x01; PS2_CLK_L; delay_us(4); } return rx; } void PS2_ReadFrame(uint8_t buf[10]) { PS2_CS_L; buf[0] PS2_Xfer(0x01); // 进入配置模式 buf[1] PS2_Xfer(0x42); // 读取数据命令 for (uint8_t i 2; i 10; i) buf[i] PS2_Xfer(0x00); PS2_CS_H; }每次调用的间隔建议控制在 10ms 以上不要连续高频调用否则偶尔会出现数据错位。如果读回来的 buf[0] 一直是 0xFF 之类异常值先检查供电和接线再检查手柄是不是绿灯模式。4. 四轮转速分配麦克纳姆轮运动学解算与摇杆映射4.1 麦克纳姆轮为什么能横移麦克纳姆轮的每个轮子外圈都套着一圈可以自由转动的小滚子小滚子的轴线与轮子轴线呈 45 度角。轮子转动时地面给轮子的摩擦力会被滚子分解成两个方向的分量其中一部分推动小车沿轮子轴向移动。四个轮子以不同速度和方向转动时各个轮子的摩擦力分量在小车中心合成就得到了任意方向的合力。决定全向控制成败的有一个硬规则麦克纳姆轮分为左旋和右旋两种安装时必须保证对角线上的两个轮子同一种旋向。左前和右后是同一种右前和左后是另一种。买轮子时商家会标注 A 型和 B 型或者直接叫左旋右旋一定不要搞混。装反了表现就是小车一推前进键就往斜上角跑非常明显。4.2 逆运动学公式麦克纳姆轮底盘的控制核心是把“我想要小车以什么速度走”翻译成“四个轮子分别以什么速度转”这一步叫逆运动学解算。用底盘正前方为 Y 正方向、正右方为 X 正方向、逆时针旋转为 ω 正方向四个轮子线速度参考计算公式如下lf vy - vx - L * wz; // 左前轮 rf vy vx L * wz; // 右前轮 lr vy vx - L * wz; // 左后轮 rr vy - vx L * wz; // 右后轮其中 vy 是纵向目标速度vx 是横向目标速度wz 是旋转角速度。L 是底盘中心到轮子的等效距离取横向半距和纵向半距之和单位随便只要所有轮子一致就行。公式算出来的值如果符号不同代表的物理意义就是轮子转向不同。这段公式虽然看起来简单但有两点必须提醒。第一不同资料里符号约定可能完全不同这是麦克纳姆轮资料里最容易让新手困惑的地方。不要迷信公式的符号上电后一定做分项验证只给 vy四个轮子应该同向同速只给 vx应该是两侧轮子反向只给 wz四个轮子应该呈对角对称方向转动。哪个分量方向反了就把对应变量的符号取反不要改电机接线。第二公式没有除以轮子半径因为我们在闭环控制中直接用这个值作为目标转速的相对大小最后会通过一个标定系数映射到编码器目标脉冲。4.3 手柄摇杆到目标速度的映射手柄映射我用的是左手管平移、右手管旋转的方案左摇杆 Y 轴控制 vy左摇杆 X 轴控制 vx右摇杆 X 轴控制 wz右摇杆 Y 轴可以空着或定义为备用。读取摇杆后减 128再做死区处理绝对值小于 10 就归零避免摇杆没有完全回中导致小车一直缓慢移动。int16_t lx buf[8] - 128; int16_t ly buf[9] - 128; int16_t rx buf[6] - 128; if (abs(lx) 10) lx 0; if (abs(ly) 10) ly 0; if (abs(rx) 10) rx 0; float vy ly / 128.0f * MAX_SPEED; float vx lx / 128.0f * MAX_SPEED; float wz rx / 128.0f * MAX_TURN;MAX_SPEED 和 MAX_TURN 是小车的最高速度和最大旋转速度根据你的电机能力和场地大小设定。刚开始调试时一定要限制得小一点比如最大速度只给目标转速的 30%否则小车一推手柄就飞出去根本没有反应时间观察问题。4.4 标定系数 K把轮速映射成编码器目标脉冲闭环控制里速度的单位是编码器脉冲数而不是米每秒。因此需要一个标定系数 K把运动学解算出来的速度值映射到编码器的目标脉冲数。标定方法很简单给某个轮子一个固定的 PWM 占空比比如 40%让轮子空转用编码器测出每 10ms 的脉冲数然后用这个数除以对应的速度值就是 K。这么做的好处是彻底绕开了轮子直径、减速比、编码器线数这些繁琐的换算直接用实验数据把所有物理参数一次性吸收进 K 里。4 个轮子因为机械差异K 值可能略有不同分别标定即可。5. 速度闭环编码器测速与PID调参的实战记录5.1 为什么四个轮子必须上闭环麦克纳姆轮小车的运动学解算成立的前提是四个轮子实际转速和期望转速严格一致。但现实很残酷四个电机之间存在个体差异左右两侧摩擦阻力也不完全相等相同占空比下的实际转速可能相差 5% 到 10%。在开环状态下这 5% 的偏差会让小车在前进时逐渐走偏在横移时变成斜线在旋转时一个方向轻一个方向重。所以四个轮子全部要上速度闭环。速度闭环的架构是外环直接对轮子转速做 PID 控制期望速度来自运动学解算实际速度来自编码器反馈PID 输出是 PWM 占空比。整个控制周期我习惯设在 10ms也就是每 10ms 采样一次编码器计数差值更新一次 PID 输出。5.2 编码器测速的两条路带编码器的直流减速电机编码器输出 A、B 两相正交方波。STM32 上有两种常见测速方法。第一种是定时器编码器模式。F103 的 TIM1、TIM2、TIM3、TIM4 都支持编码器接口把 A 相接到 CH1、B 相接到 CH2配置成编码器模式后CNT 寄存器会自动根据 A、B 相位差进行加减计数。这样测到的值自带正负号方向判断不需要额外逻辑四路编码器正好用四个定时器。第二种是外部中断加定时器统计。只接 A 相到外部中断引脚每来一个上升沿就把计数加一方向用 B 相电平来判断。这种方法省定时器但每个轮的计数都要占一个外部中断干扰多的时候边沿容易丢。我推荐用第一种省心且可靠。采样代码如下// 定时器中断每 10ms 调用一次 void Speed_Sample_Task(void) { speed_lf (int16_t)TIM1-CNT; // 10ms 内的脉冲差值 speed_rf (int16_t)TIM2-CNT; speed_lr (int16_t)TIM3-CNT; speed_rr (int16_t)TIM4-CNT; TIM1-CNT 0; TIM2-CNT 0; TIM3-CNT 0; TIM4-CNT 0; }5.3 增量式PID实现和限幅四路轮子各自用一个 PID 实例互不干扰。我用的是增量式 PID输出是 PWM 占空比的增量这种写法天然适合电机控制场景因为输出不会在目标值突变时产生过大跳变。typedef struct { float Kp, Ki, Kd; float err, last_err, prev_err; float out; } PID_t; float PID_Update(PID_t *pid, float target, float actual) { float err target - actual; float d_err err - pid-last_err; float inc pid-Kp * d_err pid-Ki * err pid-Kd * (err - 2.0f * pid-last_err pid-prev_err); pid-prev_err pid-last_err; pid-last_err err; pid-out inc; if (pid-out 999) pid-out 999; // PWM 限幅 if (pid-out -999) pid-out -999; return pid-out; }速度值的正负号代表方向PID 输出的正负号也代表方向。最终输出到驱动板时根据符号切换 AIN1/AIN2再取绝对值为占空比就能把方向控制和速度控制拆开。5.4 调参顺序和死区补偿调参我强烈建议先单轮后整车。把四个轮子全部架空代码里先只给左前轮一个固定目标速度用一个轮子把 Kp 调到一个合适范围再把参数复制给其他三个轮子。Kp 偏小表现是轮子转速跟不上目标、响应肉肉的Kp 偏大表现是轮子转速震荡、有嗡嗡声。Kp 调到基本不震荡后再逐步加 Ki 消除静差。Kd 在电机控制里通常作用不大我一般直接用很小的值或者不用。还有一个特别容易忽略的细节是 PWM 死区。直流减速电机存在启动死区占空比低于某个值比如 10% 时轴根本不动PID 积分在这个区间会不断累积等到占空比终于超过死区时电机突然猛地一冲。处理办法是在 PID 输出后加一个判断如果目标速度不为零但实际速度为零就把输出抬升到死区之上同时限制积分累积。这个补偿逻辑看起来简单但对低速微调场景帮助极大。6. 联调踩坑实录从“乱转”到“指哪打哪”的排查过程6.1 小车前进时原地画圈四个轮子方向“打架”这是麦克纳姆轮小车最经典的问题几乎每个第一次装车的人都会遇到。现象是按下前进键后小车不往前走而是在原地转圈或者斜着跑。排查链路是先拆掉运动学给四个轮子分别发送相同的正转指令逐个确认电机本身转向是否一致、编码器反馈是否为正。如果这里没问题再检查轮子旋向安装是否满足“对角线同型”规则。我遇到过一个隐蔽的坑四个轮子接线时有两根线插反了导致右前和左后两个轮子的电机正转方向相反前进指令下两个轮子向前推、两个轮子向后拽车自然就在原地打转了。解决办法是在每个轮子的电机线上贴上标签把 IN1、IN2 的正反逻辑在代码里统一不要靠物理换线解决。6.2 摇杆推“前进”却变成横移手柄映射到 vx、vy 的通道如果接反就会出现这个现象。左摇杆本来是前后推结果因为摇杆 Y 轴被当成 vx 用了前后推变成了横移。这是纯逻辑层面的错误检查主循环里手柄摇杆和运动学输入参数的对应关系即可。这类问题用我前面说的“分项验证法”很好定位单独推前后看是不是只有 vy 有值单独推左右看是不是只有 vx 有值单独转右摇杆看是不是只有 wz 有值。6.3 手柄偶尔失灵或数据全是 0xFF排查顺序依次是供电、接线、时序。先是 PS2 接收模块的 VCC 有没有稳定在 3.3V别用万用表量空载电压要带模块实测然后是 DAT、CMD、CLK、CS 四根线是否接触良好杜邦线松动会引起偶发失联最后是通信时序确认时钟频率是否过高、CS 拉低前后延时是否足够。还有一个很容易被忽略的点手柄和接收模块刚上电时有时需要几秒配对时间代码里开机后先延时 200ms 再初始化能减少很多莫名其妙的首次通信失败。6.4 一推摇杆小车就重启这个现象基本可以断定是电源问题。四个电机同时加速时电流冲击很大电池电压被瞬间拉低单片机的供电一旦跌破复位阈值就重启。排查时先用示波器或者万用表看电机启动瞬间的电池电压如果发现压降超过 0.5V就在电池输出端并联 470μF 以上电解电容。再确认一下 STM32 的供电有没有和电机驱动共用同一根线如果共用把逻辑供电独立分出来用粗一点的导线单独接到稳压模块。6.5 转弯方向老是反这通常不是硬件问题而是运动学公式中 wz 的符号约定和实际电机方向相反。解决方法是把wz变量取反或者把左右侧轮子的旋转效应互换。改代码比改结构要快得多不需要动任何接线。调试到最后分享一个小技巧把所有联调测试做成一个手柄按键菜单。按键 A 触发单轮自检依次验证四个轮子的电机和编码器方向按键 B 触发纯前后运动按键 C 触发纯横移运动按键 D 触发原地旋转。每次上电先跑一遍这个菜单确认所有硬件正常后再进入正式控制逻辑。这个习惯帮我省掉了至少 80% 的硬件排查时间强烈推荐你也这样做。本文还有配套的精品资源点击获取
返回列表