這頁內容已整合到新的說明頁:掌控者 PS2 › Arduino › Arduino IDE

掌控者 PS2 × Arduino 範例

回到 PS2 遙控接收器首頁 | ∿ MBC 通用 I2C 協議 v2(進階)

這一頁有 6 個範例,從「確認接好了沒」一路做到「坦克遙控車」。每個範例都可以直接複製、上傳,程式裡每一行都有中文說明。完全沒寫過程式也沒關係,照順序一個一個做就好。

提示

第一次用 Arduino?先看 🔰 第一次用 Arduino:從零開始,15 分鐘學會安裝軟體、上傳程式、看結果。

下載全部範例(ZIP)

接線(所有範例都一樣)

接收器 Arduino UNO / Nano Arduino UNO R4 ESP32
VCC 5V 5V 5V(VIN)
GND GND GND GND
RX(= SDA) A4 SDA GPIO 21
TX(= SCL) A5 SCL GPIO 22
掌控者 PS2 接 Arduino UNO 的接線圖

重要

  • 請用 通用 I2C 版 的接收器。SPIKE 版、EV3 版的韌體不同,不能直接接 Arduino。
  • 接收器上印的是 TX、RX:I2C 版的 TX 就是 SCL、RX 就是 SDA。
  • 手把中間的 ANALOG 鍵要讓燈亮著(類比模式),搖桿才會有數字。燈熄 = 數位模式,只有按鍵能用(編號 0B 手把類型會告訴你是 1 數位還是 2 類比)。
  • 接收器裡面已經有電壓轉換,UNO(5 V)、UNO R4 和 ESP32(3.3 V)都可以直接接。
  • 接線前先拔掉 USB 線。

指令總覽(這個接收器能讀到什麼)

接收器就像一排有編號的「格子」,每一格放一個數字(int16)。寫 [編號] 再讀 2 個 byte 就拿到一個值;連續的編號可以一次讀一串(最多 16 個)。範例裡的 mbcRead(ADDR, 編號)、mbcReadMany(ADDR, 起始編號, 幾個, 陣列) 就是在做這件事。PS2 和 PS4/PS5 接收器的編號完全一樣。

編號 名稱 範圍/單位 說明 出現在
01 十六鍵 0~65535(bit) 16 顆鍵合成一個數(PSX 排列):bit0 SELECT、1 L3、2 R3、3 START、4 ↑、5 →、6 ↓、7 ←、8 L2、9 R2、10 L1、11 R1、12 ▲、13 ●、14 ✕、15 ■;1 = 按下;未連線 = 0 範例 2、3
02 方向鍵 0~15 ↑ 1、→ 2、↓ 4、← 8,同時按相加 範例 2、3
03 形狀鍵 0~15 ▲ 1、● 2、✕ 4、■ 8 範例 2、3
04 肩鍵 0~15 L2 1、R2 2、L1 4、R1 8 範例 2、3
05 功能鍵 0~15 SELECT 1、L3 2、R3 4、START 8 範例 2、3
06 已連線 0~1 1 = 手把已連線;斷線時按鍵全 0、搖桿 0 範例 2、5、6
07 電量 0 PS2 手把沒有電量,恆 0(PS4/PS5 才有) 範例 1、2、5
08 L2 類比 0~255 DualShock 2 類比模式才有壓力值;其他手把按下 = 255、放開 = 0 範例 2、5
09 R2 類比 0~255 同上 範例 2、5
0A 系統鍵 0 PS2 手把沒有 PS 鍵,恆 0 範例 2、5
0B 手把類型 0~2 0 沒有手把、1 PS2 數位模式、2 PS2 類比模式(ANALOG 燈亮) 範例 2、5
10~`13` 搖桿 LX/LY/RX/RY −100~+100 0 = 中間,右/上為正(數位模式時都是 0) 範例 2、4、6
F0 裝置 ID — 低 byte = 出廠位址 0x42、高 byte = 款式(1 PS2、2 PS4/PS5) 範例 1
F1 協議版本 2 這份範例照版本 2 寫 範例 1
F2 韌體版本 主×100+次 例如 102 = 1.2 範例 1
F3 狀態旗標 bit b0 就緒、b1 手把已連線 範例 1、所有範例的 mbcWaitReady
F5 開機秒數 0~32767 數字變小 = 接收器重開機過 範例 1
E5(寫) 震動 [強度] PS2 接收器收到會回 ACK 但忽略(PS4/PS5 才有作用),所以這一頁沒有範例 —
E6(寫) 燈條顏色 [R][G][B] 同上,PS2 手把沒有燈條 —

注意

