☰
基于STM32与PS2手柄的四电机小车PWM差速混控方案
2026/10/26 21:51:26 网站建设 项目流程

简介:这是一份面向嵌入式初学者与电机控制实践者的PS2手柄遥控四轮全向轮小车项目源码,基于STM32RCT6微控制器,借助其丰富外设与标准库函数完成手柄信号解析、PWM调速与四轮协同控制,解决从输入设备接入到多电机联动的整链路实现问题。压缩包共233个文件,约8.28MB,以41个c源文件与42个h头文件构成主体,配合42个o目标文件、41个crf编译记录及axf、hex、map等构建产物,另含uvprojx、uvoptx工程文件与keilkilll.bat清理脚本,完整保留工程目录与编译状态。已有747人学习下载。读者可从中获得PS2手柄SPI通信解析、定时器PWM占空比调速、全向轮平移与原地旋转的软件分工思路,并借助README与USER、CORE、SYSTEM等模块划分理解板级支持与固件库的衔接方式,便于快速复现与二次开发。

1. 从一只PS2手柄说起:为什么用STM32做四电机小车的控制核心

拿一只PS2手柄配四个电机做小车底盘,乍看只有三件事——读摇杆、算PWM、驱动电机。真动手才知道,第一道坎在接收机时序:时钟边沿对不上、延迟给少了,读回来不是0xFF就是死值;第二道坎在四轮混控,一个摇杆管前后加转向,左右两侧速度怎么加怎么减、死区留多少,全得抠;第三道坎在供电,电机启动那一下的电流冲击能把控制板拉复位,接收机跟着掉线。这套方案用STM32F103做核心,GPIO模拟PS2时序读手柄,两片电机驱动带四路直流减速电机,把上面三个问题拆开处理,适合正在做嵌入式课设、毕设,或者想搭一个遥控底盘做后续扩展的人。

2. 系统总体设计:PS2接收机、STM32与四路电机驱动的职责划分

2.1 硬件链路与模块选型

控制核心用STM32F103C8T6最小系统板,72MHz主频,定时器和GPIO数量足够撑起一路PS2时序加四路PWM输出。PS2接收器选9针无线接收模块,工作电压3.3V,和STM32的IO电平直接匹配,省掉电平转换。电机驱动按电机额定电流来定:如果四个轮子用的是常见的TT马达或37mm减速电机,单路持续电流在200-500mA,两片TB6612就能带;如果电机扭矩大、额定电流接近1A,换两片L298N,压降大但扛得住。

供电分两路走:7.4V锂电池直接进电机驱动,另一路经AMS1117-3.3降到3.3V给STM32和PS2接收器。这里有一条血泪经验——不要用同一路电源同时带电机和控制板,电机启动瞬间的电流尖峰会顺着3.3V线灌进MCU,轻则读手柄数据乱跳,重则直接复位。

接线关系:

模块引脚连接到
PS2接收器VCC / GND3.3V / GND
PS2接收器DATAPA6
PS2接收器CMDPA7
PS2接收器ATTPA4
PS2接收器CLKPA5
左驱动板IN1-IN4PB0-PB3
右驱动板IN1-IN4PB4-PB7
左驱动板ENA / ENBPA0 / PA1(TIM2_CH1/CH2)
右驱动板ENA / ENBPA2 / PA3(TIM2_CH3/CH4)

每片驱动板带两个电机,两片正好四路。方向IO占8根,PWM占4根,STM32F103C8T6的引脚刚好排得开。

2.2 软件分层与主循环结构

软件分四层:硬件抽象层负责GPIO和定时器的初始化;协议解析层负责PS2时序收发和帧校验;控制逻辑层做死区处理、归一化和差速混控;输出执行层把控制量翻译成方向IO加PWM占空比。层与层之间用结构体传数据,不直接操作寄存器,后面改参数或者换驱动板的时候不用从头翻代码。

主循环节奏定在10ms一次:读PS2、解析按键和摇杆、死区处理、混控、输出PWM。10ms不是拍脑袋定的——PS2接收器一帧完整数据大约1-2ms,间隔太短会和上一次读取的ATT拉高冲突,间隔太长操作跟手性变差。实际项目里可以用定时器中断驱动,避免HAL_Delay阻塞其他逻辑。

// main.c 主循环骨架 int main(void) { HAL_Init(); SystemClock_Config(); PS2_Init(); Motor_Init(); PS2_Data_t ps2; Control_t ctrl; while (1) { if (PS2_Read(&ps2) == PS2_OK) { Control_Update(&ps2, &ctrl); Motor_Output(&ctrl); } else { Motor_StopAll(); // 接收器掉线,四轮全停 } HAL_Delay(10); } }

