單元 3 · 感測器通訊介面

I2C/SCCB、register

I2C 與感測器位址

Orin 上感測器經 I2C 控制(GMSL 時透過 deserializer 的 I2C 通道)。OV9281 常見 7-bit 位址 0x36

掃描與讀 ID
sudo i2cdetect -y <bus>          # 依 DTB 的 i2c 匯流排
sudo i2cget -y 0 0x36 0x300a     # OV9281 ID 高位 = 0x92
sudo i2cget -y 0 0x36 0x300b     # = 0x81

GMSL 的 I2C 通道

GMSL 關鍵:感測器資料與 I2C 都經 deserializer(如 MAX96712)。必須先「打通 deserializer → 切到正確 link 的 I2C 通道」,才讀得到感測器。先用 vendor 工具確認 serdes lock。

OV register 慣例

Register功能
0x300A/0x300B感測器 ID
0x3500–0x3503曝光時間
0x350A–0x350B類比增益

3.8 深入:I2C 讀寫的位元組序與 ACK 檢查

OV 感測器用 16-bit register 位址(big-endian):

S0x0x36<<1|0位址高位址低資料/讀P
讀感測器 ID
sudo i2cget -y 0 0x36 0x300a   # 預期 OV9281 ID 高位
排查:讀回全 0xFF → 上拉/位址錯;讀回全 0x00 → 上電/reset 問題。用 i2ctransfer 讀寫並回讀驗證。

3.9 練習

  1. 讀感測器 ID,確認與 datasheet 相符。
  2. 寫一組 register 後回讀驗證。
  3. 記錄 I2C 位址與匯流排編號。
看完這單元你應該能說出:
  • Orin 上 I2C 控制感測器。
  • GMSL 的 I2C 通道概念。
  • OV9281 ID register。
  • 「讀不到 ID」排查。

3.10 深入原理:I2C 傳輸的位元級細節

I2C 一次 transaction:START → 7-bit 位址 + R/W → ACK → 資料位元組 → STOP。感測器位址左移 1 位當作控制位元:0x36 << 1 | 0 = 寫,0x36 << 1 | 1 = 讀。

S0x6C(寫)reg 高reg 低資料P
ACK 檢查:感測器有回應會拉低 SDA(ACK=0);無回應時 SDA 維持高(NACK)。i2cdetect 就是靠「送位址看有沒有 ACK」來列裝置——讀到 NACK 通常代表位址錯或感測器沒上電。

3.11 深入原理:GMSL 的 I2C 位址重映射(address remap)

GMSL 的感測器 I2C 不是直接接在 Orin 的匯流排上,而是經 deserializer 轉送。deserializer 可把感測器位址重映射(remap)到不同匯流排位址,避免多顆同型感測器衝突。

GMSL I2C 除錯順序
# 1. 先用 vendor 工具或直接下 deserializer 指令確認 link lock
sudo i2cget -y 0 0x29 0x0012   # 範例:MAX96712 lock register(示意)
# 2. 確認 remap 後的感測器位址
sudo i2cdetect -y 0
# 3. 才讀感測器 ID
sudo i2cget -y 0 0x36 0x300a
錯誤順序:先讀感測器(跳過 deserializer)→ 全 NACK → 誤判感測器壞。GMSL 下「先 lock、後感測器」是不可違反的順序。

3.12 Worked Example:用 i2ctransfer 讀寫並回讀驗證

i2ctransfer(避免 i2cget 的 16-bit 位址陷阱)
# 讀 OV9281 ID:w2 = 寫 2 位元組位址,r1 = 讀 1 位元組
sudo i2ctransfer -y 0 w2@0x36 0x30 0x0a r1
# 預期回傳 0x92

# 寫入後回讀驗證(例:設曝光高位)
sudo i2ctransfer -y 0 w3@0x36 0x35 0x00 0x50
sudo i2ctransfer -y 0 w2@0x36 0x35 0x00 r1   # 應回 0x50