沒有的編號(例如 0C~`0F)會讀到 -32768(範例裡叫 MBC_NONE),讀不到接收器也是 -32768`。完整規格見 MBC 通用 I2C 協議 v2。


範例總覽

# 範例 學會什麼 另外要準備
1 跟接收器打招呼 確認接線正確、讀身分證(F0~F3)、看手把連上沒 無
2 把所有的值一次印出來 看懂每個編號放什麼(010B、1013) 無
3 按了哪一顆鍵? 十六鍵拆 bit、四組分組鍵 無
4 搖桿推多少? 搖桿 ±100,畫成十字圖 無
5 手把的狀態 連線、扳機類比、手把類型 無
6 坦克遙控車(進階) 兩支搖桿開車、斷線自動停車 兩輪小車、L298N 馬達驅動板

提示

建議順序:1 → 2 → 3 → 4 → 5 → 6。 只有 6 要另外準備零件。


1️⃣ 跟接收器打招呼

做什麼: 確認接收器接好了,印出它的「身分證」(出廠位址、款式、協議版本、韌體版本、狀態旗標),然後一直看著手把:連上、斷線都會說。每次接新的接收器,都先跑這一個。

步驟:

  1. 照上面的表格接好 4 條線,USB 插上電腦。
  2. 複製下面的程式,貼到 Arduino IDE,按 → 上傳。
  3. 打開序列埠監控窗(右上角放大鏡),速度選 115200。
  4. 把手把開機(接收器的燈亮),看電腦有沒有說「手把連上了」。

下載 ps2_01_hello.ino

/*
 * 掌控者 PS2 × Arduino 範例 1:跟接收器打招呼
 * ------------------------------------------------------------
 * 這個範例會做什麼?
 *   確認接收器有沒有接好。接好了,會印出它的身分證(裝置 ID、款式、協議版本、
 *   韌體版本)和目前狀態。之後一直看著手把:連上、斷線都會告訴你。
 *   每次接新的接收器,都建議先跑這個範例。
 *
 * 你需要:掌控者 PS2 接收器(通用 I2C 版)+ PS2 手把、Arduino UNO/Nano/UNO R4 或 ESP32。
 *
 * 接線:
 *   接收器 VCC → 5V      接收器 GND → GND
 *   接收器 RX(= SDA) → A4      接收器 TX(= SCL) → A5         (UNO / Nano)
 *   UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
 *   (I2C 版接收器上印的 TX 就是 SCL、RX 就是 SDA)
 *
 * 怎麼看結果:
 *   上傳後,打開「序列埠監控窗」,速度選 115200。
 *   把手把開機(接收器的燈亮),看電腦有沒有說「手把連上了」。
 *
 * 小提醒:
 *   手把中間的 ANALOG 鍵要讓燈亮著(類比模式),搖桿才會有數字。
 */

#include <Wire.h>                        // Arduino 內建的 I2C 通訊工具,用來跟接收器講話

const uint8_t ADDR = 0x42;               // 掌控者接收器的「門牌號碼」(I2C 位址),PS2 與 PS4 都是 0x42
const int16_t MBC_NONE = -32768;         // 「沒有這個值/讀不到」的代號

// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);

bool wasConnected = false;               // 上一次看的時候,手把有沒有連上

void setup() {
  Serial.begin(115200);                  // 打開跟電腦講話的通道,速度 115200
  mbcBegin();                            // 準備好 I2C 通訊(小幫手在檔案最下面)
  delay(500);                            // 等接收器開機 0.5 秒

  Serial.println(F("=== 掌控者 PS2:跟接收器打招呼 ==="));

  // 等接收器說「我準備好了」(狀態旗標 F3 的 bit0),最多等 5 秒
  if (!mbcWaitReady(ADDR, 5000)) {
    Serial.println(F("找不到接收器!請檢查:"));
    Serial.println(F("  1. RX(SDA)接 A4、TX(SCL)接 A5(ESP32:21 / 22),有沒有接反?"));
    Serial.println(F("  2. VCC 接 5V、GND 接 GND,有沒有插緊?接收器的燈有沒有亮?"));
    Serial.println(F("  3. 你的接收器是「通用 I2C 版」嗎?(SPIKE 版、EV3 版不能接 Arduino)"));
    while (true) {}                      // 找不到就停在這裡
  }

  // ---- F0 裝置 ID:低 byte = 出廠位址、高 byte = 款式(1 = PS2、2 = PS4/PS5) ----
  int16_t id = mbcRead(ADDR, 0xF0);
  uint8_t factoryAddr = (uint8_t)(id & 0xFF);        // 出廠位址(換過位址也不變)
  uint8_t kind        = (uint8_t)((id >> 8) & 0xFF); // hw[0]:1 = PS2 接收器、2 = PS4/PS5 接收器
  Serial.print(F("找到了!出廠位址 0x"));
  Serial.println(factoryAddr, HEX);      // 應該是 42
  Serial.print(F("款式:"));
  if (kind == 1)      Serial.println(F("PS2 2.4G 接收器"));
  else if (kind == 2) Serial.println(F("PS4/PS5 藍牙接收器"));
  else                Serial.println(F("不認識的款式"));

  // ---- F1 協議版本(這份範例是照版本 2 寫的) ----
  Serial.print(F("協議版本:"));
  Serial.println(mbcRead(ADDR, 0xF1));

  // ---- F2 韌體版本:主版×100 + 次版(例如 102 = 1.2) ----
  int16_t fw = mbcRead(ADDR, 0xF2);
  Serial.print(F("韌體版本:"));
  Serial.print(fw / 100);
  Serial.print('.');
  Serial.println(fw % 100);

  // ---- F3 狀態旗標:一個 bit 代表一件事 ----
  int16_t st = mbcRead(ADDR, 0xF3);
  Serial.println(F("目前狀態:"));
  Serial.print(F("  bit0 就緒        ")); Serial.println((st & 0x01) ? F("是") : F("否"));
  Serial.print(F("  bit1 手把已連線  ")); Serial.println((st & 0x02) ? F("是") : F("否"));

  // ---- F5 開機秒數(順便看它開機多久了) ----
  Serial.print(F("開機秒數:"));
  Serial.println(mbcRead(ADDR, 0xF5));

  Serial.println(F("接線正確!現在把手把開機(接收器的燈亮)試試看。"));
}

void loop() {
  // 每一圈都看一下 F3 的 bit1(手把有沒有連上),有變化才印,畫面才不會洗版
  int16_t st = mbcRead(ADDR, 0xF3);
  if (st == MBC_NONE) {                  // 讀不到 = 線鬆掉了
    Serial.println(F("讀不到接收器,線鬆掉了嗎?"));
    delay(1000);
    return;
  }
  bool connected = st & 0x02;
  if (connected != wasConnected) {
    if (connected) {
      Serial.print(F("手把連上了!電量 "));
      Serial.print(mbcRead(ADDR, 0x07));  // 07 電量 0~100(PS2 手把沒有電量,恆 0)
      Serial.println(F("%"));
    } else {
      Serial.println(F("手把沒有連線"));
    }
    wasConnected = connected;
  }
  delay(50);
}

// ============================================================
// 以下是「通訊小幫手」(MBC I2C 協議 v2):負責跟感應器講話。
// 所有 MBC 範例都一樣,不用改,也不用看懂。
//   讀法:寫 [編號] → 等 2 ms → 讀 2×k 個 byte = 從該編號起連續 k 個 int16(小端)
//   沒有這個編號(空號)時感應器回 -32768(MBC_NONE)
// ============================================================

// 開始 I2C 通訊(setup 裡呼叫一次)
// UNO/Nano:SDA=A4、SCL=A5;UNO R4:SDA/SCL 腳;ESP32:預設 GPIO21(SDA)/22(SCL)
void mbcBegin() {
  Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
  Wire.setWireTimeout(25000, true);      // 線沒接好時,不會讓 Arduino 當機卡住
#endif
}

// 從位址 addr 的感應器,由編號 id 起連續讀 k 個值(1~16)放進 out。全部讀到回傳 true
bool mbcReadMany(uint8_t addr, uint8_t id, uint8_t k, int16_t *out) {
  if (k < 1) k = 1;
  if (k > 16) k = 16;                    // 一次最多 16 個值(32 byte,Wire 緩衝區上限)
  Wire.beginTransmission(addr);
  Wire.write(id);                        // 告訴感應器:我要從第 id 號開始讀
  if (Wire.endTransmission() != 0) return false;
  delay(2);                              // 等 2 ms 再讀(ESP32 型的產品一定要等)
  uint8_t n = (uint8_t)(2 * k);
  if (Wire.requestFrom(addr, n) != n) return false;
  for (uint8_t i = 0; i < k; i++) {
    uint16_t lo = Wire.read();           // 先來的是低位元組
    uint16_t hi = Wire.read();           // 再來高位元組(小端)
    out[i] = (int16_t)(uint16_t)(lo | (hi << 8));
  }
  return true;
}

// 讀一個值。讀不到(沒接好)回傳 MBC_NONE
int16_t mbcRead(uint8_t addr, uint8_t id) {
  int16_t v;
  return mbcReadMany(addr, id, 1, &v) ? v : MBC_NONE;
}

// 送命令:寫 [id][參數…]。p 指向參數、n = 參數幾個 byte(沒參數就不填)。成功回傳 true
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p, uint8_t n) {
  Wire.beginTransmission(addr);
  Wire.write(id);
  for (uint8_t i = 0; i < n; i++) Wire.write(p[i]);
  return Wire.endTransmission() == 0;
}

// 等感應器準備好(狀態旗標 F3 的 bit0 = 1),最多等 ms 毫秒。準備好了回傳 true
bool mbcWaitReady(uint8_t addr, uint16_t ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    int16_t f = mbcRead(addr, 0xF3);
    if (f != MBC_NONE && (f & 1)) return true;
    delay(50);
  }
  return false;
}

你會看到:(手把連上,之後把手把關機)

=== 掌控者 PS2:跟接收器打招呼 ===
找到了!出廠位址 0x42
款式:PS2 2.4G 接收器
協議版本:2
韌體版本:1.2
目前狀態:
  bit0 就緒        是
  bit1 手把已連線  否
開機秒數:3
接線正確!現在把手把開機(接收器的燈亮)試試看。
手把連上了!電量 0%
手把沒有連線

注意

「通訊小幫手」是什麼? 每個範例最下面都有一段一模一樣的程式,負責跟接收器「講話」。你不用改它,也不用看懂。只要記得:

  • mbcRead(ADDR, 編號):讀一個值(讀不到回 -32768)
  • mbcReadMany(ADDR, 起始編號, 幾個, 陣列):從某個編號起一次讀一串(最多 16 個,同一瞬間的值)
  • mbcWrite(ADDR, 命令, 參數, 幾個 byte):送命令(PS2 用不到)
  • mbcWaitReady(ADDR, 毫秒):等接收器開機好

小幫手裡有一行 delay(2)(寫完編號等 2 毫秒再讀):PS2 接收器本身不需要,但同位址的 PS4/PS5 接收器需要,所以所有 MBC 範例都留著,代價很小。

PS2 手把沒有電量,所以「電量 0%」是正常的。


