測距者 2 路 (TOF2) × Arduino × Arduino IDE
這一頁只講 Arduino IDE(C++):在 Arduino/ESP32/UNO R4(I2C) 上用它讀測距者 2 路 (TOF2)的安裝步驟、每個值怎麼讀、能用的命令與範例。要換產品、主機或軟體,用上方的切換列。
要買哪個版本、怎麼接
買 通用 I2C 版(4 pin:5V、GND、SDA、SCL),出廠 I2C 位址 0x29。UNO/Nano 接 A4(SDA)、A5(SCL);UNO R4、ESP32 接板子上標 SDA/SCL 的腳。
安裝與設定
你需要
- 感應器:測距者 2 路 (TOF2)(通用 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,出廠位址是 0x29;協議細節看 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 是同一組。
左右怎麼分
左右以感應器朝前、從背後看為準(左=J5、右=J3)。 面對感應器看會剛好相反,所以用手測試時,請站到感應器後面、跟它同方向看。範例裡的「左邊比較近」「往左轉」都照這個方向。
第一次測試
- 上傳本頁的範例 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 行)放在程式最上面、第 ② 段貼在程式最下面,位址 0x29。
小幫手程式(① 放最上面、② 貼最下面)
① 放在程式最上面(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;
}左距離 cm leftCm
| 範圍 | 意思 |
|---|---|
0~120 cm |
左邊那顆的距離 0~120 cm(站在車後往前看,左 = 第 0 顆),120 = 前面沒東西/那顆失效 |
int16_t leftCm = mbcRead(0x29, 0x01); // 左距離 cm:0~120 cm讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
右距離 cm rightCm
| 範圍 | 意思 |
|---|---|
0~120 cm |
右邊那顆的距離 0~120 cm(右 = 第 1 顆),120 = 前面沒東西/那顆失效 |
int16_t rightCm = mbcRead(0x29, 0x02); // 右距離 cm:0~120 cm讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
較近距離 cm nearCm
| 範圍 | 意思 |
|---|---|
0~120 cm |
左右兩顆取較近的 0~120 cm |
int16_t nearCm = mbcRead(0x29, 0x03); // 較近距離 cm:0~120 cm讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
左距離 mm leftMm
| 範圍 | 意思 |
|---|---|
0~1200 mm |
左邊那顆的距離 0~1200 mm,1200 = 前面沒東西/那顆失效 |
int16_t leftMm = mbcRead(0x29, 0x04); // 左距離 mm:0~1200 mm讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
右距離 mm rightMm
| 範圍 | 意思 |
|---|---|
0~1200 mm |
右邊那顆的距離 0~1200 mm,1200 = 前面沒東西/那顆失效 |
int16_t rightMm = mbcRead(0x29, 0x05); // 右距離 mm:0~1200 mm讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
初始化位元圖 initOk
| 範圍 | 意思 |
|---|---|
0~3 bit |
bit0 = 左、bit1 = 右,1 = 那顆開機初始化成功(失敗不重試);3 = 兩顆都好 |
int16_t initOk = mbcRead(0x29, 0x06); // 初始化位元圖:0~3 bit讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
故障位元圖 fault
| 範圍 | 意思 |
|---|---|
0~3 bit |
bit0 = 左、bit1 = 右,1 = 那顆目前失效(I2C 讀失敗或 200 ms 沒更新);失效那顆距離回 120/1200 |
int16_t fault = mbcRead(0x29, 0x07); // 故障位元圖:0~3 bit讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
左右平均 mm avgMm
| 範圍 | 意思 |
|---|---|
0~1200 mm |
(左 mm + 右 mm) / 2,0~1200;跟 EV3 模式 3、SPIKE 模式 6 第 3 個值一致(共識 #8 補上) |
int16_t avgMm = mbcRead(0x29, 0x08); // 左右平均 mm:0~1200 mm讀不到(沒接、位址錯)時回 MBC_NONE(−32768)。
範例
這一頁有 6 個產品範例+3 個共用範例,從「確認接好了沒」一路做到「用 mm 精準停車」。每個範例都可以直接複製、上傳,程式裡每一行都有中文說明。完全沒寫過程式也沒關係,照順序一個一個做就好。
提示
第一次用 Arduino?先看 🔰 第一次用 Arduino:從零開始,15 分鐘學會安裝軟體、上傳程式、看結果。
接線(所有範例都一樣)
| 感應器 | Arduino UNO / Nano | ESP32 | Arduino UNO R4 |
|---|---|---|---|
| VCC | 5V | 5V(VIN) | 5V |
| GND | GND | GND | GND |
| SDA | A4 | GPIO 21 | SDA |
| SCL | A5 | GPIO 22 | SCL |
圖:/images/figures/tof2/wiring-uno.svg
重要
- 請用 通用 I2C 版 的測距者 2 路。SPIKE 版、EV3 版的韌體不同,不能直接接 Arduino。
- 兩顆雷射開機初始化要 1~3 秒,每個範例一開始都會先等
F3就緒。- 「左、右」是感應器裝在車頭(雷射朝前)、你站在車子後面往前看的左右(左=J5、右=J3);面對感應器看會剛好相反。
- 接線前先拔掉 USB 線。
指令總覽表(協議 v2)
感應器像一排有編號的「格子」,每一格放一個 16 位元的數字(int16,小端)。讀法:寫 [編號] → 等 2 ms → 讀 2×k 個 byte,就能從那個編號起一次拿到連續 k 格(k ≤ 16)。沒有的編號會回 -32768。門牌號碼(I2C 位址)是 0x29。
| 編號 | 名稱 | 範圍 / 單位 | 說明 | 出現在 |
|---|---|---|---|---|
01 |
左距離 | 0~120 cm | 左邊那顆的距離;120 = 前面沒東西/那顆失效 | 範例 2、3 |
02 |
右距離 | 0~120 cm | 右邊那顆的距離;120 = 沒東西/失效 | 範例 2、3 |
03 |
較近距離 | 0~120 cm | 左右兩顆取較近的 | 範例 3、5 |
04 |
左距離 | 0~1200 mm | 左邊那顆,精細到 1 mm;1200 = 沒東西/失效 | 範例 2、4 |
05 |
右距離 | 0~1200 mm | 右邊那顆,精細到 1 mm | 範例 2、4 |
06 |
初始化位元圖 | 0~3(bit) | bit0 = 左、bit1 = 右,1 = 那顆開機初始化成功;3 = 兩顆都好 | 範例 2、6 |
07 |
故障位元圖 | 0~3(bit) | bit0 = 左、bit1 = 右,1 = 那顆目前失效(讀失敗或 200 ms 沒更新) | 範例 2、6 |
08 |
左右平均 | 0~1200 mm | (左 mm + 右 mm) / 2;跟 EV3 模式 3、SPIKE 模式 6 一致 | 範例 2、4 |
F0 |
裝置 ID | — | 低 byte = 出廠位址 0x29、高 byte = 2(雷射顆數) | 範例 1、共用 2、共用 3 |
F1 |
協議版本 | 2 | 這份範例照版本 2 寫 | 範例 1 |
F2 |
韌體版本 | 主×100+次 | 例 102 = 1.2 | 範例 1 |
F3 |
狀態旗標 | bit | b0 就緒(兩顆都初始化完)、b1 任一顆失效 | 範例 1、6 |
F4 |
更新序號 | 0~255 | 每 10 ms +1,一直跳 = 感應器活著 | 共用 2 |
F5 |
開機秒數 | 0~32767 秒 | 變小 = 重開機過 | 範例 1、共用 2 |
F8 |
換 I2C 位址(命令) | [新位址][取反] |
[F8][00][FF] = 回出廠;重開機後生效 |
共用 1 |
EF |
重新開機(命令) | [A5] |
送 [EF][A5] |
共用 1、共用 2 |
測距者 2 路沒有產品專用的寫入命令(E0~EE)。
範例總覽
| # | 範例 | 學會什麼 | 另外要準備 |
|---|---|---|---|
| 1 | 跟感應器打招呼 | 確認接線正確,讀 F0~F3 身分證與狀態 | 無 |
| 2 | 一次看完所有讀值 | 整張值表 01~08 每一格是什麼 | 你的手 |
| 3 | 左邊近還是右邊近? | 01/02/03 三格,畫長條 | 一個盒子 |
| 4 | 用 mm 精準停車(進階) | 04/05/08 mm,到 200 mm 剛好停 | 兩輪小車、L298N 馬達驅動板 |
| 5 | 倒車雷達警報 | 用 03 較近距離控制燈與蜂鳴器 | 蜂鳴器(可不接) |
| 6 | 雷射健康檢查 | 06/07 初始化與故障位元圖、F3 | 無 |
| 共 1 | 換 I2C 位址 | F8 換位址/回出廠、EF 重開機 | 無 |
| 共 2 | 身分證+重新開機 | F0~F5 六格共用暫存器 | 無 |
| 共 3 | 掃描線上裝置 | 掃 0x08~0x77、用 F0 認產品 | 無 |
提示
建議順序:1 → 2 → 3 → 6 → 5 → 4。 先確認接好(1),看懂每一格(2),再做應用。
1️⃣ 跟感應器打招呼
做什麼: 確認感應器接好了,並讀出它的身分證:F0 裝置 ID(出廠位址+雷射顆數)、F1 協議版本、F2 韌體版本、F3 狀態旗標逐 bit 用中文印出來。每次接新的感應器,都先跑這一個。
步驟:
- 照上面的表格接好 4 條線,USB 插上電腦。
- 複製下面的程式,貼到 Arduino IDE,按 → 上傳。
- 打開序列埠監控窗(右上角放大鏡),速度選 115200。
/*
* 測距者 2 路 × Arduino 範例 1:跟感應器打招呼
* ------------------------------------------------------------
* 這個範例會做什麼?
* 確認感應器有沒有接好。接好了,會印出它的身分證(裝置 ID、協議版本、
* 韌體版本)和目前狀態(兩顆雷射是不是都正常)。
* 每次接新感應器,都建議先跑這個範例。
*
* 你需要:測距者 2 路(通用 I2C 版)、Arduino UNO/Nano/UNO R4 或 ESP32。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
*
* 怎麼看結果:
* 上傳後,打開「序列埠監控窗」,速度選 115200。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具,用來跟感應器講話
const uint8_t ADDR = 0x29; // 測距者的「門牌號碼」(I2C 位址),2 路與 8 路都是 0x29
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
void setup() {
Serial.begin(115200); // 打開跟電腦講話的通道,速度 115200
mbcBegin(); // 準備好 I2C 通訊(小幫手在檔案最下面)
delay(500); // 等感應器開機 0.5 秒
Serial.println(F("=== 測距者 2 路:跟感應器打招呼 ==="));
Serial.println(F("等兩顆雷射開機(最多 4 秒)..."));
// 等感應器說「我準備好了」(狀態旗標 F3 的 bit0 = 兩顆都初始化完),最多等 4 秒
if (!mbcWaitReady(ADDR, 4000)) {
Serial.println(F("找不到感應器!請檢查:"));
Serial.println(F(" 1. SDA 接 A4、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 = 雷射顆數 ----
int16_t id = mbcRead(ADDR, 0xF0);
uint8_t factoryAddr = (uint8_t)(id & 0xFF); // 出廠位址(換過位址也不變)
uint8_t sensors = (uint8_t)((id >> 8) & 0xFF); // hw[0] = 幾顆雷射
Serial.print(F("找到了!出廠位址 0x"));
Serial.print(factoryAddr, HEX);
Serial.print(F(",雷射數量 "));
Serial.print(sensors); // 測距者 2 路 = 2
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("是!(看範例 6 查哪一顆)") : F("否(兩顆都正常)"));
// ---- F5 開機秒數 ----
Serial.print(F("開機秒數:"));
Serial.println(mbcRead(ADDR, 0xF5));
Serial.println(F("接線正確,可以開始玩其他範例了!"));
}
void loop() {
// 這個範例只要在開機時檢查一次,所以 loop 裡什麼都不做
}
// ============================================================
// 以下是「通訊小幫手」(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;
}你會看到:
=== 測距者 2 路:跟感應器打招呼 ===
等兩顆雷射開機(最多 4 秒)...
找到了!出廠位址 0x29,雷射數量 2 顆
協議版本:2
韌體版本:2.0
目前狀態:
bit0 就緒 是(兩顆都初始化完)
bit1 任一顆失效 否(兩顆都正常)
開機秒數:5
接線正確,可以開始玩其他範例了!注意
「通訊小幫手」是什麼? 每個範例最下面都有一段一模一樣的程式,負責跟感應器「講話」。你不用改它,也不用看懂。只要記得:
mbcRead(位址, 編號):讀一格,讀不到回MBC_NONE(−32768)mbcReadMany(位址, 起始編號, 幾格, 陣列):從起始編號一次讀連續幾格(最多 16),全部來自同一瞬間mbcWrite(位址, 命令, 參數, 參數幾個):送命令,沒參數時後兩個可以不寫mbcWaitReady(位址, 最多等幾毫秒):等F3bit0 就緒
2️⃣ 一次看完所有讀值
做什麼: 每 0.5 秒用一次 mbcReadMany 把 01~08 整張值表印出來。先在這裡認識每一格,之後的範例只是各挑幾格來用。
/*
* 測距者 2 路 × Arduino 範例 2:一次看完所有讀值
* ------------------------------------------------------------
* 這個範例會做什麼?
* 把感應器「整張值表」每 0.5 秒印一次:
* 01 左 cm、02 右 cm、03 較近 cm、04 左 mm、05 右 mm、
* 06 初始化位元圖、07 故障位元圖、08 左右平均 mm。
* 之後的範例只是各挑其中幾格來用,先在這裡把每一格認識一遍。
*
* 你需要:測距者 2 路(通用 I2C 版)+ 你的手或一個盒子。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
*
* 怎麼看結果:
* 序列埠監控窗 115200。拿手在左邊、右邊的雷射前面移動。
*
* 小提醒:
* 01~08 編號連續,一次讀 8 個值(mbcReadMany),保證是同一瞬間的值。
* 120 cm/1200 mm = 前面沒東西(或那顆失效)。
*/
#include <Wire.h>
const uint8_t ADDR = 0x29; // 測距者的門牌號碼(I2C 位址)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
void setup() {
Serial.begin(115200);
mbcBegin();
delay(500);
if (!mbcWaitReady(ADDR, 4000)) {
Serial.println(F("找不到感應器,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("準備好了!拿手在左右兩顆雷射前面移動看看。"));
}
void loop() {
int16_t a[8]; // 放 01~08 這 8 格
if (!mbcReadMany(ADDR, 0x01, 8, a)) { Serial.println(F("讀不到")); delay(500); return; }
Serial.println(F("---------------------------------------"));
Serial.print(F("01 左距離 ")); Serial.print(a[0]); Serial.println(F(" cm (0~120,120 = 沒東西)"));
Serial.print(F("02 右距離 ")); Serial.print(a[1]); Serial.println(F(" cm"));
Serial.print(F("03 較近距離 ")); Serial.print(a[2]); Serial.println(F(" cm (左右取比較近的那個)"));
Serial.print(F("04 左距離 ")); Serial.print(a[3]); Serial.println(F(" mm (0~1200,比 cm 精細 10 倍)"));
Serial.print(F("05 右距離 ")); Serial.print(a[4]); Serial.println(F(" mm"));
Serial.print(F("06 初始化位元圖 ")); Serial.print(a[5]); Serial.println(F(" (bit0 左、bit1 右;3 = 兩顆都開機成功)"));
Serial.print(F("07 故障位元圖 ")); Serial.print(a[6]); Serial.println(F(" (bit0 左、bit1 右;0 = 都正常)"));
Serial.print(F("08 左右平均 ")); Serial.print(a[7]); Serial.println(F(" mm ((左 mm + 右 mm) / 2)"));
delay(50); // 讀完值歇一下
delay(450); // 每 0.5 秒更新一次,比較好看
}
// ============================================================
// 以下是「通訊小幫手」(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;
}你會看到:(手放在左邊 30 公分,右邊沒東西)
---------------------------------------
01 左距離 30 cm (0~120,120 = 沒東西)
02 右距離 120 cm
03 較近距離 30 cm (左右取比較近的那個)
04 左距離 304 mm (0~1200,比 cm 精細 10 倍)
05 右距離 1200 mm
06 初始化位元圖 3 (bit0 左、bit1 右;3 = 兩顆都開機成功)
07 故障位元圖 0 (bit0 左、bit1 右;0 = 都正常)
08 左右平均 752 mm ((左 mm + 右 mm) / 2)提示
試試看: 用一張紙斜著放在兩顆雷射前面,看
04和05差多少——這就是測「牆壁斜不斜」的方法。
3️⃣ 左邊近還是右邊近?
做什麼: 讀 01 左 cm、02 右 cm、03 較近 cm 三格,把兩顆的距離畫成兩條長條,並說出東西在哪一邊比較近。左右以感應器朝前、從背後看為準(左=J5、右=J3)。
準備: 一個盒子或你的手。
/*
* 測距者 2 路 × Arduino 範例 3:左邊近還是右邊近?
* ------------------------------------------------------------
* 這個範例會做什麼?
* 讀「01 左 cm」「02 右 cm」「03 較近 cm」三格,把兩顆雷射的距離畫成兩條長條,
* 並告訴你東西在哪一邊比較近。這是避障、找對手最常用的三格。
*
* 你需要:測距者 2 路(通用 I2C 版)+ 一個盒子或你的手。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
*
* 怎麼看結果:
* 序列埠監控窗 115200。拿手從左邊移到右邊,看長條怎麼變。
*
* 左、右怎麼算?
* 感應器裝在車頭(雷射朝前),你站在車子後面往前看的左、右。
*/
#include <Wire.h>
const uint8_t ADDR = 0x29; // 測距者的門牌號碼(I2C 位址)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
void setup() {
Serial.begin(115200);
mbcBegin();
delay(500);
if (!mbcWaitReady(ADDR, 4000)) {
Serial.println(F("找不到感應器,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("準備好了!拿手從左邊移到右邊看看。(長條越長 = 越遠)"));
}
void loop() {
int16_t v[3]; // v[0] = 01 左 cm、v[1] = 02 右 cm、v[2] = 03 較近 cm
if (!mbcReadMany(ADDR, 0x01, 3, v)) { Serial.println(F("讀不到感應器")); delay(500); return; }
int16_t left = v[0], right = v[1], nearest = v[2];
Serial.print(F("左 "));
printBar(left);
Serial.print(F(" 右 "));
printBar(right);
if (nearest >= 120) Serial.println(F(" → 120 公分內沒有東西"));
else if (left < right) { Serial.print(F(" → 左邊比較近:")); Serial.print(nearest); Serial.println(F(" 公分")); }
else if (right < left) { Serial.print(F(" → 右邊比較近:")); Serial.print(nearest); Serial.println(F(" 公分")); }
else { Serial.print(F(" → 正前方:")); Serial.print(nearest); Serial.println(F(" 公分")); }
delay(50);
delay(150); // 每 0.2 秒印一行 👉 可以改
}
// 把 0~120 公分畫成 0~12 格的長條
void printBar(int16_t cm) {
int8_t n = cm / 10;
if (n > 12) n = 12;
Serial.print('[');
for (int8_t i = 0; i < 12; i++) Serial.print(i < n ? '#' : '.');
Serial.print(']');
if (cm < 10) Serial.print(' ');
if (cm < 100) Serial.print(' ');
Serial.print(cm);
}
// ============================================================
// 以下是「通訊小幫手」(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;
}你會看到:(手從左邊移到右邊)
左 [###.........] 35 右 [############]120 → 左邊比較近:35 公分
左 [###.........] 32 右 [###.........] 32 → 正前方:32 公分
左 [############]120 右 [####........] 41 → 右邊比較近:41 公分
左 [############]120 右 [############]120 → 120 公分內沒有東西4️⃣ 用 mm 精準停車(進階)
做什麼: 用 04 左 mm、05 右 mm、08 平均 mm(精細到 1 mm),讓小車往牆開,到 STOP_MM(預設 200 mm)剛好停下;接近時自動減速。沒有小車也可以跑,只看序列埠數字。
另外要準備: 兩輪小車、L298N 馬達驅動板(或同樣接法的 TB6612)、電池、一面牆。
馬達接線:
| L298N | Arduino UNO / Nano / R4 | ESP32 | 控制 |
|---|---|---|---|
| ENA | 5 | GPIO 25 | 左輪速度 |
| IN1 / IN2 | 7 / 8 | GPIO 26 / 27 | 左輪方向 |
| ENB | 6 | GPIO 14 | 右輪速度 |
| IN3 / IN4 | 9 / 10 | GPIO 32 / 33 | 右輪方向 |
| GND | GND | GND | 一定要跟 Arduino 共地 |
/*
* 測距者 2 路 × Arduino 範例 4(進階):用 mm 精準停車
* ------------------------------------------------------------
* 這個範例會做什麼?
* 用「04 左 mm」「05 右 mm」「08 左右平均 mm」三格(精細到 1 mm),
* 讓小車往前開,到距離牆壁 STOP_MM(預設 200 mm = 20 公分)時剛好停下。
* 接近時會自動減速,所以不會衝過頭。沒有小車也可以跑:只看序列埠的數字。
*
* 你需要:兩輪小車、L298N 馬達驅動板(或同樣接法的 TB6612 等)、電池、一面牆或盒子。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
* L298N ENA → 5 IN1 → 7 IN2 → 8 (左輪)
* L298N ENB → 6 IN3 → 9 IN4 → 10 (右輪)
* (ESP32 改接:ENA → 25、IN1 → 26、IN2 → 27、ENB → 14、IN3 → 32、IN4 → 33)
* L298N 的 GND 要跟 Arduino 的 GND 接在一起!感應器裝在車頭,雷射朝前。
*
* 怎麼看結果:
* 序列埠監控窗 115200。車子停下時印出的距離應該很接近 STOP_MM。
*/
#include <Wire.h>
const uint8_t ADDR = 0x29; // 測距者的門牌號碼(I2C 位址)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
// ---- 馬達接腳 ----
#if defined(ESP32)
const int ENA = 25, IN1 = 26, IN2 = 27; // 左輪(ESP32 的 6~11 腳不能用)
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 int16_t STOP_MM = 200; // 停在離牆幾 mm(200 = 20 公分)
const int16_t SLOW_MM = 500; // 離牆幾 mm 以內開始減速
const int FAST_SPEED = 150; // 全速 0~255
const int SLOW_SPEED = 70; // 最慢的爬行速度(太慢馬達會不動)
const bool LEFT_REVERSE = false; // 左輪轉反了改成 true
const bool RIGHT_REVERSE = false; // 右輪轉反了改成 true
bool stopped = false;
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, 4000)) {
Serial.println(F("找不到感應器,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.print(F("3 秒後出發,目標:停在 ")); Serial.print(STOP_MM); Serial.println(F(" mm"));
delay(3000);
}
void loop() {
int16_t v[5]; // 04 左 mm、05 右 mm、06、07、08 平均 mm
if (!mbcReadMany(ADDR, 0x04, 5, v)) { // 從 04 號起一次讀 5 格(04~08)
drive(0, 0); // 讀不到 → 安全停車
delay(50);
return;
}
int16_t leftMm = v[0], rightMm = v[1], avgMm = v[4];
int16_t dist = (leftMm < rightMm) ? leftMm : rightMm; // 用比較近的那顆,避免斜著撞到
Serial.print(F("左 ")); Serial.print(leftMm);
Serial.print(F(" mm 右 ")); Serial.print(rightMm);
Serial.print(F(" mm 平均 ")); Serial.print(avgMm);
Serial.print(F(" mm "));
if (stopped || dist <= STOP_MM) {
drive(0, 0);
if (!stopped) { stopped = true; Serial.println(F("→ 到了,停車!")); }
else Serial.println(F("(已停)"));
} else if (dist <= SLOW_MM) {
// 越近越慢:距離從 SLOW_MM 到 STOP_MM,速度從 FAST 降到 SLOW
int speed = map(dist, STOP_MM, SLOW_MM, SLOW_SPEED, FAST_SPEED);
drive(speed, speed);
Serial.print(F("→ 減速 ")); Serial.println(speed);
} else {
drive(FAST_SPEED, FAST_SPEED);
Serial.println(F("→ 全速前進"));
}
delay(50);
}
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;
}你會看到:
3 秒後出發,目標:停在 200 mm
左 812 mm 右 805 mm 平均 808 mm → 全速前進
左 486 mm 右 480 mm 平均 483 mm → 減速 139
左 231 mm 右 226 mm 平均 228 mm → 減速 77
左 199 mm 右 196 mm 平均 197 mm → 到了,停車!提示
停得太遠就把
SLOW_SPEED調大、太近(撞到)就把SLOW_MM調大。程式用「比較近的那顆」判斷,車子斜著靠近牆也不會撞到。
5️⃣ 倒車雷達警報
做什麼: 讀 03 較近距離,用板子內建的小燈(L)和蜂鳴器當倒車雷達:沒有東西安靜,有東西就閃/嗶,越近越急,貼到 10 公分內就一直亮。
準備: 有源蜂鳴器(可不接):+ → 8(ESP32 → 27)、− → GND。
/*
* 測距者 2 路 × Arduino 範例 5:倒車雷達警報
* ------------------------------------------------------------
* 這個範例會做什麼?
* 用板子內建的小燈(L)和一顆蜂鳴器(可不接)當倒車雷達:
* 讀「03 較近距離 cm」,沒有東西就安靜,有東西就閃/嗶,越近越急,
* 貼到 STOP_CM 以內就一直亮、一直響。
*
* 你需要:測距者 2 路(通用 I2C 版);蜂鳴器可有可無(有的話接「有源蜂鳴器」)。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
* 蜂鳴器 + → 8(ESP32 → 27) 蜂鳴器 − → GND
*
* 怎麼看結果:
* 手慢慢靠近感應器,L 燈(和蜂鳴器)越閃越快;序列埠 115200 也會印距離。
*/
#include <Wire.h>
#ifndef LED_BUILTIN
#define LED_BUILTIN 2 // ESP32 開發板的內建燈通常接在 GPIO 2
#endif
const uint8_t ADDR = 0x29; // 測距者的門牌號碼(I2C 位址)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
#if defined(ESP32)
const int BUZZER = 27; // 蜂鳴器腳位(ESP32)
#else
const int BUZZER = 8; // 蜂鳴器腳位(UNO)
#endif
// ---- 👉 可以改的參數 ----
const int16_t ALARM_START_CM = 80; // 幾公分以內開始警報
const int16_t STOP_CM = 10; // 幾公分以內一直亮、一直響
unsigned long lastPrint = 0;
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(BUZZER, OUTPUT);
mbcBegin();
delay(500);
if (!mbcWaitReady(ADDR, 4000)) {
Serial.println(F("找不到感應器,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("準備好了!手慢慢靠近感應器,看 L 燈越閃越快。"));
}
void loop() {
int16_t cm = mbcRead(ADDR, 0x03); // 03 = 左右兩顆取較近的距離(cm)
if (cm == MBC_NONE) { delay(100); return; }
if (millis() - lastPrint >= 500) { // 每 0.5 秒印一次距離
lastPrint = millis();
if (cm >= 120) Serial.println(F("沒有東西"));
else { Serial.print(F("最近的東西:")); Serial.print(cm); Serial.println(F(" 公分")); }
}
if (cm >= ALARM_START_CM) { // 太遠:安靜
alarm(false);
delay(50);
} else if (cm <= STOP_CM) { // 太近:一直亮
alarm(true);
delay(50);
} else { // 中間:越近閃越快(間隔 50 ~ 400 ms)
int gap = map(cm, STOP_CM, ALARM_START_CM, 50, 400);
alarm(true); delay(gap / 2);
alarm(false); delay(gap / 2);
}
}
void alarm(bool on) {
digitalWrite(LED_BUILTIN, on ? HIGH : LOW);
digitalWrite(BUZZER, on ? HIGH : LOW);
}
// ============================================================
// 以下是「通訊小幫手」(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;
}你會看到:(手慢慢靠近)
準備好了!手慢慢靠近感應器,看 L 燈越閃越快。
沒有東西
最近的東西:65 公分
最近的東西:30 公分
最近的東西:8 公分6️⃣ 雷射健康檢查
做什麼: 讀 06 初始化位元圖、07 故障位元圖和 F3,每秒報告兩顆雷射是不是都健康。失效那顆的距離會固定回 120/1200,看起來像「沒東西」,所以要靠這兩格分辨。比賽前跑一下。
/*
* 測距者 2 路 × Arduino 範例 6:雷射健康檢查
* ------------------------------------------------------------
* 這個範例會做什麼?
* 讀「06 初始化位元圖」「07 故障位元圖」和「F3 狀態旗標」,
* 每秒報告兩顆雷射是不是都健康。比賽前跑一下,就知道感應器有沒有壞。
* 06:bit0 = 左、bit1 = 右,1 = 那顆開機初始化成功(失敗不會重試,要重新上電)
* 07:bit0 = 左、bit1 = 右,1 = 那顆現在失效(讀不到或 200 ms 沒更新)
* 失效那顆的距離會固定回 120 cm/1200 mm,看起來像「沒東西」,所以要靠這兩格分辨。
*
* 你需要:測距者 2 路(通用 I2C 版)。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
*
* 怎麼看結果:
* 序列埠監控窗 115200。
*/
#include <Wire.h>
const uint8_t ADDR = 0x29; // 測距者的門牌號碼(I2C 位址)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
void setup() {
Serial.begin(115200);
mbcBegin();
delay(500);
Serial.println(F("=== 測距者 2 路:雷射健康檢查 ==="));
// 這個範例故意不等就緒,開機失敗時也看得到 06 哪一顆沒起來
delay(3000); // 兩顆雷射開機最多約 3 秒
}
void loop() {
int16_t v[2]; // v[0] = 06 初始化、v[1] = 07 故障
int16_t st = mbcRead(ADDR, 0xF3);
if (!mbcReadMany(ADDR, 0x06, 2, v) || st == MBC_NONE) {
Serial.println(F("整個感應器都讀不到:檢查接線,或它是不是 SPIKE/EV3 版"));
delay(1000);
return;
}
int16_t initOk = v[0], fault = v[1];
Serial.println(F("---------------------------"));
Serial.print(F("F3 就緒 ")); Serial.print((st & 0x01) ? F("是") : F("否"));
Serial.print(F(" 任一顆失效 ")); Serial.println((st & 0x02) ? F("是") : F("否"));
reportOne(F("左"), initOk & 0x01, fault & 0x01);
reportOne(F("右"), initOk & 0x02, fault & 0x02);
if (initOk == 3 && fault == 0) Serial.println(F("兩顆雷射都健康!"));
else Serial.println(F("有問題:先重新上電;還是一樣就檢查那顆雷射有沒有被遮住或接頭鬆了"));
delay(50);
delay(950);
}
// 印一顆雷射的狀況
void reportOne(const __FlashStringHelper *name, int16_t ok, int16_t bad) {
Serial.print(F(" "));
Serial.print(name);
Serial.print(F(" 顆:初始化 "));
Serial.print(ok ? F("成功") : F("失敗"));
Serial.print(F(",目前 "));
Serial.println(bad ? F("失效!") : 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;
}你會看到:
=== 測距者 2 路:雷射健康檢查 ===
---------------------------
F3 就緒 是 任一顆失效 否
左 顆:初始化 成功,目前 正常
右 顆:初始化 成功,目前 正常
兩顆雷射都健康!🔧 共用命令範例(所有 MBC 通用 I2C 產品都一樣)
這三支放在 examples/i2c/arduino/,循行者、測距者、掌控者、IMU、神攝手都能用;程式開頭的 ADDR/OLD_ADDR 改成 0x29 即可。
共用 1:換 I2C 位址(F8)
做什麼: 兩個測距者 2 路要接在同一條線上(例如車頭、車尾各一個)時,用 [F8][新位址][新位址取反] 把其中一個換成別的位址(存 EEPROM);[F8][00][FF] 回出廠。換完自動送 [EF][A5] 重開機,再掃一次確認。
/*
* MBC 通用 I2C × Arduino 共用範例 1:換 I2C 位址(F8)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 同一條 I2C 線上要接兩個「門牌號碼」一樣的 MBC 感應器(例如兩個測距者 2 路,都是 0x29)
* 時,先把其中一個換成別的位址。做法:
* 寫 [F8][新位址][新位址取反] → 存進感應器 EEPROM,重開機後生效
* 寫 [F8][0x00][0xFF] → 回出廠位址
* 換完會自動送 [EF][0xA5] 重新開機,再掃一次確認新位址有出現。
*
* 你需要:任何一款 MBC 通用 I2C 版感應器(一次只接一個!)。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
*
* 怎麼用:
* 1. 把下面 OLD_ADDR 改成感應器現在的位址、NEW_ADDR 改成想換的位址(0x08~0x77)。
* 2. 上傳,打開序列埠監控窗 115200,打 y 按 Enter 就換;打 r 按 Enter 回出廠。
* 3. 之後所有範例裡的 ADDR 都要改成新位址。
*
* 小提醒:
* F0「裝置 ID」低 byte 永遠是出廠位址,換了位址也不變,所以掃描時還是認得出是哪一款。
*/
#include <Wire.h>
const uint8_t OLD_ADDR = 0x29; // 感應器現在的位址 👉 改這裡(循行者 0x16、測距者 0x29、掌控者 0x42、IMU 0x6A、神攝手 0x4C)
const uint8_t NEW_ADDR = 0x2A; // 想換成的位址 👉 改這裡(0x08 ~ 0x77,別跟其他裝置撞號)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
void setup() {
Serial.begin(115200);
mbcBegin();
delay(500);
Serial.println(F("=== MBC 通用 I2C:換位址 ==="));
Serial.print(F("目前線上有哪些 MBC 裝置:")); scan();
if (!mbcWaitReady(OLD_ADDR, 6000)) {
Serial.print(F("在 0x")); Serial.print(OLD_ADDR, HEX); Serial.println(F(" 找不到感應器,請確認 OLD_ADDR 和接線。"));
while (true) {}
}
Serial.print(F("找到了 0x")); Serial.print(OLD_ADDR, HEX);
Serial.print(F("(出廠位址 0x")); Serial.print(mbcRead(OLD_ADDR, 0xF0) & 0xFF, HEX); Serial.println(')');
Serial.print(F("打 y 換成 0x")); Serial.print(NEW_ADDR, HEX); Serial.println(F(";打 r 回出廠位址。然後按 Enter"));
}
void loop() {
if (!Serial.available()) { delay(50); return; }
char c = Serial.read();
if (c == 'y') {
if (NEW_ADDR < 0x08 || NEW_ADDR > 0x77) { Serial.println(F("NEW_ADDR 要在 0x08 ~ 0x77 之間")); return; }
uint8_t p[2] = { NEW_ADDR, (uint8_t)~NEW_ADDR }; // [新位址][新位址取反]:兩個都對才會寫入,避免誤觸
Serial.print(F("換位址 → 0x")); Serial.println(NEW_ADDR, HEX);
if (!mbcWrite(OLD_ADDR, 0xF8, p, 2)) { Serial.println(F("寫入失敗")); return; }
rebootAndCheck(NEW_ADDR);
} else if (c == 'r') {
uint8_t p[2] = { 0x00, 0xFF }; // [F8][00][FF] = 回出廠位址
Serial.println(F("回出廠位址"));
if (!mbcWrite(OLD_ADDR, 0xF8, p, 2)) { Serial.println(F("寫入失敗")); return; }
rebootAndCheck(0); // 0 = 不知道出廠位址,掃描看結果
}
}
// 送重開機命令,等它起來,再掃描一次
void rebootAndCheck(uint8_t expect) {
delay(50); // 給 EEPROM 一點時間寫完
uint8_t magic = 0xA5;
mbcWrite(OLD_ADDR, 0xEF, &magic, 1); // [EF][A5] = 重新開機(新位址重開後才生效)
Serial.println(F("重新開機中..."));
delay(3000); // 測距者 8 路開機要 2~4 秒
Serial.print(F("掃描結果:")); scan();
if (expect && mbcRead(expect, 0xF3) != MBC_NONE) {
Serial.print(F("成功!感應器現在在 0x")); Serial.println(expect, HEX);
Serial.println(F("記得把其他範例的 ADDR 改成新位址。"));
} else if (expect) {
Serial.println(F("新位址沒有回應。請重新上電再掃一次(有些板子的 I2C 要重新上電才會更新)。"));
}
Serial.println(F("(把 OLD_ADDR 改成新位址再上傳,就可以再換一次或回出廠)"));
}
// 掃 0x08~0x77,有回應的就讀 F0 看是不是 MBC 裝置
void scan() {
uint8_t found = 0;
for (uint8_t a = 0x08; a <= 0x77; a++) {
Wire.beginTransmission(a);
if (Wire.endTransmission() != 0) continue;
int16_t id = mbcRead(a, 0xF0);
Serial.print(F(" 0x")); Serial.print(a, HEX);
if (id != MBC_NONE) { Serial.print(F("(出廠 0x")); Serial.print(id & 0xFF, HEX); Serial.print(')'); }
found++;
}
if (!found) Serial.print(F("(沒有)"));
Serial.println();
}
// ============================================================
// 以下是「通訊小幫手」(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;
}共用 2:身分證+重新開機(F0~F5、EF)
做什麼: 一次讀 F0~F5 六格共用暫存器並認出產品;打 r 送 [EF][A5] 重開機,看 F5 開機秒數歸零。
下載 i2c_02_reboot_and_version.ino
/*
* MBC 通用 I2C × Arduino 共用範例 2:讀身分證+重新開機(F0~F5、EF)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 每一款 MBC 通用 I2C 感應器都有同樣的 6 格「身分證」:
* F0 裝置 ID(低 byte 出廠位址、高 byte hw[0])、F1 協議版本、F2 韌體版本、
* F3 狀態旗標、F4 更新序號(每 10 ms +1,0~255 循環)、F5 開機秒數
* 這個範例每秒印一次,並認出是哪一款產品。
* 在序列埠打 r 按 Enter → 送 [EF][0xA5] 重新開機,你會看到 F5 開機秒數歸零、F4 繼續跳。
*
* 你需要:任何一款 MBC 通用 I2C 版感應器。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
*
* 怎麼看結果:
* 序列埠監控窗 115200。
*
* 小提醒:
* F4 一圈只有 2.56 秒,不能拿來判斷有沒有重開機;要看 F5 有沒有變小。
* F4 不動 = 感應器忙(例如校準中)。
*/
#include <Wire.h>
const uint8_t ADDR = 0x16; // 感應器位址 👉 改這裡(循行者 0x16、測距者 0x29、掌控者 0x42、IMU 0x6A、神攝手 0x4C)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
void setup() {
Serial.begin(115200);
mbcBegin();
delay(500);
Serial.println(F("=== MBC 通用 I2C:身分證+重新開機 ==="));
if (!mbcWaitReady(ADDR, 6000)) {
Serial.println(F("找不到感應器,請確認 ADDR 和接線(可以先跑 i2c_03_scan 掃描)。"));
while (true) {}
}
Serial.println(F("打 r 按 Enter = 重新開機"));
}
void loop() {
if (Serial.available() && Serial.read() == 'r') {
uint8_t magic = 0xA5;
mbcWrite(ADDR, 0xEF, &magic, 1); // [EF][A5] = 重新開機;其他數字會被忽略
Serial.println(F(">>> 已送重新開機,看 F5 會不會歸零"));
delay(500);
}
int16_t f[6]; // F0~F5 編號連續,一次讀 6 格
if (!mbcReadMany(ADDR, 0xF0, 6, f)) { Serial.println(F("(重開機中/讀不到)")); delay(500); return; }
uint8_t factory = (uint8_t)(f[0] & 0xFF);
uint8_t hw0 = (uint8_t)((f[0] >> 8) & 0xFF);
Serial.println(F("---------------------------"));
Serial.print(F("F0 裝置 ID 出廠位址 0x")); Serial.print(factory, HEX);
Serial.print(F(" hw[0] = ")); Serial.print(hw0);
Serial.print(F(" → ")); printProduct(factory, hw0); Serial.println();
Serial.print(F("F1 協議版本 ")); Serial.println(f[1]);
Serial.print(F("F2 韌體版本 ")); Serial.print(f[2] / 100); Serial.print('.'); Serial.println(f[2] % 100);
Serial.print(F("F3 狀態旗標 0b"));
for (int8_t i = 7; i >= 0; i--) Serial.print((f[3] >> i) & 1);
Serial.println(F(" (bit0 = 就緒,其他 bit 看各產品說明)"));
Serial.print(F("F4 更新序號 ")); Serial.print(f[4]); Serial.println(F(" (一直在跳 = 感應器活著)"));
Serial.print(F("F5 開機秒數 ")); Serial.print(f[5]); Serial.println(F(" 秒"));
delay(50);
delay(950);
}
// 用出廠位址+hw[0] 認產品
void printProduct(uint8_t factory, uint8_t hw0) {
if (factory == 0x16 && hw0 == 8) Serial.print(F("循行者 8 路"));
else if (factory == 0x16 && hw0 == 16) Serial.print(F("循行者 16 路"));
else if (factory == 0x29 && hw0 == 2) Serial.print(F("測距者 2 路"));
else if (factory == 0x29 && hw0 == 8) Serial.print(F("測距者 8 路"));
else if (factory == 0x42 && hw0 == 1) Serial.print(F("掌控者 PS2"));
else if (factory == 0x42 && hw0 == 2) Serial.print(F("掌控者 PS4"));
else if (factory == 0x6A) Serial.print(F("IMU 陀螺儀"));
else if (factory == 0x4C && hw0 == 1) Serial.print(F("神攝手 學校版"));
else if (factory == 0x4C && hw0 == 2) Serial.print(F("神攝手 WRO 版"));
else Serial.print(F("不認識的 MBC 裝置"));
}
// ============================================================
// 以下是「通訊小幫手」(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;
}共用 3:掃描線上裝置
做什麼: 掃 0x08~0x77,有回應的就讀 F0 認出是哪一款 MBC 產品、韌體幾版、位址有沒有換過。
/*
* MBC 通用 I2C × Arduino 共用範例 3:掃描線上有哪些裝置
* ------------------------------------------------------------
* 這個範例會做什麼?
* 把 I2C 位址 0x08~0x77 全部敲一遍,有回應的就讀 F0「裝置 ID」認出是哪一款 MBC 產品
* (不是 MBC 的裝置會印「其他裝置」)。接線有沒有通、位址換成什麼、同一條線上接了幾個,
* 一跑就知道。每 3 秒掃一次。
*
* 你需要:任何 MBC 通用 I2C 版感應器(可以同時接好幾個,位址不同就行)。
*
* 接線:
* 感應器 VCC → 5V 感應器 GND → GND
* 感應器 SDA → A4 感應器 SCL → A5 (UNO / Nano)
* UNO R4 接 SDA / SCL 腳;ESP32 接 GPIO 21 (SDA) / GPIO 22 (SCL)
*
* 怎麼看結果:
* 序列埠監控窗 115200。
*/
#include <Wire.h>
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
void setup() {
Serial.begin(115200);
mbcBegin();
delay(500);
Serial.println(F("=== MBC 通用 I2C:掃描 ==="));
}
void loop() {
uint8_t found = 0;
Serial.println(F("掃描 0x08 ~ 0x77 ..."));
for (uint8_t a = 0x08; a <= 0x77; a++) {
Wire.beginTransmission(a); // 敲門
if (Wire.endTransmission() != 0) continue; // 沒人應門 → 下一個
found++;
Serial.print(F(" 0x"));
if (a < 0x10) Serial.print('0');
Serial.print(a, HEX);
Serial.print(F(" "));
int16_t id = mbcRead(a, 0xF0); // 讀裝置 ID
if (id == MBC_NONE) { Serial.println(F("其他裝置(不是 MBC,或不是通用 I2C 版)")); continue; }
uint8_t factory = (uint8_t)(id & 0xFF);
uint8_t hw0 = (uint8_t)((id >> 8) & 0xFF);
printProduct(factory, hw0);
int16_t fw = mbcRead(a, 0xF2);
Serial.print(F(" 韌體 ")); Serial.print(fw / 100); Serial.print('.'); Serial.print(fw % 100);
Serial.print(F(" 協議 v")); Serial.print(mbcRead(a, 0xF1));
if (factory != a) { Serial.print(F(" (位址已換過,出廠是 0x")); Serial.print(factory, HEX); Serial.print(')'); }
Serial.println();
}
if (!found) Serial.println(F(" 沒有任何裝置:檢查 SDA/SCL 有沒有接反、VCC/GND 有沒有插緊"));
Serial.print(F("共 ")); Serial.print(found); Serial.println(F(" 個裝置"));
delay(50);
delay(2950);
}
// 用出廠位址+hw[0] 認產品
void printProduct(uint8_t factory, uint8_t hw0) {
if (factory == 0x16 && hw0 == 8) Serial.print(F("循行者 8 路"));
else if (factory == 0x16 && hw0 == 16) Serial.print(F("循行者 16 路"));
else if (factory == 0x29 && hw0 == 2) Serial.print(F("測距者 2 路"));
else if (factory == 0x29 && hw0 == 8) Serial.print(F("測距者 8 路"));
else if (factory == 0x42 && hw0 == 1) Serial.print(F("掌控者 PS2"));
else if (factory == 0x42 && hw0 == 2) Serial.print(F("掌控者 PS4"));
else if (factory == 0x6A) Serial.print(F("IMU 陀螺儀"));
else if (factory == 0x4C && hw0 == 1) Serial.print(F("神攝手 學校版"));
else if (factory == 0x4C && hw0 == 2) Serial.print(F("神攝手 WRO 版"));
else Serial.print(F("不認識的 MBC 裝置"));
}
// ============================================================
// 以下是「通訊小幫手」(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;
}你會看到:
=== MBC 通用 I2C:掃描 ===
掃描 0x08 ~ 0x77 ...
0x29 測距者 2 路 韌體 2.0 協議 v2
共 1 個裝置常見問題
Q:一直出現「找不到感應器」?
- 檢查 SDA、SCL 有沒有接反(UNO:SDA → A4、SCL → A5;ESP32:21 / 22)。
- 檢查 VCC、GND 有沒有插緊。
- 確認是 通用 I2C 版:SPIKE 版、EV3 版的韌體不同,不能直接接 Arduino。舊板(PA9/PA10 沒有 I2C)也沒有通用 I2C 版。
- 跑 共用 3 掃描,看線上到底有沒有裝置、位址是不是被換過。
Q:讀到 -32768 代表什麼?
-32768(程式裡叫 MBC_NONE)有兩種意思:① 小幫手讀不到感應器(接線鬆了、位址錯了);② 那個編號是空號——測距者 2 路只有 01~08,去讀 09 就會拿到 -32768。正常的距離不可能是這個數字,所以可以直接拿來判斷「這筆資料不能用」。
Q:ESP32 讀不到、或讀到的值怪怪的?
MBC 有些產品(掌控者 PS4、神攝手)本身是 ESP32 做的,不能「時脈延展」,主機寫完編號要等 2 ms 再讀。小幫手 mbcReadMany 裡已經有 delay(2),請不要刪掉。ESP32 當主機時 I2C 腳位預設是 GPIO 21(SDA)/22(SCL)。
Q:距離一直是 120/1200?
那個方向 120 公分內真的沒東西,或那顆雷射失效了。跑 範例 6 看 07 故障位元圖就知道是哪一種。
Q:序列埠監控窗一堆亂碼? 速度沒選 115200,在監控窗右下角改好就正常了。
Q:可以同時接測距者 2 路和 8 路嗎?
兩款出廠都是 0x29,要先用 共用 1 把其中一個換位址。位址不同的 MBC 產品(例如循行者 0x16)本來就能接在同一組線上。
其他問題
- 一直讀到 -32768? 代表模組沒回應:檢查 SDA/SCL 有沒有接反、位址對不對、5V/GND 有沒有接好。
- 可以接很多顆嗎? 可以,每顆位址要不同;同型號用命令 F8 換位址(見通用 I2C 協議)。
還有問題?到 聯絡我們 附上你用的軟體和程式截圖。
下載
本頁依據
- 主機與軟體清單:
_build/guides.json - 值與命令:
_build/protocol/tof2.json(單一真相來源) - 安裝與設定:
_build/guides/software/arduino-ide.zh.md、_build/guides/product/tof2/arduino-ide.zh.md - 範例:
sensors/tof2/arduino-examples.md