1. 项目概述

“Fork of PS3 Controller Host”是一个面向ESP32平台的开源蓝牙主机协议栈扩展库,其核心目标是 在嵌入式设备端完整复现PS3主机(PlayStation 3)的蓝牙服务角色 ,从而欺骗原装Sixaxis或DualShock 3无线控制器,使其主动建立并维持与ESP32的稳定蓝牙连接。该库并非简单的HID设备枚举器,而是深度模拟了PS3主机特有的蓝牙配对机制、L2CAP信道协商流程、SPP(Serial Port Profile)服务发现与数据交换逻辑,以及关键的加密握手协议(包括基于控制器内部密钥的Challenge-Response认证)。其技术本质是将ESP32从一个被动的蓝牙从设备(Slave)转变为具备主机(Host)能力的可信终端,突破了传统蓝牙模块在经典蓝牙(BR/EDR)场景下对主从角色的硬性限制。

该项目严格依赖Espressif官方的ESP-IDF框架(v4.x及以后版本),同时通过Arduino Core for ESP32提供兼容层支持,使开发者可在两种主流开发范式下快速集成。其设计哲学强调 最小侵入性 最大可移植性 :所有底层蓝牙操作均封装于独立组件内,不修改ESP-IDF蓝牙协议栈源码;所有上层API保持状态机抽象,屏蔽底层连接时序、重传机制与错误恢复等复杂细节。对于硬件工程师而言,这意味着无需深入研究Bluedroid协议栈的内部消息队列或HCI事件分发机制,即可在数分钟内完成PS3控制器的数据接入。

本项目的技术价值不仅在于实现了一种复古游戏外设的复用,更在于它为嵌入式系统提供了 一个完整的、可复用的经典蓝牙主机协议栈工程范例 。其代码结构清晰展示了如何在资源受限的MCU上构建符合Bluetooth SIG规范的BR/EDR主机应用,涵盖从HCI命令下发、ACL链路管理、L2CAP信道注册、SDP服务记录发布,到SPP RFCOMM通道建立与数据流控制的全生命周期管理。这些经验可直接迁移至其他需要扮演蓝牙主机角色的工业场景,例如:蓝牙打印机网关、车载OBD-II诊断仪、医疗设备蓝牙桥接器等。

2. 技术原理与核心机制

2.1 PS3控制器的配对信任模型

PS3控制器(Sixaxis/DualShock 3)的配对机制与通用蓝牙设备存在根本性差异。其并非采用标准的SSP(Secure Simple Pairing)或Legacy PIN码配对,而是一种 单向绑定(One-Way Binding) 模型:

  • 控制器内部固件仅存储 一个且唯一 的蓝牙MAC地址——即它所信任的“主机”地址。
  • 该地址在出厂时为空,首次通过USB线缆连接至PS3主机并按下PS键后,主机通过专有USB HID报告将自身蓝牙地址写入控制器Flash的特定扇区(通常为0x0000–0x0007)。
  • 此后,控制器在蓝牙搜索阶段 只向该MAC地址发起连接请求(Page Request) ,拒绝响应任何其他设备的Page Scan或Inquiry Scan。
  • 连接建立后,双方通过L2CAP信道进行Challenge-Response认证:主机发送随机Challenge,控制器使用内置密钥(Keygen算法)生成Response,主机验证通过后才允许SPP数据通道开启。

因此,要使ESP32被PS3控制器识别为“合法主机”,必须满足两个条件之一:

  1. ESP32 MAC地址伪造 :将ESP32的蓝牙控制器MAC地址修改为与PS3主机相同的值;
  2. 控制器MAC地址重写 :读取并修改PS3控制器内部存储的MAC地址,将其指向ESP32当前的MAC地址。

项目文档中推荐的 SixaxisPairTool (Windows)或 sixaxispairer (Linux/macOS)正是用于执行第二种方案的工具,它们通过USB HID接口与控制器通信,读取/擦除/写入其Flash中的MAC地址区域。这是目前最可靠、最不依赖ESP32硬件修改的方案。

2.2 ESP32蓝牙协议栈配置要点

