掌控者 PS2 (PS2) × Arduino × Arduino IDE
這一頁只講 Arduino IDE(C++):在 Arduino/ESP32/UNO R4(I2C) 上用它讀掌控者 PS2 (PS2)的安裝步驟、每個值怎麼讀、能用的命令與範例。要換產品、主機或軟體,用上方的切換列。
要買哪個版本、怎麼接
買 通用 I2C 版(4 pin:5V、GND、SDA、SCL),出廠 I2C 位址 0x42。UNO/Nano 接 A4(SDA)、A5(SCL);UNO R4、ESP32 接板子上標 SDA/SCL 的腳。
安裝與設定
你需要
- 感應器:掌控者 PS2 (PS2)(通用 I2C 版),附 4 條線的連接線。SPIKE 版、EV3 版的韌體不同,不能直接接 Arduino。
- 板子:Arduino UNO(或 Nano)最常見;Mega、UNO R4、ESP32 也可以。
- USB 線:UNO 用方頭(USB-B)線;Nano 依板子是 Mini USB 或 Type-C。要能傳資料的線,有些便宜的線只能充電。
- 電腦:Windows、Mac 都可以。
- 軟體:Arduino IDE(免費)。
安裝軟體
提示
第一次用 Arduino?先看 第一次用 Arduino:從零開始,15 分鐘學會安裝軟體、上傳程式、看結果。
- 到官網下載:arduino.cc/en/software,選你的電腦系統(Windows 選 Windows Win 10 and newer)。
- 下載頁會問要不要捐款,點 JUST DOWNLOAD 就能直接下載。
- 安裝時一路按「下一步」,中途問要不要安裝驅動程式,全部按「安裝」。
- 想改成中文介面:File → Preferences → Language 選「中文(繁體)」,按 OK 後重開。
- 用 ESP32 的話,要先安裝 ESP32 的板子套件。
讓軟體認得感應器
不用裝函式庫。 每支範例都是單一 .ino 檔,讀寫感應器的小幫手函式(例如 mbcReadMany)已經寫在檔案裡,複製貼上就能用。
- 用 USB 線把 Arduino 接上電腦,在 IDE 上方的下拉選單選你的板子(例如 Arduino Uno),右邊會顯示 COM 幾(Mac 是
/dev/cu.usb…)。 - 在範例頁按程式框右上角的「複製」,或下載
.ino檔。 - 檔案 → 新增,把原本的內容全部刪掉,貼上程式。
- 按左上角的 →(上傳),下方出現「上傳完成」就成功了。
感應器走 I2C,出廠位址是 0x42;協議細節看 MBC 通用 I2C 協議。
接線
MBC 通用 I2C 版只用 4 條線:兩條電源(VCC、GND)、兩條訊號(SDA、SCL)。
| 感應器 | UNO/Nano | Mega | UNO R4 | ESP32 |
|---|---|---|---|---|
| VCC | 5V | 5V | 5V | 5V(VIN) |
| GND | GND | GND | GND | GND |
| SDA | A4 | 20 | A4 | GPIO 21 |
| SCL | A5 | 21 | A5 | GPIO 22 |
重要
- 接線前先拔掉 USB 線,接好、檢查一次再插回去。
- 感應器線標 TX/RX 的話:TX 接 SCL、RX 接 SDA。
- 每個產品的接頭排列不太一樣,以感應器板子上印的字為準。
- UNO 靠近 USB 接頭那一側最上面兩個沒寫數字的腳也標著 SDA、SCL,跟 A4、A5 是同一組。
讓手把連上接收器
PS2 的 2.4G 手把和它的接收頭是原廠成對的,不用按任何配對鍵。
- 接收器接好、主機開機。
- 打開手把電源,手把會自己連上。
- 看接收器板子上的綠燈:亮著=還沒接到手把;熄掉=已經接到手把。
- 手把上的 ANALOG(或 MODE)燈要亮,搖桿才有數值。接收器一接上手把會自動切到這個模式;燈沒亮就按一下手把中間的 ANALOG 鍵。
危險
2.4GHz 是公共頻段,跟 Wi-Fi、微波爐共用。同一場地有 12 套以上同時使用時可能互相干擾,這種場合建議改用藍牙版(PS4/PS5)。
警告
只按 ■ 時,十六鍵(編號
01)=0x8000= -32768,跟讀不到時的MBC_NONE一模一樣。要分辨「讀不到」,用mbcReadMany回傳的 true/false,不要拿 -32768 判斷:int16_t b; if (mbcReadMany(0x42, 0x01, 1, &b)) { … },回 true 就是有讀到,再把(uint16_t)b當十六鍵用。
第一次測試
- 上傳本頁的範例 1。
- 按 IDE 右上角的 放大鏡(序列埠監控窗),速度選 115200 baud(所有 MBC 範例都用這個速度)。
- 看到範例印出的文字和數字一直更新,就是接好了。
常見問題
- 一直出現「找不到感應器」? 檢查 SDA、SCL 有沒有接反、VCC 和 GND 有沒有插緊;確認是通用 I2C 版。
- 讀到
-32768? 代表讀不到感應器(接線鬆了、位址錯了),或那個編號是空號。大部分的值不會是這個數字;用滿 16 位元的值例外(掌控者十六鍵只按 ■、循行者 16 路壓線位元圖只有第 16 路壓線、陀螺儀的原始值),真的會讀到 -32768,這種值要用mbcReadMany回傳的 true/false 判斷有沒有讀到。 - 序列埠監控窗一堆亂碼? 速度沒選 115200。
- 下拉選單找不到板子? USB 線只能充電 → 換一條;板子是 CH340 相容板 → 先安裝 CH340 驅動程式,再拔插 USB。
- ESP32 讀不到或值怪怪的? 有些 MBC 產品本身是 ESP32 做的,不能時脈延展,寫完編號要等 2 ms 再讀;範例小幫手裡的
delay(2)不要刪。
讀值
下面的寫法都用到 mbcRead() 小幫手:展開「小幫手程式」,第 ① 段(2 行)放在程式最上面、第 ② 段貼在程式最下面,位址 0x42。
小幫手程式(① 放最上面、② 貼最下面)
① 放在程式最上面(setup() 前面)
#include <Wire.h>
const int16_t MBC_NONE = -32768; // 讀不到/空號② 貼在程式最下面(loop() 後面)
// 從編號 id 起連續讀 k 個值(k ≤ 16),int16 小端
bool mbcReadMany(uint8_t addr, uint8_t id, uint8_t k, int16_t *out) {
Wire.beginTransmission(addr);
Wire.write(id);
if (Wire.endTransmission() != 0) return false;
delay(2); // 一律等 2 ms(ESP32 產品不能時脈延展)
if (Wire.requestFrom(addr, (uint8_t)(k * 2)) != (uint8_t)(k * 2)) return false;
for (uint8_t i = 0; i < k; i++) {
uint8_t lo = Wire.read(), hi = Wire.read();
out[i] = (int16_t)(lo | (hi << 8));
}
return true;
}
// 讀 1 個值;讀不到回 MBC_NONE
int16_t mbcRead(uint8_t addr, uint8_t id) {
int16_t v;
return mbcReadMany(addr, id, 1, &v) ? v : MBC_NONE;
}
// 寫入命令(0~3 個參數)
// 2 個參數、第 1 個是 0 時要寫 (uint8_t)0,例如 mbcWrite(ADDR, 0xF8, (uint8_t)0x00, 0xFF),不然會跟下面陣列版的 mbcWrite 分不清而編不過
void mbcWrite(uint8_t addr, uint8_t id, const uint8_t *args, uint8_t n) {
Wire.beginTransmission(addr);
Wire.write(id);
for (uint8_t i = 0; i < n; i++) Wire.write(args[i]);
Wire.endTransmission();
}
void mbcWrite(uint8_t addr, uint8_t id) { mbcWrite(addr, id, nullptr, 0); }
void mbcWrite(uint8_t addr, uint8_t id, uint8_t a) { mbcWrite(addr, id, &a, 1); }
void mbcWrite(uint8_t addr, uint8_t id, uint8_t a, uint8_t b) { uint8_t p[2] = {a, b}; mbcWrite(addr, id, p, 2); }
void mbcWrite(uint8_t addr, uint8_t id, uint8_t a, uint8_t b, uint8_t c) { uint8_t p[3] = {a, b, c}; mbcWrite(addr, id, p, 3); }
// 等模組就緒(F3 bit0 = 1),最多 ms 毫秒
bool mbcWaitReady(uint8_t addr, unsigned long ms) {
unsigned long t0 = millis();
while (millis() - t0 < ms) {
int16_t st = mbcRead(addr, 0xF3);
if (st != MBC_NONE && (st & 1)) return true;
delay(20);
}
return false;
}十六鍵 buttons16
| 範圍 | 意思 |
|---|---|
0~65535 bit |
16 個按鍵合成一個數(PSX 排列,1 = 按下):bit0 SELECT、1 L3、2 R3、3 START、4 上、5 右、6 下、7 左、8 L2、9 R2、10 L1、11 R1、12 ▲、13 ●、14 ✕、15 ■;未連線 = 0 |
int16_t buttons16 = mbcRead(0x42, 0x01); // 十六鍵:0~65535 bit這個值本身就可能是 −32768(最高位元 = 1,例如 0x8000),跟讀不到時回的 MBC_NONE 一樣,不要拿 −32768 判斷有沒有讀到:要分辨就改用 mbcReadMany(0x42, 0x01, 1, &buttons16),回傳 false 才是讀不到。
方向鍵 dpad
| 範圍 | 意思 |
|---|---|
0~15 bit |
上 1、右 2、下 4、左 8(同時按相加),0~15;未連線 = 0 |
int16_t dpad = mbcRead(0x42, 0x02); // 方向鍵:0~15 bit讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
形狀鍵 shape
| 範圍 | 意思 |
|---|---|
0~15 bit |
▲ 1、● 2、✕ 4、■ 8(同時按相加),0~15;未連線 = 0;0x22 的 R 高 4 bit |
int16_t shape = mbcRead(0x42, 0x03); // 形狀鍵:0~15 bit讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
肩鍵 shoulder
| 範圍 | 意思 |
|---|---|
0~15 bit |
L2 1、R2 2、L1 4、R1 8(同時按相加),0~15;未連線 = 0 |
int16_t shoulder = mbcRead(0x42, 0x04); // 肩鍵:0~15 bit讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
功能鍵 func
| 範圍 | 意思 |
|---|---|
0~15 bit |
SELECT 1、L3 2、R3 4、START 8(同時按相加),0~15;未連線 = 0;0x22 的 G 高 4 bit |
int16_t func = mbcRead(0x42, 0x05); // 功能鍵:0~15 bit讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
已連線 connected
| 範圍 | 意思 |
|---|---|
0~1 |
1 = 手把已連線(psx.read() 成功),0 = 沒有;未連線時所有按鍵 0、搖桿回中 |
int16_t connected = mbcRead(0x42, 0x06); // 已連線:0~1讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
電量 battery
| 範圍 | 意思 |
|---|---|
0 % |
0~100;PS2 手把沒有電量 → 恆 0(PS4 才有) |
int16_t battery = mbcRead(0x42, 0x07); // 電量:0 %讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
L2 類比 l2Analog
| 範圍 | 意思 |
|---|---|
0~255 |
L2 壓力 0~255(DualShock 2 類比模式才有壓力值;手把沒有壓力值時按下 = 255、放開 = 0);未連線 = 0 |
int16_t l2Analog = mbcRead(0x42, 0x08); // L2 類比:0~255讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
R2 類比 r2Analog
| 範圍 | 意思 |
|---|---|
0~255 |
R2 壓力 0~255(同 08);未連線 = 0 |
int16_t r2Analog = mbcRead(0x42, 0x09); // R2 類比:0~255讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
系統鍵 sysKeys
| 範圍 | 意思 |
|---|---|
0 bit |
b0 PS 鍵、b1 擷取鍵;PS2 手把沒有 → 恆 0(PS4 才有) |
int16_t sysKeys = mbcRead(0x42, 0x0A); // 系統鍵:0 bit讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
手把類型 padType
| 範圍 | 意思 |
|---|---|
0~2 |
0 沒有手把、1 PS2 數位模式、2 PS2 類比模式(3 PS4、4 PS5 只有 PS4 接收器會出現) |
int16_t padType = mbcRead(0x42, 0x0B); // 手把類型:0~2讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
搖桿 LX/LY/RX/RY stick
| 範圍 | 意思 |
|---|---|
-100~+100 |
10 LX、11 LY、12 RX、13 RY:−100 ~ +100,0 = 中間,右/上為正(原始 0 |
int16_t stick[4];
mbcReadMany(0x42, 0x10, 4, stick); // 搖桿 LX/LY/RX/RY:-100~+100
for (uint8_t i = 0; i < 4; i++) { Serial.print(stick[i]); Serial.print(' '); }
Serial.println();編號 0x10~0x13 連號,一次 mbcReadMany 讀完保證同一瞬間。
命令
震動 vibrate
[強度 0~255],0 = 停;PS2 接收器收到後回 ACK 但忽略(250614 已關手把震動),PS4 才有作用
mbcWrite(0x42, 0xE5, 128); // 震動(強度 0~255)燈條顏色 setLed
[R][G][B] 各 0~255;PS2 手把沒有燈條,接收器收到後回 ACK 但忽略,PS4 才有作用
mbcWrite(0x42, 0xE6, 128, 128, 128); // 燈條顏色(紅 0~255、綠 0~255、藍 0~255)範例
這一頁有 6 個範例,從「確認接好了沒」一路做到「坦克遙控車」。每個範例都可以直接複製、上傳,程式裡每一行都有中文說明。完全沒寫過程式也沒關係,照順序一個一個做就好。
提示
第一次用 Arduino?先看 🔰 第一次用 Arduino:從零開始,15 分鐘學會安裝軟體、上傳程式、看結果。
接線(所有範例都一樣)
| 接收器 | 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 |
重要
- 請用 通用 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 | 把所有的值一次印出來 | 看懂每個編號放什麼(01 |
無 |
| 3 | 按了哪一顆鍵? | 十六鍵拆 bit、四組分組鍵 | 無 |
| 4 | 搖桿推多少? | 搖桿 ±100,畫成十字圖 | 無 |
| 5 | 手把的狀態 | 連線、扳機類比、手把類型 | 無 |
| 6 | 坦克遙控車(進階) | 兩支搖桿開車、斷線自動停車 | 兩輪小車、L298N 馬達驅動板 |
提示
建議順序:1 → 2 → 3 → 4 → 5 → 6。 只有 6 要另外準備零件。
1️⃣ 跟接收器打招呼
做什麼: 確認接收器接好了,印出它的「身分證」(出廠位址、款式、協議版本、韌體版本、狀態旗標),然後一直看著手把:連上、斷線都會說。每次接新的接收器,都先跑這一個。
步驟:
- 照上面的表格接好 4 條線,USB 插上電腦。
- 複製下面的程式,貼到 Arduino IDE,按 → 上傳。
- 打開序列埠監控窗(右上角放大鏡),速度選 115200。
- 把手把開機(接收器的燈亮),看電腦有沒有說「手把連上了」。
/*
* 掌控者 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 × 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 × 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 功能鍵 016 顆按鍵各在第幾位?
| 位 | 按鍵 | 位 | 按鍵 |
|---|---|---|---|
| 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 × 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 × 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 × 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:一直出現「找不到接收器」?
- 檢查 TX、RX 有沒有接反(UNO:RX → A4、TX → A5;R4:SDA/SCL;ESP32:21/22)。
- 檢查 VCC、GND 有沒有插緊。接收器上的燈有沒有亮?
- 確認是 通用 I2C 版:SPIKE 版、EV3 版的韌體不同,不能直接接 Arduino。
- 如果接收器是另外供電、比 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(給會寫程式的人看的進階資料)。
其他問題
- 一直讀到 -32768? 代表模組沒回應:檢查 SDA/SCL 有沒有接反、位址對不對、5V/GND 有沒有接好。
- 可以接很多顆嗎? 可以,每顆位址要不同;同型號用命令 F8 換位址(見通用 I2C 協議)。
還有問題?到 聯絡我們 附上你用的軟體和程式截圖。
下載
本頁依據
- 主機與軟體清單:
_build/guides.json - 值與命令:
_build/protocol/ps2.json(單一真相來源) - 安裝與設定:
_build/guides/software/arduino-ide.zh.md、_build/guides/product/ps2/arduino-ide.zh.md - 範例:
sensors/ps2/arduino-examples.md