STM32麦克纳姆轮底盘开环控制与PS2手柄完整工程实践

发布时间:2026/10/13 19:25:34
STM32麦克纳姆轮底盘开环控制与PS2手柄完整工程实践
简介面向STM32与麦克纳姆轮底盘开发者的开环控制方案以STM32F103ZET6为主控配合PS2手柄实现无线遥控覆盖机器人竞赛、智能车课程设计、毕设入门等场景。资源内包含实测能用的PS2驱动代码存在轻微延迟逻辑完整、L298N电机驱动原理图与底层控制代码并给出完整Keil工程。工程基于标准外设库涉及GPIO、定时器、PWM、串口等外设配置源文件与头文件一一对应可直接编译烧录。压缩包共212个文件以C源码、H头文件、编译中间文件.o为主另有PNG原理图、uvprojx工程配置、HEX烧录文件等整体约7.26MB。读者可参考其L298N接线与控制逻辑结合PS2按键映射快速移植到自己的车架即便不打算用PS2也能提取开环底盘的主循环调度思路。已有846人学习下载适合需要快速搭建麦克纳姆轮开环底盘并验证遥控逻辑的开发者。1. 麦克纳姆轮底盘STM32 开环控制与 PS2 手柄的完整落地做麦克纳姆轮底盘最难的不是让轮子转而是让四个轮子按你想要的矢量方向协同转动。很多初学者一上来就研究闭环 PID、编码器测速结果被参数调得焦头烂额车却跑不直。其实对于 PS2 手柄控制、场地巡检、竞赛演示这类场景开环控制完全够用而且调试周期能压缩到一晚上。这份资源的核心就是一套基于 STM32F103 的麦克纳姆轮底盘完整工程含 PS2 接收解析、四路 PWM 驱动、斜向运动学解算适合正在做智能车竞赛、课程设计或机器人入门项目的开发者照着接线、烧录、配对就能跑起来。全文按「硬件拆解 → PS2 协议 → 运动学代码 → 整合调试 → 进阶技巧」五个层次展开中间穿插我实际调试时踩过的坑。你拿到的源码不是我临时拼的 Demo而是从实际跑过比赛的车上扒下来的每个函数都有存在的必要。2. 底盘硬件选型轮组布局、驱动板与 STM32 的资源分配2.1 麦克纳姆轮的辊子朝向与轮组编号规则拿到一套麦克纳姆轮先别急着装。轮子上的辊子小滚柱是斜向 45 度排列的这个斜向角度决定了整车运动学公式里的正负号。拆开包装看辊子轴线与轮子主轴线的夹角有两种——左旋和右旋。标准装法是俯视底盘左前轮和右后轮用同一种旋向右前轮和左后轮用另一种旋向。你要是装反了车会往意想不到的方向飘代码怎么调都救不回来。我习惯按「左前 LF、右前 RF、左后 LB、右后 RB」编号这四个轮子分别接 STM32 的四路 PWM 输出通道。每个轮子需要一个电机驱动芯片常见的是 TB6612 或 DRV8833我这份资源里用的是 TB6612驱动板上的 IN1/IN2 控制电机正反转PWM 引脚控制转速。底盘供电建议 7.4V 锂电池经 5V BEC 给 STM32 供电驱动板直接吃电池电压。轮位旋向俯视PWM 定时器通道电机驱动引脚左前 LF左旋TIM1_CH1PA8 / PB13右前 RF右旋TIM1_CH2PA9 / PB14左后 LB右旋TIM1_CH3PA10 / PB15右后 RB左旋TIM1_CH4PA11 / PB16选 TIM1 是因为它有四路互补通道带死区插入虽然开环用不到互补输出但如果你后续要加电流反馈或刹车逻辑TIM1 的硬件资源更充裕。PA8~PA11 是 TIM1 的默认映射引脚不冲突的话别重映射。2.2 开环控制为什么够用场景边界与误差容忍度开环不是「没有反馈」而是「不需要高精度反馈」。PS2 手柄的摇杆是个模拟量人手操作本身就带 ±10% 的误差你就算把轮速闭环到 1% 精度操作手感也不会变好多少。开环适合的场景有三个特征速度不高低于 1.5m/s、地面平坦室内地砖或短毛地毯、运行时间短单次连续行驶不超过 10 分钟。这三个条件同时满足时开环的漂移量肉眼几乎看不出来。反过来如果你要做的是循迹、里程计定位、视觉跟随那就必须上编码器闭环。麦克纳姆轮的辊子与地面是点接触打滑率比普通轮子高开环跑 20 米后横漂可能累计到半米。所以拿到这份资源先别改代码先确认你的使用场景在不在开环的舒适区里。2.3 从 CubeMX 初始化到 PWM 输出一套可直接跟做的配置流程我用的开发环境是 STM32CubeMX Keil MDK。CubeMX 里只需要三块配置RCC 外部晶振、TIM1 的 PWM 输出、一个普通 GPIO 作为 PS2 接收或者用 USART 接收但 PS2 手柄是 SPI 类时序更多地是软件模拟时序。具体说先配置时钟树把 APB1 外设时钟设为 36MHzAPB2 设为 72MHzTIM1 挂在 APB2 上这样 PWM 频率可以做高一些。TIM1 的 PSC 设为 71ARR 设为 999得到 72MHz / (711) / (9991) 1kHz 的 PWM 频率对应电机驱动板的开关频率刚好在听不见的频段。// 以下为 CubeMX 生成后微调的关键参数 htim1.Init.Prescaler 71; // 预分频72MHz / 72 1MHz htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 999; // 自动重载1MHz / 1000 1kHz htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; // PWM 通道初始化参数 sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; // 初始占空比为 0防止上电猛冲 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET;参数说明Prescaler 和 Period 决定了 PWM 频率。1kHz 是大多数直流电机驱动芯片的甜点区太低会听到电机啸叫太高会增大驱动芯片的开关损耗。Pulse 初始化为 0 是血泪教训——之前有一次上电时 Pulse 默认是 255四个电机全速转车直接从桌上飞出去了。另外注意TIM1 是高级定时器必须在__HAL_TIM_MOE_ENABLE(htim1)之后 PWM 才会真正输出到引脚这是新手最容易漏的一步。3. PS2 手柄通信协议从物理层到按键数据的完整拆解3.1 手柄工作模式与 SPI 时序特点PS2 无线手柄带接收器那种和 STM32 之间走的是类似 SPI 的串行协议时钟线、数据线、命令线、片选线各一根。接收器上电后默认处于模拟模式红灯常亮这个模式下摇杆的模拟量才会输出——如果你发现摇杆数据全是 0x7F中间值先检查手柄是不是处于数字模式绿灯数字模式只输出按键不输出摇杆模拟量。协议层的关键是命令帧主机每发送一个字节从机返回一个字节。完整的数据读取过程需要发送 9 个命令字节对应接收 9 个响应字节其中第 3、4 字节是按键状态第 5、6 字节是右摇杆和左摇杆的 X/Y 模拟量。整个时序用 GPIO 模拟即可不需要硬件 SPI因为 PS2 的时钟不是标准 SPI 极性用硬件 SPI 反而要处理 CPOL/CPHA 匹配问题。// PS2 接收器引脚初始化软件模拟时序 // DI 接 PA4DO 接 PA5CS 接 PA6CLK 接 PA7 // 发送一个命令字节并读取一个响应字节 unsigned char PS2_ByteRead(unsigned char command) { unsigned char res 0; for (int i 0; i 8; i) { if (command (0x01 i)) { PS2_DI_HIGH(); } else { PS2_DI_LOW(); } PS2_CLK_FALLING(); // 时钟下降沿手柄采样命令位 delay_us(4); res | (PS2_DO_READ() i); // 数据线上读回响应位 PS2_CLK_RISING(); delay_us(4); } return res; }逻辑说明命令字节从低位开始逐位发送每发一位拉低一次时钟手柄在该下降沿锁存命令位同时把响应位放到数据线上主机在同一时刻读取。delay_us(4)是经验值PS2 协议要求时钟高电平持续时间不低于 2 微秒留一倍余量防止线材电容导致边沿变缓。你要是用杜邦线超过 20cm把这个延时加到 6 微秒更稳。3.2 摇杆数据处理死区过滤与线性映射摇杆返回的原始值是 0x00~0xFF中间值 0x7F。PS2 手柄的摇杆物理结构决定了它回中之后不一定精确停在 0x7F——我拆过三只手柄两只回中是 0x7C一只回中是 0x83。如果把原始值直接送给运动学公式车会在原地缓慢漂移。所以第一步必须做死区过滤把差值绝对值小于 8 的区间视为 0。#define JOY_DEAD_ZONE 8 // 死区阈值需根据实际手柄标定 #define JOY_MID_VALUE 0x7F // 理论中值 int16_t Joy_Process(uint8_t raw_value) { int16_t delta (int16_t)raw_value - JOY_MID_VALUE; if (delta -JOY_DEAD_ZONE delta JOY_DEAD_ZONE) { delta 0; } return delta; // 输出范围 -127 ~ 127 }第二个坑是线性映射。摇杆推到极限时输出是 0xFF差值是 128但电机 PWM 占空比如果直接给 128在 1kHz 频率下电机转速很低——因为 0~128 正好落在电机的死区电压附近。我一般会把 0~127 的摇杆差值映射到 PWM 的 20~130 区间留出余量给运动学公式叠加四路 PWM 最终输出是四个方向分量的叠加如果映射到满量程 0~255叠加后必然溢出。映射关系是PWM 20 (abs(delta) * 110 / 127)方向由 IN1/IN2 的电平决定。3.3 PS2 库的配置细节Proefile 数组与四个坑网上流传的 PS2 库很多我用过的版本里最稳定的还是那个带PS2X类的库但移植到自己的板子上需要改三个地方引脚宏定义、延时函数、以及内存分配。库内部有个Proefile数组用于缓存按键状态在某些编译优化等级下比如 -O2局部数组没初始化会导致按键数据是随机值。解决方法是把这个数组声明为全局数组并显式置零// 移植 PS2 库时的关键修改点 static unsigned char Proefile[9] {0}; // 必须全局 初始化 // 初始化 PS2 手柄 void PS2_Init(void) { PS2_CS_HIGH(); PS2_CLK_HIGH(); delay_ms(10); // 等手柄接收器上电稳定 PS2_ByteRead(0x01); // 进入配置模式 PS2_ByteRead(0x42); // 读取数据命令 PS2_ByteRead(0x00); PS2_ByteRead(0x00); PS2_ByteRead(0x00); delay_ms(20); // 配置切换需要时间 }参数说明0x01是进入配置模式命令0x42是读数据命令。如果你发现手柄初始化后数据全为 0xFF大概率是这些命令字节没有按顺序发完或者 CS 片选信号没有在整个读过程中保持低电平。还有一点容易忽略PS2 接收器上电瞬间会自检此时给它发命令会被忽略所以初始化前必须加 10ms 延时。这个问题在资源和常见项目里很容易翻车建议固化一个PS2_Init流程不要在别的函数里重复调用初始化。4. 麦克纳姆轮运动学从遥杆矢量到四轮独立转速的映射4.1 运动学公式的推导逻辑与正负号约定麦克纳姆轮底盘的运动学核心就一个公式任意方向的平面运动可以分解为三个独立分量——前后平移X 轴、左右平移Y 轴、原地旋转Z 轴。每个轮子的转速是这三个分量的线性组合。设前方为 Y 轴正方向右侧为 X 轴正方向逆时针旋转为 Z 轴正方向则四个轮子的目标转速是LF 轮速 Y分量 X分量 Z分量 RF 轮速 Y分量 - X分量 - Z分量 LB 轮速 -Y分量 - X分量 Z分量 RB 轮速 -Y分量 X分量 - Z分量注意观察正负号左右两侧的轮子对 X 分量的符号相反对 Z 分量的符号也相反这正是斜向辊子产生侧向力的结果。代码里把这个公式展开成一个宏或内联函数比先算旋转矩阵再调库函数更直观也省掉浮点运算的开销。// 运动学映射函数输入操纵杆矢量 (joy_x, joy_y) 与旋转量 rotate // 输出四路 PWM 占空比 void Kinematic_Map(int16_t joy_x, int16_t joy_y, int16_t rotate) { int16_t m1 joy_y joy_x rotate; // LF 轮速 int16_t m2 joy_y - joy_x - rotate; // RF 轮速 int16_t m3 - joy_y - joy_x rotate; // LB 轮速 int16_t m4 - joy_y joy_x - rotate; // RB 轮速 // PWM 占空比限幅防止叠加后超限 m1 Clamp(m1, -MOTOR_MAX, MOTOR_MAX); m2 Clamp(m2, -MOTOR_MAX, MOTOR_MAX); m3 Clamp(m3, -MOTOR_MAX, MOTOR_MAX); m4 Clamp(m4, -MOTOR_MAX, MOTOR_MAX); Motor_SetPWM(MOTOR_LF, m1); Motor_SetPWM(MOTOR_RF, m2); Motor_SetPWM(MOTOR_LB, m3); Motor_SetPWM(MOTOR_RB, m4); }逻辑说明joy_y推杆向前推为正车前进joy_x向右推为正车右平移rotate为正值时逆时针旋转。这个正负号约定必须和你的轮子安装旋向对应上。如果你的车装上后推杆向前但车向后把joy_y的符号取反即可不要改公式里四个轮的排列——改排列容易漏改导致对角轮转向矛盾。4.2 斜向运动全向平移与原地旋转的实现差异麦克纳姆轮的招牌动作是「斜向 45 度平移」——推杆同时向前和向右推车头朝向不变整体向右前方平移。这个动作的本质是让 X 分量和 Y 分量同时参与叠加四个轮子的转速各不相同。很多新手在这里会犯一个错以为斜向移动就是把左右轮设成不同速度、后轮保持不动那是差速转向的思路用在麦轮上会导致车轮侧滑加剧、电机过流。原地旋转则比较简单前后轮对旋转分量的贡献方向一致但左右侧相反。注意原地旋转时不要同时推平移摇杆否则合成运动是「旋转 平移」的螺旋轨迹视觉上很不稳。我从赛场上学到的是在代码里加一个互斥逻辑——rotate非零时将joy_x、joy_y直接清零这样手柄怎么推都不会出现意外的螺旋。// 旋转与平移的互斥逻辑防止遥杆误触导致螺旋轨迹 void Kinematic_Update(void) { int16_t joy_x Joy_Process(PS2_GetData(JOY_RX)); int16_t joy_y Joy_Process(PS2_GetData(JOY_RY)); int16_t rotate Joy_Process(PS2_GetData(JOY_LX)); if (abs(rotate) 20) { // 旋转优先 joy_x 0; joy_y 0; } Kinematic_Map(joy_x, joy_y, rotate); }参数说明右摇杆 X 控制左右平移右摇杆 Y 控制前后移动左摇杆 X 控制旋转。这个分配是我试了几种方案后最顺手的一套右手负责方向左手负责姿态跟玩射击游戏的直觉一致。你在自己的代码里不必照搬但建议保持「移动和旋转分属两个摇杆」的原则否则单手同时控制平移和旋转非常容易误触。4.3 斜向运动时 PWM 输出与电压的关系这里有一个开环控制特有的问题电池电压从满电 8.4V 降到 7.2V 时同样占空比下电机转速会掉 15% 左右。开环系统没有测速反馈所以这 15% 的掉速会直接表现为车越跑越慢、转向越来越飘。我的应对方案是给运动学加一个「电压补偿系数」——用 ADC 实时采样电池电压低于 7.4V 时按比例放大 PWM 占空比。uint16_t Battery_GetVoltage(void) { // 假设 ADC 通道 1 采样电池分压后的电压分压比 2:1 uint16_t adc_val ADC_GetValue(ADC_CH1); return (uint16_t)(adc_val * 2 * 3300 / 4095); // 单位 mV } // 电压低于 7400mV 时给 PWM 一个提升系数 uint16_t Voltage_Compensate(uint16_t pwm_out) { uint16_t volt Battery_GetVoltage(); if (volt 7400) { float ratio 7400.0f / volt; pwm_out (uint16_t)(pwm_out * ratio); } return (pwm_out MOTOR_MAX) ? MOTOR_MAX : pwm_out; }代码逻辑ADC 采样值先换算成毫伏再乘以分压比。补偿系数是一个简单的分数放大没有做智能 PID 平滑所以电池电压突变时 PWM 会跳变一次体感上会有一个轻微的「咯噔」。这个方案能延后换电池的时间点但不能根治电压影响——根治需要编码器闭环。如果你用的是带保护板的锂电池电压低于 6.5V 直接断电保护补偿逻辑就没机会工作这时候应当把保护板的低压阈值当作最后一次换电池提醒。5. 整合调试与常见问题从接通电源到稳定奔跑的 90 分钟5.1 上电自检流程的正确顺序与初始化陷阱拿到源码后不要直接编译烧录先按下面的顺序做一遍硬件自检。这个顺序是我血泪教训总结的先电源、后信号、最后协议。第一步单独给驱动板通电确认四个电机在不上 PWM 时不转有些驱动板 EN 引脚悬空时会默认使能电机微微抖动。第二步给 STM32 烧录一个纯 PWM 输出的测试程序让四路 PWM 依次以 30% 占空比转动 2 秒确认轮子转向是否符合 2.1 节的方向约定——这一步能排除插线接反的问题。第三步烧录完整程序连接 PS2 接收器把手柄放平观察初始化完成后手柄灯是否常亮红色。这里有个常见的初始化陷阱delay_ms(10)用的延时函数如果是基于 SysTick 的而 SysTick 初始化接在主时钟之后那么主时钟没配好之前延时是失效的。CubeMX 生成的代码顺序是先配时钟再配 SysTick一般没问题但如果你是手动移植的工程务必检查 SysTick 配置是否在延时调用之前。5.2 避坑记录调试过程中最常踩的四类问题我把这个项目从零调试到稳定运行的 90 分钟里遇到最多的四类问题罗列如下问题一手柄数据全为 0xFF现象PS2_Init 后读取到的按键和摇杆全是 0xFF。 原因手柄接收器上电瞬间的复位毛刺导致初始化命令没有被正确接收或者是硬件 SPI 模式下 CPOL/CPHA 不匹配。我排查时发现 CS 线在上电后先拉高了 1ms 又拉低而手柄需要 CS 从高到低的跳变作为命令帧起始。 解决在 CS 初始化后先强制拉高并延时 1ms再拉低等待 2ms最后拉高进入空闲状态然后再执行 3.3 节中的初始化时序。问题二电机的嗡嗡声很大但车不移动现象PWM 占空比给到 80电机有声音但轮子不动。 原因PWM 频率太低或占空比落在死区。我的板子驱动芯片是 TB6612 的低电平使能逻辑PWM 频率 1kHz 下占空比低于 20约 8% 有效电压时电机扭矩不足以克服静摩擦。 解决把MOTOR_MIN_PWM设为 20所有参与运动学的占空比先取绝对值小于 20 一律视为 0。另外检查驱动板和电池的共地——TB6612 的逻辑地必须和 STM32 的 GND 相连不共地时 PWM 无法形成有效回路。问题三左转和右转速度不对称现象同一个 rotation 值左转比右转快而且伴随明显的车体抖动。 原因四个电机中存在一个或两个接线虚焊导致某轮的实际电压降比其他轮多。另一个常见原因是左旋和右旋辊子的安装顺序混乱——再次确认 2.1 节的编号表。 解决用示波器或万用表量四个电机端子的有效电压正常时四个电压应该一致。如果不一致检查对应轮的驱动输出。另外开机后跑 5 秒直线观察车是否走偏走偏超过 20cm 则说明某个轮的辊子方向装反了。问题四摇杆回中后车还在缓慢漂移现象手柄摇杆完全松手车 3 秒内移动了 5cm。 原因摇杆物理回中位置不在 0x7F手柄老化后电位器磨损导致中值偏移。我遇到的一只旧手柄回中在 0x84差值 5 在我的死区阈值 8 以内会被过滤但如果死区阈值设成 3这个漂移就会出现。 解决把死区阈值提到 10 并在上电时增加一个「摇杆中值自校准」初始化完成后读取 20 次摇杆值取平均作为该手柄的中值基准替代固定的 0x7F。这个方法能兼容大部分旧手柄但增加约 50ms 的自检时间比赛场合看需求取舍。5.3 现场调试手法用 LED 和串口观察内部变量调试这种开环系统最怕的是「车动了但不知道对不对」。我习惯在工程里开一个串口调试通道用 115200 波特率周期输出四个轮子的 PWM 计算值和摇杆原始值。这样出现的异常运动能立刻反推是「输入错误」还是「映射错误」。手边没有串口调试助手时我还会接两个 LED 到 PB0/PB1一个指示手柄连接状态一个指示电池电压是否过低这两个 LED 在调试中能省掉大量猜疑。// 串口打印调试信息的代码片段 void Debug_Print(void) { char buf[64]; sprintf(buf, RX%d RY%d LX%d | M1%d M2%d M3%d M4%d\r\n, joy_rx_raw, joy_ry_raw, joy_lx_raw, pwm_m1, pwm_m2, pwm_m3, pwm_m4); UART_SendString(USART1, buf); }这段代码要放在主循环里用定时器以 10Hz 频率触发不要塞进死循环否则会占用大量 CPU 时间导致 PS2 的时序延时不稳定。串口输出对新手最大的价值是当你说「车往左偏」时你能立刻区分是摇杆值不对称还是轮子转速不对称。前者是输入问题后者是硬件问题排查方向完全不同。6. 进阶技巧状态机模式切换与开环系统的手感调校主循环逻辑我们一直用的裸奔式while(1)但如果你想让车具备「训练模式」「表演模式」「急停模式」这类功能切换就得把主循环改成状态机。我在竞赛前的最后一版代码里加入了模式判断逻辑思路很简单PS2 手柄的 SELECT 和 START 组合键切换模式每个模式对应不同的速度上限和摇杆映射曲线。typedef enum { MODE_NORMAL 0, // 普通模式最高 50% PWM MODE_TURBO, // 高速模式最高 80% PWM MODE_SLOW // 精调模式最高 25% PWM } RunMode_t; RunMode_t mode MODE_NORMAL; void Mode_Toggle(void) { if (PS2_IsPressed(PS2_START) PS2_IsPressed(PS2_SELECT)) { mode (mode 1) % 3; delay_ms(200); // 防抖避免一次按键切换多次模式 } }模式切换最大的用处是保护新手阶段的误操作有人在刚上手时推满摇杆高速模式下车直接蹿出去。切换到精调模式后PWM 上限被限制在 25%即使摇杆推满车也只会慢悠悠地移动非常安全。这个限幅是在运动学输出前统一乘系数实现的不需要改四个电机各自的映射逻辑只用把MOTOR_MAX变成动态值即可。另一个更隐蔽的调校点是「摇杆响应曲线」。PS2 摇杆是物理电位器它的输出在中间段变化较快、在两端接近饱和——直接线性映射会让 PWN 在小幅度操作时变化太剧烈车表现为「轻推就跑、再推就不动」。我一般在映射时加一个平方根曲线PWM MAX * sqrt(|delta| / 127) * sign(delta)。这样轻推时 PWM 是满推的 1/4 左右中段操作更细腻适合室内狭窄空间挪车。代价是满推和轻推的差异感会被放大需要两三天适应手感但习惯了之后你会觉得车「听话」很多。最后说一个从那次把车飞下桌之后养成的习惯每次改代码重新烧录后先把车架空四个轮子离地跑一遍全向运动和旋转测试确认 PWM 输出和摇杆响应正常后再放地上跑。这个习惯后来救了我两次——一次是某个通道的 Pulse 初始化改错一次是驱动板的 IN 引脚虚焊导致反转。架空测试只要 30 秒但避免了车在地上原地打转或者突然全速冲出去的尴尬。期间也遇到过手柄电池没电导致的信号中断当时车还在原地旋转差点撞到人于是后续我把「手柄信号超时自动停电机」也写进了主循环成本很低但收益很大。希望这套拆解和经验能帮你在做麦轮底盘时少走几个月的弯路。代码和工程文件都在资源包里解压后先看README.md里面有引脚对照总表和 PS2 库的使用限制说明配好环境后直接按我的顺序烧录调试即可。本文还有配套的精品资源点击获取