ESP32的Bluedroid协议栈默认以“双模”(BR/EDR + BLE)运行,但PS3控制器仅支持经典蓝牙(BR/EDR)的SPP Profile。因此,项目要求在 menuconfig 中进行如下关键配置:

配置项 路径 推荐值 工程意义
Bluetooth Controller Mode Component config → Bluetooth → Bluetooth controller → Bluetooth controller mode BR/EDR Only 禁用BLE物理层,释放RAM与CPU资源,避免BR/EDR与BLE射频冲突,确保L2CAP信道稳定性
Bluetooth Host Stack Component config → Bluetooth → Bluetooth Host Bluedroid - Dual-mode 启用Bluedroid作为上层协议栈,提供SDP、RFCOMM、SPP等Profile支持
Classic Bluetooth Component config → Bluetooth → Bluedroid Enable → Classic Bluetooth Enabled 必须启用,否则无法处理BR/EDR ACL链路与L2CAP信道
SPP (Serial Port Profile) Component config → Bluetooth → Bluedroid Enable → Classic Bluetooth → SPP Enabled 提供RFCOMM虚拟串口服务,PS3控制器通过此Profile传输控制数据包
Secure Simple Pairing Component config → Bluetooth → Bluedroid Enable → Classic Bluetooth → Secure Simple Pairing Enabled 尽管PS3不使用SSP,但启用此选项可确保HCI层支持必要的Link Key管理与加密命令

特别注意: BR/EDR Only 模式是性能与稳定性优化的关键。在实测中,若保留BLE模式,PS3控制器常因ACL链路质量波动导致频繁断连( HCI_ERR_CONN_TIMEOUT ),而强制切换至纯BR/EDR模式后,连接保持时间可从数秒提升至数小时。

2.3 协议栈交互流程解析

库的核心初始化函数 ps3Init() 触发以下精确时序的协议栈交互:

  1. HCI层初始化 :调用 esp_bt_controller_init() esp_bluedroid_init() ,启动底层控制器与Bluedroid主机栈。
  2. SDP服务注册 :在本地SDP数据库中注册PS3主机专属服务记录(Service Record),关键字段包括:
    • ServiceClassIDList : [0x1101] (SPP)
    • ProtocolDescriptorList : [L2CAP, RFCOMM]
    • BluetoothProfileDescriptorList : [SPP, 0x0100]
    • ServiceName : "PS3 Controller Host"
    • ServiceDescription : "Emulated PS3 Console"
  3. RFCOMM通道监听 :调用 esp_spp_start_srv() 启动SPP服务器,在固定RFCOMM Channel 0x01 (PS3控制器预设的连接通道)上监听连接请求。
  4. ACL链路管理 :当控制器发起Page Request时,ESP32自动响应Page Response,建立ACL链路,并在链路上协商L2CAP信道。
  5. SPP连接确认 :RFCOMM层完成Channel Negotiation后,触发 PS3_EVENT_CONNECTED 事件,此时控制器进入“已连接”状态,开始周期性发送64字节数据包(含按键、摇杆、陀螺仪、加速度计等全部状态)。

整个流程完全由Bluedroid内部状态机驱动,库仅需注册事件回调函数 ps3SetEventCallback() ,即可在 PS3_EVENT_DATA_RECEIVED 事件中获取原始数据包并解析。

3. API接口详解与工程化使用

3.1 Arduino API接口

Arduino库封装了底层复杂性,提供面向对象的 Ps3Controller 类实例 Ps3 ,其核心API如下表所示:

函数签名 参数说明 返回值 典型用途 注意事项
begin(const char* mac) mac : 格式为 "XX:XX:XX:XX:XX:XX" 的字符串,表示控制器已配对的MAC地址 bool : true 表示初始化成功 初始化库并尝试连接指定MAC的控制器 必须在 setup() 中调用,且需确保ESP32 MAC已匹配该地址
begin() 无参数 bool : true 表示初始化成功 初始化库,不指定MAC,仅用于获取ESP32当前地址 仅用于调试,实际连接仍需 begin(mac)
getAddress() 无参数 String : 当前ESP32的蓝牙MAC地址(格式同上) 获取ESP32地址,用于写入控制器 返回值为 String 对象,需转换为 uint8_t[6] 数组才能用于 ps3SetBluetoothMacAddress()
isConnected() 无参数 bool : true 表示控制器已连接 查询连接状态 loop() 中轮询使用,避免阻塞
getBatteryLevel() 无参数 uint8_t : 电池电量(0–100%) 获取当前电量 值为0xFF时表示电量未知或未连接
getButtonState() 无参数 uint32_t : 按键位图(bit0=SELECT, bit1=LSHIFT, ..., bit15=PS) 获取所有按键的瞬时状态 位定义见 Ps3Controller.h 头文件注释

Arduino代码示例:基础连接与数据读取

#include <Ps3Controller.h>

void setup() {
  Serial.begin(115200);
  // 方式1:已知控制器配对MAC,直接连接
  if (!Ps3.begin("01:02:03:04:05:06")) {
    Serial.println("PS3 init failed!");
    return;
  }
  Serial.println("PS3 connected successfully!");
}

void loop() {
  if (Ps3.isConnected()) {
    // 读取左摇杆X轴(-128 ~ +127)
    int8_t lx = Ps3.analog.stick.lx;
    // 读取右摇杆Y轴
    int8_t ry = Ps3.analog.stick.ry;
    // 检查三角键是否按下
    if (Ps3.button.triangle) {
      Serial.println("Triangle pressed!");
    }
    // 检查左摇杆是否向左推
    if (lx < -20) {
      Serial.println("Left stick pushed left!");
    }
  }
  delay(50); // 避免高频轮询
}

3.2 ESP-IDF原生API接口

ESP-IDF接口更贴近硬件,提供C语言风格的函数与结构体,适用于对实时性与内存占用有严苛要求的工业项目:

函数签名 参数说明 返回值 工程意义
ps3SetEventCallback(ps3_event_cb_t cb) cb : 事件回调函数指针 void 注册全局事件处理器,所有PS3事件(连接、断开、数据到达)均由此函数分发
ps3Init() 无参数 esp_err_t : ESP_OK 表示成功 执行协议栈初始化、SDP注册、RFCOMM监听等全部底层操作
ps3IsConnected() 无参数 bool : true 表示已连接 线程安全的连接状态查询,可在FreeRTOS任务中安全调用
ps3SetBluetoothMacAddress(uint8_t* mac) mac : 指向6字节MAC地址的指针 void 关键函数 :在 ps3Init() 前调用,强制设置ESP32蓝牙控制器MAC地址

ESP-IDF代码示例:FreeRTOS任务集成

#include "ps3.h"
#include "freertos/task.h"
#include "driver/gpio.h"

// 定义LED引脚用于状态指示
#define LED_GPIO GPIO_NUM_2

// PS3事件回调函数
static void controller_event_cb(ps3_t ps3, ps3_event_t event) {
  switch (event.type) {
    case PS3_EVENT_CONNECTED:
      gpio_set_level(LED_GPIO, 1); // 连接成功,点亮LED
      ESP_LOGI("PS3", "Controller connected. Battery: %d%%", ps3.status.battery);
      break;
    case PS3_EVENT_DISCONNECTED:
      gpio_set_level(LED_GPIO, 0); // 断开,熄灭LED
      ESP_LOGW("PS3", "Controller disconnected");
      break;
    case PS3_EVENT_DATA_RECEIVED:
      // 处理按键变化事件(下降沿)
      if (event.button_down.cross) {
        ESP_LOGI("PS3", "Cross button pressed (down)");
      }
      // 处理摇杆变化事件
      if (event.analog_changed.stick.lx) {
        ESP_LOGD("PS3", "Left stick X changed to %d", ps3.analog.stick.lx);
      }
      break;
  }
}

