资讯动态

基于STM32的PS2遥控小车制作详解:从硬件接线到代码实现

发布时间:2026/10/18 3:22:32 来源:尧图企业网站定制
简介面向STM32与嵌入式开发者的PS2遥控小车资料包围绕PS2手柄解码、舵机PWM控制与振动试验三大主题搭配详细协议手册与可运行例程帮助读者从原理到代码完整掌握手柄遥控小车的实现方式。压缩包共95个文件以C源文件与H头文件为主干另含hex固件、Keil工程、PDF说明手册、Arduino参考示例及txt说明文档整体仅1.04MB便于下载与对照学习。已有659人学习资料虽小但体系完整PS2解码通讯手册和常见问题汇总可系统理解手柄信号格式与排错要点STM32例程覆盖舵机控制与振动实验从初始化、读取手柄输入、解析数据到驱动执行机构均有代码支撑Arduino资料则可作为跨平台移植参考。适合正在学习STM32外设、串行通信、PWM信号控制或想将游戏手柄用于机器人项目的开发者既可用于课程设计也可作为竞赛作品的快速起点。1. PS2遥控小车从手柄到舵机的完整链路把 PS2 手柄和 STM32 接在一起第一次十有八九跑不通。串口打印出来全是 0xFF红灯不亮怀疑接线问题、协议问题、电源问题最后发现只是手柄没按 MODE 键进到遥控模式。这个项目就是一台基于 STM32、用 PS2 手柄遥控的小车左摇杆控制转向舵机右摇杆控制前进后退方块键触发振动试验模块做整机扫频测试。整套代码按硬件驱动、协议解析、应用逻辑三层拆好照接线图接完板子就能跑。适合手里有 PS2 手柄、想低成本做无线遥控载具、机械臂或移动底盘的人。2. 硬件与接线PS2接收器、舵机、电机和振动模块的选型与上电2.1 器件选型为什么用C8T6加TB6612这一套主控选 STM32F103C8T6。72MHz 主频跑 PS2 协议绰绰有余内部有 3 个串口、4 个通用定时器出 6 路独立 PWM 完全够用。这个项目里舵机转向、左右电机、振动模块加起来也就 4 路所以选型上真正的压力不在主控性能而在电源分配。PS2 手柄加配套接收器是这些年最成熟的无线方案。接收器和主控之间走 SPI 协议2.4GHz 射频空旷距离十米级别遥控一台小车绰绰有余。手柄自带的两个模拟摇杆天然就是转向和速度的模拟量输入比用按键控制方向手感好得多。接收器四根线 DAT、CMD、SEL、CLK兼容 3.3V 逻辑直接接 STM32 的 GPIO不需要电平转换。转向舵机用 SG90 这一档就够。SG90 扭矩 1.8kg/cm驱动一台 1kg 左右的小车转向轮没问题。如果是给机械臂或者更大功率的载具用换成 MG995扭矩能到 13kg/cm但工作电流会暴涨到 2A 级别电源方案必须同步升级。舵机信号线是 PWM供电线必须接 5V 而不是 3.3V这一点很多人会栽。电机驱动板我选 TB6612FNG 而不是经典的 L298N。TB6612 的逻辑电平直接兼容 3.3VL298N 有些板子在 3.3V 高电平下识别不稳定还得加电平转换。TB6612 导通压降小同样电压下电机转速损耗更小发热也低。它的两路输出 PWM 频率能跑到 100kHz实际用 10kHz噪音和损耗平衡得比较好。振动模块用最普通的偏心振动马达就是手机里那种。额定电压 3V 到 5V工作电流 80mA 到 150mA不能直接挂在 STM32 的 GPIO 上GPIO 推挽输出最大才 20mA 左右。驱动方式是加一个 NPN 三极管做开关基极串 1k 电阻接 STM32 的 PWM 引脚集电极接马达负极马达正极接 5V发射极接地。2.2 引脚映射一张表解决所有接线疑问直接给一张我自己量过的映射表。需要说明的是PS2 的 SPI 我没有用板载的硬件 SPI全部用 GPIO 模拟好处是时序完全可控调起 bug 来方便。功能模块信号STM32 引脚配置PS2 接收器DATMISOPA6输入上拉PS2 接收器CMDMOSIPA7推挽输出PS2 接收器SEL片选PA4推挽输出PS2 接收器CLK时钟PA5推挽输出转向舵机信号线PA0TIM2_CH1左电机PWMPB0TIM3_CH3右电机PWMPB1TIM3_CH4左电机方向 AIN1 / AIN2PB12 / PB13推挽输出右电机方向 BIN1 / BIN2PB14 / PB15推挽输出振动模块三极管基极PB8TIM4_CH3调试串口TX / RXPA9 / PA10USART1接线有几个注意点。PS2 接收器的 DAT 线不要漏接上拉STM32 内部有上拉可以直接开如果板子上的 IO 没有做内部上拉外部加一个 4.7k 到 3.3V。马达驱动板的逻辑电源 VCC 接 3.3V电机电源 VM 接电池正极。舵机三根线棕色负极、红色正极、橙色信号接反必烧舵机内电路。2.3 电源设计舵机一转MCU就复位的根源我最初图省事让 5V 的舵机和 3.3V 的 MCU 共用同一个 AMS1117 电源。结果舵机一打角串口就乱码严重时 MCU 直接重启。原因是舵机启动瞬间电流可以冲到 700mA 以上AMS1117 最大输出电流也就 800mA电压被拉低到 MCU 欠压阈值以下就掉电。正确的做法是分开供电最后只共地。7.4V 2S 锂电池直接进 TB6612 的 VM驱动两个直流电机。电池再进一个 5V 降压模块比如 MP1584输出给舵机和振动模块。5V 再接一路 AMS1117-3.3V只给 MCU 和 PS2 接收器供电。三路电源的 GND 最终在同一个点汇合。这样舵机拉电流不会拖垮 MCU。电容也别省5V 输出端并联一个 470uF 电解电容和一个 0.1uF 陶瓷电容能有效抑制舵机回正时产生的尖峰。TB6612 的 VM 引脚旁边也放一个 100uF 电容电机启停瞬间电流变化非常大。提示如果做了独立供电之后 MCU 还是复位先检查共地有没有接好。MCU 和舵机不共地时PWM 信号参考地不对舵机信号传不过去表现出来的往往是舵机乱抖而不是直接复位。3. 工程结构与数据流PS2小车代码怎么组织才不乱3.1 Keil工程目录底层驱动与应用逻辑分离拿到源码第一件事是看清目录。整个工程按 App、Driver、BSP 三层拆开底层驱动完全不依赖应用逻辑换一块板子只需要改 BSP 层的引脚配置。STM32-PS2-Car/ ├── App/ │ ├── main.c # 主循环轮询手柄、更新电机舵机 │ ├── remote_control.c # 遥控逻辑摇杆值到速度/角度换算 │ └── remote_control.h ├── Driver/ │ ├── ps2_driver.c # PS2协议层GPIO模拟SPI时序、按键解析 │ ├── ps2_driver.h │ ├── servo_driver.c # 舵机驱动角度到PWM占空比换算 │ ├── servo_driver.h │ ├── motor_driver.c # 电机驱动方向速度 │ ├── motor_driver.h │ ├── vibrate_driver.c # 振动模块频率切换 │ └── vibrate_driver.h ├── BSP/ │ ├── gpio.c # 所有GPIO初始化 │ ├── tim.c # PWM定时器初始化 │ └── uart.c # 调试串口 └── README.md # 接线图、引脚表、操作说明Driver 层只做一件事把硬件能力暴露成函数接口比如PS2_SendByte()、Servo_SetAngle()、Motor_SetSpeed()。App 层只负责逻辑不考虑具体某个引脚在哪个 GPIO。以后改成蓝牙遥控只需要在 Driver 层新增一个蓝牙驱动App 层的遥控逻辑不用动。3.2 主循环数据流摇杆值到PWM波形的完整链路主循环没有上操作系统就是一个 10ms 周期的无限循环。每一轮做三件事扫描手柄、解析数据、更新 PWM 输出。主循环调用PS2_Scan()通过 GPIO 模拟 SPI 时序和手柄交换数据拿到 9 个字节保存在全局数组ps2_buf[]。RemoteControl_Update()从ps2_buf[]里提取左摇杆 X 值和右摇杆 Y 值换算成舵机角度和电机速度。换算结果通过Servo_SetAngle()和Motor_SetSpeed()落到定时器比较寄存器马上变成舵机和电机的动作。int main(void) { HAL_Init(); SystemClock_Config(); // 72MHz 主频 MX_GPIO_Init(); MX_TIM2_Init(); // 舵机PWM 50Hz MX_TIM3_Init(); // 电机PWM 10kHz MX_TIM4_Init(); // 振动PWM 1kHz MX_USART1_UART_Init(); // 调试串口 while (1) { PS2_Scan(ps2_buf); // 第一步读手柄 RemoteControl_Update(ps2_buf); // 第二步解析并计算 HAL_Delay(10); // 第三步10ms 周期 } }主循环周期 10ms 不等于舵机更新周期也是 10ms。PS2 手柄本身的刷新率大约在 100Hz 量级10ms 轮询已经比手柄还快不会漏按键。舵机的 PWM 是 50Hz一个周期 20ms即使每一轮主循环都写一次比较寄存器舵机看到的还是一个平滑的 50Hz 波形占空比在平滑渐变。我试过把这个周期调成 1msPS2 数据解析反而出错因为 GPIO 模拟 SPI 时需要微秒级延时循环体被拖长调成 100ms 遥控手感会明显变肉转向滞后很明显。10ms 是个折中值。电机速度的换算也放在RemoteControl_Update()里读的是右摇杆 Y 值/* 右摇杆Y0xFF 表示推到最下0x00 表示推到最上0x80 附近为自然回中 */ uint16_t speed; if (ps2_buf[5] 0x70) { // 上推 Motor_SetDirection(IN_FORWARD); speed (0x7F - ps2_buf[5]) * 2; // 差值放大两倍最快约 254 } else if (ps2_buf[5] 0x90) { // 下推 Motor_SetDirection(IN_BACKWARD); speed (ps2_buf[5] - 0x7F) * 2; } else { speed 0; // 中间死区电机停转 } Motor_SetSpeed(speed);这里速度值直接作为 PWM 比较值写入 TIM30 到 254 对应占空比 0% 到 100%。TB6612 的方向由 AIN1、AIN2 两个引脚的组合决定PWM 只控制输出功率方向和速度解耦逻辑上清爽很多。4. 核心代码拆解PS2解析、舵机映射与振动扫频4.1 PS2协议解析主机是怎么把手柄按键读进内存的PS2 手柄的物理层就是 SPI只是时序要求主机主动拉低片选然后先发命令字节再读数据。完整流程是拉低 SEL发送命令 0x01 握手再发送 0x42 请求按键数据然后连续读 9 个字节。每个字节的收发在同一组时钟脉冲里完成。uint8_t PS2_SendByte(uint8_t cmd) { uint8_t ret 0; for (uint8_t i 0; i 8; i) { LL_GPIO_ResetOutputPin(PS2_CLK_PORT, PS2_CLK_PIN); /* CLK拉低 */ delay_us(2); if (cmd 0x80) LL_GPIO_SetOutputPin(PS2_CMD_PORT, PS2_CMD_PIN); /* 输出最高位 */ else LL_GPIO_ResetOutputPin(PS2_CMD_PORT, PS2_CMD_PIN); LL_GPIO_SetOutputPin(PS2_CLK_PORT, PS2_CLK_PIN); /* CLK拉高从机锁存 */ delay_us(2); ret 1; if (LL_GPIO_IsInputPinSet(PS2_DAT_PORT, PS2_DAT_PIN)) ret | 0x01; /* 从DAT读回一位 */ cmd 1; } return ret; } void PS2_Scan(uint8_t *buf) { LL_GPIO_ResetOutputPin(PS2_SEL_PORT, PS2_SEL_PIN); /* 片选拉低 */ PS2_SendByte(0x01); /* 握手命令 */ PS2_SendByte(0x42); /* 请求数据 */ for (uint8_t i 0; i 9; i) { buf[i] PS2_SendByte(0x00); /* 空字节换回数据 */ } LL_GPIO_SetOutputPin(PS2_SEL_PORT, PS2_SEL_PIN); /* 片选拉高 */ }为什么用 2us 延时而不是更短。PS2 接收器是 2.4GHz 射频模块内部状态机翻转需要时间时钟太快会读到不稳定的数据。2us 的拉高拉低间隔折合时钟约 250kHz比规范里的 500kHz 慢一半留了余量。如果你偷懒用HAL_Delay(2)代替微秒延时整个循环会被拖到毫秒级遥控手感会非常差。读回数据后第一件事是判断手柄是否在线。红灯模拟模式下buf[0]固定为 0x5A如果读到 0xFF基本可以断定手柄没开电源或者没进遥控模式这帧数据直接丢弃。按键位定义我沿用常见的约定buf[1]和buf[2]是数字按键状态按下的位为 0buf[4]到buf[7]分别是右摇杆 X、右摇杆 Y、左摇杆 X、左摇杆 Y。4.2 舵机控制摇杆模拟量到PWM占空比的换算舵机控制的核心是 PWM 周期固定、占空比可变。SG90 的周期是 20ms也就是 50Hz高电平时间在 0.5ms 到 2.5ms 之间对应 0 到 180 度。TIM2 的配置是预分频 71、自动重载值 19999计数频率 1MHz周期正好 20ms。void Servo_SetAngle(uint16_t angle) { uint32_t pulse; if (angle 180) angle 180; pulse 500 (uint32_t)angle * 2000 / 180; /* 0度500, 180度2500 */ __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, pulse); }500 angle * 2000 / 180的意思是0 度对应比较值 500也就是 500us 高电平180 度对应 2500中间线性插值。SG90 的角度线性度在正负 3 度以内不需要做查表校正。摇杆到角度的映射放在遥控逻辑层不放在驱动层这样以后改成自动巡线模式可以直接复用Servo_SetAngle()uint16_t RemoteControl_Steer(uint8_t lx) { if (lx 0x6E) return 30; /* 左摇杆左推 */ if (lx 0x91) return 150; /* 左摇杆右推 */ return 90; /* 中间死区舵机回正 */ }这里有个玄学细节左右各留了大约 15% 的摇杆行程做死区。因为摇杆弹簧回中并不能保证完全回到 0x7F常在 0x78 到 0x86 之间漂移没有死区的话车会在直道上轻微摆头。死区留太大转向响应慢我试过 20 就很肉15 是折中值。4.3 振动试验定时器重装载实现四档扫频振动试验要验证的是偏心马达在不同 PWM 频率下的驱动力矩不同底盘在某些频率点会发生共振。代码里做了四档频率切换对应停止、500Hz、2kHz、8kHz 四种状态。#define VIB_STOP 0 #define VIB_FREQ_1 500 #define VIB_FREQ_2 2000 #define VIB_FREQ_3 8000 void Vibration_SetFreq(uint16_t freq_hz) { if (freq_hz 0) { __HAL_TIM_SET_COMPARE(htim4, TIM_CHANNEL_3, 0); /* 停止输出 */ return; } uint32_t arr 1000000 / freq_hz - 1; /* 计数频率1MHz */ __HAL_TIM_SET_AUTORELOAD(htim4, arr); __HAL_TIM_SET_COMPARE(htim4, TIM_CHANNEL_3, arr / 2); /* 固定50%占空比 */ }占空比固定 50% 是为了让频率成为唯一变量。如果频率和占空比一起变试验结果会出现两个变量分不清共振点到底是频率引起的还是驱动力矩引起的。频率越高马达转速越快但超过某个频率后马达感抗会限制电流驱动力矩反而下降这个拐点可以在串口打印里标出来。按键触发逻辑写在主循环里按方块键切挡。PS2 的按键数据是低电平有效解析时用位与判断uint8_t prev_square 1; /* 上一帧方块键状态1松开 */ if ((ps2_buf[2] 0x80) 0) { /* 方块键按下bit7清零 */ if (prev_square 1) { /* 只在下降沿触发一次 */ v_freq_idx (v_freq_idx 1) % 4; Vibration_SetFreq(v_freq_table[v_freq_idx]); printf(vibrate freq: %d Hz\n, v_freq_table[v_freq_idx]); } } else { prev_square 0; }这里一定要做边沿检测。主循环周期只有 10ms而人手按一次键的物理时间在 100ms 以上主循环会连续检测到十几次按下不做边缘检测的话频率档位会一次跳好几格这也是我最初翻车最多的位置。5. 避坑手册四个让遥控车趴窝的常见问题5.1 串口打印全是0xFF现象串口一直打印 0xFF手柄怎么按都没反应。原因九成是手柄没有进遥控模式。PS2 手柄上电默认是绿灯模式绿灯模式下摇杆全输出 0x80数字按键只有部分有效。只有按手柄背部的 MODE 键切到红灯模式模拟摇杆数据才完整。解决手柄装电池后先按一次 MODE确认红灯亮再操作。如果红灯亮了还是 0xFF检查 DAT 和 CMD 两根线有没有接反我见过不止一次这两个信号对调的情况。5.2 舵机不转用镊子触碰信号线却能转现象舵机完全不动但用镊子把信号线短路到 3.3V 时会转一下。原因PWM 频率配置错了。舵机要求 50Hz 周期很多人把定时器算到了 50kHz输出到舵机的是几乎恒定的高电平舵机误以为要转到极限角度然后堵转发烫。用示波器看波形正常舵机信号是 20ms 周期内一个 0.5ms 到 2.5ms 的高电平而 50kHz 下整个波形都是高电平。解决检查定时器分频和重载值。72MHz 主频下预分频 71 得到 1MHz 计数频率设置自动重载 19999输出频率正好是 50Hz。比较值在 500 到 2500 之间变化。5.3 舵机打角的瞬间MCU复位现象小车一动方向MCU 就重启串口重新打印启动信息。原因电源问题。舵机启动瞬态电流能到 700mA 以上如果 MCU 和舵机共用一路 5V 电源电压跌到 MCU 欠压阈值以下就复位。这属于硬件问题软件调参救不回来。解决舵机和 MCU 独立供电、最后共地5V 输出端并联 470uF 电解电容。注意共地不能省不共地会表现为舵机随机抖动而不是复位。5.4 振动马达不转但GPIO有电平现象GPIO 量出来有高电平3V 马达就是转不起来。原因GPIO 驱动能力不够。STM32 的 GPIO 推挽输出最大电流 20mA 左右偏心马达正常工作电流在 80mA 到 150mAGPIO 输出电压被拉低到 1V 以下马达只响不转。解决用 NPN 三极管做开关基极串 1k 电阻接 PWM 引脚集电极接马达负极马达正极接 5V。PWM 频率在 1kHz 到 8kHz 之间调节频率太高马达感抗太大反而转不动。6. 二次开发把遥控小车改成循迹车的落地思路最后给一个实际改造路径。不改变硬件大前提把遥控模式改成自动循迹模式核心是把手柄输入这一层换成传感器输入这一层。第一步加硬件。循迹小车最需要的是灰度传感器两个就够分别放在车头左右两侧。传感器输出数字量接 STM32 的空闲 GPIO比如 PB9 和 PB10。灰度模块自带比较器检测到黑线输出低电平白底输出高电平不需要做 ADC 采样。第二步在 App 层加模式切换变量uint8_t run_mode MODE_REMOTE; /* MODE_REMOTE / MODE_TRACK */ if (run_mode MODE_TRACK) { uint8_t l_sensor HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_9); uint8_t r_sensor HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_10); if (l_sensor 0 r_sensor 1) { /* 左传感器压线 */ Servo_SetAngle(140); /* 舵机右转 */ Motor_SetSpeed(150, 100); /* 左轮快右轮慢差速辅助 */ } else if (l_sensor 1 r_sensor 0) { Servo_SetAngle(40); /* 舵机左转 */ Motor_SetSpeed(100, 150); } else { Servo_SetAngle(90); /* 直线行驶 */ Motor_SetSpeed(150, 150); } }第三步用手柄的 SELECT 键切换run_mode切换时用串口打印当前模式方便确认状态。这样整台车同时具备手动和自动两种能力硬件只多两个传感器和几根线。这套改造在前面工程结构里的落脚点很清晰新写一个track_control.c和remote_control.c实现同一个Control_Update()入口主循环根据run_mode调用对应的实现。这就是把遥控逻辑和驱动逻辑分离的意义不是为了让代码好看是改起功能来真的省时间。从那以后我每次接新传感器都强制走一遍「Driver 层加驱动、App 层注册回调、主循环绑定模式」这个流程踩坑率明显降下来了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