「寫入 → 回讀 → 比對」是驗證 register 是否真的寫進感測器的唯一方法。寫了讀不回 = 電源/reset/時序問題(回顧單元 6)。

3.13 疑難排解決策樹:讀不到感測器 ID

決策樹
1. i2cdetect 看得到裝置?
   ├─ 看不到 ─┬─ 非 GMSL:位址/上拉/電源
   │          └─ GMSL:先查 serdes lock 與 remap
   └─ 看得到 ┐
2. 讀回全 0xFF?
   ├─ 是 ─→ 位址錯 / 上拉電壓不足
   └─ 否 ┐
3. 讀回全 0x00?
   ├─ 是 ─→ 上電 / reset 未釋放
   └─ 否 ─→ 讀到預期 ID → ✅

3.14 常見錯誤 / 陷阱

陷阱 ①:i2cget 對 16-bit register 位址的處理各工具不同——i2cget -y bus addr reg 的 reg 只送 1 位元組,OV 系 16-bit 位址要用 i2ctransfer 的 w2
陷阱 ②:多顆感測器共用匯流排時位址衝突——用 deserializer remap 或 DTB 指派不同位址。
陷阱 ③:GMSL 上「reset 未釋放」的感測器會吞掉所有 I2C 寫入(像死掉),但讀 ID 又可能是 0x00——先看 reset GPIO 再怪硬體。

3.15 練習

  1. 用 i2ctransfer 讀 OV9281 與 IMX219 的 ID。
  2. 對任一 register 做「寫入 → 回讀 → 比對」。
  3. 若使用 GMSL,寫下你的 remap 流程與 lock 驗證指令。

3.16 進階:SCCB 與 I2C 的相容性

OmniVision 的 register 介面源自 SCCB(Serial Camera Control Bus)——它與 I2C 高度相容(位址 + 資料、START/STOP),差異多在細節(如是否支援重複起始、時脈規格)。實務上 Linux i2c 工具可直接操作,不必特判 SCCB。

常見誤解:有人以為「SCCB 就不能用 i2cget」——實際上 OV 感測器在 Linux 上都走標準 I2C bus,工具照用,只需注意 register 位址寬度(8-bit vs 16-bit)。

3.17 快速參考:I2C 除錯小抄

症狀最可能原因先查
i2cdetect 看不到位址錯 / 沒上電 / GMSL 沒通deserializer lock → remap
讀回全 0xFF上拉不足 / 位址錯電壓、pull-up
讀回全 0x00reset 未釋放 / 上電不完整GPIO 時序
寫了讀不回register 位址寬度錯用 w2 送 16-bit 位址

3.18 看完本單元該記住的三件事

感測器經 I2C 控制,GMSL 下要「先打通 deserializer、再讀感測器」。
驗證 register 的唯一方法:寫入 → 回讀 → 比對。
讀到全 0xFF / 全 0x00 各有不同意涵,別急著怪硬體。

延伸閱讀

3.11 進階真實情境 Worked Example:GMSL 模組的 I2C 與 SCCB 透傳

場景:透過 GMSL deserializer(MAX96712)遠端控制 OV9281 的 register。GMSL 把 I2C 訊號「隧道化」到遠端 serializer + 感測器。

GMSL I2C 透傳驗證
# 1. 確認 deserializer 本身可讀(local I2C)
sudo i2cget -y 0 0x48 0x000d   # MAX96712 device ID register
# 2. 設定 GMSL link 的 I2C remap(把遠端感測器映射到不同位址)
sudo i2cset -y 0 0x48 0x0000 0x36   # link0 remote sensor alias = 0x36
sudo i2cset -y 0 0x48 0x0001 0x10   # link0 remote sensor base = 0x10
# 3. 透過 deserializer 讀遠端 OV9281 的 chip ID
sudo i2cget -y 0 0x36 0x300a   # OV9281 chip ID register → 應回 0x9281
# 4. 若步驟 3 失敗:確認 link lock + 線材
i2cget -y 0 0x48 0x0013   # link0 lock status