2️⃣ 把所有的值一次印出來

做什麼: 每 0.5 秒把值表裡的每一個值都印出來:010B 一次讀 11 個、1013 一次讀 4 個。按按鍵、推搖桿,看哪一行在變,就知道每個編號是什麼。

下載 ps2_02_all_values.ino

/*
 * 掌控者 PS2 × Arduino 範例 2:把所有的值一次印出來
 * ------------------------------------------------------------
 * 這個範例會做什麼?
 *   每 0.5 秒把接收器「值表」裡的每一個值都印出來,讓你看懂每個編號放什麼:
 *     01~0B:按鍵、連線、電量、扳機、系統鍵、手把類型(一次讀 11 個)
 *     10~13:四根搖桿軸 LX、LY、RX、RY(一次讀 4 個)
 *   連續的編號可以「一次讀一串」(mbcReadMany),讀到的是同一瞬間的值。
 *
 * 你需要:掌控者 PS2 接收器(通用 I2C 版)+ PS2 手把、Arduino UNO/Nano/UNO R4 或 ESP32。
 *
 * 接線:
 *   接收器 VCC → 5V      接收器 GND → GND
 *   接收器 RX(= SDA) → A4      接收器 TX(= SCL) → A5         (UNO / Nano)
 *   UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
 *   (I2C 版接收器上印的 TX 就是 SCL、RX 就是 SDA)
 *
 * 怎麼看結果:
 *   上傳後,打開「序列埠監控窗」,速度選 115200。按按鍵、推搖桿,看哪個數字在變。
 *
 * 小提醒:
 *   手把中間的 ANALOG 鍵要讓燈亮著(類比模式),搖桿才會有數字。
 */

#include <Wire.h>                        // Arduino 內建的 I2C 通訊工具

const uint8_t ADDR = 0x42;               // 掌控者接收器的「門牌號碼」(I2C 位址),PS2 與 PS4 都是 0x42
const int16_t MBC_NONE = -32768;         // 「沒有這個值/讀不到」的代號

// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);

unsigned long lastPrint = 0;             // 上一次印的時間

void setup() {
  Serial.begin(115200);
  mbcBegin();
  delay(500);
  if (!mbcWaitReady(ADDR, 5000)) {
    Serial.println(F("找不到接收器,請先跑範例 1 檢查接線。"));
    while (true) {}
  }
  Serial.println(F("準備好了!每 0.5 秒印一次所有的值。"));
}

void loop() {
  if (millis() - lastPrint >= 500) {     // 👉 可以改:每 0.5 秒印一次
    lastPrint = millis();

    // ---- 第一段:編號 01~0B,一次讀 11 個值 ----
    int16_t a[11];
    if (!mbcReadMany(ADDR, 0x01, 11, a)) { Serial.println(F("讀不到(第一段)")); return; }
    // ---- 第二段:編號 10~13,一次讀 4 個值 ----
    int16_t s[4];
    if (!mbcReadMany(ADDR, 0x10, 4, s)) { Serial.println(F("讀不到(第二段)")); return; }

    Serial.println(F("------------------------------"));
    Serial.print(F("01 十六鍵(PSX 排列,二進位):")); printBits16((uint16_t)a[0]); Serial.println();
    Serial.print(F("02 方向鍵(上1 右2 下4 左8)  :")); Serial.println(a[1]);
    Serial.print(F("03 形狀鍵(▲1 ●2 ✕4 ■8)     :")); Serial.println(a[2]);
    Serial.print(F("04 肩鍵  (L2 1 R2 2 L1 4 R1 8):")); Serial.println(a[3]);
    Serial.print(F("05 功能鍵(SELECT1 L3 2 R3 4 START8):")); Serial.println(a[4]);
    Serial.print(F("06 已連線(1 = 有)           :")); Serial.println(a[5]);
    Serial.print(F("07 電量 0~100(PS2 恆 0)     :")); Serial.println(a[6]);
    Serial.print(F("08 L2 類比 0~255              :")); Serial.println(a[7]);
    Serial.print(F("09 R2 類比 0~255              :")); Serial.println(a[8]);
    Serial.print(F("0A 系統鍵(b0 PS、b1 擷取)   :")); Serial.println(a[9]);
    Serial.print(F("0B 手把類型 0~4               :")); Serial.println(a[10]);
    Serial.print(F("10 LX  11 LY  12 RX  13 RY(±100):"));
    for (uint8_t i = 0; i < 4; i++) { Serial.print(s[i]); Serial.print(i < 3 ? F("  ") : F("")); }
    Serial.println();
  }
  delay(50);
}

// 把 16 位數字印成 16 個 0/1(左邊是 bit15 ■、右邊是 bit0 SELECT)
void printBits16(uint16_t v) {
  for (int8_t i = 15; i >= 0; i--) {
    Serial.print((v >> i) & 1);
    if (i % 4 == 0 && i > 0) Serial.print(' ');   // 每 4 位空一格比較好看
  }
}

// ============================================================
// 以下是「通訊小幫手」(MBC I2C 協議 v2):負責跟感應器講話。
// 所有 MBC 範例都一樣,不用改,也不用看懂。
//   讀法:寫 [編號] → 等 2 ms → 讀 2×k 個 byte = 從該編號起連續 k 個 int16(小端)
//   沒有這個編號(空號)時感應器回 -32768(MBC_NONE)
// ============================================================

// 開始 I2C 通訊(setup 裡呼叫一次)
// UNO/Nano:SDA=A4、SCL=A5;UNO R4:SDA/SCL 腳;ESP32:預設 GPIO21(SDA)/22(SCL)
void mbcBegin() {
  Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
  Wire.setWireTimeout(25000, true);      // 線沒接好時,不會讓 Arduino 當機卡住
#endif
}

// 從位址 addr 的感應器,由編號 id 起連續讀 k 個值(1~16)放進 out。全部讀到回傳 true
bool mbcReadMany(uint8_t addr, uint8_t id, uint8_t k, int16_t *out) {
  if (k < 1) k = 1;
  if (k > 16) k = 16;                    // 一次最多 16 個值(32 byte,Wire 緩衝區上限)
  Wire.beginTransmission(addr);
  Wire.write(id);                        // 告訴感應器:我要從第 id 號開始讀
  if (Wire.endTransmission() != 0) return false;
  delay(2);                              // 等 2 ms 再讀(ESP32 型的產品一定要等)
  uint8_t n = (uint8_t)(2 * k);
  if (Wire.requestFrom(addr, n) != n) return false;
  for (uint8_t i = 0; i < k; i++) {
    uint16_t lo = Wire.read();           // 先來的是低位元組
    uint16_t hi = Wire.read();           // 再來高位元組(小端)
    out[i] = (int16_t)(uint16_t)(lo | (hi << 8));
  }
  return true;
}

// 讀一個值。讀不到(沒接好)回傳 MBC_NONE
int16_t mbcRead(uint8_t addr, uint8_t id) {
  int16_t v;
  return mbcReadMany(addr, id, 1, &v) ? v : MBC_NONE;
}

// 送命令:寫 [id][參數…]。p 指向參數、n = 參數幾個 byte(沒參數就不填)。成功回傳 true
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p, uint8_t n) {
  Wire.beginTransmission(addr);
  Wire.write(id);
  for (uint8_t i = 0; i < n; i++) Wire.write(p[i]);
  return Wire.endTransmission() == 0;
}

// 等感應器準備好(狀態旗標 F3 的 bit0 = 1),最多等 ms 毫秒。準備好了回傳 true
bool mbcWaitReady(uint8_t addr, uint16_t ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    int16_t f = mbcRead(addr, 0xF3);
    if (f != MBC_NONE && (f & 1)) return true;
    delay(50);
  }
  return false;
}

你會看到:(按住 ✕ 和 ↑,左搖桿推到最右;手把在類比模式)

