This commit is contained in:
zhoujie 2026-03-15 09:01:02 +08:00
parent a87625df90
commit ccac60a1e9
11 changed files with 2001 additions and 616 deletions

View File

@ -204,6 +204,115 @@ void OnConfigUpdated(const ConfigCommon_t* common, const Config2D_t* cfg2d, cons
Preprocess_Settings_Change(cfg2d, cfg1d, common);
}
## 4.6 TcpLogic_InjectTestConfig测试模式配置注入
功能:在不依赖服务器的情况下,直接向 TCP 逻辑模块注入配置参数。注入后走与服务器下发完全相同的缓存 + 回调路径。仅非 NULL 的参数会被更新。
原型:
```c
void TcpLogic_InjectTestConfig(const ConfigCommon_t *common, const Config2D_t *cfg2d, const Config1D_t *cfg1d);
```
入参:
- `common`通用配置指针NULL 表示不更新
- `cfg2d`2D 配置指针NULL 表示不更新
- `cfg1d`1D 配置指针NULL 表示不更新
内部行为:
1. 互斥锁保护下写入内部缓存
2. 设置 `has_valid_config = 1`
3. 触发已注册的 `ConfigUpdateCallback`
使用场景:
- 测试模式(`TEST_PATTERN_MODE=1`)启动时注入默认 2D 配置,使自主触发管线无需服务器即可工作
- 注入后若服务器连接并下发新配置,新配置会覆盖注入的默认值
示例:
```c
Config2D_t test_cfg2d;
memset(&test_cfg2d, 0, sizeof(test_cfg2d));
test_cfg2d.Enabled = 1;
test_cfg2d.TriggerMode = 1; // 内部触发
test_cfg2d.TargetWidth = 64;
test_cfg2d.TargetHeight = 64;
test_cfg2d.TriggerTemperatureThreshold = 800; // 80.0°C
test_cfg2d.TriggerBurstCount = 3;
TcpLogic_InjectTestConfig(NULL, &test_cfg2d, NULL);
```
## 8. MCU 实际业务调用流程CH32V307 实现)
以下为 MCU 中 main.c 的实际初始化和业务循环流程,作为第 7 节伪代码的具体实现参考:
```c
// ===== 初始化阶段 (main 函数) =====
// 1. 硬件初始化
USART_Printf_Init(921600);
Config_Flash_SRAM(FLASH_128_SRAM_192); // 128K Flash + 192K SRAM
#if !TEST_PATTERN_MODE
DVP_Init(); // 正常模式初始化 DVP
#endif
TIM2_Init(); // WCHNET 10ms 定时基准
ExtTrigger_GPIO_Init(); // PA15/EXTI15 外部触发
NG_GPIO_Init(); // PA8 NG 输出
// 2. 网络初始化
ETH_LibInit(IPAddr, GWIPAddr, IPMask, MACAddr);
qdx_port_init();
// 3. 库初始化 + 回调注册
Preprocess_Init(SENSOR_WIDTH, SENSOR_HEIGHT);
TcpLogic_Init(MACAddr, NULL);
TcpLogic_RegisterConfigCallback(OnConfigUpdate);
TcpLogic_RegisterDetectionCallback(OnDetectionResult);
TcpLogic_RegisterTempFrameRequestCallback(OnTempFrameRequest);
// 4. 测试模式注入默认配置
#if TEST_PATTERN_MODE
TcpLogic_InjectTestConfig(NULL, &test_cfg2d, NULL);
#endif
// 5. 启动 TCP 引擎 + RTOS 任务
TcpLogic_Start();
xTaskCreate(task_wchnet_entry, "wchnet", 512, NULL, 6, NULL);
xTaskCreate(task_business_entry, "business", 512, NULL, 5, NULL);
xTaskCreate(task_heartbeat_entry,"hb", 256, NULL, 3, NULL);
#if TEST_PATTERN_MODE
xTaskCreate(task_test_pattern_entry, "testpat", 256, NULL, 4, NULL);
#endif
vTaskStartScheduler();
// ===== 业务循环 (task_business_entry) =====
// 全部行为由运行时 Config 驱动,无编译期功能开关
while (1) {
// NG 脉冲超时检查
if (g_ng_off_time && sys_tick_ms >= g_ng_off_time)
GPIO_ResetBits(NG_GPIO_PORT, NG_GPIO_PIN);
#if !TEST_PATTERN_MODE
DVP_Task(); // 行 DMA 搬运
#endif
// 辅助通道TEMP_REQ 服务器按需截图
if (g_temp_req_pending) {
// → 2D: Preprocess_Execute → TcpLogic_BuildAndSendTemperatureFrame
// → 1D: send_1d_snapshot (30点空间采样)
}
// 主管线:自主触发
if (Frame_Ready_Flag) {
TcpLogic_GetLatestConfig(&tc, &t2, &t1);
if (t2.Enabled)
handle_2d_trigger(&raw, &t2, &use_buf); // 2D 状态机
else if (t1.Enabled)
handle_1d_trigger(&raw, &t1, &use_buf); // 1D 状态机
}
}
```
# Page 5
6. 函数调⽤时序图 (Control Flow)
@ -235,3 +344,56 @@ TCP 数据流返回 Defect Result (NG)
拉⾼ DO1 定时执⾏剔除动作
主机/中断(Main) TcpLogic (⽹络库) Preprocess (预处理库) ConfigServer (上位机)
# Page 7
## 7. 多级调试打印系统
### 7.1 概述
调试打印通过 `qdx_port.h` 中的宏体系实现,支持 **5 个级别****7 个分类**。仅需修改一个宏即可全局控制输出量:
```c
#define DBG_LEVEL DBG_LEVEL_NORMAL // 修改此值控制输出详细度
```
### 7.2 级别定义
| 值 | 宏名 | 说明 | 适用场景 |
|---|---|---|---|
| 0 | `DBG_LEVEL_NONE` | 完全关闭 | 量产固件 |
| 1 | `DBG_LEVEL_ERR` | 仅错误 | 最小输出排障 |
| 2 | `DBG_LEVEL_BRIEF` | + 服务器配置/网络事件 | **推荐日常使用** |
| 3 | `DBG_LEVEL_NORMAL` | + 触发/数据/初始化 | 开发调试(默认) |
| 4 | `DBG_LEVEL_VERBOSE` | + 心跳/TLV详情 | 深度调试 |
### 7.3 分类宏及打印级别
| 宏 | 级别阈值 | 前缀 | 用途 |
|---|---|---|---|
| `DBG_ERR` | ≥1 (ERR) | `[ERR]` | HardFault、CRC校验失败、连接失败、PP失败 |
| `DBG_CFG` | ≥2 (BRIEF) | `[CFG]` | **服务器下发的所有配置**Config2D/1D/Common、DevID、ACK、DetResult、TempReq |
| `DBG_NET` | ≥2 (BRIEF) | `[NET]` | TCP连接/断开、PHY状态、Socket事件 |
| `DBG_TRIG` | ≥3 (NORMAL) | `[TRIG]` | 外部/内部触发、防抖、Burst启停 |
| `DBG_DATA` | ≥3 (NORMAL) | `[DATA]` | 2D/1D帧发送结果、TEMP_REQ发送 |
| `DBG_INIT` | ≥3 (NORMAL) | `[INIT]` | 开机初始化传感器、DVP、WCHNET、RTOS |
| `DBG_HB` | ≥4 (VERBOSE) | `[HB]` | 心跳打印、TLV包头详情高频 |
### 7.4 使用示例
```c
DBG_ERR("CRC fail: calc=0x%04X recv=0x%04X\r\n", calc, recv);
DBG_CFG("<< Config2D: En=%d %dx%d Trig=%d\r\n", en, w, h, trig);
DBG_NET("[Control] Connected to %s:%d\r\n", ip, port);
DBG_TRIG("2D ext trigger\r\n");
DBG_DATA("2D SEND frm=%d ret=%d\r\n", frm, ret);
DBG_INIT("Sensor: Y16 OK\r\n");
DBG_HB("%d tick=%d frm=%d\r\n", cnt, tick, frm);
```
### 7.5 各级别输出预期
- **NONE (0)**:静默,仅 HardFault 的 raw printf 输出
- **ERR (1)**:错误信息 — 约 13 条打印点
- **BRIEF (2)**+ 服务器配置全量打印 + 网络事件 — 上位机交互一目了然
- **NORMAL (3)**+ 触发事件、数据发送、初始化 — 标准开发模式
- **VERBOSE (4)**+ 心跳每2秒、每个TLV包详情 — 仅深度调试使用

View File

