STM32 PS2遥控小车:舵机云台与振动试验的完整实战解析
2026/9/7 9:30:13 网站建设 项目流程

简介:基于STM32的PS2遥控小车项目资源包,面向嵌入式学习者和电子制作爱好者,覆盖PS2手柄解码、舵机控制与振动试验三大模块,可帮助读者从零搭建遥控小车控制系统。压缩包共95个文件,包含C/H源码、hex固件、Keil工程文件、PDF技术手册及文本说明,总大小仅1.04MB,源码与文档分类存放,便于按需查阅。包内附有PS2解码通讯手册及常见问题汇总,并配有STM32例程与Arduino参考代码,涵盖振动实验、舵机控制和遥控小车三个完整工程,从协议解析到PWM输出均有实现示例,方便对照移植与二次开发。对刚接触STM32及外设控制的读者,既能通过文档理解协议,也能直接运行例程观察效果。目前已有650人学习,适合需要快速入门PS2手柄通信或正在调试STM32小车项目的开发者参考借鉴。 做智能小车这个事,很多玩嵌入式的朋友迟早都会碰上一次。不过大部分人做完“手机蓝牙遥控+避障”就觉得到头了,其实这里面还藏着不少可以深挖的东西。我最近在STM32上把一套“PS2遥控小车+舵机云台控制+振动试验”的系统完整跑通了,整个过程比预想中要曲折,但收获也实实在在。PS2手柄的通信协议、舵机的PWM控制细节、振动数据的采集与分析,这三个模块单独拎出来都不算难,可一旦把它们塞进同一块板子、同一个电源系统里,各种“看不见的问题”就开始冒头了。这篇文章我就把这套系统的设计思路、关键代码、以及我踩过的坑都摊开来讲,给正准备上手或者已经卡在某个环节的朋友一个参考。

1. 这个项目到底在做一件什么事

1.1 系统功能拆解

先说清楚这套小车能干什么。整机包含三个核心功能模块:遥控底盘、舵机云台、振动采集。遥控底盘用的是PS2无线手柄,通过2.4GHz接收器与STM32通信,控制两个直流减速电机,实现前进、后退、转向和差速转弯。舵机云台装在小车前方,由一个PWM信号控制的舵机组成,用来带动摄像头或超声波传感器左右转动。振动采集部分则用振动传感器持续监测车体在行驶过程中的振动状态,既可以在不平整路面行驶时输出“路面状况”的粗略评估,也可以作为一个结构可靠性的简易检测手段。

这个项目的精妙之处在于,它把嵌入式开发里最常用的三大外设——通信接口(SPI/GPIO模拟)、定时器PWM、ADC数据采集——全部串到了一个完整场景中。做完这套系统,你基本就把STM32的常用外设练了一遍,而且是有反馈、有互动地练,不需要对着开发板干巴巴地跑例程。

1.2 为什么选STM32而不是Arduino

我知道很多入门玩家第一反应是用Arduino,毕竟PS2库、Servo库都是现成的,十几行代码就能动起来。但问题在于,Arduino把太多细节封装掉了,你根本不知道PS2接收器那根CLK线到底在跳什么,也不知道舵机为什么会有抖动的瞬间。STM32版的价值就在于,你得自己面对寄存器、自己配置CubeMX、自己处理中断和DMA。这套流程走完,你对整个系统的理解深度是Arduino给不了的。

另一个实际原因是在性能余量上。STM32F103C8T6主频72MHz,跑PS2通信和舵机PWM控制完全无压力,剩下的大量CPU时间可以用来做振动数据的实时处理和串口打印,后续想加OLED显示、PID闭环调速或者无线图传模块,都有足够资源扩展。

1.3 硬件清单与选型逻辑

我的核心板用的是最常见的STM32F103C8T6蓝色开发板,芯片本身是72MHz主频、64KB Flash、20KB RAM,价格便宜生态成熟,教程资料遍地都是。电机驱动选择TB6612FNG而不是L298N,原因很直接——L298N的导通压降太大了,在7.4V供电下电机实际得到的电压低不少,而且发热严重,TB6612的内阻小、体积小、效率高,电流也足够驱动普通的TT马达。