// PS3管理任务
static void ps3_task(void* pvParameters) {
  // 设置ESP32 MAC地址(必须在ps3Init前!)
  uint8_t new_mac[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
  ps3SetBluetoothMacAddress(new_mac);

  // 注册事件回调
  ps3SetEventCallback(controller_event_cb);

  // 初始化PS3库
  esp_err_t ret = ps3Init();
  if (ret != ESP_OK) {
    ESP_LOGE("PS3", "ps3Init failed: %s", esp_err_to_name(ret));
    vTaskDelete(NULL);
  }

  // 等待连接(带看门狗喂食)
  while (!ps3IsConnected()) {
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }

  ESP_LOGI("PS3", "Ready to receive data");

  // 主循环:持续运行,事件由回调函数处理
  while (1) {
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}

// 应用入口
void app_main() {
  gpio_reset_pin(LED_GPIO);
  gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);

  // 创建PS3管理任务
  xTaskCreate(ps3_task, "ps3_task", 4096, NULL, 5, NULL);
}

3.3 数据结构与状态解析

PS3控制器每20ms发送一个64字节数据包,库将其解析为 ps3_t 结构体,其关键成员如下:

typedef struct {
  struct {
    uint8_t battery;     // 电池电量 (0x00=0%, 0x01=10%, ..., 0x0A=100%, 0xFF=unknown)
    bool charging;       // 是否正在充电 (true/false)
  } status;

  struct {
    uint8_t lx, ly;      // 左摇杆 X/Y (-128 ~ +127)
    uint8_t rx, ry;      // 右摇杆 X/Y (-128 ~ +127)
  } analog;

  struct {
    bool select;         // SELECT键
    bool lshift;         // L3键(左摇杆按下)
    bool rshift;         // R3键(右摇杆按下)
    bool start;          // START键
    bool up;             // 方向键上
    bool right;          // 方向键右
    bool down;           // 方向键下
    bool left;           // 方向键左
    bool l1;             // L1键
    bool r1;             // R1键
    bool l2;             // L2键
    bool r2;             // R2键
    bool triangle;       // △键
    bool circle;         // ○键
    bool cross;          // ×键
    bool square;         // □键
    bool ps;             // PS键
  } button;

  struct {
    int16_t x, y, z;     // 三轴加速度计原始值(单位:mg)
  } accelerometer;

  struct {
    int16_t x, y, z;     // 三轴陀螺仪原始值(单位:deg/s)
  } gyroscope;
} ps3_t;

数据精度与校准提示

  • 加速度计与陀螺仪数据为16位有符号整数,但其量程与零偏需通过PS3主机固件校准。在ESP32端,建议在静止状态下采集1000个样本计算平均值作为零点偏移( offset_x , offset_y , offset_z ),后续数据减去偏移量再进行物理单位换算。
  • 摇杆数据存在非线性死区(Dead Zone),典型值为±15。工程实践中应在应用层添加死区判断: if (abs(lx) < 15) lx = 0; ,避免微小抖动触发误动作。

4. 实战调试与故障排除

4.1 常见编译错误与解决方案

错误现象 error: 'esp_bt_dev_get_address' was not declared in this scope
原因 :ESP-IDF版本升级导致内部API变更(如v5.0+移除了部分旧版HCI函数)。
解决方案

  • 检查 ps3.c 中调用 esp_bt_dev_get_address() 的位置,替换为新API:
    // 旧版(v4.x)
    esp_bt_dev_get_address(bt_addr);
    // 新版(v5.0+)
    esp_bt_dev_get_bda(bt_addr);
    
  • 若项目必须使用新版IDF,需同步更新 ps3.h ps3SetBluetoothMacAddress() 的实现,改用 esp_bt_dev_set_bda() 函数。

错误现象 undefined reference to 'esp_spp_start_srv'
原因 menuconfig 中未正确启用SPP Profile,或链接时未包含 bt 组件。
解决方案

  • 运行 idf.py menuconfig ,严格按2.2节配置路径启用SPP;
  • CMakeLists.txt 中确认 REQUIRES bt 已声明:
    set(COMPONENT_REQUIRES bt)
    

4.2 连接失败的系统性排查

ps3IsConnected() 始终返回 false 时,按以下顺序排查:

  1. 硬件层验证

    • 使用手机蓝牙扫描APP(如nRF Connect)确认ESP32蓝牙模块已上电且可见(名称应为 ESP32 或自定义名);
    • 检查PS3控制器红灯是否闪烁(表示正在搜索主机),若常亮则说明已连接其他设备。
  2. MAC地址一致性验证

    • 在ESP32端打印当前MAC: ESP_LOGI("BT", "ESP32 MAC: %02X:%02X:%02X:%02X:%02X:%02X", ...)
    • 使用 SixaxisPairTool 读取控制器内存储的MAC,二者必须 逐字节完全相同
  3. 协议栈日志分析

    • menuconfig 中启用 Component config → Bluetooth → Bluedroid Enable → Bluedroid debug log ,等级设为 VERBOSE
    • 观察日志中是否出现 SPP server started on channel 1 ,若无此日志,说明SPP服务未启动;
    • 搜索 HCI_EVT_CMD_STATUS ,确认 esp_spp_start_srv() 返回 SUCCESS
  4. 电源与干扰排查

    • PS3控制器对电源噪声敏感,确保ESP32供电稳定(推荐使用≥2A的5V电源);
    • 避免将ESP32置于金属外壳内,或靠近Wi-Fi路由器、微波炉等2.4GHz强干扰源。

4.3 性能优化实践

  • 降低数据上报频率 :PS3控制器默认20ms发送一包,但多数应用无需如此高频率。可在 ps3.c 中修改 PS3_DATA_INTERVAL_MS 宏为 50 100 ,减少CPU中断负载;
  • 禁用未使用传感器 :若仅需按键与摇杆,可在 ps3Init() 后调用 ps3DisableSensor(PS3_SENSOR_ACCEL) 禁用加速度计,节省约15%的功耗;
  • 使用DMA加速UART输出 :在 Ps3DataNotify 示例中,将 Serial.print() 替换为 uart_write_bytes() 配合DMA,可将串口输出延迟从毫秒级降至微秒级。

5. 扩展应用场景与工程集成

5.1 机器人遥控中枢

将PS3控制器作为移动机器人主控手柄,利用其六轴IMU实现姿态控制:

// 读取陀螺仪Z轴角速度,控制机器人原地旋转
float gyro_z_deg_s = (float)ps3.gyroscope.z / 100.0f; // 假设校准后每100单位=1 deg/s
int16_t pwm_cmd = (int16_t)(gyro_z_deg_s * 5); // 比例系数5
set_motor_pwm(LEFT_MOTOR, -pwm_cmd);
set_motor_pwm(RIGHT_MOTOR, pwm_cmd);

5.2 工业HMI面板

结合OLED显示屏,将PS3控制器改造为工业设备调试面板:

  • SELECT + START 组合键触发设备重启;
  • L1/R1 调节参数步进值(1/10/100);
  • 摇杆 控制菜单光标, △/○/×/□ 对应功能按钮;
  • 屏幕实时显示 ps3.status.battery ps3.status.charging ,预警低电量。

5.3 与FreeRTOS高级特性集成

  • 按键事件队列 :创建 QueueHandle_t ps3_event_queue ,在 controller_event_cb() 中将 event 结构体 xQueueSend() 入队,由独立任务消费,解耦事件处理与UI刷新;
  • 连接状态信号量 :使用 SemaphoreHandle_t conn_sem ,在 PS3_EVENT_CONNECTED xSemaphoreGive() ,在 PS3_EVENT_DISCONNECTED xSemaphoreTake() ,使关键任务仅在连接有效时运行;
  • 低功耗模式联动 :当 ps3.status.battery < 20 !ps3.status.charging 时,调用 esp_light_sleep_start() 进入Light Sleep,由PS键GPIO中断唤醒。

该库的真正价值,在于它将一个消费级游戏外设,转化为一个经过充分验证的、具备六自由度输入与可靠无线连接的工业级人机接口模块。其代码结构、错误处理逻辑与资源管理策略,均可直接作为嵌入式蓝牙主机开发的参考模板。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