準備好了!每 0.5 秒印一次所有的值。
------------------------------
01 十六鍵(PSX 排列,二進位):0100 0000 0001 0000
02 方向鍵(上1 右2 下4 左8)  :1
03 形狀鍵(▲1 ●2 ✕4 ■8)     :4
04 肩鍵  (L2 1 R2 2 L1 4 R1 8):0
05 功能鍵(SELECT1 L3 2 R3 4 START8):0
06 已連線(1 = 有)           :1
07 電量 0~100(PS2 恆 0)     :0
08 L2 類比 0~255              :0
09 R2 類比 0~255              :0
0A 系統鍵(b0 PS、b1 擷取)   :0
0B 手把類型 0~4               :2
10 LX  11 LY  12 RX  13 RY(±100):100  0  0  0

提示

二進位那一行左邊是 bit15(■)、右邊是 bit0(SELECT)。上面 ✕ 是 bit14、↑ 是 bit4,所以看到兩個 1。搖桿全部是 0 的話,按一下手把的 ANALOG 鍵(燈要亮)。


3️⃣ 按了哪一顆鍵?

做什麼: 按下手把上任何一顆鍵,電腦會用中文告訴你按了哪一顆;放開時也會說。可以同時按好幾顆。四組「分組鍵」(方向、形狀、肩、功能)有變化時也會印一行。

下載 ps2_03_buttons.ino

/*
 * 掌控者 PS2 × Arduino 範例 3:按了哪一顆鍵?
 * ------------------------------------------------------------
 * 這個範例會做什麼?
 *   按下手把上任何一顆鍵,電腦會用中文告訴你按了哪一顆;放開時也會說。
 *   可以同時按好幾顆。
 *   16 顆鍵的狀態放在編號 01(十六鍵)一個數字裡,一顆鍵佔一個「位」(bit):
 *   按下 = 1、沒按 = 0,就像 16 個電燈開關排成一排。
 *   編號 02~05 是同一批按鍵的「分組版」:方向鍵、形狀鍵、肩鍵、功能鍵各 4 顆,
 *   一組 0~15,同時按就相加(例如 ↑ + → = 1 + 2 = 3)。
 *
 * 你需要:掌控者 PS2 接收器(通用 I2C 版)+ PS2 手把、Arduino UNO/Nano/UNO R4 或 ESP32。
 *
 * 接線:
 *   接收器 VCC → 5V      接收器 GND → GND
 *   接收器 RX(= SDA) → A4      接收器 TX(= SCL) → A5         (UNO / Nano)
 *   UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
 *   (I2C 版接收器上印的 TX 就是 SCL、RX 就是 SDA)
 *
 * 怎麼看結果:
 *   上傳後,打開「序列埠監控窗」,速度選 115200。把手把開機(接收器的燈亮),按按看。
 *
 * 小提醒:
 *   手把中間的 ANALOG 鍵要讓燈亮著(類比模式),搖桿才會有數字。
 *   PS2 手把沒有 PS 鍵,編號 0A 系統鍵恆 0。
 */

#include <Wire.h>                        // Arduino 內建的 I2C 通訊工具

const uint8_t ADDR = 0x42;               // 掌控者接收器的「門牌號碼」(I2C 位址),PS2 與 PS4 都是 0x42
const int16_t MBC_NONE = -32768;         // 「沒有這個值/讀不到」的代號

// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);

uint16_t lastButtons = 0;                // 上一次看到的十六鍵狀態
int16_t  lastGroup[4] = {0, 0, 0, 0};    // 上一次看到的 02~05 四組

void setup() {
  Serial.begin(115200);
  mbcBegin();
  delay(500);
  if (!mbcWaitReady(ADDR, 5000)) {
    Serial.println(F("找不到接收器,請先跑範例 1 檢查接線。"));
    while (true) {}
  }
  Serial.println(F("準備好了!按手把上的按鍵看看。"));
}

void loop() {
  // 一次讀編號 01~05 共 5 個值:[0] 十六鍵 [1] 方向鍵 [2] 形狀鍵 [3] 肩鍵 [4] 功能鍵
  int16_t v[5];
  if (!mbcReadMany(ADDR, 0x01, 5, v)) {
    delay(100);                          // 這次沒讀到,等一下再試
    return;
  }
  uint16_t buttons = (uint16_t)v[0];     // 十六鍵:PSX 排列(bit0 SELECT … bit15 ■)

  // ---- 1. 一顆一顆檢查:哪些鍵「剛按下」、哪些「剛放開」 ----
  for (uint8_t i = 0; i < 16; i++) {
    uint16_t mask = 1u << i;             // 第 i 顆鍵的「位」
    bool now    = buttons & mask;        // 現在有沒有按
    bool before = lastButtons & mask;    // 上一次有沒有按
    if (now && !before) { Serial.print(F("按下:")); printButtonName(i); Serial.println(); }
    if (!now && before) { Serial.print(F("放開:")); printButtonName(i); Serial.println(); }
  }
  lastButtons = buttons;                 // 記住這一次,下一次拿來比較

  // ---- 2. 四組分組值有變化就印一行(方向/形狀/肩/功能,各 0~15) ----
  bool changed = false;
  for (uint8_t i = 0; i < 4; i++) if (v[i + 1] != lastGroup[i]) changed = true;
  if (changed) {
    Serial.print(F("    分組 → 方向鍵 ")); Serial.print(v[1]);
    Serial.print(F("  形狀鍵 ")); Serial.print(v[2]);
    Serial.print(F("  肩鍵 ")); Serial.print(v[3]);
    Serial.print(F("  功能鍵 ")); Serial.println(v[4]);
    for (uint8_t i = 0; i < 4; i++) lastGroup[i] = v[i + 1];
  }

  delay(50);                             // 每 0.05 秒看一次,按很快也抓得到
}

// 印出十六鍵裡第 i 位的按鍵名字(PSX 排列)
void printButtonName(uint8_t i) {
  switch (i) {
    case 0:  Serial.print(F("SELECT(PS4:SHARE/PS5:建立)")); break;
    case 1:  Serial.print(F("L3(左搖桿往下壓)")); break;
    case 2:  Serial.print(F("R3(右搖桿往下壓)")); break;
    case 3:  Serial.print(F("START(PS4/PS5:OPTIONS)")); break;
    case 4:  Serial.print(F("↑ 方向鍵上")); break;
    case 5:  Serial.print(F("→ 方向鍵右")); break;
    case 6:  Serial.print(F("↓ 方向鍵下")); break;
    case 7:  Serial.print(F("← 方向鍵左")); break;
    case 8:  Serial.print(F("L2")); break;
    case 9:  Serial.print(F("R2")); break;
    case 10: Serial.print(F("L1")); break;
    case 11: Serial.print(F("R1")); break;
    case 12: Serial.print(F("▲ 三角形")); break;
    case 13: Serial.print(F("● 圓圈")); break;
    case 14: Serial.print(F("✕ 叉叉")); break;
    case 15: Serial.print(F("■ 正方形")); break;
  }
}

// ============================================================
// 以下是「通訊小幫手」(MBC I2C 協議 v2):負責跟感應器講話。
// 所有 MBC 範例都一樣,不用改,也不用看懂。
//   讀法:寫 [編號] → 等 2 ms → 讀 2×k 個 byte = 從該編號起連續 k 個 int16(小端)
//   沒有這個編號(空號)時感應器回 -32768(MBC_NONE)
// ============================================================

// 開始 I2C 通訊(setup 裡呼叫一次)
// UNO/Nano:SDA=A4、SCL=A5;UNO R4:SDA/SCL 腳;ESP32:預設 GPIO21(SDA)/22(SCL)
void mbcBegin() {
  Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
  Wire.setWireTimeout(25000, true);      // 線沒接好時,不會讓 Arduino 當機卡住
#endif
}