电机我用了两路带减速箱的TT马达,配合编码器接口(虽然这次没用编码器测速,但预留了扩展位)。舵机是SG90,9g小舵机,扭矩和速度足够带动轻量云台。振动传感器我用的是SW-420模块,它是数字量输出,电路上带一个比较器LM393,检测到振动时输出电平跳变,简单可靠。如果你想采集更精细的振动波形,可以换ADXL335模拟量加速度计或者MPU6050,后面我会单独讲两条路线的取舍。

供电系统是整车的另一条主线。电池用两节18650锂电池串联,标称7.4V,直接给TB6612驱动电机。另外用一颗AMS1117-5.0稳压模块把7.4V降到5V,给舵机和STM32开发板的5V引脚供电。开发板上的稳压电路再产生3.3V给PS2接收器和SW-420模块。三路供电分离,这个设计后面在联调部分会具体分析原因。

2. PS2手柄通信:那一套被很多人抄错的上报协议

2.1 PS2接收器怎么接线

PS2无线手柄接收器有6个引脚,除了VCC和GND之外,还有DAT(数据输出)、CMD(命令输入)、CS(片选)、CLK(时钟)。这四根信号线本质上是一个SPI-like的串行接口,STM32作为主机,PS2接收器作为从机。

接线对应关系如下:

PS2接收器引脚STM32引脚说明
VCC3.3V逻辑电平必须匹配
GNDGND与系统共地
DATPA6SPI1_MISO,也可以GPIO模拟
CMDPA7SPI1_MOSI
CSPA4SPI1_NSS
CLKPA5SPI1_SCK

这里有一个特别容易被坑的点:PS2接收器一定要接3.3V而不是5V,因为它的逻辑电平是3.3V,接5V电源虽然模块可能不烧,但返回的信号电平会达到5V,灌进STM32的引脚就超限了。反过来,如果接收器标称是5V供电的版本,则需要确认电平是否兼容3.3V逻辑。我的做法很保守,3.3V供电加外部上拉,实测非常稳定。

2.2 通信时序:一次轮询到底在干什么

PS2手柄的通信机制是命令应答模式,STM32每次向手柄发送一组命令字节,手柄返回一组状态字节,其中包含所有按键和摇杆的当前状态。

标准的正常模式通信会发送9个字节并接收9个字节,但如果只需要读取按键状态,我推荐使用“绿色模式”,发送1个字节就能取回1个字节,效率高出很多。简单说流程是这样的:

  1. CS拉低,启动通信
  2. CLK产生8个脉冲(或更多,取决于要读多少字节)
  3. 每个CLK下降沿,STM32通过CMD线发送一个bit,同时从DAT线读取一个bit
  4. CS拉高,结束通信,解析得到的数据

第一次初始化时,需要发送一些配置指令,把手柄配置到模拟模式。这也是为什么很多人的代码里有一段看不懂的数组,比如0x01, 0x44, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00——这段就是进入配置模式、启用模拟输出的指令序列。配置完成后,手柄的红色LED会亮起,此时左右摇杆的模拟值才有效。

下面是核心的解码函数,我用的是GPIO模拟方式,不依赖SPI外设,移植性更好:

uint8_t PS2_ReadByte(uint8_t cmd) { uint8_t res = 0; for (uint8_t i = 0; i < 8; i++) { CLK_LOW(); delay_us(5); if (cmd & 0x80) CMD_HIGH(); else CMD_LOW(); cmd <<= 1; delay_us(5); CLK_HIGH(); delay_us(5); res <<= 1; if (DAT_READ()) res |= 0x01; delay_us(5); } return res; } void PS2_ReadData(void) { CS_LOW(); delay_us(16); // 进入配置模式并读取数据 PS2_ReadByte(0x01); PS2_ReadByte(0x42); // 后面填充读取位 for (uint8_t i = 0; i < 6; i++) { PS2_Buffer[i] = PS2_ReadByte(0x00); } CS_HIGH(); // 解析 PS2_Buffer[0] 和 PS2_Buffer[1] 为按键状态 // PS2_Buffer[2]~[5] 为右x、右y、左x、左y 摇杆模拟值 }

按键解析的逻辑不复杂,每个bit对应一个按键,0表示按下,1表示松开。摇杆值范围是0x00~0xFF,中间值大约是0x80,对应摇杆居中位置。

2.3 解码库的选择:自带库还是自己写

Arduino生态里有一个非常流行的PS2X库,很多人直接移植到STM32上也能用。但我的建议是,如果你想真正理解这套系统的行为,第一版尽量自己写。不是说我反对用库,而是PS2通信的几个关键细节——时序延迟、CS拉低到第一个CLK的时间、以及绿色模式的字节数——如果不亲手调一遍,出了问题你根本无从下手。

我实测下来,CLK频率不能太高,一般控制在几十kHz。STM32的SPI外设最低分频后通常也有几百kHz,有些时候反而和PS2接收器的时序对不上,这也是很多人SPI配置得好好的却不工作的原因之一。GPIO模拟虽然代码看起来原始,但稳,而且不受SPI外设工作模式的影响。

2.4 实测中常见的解码失败原因

PS2手柄通信“大多数时间正常,偶尔卡一下”的现象非常常见,我总结了三个高频原因:

第一个是供电纹波。电机转动瞬间电流波动会传导到3.3V电源上,导致PS2接收器逻辑电平不稳定,数据偶尔出错。解决方法是PS2接收器的3.3V单独用一颗LDO供电,并且在电源引脚旁边加10uF和100nF电容做去耦。

第二个是CLK时序的微延迟。手柄内部对CLK建立时间有要求,如果程序里CLK拉低后立即发送数据位,手柄可能采样不到正确数据。每个bit之间插入5~10微秒的延迟,问题立刻消失。

第三个是共地问题。如果PS2接收器的GND和电机驱动的GND不连在一起,信号线上的参考电位是浮动的,数据必然乱。这个问题在新手那里出现频率极高。

3. 舵机控制:PWM不是发出来就完事

3.1 舵机的PWM参数到底怎么理解

舵机的控制说穿了就是PWM脉宽控制。SG90这类模拟舵机标准周期是20ms,即50Hz,在这个周期内,高电平脉宽从0.5ms到2.5ms,对应舵机从0度转到180度。0.5ms对应0度,1.0ms对应45度,1.5ms对应90度,2.5ms对应180度,基本上是线性映射。

很多人不理解为什么周期是20ms,而不是1ms或者100ms。原因是舵机内部的比较电路需要在一个周期内检测脉宽并调整电机旋转,如果周期太长,响应会迟钝;周期太短则舵机来不及完成比较,会出现抖舵。20ms是模拟舵机的行业标准值,数字舵机可以支持更高频率,但SG90按50Hz用就对了。

3.2 CubeMX里的定时器配置细节

我用TIM2的CH1输出PWM,引脚是PA0。在CubeMX里做如下配置:

  • 时钟源:内部时钟,APB1定时器时钟72MHz
  • 预分频器PSC = 71,这样计数频率是72MHz / (71+1) = 1MHz,每个计数单位是1微秒
  • 自动重载值ARR = 19999,周期是20000微秒,正好20ms
  • 初始脉冲宽度设为1500,即1.5ms,舵机初始停在90度

这样配置的价值在于,占空比寄存器的值直接就是微秒数,代码里不需要做复杂的换算。比如要让舵机转到90度,直接写__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1500)即可。

舵机角度到脉宽的换算函数:

void Servo_SetAngle(float angle) { // angle 范围 0~180 uint32_t pulse = (uint32_t)(500 + (angle / 180.0f) * 2000.0f); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse); }

这里500对应0.5ms,加上的范围是2000微秒,对应0~180度。需要注意,SG90实际可用脉宽范围可能略小于0.5~2.5ms,有些舵机在两端会堵转,实测多数SG90用550~2450比较安全。

3.3 供电问题:为什么一动舵机MCU就重启

这是我这次调试过程中遇到的最有代表性的问题。第一次给舵机上电的时候,只要转到一个大角度,整个开发板就瞬间重启,串口打印都来不及输出。用万用表一量,5V总线在舵机启动瞬间被拉低到了3V以下。

原因并不难分析:SG90堵转电流可以达到700mA~1A,AMS1117-5.0的最大输出电流也就800mA左右,而且它的压差特性决定了在负载急剧增加时输出电压会被拽下来。舵机一启动,5V总线电压跌落,STM32的工作电压也随之下降,直接触发欠压复位。

解决办法是把舵机供电从AMS1117的5V输出上移出去。我用了一路独立的5V降压模块(MP1584可调降压模块),电池7.4V直接进模块,输出5V单独给舵机供电,开发板的5V另一路单独供。最关键的是,舵机的GND必须和整个系统的GND共地,否则PWM信号没有参考基准,舵机根本不会转。

另外在舵机电源两端并联了一个大电容(470uF/16V),用来吸收启动瞬间的电流毛刺,效果立竿见影。这个电容在物理上离舵机接线端子越近越好。

4. 振动试验:从传感器选型到数据解读

4.1 振动检测的两条路线怎么选

振动采集的硬件方案主要分两种:开关量模块和模拟量传感器。开关量模块的典型代表就是我用的SW-420,它的核心是一个弹簧振动开关加LM393比较器,当振动幅度超过某个阈值时,比较器输出电平翻转。这种方案的优势是电路简单、不需要ADC采集,直接接GPIO就能检测,适合做碰撞报警、投放回收检测这类二值判断场景。

模拟量方案的典型代表是ADXL335三轴加速度计,它输出三路模拟电压,电压值对应加速度的大小和方向。用STM32的ADC采集后,你可以还原出连续的振动波形,进一步做FFT频谱分析、RMS统计等更精细的处理。MPU6050则是数字输出(I2C),内部已经集成了加速度计和陀螺仪,性能更好,但代码量也更大。

我的建议是:如果只想知道“当前有没有在振动”或者“这波振动大不大”,SW-420绰绰有余;如果后续想分析振动的频率成分,研究车体在不同路面上30Hz还是100Hz的振动更强,那就直接上MPU6050。我在这次项目中用SW-420解决了基本检测问题,同时预留了ADC输入接口,随时可以接ADXL335升级。

4.2 ADC+DMA连续采样,别用阻塞式delay

振动检测要有效果,采集方式比传感器本身更重要。SW-420虽然输出的是数字量,但它的输出在振动时是持续跳变的,你不可能用HAL_Delay在while循环里一遍遍去读GPIO——这样CPU被占满,其他任务全部堵死。

正确做法是ADC配合DMA连续采样。即使当前用的是SW-420,我也建议把它接到ADC引脚上:因为SW-420模块的实际输出在振动时会产生一个脉冲串,用ADC以较高的采样率把这个脉冲串记录下来,处理后可以得到振动持续时间和强度的粗略估计,比单纯看一个GPIO电平更有价值。这个思路为我后续升级模拟量传感器留了后路,采样代码完全不用改。

CubeMX里的配置流程:

  1. ADC1使能,通道选择IN1(PA1),分辨率12位
  2. 启用连续转换模式、DMA循环模式
  3. DMA设置为Circular模式,数据宽度Half Word
  4. 采样时间拉到最大(239.5周期),降低源阻抗影响
  5. 在代码里定义一个volatile uint16_t adc_buf[256],硬件会不断往这个数组填数据,不占用CPU

采集代码逻辑:

volatile uint16_t adc_buf[256]; volatile uint8_t data_ready = 0; volatile uint32_t adc_index = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { // 用 DMA 的当前计数判断填充位置 adc_index = 256 - __HAL_DMA_GET_COUNTER(&hdma_adc1); data_ready = 1; } }

4.3 简单的振动触发逻辑和阈值设定

采集到原始数据后,要定义一个“振动事件”。我用的是滑动窗口内的峰峰值(最大值减最小值)作为触发指标。窗口大小取64个采样点,采样率设置为1kHz,也就是每64ms评估一次。

具体逻辑是:每收到DMA半传输完成或传输完成中断,就把当前缓冲区扫描一遍,计算max-min。如果峰峰值超过预设阈值(比如SW-420直接输出的跳变幅度很大,阈值设成1.2V对应的ADC读数即可),就判定为一次有效振动事件,同时记录事件次数。为了避免连续抖动导致重复计数,我加入了一个冷却时间:事件触发后50ms内不再接受新事件。

对于SW-420这类数字输出模块,阈值设定很简单,但这里有个隐藏问题——SW-420的灵敏度是可调的(模块上有个蓝色电位器),拧得越紧,触发越灵敏。我实测默认位置的灵敏度在测试台上够用,一旦装到车体上,电机和齿轮的固有振动就可能误触发,必须把电位器往灵敏度低的方向调节,直到电机全速旋转时输出保持稳定。

4.4 顺便做了一次结构振动可靠性验证

这个项目的标题里带了“振动试验”,所以我额外做了一件事:用小车的实测场景验证了“振动试验”的另一层含义——结构可靠性。方法是把舵机当作激振器,让舵机以2Hz的频率在0度和90度之间反复摆动,利用舵机转动时的反作用力让整个底盘产生振动,同时用SW-420监测底盘上电池固定架、PCB底板、传感器安装位的振动响应。

这个试验的朴素逻辑是:如果某个机械连接件松动,它的振动响应会明显区别于紧固状态。连续让舵机摆动5分钟,再检查所有螺丝和插接件是否松动。实测下来,电池盒的螺丝确实有轻微松脱迹象,这提醒我一个在普通小车项目里不会注意到的细节——振动环境下的机械设计要考虑防松措施,比如弹簧垫圈、螺纹胶或者扎带固定。这个“以舵机为激振源的简易振动台”方案成本为零,效果却很直观,推荐有条件的同学试一试。

5. 整车联调:接线、供电、地线这些容易翻车的地方

5.1 整车接线一览

联调之前先把整车的接线关系理清楚。我的整车连接如下:

模块引脚/接口连接目标
PS2接收器VCC3.3V
PS2接收器GNDGND
PS2接收器DAT/CMD/CS/CLKPA6/PA7/PA4/PA5
TB6612VM电池7.4V
TB6612VCC5V(逻辑电源)
TB6612AIN1/AIN2、BIN1/BIN2两个方向控制引脚
TB6612PWMA/PWMBTIM1的PWM输出
舵机SG90红线独立5V模块输出
舵机SG90棕线GND
舵机SG90橙线PA0(TIM2_CH1)
SW-420VCC/GND/DO3.3V/GND/PA1(ADC1_IN1)

特别注意TB6612的逻辑电源VCC是5V,它的逻辑输入引脚和STM32的3.3V GPIO并不总是兼容,建议查一下模块的规格。我用的这款模块逻辑输入高电平最低大概1.5V,所以3.3V直接驱动没问题。但如果你用的是某些需要5V逻辑电平的模块,可能需要加电平转换或者用开漏加外部上拉的方式。

5.2 供电拓扑:为什么必须三路分离

这个项目我把供电分成了三路,很多人觉得麻烦,但这是稳定运行的底线。

第一路是电机驱动路,直接接7.4V电池。电机是电流负载,工作时电流波动大,而且有反电动势冲击。如果让这个波动进入5V逻辑电源,MCU就会不稳定。

第二路是舵机路,独立5V模块输出。舵机瞬时电流可以达到1A,是板上最大的电流尖峰来源,必须单独供电并加电容。

第三路是逻辑路,7.4V先降到5V给开发板,再由板载稳压器产生3.3V。PS2接收器和SW-420都挂在3.3V上,因为它们的逻辑电平和STM32一致。

三路电源的GND必须全连在一起,这是整个系统正常通信的前提。地线网我用了一条粗的杜邦线把所有GND节点串起来,又额外用一段飞线把电池负极、TB6612 GND、5V模块GND和开发板GND做了星型汇聚,实测噪声明显低于串联连接。

5.3 联调流程:从最薄弱环节开始验证

整个系统不建议一次性全部上电,我的调试验证顺序是这样的:

第一步,只给开发板USB供电,屏蔽所有外设,先跑PS2解码程序。手柄按按键,串口能打印对应键值说明PS2通道OK。

第二步,PS2通道OK后,接入舵机(用独立5V模块供电),串口发送指令让舵机从0度转到180度再转回来,确认舵机转动顺滑且无抖动。

第三步,接入TB6612和电机,用PS2按键控制电机正反转和PWM调速。这个阶段要重点听电机声音,如果出现明显的啸叫或顿挫感,多半是PWM频率在可听范围内,需要把PWM频率从1kHz提到20kHz左右。我用的TIM1 PWM输出,直接配置输出频率16kHz,人耳基本听不到噪声。

第四步,最后接入SW-420振动模块,在静止状态下确认传感器无触发,然后用手敲击桌面,串口打印应该输出一次振动事件。全部OK之后再整合到一个主循环里跑完整功能。

5.4 我踩过的三个比较典型的坑

第一个坑是PS2接收器的供电问题。最开始我把PS2接收器直接接到了5V引脚上,通信时好时坏,用示波器测量DAT线发现高电平达到了4.6V,超出了STM32的耐压范围。虽然F103的引脚标称容忍5V,但PS2接收器本身是3.3V器件,长期这样接寿命堪忧。改成3.3V后问题彻底消失。

第二个坑是舵机角度突变导致的电压跌落。最初我用按键直接控制舵机在两个极限角度之间跳转,每次跳转都能看到SW-420误报一次振动事件。后来把舵机转动改成了渐变处理,在中断或定时器里逐步改变占空比值,每次增加一小步,舵机转得平滑了,误触发也消失了。

第三个坑是ADC采样和主循环之间的数据竞争。DMA是后台运行的,主循环随时可能去读adc_buf,如果正好读到DMA写了一半的数据,会得到一些奇怪的中间值。解决方法是把缓冲区做成双缓冲结构,或者参考DMA的当前计数器来判断哪些数据是安全的。我用的是后一种方案,在回调函数里记录当前索引,主循环只处理索引之前的数据。

6. 实测效果与几个可以继续玩的方向

6.1 整车实测效果

整套系统跑起来之后,最直观的体验是PS2手柄控制的流畅度比蓝牙方案好很多。PS2手柄是2.4GHz专用通信,延迟大概在几毫秒量级,转向反应几乎没有可感知的延迟,和有线手柄的手感几乎一致。左摇杆控制前进后退和转向,右摇杆直接控制舵机云台左右旋转,右手拇指一拨,摄像头方向就跟着走,这种“人机合一”的体验确实值得拥有。

振动检测部分,在平坦地面上行驶时,SW-420几乎没有触发输出。走过减速带或者路面有裂缝时,触发的振动事件能在串口里清楚看到。把车停下来原地转动机身,让轮子在地面打滑,振动传感器的响应非常灵敏,能检测到和正常行驶完全不同的高频振动波形。

6.2 几个值得继续扩展的方向

第一,加编码器电机测速。TB6612只是驱动,没有闭环概念。换带霍尔编码器的电机,用STM32的定时器编码器模式读取转速,可以做一个简单的PID闭环调速,让小车在不同负载下保持恒速。

第二,振动分析升级到MPU6050。SW-420只能判断有没有振动,MPU6050可以告诉你是哪个方向、多大频率在振动。把ADC采集代码换成I2C读取,把振动波形用串口发到电脑上,在Python里做FFT,分析不同路面状态下的振动频谱,这个项目就能升级成一个小型的数据采集分析系统。

第三,接入ESP8266模块。用串口把MCU的状态同步到局域网,手机网页上可以实时看到当前按键状态、舵机角度和振动事件次数。这一步主要练一下AT指令的使用和状态同步协议的设计,对小项目的可玩性提升非常明显。

第四,把这个遥控系统移植成自动巡线。手柄不是目标,只是调试工具。用舵机云台上的传感器识别路径信息,让小车自主循迹,PS2手柄作为人工接管手段,在紧急情况下打断自动控制。这个结构其实就是一个简化版的自主移动机器人原型了。

整套项目做下来,最大的体会是:STM32开发里真正花时间的不是写那些“看起来高级”的外设驱动代码,而是让几个外设在同一个电源环境、同一个中断优先级体系中稳定协同工作。PS2的时序、舵机的供电、DMA和主循环的协作,每一个环节单独跑都没问题,组合在一起才是考验内功的时候。希望这篇文章能帮你绕开我走过的弯路,把你的遥控小车顺利跑起来。

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

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

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

立即咨询