設計決策:GMSL 的 I2C 透傳對驅動完全透明——tegracam 驅動以為自己在做 local I2C,實際上經過了 GMSL tunnel。這就是為什麼「除錯第一步是確認 lock」:lock 不通,I2C 就到不了遠端。

3.12 深入原理擴充:SCCB vs I2C 的微妙差異

OmniVision 感測器使用 SCCB(Serial Camera Control Bus),外觀與 I2C 極像但有細微差異:

面向I2CSCCB
時脈線SCL(可被 slave 拉低 Stretch)SCCB_CLK(不支持 stretch)
資料線SDA(雙向)SCCB_DATA(三態,含 acknowledge)
Acknowledge第 9 個 clock 由 slave 拉低SCCB 用 "don't care" 位(bit 8)
相容性標準 I2C 控制器可驅動 SCCBSCCB 設備接 I2C 匯流排通常可行
陷阱:「I2C 讀 OV9281 沒有 ACK」——有時候不是感測器壞了,而是 I2C 控制器的 clock stretch 設定與 SCCB 衝突。Orin 的 I2C 控制器預設有 stretch timeout,若 OV9281 的 SCCB 不 stretch,timeout 條件可能誤觸。解法:降低 I2C 時脈頻率(從 400kHz 降到 100kHz)測試。

3.13 診斷式疑難排解表

症狀可能原因解決方案
i2cget 回傳 -1(Read failed)I2C 匯流排被其他裝置鎖住 或 位址衝突用 i2cdetect 掃描整條 匯流排確認佔用裝置;暫時移除其他 I2C 裝置測試
GMSL 遠端感測器 I2C 讀不到deserializer link 未 lock 或 I2C remap 未設定先讀 deserializer lock register;再設定正確的 remote alias/base
I2C 讀得到 ID 但設定 register 後無效register 位址位元深(8-bit vs 16-bit)設定錯確認感測器的 register addressing 模式(8-bit 位址用 i2cset -y 0 ADDR REG8 VAL;16-bit 用 i2cset -y 0 ADDR REG16 VAL)
I2C 通訊偶爾成功偶爾失敗線材過長 或 pull-up 電阻值不匹配縮短 I2C 線長;確認 pull-up 在 2.2kΩ~4.7kΩ 範圍;降低 I2C 時脈
多顆感測器共享 I2C 匯流排時互相干擾I2C 位址衝突(同型感測器預設位址相同)用 GMSL I2C remap 或 硬體 solder 切換位址腳

3.14 進階挑戰題

  1. 用 `i2cdump` 工具 dump OV9281 的前 256 個 register,與 datasheet 比對,找出哪幾個 register 在上電後有「非預設值」(這些就是 initialization 的關鍵)。
  2. 設計一個「I2C 健康檢查」腳本:依序掃描所有 I2C 匯流排、列出每個位址的裝置類型、標記異常(回傳值不一致的裝置)。
  3. 若你的 GMSL 系統有 3 條 link,畫出完整的 I2C remap 對照表:每條 link 的 serializer 位址、deserializer local alias、感測器 remote base。

3.15 專案級端到端 Worked Example:I2C 工具鏈專案(register 讀寫庫 + 健康檢查)

場景:團隊有多顆感測器要 bring-up,每次手動 i2cget/i2cset 容易出錯。這個專案建立一套可重用的 I2C 工具鏈:① register 讀寫 Python 庫 ② 全匯流排健康檢查 ③ 兩份 register dump 的自動 diff(呼應 6.17 的需求)。

工具鏈骨架
# regctl.py — 包裝 i2ctransfer,支援 8/16-bit 位址
python3 - <<'EOF'
import subprocess
def read16(bus, addr, reg, n=1):
    hi, lo = reg >> 8, reg & 0xFF
    out = subprocess.check_output(
        ['i2ctransfer', '-y', str(bus), f'w2@{addr:x}', f'{hi:x}', f'{lo:x}'] + [f'r{n}'])
    return int(out.strip(), 16)