// 從位址 addr 的感應器,由編號 id 起連續讀 k 個值(1~16)放進 out。全部讀到回傳 true
bool mbcReadMany(uint8_t addr, uint8_t id, uint8_t k, int16_t *out) {
  if (k < 1) k = 1;
  if (k > 16) k = 16;                    // 一次最多 16 個值(32 byte,Wire 緩衝區上限)
  Wire.beginTransmission(addr);
  Wire.write(id);                        // 告訴感應器:我要從第 id 號開始讀
  if (Wire.endTransmission() != 0) return false;
  delay(2);                              // 等 2 ms 再讀(ESP32 型的產品一定要等)
  uint8_t n = (uint8_t)(2 * k);
  if (Wire.requestFrom(addr, n) != n) return false;
  for (uint8_t i = 0; i < k; i++) {
    uint16_t lo = Wire.read();           // 先來的是低位元組
    uint16_t hi = Wire.read();           // 再來高位元組(小端)
    out[i] = (int16_t)(uint16_t)(lo | (hi << 8));
  }
  return true;
}

// 讀一個值。讀不到(沒接好)回傳 MBC_NONE
int16_t mbcRead(uint8_t addr, uint8_t id) {
  int16_t v;
  return mbcReadMany(addr, id, 1, &v) ? v : MBC_NONE;
}

// 送命令:寫 [id][參數…]。p 指向參數、n = 參數幾個 byte(沒參數就不填)。成功回傳 true
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p, uint8_t n) {
  Wire.beginTransmission(addr);
  Wire.write(id);
  for (uint8_t i = 0; i < n; i++) Wire.write(p[i]);
  return Wire.endTransmission() == 0;
}

// 等感應器準備好(狀態旗標 F3 的 bit0 = 1),最多等 ms 毫秒。準備好了回傳 true
bool mbcWaitReady(uint8_t addr, uint16_t ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    int16_t f = mbcRead(addr, 0xF3);
    if (f != MBC_NONE && (f & 1)) return true;
    delay(50);
  }
  return false;
}

你會看到:(按 ✕、再同時按 ↑ 和 R1)

準備好了!按手把上的按鍵看看。
按下:✕ 叉叉
    分組 → 方向鍵 0  形狀鍵 4  肩鍵 0  功能鍵 0
放開:✕ 叉叉
    分組 → 方向鍵 0  形狀鍵 0  肩鍵 0  功能鍵 0
按下:↑ 方向鍵上
按下:R1
    分組 → 方向鍵 1  形狀鍵 0  肩鍵 8  功能鍵 0

16 顆按鍵各在第幾位?

位 按鍵 位 按鍵
0 SELECT 8 L2
1 L3(左搖桿往下壓) 9 R2
2 R3(右搖桿往下壓) 10 L1
3 START 11 R1
4 ↑ 上 12 ▲ 三角形
5 → 右 13 ● 圓圈
6 ↓ 下 14 ✕ 叉叉
7 ← 左 15 ■ 正方形

提示

試試看: 想知道「✕ 有沒有按」,寫 if (buttons & (1u << 14)) { ... } 就好;或者讀編號 03 形狀鍵,if (v & 4)。


4️⃣ 搖桿推多少?

做什麼: 把兩支搖桿的位置變成數字(編號 10 LX、11 LY、12 RX、13 RY):放開 = 0,往右、往上推是正數,往左、往下推是負數,推到底是 +100 或 -100。每 0.3 秒印一次,還畫成兩個十字圖。

下載 ps2_04_sticks.ino

/*
 * 掌控者 PS2 × Arduino 範例 4:搖桿推多少?
 * ------------------------------------------------------------
 * 這個範例會做什麼?
 *   把兩支搖桿的位置變成數字(編號 10 LX、11 LY、12 RX、13 RY):
 *     放開(在中間)= 0;往右推、往上推 = 正數,推到底 = +100;
 *     往左推、往下推 = 負數,推到底 = -100。
 *   每 0.3 秒印一次數字,並畫成兩個「十字圖」:O 就是搖桿現在的位置。
 *
 * 你需要:掌控者 PS2 接收器(通用 I2C 版)+ PS2 手把、Arduino UNO/Nano/UNO R4 或 ESP32。
 *
 * 接線:
 *   接收器 VCC → 5V      接收器 GND → GND
 *   接收器 RX(= SDA) → A4      接收器 TX(= SCL) → A5         (UNO / Nano)
 *   UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
 *   (I2C 版接收器上印的 TX 就是 SCL、RX 就是 SDA)
 *
 * 怎麼看結果:
 *   上傳後,打開「序列埠監控窗」,速度選 115200。慢慢推搖桿,看 O 怎麼跑。
 *
 * 小提醒:
 *   搖桿在中間附近輕晃,數字會停在 0,這是接收器故意的(叫做「死區」),放開時車子才不會自己慢慢跑。
 *   手把中間的 ANALOG 鍵要讓燈亮著(類比模式),搖桿才會有數字。
 */

#include <Wire.h>                        // Arduino 內建的 I2C 通訊工具

const uint8_t ADDR = 0x42;               // 掌控者接收器的「門牌號碼」(I2C 位址),PS2 與 PS4 都是 0x42
const int16_t MBC_NONE = -32768;         // 「沒有這個值/讀不到」的代號

// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);

unsigned long lastPrint = 0;

void setup() {
  Serial.begin(115200);
  mbcBegin();
  delay(500);
  if (!mbcWaitReady(ADDR, 5000)) {
    Serial.println(F("找不到接收器,請先跑範例 1 檢查接線。"));
    while (true) {}
  }
  Serial.println(F("準備好了!推推看搖桿。"));
}

void loop() {
  if (millis() - lastPrint >= 300) {     // 👉 可以改:每 0.3 秒印一次
    lastPrint = millis();

    // 一次讀編號 10~13 共 4 個值:[0] LX [1] LY [2] RX [3] RY(-100 ~ +100)
    int16_t s[4];
    if (!mbcReadMany(ADDR, 0x10, 4, s)) { Serial.println(F("讀不到接收器")); return; }

    Serial.print(F("左搖桿 X ")); Serial.print(s[0]);
    Serial.print(F("  Y ")); Serial.print(s[1]);
    Serial.print(F("   |   右搖桿 X ")); Serial.print(s[2]);
    Serial.print(F("  Y ")); Serial.println(s[3]);
    drawCross(s[0], s[1], s[2], s[3]);   // 畫兩個十字圖
  }
  delay(50);
}

// 畫兩個 7×7 的十字圖(左搖桿、右搖桿並排)。中心是 +,搖桿位置畫 O
//   X:-100 ~ +100 → 第 0 ~ 6 格(左 → 右);Y:上為正,所以 +100 在最上面一列
void drawCross(int lx, int ly, int rx, int ry) {
  int lcol = map(lx, -100, 100, 0, 6), lrow = map(ly, 100, -100, 0, 6);
  int rcol = map(rx, -100, 100, 0, 6), rrow = map(ry, 100, -100, 0, 6);
  for (int row = 0; row < 7; row++) {
    for (int col = 0; col < 7; col++) Serial.print(cell(row, col, lrow, lcol));
    Serial.print(F("     "));            // 兩個圖中間空 5 格
    for (int col = 0; col < 7; col++) Serial.print(cell(row, col, rrow, rcol));
    Serial.println();
  }
  Serial.println();
}

// 決定圖上某一格要印什麼字:O = 搖桿位置、+ = 中心、| 和 - = 十字線、. = 空白
char cell(int row, int col, int posRow, int posCol) {
  if (row == posRow && col == posCol) return 'O';
  if (row == 3 && col == 3) return '+';
  if (col == 3) return '|';
  if (row == 3) return '-';
  return '.';
}

// ============================================================
// 以下是「通訊小幫手」(MBC I2C 協議 v2):負責跟感應器講話。
// 所有 MBC 範例都一樣,不用改,也不用看懂。
//   讀法:寫 [編號] → 等 2 ms → 讀 2×k 個 byte = 從該編號起連續 k 個 int16(小端)
//   沒有這個編號(空號)時感應器回 -32768(MBC_NONE)
// ============================================================