@ -1,24 +1,32 @@
# CH32V307 TCPClient 模式配置与功能说明
> **版本**: v2.0 — 自主触发架构
> **更新日期**: 2026-03
> **适用代码**: main.c 自主触发架构(已移除所有 `TEST_ENABLE_*` 编译开关)
## 目录
1. [系统架构概述](#1-系统架构概述)
2. [运行模式说明](#2-运行模式说明)
3. [宏开关配置总表](#3-宏开关配置总表)
4. [测试模式详解](#4-测试模式详解)
5. [正常模式详解](#5-正常模式详解)
6. [网络与服务器配置](#6-网络与服务器配置)
7. [配置下发流程](#7-配置下发流程)
8. [RTOS 任务架构](#8-rtos-任务架构)
9. [切换模式操作指南](#9-切换模式操作指南)
10. [常见配置场景](#10-常见配置场景)
11. [已知注意事项](#11-已知注意事项)
3. [编译期常量总表](#3-编译期常量总表)
4. [自主触发架构详解](#4-自主触发架构详解)
5. [2D 触发状态机](#5-2d-触发状态机)
6. [1D 采集状态机](#6-1d-采集状态机)
7. [TEMP_REQ 辅助通道](#7-temp_req-辅助通道)
8. [测试模式详解](#8-测试模式详解)
9. [正常模式详解](#9-正常模式详解)
10. [网络与服务器配置](#10-网络与服务器配置)
11. [配置下发流程](#11-配置下发流程)
12. [RTOS 任务架构](#12-rtos-任务架构)
13. [GPIO 引脚分配](#13-gpio-引脚分配)
14. [功能完整性对照表](#14-功能完整性对照表)
15. [已知注意事项](#15-已知注意事项)
---
## 1. 系统架构概述
本系统基于 CH32V307WCU6 (RISC-V) 微控制器,运行 FreeRTOS 实时操作系统,通过 WCHNET TCP/IP 协议栈与上位机通信。系统支持两种运行模式:**测试模式**(软件生成模拟热成像数据)和**正常模式**(通过 DVP 接口采集 Mini212G2 红外传感器真实数据)
本系统基于 CH32V307WCU6 (RISC-V) 微控制器,运行 FreeRTOS 实时操作系统,通过 WCHNET TCP/IP 协议栈与上位机通信。系统行为完全由运行时服务器配置驱动——服务器下发 Config2D/Config1D 并设置 `Enabled=1`MCU 自主执行触发检测和数据采集上报
### 硬件资源
@ -37,57 +45,63 @@
|------|------|
| FreeRTOS v202112.00 | 实时操作系统Heap=16KB |
| WCHNET | 万瑞 TCP/IP 协议栈 |
| QDX 协议栈 | 自定义 TLV 二进制通信协议 |
| 预处理模块 | 滑窗 ROI 搜索 + 温度统计 |
| QDX 协议栈 | 自定义 TLV 二进制通信协议 (0x55AA, v2.0) |
| 预处理模块 | 滑窗 ROI 搜索 + 温度统计 + 自动降尺寸 |
---
## 2. 运行模式说明
系统仅保留一个编译期宏开关 `TEST_PATTERN_MODE`控制数据来源。所有业务功能触发、采集、NG 输出、心跳)在两种模式下均始终启用,行为由服务器运行时配置驱动。
### 两种模式对比
| 对比项 | 测试模式 (`TEST_PATTERN_MODE=1`) | 正常模式 (`TEST_PATTERN_MODE=0`) |
|--------|------|------|
| **数据来源** | 软件生成模拟热图 (~10 FPS) | Mini212G2 传感器 DVP/DMA 采集 |
| **硬件依赖** | 仅需网口,无需传感器 | 需连接 Mini212G2 + DVP 线缆 |
| **初始化** | 跳过 `Mini212G2_Init()` 和 `DVP_Init()` | 执行传感器初始化 + DVP 配置 |
| **初始化** | 跳过 `DVP_Init()` | 执行 DVP 初始化 |
| **帧生成** | `task_test_pattern_entry` 任务周期填充 | DVP 行中断逐行 DMA 搬运 |
| **心跳显示** | `[HB] N tick=X TEST_MODE frm=Y` | `[HB] N tick=X dvp_frm=Y row_irq=Z` |
| **默认配置** | 自动加载测试配置64×64, 80°C 阈值) | 等待服务器下发,默认 1×1 |
| **适用场景** | 软件调试、协议验证、无硬件开发 | 生产环境、实际传感器采集 |
| **默认配置** | 自动注入测试配置2D Enabled=1, 内部触发, 80°C | 等待服务器下发,默认不使能 |
| **自主触发** | ✅ 立即工作(无需服务器) | ✅ 需服务器下发 Enabled=1 后激活 |
| **TEMP_REQ** | ✅ 可用(需对应模式 Enabled=1 | ✅ 可用 |
| **NG GPIO** | ✅ PA8 始终初始化 | ✅ PA8 始终初始化 |
| **外部触发 GPIO** | ✅ PA0/EXTI0 始终初始化 | ✅ PA0/EXTI0 始终初始化 |
---
## 3. 宏开关配置总表
## 3. 编译期常量总表
所有宏定义位于 `prj/TCPClient/User/main.c` 文件头部。
### 3.1 主模式开关
| 宏名 | 文件位置 | 当前值 | 说明 |
|------|---------|--------|------|
| `TEST_PATTERN_MODE` | main.c:18 | **1** | **主开关**1=测试模式0=正常模式 |
| 宏名 | 当前值 | 说明 |
|------|--------|------|
| `TEST_PATTERN_MODE` | **1** | **唯一编译开关**1=测试模式0=正常模式 |
### 3.2 功能子开关
| 宏名 | 当前值 | 控制功能 | 依赖条件 |
|------|--------|---------|---------|
| `TEST_ENABLE_HEARTBEAT` | **1** | 周期心跳调试打印2秒间隔 | 无 |
| `TEST_ENABLE_TRIGGER` | **0** | 内部触发 + 连拍上传 | 需配合阈值配置 |
| `TEST_ENABLE_NG_GPIO` | **0** | PA8 NG GPIO 脉冲输出 | 需硬件连接 |
| `TEST_ENABLE_TEMP_REQ` | **1** | 服务器按需帧请求响应 | 需服务器发送请求 |
| `TEST_ENABLE_TCP_SEND` | **1** | TCP 数据通道发送 | 需网络连接 |
### 3.3 其他编译期常量
### 3.2 业务常量
| 宏名 | 值 | 说明 |
|------|-----|------|
| `KEEPALIVE_ENABLE` | 1 | TCP KeepAlive 保持20s 探测) |
| `DEFAULT_BURST_COUNT` | 3 | 默认连拍帧数 |
| `DEFAULT_BURST_INTERVAL_MS` | 200 | 默认连拍帧间隔 (ms) |
| `DEFAULT_BURST_COUNT` | 3 | 服务器未配置时的默认连拍帧数 |
| `DEFAULT_BURST_INTERVAL_MS` | 200 | 服务器未配置时的默认连拍间隔 (ms) |
| `MAX_TCP_PAYLOAD_SIZE` | 10240 | TCP 发送缓冲区大小 (字节) |
| `TEST_FPS_DELAY_MS` | 100 | 测试模式帧间隔 (~10 FPS) |
| `TEST_1D_POINTS` | 30 | 1D 模式采样点数 |
| `KEEPALIVE_ENABLE` | 1 | TCP KeepAlive20s 空闲 / 15s 探测 / 9 次) |
| `TEST_FPS_DELAY_MS` | 100 | 测试模式帧生成间隔 (~10 FPS) |
| `SNAPSHOT_1D_POINTS` | 30 | TEMP_REQ 1D 空间采样点数 |
| `MAX_1D_POINTS` | 512 | 1D 采集缓冲最大样本数 |
| `NG_PULSE_MS` | 200 | NG 脉冲默认宽度(服务器可通过 NGioDelay 覆盖) |
### 3.3 GPIO 引脚宏
| 宏名 | 值 | 说明 |
|------|-----|------|
| `EXT_TRIG_GPIO_PORT` | GPIOA | 外部触发输入端口 |
| `EXT_TRIG_GPIO_PIN` | GPIO_Pin_15 | PA15 |
| `EXT_TRIG_EXTI_LINE` | EXTI_Line15 | 外部中断线 |
| `NG_GPIO_PORT` | GPIOA | NG 输出端口 |
| `NG_GPIO_PIN` | GPIO_Pin_8 | PA8 |
### 3.4 传感器相关宏 (dvp.h)
@ -104,15 +118,222 @@
| 宏名 | 当前值 | 说明 |
|------|--------|------|
| `SENSOR_USE_USART3` | 0 | 启用后必须为 1USART2 不可用) |
| `SENSOR_UART_ENABLE` | **0 (已禁用)** | MCU UART 配置已禁用,传感器通过 USB 预配置 |
| `SENSOR_UART_BAUD` | 115200 | 传感器 UART 波特率(未使用) |
| `SENSOR_UART_ENABLE` | **0 (已禁用)** | 传感器通过 USB 预配置 |
---
## 4. 测试模式详解
## 4. 自主触发架构详解
### 4.1 启用方式
### 4.1 核心设计原则
系统的所有业务行为由运行时服务器配置驱动,不再使用编译期功能开关:
```
服务器下发 Config2D (Enabled=1) → MCU 自主执行 2D 触发/连拍/上报
服务器下发 Config1D (Enabled=1) → MCU 自主执行 1D 采集/上报
两者互斥Config2D.Enabled 优先级高于 Config1D.Enabled
```
### 4.2 主循环逻辑流程
```
task_business_entry (优先级 5, 2ms 轮询)
├── [1] NG 脉冲超时检查 → 到期则拉低 PA8
├── [2] DVP_Task() (仅正常模式)
│ ├── 行 DMA 完成 → 复制到 FrameBuffer
│ └── 最后一行 → Frame_Ready_Flag=1
├── [3] TEMP_REQ 辅助通道(服务器按需截图)
│ ├── 前提:对应模式 Enabled=1
│ ├── is2D=1 → Preprocess_Execute() → TCP 发送 (frameType=0x02)
│ └── is2D=0 → send_1d_snapshot() → 30 点空间采样
└── [4] 自主触发主管线
│ 前提Frame_Ready_Flag=1 && TcpLogic_GetLatestConfig OK
├── Config2D.Enabled=1 → handle_2d_trigger()
2D 触发状态机:消抖 → 延迟 → 连拍)
└── Config1D.Enabled=1 → handle_1d_trigger()
1D 采集状态机IDLE → DEBOUNCE → COLLECTING
```
### 4.3 回调注册
| 回调 | 注册函数 | 触发时机 | 处理 |
|------|---------|---------|------|
| `OnConfigUpdate` | `TcpLogic_RegisterConfigCallback` | 服务器下发配置 | → `Preprocess_Settings_Change()` |
| `OnDetectionResult` | `TcpLogic_RegisterDetectionCallback` | 服务器返回检测结果 (resultStatus=0 为 NG) | → PA8 NG 脉冲 |
| `OnTempFrameRequest` | `TcpLogic_RegisterTempFrameRequestCallback` | 服务器请求截图 | → 设置 `g_temp_req_pending` |
---
## 5. 2D 触发状态机
### 5.1 状态机流程
```
┌─────────────┐
│ IDLE │
│ 等待触发 │
└──────┬──────┘
┌────────────┼────────────┐
│ TriggerMode=0 │ TriggerMode=1
│ (外部触发) │ (内部触发)
▼ ▼
PA0/EXTI0 上升沿 Preprocess_CheckInternalTrigger2D()
│ │ (TriggerRoi 区域 Max/Avg > Threshold)
▼ │
┌──────────────┐ │
│ DEBOUNCE │ │
│ DebounceMs │ │
└──────┬───────┘ │
│ │
└─────────┬───────────────┘
┌──────────────┐
│ DELAY │
│ DelayMs │
└──────┬───────┘
┌──────────────┐
│ BURST │──┐
│ 发送第1帧 │ │ 剩余 BurstCount-1 帧
└──────────────┘ │ 间隔 InternalIntervalMs
▲ │
└──────────┘
Burst 完成 → 回到 IDLE
```
### 5.2 关键 Config2D 字段
| 字段 | 类型 | 2D 触发中的作用 |
|------|------|----------------|
| `Enabled` | u8 | 1=启用 2D 管线 |
| `TriggerMode` | u8 | 0=外部(GPIO), 1=内部(温度) |
| `TriggerDebounceIntervalMs` | u16 | 外部触发消抖等待 (ms) |
| `TriggerDelayMs` | u16 | 触发确认后延迟采集 (ms) |
| `TriggerBurstCount` | u8 | 连拍帧数 |
| `TriggerInternalIntervalMs` | u16 | 连拍帧间隔 (ms) |
| `TriggerCondition` | u8 | 内部触发判定0=平均温度, 1=最大温度 |
| `TriggerTemperatureThreshold` | i16 | 内部触发温度阈值 (0.1°C/LSB) |
| `TriggerRoiX/Y/W/H` | u16×4 | 内部触发检测区域 |
| `TargetWidth` / `TargetHeight` | u16 | 预处理输出尺寸 |
| `NGioDelay` | u16 | NG 脉冲宽度 (ms) |
### 5.3 2D 帧发送路径
```
handle_2d_trigger() → start_2d_burst()
→ do_2d_capture_send()
→ Preprocess_Execute(raw, tx_buf, &meta) // 滑窗 ROI 搜索
→ TcpLogic_BuildAndSendTemperatureFrame(tx_buf, &meta, 0x01, 1)
// frameType=TRIGGER, is2D=1
```
---
## 6. 1D 采集状态机
### 6.1 状态机流程
```
┌─────────────┐
│ S1D_IDLE │ (RunMode 必须=1)
│ 等待触发 │
└──────┬──────┘
┌────────────┼────────────┐
│ TriggerType=0 │ TriggerType=1
│ (外部触发) │ (内部触发)
▼ ▼
PA0/EXTI0 上升沿 前置环形缓冲(3样本)
│ 连续3个 ≥ TriggerTempLimit
▼ │
┌───────────────┐ │
│ S1D_DEBOUNCE │ │
│ HighTimerLimit │ │
└──────┬────────┘ │
│ │
└─────────┬───────────────┘
┌────────────────┐
│ S1D_COLLECTING │
│ 逐帧采集温度点 │
│ (中心行最大值) │
└───────┬────────┘
┌────────┼────────┐
│ 停止条件1 │ 停止条件2
│ BufferSize 满 │ 已触发 + NgCountLimit 连续冷点
│ │
└────────┬────────┘
切片 [LSizeStart, -RSizeStart]
send_1d_collection() → TCP 发送
回到 S1D_IDLE
```
### 6.2 1D 温度采样方式
每个 DVP 帧产生一个温度样本:取中心行所有像素的最大温度值(`get_1d_sample()`)。
### 6.3 内部触发预环形缓冲
内部触发模式维护长度为 3 的环形缓冲:
- 每帧写入一个样本到环形缓冲
- 当 3 个样本全部 ≥ `TriggerTempLimit` → 进入 COLLECTING 状态
- 环形缓冲内容(最旧在前)写入采集数组前 3 位
- 从第 4 位起继续采集
### 6.4 关键 Config1D 字段
| 字段 | 类型 | 1D 采集中的作用 |
|------|------|----------------|
| `Enabled` | u8 | 1=启用 1D 管线 |
| `RunMode` | u8 | 0=STOP, 1=RUN |
| `TriggerType` | u8 | 0=外部(GPIO), 1=内部(温度) |
| `BufferSize` | u16 | 单次采集最大样本数(停止条件) |
| `TriggerTempLimit` | i16 | 触发/采集温度阈值 (0.1°C/LSB) |
| `HighTimerLimit` | u16 | 外部触发消抖时间 (ms) |
| `NgCountLimit` | u8 | 连续冷点数→停止采集 |
| `LSizeStart` | u16 | 切片左起始偏移 |
| `RSizeStart` | u16 | 切片右侧移除点数 |
### 6.5 1D 数据打包格式
每个样本点 4 字节 (Little-Endian)
| 偏移 | 类型 | 说明 |
|------|------|------|
| 0-1 | u16 LE | TimeOffset (距采集开始的毫秒偏移) |
| 2-3 | u16 LE | Temperature (0.1°C/LSB) |
---
## 7. TEMP_REQ 辅助通道
TEMP_REQ 是服务器主动请求的按需截图通道,与自主触发管线独立工作:
- **2D 截图**`Preprocess_Execute()` → 滑窗搜索 → 发送 `frameType=0x02`MASKED
- **1D 截图**`send_1d_snapshot()` → 从中心行等间距采样 30 点 → 发送
前提:对应模式 `Enabled=1`,否则请求被忽略并打印 `TEMP_REQ ignored`
---
## 8. 测试模式详解
### 8.1 启用方式
```c
#define TEST_PATTERN_MODE 1
@ -120,7 +341,7 @@
编译烧录即可,**无需连接任何传感器硬件**,仅需以太网连接。
### 4.2 测试图案
### 8.2 测试图案
系统自动循环生成 4 种测试图案256×192 Y16 格式):
@ -133,65 +354,50 @@
总周期40 帧 ≈ 4 秒循环一次。
### 4.3 默认配置参数
### 8.3 自动注入测试配置
测试模式启动时自动加载以下配置(无需服务器下发即可工作
测试模式启动时通过 `TcpLogic_InjectTestConfig()` 注入以下配置(走标准缓存+回调路径
```
TargetWidth = 64 # 预处理输出宽度
TargetHeight = 64 # 预处理输出高度
TriggerRoiX = 0 # 触发检测区域起点 X
TriggerRoiY = 0 # 触发检测区域起点 Y
TriggerRoiW = 256 # 触发检测区域宽度(全幅)
TriggerRoiH = 192 # 触发检测区域高度(全幅)
TriggerCondition = 1 # 触发条件1=最大温度
TriggerTemperatureThreshold = 800 # 80.0°C (0.1°C/LSB)
TriggerBurstCount = 3 # 连拍帧数
TriggerInternalIntervalMs = 200 # 连拍间隔 (ms)
NGioDelay = 200 # NG 脉冲宽度 (ms)
Config2D:
Enabled = 1 # 自动启用 2D 管线
TriggerMode = 1 # 内部触发(温度检测)
TargetWidth = 64 # 预处理输出 64×64
TargetHeight = 64
TriggerRoiX/Y = 0, 0 # 全幅扫描
TriggerRoiW/H = 256, 192
TriggerCondition = 1 # 最大温度
TriggerTemperatureThreshold = 800 # 80.0°C
TriggerBurstCount = 3 # 3 帧连拍
TriggerInternalIntervalMs = 200
NGioDelay = 200
```
> **注意**:如果服务器连接后下发了 Config2D将覆盖上述默认配置
> **注入路径**`TcpLogic_InjectTestConfig()` → 内部缓存 + `has_valid_config=1` → 触发 `OnConfigUpdate` 回调 → `Preprocess_Settings_Change()`。与服务器下发配置走完全相同的路径
### 4.4 测试模式数据流
### 8.4 启动串口输出示例
```
task_test_pattern_entry (优先级 4, 100ms 周期)
│ 生成 256×192 Y16 测试图案
│ 写入 FrameBuffer[]
│ 设置 Frame_Ready_Flag=1
task_business_entry (优先级 5)
├── 检查 g_temp_req_pending (服务器请求)
│ ├── is2D=1 → Preprocess_Execute() → 滑窗搜索 → 发送 2D 帧
│ └── is2D=0 → send_1d_frame_from_raw() → 采样中心行 → 发送 1D 帧
└── 检查 Frame_Ready_Flag (仅 TRIGGER 启用时)
└── Preprocess_CheckInternalTrigger2D()
└── 触发 → 连拍发送
```
### 4.5 启动串口输出示例
```
TCPClient Test
TCPClient
SystemClk:144000000
=== Feature Switches ===
PATTERN=1 TRIGGER=0 NG_GPIO=0 TEMP_REQ=1 TCP_SEND=1 HB=1
TEST_PATTERN=1
UserByte: c0
=== TEST PATTERN MODE === No sensor/DVP hardware needed
net version:1c
WCHNET_LibInit Success
Test default config loaded: trigger thresh=800 ROI=full burst=3
Test config injected: En=1 TrigMode=Internal thresh=800 Tgt=64x64 burst=3
[HB] 0 tick=1010 TEST_MODE frm=9
[HB] 1 tick=3010 TEST_MODE frm=29
2D internal trigger
TRIGGER frm=21
2D SEND frm=21 64x64 ret=0
Burst start: 3 frames, interval=200 ms
```
---
## 5. 正常模式详解
## 9. 正常模式详解
### 5.1 启用方式
### 9.1 启用方式
```c
#define TEST_PATTERN_MODE 0
@ -199,9 +405,9 @@ Test default config loaded: trigger thresh=800 ROI=full burst=3
需要:
1. Mini212G2 传感器正确连接 DVP 接口
2. 传感器已通过 USB 或 UART 预配置为 CMOS/DVP Y16 输出
2. 传感器已通过 USB 预配置为 CMOS/DVP Y16 输出
### 5.2 硬件连接
### 9.2 硬件连接
#### DVP 引脚定义
@ -231,20 +437,7 @@ Test default config loaded: trigger thresh=800 ROI=full burst=3
> 注意:启用 USART3 传感器通信会占用 printf 调试端口,需通过 JTAG 调试。
### 5.3 传感器配置选项
修改 `mini212g2.h`
```c
// 当前状态MCU UART 配置已禁用(#if 0
// 模组已通过 USB 预配置,无需 MCU 配置
// 如需重新启用 MCU UART 配置,将 mini212g2.h 中的 #if 0 改为 #if 1
// 仅可使用 USART3 (PB10/PB11)USART2 引脚已被占用
// ⚠ 启用后 printf 串口不可用,需通过 JTAG 调试
```
### 5.4 正常模式数据流
### 9.3 正常模式数据流
```
DVP 硬件中断 (DVP_IRQHandler)
@ -257,40 +450,23 @@ task_business_entry (优先级 5)
│ ├── 复制 DMA 行缓冲 → FrameBuffer[line_idx]
│ └── 最后一行 → Frame_Ready_Flag=1
├── 检查 g_temp_req_pending (服务器请求)
│ ├── is2D=1 → Preprocess_Execute() → 发送
│ └── is2D=0 → 1D 采样 → 发送
├── [TEMP_REQ] 服务器按需截图
└── 检查 Frame_Ready_Flag (TRIGGER 启用时)
└── Preprocess_CheckInternalTrigger2D()
└── [自主触发] Config2D.Enabled → 2D 状态机
Config1D.Enabled → 1D 状态机
```
### 5.5 正常模式注意事项
### 9.4 正常模式注意事项
- **无默认预处理参数**:正常模式不加载测试默认配置,`Preprocess_Init()` 仅设置 `TargetWidth=1, TargetHeight=1`。必须等服务器下发 Config2D 后才能获取正确的目标尺寸。
- **传感器初始化检查**:可通过 JTAG 查看 `sensor_init_ok` / `sensor_init_fail` 变量确认初始化结果。
- **帧率取决于传感器**Mini212G2 在 DVP 模式下的帧率由传感器内部配置决定,不通过软件控制。
### 5.6 启动串口输出示例(预期)
```
TCPClient Test
SystemClk:144000000
=== Feature Switches ===
PATTERN=0 TRIGGER=0 NG_GPIO=0 TEMP_REQ=1 TCP_SEND=1 HB=1
UserByte: c0
[DVP] Init done CR0=0x2C CR1=0x04 ROW=1 COL=512
[DVP] DMA_BUF0=0x200XXXXX DMA_BUF1=0x200XXXXX
net version:1c
WCHNET_LibInit Success
[HB] 0 tick=1010 dvp_frm=0 row_irq=0
```
- **无默认配置**:正常模式不注入测试配置,`Preprocess_Init()` 仅设置 `TargetWidth=1, TargetHeight=1`。必须等服务器下发 Config2D/Config1D 后才能工作。
- **传感器初始化**:模组已通过 USB 预配置,`mini212g2.h` 中 UART 配置已关闭。
- **帧率取决于传感器**Mini212G2 在 DVP 模式下的帧率由传感器内部配置决定。
---
## 6. 网络与服务器配置
## 10. 网络与服务器配置
### 6.1 MCU 网络参数
### 10.1 MCU 网络参数
定义位置:`main.c`
@ -301,7 +477,7 @@ WCHNET_LibInit Success
| 子网掩码 | 255.255.255.0 | C 类子网 |
| MAC 地址 | 由芯片自动获取 | 硬件 MAC |
### 6.2 服务器连接参数
### 10.2 服务器连接参数
定义位置:`qdx_tcp_logic.c`
@ -311,47 +487,62 @@ WCHNET_LibInit Success
| `CONTROL_PORT` | 5511 | 控制通道端口 |
| `DATA_PORT` | 5512 | 数据通道端口 |
> **修改方法**:直接编辑 `qdx_tcp_logic.c` 中的常量值,重新编译。
### 6.3 TCP KeepAlive
### 10.3 TCP KeepAlive
```c
#define KEEPALIVE_ENABLE 1 // main.c
// 探测配置: 20s 空闲后每 15s 探测一次,最多 9 次
```
### 10.4 TCP 内部参数
| 参数 | 值 | 说明 |
|------|-----|------|
| `HEARTBEAT_INTERVAL_MS` | 2000 | 心跳发送间隔 |
| `SERVER_TIMEOUT_MS` | 6000 | 服务器超时断连 |
| `RECONNECT_DELAY_MS` | 3000 | 断连后重连等待 |
| `MAX_FRAGMENT_PAYLOAD` | 1400 | 分片最大载荷 |
---
## 7. 配置下发流程
## 11. 配置下发流程
### 7.1 TLV 配置类型
### 11.1 TLV 消息类型
| TLV Type | 名称 | 说明 |
|----------|------|------|
| 0x20 | TYPE_CONFIG_COMMON | 通用配置(模式、严格标记等) |
| 0x22 | TYPE_CONFIG_2D | 2D 配置(目标尺寸、触发、连拍等) |
| 0x23 | TYPE_CONFIG_1D | 1D 配置 |
| TLV Type | 名称 | 方向 | 说明 |
|----------|------|------|------|
| 0x01 | TYPE_HANDSHAKE | ↔ | 握手UUID、版本 |
| 0x02 | TYPE_HEARTBEAT | ↔ | 心跳(上行/下行) |
| 0x05 | TYPE_DEVID_ASSIGN | ← | 设备 ID 分配(触发重连) |
| 0x10 | TYPE_TEMP_FRAME | ← | 请求温度帧TEMP_REQ |
| 0x20 | TYPE_CONFIG_COMMON | ← | 通用配置 |
| 0x22 | TYPE_CONFIG_2D | ← | 2D 配置 |
| 0x23 | TYPE_CONFIG_1D | ← | 1D 配置 |
| 0x30 | TYPE_ACK_PAYLOAD | ← | ACK 响应 |
| 0x40 | TYPE_DETECTION_RESULT | ← | 检测结果OK/NG |
### 7.2 Config2D 关键字段
### 11.2 Config2D 关键字段
| 字段 | 偏移 | 类型 | 说明 |
|------|------|------|------|
| Width | 4 | u16 LE | 设备/传感器分辨率宽度(仅标记,不影响预处理) |
| Height | 6 | u16 LE | 设备/传感器分辨率高度(仅标记,不影响预处理) |
| **TargetWidth** | **23** | **u16 LE** | **预处理输出宽度(直接控制帧大小)** |
| **TargetHeight** | **25** | **u16 LE** | **预处理输出高度(直接控制帧大小)** |
| Fps | 8 | u16 LE | 帧率 |
| TriggerCondition | 27 | u8 | 0=平均, 1=最大 |
| TriggerTemperatureThreshold | 28 | u16 LE | 触发温度阈值 (0.1°C/LSB) |
| Enabled | 0 | u8 | **使能开关** |
| Width | 4 | u16 LE | 设备分辨率宽度(仅标记) |
| Height | 6 | u16 LE | 设备分辨率高度(仅标记) |
| **TargetWidth** | **23** | **u16 LE** | **预处理输出宽度** |
| **TargetHeight** | **25** | **u16 LE** | **预处理输出高度** |
| TriggerMode | 21 | u8 | 0=外部, 1=内部 |
| TriggerCondition | 37 | u8 | 0=平均, 1=最大 |
| TriggerTemperatureThreshold | 28 | i16 LE | 触发阈值 (0.1°C/LSB) |
| TriggerBurstCount | 30 | u8 | 连拍帧数 |
| TriggerInternalIntervalMs | 31 | u16 LE | 连拍帧间隔 (ms) |
| TriggerRoiX/Y/W/H | 33-40 | u16 LE ×4 | 触发检测 ROI 区域 |
| TriggerInternalIntervalMs | 31 | u16 LE | 连拍间隔 (ms) |
| TriggerDebounceIntervalMs | 35 | u16 LE | 外部触发消抖 (ms) |
| TriggerDelayMs | 23 | u16 LE | 触发后延迟 (ms) |
| TriggerRoiX/Y/W/H | 33-40 | u16 LE ×4 | 内部触发检测 ROI |
| NGioDelay | 41 | u16 LE | NG 脉冲宽度 (ms) |
> **重要**Width/Height 与 TargetWidth/TargetHeight 是两组不同的字段!
> - `Width/Height`:设备分辨率标记(偏移 4-7**不影响预处理**
> - `TargetWidth/TargetHeight`:预处理实际输出尺寸(偏移 23-26**直接决定帧数据大小**
> **重要**`Width/Height``TargetWidth/TargetHeight` 是两组不同的字段!前者仅做标识,后者直接控制帧大小。
### 7.3 缓冲区容量约束
### 11.3 缓冲区容量约束
```
MAX_TCP_PAYLOAD_SIZE = 10240 字节
@ -360,189 +551,119 @@ HeadOffset = 64 字节(协议头预留)
最大像素数 = 10176 / 2 = 5088 像素
推荐安全目标尺寸:
64 × 64 = 4096 像素 = 8192 字节 ✓ (< 10176)
71 × 71 = 5041 像素 = 10082 字节 ✓ (< 10176)
72 × 72 = 5184 像素 = 10368 字节 ✗ (> 10176触发降尺寸)
64 × 64 = 4096 像素 = 8192 字节 ✓
71 × 71 = 5041 像素 = 10082 字节 ✓
72 × 72 = 5184 像素 = 10368 字节 ✗ (触发降尺寸)
```
### 7.4 自动降尺寸保护
### 11.4 自动降尺寸保护
当服务器下发的 TargetWidth × TargetHeight × 2 超过可用缓冲区时,MCU 会自动等比减半目标尺寸直到适合
TargetWidth × TargetHeight × 2 超过可用缓冲区时,自动等比减半:
```
示例:服务器设 TargetWidth=185, TargetHeight=70
185 × 70 × 2 = 25,900 > 10,176 → 减半
93 × 35 × 2 = 6,510 < 10,176 使用 93×35
日志输出PP: target clamped 185x70 -> 93x35 (buf=10176)
示例:服务器设 185×70 → 185×70×2=25900 > 10176 → 减半 → 93×35
日志PP: target clamped 185x70 -> 93x35 (buf=10176)
```
### 7.5 配置传递链
### 11.5 配置传递链
```
服务器 TCP 下发 TLV
→ qdx_tcp_logic.c: parse_and_dispatch_tlv()
→ 反序列化 → cached_cfg2d / cached_cfg1d / cached_common
→ 反序列化 → cached_cfg{2d,1d,common}
→ OnConfigUpdate() 回调
→ Preprocess_Settings_Change() 互斥锁更新
→ 下次 Preprocess_Execute() 使用新配置
→ 下次帧处理使用新配置
```
---
## 8. RTOS 任务架构
## 12. RTOS 任务架构
| 任务名 | 优先级 | 栈 (words) | 条件 | 功能 |
|--------|--------|-----------|------|------|
| `wchnet` | 6 (最高) | 512 | 始终创建 | WCHNET 协议栈轮询 |
| `business` | 5 | 512 | `TEST_ENABLE_TCP_SEND=1` | DVP/预处理/发送主循环 |
| `testpat` | 4 | 256 | `TEST_PATTERN_MODE=1` | 测试图案生成 |
| `hb` | 3 (最低) | 256 | `TEST_ENABLE_HEARTBEAT=1` | 心跳调试打印 |
| TcpLogic 内部 | - | - | 始终创建 | 管理线程 (连接/心跳) |
> 测试模式创建 4 个任务,正常模式创建 3 个(无 testpat
| `wchnet` | 6 (最高) | 512 | 始终创建 | WCHNET 协议栈轮询 (5ms) |
| `business` | 5 | 512 | 始终创建 | DVP + 触发状态机 + 发送 |
| `testpat` | 4 | 256 | `TEST_PATTERN_MODE=1` | 测试图案生成 (~10 FPS) |
| `hb` | 3 (最低) | 256 | 始终创建 | 心跳调试打印 (2s) |
| TcpLogic 内部 | - | - | 始终创建 | 管理线程 (连接/心跳/重连) |
---
## 9. 切换模式操作指南
## 13. GPIO 引脚分配
### 9.1 从测试模式 → 正常模式
1. **修改宏**:打开 `prj/TCPClient/User/main.c`,将:
```c
#define TEST_PATTERN_MODE 1
```
改为:
```c
#define TEST_PATTERN_MODE 0
```
2. **检查传感器配置**:当前模组已通过 USB 预配置,`mini212g2.h` 中 UART 配置已用 `#if 0` 关闭,无需修改。如需重新启用,将 `#if 0` 改为 `#if 1`
3. **检查功能开关**:根据需要启用/禁用各子功能:
```c
#define TEST_ENABLE_TRIGGER 1 // 启用内部触发
#define TEST_ENABLE_NG_GPIO 1 // 启用 NG 输出(需接 PA8
```
4. **编译烧录**:使用 MRS IDE 编译并烧录。
### 9.2 从正常模式 → 测试模式
1. **修改宏**`TEST_PATTERN_MODE` 改为 `1`
2. **无需断开传感器**:测试模式跳过所有硬件初始化,传感器接线不影响
3. **编译烧录**
### 9.3 功能开关独立配置
各功能子开关在两种模式下均可独立启用/禁用,无互相依赖:
| 开关组合 | 行为 |
|---------|------|
| PATTERN=1, TRIGGER=1 | 测试模式 + 内部触发连拍(测试热点图案会触发) |
| PATTERN=1, TRIGGER=0, TEMP_REQ=1 | 测试模式 + 服务器按需请求(当前配置) |
| PATTERN=0, TRIGGER=1, TEMP_REQ=1 | 正常模式 + 自动触发 + 服务器请求(完整功能) |
| PATTERN=0, TRIGGER=0, TEMP_REQ=1 | 正常模式 + 仅服务器请求(最简运行) |
| TCP_SEND=0 | 禁用 business 任务,系统仅运行网络协议但不处理/发送帧 |
| GPIO | 功能 | 方向 | 配置 | 说明 |
|------|------|------|------|------|
| PA15 | 外部触发输入 | 输入 | 下拉 + EXTI15 上升沿 | 2D/1D 共用(互斥) |
| PA8 | NG 输出 | 推挽输出 | 低速 2MHz | 检测 NG 时脉冲拉高 |
| PA4-PA6, PA9-PA10, PB3, PB8-PB9 | DVP 数据线 | 输入 | 浮空 | 仅正常模式使用 |
| PC8, PC9, PC11 | DVP HSYNC/VSYNC/PCLK | 输入 | - | 仅正常模式使用 |
| PA2, PA3 | **已占用** | - | - | USART2不可用 |
---
## 10. 常见配置场景
## 14. 功能完整性对照表
### 场景 A纯软件协议调试无硬件
### 协议规范 vs MCU 实现
```c
#define TEST_PATTERN_MODE 1
#define TEST_ENABLE_HEARTBEAT 1
#define TEST_ENABLE_TRIGGER 0
#define TEST_ENABLE_NG_GPIO 0
#define TEST_ENABLE_TEMP_REQ 1
#define TEST_ENABLE_TCP_SEND 1
```
**适用**:验证 TLV 协议、握手流程、Config 下发、帧分片发送等。
### 场景 B测试内部触发 + 连拍
```c
#define TEST_PATTERN_MODE 1
#define TEST_ENABLE_TRIGGER 1 // ← 打开
#define TEST_ENABLE_NG_GPIO 0
#define TEST_ENABLE_TEMP_REQ 1
#define TEST_ENABLE_TCP_SEND 1
```
**适用**:验证 80°C 触发阈值、3 帧连拍、连拍间隔。热点和棋盘格图案会触发。
### 场景 C正常模式完整功能
```c
#define TEST_PATTERN_MODE 0
#define TEST_ENABLE_TRIGGER 1
#define TEST_ENABLE_NG_GPIO 1 // ← PA8 接报警
#define TEST_ENABLE_TEMP_REQ 1
#define TEST_ENABLE_TCP_SEND 1
```
**适用**:生产运行,传感器实时采集 + 自动触发 + NG 报警 + 服务器请求。
### 场景 D正常模式最小功能验证
```c
#define TEST_PATTERN_MODE 0
#define TEST_ENABLE_TRIGGER 0
#define TEST_ENABLE_NG_GPIO 0
#define TEST_ENABLE_TEMP_REQ 1 // 仅响应服务器请求
#define TEST_ENABLE_TCP_SEND 1
```
**适用**:验证传感器数据采集 + 网络发送,无自动触发逻辑干扰。
### 场景 E网络连接诊断
```c
#define TEST_PATTERN_MODE 1
#define TEST_ENABLE_HEARTBEAT 1
#define TEST_ENABLE_TRIGGER 0
#define TEST_ENABLE_NG_GPIO 0
#define TEST_ENABLE_TEMP_REQ 0 // 关闭帧请求
#define TEST_ENABLE_TCP_SEND 0 // 关闭 business 任务
```
**适用**:仅测试 TCP 连接/断开/重连,不涉及数据帧处理。心跳打印确认系统活跃。
| 协议功能 | 实现状态 | 备注 |
|---------|---------|------|
| 握手 (Handshake) | ✅ | UUID + HW/FW 版本 |
| 心跳 (Heartbeat) | ✅ | 2s 间隔6s 超时断连 |
| 设备 ID 分配 (DevID Assign) | ✅ | 触发自动重连 |
| Config Common/2D/1D 下发 | ✅ | 解析+缓存+回调 |
| ACK 响应 | ✅ | 发送+接收 |
| TEMP_REQ 截图 | ✅ | 2D 预处理 / 1D 空间采样 |
| Detection Result | ✅ | NG → PA8 脉冲 |
| 2D 外部触发 | ✅ | GPIO → 消抖 → 延迟 → 连拍 |
| 2D 内部触发 | ✅ | ROI Max/Avg → 延迟 → 连拍 |
| 1D 外部触发 | ✅ | GPIO → 消抖 → 采集 → 切片 |
| 1D 内部触发 | ✅ | 3 样本预环形 → 采集 → 切片 |
| 温度矩阵分片发送 | ✅ | 每片 ≤1400 字节 |
| TCP KeepAlive | ✅ | 20s/15s/9次 |
| CRC16-MODBUS 校验 | ✅ | 每帧校验 |
| IsLive 实时流 | ❌ | MCU 资源不足,触发场景不需要 |
| Training 训练模式 | ❌ | 服务器端完成 |
| SyncTime 时间同步 | ❌ | 协议已定义 (0x03),未解析 |
| StatusEvent 状态上报 | ❌ | 服务器通过心跳判断在线 |
| Alarm GPIO | ❌ | 结构体有字段,硬件未接线 |
| 1D StartPointsToRemove / ReferenceLength / TimerCLimit | ❌ | 已解析到结构体,业务逻辑未使用 |
---
## 11. 已知注意事项
## 15. 已知注意事项
### 11.1 温度单位
### 15.1 温度单位
系统统一使用 **0.1°C/LSB**
- 温度值 350 = 35.0°C
- 温度值 900 = 90.0°C
- `TEMP_RAW(deg_c) = (uint16_t)(deg_c * 10)`
### 11.2 TriggerTemperatureThreshold 值
### 15.2 Config2D 字段区分
温度阈值使用 0.1°C/LSB 单位,与系统所有温度值一致。当前测试默认 `TriggerTemperatureThreshold = 800`80.0°C测试热点图案 90.0°C值 900可正常触发。
- **Width / Height**:设备分辨率标识(如 256×192不影响 MCU 预处理
- **TargetWidth / TargetHeight**:预处理实际输出尺寸,**必须正确设置**
### 11.3 服务器 Config2D 字段区分
### 15.3 正常模式默认目标尺寸
- **Width / Height**(偏移 4-7设备分辨率标识如 256×192不影响 MCU 预处理
- **TargetWidth / TargetHeight**(偏移 23-26预处理输出目标尺寸**必须正确设置**
`TargetWidth=1, TargetHeight=1`(最小默认),必须等服务器下发 Config2D 后才能正常输出。
如果服务器只设了 Width/Height=64×64 而未设 TargetWidth/TargetHeightMCU 仍会使用超大目标尺寸(或上次缓存值),导致 `PP fail ret=-3` 或触发自动降尺寸。
### 15.4 IP 与端口硬编码
### 11.4 正常模式默认目标尺寸
正常模式启动时 `TargetWidth=1, TargetHeight=1`(最小默认),必须等服务器下发 Config2D 后才会更新为正确值。测试模式自动加载 64×64。
### 11.5 IP 与端口硬编码
当前网络参数IP、端口为编译期常量更改需修改源码并重新编译
网络参数为编译期常量,更改需修改源码并重新编译:
- MCU IP`main.c``IPAddr[]`
- 服务器 IP`qdx_tcp_logic.c``SERVER_IP`
- 端口号:`qdx_tcp_logic.c``CONTROL_PORT` / `DATA_PORT`
### 11.6 printf 限制
### 15.5 printf 限制
使用 newlib-nano仅支持 `%d, %x, %s` 格式。所有 `uint32_t` 变量必须强制转换 `(int)` 后打印,否则可能 HardFault。不支持 `%u, %lu, %f`
使用 newlib-nano仅支持 `%d, %x, %s` 格式。所有 `uint32_t` 必须 `(int)` 强转后打印,否则 HardFault。不支持 `%u, %lu, %f`
### 15.6 双缓冲发送
系统使用 A/B 交替 `TcpTxBuffer_t`,避免发送过程中缓冲区被覆盖。`use_buffer_A` 标志在每次发送后翻转。
### 15.7 2D/1D 互斥
主循环中 `Config2D.Enabled` 优先级高于 `Config1D.Enabled`。若两者同时 Enabled=1仅 2D 管线运行。

View File

@ -2,6 +2,7 @@
#include "ch32v30x_dvp.h"
#include "eth_driver.h"
#include "string.h"
#include "qdx_port.h"
__attribute__((aligned(4))) uint8_t DMA_LineBuf0[BYTES_PER_LINE];
__attribute__((aligned(4))) uint8_t DMA_LineBuf1[BYTES_PER_LINE];
@ -63,10 +64,10 @@ void DVP_Init(void)
DVP->CR1 = RB_DVP_DMA_EN; /* DMA on, CM=0 continuous, no reset bits */
DVP->CR0 |= RB_DVP_ENABLE;
printf("[DVP] Init done CR0=0x%02x CR1=0x%02x ROW=%d COL=%d\r\n",
DBG_INIT("DVP CR0=0x%02x CR1=0x%02x ROW=%d COL=%d\r\n",
(int)(DVP->CR0 & 0xFF), (int)(DVP->CR1 & 0xFF),
(int)DVP->ROW_NUM, (int)DVP->COL_NUM);
printf("[DVP] DMA_BUF0=0x%08x DMA_BUF1=0x%08x\r\n",
DBG_INIT("DVP DMA_BUF0=0x%08x DMA_BUF1=0x%08x\r\n",
(int)DVP->DMA_BUF0, (int)DVP->DMA_BUF1);
}

View File

@ -1,6 +1,7 @@
#include "mini212g2.h"
#include "debug.h"
#include "string.h"
#include "qdx_port.h"
#if SENSOR_UART_ENABLE
@ -165,7 +166,7 @@ int Mini212G2_SendCmd(const uint8_t *cmd, uint8_t len)
if (n == ACK_LEN && memcmp(resp, ACK_PATTERN, ACK_LEN) == 0)
return 0;
printf("[Sensor] resp(%d):", n);
DBG_INIT("Sensor resp(%d):", n);
for (int j = 0; j < n; j++) printf(" %02x", resp[j]);
printf("\r\n");
return -1;
@ -176,18 +177,18 @@ int Mini212G2_SendCmd(const uint8_t *cmd, uint8_t len)
int Mini212G2_Init(void)
{
#if !SENSOR_UART_ENABLE
printf("[Sensor] UART disabled, assuming pre-configured via USB\r\n");
printf("[Sensor] Expected: CMOS output, CMOS8(MSB), Y16, 30Hz\r\n");
DBG_INIT("Sensor: UART disabled, pre-configured via USB\r\n");
DBG_INIT("Sensor: CMOS output, CMOS8(MSB), Y16, 30Hz\r\n");
return 0;
#else
int ok = 0, fail = 0;
Sensor_UART_Init();
printf("[Sensor] UART init %d, baud=%d\r\n",
DBG_INIT("Sensor: UART%d baud=%d\r\n",
(int)(SENSOR_UART == USART3 ? 3 : 2), (int)SENSOR_UART_BAUD);
/* Sensor needs several seconds to boot after power-on */
printf("[Sensor] Waiting 3s for sensor boot...\r\n");
DBG_INIT("Sensor: Waiting 3s for boot...\r\n");
Delay_Ms(3000);
/* === Communication test: send status query === */
@ -204,51 +205,51 @@ int Mini212G2_Init(void)
}
Delay_Ms(200);
printf("[Sensor] Configuring CMOS/DVP output...\r\n");
DBG_INIT("Sensor: Configuring CMOS/DVP output...\r\n");
/* 1. Shutter compensation */
if (Mini212G2_SendCmd(CMD_SHUTTER, sizeof(CMD_SHUTTER)) == 0)
{ ok++; printf("[Sensor] Shutter OK\r\n"); }
{ ok++; DBG_INIT("Sensor: Shutter OK\r\n"); }
else
{ fail++; printf("[Sensor] Shutter FAIL\r\n"); }
{ fail++; DBG_ERR("Sensor: Shutter FAIL\r\n"); }
Delay_Ms(200);
/* 2. Digital port → CMOS */
if (Mini212G2_SendCmd(CMD_DIGITAL_CMOS, sizeof(CMD_DIGITAL_CMOS)) == 0)
{ ok++; printf("[Sensor] Digital->CMOS OK\r\n"); }
{ ok++; DBG_INIT("Sensor: Digital->CMOS OK\r\n"); }
else
{ fail++; printf("[Sensor] Digital->CMOS FAIL\r\n"); }
{ fail++; DBG_ERR("Sensor: Digital->CMOS FAIL\r\n"); }
Delay_Ms(200);
/* 3. CMOS interface → 8-bit MSB */
if (Mini212G2_SendCmd(CMD_CMOS8_MSB, sizeof(CMD_CMOS8_MSB)) == 0)
{ ok++; printf("[Sensor] CMOS8(MSB) OK\r\n"); }
{ ok++; DBG_INIT("Sensor: CMOS8(MSB) OK\r\n"); }
else
{ fail++; printf("[Sensor] CMOS8(MSB) FAIL\r\n"); }
{ fail++; DBG_ERR("Sensor: CMOS8(MSB) FAIL\r\n"); }
Delay_Ms(200);
/* 4. CMOS content → Y16 (raw 16-bit thermal) */
if (Mini212G2_SendCmd(CMD_CONTENT_Y16, sizeof(CMD_CONTENT_Y16)) == 0)
{ ok++; printf("[Sensor] Y16 OK\r\n"); }
{ ok++; DBG_INIT("Sensor: Y16 OK\r\n"); }
else
{ fail++; printf("[Sensor] Y16 FAIL\r\n"); }
{ fail++; DBG_ERR("Sensor: Y16 FAIL\r\n"); }
Delay_Ms(200);
/* 5. Frame rate → 30Hz */
if (Mini212G2_SendCmd(CMD_FPS_30HZ, sizeof(CMD_FPS_30HZ)) == 0)
{ ok++; printf("[Sensor] 30Hz OK\r\n"); }
{ ok++; DBG_INIT("Sensor: 30Hz OK\r\n"); }
else
{ fail++; printf("[Sensor] 30Hz FAIL\r\n"); }
{ fail++; DBG_ERR("Sensor: 30Hz FAIL\r\n"); }
Delay_Ms(200);
/* 6. Save settings to flash */
if (Mini212G2_SendCmd(CMD_SAVE, sizeof(CMD_SAVE)) == 0)
{ ok++; printf("[Sensor] Save OK\r\n"); }
{ ok++; DBG_INIT("Sensor: Save OK\r\n"); }
else
{ fail++; printf("[Sensor] Save FAIL\r\n"); }
{ fail++; DBG_ERR("Sensor: Save FAIL\r\n"); }
Delay_Ms(500);
printf("[Sensor] Config result: %d ok, %d fail\r\n", ok, fail);
DBG_INIT("Sensor: %d ok, %d fail\r\n", ok, fail);
sensor_init_ok = (uint8_t)ok;
sensor_init_fail = (uint8_t)fail;
@ -268,7 +269,7 @@ void Mini212G2_PrintDigitalVideoStatus(void)
int n = Sensor_ReadResp(resp, sizeof(resp), 500);
if (n < 24) {
printf("[Sensor] Query failed (got %d bytes)\r\n", n);
DBG_ERR("Sensor: Query failed (got %d bytes)\r\n", n);
return;
}
@ -292,16 +293,16 @@ void Mini212G2_PrintDigitalVideoStatus(void)
uint8_t fps = resp[9];
uint8_t clk_phase = resp[11];
printf("[Sensor] Digital Video Status:\r\n");
printf(" Port: %s (%d)\r\n",
DBG_INIT("Sensor Digital Video Status:\r\n");
DBG_INIT(" Port: %s (%d)\r\n",
(port_type < 10) ? port_names[port_type] : "?", (int)port_type);
printf(" Content: %s (%d)\r\n",
DBG_INIT(" Content: %s (%d)\r\n",
(content < 6) ? content_names[content] : "?", (int)content);
printf(" Interface: %s (%d)\r\n",
DBG_INIT(" Interface: %s (%d)\r\n",
(iface < 3) ? iface_names[iface] : "?", (int)iface);
printf(" FPS: %s (%d)\r\n",
DBG_INIT(" FPS: %s (%d)\r\n",
(fps < 4) ? fps_names[fps] : "?", (int)fps);
printf(" ClkPhase: %s (%d)\r\n",
DBG_INIT(" ClkPhase: %s (%d)\r\n",
(clk_phase == 0) ? "Rising" : "Falling", (int)clk_phase);
}

View File

@ -176,7 +176,7 @@ void qdx_port_sock_recv_notify(uint8_t sockid)
total += len;
if (err != WCHNET_ERR_SUCCESS) break;
}
DBG_PORT("recv_notify sock%d: %u bytes, ring=%u\r\n",
DBG_HB("recv_notify sock%d: %u bytes, ring=%u\r\n",
sockid, (unsigned)total, (unsigned)ring_available(&ctx->rx_ring));
/* Wake blocking recv thread */
xSemaphoreGive(ctx->rx_sem);
@ -187,7 +187,7 @@ void qdx_port_sock_connect_notify(uint8_t sockid)
SocketCtx_t *ctx = find_ctx_by_wchnet_id(sockid);
if (!ctx) return;
ctx->connected = 1;
DBG_PORT("connect_notify sock%d\r\n", sockid);
DBG_NET("connect_notify sock%d\r\n", sockid);
xSemaphoreGive(ctx->connect_sem);
}
@ -196,7 +196,7 @@ void qdx_port_sock_disconnect_notify(uint8_t sockid)
SocketCtx_t *ctx = find_ctx_by_wchnet_id(sockid);
if (!ctx) return;
ctx->connected = 0;
DBG_PORT("disconnect_notify sock%d\r\n", sockid);
DBG_NET("disconnect_notify sock%d\r\n", sockid);
/* Wake recv thread so it can detect disconnect */
xSemaphoreGive(ctx->rx_sem);
}
@ -209,7 +209,7 @@ void qdx_port_init(void)
{
memset(g_sock_ctx, 0, sizeof(g_sock_ctx));
g_wchnet_mutex = xSemaphoreCreateMutex();
DBG_PORT("init done, mutex=%p\r\n", g_wchnet_mutex);
DBG_INIT("qdx_port init done, mutex=%p\r\n", g_wchnet_mutex);
}
/* ============================================================
@ -268,7 +268,7 @@ int8_t qdx_port_thread_create(const char *name, qdx_thread_entry_t entry,
BaseType_t ret = xTaskCreate((TaskFunction_t)entry, name,
(uint16_t)stack_words, arg,
(UBaseType_t)priority, NULL);
DBG_PORT("thread_create \"%s\" stack=%d pri=%d -> %s\r\n",
DBG_INIT("thread_create \"%s\" stack=%d pri=%d -> %s\r\n",
name, (int)stack_words, (int)priority, (ret == pdPASS) ? "OK" : "FAIL");
return (ret == pdPASS) ? 0 : -1;
}
@ -281,15 +281,15 @@ qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port)
{
uint8_t dest_ip[4];
if (parse_ip(ip, dest_ip) != 0) {
DBG_PORT("bad IP \"%s\"\r\n", ip);
DBG_ERR("bad IP \"%s\"\r\n", ip);
return NULL;
}
DBG_PORT("connecting to %s:%d\r\n", ip, port);
DBG_NET("connecting to %s:%d\r\n", ip, port);
SocketCtx_t *ctx = alloc_sock_ctx();
if (!ctx) {
DBG_PORT("no free SocketCtx\r\n");
DBG_ERR("no free SocketCtx\r\n");
return NULL;
}
@ -300,7 +300,7 @@ qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port)
sock_inf.DesPort = port;
memcpy(sock_inf.IPAddr, dest_ip, 4);
DBG_PORT("SOCK_INF: proto=%d dst=%d.%d.%d.%d:%d\r\n",
DBG_NET("SOCK_INF: proto=%d dst=%d.%d.%d.%d:%d\r\n",
sock_inf.ProtoType,
sock_inf.IPAddr[0], sock_inf.IPAddr[1],
sock_inf.IPAddr[2], sock_inf.IPAddr[3],
@ -313,11 +313,11 @@ qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port)
xSemaphoreGive(g_wchnet_mutex);
if (err != WCHNET_ERR_SUCCESS) {
DBG_PORT("SocketCreat fail %02X\r\n", err);
DBG_ERR("SocketCreat fail %02X\r\n", err);
free_sock_ctx(ctx);
return NULL;
}
DBG_PORT("SocketCreat OK, wchnet_id=%d\r\n", wchnet_id);
DBG_NET("SocketCreat OK, wchnet_id=%d\r\n", wchnet_id);
ctx->wchnet_sock_id = wchnet_id;
@ -330,12 +330,12 @@ qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port)
xSemaphoreGive(g_wchnet_mutex);
if (err != WCHNET_ERR_SUCCESS) {
DBG_PORT("SocketConnect fail %02X\r\n", err);
DBG_ERR("SocketConnect fail %02X\r\n", err);
WCHNET_SocketClose(wchnet_id, TCP_CLOSE_RST);
free_sock_ctx(ctx);
return NULL;
}
DBG_PORT("SocketConnect OK (err=0x%02X), waiting connect_sem (5s)...\r\n", err);
DBG_NET("SocketConnect OK (err=0x%02X), waiting connect_sem (5s)...\r\n", err);
/* Block until SINT_STAT_CONNECT or 5s timeout */
uint32_t t0 = xTaskGetTickCount();
@ -343,14 +343,14 @@ qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port)
uint32_t elapsed = (xTaskGetTickCount() - t0) * portTICK_PERIOD_MS;
if (sem_ret != pdTRUE) {
DBG_PORT("connect_sem TIMEOUT after %d ms -> %d.%d.%d.%d:%d\r\n",
DBG_ERR("connect_sem TIMEOUT after %d ms -> %d.%d.%d.%d:%d\r\n",
(int)elapsed, dest_ip[0], dest_ip[1], dest_ip[2], dest_ip[3], port);
DBG_PORT(" ctx->connected=%d wchnet_id=%d\r\n", ctx->connected, wchnet_id);
DBG_ERR(" ctx->connected=%d wchnet_id=%d\r\n", ctx->connected, wchnet_id);
WCHNET_SocketClose(wchnet_id, TCP_CLOSE_RST);
free_sock_ctx(ctx);
return NULL;
}
DBG_PORT("connect_sem got after %d ms, connected=%d\r\n", (int)elapsed, ctx->connected);
DBG_NET("connect_sem got after %d ms, connected=%d\r\n", (int)elapsed, ctx->connected);
if (!ctx->connected) {
WCHNET_SocketClose(wchnet_id, TCP_CLOSE_RST);
@ -362,7 +362,7 @@ qdx_socket_t qdx_port_tcp_connect(const char *ip, uint16_t port)
WCHNET_SocketSetKeepLive(wchnet_id, ENABLE);
#endif
DBG_PORT("connected sock %d -> %d.%d.%d.%d:%d\r\n",
DBG_NET("connected sock %d -> %d.%d.%d.%d:%d\r\n",
wchnet_id, dest_ip[0], dest_ip[1], dest_ip[2], dest_ip[3], port);
return (qdx_socket_t)ctx;
}
@ -388,7 +388,7 @@ int32_t qdx_port_tcp_send(qdx_socket_t sock, const uint8_t *data, uint32_t len)
if (err != WCHNET_ERR_SUCCESS && send_len == 0) {
/* WCHNET send buffer full — yield so wchnet task can flush */
if (++retries > 50) {
DBG_PORT("send fail after retries, err=0x%02X\r\n", err);
DBG_ERR("send fail after retries, err=0x%02X\r\n", err);
return -1;
}
vTaskDelay(pdMS_TO_TICKS(2));

View File

@ -18,29 +18,48 @@ extern "C" {
#endif
/* ============================================================
* Debug Print Macros (set to 0 to disable, 1 to enable)
* Multi-Level Debug Print System
*
* Verbosity levels (set DBG_LEVEL to control output volume):
* 0 = NONE all debug prints disabled
* 1 = ERR errors only
* 2 = BRIEF + server config / network events (recommended)
* 3 = NORMAL + trigger / data / init events (default)
* 4 = VERBOSE + heartbeat / detailed internals
*
* Category macros each prints when DBG_LEVEL >= its threshold:
* DBG_ERR (>=1) Errors, HardFault, CRC fail, bad frames
* DBG_CFG (>=2) Server-received config (Config2D/1D/Common, DevID, ACK, DetResult)
* DBG_NET (>=2) TCP connect/disconnect, PHY change, socket events
* DBG_TRIG (>=3) Trigger events (ext/int), burst start/complete
* DBG_DATA (>=3) Frame send results (2D/1D/TEMP_REQ)
* DBG_INIT (>=3) Boot-time init messages (sensor, DVP, WCHNET)
* DBG_HB (>=4) Heartbeat prints (high-frequency, debug only)
* ============================================================ */
#define QDX_DEBUG_PORT 1 /* qdx_port.c: socket/mutex/thread ops */
#define QDX_DEBUG_LOGIC 1 /* qdx_tcp_logic.c: protocol/TLV parsing */
#define QDX_DEBUG_APP 1 /* main.c: application-level events */
#define DBG_LEVEL_NONE 0
#define DBG_LEVEL_ERR 1
#define DBG_LEVEL_BRIEF 2
#define DBG_LEVEL_NORMAL 3
#define DBG_LEVEL_VERBOSE 4
#if QDX_DEBUG_PORT
#define DBG_PORT(fmt, ...) printf("[PORT] " fmt, ##__VA_ARGS__)
#else
#define DBG_PORT(fmt, ...) ((void)0)
#endif
/* >>> Change this single value to control output volume <<< */
#define DBG_LEVEL DBG_LEVEL_NORMAL
#if QDX_DEBUG_LOGIC
#define DBG_LOGIC(fmt, ...) printf("[LOGIC] " fmt, ##__VA_ARGS__)
#else
#define DBG_LOGIC(fmt, ...) ((void)0)
#endif
#define DBG_PRINT_(threshold, tag, fmt, ...) \
do { if (DBG_LEVEL >= (threshold)) printf(tag fmt, ##__VA_ARGS__); } while(0)
#if QDX_DEBUG_APP
#define DBG_APP(fmt, ...) printf("[APP] " fmt, ##__VA_ARGS__)
#else
#define DBG_APP(fmt, ...) ((void)0)
#endif
#define DBG_ERR(fmt, ...) DBG_PRINT_(DBG_LEVEL_ERR, "[ERR] ", fmt, ##__VA_ARGS__)
#define DBG_CFG(fmt, ...) DBG_PRINT_(DBG_LEVEL_BRIEF, "[CFG] ", fmt, ##__VA_ARGS__)
#define DBG_NET(fmt, ...) DBG_PRINT_(DBG_LEVEL_BRIEF, "[NET] ", fmt, ##__VA_ARGS__)
#define DBG_TRIG(fmt, ...) DBG_PRINT_(DBG_LEVEL_NORMAL, "[TRIG] ", fmt, ##__VA_ARGS__)
#define DBG_DATA(fmt, ...) DBG_PRINT_(DBG_LEVEL_NORMAL, "[DATA] ", fmt, ##__VA_ARGS__)
#define DBG_INIT(fmt, ...) DBG_PRINT_(DBG_LEVEL_NORMAL, "[INIT] ", fmt, ##__VA_ARGS__)
#define DBG_HB(fmt, ...) DBG_PRINT_(DBG_LEVEL_VERBOSE, "[HB] ", fmt, ##__VA_ARGS__)
/* Legacy compatibility aliases — avoid using in new code */
#define DBG_PORT(fmt, ...) DBG_INIT(fmt, ##__VA_ARGS__)
#define DBG_LOGIC(fmt, ...) DBG_CFG(fmt, ##__VA_ARGS__)
#define DBG_APP(fmt, ...) DBG_DATA(fmt, ##__VA_ARGS__)
/* ============================================================
* Time & Delay

View File

@ -109,7 +109,7 @@ int8_t Preprocess_Execute(const RawImageBuffer_t *input,
tgt_w = (tgt_w + 1) / 2;
tgt_h = (tgt_h + 1) / 2;
}
DBG_PORT("PP: target clamped %dx%d -> %dx%d (buf=%d)\r\n",
DBG_INIT("PP: target clamped %dx%d -> %dx%d (buf=%d)\r\n",
(int)orig_w, (int)orig_h, (int)tgt_w, (int)tgt_h, (int)max_payload);
}

View File

@ -69,7 +69,7 @@ static void tcp_stream_init(TcpStreamCtx_t *ctx, const char *label) {
}
static void tcp_stream_disconnect(TcpStreamCtx_t *ctx) {
DBG_LOGIC("!! [%s] Disconnected\r\n", ctx->label);
DBG_NET("[%s] Disconnected\r\n", ctx->label);
ctx->is_connected = 0;
if (ctx->sock) {
qdx_port_tcp_close(ctx->sock);
@ -79,10 +79,10 @@ static void tcp_stream_disconnect(TcpStreamCtx_t *ctx) {
static int8_t tcp_stream_connect(TcpStreamCtx_t *ctx, const char *ip,
uint16_t port) {
DBG_LOGIC("[%s] connecting %s:%d...\r\n", ctx->label, ip, port);
DBG_NET("[%s] connecting %s:%d...\r\n", ctx->label, ip, port);
ctx->sock = qdx_port_tcp_connect(ip, port);
if (ctx->sock == NULL) {
DBG_LOGIC("[%s] connect FAILED\r\n", ctx->label);
DBG_ERR("[%s] connect FAILED\r\n", ctx->label);
return -1;
}
@ -90,7 +90,7 @@ static int8_t tcp_stream_connect(TcpStreamCtx_t *ctx, const char *ip,
ctx->last_activity_ms = qdx_port_get_tick_ms();
ctx->last_heartbeat_ms = ctx->last_activity_ms;
ctx->recv_len = 0;
DBG_LOGIC(">> [%s] Connected to %s:%d\r\n", ctx->label, ip, port);
DBG_NET("[%s] Connected to %s:%d\r\n", ctx->label, ip, port);
return 0;
}
@ -121,7 +121,7 @@ static int32_t tcp_send_frame(TcpStreamCtx_t *ctx, uint8_t msg_class,
}
static void tcp_send_handshake(TcpStreamCtx_t *ctx) {
DBG_LOGIC(">> [%s] Handshake (DevID=%d)\r\n", ctx->label, (int)g_TcpLogic.dev_id);
DBG_NET("[%s] Handshake (DevID=%d)\r\n", ctx->label, (int)g_TcpLogic.dev_id);
uint8_t payload[54];
memset(payload, 0, sizeof(payload));
qdx_write_u16_le(payload + 0, 0x0200);
@ -250,17 +250,17 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
uint8_t cfg_updated = 0;
DBG_LOGIC("[%s] TLV pkt: Seq=%d, PayloadLen=%d\r\n", ctx->label,
DBG_HB("[%s] TLV pkt: Seq=%d Len=%d\r\n", ctx->label,
hdr_seq, payload_len);
while (parsed_len <= payload_len - 3) {
uint8_t type = packet[offset];
uint16_t len = qdx_read_u16_le(packet + offset + 1);
DBG_LOGIC("[%s] TLV: Type=0x%02X, Len=%d\r\n", ctx->label, type, len);
DBG_HB("[%s] TLV: Type=0x%02X Len=%d\r\n", ctx->label, type, len);
if (parsed_len + 3 + len > payload_len) {
DBG_LOGIC("[%s] ! TLV truncated (need %d, have %d)\r\n",
DBG_ERR("[%s] TLV truncated (need %d, have %d)\r\n",
ctx->label, len, payload_len - parsed_len - 3);
break; /* Malformed */
}
@ -278,16 +278,16 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
uint16_t ack_seq = qdx_read_u16_le(value);
uint8_t status = value[2];
uint16_t err_code = (len >= 5) ? qdx_read_u16_le(value + 3) : 0;
DBG_LOGIC("<< [%s] ACK: seq=%d status=%d err=0x%04x\r\n",
ctx->label, (int)ack_seq, (int)status, (int)err_code);
DBG_CFG("<< ACK: seq=%d status=%d err=0x%04x\r\n",
(int)ack_seq, (int)status, (int)err_code);
}
break;
}
case TYPE_DEVID_ASSIGN: {
if (len >= sizeof(DevIDAssignment_t)) {
uint16_t new_id = qdx_read_u16_le(value);
DBG_LOGIC("<< [%s] DevID assigned: %d -> %d\r\n",
ctx->label, (int)g_TcpLogic.dev_id, (int)new_id);
DBG_CFG("<< DevID assigned: %d -> %d\r\n",
(int)g_TcpLogic.dev_id, (int)new_id);
g_TcpLogic.pending_new_dev_id = new_id;
tcp_send_ack(ctx, hdr_seq, 0, 0);
}
@ -300,8 +300,7 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
g_TcpLogic.has_valid_config = 1;
cfg_updated = 1;
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
DBG_LOGIC("<< [%s] ConfigCommon: Mode=%d Tag=%d Strict=%d\r\n",
ctx->label,
DBG_CFG("<< ConfigCommon: Mode=%d Tag=%d Strict=%d\r\n",
(int)g_TcpLogic.cached_common.WorkMode,
(int)g_TcpLogic.cached_common.ConfigTag,
(int)g_TcpLogic.cached_common.StrictnessLevel);
@ -315,10 +314,9 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
g_TcpLogic.has_valid_config = 1;
cfg_updated = 1;
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
DBG_LOGIC("<< [%s] Config2D: En=%d %dx%d Tgt=%dx%d Fps=%d "
DBG_CFG("<< Config2D: En=%d %dx%d Tgt=%dx%d Fps=%d "
"Trig=%d Burst=%d Intv=%d Thresh=%d "
"ROI(%d,%d,%d,%d) NGio=%d\r\n",
ctx->label,
(int)g_TcpLogic.cached_cfg2d.Enabled,
(int)g_TcpLogic.cached_cfg2d.Width,
(int)g_TcpLogic.cached_cfg2d.Height,
@ -335,8 +333,8 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
(int)g_TcpLogic.cached_cfg2d.TriggerRoiH,
(int)g_TcpLogic.cached_cfg2d.NGioDelay);
} else {
DBG_LOGIC("<< [%s] Config2D bad len=%d (need %d)\r\n",
ctx->label, len, (int)sizeof(Config2D_t));
DBG_ERR("<< Config2D bad len=%d (need %d)\r\n",
len, (int)sizeof(Config2D_t));
}
break;
}
@ -347,9 +345,8 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
g_TcpLogic.has_valid_config = 1;
cfg_updated = 1;
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
DBG_LOGIC("<< [%s] Config1D: En=%d RunMode=%d TrigType=%d "
DBG_CFG("<< Config1D: En=%d RunMode=%d TrigType=%d "
"BufSz=%d TempLim=%d NGio=%d\r\n",
ctx->label,
(int)g_TcpLogic.cached_cfg1d.Enabled,
(int)g_TcpLogic.cached_cfg1d.RunMode,
(int)g_TcpLogic.cached_cfg1d.TriggerType,
@ -360,13 +357,13 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
break;
}
case TYPE_TEMP_FRAME: {
DBG_LOGIC("<< [%s] TempFrame request (len=%d)\r\n", ctx->label, (int)len);
DBG_CFG("<< TempFrame request (len=%d)\r\n", (int)len);
if (g_TcpLogic.temp_req_cb) {
uint8_t is2d = 0;
if (len >= 18) {
is2d = value[18];
}
DBG_LOGIC(" -> callback: is2D=%d\r\n", (int)is2d);
DBG_CFG(" -> callback: is2D=%d\r\n", (int)is2d);
g_TcpLogic.temp_req_cb(is2d);
}
break;
@ -375,8 +372,8 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
if (len >= sizeof(DetectionResult_t)) {
uint32_t frame_num = qdx_read_u32_le(value);
uint8_t result_status = value[4];
DBG_LOGIC("<< [%s] DetectionResult: frm=%d result=%d\r\n",
ctx->label, (int)frame_num, (int)result_status);
DBG_CFG("<< DetectionResult: frm=%d result=%d\r\n",
(int)frame_num, (int)result_status);
if (g_TcpLogic.detect_cb)
g_TcpLogic.detect_cb(frame_num, result_status);
}
@ -386,8 +383,7 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
/* Server echoes handshake info — safe to ignore */
break;
default:
DBG_LOGIC("<< [%s] Unknown TLV type=0x%02x len=%d\r\n",
ctx->label, (int)type, (int)len);
DBG_ERR("Unknown TLV type=0x%02x len=%d\r\n", (int)type, (int)len);
break;
}
@ -396,7 +392,7 @@ static void parse_and_dispatch_tlv(TcpStreamCtx_t *ctx, const uint8_t *packet,
}
if (cfg_updated && g_TcpLogic.config_cb && g_TcpLogic.has_valid_config) {
DBG_LOGIC("<< [%s] Config updated -> notify app\r\n", ctx->label);
DBG_CFG("Config updated -> notify app\r\n");
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
g_TcpLogic.config_cb(&g_TcpLogic.cached_common, &g_TcpLogic.cached_cfg2d,
&g_TcpLogic.cached_cfg1d);
@ -438,7 +434,7 @@ static void tcp_process_rx_buffer(TcpStreamCtx_t *ctx) {
uint16_t length = qdx_read_u16_le(ctx->recv_buffer + 3);
if (version != PROTO_VERSION || length < HEADER_SIZE + CRC_SIZE) {
DBG_LOGIC("[%s] bad header: ver=0x%02X(exp 0x%02X) len=%d\r\n",
DBG_ERR("[%s] bad header: ver=0x%02X(exp 0x%02X) len=%d\r\n",
ctx->label, version, PROTO_VERSION, length);
memmove(ctx->recv_buffer, ctx->recv_buffer + 2, ctx->recv_len - 2);
ctx->recv_len -= 2;
@ -446,7 +442,7 @@ static void tcp_process_rx_buffer(TcpStreamCtx_t *ctx) {
}
if (length > sizeof(ctx->recv_buffer)) {
DBG_LOGIC("[%s] frame too large: %d > %d\r\n", ctx->label,
DBG_ERR("[%s] frame too large: %d > %d\r\n", ctx->label,
length, (int)sizeof(ctx->recv_buffer));
ctx->recv_len = 0;
break;
@ -464,7 +460,7 @@ static void tcp_process_rx_buffer(TcpStreamCtx_t *ctx) {
/* 3. Dispatch */
parse_and_dispatch_tlv(ctx, ctx->recv_buffer, length);
} else {
DBG_LOGIC("[%s] CRC fail: calc=0x%04X recv=0x%04X len=%d\r\n",
DBG_ERR("[%s] CRC fail: calc=0x%04X recv=0x%04X len=%d\r\n",
ctx->label, calculated_crc, received_crc, length);
}
@ -642,6 +638,22 @@ void TcpLogic_RegisterTempFrameRequestCallback(TempFrameRequestCallback_t cb) {
g_TcpLogic.temp_req_cb = cb;
}
void TcpLogic_InjectTestConfig(const ConfigCommon_t *common,
const Config2D_t *cfg2d,
const Config1D_t *cfg1d) {
qdx_port_mutex_lock(g_TcpLogic.config_mutex);
if (common) memcpy(&g_TcpLogic.cached_common, common, sizeof(*common));
if (cfg2d) memcpy(&g_TcpLogic.cached_cfg2d, cfg2d, sizeof(*cfg2d));
if (cfg1d) memcpy(&g_TcpLogic.cached_cfg1d, cfg1d, sizeof(*cfg1d));
g_TcpLogic.has_valid_config = 1;
qdx_port_mutex_unlock(g_TcpLogic.config_mutex);
if (g_TcpLogic.config_cb) {
g_TcpLogic.config_cb(&g_TcpLogic.cached_common,
&g_TcpLogic.cached_cfg2d,
&g_TcpLogic.cached_cfg1d);
}
}
/* ============================================================
* Zero-Copy Frame Building & Fragmentation for Temperature Data
* ============================================================ */
@ -658,7 +670,7 @@ TcpLogic_BuildAndSendTemperatureFrame(TcpTxBuffer_t *io_buffer,
g_TcpLogic.frame_count++;
DBG_LOGIC(">> [Data] TempFrame #%d: %dx%d type=%d is2D=%d payload=%d\r\n",
DBG_DATA(">> TempFrame #%d: %dx%d type=%d is2D=%d payload=%d\r\n",
(int)processMeta->FrameNumber,
(int)processMeta->ValidWidth, (int)processMeta->ValidHeight,
(int)frameType, (int)is2D, (int)io_buffer->ValidPayloadLen);
@ -725,7 +737,7 @@ TcpLogic_BuildAndSendTemperatureFrame(TcpTxBuffer_t *io_buffer,
uint32_t frag_count =
(total_tlv_len + MAX_FRAGMENT_PAYLOAD - 1) / MAX_FRAGMENT_PAYLOAD;
DBG_LOGIC(">> [Data] Fragmented: %d frags, total=%d\r\n",
DBG_DATA(">> Fragmented: %d frags, total=%d\r\n",
(int)frag_count, (int)total_tlv_len);
for (uint32_t i = 0; i < frag_count; i++) {

View File

@ -105,6 +105,16 @@ typedef void (*TempFrameRequestCallback_t)(uint8_t is2dRequest);
*/
void TcpLogic_RegisterTempFrameRequestCallback(TempFrameRequestCallback_t cb);
/**
* @brief Inject configuration directly (for test mode without server).
*
* Sets the internal config cache and fires the config callback.
* Only non-NULL parameters are updated.
*/
void TcpLogic_InjectTestConfig(const ConfigCommon_t *common,
const Config2D_t *cfg2d,
const Config1D_t *cfg1d);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,709 @@
#include "ch32v30x.h"
#include "string.h"
#include "eth_driver.h"
#include "dvp.h"
#include "mini212g2.h"
#include "qdx_port.h"
#include "qdx_preprocess.h"
#include "qdx_tcp_logic.h"
#include "FreeRTOS.h"
#include "task.h"
/* ============================================================
* TEST MODE: Generate simulated sensor data without real hardware.
* Set to 1 to enable test pattern generation (no sensor/DVP needed).
* Set to 0 for normal operation with real Mini212G2 sensor.
* ============================================================ */
#define TEST_PATTERN_MODE 1
/* ============================================================
* Feature Test Switches set to 1 to enable, 0 to disable.
* Allows testing each feature independently.
* ============================================================ */
#define TEST_ENABLE_HEARTBEAT 1 /* Periodic heartbeat debug print task */
#define TEST_ENABLE_TRIGGER 0 /* Internal trigger + burst upload */
#define TEST_ENABLE_NG_GPIO 0 /* NG GPIO pulse output on detection */
#define TEST_ENABLE_TEMP_REQ 1 /* On-demand frame upload from server */
#define TEST_ENABLE_TCP_SEND 1 /* TCP data channel sending */
/* Default burst parameters — used when server hasn't sent config yet */
#define DEFAULT_BURST_COUNT 3
#define DEFAULT_BURST_INTERVAL_MS 200
#define MAX_TCP_PAYLOAD_SIZE 10240
uint8_t g_TxNetBuffer_A_Mem[MAX_TCP_PAYLOAD_SIZE];
uint8_t g_TxNetBuffer_B_Mem[MAX_TCP_PAYLOAD_SIZE];
TcpTxBuffer_t g_TxNetBuffer_A = {
.pBuffer = g_TxNetBuffer_A_Mem,
.TotalCapacity = MAX_TCP_PAYLOAD_SIZE,
.HeadOffset = 64,
.ValidPayloadLen = 0
};
TcpTxBuffer_t g_TxNetBuffer_B = {
.pBuffer = g_TxNetBuffer_B_Mem,
.TotalCapacity = MAX_TCP_PAYLOAD_SIZE,
.HeadOffset = 64,
.ValidPayloadLen = 0
};
void OnConfigUpdate(const ConfigCommon_t *common, const Config2D_t *cfg2d, const Config1D_t *cfg1d)
{
Preprocess_Settings_Change(cfg2d, cfg1d, common);
}
extern volatile uint32_t sys_tick_ms;
#if TEST_ENABLE_NG_GPIO
/* NG output GPIO: PA8 push-pull, active-high when NG detected */
#define NG_GPIO_PORT GPIOA
#define NG_GPIO_PIN GPIO_Pin_8
#define NG_GPIO_CLK RCC_APB2Periph_GPIOA
#define NG_PULSE_MS 200 /* default NG pulse width */
static volatile uint32_t g_ng_off_time = 0;
static void NG_GPIO_Init(void)
{
GPIO_InitTypeDef gpio = {0};
RCC_APB2PeriphClockCmd(NG_GPIO_CLK, ENABLE);
gpio.GPIO_Pin = NG_GPIO_PIN;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(NG_GPIO_PORT, &gpio);
GPIO_ResetBits(NG_GPIO_PORT, NG_GPIO_PIN);
}
#endif /* TEST_ENABLE_NG_GPIO */
#if TEST_ENABLE_TEMP_REQ
/* Flag set by TempFrameRequest callback; consumed by business task */
static volatile uint8_t g_temp_req_pending = 0;
static volatile uint8_t g_temp_req_is2d = 1;
void OnTempFrameRequest(uint8_t is2dRequest)
{
g_temp_req_is2d = is2dRequest;
g_temp_req_pending = 1;
DBG_APP("TempFrameReq is2d=%d\r\n", (int)is2dRequest);
}
#endif /* TEST_ENABLE_TEMP_REQ */
#if TEST_ENABLE_NG_GPIO
void OnDetectionResult(uint32_t frameNumber, uint8_t resultStatus)
{
(void)frameNumber;
/* resultStatus: 0 = NG, 1 = OK */
if (resultStatus == 0) {
ConfigCommon_t tmp_common;
Config2D_t tmp_cfg2d;
Config1D_t tmp_cfg1d;
uint32_t pulse_ms = NG_PULSE_MS; /* fallback default */
if (TcpLogic_GetLatestConfig(&tmp_common, &tmp_cfg2d, &tmp_cfg1d) == 0
&& tmp_cfg2d.NGioDelay > 0) {
pulse_ms = tmp_cfg2d.NGioDelay;
}
GPIO_SetBits(NG_GPIO_PORT, NG_GPIO_PIN);
g_ng_off_time = sys_tick_ms + pulse_ms;
}
}
#endif /* TEST_ENABLE_NG_GPIO */
#define KEEPALIVE_ENABLE 1
/* ============================================================
* FLASH / SRAM (Option Bytes RAM_CODE_MOD[2:0])
*
* ============================================================ */
typedef enum {
FLASH_192_SRAM_128 = 0, /* 00x 默认 */
FLASH_224_SRAM_96, /* 01x */
FLASH_256_SRAM_64, /* 10x */
FLASH_128_SRAM_192, /* 110 */
FLASH_288_SRAM_32 /* 111 */
} FLASH_SRAM_DEFIN;
static void Config_Flash_SRAM(FLASH_SRAM_DEFIN mode)
{
uint8_t UserByte = FLASH_GetUserOptionByte() & 0xFF;
uint8_t newByte = UserByte & ~0xE0; /* clear bits [7:5] */
switch (mode) {
case FLASH_192_SRAM_128: break; /* 000 */
case FLASH_224_SRAM_96: newByte |= 0x40; break; /* 010 */
case FLASH_256_SRAM_64: newByte |= 0x80; break; /* 100 */
case FLASH_128_SRAM_192: newByte |= 0xC0; break; /* 110 */
case FLASH_288_SRAM_32: newByte |= 0xE0; break; /* 111 */
default: return;
}
if (newByte == UserByte) return; /* already configured */
FLASH_Unlock();
FLASH_ProgramOptionByteData(0x1FFFF802, newByte);
FLASH_Lock();
printf("Flash/SRAM config changed to %d, resetting...\r\n", mode);
NVIC_SystemReset();
}
u8 MACAddr[6];
u8 IPAddr[4] = {192, 168, 7, 10};
u8 GWIPAddr[4] = {192, 168, 7, 1};
u8 IPMask[4] = {255, 255, 255, 0};
u8 DESIP[4] = {192, 168, 7, 50};
u16 desport = 5512;
u16 srcport = 5511;
u8 SocketId;
u8 SocketRecvBuf[WCHNET_MAX_SOCKET_NUM][RECE_BUF_LEN];
void mStopIfError(u8 iError)
{
if (iError == WCHNET_ERR_SUCCESS) return;
printf("Error: %02X\r\n", (u16)iError);
}
void TIM2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure = {0};
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStructure.TIM_Period = WCHNETTIMERPERIOD * 1000 - 1;
TIM_TimeBaseStructure.TIM_Prescaler = SystemCoreClock / 1000000 - 1;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM2, ENABLE);
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
NVIC_EnableIRQ(TIM2_IRQn);
}
/* qdx_port.c notification hooks */
extern void qdx_port_sock_recv_notify(uint8_t sockid);
extern void qdx_port_sock_connect_notify(uint8_t sockid);
extern void qdx_port_sock_disconnect_notify(uint8_t sockid);
extern void qdx_port_init(void);
extern void qdx_port_net_lock(void);
extern void qdx_port_net_unlock(void);
void WCHNET_HandleSockInt(u8 socketid, u8 intstat)
{
DBG_APP("SockInt: id=%d stat=0x%02X\r\n", socketid, intstat);
if (intstat & SINT_STAT_RECV)
{
qdx_port_sock_recv_notify(socketid);
}
if (intstat & SINT_STAT_CONNECT)
{
WCHNET_ModifyRecvBuf(socketid, (u32)SocketRecvBuf[socketid], RECE_BUF_LEN);
qdx_port_sock_connect_notify(socketid);
DBG_APP("TCP Connected, socket %d\r\n", socketid);
}
if (intstat & SINT_STAT_DISCONNECT)
{
qdx_port_sock_disconnect_notify(socketid);
DBG_APP("TCP Disconnected, socket %d\r\n", socketid);
}
if (intstat & SINT_STAT_TIM_OUT)
{
qdx_port_sock_disconnect_notify(socketid);
DBG_APP("TCP Timeout, socket %d\r\n", socketid);
}
}
void WCHNET_HandleGlobalInt(void)
{
u8 intstat;
u16 i;
u8 socketint;
intstat = WCHNET_GetGlobalInt();
DBG_APP("GlobalInt: 0x%02X\r\n", intstat);
if (intstat & GINT_STAT_UNREACH) DBG_APP("GINT_STAT_UNREACH\r\n");
if (intstat & GINT_STAT_IP_CONFLI) DBG_APP("GINT_STAT_IP_CONFLI\r\n");
if (intstat & GINT_STAT_PHY_CHANGE) {
i = WCHNET_GetPHYStatus();
DBG_APP("PHY_CHANGE: status=0x%04X %s\r\n", i,
(i & PHY_Linked_Status) ? "LINK_UP" : "LINK_DOWN");
}
if (intstat & GINT_STAT_SOCKET) {
for (i = 0; i < WCHNET_MAX_SOCKET_NUM; i++) {
socketint = WCHNET_GetSocketInt(i);
if (socketint) WCHNET_HandleSockInt(i, socketint);
}
}
}
#if TEST_ENABLE_HEARTBEAT
/* ============================================================
* RTOS Task: Heartbeat (periodic print for debug)
* ============================================================ */
static void task_heartbeat_entry(void *pvParameters)
{
(void)pvParameters;
uint32_t cnt = 0;
while (1)
{
#if TEST_PATTERN_MODE
printf("[HB] %d tick=%d TEST_MODE frm=%d\r\n",
(int)cnt++, (int)xTaskGetTickCount(),
(int)dvp_frame_count);
#else
printf("[HB] %d tick=%d dvp_frm=%d row_irq=%d\r\n",
(int)cnt++, (int)xTaskGetTickCount(),
(int)dvp_frame_count, (int)dvp_row_irq_cnt);
#endif
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
#endif /* TEST_ENABLE_HEARTBEAT */
/* ============================================================
* RTOS Task: WCHNET protocol stack driver (highest priority)
* ============================================================ */
static void task_wchnet_entry(void *pvParameters)
{
(void)pvParameters;
while (1)
{
qdx_port_net_lock();
WCHNET_MainTask();
if (WCHNET_QueryGlobalInt())
WCHNET_HandleGlobalInt();
qdx_port_net_unlock();
vTaskDelay(pdMS_TO_TICKS(5));
}
}
/* ============================================================
* RTOS Task: Business logic (DVP + preprocess + send)
* ============================================================ */
#if TEST_ENABLE_TRIGGER
/* ============================================================
* Burst capture state machine
* ============================================================ */
static uint8_t burst_active = 0; /* 1 = currently in burst */
static uint8_t burst_remaining = 0; /* frames left to capture */
static uint32_t burst_next_time_ms = 0; /* next burst frame time */
#endif /* TEST_ENABLE_TRIGGER */
#if TEST_PATTERN_MODE
/* ============================================================
* Test Pattern Generator
* Generates simulated 256x192 Y16 thermal images at ~10 FPS.
* Pattern cycles through several types so all pipeline paths
* can be exercised.
*
* Pattern 0: Gradient (25.00~45.00°C) baseline, no trigger
* Pattern 1: Hot center spot (90.00°C) should trigger alarm
* Pattern 2: Uniform warm (35.00°C) no trigger
* Pattern 3: Checkerboard with hot cells tests ROI search
* ============================================================ */
#define TEST_FPS_DELAY_MS 100 /* ~10 FPS */
#define TEMP_RAW(deg_c) ((uint16_t)((deg_c) * 10)) /* e.g. 35.0°C → 350, 0.1°C/LSB */
static void test_fill_gradient(uint16_t *buf, uint16_t w, uint16_t h)
{
/* Diagonal gradient: 25°C at top-left → 45°C at bottom-right
* Both X and Y contribute so 1D center-row also shows variation */
for (uint16_t y = 0; y < h; y++) {
for (uint16_t x = 0; x < w; x++) {
uint32_t pos = (uint32_t)y * w + (uint32_t)x * h; /* combined weight */
uint16_t temp = TEMP_RAW(25.0) + (uint16_t)(pos * TEMP_RAW(20.0) / ((uint32_t)w * h));
buf[y * w + x] = temp;
}
}
}
static void test_fill_hotspot(uint16_t *buf, uint16_t w, uint16_t h)
{
/* Background 30°C with slight X variation, center 32x32 block at 90°C */
for (uint16_t y = 0; y < h; y++)
for (uint16_t x = 0; x < w; x++)
buf[y * w + x] = TEMP_RAW(28.0) + (uint16_t)((uint32_t)x * TEMP_RAW(4.0) / w);
uint16_t cx = w / 2, cy = h / 2;
for (uint16_t y = cy - 16; y < cy + 16; y++)
for (uint16_t x = cx - 16; x < cx + 16; x++)
buf[y * w + x] = TEMP_RAW(90.0);
}
static void test_fill_uniform(uint16_t *buf, uint16_t w, uint16_t h)
{
for (uint16_t y = 0; y < h; y++)
for (uint16_t x = 0; x < w; x++)
buf[y * w + x] = TEMP_RAW(35.0);
}
static void test_fill_checker(uint16_t *buf, uint16_t w, uint16_t h)
{
/* 32x32 checkerboard: alternating 30°C and 85°C blocks */
for (uint16_t y = 0; y < h; y++)
for (uint16_t x = 0; x < w; x++) {
uint8_t cell = ((y / 32) + (x / 32)) & 1;
buf[y * w + x] = cell ? TEMP_RAW(85.0) : TEMP_RAW(28.0);
}
}
static void task_test_pattern_entry(void *pvParameters)
{
(void)pvParameters;
uint32_t frame_num = 0;
uint8_t pattern_idx = 0;
/* Cycle: gradient x20, hotspot x5, uniform x10, checker x5 */
const uint8_t pattern_seq[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,
2,2,2,2,2,2,2,2,2,2,
3,3,3,3,3};
const uint8_t seq_len = sizeof(pattern_seq);
while (1)
{
uint16_t *buf = (uint16_t *)FrameBuffer;
switch (pattern_seq[pattern_idx % seq_len]) {
case 0: test_fill_gradient(buf, SENSOR_WIDTH, SENSOR_HEIGHT); break;
case 1: test_fill_hotspot(buf, SENSOR_WIDTH, SENSOR_HEIGHT); break;
case 2: test_fill_uniform(buf, SENSOR_WIDTH, SENSOR_HEIGHT); break;
case 3: test_fill_checker(buf, SENSOR_WIDTH, SENSOR_HEIGHT); break;
}
pattern_idx++;
if (pattern_idx >= seq_len) pattern_idx = 0;
frame_num++;
dvp_frame_count = frame_num;
Ready_Frame_Count = frame_num;
Frame_Ready_Flag = 1;
vTaskDelay(pdMS_TO_TICKS(TEST_FPS_DELAY_MS));
}
}
#endif /* TEST_PATTERN_MODE */
#if TEST_ENABLE_TEMP_REQ
/* ============================================================
* 1D mode: build and send a single 1D temperature frame from
* the current raw image. Scans the center row and packs
* TempPoint1D_t (2B time_offset + 2B temp, little-endian).
* ============================================================ */
#define TEST_1D_POINTS 30 /* 1D test: sample 30 points from center row */
static void send_1d_frame_from_raw(const RawImageBuffer_t *raw, TcpTxBuffer_t *tx_buf)
{
uint16_t w = raw->Width;
uint16_t h = raw->Height;
uint16_t *src = raw->pData;
uint16_t row = h / 2; /* center row */
tx_buf->ValidPayloadLen = 0;
uint8_t *dest = tx_buf->pBuffer + tx_buf->HeadOffset;
uint32_t capacity = tx_buf->TotalCapacity - tx_buf->HeadOffset;
uint16_t points = TEST_1D_POINTS;
if (points > w) points = w;
if ((uint32_t)points * 4 > capacity)
points = (uint16_t)(capacity / 4);
int16_t min_t = 32767, max_t = -32768;
int32_t sum_t = 0;
for (uint16_t i = 0; i < points; i++) {
/* Evenly sample across the full row width */
uint16_t col = (uint16_t)((uint32_t)i * (w - 1) / (points > 1 ? points - 1 : 1));
uint16_t temp = src[row * w + col];
uint16_t time_offset = (uint16_t)((uint32_t)i * 600 / (points > 1 ? points - 1 : 1));
int16_t t = (int16_t)temp;
if (t < min_t) min_t = t;
if (t > max_t) max_t = t;
sum_t += t;
dest[0] = (uint8_t)(time_offset & 0xFF);
dest[1] = (uint8_t)((time_offset >> 8) & 0xFF);
dest[2] = (uint8_t)(temp & 0xFF);
dest[3] = (uint8_t)((temp >> 8) & 0xFF);
dest += 4;
}
tx_buf->ValidPayloadLen = (uint32_t)points * 4;
PreprocessResult_t meta;
memset(&meta, 0, sizeof(meta));
meta.pValidData = tx_buf->pBuffer + tx_buf->HeadOffset;
meta.DataLength = tx_buf->ValidPayloadLen;
meta.ValidWidth = points;
meta.ValidHeight = 1;
meta.MinTemp = min_t;
meta.MaxTemp = max_t;
meta.AvgTemp = (int16_t)(sum_t / (points > 0 ? points : 1));
meta.RoiTemp = meta.AvgTemp;
meta.FrameNumber = raw->FrameNumber;
TcpLogic_BuildAndSendTemperatureFrame(tx_buf, &meta, 0x01, 0 /* IS_1D */);
}
#endif /* TEST_ENABLE_TEMP_REQ */
static void task_business_entry(void *pvParameters)
{
(void)pvParameters;
static uint8_t use_buffer_A = 1;
while (1)
{
#if TEST_ENABLE_NG_GPIO
/* NG pulse off check */
if (g_ng_off_time && sys_tick_ms >= g_ng_off_time) {
GPIO_ResetBits(NG_GPIO_PORT, NG_GPIO_PIN);
g_ng_off_time = 0;
}
#endif
#if !TEST_PATTERN_MODE
DVP_Task();
#endif
#if TEST_ENABLE_TEMP_REQ
/* Handle on-demand frame request from server */
if (g_temp_req_pending)
{
g_temp_req_pending = 0;
/* 检查对应模式是否已由服务器使能 */
ConfigCommon_t tc; Config2D_t t2; Config1D_t t1;
uint8_t mode_enabled = 0;
if (TcpLogic_GetLatestConfig(&tc, &t2, &t1) == 0) {
if (g_temp_req_is2d && t2.Enabled)
mode_enabled = 1;
else if (!g_temp_req_is2d && t1.Enabled)
mode_enabled = 1;
}
if (!mode_enabled) {
DBG_APP("TEMP_REQ ignored: %s not enabled\r\n",
g_temp_req_is2d ? "2D" : "1D");
} else {
RawImageBuffer_t raw_img;
raw_img.pData = (uint16_t *)FrameBuffer;
raw_img.Width = SENSOR_WIDTH;
raw_img.Height = SENSOR_HEIGHT;
raw_img.FrameNumber = Ready_Frame_Count;
TcpTxBuffer_t *tx_buf = use_buffer_A ? &g_TxNetBuffer_A : &g_TxNetBuffer_B;
use_buffer_A = !use_buffer_A;
tx_buf->ValidPayloadLen = 0;
if (g_temp_req_is2d) {
PreprocessResult_t meta;
int8_t pp_ret = Preprocess_Execute(&raw_img, tx_buf, &meta);
if (pp_ret == 0) {
int8_t tx_ret = TcpLogic_BuildAndSendTemperatureFrame(tx_buf, &meta, 0x02 /* REQUEST */, 1);
DBG_APP("TEMP_REQ SEND 2D frm=%d %dx%d ret=%d\r\n",
(int)meta.FrameNumber, (int)meta.ValidWidth,
(int)meta.ValidHeight, (int)tx_ret);
} else {
DBG_APP("TEMP_REQ PP fail ret=%d\r\n", (int)pp_ret);
}
} else {
send_1d_frame_from_raw(&raw_img, tx_buf);
DBG_APP("TEMP_REQ SEND 1D frm=%d\r\n", (int)raw_img.FrameNumber);
}
} /* mode_enabled */
}
#endif /* TEST_ENABLE_TEMP_REQ */
if (Frame_Ready_Flag)
{
Frame_Ready_Flag = 0;
RawImageBuffer_t raw_img;
raw_img.pData = (uint16_t *)FrameBuffer;
raw_img.Width = SENSOR_WIDTH;
raw_img.Height = SENSOR_HEIGHT;
raw_img.FrameNumber = Ready_Frame_Count;
#if TEST_ENABLE_TRIGGER
/* 触发逻辑仅在服务器下发 Config2D 且 Enabled=1 后才激活 */
{
ConfigCommon_t tc; Config2D_t t2; Config1D_t t1;
uint8_t trigger_armed = 0;
if (TcpLogic_GetLatestConfig(&tc, &t2, &t1) == 0 && t2.Enabled)
trigger_armed = 1;
if (trigger_armed)
{
if (burst_active)
{
/* Burst mode: wait for interval, then capture & send */
if (sys_tick_ms >= burst_next_time_ms)
{
PreprocessResult_t meta;
TcpTxBuffer_t *tx_buf = use_buffer_A ? &g_TxNetBuffer_A : &g_TxNetBuffer_B;
use_buffer_A = !use_buffer_A;
tx_buf->ValidPayloadLen = 0;
if (Preprocess_Execute(&raw_img, tx_buf, &meta) == 0)
{
TcpLogic_BuildAndSendTemperatureFrame(tx_buf, &meta, 0x01, 1);
}
burst_remaining--;
if (burst_remaining == 0) {
burst_active = 0;
DBG_APP("Burst complete\r\n");
} else {
ConfigCommon_t tc; Config2D_t t2; Config1D_t t1;
uint16_t interval_ms = DEFAULT_BURST_INTERVAL_MS;
if (TcpLogic_GetLatestConfig(&tc, &t2, &t1) == 0
&& t2.TriggerInternalIntervalMs > 0)
interval_ms = t2.TriggerInternalIntervalMs;
burst_next_time_ms = sys_tick_ms + interval_ms;
}
}
}
else if (Preprocess_CheckInternalTrigger2D(&raw_img) == 1)
{
/* Trigger hit! Start burst: this frame is frame #0 */
DBG_APP("TRIGGER frm=%d\r\n", (int)raw_img.FrameNumber);
PreprocessResult_t meta;
TcpTxBuffer_t *tx_buf = use_buffer_A ? &g_TxNetBuffer_A : &g_TxNetBuffer_B;
use_buffer_A = !use_buffer_A;
tx_buf->ValidPayloadLen = 0;
int8_t pp_ret = Preprocess_Execute(&raw_img, tx_buf, &meta);
if (pp_ret == 0)
{
int8_t tx_ret = TcpLogic_BuildAndSendTemperatureFrame(tx_buf, &meta, 0x01, 1);
DBG_APP("SEND frm=%d %dx%d ret=%d\r\n",
(int)meta.FrameNumber, (int)meta.ValidWidth,
(int)meta.ValidHeight, (int)tx_ret);
} else {
DBG_APP("PP fail ret=%d\r\n", (int)pp_ret);
}
/* Determine burst count from config (fall back to defaults) */
ConfigCommon_t tc; Config2D_t t2; Config1D_t t1;
uint8_t total_burst = DEFAULT_BURST_COUNT;
uint16_t interval_ms = DEFAULT_BURST_INTERVAL_MS;
if (TcpLogic_GetLatestConfig(&tc, &t2, &t1) == 0) {
if (t2.TriggerBurstCount >= 1)
total_burst = t2.TriggerBurstCount;
if (t2.TriggerInternalIntervalMs > 0)
interval_ms = t2.TriggerInternalIntervalMs;
}
if (total_burst > 1) {
burst_active = 1;
burst_remaining = total_burst - 1; /* frame #0 already sent */
burst_next_time_ms = sys_tick_ms + interval_ms;
DBG_APP("Burst start: %d frames, interval=%d ms\r\n",
(int)total_burst, (int)interval_ms);
}
}
} /* if trigger_armed */
} /* trigger scope */
#endif /* TEST_ENABLE_TRIGGER */
}
else
{
vTaskDelay(pdMS_TO_TICKS(2));
}
}
}
int main(void)
{
u8 i;
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(921600);
printf("TCPClient Test\r\nSystemClk:%d\r\n", SystemCoreClock);
printf("=== Feature Switches ===\r\n");
printf(" PATTERN=%d TRIGGER=%d NG_GPIO=%d TEMP_REQ=%d TCP_SEND=%d HB=%d\r\n",
TEST_PATTERN_MODE, TEST_ENABLE_TRIGGER, TEST_ENABLE_NG_GPIO,
TEST_ENABLE_TEMP_REQ, TEST_ENABLE_TCP_SEND, TEST_ENABLE_HEARTBEAT);
printf("UserByte: %02x\r\n", FLASH_GetUserOptionByte() & 0xFF);
Config_Flash_SRAM(FLASH_128_SRAM_192);
printf("net version:%x\n", WCHNET_GetVer());
if (WCHNET_LIB_VER != WCHNET_GetVer()) printf("version error.\n");
WCHNET_GetMacAddr(MACAddr);
printf("mac addr:");
for (i = 0; i < 6; i++) printf("%x ", MACAddr[i]);
printf("\n");
#if TEST_PATTERN_MODE
printf("=== TEST PATTERN MODE === No sensor/DVP hardware needed\r\n");
/* Skip Mini212G2_Init() and DVP_Init() */
#else
/* 模组已通过 USB 预配置,无需 MCU UART 配置,直接初始化 DVP */
DVP_Init();
#endif
TIM2_Init();
#if TEST_ENABLE_NG_GPIO
NG_GPIO_Init();
#endif
i = ETH_LibInit(IPAddr, GWIPAddr, IPMask, MACAddr);
mStopIfError(i);
if (i == WCHNET_ERR_SUCCESS) printf("WCHNET_LibInit Success\r\n");
#if KEEPALIVE_ENABLE
{
struct _KEEP_CFG cfg = {20000, 15000, 9};
WCHNET_ConfigKeepLive(&cfg);
}
#endif
qdx_port_init();
Preprocess_Init(SENSOR_WIDTH, SENSOR_HEIGHT);
#if TEST_PATTERN_MODE
/* Set default preprocess config so trigger fires with test patterns.
* Without this, all Config2D_t fields are 0 and trigger never fires
* (roi_w==0 immediate return 0). */
{
Config2D_t test_cfg2d;
memset(&test_cfg2d, 0, sizeof(test_cfg2d));
test_cfg2d.TargetWidth = 64; /* ROI: 64x64 = 8KB, fits in 10KB buffer */
test_cfg2d.TargetHeight = 64;
test_cfg2d.TriggerRoiX = 0; /* Full-frame trigger area */
test_cfg2d.TriggerRoiY = 0;
test_cfg2d.TriggerRoiW = SENSOR_WIDTH;
test_cfg2d.TriggerRoiH = SENSOR_HEIGHT;
test_cfg2d.TriggerCondition = 1; /* Max temperature */
test_cfg2d.TriggerTemperatureThreshold = 800; /* 80.0°C (0.1°C/LSB) */
test_cfg2d.TriggerBurstCount = 3;
test_cfg2d.TriggerInternalIntervalMs = 200;
test_cfg2d.NGioDelay = 200;
Preprocess_Settings_Change(&test_cfg2d, NULL, NULL);
printf("Test default config loaded: trigger thresh=800 ROI=full burst=3\r\n");
}
#endif
TcpLogic_Init(MACAddr, NULL);
TcpLogic_RegisterConfigCallback(OnConfigUpdate);
#if TEST_ENABLE_NG_GPIO
TcpLogic_RegisterDetectionCallback(OnDetectionResult);
#endif
#if TEST_ENABLE_TEMP_REQ
TcpLogic_RegisterTempFrameRequestCallback(OnTempFrameRequest);
#endif
DBG_APP("TcpLogic_Start...\r\n");
TcpLogic_Start();
DBG_APP("Creating RTOS tasks...\r\n");
xTaskCreate(task_wchnet_entry, "wchnet", 512, NULL, 6, NULL);
#if TEST_ENABLE_TCP_SEND
xTaskCreate(task_business_entry, "business", 512, NULL, 5, NULL);
#endif
#if TEST_ENABLE_HEARTBEAT
xTaskCreate(task_heartbeat_entry, "hb", 256, NULL, 3, NULL);
#endif
#if TEST_PATTERN_MODE
xTaskCreate(task_test_pattern_entry, "testpat", 256, NULL, 4, NULL);
#endif
DBG_APP("Starting scheduler\r\n");
vTaskStartScheduler();
/* Should never reach here */
while (1) {}
}