
1. 从一个吃灰的PS2手柄说起手头攒了太多电子垃圾这是每个折腾嵌入式的人都逃不过的宿命。前阵子翻箱子翻出来一个落满灰的PS2手柄接收器就是那种十几年前插在电脑USB口上、用来接两个PS2手柄玩模拟器的小板子。当年这东西便宜得很现在早就被蓝牙手柄取代了。但扔了可惜拆开看看里面是什么芯片说不定还能废物利用一下。拆开之后发现接收器里面是一颗常见的USB HID转接芯片负责把两个PS2手柄的信号转成USB协议发给电脑。而PS2手柄本身其实是一个非常经典的嵌入式外设——它用的是SPI类似的同步串行协议只需要几根线就能读出按键和摇杆的全部状态。这就意味着只要我有一块带SPI接口的单片机板子就能直接跟PS2手柄通信不需要那个接收器。于是就有了这个项目用一块STM32最小系统板直接读取PS2手柄的数据并且用手柄上的按键去控制板载的LED灯。听起来很简单对吧但实际做下来从协议时序到电源电平从按键映射到调试排错坑是一个接一个。这篇文章就把整个折腾过程完整记录下来包括硬件接线、协议解析、代码实现、调试技巧以及我踩过的那些坑。如果你手头也有吃灰的PS2手柄或者想找一个练手SPI通信的小项目这篇内容应该能帮你省下不少时间。这个项目适合谁呢刚学完STM32的GPIO和SPI外设、想找个实际项目练手的朋友手头有PS2手柄想改造利用的电子爱好者以及需要给某个设备加一个低成本物理控制器的开发者。不需要太深的基础但至少要会用Keil或者STM32CubeIDE建工程、会看时序图、会用万用表测通断。下面我会尽量把每一步都讲清楚包括为什么这么做、不这么做会怎样。2. 整体方案设计与选型考量2.1 为什么选PS2手柄而不是其他方案市面上能买到的物理控制器方案其实不少蓝牙手柄、2.4G无线手柄、USB手柄、甚至自己用摇杆模块搭一个。但PS2手柄有几个独特的优势让它特别适合嵌入式练手项目。第一是协议简单且公开。PS2手柄用的是类似SPI的同步串行协议时钟频率可以低到几十kHz对单片机没有任何压力。协议文档在网上流传很广数据格式清晰不需要逆向工程。相比之下蓝牙手柄要处理蓝牙协议栈USB手柄要写USB Host驱动复杂度高了好几个数量级。第二是硬件成本极低。一个二手PS2手柄加一个接收器在二手市场上可能只要十几块钱。如果只用手柄本体连接收器都不需要直接飞线就能用。手柄内部有现成的按键、摇杆、震动马达结构完整手感也不错比买散件自己搭要划算得多。第三是供电简单。PS2手柄工作电压是3.3V到5V电流大概几十毫安直接用单片机板子上的3.3V或者5V供电就行不需要额外的电源管理。这一点比某些需要特定电压的传感器要友好得多。当然也有缺点。PS2手柄的接口是那种薄薄的排线插座不太好焊接需要小心操作。而且手柄线缆里除了电源和地还有几根信号线颜色定义在不同批次的手柄上可能不一样需要自己测。但这些问题都有成熟的解决方案后面会详细说。2.2 主控板的选择与资源分配我手头用的是一块常见的STM32F103C8T6最小系统板也就是大家常说的“蓝色药丸”。这块板子便宜、资料多、GPIO丰富用来做这个项目绰绰有余。选它的理由很简单有硬件SPI接口可以配置成主机模式有足够的GPIO做片选和应答板载LED可以直接用来做实验不需要额外接线。具体资源分配是这样的SPI1的SCK和MOSI用来给手柄提供时钟和发送命令MISO用来接收手柄返回的数据。片选信号ATT用任意一个GPIO控制我选的是PA4。手柄还有一个ACK信号用来指示它什么时候准备好数据我接在PA5上。板载LED用的是PC13这是蓝色药丸上自带的LED低电平点亮。这里有个细节需要注意PS2手柄的SPI协议跟标准SPI不太一样。标准SPI是全双工的主机发一个字节的同时从机回一个字节。但PS2手柄是半双工的主机先发命令然后手柄再回数据中间有一个应答过程。所以不能直接用STM32的硬件SPI外设去读需要手动控制时序。我试过用硬件SPI发现时序对不上最后还是老老实实用GPIO模拟SPI。这一点后面会详细解释。2.3 电平匹配与电源方案PS2手柄的工作电压范围是3.3V到5V但它的信号线电平是跟供电电压相关的。如果手柄用3.3V供电信号线就是3.3V电平可以直接跟STM32的3.3V GPIO对接。如果手柄用5V供电信号线就是5V电平直接接STM32的GPIO可能会损坏芯片。我一开始想用手柄原装的5V供电因为接收器是插USB的USB就是5V。但后来发现STM32的GPIO耐压虽然是5V部分引脚但为了保险起见还是统一用3.3V供电更稳妥。实测下来PS2手柄在3.3V下工作完全正常按键响应和摇杆数据都没有问题。震动马达可能会因为电压低而力度弱一些但这个项目里用不到震动所以无所谓。电源方面我直接从蓝色药丸的3.3V引脚取电给手柄。需要注意的是PS2手柄的电流峰值可能达到50mA左右如果板子上的3.3V稳压器余量不够可能会导致单片机复位。我实测下来蓝色药丸上的AMS1117稳压器带一个手柄没问题但如果同时接两个手柄或者加上其他外设最好还是单独供电。接线方面PS2手柄的排线里通常有9根线但实际用到的只有6根电源、地、ATT、CMD、CLK、DAT。有些手柄还有ACK和震动马达的电源线。不同批次的手柄线序可能不同我建议先用万用表测一下找到电源和地然后再根据协议文档确定信号线。网上有现成的PS2手柄引脚图但最好还是自己验证一下避免接错烧板子。3. PS2手柄协议深度解析与实操要点3.1 手柄接口的物理层与信号定义PS2手柄的接口是一个9针的排线插座但实际用到的引脚只有6个。从手柄端看过去引脚定义通常是这样的1脚是DAT数据2脚是CMD命令3脚是震动马达电源4脚是GND5脚是VCC3.3V6脚是ATT片选7脚是CLK时钟8脚是空9脚是ACK应答。但不同厂家、不同批次的手柄线序可能会有差异所以一定要先确认。我手头这个手柄的排线颜色是棕色、橙色、红色、黑色、绿色、蓝色、黄色、灰色、白色。用万用表测了一下黑色是GND红色是VCC其他几根需要根据协议来判断。我的方法是先接好电源和地然后用示波器或者逻辑分析仪看手柄上电后的信号线状态。ATT和CLK在空闲时是高电平CMD和DAT也是高电平。如果某根线一直是低电平那可能是震动马达的电源或者ACK。确认线序之后接线就简单了。我把手柄的DAT接到STM32的MISOPA6CMD接到MOSIPA7CLK接到SCKPA5ATT接到PA4ACK接到PA3。VCC和GND分别接3.3V和地。注意PS2手柄的DAT和CMD线需要上拉电阻通常手柄内部已经有上拉了但如果没有需要在单片机端加上4.7k到10k的上拉电阻。我实测下来手柄内部有上拉所以直接接就行。注意接线之前一定要断电接好之后再上电。热插拔可能会导致手柄内部的芯片损坏尤其是电源线接反的情况。我有个朋友就是因为电源接反把手柄烧了虽然不贵但重新买一个也要等快递。3.2 通信时序的逐帧拆解PS2手柄的通信协议是主机发起、从机响应的模式。一次完整的通信包含8个字节的数据交换每个字节8位低位在前。主机先拉低ATT然后在每个时钟周期发送一位命令同时从机在时钟下降沿之后把数据放到DAT线上。主机在时钟上升沿读取DAT线上的数据。具体时序是这样的空闲时ATT、CLK、CMD都是高电平。通信开始时主机拉低ATT然后开始发送第一个字节0x01这是起始命令。发送完8位之后主机释放CMD线然后发送一个字节0xFF作为占位同时读取从机返回的第一个字节。从机返回的第一个字节通常是0xFF或者0x41表示手柄的类型。接下来的几个字节依次是按键数据、摇杆数据等。这里有个关键点PS2手柄的时钟极性是空闲高电平数据在下降沿改变在上升沿采样。这跟标准SPI的Mode 3CPOL1CPHA1有点像但又不完全一样。标准SPI是全双工的主机发一位的同时从机回一位。但PS2手柄是主机先发命令然后从机再回数据中间有一个字节的延迟。所以不能用硬件SPI的收发函数直接读需要手动控制时序。我一开始尝试用STM32的硬件SPI配置成Mode 3然后调用HAL_SPI_TransmitReceive函数。结果读出来的数据全是0xFF完全不对。后来用逻辑分析仪抓了一下时序发现硬件SPI的时钟频率太高而且片选信号的控制方式也不对。PS2手柄要求时钟频率在100kHz到500kHz之间太高了不行。而且ATT信号需要在发送第一个字节之前拉低发送完最后一个字节之后拉高中间不能有间隙。硬件SPI的片选控制是自动的很难精确控制这个时序。最后我还是用GPIO模拟SPI手动控制CLK和CMD线在时钟上升沿读取DAT线。这样虽然代码麻烦一点但时序完全可控实测下来非常稳定。下面是一个典型的通信时序// 拉低ATT开始通信 GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 发送第一个字节0x01 for (int i 0; i 8; i) { // 在时钟下降沿设置CMD线 GPIO_ResetBits(GPIOA, GPIO_Pin_5); if (cmd 0x01) { GPIO_SetBits(GPIOA, GPIO_Pin_7); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_7); } cmd 1; // 在时钟上升沿读取DAT线 GPIO_SetBits(GPIOA, GPIO_Pin_5); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)) { data | (1 i); } } // 发送完8位之后释放CMD线 GPIO_SetBits(GPIOA, GPIO_Pin_7); // 继续发送0xFF读取从机返回的数据 // ... // 拉高ATT结束通信 GPIO_SetBits(GPIOA, GPIO_Pin_4);这段代码只是示意实际使用的时候需要把8个字节的收发都封装好。注意每个字节之间要有一定的延时通常10微秒左右就够了。延时太短了手柄反应不过来太长了会影响刷新率。我实测下来10微秒的延时可以稳定工作手柄的刷新率大概在100Hz左右完全够用。3.3 数据帧格式与按键映射PS2手柄返回的数据帧是8个字节每个字节的含义如下字节序号含义说明0起始字节通常为0xFF或0x411手柄类型0x41表示数字手柄0x73表示模拟手柄2按键数据1包含SELECT、L3、R3、START、上、右、下、左3按键数据2包含L2、R2、L1、R1、三角、圆圈、叉、方块4右摇杆X0x00到0xFF中间值0x805右摇杆Y0x00到0xFF中间值0x806左摇杆X0x00到0xFF中间值0x807左摇杆Y0x00到0xFF中间值0x80按键数据是低电平有效的也就是说如果某个按键被按下对应的位是0否则是1。比如字节2的bit0是SELECT键如果SELECT被按下bit0就是0。字节3的bit3是三角键如果三角被按下bit3就是0。这里有个坑不同型号的手柄数据帧的格式可能略有不同。比如有些第三方手柄的起始字节不是0xFF而是0x41或者0x79。而且模拟手柄和数字手柄的返回数据长度也不一样。数字手柄只返回前4个字节后面的摇杆数据是0xFF。模拟手柄返回全部8个字节。我手头这个是模拟手柄所以能读到摇杆数据。还有一个坑手柄的按键是低电平有效的但摇杆数据是正常的0x00到0xFF。所以在处理按键的时候需要取反或者判断是否为0。我一开始没注意以为按键按下是1结果读出来的数据全是反的调了半天才发现这个问题。3.4 供电与信号完整性的注意事项PS2手柄的供电电压范围是3.3V到5V但它的信号线电平是跟供电电压相关的。如果手柄用3.3V供电信号线就是3.3V电平可以直接跟STM32的3.3V GPIO对接。如果手柄用5V供电信号线就是5V电平直接接STM32的GPIO可能会损坏芯片。我一开始想用手柄原装的5V供电因为接收器是插USB的USB就是5V。但后来发现STM32的GPIO耐压虽然是5V部分引脚但为了保险起见还是统一用3.3V供电更稳妥。实测下来PS2手柄在3.3V下工作完全正常按键响应和摇杆数据都没有问题。震动马达可能会因为电压低而力度弱一些但这个项目里用不到震动所以无所谓。电源方面我直接从蓝色药丸的3.3V引脚取电给手柄。需要注意的是PS2手柄的电流峰值可能达到50mA左右如果板子上的3.3V稳压器余量不够可能会导致单片机复位。我实测下来蓝色药丸上的AMS1117稳压器带一个手柄没问题但如果同时接两个手柄或者加上其他外设最好还是单独供电。信号完整性方面PS2手柄的通信速率不高所以对走线要求不严格。但如果线太长比如超过30厘米可能会出现数据错误。我建议尽量用手柄原装的排线不要自己延长。如果一定要延长最好用屏蔽线并且在单片机端加上拉电阻。另外CLK和DAT线不要跟电源线平行走避免串扰。4. 代码实现与核心环节拆解4.1 GPIO模拟SPI的底层驱动前面说了PS2手柄的时序跟标准SPI不太一样所以不能用硬件SPI外设。我用的是GPIO模拟的方式手动控制CLK、CMD、ATT和ACK线。下面是一个完整的字节收发函数// 向PS2手柄发送一个字节并返回接收到的字节 uint8_t PS2_Transfer(uint8_t cmd) { uint8_t data 0; for (int i 0; i 8; i) { // 在时钟下降沿设置CMD线 GPIO_ResetBits(GPIOA, GPIO_Pin_5); // CLK低 if (cmd 0x01) { GPIO_SetBits(GPIOA, GPIO_Pin_7); // CMD高 } else { GPIO_ResetBits(GPIOA, GPIO_Pin_7); // CMD低 } cmd 1; // 延时保证时序 delay_us(5); // 在时钟上升沿读取DAT线 GPIO_SetBits(GPIOA, GPIO_Pin_5); // CLK高 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6)) { data | (1 i); } delay_us(5); } return data; }这个函数每次收发一个字节低位在前。注意在时钟下降沿设置CMD线在时钟上升沿读取DAT线。延时5微秒是为了保证时序实测下来这个延时可以稳定工作。如果延时太短手柄可能反应不过来如果太长刷新率会下降。接下来是完整的通信函数一次读取手柄的全部数据// 读取PS2手柄的数据 void PS2_ReadData(uint8_t *data) { // 拉低ATT开始通信 GPIO_ResetBits(GPIOA, GPIO_Pin_4); // 发送起始命令0x01 PS2_Transfer(0x01); // 发送0x42请求数据 PS2_Transfer(0x42); // 发送0x00占位 PS2_Transfer(0x00); // 读取后续数据 for (int i 0; i 6; i) { data[i] PS2_Transfer(0x00); } // 拉高ATT结束通信 GPIO_SetBits(GPIOA, GPIO_Pin_4); // 字节之间的延时 delay_us(10); }这个函数先发送0x01作为起始命令然后发送0x42请求数据接着发送0x00占位最后读取6个字节的数据。注意第一个字节0x01是固定的第二个字节0x42是请求数据后面的0x00是占位因为手柄需要时间准备数据。读取到的6个字节依次是手柄类型、按键数据1、按键数据2、右摇杆X、右摇杆Y、左摇杆X、左摇杆Y。实际上第一个字节是0xFF或者0x41第二个字节才是手柄类型。这里有个细节PS2手柄在收到0x01之后会返回一个0xFF或者0x41表示它已经准备好了。然后主机发送0x42手柄返回0x5A表示确认。接着主机发送0x00手柄返回按键数据。所以完整的通信过程是0x01 - 0xFF0x42 - 0x5A0x00 - 按键数据10x00 - 按键数据2以此类推。我一开始没注意这个细节以为发送0x01之后直接读数据就行结果读出来的全是0xFF。后来看了协议文档才发现需要先发送0x01等手柄返回0xFF之后再发送0x42等手柄返回0x5A之后才能开始读数据。这个握手过程很重要少了这一步手柄不会返回有效数据。4.2 按键解析与LED控制逻辑读取到手柄数据之后下一步就是解析按键然后控制LED。PS2手柄的按键数据是低电平有效的所以需要取反或者判断是否为0。下面是一个简单的按键解析函数// 解析按键数据返回按键状态 uint16_t PS2_GetButtons(uint8_t *data) { uint16_t buttons 0; // 字节2的bit0到bit7分别对应SELECT、L3、R3、START、上、右、下、左 if (!(data[1] 0x01)) buttons | BUTTON_SELECT; if (!(data[1] 0x02)) buttons | BUTTON_L3; if (!(data[1] 0x04)) buttons | BUTTON_R3; if (!(data[1] 0x08)) buttons | BUTTON_START; if (!(data[1] 0x10)) buttons | BUTTON_UP; if (!(data[1] 0x20)) buttons | BUTTON_RIGHT; if (!(data[1] 0x40)) buttons | BUTTON_DOWN; if (!(data[1] 0x80)) buttons | BUTTON_LEFT; // 字节3的bit0到bit7分别对应L2、R2、L1、R1、三角、圆圈、叉、方块 if (!(data[2] 0x01)) buttons | BUTTON_L2; if (!(data[2] 0x02)) buttons | BUTTON_R2; if (!(data[2] 0x04)) buttons | BUTTON_L1; if (!(data[2] 0x08)) buttons | BUTTON_R1; if (!(data[2] 0x10)) buttons | BUTTON_TRIANGLE; if (!(data[2] 0x20)) buttons | BUTTON_CIRCLE; if (!(data[2] 0x40)) buttons | BUTTON_CROSS; if (!(data[2] 0x80)) buttons | BUTTON_SQUARE; return buttons; }这个函数把按键数据解析成一个16位的位掩码每个位对应一个按键。然后就可以根据按键状态控制LED了。比如按下叉键点亮LED松开熄灭// 主循环 while (1) { uint8_t data[8]; PS2_ReadData(data); uint16_t buttons PS2_GetButtons(data); if (buttons BUTTON_CROSS) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 点亮LED } else { GPIO_SetBits(GPIOC, GPIO_Pin_13); // 熄灭LED } delay_ms(10); }这段代码很简单但实际使用的时候需要注意几点。第一PS2_ReadData函数需要一定的时间大概几百微秒所以主循环的刷新率不会太高。实测下来10毫秒的延时可以稳定工作LED的响应也很跟手。第二按键需要消抖虽然PS2手柄的按键质量不错但偶尔也会有抖动。我加了一个简单的软件消抖连续两次读到相同的按键状态才认为有效。4.3 摇杆数据的处理与扩展应用除了按键PS2手柄还有两个摇杆每个摇杆有两个轴总共4个模拟量。摇杆数据的范围是0x00到0xFF中间值是0x80。我可以用摇杆来控制LED的亮度或者控制其他外设。比如用左摇杆的X轴控制PWM的占空比从而调节LED的亮度// 读取左摇杆X轴的值 uint8_t lx data[5]; // 把0x00到0xFF映射到PWM的占空比 uint16_t duty (lx * 1000) / 255; // 设置PWM占空比 TIM_SetCompare1(TIM2, duty);这段代码把摇杆的0x00到0xFF映射到PWM的0到1000然后设置定时器的比较值。实测下来摇杆的控制非常平滑LED的亮度变化也很自然。不过需要注意的是摇杆的中间值可能不是精确的0x80会有一些偏差。我实测下来中间值大概在0x7C到0x84之间波动。如果要做精确控制需要先校准中间值。摇杆还有一个问题它的输出是模拟量但STM32的GPIO是数字的所以只能通过SPI读取数字化的摇杆值。PS2手柄内部有一个ADC把摇杆的模拟量转换成8位的数字量。这个ADC的精度不高只有8位所以摇杆的分辨率有限。如果需要更高的精度可能需要换用其他手柄或者自己加ADC。4.4 完整工程结构与编译配置整个工程的目录结构大概是这样的PS2_LED_Control/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── ps2.h │ │ └── stm32f1xx_hal_conf.h │ └── Src/ │ ├── main.c │ ├── ps2.c │ └── stm32f1xx_hal_msp.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── MDK-ARM/ │ └── PS2_LED_Control.uvprojx └── README.mdps2.c里面是PS2手柄的驱动代码包括GPIO初始化、字节收发、数据读取和按键解析。main.c里面是主循环和LED控制逻辑。编译配置方面我用的是Keil MDK需要开启STM32F1xx的HAL库并且配置好时钟和GPIO。注意GPIO的初始化需要把CLK、CMD、ATT配置成推挽输出DAT和ACK配置成上拉输入。SPI的时钟频率不需要配置因为用的是GPIO模拟。编译的时候可能会遇到一些问题比如HAL库的版本不匹配、头文件路径不对等。我建议直接用STM32CubeMX生成工程然后手动添加ps2.c和ps2.h。CubeMX会自动配置好时钟和GPIO省去很多麻烦。生成工程之后把PS2手柄的驱动代码加进去然后在main.c里面调用就行了。提示如果编译的时候提示找不到HAL库的头文件检查一下Include Paths里面有没有添加Drivers/STM32F1xx_HAL_Driver/Inc和Drivers/CMSIS/Device/ST/STM32F1xx/Include。这两个路径是必须的。5. 调试排错与常见问题实录5.1 读不到数据怎么办这是最常见的问题也是最让人头疼的。我一开始接好线之后读出来的数据全是0xFF完全不对。排查了半天发现是几个原因叠加在一起。第一个原因是时序不对。PS2手柄要求时钟频率在100kHz到500kHz之间我一开始用的延时是1微秒时钟频率大概在500kHz左右理论上没问题。但实际用逻辑分析仪抓了一下发现时钟的占空比不对高电平时间太短手柄反应不过来。后来把延时改成5微秒时钟频率降到100kHz左右就正常了。第二个原因是片选信号的控制不对。PS2手柄要求ATT信号在发送第一个字节之前拉低发送完最后一个字节之后拉高中间不能有间隙。我一开始用的是硬件SPI的片选控制结果片选信号在字节之间会拉高导致手柄复位。后来改成手动控制ATT问题就解决了。第三个原因是电源问题。我一开始用手柄原装的5V供电但STM32的GPIO是3.3V的电平不匹配。虽然STM32的GPIO耐压是5V但输入高电平的门限是0.7VDD也就是2.31V。5V的信号虽然不会烧芯片但可能会导致输入不稳定。后来改成3.3V供电问题就解决了。排查的时候我建议先用逻辑分析仪或者示波器抓一下时序看看CLK、CMD、DAT和ATT的信号是否正常。如果没有逻辑分析仪可以用万用表测一下电压ATT和CLK在空闲时应该是高电平通信时会有脉冲。如果电压不对检查一下接线和电源。5.2 按键响应不灵敏或误触发按键响应不灵敏或者误触发通常有几个原因。第一个原因是消抖没做好。PS2手柄的按键是机械触点按下和松开的时候会有抖动如果不消抖可能会读到多次按键。我加了一个简单的软件消抖连续两次读到相同的按键状态才认为有效问题就解决了。第二个原因是刷新率太低。如果主循环的延时太长按键的响应会变慢。我一开始用的是100毫秒的延时结果按键要按很久才有反应。后来改成10毫秒响应就快多了。但延时也不能太短太短了手柄反应不过来数据会出错。实测下来10毫秒是一个比较合适的值。第三个原因是电源噪声。如果手柄的电源不干净可能会导致数据错误。我建议在电源引脚旁边加一个100微法的电解电容和一个0.1微法的陶瓷电容滤除高频和低频噪声。实测下来加了电容之后数据的稳定性明显提高。还有一个可能的原因是手柄本身的问题。有些第三方手柄的兼容性不好返回的数据格式可能跟原装手柄不一样。如果排查了半天还是不行换一个原装手柄试试。我手头这个手柄是原装的所以没遇到这个问题但网上有人反映第三方手柄会有兼容性问题。5.3 常见问题速查表问题现象可能原因排查方法解决方案读不到数据全是0xFF时序不对用逻辑分析仪抓时序调整延时降低时钟频率读不到数据全是0x00接线错误检查DAT和CMD线是否接反交换DAT和CMD线数据偶尔出错电源噪声用示波器看电源纹波加滤波电容按键响应慢刷新率太低检查主循环延时减小延时到10毫秒按键误触发消抖没做好检查按键数据是否抖动加软件消抖摇杆数据不准中间值偏差读取摇杆中间值校准中间值手柄发热电源接反检查VCC和GND立即断电重新接线单片机复位电源电流不足测量3.3V电压单独给手柄供电这个表格是我在实际调试中总结出来的基本上涵盖了常见的问题。如果你遇到了其他问题可以在评论区留言我会尽量帮忙排查。5.4 几个容易被忽略的细节第一个细节是PS2手柄的排线插座。那个插座是1.0毫米间距的焊接的时候需要小心不要短路。我建议用细一点的漆包线先焊好再插到手柄上。如果直接用手柄原装的排线注意不要反复弯折容易断。第二个细节是STM32的GPIO模式。CLK、CMD、ATT需要配置成推挽输出DAT和ACK需要配置成上拉输入。如果配置错了可能会导致信号无法正常传输。我一开始把DAT配置成了推挽输出结果读不到数据后来改成上拉输入就好了。第三个细节是延时函数。我用的是HAL_Delay但这个函数是毫秒级的微秒级的延时需要自己实现。我用的是SysTick定时器配置成1微秒的中断然后在中断里计数。也可以用空循环来实现但精度不高。我建议用SysTick精度高而且不占用CPU。第四个细节是手柄的初始化。PS2手柄上电之后需要一定的时间来初始化大概几百毫秒。如果上电之后立刻通信可能会读不到数据。我建议上电之后延时500毫秒再开始通信。实测下来这个延时是必要的。第五个细节是震动马达。PS2手柄的震动马达需要额外的电源通常是7.5V左右。如果只给3.3V震动马达不会转但不会影响按键和摇杆的功能。如果需要震动功能需要单独给马达供电并且加一个驱动电路。这个项目里用不到震动所以没接。6. 项目扩展与个人经验分享6.1 从LED控制到更多应用场景这个项目虽然简单但扩展性很强。掌握了PS2手柄的通信协议之后你可以把它用到很多地方。比如用摇杆控制舵机做一个简单的机械臂用按键控制继电器做一个智能家居的控制器或者用摇杆控制电机的转速和方向做一个小车。PS2手柄的按键多、摇杆精度够用、成本低非常适合做原型验证。我自己后来把这个项目扩展了一下用左摇杆控制两个舵机做了一个二自由度的云台。右摇杆控制LED的亮度和颜色按键控制云台的复位和校准。整个系统用一块STM32F103就能搞定成本不到50块钱。如果你手头有吃灰的PS2手柄不妨试试这个方向。还有一个有意思的扩展是用PS2手柄控制电脑。虽然PS2手柄原本就是用来玩游戏的但通过STM32转成USB HID设备可以让电脑把它识别成一个标准的游戏手柄。这样就不用买那个接收器了直接一根USB线就能用。这个扩展需要用到STM32的USB外设稍微复杂一点但网上有现成的例程可以参考。6.2 我踩过的坑与实操心得第一个坑是电源接反。我一开始没注意线序把VCC和GND接反了结果手柄发热幸好发现得早没烧坏。后来我用万用表测了一下确认了线序才接。所以接线之前一定要测一下不要凭感觉。第二个坑是时序不对。我一开始用硬件SPI配置成Mode 3结果读出来的数据全是0xFF。后来用逻辑分析仪抓了一下发现硬件SPI的时钟频率太高而且片选信号的控制方式也不对。最后改成GPIO模拟SPI问题就解决了。所以不要迷信硬件外设有时候软件模拟更灵活。第三个坑是消抖没做好。我一开始没加消抖结果按键偶尔会误触发。后来加了一个简单的软件消抖连续两次读到相同的按键状态才认为有效问题就解决了。消抖的时间不能太长太长会影响响应速度我实测下来10毫秒的消抖时间比较合适。第四个坑是摇杆的中间值偏差。我一开始以为摇杆的中间值是0x80结果实测下来是0x7C到0x84之间波动。如果要做精确控制需要先校准中间值。我的做法是上电之后先读几次摇杆数据取平均值作为中间值然后在后续的处理中减去这个偏差。第五个坑是排线插座焊接。那个插座是1.0毫米间距的焊接的时候很容易短路。我一开始焊了好几次都不行后来用细一点的漆包线先焊好再插到手柄上问题就解决了。如果你没有焊接经验建议买一个现成的转接板省去焊接的麻烦。6.3 后续可以继续折腾的方向这个项目做完之后还有很多可以继续折腾的方向。比如把PS2手柄的数据通过串口发送到电脑用Python或者Processing做一个可视化界面实时显示按键和摇杆的状态。这样调试的时候会更直观也方便做数据分析。还可以把PS2手柄的数据通过无线模块发送出去做一个无线的控制器。比如用NRF24L01或者蓝牙模块把STM32采集到的数据发送到另一个STM32或者电脑上。这样就不用拖着一条线了使用起来更方便。另外一个方向是优化代码提高刷新率。目前主循环的延时是10毫秒刷新率大概在100Hz左右。如果优化一下代码把延时降到5毫秒刷新率可以提高到200Hz。不过PS2手柄本身的响应速度有限太高的刷新率也没有太大意义。我实测下来100Hz的刷新率已经足够跟手了。最后如果你对PS2手柄的协议感兴趣可以深入研究一下它的震动马达控制和压力感应按键。PS2手柄的按键是压力感应的可以读出按压力度这个功能在有些游戏里会用到。不过这个功能需要额外的命令来启用而且不同型号的手柄支持程度不一样折腾起来比较麻烦。如果你有兴趣可以试试看。这个项目从头到尾大概花了我一个周末的时间其中大部分时间都花在调试时序和排查接线上了。但做完之后看着手柄上的按键控制LED亮灭还是挺有成就感的。如果你也在折腾类似的项目希望这篇文章能帮你少走一些弯路。有什么问题欢迎在评论区交流我看到都会回复。