// 開始 I2C 通訊(setup 裡呼叫一次)
// UNO/Nano:SDA=A4、SCL=A5;UNO R4:SDA/SCL 腳;ESP32:預設 GPIO21(SDA)/22(SCL)
void mbcBegin() {
  Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
  Wire.setWireTimeout(25000, true);      // 線沒接好時,不會讓 Arduino 當機卡住
#endif
}

// 從位址 addr 的感應器,由編號 id 起連續讀 k 個值(1~16)放進 out。全部讀到回傳 true
bool mbcReadMany(uint8_t addr, uint8_t id, uint8_t k, int16_t *out) {
  if (k < 1) k = 1;
  if (k > 16) k = 16;                    // 一次最多 16 個值(32 byte,Wire 緩衝區上限)
  Wire.beginTransmission(addr);
  Wire.write(id);                        // 告訴感應器:我要從第 id 號開始讀
  if (Wire.endTransmission() != 0) return false;
  delay(2);                              // 等 2 ms 再讀(ESP32 型的產品一定要等)
  uint8_t n = (uint8_t)(2 * k);
  if (Wire.requestFrom(addr, n) != n) return false;
  for (uint8_t i = 0; i < k; i++) {
    uint16_t lo = Wire.read();           // 先來的是低位元組
    uint16_t hi = Wire.read();           // 再來高位元組(小端)
    out[i] = (int16_t)(uint16_t)(lo | (hi << 8));
  }
  return true;
}

// 讀一個值。讀不到(沒接好)回傳 MBC_NONE
int16_t mbcRead(uint8_t addr, uint8_t id) {
  int16_t v;
  return mbcReadMany(addr, id, 1, &v) ? v : MBC_NONE;
}

// 送命令:寫 [id][參數…]。p 指向參數、n = 參數幾個 byte(沒參數就不填)。成功回傳 true
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p, uint8_t n) {
  Wire.beginTransmission(addr);
  Wire.write(id);
  for (uint8_t i = 0; i < n; i++) Wire.write(p[i]);
  return Wire.endTransmission() == 0;
}

// 等感應器準備好(狀態旗標 F3 的 bit0 = 1),最多等 ms 毫秒。準備好了回傳 true
bool mbcWaitReady(uint8_t addr, uint16_t ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    int16_t f = mbcRead(addr, 0xF3);
    if (f != MBC_NONE && (f & 1)) return true;
    delay(50);
  }
  return false;
}

你會看到:(左搖桿推到右上角,右搖桿放開)

準備好了!推推看搖桿。
左搖桿 X 100  Y 100   |   右搖桿 X 0  Y 0
...|..O     ...|...
...|...     ...|...
...|...     ...|...
---+---     ---O---
...|...     ...|...
...|...     ...|...
...|...     ...|...

注意

搖桿在中間附近輕輕晃時數字會停在 0,這是接收器故意的(叫做「死區」),放開搖桿時車子才不會自己慢慢跑。數字一直是 0:按一下手把的 ANALOG 鍵。


5️⃣ 手把的狀態

做什麼: 每 0.5 秒印出編號 06~`0B`(一次讀 6 個):已連線、電量(PS2 恆 0)、L2/R2 扳機、系統鍵(PS2 恆 0)、手把類型(1 數位/2 類比)。連線狀態有變化時另外提醒一行。

下載 ps2_05_status.ino

/*
 * 掌控者 PS2 × Arduino 範例 5:手把的狀態
 * ------------------------------------------------------------
 * 這個範例會做什麼?
 *   每 0.5 秒印出跟「按鍵、搖桿」無關的狀態值(編號 06~0B,一次讀 6 個):
 *     06 已連線(1 = 有)、07 電量 0~100、08 L2 扳機壓多深 0~255、
 *     09 R2 扳機壓多深 0~255、0A 系統鍵(b0 PS 鍵、b1 擷取鍵)、0B 手把類型。
 *   連線狀態有變化時會另外印一行提醒。
 *
 * 你需要:掌控者 PS2 接收器(通用 I2C 版)+ PS2 手把、Arduino UNO/Nano/UNO R4 或 ESP32。
 *
 * 接線:
 *   接收器 VCC → 5V      接收器 GND → GND
 *   接收器 RX(= SDA) → A4      接收器 TX(= SCL) → A5         (UNO / Nano)
 *   UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
 *   (I2C 版接收器上印的 TX 就是 SCL、RX 就是 SDA)
 *
 * 怎麼看結果:
 *   上傳後,打開「序列埠監控窗」,速度選 115200。慢慢壓 L2、R2 看數字變化;把手把關機看「已連線」變 0。
 *
 * 小提醒:
 *   PS2 手把沒有電量、PS 鍵:07 和 0A 恆 0。
 *   L2/R2 類比:只有 DualShock 2 類比模式才有壓力值,其他手把按下 = 255、放開 = 0。
 */

#include <Wire.h>                        // Arduino 內建的 I2C 通訊工具

const uint8_t ADDR = 0x42;               // 掌控者接收器的「門牌號碼」(I2C 位址),PS2 與 PS4 都是 0x42
const int16_t MBC_NONE = -32768;         // 「沒有這個值/讀不到」的代號

// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);

unsigned long lastPrint = 0;
int16_t lastConnected = -1;              // 上一次的連線狀態(-1 = 還沒看過)

void setup() {
  Serial.begin(115200);
  mbcBegin();
  delay(500);
  if (!mbcWaitReady(ADDR, 5000)) {
    Serial.println(F("找不到接收器,請先跑範例 1 檢查接線。"));
    while (true) {}
  }
  Serial.println(F("準備好了!每 0.5 秒印一次手把狀態。"));
}

void loop() {
  if (millis() - lastPrint >= 500) {     // 👉 可以改:每 0.5 秒印一次
    lastPrint = millis();

    // 一次讀編號 06~0B 共 6 個值:[0] 已連線 [1] 電量 [2] L2 [3] R2 [4] 系統鍵 [5] 手把類型
    int16_t v[6];
    if (!mbcReadMany(ADDR, 0x06, 6, v)) { Serial.println(F("讀不到接收器")); return; }

    // 連線狀態有變化就提醒一行
    if (v[0] != lastConnected) {
      Serial.println(v[0] ? F(">>> 手把連上了!") : F(">>> 手把沒有連線"));
      lastConnected = v[0];
    }

    Serial.print(F("已連線 ")); Serial.print(v[0]);
    Serial.print(F(" | 電量 ")); Serial.print(v[1]); Serial.print(F("%"));
    Serial.print(F(" | L2 ")); Serial.print(v[2]);
    Serial.print(F(" R2 ")); Serial.print(v[3]);
    Serial.print(F(" | PS 鍵 ")); Serial.print((v[4] & 0x01) ? 1 : 0);     // 系統鍵 bit0
    Serial.print(F(" 擷取鍵 ")); Serial.print((v[4] & 0x02) ? 1 : 0);     // 系統鍵 bit1
    Serial.print(F(" | 手把:"));
    printPadType(v[5]);
    Serial.println();
  }
  delay(50);
}

// 0B 手把類型:0 沒有、1 PS2 數位、2 PS2 類比、3 PS4、4 PS5(或其他)
void printPadType(int16_t t) {
  switch (t) {
    case 0: Serial.print(F("沒有手把")); break;
    case 1: Serial.print(F("PS2 手把(數位模式,搖桿沒作用)")); break;
    case 2: Serial.print(F("PS2 手把(類比模式,ANALOG 燈亮)")); break;
    case 3: Serial.print(F("PS4 手把")); break;
    case 4: Serial.print(F("PS5 手把(或其他)")); break;
    default: Serial.print(F("不認識"));
  }
}