def write16(bus, addr, reg, val):
    hi, lo = reg >> 8, reg & 0xFF
    vhi, vlo = val >> 8, val & 0xFF
    subprocess.run(['i2ctransfer', '-y', str(bus),
                    f'w4@{addr:x}', f'{hi:x}', f'{lo:x}', f'{vhi:x}', f'{vlo:x}'],
                   check=True)
# 使用:
print(hex(read16(0, 0x36, 0x300A)))   # OV9281 ID → 0x92
write16(0, 0x36, 0x3500, 0x0050)      # 寫曝光高位
EOF
# health_check.sh — 掃全部 bus 並標記異常位址
for bus in 0 1 2 3 4 5 6 7 8; do
  echo "== bus $bus =="
  sudo i2cdetect -y $bus | grep -v "^ " | grep -v "^\s*$"
done

專案輸出:regctl.py(讀寫庫)+ health_check.sh(健康檢查)+ regdiff.py(兩份 dump 的 diff)。之後所有感測器 bring-up 都改用這套工具,避免手動打錯位址。

3.16 量測/驗證 SOP:I2C 通訊驗證

  1. 匯流排可用i2cdetect -y <bus> → 看到預期位址。
  2. 位址格式:確認感測器是 8-bit 還是 16-bit register 位址(OV 系 16-bit 用 w2)。
  3. ACK 檢查:讀回全 0xFF → 位址錯/上拉;全 0x00 → reset/上電;NACK → 裝置不存在。
  4. 寫入回讀:對任一 register「寫入 → 回讀 → 比對」,驗證真的寫入。
  5. GMSL 特例:先確認 deserializer lock,再驗證 remap 後的感測器位址。
  6. 穩定性:連續讀 100 次 ID,記錄失敗率;>1% 需查線材/上拉。
驗收指標:ID 讀回正確、寫入回讀一致、連續 100 次讀取 0 失敗。

3.17 平台間對照:I2C 介面

面向Orin NanoRPi5Orange PiThor
主要 busi2c0 / i2c1 / i2c8…i2c22(CSI)依型號 i2c3…依配置
GMSL I2C remap✅ 常用✅ 常用
SCCB 相容✅(標準 I2C)
工具i2c-tools 全平台相同(i2cdetect / i2cget / i2cset / i2ctransfer)
時脈預設400kHz100kHz100kHz400kHz
重點:I2C 是「平台差異最小」的一層——工具、語法、register 全部共通;唯一要記的是 bus 編號與 GMSL remap 流程。

3.18 互動式檢核清單:I2C 驗收

3.19 Register 位元級完整工作流:OV9281 I2C 讀寫完整範例

步驟操作I2C Transaction預期回傳判斷
1. 掃描匯流排i2cdetectS→0x6C→ACK0x36 出現感測器活著
2. 讀 ID 高位i2ctransferw2@0x36 0x30 0x0a r10x92OV9281 確認
3. 讀 ID 低位i2ctransferw2@0x36 0x30 0x0b r10x81完整 ID
4. 讀曝光高位i2ctransferw2@0x36 0x35 0x00 r1依設定確認目前值
5. 寫曝光高位i2ctransferw3@0x36 0x35 0x00 0x05寫入成功
6. 回讀驗證i2ctransferw2@0x36 0x35 0x00 r10x05Verify OK
完整 I2C 讀寫 + 回讀腳本
#!/bin/bash
# i2c_rw_test.sh — OV9281 完整 I2C 驗證
ADDR=0x36
BUS=0

echo "=== 1. Scan ==="
i2cdetect -y $BUS | grep -c "$ADDR" && echo "Sensor found" || echo "MISSING"

echo "=== 2. Read ID ==="
id_hi=$(i2ctransfer -y $BUS w2@0x$(printf '%x' $ADDR) 0x30 0x0a r1)
id_lo=$(i2ctransfer -y $BUS w2@0x$(printf '%x' $ADDR) 0x30 0x0b r1)
echo "ID = 0x$id_hi$id_lo (expect 0x9281)"

