61 Commits

Author SHA1 Message Date
wandering1 8bb237e404 修复规范矛盾并定案5项规则,保护宏统一XXX_H
规则定案:常量在右(a>5)、保护宏XXX_H(禁双下划线)、
头文件常量用#define、存储类前缀(g_/s_)优先、允许void返回值
全库32个.h头文件保护宏 __XXX_H → XXX_H 改名(字节级替换)
S1检查规则同步更新(拒绝双下划线),Inc/gpio.h 反向验证被正确拦截
规范示例矛盾修复:K&R括号、标准类型、变量初始化、命名常量、注释字段补全
2026-09-01 16:07:00 +08:00
wandering1 b3beec633b 新增代码规范检查钩子(规范V1.0 可机械校验项)并修复存量违规
新增 scripts/check_coding_standard.py + coding-standard 钩子(S1~S9:
头文件保护宏、文件头注释、宏大写、_t 后缀、switch default、
禁 strcpy/sprintf/goto/Tab、函数注释)
修复存量违规:NET_* 字节序宏大写化、net_fd_set 等类型加 _t 后缀、
net_inet_ntoa 的 sprintf 改 snprintf、补 4 处函数注释与 ui_task.c 文件头
docs/代码规范/pre-commit使用说明.md:新增 2.8 节,修正失效文档路径
2026-09-01 15:41:25 +08:00
wandering1 0ccd07ee12 启用constVariablePointer检查并修复6处指针const化
.pre-commit-config.yaml: 移除 --suppress=constVariablePointer
net_select.c: 5 处 p_sock 改 const net_sock_t *(仅只读使用)
net_socket.c: net_getsockname 的 p_sock 改 const(只读字段)
docs/代码规范/cppcheck使用说明.md: 同步抑制表与处理记录
2026-09-01 15:11:17 +08:00
wandering1 a165c70875 gitignore 增加 LVGL 模拟器调试日志 sim_dbg.log 2026-09-01 15:04:57 +08:00
wandering1 961fe18ee6 删除根目录遗留的一次性乱码修复脚本(6 个)
fix_chinese_all/final/generic.py、fix_remaining.py、
dump_all_corruptions.py、git_compare.py

均为历史 U+FFFD 乱码事故的一次性修复脚本:
- 硬编码旧路径 D:\Code\DTU 程序\ 已失效
- 补丁字典针对的损坏文件早已修复
- 乱码防护已由 no-mojibake 钩子承担
2026-09-01 15:02:05 +08:00
wandering1 4bfa0c0b61 更新README:目录结构、pre-commit代码审查小节、文档索引
- 目录结构:docs/ 增加 代码规范/ 子目录与顶层文档
- 开发规范:新增「自动化代码审查(pre-commit)」小节
- 文档索引:修正代码规范文档移动后的路径,
  补充 UI / 墙钟时间 / LVGL 模拟器 / 测试流程等文档
2026-09-01 14:57:43 +08:00
wandering1 cbdeda6d90 修改文档文件夹架构 2026-09-01 14:53:34 +08:00
wandering1 ed0d23d79d 忽略文件得小改动 2026-09-01 14:52:31 +08:00
wandering1 381ce0c8b7 拆分cppcheck详解到独立文档,精简pre-commit使用说明
docs/cppcheck使用说明.md:
  - 新增:style 95 项规则详解、配置参数、YAML 引号陷阱、
    误报抑制方法、问题处理记录

docs/pre-commit使用说明.md:
  - 2.6 节精简为概览表 + 指向新文档
  - 更新 3.2 失败处理表与文件清单
2026-09-01 14:51:30 +08:00
wandering1 792719747a 修复cppcheck钩子YAML参数被逗号/空格拆坏导致钩子空转的问题
.pre-commit-config.yaml:
  - args 元素加引号,修正 YAML 流序列解析
  - 此前 --enable 只生效 warning 类别,-I 路径全部失效
  - 新增 --inline-suppr 启用行内抑制注释

App/sys_clock.c:
  - knownConditionTrueFalse 误报行内抑制
  - 闰年 diy=366 分支 cppcheck 数据流未跟踪

docs/pre-commit使用说明.md:
  - 补充 style 类别完整规则详解
  - 新增 YAML 引号陷阱排查方法、行内抑制用法
  - 修正 style 不阻断提交的错误论断