// ============================================================
// 以下是「通訊小幫手」(MBC I2C 協議 v2):負責跟感應器講話。
// 所有 MBC 範例都一樣,不用改,也不用看懂。
//   讀法:寫 [編號] → 等 2 ms → 讀 2×k 個 byte = 從該編號起連續 k 個 int16(小端)
//   沒有這個編號(空號)時感應器回 -32768(MBC_NONE)
// ============================================================

// 開始 I2C 通訊(setup 裡呼叫一次)
// UNO/Nano:SDA=A4、SCL=A5;UNO R4:SDA/SCL 腳;ESP32:預設 GPIO21(SDA)/22(SCL)
void mbcBegin() {
  Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
  Wire.setWireTimeout(25000, true);      // 線沒接好時,不會讓 Arduino 當機卡住
#endif
}

// 從位址 addr 的感應器,由編號 id 起連續讀 k 個值(1~16)放進 out。全部讀到回傳 true
bool mbcReadMany(uint8_t addr, uint8_t id, uint8_t k, int16_t *out) {
  if (k < 1) k = 1;
  if (k > 16) k = 16;                    // 一次最多 16 個值(32 byte,Wire 緩衝區上限)
  Wire.beginTransmission(addr);
  Wire.write(id);                        // 告訴感應器:我要從第 id 號開始讀
  if (Wire.endTransmission() != 0) return false;
  delay(2);                              // 等 2 ms 再讀(ESP32 型的產品一定要等)
  uint8_t n = (uint8_t)(2 * k);
  if (Wire.requestFrom(addr, n) != n) return false;
  for (uint8_t i = 0; i < k; i++) {
    uint16_t lo = Wire.read();           // 先來的是低位元組
    uint16_t hi = Wire.read();           // 再來高位元組(小端)
    out[i] = (int16_t)(uint16_t)(lo | (hi << 8));
  }
  return true;
}

// 讀一個值。讀不到(沒接好)回傳 MBC_NONE
int16_t mbcRead(uint8_t addr, uint8_t id) {
  int16_t v;
  return mbcReadMany(addr, id, 1, &v) ? v : MBC_NONE;
}

// 送命令:寫 [id][參數…]。p 指向參數、n = 參數幾個 byte(沒參數就不填)。成功回傳 true
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p, uint8_t n) {
  Wire.beginTransmission(addr);
  Wire.write(id);
  for (uint8_t i = 0; i < n; i++) Wire.write(p[i]);
  return Wire.endTransmission() == 0;
}

// 等感應器準備好(狀態旗標 F3 的 bit0 = 1),最多等 ms 毫秒。準備好了回傳 true
bool mbcWaitReady(uint8_t addr, uint16_t ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    int16_t f = mbcRead(addr, 0xF3);
    if (f != MBC_NONE && (f & 1)) return true;
    delay(50);
  }
  return false;
}

你會看到:(手把連上、按住 L2,然後按 ANALOG 鍵切到數位模式)

準備好了!每 0.5 秒印一次手把狀態。
>>> 手把連上了!
已連線 1 | 電量 0% | L2 255 R2 0 | PS 鍵 0 擷取鍵 0 | 手把:PS2 手把(類比模式,ANALOG 燈亮)
已連線 1 | 電量 0% | L2 0 R2 0 | PS 鍵 0 擷取鍵 0 | 手把:PS2 手把(數位模式,搖桿沒作用)

6️⃣ 坦克遙控車(進階)

做什麼: 用兩支搖桿開兩輪小車:左搖桿上下(LY)控制左輪、右搖桿上下(RY)控制右輪,搖桿的 -100~`+100` 直接乘上速度。手把斷線或讀不到接收器就立刻停車。

另外要準備: 兩輪小車、L298N 馬達驅動板(或同樣接法的 TB6612)、電池。

馬達接線:

L298N Arduino UNO / Nano / R4 ESP32
ENA、IN1、IN2(左輪) 5、7、8 25、26、27
ENB、IN3、IN4(右輪) 6、9、10 14、32、33
GND GND GND

警告

第一次跑之前,把車子架高(輪子懸空)再上傳:兩支搖桿一起往上推,確認兩個輪子都往前轉(轉反了就改 LEFT_REVERSE/RIGHT_REVERSE);然後把手把關機,確認輪子會停,再放到地上開。

下載 ps2_06_tank_car.ino

/*
 * 掌控者 PS2 × Arduino 範例 6:坦克遙控車(進階)
 * ------------------------------------------------------------
 * 這個範例會做什麼?
 *   用手把開兩輪小車,開法跟坦克一樣:
 *     左搖桿上下(LY)→ 左輪;右搖桿上下(RY)→ 右輪
 *     兩支一起往上推 = 直走;一前一後 = 原地轉彎。
 *   搖桿的 -100 ~ +100 直接當馬達速度用(再乘上最高速度)。
 *   手把斷線、或讀不到接收器,車子會立刻停下來。
 *
 * 你需要:掌控者 PS2 接收器(通用 I2C 版)+ PS2 手把、Arduino UNO/Nano/UNO R4 或 ESP32。另外要:兩輪小車、L298N 馬達驅動板(或同樣接法的 TB6612)、電池。
 *
 * 接線:
 *   接收器 VCC → 5V      接收器 GND → GND
 *   接收器 RX(= SDA) → A4      接收器 TX(= SCL) → A5         (UNO / Nano)
 *   UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
 *   (I2C 版接收器上印的 TX 就是 SCL、RX 就是 SDA)
 *   L298N ENA → 5     IN1 → 7     IN2 → 8      (左輪)
 *   L298N ENB → 6     IN3 → 9     IN4 → 10     (右輪)
 *   L298N 的 GND 要跟 Arduino 的 GND 接在一起!
 *   (ESP32 的接腳不一樣,請看下面「馬達接腳」)
 *
 * 準備:
 *   1. 把車子架高(輪子懸空)再上傳,兩支搖桿一起往上推,確認兩個輪子都「往前」轉。
 *      哪個輪子轉反了,就把 LEFT_REVERSE 或 RIGHT_REVERSE 改成 true。
 *   2. ⚠️ 車子還架高時,把手把關機,確認輪子會停下來,再放到地上開。
 *
 * 怎麼看結果:
 *   序列埠監控窗(115200)每 0.3 秒印一次左右輪速度。
 *
 * 小提醒:
 *   手把中間的 ANALOG 鍵要讓燈亮著(類比模式),搖桿才會有數字。
 */

#include <Wire.h>

const uint8_t ADDR = 0x42;               // 掌控者接收器的「門牌號碼」(I2C 位址),PS2 與 PS4 都是 0x42
const int16_t MBC_NONE = -32768;         // 「沒有這個值/讀不到」的代號

// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);

// ---- 馬達接腳 ----
#if defined(ESP32)
// ESP32 的第 6 ~ 11 腳接著板子上的記憶體晶片,不能拿來用,所以改用這幾腳
const int ENA = 25, IN1 = 26, IN2 = 27;  // 左輪
const int ENB = 14, IN3 = 32, IN4 = 33;  // 右輪
#else
const int ENA = 5, IN1 = 7, IN2 = 8;     // 左輪
const int ENB = 6, IN3 = 9, IN4 = 10;    // 右輪
#endif

// ---- 👉 可以改的參數 ----
const int  MAX_SPEED     = 200;          // 搖桿推到底的速度:0 ~ 255,新手先用慢一點
const bool LEFT_REVERSE  = false;        // 左輪轉反了改成 true
const bool RIGHT_REVERSE = false;        // 右輪轉反了改成 true

bool stopped = true;                     // 現在是不是停車狀態(一開始算停車)
unsigned long lastPrint = 0;

void setup() {
  Serial.begin(115200);
  pinMode(ENA, OUTPUT); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(ENB, OUTPUT); pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
  drive(0, 0);                           // 先停好

  mbcBegin();
  delay(500);
  if (!mbcWaitReady(ADDR, 5000)) {
    Serial.println(F("找不到接收器,請先跑範例 1 檢查接線。"));
    while (true) {}
  }
  Serial.println(F("準備好了!用兩支搖桿開車。"));
}