echo "=== 3. Write + Verify ==="
i2ctransfer -y $BUS w3@0x$(printf '%x' $ADDR) 0x35 0x00 0x05
readback=$(i2ctransfer -y $BUS w2@0x$(printf '%x' $ADDR) 0x35 0x00 r1)
[ "0x$readback" = "0x05" ] && echo "VERIFY OK" || echo "VERIFY FAIL: got 0x$readback"

echo "=== 4. Stability Test (100 reads) ==="
fail=0
for i in $(seq 1 100); do
  v=$(i2ctransfer -y $BUS w2@0x$(printf '%x' $ADDR) 0x30 0x0a r1)
  [ "0x$v" != "0x92" ] && fail=$((fail+1))
done
echo "Failures: $fail/100"

3.20 多層疑難排解決策樹

決策樹 A:i2cget 回傳 Read failed
1. i2cdetect 看得到 0x36?
   ├─ 看不到 ─┬─ 非 GMSL:量 SDA/SCL 電位(正常應 ~3.3V)
   │          │          若 0V → 短路或某裝置 hang
   │          └─ GMSL:deserializer lock?
   │                   ├─ 無 → 線材 / 同軸端接 / 電源
   │                   └─ 有 → I2C remap 設定?
   │                           ├─ 否 → 設定 remote alias/base
   │                           └─ 是 → 位址衝突(同型多顆)
   └─ 看得到 ┐
2. 讀回全 0xFF?
   ├─ 是 → I2C pull-up 不足(量電壓)或位址錯
   └─ 否 ┐
3. 讀回全 0x00?
   ├─ 是 → reset GPIO 未釋放 / MCLK 未到
   └─ 否 ┐
4. 讀回其他值?
   ├─ 不一致 → 線材干擾 / pull-up 不匹配
   └─ 一致 → ✅ I2C 正常
決策樹 B:GMSL 遠端 I2C 不通
1. deserializer 本身讀得到?(i2cget 0x48)
   ├─ 無 → deserializer 沒電 / 位址錯
   └─ 有 ┐
2. link lock status?(寄存器 0x0013)
   ├─ 全 0 → 線材 / serializer / GMSL 頻率
   └─ 有 1 ┐
3. I2C remap 已設定?
   ├─ 否 → 設定 alias + source 位址
   └─ 是 ┐
4. 遠端感測器讀得到 ID?
   ├─ 否 → 降低 I2C 時脈(400kHz→100kHz)重試
   └─ 是 → ✅

3.21 量測驗證完整 SOP

  1. 匯流排可用i2cdetect -y <bus> → 看到預期位址(0x36)。
  2. 位址格式確認:OV 系 16-bit 位址 → 用 w2(如 w2@0x36 0x30 0x0a r1)。
  3. ACK 檢查:讀回全 0xFF → pull-up / 位址問題;全 0x00 → reset / 上電問題。
  4. 寫入回讀:對 0x3500 寫入 0x05 → 回讀 → 比對 = 0x05。
  5. GMSL 特例:先讀 deserializer ID(0x48)→ 確認 lock → 設 remap → 讀感測器。
  6. 穩定性:連續讀 100 次 ID,記錄失敗率;> 1% 需查線材/pull-up。
  7. SCCB 兼容性:I2C 時脈從 400kHz 降到 100kHz,重複步驟 4;若問題消失 → clock stretch 衝突。
判讀標準:ID 讀回正確、寫入回讀一致、連續 100 次 0 失敗、100kHz 時脈下穩定。

3.22 四平台终极對照

面向Orin NanoRPi5Orange PiThor推薦
I2C 主要 busi2c0/i2c1/i2c8i2c22(CSI)依型號 i2c3…依配置查 DTB 確認
GMSL I2C remap✅ 常用✅ 常用GMSL 必備
SCCB 相容全平台 OK
I2C 工具i2c-tools 全平台相同統一工具鏈
時脈預設400kHz100kHz100kHz400kHz不穩時降速
Register 位址寬度OV 系 16-bit,IMX 系 8-bit依感測器
回讀驗證write→read→compare 全平台相同統一流程

3.23 完整 Bring-up 專案 Checklist