sensor AEC/AGC
| 要素 | register | 注意 |
|---|---|---|
| 快門時間 | 0x3500–0x3503 | 太長模糊/banding |
| 類比增益 | 0x350A–0x350B | 太高雜訊 |
| 數位增益 | 平台/後處理 | 最後手段 |
v4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=0 # 關自動 v4l2-ctl -d /dev/v4l-subdev0 -c exposure=1000 # 曝光(依驅動單位) v4l2-ctl -d /dev/v4l-subdev0 -c analogue_gain=200
單位與範圍看 --list-ctrls 的 min/max/step。
for r in 0x3501 0x3502 0x3503; do h=${r:2:2}; l=${r:4:2}; echo -n "$r: "; sudo i2ctransfer -y 3 w2@0x3c 0x$h 0x$l r1; done曝光值依感測器位元布局組合(單元 3 的 16-bit 位址規則)。正式流程用驅動/平台控制,這裡只是理解。
v4l2-ctl -d /dev/v4l-subdev0 -c exposure=1000
OV5640 的曝光時間由 0x3500–0x3503 四個 byte 組合,且受 0x3503 的 bit 開關控制:
| Register | 欄位 | 說明 |
|---|---|---|
| 0x3500 | exposure[19:16] | 曝光最高位元 |
| 0x3501 | exposure[15:8] | 中位元組 |
| 0x3502 | exposure[7:0] | 低位元組 |
| 0x3503[1] | manual exposure en | 1 = 手動(AEC 關) |
| 0x3503[2] | manual gain en | 1 = 手動(AGC 關) |
曝光值單位是「感測器 row 週期」(1 line)。AEC 收斂策略:先加曝光到接近幀上限(避免運動模糊),再升增益,最後才用數位增益——這就是「先快門 → 類比 → 數位」鐵則的由來。
for r in 0x3500 0x3501 0x3502 0x3503 0x350a 0x350b; do
h=${r:2:2}; l=${r:4:2}
echo -n "$r: "; sudo i2ctransfer -y 3 w2@0x3c 0x$h 0x$l r1
donev4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=0 v4l2-ctl -d /dev/v4l-subdev0 -c auto_gain=0
v4l2-ctl -d /dev/v4l-subdev0 -c exposure=2000
v4l2-ctl -d /dev/v4l-subdev0 -C exposure # 讀回,應等於 2000# 依序取 exposure=500/2000/6000 三幀,統計平均亮度
for e in 500 2000 6000; do
v4l2-ctl -d /dev/v4l-subdev0 -c exposure=$e
v4l2-ctl -d /dev/video0 --stream-mmap=3 --stream-count=1 --stream-to=e$e.yuv
done| 現象 | 方向 |
|---|---|
| 太暗、調 exposure 無效 | AEC 沒關 / 已到曝光上限 → 補光 |
| 太亮、clipping | 曝光/增益太高或場景反光 → 降曝光 |
| 日光燈下橫條紋 | banding:曝光非 50/60Hz 倍數 → 曝光調成 10ms/8.3ms 倍數 |
| 設定後立刻跳回自動值 | 另一層 3A 在跑(平台/後處理)→ 關所有 auto |
| 亮暗閃爍 | AEC 收斂慢/oscillation → 鎖定曝光改調增益 |
--list-ctrls 的說明;② 只調 exposure 不關 auto_exposure,設了等於沒設;③ banding 時猛拉增益——先對齊電源頻率倍數;④ 在低光硬拉曝光造成 motion blur——先想「要不要補光」。exposure_sweep.sh,自動掃曝光並輸出亮度曲線。場景:Orange Pi 5 Plus(RK3588)接 OV5640,設定 30fps,計算最大可用曝光時間與 frame rate 的 trade-off。
# OV5640 30fps, 1080p: 1125 lines/frame (含 blanking) # row 週期 = 1/30/1125 ≈ 29.6 µs/line # 最大曝光 ≈ 1125 lines (但會降到 ~15fps)
v4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=0
for e in 500 1000 2000 4000 8000; do
v4l2-ctl -d /dev/v4l-subdev0 -c exposure=$e
v4l2-ctl -d /dev/video0 --stream-mmap=3 --stream-count=30 --stream-to=exp${e}.yuv
echo "exposure=$e fps=$(ls -l exp${e}.yuv | awk '{print $5}')"
doneOV5640 的曝光值由 0x3500–0x3503 四個 byte 組合,但位元組序容易搞錯:
| Register | 欄位 | 位元組序 |
|---|---|---|
| 0x3500 | exposure[19:16] | 最高 byte(先送) |
| 0x3501 | exposure[15:8] | 中 high |
| 0x3502 | exposure[7:0] | 中 low |
| 0x3503 | exposure[3:0] + 控制位元 | 最低 byte(後送) |
容易忽略的邊界案例:OV5640 的曝光值是 20-bit(不是 16-bit),最高 4 個 bit 在 0x3500 的 bit[3:0]。如果你只讀 0x3501/0x3502(16-bit),會漏掉最高位元——在長曝光場景下曝光值被截斷到 65535 lines,實際應該是 1048575 lines(20-bit)。
i2ctransfer 只讀 0x3501/0x3502 來判斷曝光值,在長曝光時會看到「曝光設定 8000 但讀回只有 65535」的假象——實際是 20-bit 被截斷。| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
| 設定 exposure=8000 但讀回 65535 | 只讀了 16-bit(0x3501/0x3502),漏掉 20-bit 高位 | 讀全部四個 byte(0x3500–0x3503)組合成 20-bit 值 |
| 日光燈下曝光設 10ms 仍有 banding | 10ms 是 50Hz 倍數但 OV5640 的 banding filter 未啟用 | 設 0x3013[7]=1 啟用 banding filter;或用 8.3ms(60Hz) |
| AEC 收斂後曝光跳動 | AEC 演算法在兩種曝光/增益組合間震盪 | 設 AEC 收斂速度(0x350E)或鎖定增益只調曝光 |
| 低光場景拉增益後雜訊暴增 | 類比增益超過安全上限 | 先算 SNR 預算(單元 13),設增益上限 |
| 曝光設為 0 但畫面仍亮 | AEC 沒關或數位增益在補 | 確認 auto_exposure=0 且 auto_gain=0 |
exposure control 的一致性。把單元 10 的曝光知識做成完整曝光調校專案:為你的應用(固定幀率、日夜模式)訂出曝光/增益上限、驗證 AEC 收斂、建立「場景 → 曝光參數」對照表。這是從「能拍照」走向「曝光可控」的關鍵專案。
python3 - <<'EOF' fps = 30 lines_per_frame = 1125 # 1080p@30fps 含 blanking row_period_us = 1e6 / fps / lines_per_frame max_exposure_lines = lines_per_frame * 0.9 # 留 10% blanking print(f'row 週期 = {row_period_us:.1f} µs') print(f'最大曝光 = {max_exposure_lines:.0f} lines ≈ {max_exposure_lines*row_period_us:.0f} µs') EOF
v4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=1 for i in 1 2 3 4 5; do sleep 1 v4l2-ctl -d /dev/v4l-subdev0 -C exposure v4l2-ctl -d /dev/video0 --stream-mmap=3 --stream-count=1 --stream-to=aec_$i.yuv done
v4l2-ctl -d /dev/v4l-subdev0 -c auto_exposure=0 for e in 500 1000 2000 4000 8000 16000; do v4l2-ctl -d /dev/v4l-subdev0 -c exposure=$e v4l2-ctl -d /dev/video0 --stream-mmap=3 --stream-count=1 --stream-to=exp_$e.yuv done
python3 - <<'EOF'
import numpy as np
for e in [500,1000,2000,4000,8000,16000]:
buf = np.fromfile(f'exp_{e}.yuv', dtype=np.uint8)
y = buf.reshape(720,1280,2)[...,0]
print(f'exposure={e:6d} Y mean={y.mean():6.1f} peak={y.max():3d}')
EOF| 步驟 | 作法 | 通過判據 |
|---|---|---|
| 1. 確認曝光單位 | --list-ctrls 看 exposure 的 min/max/step | 知道單位(lines 或 µs) |
| 2. 驗證手動生效 | 關 auto 設值後讀回 | 讀回與設定一致 |
| 3. 驗證單調性 | 掃描曝光觀察亮度 | 亮度隨曝光單調上升 |
| 4. 頻閃測試 | 日光燈下掃描曝光找 banding | 找出 50/60Hz 安全區間 |
| 5. AEC 收斂 | 場景切換後量收斂時間 | 記錄收斂到穩定所需的幀數 |
| 面向 | Orange Pi | RPi5 | Orin Nano | Thor |
|---|---|---|---|---|
| 曝光 control | V4L2 exposure | ExposureTime(libcamera) | SensorMode exp_time | SensorMode exp_time |
| 單位 | lines(OV) | µs | µs | µs |
| AEC | 感測器內建 | libcamera AE | NVIDIA AE | NVIDIA AE |
| 增益上限 | 自訂(後處理輔助) | libcamera 限制 | NVIDIA 設定 | NVIDIA 設定 |
| 頻閃/banding | 手動控制 | libcamera 支援 | NVIDIA 支援 | NVIDIA 支援 |
- [ ] 我能完成曝光調校專案並產出曝光曲線。 - [ ] 我能換算 lines ↔ µs 的曝光單位。 - [ ] 我已訂出本應用的曝光與增益上限。 - [ ] 我能用掃描找出日光燈下的安全曝光區間。 - [ ] 我理解「先快門 → 增益 → 數位增益」的物理依據。 - [ ] 我能驗證 AEC 收斂並記錄收斂時間。
曝光控制是 ISP 調校的核心。以下是 OV5640 曝光控制的位元級完整序列:
| 步驟 | 暫存器/位址 | 位元欄位 | 操作 | 預期值 |
|---|---|---|---|---|
| 1. 開啟手動 AEC | 0x3503 | [0] AEC manual | i2cset -y 3 0x3c 0x3503 0x08 | 0x08(手動) |
| 2. 設定 exposure high | 0x3500 | [7:4] exposure[19:16] | i2cset -y 3 0x3c 0x3500 0x01 | 依場景 |
| 3. 設定 exposure mid | 0x3501 | [7:0] exposure[15:8] | i2cset -y 3 0x3c 0x3501 0xF4 | 500 lines |
| 4. 設定 exposure low | 0x3502 | [7:0] exposure[7:0] | i2cset -y 3 0x3c 0x3502 0x00 | 0x00 |
| 5. 設定 analog gain | 0x350A | [1:0] gain | i2cset -y 3 0x3c 0x350a 0x00 | 1× gain |
| 6. 回讀驗證 | 0x3503 | [0] | i2cget -y 3 0x3c 0x3503 | 0x08 |
| 7. 取幀驗證 | v4l2-ctl | — | v4l2-ctl --stream-mmap=1 --stream-count=1 | 亮度符合預期 |
決策樹 A:AEC 收斂太慢
AEC 收斂時間異常? ├─ 太慢(>500ms) │ ├─ 增加 AEC 響應速度:0x3A0F/0x3A10 │ └─ 減少收斂範圍:0x3A1D/0x3A1E ├─ 太快(oscillating) │ ├─ 降低響應速度 │ └─ 增加收斂範圍 └─ 不收斂 → AEC 被覆蓋 └─ 檢查 0x3503 是否為 0x00(auto)
決策樹 B:日光燈頻閃
影像有橫向條紋? ├─ 50Hz 頻閃 → 曝光非整數倍 │ ├─ 計算:1/50s = 20ms → 20ms / 29.6µs ≈ 676 lines │ └─ 設定 exposure = 676 × N(N=1,2,3...) ├─ 60Hz 頻閃 → 同理 │ └─ 1/60s = 16.67ms → 563 lines × N └─ 無頻閃 → 正常
| 步驟 | 指令 | 預期輸出 | 判讀標準 |
|---|---|---|---|
| 1. 開啟手動 AEC | i2cset -y 3 0x3c 0x3503 0x08 | 手動模式 | 0x3503 = 0x08 |
| 2. 設定 exposure | i2cset ... 0x3500 0x01; i2cset ... 0x3501 0xF4 | 500 lines | 對應亮度 |
| 3. 設定 gain | i2cset ... 0x350a 0x00 | 1× gain | 最低雜訊 |
| 4. 取幀 | v4l2-ctl --stream-mmap=1 --stream-count=1 | 成功 | 亮度符合預期 |
| 5. 掃描增益上限 | 遞增 0x350a 值 | 雜訊增加 | 訂出增益上限 |
| 6. 測 AEC 收斂 | 切換 auto(0x3503=0x00) | 收斂時間 | <500ms |
| 面向 | Orange Pi | RPi5 | Orin Nano | Thor | 推薦 |
|---|---|---|---|---|---|
| 曝光單位 | lines | µs | µs | µs | 先換算 row 週期 |
| 增益控制 | 0x350A(整數) | AnalogueGain(浮點) | NVIDIA AE | NVIDIA AE | 依平台 |
| AEC | 感測器內建 | libcamera AE | NVIDIA AE | NVIDIA AE | 底層→感測器 |
| 頻閃/banding | 手動控制 | libcamera 支援 | NVIDIA 支援 | NVIDIA 支援 | 底層→手動 |
| 收斂速度 | 可調(register) | tuning 可調 | 封閉 | 封閉 | 學習→Orange Pi |
- [ ] 完成「曝光調校專案」並產出曝光曲線 - [ ] 換算 lines ↔ µs(用 row 週期:1 line ≈ 29.6µs @ 1080p30) - [ ] 訂出本應用的曝光上限(exposure_max lines) - [ ] 訂出類比增益上限(gain_max) - [ ] 測 AEC 收斂時間(< 500ms 為佳) - [ ] 測日光燈頻閃:找安全曝光區間(676 × N lines @ 50Hz) - [ ] 驗證「先快門 → 增益 → 數位增益」的順序 - [ ] 產出「exposure tuning log」(含 gain/exposure/time/quality) - [ ] 確認 AEC 收斂後亮度穩定無 oscillation - [ ] 測試「補光 vs 硬調」取捨並記錄