目录

引言:为什么选择PS2手柄?

第一章:初识PS2手柄 - 不只是游戏机配件

1.1 PS2手柄的前世今生

1.2 PS2手柄的"解剖学"

第二章:PS2手柄的通信秘密

2.1 它不是USB!了解SPI协议

2.2 数据格式:手柄在"说"什么?

第三章:硬件连接 - 手把手教学

3.1 你需要准备什么?

 3.2 常见接线错误

第四章:软件编程 - 让手柄"活"起来

4.1 基础库函数编写

4.2 第一个测试程序

第五章:实践项目 - 制作PS2遥控小车

5.1 项目概述

5.2 硬件扩展

5.3 完整控制代码

第六章:高级技巧与优化

6.1 摇杆校准与死区处理

6.2 组合键与宏功能

6.3 使用中断提高响应速度

第七章:常见问题与解决方案

7.1 问题排查清单

7.2 调试技巧

第八章:创意项目扩展

8.1 项目灵感

8.2 无线改造

结语:开启你的PS2控制之旅


引言:为什么选择PS2手柄?

作为一名嵌入式开发者,你可能已经厌倦了传统的按键控制方式。PS2手柄作为游戏控制器,凭借其人性化的设计和精确的操控体验,成为了机器人、遥控车等项目的理想选择。今天,我将带你深入解析如何用STM32微控制器轻松驾驭PS2手柄。

第一章:初识PS2手柄 - 不只是游戏机配件

1.1 PS2手柄的前世今生

你知道吗? PS2手柄其实是索尼PlayStation 2游戏机的标配控制器,但它早已被嵌入式开发者"征用",成为了机器人、无人机、遥控车等项目的理想控制设备。为什么?

  • 人体工学设计:握感舒适,按键布局合理

  • 双摇杆+16按键:控制维度丰富

  • 震动反馈:增强交互体验

  • 价格亲民:二手市场几十元就能买到

1.2 PS2手柄的"解剖学"

让我们拆解一下这个神奇的小设备:

主要组件

  1. 左摇杆:通常用于方向控制

  2. 右摇杆:用于视角或精确控制

  3. 方向键:上下左右

  4. 动作按钮:△○×□(索尼的标志性设计)

  5. 肩部按键:L1、L2、R1、R2

  6. 功能键:SELECT、START

  7. 模拟开关: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 你需要准备什么?

硬件清单

  1. PS2手柄 + 接收器(约40-60元)

  2. STM32开发板(如STM32F103C8T6,即"蓝色药丸")

  3. 杜邦线若干(母对母)

  4. 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 调试技巧

  1. LED指示法:用LED显示手柄状态

    if(PS2_IsConnected()) {
        GPIO_SetBits(GPIOC, GPIO_Pin_13);  // 点亮LED
    } else {
        GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 熄灭LED
    }

  2. 串口打印法:实时输出手柄数据

    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));
  3. 逻辑分析仪:捕获SPI波形,验证时序

第八章:创意项目扩展

8.1 项目灵感

  1. PS2遥控机械臂:用摇杆控制多个舵机

  2. PS2智能家居控制器:用手柄控制灯光、窗帘

  3. PS2体感游戏:用手柄的加速度计(部分型号有)做体感控制

  4. PS2 MIDI控制器:把按键映射成音乐音符

  5. 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开始,到控制复杂的机器人,每一步都充满成就感。

下一步建议

  1. 先用手柄控制一个LED的亮灭

  2. 实现按键控制舵机转动

  3. 制作完整的遥控小车

  4. 尝试添加摄像头实现FPV(第一人称视角)

祝你玩得开心,创造出令人惊艳的项目!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