gf_ai_box/docs/technical_report.md

1617 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# KL630 Host Stream 技術報告
## 目錄
1. [系統概覽](#1-系統概覽)
2. [硬體架構](#2-硬體架構)
3. [軟體管線架構](#3-軟體管線架構)
4. [IMX662 DOL-HDR 雙曝光 ISP](#4-imx662-dol-hdr-雙曝光-isp)
5. [程式碼結構詳解](#5-程式碼結構詳解)
6. [從零開始:新裝置完整部署流程](#6-從零開始新裝置完整部署流程)
7. [常見問題排查](#7-常見問題排查)
8. [關鍵參數速查表](#8-關鍵參數速查表)
9. [STDC 語義分割邏輯詳解](#9-stdc-語義分割邏輯詳解)
10. [下一階段:高爾夫球車警示與煞車控制](#10-下一階段高爾夫球車警示與煞車控制)
11. [藍牙通訊DX-BT24 BLE UART 模組](#11-藍牙通訊dx-bt24-ble-uart-模組)
12. [CAN Bus 通訊MCP2515 SPI 控制器](#12-can-bus-通訊mcp2515-spi-控制器)
13. [事件錄製器Event Recorder](#13-事件錄製器event-recorder)
14. [GPIO 直接存取gpio_devmem](#14-gpio-直接存取gpio_devmem)
15. [STDC 模型重訓練與後處理 Python→C 移植](#15-stdc-模型重訓練與後處理-pythonc-移植)
16. [快照功能:實作 / 事件觸發 / 打包上傳](#16-快照功能實作--事件觸發--打包上傳)
17. [本週進度與下週計畫2026-04-16](#17-本週進度與下週計畫2026-04-16)
---
## 1. 系統概覽
本專案在 Kneron KL630 AI SoC 上實現一個完整的即時視覺 AI 推論管線:
```
IMX662 魚眼攝影機 (DOL-HDR 雙曝光)
KL630 ISP / IFP
(去馬賽克、AWB、AE、色調映射)
VMF 影像管線 (YM12 planar YUV420)
├── Stream 0: 1920×1080 → H.264 → RTSP
└── Stream 1: 724×362 → NPU 推論 (STDC 語義分割)
KL630 NPU (STDC_0520.nef)
語義分割:道路 / 車輛 / 行人
結果疊加 → HDMI (VOC) 或 RTSP 輸出
```
**主要元件版本:**
| 元件 | 版本 / 規格 |
|------|------------|
| SoC | Kneron KL630 (ARMv7-A Cortex-A7) |
| SDK | VMF Vienna SDK 2.5.6 |
| 感測器 | Sony IMX662 1920×1080 DOL-HDR |
| AI 模型 | STDC (Short-Term Dense Concatenate) 語義分割ModelId=32769 |
| 執行環境 | uClibc 1.0.34LD_LIBRARY_PATH=/mnt/flash/vienna/lib |
---
## 2. 硬體架構
### KL630 SoC 功能方塊
```
┌──────────────────────────────────────────────────────────┐
│ KL630 │
│ │
│ MIPI CSI-2 ──► IFP (Image Front-end Processor) │
│ │ Bayer RAW 處理 │
│ │ DOL-HDR 合成 (長短曝光 Bayer → HDR) │
│ ▼ │
│ ISP (Image Signal Processor) │
│ │ AWB / AE / GTR (Global Tone Remap) │
│ │ 輸出YM12 (planar YUV420) │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ VMF (Vatics Media Framework) │ │
│ │ SSM 共享記憶體環形緩衝區 │ │
│ │ Video Encoder (H.264/H.265/MJPEG) │ │
│ │ VOC (Video Output Component/HDMI) │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ NPU (Neural Processing Unit) │
│ │ 執行 .nef 模型 │
│ │ FifoQ 非同步推論佇列 │
│ ▼ │
│ 推論結果回傳 → 主程式後處理 │
│ │
│ /mnt/flash/ ── eMMC flash重開機資料保留 │
│ /dev/shm/ ── 揮發性 RAM shared memory │
└──────────────────────────────────────────────────────────┘
```
### 儲存空間配置
```
/mnt/flash/vienna/
├── kp_firmware_host_stream ← 主程式 binary
├── lib/ ← VMF 動態函式庫
├── demo_rtsp.sh ← RTSP 模式啟動腳本
├── demo_hdmi.sh ← HDMI 模式啟動腳本
├── demo_rtsp_hdmi.sh ← RTSP+HDMI 同時輸出啟動腳本
└── drivers/ ← 硬體 kernel module
├── driver.sh ← 一鍵載入所有驅動腳本
├── vpl_edmc_v6.3.0.2.ko ← EDMC DMA 控制器(必須第一個載入)
├── vpl_dmac_v2.0.0.3.ko
├── vma_ifpe_v1.1.0.0.ko ← IFP 前端處理
├── vma_ispe_v2.0.0.5.ko ← ISP 影像處理
├── vpl_voc_v2.2.0.1.ko ← VOC HDMI 輸出
├── it66121enc_v1.0.0.3.ko ← HDMI encoder
├── IMX662_v1.0.0.1.ko ← 感測器驅動
└── ...(其他 VMA/VPL 模組)
/mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin/
├── ini/
│ ├── host_stream.ini ← 主設定檔
│ ├── demo_rtsp.sh ← 腳本副本(從 vienna/ 複製)
│ ├── demo_hdmi.sh
│ ├── demo_rtsp_hdmi.sh
│ └── fec_calibrate.ini / fec_conf.ini
├── nef/
│ └── STDC_0520.nef ← NPU 模型
└── Resource/
├── VIC/0/imx662_1920x1080_ch0.cfg ← 主感測器設定
├── VIC/1/imx662_1920x1080_ch1.cfg ← Fusion (短曝光通道)
├── AWB/AutoWhiteBalance.ini
└── ISP/0/ 及 ISP/1/
└── pqtable_ispe_Config.cfg
```
> **重要:** `/mnt/flash/etc/` 雖然是 UBIFS 分區的一部分但寫入的變更在斷電後不會持久UBIFS journal 未 flush。寫入後必須執行 `sync` 才能確保資料寫入 flash。`/mnt/flash/vienna/` 和 `/mnt/flash/plus/` 不受此限制。
---
## 3. 軟體管線架構
### 執行緒模型
```
主執行緒 (kp_firmware.c: main)
├── loadConfig() 讀取 host_stream.ini
├── init_video_source() 初始化 VMF_VSRC (IFP + ISP)
├── 建立 FifoQ NPU 推論佇列
├── Thread A: kdp2_host_stream_image_thread
│ │ SSM_Reader → 取得 YUV 影格
│ │ app_hdr_send_inf_cb → 放入 FifoQ
│ └► 餵圖給 NPU
├── Thread B: kdp2_host_update_result_thread
│ └► NPU 結果回傳 → 解析 STDC 輸出 → g_atDrawInfo[]
├── Thread C: kdp2_host_stream_draw_box (DrawBoxEnable=1 時)
│ │ SSM_Reader → DMA 複製 YUV → 在 YUV 上畫框
│ └► SSM_Writer → 送給 H.264 Encoder
├── Thread D: kdp2_host_video_thread
│ └► H.264 Encoder → SRB → rtsps (RTSP server)
└── Thread E: kdp2_host_voc_thread (VOC=1 時)
└► SSM_Reader → VOC (HDMI 輸出)
```
### SSM (Sync Shared Memory) 緩衝區格式
ISP 輸出的每個影格存放在 SSM 緩衝區中,格式為 **YM12 planar YUV420**
```
SSM 緩衝區佈局 (1920×1080 為例)
┌─────────────────────────────────┐ ← buffer 起始
│ VMF_FRAME_INFO_T header │ 偏移 0大小 = VMF_MAX_SSM_HEADER_SIZE (256 bytes)
│ 包含dwOffset[0/1/2] │
│ dwYStride, dwYSize │
│ dwUVSize, dwType... │
├─────────────────────────────────┤ ← dwOffset[0] (通常 = 25616-byte aligned)
│ Y 平面 │ dwYSize = dwYStride × H = 1920 × 1080
│ 1920 × 1080 = 2,073,600 bytes │
│ stride = 1920 (無 padding) │
├─────────────────────────────────┤ ← dwOffset[1] (≠ 256+YSize需 stride 對齊)
│ Cb 平面 │ dwUVSize = dwYStride/2 × H/2 = 960 × 540
│ 960 × 540 = 518,400 bytes │
├─────────────────────────────────┤ ← dwOffset[2]
│ Cr 平面 │ 同 Cb 大小
│ 960 × 540 = 518,400 bytes │
└─────────────────────────────────┘
重要dwOffset[1] 由 ISP 計算含對齊 padding
不等於 VMF_MAX_SSM_HEADER_SIZE + dwYSize。
所有 DMA 操作必須使用 dwOffset[0/1/2],不可自行計算。
```
---
## 4. IMX662 DOL-HDR 雙曝光 ISP
### 4.1 為什麼 IMX662 需要特殊設定
IMX662 是 Sony 的 StarVis 2 感測器,支援 **DOL-HDR (Digital Overlap High Dynamic Range)** 模式。在此模式下,感測器在同一條 MIPI 資料流中交錯輸出兩次曝光的 Bayer 資料:
```
MIPI 資料流內容 (DOL-HDR)
┌──────────────────────────────────────────┐
│ 列 0 (短曝光 Bayer) ← 亮部細節 │
│ 列 1 (長曝光 Bayer) ← 暗部細節 │
│ 列 2 (短曝光 Bayer) │
│ 列 3 (長曝光 Bayer) │
│ ... │
└──────────────────────────────────────────┘
IFP 接收後格式標識VMF_FRAME_FORMAT_FUSION_BAY = 31
```
這個格式讓 ISP 能夠合成高動態範圍影像——長曝光捕捉暗部,短曝光捕捉亮部,合成後得到超過單次曝光能表現的動態範圍。
### 4.2 雙曝光模式下 AWB 的問題
AWB (Auto White Balance) 需要分析影像的色彩統計數據才能決定色溫增益。對於 DOL-HDR 感測器ISP 的 AES (Auto Exposure Statistics) 模組必須同時處理兩個曝光通道的統計數據。
**錯誤模式(使用 NORMAL 模式):**
```
程式碼vsrc_initopt.eAppMode = VMF_VSRC_APP_MODE_NORMAL
→ IFP 嘗試用單通道統計模式處理雙通道 Bayer 資料
→ VSRC_AES_GetViBuffer() 持續失敗,錯誤碼 0x801C000B
→ AWB 無法取得有效統計數據
→ AWB 卡在冷啟動初始增益 (Cr 增益偏高)
→ 輸出影像持續偏粉紅色
```
**正確模式(使用 FUSION 模式):**
```
程式碼vsrc_initopt.eAppMode = VMF_VSRC_APP_MODE_FUSION
→ IFP 使用雙曝光統計模式 (dwStatisticsSrcType = 2)
→ AES 成功取得兩個通道的統計數據
→ AWB 正常收斂,色溫 Cr/Cb 趨近 128 (中性)
→ 輸出色彩正常
```
### 4.3 啟用 Fusion 模式的完整條件
Fusion 模式由三個層面共同決定,**三個必須同時正確**
#### 層面一INI 設定 (`host_stream.ini`)
```ini
[sensor]
sensor_cfg = "./Resource/VIC/0/imx662_1920x1080_ch0.cfg" ; 主通道(長曝光)
fusion_cfg = "./Resource/VIC/1/imx662_1920x1080_ch1.cfg" ; Fusion 通道(短曝光)
```
`fusion_cfg` 這一行**不可被注解或刪除**。程式碼邏輯:
```c
// kp_firmware.c → loadConfig()
tmp = iniparser_getstring(ini, "sensor:fusion_cfg", NULL);
if (tmp)
pHostStreamInit->pszFusionConfigPath = strdup(tmp);
// kdp2_host_stream.c → init_video_source()
if (pHostStreamInit->pszFusionConfigPath) {
tFrontConfig.dwSensorConfigCount = 2;
tFrontConfig.apszSensorConfig[0] = pHostStreamInit->pszSensorConfigPath;
tFrontConfig.apszSensorConfig[1] = pHostStreamInit->pszFusionConfigPath;
vsrc_initopt.eAppMode = VMF_VSRC_APP_MODE_FUSION; // ← 關鍵
}
```
#### 層面二ISP AWB 統計設定 (裝置端 flash)
```
檔案:$BIN_DIR/Resource/AWB/AutoWhiteBalance.ini
必須設定dwStatisticsSrcType = 2
0 = 單曝光2 = 雙曝光 DOL-HDR
```
此設定告訴 AWB 演算法從哪個統計來源取色彩數據。設為 0 時,即使 eAppMode=FUSIONAWB 仍然只看單通道統計,無法正確收斂。
#### 層面三ISP 色調映射設定 (裝置端 flash)
```
檔案:$BIN_DIR/Resource/ISP/0/pqtable_ispe_Config.cfg
$BIN_DIR/Resource/ISP/1/pqtable_ispe_Config.cfg
必須設定bGTREnable = 1
GTR = Global Tone Remapping全域色調重映射
```
GTR 是 HDR 到 SDR 的色調壓縮演算法。DOL-HDR 合成的高動態範圍影像若不經過 GTR 壓縮,在標準顯示器上亮部會過飽和,色彩失真。
### 4.4 三個層面的關係圖
```
┌──────────────────────────────┐
INI: fusion_cfg ───►│ eAppMode = FUSION │
│ dwSensorConfigCount = 2 │
│ 提供長短曝光兩組 .cfg 給 IFP │
└──────────────┬───────────────┘
┌──────────────────────────────┐
Resource: AWB ─────►│ dwStatisticsSrcType = 2 │
│ AES 模組使用雙通道統計 │
│ AWB 收斂 → 色溫正確 │
└──────────────┬───────────────┘
┌──────────────────────────────┐
Resource: ISP ─────►│ bGTREnable = 1 │
│ GTR 壓縮 HDR→SDR │
│ 亮度/色彩還原正常 │
└──────────────┬───────────────┘
✅ 色彩正確的 YM12 影像
```
---
## 5. 程式碼結構詳解
### 5.1 原始碼目錄
```
kl630_build/
├── compile.sh ← 交叉編譯腳本 (ARM armv7-a)
├── ini/
│ └── host_stream.ini ← 主設定檔(部署時下載至裝置)
├── src/
│ ├── host_stream/
│ │ ├── kp_firmware.c ← main()loadConfig(),執行緒管理
│ │ ├── kdp2_host_stream.c ← 核心管線影像執行緒、畫框、VOC
│ │ ├── application_init.c ← NPU job dispatch (STDC_JOB_ID=200)
│ │ └── app_header_init.c ← FifoQ 封包封裝
│ ├── app_flow/
│ │ └── demo_customize_inf_single_model.c ← 推論回呼
│ ├── pre_post/
│ │ └── stdc_post_process.c ← STDC 後處理(分割圖解析)
│ └── stdc/
│ ├── fec_api.c ← FEC 魚眼校正 API
│ ├── stat_shim.c ← stat64 shim交叉編譯相容
│ └── glibc_shim.c ← glibc ABI 相容層
├── lib/ ← 裝置端 .so 函式庫(從裝置複製)
├── tools/device/
│ ├── deploy.sh ← 一鍵部署腳本
│ ├── demo_rtsp.sh ← 裝置端 RTSP 啟動腳本
│ └── demo_hdmi.sh ← 裝置端 HDMI 啟動腳本
└── SDK/ ← Kneron VMF SDK 原始碼(參考用)
```
### 5.2 kp_firmware.c — 入口點
```
main()
├── loadConfig(HOST_STREAM_CONFIG_PATH)
│ ├── 讀取 [sensor] sensor_cfg, fusion_cfg, fec_mode ...
│ ├── 讀取 [nnm] ModelPath, ModelId, JobId, InferenceStream ...
│ ├── 讀取 [streamer] StreamCount ...
│ └── 讀取 [voc] → g_dwVocPixFmt = YM12 (hardcoded)
├── VMF_NNM_Fifoq_Manager_Init() ← NPU 推論佇列初始化
├── VMF_NNM_Inference_App_Init() ← 載入 .nef 模型
├── kdp2_host_stream_init(pHostStreamInit)
│ ├── init_video_source()
│ │ ├── loadFECConfig() ← 魚眼校正參數
│ │ ├── tFrontConfig.apszSensorConfig[0/1]
│ │ ├── eAppMode = FUSION / NORMAL
│ │ └── VMF_VSRC_Init() + VMF_VSRC_Start()
│ │
│ └── 啟動各執行緒
└── 等待 SIGINT/SIGTERM 清理退出
```
### 5.3 kdp2_host_stream.c — 核心執行緒
#### 影像採集執行緒 (`kdp2_host_stream_image_thread`)
```c
// 核心迴圈
SSM_Reader_ReturnReceiveNewestBuff(ptSsmHandle, &ssm_buf, eImageBufMode);
VMF_VSRC_SSM_GetInfo(ssm_buf.buffer, &vsrc_ssm_info);
// 取得 YUV 影格,交給 NPU FifoQ
app_hdr_send_inf_cb(buf_addr, ..., ssm_buf.buffer, &vsrc_ssm_info);
```
#### 畫框執行緒 (`kdp2_host_stream_draw_box`)
此執行緒在 DrawBoxEnable=1 時啟動,用 DMA 複製 YUV 影格後疊加邊界框:
```c
// 正確的 DMA 目標位址(使用 SSM header 中的對齊偏移)
dma_addr.pbyDstYPhysAddr = buffer_phys + vsrc_ssm_reader_info.dwOffset[0];
dma_addr.pbyDstCbPhysAddr = buffer_phys + vsrc_ssm_reader_info.dwOffset[1];
dma_addr.pbyDstCrPhysAddr = buffer_phys + vsrc_ssm_reader_info.dwOffset[2];
// ↑ 關鍵:不可用 VMF_MAX_SSM_HEADER_SIZE + dwYSize
// 因為 dwOffset[1] 含有 stride 對齊的 padding數值不同。
```
### 5.4 application_init.c — NPU Job 分派
```c
switch (job_id) {
case STDC_JOB_ID: // = 200
stdc_inference(job_id, ...);
break;
// 其他 job ID...
}
```
### 5.5 host_stream.ini — 關鍵設定說明
```ini
[sensor]
sensor_cfg = "./Resource/VIC/0/imx662_1920x1080_ch0.cfg"
fusion_cfg = "./Resource/VIC/1/imx662_1920x1080_ch1.cfg" ; ← 必須!啟用 DOL-HDR
fec_mode = 0 ; 0=直通(不做魚眼校正),其他值啟用 FEC
[nnm]
ModelPath = "nef/SDTC_0520.nef"
ModelId = 32800 ; STDC segmentation model id
JobId = 212 ; STDC_JOB_ID對應 application_init.c
InferenceStream = 1 ; 使用 stream1 (724×362) 做推論
; RTSP+HDMI 同時模式必須設為 0否則 SIGSEGV
Fps = 25
DrawBoxEnable = 1 ; 必須為 1才會啟動畫框執行緒與語義分割疊加
DrawOnResize = 0 ; InferenceStream=0 時必須設為 1在 resize stream 畫框)
[streamer]
StreamCount = 2 ; 開啟 stream0 (1920×1080) 和 stream1 (724×362)
; HDMI 單獨模式設為 1
[stream0]
Width = 1920 Height = 1080 FPS = 25 Bitrate = 2000000
[stream1]
Width = 724 Height = 362 FPS = 25 Bitrate = 2000000
[voc]
voc_enable = 1 ; 0=關閉 HDMI1=啟用
VocWidth = 1920 VocHeight = 1080
PixFmt = NV12 ; 注意:此值被程式碼忽略,實際固定為 YM12
```
### 5.6 三種輸出模式的 INI 差異
| 模式 | voc_enable | StreamCount | InferenceStream | DrawOnResize |
|------|-----------|-------------|----------------|--------------|
| RTSP only | 0 | 2 | 1 | 0 |
| HDMI only | 1 | 1 | 0 | 1 |
| RTSP + HDMI | 1 | 2 | **0** (必須!) | 1 |
> `voc_enable=1 + StreamCount=2 + InferenceStream=1` 會造成 **SIGSEGV** crash因此 RTSP+HDMI 模式必須將 InferenceStream 設為 0並用 DrawOnResize=1 讓畫框作用在 resize stream 上。
---
## 6. 從零開始:新裝置完整部署流程
### 步驟 0前置準備 — 準備開發環境
**在 Host PC 上Windows + Docker**
```bash
# 1. 確認 Docker 安裝並有 ARM 交叉編譯環境
docker run --rm arm-compiler:latest arm-linux-gnueabihf-gcc --version
# 2. 確認 lib/ 目錄有裝置端函式庫(從裝置複製或 SDK 提供)
ls kl630_build/lib/
# 應包含libvmf.so, libmembroker.so, libmsgbroker.so,
# libsyncringbuffer.so, libiniparser.so,
# libvmf_nnm.so, libkutils.so, libapp_yolo.so,
# libuClibc-1.0.34.so 等
# 3. 確認 SDK headers 路徑
ls kl630_build/SDK/sdk/vtcs_root_vienna/include/vmf/
# 應包含video_source.h, video_encoder.h, ssm_info.h 等
```
### 步驟 1編譯 Binary
```bash
# 在 Host PC 上,於 Docker 容器內執行
docker run --rm \
-v "C:/Users/hipe5/Desktop/kl630_build:/workspace/kl630_build" \
kl630-dev \
bash /workspace/kl630_build/compile.sh
# 編譯成功後輸出:
# === SUCCESS: /workspace/kl630_build/build/kp_firmware_host_stream ===
# 並顯示 patchelf 修補後的 NEEDED 清單(應無 ld-linux-armhf.so.3
```
**compile.sh 做了什麼:**
1.`arm-linux-gnueabihf-gcc` 編譯所有 `.c``.o`
2. Link 成 ELF binaryrpath 指向 `$ORIGIN/lib`
3.`patchelf` 將 dynamic linker 從 glibc 改為 uClibc
4.`patchelf``libc.so.6``libc.so.0`uClibc soname
### 步驟 2架設 HTTP 伺服器Host PC
```bash
# 在 kl630_build/build/ 目錄下架設 HTTP server
cd kl630_build/build/
cp ../ini/host_stream.ini . # 複製最新 INI
python -m http.server 8080
# 或 Windows 版
python -m http.server 8080
```
確認裝置能連到 Host PC
- Host PC IP`192.168.3.1`(透過 USB 網路介面)
- Port`8080`
- 提供檔案:`kp_firmware_host_stream``host_stream.ini`
### 步驟 3連線裝置並執行一次性 ISP 設定
```bash
# SSH 連入裝置USB 網路或序列埠)
ssh root@192.168.3.10
# 在裝置上,首次只需執行一次:
sh /tmp/deploy.sh --setup
```
或手動執行以下指令(效果相同):
```sh
BIN_DIR=/mnt/flash/plus/kp_firmware/kp_firmware_0/kp_firmware/bin
# 修正 1AWB 雙曝光統計模式
sed -i 's/dwStatisticsSrcType = 0/dwStatisticsSrcType = 2/' \
$BIN_DIR/Resource/AWB/AutoWhiteBalance.ini
# 修正 2啟用 GTR 色調映射(兩個 ISP 通道)
sed -i 's/bGTREnable = 0/bGTREnable = 1/' \
$BIN_DIR/Resource/ISP/0/pqtable_ispe_Config.cfg
sed -i 's/bGTREnable = 0/bGTREnable = 1/' \
$BIN_DIR/Resource/ISP/1/pqtable_ispe_Config.cfg
# 確認
grep dwStatisticsSrcType $BIN_DIR/Resource/AWB/AutoWhiteBalance.ini
grep bGTREnable $BIN_DIR/Resource/ISP/0/pqtable_ispe_Config.cfg
```
> **這個步驟只需要在新裝置上執行一次。**
> 所有變更寫入 `/mnt/flash/`eMMC重開機後永久保留。
### 步驟 4部署新 Binary每次重新編譯後執行
```bash
# 在裝置上
wget -q http://192.168.3.1:8080/deploy.sh -O /tmp/deploy.sh
sh /tmp/deploy.sh
```
`deploy.sh` 自動執行:
```
1. killall -9 kp_firmware_host_stream
2. killall -9 rtsps
3. rm -f /dev/shm/*
4. wget kp_firmware_host_stream → /mnt/flash/vienna/kp_firmware_host_stream
5. wget host_stream.ini → $BIN_DIR/ini/host_stream.ini
6. cd $BIN_DIR && sh ./ini/demo_rtsp.sh
```
### 步驟 5驗證執行
```bash
# 在裝置上確認程式執行
ps | grep kp_firmware
ps | grep rtsps
# 查看啟動 log
# 應看到:
# [NNM] fusion_cfg: ./Resource/VIC/1/imx662_1920x1080_ch1.cfg
# [init_video_source] Fusion Mode
# [VOC] PixFmt: YM12 (0x324D5930)
# 用 VLC 或 ffplay 播放 RTSP
# rtsp://192.168.3.10/live1.sdp
```
### 步驟 6確認色彩正常
| 現象 | 可能原因 | 解法 |
|------|---------|------|
| 粉紅色影像 | fusion_cfg 沒設定 | 檢查 host_stream.ini 第二行有無 `fusion_cfg` |
| 粉紅色影像 | dwStatisticsSrcType = 0 | 重新執行 `deploy.sh --setup` |
| 粉紅色影像 | bGTREnable = 0 | 重新執行 `deploy.sh --setup` |
| 色彩正常但有條紋 | DMA 畫框位址錯誤 | 已修正,重新編譯部署即可 |
---
## 7. 常見問題排查
### 7.1 `VSRC_AES_GetViBuffer() failed: 801C000B`
```
問題log 出現此錯誤且持續出現(每幀)
原因eAppMode = NORMALAES 無法處理 FUSION_BAY (fmt=31) 格式
解法:確認 host_stream.ini 有 fusion_cfg 這一行且未被注解
```
### 7.2 Stack Smashing 崩潰
```
問題:*** stack smashing detected *** : Aborted
原因stat64/fstat64 符號在 uClibc 上與 glibc 不相容
解法:已在 src/stdc/stat_shim.c 加入 shim確認編譯時包含此檔案
```
### 7.3 程式啟動後立即退出
```
問題VMF_VSRC_Init() 回傳 NULL
檢查清單:
1. LD_LIBRARY_PATH=/mnt/flash/vienna/lib 是否設定
2. /dev/shm/ 是否清空(執行 rm -f /dev/shm/*
3. 舊的 kp_firmware_host_stream 是否已 kill
4. Resource/ 目錄下的設定檔是否存在
```
### 7.4 RTSP 連不上
```
問題VLC 打開 rtsp://192.168.3.10/live1.sdp 失敗
檢查:
1. demo_rtsp.sh 中 sleep 4 後 rtsps 是否已啟動
→ ps | grep rtsps
2. stream_server_config.ini 是否存在於 $BIN_DIR
3. venc_srb_* 共享記憶體是否存在
→ ls /dev/shm/
```
### 7.5 HDMI 無輸出
```
問題voc_enable = 1 但 HDMI 無畫面
檢查:
1. VocWidth/VocHeight 是否正確設定
2. ISP 輸出格式:固定為 YM12程式碼已 hardcode
3. HDMI 顯示器是否支援 1920×1080
```
### 7.6 `[MemMgr][Error]: Open EDMC device fail !!`
```
問題firmware 啟動後立即出現此錯誤並退出
原因:硬體 kernel module 尚未載入,/dev/vpl_edmc 不存在
EDMC = Enhanced DMA Controller由 vpl_edmc_v6.3.0.2.ko 提供)
排查:
strace 確認:
open("/dev/vpl_edmc", O_RDWR) = -1 ENOENT (No such file or directory)
解法:
cd /mnt/flash/vienna/drivers && sh driver.sh 2>/dev/null
然後重新執行 firmware
注意:
- /lib/modules/ 中有兩個版本Godshand.ko 和 Godshand_v2.1.0.1.ko
系統預設載入的是 Godshand.ko兩者皆不建立 /dev/vpl_edmc
vpl_edmc 必須由 driver.sh 明確載入 vpl_edmc_v6.3.0.2.ko 才會建立
- driver.sh 使用相對路徑 insmod必須從 drivers/ 目錄執行
- demo_rtsp.sh / demo_hdmi.sh / demo_rtsp_hdmi.sh 已內建自動檢查:
[ -e /dev/vpl_edmc ] || (cd /mnt/flash/vienna/drivers && sh driver.sh 2>/dev/null && sleep 1)
```
### 7.7 Write Autostart 後重開機無效
```
問題:透過 web UI Write Autostart 後,重開機 firmware 沒有自動啟動
原因:/mnt/flash/etc/ 的寫入在斷電重開後會消失
UBIFS journal 未 flush 到 flash
解法:
web_serve.py 的 api_autostart_write 在寫完 rc.local 後自動執行 sync
手動寫入時必須在斷電前執行sync
開機自動啟動原理:
/etc/init.d/rcS → S95done → insmod /lib/modules/Godshand.ko
→ sh /mnt/flash/etc/rc.local若存在
rc.local 內容:
cd /mnt/flash/vienna/drivers && sh driver.sh 2>/dev/null ← 載入硬體驅動
sleep 2
cd /mnt/flash/vienna
sleep 3
nohup sh /mnt/flash/vienna/demo_*.sh > /tmp/fw.log 2>&1 &
```
---
## 8. 關鍵參數速查表
### 每次部署需確認的 INI 設定
| 參數 | 位置 | 必要值 | 說明 |
|------|------|--------|------|
| `fusion_cfg` | `[sensor]` | `./Resource/VIC/1/...` | DOL-HDR 必須,不可注解 |
| `ModelPath` | `[nnm]` | `nef/SDTC_0520.nef` | STDC 模型路徑 |
| `ModelId` | `[nnm]` | `32800` | STDC segmentation model id |
| `JobId` | `[nnm]` | `212` | 對應 `STDC_JOB_ID` |
| `InferenceStream` | `[nnm]` | `1`RTSP/ `0`HDMI/RTSP+HDMI | 推論用 stream 索引 |
| `DrawBoxEnable` | `[nnm]` | `1` | 必須,否則語義分割疊加不會執行 |
| `DrawOnResize` | `[nnm]` | `1`InferenceStream=0 時) | 在 resize stream 上畫框 |
| `StreamCount` | `[streamer]` | `2`RTSP/ `1`HDMI only | stream 數量 |
### 裝置端 flash 一次性設定
| 參數 | 檔案 | 必要值 | 說明 |
|------|------|--------|------|
| `dwStatisticsSrcType` | `AWB/AutoWhiteBalance.ini` | `2` | 雙曝光統計模式 |
| `bGTREnable` | `ISP/0/pqtable_ispe_Config.cfg` | `1` | 色調映射開啟 |
| `bGTREnable` | `ISP/1/pqtable_ispe_Config.cfg` | `1` | 色調映射開啟 |
### 編譯關鍵 Flags
| Flag | 說明 |
|------|------|
| `-DVATICS_PLATFORM` | VMF SDK 平台識別 |
| `-DKL630` | KL630 特定程式碼路徑 |
| `-march=armv7-a -mfpu=neon -mfloat-abi=hard` | Cortex-A7 + NEON |
| `-Wl,--dynamic-linker,/lib/ld-uClibc.so.1` | 指定 uClibc dynamic linker |
| patchelf: `libc.so.6``libc.so.0` | glibc soname → uClibc soname |
---
## 9. STDC 語義分割邏輯詳解
本節說明目前 `stdc_post_process.c``app_header_init.c` 實作的完整判斷邏輯,作為下一階段警示/煞車控制的輸入基礎。
### 9.1 分割類別定義
STDC 模型輸出 128×64 的像素分類圖,每個像素屬於以下 8 個類別之一:
| 索引 | 類別 | 說明 |
|------|------|------|
| 0 | `BUNKER` | 沙坑 |
| 1 | `CAR` | 車輛 |
| 2 | `GRASS` | 草地 |
| 3 | `GREENERY` | 植被(灌木、雜草等) |
| 4 | `PERSON` | 行人 |
| 5 | `POND` | 水塘 |
| 6 | `ROAD` | 道路/路徑 |
| 7 | `TREE` | 樹木 |
每個類別都計算全域佔比 `class_ratio[ci]`(該類像素數 ÷ 總像素數)。
### 9.2 感興趣區域ROI定義
#### 碰撞 ROICollision ROI
以影像分割圖比例定義,判斷是否有危險物體進入正前方區域:
```
左邊界: 25% 寬度 右邊界: 75% 寬度
上邊界: 25% 高度 下邊界: 70% 高度
對應 1920×1080 畫面x=480 y=270 w=960 h=486DrawBox 視覺輸出位置)
```
#### 前進 ROIForward ROI— 梯形
模擬球車前方路面的透視梯形,用於草地/樹木判斷:
```
頂邊:左 45% ─────────── 右 55% (影像 55% 高度處)
底邊:左 30% ─────────────────── 右 70% (影像 95% 高度處)
```
### 9.3 警告旗標與閥值
`stdc_analysis_t` 中的 boolean 旗標由以下邏輯設定:
| 旗標 | 條件 | 閥值來源 |
|------|------|---------|
| `on_grass` | 前進 ROI 內草地佔比 > 30% | `THR_GRASS_ROI = 0.30` |
| `grass_warning` | `on_grass` 持續時間 > 2 秒 | `STDC_WARNING_SECONDS = 2.0` |
| `person_warning` | 全域行人佔比 > 1% | `THR_PERSON_GLOBAL = 0.01` |
| `car_warning` | 全域車輛佔比 > 5% | `THR_CAR_GLOBAL = 0.05` |
| `greenery_warning` | 全域植被佔比 > 20% | `THR_GREENERY_GLOBAL = 0.20` |
| `bunker_warning` | 全域沙坑佔比 > 1% | `THR_BUNKER_GLOBAL = 0.01` |
| `pond_warning` | 全域水塘佔比 > 1% | `THR_POND_GLOBAL = 0.01` |
| `tree_dense` | 全域樹木佔比 > 30% | `THR_TREE_DENSE = 0.30` |
| `tree_approaching` | 前進 ROI 內樹木佔比相較前幀成長 > 5% | `STDC_TREE_GROWTH_THR = 0.05` |
| `collision_risk` | 碰撞 ROI 內任一危險類別超過各自閥值(見下表) | — |
| `is_moving` | 前進 ROI 內 luma 平均差 > 3.0 | `STDC_MOTION_THRESHOLD = 3.0` |
#### `collision_risk` 詳細條件(碰撞 ROI 內)
| 類別 | 閥值 |
|------|------|
| 行人 (`col_person_ratio`) | > 1% |
| 車輛 (`col_car_ratio`) | > 5% |
| 樹木 (`col_tree_ratio`) | > 20% |
| 沙坑 (`col_bunker_ratio`) | > 1% |
| 水塘 (`col_pond_ratio`) | > 1% |
任一條件成立即觸發 `collision_risk = 1`
### 9.4 警告優先層級
目前的邏輯是平坦的 bitmask無優先順序下一階段需要整合成層級
```
Level 2緊急 ← collision_risk = 1
OR person_warning = 1
Level 1警告 ← car_warning = 1
OR pond_warning = 1
OR bunker_warning = 1
OR tree_approaching = 1
Level 0正常 ← 其餘情況
```
`grass_warning``greenery_warning``tree_dense` 在球場環境中幾乎常時觸發,**不納入煞車邏輯**,僅保留為 HUD 顯示資訊。
### 9.5 視覺疊加輸出DrawBox1920×1080
```
畫面區域 內容
─────────────────────────────────────────────────────
中央大框 碰撞 ROI 輪廓480,270 960×486
collision_risk 時加畫第二層內框492,282 936×462
中央下框 前進 ROI 輪廓576,594 768×432
左側警告欄 (200×55) y=10 collision_risk
y=73 person_warning
y=136 car_warning
y=199 grass_warning
y=262 tree_dense
y=325 tree_approaching
右側類別欄 (40×55) 每個 class 當比例超過各自閥值時亮起
bunker/car/grass/greenery/person/pond/road/tree由上到下
```
---
## 10. 下一階段:高爾夫球車警示與煞車控制
### 10.1 目標
在現有 KL630 AI 視覺管線上疊加一套安全警示與動力介入系統,適用於高爾夫球場電動球車:
- **行人/障礙物偵測** → 即時警告
- **緊急煞車** → GPIO 訊號觸發 relay截斷馬達動力或拉起電磁煞車
- **未來擴充** → 油門 PWM 控制(依障礙物距離梯度減速)
### 10.2 系統架構變化
```
現有管線(第一階段)
STDC 分割 → stdc_analysis_t flags → printf 警告 + DrawBox 視覺疊加
新增(第二階段)
stdc_analysis_t flags
warn_level 整合0/1/2 ← app_header_init.c 新增
┌────┴─────────┐
▼ ▼
GPIO 煞車訊號 GPIO 警告燈 ← gpio_ctrl.c 新增
/sys/class/gpio/ relay/buzzer
馬達控制器 / 電磁煞車
```
### 10.3 已完成 / 待完成項目
| 檔案 | 狀態 | 說明 |
|------|------|------|
| `src/host_stream/event_recorder.c` | ✅ **已實作** | 草地狀態機 + 單次碰撞事件 + JPEG 快照 + tar.gz 上傳(見第 13 節) |
| `src/host_stream/can_bus.c` | ✅ **已實作** | CAN 控制指令 `can_bus_send_control_cmd(level)`(見第 12 節) |
| `src/host_stream/gpio_devmem.c` | ✅ **已實作** | /dev/mem GPIOSPI bit-bang 用,見第 14 節) |
| `src/host_stream/bt_uart.c` | ✅ **已實作** | BLE JSON 事件推送(見第 11 節) |
| 警告燈 / 煞車 relay GPIO | ❌ **待實作** | 需確認板上 GPIO pin 與繼電器接線後實作 `gpio_set_warning(level)` |
| `ini/host_stream.ini [golf_cart]` | ❌ **待實作** | 警告/煞車 GPIO pin 號設定 |
### 10.4 警告等級 → GPIO 輸出對應
```
warn_level 0正常 → GPIO_WARN=0 GPIO_BRAKE=0
warn_level 1警告 → GPIO_WARN=1 GPIO_BRAKE=0 (警示燈亮)
warn_level 2緊急 → GPIO_WARN=1 GPIO_BRAKE=1 (警示燈 + 煞車 relay 觸發)
```
觸發條件:
- Level 2`collision_risk == 1``person_warning == 1`
- Level 1`car_warning == 1``pond_warning == 1``bunker_warning == 1``tree_approaching == 1`
### 10.5 新增 INI 參數規劃
```ini
[golf_cart]
BrakeOnCollision = 1 ; collision_risk 觸發煞車0=停用)
BrakeOnPerson = 1 ; person_warning 觸發煞車0=停用)
WarnOnCar = 1 ; car_warning 觸發警告燈
WarnOnPond = 1 ; pond_warning 觸發警告燈
WarnOnBunker = 1 ; bunker_warning 觸發警告燈
GpioPinWarn = 5 ; 警告燈/蜂鳴器 GPIO pin 編號
GpioPinBrake = 6 ; 煞車 relay GPIO pin 編號
BrakePulseMs = 0 ; 0=持續拉高;>0=觸發後維持 N ms 再釋放pulse 模式)
```
### 10.6 待確認事項
| 項目 | 說明 |
|------|------|
| KL630 GPIO pin mapping | 確認板上哪些 pin 拉出,對應 `/sys/class/gpio/gpioX` 編號(需查 BSP / schematic |
| 煞車介面 | relay DO直接 GPIO或需要 PWM 訊號給馬達控制器 |
| 草地閥值調整 | 球場全是草,`THR_GRASS_ROI=0.30` 會讓 `on_grass` 幾乎常時觸發;考慮提高閥值或直接排除 grass 警告出煞車邏輯 |
| 反應延遲 | 目前 STDC 推論速率約 25 fpscollision_risk 觸發到 GPIO 拉高延遲 < 80ms是否符合需求 |
| 安全冗餘 | 視覺 AI 單點失效時的 failsafe 策略例如推論超時 強制觸發警告 |
---
---
## 11. 藍牙通訊DX-BT24 BLE UART 模組
### 11.1 模組規格
| 項目 | 規格 |
|------|------|
| 模組型號 | DX-BT24 |
| 通訊介面 | 透明 UART BLEBLE 4.2 |
| BLE 服務 | UUID `0xFFE0`Notify `FFE1`裝置手機Write `FFE2`手機裝置 |
| UART 預設鮑率 | 9600 bps出廠一次性升級至 115200 bps |
| KL630 UART 腳位 | `/dev/ttyS1`J15 擴充連接器 |
### 11.2 功能架構
```
KL630 firmware
event_recorder_update()
bt_uart_send_json() ← 非阻塞:放入 FIFO 佇列即返回
[bt_writer_thread] ← 專用執行緒,實際寫入 UART
/dev/ttyS1 → DX-BT24 → BLE → iPhone nRF Connect / 球場 iPad App
```
**關鍵設計**`bt_uart_send_json()` 只做 `malloc + enqueue + cond_signal`永不阻塞推論主迴圈寫入執行緒在有訊息時才喚醒並寫入 UART
### 11.3 JSON 事件格式
每次偵測到違規即送出一個 JSON 字串 `\r\n`BLE 以封包為單位
```json
{"class":"car","level":1}
{"class":"person","level":2}
{"class":"grass","level":1}
{"class":"boot","level":0}
```
| 欄位 | 說明 |
|------|------|
| `class` | 違規類型`boot` / `road` / `grass` / `car` / `person` / `pond` / `bunker` / `tree` / `hazard` |
| `level` | 嚴重等級0=正常/開機1=警告2=緊急 |
> **注意**:不加 `\r\n`。加了 DX-BT24 會在換行後額外送出一個空的 BLE Notification手機端看到非 UTF-8 內容。
### 11.4 一次性鮑率升級
出廠模組預設 9600 bps升級步驟僅需做一次
1. `ini/host_stream.ini` `bt_at_probe = 1`
2. 啟動韌體 `bt_uart_init()` 偵測模組當前鮑率並送出 `AT+BAUD7`(→115200 `AT+RESET`
3. 確認 `/tmp/fw.log` 出現 `[BT] upgrade complete`
4. `bt_at_probe` 改回 `0`避免 AT 指令被 BLE 客戶端看到
### 11.5 INI 參數
```ini
[event]
enable = 1
bt_uart_dev = /dev/ttyS1 # UART 裝置節點
bt_at_probe = 0 # 0: 直接以 115200 開啟(正常使用)
# 1: 執行一次性 AT 鮑率升級(出廠/重置後)
```
### 11.6 相關原始碼
| 檔案 | 說明 |
|------|------|
| `src/host_stream/bt_uart.c` | UART 驅動FIFO 佇列 + 專用寫入執行緒 + AT 探測/升級邏輯 |
| `include/host_stream/bt_uart.h` | 公開 API`bt_uart_init()` / `bt_uart_send_json()` / `bt_uart_close()` |
| `src/host_stream/event_recorder.c` | `fire_json_async()` 呼叫 `bt_uart_send_json()` |
| `src/host_stream/kp_firmware.c` | `loadConfig()` 中讀取 INI 並呼叫 `bt_uart_init()` |
### 11.7 驗證方法
裝置端韌體啟動後
```sh
# 手動送出 JSONstty 先設定鮑率)
stty -F /dev/ttyS1 115200 raw cs8 -parenb -cstopb -echo
printf '{"class":"test","level":0}' > /dev/ttyS1
```
iPhone 開啟 nRF Connect 掃描 DX-BT24 連線 訂閱 FFE1 Notify 應看到 `{"class":"test","level":0}`正確 UTF-8 字串)。
---
## 12. CAN Bus 通訊MCP2515 SPI 控制器
### 12.1 硬體配置
| 項目 | 規格 |
|------|------|
| CAN 控制器 | MCP2515 |
| 介面 | SPI via J16 擴充連接器 |
| SPI 裝置節點 | `/dev/spidev1.0` |
| SPI 時脈 | 1 MHz |
| 晶振 | 8 MHz |
| 預設 CAN 速率 | 250 kbps |
| 輸出 CAN Frame ID | 0x10011-bit Standard Frame INI 設定 |
J16 接線依板上標示順序
| J16 腳位 | MCP2515 模組 |
|---|---|
| CS | CS |
| DO | SI (MOSI) |
| CLK | SCK |
| RESERVED | SO (MISO) |
若板上 `RESERVED` 版本實際未接到 `SPI_1_DI_E`MCP2515 將無法回應
### 12.2 功能架構
本週重構為**控制訊號模式control-frame mode**移除了 MsgBroker FIFO 依賴直接使用 CAN 控制幀進行速度控制
```
KL630 firmware
event_recorder.c: grass_enter_level(level) / collision detection
msg_send() → can_bus_send_control_cmd(level) ← 直接呼叫(同步,有 retry
send_one_frame_with_retry() ← 最多 20 次重試 + TX stuck 恢復
mcp2515_sendMessage() ← SPI ioctl 到 /dev/spidev1.0
│ (或 SPI_BITBANG 模式gpio_devmem
MCP2515 (J16) → CAN bus → 車體馬達控制器
```
> **重要改動**`msg_send("setSpeed")` 現在直接呼叫 `can_bus_send_control_cmd()`,不再依賴 `/tmp/canbus/c0/command.fifo` MsgBroker FIFO。JSON 事件僅透過藍牙Channel A傳送給 iPad。
CAN 控制器包含 keep-alive 機制 200ms 自動重發最後的速度指令確保馬達控制器不會因通訊中斷而失去控制
### 12.3 CAN Frame 格式
#### 控制 Frame主要輸出
```
CAN ID : 0x75 (11-bit, hardcoded in s_ctl_can_id)
DLC : 8
Data[0] : 速度控制指令(見下表)
Data[1-7]: 0x00 (reserved)
```
| Data[0] | 定義常數 | 觸發條件 | 說明 |
|---------|---------|---------|------|
| `10` | `SPEED_LEVEL_0` | 碰撞事件 | 緊急停車單次觸發 |
| `10` | `SPEED_LEVEL_1` | 草地 L3T+10s | 嚴重違規強制停車 |
| `10` | `SPEED_LEVEL_2` | 草地 L2T+6s | 持續違規中度減速 |
| `10` | `SPEED_LEVEL_3` | 草地 L1 持續 | 持續違規輕度減速 |
| `10` | `SPEED_LEVEL_4` | 草地 L1 進入 | 初次草地違規警告 |
| `240` | `SPEED_LEVEL_5` | 正常狀態 / 事件結束 | 恢復正常全速 |
> **注意**:目前測試階段所有 SPEED_LEVEL 均設為 10低速正常速度為 240。生產環境可依需求調整各等級的速度值。
控制指令由 `event_recorder.c` 的草地狀態機和碰撞檢測觸發詳見第 13 )。CAN 控制器會每 200ms 自動重發最後的控制指令作為 keep-alive
#### JSON 多幀(保留,目前停用)
```
CAN ID : 0x100 (INI can_id可設定)
DLC : 最多 8 bytes / frame
格式 : JSON 字串分段,每幀 8 bytes最後一幀以 0x00 補齊
接收端 : 逐幀拼接直到遇到 null byte
```
此模式已保留程式碼但 `can_bus_send_json()` 目前不發送若日後需要同時傳送 JSON CAN移除 `can_bus_send_json()` 中的 early return 即可
### 12.4 初始化流程
```c
/* can_bus_init 內部步驟 */
s_dev = new_mcp2515_dev(spidev); // 配置 SPI: 1MHz, 8-bit
// 或 "gpio-bitbang"SPI_BITBANG 模式)
mcp2515_initial(s_dev); // SPI reset → 進入 CONFIG mode
mcp2515_can_speed(s_dev, CAN_250KBPS, MCP_8MHZ); // 8MHz 晶振 @ 250kbps
mcp2515_setMode(s_dev, CANCTRL_REQOP_NORMAL); // 進入 NORMAL mode
/* 啟動迴環自測 (loopback self-test) */
mcp2515_setMode(LOOPBACK) sendMessage(0x321, 0xA5) readMessage()
// 成功log "[CAN] startup loopback self-test OK"
// 失敗log "[CAN] startup loopback self-test failed (-N)"
// 啟動 can_writer_thread佇列消費用目前主要由 can_bus_send_control_cmd 直接送)
// 啟動 can_keepalive_thread每 200ms 重發最後控制指令 (預設 SPEED_LEVEL_5 = 240)
// 初始化完成後送出 boot 控制 frame: can_bus_send_control_cmd(SPEED_LEVEL_5)
```
### 12.5 TX 錯誤恢復機制
`mcp2515_sendMessage()` 失敗時觸發分層恢復
```
sendMessage() 回傳 ERROR_ALLTXBUSY 或 ERROR_FAILTX
重試最多 20 次(每次 backoff 2ms
├── 仍失敗 → 讀取 EFLG / TEC / REC 暫存器
│ 印出 hintTX bus-off / TX error-passive / 警告)
│ can_recover_tx_stuck():
│ CANCTRL_ABAT → 中止掛起的重傳
│ 清除 TXB0/1/2 TXREQ bit
│ 恢復 NORMAL mode
└── 若 EFLG_TXBO 或 TEC ≥ 128 (bus-off 狀態)
can_reinit_controller_locked(): reset + 重設速率 + NORMAL
can_loopback_selftest_locked(): 確認 SPI 路徑健康
```
### 12.6 INI 參數
```ini
[can]
enable = 1 # 1: 啟用 CAN 輸出0: 停用
spidev = /dev/spidev1.0 # SPI 裝置路徑SPI_BITBANG 模式下忽略)
speed_kbps = 250 # CAN 速率125 / 250 / 500 / 1000 kbps
can_id = 0x100 # JSON 多幀的 11-bit CAN ID目前停用
# 控制 Frame 固定使用 ID 0x75
```
> **本週變更**`enable` 已從 `0` 改為 `1`(預設開啟)。
### 12.7 SPI Bit-bang 模式
compile.sh 加上 `-DSPI_BITBANG` MCP2515 透過 gpio_devmem 以軟體模擬 SPI
```
/dev/spidev1.0 不可用(被其他 kernel module 佔用)時使用此模式
J15 腳位 (GPIOC_0)
GPIO1 = CS (SPI_GPIO_CS, compile.sh 定義)
GPIO2 = MOSI (SPI_GPIO_MOSI)
GPIO3 = SCK (SPI_GPIO_SCK)
GPIO4 = MISO (SPI_GPIO_MISO)
底層實作gpio_devmem_set(pin, 0/1) → /dev/mem → KL630 GPIO_C 0x402E0000
```
詳細暫存器配置見第 14
### 12.8 相關原始碼
| 檔案 | 說明 |
|------|------|
| `src/host_stream/mcp2515.c` | MCP2515 低層 SPI 驅動 |
| `include/host_stream/mcp2515.h` | MCP2515 暫存器定義API 宣告 |
| `src/host_stream/can_bus.c` | 高層封裝`can_bus_send_control_cmd()` + 錯誤恢復 |
| `include/host_stream/can_bus.h` | 公開 API`can_bus_init()` / `can_bus_send_control_cmd()` / `can_bus_close()` |
| `src/host_stream/gpio_devmem.c` | SPI bit-bang 用的 GPIO /dev/mem 驅動 14 |
| `src/host_stream/kp_firmware.c` | `loadConfig()` 讀取 `[can]` INI 並呼叫 `can_bus_init()` |
### 12.9 待確認事項
| 項目 | 說明 |
|------|------|
| J16 MISO 接腳 | `RESERVED` 需確認為 `SPI_1_DI_E`否則 MCP2515 回應讀不回來 |
| 控制 Frame ID 0x75 | 與車體 ECU 協調確認不與其他 node 衝突 |
| 控制指令語意 | cmd=1/2/3 的具體油門百分比或減速邏輯由馬達控制器端定義 |
| 接收端需求 | 目前只實作發送若需接收回授 can_writer_thread mcp2515_readMessage() 輪詢 |
| CAN 速率匹配 | 車體 CAN bus 速率需與 `speed_kbps` 一致否則全部 Frame 被丟棄 |
---
---
## 13. 事件錄製器Event Recorder
`event_recorder.c` 是本週新增的核心模組負責整合 STDC 分析結果觸發雙通道事件輸出並將現場 JPEG 快照打包上傳
### 13.1 雙通道架構
```
STDC 分析結果 (stdc_analysis_t)
event_recorder_update() ← 在 recv callback約 25fps呼叫
┌────┴──────────────────────────────┐
▼ ▼
Channel A (BLE/iPad) Channel B (tar.gz 上傳)
bt_uart_send_json(json) VMF_SNAP → JPEG → event.json → tar.gz
{"class":"lane","level":1} → kCurl POST → /api/golf.cgi
│ │
DX-BT24 BLE → iPad SD 卡存檔 (最多 7GB)
```
另外草地事件同時觸發 CAN 控制指令Channel C 12
```
grass_enter_level(level)
├── fire_json_async() → BLE JSON
└── can_bus_send_control_cmd(level) → CAN 馬達控制
```
### 13.2 草地違規狀態機
觸發條件`on_grass == 1 AND is_moving == 1`靜止停車在草地不觸發
```
GRASS_IDLE ──── on_grass & is_moving ────► GRASS_L1 (T+0s)
│ fire BT level=1, CAN cmd=1, snap level1.jpg
!on_grass 持續 2s ◄─────────────┤─── on_grass & 經過 6s ──► GRASS_L2 (T+6s)
(exit hysteresis) │ │ fire BT level=2
│ │ │ CAN cmd=2, snap level2.jpg
▼ │ !on_grass 持續 2s ◄─────────┤─── 經過 10s ──► GRASS_L3 (T+10s)
GRASS_DONE │ │ │ │ fire BT level=3
fire BT level=0 │ ▼ │ │ CAN cmd=3
CAN cmd=0 │ GRASS_DONE │ !on_grass 持續 2s
launch_upload() └─────────────────────────────── │ ──────────────► GRASS_DONE
upload_thread (detached)
delay → event.json → tar.gz → kCurl POST → SD 清理
GRASS_IDLE
```
**退出遲滯exit hysteresis**`GRASS_EXIT_HYSTERESIS_MS = 2000ms`
球場上 STDC 偶爾在路邊草地閃爍2 秒遲滯避免誤以為事件結束
### 13.3 單次碰撞事件Single-shot
以下事件僅在碰撞 ROI 內偵測到且只觸發一次Rising edge 01
| 類型 | 閾值 | 觸發行為 |
|------|------|---------|
| `person` | `col_person_ratio ≥ THR_PERSON_COLLISION` | BT JSON level=1 + snap + 立即上傳 |
| `bunker` | `col_bunker_ratio ≥ THR_BUNKER_COLLISION` | BT JSON level=1 + snap + 立即上傳 |
| `pond` | `col_pond_ratio ≥ THR_POND_COLLISION` | BT JSON level=1 + snap + 立即上傳 |
| `tree` | `col_tree_ratio ≥ THR_TREE_COLLISION` | BT JSON level=1 + snap + 立即上傳 |
碰撞事件不觸發 CAN 控制指令目前設計日後可加)。
### 13.4 JSON 事件格式BLE 輸出)
```json
{"class":"lane","level":1} 草地 L1
{"class":"lane","level":2} 草地 L2
{"class":"lane","level":3} 草地 L3
{"class":"lane","level":0} 草地結束(恢復正常)
{"class":"person","level":1} 行人碰撞 ROI
{"class":"bunker","level":1} 沙坑碰撞 ROI
```
> **type "lane"**:草地事件統一以 `lane`(超出車道邊界)回報,方便前端 app 分類。
### 13.5 JPEG 快照與 tar.gz 打包
快照由 `event_recorder_provide_frame()` VMF 影格發送 callback 中執行
```
[主執行緒: send callback]
g_snap_req.active == 1
→ VMF_SNAP_ProcessOneFrame(1920×1080, QP=75)
→ save_jpeg("/tmp/ev_<id>/level1.jpg")
→ 草地事件:保留,等事件結束後統一打包
→ 碰撞事件:立即 launch_upload()
```
tar.gz 結構
```
event_<id>_<timestamp>.tar.gz
├── event.json ← id, date(UTC+8), type, max_level, duration_sec, images[]
├── level1.jpg ← 草地 L1 快照(若存在)
├── level2.jpg ← 草地 L2 快照(若存在)
├── level3.jpg ← 草地 L3 快照(若存在)
└── snapshot.jpg ← 碰撞事件快照
```
SD 卡清理總大小超過 `sd_max_mb`預設 7168 MB = 7GB mtime 由舊到新刪除
### 13.6 INI 參數
```ini
[event]
enable = 1
bt_uart_dev = /dev/ttyS1 # Channel A: DX-BT24 UART 節點
bt_at_probe = 0 # 0: 直接 115200; 1: 一次性 AT 升級
upload_url = http://192.168.0.114:8081/api/golf.cgi # Channel B: tar.gz 上傳端點
# Production: http://192.168.0.99/api/golf.cgi
sd_path = /tmp/sdcard/events # SD 卡存檔路徑
sd_max_mb = 7168 # SD 卡容量上限MB
upload_delay_ms = 0 # 0: 事件結束後立即打包上傳
```
### 13.7 相關原始碼
| 檔案 | 說明 |
|------|------|
| `src/host_stream/event_recorder.c` | 狀態機快照打包上傳全部邏輯 |
| `include/host_stream/event_recorder.h` | 公開 API`event_recorder_init()` / `event_recorder_update()` / `event_recorder_provide_frame()` |
| `src/host_stream/kp_firmware.c` | `loadConfig()` 讀取 `[event]` INI 並呼叫 `event_recorder_init()` |
| `src/host_stream/kp_firmware.c` | recv callback 呼叫 `event_recorder_update(ana)` |
---
## 14. GPIO 直接存取gpio_devmem
### 14.1 背景
KL630 J15 擴充連接器的 GPIO 腳位在部分韌體版本中已被 `dh2228fv` display driver 透過 pinmux 佔用導致 `/sys/class/gpio` 介面無法使用解法是透過 `/dev/mem` 直接存取實體暫存器繞過 pinmux 衝突
### 14.2 硬體對應
```
KL630 GPIO_C 基底位址0x402E0000
J15 連接器腳位對應GPIOC_0 group
J15 Pin 1 (CS) → GPIO1 = GPIOC_0_IO_DATA_1
J15 Pin 2 (MOSI) → GPIO2 = GPIOC_0_IO_DATA_2
J15 Pin 3 (SCK) → GPIO3 = GPIOC_0_IO_DATA_3
J15 Pin 4 (MISO) → GPIO4 = GPIOC_0_IO_DATA_4
```
### 14.3 暫存器對應
| 偏移 | 暫存器 | 功能 |
|------|--------|------|
| `+0x0000` | `GPIOD_PSR` | Port Status唯讀反映實際 pin 狀態 |
| `+0x0004` | `GPIOD_PDDR` | Port Data Directionbit=1 輸出bit=0 輸入 |
| `+0x0008` | `GPIOD_PSOR` | Port Set 1 該腳拉高其他 pin 不影響 |
| `+0x000C` | `GPIOD_PCOR` | Port Clear 1 該腳拉低其他 pin 不影響 |
| `+0x0010` | `GPIOD_PTOR` | Port Toggle |
| `+0x0014` | `GPIOD_PIDR` | Port Input Disable |
### 14.4 API
```c
int gpio_devmem_init(void); // 開啟 /dev/mem + mmap GPIO_C
void gpio_devmem_cleanup(void); // munmap + close
int gpio_devmem_set_direction(int gpio, int out); // gpio: 1-4out: 1=輸出
int gpio_devmem_set(int gpio, int value); // set high/low via PSOR/PCOR
int gpio_devmem_get(int gpio); // 讀 PSR bit0 或 1
```
### 14.5 使用方式SPI bit-bang
`compile.sh` 加入 `-DSPI_BITBANG` `can_bus.c` 改用 gpio_devmem 模擬 SPI 時序
```bash
# compile.sh 加入的 flags
-DSPI_BITBANG
-DSPI_GPIO_CS=1 # J15 Pin1
-DSPI_GPIO_MOSI=2 # J15 Pin2
-DSPI_GPIO_SCK=3 # J15 Pin3
-DSPI_GPIO_MISO=4 # J15 Pin4
```
### 14.6 注意事項
- 需要 root 權限`CAP_SYS_RAWIO`firmware 本身已以 root 執行
- mmap page4096 bytes為單位base 已自動對齊偏移在程式碼中修正
- 若日後 pinmux 問題解決可切回 `/dev/spidev1.0` 硬體 SPI移除 `-DSPI_BITBANG`
---
---
## 15. STDC 模型重訓練與後處理 Python→C 移植
### 15.1 重訓練背景
原始 STDC 模型以公開道路資料集訓練對高爾夫球場環境的辨識率不足
- 球道fairway被誤判為草地grass或道路road
- 沙坑bunker辨識率偏低
- 球場邊緣樹木與一般道路樹木特徵差異大
因此收集球場現場影像重新標注並 fine-tune 模型提升球場環境下的分類準確度
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(訓練資料範例圖)*
### 15.2 重訓練流程
```
現場採集影像
人工標注8 類:道路、草地、車輛、行人、沙坑、水塘、植被、樹木)
Fine-tune STDC 模型(基於原始權重)
Python 推論驗證stdc630inference.py
Kneron Model Zoo 轉換 → .nef 格式KL630 NPU 使用)
STDC_0520.nef → 部署到裝置
```
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(訓練前後準確度對比圖)*
### 15.3 後處理 Python → C 移植
原先推論結果的後處理ROI 計算類別佔比警告旗標全部在 Python`stdc630inference.py`中完成無法在嵌入式端即時執行
**移植目標**完全在 KL630 上執行 Python 依賴
| 項目 | Python 原版 | C 移植版`stdc_post_process.c` |
|------|------------|----------------------------------|
| 分割圖解析 | numpy 陣列切片 | 直接 pointer 操作 NPU output buffer |
| 前進 ROI梯形 | 向量化條件判斷 | `stdc_is_in_forward_roi()` 逐像素計算 |
| 碰撞 ROI | slice 計算 | 固定比例邊界整數乘法 |
| 動態偵測is_moving | frame diff | `stdc_compute_motion_diff()` + `prev_roi_luma` 快取 |
| 草地持續時間計時 | time.time() | `gettimeofday()` + `consecutive_grass_frames` |
| 輸出 | JSON / 終端機 | `stdc_analysis_t` struct event_recorder |
**正規化對齊**C 版輸入正規化設為 `KP_NORMALIZE_KNERON`= `/256 0.5` Python 訓練時的前處理一致確保推論結果數值相同
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(Python vs C 輸出比對畫面)*
### 15.4 相關原始碼
| 檔案 | 說明 |
|------|------|
| `src/pre_post_proc/stdc_post_process.c` | 後處理主體ROI 計算類別佔比警告旗標動態偵測 |
| `src/app_flow/stdc_inf_single_model.c` | NPU 推論配置正規化resizepost_proc callback 綁定 |
| `include/stdc_post_process.h` | `stdc_analysis_t` 結構定義閾值常數 |
---
---
## 16. 快照功能:實作 / 事件觸發 / 打包上傳
### 16.1 Snapshot function implement
#### 架構選擇
KL630 VMF 提供 `VMF_SNAP`Video Snapshot Mechanism模組可從現有 SSM 影像管線直接擷取 JPEG無需額外複製 YUV 緩衝區
```
SSM Ring Buffer (vsrc_ssm_0, 1920×1080)
VMF_SNAP_Init() ← Lazy init首次觸發時才建立避免閒置佔資源
opt.dwStreamIdx = 0
opt.dwQp = 75 ← JPEG 品質
VMF_SNAP_ProcessOneFrame() ← 在 VMF 影格 send callback 呼叫
│ (必須在 VMF 執行緒內,不可跨執行緒)
JPEG buffer (MemBroker, 最大 2MB)
save_jpeg("/tmp/ev_<id>/level1.jpg")
```
#### 跨執行緒協調SnapReq 機制)
VMF_SNAP 只能在 VMF 影格 callback 執行緒中呼叫事件判斷在 recv callback不同執行緒)。解法
```
[recv callback 執行緒] [VMF send callback 執行緒]
event 觸發
→ request_snap() → event_recorder_provide_frame()
寫入 g_snap_req 讀取 g_snap_req.active
g_snap_req.active = 1 若 active → 執行 VMF_SNAP
mutex 保護) 清除 active
存檔
```
若上一次快照尚未完成新請求會被丟棄並 log避免 race condition)。
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(快照存檔後的 JPEG 範例)*
---
### 16.2 Snapshot trigger by specific event
快照由兩類事件觸發時機與邏輯不同
#### 草地違規(連續事件)
每升一個 level 拍一張事件結束後統一打包
```
GRASS_L1 進入 → snap "level1.jpg"immediate_upload = 0
GRASS_L2 進入 → snap "level2.jpg"immediate_upload = 0
GRASS_L3 進入 → snap "level3.jpg"immediate_upload = 0
事件結束 → launch_upload(images=["level1.jpg", "level2.jpg", ...])
```
#### 碰撞 ROI 單次事件Rising edge
偵測到的瞬間拍照並立即上傳
```
偵測到 person / bunker / pond / tree 進入碰撞 ROI0→1 上升緣)
→ snap "snapshot.jpg"immediate_upload = 1
→ 快照完成後立即 launch_upload()delay_ms = 0
```
| 事件類型 | 觸發條件 | 快照檔名 | 上傳時機 |
|---------|---------|---------|---------|
| 草地 L1 | on_grass & is_moving | level1.jpg | 事件結束後 |
| 草地 L2 | 草地持續 6s | level2.jpg | 事件結束後 |
| 草地 L3 | 草地持續 10s | level3.jpg | 事件結束後 |
| 行人碰撞 | col_person_ratio 01 | snapshot.jpg | 立即 |
| 沙坑碰撞 | col_bunker_ratio 01 | snapshot.jpg | 立即 |
| 水塘碰撞 | col_pond_ratio 01 | snapshot.jpg | 立即 |
| 樹木碰撞 | col_tree_ratio 01 | snapshot.jpg | 立即 |
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(各事件觸發快照的對比圖)*
---
### 16.3 Zip event snapshots and send to GF cloud
#### 打包流程
事件結束後由獨立 detached thread 執行不阻塞推論主迴圈
```
upload_thread (detached)
├─ [可選] sleep upload_delay_ms目前設 0立即執行
├─ write_event_json("/tmp/ev_<id>/event.json")
│ {
│ "id": "<unix timestamp>",
│ "date": "2026-04-16T10:30:00+08:00", ← 台灣時間 UTC+8
│ "type": "grass",
│ "max_level": 2,
│ "duration_sec": 8.3,
│ "images": ["level1.jpg", "level2.jpg"]
│ }
├─ build_targz()
│ (cd /tmp/ev_<id> && tar cf - .) | gzip -c > /tmp/sdcard/events/event_<id>_<ts>.tar.gz
│ sync() ← SD 卡寫入確保
├─ http_post_file()
│ kCurl --data-binary @event_<id>_<ts>.tar.gz
│ 'http://192.168.0.99/api/golf.cgi?filename=event_<id>_<ts>.tar.gz'
├─ sd_cleanup() ← 總大小超過 7GB 時刪除最舊的 .tar.gz
└─ rm -rf /tmp/ev_<id> ← 清除暫存工作目錄
```
#### tar.gz 內容結構
```
event_<id>_20260416_103000.tar.gz
├── event.json ← 事件 metadata
├── level1.jpg ← 草地 L1 現場快照
├── level2.jpg ← 草地 L2 現場快照(若存在)
└── level3.jpg ← 草地 L3 現場快照(若存在)
```
#### 上傳端點
| 環境 | URL |
|------|-----|
| 開發測試 | `http://192.168.0.114:8081/api/golf.cgi` |
| 生產GF cloud | `http://192.168.0.99/api/golf.cgi` |
透過 INI `upload_url` 切換無需重新編譯
#### SD 卡管理
- 每次上傳後執行 `sd_cleanup()`
- 掃描 `/tmp/sdcard/events/` 下所有 `.tar.gz`超過 `sd_max_mb`預設 7168 MB時按 mtime 由舊到新刪除
- 即使網路不通tar.gz 仍保留在 SD
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> *(GF cloud 後台收到事件的截圖)*
---