Files
Loongson_2k0300_SmartCar/docs/ARCHITECTURE.md
T

303 lines
16 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.
# SmartCar 决策总架构:从视频帧到执行器
## 坐标系统一览
```
Camera 输出 640×480 MJPEG
CONVERT_RGB=0 + IMREAD_REDUCED_COLOR_4
→ 1/4 MJPEG 解码 → raw_frame = 160×120
┌───────────┤
▼ ▼ (raw_frame.cols/rows 决定分母)
巡线空间 显示空间 模型空间
(line_tracking (newWidth × newHeight) 160×120 (正常)
_width × calc_scale = 2 640×480 (回退模式)
line_tracking newWidth = lt_w × 2
_height) newHeight = lt_h × 2
典型值 80×60
```
**关键换算关系(正常模式 raw_frame = 160×120):**
- 模型空间 → 巡线空间:`col_lt = cx * line_tracking_width / 160``row_lt = cy * line_tracking_height / 120`
- 巡线空间 → 显示空间:`pixel = value * calc_scale`calc_scale = 2
- newWidth / newHeight 取决于屏幕物理分辨率,由 `CameraInit` 动态计算
- line_tracking_width / height = newWidth/calc_scale, newHeight/calc_scale
**⚠ 回退模式:** 当 CONVERT_RGB=0 未生效时,raw_frame = 640×480,模型框坐标也在 640×480 空间。
此时 LCD 渲染仍硬编码除以 160/120(会出错),但正常运行时不会进入此模式。
---
## 主循环 (main.cpp → CameraHandler)
```
main()
├── cfg_load_all() # 从工作目录文件读取全部配置
├── CameraInit(0, dest_fps, 320, 240) # 打开摄像头, LCD, 模型, I2C
├── ControlInit() # 初始化双电机 GPIO + PWM
└── while(running):
└── CameraHandler() # ★ 以下逐帧执行
```
---
## CameraHandler 9 步流水线
```
┌─────────────────────┐
Step 1 │ cap.read(raw_mat) │ 640×480 MJPEG 原始字节流
取帧+解码 │ raw_mat 单通道(字节) │ → IMREAD_REDUCED_COLOR_4
│ → cv::imdecode │ → raw_frame = 160×120 BGR
│ raw_frame = decoded │ (回退: raw_mat 三通道→直用 640×480)
└────────┬────────────┘
Step 2 ┌────────▼────────────┐
保存图像 (条件) │ g_cfg.debug==1 │ 写入 ./image/image_XXXXX.jpg
│ && saveImg==1 → │ (双重条件,缺一不可)
│ saveCameraImage() │
└────────┬────────────┘
Step 3 ┌────────▼────────────┐
视觉巡线 │ image_main() │ raw_frame → resize(lt_w×lt_h)
(每帧都跑) │ │ → HSV双通道Otsu → floodFill
│ 输出: left_line[] │ → 逐行最长连续段搜索
│ right_line[] │ → 丢线补全(仅 row 10~59)
│ mid_line[] │ → track (赛道蒙版 128/0)
└────────┬────────────┘
Step 4 ┌────────▼────────────┐
模型推理 │ model_v10_detect() │ 每 2 帧推理一次
(每 2 帧) │ raw_frame (160×120) │ 4 类: 0=锥桶 1=红灯 2=绿灯 3=斑马线
│ → g_boxes[16] │ g_thresh = [0.80,0.80,0.80,0.75]
│ → g_box_count │ 框坐标 ∈ raw_frame 空间
└────────┬────────────┘
Step 5 ┌────────▼────────────┐
斑马线状态机 │ 遍历 g_boxes │ 仅处理 cls=3,取第一个斑马线框
│ 去抖计数+远近判断 │ NORMAL→STOP(4s)→COOLDOWN(5s)
│ │ I2C 语音播报 @ 0x34
└────────┬────────────┘
Step 6 ┌────────▼────────────┐
舵机控制 │ if g_cfg.start: │
│ foresee→check_row │ 偏差 = mid_line[row]×2 - newWidth/2
│ mid_line[check_row]│ → g_steer_deviation ∈ [-1, 1]
│ → deviation │ → servo duty: 1500000 ± offset
│ → deadband 过滤 │ clamp [1.2M, 1.8M] ns
│ → servo.setDuty()│ mid_val==255 → 跳过(无效行)
└────────┬────────────┘
Step 7 ┌────────▼────────────┐
电机控制 │ ControlUpdate( │
(开环) │ target_speed, │ if zebra_STOP: duty=0 (刹停)+GPIO73=0
│ g_zstate==Z_STOP) │ else: speed × curve
│ │ curve = 1 - |deviation|×0.4
│ │ curve ∈ [0.6, 1.0]
│ │ 左右电机同速(无差速)
└────────┬────────────┘
Step 8 ┌────────▼────────────┐
LCD 渲染 │ if g_lcd_on: │ g_lcd_on = g_cfg.showImg 的缓存
│ track→BGR→ROI │ (每10帧刷新一次缓存)
│ + 边界线(红) │ 检测框坐标 bx=newWidth/160 缩放
│ + 中线(蓝) │ 状态指示 N(绿)/S(红)/C(黄)
│ + 检测框标注 │ → RGB565 → /dev/fb0 mmap
│ (L1条状:跳过红灯) │
└────────┬────────────┘
Step 9 ┌────────▼────────────┐
FPS 统计 │ 每 15 帧输出 │ stdout: "fps=XX.X|rd=X vi=X md=X ct=X ms"
│ 分步计时+总帧率 │ 15帧平均
└─────────────────────┘
```
---
## 视觉巡线深度展开 (image_main)
```
raw_frame (160×120 或 640×480 BGR)
├── cv::resize → (line_tracking_w × line_tracking_h) 典型 80×60
├── image_binerize():
│ BGR→HSV → H通道Otsu(THRESH_BINARY_INV) → S通道Otsu → bitwise_or
│ 赛道=255(白), 背景=0(黑)
│ 原理:蓝底赛道H偏聚集(100~130)、S高;灰路面S低
│ bitwise_or 取并集,H或S任一判为赛道即保留
├── find_road():
│ MORPH_OPEN(2×2 CROSS) → 种子点(lt_w/2, lt_h-10)
│ → 种子点处画实心圆(半径10, 255) 防种子落在黑色区域
│ → floodFill(loDiff=20, upDiff=20, 8邻域, newVal=128)
│ → mask 提取 ROI → track (赛道内部=128, 外部=0)
├── 逐行最长连续段搜索:
│ uchar(*IMG)[lt_w] = track.data
│ 每行扫描,找最长连续非零段
│ 有多个白段时取最后一个(≥取等,偏右)
│ → left_line[row], right_line[row]
│ → 全零行: left=right=-1
└── 中线 + 丢线补全 (row = lt_h-1 → 10):
有边界: mid = (left+right)/2, int 整除
丢线: mid[row] = mid[row+1] (继承下行)
若 mid > lt_w/2 → left=mid, right=lt_w-1 (偏右)
若 mid ≤ lt_w/2 → left=0, right=mid (偏左)
```
**边界线区间有效性:** row 10~59 有可靠数据;row 0~9 的 mid_line 保持初始值 -1(不可靠,不要写入 box 过滤逻辑)。
---
## 舵机控制数学
```
foresee = g_cfg.foresee ← 前瞻行索引(像素,显示空间),默认 40
check_row = foresee / calc_scale ← 转为巡线空间行号(calc_scale=2, int 整除)
mid_val = mid_line[check_row] ← 该行中线列坐标 (0~79)
如果 mid_val == 255 → 跳过(丢线补全也写不到 255,仅作防御)
否则:
deviation = mid_val × 2 - newWidth / 2 ← 转为显示空间偏差(px)
deviation -= g_cfg.center_bias ← 中心偏置修正
g_steer_deviation = deviation / (newWidth/2) ← 归一化到 [-1, 1]
if |deviation| < deadband:
servo = 1500000 ns (直行)
else:
norm = deviation / (newWidth/2)
offset = norm × steer_gain × 300000
duty = clamp(1500000 + offset, 1200000, 1800000)
servo.setDutyCycle(duty)
```
**舵机量程:** 1,200,000 ~ 1,800,000 ns,中位 1,500,000 ns,±300,000 ns 对应满偏。
**deadband 单位:** 显示空间像素(deviation 的单位)。
---
## 电机控制数学
```
ControlUpdate(speed, zebra_block):
if zebra_block || !g_cfg.start:
motor[0].updateduty(0) → 两电机停转
motor[1].updateduty(0)
if !g_cfg.start: mortorEN.setValue(0) → GPIO73 拉低
return
curve = 1.0 - |g_steer_deviation| × 0.4
curve = max(curve, 0.6) ← 最低不低于 60% 速度
spd = speed × curve
motor[0].updateduty(spd) → 左电机 (pwmchip8/pwm2, gpio12 方向)
motor[1].updateduty(spd) → 右电机 (pwmchip8/pwm1, gpio13 方向)
mortorEN.setValue(1) → GPIO73 拉高使能
```
**motor[0] vs motor[1]** 左/右区别仅在于 gpioNum12 vs 13)和 pwm通道(2 vs 1),函数调用完全相同。
**updateduty(duty) 内部:**
- `pwm_period * |duty| / 100` → 转占空比 ns 写入 sysfs
- duty > 0 → GPIO 方向 = 1(正转),duty ≤ 0 → GPIO 方向 = 0(反转)
**速度控制是开环的:** `MotorController` 仅包含 `updateduty()` 方法,无编码器反馈。
`PIDController` 类存在但从未被实例化或调用。
MotorController.h 中也无 `updateSpeed()` 方法(之前的文档中误记了此函数)。
---
## 斑马线状态机
```
检测到 cls=3 → 取第一个斑马线框 → 去抖计数+追踪最远cy
ZNORMAL 期间:
zebra_seen: g_zc_frames++, g_zc_min_cy = min(g_zc_min_cy, zebra_cy)
未 seen: g_zc_frames = max(0, g_zc_frames - 2)
g_zc_frames==0 → g_zc_min_cy=120 (重置)
触发条件 (仅 ZNORMAL):
zebra_near (cy > g_cfg.zebrasee)
&& g_zc_frames >= ZEBRA_MIN_FRAMES (5)
&& g_zc_min_cy <= ZEBRA_FAR_CY (50)
→ play_zebra_audio() → ZSTOP
┌──────┐ 条件: 连续5帧 + 起源于远(cy_min≤50) + 当前近(cy>zebrasee)
│NORMAL│ ─────────────────────────────────────► ┌──────┐
│ │ ◄───────────────────────────────────── │ STOP │
└──────┘ 冷却5秒结束 │ │
▲ └──┬───┘
│ 4秒后 │
│ ┌──────────┐ ◄─────────────────────┘
└───────────│ COOLDOWN │
└──────────┘
NORMAL: 允许通行,检测斑马线
STOP: 刹停 4 秒,g_zstate==Z_STOP 传给 ControlUpdate 第二个参数
COOLDOWN: 恢复行驶但斑马线状态机停止检测 5 秒(防止重复触发)
仅跳过检测,不影响其他功能(舵机/巡线正常)
```
**关键变量:**
- `g_zc_min_cy`:NORMAL 期间追踪斑马线出现时的最小 cy(越远值越小)。未检测到斑马线时衰减减2/帧,归零后重置为 120。
- `ZEBRA_FAR_CY = 50`:斑马线的 cy 必须曾在 ≤50 处出现过(即"从远处来")
- `zebrasee`(默认 60):当前斑马线 cy > 此值视为"足够近",触发停车
- 去抖衰减速度:未检测到时每次 `-2`(比锥桶设计中的 `-1` 更快下降)
- Box 遍历在找到第一个 cls=3 后 `break`,忽略同帧其他斑马线框
---
## 配置系统
所有配置通过工作目录下的纯文本文件读写。文件不存在时 readDoubleFromFile 返回 0
| 文件 | 类型 | 代码默认 | ctl.sh 写入 | 含义 |
|---|---|---|---|---|
| `./speed` | double | 60 | 11 | 目标速度 (% 占空比) |
| `./start` | int(0/1) | 0 | 0 | 使能开关,1=电机运行 |
| `./debug` | int(0/1) | 0 | (不写入) | 每7帧重载配置+允许存图 |
| `./showImg` | int(0/1) | 0 | (不写入) | LCD 显示(每10帧轮询→g_lcd_on |
| `./foresee` | double | 80 | 40 | 舵机前瞻行 (显示空间像素) |
| `./deadband` | double | 5 | 8 | 舵机死区 (显示空间像素) |
| `./steer_gain` | double | 1.0 | 1.5 | 舵机增益 |
| `./center_bias` | double | 0 | (不写入) | 中线偏置修正 |
| `./kp`,`./ki`,`./kd` | double | - | 3.5/0.3/2.0 | PID 参数 (当前未使用) |
| `./mortor_kp/ki/kd` | double | - | 0.6/0.2/0 | 电机编码器 PID (当前未使用) |
| `./zebrasee` | double | 60 | (不写入) | 斑马线近界阈值 (模型空间cy) |
| `./destfps` | double | 30* | (不写入) | 目标帧率 (*代码兜底值) |
| `./saveImg` | int(0/1) | - | (不写入) | 保存帧图像 (需 debug=1) |
**debug 模式:** `./debug` = 1 时,主循环每7帧调用一次 `cfg_load_all()` 重读全部配置,支持热更新调参。同时允许 `saveImg` 保存图像。
**注意**`global.h` 中的 `CfgCache` 默认值和 `ctl.sh` 写入的值不一致(如 speed: 60 vs 11)。`ctl.sh` 写入的值覆盖代码默认值,是实际运行参数。
---
## 代码中未使用的模块
以下 `.cpp` 文件已编译进 `common_lib` 但主循环中从未调用:
| 文件 | 功能 | 状态 |
|---|---|---|
| `PIDController.cpp` | 位置式/增量式 PID | 类存在但无实例化,无调用 |
| `serial.cpp` | VOFA 串口可视化 (vofa_justfloat/vofa_image) | 已实现但无调用 |
| `Timer.cpp` | 定时器线程 | 已实现但无调用(Video 依赖它但 Video 也未用) |
| `video.cpp` | 视频文件流读取 | 已实现但无调用 |
---
## 当前已知缺陷 / 未利用能力
1. **cls=0 锥桶** — 模型已检测但被忽略(CONE_DESIGN.md 设计了方案)
2. **cls=1 红灯 / cls=2 绿灯** — 检测框跳过不画(LCD 渲染中 `if g_boxes[i].cls == 1 || g_boxes[i].cls == 2: continue`),无决策
3. **电机开环** — 无编码器反馈,PID 类已实现但无调用点,`MotorController``updateSpeed()` 方法
4. **舵机死区** — 用 `abs(deviation) < deadband` 比较像素值,deadband 单位是显示空间像素
5. **中线 row 范围** — 丢线补全仅覆盖 row 10~59row 0~9 的 mid_line 保持 -1 不可靠
6. **mid_val == 255 防御** — 代码中写死但 255 永远不会出现在 mid_line 中(当前实现下)
7. **LCD 坐标缩放硬编码** — 检测框绘制用 `newWidth/160.0f`,假设 raw_frame 宽=160。回退模式下 raw_frame=640 时出错