简介:这是一份面向嵌入式初学者与电机控制实践者的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 / GND | 3.3V / GND |
| PS2接收器 | DATA | PA6 |
| PS2接收器 | CMD | PA7 |
| PS2接收器 | ATT | PA4 |
| PS2接收器 | CLK | PA5 |
| 左驱动板 | IN1-IN4 | PB0-PB3 |
| 右驱动板 | IN1-IN4 | PB4-PB7 |
| 左驱动板 | ENA / ENB | PA0 / PA1(TIM2_CH1/CH2) |
| 右驱动板 | ENA / ENB | PA2 / 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个字节,格式如下:
| 字节 | 内容 | 说明 |
|---|---|---|
| 0 | 0x5A | 固定头,判断通信是否正常 |
| 1 | 按键低字节 | SELECT / L3 / R3 / START / 方向键 |
| 2 | 按键高字节 | L2 / R2 / L1 / R1 / 三角 / 圆圈 / 叉 / 方块 |
| 3 | 右摇杆X | 0x00-0xFF,中间约0x80 |
| 4 | 右摇杆Y | 0x00-0xFF,中间约0x80 |
| 5 | 左摇杆X | 0x00-0xFF,中间约0x80 |
| 6 | 左摇杆Y | 0x00-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桥真值表:
| IN1 | IN2 | 电机状态 |
|---|---|---|
| 0 | 0 | 停止(滑行) |
| 1 | 0 | 正转 |
| 0 | 1 | 反转 |
| 1 | 1 | 刹车 |
把速度值翻译成方向加占空比的函数:
// 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响应,确认四路方向都对、占空比跟手,再放到地上跑。架空测试能排除地面摩擦和电池电压波动的干扰,快速区分是控制逻辑问题还是机械问题。这个习惯后来做其他电机项目也一直保留,省了不少返工。希望帮到你。
本文还有配套的精品资源,点击获取