PS2手柄从入门到精通:玩转游戏控制器与嵌入式开发
我们将用PS2手柄控制一个简单的两轮小车:左摇杆上下:控制前进后退右摇杆左右:控制转向△按钮:加速×按钮:刹车START按钮:紧急停止通过这篇教程,你已经掌握了:✅ PS2手柄的基本原理和工作方式✅ 如何正确接线和供电✅ 基础的SPI通信编程✅ 读取按键和摇杆数据✅ 实现一个简单的遥控小车✅ 高级优化技巧和调试方法记住:嵌入式开发最有魅力的地方就是"动手实践"。不要害怕犯错,每个错误都是学习的机会
目录
引言:为什么选择PS2手柄?
作为一名嵌入式开发者,你可能已经厌倦了传统的按键控制方式。PS2手柄作为游戏控制器,凭借其人性化的设计和精确的操控体验,成为了机器人、遥控车等项目的理想选择。今天,我将带你深入解析如何用STM32微控制器轻松驾驭PS2手柄。
第一章:初识PS2手柄 - 不只是游戏机配件
1.1 PS2手柄的前世今生
你知道吗? PS2手柄其实是索尼PlayStation 2游戏机的标配控制器,但它早已被嵌入式开发者"征用",成为了机器人、无人机、遥控车等项目的理想控制设备。为什么?
-
人体工学设计:握感舒适,按键布局合理
-
双摇杆+16按键:控制维度丰富
-
震动反馈:增强交互体验
-
价格亲民:二手市场几十元就能买到
1.2 PS2手柄的"解剖学"
让我们拆解一下这个神奇的小设备:
主要组件:
-
左摇杆:通常用于方向控制
-
右摇杆:用于视角或精确控制
-
方向键:上下左右
-
动作按钮:△○×□(索尼的标志性设计)
-
肩部按键:L1、L2、R1、R2
-
功能键:SELECT、START
-
模拟开关:ANALOG键(切换数字/模拟模式)
第二章:PS2手柄的通信秘密
2.1 它不是USB!了解SPI协议
PS2手柄使用SPI(串行外设接口) 通信,这是一种常见的同步串行通信协议。简单来说:
主机(STM32) <--SPI--> 接收器 <--无线/有线--> 手柄
SPI四线制:
-
MOSI:主设备输出,从设备输入
-
MISO:主设备输入,从设备输出
-
SCK:时钟信号
-
CS/SS:片选信号(选择哪个设备通信)
2.2 数据格式:手柄在"说"什么?
每次通信,手柄会发送9字节数据:
// 示例数据结构
typedef struct {
uint8_t start; // 起始字节
uint8_t id; // 设备ID
uint8_t buttons1; // 第一组按键状态
uint8_t buttons2; // 第二组按键状态
uint8_t right_x; // 右摇杆X轴
uint8_t right_y; // 右摇杆Y轴
uint8_t left_x; // 左摇杆X轴
uint8_t left_y; // 左摇杆Y轴
} PS2_Data;
重要:按键按下时为0,释放时为1(注意是反逻辑!)
第三章:硬件连接 - 手把手教学
3.1 你需要准备什么?
硬件清单:
-
PS2手柄 + 接收器(约40-60元)
-
STM32开发板(如STM32F103C8T6,即"蓝色药丸")
-
杜邦线若干(母对母)
-
3.3V电源(开发板通常提供
3.2 常见接线错误
❌ 错误1:把VCC接到5V上(会烧坏接收器!)
✅ 正确:一定要接3.3V
❌ 错误2:MOSI和MISO接反
✅ 正确:接收器的DATA接开发板的MISO,COMMAND接MOSI
❌ 错误3:忘记接地线
✅ 正确:GND必须连接,形成完整回路
第四章:软件编程 - 让手柄"活"起来
4.1 基础库函数编写
让我们从零开始,编写最简单的PS2驱动:
// ps2_basic.h
#ifndef PS2_BASIC_H
#define PS2_BASIC_H
#include <stdint.h>
// 引脚定义(根据你的接线修改)
#define PS2_DAT_PORT GPIOA
#define PS2_DAT_PIN GPIO_Pin_6 // MISO
#define PS2_CMD_PORT GPIOA
#define PS2_CMD_PIN GPIO_Pin_7 // MOSI
#define PS2_CS_PORT GPIOA
#define PS2_CS_PIN GPIO_Pin_4 // 片选
#define PS2_CLK_PORT GPIOA
#define PS2_CLK_PIN GPIO_Pin_5 // 时钟
// 简化按键定义
#define PS2_BTN_SELECT 0
#define PS2_BTN_START 1
#define PS2_BTN_UP 2
#define PS2_BTN_RIGHT 3
#define PS2_BTN_DOWN 4
#define PS2_BTN_LEFT 5
// ... 更多按键
// 初始化函数
void PS2_Init(void);
// 读取数据
uint8_t PS2_ReadByte(uint8_t cmd);
// 获取按键状态
uint8_t PS2_GetButton(uint8_t button);
// 获取摇杆值
uint8_t PS2_GetJoystick(uint8_t axis);
#endif
// ps2_basic.c
#include "ps2_basic.h"
#include "stm32f10x.h"
// 简单延时函数
static void delay_us(uint32_t us) {
us *= 8; // 粗略延时,实际项目用定时器
while(us--) __NOP();
}
// 初始化GPIO
void PS2_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
// 开启GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置CS和CLK为输出
GPIO_InitStructure.GPIO_Pin = PS2_CS_PIN | PS2_CLK_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(PS2_CS_PORT, &GPIO_InitStructure);
// 配置CMD为输出
GPIO_InitStructure.GPIO_Pin = PS2_CMD_PIN;
GPIO_Init(PS2_CMD_PORT, &GPIO_InitStructure);
// 配置DAT为输入
GPIO_InitStructure.GPIO_Pin = PS2_DAT_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(PS2_DAT_PORT, &GPIO_InitStructure);
// 初始状态
GPIO_SetBits(PS2_CS_PORT, PS2_CS_PIN); // CS高电平
GPIO_ResetBits(PS2_CLK_PORT, PS2_CLK_PIN); // CLK低电平
}
// 发送一个字节并接收一个字节
uint8_t PS2_ReadByte(uint8_t cmd) {
uint8_t result = 0;
// 拉低CS开始通信
GPIO_ResetBits(PS2_CS_PORT, PS2_CS_PIN);
delay_us(10);
// 发送8位数据
for(uint8_t i = 0; i < 8; i++) {
// 设置CMD引脚
if(cmd & 0x01) {
GPIO_SetBits(PS2_CMD_PORT, PS2_CMD_PIN);
} else {
GPIO_ResetBits(PS2_CMD_PORT, PS2_CMD_PIN);
}
// 产生时钟上升沿
GPIO_SetBits(PS2_CLK_PORT, PS2_CLK_PIN);
delay_us(5);
// 读取数据
if(GPIO_ReadInputDataBit(PS2_DAT_PORT, PS2_DAT_PIN)) {
result |= (1 << i);
}
// 时钟下降沿
GPIO_ResetBits(PS2_CLK_PORT, PS2_CLK_PIN);
delay_us(5);
cmd >>= 1; // 准备下一位
}
// 拉高CS结束通信
GPIO_SetBits(PS2_CS_PORT, PS2_CS_PIN);
return result;
}
4.2 第一个测试程序
// main.c - 最简单的测试程序
#include "stm32f10x.h"
#include "ps2_basic.h"
#include <stdio.h>
// 简单串口打印函数(需要实现)
void USART_SendString(char *str);
int main(void) {
// 系统初始化
SystemInit();
// PS2初始化
PS2_Init();
// 配置手柄进入模拟模式
uint8_t config_cmd[] = {0x01, 0x43, 0x00, 0x01, 0x00};
for(int i = 0; i < 5; i++) {
PS2_ReadByte(config_cmd[i]);
}
USART_SendString("PS2手柄初始化完成!\r\n");
while(1) {
// 读取手柄ID(测试通信是否正常)
uint8_t id = PS2_ReadByte(0x01);
id = PS2_ReadByte(0x42);
if(id == 0x73) {
USART_SendString("检测到红灯模式(数字模式)\r\n");
} else if(id == 0x41) {
USART_SendString("检测到绿灯模式(模拟模式)\r\n");
} else {
USART_SendString("未检测到手柄,请检查连接!\r\n");
}
// 简单延时
for(int i = 0; i < 1000000; i++);
}
}
第五章:实践项目 - 制作PS2遥控小车
5.1 项目概述
我们将用PS2手柄控制一个简单的两轮小车:
-
左摇杆上下:控制前进后退
-
右摇杆左右:控制转向
-
△按钮:加速
-
×按钮:刹车
-
START按钮:紧急停止
5.2 硬件扩展
PS2接收器 -> STM32 -> 电机驱动 -> 直流电机
|
v
电池供电
5.3 完整控制代码
// car_control.c
#include "ps2_advanced.h"
#include "motor.h"
#include "oled.h"
// 小车状态结构体
typedef struct {
int16_t speed; // 速度:-1000 ~ 1000
int16_t steering; // 转向:-500 ~ 500(左转为负)
uint8_t gear; // 档位:0-停止,1-慢速,2-中速,3-快速
uint8_t emergency; // 紧急停止标志
} CarState;
CarState myCar = {0};
// 处理PS2输入
void ProcessPS2Input(void) {
static uint8_t last_buttons = 0xFF;
uint8_t current_buttons = PS2_GetAllButtons();
// 检查START按钮(紧急停止)
if((current_buttons & (1 << PS2_BTN_START)) == 0) {
if((last_buttons & (1 << PS2_BTN_START)) != 0) {
// START按钮刚被按下
myCar.emergency = 1;
myCar.speed = 0;
OLED_ShowString(1, 1, "EMERGENCY STOP!");
}
} else {
myCar.emergency = 0;
}
// 读取左摇杆Y轴(前进后退)
uint8_t left_y = PS2_GetJoystick(PS2_JOY_LEFT_Y);
if(left_y < 120) { // 前进
myCar.speed = (120 - left_y) * 8;
} else if(left_y > 135) { // 后退
myCar.speed = (left_y - 135) * (-8);
} else { // 死区
myCar.speed = 0;
}
// 读取右摇杆X轴(转向)
uint8_t right_x = PS2_GetJoystick(PS2_JOY_RIGHT_X);
if(right_x < 120) { // 左转
myCar.steering = (120 - right_x) * (-4);
} else if(right_x > 135) { // 右转
myCar.steering = (right_x - 135) * 4;
} else { // 直行
myCar.steering = 0;
}
// 保存当前按钮状态
last_buttons = current_buttons;
}
// 更新电机输出
void UpdateMotors(void) {
if(myCar.emergency) {
Motor_Stop(MOTOR_LEFT);
Motor_Stop(MOTOR_RIGHT);
return;
}
// 差速转向计算
int16_t left_speed = myCar.speed + myCar.steering;
int16_t right_speed = myCar.speed - myCar.steering;
// 限幅
if(left_speed > 1000) left_speed = 1000;
if(left_speed < -1000) left_speed = -1000;
if(right_speed > 1000) right_speed = 1000;
if(right_speed < -1000) right_speed = -1000;
// 设置电机速度
Motor_SetSpeed(MOTOR_LEFT, left_speed);
Motor_SetSpeed(MOTOR_RIGHT, right_speed);
}
// 主控制循环
void CarControlLoop(void) {
while(1) {
// 1. 读取PS2输入
ProcessPS2Input();
// 2. 更新电机
UpdateMotors();
// 3. 显示状态
static char buffer[32];
sprintf(buffer, "Speed:%4d Steer:%4d",
myCar.speed, myCar.steering);
OLED_ShowString(1, 1, buffer);
// 4. 简单延时(控制频率约50Hz)
Delay_ms(20);
}
}
第六章:高级技巧与优化
6.1 摇杆校准与死区处理
问题:摇杆有物理偏差,中间位置不一定是128
解决:自动校准
// 摇杆校准结构
typedef struct {
uint8_t center_x;
uint8_t center_y;
uint8_t deadzone; // 死区大小
} JoystickCalibration;
// 自动校准函数
void AutoCalibrateJoystick(JoystickCalibration *cal) {
uint32_t sum_x = 0, sum_y = 0;
// 采集100个样本
for(int i = 0; i < 100; i++) {
sum_x += PS2_GetJoystick(PS2_JOY_LEFT_X);
sum_y += PS2_GetJoystick(PS2_JOY_LEFT_Y);
Delay_ms(10);
}
cal->center_x = sum_x / 100;
cal->center_y = sum_y / 100;
cal->deadzone = 5; // 5%的死区
printf("校准完成:中心点(%d, %d)\r\n",
cal->center_x, cal->center_y);
}
6.2 组合键与宏功能
// 定义组合键宏
#define COMBO_UP_R1 0 // 上+R1
#define COMBO_DOWN_L1 1 // 下+L1
// ...
// 检测组合键
uint8_t CheckComboKeys(void) {
uint8_t buttons = PS2_GetAllButtons();
// 同时按下上和R1
if((buttons & (1 << PS2_BTN_UP)) == 0 &&
(buttons & (1 << PS2_BTN_R1)) == 0) {
return COMBO_UP_R1;
}
// 同时按下下和L1
if((buttons & (1 << PS2_BTN_DOWN)) == 0 &&
(buttons & (1 << PS2_BTN_L1)) == 0) {
return COMBO_DOWN_L1;
}
return 0xFF; // 无组合键
}
6.3 使用中断提高响应速度
// 使用外部中断检测ATT线变化
void PS2_Init_Interrupt(void) {
// 配置ATT引脚为外部中断
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
RCC_APB2Periph_AFIO, ENABLE);
// 配置ATT为输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置外部中断
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource4);
EXTI_InitStructure.EXTI_Line = EXTI_Line4;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 配置NVIC
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
// 中断服务函数
void EXTI4_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line4) != RESET) {
// PS2有数据需要读取
PS2_ReadData();
EXTI_ClearITPendingBit(EXTI_Line4);
}
}
第七章:常见问题与解决方案
7.1 问题排查清单
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 手柄无反应 | 电源问题 | 检查是否为3.3V供电 |
| 数据全为0 | 接线错误 | 检查MOSI/MISO是否接反 |
| 按键响应慢 | 程序阻塞 | 优化主循环,使用中断 |
| 摇杆值跳动 | 接触不良 | 检查接线,增加软件滤波 |
| 偶尔断开 | 信号干扰 | 缩短连线,增加滤波电容 |
7.2 调试技巧
-
LED指示法:用LED显示手柄状态
if(PS2_IsConnected()) { GPIO_SetBits(GPIOC, GPIO_Pin_13); // 点亮LED } else { GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 熄灭LED } -
串口打印法:实时输出手柄数据
printf("LX:%3d LY:%3d RX:%3d RY:%3d\r\n", PS2_GetJoystick(PS2_JOY_LEFT_X), PS2_GetJoystick(PS2_JOY_LEFT_Y), PS2_GetJoystick(PS2_JOY_RIGHT_X), PS2_GetJoystick(PS2_JOY_RIGHT_Y)); -
逻辑分析仪:捕获SPI波形,验证时序
第八章:创意项目扩展
8.1 项目灵感
-
PS2遥控机械臂:用摇杆控制多个舵机
-
PS2智能家居控制器:用手柄控制灯光、窗帘
-
PS2体感游戏:用手柄的加速度计(部分型号有)做体感控制
-
PS2 MIDI控制器:把按键映射成音乐音符
-
PS2电脑遥控器:通过USB转接,控制电脑媒体播放
8.2 无线改造
进阶项目:将PS2接收器改造成无线版本
// 使用NRF24L01实现无线传输
void PS2_Wireless_Setup(void) {
// 初始化无线模块
NRF24L01_Init();
// 配置为发送模式
NRF24L01_TX_Mode();
// 定时发送手柄数据
while(1) {
PS2_Data data = PS2_ReadAllData();
NRF24L01_SendData((uint8_t*)&data, sizeof(data));
Delay_ms(20); // 50Hz发送频率
}
}
结语:开启你的PS2控制之旅
通过这篇教程,你已经掌握了:
✅ PS2手柄的基本原理和工作方式
✅ 如何正确接线和供电
✅ 基础的SPI通信编程
✅ 读取按键和摇杆数据
✅ 实现一个简单的遥控小车
✅ 高级优化技巧和调试方法
记住:嵌入式开发最有魅力的地方就是"动手实践"。不要害怕犯错,每个错误都是学习的机会。从点亮第一个LED开始,到控制复杂的机器人,每一步都充满成就感。
下一步建议:
-
先用手柄控制一个LED的亮灭
-
实现按键控制舵机转动
-
制作完整的遥控小车
-
尝试添加摄像头实现FPV(第一人称视角)
祝你玩得开心,创造出令人惊艳的项目!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)