陀螺儀 IMU × Arduino 範例
回到陀螺儀 IMU 首頁 | MBC 通用 I2C 協議 v2(進階)
這一頁有 12 個範例,從「確認接好了沒」一路做到「小車原地轉 90 度」「走直線」「轉到指定的方向」、校準和進階命令。每個範例都可以直接複製、上傳,程式裡每一行都有中文說明。完全沒寫過程式也沒關係,照順序一個一個做就好。
提示
第一次用 Arduino?先看 🔰 第一次用 Arduino:從零開始,15 分鐘學會安裝軟體、上傳程式、看結果。
接線(所有範例都一樣)
| 陀螺儀 | Arduino UNO / Nano | Arduino UNO R4 | ESP32 |
|---|---|---|---|
| VCC | 5V | 5V | 3V3 或 5V(VIN) |
| GND | GND | GND | GND |
| SDA | A4 | SDA | GPIO 21 |
| SCL | A5 | SCL | GPIO 22 |
重要
- 請用 通用 I2C 版 的陀螺儀。SPIKE 版、EV3 版的韌體不同,不能直接接 Arduino。
- 陀螺儀開機時要先放著不動約 0.5 秒,它會自己記住「哪一面朝上」並把航向歸零。
- 遠離馬達、喇叭、鐵桌:磁力計會被干擾(範例 6 可以看干擾度)。
- 接線前先拔掉 USB 線。
指令總覽(這個陀螺儀能讀到什麼)
陀螺儀就像一排有編號的「格子」,每一格放一個數字(int16)。寫 [編號] 再讀 2 個 byte 就拿到一個值;連續的編號可以一次讀一串(最多 16 個)。範例裡的 mbcRead(ADDR, 編號)、mbcReadMany(ADDR, 起始編號, 幾個, 陣列) 就是在做這件事。
| 編號 | 名稱 | 範圍/單位 | 說明 | 出現在 |
|---|---|---|---|---|
01 |
陀螺 3D 航向 | 度 | 用三軸算的航向,開機/歸零 = 0;從上往下看右轉(順時針)變大,會一直累加(轉兩圈 = 720),到 ±32767 繞回 | 範例 2、3 |
02 |
融合航向 | 度 | 陀螺 3D 再用磁力計慢慢修正,久了不會漂 | 範例 2、3、7 |
03 |
陀螺 1D 航向 | 度 | 只用轉向那一軸積分 | 範例 2、3 |
04 |
磁力航向 | 度 | 只看磁力計(像指南針,會被鐵器干擾) | 範例 2、3 |
05 |
轉向軸角速度 | 0.1 度/秒 | 繞轉向軸轉多快(123 = 12.3 度/秒),正負跟航向同向 | 範例 2、4 |
06 |
俯仰角 | 度 | 板子繞絲印 X 軸翹起幾度,平放 = 0 | 範例 2、4 |
07 |
俯仰角速度 | 0.1 度/秒 | 俯仰角變化多快 | 範例 2、4 |
08 |
磁力信任度 | 0~255 | 融合演算法多相信磁力計,越高越好 | 範例 2、6 |
09 |
磁場干擾度 | 0~255 | 附近鐵器、馬達的干擾,越低越好 | 範例 2、6 |
0A |
朝上的面 | 0~5 | 0 +X、1 +Y、2 +Z(平放)、3 −X、4 −Y、5 −Z | 範例 2、6 |
0B |
命令狀態 | — | 最後一個命令碼 × 256 + 狀態(0 完成、1 忙碌、2 失敗、3 拒絕) | 範例 11 |
10~`12` |
角速度 X/Y/Z | 0.1 度/秒 | 實體軸原始值 | 範例 2、5 |
13~`15` |
加速度 X/Y/Z | mg | 1000 = 1 g,平放時 Z 約 1000 | 範例 2、5 |
16~`18` |
磁力 X/Y/Z | mG | 磁場強度(毫高斯) | 範例 2、5 |
F0 |
裝置 ID | — | 低 byte = 出廠位址 0x6A、高 byte = 1 | 範例 1 |
F1 |
協議版本 | 2 | 這份範例照版本 2 寫 | 範例 1 |
F2 |
韌體版本 | 主×100+次 | 例如 102 = 1.2 | 範例 1 |
F3 |
狀態旗標 | bit | b0 就緒、b1 靜止、b2 已擷取安裝軸、b3 安裝過歪、b4 加速度計校準中、b5 磁力計校準中、b6 磁力融合已初始化、b7 磁力自動校準已收斂 | 範例 1、8 |
F4 |
快照序號 | — | 每 10 毫秒或收到命令後 +1 | 範例 11 |
F5 |
開機秒數 | 0~32767 | 數字變小 = 重開機過 | 範例 1 |
E0(寫) |
航向歸零 | 無參數 | 四個航向都歸零 | 範例 3、7 |
E1/E2(寫) |
加速度計校準開始/結束 | 無參數 | 期間 F3 b4 = 1;結束存 EEPROM 並自動歸零航向 | 範例 8 |
E3/E4(寫) |
磁力計校準開始/結束 | 無參數 | 期間 F3 b5 = 1;結束存 EEPROM | 範例 8 |
E5(寫) |
航向設成指定角度 | [角度] int16 | 四個航向都設成這個角度;只存 RAM,斷電回 0 | 範例 11 |
E6(寫) |
以磁北為 0 | 無參數 | 融合航向改成目前的磁北方位 0~359;約 3 秒內完成 | 範例 11 |
E7(寫) |
陀螺零點重估 | 無參數 | 放著不動約 1.5 秒,重新估計陀螺零點 | 範例 11 |
E8(寫) |
重抓安裝軸 | 無參數 | 重新判定安裝方向,抓完自動歸零 | — |
E9(寫) |
取消校準 | 無參數 | 取消進行中的加速度計/磁力計校準,不存檔 | — |
EA(寫) |
航向修正係數 | 0、3500~3700 | 轉一圈實際讀到的度數 × 10;0 = 回到 360.0;存 EEPROM | 範例 12 |
EB(寫) |
校準回出廠 | [0xA5] | 加速度計、磁力計校準與航向修正係數都回出廠值 | — |
注意
沒有的編號(例如
0C~`0F)會讀到-32768(範例裡叫MBC_NONE),讀不到陀螺儀也是-32768`。完整規格見 MBC 通用 I2C 協議 v2。
範例總覽
| # | 範例 | 學會什麼 | 另外要準備 |
|---|---|---|---|
| 1 | 跟陀螺儀打招呼 | 確認接線正確、讀身分證、F3 的 8 個 bit | 無 |
| 2 | 把所有的值一次印出來 | 看懂每個編號放什麼(01 |
無 |
| 3 | 我轉了幾度? | 四種航向、E0 歸零 | 無 |
| 4 | 轉多快?翹多高? | 角速度、俯仰角 | 一本書(墊高一邊) |
| 5 | 九軸原始值 | 角速度/加速度/磁力 XYZ | 一塊磁鐵(選用) |
| 6 | 哪一面朝上? | 朝上的面、磁力信任度/干擾度 | 無 |
| 7 | 小車原地轉 90 度(進階) | 用航向控制轉彎 | 兩輪小車、L298N 馬達驅動板 |
| 8 | 校準加速度計與磁力計 | 寫入命令 E1~E4、看 F3 | 無 |
| 9 | 小車走直線(進階) | 用航向修正左右輪速度 | 兩輪小車、L298N 馬達驅動板 |
| 10 | 小車轉到指定的方向(進階) | 轉到絕對角度(跟開機方向比) | 兩輪小車、L298N 馬達驅動板 |
| 11 | 下命令並確認做完了沒(進階) | 命令 E5~E7、看命令狀態 0B | 無 |
| 12 | 航向修正係數(進階) | 量轉一圈讀到幾度、命令 EA 存檔 | 一張有直邊的桌子(或一本書) |
提示
建議順序:1 → 2 → 3 → 4 → 6 → 5 → 7 → 9 → 10 → 8 → 11 → 12。 7、9、10 用同一台小車,要另外準備零件;8 只有航向漂得很快時才需要。11、12 是進階命令(新版韌體才有),前面都熟了再做;12 只有轉一圈讀到的不是 360 度時才需要。
1️⃣ 跟陀螺儀打招呼
做什麼: 確認陀螺儀接好了,印出它的「身分證」(出廠位址、協議版本、韌體版本)和狀態旗標 F3 的 8 個 bit。每次接新的陀螺儀,都先跑這一個。
步驟:
- 照上面的表格接好 4 條線,USB 插上電腦。
- 複製下面的程式,貼到 Arduino IDE,按 → 上傳。
- 打開序列埠監控窗(右上角放大鏡),速度選 115200。
/*
* IMU 陀螺儀 × Arduino 範例 1:跟陀螺儀打招呼
* ------------------------------------------------------------
* 這個範例會做什麼?
* 確認陀螺儀有沒有接好。接好了,會印出它的身分證(裝置 ID、協議版本、
* 韌體版本)和狀態旗標 F3 的 8 個 bit(靜止中?校準中?安裝有沒有歪?)。
* 每次接新的陀螺儀,都建議先跑這個範例。
*
* 你需要:IMU 陀螺儀(通用 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。
*
* 小提醒:
* 陀螺儀開機時要先放著不動約 0.5 秒,它會自己記住「哪一面朝上」並歸零。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具,用來跟陀螺儀講話
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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("=== IMU 陀螺儀:跟陀螺儀打招呼 ==="));
// 等陀螺儀說「我準備好了」(狀態旗標 F3 的 bit0),最多等 3 秒
if (!mbcWaitReady(ADDR, 3000)) {
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 = 出廠位址(0x6A)、高 byte = hw[0](IMU = 1) ----
int16_t id = mbcRead(ADDR, 0xF0);
Serial.print(F("找到了!出廠位址 0x"));
Serial.print((uint8_t)(id & 0xFF), HEX);
Serial.print(F(",款式代號 "));
Serial.println((uint8_t)((id >> 8) & 0xFF)); // IMU = 1
// ---- 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 狀態旗標:8 個 bit,一個 bit 代表一件事 ----
int16_t st = mbcRead(ADDR, 0xF3);
Serial.println(F("目前狀態:"));
printFlags(st);
// ---- F5 開機秒數 ----
Serial.print(F("開機秒數:"));
Serial.println(mbcRead(ADDR, 0xF5));
Serial.println(F("接線正確,可以開始玩其他範例了!"));
}
void loop() {
// 這個範例只要在開機時檢查一次,所以 loop 裡什麼都不做
}
// 把 F3 的 8 個 bit 一行一行印出來
void printFlags(int16_t st) {
Serial.print(F(" bit0 就緒 ")); Serial.println((st & 0x01) ? F("是") : F("否"));
Serial.print(F(" bit1 靜止中 ")); Serial.println((st & 0x02) ? F("是") : F("否(正在動)"));
Serial.print(F(" bit2 已擷取安裝軸 ")); Serial.println((st & 0x04) ? F("是(知道哪面朝上)") : F("否(放著不動等一下)"));
Serial.print(F(" bit3 安裝過歪 ")); Serial.println((st & 0x08) ? F("是(請放平一點)") : F("否"));
Serial.print(F(" bit4 加速度計校準中 ")); Serial.println((st & 0x10) ? F("是") : F("否"));
Serial.print(F(" bit5 磁力計校準中 ")); Serial.println((st & 0x20) ? F("是") : F("否"));
Serial.print(F(" bit6 磁力融合已初始化 ")); Serial.println((st & 0x40) ? F("是(融合航向可用)") : F("否"));
Serial.print(F(" bit7 磁力自動校準收斂 ")); Serial.println((st & 0x80) ? 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;
}你會看到:
=== IMU 陀螺儀:跟陀螺儀打招呼 ===
找到了!出廠位址 0x6A,款式代號 1
協議版本:2
韌體版本:1.2
目前狀態:
bit0 就緒 是
bit1 靜止中 是
bit2 已擷取安裝軸 是(知道哪面朝上)
bit3 安裝過歪 否
bit4 加速度計校準中 否
bit5 磁力計校準中 否
bit6 磁力融合已初始化 是(融合航向可用)
bit7 磁力自動校準收斂 否
開機秒數:2
接線正確,可以開始玩其他範例了!注意
「通訊小幫手」是什麼? 每個範例最下面都有一段一模一樣的程式,負責跟陀螺儀「講話」。你不用改它,也不用看懂。只要記得:
mbcRead(ADDR, 編號):讀一個值(讀不到回-32768)mbcReadMany(ADDR, 起始編號, 幾個, 陣列):從某個編號起一次讀一串(最多 16 個,同一瞬間的值)mbcWrite(ADDR, 命令):送命令(例如0xE0歸零)mbcWaitReady(ADDR, 毫秒):等陀螺儀開機好
2️⃣ 把所有的值一次印出來
做什麼: 每 0.5 秒把值表裡的每一個值都印出來:010A 一次讀 10 個、1018 一次讀 9 個。轉一轉、翻一翻,看哪一行在變。
/*
* IMU 陀螺儀 × Arduino 範例 2:把所有的值一次印出來
* ------------------------------------------------------------
* 這個範例會做什麼?
* 每 0.5 秒把陀螺儀「值表」裡的每一個值都印出來,讓你看懂每個編號放什麼:
* 01~0A:四種航向、角速度、俯仰角、磁力信任度/干擾度、朝上的面(一次讀 10 個)
* 10~18:角速度、加速度、磁力各 X/Y/Z(一次讀 9 個)
* 連續的編號可以「一次讀一串」(mbcReadMany),讀到的是同一瞬間的值。
*
* 你需要:IMU 陀螺儀(通用 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。轉一轉、翻一翻陀螺儀,看哪些數字在變。
*
* 小提醒:
* 陀螺儀開機時要先放著不動約 0.5 秒,它會自己記住「哪一面朝上」並歸零。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("準備好了!每 0.5 秒印一次所有的值。"));
}
void loop() {
if (millis() - lastPrint >= 500) { // 👉 可以改:每 0.5 秒印一次
lastPrint = millis();
// ---- 第一段:編號 01~0A,一次讀 10 個值 ----
int16_t a[10];
if (!mbcReadMany(ADDR, 0x01, 10, a)) { Serial.println(F("讀不到(第一段)")); return; }
// ---- 第二段:編號 10~18,一次讀 9 個值 ----
int16_t r[9];
if (!mbcReadMany(ADDR, 0x10, 9, r)) { Serial.println(F("讀不到(第二段)")); return; }
Serial.println(F("------------------------------"));
Serial.print(F("01 陀螺 3D 航向(度) :")); Serial.println(a[0]);
Serial.print(F("02 融合航向(度) :")); Serial.println(a[1]);
Serial.print(F("03 陀螺 1D 航向(度) :")); Serial.println(a[2]);
Serial.print(F("04 磁力航向(度) :")); Serial.println(a[3]);
Serial.print(F("05 轉向軸角速度(0.1 度/秒) :")); Serial.println(a[4]);
Serial.print(F("06 俯仰角(度) :")); Serial.println(a[5]);
Serial.print(F("07 俯仰角速度(0.1 度/秒) :")); Serial.println(a[6]);
Serial.print(F("08 磁力信任度 0~255 :")); Serial.println(a[7]);
Serial.print(F("09 磁場干擾度 0~255 :")); Serial.println(a[8]);
Serial.print(F("0A 朝上的面 0~5 :")); Serial.println(a[9]);
Serial.print(F("10~12 角速度 X/Y/Z(0.1 度/秒):")); print3(r[0], r[1], r[2]);
Serial.print(F("13~15 加速度 X/Y/Z(mg) :")); print3(r[3], r[4], r[5]);
Serial.print(F("16~18 磁力 X/Y/Z(mG) :")); print3(r[6], r[7], r[8]);
}
delay(50);
}
// 三個數字印成一行
void print3(int16_t x, int16_t y, int16_t z) {
Serial.print(x); Serial.print(F(" ")); Serial.print(y); Serial.print(F(" ")); Serial.println(z);
}
// ============================================================
// 以下是「通訊小幫手」(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 陀螺 3D 航向(度) :0
02 融合航向(度) :0
03 陀螺 1D 航向(度) :0
04 磁力航向(度) :1
05 轉向軸角速度(0.1 度/秒) :0
06 俯仰角(度) :0
07 俯仰角速度(0.1 度/秒) :0
08 磁力信任度 0~255 :230
09 磁場干擾度 0~255 :12
0A 朝上的面 0~5 :2
10~12 角速度 X/Y/Z(0.1 度/秒):0 1 0
13~15 加速度 X/Y/Z(mg) :3 -5 1002
16~18 磁力 X/Y/Z(mG) :210 -95 -3803️⃣ 我轉了幾度?(四種航向)
做什麼: 每 0.1 秒印出四種航向(01~`04一次讀 4 個)。從上往下看**右轉(順時針)數字變大**,會一直累加,可以變負數。在序列埠監控窗輸入z再按 Enter,會送命令E0` 把四個航向都歸零。
/*
* IMU 陀螺儀 × Arduino 範例 3:我轉了幾度?(四種航向)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 每 0.1 秒印出四種航向(編號 01~04,一次讀 4 個):
* 01 陀螺 3D 航向:用全部三軸算的,最常用
* 02 融合航向:陀螺 3D 再用磁力計慢慢修正,久了不會漂
* 03 陀螺 1D 航向:只用轉向那一軸積分
* 04 磁力航向:只看磁力計(像指南針,會被鐵器干擾)
* 從上往下看「順時針」(右轉)數字變大,會一直累加(轉兩圈 = 720),可以變負數。
* 在序列埠監控窗輸入 z 再按 Enter,會送命令 E0 把四個航向都歸零。
*
* 你需要:IMU 陀螺儀(通用 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。把陀螺儀平放,慢慢轉一圈,看數字。
*
* 小提醒:
* 陀螺儀開機時要先放著不動約 0.5 秒,它會自己記住「哪一面朝上」並歸零。
* 要 0~359 的話自己換算:((h % 360) + 360) % 360。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("準備好了!轉轉看陀螺儀。輸入 z + Enter 可以歸零。"));
Serial.println(F(" 陀螺3D 融合 陀螺1D 磁力"));
}
void loop() {
// ---- 1. 電腦有送 z 過來嗎?有就送命令 E0(航向歸零,沒有參數) ----
if (Serial.available()) {
char ch = Serial.read();
if (ch == 'z' || ch == 'Z') {
mbcWrite(ADDR, 0xE0); // 命令 E0:四個航向都歸零
Serial.println(F(">>> 已歸零"));
}
}
// ---- 2. 每 0.1 秒讀編號 01~04 四個航向 ----
if (millis() - lastPrint >= 100) { // 👉 可以改:每 0.1 秒印一次
lastPrint = millis();
int16_t h[4];
if (!mbcReadMany(ADDR, 0x01, 4, h)) { Serial.println(F("讀不到陀螺儀")); return; }
char line[48];
snprintf(line, sizeof(line), "%7d %7d %7d %7d", h[0], h[1], h[2], h[3]); // 每個數字佔 7 格,排整齊
Serial.println(line);
}
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;
}你會看到:(右轉約 90 度,再輸入 z)
準備好了!轉轉看陀螺儀。輸入 z + Enter 可以歸零。
陀螺3D 融合 陀螺1D 磁力
0 0 0 1
45 45 44 47
90 90 89 93
>>> 已歸零
0 0 0 0提示
- 平常用
02融合航向就好:轉得快時跟陀螺一樣準,放久了也不會漂。- 要 0~359 的話自己換算:
((h % 360) + 360) % 360。- 三種航向都是「相對值」(開機或歸零的方向 = 0),不是指北的羅盤角度;
04磁力航向也是減去零點。
4️⃣ 轉多快?翹多高?(角速度與俯仰角)
做什麼: 每 0.2 秒印出 05 轉向軸角速度、06 俯仰角、07 俯仰角速度(一次讀 3 個)。角速度單位是 0.1 度/秒,程式會印成 12.3 這種格式;轉超過 30 度/秒會提醒「轉太快」。
/*
* IMU 陀螺儀 × Arduino 範例 4:轉多快?翹多高?(角速度與俯仰角)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 每 0.2 秒印出編號 05~07(一次讀 3 個):
* 05 轉向軸角速度:繞「轉向軸」轉多快,單位 0.1 度/秒(123 = 12.3 度/秒),正負跟航向同向
* 06 俯仰角:板子繞絲印 X 軸翹起幾度(平放 = 0)
* 07 俯仰角速度:俯仰角變化多快,單位 0.1 度/秒
* 轉向軸角速度超過 30 度/秒時會提醒「轉太快」。
*
* 你需要:IMU 陀螺儀(通用 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。原地轉陀螺儀看 05;把一邊翹起來看 06。
*
* 小提醒:
* 陀螺儀開機時要先放著不動約 0.5 秒,它會自己記住「哪一面朝上」並歸零。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
const int FAST_DPS = 30; // 👉 可以改:超過幾度/秒算「轉太快」
unsigned long lastPrint = 0;
void setup() {
Serial.begin(115200);
mbcBegin();
delay(500);
if (!mbcWaitReady(ADDR, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("準備好了!轉轉看、翹翹看。"));
}
void loop() {
if (millis() - lastPrint >= 200) { // 👉 可以改:每 0.2 秒印一次
lastPrint = millis();
// 一次讀編號 05~07 共 3 個值:[0] 轉向軸角速度 [1] 俯仰角 [2] 俯仰角速度
int16_t v[3];
if (!mbcReadMany(ADDR, 0x05, 3, v)) { Serial.println(F("讀不到陀螺儀")); return; }
Serial.print(F("轉向角速度 "));
Serial.print(v[0] / 10); Serial.print('.'); Serial.print(abs(v[0] % 10)); // 0.1 度/秒 → 印成 12.3
Serial.print(F(" 度/秒 | 俯仰角 "));
Serial.print(v[1]);
Serial.print(F(" 度 | 俯仰角速度 "));
Serial.print(v[2] / 10); Serial.print('.'); Serial.print(abs(v[2] % 10));
Serial.print(F(" 度/秒"));
if (abs(v[0]) > FAST_DPS * 10) Serial.print(F(" <<< 轉太快!"));
Serial.println();
}
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;
}你會看到:(平放,然後快速右轉,然後把一邊墊在書上)
準備好了!轉轉看、翹翹看。
轉向角速度 0.0 度/秒 | 俯仰角 0 度 | 俯仰角速度 0.0 度/秒
轉向角速度 85.3 度/秒 | 俯仰角 0 度 | 俯仰角速度 0.2 度/秒 <<< 轉太快!
轉向角速度 0.1 度/秒 | 俯仰角 14 度 | 俯仰角速度 0.0 度/秒5️⃣ 九軸原始值
做什麼: 每 0.2 秒印出 10~`18` 的九個原始值(一次讀 9 個):角速度 XYZ(0.1 度/秒)、加速度 XYZ(mg,1000 = 1 g)、磁力 XYZ(mG)。做自己的演算法時用。
/*
* IMU 陀螺儀 × Arduino 範例 5:九軸原始值
* ------------------------------------------------------------
* 這個範例會做什麼?
* 每 0.2 秒印出編號 10~18 的九個原始值(一次讀 9 個):
* 10~12 角速度 X/Y/Z,單位 0.1 度/秒
* 13~15 加速度 X/Y/Z,單位 mg(1000 = 1 g,平放時 Z 約 1000)
* 16~18 磁力 X/Y/Z,單位 mG(毫高斯)
* 這些是「實體軸」的值(板子絲印的 X、Y、Z),做自己的演算法時用。
*
* 你需要:IMU 陀螺儀(通用 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。平放時加速度 Z 約 1000、X/Y 約 0;拿磁鐵靠近看磁力變化。
*
* 小提醒:
* 陀螺儀開機時要先放著不動約 0.5 秒,它會自己記住「哪一面朝上」並歸零。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("準備好了!"));
Serial.println(F("角速度 X/Y/Z(0.1度/秒) | 加速度 X/Y/Z(mg) | 磁力 X/Y/Z(mG)"));
}
void loop() {
if (millis() - lastPrint >= 200) { // 👉 可以改:每 0.2 秒印一次
lastPrint = millis();
// 一次讀編號 10~18 共 9 個值:[0..2] 角速度 [3..5] 加速度 [6..8] 磁力
int16_t r[9];
if (!mbcReadMany(ADDR, 0x10, 9, r)) { Serial.println(F("讀不到陀螺儀")); return; }
char line[96];
snprintf(line, sizeof(line), "%6d %6d %6d | %6d %6d %6d | %6d %6d %6d",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8]); // 每個數字佔 6 格
Serial.println(line);
}
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;
}你會看到:(平放,然後拿磁鐵靠近)
準備好了!
角速度 X/Y/Z(0.1度/秒) | 加速度 X/Y/Z(mg) | 磁力 X/Y/Z(mG)
0 1 0 | 3 -5 1002 | 210 -95 -380
0 0 1 | 2 -4 1001 | 1850 -620 -4106️⃣ 哪一面朝上?磁力可不可信?
做什麼: 每 0.3 秒讀 08 磁力信任度、09 磁場干擾度、0A 朝上的面(一次讀 3 個)。朝上的面有變化就印一行;平放(+Z 朝上)時板子內建的 LED 會亮。
/*
* IMU 陀螺儀 × Arduino 範例 6:哪一面朝上?磁力可不可信?
* ------------------------------------------------------------
* 這個範例會做什麼?
* 每 0.3 秒讀編號 08~0A(一次讀 3 個):
* 0A 朝上的面:0 +X、1 +Y、2 +Z、3 −X、4 −Y、5 −Z(板子絲印的軸)
* 08 磁力信任度 0~255:融合演算法多相信磁力計(越高越好)
* 09 磁場干擾度 0~255:附近有沒有鐵器、馬達干擾(越低越好)
* 朝上的面有變化就印一行;平放(+Z 朝上)時板子內建的 LED 會亮。
*
* 你需要:IMU 陀螺儀(通用 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。把陀螺儀翻來翻去,看哪一面朝上;拿磁鐵靠近看干擾度。
*
* 小提醒:
* 陀螺儀開機時要先放著不動約 0.5 秒,它會自己記住「哪一面朝上」並歸零。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具
#ifndef LED_BUILTIN
#define LED_BUILTIN 2 // 有些 ESP32 板子沒寫內建燈在哪,就用第 2 腳
#endif
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
const int LED = LED_BUILTIN; // 板子內建的燈(UNO 是第 13 腳)
unsigned long lastPrint = 0;
int16_t lastFace = -1; // 上一次朝上的面(-1 = 還沒看過)
void setup() {
Serial.begin(115200);
pinMode(LED, OUTPUT);
mbcBegin();
delay(500);
if (!mbcWaitReady(ADDR, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("準備好了!翻翻看陀螺儀。"));
}
void loop() {
if (millis() - lastPrint >= 300) { // 👉 可以改:每 0.3 秒看一次
lastPrint = millis();
// 一次讀編號 08~0A 共 3 個值:[0] 磁力信任度 [1] 磁場干擾度 [2] 朝上的面
int16_t v[3];
if (!mbcReadMany(ADDR, 0x08, 3, v)) { Serial.println(F("讀不到陀螺儀")); return; }
digitalWrite(LED, v[2] == 2 ? HIGH : LOW); // +Z 朝上(平放)→ 燈亮
if (v[2] != lastFace) { // 朝上的面有變才印
lastFace = v[2];
Serial.print(F("朝上的面:"));
printFace(v[2]);
Serial.print(F(" | 磁力信任度 ")); Serial.print(v[0]);
Serial.print(F(" | 磁場干擾度 ")); Serial.print(v[1]);
if (v[1] > 100) Serial.print(F("(干擾大,附近有鐵器或馬達?)"));
Serial.println();
}
}
delay(50);
}
// 0A 朝上的面:0 +X、1 +Y、2 +Z、3 −X、4 −Y、5 −Z
void printFace(int16_t f) {
switch (f) {
case 0: Serial.print(F("+X(X 軸朝上)")); break;
case 1: Serial.print(F("+Y(Y 軸朝上)")); break;
case 2: Serial.print(F("+Z(平放,正面朝上)")); break;
case 3: Serial.print(F("-X")); break;
case 4: Serial.print(F("-Y")); break;
case 5: Serial.print(F("-Z(翻面,背面朝上)")); break;
default: 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;
}你會看到:(平放,翻面,再立起來)
準備好了!翻翻看陀螺儀。
朝上的面:+Z(平放,正面朝上) | 磁力信任度 230 | 磁場干擾度 12
朝上的面:-Z(翻面,背面朝上) | 磁力信任度 228 | 磁場干擾度 15
朝上的面:+X(X 軸朝上) | 磁力信任度 225 | 磁場干擾度 187️⃣ 小車原地轉 90 度(進階)
做什麼: 開機先送 E0 歸零,然後每 3 秒原地右轉 90 度一次:用 02 融合航向看現在轉到幾度,差很多就轉快、快到了就轉慢,到了就停。轉 4 次回到原來的方向。
另外要準備: 兩輪小車、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。開機時車子不要動(陀螺儀要先記住哪一面朝上)。
/*
* IMU 陀螺儀 × Arduino 範例 7:小車原地轉 90 度(進階)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 開機後先送命令 E0 把航向歸零,然後每 3 秒讓兩輪小車原地右轉 90 度一次:
* 用編號 02 融合航向當「現在轉到幾度」,差距大就轉快、快到了就轉慢,
* 到了就停。轉 4 次就回到原來的方向。
* 從上往下看順時針(右轉)航向變大,所以右轉時目標 = 原本 + 90。
*
* 你需要:IMU 陀螺儀(通用 I2C 版)、Arduino UNO/Nano/UNO R4 或 ESP32。另外要:兩輪小車、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 (右輪)
* L298N 的 GND 要跟 Arduino 的 GND 接在一起!
* (ESP32 的接腳不一樣,請看下面「馬達接腳」)
*
* 準備:
* 1. 陀螺儀要平放固定在車上,開機時車子不要動(它要先記住哪一面朝上)。
* 2. 先把車架高測試:右轉時左輪應該往前、右輪往後。轉反了就改 LEFT_REVERSE/RIGHT_REVERSE。
* 3. 轉過頭就把 TURN_SPEED 調小、或 SLOW_ZONE 調大。
*
* 怎麼看結果:
* 序列埠監控窗(115200)會印目標角度與現在的航向。
*
* 小提醒:
* 想左轉:把 TURN_ANGLE 改成 -90。
*/
#include <Wire.h>
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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 TURN_ANGLE = 90; // 每次轉幾度:正數 = 右轉,負數 = 左轉
const int TURN_SPEED = 150; // 轉彎速度 0~255
const int SLOW_SPEED = 80; // 快到了時的慢速
const int SLOW_ZONE = 25; // 剩不到幾度就改用慢速
const int DONE_ZONE = 2; // 差不到幾度算到了
const unsigned long WAIT_MS = 3000; // 每次轉完等幾毫秒
const bool LEFT_REVERSE = false; // 左輪轉反了改成 true
const bool RIGHT_REVERSE = false; // 右輪轉反了改成 true
long target = 0; // 這一次要轉到的角度
unsigned long doneAt = 0; // 上一次轉完的時間
bool turning = 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(1000); // 多等一下,讓陀螺儀記住哪一面朝上
if (!mbcWaitReady(ADDR, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
mbcWrite(ADDR, 0xE0); // 命令 E0:航向歸零(現在的方向 = 0 度)
delay(100);
Serial.println(F("已歸零。每 3 秒原地轉 90 度一次。"));
doneAt = millis();
}
void loop() {
int16_t heading = mbcRead(ADDR, 0x02); // 02 融合航向(度),右轉變大
if (heading == MBC_NONE) { // 讀不到陀螺儀 → 為了安全,停車
drive(0, 0);
Serial.println(F("讀不到陀螺儀 → 停車"));
delay(50);
return;
}
if (!turning) {
// ---- 休息中:時間到了就設定下一個目標 ----
if (millis() - doneAt >= WAIT_MS) {
target += TURN_ANGLE;
turning = true;
Serial.print(F("開始轉,目標 ")); Serial.print(target); Serial.println(F(" 度"));
}
} else {
// ---- 轉彎中:看還差幾度 ----
long error = target - heading; // 正數 = 還要往右轉,負數 = 要往左(或轉過頭了)
if (abs(error) <= DONE_ZONE) { // 到了
drive(0, 0);
turning = false;
doneAt = millis();
Serial.print(F("到了!現在航向 ")); Serial.println(heading);
} else {
int spd = (abs(error) < SLOW_ZONE) ? SLOW_SPEED : TURN_SPEED; // 快到了就慢一點
if (error > 0) drive(spd, -spd); // 右轉:左輪前進、右輪後退
else drive(-spd, spd); // 左轉
}
}
delay(50);
}
// 控制兩個輪子:速度 -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;
}你會看到:
已歸零。每 3 秒原地轉 90 度一次。
開始轉,目標 90 度
到了!現在航向 89
開始轉,目標 180 度
到了!現在航向 180提示
轉過頭:把
TURN_SPEED調小、SLOW_ZONE調大。想左轉:TURN_ANGLE改成-90。
8️⃣ 校準加速度計與磁力計
做什麼: 在序列埠監控窗打字下命令:1 E1 加速度計校準開始、2 E2 結束(存檔,並自動歸零航向)、3 E3 磁力計校準開始、4 E4 結束(存檔)。校準中 F3 的 bit4/bit5 會是 1,程式每 0.5 秒印一次。
怎麼校準:
- 加速度計:按
1,把陀螺儀六個面各朝上平放幾秒(正面、背面、四個側面),再按2。 - 磁力計:按
3,把陀螺儀朝各個方向慢慢轉(畫 8 字)約 20 秒,遠離鐵器,再按4。
/*
* IMU 陀螺儀 × Arduino 範例 8:校準加速度計與磁力計
* ------------------------------------------------------------
* 這個範例會做什麼?
* 用序列埠監控窗打字下命令,幫陀螺儀做校準(結果會存進它的 EEPROM):
* 1 → E1 加速度計校準開始 2 → E2 加速度計校準結束(存檔,並自動歸零航向)
* 3 → E3 磁力計校準開始 4 → E4 磁力計校準結束(存檔)
* 校準中狀態旗標 F3 的 bit4(加速度計)或 bit5(磁力計)會是 1,程式每 0.5 秒印一次。
* 加速度計校準:按 1 之後把陀螺儀六個面各朝上放平幾秒,再按 2。
* 磁力計校準:按 3 之後把陀螺儀朝各個方向慢慢轉(畫 8 字)約 20 秒,再按 4。
*
* 你需要:IMU 陀螺儀(通用 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,在上面的輸入框打 1~4 再按 Enter。
*
* 小提醒:
* 不確定要不要校準?出廠已經校過;只有航向漂得很快、或朝上的面判斷錯時才需要。
* 校準時遠離馬達、喇叭、鐵桌。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("=== IMU 校準 ==="));
Serial.println(F("輸入 1 = 加速度計校準開始、2 = 結束;3 = 磁力計校準開始、4 = 結束"));
}
void loop() {
// ---- 1. 看電腦有沒有打字過來 ----
if (Serial.available()) {
char ch = Serial.read();
switch (ch) {
case '1': mbcWrite(ADDR, 0xE1); Serial.println(F(">>> E1 加速度計校準開始:六個面各朝上放平幾秒")); break;
case '2': mbcWrite(ADDR, 0xE2); Serial.println(F(">>> E2 加速度計校準結束(存檔,航向歸零)")); break;
case '3': mbcWrite(ADDR, 0xE3); Serial.println(F(">>> E3 磁力計校準開始:慢慢畫 8 字轉約 20 秒")); break;
case '4': mbcWrite(ADDR, 0xE4); Serial.println(F(">>> E4 磁力計校準結束(存檔)")); break;
default: break; // 其他字(例如換行)不理
}
}
// ---- 2. 每 0.5 秒印一次校準狀態(F3 的 bit4、bit5)----
if (millis() - lastPrint >= 500) {
lastPrint = millis();
int16_t st = mbcRead(ADDR, 0xF3);
if (st == MBC_NONE) { Serial.println(F("讀不到陀螺儀")); return; }
Serial.print(F("加速度計校準中 ")); Serial.print((st & 0x10) ? F("是") : F("否")); // bit4
Serial.print(F(" | 磁力計校準中 ")); Serial.print((st & 0x20) ? F("是") : F("否")); // bit5
Serial.print(F(" | 磁力融合已初始化 ")); Serial.print((st & 0x40) ? F("是") : F("否")); // bit6
Serial.print(F(" | 磁力自動校準收斂 ")); Serial.println((st & 0x80) ? F("是") : F("否")); // bit7
}
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;
}你會看到:(輸入 3,轉一轉,再輸入 4)
=== IMU 校準 ===
輸入 1 = 加速度計校準開始、2 = 結束;3 = 磁力計校準開始、4 = 結束
加速度計校準中 否 | 磁力計校準中 否 | 磁力融合已初始化 是 | 磁力自動校準收斂 否
>>> E3 磁力計校準開始:慢慢畫 8 字轉約 20 秒
加速度計校準中 否 | 磁力計校準中 是 | 磁力融合已初始化 是 | 磁力自動校準收斂 否
>>> E4 磁力計校準結束(存檔)
加速度計校準中 否 | 磁力計校準中 否 | 磁力融合已初始化 是 | 磁力自動校準收斂 是注意
出廠已經校準過。只有航向漂得很快、或「朝上的面」判斷錯時才需要重做。
9️⃣ 小車走直線(進階)
注意
程式已在 Nano/UNO R4/ESP32 編譯通過,用到的讀值與命令都實測過;小車控制還沒上車實測,第一次跑先把車架高確認輪子方向。
做什麼: 開機先送 E0 把航向歸零,然後一直往前走。用 02 融合航向看有沒有走歪:修正量 fix = KP × 航向(最多 MAX_FIX),左輪 = BASE_SPEED − fix、右輪 = BASE_SPEED + fix。從上往下看順時針(往右歪)航向變大 → 左輪減速、右輪加速 → 車頭轉回左邊;往左歪則相反。讀不到陀螺儀就停車。
另外要準備: 兩輪小車、L298N 馬達驅動板(或同樣接法的 TB6612)、電池。陀螺儀平放固定在車上(可以直接用範例 7 的小車)。
馬達接線:(跟範例 7 一樣;陀螺儀照最上面的表接)
| 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。開機時車子不要動(陀螺儀要先記住哪一面朝上)。
/*
* IMU 陀螺儀 × Arduino 範例 9:小車走直線(進階)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 開機後先送命令 E0 把航向歸零(現在車頭的方向 = 0 度),然後一直往前走。
* 用編號 02 融合航向看車子有沒有走歪,歪了就讓兩輪速度不一樣,把車頭拉回來:
* 修正量 fix = KP × 航向(最多 MAX_FIX)
* 左輪 = BASE_SPEED − fix 右輪 = BASE_SPEED + fix
* 從上往下看順時針(往右歪)航向變大 → fix 是正的 → 左輪減速、右輪加速 → 車頭往左轉回來。
* 往左歪航向變小(負數)→ fix 是負的 → 左輪加速、右輪減速 → 車頭往右轉回來。
* 讀不到陀螺儀就馬上停車。
*
* 你需要:IMU 陀螺儀(通用 I2C 版)、Arduino UNO/Nano/UNO R4 或 ESP32。另外要:兩輪小車、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 (右輪)
* L298N 的 GND 要跟 Arduino 的 GND 接在一起!
* (ESP32 的接腳不一樣,請看下面「馬達接腳」)
*
* 準備:
* 1. 陀螺儀要平放固定在車上,開機時車子不要動(它要先記住哪一面朝上)。
* 2. 先把車架高測試:兩輪都應該往前轉。轉反了就改 LEFT_REVERSE/RIGHT_REVERSE。
* 3. 車子左右扭來扭去(蛇行)→ KP 調小;歪了拉不回來 → KP 調大。
*
* 怎麼看結果:
* 序列埠監控窗(115200)每 0.2 秒印一次:航向、修正量、左右輪速度。
* 用手把車頭轉歪一點,會看到修正量變大、車子自己轉回 0 度。
*
* 小提醒:
* 想改用 03 陀螺 1D 航向:把 HEADING_ID 改成 0x03(不受磁場影響,旁邊有馬達、鐵桌時比較穩)。
* 不想一直走:把 RUN_MS 改成想走的毫秒數(例如 5000 = 走 5 秒就停)。
*/
#include <Wire.h>
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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 BASE_SPEED = 150; // 往前走的基本速度 0~255
const float KP = 4.0; // 歪 1 度要修正多少速度(越大修得越用力)
const int MAX_FIX = 80; // 修正量最多多少(不讓一邊輪子停下來或倒轉)
const uint8_t HEADING_ID = 0x02; // 用哪一種航向:0x02 融合航向、0x03 陀螺 1D 航向
const unsigned long RUN_MS = 0; // 走幾毫秒就停:0 = 一直走
const bool LEFT_REVERSE = false; // 左輪轉反了改成 true
const bool RIGHT_REVERSE = false; // 右輪轉反了改成 true
unsigned long startAt = 0; // 開始走的時間
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(1000); // 多等一下,讓陀螺儀記住哪一面朝上
if (!mbcWaitReady(ADDR, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
mbcWrite(ADDR, 0xE0); // 命令 E0:航向歸零(現在車頭的方向 = 0 度)
delay(100);
Serial.println(F("已歸零。開始往前走直線。"));
Serial.println(F("航向 修正量 左輪 右輪"));
startAt = millis();
}
void loop() {
// ---- 1. 走夠久了就停(RUN_MS = 0 就一直走) ----
if (RUN_MS > 0 && millis() - startAt >= RUN_MS) {
drive(0, 0);
return;
}
// ---- 2. 讀航向:0 = 車頭正前方,往右歪變大、往左歪變小 ----
int16_t heading = mbcRead(ADDR, HEADING_ID);
if (heading == MBC_NONE) { // 讀不到陀螺儀 → 為了安全,停車
drive(0, 0);
Serial.println(F("讀不到陀螺儀 → 停車"));
delay(50);
return;
}
// ---- 3. 算修正量:歪越多修越多,但不超過 MAX_FIX ----
int fix = (int)(KP * heading);
fix = constrain(fix, -MAX_FIX, MAX_FIX);
int left = BASE_SPEED - fix; // 往右歪(fix 正)→ 左輪慢一點
int right = BASE_SPEED + fix; // → 右輪快一點,車頭轉回左邊
drive(left, right);
// ---- 4. 每 0.2 秒印一次 ----
if (millis() - lastPrint >= 200) {
lastPrint = millis();
char line[40];
snprintf(line, sizeof(line), "%4d %6d %4d %4d", heading, fix, left, right);
Serial.println(line);
}
delay(20);
}
// 控制兩個輪子:速度 -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;
}可以改的參數:
BASE_SPEED(150):往前走的基本速度 0~255。KP(4.0):歪 1 度要修正多少速度,越大修得越用力。MAX_FIX(80):修正量最多多少。HEADING_ID(0x02融合航向;0x03陀螺 1D 航向)。RUN_MS(0 = 一直走):走幾毫秒就停。LEFT_REVERSE/RIGHT_REVERSE:輪子轉反了改成true。
怎麼看結果: 序列埠監控窗選 115200,每 0.2 秒印一行「航向 修正量 左輪 右輪」。用手把車頭轉歪一點,修正量會變大,車子自己轉回 0 度。
常見問題:
- 車子一開始就往一邊衝、越修越歪: 有一個輪子轉反了。把車架高看,兩輪都要往前;改
LEFT_REVERSE/RIGHT_REVERSE。 - 蛇行(左右扭):
KP調小(例如 2.0)。 - 歪了拉不回來:
KP調大,或MAX_FIX調大。 - 旁邊有馬達、鐵桌,方向慢慢偏:
HEADING_ID改成0x03(陀螺 1D 不看磁力計)。
🔟 小車轉到指定的方向(進階)
注意
程式已在 Nano/UNO R4/ESP32 編譯通過,用到的讀值與命令都實測過;小車控制還沒上車實測,第一次跑先把車架高確認輪子方向。
做什麼: 開機送 E0 歸零一次,然後依序轉到 90 → 180 → 0 → −90 → 0 度,每個停 2 秒,轉完停車。「目標 − 現在」是正數就右轉、負數就左轉;差距小於 SLOW_ZONE 改慢速,差 ≤ DONE_ZONE 就停。
另外要準備: 同範例 9(兩輪小車、L298N、電池,陀螺儀平放固定在車上)。
馬達接線:(同範例 7、9)
| 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。開機時車子不要動(陀螺儀要先記住哪一面朝上)。
/*
* IMU 陀螺儀 × Arduino 範例 10:小車轉到指定的方向(進階)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 開機時送命令 E0 把航向歸零一次(現在車頭的方向 = 0 度),之後依序轉到
* 90 → 180 → 0 → -90 → 0 度,每轉到一個就停 2 秒,全部轉完就停車。
* 這些是「絕對角度」:都是跟開機時的車頭方向比,中間不再歸零,所以誤差不會越轉越多。
* 每次用「目標 − 現在」決定怎麼轉:正數 = 右轉、負數 = 左轉;
* 差距大就轉快、快到了就轉慢,差不到 DONE_ZONE 度就停。
* 從上往下看順時針(右轉)航向變大。
*
* 你需要:IMU 陀螺儀(通用 I2C 版)、Arduino UNO/Nano/UNO R4 或 ESP32。另外要:兩輪小車、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 (右輪)
* L298N 的 GND 要跟 Arduino 的 GND 接在一起!
* (ESP32 的接腳不一樣,請看下面「馬達接腳」)
*
* 準備:
* 1. 陀螺儀要平放固定在車上,開機時車子不要動(它要先記住哪一面朝上)。
* 2. 先把車架高測試:右轉時左輪應該往前、右輪往後。轉反了就改 LEFT_REVERSE/RIGHT_REVERSE。
* 3. 轉過頭就把 TURN_SPEED 調小、或 SLOW_ZONE 調大。
*
* 怎麼看結果:
* 序列埠監控窗(115200)會印每一個目標角度、往哪邊轉、到了以後的航向。
* 全部轉完,車頭應該對回開機時的方向。
*
* 小提醒:
* 想走別的路線:改 TARGETS[] 裡的數字(可以超過 360,例如 450 = 先轉一圈再多 90)。
* 跟範例 7 的差別:範例 7 每次「目標 += 90」(相對轉),這裡直接寫「要轉到幾度」(絕對角度)。
*/
#include <Wire.h>
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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 TARGETS[] = {90, 180, 0, -90, 0}; // 依序要轉到的角度(跟開機時的車頭方向比)
const int TURN_SPEED = 150; // 轉彎速度 0~255
const int SLOW_SPEED = 80; // 快到了時的慢速
const int SLOW_ZONE = 25; // 剩不到幾度就改用慢速
const int DONE_ZONE = 2; // 差不到幾度算到了
const unsigned long WAIT_MS = 2000; // 每次轉完等幾毫秒
const bool LEFT_REVERSE = false; // 左輪轉反了改成 true
const bool RIGHT_REVERSE = false; // 右輪轉反了改成 true
const int N_TARGETS = sizeof(TARGETS) / sizeof(TARGETS[0]); // 一共幾個目標
int step = 0; // 現在要轉到第幾個目標(從 0 開始數)
unsigned long doneAt = 0; // 上一次轉完的時間
bool turning = 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(1000); // 多等一下,讓陀螺儀記住哪一面朝上
if (!mbcWaitReady(ADDR, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
mbcWrite(ADDR, 0xE0); // 命令 E0:航向歸零(只在開機做這一次)
delay(100);
Serial.println(F("已歸零(現在的車頭方向 = 0 度)。依序轉到 90 → 180 → 0 → -90 → 0。"));
doneAt = millis();
}
void loop() {
if (step >= N_TARGETS) { // 全部轉完了:停車,什麼都不做
drive(0, 0);
return;
}
int16_t heading = mbcRead(ADDR, 0x02); // 02 融合航向(度),右轉變大
if (heading == MBC_NONE) { // 讀不到陀螺儀 → 為了安全,停車
drive(0, 0);
Serial.println(F("讀不到陀螺儀 → 停車"));
delay(50);
return;
}
int target = TARGETS[step];
if (!turning) {
// ---- 休息中:時間到了就開始轉下一個目標 ----
if (millis() - doneAt >= WAIT_MS) {
turning = true;
Serial.print(F("目標 ")); Serial.print(target); Serial.print(F(" 度,現在 "));
Serial.print(heading); Serial.print(F(" 度 → "));
Serial.println(target - heading > 0 ? F("右轉") : F("左轉"));
}
} else {
// ---- 轉彎中:目標 − 現在 = 還差幾度 ----
long error = (long)target - heading; // 正數 = 還要往右轉,負數 = 要往左(或轉過頭了)
if (abs(error) <= DONE_ZONE) { // 到了
drive(0, 0);
turning = false;
doneAt = millis();
Serial.print(F("到了!現在航向 ")); Serial.println(heading);
step++; // 換下一個目標
if (step >= N_TARGETS) Serial.println(F("全部轉完,停車。按 Arduino 的重置鍵可以再跑一次。"));
} else {
int spd = (abs(error) < SLOW_ZONE) ? SLOW_SPEED : TURN_SPEED; // 快到了就慢一點
if (error > 0) drive(spd, -spd); // 右轉:左輪前進、右輪後退
else drive(-spd, spd); // 左轉
}
}
delay(50);
}
// 控制兩個輪子:速度 -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;
}可以改的參數:
TARGETS[]({90, 180, 0, -90, 0}):依序要轉到的角度,都跟開機時的車頭方向比。TURN_SPEED(150):轉彎速度 0~255。SLOW_SPEED(80):快到了時的慢速。SLOW_ZONE(25):剩不到幾度就改用慢速。DONE_ZONE(2):差不到幾度算到了。WAIT_MS(2000):每次轉完等幾毫秒。LEFT_REVERSE/RIGHT_REVERSE:輪子轉反了改成true。
怎麼看結果: 序列埠監控窗選 115200,會印每個目標、往哪邊轉、到了以後的航向。最後車頭會對回開機時的方向。
常見問題:
- 跟範例 7 有什麼不一樣? 範例 7 每次
target += 90(相對轉:從現在再轉 90 度);範例 10 直接寫「要轉到幾度」(絕對角度,都跟開機方向比)。中間不再歸零,每次都是對「同一個 0 度」修正,所以停下來差 1~2 度也不會一次一次累加;如果每次都歸零再轉,誤差就會越積越多。 - 從 180 轉到 0,為什麼是左轉 180 度,不是右轉? 航向是連續累加的(轉兩圈 = 720),程式照「目標 − 現在」= −180 決定左轉。想要右轉回到正前方,把目標寫成 360。
- 轉過頭、來回晃:
TURN_SPEED調小,或SLOW_ZONE調大。 - 一直轉停不下來: 輪子轉反了(右轉時左輪要往前、右輪往後),改
LEFT_REVERSE/RIGHT_REVERSE。
1️⃣1️⃣ 下命令並確認做完了沒(進階)
做什麼: 不用小車。一步一步示範新命令,每個命令都讀 0B「命令狀態」確認結果:讀目前航向 → E5 [90] 把航向設成 90 度(01/02/03 都約 90)→ E0 歸零 → E7 陀螺零點重新估計(放著不動約 1.5 秒)→ E6 以磁北為 0(02 融合航向 = 目前的磁北方位 0~359)→ 印出命令狀態碼對照表。跑完打任何一個字再按 Enter,可以再跑一次。
另外要準備: 無(陀螺儀照最上面的表接好就可以)。
這個範例用到的命令與編號:
| 編號 | 做什麼 |
|---|---|
E5 [角度](寫) |
把航向設成指定角度(例如 E5 [90]) |
E6(寫) |
以磁北為 0:02 融合航向變成目前的磁北方位(0 北、90 東、180 南、270 西) |
E7(寫) |
陀螺零點重新估計,要放著不動約 1.5 秒 |
0B(讀) |
命令狀態 =(最後一個命令碼 × 256)+狀態,例如 0xE500 = E5 完成、0xE701 = E7 忙碌中 |
F4(讀) |
快照序號:每 10 毫秒或收到命令後 +1 |
注意
這些命令和
0B、F4要 2026-11 以後的新版韌體才有(2026-11-02 Arduino Nano 實測通過)。E5、E6只改 RAM,斷電就沒了,開機時航向一樣從 0 開始。
命令小幫手 mbcCmd(addr, code, p, n, timeoutMs): 先記下 F4,送命令,然後一直讀 F4、0B,等到「F4 比送命令前多 2 以上、0B 高 byte = 這個命令碼、低 byte 不是 1 忙碌」才回傳。回傳 0 完成、1 忙碌(逾時)、2 失敗、3 拒絕、−1 讀不到。要等 F4 是因為同一個命令連送時,剛送完的 0B 可能還是上一次的結果。
/*
* IMU 陀螺儀 × Arduino 範例 11:下命令並確認做完了沒(命令狀態 0B)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 一步一步示範陀螺儀的新命令,每送一個命令就看「命令狀態」(編號 0B)確認結果:
* 1. 讀現在的航向
* 2. E5 [90]:把航向設成 90 度 → 讀 01/02/03 都約 90
* 3. E0:航向歸零 → 都約 0
* 4. E7:陀螺零點重新估計(要放著不動約 1.5 秒)
* 5. E6:以磁北為 0 → 02 融合航向 = 現在車頭朝向的磁北方位(0 北、90 東、180 南、270 西)
* 6. 印出「命令狀態碼對照表」
* 0B = (最後一個命令碼 × 256) + 狀態,例如 0xE500 = E5 完成、0xE701 = E7 忙碌中。
* 狀態:0 完成、1 忙碌(還在做)、2 失敗、3 拒絕(現在不能做)。
*
* 你需要:IMU 陀螺儀(通用 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。陀螺儀平放在桌上、不要碰,看它一步一步印出來。
* 跑完之後打任何一個字再按 Enter,可以再跑一次。
*
* 小提醒:
* E6 要磁力計正常:遠離馬達、喇叭、鐵桌;回「3 拒絕」通常是磁力融合還沒準備好,等幾秒再試,或先跑範例 8 校準磁力計。
* E5、E6 只改 RAM,斷電就沒了;開機時航向一樣是 0。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
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);
mbcBegin();
delay(500);
if (!mbcWaitReady(ADDR, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
delay(2000); // 開機後多等 2 秒,讓磁力融合先準備好(E6 要用)
runDemo();
}
void loop() {
// 打任何字 + Enter → 再跑一次
if (Serial.available()) {
delay(50);
while (Serial.available()) Serial.read(); // 把這一行剩下的字(例如換行)清掉
runDemo();
}
}
// ---- 整個示範流程 ----
void runDemo() {
Serial.println();
Serial.println(F("=== IMU 陀螺儀:新命令與命令狀態 0B ==="));
// ---- 1. 現在的航向 ----
Serial.println(F("[1] 現在的航向"));
printHeadings();
// ---- 2. E5 [90]:航向設成 90 度(參數 int16 小端:先送低 byte 再送高 byte) ----
Serial.println(F("[2] 送 E5 [90]:把航向設成 90 度"));
int16_t angle = 90;
uint8_t p[2] = {(uint8_t)(angle & 0xFF), (uint8_t)((angle >> 8) & 0xFF)};
int st = mbcCmd(ADDR, 0xE5, p, 2, 1000);
printStatus(st);
if (st == 0) {
waitFused(90); // 02 融合航向要等融合重新初始化(最多約 3 秒)
printHeadings();
}
// ---- 3. E0:航向歸零 ----
Serial.println(F("[3] 送 E0:航向歸零"));
st = mbcCmd(ADDR, 0xE0, NULL, 0, 1000);
printStatus(st);
if (st == 0) {
waitFused(0);
printHeadings();
}
// ---- 4. E7:陀螺零點重新估計(要靜止約 1.5 秒,4 秒內湊不到就失敗) ----
Serial.println(F("[4] 送 E7:陀螺零點重新估計。請不要動陀螺儀!"));
for (int i = 3; i >= 1; i--) { // 倒數 3 秒,讓手離開
Serial.print(F(" ")); Serial.print(i); Serial.println(F("…"));
delay(1000);
}
unsigned long t0 = millis();
st = mbcCmd(ADDR, 0xE7, NULL, 0, 5000);
printStatus(st);
Serial.print(F(" 花了 ")); Serial.print(millis() - t0); Serial.println(F(" ms"));
if (st == 2) Serial.println(F(" 失敗 = 這段時間被移動了,放平不要碰再跑一次。"));
// ---- 5. E6:以磁北為 0(完成後 02 融合航向 = 目前的磁北方位 0~359) ----
Serial.println(F("[5] 送 E6:以磁北為 0"));
st = mbcCmd(ADDR, 0xE6, NULL, 0, 4000);
printStatus(st);
if (st == 0) {
delay(50);
int16_t fused = mbcRead(ADDR, 0x02);
int16_t mag = mbcRead(ADDR, 0x04);
Serial.print(F(" 02 融合航向 = ")); Serial.print(fused);
Serial.print(F(" 度(= 現在朝向的磁北方位:0 北、90 東、180 南、270 西) 04 磁力航向 = "));
Serial.println(mag);
} else if (st == 3) {
int16_t f3 = mbcRead(ADDR, 0xF3);
Serial.print(F(" 拒絕:磁力計還沒就緒。F3 bit6 磁力融合已初始化 = "));
Serial.println((f3 != MBC_NONE && (f3 & 0x40)) ? F("是") : F("否"));
Serial.println(F(" (或正在校準)。遠離鐵器、放著等幾秒再試;一直不行就跑範例 8 校準磁力計。"));
} else if (st == 2) {
Serial.println(F(" 失敗:融合 3 秒內沒準備好(磁場干擾太大?),換個地方再試。"));
}
// ---- 6. 對照表 ----
printStatusTable();
Serial.println(F("跑完了。打任何一個字再按 Enter,可以再跑一次。"));
}
// 一次讀 01~03 三個航向印出來
void printHeadings() {
int16_t h[3];
if (!mbcReadMany(ADDR, 0x01, 3, h)) { Serial.println(F(" 讀不到陀螺儀")); return; }
Serial.print(F(" 01 陀螺3D = ")); Serial.print(h[0]);
Serial.print(F(" 02 融合 = ")); Serial.print(h[1]);
Serial.print(F(" 03 陀螺1D = ")); Serial.println(h[2]);
}
// 等 02 融合航向跟上目標(差 10 度以內),最多 4 秒
void waitFused(int target) {
unsigned long start = millis();
while (millis() - start < 4000) {
int16_t f = mbcRead(ADDR, 0x02);
if (f != MBC_NONE && abs(f - target) <= 10) return;
delay(100);
}
}
// 印出狀態碼的意思,還有 0B 原始值
void printStatus(int st) {
Serial.print(F(" → 狀態 ")); Serial.print(st); Serial.print(' ');
switch (st) {
case 0: Serial.print(F("完成")); break;
case 1: Serial.print(F("忙碌(等到逾時還沒做完)")); break;
case 2: Serial.print(F("失敗")); break;
case 3: Serial.print(F("拒絕(現在不能做)")); break;
default: Serial.print(F("讀不到陀螺儀")); break;
}
int16_t b = mbcRead(ADDR, 0x0B);
if (b != MBC_NONE) {
Serial.print(F(" 0B = 0x")); Serial.print((uint16_t)b, HEX);
}
Serial.println();
}
void printStatusTable() {
Serial.println(F("=== 命令狀態碼對照表(0B 的低 byte;高 byte = 命令碼)==="));
Serial.println(F(" 0 完成:命令做完了"));
Serial.println(F(" 1 忙碌:還在做(E6 等磁力融合、E7 等靜止、E8 等擷取安裝軸)"));
Serial.println(F(" 2 失敗:參數錯(少參數、超出範圍、確認碼不對),或做到一半沒成功(例如 E7 被移動)"));
Serial.println(F(" 3 拒絕:現在不能做(校準中不能下 E5~E8、EB;沒在校準卻下 E2/E4/E9;E6 磁力計沒就緒)"));
Serial.println(F(" -1 讀不到:I2C 沒回應,檢查接線(這是範例自己定的,不是陀螺儀回的)"));
}
// ============================================================
// 以下是「通訊小幫手」(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;
}
// ============================================================
// 命令小幫手(範例 11、12 才有):送命令,等「命令狀態」0B 回報結果。
// 0B = (命令碼 << 8) | 狀態;狀態 0 完成、1 忙碌、2 失敗、3 拒絕。
// 注意:同一個命令連送兩次時,剛送完那一下 0B 可能還是「上一次」的結果。
// 所以先記下 F4(快照序號,每 10 ms 或收到命令後 +1,0~255 繞回),
// 等 F4 比送命令前多 2 以上,那時讀到的 0B 一定是這次命令之後的。
// 回傳:0 完成、1 忙碌(等到 timeoutMs 還沒好)、2 失敗、3 拒絕、-1 讀不到/寫不進去
// ============================================================
int mbcCmd(uint8_t addr, uint8_t code, const uint8_t *p, uint8_t n, uint16_t timeoutMs) {
int16_t s0 = mbcRead(addr, 0xF4); // 送命令前的快照序號
if (s0 == MBC_NONE) return -1;
if (!mbcWrite(addr, code, p, n)) return -1;
bool fresh = false; // F4 已經多 2 了嗎?(多了以後的 0B 才採信)
bool gotAny = false; // 有沒有讀到過任何東西
unsigned long start = millis();
while (true) {
int16_t s = mbcRead(addr, 0xF4); // 先讀 F4、再讀 0B(0B 一定不會比 F4 舊)
int16_t b = mbcRead(addr, 0x0B);
if (s != MBC_NONE && b != MBC_NONE) {
gotAny = true;
if ((uint8_t)((uint8_t)s - (uint8_t)s0) >= 2) fresh = true;
uint8_t who = (uint16_t)b >> 8; // 高 byte:最後一個命令碼
uint8_t state = b & 0xFF; // 低 byte:狀態
if (fresh && who == code && state != 1) return state;
}
if (millis() - start >= timeoutMs) return gotAny ? 1 : -1;
delay(10);
}
}怎麼看結果: 序列埠監控窗選 115200,陀螺儀平放、不要碰。正常會看到每一步都有「→ 狀態 0 完成 0B = 0xE500」這樣的一行。
命令狀態碼(0B 低 byte):
| 碼 | 意思 | 例子 |
|---|---|---|
| 0 | 完成 | E0、E5 馬上完成;E7 靜止約 1.5 秒後完成 |
| 1 | 忙碌 | E6 等磁力融合、E7 等靜止、E8 等擷取安裝軸 |
| 2 | 失敗 | 少參數、EA 超出 3500~3700、EB 確認碼不對;E7 4 秒內被移動;E6 3 秒內融合沒好 |
| 3 | 拒絕 | 校準中下 E5~E8、EB;沒在校準卻下 E2/E4/E9;E6 磁力計沒就緒 |
| −1 | 讀不到 | 範例自己定的:I2C 沒回應 |
常見問題:
- E6 回 3 拒絕: 磁力融合還沒初始化(
F3bit6 = 0)或正在校準。遠離馬達、鐵桌,放著等幾秒再跑;一直不行就用範例 8 校準磁力計。 - E7 回 2 失敗: 這段時間被碰到了。放平、手離開再跑。
- E5 之後
02融合航向慢一點才到 90: 融合要重新初始化(最多約 3 秒),程式會等它。 - E5、E6 斷電就沒了嗎? 對,只改 RAM,開機航向一樣從 0 開始。
1️⃣2️⃣ 航向修正係數(進階)
做什麼: 有些陀螺儀轉一圈讀到的不是剛好 360 度。這個範例量出「實際轉一圈讀到幾度」,算出 EA = 每圈度數 × 10,用命令 EA 存進陀螺儀。
另外要準備: 一張有直邊的桌子(或一本書)當對齊線。
步驟:
- 陀螺儀平放,前端對齊桌邊。
- 按 Enter(程式先送
EA 0回到每圈 360.0 並歸零)。 - 從上往下看順時針轉整整 5 圈,對齊回原位。
- 再按 Enter:程式讀
03陀螺 1D 航向、算出 EA,在 3500~3700 之間才寫入。 - 隨時輸入
r再按 Enter,可以把 EA 寫回 0。
換算方向: 轉 5 圈讀到 1750 度 → 每圈 350.0 → EA = 3500。寫入後陀螺儀把讀值乘 360 ÷ 350.0,轉一圈就讀到 360。
注意
命令
EA要 2026-11 以後的新版韌體才有(2026-11-02 Arduino Nano 實測通過)。
下載 imu_12_heading_correction.ino
/*
* IMU 陀螺儀 × Arduino 範例 12:航向修正係數(轉一圈不是剛好 360 度時)
* ------------------------------------------------------------
* 這個範例會做什麼?
* 有些陀螺儀轉整整一圈,讀到的不是剛好 360 度(例如 357 或 363)。
* 這個範例帶你量出「每轉一圈實際讀到幾度」,算出修正係數,用命令 EA 存進陀螺儀:
* 1. 陀螺儀平放、前端對齊桌邊 → 按 Enter
* (程式先送 EA 0 回到「每圈 360.0」,再量,量到的才是沒修正過的原始角度)
* 2. 從上往下看「順時針」轉整整 TURNS 圈(預設 5 圈),前端對齊回同一個桌邊 → 按 Enter
* 3. 讀 03 陀螺 1D 航向累積的角度,算出 EA = 每圈讀到的度數 × 10
* 例:轉 5 圈讀到 1785 度 → 每圈 357.0 度 → EA = 3570
* 4. EA 在 3500~3700 才寫進去(超出就請你重做)
* 寫進去以後,陀螺儀會把讀值放大或縮小成每圈 360 度(讀值 × 360 ÷ 357.0)。
* 隨時輸入 r 再按 Enter:把 EA 寫回 0(回到每圈 360.0,不修正)。
*
* 你需要:IMU 陀螺儀(通用 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,照著畫面的提示做。
* 監控窗右下角選「換行」(或「NL 和 CR 都要」),直接按 Enter 就可以;
* 選「沒有行結尾」的話,請打任何一個字(例如 g)再按 Enter。
*
* 小提醒:
* 寫入後陀螺儀要「停 3 秒」才存進 EEPROM,請等 3 秒以上再拔電;存好以後斷電也記得。
* 改了係數,航向會自動歸零一次。
* 每次量都會先把舊係數清成 0;量完沒寫入(超出範圍、重做)的話,係數就是 360.0。
* 轉的時候慢慢轉、線先繞好(或把 Arduino 一起轉),圈數一定要數對。
*/
#include <Wire.h> // Arduino 內建的 I2C 通訊工具
const uint8_t ADDR = 0x6A; // IMU 陀螺儀的「門牌號碼」(I2C 位址)
const int16_t MBC_NONE = -32768; // 「沒有這個值/讀不到」的代號
// 小幫手的宣告(實作在檔案最下面);mbcWrite 的參數可以省略
bool mbcWrite(uint8_t addr, uint8_t id, const uint8_t *p = NULL, uint8_t n = 0);
// ---- 👉 可以改的參數 ----
const int TURNS = 5; // 要轉幾圈(越多圈越準,最多 80 圈)
const uint16_t EA_MIN = 3500; // 陀螺儀接受的範圍:每圈 350.0 ~ 370.0 度
const uint16_t EA_MAX = 3700;
void setup() {
Serial.begin(115200);
mbcBegin();
delay(500);
if (!mbcWaitReady(ADDR, 3000)) {
Serial.println(F("找不到陀螺儀,請先跑範例 1 檢查接線。"));
while (true) {}
}
Serial.println(F("=== IMU 陀螺儀:航向修正係數 EA ==="));
printMenu();
}
void loop() {
char ch = waitKey(false); // 等使用者按 Enter(或打字)
if (ch == 'r' || ch == 'R') resetCorrection();
else measure();
printMenu();
}
void printMenu() {
Serial.println();
Serial.println(F("按 Enter 開始量;輸入 r 再按 Enter = 把 EA 寫回 0(每圈 360.0)"));
}
// ---- 量一次、算 EA、寫進去 ----
void measure() {
// 步驟 1:對齊
Serial.println(F("步驟 1:陀螺儀平放在桌上,前端(絲印 X 箭頭那端)對齊桌邊,放好不要動,然後按 Enter"));
char ch = waitKey(false);
if (ch == 'r' || ch == 'R') { resetCorrection(); return; }
// 先回到每圈 360.0(不修正),量到的才是原始角度;EA 會順便把航向歸零
int st = sendEA(0);
if (st != 0) { Serial.print(F("EA 0 沒成功,狀態 ")); Serial.println(st); return; }
delay(300);
Serial.print(F("已歸零,03 陀螺 1D 航向 = ")); Serial.println(mbcRead(ADDR, 0x03));
// 步驟 2:轉 TURNS 圈
Serial.print(F("步驟 2:從上往下看「順時針」慢慢轉整整 "));
Serial.print(TURNS);
Serial.println(F(" 圈,前端對齊回同一個桌邊,放好不要動,然後按 Enter"));
waitKey(true); // 等的時候每秒印一次目前角度
delay(300);
int16_t d = mbcRead(ADDR, 0x03); // 03 陀螺 1D 航向:一直累積,轉 5 圈約 1800
if (d == MBC_NONE) { Serial.println(F("讀不到陀螺儀")); return; }
if (d < 0) {
Serial.print(F("讀到 ")); Serial.print(d);
Serial.println(F(" 度(負數):好像轉成「逆時針」了。請從上往下看順時針轉,重做一次。"));
return;
}
// 步驟 3:算 EA = 每圈讀到的度數 × 10(四捨五入)
long ea = ((long)d * 10 + TURNS / 2) / TURNS;
Serial.print(F("轉 ")); Serial.print(TURNS); Serial.print(F(" 圈讀到 ")); Serial.print(d);
Serial.print(F(" 度 → 每圈 ")); Serial.print(ea / 10); Serial.print('.'); Serial.print(ea % 10);
Serial.print(F(" 度 → EA = ")); Serial.println(ea);
if (ea < EA_MIN || ea > EA_MAX) {
Serial.println(F("超出 3500~3700(每圈 350~370 度):可能圈數數錯、沒對齊、或轉太快。不寫入,請重做一次。"));
return;
}
// 步驟 4:寫進陀螺儀
st = sendEA((uint16_t)ea);
if (st == 0) {
Serial.print(F("已寫入 EA = ")); Serial.print(ea);
Serial.println(F("。陀螺儀停 3 秒才存進 EEPROM,請等一下再拔電;航向已歸零。"));
delay(3500);
Serial.print(F("存好了。想檢查的話,開範例 3、輸入 z 歸零,再順時針轉 "));
Serial.print(TURNS); Serial.print(F(" 圈,03 應該讀到約 ")); Serial.println(TURNS * 360L);
} else {
Serial.print(F("寫入失敗,狀態 ")); Serial.println(st);
}
}
// ---- EA 寫回 0 ----
void resetCorrection() {
int st = sendEA(0);
if (st == 0) Serial.println(F("已把 EA 寫回 0(每圈 360.0,不修正),航向已歸零。3 秒後存進 EEPROM。"));
else { Serial.print(F("EA 0 沒成功,狀態 ")); Serial.println(st); }
}
// 送 EA [係數 uint16 小端]:先送低 byte 再送高 byte。回傳 mbcCmd 的狀態
int sendEA(uint16_t v) {
uint8_t p[2] = {(uint8_t)(v & 0xFF), (uint8_t)(v >> 8)};
return mbcCmd(ADDR, 0xEA, p, 2, 1500);
}
// 等使用者按 Enter(或打一個字再按 Enter)。回傳打的第一個字(只按 Enter = '\n')
// showAngle = true 時,等的時候每秒印一次 03 陀螺 1D 航向
char waitKey(bool showAngle) {
while (Serial.available()) Serial.read(); // 先把之前剩下的字清掉
unsigned long last = millis();
while (!Serial.available()) {
if (showAngle && millis() - last >= 1000) {
last = millis();
Serial.print(F(" 現在 03 = ")); Serial.println(mbcRead(ADDR, 0x03));
}
delay(10);
}
char ch = Serial.read();
delay(50);
while (Serial.available()) Serial.read(); // 這一行剩下的字(例如換行)不要
return ch;
}
// ============================================================
// 以下是「通訊小幫手」(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;
}
// ============================================================
// 命令小幫手(範例 11、12 才有):送命令,等「命令狀態」0B 回報結果。
// 0B = (命令碼 << 8) | 狀態;狀態 0 完成、1 忙碌、2 失敗、3 拒絕。
// 注意:同一個命令連送兩次時,剛送完那一下 0B 可能還是「上一次」的結果。
// 所以先記下 F4(快照序號,每 10 ms 或收到命令後 +1,0~255 繞回),
// 等 F4 比送命令前多 2 以上,那時讀到的 0B 一定是這次命令之後的。
// 回傳:0 完成、1 忙碌(等到 timeoutMs 還沒好)、2 失敗、3 拒絕、-1 讀不到/寫不進去
// ============================================================
int mbcCmd(uint8_t addr, uint8_t code, const uint8_t *p, uint8_t n, uint16_t timeoutMs) {
int16_t s0 = mbcRead(addr, 0xF4); // 送命令前的快照序號
if (s0 == MBC_NONE) return -1;
if (!mbcWrite(addr, code, p, n)) return -1;
bool fresh = false; // F4 已經多 2 了嗎?(多了以後的 0B 才採信)
bool gotAny = false; // 有沒有讀到過任何東西
unsigned long start = millis();
while (true) {
int16_t s = mbcRead(addr, 0xF4); // 先讀 F4、再讀 0B(0B 一定不會比 F4 舊)
int16_t b = mbcRead(addr, 0x0B);
if (s != MBC_NONE && b != MBC_NONE) {
gotAny = true;
if ((uint8_t)((uint8_t)s - (uint8_t)s0) >= 2) fresh = true;
uint8_t who = (uint16_t)b >> 8; // 高 byte:最後一個命令碼
uint8_t state = b & 0xFF; // 低 byte:狀態
if (fresh && who == code && state != 1) return state;
}
if (millis() - start >= timeoutMs) return gotAny ? 1 : -1;
delay(10);
}
}可以改的參數:
TURNS(5,最多 80):要轉幾圈,越多圈越準。
怎麼看結果: 序列埠監控窗選 115200。右下角選「換行」就能直接按 Enter;選「沒有行結尾」要先打一個字(例如 g)再按 Enter。轉的時候每秒印一次目前的 03。
常見問題:
- 寫完多久可以拔電? 陀螺儀停 3 秒才存進 EEPROM(值沒變就不寫),等 3 秒以上再拔;存好以後斷電也記得。
- 寫完航向跳回 0? 改係數一定會自動歸零一次,正常。
- 「超出 3500~3700」: 圈數數錯(少轉或多轉一圈會差 70 度以上)、沒對齊,或轉太快。慢慢轉、數清楚再做一次。
- 讀到負數: 轉成逆時針了,要從上往下看順時針轉。
- 量一半不做了,係數變成什麼? 每次量都會先送
EA 0,所以沒寫入就是 360.0(不修正)。 - 想回出廠: 輸入
r(只清航向修正係數);要連加速度計、磁力計校準一起回出廠是EB [A5](這個範例不做)。 - 線會纏住: 線先反方向繞好,或把 Arduino 一起轉。
常見問題
Q:一直出現「找不到陀螺儀」?
- 檢查 SDA、SCL 有沒有接反(UNO:SDA → A4、SCL → A5;R4:SDA/SCL;ESP32:21/22)。
- 檢查 VCC、GND 有沒有插緊。
- 確認是 通用 I2C 版:SPIKE 版、EV3 版的韌體不同,不能直接接 Arduino。
Q:序列埠監控窗一堆亂碼? 速度沒選 115200,在監控窗右下角改好就正常了。
Q:航向放著不動也一直慢慢變?
- 開機時有沒有先放著不動 0.5 秒?沒有就按 Arduino 的 RESET 鍵重來。
- 用
02融合航向(有磁力計修正),不要用01/03。 - 遠離馬達、喇叭、鐵桌;用範例 6 看磁場干擾度。
- 還是漂:跑範例 8 重新校準。
Q:讀到 -32768 是什麼意思?
這是「沒有這個值」的代號(範例裡叫 MBC_NONE):編號是空號(例如 0C~`0F),或是讀不到陀螺儀(線鬆了)。寫程式時先檢查 v != MBC_NONE` 再用。
Q:小幫手裡的 delay(2) 可以拿掉嗎?
陀螺儀本身不需要,但 PS4/PS5 接收器和神攝手(ESP32 做的)一定要,所以 MBC 所有範例一律等 2 毫秒。建議留著。
Q:ESP32 當主機時要注意什麼?
接線用 GPIO 21(SDA)/22(SCL)。小車範例的馬達腳位 ESP32 跟 UNO 不同,程式裡已用 #if defined(ESP32) 分開;ESP32 沒有 LED_BUILTIN 的板子,範例 6 用第 2 腳。
Q:想知道每一個格子放什麼資料? 請看上面的 指令總覽 和 MBC 通用 I2C 協議 v2(給會寫程式的人看的進階資料)。