PS2_Read返回PS2_OK才继续控制,否则全停。这个判断不能省,接收器掉线之后如果继续用上一次的PWM值输出,车会保持最后一脚油门一直冲出去。Motor_StopAll()把四路PWM占空比清零,方向IO全部拉低,电机进入滑行状态。

3. PS2手柄协议解析:SPI时序、数据帧与摇杆映射

3.1 PS2接收机的通信方式与接线

PS2接收器和STM32之间走的是类SPI的同步串行协议,但不是标准SPI——时钟极性、相位、字节顺序都和硬件SPI外设对不上,所以我一般直接用GPIO模拟时序,不依赖硬件外设。这样调起来直观,出问题拿示波器一抓就能定位到是哪个边沿偏了。

时序逻辑:ATT拉低选中接收器,CLK下降沿把命令位放到CMD线上,上升沿之后从DATA线采样。一个字节8个位,位间延迟给10us左右。不同批次的接收器对延迟容忍度不一样,有的5us就能跑,有的要给到20us,先用10us试,读不到0x5A再往上加。

接线有两点要注意:ATT、CLK、CMD、DATA四根线尽量短,超过20cm就容易受电机干扰;接收器VCC一定接3.3V,有些模块标称兼容5V,但长期跑下来DATA线电平会漂,表现为数据偶尔全0xFF。

3.2 数据帧格式与按键/摇杆解析

一次完整读取返回9个字节,格式如下:

字节内容说明
00x5A固定头,判断通信是否正常
1按键低字节SELECT / L3 / R3 / START / 方向键
2按键高字节L2 / R2 / L1 / R1 / 三角 / 圆圈 / 叉 / 方块
3右摇杆X0x00-0xFF,中间约0x80
4右摇杆Y0x00-0xFF,中间约0x80
5左摇杆X0x00-0xFF,中间约0x80
6左摇杆Y0x00-0xFF,中间约0x80
7-8预留/压力值一般不用

读取流程:先发0x01启动,再发0x42请求数据,后面连续发0x00把9个字节读回来。如果第一个返回字节不是0x5A,说明时序或接线有问题,这一帧数据整体丢掉,不参与控制。

// ps2.c 读取一帧数据 uint8_t PS2_Read(PS2_Data_t *out) { uint8_t buf[9]; PS2_CS_LOW(); // ATT拉低 PS2_Transfer(0x01); // 启动 PS2_Transfer(0x42); // 请求数据 for (int i = 0; i < 9; i++) { buf[i] = PS2_Transfer(0x00); } PS2_CS_HIGH(); // ATT拉高 if (buf[0] != 0x5A) { // 头字节校验 return PS2_ERR; } out->btn_low = buf[1]; out->btn_high = buf[2]; out->rjoy_x = buf[3]; out->rjoy_y = buf[4]; out->ljoy_x = buf[5]; out->ljoy_y = buf[6]; return PS2_OK; }

PS2_Transfer是位收发函数,内部用GPIO翻转CLK:下降沿把命令位写到CMD,上升沿之后从DATA采样。buf[0] != 0x5A是唯一可靠的通信质量判据,头字节不对的时候后面8个字节可能是任意值,不能拿来用。

按键解析用位操作,PS2手柄按下时对应位为0:

#define PS2_BTN_CROSS(d) (!((d)->btn_high & 0x40)) #define PS2_BTN_CIRCLE(d) (!((d)->btn_high & 0x20)) #define PS2_BTN_START(d) (!((d)->btn_low & 0x08))

叉键和圆圈键可以用来做点动微调或者切换速度档位,START键做急停。急停逻辑放在按键解析之后、混控之前,按下直接把ctrl四个轮子的速度清零。

3.3 摇杆量到控制量的映射策略

摇杆原始值0x00-0xFF,中间约0x80,但不同手柄的中位有偏差,旧的摇杆回中可能偏到0x78或者0x88,直接用会在无人操作时往一个方向蠕动。处理分两步:先扣中位去死区,再归一化到-100到+100。

// 摇杆量归一化 int16_t Joy_Normalize(uint8_t raw, uint8_t center) { int16_t val = (int16_t)raw - (int16_t)center; if (val > -10 && val < 10) { return 0; // 死区 } return val * 100 / 118; // 映射到约-100~+100 }