void loop() {
  // 一次讀編號 06 ~ 13 共 14 個值(同一瞬間的資料):
  //   v[0] = 06 已連線 … v[10] = 10 LX、v[11] = 11 LY、v[12] = 12 RX、v[13] = 13 RY
  //   (中間的 0C~0F 是空號,會讀到 -32768,不用理它)
  int16_t v[14];
  if (!mbcReadMany(ADDR, 0x06, 14, v) || v[0] == 0) {
    safeStop();                          // 讀不到接收器,或手把沒連線 → 立刻停車
    delay(50);
    return;
  }
  if (stopped) {
    stopped = false;
    Serial.println(F("手把連上了,可以開車!"));
  }

  int ly = v[11];                        // 左搖桿上下:上 = 正(-100 ~ +100)
  int ry = v[13];                        // 右搖桿上下:上 = 正

  // 搖桿推多少,輪子就轉多快:-100 ~ +100 換算成 -MAX_SPEED ~ +MAX_SPEED
  int left  = (long)ly * MAX_SPEED / 100;
  int right = (long)ry * MAX_SPEED / 100;
  drive(left, right);

  if (millis() - lastPrint >= 300) {     // 每 0.3 秒印一次速度
    lastPrint = millis();
    Serial.print(F("左輪 ")); Serial.print(left);
    Serial.print(F(" 右輪 ")); Serial.println(right);
  }
  delay(50);
}

// 停車,並且只在剛停的那一下說一次
void safeStop() {
  drive(0, 0);
  if (!stopped) {
    stopped = true;
    Serial.println(F("讀不到手把 → 停車!"));
  }
}

// 控制兩個輪子:速度 -255(全速後退)~ 255(全速前進)
void drive(int left, int right) {
  if (LEFT_REVERSE)  left  = -left;
  if (RIGHT_REVERSE) right = -right;
  setMotor(ENA, IN1, IN2, left);
  setMotor(ENB, IN3, IN4, right);
}

void setMotor(int en, int inA, int inB, int speed) {
  speed = constrain(speed, -255, 255);   // 不讓速度超過範圍
  digitalWrite(inA, speed >= 0 ? HIGH : LOW);
  digitalWrite(inB, speed >= 0 ? LOW : HIGH);
  analogWrite(en, abs(speed));
}

// ============================================================
// 以下是「通訊小幫手」(MBC I2C 協議 v2):負責跟感應器講話。
// 所有 MBC 範例都一樣,不用改,也不用看懂。
//   讀法:寫 [編號] → 等 2 ms → 讀 2×k 個 byte = 從該編號起連續 k 個 int16(小端)
//   沒有這個編號(空號)時感應器回 -32768(MBC_NONE)
// ============================================================

// 開始 I2C 通訊(setup 裡呼叫一次)
// UNO/Nano:SDA=A4、SCL=A5;UNO R4:SDA/SCL 腳;ESP32:預設 GPIO21(SDA)/22(SCL)
void mbcBegin() {
  Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
  Wire.setWireTimeout(25000, true);      // 線沒接好時,不會讓 Arduino 當機卡住
#endif
}

// 從位址 addr 的感應器,由編號 id 起連續讀 k 個值(1~16)放進 out。全部讀到回傳 true
bool mbcReadMany(uint8_t addr, uint8_t id, uint8_t k, int16_t *out) {
  if (k < 1) k = 1;
  if (k > 16) k = 16;                    // 一次最多 16 個值(32 byte,Wire 緩衝區上限)
  Wire.beginTransmission(addr);
  Wire.write(id);                        // 告訴感應器:我要從第 id 號開始讀
  if (Wire.endTransmission() != 0) return false;
  delay(2);                              // 等 2 ms 再讀(ESP32 型的產品一定要等)
  uint8_t n = (uint8_t)(2 * k);
  if (Wire.requestFrom(addr, n) != n) return false;
  for (uint8_t i = 0; i < k; i++) {
    uint16_t lo = Wire.read();           // 先來的是低位元組
    uint16_t hi = Wire.read();           // 再來高位元組(小端)
    out[i] = (int16_t)(uint16_t)(lo | (hi << 8));
  }
  return true;
}

// 讀一個值。讀不到(沒接好)回傳 MBC_NONE
int16_t mbcRead(uint8_t addr, uint8_t id) {
  int16_t v;
  return mbcReadMany(addr, id, 1, &v) ? v : MBC_NONE;
}

// 送命令:寫 [id][參數…]。p 指向參數、n = 參數幾個 byte(沒參數就不填)。成功回傳 true
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p, uint8_t n) {
  Wire.beginTransmission(addr);
  Wire.write(id);
  for (uint8_t i = 0; i < n; i++) Wire.write(p[i]);
  return Wire.endTransmission() == 0;
}

// 等感應器準備好(狀態旗標 F3 的 bit0 = 1),最多等 ms 毫秒。準備好了回傳 true
bool mbcWaitReady(uint8_t addr, uint16_t ms) {
  unsigned long start = millis();
  while (millis() - start < ms) {
    int16_t f = mbcRead(addr, 0xF3);
    if (f != MBC_NONE && (f & 1)) return true;
    delay(50);
  }
  return false;
}

你會看到:(兩支搖桿往上推一半,然後把手把關機)

準備好了!用兩支搖桿開車。
手把連上了,可以開車!
左輪 100 右輪 100
讀不到手把 → 停車!

提示

這支範例用一次 mbcReadMany(ADDR, 0x06, 14, v) 從 06 讀到 13(共 14 個),v[0] 是「已連線」、v[11] 是 LY、v[13] 是 RY,同一瞬間的資料,不用分兩次讀。中間的 0C~`0F是空號,會讀到-32768`,不用理它。


常見問題

Q:一直出現「找不到接收器」?

  1. 檢查 TX、RX 有沒有接反(UNO:RX → A4、TX → A5;R4:SDA/SCL;ESP32:21/22)。
  2. 檢查 VCC、GND 有沒有插緊。接收器上的燈有沒有亮?
  3. 確認是 通用 I2C 版:SPIKE 版、EV3 版的韌體不同,不能直接接 Arduino。
  4. 如果接收器是另外供電、比 Arduino 晚開機,按一下 Arduino 的 RESET 鍵重來。

Q:序列埠監控窗一堆亂碼? 速度沒選 115200,在監控窗右下角改好就正常了。

Q:按鍵讀得到,搖桿都是 0? 手把在數位模式。按一下手把中間的 ANALOG 鍵,燈亮了就有搖桿。編號 0B 會從 1 變 2。

Q:讀到 -32768 是什麼意思? 這是「沒有這個值」的代號(範例裡叫 MBC_NONE):編號是空號(例如 0C~`0F),或是讀不到接收器(線鬆了)。寫程式時先檢查 v != MBC_NONE` 再用。

Q:小幫手裡的 delay(2) 可以拿掉嗎? PS2 接收器本身不需要,但 PS4/PS5 接收器(ESP32 做的)和神攝手一定要;兩款接收器位址一樣,程式事先分不出來,所以 MBC 所有範例一律等 2 毫秒。建議留著。

Q:ESP32 當主機時要注意什麼? 接線用 GPIO 21(SDA)/22(SCL)。小車範例的馬達腳位 ESP32 跟 UNO 不同,程式裡已用 #if defined(ESP32) 分開。

Q:可以同時接 PS2 和 PS4/PS5 接收器嗎? 不行。兩款的門牌號碼都是 0x42,同一組 SDA/SCL 上只能接一個。但是兩款的編號完全一樣,這一頁的範例接 PS4/PS5 接收器也能用;震動、燈條請看 PS4/PS5 的範例 7、8。

Q:想知道每一個格子放什麼資料? 請看上面的 指令總覽 和 MBC 通用 I2C 協議 v2(給會寫程式的人看的進階資料)。

LINE 詢價選購指南