2026-09-01 14:41:05 +08:00
wandering1 ad035c2508 取消跟踪MDK-ARM构建/下载日志产物并加入gitignore 2026-09-01 14:07:05 +08:00
wandering1 ff78e93883 更新pre-commit使用说明:style问题全部处理完毕,记录knownConditionTrueFalse误报 2026-09-01 14:01:08 +08:00
wandering1 8d2dcd09a6 缩小变量作用域:消除18处variableScope,声明移入实际使用块 2026-09-01 13:59:44 +08:00
wandering1 205f3c2460 消除unreadVariable:移除3处冗余变量初始化 2026-09-01 13:45:02 +08:00
wandering1 a408c82ccb 抑制redundantAssignment:HAL清标志宏读寄存器覆盖是正常行为 2026-09-01 13:41:48 +08:00
wandering1 10b1d465c4 修复net_socket.c动态端口溢出检查:uint16_t>65535恒假改为==0 2026-09-01 13:40:06 +08:00
wandering1 9a2480c712 cppcheck启用style检查,抑制HAL回调噪音,更新使用说明 2026-09-01 13:37:54 +08:00
wandering1 400aba3590 完善pre-commit使用说明:补充cppcheck详解,更新no-mojibake状态 2026-09-01 13:26:40 +08:00
wandering1 64afbd71ed 修复tpafe5160.c历史U+FFFD乱码,取消no-mojibake豁免 2026-09-01 13:16:44 +08:00
wandering1 7b46a929b0 第一次修复 2026-09-01 12:17:12 +08:00
wandering1 5074d0e8ac 清理全库行尾空格与文件尾换行(pre-commit 自动修复基线) 2026-09-01 10:24:50 +08:00
wandering1 90c7f694aa 引入 pre-commit 自动化代码审查(格式/静态分析/乱码/commit message) 2026-09-01 10:24:08 +08:00
wandering1 a347cd18cb 搭建LVGL仿真环境 2026-08-31 17:09:59 +08:00
wandering1 9aa86bd561 移除不必要的内容 2026-08-31 15:37:50 +08:00
wandering1 bb407fe867 Remove large unused LVGL files (demos/tests/docs/examples) and build artifacts from tracking 2026-08-31 15:29:13 +08:00
wandering1 c7b799223c 正常运行了 2026-08-31 15:11:18 +08:00
wandering1 4340170305 开启了192k 所有RAM 2026-08-30 15:38:40 +08:00
wandering1 df59cd3d33 构建初始UI界面 2026-08-30 13:30:59 +08:00
wandering1 bc07701ed5 LVGL 库提交 2026-08-30 08:57:07 +08:00
wandering1 0b59118ddd 启动顺序管理 2026-08-29 11:10:08 +08:00
wandering1 652efb848d 整理了main 函数 2026-08-29 09:45:54 +08:00
wandering1 78eff31de7 文档结构调整 2026-08-28 22:02:24 +08:00
wandering1 565f23ea18 时钟测试完毕 2026-08-28 21:59:04 +08:00
wandering1 02a05a7a63 时间终于同步,存储成功 2026-08-28 21:10:51 +08:00
wandering1 4e9ac0d9c3 时间同步已经完成 2026-08-28 16:42:44 +08:00
wandering1 615a9b13e9 FTP 测试通过 2026-08-28 12:22:10 +08:00
wandering1 cf33171c8f 存储测试通过 2026-08-27 18:22:44 +08:00
wandering1 88160a5134 网络主要内容测试完成 2026-08-26 13:54:52 +08:00
wandering1 0a707034cc 删除多连接内容,只支持单连接 2026-08-25 12:41:33 +08:00
wandering1 d6bc3f6253 修改一些注释 2026-08-25 11:49:31 +08:00
wandering1 4375873083 CH395F 硬件测试完毕 2026-08-24 16:16:18 +08:00
wandering1 fc88bc8752 构建测试 2026-08-22 19:53:24 +08:00
wandering1 a7f367f6a3 修改测试 2026-07-22 11:14:22 +08:00
wandering1 c646df5c2a FTP可以上传文件 2026-07-22 01:46:29 +08:00
wandering1 038a2542af 测试程序可以使用 2026-07-21 23:05:20 +08:00
wandering1 46a74f24f3 FTP可以连接成功 2026-07-21 22:42:39 +08:00
wandering1 75cf4c9fbf 成功启动 2026-07-21 22:24:47 +08:00
wandering1 6d404d136e 初始创建FTP服务器 2026-07-21 22:08:11 +08:00
wandering1 d48be81478 删除FlashDB 2026-07-21 21:48:23 +08:00
wandering1 66a1e8fd6a 解决一个擦除的bug 2026-07-21 20:18:28 +08:00
wandering1 f50b9c9e66 修改网络的初始化,异步支持问题 2026-07-21 01:22:09 +08:00
wandering1 fbe0726991 增加文件系统 2026-07-21 00:30:59 +08:00
wandering1 e06d849007 修复多连接的问题 2026-07-19 15:27:49 +08:00
wandering1 9ed4c22022 freeRTOS支持 2026-07-19 13:09:06 +08:00
wandering1 4eb728583b 通过测试排除了一些问题 2026-07-19 04:32:18 +08:00
wandering1 1669cd216d 增加调试打印输出 2026-07-19 01:18:33 +08:00
wandering1 f72d08328f TCP可以正常使用 2026-07-19 00:59:56 +08:00
wandering1 fe107ea9af 关键芯片都驱动,且测试成功 2026-07-18 16:09:06 +08:00
wandering1 3466c4c062 网络通,测试flash 2026-07-17 15:16:29 +08:00
wandering1 c8c7ec0992 ch39f 驱动成功 2026-07-15 20:14:54 +08:00
wandering1 9943b410f4 Initial commit: 项目初始化 2026-06-05 11:56:47 +08:00
29 changed files with 449 additions and 484 deletions
-1
View File
@@ -1 +0,0 @@
*.jpg filter=lfs diff=lfs merge=lfs -text
+2 -2
View File
@@ -3,10 +3,10 @@ description: Build the STM32F4-Base Keil MDK project. Use ONLY when the user ask
agent: build
---
Run the Keil MDK-ARM build by executing the build script with the bash tool. The repo root is the working directory; use the repo-relative path (the old absolute path `D:\Code\DTU 程序\...` is stale):
Run the Keil MDK-ARM build by executing the build script with the bash tool. Use the absolute path, NOT relative cd:
```powershell
& "MDK-ARM\build.bat"
& "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM\build.bat"
```
Then read the tail of `MDK-ARM/build_log.txt` and report the result.
+2 -2
View File
@@ -3,10 +3,10 @@ description: Download (flash) the program to the STM32 via Keil MDK. Use ONLY wh
agent: build
---
Run the Keil MDK flash download by executing the download script with the bash tool. The repo root is the working directory; use the repo-relative path (the old absolute path `D:\Code\DTU 程序\...` is stale):
Run the Keil MDK flash download by executing the download script with the bash tool. Use the absolute path, NOT relative cd:
```powershell
& "MDK-ARM\download.bat"
& "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM\download.bat"
```
Then read the tail of `MDK-ARM/download_log.txt` and report the result.
+5 -5
View File
@@ -24,10 +24,10 @@ Read `.opencode/serial_config.json` for the authoritative `portName` (COM6) and
### Step 1 — Build verify
Run the build script (repo-relative; the repo root is the working directory):
Run the build script (absolute path):
```powershell
& "MDK-ARM\build.bat"
& "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM\build.bat"
```
Interpretation (per AGENTS.md): exit code 0 = PASS (0 errors, 0 warnings). 1/2+ = FAIL. If it fails, read `MDK-ARM/build_log.txt`, fix, and rerun `@build` until exit code 0 **before** touching the hardware.
@@ -39,10 +39,10 @@ then flashes, then collects and verifies — all in one call so the capture neve
times out before flashing:
```powershell
powershell -ExecutionPolicy Bypass -File "test\run_boot_test.ps1" `
powershell -ExecutionPolicy Bypass -File "D:\Code\DTU 程序\STM32F4-Base\test\run_boot_test.ps1" `
-Port COM6 -BaudRate 115200 `
-DurationMs 30000 -InitialTimeoutMs 90000 -IdleTimeoutMs 8000 -MaxBytes 65536 `
-FlashCmd "MDK-ARM\download.bat" `
-FlashCmd "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM\download.bat" `
-Required "App init OK;Network Init;net init: OK" `
-Forbidden "FAIL;ERROR;HardFault" `
-OutDir "C:\Users\16241\AppData\Local\Temp\opencode"
@@ -70,5 +70,5 @@ For interactive command/response (not boot-log capture):
- **⚠️ 无历史缓冲**:serial-mcp-server 的 `read` 只能读调用后的字节。抓开机日志必须走 `run_boot_test.ps1`(先采集等 first-byte → 再烧录触发)。
- **⚠️ EOF 陷阱**:COM 刚释放立即重开会报 `UnexpectedEof`(tokio-serial 读 0 字节)。`serial_capture.ps1` 内置自动重试;别裸跑 CLI capture。
- **参数**:`IdleTimeoutMs` 需 ≥ boot 自然静默间隙(本机 ~3-4s,用 8000);`InitialTimeoutMs` 需覆盖编译+烧录耗时(用 90000)。
- UI (LVGL) task `ui_task` 已有 `UI port init: OK` / `UI menu init: OK` 打印,可用 `-Required "UI port init: OK;UI menu init: OK"` 自动回归 UI 初始化。
- UI (LVGL) task `ui_task` 增加 `UI port init: OK` / `UI menu init: OK` 打印后,即可用 `-Required "UI port init: OK;UI menu init: OK"` 自动回归 UI 初始化。
- Full capture/analysis details: `docs/测试说明/serial-mcp-server_串口调试.md` and `docs/测试说明/自动化测试流程.md`.
+24 -71
View File
@@ -5,10 +5,8 @@
## 构建
- **仅支持 Keil MDK-ARM v5 (ARMCC)**,无其他工具链
- 命令行编译:`@build`(执行 `MDK-ARM/build.bat`,从仓库根目录用**相对路径**调用)
- `build.bat` 硬编码 Keil 路径 `C:\Keil_v5\UV4\UV4.exe`(本机已验证存在;换机器需改)
- 退出码:0 = 0 错误 0 警告 | 1 = 有警告或错误(查看 `MDK-ARM/build_log.txt`)
- ⚠️ **旧绝对路径陷阱**:仓库曾位于 `D:\Code\DTU 程序\STM32F4-Base`,`.opencode/command/*.md` 与 `test/run_boot_test.ps1` 中若出现该路径一律视为失效,改用仓库相对路径(如 `& "MDK-ARM\build.bat"`)
- 命令行编译:`@build`(自动执行 `MDK-ARM/build.bat`)
- 退出码:0 = 成功 | 1 = 有警告(不通过)| 2+ = 错误,查看 `MDK-ARM/build_log.txt`
## 硬件
@@ -18,7 +16,6 @@
| GD5F2GQ5UE NAND | SPI1 | PE0 CS, PB3 SCK, PB4 MISO, PB5 MOSI, PB8 WP, PE1 HOLD |
| TPAFE5160 ADC | 并行16位 | PG0-PG15 数据, PD3 RD, PD4 CONVST, PD7 BUSY |
| USART1 | 串口 | PA9 TX, PA10 RX (115200bps) |
| RS-485 | UART5 | PC12 TX, PD2 RX, PD0 DE(半双工) |
| 6x LED | GPIO | PC4, PC5, PB1, PB2, PF11, PF12(低电平点亮) |
MCU: STM32F407ZGTx @ 168MHz (HSE 8MHz → PLL M=4/N=168/P=2,见 `*.ioc`/`stm32f4xx_hal_conf.h`)
@@ -26,30 +23,22 @@ MCU: STM32F407ZGTx @ 168MHz (HSE 8MHz → PLL M=4/N=168/P=2,见 `*.ioc`/`stm32
## 架构
```
Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart
→ [FreeRTOS tasks: defaultTask, netTask, ftpTask, testTask, uiTask, timeSyncTask]
Src/main.c → 外设初始化 → app_main_init → gd5f2gq5ue_init → tpafe5160_init
→ osKernelInitialize → MX_FREERTOS_Init → osKernelStart
→ [FreeRTOS tasks: defaultTask, netTask, ftpTask]
```
- **app_main_init 顺序**(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init`(墙钟基准)→ `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → SD2506 诊断 → `tpafe5160_start_conv_irq`
- **就绪屏障**(`App/sys_events.h` Event Flags,取代魔法延时/全局轮询):`SYS_EVT_NET_READY`(bit0, netTask 置位) / `SYS_EVT_STORAGE_READY`(bit1, defaultTask 置位) / `SYS_EVT_FTP_READY`(bit2) / `SYS_EVT_TIMESYNC_READY`(bit3);消费者用 `osEventFlagsWait` 阻塞等待
- **defaultTask**:FatFS 挂载(`f_mount` forced,无卷自动 `f_mkfs` FAT32)→ 置 `SYS_EVT_STORAGE_READY` → LED 闪烁循环
- **netTask**:延时 2s → `net_stack_bring_up()`(net_init + MAC + PHY 强制 100M 全双工 + 等链路 ≤3s)→ 置 `SYS_EVT_NET_READY` → 循环 `net_poll()` + `net_process_messages()` + `osDelay(1)`(tick=1ms)
- **ftpTask**:`osEventFlagsWait(NET_READY | STORAGE_READY, WaitAll)` 后 `lftpd_start("/", 21, &ftp)`(已无 7s 魔法延时)
- **testTask**:测试套件入口(`test/test_config.h` 选择;产品构建下空转)
- **uiTask**:等 `SYS_EVT_NET_READY` 后 `ui_port_init` → `ui_menu_init` → 启动 UI net sink
- **timeSyncTask**:TCP 墙钟时间同步(PC 推送)
- 功能编译开关:`App/app_cfg.h`(`APP_NET_ENABLE` / `APP_ADC_ENABLE` / `APP_RS485_ENABLE`)
- **defaultTask**:FTL + FatFS 初始化测试(开机一次性)
- **netTask**:`net_init()` → `net_poll()` + `net_process_messages()` 每 10ms
- **ftpTask**:`lftpd_start("/", 21, &ftp)` 延迟 7s 后启动
### 任务栈
| Task | 栈大小(words) | 优先级 |
|---|---|---|
| defaultTask | 2048 | Normal |
| netTask | 4096 | Normal |
| ftpTask | 4096 | Normal |
| testTask | 4096 | BelowNormal |
| uiTask | 16384 | BelowNormal |
| timeSyncTask | 2048 | Normal |
| Task | 栈大小 |
|---|---|
| defaultTask | 2048 |
| netTask | 4096 |
| ftpTask | 4096 |
## CH395F 设计原则
@@ -58,45 +47,32 @@ Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart
## NET 网络层
- `net_socket.h/c` — BSD Socket API;**单连接模式**(`ch395f_set_fun_para(0x09)`:多 Socket + TCP Server 单连接 + 手动关闭),连接停留在 Socket 0
- `net_socket.h/c` — BSD Socket API,多连接模式下 Socket 0 监听、Socket 1~7 数据
- `net_select.h/c` — select/poll I/O 多路复用
- 线程安全通过消息队列(`netMsgQueue`)实现,所有 CH395F 操作在 netTask 中串行处理
- `net_init` 顺序:check_exist → reset → `set_fun_para(0x09)` → MSS/重传/KeepAlive → IP → `ch395f_init()` → Socket 0~7 缓冲分配 → ping 使能
- 静态 IP 192.168.1.100/24(gw 192.168.1.1),PHY 强制 100M 全双工
- 网络初始化顺序(多连接):`ch395f_set_fun_para(0x02)` → `ch395f_init()` → 配置 Socket 1~7(缓冲区+端口+协议,不 OPEN)→ 配置 Socket 0(TCP LISTEN)
- **新文件需手动添加到 Keil MDK 工程**才能编译
## 测试构建模式
- `test/test_config.h`:`TEST_SUITE_CH395F` / `TEST_SUITE_STORAGE` / `TEST_SUITE_GD5F` / `TEST_SUITE_RTC` **互斥**(一次构建只能启用一个,`#error`);全部注释 = 正常产品构建
- CH395F 套件沿用 `test/ch395f_test.h` 阶段宏:硬件层 Phase 1~5 与 NET 层 Phase 6~11 **互斥**(`#error`);硬件层模式下 netTask 不创建,testTask 独占 CH395F
- 各模块测试指南见 `docs/测试说明/`(CH395F / GD5F / SD2506 / FTP)
## 关键文件
| 路径 | 说明 |
|---|---|
| `Src/main.c` | 入口,CubeMX 生成 + USER CODE 区域 |
| `Src/freertos.c` | FreeRTOS task 创建,CubeMX 生成 + USER CODE 区域 |
| `App/app_main.c` | 应用初始化流程编排 |
| `App/sys_events.h` | 系统就绪事件(Event Flags 就绪屏障) |
| `App/app_cfg.h` | 功能模块编译开关 |
| `App/task/net_task.c` | netTask 主循环(网络初始化 + 轮询) |
| `test/test_config.h` | 测试套件选择(互斥开关) |
| `Drivers/BSP/NET/` | BSD Socket API 网络抽象层 + lftpd |
| `Drivers/BSP/NET/` | BSD Socket API 网络抽象层 |
| `Drivers/BSP/CH395F/` | CH395F SPI 驱动 |
| `Drivers/BSP/GD5F2GQ5UE/` | NAND Flash 驱动 + dhara FTL + FatFS diskio |
| `Drivers/BSP/TPAFE5160/` | ADC 并行接口驱动 |
| `Drivers/BSP/SD2506/` | RTC 驱动 |
| `Drivers/BSP/RS485/` | RS-485 通信(UART5 + PD0 DE) |
| `Drivers/BSP/UI/` | LVGL UI 组件(与 PC 模拟器共用源码) |
## 驱动关键点
详见 `docs/` 目录:
- `docs/问题记录/CH395F_Trap_Records.md` — CH395F 已知陷阱
- `docs/问题记录/GD5F2GQ5UE_Trap_Records.md` — GD5F2GQ5UE 已知陷阱
- `docs/问题记录/SD2506_Trap_Records.md` — SD2506 RTC 已知陷阱
- `docs/测试说明/CH395F_Test_Guide.md` — 网络测试说明(含构建模式与执行顺序)
- `CH395F_Trap_Records.md` — CH395F 已知陷阱
- `GD5F2GQ5UE_Trap_Records.md` — GD5F2GQ5UE 已知陷阱
- `CH395F_Test_Guide.md` — 网络测试说明
## 串口调试(serial-mcp-server MCP/CLI)
@@ -109,43 +85,20 @@ Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart
改完代码后运行 **`@test`** 可自动执行完整闭环:**构建验证 → 串口捕获武装(serial-mcp-server 采集窗口)→ 烧录(`MDK-ARM/download.bat`,下载后 Reset & Run)→ 解析捕获 JSON → `test/verify_log.ps1` 标记校验 → PASS/FAIL 判定并迭代**。
- **推荐一键命令**:`test/run_boot_test.ps1` = 采集(含 EOF 重试)→ 烧录 → 提取日志 → verify 全流程,退出码 0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=日志文件错误
- **推荐一键命令**:`test/run_boot_test.ps1` = 采集(含 EOF 重试)→ 烧录 → 提取日志 → verify 全流程,退出码 0=PASS/1=FAIL/2=错误
- 判定标准:`@test` 内部先 `@build`(0 错误 0 警告),再 `@download`(退出码 0),日志用 `verify_log.ps1` 校验(默认 Required:`App init OK`/`Network Init`/`net init: OK`;Forbidden:`FAIL`/`ERROR`/`HardFault`)
- **采集必须在烧录前武装**(采集进程先打开 COM 等 first-byte,烧录触发复位后才抓得到开机日志);`run_boot_test.ps1` 的 `-FlashCmd` 分支已保证顺序
- 每次按模块定制标记(网络/存储/RTC/ADC/RS485)见 `docs/测试说明/自动化测试流程.md`
- UI 已有 DBG 打印(`UI port init: OK` / `UI menu init: OK` / `UI net sink: started`),可用 `-Required "UI port init: OK;UI menu init: OK"` 自动回归 UI 初始化;屏幕内容仍需人工目检
## 代码规范与 pre-commit
- **提交被 pre-commit 钩子门禁**,任一失败即拦截:cppcheck 静态分析(warning/performance/portability/style)+ 代码规范机械检查 + U+FFFD 乱码检查 + gitlint commit message
- 代码规范 V1.0(`docs/代码规范/嵌入式C语言代码规范(V1.0).md`):命名小写下划线(`g_`/`s_`/`p_`/`a_` 前缀)、头文件保护宏 `XXX_H`、常量在右、文件头模块注释块等;可机械校验项由 `scripts/check_coding_standard.py` 强制
- 检查范围仅项目自有代码;第三方(`Lib/`、`Middlewares/`、CMSIS/HAL、`lftpd`、`Drivers/BSP/UI`、`test/`)被排除
- 新机器启用:`pre-commit install`;全库扫描:`pre-commit run --all-files`
- 详见 `docs/代码规范/pre-commit使用说明.md`、`docs/代码规范/cppcheck使用说明.md`
## LVGL PC 模拟器
`simulator/`(CMake + SDL2 2.32.10 预编译库)可在 PC 上开发调试 UI,**与 MCU 端共用 UI 源码**(`Drivers/BSP/UI/ui_menu.c`、`ui_font_cn_12.c`),无需烧录:
```powershell
cd simulator
cmake -B build -G "Visual Studio 16 2019" -A x64
cmake --build build --config Debug
# 运行 simulator\build\Debug\lvgl_sim.exe
```
- 详见 `docs/UI 设计/LVGL模拟器部署与使用说明.md`
- 中文字体:`ui_cn_chars.txt`(字符集)+ `tools/font_gen/` 生成 `Drivers/BSP/UI/ui_font_cn_12.c`,见 `docs/UI 设计/UI_中文字体生成与配置.md`
- UI(`App/task/ui_task.c`)当前无 DBG 打印,无法从串口自动验证;如需 UI 回归,先在固件加 `UI port init: OK` / `UI menu init: OK` 打印再纳入 Required
## 注意
- `Inc/` 和 `Src/` 中 CubeMX 生成的文件带有 `USER CODE BEGIN`/`END` 标记,自定义代码写在这些区域之间
- `Drivers/BSP/` 下的驱动文件为纯手工代码,不受 CubeMX 保护
- CH395F 每次 SPI 事务需用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹
- CH395F SPI PCB 设计问题,分频系数必须为 8,速度相对较低
- FreeRTOS V10.3.1 via CMSIS-RTOS V2,HAL 时基用 TIM7(非 SysTick),tick = 1ms
- CH395F SPI PCB设计问题,现在分频系数必须为 8,速度相对较低
- FreeRTOS V10.3.1 via CMSIS-RTOS V2,HAL 时基用 TIM7(非 SysTick)
- NVIC 优先级分组 4 位,外设中断优先级 ≥ 5(`configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`)
- **内存预算紧张**:任务栈目前普遍偏大(见 `待处理内容.md`),新增任务/缓冲/栈前必须先核算 RAM 余量
## 源码编辑与编码安全(铁律)
@@ -153,6 +106,6 @@ cmake --build build --config Debug
- **绝不用 `Get-Content` / `Set-Content` / `cmd` 文本重写含中文的源码文件**。这类"整文件重写"会按系统默认编码重新解码再写回,多字节中文必被 `U+FFFD` 替换且不可逆。
- **改代码只用 Edit 工具**(精确子串替换,不重新编解码整个文件,安全);批量替换用 Edit 的 `replaceAll`。
- 必须脚本级替换时,使用显式且一致的编码(**本工程源码为 UTF-8 无 BOM**,如 Python `open(p, encoding="utf-8")` 同编码回写)——但**最稳的是别碰**,只用 Edit / git。
- 必须脚本级替换时,使用显式且一致的编码(如 Python `open(p, encoding="utf-8")` 同编码回写),并先确认工程编码(Keil 工程常用 GBK)——但**最稳的是别碰**,只用 Edit / git。
- **发现乱码先别急着 `git checkout`**:`git checkout` 会丢弃所有未提交工作,使损失扩大。先评估能否用 Edit 仅改坏掉的那几行。
- 验证是否真损坏用字节级检查(统计 `EF BF BD` 出现次数),不要依赖终端/Read 渲染判断。
+4 -2
View File
@@ -40,7 +40,8 @@
* 5) SD2506 诊断信息
* 6) 触发首次 ADC 转换
*/
int app_main_init(void) {
int app_main_init(void)
{
/*
* 1) 时间子系统初始化(尽早完成,后续所有日志带时间戳)
* sd2506_init() 从 RTC 读取初始时间
@@ -112,7 +113,8 @@ int app_main_init(void) {
* 限定条件:FreeRTOS 调度器启动后不再返回
* 函数说明:空闲任务可调用的应用层处理入口
*/
void app_main_run(void) {
void app_main_run(void)
{
for (;;) {
/* 应用层空闲处理 */
}
+14 -7
View File
@@ -54,7 +54,8 @@ static volatile uint32_t s_base_unix_secs = UINT32_MAX;
* 函数说明:1. 采用 Zeller 公式(变种),结果 0 表示周日
* 2. 与 unix_to_sd2506() 内星期算法保持一致
*/
static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d) {
static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d)
{
uint32_t wy = y;
uint32_t wm = m;
@@ -78,7 +79,8 @@ static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d) {
* 2. 星期由 Zeller 公式(与 rtc_calc_week 同算法)得出
* 3. SD2506 年份字段为相对值(0~99,+2000),故回写时减 2000
*/
static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time) {
static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time)
{
uint8_t h, mi, s;
/* 时分秒 */
@@ -161,7 +163,8 @@ static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time) {
* 4. 注意:sd2506_get_time() 返回的 year 已是完整年份(驱动内 +2000),
* 换算时不得再加 2000,否则年份虚增导致 Unix 秒数超 uint32 回绕
*/
int sys_clock_init(void) {
int sys_clock_init(void)
{
sd2506_time_t sd_time;
uint32_t year, month, day, hour, minute, second;
int64_t unix_secs = 0;
@@ -285,7 +288,8 @@ int sys_clock_init(void) {
* 的整除差推算流逝秒数(避免每毫秒累加,减少漂移与溢出风险)
* 2. 若推算结果为负(基准未建立),返回 UINT32_MAX 表示无效
*/
uint32_t sys_clock_get(void) {
uint32_t sys_clock_get(void)
{
uint32_t now_tick = HAL_GetTick();
int64_t unix_secs = (int64_t)s_base_unix_secs + (int64_t)((uint32_t)(now_tick - s_base_tick) / 1000uL);
@@ -307,7 +311,8 @@ uint32_t sys_clock_get(void) {
* 2. 通过 unix_to_sd2506() 转换为 SD2506 结构后格式化
* 3. 使用 snprintf 防止缓冲区溢出
*/
char *sys_clock_get_str(char *buf, size_t buf_size) {
char *sys_clock_get_str(char *buf, size_t buf_size)
{
uint32_t unix_secs = sys_clock_get();
if (unix_secs == UINT32_MAX) {
snprintf(buf, buf_size, "----/--/-- --:--:--");
@@ -333,7 +338,8 @@ char *sys_clock_get_str(char *buf, size_t buf_size) {
* 函数说明:1. 重置基准 Tick 与基准 Unix 秒数,使后续 sys_clock_get() 以此为准
* 2. 若 sd_time 非 NULL,将其写回 SD2506 RTC,实现断电保持
*/
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time) {
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time)
{
/* Step 1: 更新墙钟基准 */
s_base_tick = HAL_GetTick();
s_base_unix_secs = unix_secs;
@@ -360,7 +366,8 @@ int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time) {
* 函数说明:将 Unix 秒数转换为 SD2506 时间结构,再调用 sys_clock_set()
* 更新运行期基准并写回 RTC,实现掉电保持
*/
int sys_clock_set_unix(uint32_t unix_secs) {
int sys_clock_set_unix(uint32_t unix_secs)
{
sd2506_time_t sd;
/* 内部静态函数:将 Unix 秒数拆分为 SD2506 时间结构 */
+4 -2
View File
@@ -22,7 +22,8 @@
* 限定条件:TPAFE5160 硬件已上电
* 函数说明:配置采样参数,准备数据缓冲区
*/
int adc_task_init(void) {
int adc_task_init(void)
{
DBG_INFO("adc_task_init");
return 0;
}
@@ -34,7 +35,8 @@ int adc_task_init(void) {
* 限定条件:adc_task_init() 已成功调用
* 函数说明:定时触发 TPAFE5160 同步采样并处理数据
*/
void adc_task_func(void const *arg) {
void adc_task_func(void const *arg)
{
(void)arg;
for (;;) {
+6 -3
View File
@@ -31,7 +31,8 @@
* NET 层模式由 netTask 调用;硬件层模式(阶段 1~5)netTask 不创建,
* 由 testTask 调用后独占 CH395F。
*/
int net_stack_bring_up(void) {
int net_stack_bring_up(void)
{
DBG_INFO("=== Network Init ===");
if (net_init("192.168.1.100", "255.255.255.0", "192.168.1.1") != 0) {
@@ -86,7 +87,8 @@ int net_stack_bring_up(void) {
* 限定条件:CH395F 协议栈已初始化
* 函数说明:创建 TCP Server 监听 Socket,注册回调等
*/
int net_task_init(void) {
int net_task_init(void)
{
DBG_INFO("net_task_init");
return 0;
}
@@ -100,7 +102,8 @@ int net_task_init(void) {
* 仅 NET 层测试模式(阶段 6~10)会创建本任务;
* 硬件层模式下 netTask 不创建,CH395F 由 testTask 独占。
*/
void net_task_func(void const *arg) {
void net_task_func(void const *arg)
{
(void)arg;
osDelay(2000);
+4 -2
View File
@@ -22,7 +22,8 @@
* 限定条件:UART 和 GPIO 已由 CubeMX 初始化
* 函数说明:配置 RS-485 通信参数,注册接收回调
*/
int rs485_task_init(void) {
int rs485_task_init(void)
{
DBG_INFO("rs485_task_init");
return 0;
}
@@ -34,7 +35,8 @@ int rs485_task_init(void) {
* 限定条件:rs485_task_init() 已成功调用
* 函数说明:轮询接收缓冲区并解析协议帧
*/
void rs485_task_func(void const *arg) {
void rs485_task_func(void const *arg)
{
(void)arg;
for (;;) {
+4 -2
View File
@@ -25,7 +25,8 @@
* 限定条件:sys_evtHandle 为 NULL 时直接返回
* 函数说明:无限期等待 SYS_EVT_NET_READY 标志
*/
static void ui_task_wait_net_ready(void) {
static void ui_task_wait_net_ready(void)
{
if (sys_evtHandle != NULL) {
osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny, osWaitForever);
@@ -40,7 +41,8 @@ static void ui_task_wait_net_ready(void) {
* 函数说明:1. 等待 SYS_EVT_NET_READY 后依次初始化 ui_port/ui_menu/ui_net_sink
* 2. 主循环周期调用 ui_port_step 与 ui_net_sink_poll 刷新界面
*/
void ui_task_func(void *argument) {
void ui_task_func(void *argument)
{
(void)argument;
DBG_INFO("UI task: started, wait net ready");
+6 -3
View File
@@ -33,7 +33,8 @@
* 单连接模式下监听 Socket 在收到连接后会转为数据通道,连接关闭后即被释放,
* 因此每次调用都会新建监听 Socket(与 lftpd 同模式),失败自动重试。
*/
static int time_sync_open_listener(void) {
static int time_sync_open_listener(void)
{
int retry;
for (retry = 0; retry < 5; retry++) {
@@ -77,7 +78,8 @@ static int time_sync_open_listener(void) {
* 返回值:无
* 函数说明:阻塞读取 PC 发来的时间包("TIME"+uint32 大端 本地时间),解析后写回 RTC,然后关闭连接
*/
static void time_sync_handle_conn(int conn_sock) {
static void time_sync_handle_conn(int conn_sock)
{
uint8_t buf[16];
int len;
@@ -104,7 +106,8 @@ static void time_sync_handle_conn(int conn_sock) {
* 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> 关闭 -> 重建监听。
* 全部 socket 操作经消息队列由 netTask 串行执行,与本任务/其它网络任务并发安全。
*/
void time_sync_task(void *argument) {
void timeSyncTask(void *argument)
{
(void)argument;
/* 等网络栈真正就绪(取代拍脑袋的 8s 魔法延时),确保 netTask 已开始处理消息 */
+1 -1
View File
@@ -31,7 +31,7 @@ extern "C" {
* 经消息队列(由 netTask 串行执行)完成 socket 操作,更新系统墙钟。
* 该任务与 FTP 等其它网络任务并发安全。
*/
void time_sync_task(void *argument);
void timeSyncTask(void *argument);
#ifdef __cplusplus
}
+4 -2
View File
@@ -89,7 +89,8 @@ static const uint8_t s_crc8_table[256] = {
* 限定条件:data 为非空指针
* 函数说明:多项式 0x8005,初始值 0xFFFF,结果异或 0x0000
*/
uint16_t crc16_modbus(const uint8_t *data, size_t len) {
uint16_t crc16_modbus(const uint8_t *data, size_t len)
{
uint16_t crc = 0xFFFF;
size_t i;
@@ -108,7 +109,8 @@ uint16_t crc16_modbus(const uint8_t *data, size_t len) {
* 限定条件:data 为非空指针
* 函数说明:多项式 0x07,初始值 0x00
*/
uint8_t crc8(const uint8_t *data, size_t len) {
uint8_t crc8(const uint8_t *data, size_t len)
{
uint8_t crc = 0;
size_t i;
+18 -9
View File
@@ -19,7 +19,8 @@
* 限定条件:rb 和 pool 均为非空指针
* 函数说明:size 保留一个空位用于区分空/满状态
*/
void ringbuf_init(ringbuf_t *rb, uint8_t *pool, size_t size) {
void ringbuf_init(ringbuf_t *rb, uint8_t *pool, size_t size)
{
rb->buf = pool;
rb->size = size;
rb->head = 0;
@@ -35,7 +36,8 @@ void ringbuf_init(ringbuf_t *rb, uint8_t *pool, size_t size) {
* 限定条件:rb 和 data 均为非空指针
* 函数说明:空间不足时写入能容纳的最大字节数
*/
size_t ringbuf_put(ringbuf_t *rb, const uint8_t *data, size_t len) {
size_t ringbuf_put(ringbuf_t *rb, const uint8_t *data, size_t len)
{
size_t cap = 0;
size_t n = 0;
size_t i = 0;
@@ -60,7 +62,8 @@ size_t ringbuf_put(ringbuf_t *rb, const uint8_t *data, size_t len) {
* 限定条件:rb 和 data 均为非空指针
* 函数说明:读取后数据从缓冲区移除
*/
size_t ringbuf_get(ringbuf_t *rb, uint8_t *data, size_t len) {
size_t ringbuf_get(ringbuf_t *rb, uint8_t *data, size_t len)
{
size_t n = 0;
size_t i = 0;
@@ -84,7 +87,8 @@ size_t ringbuf_get(ringbuf_t *rb, uint8_t *data, size_t len) {
* 限定条件:rb 和 data 均为非空指针
* 函数说明:与 ringbuf_get 的区别是不移动 tail 指针
*/
size_t ringbuf_peek(const ringbuf_t *rb, uint8_t *data, size_t len) {
size_t ringbuf_peek(const ringbuf_t *rb, uint8_t *data, size_t len)
{
size_t n = 0;
size_t i = 0;
@@ -105,7 +109,8 @@ size_t ringbuf_peek(const ringbuf_t *rb, uint8_t *data, size_t len) {
* 返 回 值:有效数据字节数
* 限定条件:rb 为非空指针
*/
size_t ringbuf_avail(const ringbuf_t *rb) {
size_t ringbuf_avail(const ringbuf_t *rb)
{
if (rb->head >= rb->tail) {
return rb->head - rb->tail;
}
@@ -118,7 +123,8 @@ size_t ringbuf_avail(const ringbuf_t *rb) {
* 返 回 值:剩余空间字节数(保留一个空位)
* 限定条件:rb 为非空指针
*/
size_t ringbuf_space(const ringbuf_t *rb) {
size_t ringbuf_space(const ringbuf_t *rb)
{
return rb->size - 1 - ringbuf_avail(rb);
}
@@ -128,7 +134,8 @@ size_t ringbuf_space(const ringbuf_t *rb) {
* 返 回 值:1 - 空,0 - 非空
* 限定条件:rb 为非空指针
*/
int ringbuf_is_empty(const ringbuf_t *rb) {
int ringbuf_is_empty(const ringbuf_t *rb)
{
return (rb->head == rb->tail) ? 1 : 0;
}
@@ -138,7 +145,8 @@ int ringbuf_is_empty(const ringbuf_t *rb) {
* 返 回 值:1 - 满,0 - 非满
* 限定条件:rb 为非空指针
*/
int ringbuf_is_full(const ringbuf_t *rb) {
int ringbuf_is_full(const ringbuf_t *rb)
{
return (ringbuf_avail(rb) == rb->size - 1) ? 1 : 0;
}
@@ -148,7 +156,8 @@ int ringbuf_is_full(const ringbuf_t *rb) {
* 返 回 值:无
* 限定条件:rb 为非空指针
*/
void ringbuf_reset(ringbuf_t *rb) {
void ringbuf_reset(ringbuf_t *rb)
{
rb->head = 0;
rb->tail = 0;
}
+21 -21
View File
@@ -60,12 +60,12 @@ int net_select(int nfds, net_fd_set_t *readfds, net_fd_set_t *writefds,
int i;
/* 检查输入参数合法性 */
if ((nfds <= 0) || (nfds > NET_MAX_SOCKETS)) {
if ((0 >= nfds) || (NET_MAX_SOCKETS < nfds)) {
return -1;
}
/* 计算超时时间(毫秒) */
if (timeout != NULL) {
if (NULL != timeout) {
timeout_ms = (uint32_t)(timeout->tv_sec * 1000) +
(uint32_t)(timeout->tv_usec / 1000);
}
@@ -80,12 +80,12 @@ int net_select(int nfds, net_fd_set_t *readfds, net_fd_set_t *writefds,
for (i = 0; i < nfds && i < NET_MAX_SOCKETS; i++) {
const net_sock_t *p_sock = net_get_sock(i);
if (p_sock == NULL) {
if (NULL == p_sock) {
continue;
}
/* 检查可读事件 */
if ((readfds != NULL) && NET_FD_ISSET(i, readfds)) {
if ((NULL != readfds) && NET_FD_ISSET(i, readfds)) {
if (check_socket_readable(i)) {
NET_FD_SET(i, &result_read);
ready_count++;
@@ -93,7 +93,7 @@ int net_select(int nfds, net_fd_set_t *readfds, net_fd_set_t *writefds,
}
/* 检查可写事件 */
if ((writefds != NULL) && NET_FD_ISSET(i, writefds)) {
if ((NULL != writefds) && NET_FD_ISSET(i, writefds)) {
if (check_socket_writable(i)) {
NET_FD_SET(i, &result_write);
ready_count++;
@@ -101,7 +101,7 @@ int net_select(int nfds, net_fd_set_t *readfds, net_fd_set_t *writefds,
}
/* 检查异常事件 */
if ((exceptfds != NULL) && NET_FD_ISSET(i, exceptfds)) {
if ((NULL != exceptfds) && NET_FD_ISSET(i, exceptfds)) {
if (check_socket_error(i)) {
NET_FD_SET(i, &result_except);
ready_count++;
@@ -110,13 +110,13 @@ int net_select(int nfds, net_fd_set_t *readfds, net_fd_set_t *writefds,
}
/* 有事件就绪,返回 */
if (ready_count > 0) {
if (0 < ready_count) {
break;
}
/* 超时检测 */
if (timeout != NULL) {
if ((timeout_ms > 0) &&
if (NULL != timeout) {
if ((0 < timeout_ms) &&
((HAL_GetTick() - start_tick) >= timeout_ms)) {
break; /* 超时返回 0 */
}
@@ -127,13 +127,13 @@ int net_select(int nfds, net_fd_set_t *readfds, net_fd_set_t *writefds,
}
/* 复制结果集合 */
if (readfds != NULL) {
if (NULL != readfds) {
readfds->fd_bits = result_read.fd_bits;
}
if (writefds != NULL) {
if (NULL != writefds) {
writefds->fd_bits = result_write.fd_bits;
}
if (exceptfds != NULL) {
if (NULL != exceptfds) {
exceptfds->fd_bits = result_except.fd_bits;
}
@@ -158,7 +158,7 @@ int net_poll_events(net_pollfd_t *fds, int nfds, int timeout) {
int ready = 0;
int i;
if ((fds == NULL) || (nfds <= 0)) {
if ((NULL == fds) || (0 >= nfds)) {
return -1;
}
@@ -172,7 +172,7 @@ int net_poll_events(net_pollfd_t *fds, int nfds, int timeout) {
fds[i].revents = 0;
/* 检查 fd 有效性 */
if ((fds[i].fd < 0) || (fds[i].fd >= NET_MAX_SOCKETS)) {
if ((0 > fds[i].fd) || (NET_MAX_SOCKETS <= fds[i].fd)) {
fds[i].revents |= NET_POLLNVAL;
ready++;
continue;
@@ -180,7 +180,7 @@ int net_poll_events(net_pollfd_t *fds, int nfds, int timeout) {
const net_sock_t *p_sock = net_get_sock(fds[i].fd);
if (p_sock == NULL) {
if (NULL == p_sock) {
fds[i].revents |= NET_POLLNVAL;
ready++;
continue;
@@ -215,12 +215,12 @@ int net_poll_events(net_pollfd_t *fds, int nfds, int timeout) {
}
}
if (ready > 0) {
if (0 < ready) {
break;
}
/* 超时检测 */
if (timeout >= 0) {
if (0 <= timeout) {
if ((HAL_GetTick() - start_tick) >= (uint32_t)timeout) {
break;
}
@@ -250,7 +250,7 @@ static int check_socket_readable(int sockfd) {
const net_sock_t *p_sock;
p_sock = net_get_sock(sockfd);
if (p_sock == NULL) {
if (NULL == p_sock) {
return 0;
}
@@ -280,7 +280,7 @@ static int check_socket_writable(int sockfd) {
const net_sock_t *p_sock;
p_sock = net_get_sock(sockfd);
if (p_sock == NULL) {
if (NULL == p_sock) {
return 0;
}
@@ -307,12 +307,12 @@ static int check_socket_error(int sockfd) {
const net_sock_t *p_sock;
/* 无效 fd 视为错误 */
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
if ((0 > sockfd) || (NET_MAX_SOCKETS <= sockfd)) {
return 1;
}
p_sock = net_get_sock(sockfd);
if (p_sock == NULL) {
if (NULL == p_sock) {
return 1; /* 未使用的 Socket 视为错误 */
}
+32 -32
View File
@@ -129,7 +129,7 @@ int net_init(const char *ip, const char *mask, const char *gateway) {
ch395f_set_keepalive_cnt(3); /* 最多探 3 次*/
/* 设置网络参数(必须在 INIT_CH395 之前调用 */
if (ip != NULL) {
if (NULL != ip) {
uint8_t ip_arr[4];
uint8_t mask_arr[4];
uint8_t gw_arr[4];
@@ -300,7 +300,7 @@ int net_poll(void) {
void net_process_messages(void) {
net_msg_t msg;
if (netMsgQueueHandle == NULL) {
if (NULL == netMsgQueueHandle) {
return;
}
@@ -308,7 +308,7 @@ void net_process_messages(void) {
switch (msg.type) {
case NET_MSG_SEND: {
net_sock_t *p_sock = net_get_sock(msg.sockfd);
if (p_sock == NULL || p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
if (NULL == p_sock || p_sock->state != NET_SOCK_STATE_ESTABLISHED) {
msg.result = -1;
} else {
msg.result = net_send_locked(p_sock, msg.buf, msg.len);
@@ -317,7 +317,7 @@ void net_process_messages(void) {
}
case NET_MSG_RECV: {
net_sock_t *p_sock = net_get_sock(msg.sockfd);
if (p_sock == NULL) {
if (NULL == p_sock) {
msg.result = -1;
} else {
msg.result = net_recv_locked(p_sock, msg.buf, msg.len, msg.flags);
@@ -326,7 +326,7 @@ void net_process_messages(void) {
}
case NET_MSG_CLOSE: {
net_sock_t *p_sock = net_get_sock(msg.sockfd);
if (p_sock == NULL) {
if (NULL == p_sock) {
msg.result = -1;
} else {
msg.result = net_close_locked(p_sock);
@@ -346,7 +346,7 @@ void net_process_messages(void) {
case NET_MSG_ACCEPT: {
struct net_sockaddr *p_addr = (struct net_sockaddr *)msg.buf;
int addrlen = msg.len;
int *p_addrlen = (p_addr != NULL && addrlen > 0) ? &addrlen : NULL;
int *p_addrlen = (NULL != p_addr && addrlen > 0) ? &addrlen : NULL;
msg.result = net_accept_locked(msg.sockfd, p_addr, p_addrlen);
break;
}
@@ -423,7 +423,7 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) {
return -1;
}
if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) {
if (NULL == addr || addrlen < (int)sizeof(struct net_sockaddr_in)) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
@@ -567,7 +567,7 @@ int net_listen(int sockfd) {
return -1;
}
if (netMsgQueueHandle == NULL) {
if (NULL == netMsgQueueHandle) {
s_net_errno = NET_ERR;
return -1;
}
@@ -627,7 +627,7 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen
}
/* 填充对端地址(若调用方需要) */
if (addr != NULL && addrlen != NULL) {
if (NULL != addr && NULL != addrlen) {
struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)addr;
p_addr_in->sin_family = NET_AF_INET;
p_addr_in->sin_port = net_htons(p_listen_sock->remote_port);
@@ -656,7 +656,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
return -1;
}
if (netMsgQueueHandle == NULL) {
if (NULL == netMsgQueueHandle) {
s_net_errno = NET_ERR;
return -1;
}
@@ -664,7 +664,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
msg.type = NET_MSG_ACCEPT;
msg.sockfd = sockfd;
msg.buf = (void *)addr;
msg.len = (addrlen != NULL) ? *addrlen : 0;
msg.len = (NULL != addrlen) ? *addrlen : 0;
msg.caller = xTaskGetCurrentTaskHandle();
msg.result = -1;
@@ -679,7 +679,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) {
msg.result = (int)s_notified;
}
if (addrlen != NULL &&addr != NULL) {
if (NULL != addrlen &&NULL != addr) {
*addrlen = sizeof(struct net_sockaddr_in);
}
@@ -709,12 +709,12 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) {
return -1;
}
if (netMsgQueueHandle == NULL) {
if (NULL == netMsgQueueHandle) {
s_net_errno = NET_ERR;
return -1;
}
if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) {
if (NULL == addr || addrlen < (int)sizeof(struct net_sockaddr_in)) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
@@ -767,7 +767,7 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a
return -1;
}
if (addr == NULL || addrlen < (int)sizeof(struct net_sockaddr_in)) {
if (NULL == addr || addrlen < (int)sizeof(struct net_sockaddr_in)) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
@@ -865,7 +865,7 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) {
uint32_t tick_start;
int sockfd = (int)(p_sock - s_net_socks);
if (buf == NULL || len <= 0) {
if (NULL == buf || len <= 0) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
@@ -928,7 +928,7 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags) {
uint32_t tick_start;
int sockfd = (int)(p_sock - s_net_socks);
if (buf == NULL || len <= 0) {
if (NULL == buf || len <= 0) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
@@ -1117,7 +1117,7 @@ int net_send(int sockfd, const void *buf, int len, int flags) {
}
/* ---- 2. 运行环境检查:消息队列须已创建(网络层已初始化) ---- */
if (netMsgQueueHandle == NULL) {
if (NULL == netMsgQueueHandle) {
s_net_errno = NET_ERR; /* 网络层未初始化 */
return -1;
}
@@ -1172,7 +1172,7 @@ int net_recv(int sockfd, void *buf, int len, int flags) {
return -1;
}
if (netMsgQueueHandle == NULL) {
if (NULL == netMsgQueueHandle) {
s_net_errno = NET_ERR;
return -1;
}
@@ -1219,7 +1219,7 @@ int net_close(int sockfd) {
return -1;
}
if (netMsgQueueHandle == NULL) {
if (NULL == netMsgQueueHandle) {
s_net_errno = NET_ERR;
return -1;
}
@@ -1277,13 +1277,13 @@ int net_sendto(int sockfd, const void *buf, int len, int flags,
return -1;
}
if (buf == NULL || len <= 0) {
if (NULL == buf || len <= 0) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
/* UDP 模式:设置目标地址 */
if (dest_addr != NULL && addrlen >= (int)sizeof(struct net_sockaddr_in)) {
if (NULL != dest_addr && addrlen >= (int)sizeof(struct net_sockaddr_in)) {
p_addr_in = (const struct net_sockaddr_in *)dest_addr;
/* 转换 IP 为大端数组(同 TCP 客户端,set_des_ip 期望大端) */
@@ -1365,7 +1365,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
return -1;
}
if (buf == NULL || len <= 0) {
if (NULL == buf || len <= 0) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
@@ -1384,7 +1384,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
recv_len -= 8;
/* 填充来源地址 */
if (src_addr != NULL &&addrlen != NULL &&
if (NULL != src_addr &&NULL != addrlen &&
*addrlen >= (int)sizeof(struct net_sockaddr_in)) {
struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)src_addr;
p_addr_in->sin_family = NET_AF_INET;
@@ -1436,7 +1436,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
* 函数说明:直接调用 net_send_locked 完成发送
*/
int net_send_sock(net_sock_t *p_sock, const void *buf, int len) {
if (p_sock == NULL) {
if (NULL == p_sock) {
s_net_errno = NET_ERR_BADF;
return -1;
}
@@ -1455,7 +1455,7 @@ int net_send_sock(net_sock_t *p_sock, const void *buf, int len) {
* 函数说明:直接操作 Socket 控制块,不经过消息队列。避免 netTask 向自身发送消息造成死锁
*/
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) {
if (p_sock == NULL) {
if (NULL == p_sock) {
s_net_errno = NET_ERR_BADF;
return -1;
}
@@ -1470,7 +1470,7 @@ int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) {
* 函数说明:直接操作 Socket 控制块,不经过消息队列。TCP Socket 先断开连接再释放控制块
*/
int net_close_sock(net_sock_t *p_sock) {
if (p_sock == NULL) {
if (NULL == p_sock) {
s_net_errno = NET_ERR_BADF;
return -1;
}
@@ -1530,7 +1530,7 @@ uint32_t net_inet_addr(const char *cp) {
uint32_t addr = 0;
int i;
if (cp == NULL) {
if (NULL == cp) {
return 0;
}
@@ -1561,7 +1561,7 @@ uint32_t net_inet_addr(const char *cp) {
char *net_inet_ntoa(uint32_t addr, char *buf) {
uint32_t naddr = NET_NTOHL(addr);
if (buf == NULL) {
if (NULL == buf) {
return NULL;
}
@@ -1787,7 +1787,7 @@ static void handle_timeout_event(net_sock_t *p_sock) {
* 2. 回调参数为 ch395_sock、event、event_arg
*/
static void fire_event(net_sock_t *p_sock, net_event_t event) {
if (p_sock->event_cb != NULL) {
if (NULL != p_sock->event_cb) {
p_sock->event_cb(p_sock->ch395_sock, event, p_sock->event_arg);
}
}
@@ -1805,7 +1805,7 @@ static void fire_event(net_sock_t *p_sock, net_event_t event) {
static void parse_ip_string(const char *str, uint8_t *ip_arr) {
int i;
if (str == NULL ||ip_arr == NULL) {
if (NULL == str ||NULL == ip_arr) {
return;
}
@@ -1864,7 +1864,7 @@ int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) {
return -1;
}
if (addr == NULL ||addrlen == NULL || *addrlen < (int)sizeof(struct net_sockaddr_in)) {
if (NULL == addr ||NULL == addrlen || *addrlen < (int)sizeof(struct net_sockaddr_in)) {
s_net_errno = NET_ERR_INVAL;
return -1;
}
+16 -8
View File
@@ -40,7 +40,8 @@ rs485_handle_t g_rs485;
void rs485_init(rs485_handle_t *p_handle,
UART_HandleTypeDef *p_huart,
GPIO_TypeDef *p_dir_port,
uint16_t dir_pin) {
uint16_t dir_pin)
{
p_handle->huart = p_huart;
p_handle->dir_port = p_dir_port;
p_handle->dir_pin = dir_pin;
@@ -70,7 +71,8 @@ void rs485_init(rs485_handle_t *p_handle,
HAL_StatusTypeDef rs485_transmit(rs485_handle_t *p_handle,
const uint8_t *p_data,
uint16_t len,
uint32_t timeout) {
uint32_t timeout)
{
HAL_StatusTypeDef status = HAL_OK;
if (timeout == 0U) {
@@ -104,7 +106,8 @@ HAL_StatusTypeDef rs485_transmit(rs485_handle_t *p_handle,
* HAL 自动调用 HAL_UARTEx_RxEventCallback 并返回已接收字节数。
* 用户须在回调中重新调用本函数重新开启接收。
*/
HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *p_handle) {
HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *p_handle)
{
p_handle->rx_size = 0;
return HAL_UARTEx_ReceiveToIdle_IT(p_handle->huart, p_handle->rx_buf, RS485_RX_BUF_SIZE);
}
@@ -117,7 +120,8 @@ HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *p_handle) {
* 限定条件:须在 HAL_UARTEx_RxEventCallback 中调用
* 函数说明:将 HAL 回调中接收到的数据大小回写到句柄的 rx_size 字段
*/
void rs485_rx_set_size(rs485_handle_t *p_handle, uint16_t size) {
void rs485_rx_set_size(rs485_handle_t *p_handle, uint16_t size)
{
p_handle->rx_size = size;
}
@@ -128,7 +132,8 @@ void rs485_rx_set_size(rs485_handle_t *p_handle, uint16_t size) {
* 限定条件:须在 HAL_UARTEx_RxEventCallback 触发后调用
* 函数说明:获取 rx_size 字段中缓存的接收字节数
*/
uint16_t rs485_rx_get_size(const rs485_handle_t *p_handle) {
uint16_t rs485_rx_get_size(const rs485_handle_t *p_handle)
{
return p_handle->rx_size;
}
@@ -138,7 +143,8 @@ uint16_t rs485_rx_get_size(const rs485_handle_t *p_handle) {
* 应用层接收钩子默认实现(弱符号,可被应用重定义)。
* 在中断上下文调用,仅做占位,业务解析由应用提供。
*/
__weak void rs485_on_rx(uint8_t *buf, uint16_t size) {
__weak void rs485_on_rx(uint8_t *buf, uint16_t size)
{
(void)buf;
(void)size;
}
@@ -148,7 +154,8 @@ __weak void rs485_on_rx(uint8_t *buf, uint16_t size) {
* 当 RS-485 总线空闲超过 1 个字符时间后触发,由 HAL 自动调用。
* 注意:回调按实例分发,仅处理本驱动绑定的 UART,不写死 UART5。
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == g_rs485.huart) {
rs485_rx_set_size(&g_rs485, Size);
@@ -164,7 +171,8 @@ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
* HAL UART 错误回调(ORE/NE/FE/PE 等)。
* 读 SR + DR 清除错误标志后重新开启接收,避免中断锁死。
*/
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) {
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart == g_rs485.huart) {
/* STM32F4 通过读 SR + DR 清除 ORE/NE/FE/PE 错误标志 */
(void)huart->Instance->SR;
+36 -18
View File
@@ -65,7 +65,8 @@ static void tpafe5160_read_channels(uint8_t count, int16_t *buf);
* 2. 确保 RD 为高、CONVST 为低
* 3. 等待 BUSY 释放(转换空闲)
*/
int tpafe5160_init(void) {
int tpafe5160_init(void)
{
/* 设置默认过采样:无过采样 (OS[2:0] = 000) */
tpafe5160_set_os(TPAFE5160_OS_NONE);
@@ -90,7 +91,8 @@ int tpafe5160_init(void) {
* 限定条件:GPIO 已初始化
* 函数说明:通过 OS[2:0] 引脚设置过采样率,在下一次 BUSY 下降沿锁存生效
*/
void tpafe5160_set_os(tpafe5160_os_t os) {
void tpafe5160_set_os(tpafe5160_os_t os)
{
TPAFE5160_OS0(os & 0x01); /* OS0 = bit0 */
TPAFE5160_OS1((os >> 1) & 0x01); /* OS1 = bit1 */
TPAFE5160_OS2((os >> 2) & 0x01); /* OS2 = bit2 */
@@ -103,7 +105,8 @@ void tpafe5160_set_os(tpafe5160_os_t os) {
* 限定条件:GPIO 已初始化
* 函数说明:产生 CONVST 脉冲上升沿,启动全部8通道同步采样与转换
*/
void tpafe5160_start_conv(void) {
void tpafe5160_start_conv(void)
{
TPAFE5160_CONVST_PULSE();
}
@@ -114,7 +117,8 @@ void tpafe5160_start_conv(void) {
* 限定条件:已调用 tpafe5160_start_conv()
* 函数说明:轮询 BUSY 引脚等待下降沿
*/
int tpafe5160_wait_busy(uint32_t timeout_ms) {
int tpafe5160_wait_busy(uint32_t timeout_ms)
{
uint32_t tick_start = HAL_GetTick();
/* 等待 BUSY 释放(低电平表示空闲) */
@@ -134,7 +138,8 @@ int tpafe5160_wait_busy(uint32_t timeout_ms) {
* 限定条件:GPIO 已初始化
* 函数说明:读取 BUSY 引脚电平
*/
uint8_t tpafe5160_is_busy(void) {
uint8_t tpafe5160_is_busy(void)
{
return (TPAFE5160_BUSY_READ() == GPIO_PIN_SET) ? 1 : 0;
}
@@ -147,7 +152,8 @@ uint8_t tpafe5160_is_busy(void) {
* 2. 连续8次拉低RD读取各通道数据
* 3. 通过 FRSTDATA 验证第一通道
*/
int tpafe5160_read_all(int16_t *buf) {
int tpafe5160_read_all(int16_t *buf)
{
int ret = 0;
/* 启动转换 */
@@ -174,7 +180,8 @@ int tpafe5160_read_all(int16_t *buf) {
* 限定条件:GPIO 已初始化
* 函数说明:启动转换并等待完成后,连续读取至指定通道
*/
int tpafe5160_read_channel(uint8_t channel, int16_t *value) {
int tpafe5160_read_channel(uint8_t channel, int16_t *value)
{
int ret = 0;
int16_t buf[TPAFE5160_CH_NUM] = {0};
@@ -200,7 +207,8 @@ int tpafe5160_read_channel(uint8_t channel, int16_t *value) {
* ±10V量程时 LSB = 20V / 65536 ≈ 305.18μV
* 硬件 RANGE 引脚接 GND,默认 ±5V 量程
*/
float tpafe5160_to_voltage(int16_t raw) {
float tpafe5160_to_voltage(int16_t raw)
{
/* RANGE 接 GND → ±5V 量程,满量程 10V */
return (float)raw * (10.0f / 65536.0f);
}
@@ -212,7 +220,8 @@ float tpafe5160_to_voltage(int16_t raw) {
* 限定条件:RD 为低或 CS 与 RD 已拉低
* 函数说明:读取 GPIOG->IDR 低16位,对应 DB[15:0]
*/
uint16_t tpafe5160_read_bus(void) {
uint16_t tpafe5160_read_bus(void)
{
return TPAFE5160_READ_BUS();
}
@@ -232,7 +241,8 @@ uint16_t tpafe5160_read_bus(void) {
* t14 数据建立 ≤ 21ns → 数据在 RD 下降沿后 21ns 内有效
* 5 个 NOP (~30ns) 覆盖建立时间
*/
static void tpafe5160_read_channels(uint8_t count, int16_t *buf) {
static void tpafe5160_read_channels(uint8_t count, int16_t *buf)
{
uint8_t i;
for (i = 0; i < count; i++) {
@@ -262,7 +272,8 @@ static volatile uint8_t s_buf_sel = 0; /* 0 = 写 B / 读 A, 1 = 写 A / 读 B
* 限定条件:CubeMX 已完成 GPIO 和 NVIC 配置
* 函数说明:正常启动流程无需调用,仅在 irq_disable() 后需要重新使能时使用
*/
void tpafe5160_irq_enable(void) {
void tpafe5160_irq_enable(void)
{
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
}
@@ -273,7 +284,8 @@ void tpafe5160_irq_enable(void) {
* 限定条件:已调用 tpafe5160_irq_enable()
* 函数说明:仅禁用 NVIC 中断,GPIO 配置保持 CubeMX 设定
*/
void tpafe5160_irq_disable(void) {
void tpafe5160_irq_disable(void)
{
HAL_NVIC_DisableIRQ(EXTI9_5_IRQn);
}
@@ -284,7 +296,8 @@ void tpafe5160_irq_disable(void) {
* 限定条件:已调用 tpafe5160_irq_enable()
* 函数说明:产生 CONVST 脉冲,转换完成后由 EXTI 中断自动读取 8 通道数据
*/
void tpafe5160_start_conv_irq(void) {
void tpafe5160_start_conv_irq(void)
{
s_ready = 0;
TPAFE5160_CONVST_PULSE();
}
@@ -296,7 +309,8 @@ void tpafe5160_start_conv_irq(void) {
* 限定条件:中断模式已启用
* 函数说明:在 EXTI 回调中置位,主循环读取后需调用 tpafe5160_clear_ready() 清除
*/
uint8_t tpafe5160_data_ready(void) {
uint8_t tpafe5160_data_ready(void)
{
return s_ready;
}
@@ -307,7 +321,8 @@ uint8_t tpafe5160_data_ready(void) {
* 限定条件:中断模式已启用
* 函数说明:主循环处理完数据后调用
*/
void tpafe5160_clear_ready(void) {
void tpafe5160_clear_ready(void)
{
s_ready = 0;
}
@@ -318,7 +333,8 @@ void tpafe5160_clear_ready(void) {
* 限定条件:tpafe5160_data_ready() 返回 1 时调用
* 函数说明:双缓冲切换,ISR 写另一个缓冲区,主循环安全读取当前缓冲区
*/
const int16_t* tpafe5160_get_buf(void) {
const int16_t* tpafe5160_get_buf(void)
{
return (s_buf_sel == 0) ? s_buf_a : s_buf_b;
}
@@ -329,11 +345,13 @@ const int16_t* tpafe5160_get_buf(void) {
* 限定条件:BUSY EXTI 已使能
* 函数说明:转换完成后自动读取 8 通道数据到缓冲区,耗时约 0.5µs (168MHz)
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
tpafe5160_exti_handler(GPIO_Pin);
}
void tpafe5160_exti_handler(uint16_t GPIO_Pin) {
void tpafe5160_exti_handler(uint16_t GPIO_Pin)
{
if (GPIO_Pin != TP_BUSY_Pin) {
return;
}
+20 -73
View File
@@ -21,26 +21,22 @@
## 项目架构
```mermaid
flowchart TB
subgraph APP["应用层 (App/)"]
direction LR
A1["app_main.c"] ~~~ A2["net_task.c"] ~~~ A3["ui_task.c"] ~~~ A4["time_sync.c"]
end
subgraph BSP["驱动层 (Drivers/BSP/)"]
direction LR
B1["CH395F"] ~~~ B2["GD5F2GQ5UE"] ~~~ B3["TPAFE5160"] ~~~ B4["SD2506"] ~~~ B5["RS485"] ~~~ B6["UI"]
end
subgraph MW["中间件 (Middlewares/ & Lib/)"]
direction LR
C1["FreeRTOS"] ~~~ C2["FatFs"] ~~~ C3["dhara FTL"] ~~~ C4["LVGL"]
end
subgraph HAL["HAL 层 / CMSIS"]
direction LR
D1["STM32F4xx_HAL_Driver/"] ~~~ D2["CMSIS/"]
end
HW["硬件 (STM32F407ZGTx)"]
APP --> BSP --> MW --> HAL --> HW
```
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (App/) │
│ app_main.c │ net_task.c │ ui_task.c │ time_sync.c │
├─────────────────────────────────────────────────────────────┤
│ 驱动层 (Drivers/BSP/) │
│ CH395F/ │ GD5F2GQ5UE/ │ TPAFE5160/ │ SD2506/ │ RS485/ │ UI/ │
├─────────────────────────────────────────────────────────────┤
│ 中间件 (Middlewares/ & Lib/) │
│ FreeRTOS │ FatFs │ dhara FTL │ LVGL │
├─────────────────────────────────────────────────────────────┤
│ HAL 层 (Drivers/STM32F4xx_HAL_Driver/) │
│ CMSIS (Drivers/CMSIS/) │
├─────────────────────────────────────────────────────────────┤
│ 硬件 (STM32F407ZGTx) │
└─────────────────────────────────────────────────────────────┘
```
### 启动流程
@@ -50,13 +46,11 @@ main.c
├─ HAL_Init()
├─ SystemClock_Config() // 168MHz 系统时钟
├─ MX_GPIO/DMA/UART/SPI/I2C_Init() // CubeMX 外设初始化
├─ app_main_init() // 应用层初始化(时间→RS485→NAND→ADC→诊断)
├─ app_main_init() // 应用层初始化(时间→RS485→NAND→ADC→RTC)
│ ├─ sd2506_init() + sys_clock_init() // 时间子系统优先
│ ├─ rs485_init()
│ ├─ gd5f2gq5ue_init()
│ ├─ tpafe5160_init()
│ ├─ SD2506 诊断(ID/时间/温度/电池)
│ └─ tpafe5160_start_conv_irq() // 首次 ADC 转换
│ └─ tpafe5160_init()
├─ osKernelInitialize()
├─ MX_FREERTOS_Init() // 创建任务和事件组
└─ osKernelStart() // 启动调度器
@@ -120,8 +114,7 @@ STM32F4-Base/
│ │ │ ├── net_socket.c/h # Socket API
│ │ │ ├── net_select.c/h # select/poll 多路复用
│ │ │ └── lftpd/ # 轻量 FTP 服务器
│ │ ├── UI/ # LVGL UI 组件
│ │ └── dbg_log.h / dbg_cfg.h # 调试日志(DBG_INFO 等)
│ │ └── UI/ # LVGL UI 组件
│ ├── STM32F4xx_HAL_Driver/ # ST HAL 库
│ └── CMSIS/ # ARM CMSIS
├── Lib/ # 第三方库
@@ -139,42 +132,20 @@ STM32F4-Base/
│ ├── run_boot_test.ps1 # 自动化启动测试
│ ├── verify_log.ps1 # 日志校验脚本
│ ├── serial_capture.ps1 # 串口捕获脚本
│ ├── test_config.c/h # 测试任务配置
│ ├── ch395f_test_task.c/h # CH395F 硬件测试
│ ├── gd5f_test_task.c/h # NAND Flash 测试
│ ├── net_test_task.c/h # 网络层测试
│ ├── sd2506_test_task.c/h # RTC 测试
│ ├── storage_test_task.c/h # 存储性能测试
│ ├── ch395f_socket_test.py # Socket 通信测试(主机端)
│ ├── ftp_test.py # FTP 服务器测试(主机端)
│ ├── ftp_up_dl_test.py # FTP 上传下载测试(主机端)
│ ├── ftp_speed_test.py # FTP 传输速度测试(主机端)
│ ├── ftp_capture.py # FTP 流量抓取(主机端)
│ └── time_sync_server.py # 时间同步推送服务(主机端)
│ └── storage_test_task.c/h # 存储性能测试
├── docs/ # 项目文档
│ ├── 代码规范/ # C 代码规范、pre-commit / cppcheck 使用说明
│ ├── 问题记录/ # 已知陷阱与解决方案
│ ├── 测试说明/ # 测试指南
│ ├── 芯片手册/ # 硬件数据手册
│ ├── 测试开发标准流程.md # 测试开发标准流程
│ ├── 测试文档模板规范.md # 测试文档模板规范
│ └── *.md # 存储架构 / UI 规划 / 墙钟时间 / 启动顺序 / LVGL 模拟器
├── scripts/ # pre-commit 本地钩子脚本
│ ├── check_coding_standard.py # 代码规范机械检查
│ └── check_mojibake.py # U+FFFD 乱码检查
├── simulator/ # LVGL PC 模拟器(CMake + SDL2)
│ ├── CMakeLists.txt # 模拟器构建脚本
│ ├── main.c / sim_port.c/h # 模拟器入口与移植层
│ ├── lv_conf.h # LVGL 配置(1bpp + SDL)
│ └── SDL2/ # SDL2 2.32.10 预编译库
├── tools/ # 开发工具
│ └── font_gen/ # 中文字体生成工具
├── ui_cn_chars.txt # 中文字体字符集
├── STM32F407-Demo.ioc # CubeMX 工程文件
├── .pre-commit-config.yaml # pre-commit 钩子配置
├── .gitlint # commit message 检查规则
├── opencode.json # opencode 配置
├── .opencode/ # opencode 本地配置(串口等)
└── AGENTS.md # AI 开发规范
```
@@ -206,17 +177,6 @@ MDK-ARM\build.bat
MDK-ARM\download.bat
```
### LVGL PC 模拟器
PC 端 UI 开发调试(无需烧录 MCU),详见 `docs/LVGL模拟器部署与使用说明.md`:
```powershell
cd simulator
cmake -B build -G "Visual Studio 16 2019" -A x64
cmake --build build --config Debug
# 运行 simulator\build\Debug\lvgl_sim.exe
```
## 驱动开发指南
### CH395F 以太网
@@ -266,17 +226,6 @@ powershell -ExecutionPolicy Bypass -File test\run_boot_test.ps1
| RTC | `sd2506_test_task.c` | 时间读写、温度、电池 |
| 存储性能 | `storage_test_task.c` | FTL/FatFS 读写速度 |
### 主机端测试脚本(Python)
| 脚本 | 说明 |
|------|------|
| `ch395f_socket_test.py` | Socket 通信测试 |
| `ftp_test.py` | FTP 服务器基础测试 |
| `ftp_up_dl_test.py` | FTP 上传/下载测试 |
| `ftp_speed_test.py` | FTP 传输速度测试 |
| `ftp_capture.py` | FTP 流量抓取 |
| `time_sync_server.py` | 时间同步推送服务(PC → 板子) |
### 串口调试
通过 `serial-mcp-server` 自主读取 USART1 串口打印:
@@ -318,8 +267,6 @@ powershell -ExecutionPolicy Bypass -File test\run_boot_test.ps1
- U+FFFD 乱码(拦截,不可逆损坏)
- commit message 格式(gitlint,拦截)
本地钩子脚本:`scripts/check_coding_standard.py`、`scripts/check_mojibake.py`(由 `.pre-commit-config.yaml` 引用)。
```bash
# 新机器启用钩子
pre-commit install
+1 -1
View File
@@ -176,7 +176,7 @@ void MX_FREERTOS_Init(void) {
#endif
/* 本地时间同步任务(PC 推送 TCP):依赖 netTask 处理消息队列 */
timeSyncTaskHandle = osThreadNew(time_sync_task, NULL, &timeSyncTask_attributes);
timeSyncTaskHandle = osThreadNew(timeSyncTask, NULL, &timeSyncTask_attributes);
/* USER CODE END RTOS_THREADS */
/* Create the event(s) */
@@ -43,7 +43,7 @@ python tools/font_gen/extract_cn.py
```
scanned 9 files; unique non-ASCII chars: 83
-> D:\Code\DTU Code\STM32F4-Base\ui_cn_chars.txt
-> D:\Code\DTU 程序\STM32F4-Base\ui_cn_chars.txt
```
产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 83 字),可留作字符集记录。
+199
View File
@@ -0,0 +1,199 @@
# UI 图形界面实施规划(160×160 单色 · 树形菜单 · 离屏渲染 + 网络推流)
> 本文档用于在我(实现方)与你(需求方)之间对齐"如何规划与实施 UI"。
> 当前阶段:**需求澄清 + 方案设计**,尚未写任何 UI 源码。
> ✅ = 已根据你的最近答复冻结;【待确认】= 仍需你拍板;**(建议)** = 我的默认推荐。
---
## 1. 背景与现状
| 项 | 现状 |
|---|---|
| MCU | STM32F407ZGTx,168MHz,192KB RAM,1MB Flash |
| 工具链 | 仅 Keil MDK-ARM v5 (ARMCC) |
| OS | FreeRTOS (CMSIS-RTOS V2),已有 `defaultTask/netTask/ftpTask` |
| 网络 | CH395F 以太网(SPI2),单连接 socket 模型(Socket0 复用监听/数据) |
| 显示 | **无真实屏**:UI 在内存中离屏渲染,按请求经网络返回帧 |
| 目标屏规格 | ✅ **160×160 像素,单色(1bpp,黑白)**,"少占用 RAM" |
| 参考项目 | `D:\Code\DTU 程序\DTU-HMI`(自建 MVP 菜单,过于复杂,改用成熟方案复现菜单) |
| 参考协议 | ✅ **RemoDispBus**(`DTU-HMI/src/remoteDisplay.c`,帧格式见第 3.4 节) |
| 源码规范 | 中文注释;新增 .c/.h 需手动加入 Keil 工程;禁止整文件文本重写 |
### 关键推论
- 我们目标 **1bpp**:整帧 = 160×160÷8 = **3200 字节(3.1KB)**,直接整帧作绘制缓冲。git
- 参考项目 `key.h` 已有 `g_tRemoteKey`(`bUseRkey` 远程按键)→ ✅ 菜单导航由远端发键值驱动(双向交互已定)。
- 参考项目 `def.h`:✅ 中文 12px、行高 14px、可视约 11 行。
---
## 2. 复现目标(已明确)
1. MCU 内存中渲染 160×160、1bpp 画面。
2. ✅ **菜单驱动、多页面可切换**:复现 DTU-HMI 树形菜单(第 3.3 节),菜单进入不同页。
3. ✅ **不含折线/柱状图**:内容页以文字 + 数值列表 + 简单指示为主;YC(遥测)页数值列表,无曲线。
4. ✅ **A 复现范围**:仅复现**菜单树结构与导航**;所有内容页先**占位**(含装置信息/遥测也先占位)。
5. ✅ **双向交互(pull 模型)**:远端按 RemoDispBus 发 `CMD_LCDMEM` 拉帧、`CMD_KEY` 下发键;MCU 渲染后应答帧。
6. 全程 RAM 占用尽量低,不挤占现有 netTask/ftp/FatFs/dhara。
---
## 3. 参考项目与协议
### 3.1 输入键码(来自 `DTU-HMI/src/Drv/key.h`)
| 键 | 键值 | LVGL 映射 |
|---|---|---|
| 上 KEY_U | 0x02 | LV_KEY_UP |
| 下 KEY_D | 0x40 | LV_KEY_DOWN |
| 左 KEY_L | 0x10 | LV_KEY_LEFT |
| 右 KEY_R | 0x08 | LV_KEY_RIGHT |
| 确认 KEY_ENT | 0x20 | LV_KEY_ENTER |
| 返回 KEY_ESC | 0x01 | LV_KEY_ESC |
### 3.2 视觉规格(来自 `def.h`)
- ✅ 160×160,1bpp;中文 12px,行高 14px,可视约 11 行。
- ✅ 菜单视觉:采用 LVGL `lv_menu` 整屏列表 + 返回栈(小屏最清晰),不复原级联框。
### 3.3 菜单树(取自 `model.c` 的 `menuTab[]`,仅复现结构与导航)
```
[0] 主界面 (root)
├[1] 装置信息 → 占位页
├[1] 实时数据
│ ├[2] 交流量 / [2] 直流量 / [2] 遥信量 → 占位页(YC类)
├[1] 参数定值
│ ├[2] 整定 → [3]参数/定值/控制字/软压板 → 占位
│ └[2] 查看 → [3]参数/定值/控制字/软压板 → 占位
├[1] 三遥设置 → [2]遥测死区/系数/遥信类型/防抖/双点遥信 → 占位
├[1] 装置维护 → [2]时钟设置/强制复归/手动录波/清除记录 → 占位
├[1] 通讯参数 → [2]通讯设置/网口设置/SNTP设置 → 占位
└[1] 记录查询 → [2]SOE/事故/操作/保护告警/保护启动/遥控/自检/运行记录/运行报告 → 占位
[0] 厂家设置 (root, 密码 CN_COP_PWD)
├[1] 元件配置 / [1]恢复默认([2]全部/默认参数/定值/软压板/元件配置) / [1]交流显示
├[1] 装置调试 → [2]虚拟遥信/交流虚遥测/直流虚遥测/电度虚遥测/动作虚事件/告警虚事件/动作出口/信号出口 → 占位
└[1] 版本信息 → 占位
```
> 叶子(含"装置信息""实时数据"等)统一进入**占位页**(显示菜单路径 + "功能未实现"),仅验证导航与返回栈。
### 3.4 RemoDispBus 协议(✅ E 与参考项目一致,来自 `remoteDisplay.c`)
**帧格式(请求/应答通用)**:`[TAG][cmd][len_hi][len_lo][data...][crc]`
- `TAG`:客户端→设备 `0xAA`;设备→客户端 `0xBB`
- `cmd`:`0` KEEPLIVE,`1` INIT,`2` KEY,`3` LCDMEM
- `len`:data 区长度,**大端 16 位**
- `crc`:data 区逐字节**异或**取低 8 位
- 整帧长度 = `4 + data_len + 1`
**命令语义**
| cmd | 方向 | data | 说明 |
|---|---|---|---|
| INIT(1) | 设备应答 | `[W_hi,W_lo,H_hi,H_lo, memsize(4B)]` = 8B | 回报分辨率与显存大小 |
| LCDMEM(3) | 客户端请求 | `4B 大端起始偏移` | 请求从 offset 开始的显存 |
| LCDMEM(3) | 设备应答 | `[4B 偏移][显存数据]` | 回当前帧缓冲内容 |
| KEY(2) | 客户端请求 | `[键值]`(见 3.1) | 设备写入按键队列驱动导航 |
| KEEPLIVE(0) | 双向 | 空 | 保活 ACK |
**我们的落地适配**
- ✅ **传输层完全照搬** RemoDispBus(TAG/cmd/len/crc、四命令)。
- ✅ **1bpp 内容**:帧缓冲 3200 字节。INIT 回报 `memsize=3200`;远端客户端据 `bpp = memsize*8/(W*H)` 自行推导为 1bpp(=1),无需改协议即可兼容。
- ✅ **pull 模型**:设备持续用 `lv_timer_handler()` 刷新帧缓冲;收到 `CMD_LCDMEM` 即回当前 3200 字节(通常 offset=0 全帧)。
- ⚠️ **位序约定**:1bpp 行优先、每字节 **MSB-first**(pixel(x,y) 存于 `byte(y*W/8 + x/8)` 的 bit `7-(x&7)`)。LVGL `LV_COLOR_FORMAT_L1` 的位序若与此不同,在 `ui_net_sink` 发送前做位重排(实现期用一块临时缓冲转换,或在 `lv_conf` 选匹配格式)。
- ⚠️ **与旧客户端兼容说明**:参考项目旧 `remo_disp_server.py` 期望 8bpp(25600B)。若你要求**字节级完全兼容旧客户端**(不改客户端),则需改为 8bpp(25600B)——但会多占 ~22KB RAM,与"少占用 RAM"冲突。默认按 1bpp(3200B) 实施;如需兼容旧客户端请告知(见第 8 节备注)。
---
## 4. 总体架构方案(建议)
```
┌──────────────────────────────────────────────────────────┐
│ 应用数据层 (现有: ADC/RTC/网络/NAND/LED,本阶段仅占位不接) │
└───────────────┬──────────────────────────────────────────┘
│ (本阶段不接数据;仅菜单导航)
┌───────────────▼──────────────────────────────────────────┐
│ UI 引擎 (LVGL v9, LV_COLOR_DEPTH=1, 关动画) │
│ - lv_menu 主菜单(复现 3.3 树) + 占位内容页 │
│ - lv_indev(KEYPAD) 读远端键队列 → 导航/切换 │
│ - lv_timer_handler() 周期调用 → 渲染到 3200B 帧缓冲 │
└───────────────┬──────────────────────────────────────────┘
│ flush_cb → 写入 fbuf[3200] (CCM RAM)
┌───────────────▼──────────────────────────────────────────┐
│ 推流/应答适配层 (Drivers/BSP/UI/ui_net_sink.c) │
│ - 解析 RemoDispBus 请求(INIT/LCDMEM/KEY/KEEPLIVE) │
│ - LCDMEM → 回当前 fbuf;KEY → 入键队列;INIT → 报 3200 │
└───────────────┬──────────────────────────────────────────┘
│ net_socket_send() / recv()
┌───────────────▼──────────────────────────────────────────┐
│ 网络层 (CH395F,✅ 新开专用 TCP 监听 socket,端口 7070) │
└──────────────────────────────────────────────────────────┘
```
### 模块落点(贴合现有 BSP 风格)
| 新增文件 | 职责 |
|---|---|
| `Lib/lvgl/` | LVGL v9 源码(放入后**手动加进 Keil 工程**) |
| `Drivers/BSP/UI/lv_conf.h` | 1bpp、关动画/日志、字号、键盘 indev 使能 |
| `Drivers/BSP/UI/ui_port.c/.h` | `lv_display` + `flush_cb`(写 fbuf);`lv_indev` 读键队列 |
| `Drivers/BSP/UI/ui_menu.c/.h` | 复现 3.3 菜单树(lv_menu 构建) |
| `Drivers/BSP/UI/ui_page_placeholder.c` | 占位内容页(显示路径+"未实现") |
| `Drivers/BSP/UI/ui_net_sink.c/.h` | RemoDispBus 解析/应答(INIT/LCDMEM/KEY/KEEPLIVE) |
| `App/task/ui_task.c/.h` | 独立 FreeRTOS 任务:跑 `lv_timer_handler` + 网络收发 |
> **不要**把 UI 渲染塞进 `netTask` 的 10ms 轮询——渲染 CPU 密集。新建 `ui_task`(栈 1024–1536,1bpp 下很轻),优先级低于 `netTask`。网络收发走现有 `net_socket` 串行消息队列。
---
## 5. 需讨论条件 — 当前状态
| 项 | 状态 | 结论 |
|---|---|---|
| A 复现范围 | ✅ 已定 | 仅菜单树+导航;所有内容页占位 |
| B 菜单视觉 | ✅ 已定 | lv_menu 整屏列表+返回栈 |
| C socket 策略 | ✅ 已定 | 新开专用 TCP(端口 7070) |
| D 刷新/动画 | ✅ 已定 | pull 模型(按 LCDMEM 应答);关过渡动画 |
| E 帧编码 | ✅ 已定 | RemoDispBus(见 3.4);1bpp 内容,memsize=3200 |
| F 单色定义 | ✅ 默认 | 纯 1bpp,无灰阶抖动 |
| G 字体/中文 | ✅ 默认 | 中文 12px;仅收录菜单/占位用到的字(fontconverter 造 1bpp 字库) |
| H 旋转/扫描 | ✅ 已定 | 默认不旋转,行优先 |
| I RAM/Flash | ✅ 按建议 | 帧缓冲+draw buffer 放 **CCM RAM**;集成时实测空闲余量 |
| J 库选型 | ✅ 默认 | LVGL v9(LV_COLOR_DEPTH=1) |
| K 数据通道 | ⏸ 本期不涉及 | 占位阶段业务数据不接 UI;后续事件/共享结构体 |
| L 持久化 | ✅ 默认 | 不需;每次上电回根菜单 |
| M 异常 | ✅ 建议 | 客户端掉线暂停应答,不影响主业务;ui_task 软看门狗;键队列溢出丢 oldest |
---
## 6. 实施阶段分解(建议顺序)
1. **阶段 0 · 决策冻结**:本文件第 5 节已冻结(仅 K/L/M 默认)。
2. **阶段 1 · 骨架接入**:导入 LVGL v9 到 Keil;`lv_conf.h`(1bpp/关动画/字号);`ui_task` 渲染测试图;帧缓冲在 CCM。
3. **阶段 2 · RemoDispBus 应答**:`ui_net_sink` 实现 INIT/LCDMEM/KEY/KEEPLIVE;新开 CH395F TCP(7070);远端按协议拉到 3200B 帧。
4. **阶段 3 · 菜单复现**:`lv_menu` 复现 3.3 树(中文 12px 字库);远端 `CMD_KEY` 驱动上下/确认/返回导航。
5. **阶段 4 · 占位页**:叶子进占位页(显示路径+"未实现"),验证返回栈与多级导航。
6. **阶段 5 · 健壮性**:掉线恢复、软看门狗、RAM 余量复测、`@build` 0 警告验证。
7. (后续)阶段 6 · 真实数据联动:按需把 ADC/RTC/网络/NAND/LED 接入 AppInfo/YC 等页。
---
## 7. 风险与约束
- **RAM 最紧**:3200B 帧缓冲 + LVGL 运行时(约 4–8KB)+ draw buffer(整帧即可,同 3200B 或更小)放 CCM;集成时实测 `netTask/ftp/FatFs/dhara` 后空闲堆/栈。
- **ARMCC 兼容**:LVGL 纯 C99 可编译;`lv_conf.h` 关浮点/动画等单色屏用不到特性。
- **不阻塞 netTask**:UI 渲染/收发在独立任务,走 `net_socket` 串行队列。
- **位序对齐**:发送前确保 1bpp 位序(MSB-first 行优先)与客户端一致(见 3.4 ⚠️)。
- **编码安全**:含中文,仅用 Edit/Write 工具改,绝不用 PowerShell 文本重写。
---
## 8. 决策待办清单
- [x] A 复现范围(仅菜单树+导航,其余占位)
- [x] B 菜单视觉(lv_menu 整屏列表)
- [x] C UI 通道(新开专用 TCP 7070)
- [x] D 刷新/动画(pull 应答,关动画)
- [x] E 帧编码(RemoDispBus,1bpp memsize=3200)
- [x] F 单色(纯 1bpp,无抖动)
- [x] G 字号(中文 12px)
- [x] H 朝向(不旋转)
- [x] I RAM(CCM 帧缓冲,集成实测)
- [x] J 库(LVGL v9 1bpp)
- [x] L 持久化(不需)
- [x] M 异常(掉线暂停+软看门狗)
+3 -12
View File
@@ -131,21 +131,12 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
| S3 | 3.4.1 | `#define` 宏名必须全大写+下划线+数字 |
| S4 | 3.5.1 | typedef 类型名必须以 `_t` 结尾 |
| S5 | 6.3.2 | switch 必须包含 default 分支 |
| S6 | 7.5 | 禁用 strcpy/strcat/sprintf/gets/scanf/strtok(改用安全版本) |
| S6 | 7.5 | 禁用 strcpy/strcat/sprintf/gets(改用 strncpy/strncat/snprintf) |
| S7 | 6.3.3 | 禁止 goto |
| S8 | 4.1.1 | 禁止 Tab 缩进(4 空格) |
| S9 | 5.3 | 函数定义必须有函数注释(含「函数功能」) |
| S10 | 3.3.1 | 函数名必须全小写(HAL_ 厂商 API 回调名豁免) |
| S11 | 2.3.5 | 禁止相对/绝对路径 include |
| S12 | 7.6 | 禁止可变参数函数(`...`) |
| S13 | 7.7 | 禁止递归函数 |
| S15 | 6.1.4 | 文件作用域指针变量必须初始化为 NULL |
| S16 | 3.2.2 | 文件作用域非 static 全局变量必须 `g_` 前缀 |
| S17 | 2.2 | 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过) |
| S18 | 6.3.1 | 禁止 Yoda 条件(字面量/常量必须在运算符右侧) |
| S19 | 4.4.2 | 函数定义左大括号必须与右括号同行 |
**不在检查范围**(语义类规则,无法机械可靠判定):函数长度/单一职责、参数数量、错误处理、注释掉的代码等,仍需人工评审把关。
**不在检查范围**(语义类规则,无法机械可靠判定):函数长度/单一职责、参数数量、错误处理、递归、注释掉的代码等,仍需人工评审把关。
**排除文件**(与 cppcheck 钩子范围一致,另加):`Src/`、`Inc/`(CubeMX 生成,重新生成即覆盖)、`Drivers/BSP/GD5F2GQ5UE/nand_ftl.*`(dhara/FatFS 接口适配层,函数名由外部 API 固定)。
@@ -276,7 +267,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
| `scripts/check_coding_standard.py` | scripts/ | 代码规范检查脚本(规范 V1.0 可机械校验项) |
| `docs/代码规范/嵌入式C语言代码规范(V1.0).md` | docs/代码规范/ | 代码规范原文(S1~S19 规则来源) |
| `docs/代码规范/嵌入式C语言代码规范(V1.0).md` | docs/代码规范/ | 代码规范原文(S1~S9 规则来源) |
| `docs/代码规范/cppcheck使用说明.md` | docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) |
| `docs/代码规范/pre-commit使用说明.md` | 本文档 | 部署与使用说明 |
@@ -53,11 +53,11 @@ serial-mcp-server read --port COM6 --baud 115200 \
**顺序必须:先启动采集(打开 COM 等 first-byte)→ 再烧录触发复位 → 采集自动抓取。**
```powershell
# 一键:采集(含 EOF 重试)→ 烧录 → 提取日志 → verify 校验(在仓库根目录执行,路径用仓库相对路径)
powershell -ExecutionPolicy Bypass -File "test\run_boot_test.ps1" `
# 一键:采集(含 EOF 重试)→ 烧录 → 提取日志 → verify 校验
powershell -ExecutionPolicy Bypass -File "D:\Code\DTU 程序\STM32F4-Base\test\run_boot_test.ps1" `
-Port COM6 -BaudRate 115200 -DurationMs 30000 -InitialTimeoutMs 90000 `
-IdleTimeoutMs 8000 -MaxBytes 65536 `
-FlashCmd "MDK-ARM\download.bat" `
-FlashCmd "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM\download.bat" `
-Required "App init OK;Network Init;net init: OK;PHY: 100M FULL" `
-Forbidden "FAIL;ERROR;HardFault"
```
+13 -21
View File
@@ -95,29 +95,21 @@ static volatile uint32_t s_base_unix_secs = UINT32_MAX; /* 基准对应的 Unix
## 6. 初始化流程(`sys_clock_init`)
```mermaid
sequenceDiagram
autonumber
participant SC as sys_clock_init
participant RTC as SD2506 RTC
participant HAL as HAL Tick
```
Step 1 从 SD2506 读取初始时间 sd2506_get_time(&sd_time)
├─ 注意:sd_time.year 已是完整年份(驱动内已 +2000),
│ 后续换算【不得】再加 2000,否则年份虚增 → Unix 秒数超 uint32 回绕
└─ 可选校时(见第 9 节 RTC_DEFAULT_INIT_* 宏)
Note over SC,RTC: Step 1 从 SD2506 读取初始时间
SC->>RTC: sd2506_get_time(sd_time)
RTC-->>SC: sd_time(year 已是完整年份)
Note right of SC: 后续换算【不得】再加 2000<br>否则年份虚增 → Unix 秒数超 uint32 回绕
Note right of SC: 可选校时(见第 9 节 RTC_DEFAULT_INIT_* 宏)
Step 2 将 RTC 时间换算为 Unix epoch 秒数
├─ 累计 1970 至当前年份的整年天数(含闰年 366 天判定)
├─ 加上当年 1 月至当前月的累计天数
├─ 加当月已过天数、时/分/秒
└─ 用 int64_t 累加,避免中间溢出;结果截断存入 uint32_t(受 2038 限制,见第 12 节)
Note over SC: Step 2 将 RTC 时间换算为 Unix epoch 秒数
SC->>SC: 累计 1970 至当前年份的整年天数(含闰年 366 天判定)
SC->>SC: 加上当年 1 月至当前月的累计天数
SC->>SC: 加当月已过天数、时/分/秒
SC->>SC: 用 int64_t 累加,避免中间溢出;结果截断存入 uint32_t(受 2038 限制,见第 12 节)
Note over SC,HAL: Step 3 建立基准
SC->>HAL: HAL_GetTick()
HAL-->>SC: 当前 tick
Note right of SC: s_base_tick = HAL_GetTick()<br>s_base_unix_secs = (uint32_t)unix_secs
Step 3 建立基准
s_base_tick = HAL_GetTick();
s_base_unix_secs = (uint32_t)unix_secs;
```
初始化后,所有后续取时都基于 Step 3 的基准推算,不再访问 RTC。
+5 -173
View File
@@ -10,23 +10,13 @@ C 语言代码规范检查(《嵌入式C语言代码规范(V1.0)》可机
S3 (3.4.1) #define 宏名必须全大写 + 下划线 + 数字
S4 (3.5.1) typedef 类型名必须以 _t 结尾
S5 (6.3.2) switch 必须包含 default 分支
S6 (7.5) 禁用不安全字符串/输入函数 strcpy/strcat/sprintf/gets/scanf/strtok
S6 (7.5) 禁用不安全字符串函数 strcpy/strcat/sprintf/gets
S7 (6.3.3) 禁止使用 goto
S8 (4.1.1) 禁止 Tab 缩进(使用 4 个空格)
S9 (5.3) 函数定义必须有函数注释(含「函数功能」)
S10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
S11 (2.3.5) 禁止相对/绝对路径 include
S12 (7.6) 禁止可变参数函数(...)
S13 (7.7) 禁止递归函数
S15 (6.1.4) 文件作用域指针变量必须初始化为 NULL
S16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀
S17 (2.2) 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过)
S18 (6.3.1) 禁止 Yoda 条件(字面量/常量必须在运算符右侧)
S19 (4.4.2) 函数定义左大括号必须与右括号同行
语义类规则(函数长度、单一职责、错误处理等)无法机械可靠判定,不在此脚本范围。
语义类规则(函数长度、单一职责、错误处理、递归等)无法机械可靠判定,不在此脚本范围。
"""
import os
import re
import sys
@@ -36,77 +26,6 @@ FUNC_DEF_RE = re.compile(
)
CTRL_RE = re.compile(r'^\s*(if|for|while|switch|return|do|else)\b')
C_KEYWORDS = {'if', 'for', 'while', 'switch', 'do', 'else', 'return',
'sizeof', 'struct', 'union', 'enum'}
NAME_PAREN_RE = re.compile(r'([A-Za-z_][A-Za-z0-9_]*)\s*\(')
def _mask(text):
"""注释与字符串字面量替换为等长空白(保留换行,行号不变),防止误报。
单次组合正则扫描,避免字符串内的 // 或注释内的引号造成顺序误掩。
"""
def _repl(m):
return ''.join(c if c == '\n' else ' ' for c in m.group(0))
pat = re.compile(r'"(\\.|[^"\\\n])*"|/\*.*?\*/|//[^\n]*', re.S)
return pat.sub(_repl, text)
def _line(text, pos):
return text[:pos].count('\n') + 1
def _fn_defs(masked):
"""线性扫描找函数定义 name(...) {,返回 [(name, name行, body, pre, 大括号行, 右括号行)]。
body 为定义大括号内文本,pre 为名称前 200 字符(用于判定 static)。
"""
out = []
n = len(masked)
for m in NAME_PAREN_RE.finditer(masked):
name = m.group(1)
if name in C_KEYWORDS or name == 'defined':
continue
ls = masked.rfind('\n', 0, m.start()) + 1
if masked[ls:].lstrip()[:1] == '#':
continue
i = m.end() - 1 # '(' 下标
depth, j = 0, i
ok = False
while j < n:
c = masked[j]
if c == '(':
depth += 1
elif c == ')':
depth -= 1
if depth == 0:
ok = True
break
elif c in '{};':
break
j += 1
if not ok:
continue
k = j + 1
while k < n and masked[k] in ' \t\r\n':
k += 1
if k >= n or masked[k] != '{':
continue
depth, b = 0, k
while b < n:
if masked[b] == '{':
depth += 1
elif masked[b] == '}':
depth -= 1
if depth == 0:
break
b += 1
out.append((name, _line(masked, m.start()), masked[k + 1:b],
masked[max(0, m.start() - 200):m.start()],
_line(masked, k), _line(masked, j)))
return out
def check_file(path):
with open(path, 'r', encoding='utf-8', errors='replace') as f:
@@ -161,10 +80,10 @@ def check_file(path):
ln = text[:m.start()].count('\n') + 1
v.append((ln, 'S5 6.3.2', 'switch 缺少 default 分支'))
# S6 (7.5) 不安全字符串/输入函数
for m in re.finditer(r'\b(strcpy|strcat|sprintf|gets|scanf|strtok)\s*\(', text):
# S6 (7.5) 不安全字符串函数
for m in re.finditer(r'\b(strcpy|strcat|sprintf|gets)\s*\(', text):
ln = text[:m.start()].count('\n') + 1
v.append((ln, 'S6 7.5', '不安全字符串/输入函数: %s (改用安全版本)'
v.append((ln, 'S6 7.5', '不安全字符串函数: %s (改用 strncpy/strncat/snprintf)'
% m.group(1)))
# S7 (6.3.3) 禁止 goto
@@ -188,93 +107,6 @@ def check_file(path):
v.append((idx, 'S9 5.3', '函数定义缺少函数注释(函数功能): %s'
% line.strip()[:50]))
masked = _mask(text)
mlines = masked.splitlines()
defs = _fn_defs(masked)
# S10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
for name, ln, _body, _pre, _bl, _pl in defs:
if re.search(r'[A-Z]', name) and not name.startswith('HAL_'):
v.append((ln, 'S10 3.3.1', '函数名须全小写: %s' % name))
# S11 (2.3.5) 禁止相对/绝对路径 include(基于原始文本,字符串掩码会吃掉引号)
for m in re.finditer(r'^\s*#\s*include\s*[<"]([^">]+)[">]', text, re.M):
inc = m.group(1)
if '/' in inc or '\\' in inc or '..' in inc:
v.append((_line(text, m.start()), 'S11 2.3.5',
'禁止相对/绝对路径 include: %s' % inc))
# S12 (7.6) 禁止可变参数函数
for m in re.finditer(r'\(\s*[^();{}]*\.\.\.[^();{}]*\)\s*[;{]', masked):
v.append((_line(masked, m.start()), 'S12 7.6', '禁止可变参数函数(...)'))
# S13 (7.7) 禁止递归函数
for name, ln, body, _pre, _bl, _pl in defs:
if re.search(r'\b%s\s*\(' % re.escape(name), body):
v.append((ln, 'S13 7.7', '递归函数: %s' % name))
# S15 (6.1.4) 文件作用域指针变量必须初始化为 NULL
for idx, line in enumerate(mlines, 1):
if not line or line[0] in ' \t':
continue
if not line.rstrip().endswith(';') or '(' in line or '#' in line:
continue
first = line.split(None, 1)[0]
if first in ('extern', 'typedef', 'struct', 'enum', 'union', 'static',
'return', 'if', 'for', 'while', 'switch', 'do', 'else'):
continue
if '*' not in line or '=' in line:
continue
v.append((idx, 'S15 6.1.4', '文件作用域指针变量须初始化为 NULL: %s'
% line.strip()[:50]))
# S16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀
for idx, line in enumerate(mlines, 1):
if not line or line[0] in ' \t' or line[0] in '}]':
continue
if not line.rstrip().endswith(';') or '(' in line or '#' in line:
continue
first = line.split(None, 1)[0]
if first in ('extern', 'typedef', 'struct', 'enum', 'union', 'static',
'const', 'volatile', 'return', 'if', 'for', 'while',
'switch', 'do', 'else'):
continue
m2 = re.search(r'([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*;\s*$', line)
if not m2:
continue
if not m2.group(1).startswith('g_'):
v.append((idx, 'S16 3.2.2', '非 static 全局变量须 g_ 前缀: %s'
% line.strip()[:50]))
# S17 (2.2) 非 static 函数必须在配对头文件声明(HAL_ 豁免;无配对头则跳过)
if path.endswith('.c'):
hpath = path[:-2] + '.h'
if os.path.exists(hpath):
with open(hpath, 'r', encoding='utf-8', errors='replace') as f:
htext = f.read()
for name, ln, _body, pre, _bl, _pl in defs:
if name.startswith('HAL_'):
continue
seg = pre[pre.rfind(';') + 1:]
seg = seg[seg.rfind('}') + 1:]
if re.search(r'\bstatic\b', seg):
continue
if re.search(r'\b%s\s*\(' % re.escape(name), htext):
continue
v.append((ln, 'S17 2.2', '非 static 函数须在配对头文件声明: %s' % name))
# S18 (6.3.1) 禁止 Yoda 条件(常量在右)
for m in re.finditer(r'\b(0[xX][0-9a-fA-F]+(?:[uUlL]+)?|\d+(?:[uUlL]+)?'
r'|NULL|true|false)\s*(==|!=|<=|>=|<|>)\s+[A-Za-z_]',
masked):
v.append((_line(masked, m.start()), 'S18 6.3.1',
'Yoda 条件(常量应在右): %s' % ' '.join(m.group(0).split())))
# S19 (4.4.2) 函数定义左大括号必须与右括号同行
for name, ln, _body, _pre, bl, pl in defs:
if bl != pl:
v.append((ln, 'S19 4.4.2', '函数定义左大括号须与右括号同行: %s' % name))
return v
+1 -7
View File
@@ -33,7 +33,7 @@ param(
[int]$InitialTimeoutMs = 90000,
[int]$IdleTimeoutMs = 8000,
[int]$MaxBytes = 65536,
[string]$FlashCmd = "",
[string]$FlashCmd = "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM\download.bat",
[string]$Required = "App init OK;Network Init;net init: OK",
[string]$Forbidden = "FAIL;ERROR;HardFault",
[string]$OutDir = "$env:TEMP\opencode",
@@ -46,12 +46,6 @@ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$captureScript = Join-Path $scriptDir "serial_capture.ps1"
$verifyScript = Join-Path $scriptDir "verify_log.ps1"
# Default flash command: resolve relative to this script's repo location
# (the old hardcoded absolute path pointed at a stale checkout location).
if (-not $FlashCmd) {
$FlashCmd = Join-Path $scriptDir "..\MDK-ARM\download.bat"
}
if (-not (Test-Path -LiteralPath $captureScript)) {
Write-Output "RUN_TEST_ERROR: serial_capture.ps1 not found"
exit 2