118等于center - 10,把死区之外的范围压到-100到+100。死区取±10是折中值:太小噪声会被当成输入,车在静止时小幅抖动;太大推杆初段没反应,手感发空。如果你的手柄比较新,±6就够;旧手柄可以放到±12。中心值center不建议写死0x80,上电时让摇杆自然回中读100次取平均更稳。

4. 四电机驱动与差速混控:PWM调速、正反转和转向算法

4.1 电机驱动模块选型与接线

电机驱动选型按电流走:L298N一片带两个电机,内部压降大约1.4V,7.4V供电进去电机端只剩6V左右,转速上不去,但持续电流能到2A,带大扭矩电机不容易烧。TB6612效率高、体积小,单路持续1.2A、峰值2A,带TT马达绰绰有余,但堵转时间一长就进热保护,表现为车突然停住、过几秒又自己恢复。

接线方式:每片驱动板的OUT1/OUT2接一个电机,OUT3/OUT4接另一个,IN1/IN2控制第一个电机的方向,IN3/IN4控制第二个,ENA/ENB接PWM。四轮布局下,左前和左后并到一片驱动板,右前和右后并到另一片,这样左右两侧可以独立调速,差速转向才有物理基础。

L298N的ENA/ENB跳线帽要拔掉,不然使能端一直拉高,PWM输不进去。这个细节容易翻车——第一次接线忘了拔跳线帽,代码里PWM怎么改电机都是全速转。

4.2 PWM调速与方向控制实现

STM32的TIM2四个通道正好输出四路PWM,频率设10kHz。频率太低电机会有可闻啸叫,低速时转矩脉动明显;频率太高L298N开关损耗上来,驱动板发热。10kHz刚好在听觉边缘之外,开关损耗也能接受。

方向控制就是H桥真值表:

IN1IN2电机状态
00停止(滑行)
10正转
01反转
11刹车

把速度值翻译成方向加占空比的函数:

// motor.c 单个电机输出 void Motor_SetSpeed(Motor_t *m, int16_t speed) { // speed范围: -1000 ~ +1000 if (speed > 1000) speed = 1000; if (speed < -1000) speed = -1000; if (speed >= 0) { HAL_GPIO_WritePin(m->in1_port, m->in1_pin, GPIO_PIN_SET); HAL_GPIO_WritePin(m->in2_port, m->in2_pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(m->htim, m->channel, speed); } else { HAL_GPIO_WritePin(m->in1_port, m->in1_pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(m->in2_port, m->in2_pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(m->htim, m->channel, -speed); } }

speed的正负代表方向,绝对值代表占空比千分比。这里要求TIM2的ARR设成1000,比较寄存器的值就直接对应占空比,不用再乘系数。换驱动板或者换电机的时候,只需要改这个函数里的方向IO映射,上层混控代码不动。

四个电机因为有减速箱和轮径公差,同一路PWM下实际转速不完全一致,直行时会往一边偏。两个办法:一是软件补偿,给一侧乘一个0.95左右的系数;二是选带编码器的电机做闭环。课设级别用第一种就够,补偿系数在架空测试时调到直行不偏即可。

4.3 四轮差速混控算法

混控是整个控制逻辑的核心:左摇杆Y管前进后退,右摇杆X管转向。左侧两个轮子速度等于油门加转向,右侧等于油门减转向。左转时右侧加速、左侧减速,右转反过来。

// control.c 差速混控 void Control_Update(PS2_Data_t *ps2, Control_t *out) { int16_t throttle = Joy_Normalize(ps2->ljoy_y, 0x80); int16_t steer = Joy_Normalize(ps2->rjoy_x, 0x80); int16_t left = throttle + steer; int16_t right = throttle - steer; // 限幅:按比例缩放,保持转向比例 int16_t peak = (left > right) ? left : right; if (peak > 100) { left = left * 100 / peak; right = right * 100 / peak; } out->lf = left; // 左前 out->lr = left; // 左后 out->rf = right; // 右前 out->rr = right; // 右后 }

限幅这一步不能省。油门50、转向60的时候,左侧是110、右侧是-10,如果不做比例缩放,左侧PWM溢出被截断到100,右侧只有10,车会往右偏。按峰值比例压到100以内之后,左右比例保持不变,只是整体速度降下来,转弯半径稳定。这个细节在答辩时经常被追问,属于差速混控里最容易忽略的一环。

如果想让原地转向更利索,可以单独加一个旋转模式:左摇杆X不动、右摇杆X拉满时,左侧和右侧取相反值,车绕中心转。代码里加一个判断,当throttle接近0且steer绝对值超过60时,把left和right按steer的正负取正反即可。

