ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 HAL库驱动PS2手柄:CubeMX配置与SPI协议实战

STM32 HAL库驱动PS2手柄:CubeMX配置与SPI协议实战 简介一份面向STM32开发者的PS2手柄通信实例源码包基于HAL库与CubeMX配置适合正在学习外设驱动、串行协议或准备做遥控小车/机器人项目的嵌入式爱好者。压缩包共970个文件约22.2MB以C源文件559个和头文件243个为主同时包含汇编、链接脚本、工程配置及编译输出文件其中.ioc和.mxproject可用于直接打开CubeMX与MDK工程Src目录下提供PS2初始化、数据读取与中断处理等关键驱动实现。资源已有5409人学习项目结构清晰从CubeMX图形化配置到HAL库底层操作均有覆盖读者可借助完整工程快速理解PS2协议帧格式与GPIO/定时器配置思路并在此基础上扩展按键映射或无线控制功能。无论初学者还是有一定经验的开发者都能通过这份源码减少移植成本直接获得一套可编译、可调试的参考实现。 做STM32项目的人大概率都会经历这样一个阶段板子能跑、灯能亮、串口能打印了接下来就想做个稍微像样的东西出来。这时候“PS2手柄HAL库CubeMX源码”这套组合几乎是最顺手的输入方案之一。PS2无线手柄的接收器拆开之后和STM32之间其实只跑着一路SPI协议按键状态和双摇杆模拟量全部塞在9个字节的帧里来回传。配上STM32F103C8T6这种烂大街的板子和HAL库工程从CubeMX配好引脚到代码里读出数据一两个小时就能跑通。这篇文章就是我实际验证过的完整方案适合想给遥控小车、机械臂、机器人底盘加无线手柄控制的新手也适合已经能跑串口、GPIO但没碰过SPI协议的人拿来练手整个思路可以平移到任何带SPI外设的STM32型号上。1. 项目整体思路与方案选型1.1 先搞清楚PS2手柄在底层到底传什么很多人一听到“PS2手柄”就觉得它是什么专用总线其实不是。PS2无线手柄的接收器本质上就是一个SPI从机它和单片机之间只有四根线在跑数据SCK时钟、一个主机发命令的线路、一个从机回数据的线路、还有片选CS。主机拉低CS按SCK节奏把命令字节发过去与此同时从机把自己状态数据一个字节一个字节回传。整个通信就像是两个人在对暗号主机问一句从机答一句一问一答同时发生。所以这个项目的技术核心并不是什么高深算法而是把SPI用好并且严格遵守PS2手柄的帧协议。你发出去的每一帧既决定了手柄进入什么模式也决定了它返回什么数据。我见过不少人把代码抄过去结果手柄红绿灯乱跳、摇杆乱抖就是没搞懂这一层。1.2 为什么我选HAL库加CubeMX而不是标准库STM32的开发方式现在就两大派标准外设库和HAL库。标准库的寄存器封装更薄、代码执行效率高但初始化外设时要自己对照手册一个个配寄存器。HAL库虽然代码量大、运行开销高一些但它把SPI、I2C这类外设的初始化和收发都封装成了现成接口再加上CubeMX图形化配置工具很多容易踩坑的时钟树、引脚复用问题在生成工程的时候就自动解决了。我这个项目之所以推荐HAL库加CubeMX不是因为它比标准库高级而是因为PS2手柄的SPI通信真正难调的是时序细节不是寄存器本身。用CubeMX把SPI参数选对之后驱动部分只需要调用HAL_SPI_TransmitReceive这一个函数就能在一问一答的过程中同时拿到手柄回传的数据。代码量少逻辑清晰对新手来说排错也容易。其实标准库也能做只是SPI初始化那堆代码会分散注意力。1.3 硬件SPI还是模拟SPI两种方案对比PS2手柄驱动在网上有两种写法一种是用STM32的硬件SPI外设一种是用GPIO模拟SPI时序。硬件SPI的优点是占用CPU少、时钟稳定CubeMX配好之后收发代码非常干净缺点是SPI的极性和相位必须选对否则手柄完全不响应。模拟SPI的优点是引脚随便选、时序可以软件里随意调整缺点是每个字节都要自己拉CLK主循环会被通信过程卡住而且代码量明显膨胀。我自己实际使用下来的建议是优先用硬件SPI加软件控制CS。PS2手柄接收器对时钟极性的要求其实只有两种兼容组合只要把CPOL和CPHA配置成SPI mode 0或者mode 3再加上时钟频率压到1MHz附近基本都能稳定读。模拟SPI可以作为备用方案比如你用到的单片机刚好没空闲SPI外设或者想彻底搞清楚时序再动手那时候再翻出来用。2. 核心细节解析PS2协议帧与数据格式2.1 一条完整的命令帧长什么样PS2手柄的命令帧固定是9个字节主机每次以CS拉低开始连续发送9个字节同时接收9个字节回包。回包的前2个字节基本是命令回显真正有价值的数据从第3个字节开始。最常见的三条命令如下表命令目的第1字节第2字节后续字节作用进入配置模式0x010x430x00 0x01 0x00...解锁手柄设置切换模拟模式0x010x440x00 0x01 0x03 0x00...启动模拟摇杆和按键连发轮询读取数据0x010x420x00 x 7读取当前按键和摇杆状态其中0x44那一条命令里有一个重要参数表示模式选择。0x01是数字模式只支持方向键和部分功能键摇杆没有输出0x03是模拟模式红灯亮起摇杆才开始工作。很多人在网上抄代码发现摇杆读出来一直是128左右十有八九就是模式没切过去。实测下来进入配置模式后最好加10毫秒左右的延时再切模拟模式切完再延时一下让手柄完成内部状态切换紧接着再发读数据命令。2.2 按键数据的位映射读数据命令返回的9个字节中第3字节和第4字节分别对应两排按键状态每个键占一位。PS2手柄的设计是按下为0松开为1这和大部分按键扫描电路正好相反一开始特别容易写反逻辑。第3字节数据帧中的rx[2]的位定义是位76543210按键左下右上StartR3L3Select第4字节数据帧中的rx[3]的位定义是位76543210按键方框叉圆圈三角R1L1R2L2第一次调通时建议直接用串口把rx[2]和rx[3]的十六进制值打印出来然后逐一手动按键对着表格核对一遍。不同批次的手柄接收器在某些键位的定义上可能有细微区别实测为准比什么都重要。2.3 摇杆模拟量的读取与判断模拟模式下读回来的第5到第8字节分别对应两个摇杆的X轴和Y轴每个轴是8位无符号数范围0到255。摇杆居中时读数一般在128附近不同手柄会有10个点左右的偏差摇杆推到一端是0推到另一端是255。需要注意的是左右摇杆四路数据的排列顺序偶尔会因为驱动代码不同而错位网上流传的版本里有的把rx[4]定义成左摇杆X有的定义成右摇杆X这很让人头疼。我的做法是拿到手柄后先做个简单标定把手柄静止不动时读到的四个值打印出来再分别推四个方向看哪一路数据在变化用实测结果统一到自己的结构体里。摇杆数据在跳变时会有一些抖动特别是旧手柄碳膜磨损后读数会在目标值附近来回飘。这一层用软件滤波就能压下去。比如我做小车底盘遥控时对摇杆数据做了一阶低通滤波新读数只取30%的权重效果很明显电机左右轮速度差就稳下来了。3. CubeMX配置与HAL库驱动实现3.1 CubeMX引脚分配和SPI参数怎么填CubeMX部分不复杂但有几个选项填错了后面会折腾很久。我以STM32F103C8T6为例说明。芯片选好之后先配置调试口SYS页面把Debug选成Serial Wire避免调试器和代码打架。然后进入SPI1配置页面Mode选择Full-Duplex Master硬件NSS选择Disable因为PS2接收器片选信号我们自己用普通GPIO控制。PB引脚不用管SPI1默认可以映射到PA5、PA6、PA7正好是SCK、MISO、MOSI。关键参数照着下面这张表填参数项配置值说明ModeFull-Duplex Master标准SPI主机Hardware NSS SignalDisable用软件控制CSData Size8 Bits每次收发一字节First BitMSB First协议要求高位先发Clock Polarity (CPOL)Low空闲时钟为低电平Clock Phase (CPHA)1 Edge第一个边沿采样数据Prescaler64约1.125MHzSPI时钟频率很关键PS2手柄最早是基于3.3V逻辑设计的通信速度本来就不高建议控制在250kHz到1MHz之间。我直接把Prescaler设成6472MHz系统时钟算下来约1.125MHz实测配合杜邦线也没问题。CS引脚我用的PA4设置成GPIO输出初始电平拉高代表不选中手柄。3.2 驱动源码关键函数拆解CubeMX生成工程之后核心驱动其实就两个动作CS拉低发命令读回数据CS拉高。我封装成一个底层命令函数#define PS2_CS_LOW() HAL_GPIO_WritePin(PS2_CS_GPIO_Port, PS2_CS_Pin, GPIO_PIN_RESET) #define PS2_CS_HIGH() HAL_GPIO_WritePin(PS2_CS_GPIO_Port, PS2_CS_Pin, GPIO_PIN_SET) void PS2_Cmd(uint8_t *tx, uint8_t *rx, uint8_t len) { PS2_CS_LOW(); HAL_Delay(1); HAL_SPI_TransmitReceive(hspi1, tx, rx, len, 100); HAL_Delay(1); PS2_CS_HIGH(); }这里有两个小细节。CS拉低之后最好加1毫秒延时再开始发数据让手柄接收器稳定响应。数据发完之后也别立刻拉高CS稍微等一拍再释放片选。虽然这些延时看起来不严谨但PS2手柄属于低速设备CS时序稍微容错一点反而更稳实测下来没有因为多这1毫秒影响任何性能还省掉很多奇奇怪怪的偶发问题。3.3 初始化命令与轮询读取实现手柄初始化写成一个函数依次完成进入配置模式和切换模拟模式。特别提醒一下在发送0x44那条命令时字节顺序必须精确很多踩坑都是因为手动修改代码时把其中一个参数丢了。uint8_t PS2_Init(void) { uint8_t tx[9] {0}; uint8_t rx[9] {0}; // 进入配置模式 tx[0] 0x01; tx[1] 0x43; tx[2] 0x00; tx[3] 0x01; tx[4] 0x00; PS2_Cmd(tx, rx, 9); HAL_Delay(10); // 切换模拟模式 memset(tx, 0, sizeof(tx)); tx[0] 0x01; tx[1] 0x44; tx[2] 0x00; tx[3] 0x01; tx[4] 0x03; PS2_Cmd(tx, rx, 9); HAL_Delay(10); return 0; }轮询读取函数也只做一件事发送0x01 0x42开头的帧然后从回包里提取有效数据。我习惯把返回的按键和摇杆数据直接存到一个结构体里这样主循环里用起来非常顺手。typedef struct { uint8_t select, l3, r3, start, up, right, down, left; uint8_t l2, r2, l1, r1, triangle, circle, cross, square; uint8_t r_x, r_y, l_x, l_y; } PS2_Data_t; uint8_t PS2_Read(PS2_Data_t *data) { uint8_t tx[9] {0x01, 0x42, 0, 0, 0, 0, 0, 0, 0}; uint8_t rx[9] {0}; PS2_Cmd(tx, rx, 9); >uint8_t filtered; static uint8_t lx_last; lx_last (uint16_t)(lx_last * 7 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
返回列表