5. 避坑与排查:PS2通信、电机干扰和供电的典型问题

5.1 PS2接收机通信不稳定的排查

现象:读出来的数据偶尔全是0xFF,头字节不是0x5A,车会突然抽一下,或者摇杆推着推着失灵。

原因:最常见是信号线太长、接收器GND和STM32 GND没有直接连、或者主循环里读得太频繁,上一次ATT还没拉高就开始下一次读取。

解决:先把四根信号线缩到10cm以内,接收器GND直接连到STM32的GND引脚。如果还不行,在PS2_Transfer里把位间延迟从10us加到20us试试。读取周期固定10ms,不要在中断里连续读。判断通信质量用头字节错误率:连续跑10分钟,错误次数超过1%就得查线。

5.2 电机干扰导致STM32复位的问题

现象:小车一启动电机就复位,或者正反转切换时MCU重启,PS2接收器也跟着掉线。

原因:电机启动电流尖峰通过电源线灌到3.3V,或者电机换向时的反向电动势通过底盘传导。L298N内部没有隔离,电机线和信号线捆在一起走线时干扰最明显。

解决:电机电源和控制电源分开走,电机驱动输入端并一个1000uF电解电容加一个0.1uF陶瓷电容。STM32的3.3V输入端也加100uF加0.1uF。电机线和信号线分开走,不要用扎带捆成一束。如果是金属底盘,确认电机外壳没有和信号线短接。

5.3 供电与共地问题

现象:PS2接收器偶尔掉线,摇杆读数往一边漂,电机转起来之后又恢复正常。

原因:接收器GND和STM32 GND不是同一路,或者GND线经过了电机驱动板,电机电流在GND线上产生压降,接收器参考电平跟着抖。

解决:单点接地。电池负极先到电机驱动板,再从驱动板GND接到STM32的GND,接收器GND直接连STM32的GND引脚,中间不串其他负载。共地线用粗一点的线,别用杜邦线里最细的那根。

5.4 PWM频率与电机噪声

现象:电机低速时啸叫明显,或者低速不转、给一点油就突然窜出去。

原因:PWM频率太低,1kHz左右电机转矩脉动大,听得见啸叫;频率太高,L298N开关损耗上来,驱动板发烫。低速不转是死区设太大,摇杆初段被吃掉。

解决:频率设8-12kHz之间,我用10kHz。低速不转把Joy_Normalize里的死区从±10改成±6试试。如果改了还不行,检查电机减速箱是不是缺润滑,新电机跑一段时间会顺一些。

5.5 摇杆死区与线性度

现象:松手之后车还在慢慢走,或者推一点点没反应、推多了突然窜。

原因:旧手柄摇杆回中不准,中位偏离0x80;或者死区太小,ADC噪声被当成输入。

解决:上电时做一次中位校准,摇杆放中间读100次取平均作为center。死区按手柄新旧取±8到±12。如果推杆初段发空,把归一化里的映射范围从center - 10改成center - 6,让有效行程提前。

6. 验收与答辩:怎么证明这套小车方案是完整可跑的

验收不要只演示“能跑”,按指标逐条过。我一般准备一张表,答辩时直接对着表讲:

验收项测试方法合格标准
PS2通信稳定性连续运行10分钟,记录头字节错误次数错误率低于1%
摇杆方向分别推前进/后退/左转/右转四轮方向正确,无卡顿
死区松手观察电机四轮全停,无蠕动
差速转向前进+左转左侧减速、右侧加速,转弯半径稳定
掉线保护拔掉接收器2秒内四轮全停
供电稳定性电机正反转切换20次MCU不复位,接收器不掉线

答辩被问得最多的是两个问题。一个是“为什么用GPIO模拟SPI而不是硬件SPI”,回答落在时序可控性上——PS2协议不是标准SPI,硬件外设的极性和相位不一定对得上,模拟时序改一个延迟参数就能适配不同批次的接收器。另一个是“差速混控的限幅为什么做比例缩放”,拿油门50、转向60举例,左右分别是110和-10,不做缩放左侧溢出截断、右侧只剩10,车会往右偏;按峰值压到100以内之后左右比例不变,转弯半径才稳。

我自己的习惯是每次改完混控参数,先把车架空,用手转轮子看PWM响应,确认四路方向都对、占空比跟手,再放到地上跑。架空测试能排除地面摩擦和电池电压波动的干扰,快速区分是控制逻辑问题还是机械问题。这个习惯后来做其他电机项目也一直保留,省了不少返工。希望帮到你。

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

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

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

立即咨询