Compare commits

...

22 Commits

Author SHA1 Message Date
78eff31de7 文档结构调整 2026-08-28 22:02:24 +08:00
565f23ea18 时钟测试完毕 2026-08-28 21:59:04 +08:00
02a05a7a63 时间终于同步,存储成功 2026-08-28 21:10:51 +08:00
4e9ac0d9c3 时间同步已经完成 2026-08-28 16:42:44 +08:00
615a9b13e9 FTP 测试通过 2026-08-28 12:22:10 +08:00
cf33171c8f 存储测试通过 2026-08-27 18:22:44 +08:00
88160a5134 网络主要内容测试完成 2026-08-26 13:54:52 +08:00
0a707034cc 删除多连接内容,只支持单连接 2026-08-25 12:41:33 +08:00
d6bc3f6253 修改一些注释 2026-08-25 11:49:31 +08:00
4375873083 CH395F 硬件测试完毕 2026-08-24 16:16:18 +08:00
fc88bc8752 构建测试 2026-08-22 19:53:24 +08:00
a7f367f6a3 修改测试 2026-07-22 11:14:22 +08:00
c646df5c2a FTP可以上传文件 2026-07-22 01:46:29 +08:00
038a2542af 测试程序可以使用 2026-07-21 23:05:20 +08:00
46a74f24f3 FTP可以连接成功 2026-07-21 22:42:39 +08:00
75cf4c9fbf 成功启动 2026-07-21 22:24:47 +08:00
6d404d136e 初始创建FTP服务器 2026-07-21 22:08:11 +08:00
d48be81478 删除FlashDB 2026-07-21 21:48:23 +08:00
66a1e8fd6a 解决一个擦除的bug 2026-07-21 20:18:28 +08:00
f50b9c9e66 修改网络的初始化,异步支持问题 2026-07-21 01:22:09 +08:00
fbe0726991 增加文件系统 2026-07-21 00:30:59 +08:00
e06d849007 修复多连接的问题 2026-07-19 15:27:49 +08:00
131 changed files with 27214 additions and 5605 deletions

View File

@@ -0,0 +1,14 @@
---
description: Build the STM32F4-Base Keil MDK project. Use ONLY when the user asks to build, compile, or verify the project.
---
Run the Keil MDK-ARM build from the project root:
```
cmd /c "cd /d "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM" && build.bat"
```
Check the last 10 lines of `MDK-ARM/build_log.txt` for errors/warnings.
Return code 0 = success (0 errors, 0 warnings).
Return code 1 = warnings-only (also failure per project rules).
Return code 2+ = compile errors.

284
AGENTS.md
View File

@@ -1,230 +1,94 @@
# STM32F4-Base # STM32F4-Base
## 项目概述 > **编译验证:所有代码修改后必须执行 `@build` 验证编译通过0 错误 0 警告),否则不要提交。**
STM32F407ZGTx (Cortex-M4 FPU) 基础固件项目,集成 CH395F 以太网控制器 SPI 驱动、GD5F2GQ5UE SPI NAND Flash 存储(含 FlashDB KVDB/TSDB 数据库)和 TPAFE5160 16位8通道同步采样 ADC 并行接口驱动。 ## 构建
## 目录结构 - **仅支持 Keil MDK-ARM v5 (ARMCC)**,无其他工具链
- 命令行编译:`@build`(自动执行 `MDK-ARM/build.bat`
- 退出码0 = 成功 | 1 = 有警告(不通过)| 2+ = 错误,查看 `MDK-ARM/build_log.txt`
## 硬件
| 外设 | 接口 | 引脚 |
|------|------|------|
| CH395F 以太网 | SPI2 | PB12 CS, PB13 SCK, PB14 SDO, PB15 SDI |
| 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) |
| 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`)
## 架构
``` ```
STM32F4-Base/ Src/main.c → 外设初始化 → app_main_init → gd5f2gq5ue_init → tpafe5160_init
├── Src/ # CubeMX 生成的外设初始化 + main → osKernelInitialize → MX_FREERTOS_Init → osKernelStart
├── Inc/ # CubeMX 生成的头文件 → [FreeRTOS tasks: defaultTask, netTask, ftpTask]
├── Drivers/
│ ├── BSP/
│ │ ├── CH395F/ # CH395F 以太网芯片驱动(手写)
│ │ ├── NET/ # BSD Socket API 网络抽象层(手写)
│ │ │ ├── net_config.h # 网络配置宏定义
│ │ │ ├── net_types.h # 类型定义、地址结构、Socket 控制块
│ │ │ ├── net_socket.h # BSD Socket API 头文件
│ │ │ ├── net_socket.c # API 核心实现 + 状态机
│ │ │ ├── net_select.h # select/poll API 头文件
│ │ │ └── net_select.c # select/poll 实现
│ │ ├── GD5F2GQ5UE/ # GD5F2GQ5UE NAND Flash 驱动(手写)
│ │ │ ├── gd5f2gq5ue.h/c # 底层 SPI 驱动
│ │ │ ├── fal_flash_gd5f2gq5ue.c # FAL 设备适配层
│ │ │ ├── fal_cfg.h # FAL 设备表 + 分区表
│ │ │ └── fdb_cfg.h # FlashDB 功能配置
│ │ └── TPAFE5160/ # TPAFE5160 ADC 并行接口驱动(手写)
│ ├── STM32F4xx_HAL_Driver/ # ST HAL 库CubeMX 生成)
│ └── CMSIS/ # ARM CMSISCubeMX 生成)
├── Lib/
│ └── FlashDB/ # FlashDB 数据库库v2.2.99
│ ├── src/ # FlashDB 核心源码
│ ├── inc/ # FlashDB 头文件
│ └── port/fal/ # FAL 抽象层
├── MDK-ARM/ # Keil MDK 工程文件
├── docs/ # 参考文档
└── STM32F407-Demo.ioc # STM32CubeMX 项目源文件
``` ```
- **defaultTask**FTL + FatFS 初始化测试(开机一次性)
- **netTask**`net_init()``net_poll()` + `net_process_messages()` 每 10ms
- **ftpTask**`lftpd_start("/", 21, &ftp)` 延迟 7s 后启动
### 任务栈
| Task | 栈大小 |
|---|---|
| defaultTask | 2048 |
| netTask | 4096 |
| ftpTask | 4096 |
## CH395F 设计原则
- **单连接 socket**Socket 0 复用监听和数据通道,收到 CONNECT 后切换为数据通道DISCONNECT/TIMEOUT 后重新 OPEN → LISTEN
- **中断统一处理**:所有 CH395F 中断在 `net_poll()` 中通过 `GET_GLOB_INT_STATUS` 集中读取并清除,不得在 ISR 或其他地方单独处理
## NET 网络层
- `net_socket.h/c` — BSD Socket API多连接模式下 Socket 0 监听、Socket 1~7 数据
- `net_select.h/c` — select/poll I/O 多路复用
- 线程安全通过消息队列(`netMsgQueue`)实现,所有 CH395F 操作在 netTask 中串行处理
- 网络初始化顺序(多连接):`ch395f_set_fun_para(0x02)``ch395f_init()` → 配置 Socket 1~7缓冲区+端口+协议,不 OPEN→ 配置 Socket 0TCP LISTEN
- **新文件需手动添加到 Keil MDK 工程**才能编译
## 关键文件 ## 关键文件
| 路径 | 说明 | | 路径 | 说明 |
|---|---| |---|---|
| `Src/main.c` | 程序入口,初始化序列及主循环 | | `Src/main.c` | 入口,CubeMX 生成 + USER CODE 区域 |
| `Drivers/BSP/CH395F/ch395f.c/h` | CH395F 以太网芯片 SPI 驱动 | | `Src/freertos.c` | FreeRTOS task 创建CubeMX 生成 + USER CODE 区域 |
| `Drivers/BSP/NET/net_socket.c/h` | BSD Socket API 网络抽象层 | | `App/task/net_task.c` | netTask 主循环(网络初始化 + 轮询) |
| `Drivers/BSP/NET/net_select.c/h` | select/poll I/O 多路复用 | | `Drivers/BSP/NET/` | BSD Socket API 网络抽象层 |
| `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c/h` | GD5F2GQ5UE NAND Flash SPI 驱动 | | `Drivers/BSP/CH395F/` | CH395F SPI 驱动 |
| `Drivers/BSP/GD5F2GQ5UE/fal_flash_gd5f2gq5ue.c` | FAL 设备适配层 | | `Drivers/BSP/GD5F2GQ5UE/` | NAND Flash 驱动 + dhara FTL + FatFS diskio |
| `Drivers/BSP/TPAFE5160/tpafe5160.c/h` | TPAFE5160 ADC 并行接口驱动 | | `Drivers/BSP/TPAFE5160/` | ADC 并行接口驱动 |
| `Src/freertos.c` | FreeRTOS task 创建和任务函数CubeMX 生成 + 手写) | | `Drivers/BSP/SD2506/` | RTC 驱动 |
| `Inc/FreeRTOSConfig.h` | FreeRTOS 内核配置 | | `Drivers/BSP/RS485/` | RS-485 通信UART5 + PD0 DE |
| `MDK-ARM/STM32F407-Demo.uvprojx` | Keil MDK 项目文件 |
| `STM32F407-Demo.ioc` | STM32CubeMX 项目源文件 |
## 构建 ## 驱动关键点
仅支持 Keil MDK-ARM v5 (ARMCC)。打开 `MDK-ARM/STM32F407-Demo.uvprojx` 编译。 详见 `docs/` 目录:
- `CH395F_Trap_Records.md` — CH395F 已知陷阱
- 编译器ARMCC V5.06 update 7 - `GD5F2GQ5UE_Trap_Records.md` — GD5F2GQ5UE 已知陷阱
- 优化等级:`-O4` (项目级)`spi.c`/`usart.c` / HAL 源文件使用 `-O0` - `CH395F_Test_Guide.md` — 网络测试说明
- C 标准C99
- 全局宏定义:`USE_HAL_DRIVER, STM32F407xx`
### 命令行编译Agent 使用)
修改代码后必须执行命令行编译验证,编译脚本位于 `MDK-ARM/build.bat`
```
cmd /c "cd /d "工作目录" && build.bat"
```
实际执行示例(从项目根目录):
```
cmd /c "cd /d "D:\Code\DTU 程序\STM32F4-Base\MDK-ARM" && build.bat"
```
**编译结果判读:**
- 退出码 0无错误无警告编译成功
- 退出码 1有警告无错误编译失败警告不可接受需修复
- 退出码 2+:编译失败,需查看 `MDK-ARM/build_log.txt` 定位错误
**UV4.exe 路径:** `C:\Keil_v5\UV4\UV4.exe`
**工程文件:** `MDK-ARM/STM32F407-Demo.uvprojx`
**目标名称:** `STM32F407-Demo`
## 硬件配置
- **主频:** HSE 25MHz → PLL 168MHz (4/168/2)
- **6 个 LED** PC4, PC5, PB1, PB2, PF11, PF12低电平点亮
- **CH395F** SPI2 (PB12 CS, PB13 SCK, PB14 SDO, PB15 SDI)
- **GD5F2GQ5UE** SPI1 (PE0 CS, PB3 SCK, PB4 MISO, PB5 MOSI, PB8 WP, PE1 HOLD)
- **TPAFE5160** 并行16位 (PG0-PG15 数据, PD3 RD, PD4 CONVST, PD7 BUSY, PD1 FRSTDATA, PF13-15 OS[2:0])
- **USART1** PA9 TX, PA10 RX (115200bps)
## 启动顺序
```
HAL_Init() → SystemClock_Config() → MX_GPIO_Init() → MX_USART1_UART_Init() → MX_SPI2_Init() → MX_SPI1_Init()
→ gd5f2gq5ue_init() → fdb_kvdb_init() → [USER CODE: net_init + TCP listen + 其他外设初始化]
→ osKernelInitialize() → MX_FREERTOS_Init() → osKernelStart() ← FreeRTOS 调度器启动
→ [FreeRTOS tasks: defaultTask, netTask 运行]
```
## 代码规范
参考 `嵌入式C语言代码规范V1.0.md`,关键要点:
- 缩进4 空格,禁止 Tab
- 命名:小写字母+下划线;全局变量 `g_` 前缀,静态 `s_`,指针 `p_`,数组 `a_`
- 函数注释块需包含:函数功能、入口参数、返回值、限定条件、函数说明
- 大括号K&R 风格(左大括号不换行)
- 文件头注释:模块名称、功能、平台、作者、日期、修改记录
- 头文件保护宏:`__MODULE_NAME_H` 格式,带 `extern "C"`
## 注意 ## 注意
- `Inc/``Src/` 中 CubeMX 生成的文件带有 `USER CODE BEGIN`/`END` 标记,自定义代码写在这些区域之间 - `Inc/``Src/` 中 CubeMX 生成的文件带有 `USER CODE BEGIN`/`END` 标记,自定义代码写在这些区域之间
- `ch395f.c/h``gd5f2gq5ue.c/h``fal_flash_gd5f2gq5ue.c` 为纯手工代码,不受 CubeMX 保护 - `Drivers/BSP/` 下的驱动文件为纯手工代码,不受 CubeMX 保护
- `tpafe5160.c/h` 为纯手工代码,不受 CubeMX 保护 - CH395F 每次 SPI 事务需用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹
- CH395F 每次 SPI 事务需调用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹 - CH395F SPI PCB设计问题现在分频系数必须为 8速度相对较低
- GD5F2GQ5UE 的 `gd5f2gq5ue.c` 中声明了 `extern SPI_HandleTypeDef hspi1`,需确保 SPI1 已初始化 - FreeRTOS V10.3.1 via CMSIS-RTOS V2HAL 时基用 TIM7非 SysTick
- FlashDB 使用 FAL 模式,`fdb_cfg.h` 中定义 `FDB_USING_FAL_MODE``fal_cfg.h` 中定义分区表 - NVIC 优先级分组 4 位,外设中断优先级 ≥ 5`configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`
- FlashDB 详细使用说明见 `FlashDB使用说明.md`
- `sd2506.c/h` 为纯手工代码,不受 CubeMX 保护
- FreeRTOS Kernel V10.3.1 via CMSIS-RTOS V2 接口HAL 时基使用 TIM7非 SysTick避免与 FreeRTOS 冲突)
- NVIC 优先级分组 4 位,外设中断优先级全部 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`5确保 ISR 可调 FreeRTOS API
- 网络处理在 `netTask` 中运行,`net_poll()` 每 10ms 调用一次TCP echo 在 netTask 中完成
- `freeertos.c``USER CODE` 区域可添加自定义 task、mutex、semaphore、queue
## CH395F 驱动关键点 ## 源码编辑与编码安全(铁律)
驱动初始化顺序、SPI/DMA 通信、TCP/UDP 参数配置、Socket 缓冲区分配等完整关键点见 `docs/CH395F_Trap_Records.md` → 附CH395F 驱动关键点。 本工程源码含大量中文注释,曾因错误的文本重写方式导致整文件乱码(`U+FFFD` 替换符,不可逆)。以下规则必须遵守:
## 测试说明 - **绝不用 `Get-Content` / `Set-Content` / `cmd` 文本重写含中文的源码文件**。这类"整文件重写"会按系统默认编码重新解码再写回,多字节中文必被 `U+FFFD` 替换且不可逆。
- **改代码只用 Edit 工具**(精确子串替换,不重新编解码整个文件,安全);批量替换用 Edit 的 `replaceAll`
- **测试代码文件**`Drivers/BSP/CH395F/ch395f_test.h` / `ch395f_test.c` - 必须脚本级替换时,使用显式且一致的编码(如 Python `open(p, encoding="utf-8")` 同编码回写并先确认工程编码Keil 工程常用 GBK——但**最稳的是别碰**,只用 Edit / git。
- **PC 端测试脚本**`test/ch395f_socket_test.py`,支持 Python 3.7+ - **发现乱码先别急着 `git checkout`**`git checkout` 会丢弃所有未提交工作,使损失扩大。先评估能否用 Edit 仅改坏掉的那几行。
- **调用入口**`Src/main.c``USER CODE BEGIN 2` 区域 - 验证是否真损坏用字节级检查(统计 `EF BF BD` 出现次数),不要依赖终端/Read 渲染判断。
- **Phase 1**9 项寄存器测试):✅ 通过
- **Phase 2**TCP Client 收发 + 关闭重连 ×3 轮):✅ 通过
- **Phase 3**UDP Server Echo + PING + 大包):✅ 通过
- **Phase 4**NET 层 TCP Echo✅ 通过netTask 中永久运行)
- **Phase 5**NET 层 UDP Echo✅ 通过
- **Phase 6**DHCP 自动获取 IP✅ 通过MCU 广播宣告 + PC 监听发现)
- **Phase 7**Select/Poll I/O 多路复用):✅ 通过
- **Phase 8**(多客户端 7 并发):✅ 通过,成功率 100%
- **测试拓扑**PC (192.168.1.2) ↔ 交换机 ↔ CH395F (192.168.1.100)
- **详细使用说明**:见 `docs/CH395F_Test_Guide.md`
## GD5F2GQ5UE 驱动关键点
- SPI Mode 0CPOL=0, CPHA=0时钟 42MHz
- 初始化必须按顺序:复位 → 读 ID → 使能 ECCB0h=10h→ 解除块保护A0h=00h
- SET_FEATURE 命令前必须先发写使能06h
- 块擦除D8h参数是字节地址块编号 × 128KB不是块编号
- 读取 ID9Fh返回 3 字节,第 0 字节无意义,第 1 字节 MID第 2 字节 DID
## FlashDB 分区规划
| 分区名 | 偏移 | 大小 | 用途 |
|--------|------|------|------|
| fdb_kvdb1 | 0 | 64MB | KVDB 键值数据库 |
| fdb_tsdb1 | 64MB | 64MB | TSDB 时序数据库 |
## TPAFE5160 驱动关键点
- AD7606 P2P 兼容替代品,并行接口协议一致
- 并行模式CS 接地始终选中PAR/SER/BYTE SEL 接 GND并行DB15/BYTE SEL 接 GND非字节模式
- 数据总线 DB[15:0] 接 GPIOG[15:0],通过 `(uint16_t)GPIOG->IDR` 一次读取16位
- CONVST 上升沿触发全部8通道同步采样BUSY 高电平表示转换中
- RD 下降沿输出通道数据按通道1~8顺序依次输出
- FRSTDATA 在第一个 RD 下降沿变高指示通道1数据就绪
- 过采样 OS[2:0] 在 BUSY 下降沿锁存,无过采样时 tCONV=1.74µs64倍过采样时 tCONV=167µs
- 读取时序168MHz 下 GPIO 写操作 + 5个 NOP (~30ns) 覆盖 t10=22ns 和 t14=21ns 要求
- 硬件 RANGE 接 GND → ±5V 量程LSB=152.59µV
## NET 网络层关键点
- **文件结构**`Drivers/BSP/NET/` 下包含 net_config.h、net_types.h、net_socket.h/c、net_select.h/c
- **使用前必须调用** `net_init(ip, mask, gateway)` 初始化网络子系统
- **`net_poll()` 在 FreeRTOS netTask 中定期调用**10ms 周期),轮询所有 Socket 状态,处理中断事件
- **TCP Server 多连接模式**1 个监听 Socket + 最多 7 个数据 Socket
- **select/poll 支持**`net_select()``net_poll_events()` 用于 I/O 多路复用
- **事件回调**:可通过 `net_set_event_cb()` 注册连接/断开/数据到达等事件回调
- **非阻塞模式**`net_recv()` 使用 `NET_MSG_DONTWAIT` 标志,或使用 select/poll
- **字节序转换**:使用 `net_htons()/net_ntohs()/net_htonl()/net_ntohl()` 进行主机序/网络序转换
- **IP 地址转换**`net_inet_addr("192.168.1.100")` 字符串转网络序,`net_inet_ntoa()` 反向转换
- **注意**:新文件需手动添加到 Keil MDK 工程中才能编译
- **RECV 中断是电平触发的**`net_poll()``do { ... } while(0)` 只处理一批中断,由主循环读取数据,避免无限循环导致主循环饿死
- **TCP Server 多连接模式**Socket 0 专职监听Socket 1~7 自动分配
- **多连接模式初始化顺序(关键!遗漏将导致 TCP 连接失败)**
1. `ch395f_set_fun_para(0x02)` — 启用多连接(在 `INIT_CH395` 之前)
2. `ch395f_init()` — 协议栈初始化
3. **先配置数据 Socket 1~7**(必须!必须在监听 Socket 之前配置CH395F 在 `TCP_LISTEN` 时刻扫描可用 Socket
- `SET_SEND_BUF``SET_RECV_BUF``SET_SOUR_PORT与监听端口相同``SET_PROTO_TCP`
- 数据 Socket 不调用 `OPEN_SOCKET`,由 CH395F 在连接到达时自动打开
4. **再配置监听 Socket 0**
- `SET_PROTO_TCP → SET_SOUR_PORT → OPEN → TCP_LISTEN`
- 如果不按此顺序Socket 4~7 不可用,最多只接受 3 个并发连接
- **TCP Server 单连接模式**Socket 先 `OPEN``SET_PROTO_TCP``SET_SOUR_PORT``OPEN``TCP_LISTEN`;收到 `SINT_STAT_CONNECT` 后 Socket 自动切换为数据通道;`DISCONNECT`/`TIMEOUT` 后需重新 `OPEN``TCP_LISTEN`
## 测试状态
| Phase | 描述 | 状态 | 备注 |
|-------|------|------|------|
| 1 | 寄存器测试 | ✅ 19/19 | |
| 2 | TCP Client | ✅ 3/3 | |
| 3 | UDP Server | ✅ 通过 | |
| 4 | NET TCP Echo | ✅ 通过 | netTask 中永久运行 |
| 5 | NET UDP Echo | ✅ 通过 | |
| 6 | DHCP | ✅ 通过 | 支持广播宣告 + DHCP Server 两种模式 |
| 7 | Select/Poll | ✅ 通过 | |
| 8 | 多客户端 7 并发 | ✅ 70/70 100% | 修复顺序后通过 |
## 陷阱记录
详细陷阱记录(含根因分析、解决方案、发现时间)见 `docs/CH395F_Trap_Records.md`
当前已记录陷阱:
- **Trap 01** Socket 4~7 自动分配不到(初始化顺序)
- **Trap 02** 数据 Socket 缓冲区重叠
- **Trap 03** DHCP 包 xid 偏移错误
- **Trap 04** UDP 发送未等待 SENDBUF_FREE
- **Trap 05** CH395F 与 RTL8305NBI 自动协商不兼容
- **Trap 06** TCP KeepAlive 参数非 500ms 倍数
- **Trap 07** TCP 关闭误用 disconnect 导致 FIN_WAIT_2
- **Trap 08** RECV 中断电平触发无限循环

36
App/app_cfg.h Normal file
View File

@@ -0,0 +1,36 @@
/*
* 模块名称Application Configuration
* 模块功能:应用层全局配置宏,控制功能模块的编译开关
* 适用平台STM32F407ZGTx
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#ifndef __APP_CFG_H
#define __APP_CFG_H
#ifdef __cplusplus
extern "C" {
#endif
/*
* 系统时钟源选择
* 0 - DWT CYCCNT默认0 外设开销)
* 1 - TIM232-bit 定时器,独立于 DWT
*/
#define APP_SYS_TIME_SRC 0
/*
* 功能模块使能开关
*/
#define APP_NET_ENABLE
#define APP_ADC_ENABLE
#define APP_RS485_ENABLE
#ifdef __cplusplus
}
#endif
#endif /* __APP_CFG_H */

42
App/app_main.c Normal file
View File

@@ -0,0 +1,42 @@
/*
* 模块名称Application Main Entry
* 模块功能:应用初始化流程编排、各模块启动、主循环
* 适用平台STM32F407ZGTx
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#include "app_main.h"
#include "app_cfg.h"
#define DBG_TAG "[APP]"
#include "dbg_log.h"
/*
* 函数功能:应用层初始化
* 入口参数:无
* 返 回 值0 - 成功,-1 - 失败
* 限定条件CubeMX 外设初始化已完成
* 函数说明:在 CubeMX 外设初始化之后、FreeRTOS 调度器启动之前调用
*/
int app_main_init(void)
{
DBG_INFO("App init OK");
return 0;
}
/*
* 函数功能:应用主循环
* 入口参数:无
* 返 回 值:无
* 限定条件FreeRTOS 调度器启动后不再返回
* 函数说明:空闲任务可调用的应用层处理入口
*/
void app_main_run(void)
{
for (;;) {
/* 应用层空闲处理 */
}
}

27
App/app_main.h Normal file
View File

@@ -0,0 +1,27 @@
/*
* 模块名称Application Main Entry
* 模块功能:应用层入口,由 main.c 调用完成所有应用初始化与启动
* 适用平台STM32F407ZGTx
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#ifndef __APP_MAIN_H
#define __APP_MAIN_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
int app_main_init(void);
void app_main_run(void);
#ifdef __cplusplus
}
#endif
#endif /* __APP_MAIN_H */

0
App/protocol/.gitkeep Normal file
View File

369
App/sys_clock.c Normal file
View File

@@ -0,0 +1,369 @@
/*
* 模块名称System Clock — Wall Clock Time Maintenance
* 模块功能:实现系统墙钟时间维护,包括 RTC 初始读取、HAL Tick 漂移维持
* 适用平台STM32F407ZGTx (Cortex-M4 FPU, 168MHz)
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#include "sys_clock.h"
#include "main.h"
#include <string.h>
#include <stdio.h>
#define DBG_TAG "[CLK]"
#include "dbg_log.h"
/* ============================================================
* RTC 校时(开发期一次性)
* 使能 RTC_DEFAULT_INIT_ENABLE 后:
* RTC_DEFAULT_INIT_FORCE=1每次启动都写入默认时间首次/覆盖校时用)
* RTC_DEFAULT_INIT_FORCE=0仅当 RTC 时间无效(越界)时写入
* 校时完成后建议将 FORCE 置 0或注释 ENABLE避免每次开机覆盖用户设的时间。
* 修改日期/时间时同步更新下列宏。
* ============================================================ */
#define RTC_DEFAULT_INIT_ENABLE 1
#define RTC_DEFAULT_INIT_FORCE 0
#define RTC_DEFAULT_YEAR 2026
#define RTC_DEFAULT_MONTH 8
#define RTC_DEFAULT_DAY 24
#define RTC_DEFAULT_HOUR 0
#define RTC_DEFAULT_MINUTE 0
#define RTC_DEFAULT_SECOND 0
/* ============================================================
* 内部状态:系统墙钟
* ============================================================ */
static volatile uint32_t s_base_tick = 0; /* HAL_GetTick() 基准 */
static volatile uint32_t s_base_unix_secs = UINT32_MAX;
/* ============================================================
* 内部辅助函数Unix time ↔ sd2506_time_t 转换
* ============================================================ */
/*
* 函数功能:根据公历日期计算星期几
* 入口参数y - 年份 uint16_t 1970~2099
* m - 月份 uint8_t 1~12
* d - 日 uint8_t 1~31
* 返回值:星期编号 uint8_t 0=Sunday, 1=Monday, ... 6=Saturday
* 限定条件:输入为公历合法日期
* 函数说明1. 采用 Zeller 公式(变种),结果 0 表示周日
* 2. 与 unix_to_sd2506() 内星期算法保持一致
*/
static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d)
{
uint32_t wy = y;
uint32_t wm = m;
if (wm <= 2) {
wy--;
wm += 12u;
}
/* Zeller 公式:世纪项 + 月项 + 日项,模 7 得星期0=周日) */
return (uint8_t)(((uint32_t)(wy % 100u + wy / 100u / 4u - wy / 100u / 15u) +
(uint32_t)((26u * (wm + 1u)) / 10u) + (uint32_t)(d % 100u)) % 7u);
}
/*
* 函数功能:将 Unix 时间戳(自 1970-01-01 00:00:00 起的秒数)转换为 SD2506 RTC 时间结构
* 入口参数unix_secs - Unix 时间戳 uint32_t 0~0x7FFFFFFF~2038
* p_sd_time - 输出时间结构指针 sd2506_time_t* 不得为 NULL
* 出口参数p_sd_time - 填充年/月/日/时/分/秒/星期
* 返回值:无
* 限定条件p_sd_time 必须指向有效缓冲区
* 函数说明1. 先取模拆分时分秒,再按累计天数推算年月日
* 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)
{
uint32_t y, m, d;
uint8_t h, mi, s;
int is_leap;
/* 时分秒 */
{
uint64_t total = (uint64_t)unix_secs;
s = (uint8_t)(total % 60uLL);
total /= 60uLL;
mi = (uint8_t)(total % 60uLL);
total /= 60uLL;
h = (uint8_t)(total % 24uL);
}
/* 年月日 */
{
uint64_t day_count = ((uint64_t)unix_secs / 86400uL);
y = 1970u;
while (day_count >= 365uLL) {
uint64_t diy = 365uLL;
if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) {
diy = 366uLL;
}
if (day_count < diy) break;
day_count -= diy;
y++;
}
is_leap = ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0));
uint8_t mdays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (is_leap) mdays[1] = 29;
m = 0;
while (m < 12u) {
if (day_count < (uint64_t)mdays[m]) break;
day_count -= mdays[m];
m++;
}
d = (uint32_t)(day_count + 1uL);
/* 星期Zeller's congruence0=Sunday */
uint64_t wy = y;
uint64_t wm = m;
if (wm <= 2) {
wy--;
wm += 12uL;
}
/* Zeller 公式同 rtc_calc_week():世纪项 + 月项 + 日项,模 7 得星期0=周日) */
uint8_t week = ((uint32_t)(wy % 100uLL + wy / 100uLL / 4uLL - wy / 100uLL / 15uLL) +
(uint32_t)(((uint64_t)26 * (wm + 1uL)) / 10uLL) +
(uint32_t)(d % 100uLL)) % 7uLL;
/* sd2506_time_t.year 采用完整年份(如 2026与 sd2506_set_time
* 中 year-2000、sd2506_get_time 中 +2000 的约定保持一致;
* 此处绝不能再减 2000否则写 RTC 时会被二次减 2000 导致年份错乱。 */
p_sd_time->year = (uint16_t)y;
p_sd_time->month = m + 1uL; /* 月份从 0 → 1 */
p_sd_time->day = d;
p_sd_time->hour = h;
p_sd_time->minute = mi;
p_sd_time->second = s;
p_sd_time->week = week;
}
}
/* ============================================================
* 系统墙钟时间维护
* ============================================================ */
/*
* 函数功能:初始化系统墙钟时间基准
* 入口参数:无
* 返回值0 - 成功(始终返回 0
* 限定条件CubeMX 外设初始化完成、SD2506 RTC 驱动已初始化
* 函数说明1. 从 SD2506 读取初始时间必要时按编译期默认宏校时RTC_DEFAULT_INIT_*
* 2. 将 RTC 时间换算为 Unix epoch 秒数
* 3. 记录当前 HAL Tick 与 Unix 秒数为基准,供 sys_clock_get() 后续推算
* 4. 注意sd2506_get_time() 返回的 year 已是完整年份(驱动内 +2000
* 换算时不得再加 2000否则年份虚增导致 Unix 秒数超 uint32 回绕
*/
int sys_clock_init(void)
{
sd2506_time_t sd_time;
uint32_t year, month, day, hour, minute, second;
int64_t unix_secs = 0;
int i;
/* ============================================================
* Step 1从 SD2506 硬件 RTC 读取初始时间
* ============================================================
* 注意sd2506_get_time() 返回的 year 已是完整年份(如 2026
* 驱动内部已 +2000。此处【禁止】再 +2000否则年份虚增到 4026
* 换算出的 Unix 秒数远超 uint32_t 上限被截断回绕,时间彻底错乱。 */
sd2506_get_time(&sd_time);
/* ============================================================
* Step 1.1:开发期校时(可选,由 RTC_DEFAULT_INIT_* 宏控制)
* ============================================================
* 作用RTC 出厂/掉电异常导致时间非法时,写入一个可用初始值;
* FORCE=1 时每次启动都强制写入,便于首次烧录或硬件时钟丢失。
* 判定"时间无效":年份越界、月/日越界(仅粗判,不细校 2 月 29 日等)。 */
#if defined(RTC_DEFAULT_INIT_ENABLE)
{
int rtc_invalid = (sd_time.year < 2000 || sd_time.year > 2099 ||
sd_time.month < 1 || sd_time.month > 12 ||
sd_time.day < 1 || sd_time.day > 31);
#if (RTC_DEFAULT_INIT_FORCE == 1)
rtc_invalid = 1; /* 强制每次启动校时:会覆盖用户/网络已设的正确时间,仅限开发期 */
#endif
if (rtc_invalid) {
sd2506_time_t init_t;
/* 用编译期默认宏构造初始时间week 由 Zeller 公式rtc_calc_week推算 */
init_t.year = RTC_DEFAULT_YEAR;
init_t.month = RTC_DEFAULT_MONTH;
init_t.day = RTC_DEFAULT_DAY;
init_t.hour = RTC_DEFAULT_HOUR;
init_t.minute = RTC_DEFAULT_MINUTE;
init_t.second = RTC_DEFAULT_SECOND;
init_t.week = rtc_calc_week(init_t.year, init_t.month, init_t.day);
/* 写回 SD2506断电后该时间被保留下次上电即为有效值 */
if (sd2506_set_time(&init_t) == SD2506_OK) {
DBG_INFO("sys_clock_init: RTC set %04d-%02d-%02d %02d:%02d:%02d",
init_t.year, init_t.month, init_t.day,
init_t.hour, init_t.minute, init_t.second);
sd2506_get_time(&sd_time); /* 重新读取校准后的值,确保后续换算用真实 RTC 数据 */
} else {
DBG_ERROR("sys_clock_init: RTC default write FAIL");
}
}
}
#endif
/* 取出各时间字段year 已是完整年份,直接用于下文 Unix 换算) */
year = (uint32_t)sd_time.year;
month = (uint32_t)sd_time.month;
day = (uint32_t)sd_time.day;
hour = (uint32_t)sd_time.hour;
minute = (uint32_t)sd_time.minute;
second = (uint32_t)sd_time.second;
/* ============================================================
* Step 2将 RTC 日历时间换算为 Unix epoch 秒数
* ============================================================
* Unix 时间定义:自 1970-01-01 00:00:00 起经过的秒数。
* 算法总天数1970~去年整年 + 今年到上月 + 本月已过天)× 86400
* + 当天时分秒。
* 用 int64_t / uint64_t 累加避免中间溢出;最终结果(受 uint32_t 限制)
* 有效范围约 1970-01-01 ~ 2038-01-19超出将回绕见文档第 12 节)。 */
{
uint64_t total_days = 0;
/* 2.1 累加 1970 至(今年-1的整年天数闰年 366、平年 365
* 闰年规则:能被 4 整除但不能被 100 整除,或能被 400 整除 */
for (i = 1970; i < year; i++) {
if ((i % 4 == 0 && i % 100 != 0) || (i % 400 == 0)) {
total_days += 366uLL;
} else {
total_days += 365uLL;
}
}
/* 2.2 判断今年是否闰年构造各月天数表2 月特判) */
int is_leap = ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
uint8_t mdays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (is_leap) mdays[1] = 29; /* 闰年 2 月 29 天 */
/* 2.3 累加今年 1 月至(本月-1月的天数 */
for (i = 0; i < month - 1; i++) {
total_days += mdays[i];
}
/* 2.4 加上本月已过天数day-1从今天 0 点起算) */
total_days += day - 1uL;
/* 2.5 总天数×86400一天秒数+ 当日时分秒 = 绝对 Unix 秒数 */
unix_secs = (int64_t)(total_days * 86400LL) + hour * 3600LL + minute * 60LL + second;
}
/* ============================================================
* Step 3建立"墙钟基准"(基准法核心)
* ============================================================
* 记录当前 HAL Tick 与对应的 Unix 秒数。此后 sys_clock_get() 仅用
* now_unix = s_base_unix_secs + (HAL_GetTick() - s_base_tick)/1000
* 推算当前时间,运行期不再访问 I2C RTC零阻塞、无累积误差。 */
s_base_tick = HAL_GetTick();
s_base_unix_secs = (uint32_t)unix_secs;
DBG_INFO("sys_clock_init: RTC initial time %04d-%02d-%02d %02d:%02d:%02d",
year, month, day, hour, minute, second);
DBG_INFO("sys_clock_init: wall clock base = unix %lu, base_tick=%lu",
(unsigned long)s_base_unix_secs, (unsigned long)s_base_tick);
return 0;
}
/*
* 函数功能获取当前系统墙钟时间Unix 时间戳)
* 入口参数:无
* 返回值:当前 Unix 秒数 uint32_t 未初始化时返回 UINT32_MAX
* 限定条件sys_clock_init() 已成功调用
* 函数说明1. 以基准 Tick 与基准 Unix 秒数为起点,按当前 HAL Tick 与 1000ms
* 的整除差推算流逝秒数(避免每毫秒累加,减少漂移与溢出风险)
* 2. 若推算结果为负(基准未建立),返回 UINT32_MAX 表示无效
*/
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);
if (unix_secs < 0) {
return UINT32_MAX; /* 未初始化 */
}
return (uint32_t)unix_secs;
}
/*
* 函数功能获取当前墙钟时间的可读字符串YYYY-MM-DD HH:MM:SS
* 入口参数buf - 输出字符串缓冲区 char* 不得为 NULL
* buf_size - 缓冲区大小 size_t 须 ≥ 20含结尾 '\0'
* 出口参数buf - 填充格式化时间字符串
* 返回值buf 指针(便于链式调用)
* 限定条件buf 指向至少 buf_size 字节的有效缓冲区sys_clock_init() 已调用
* 函数说明1. 内部先取 Unix 秒数,未初始化时输出占位串 "----/--/-- --:--:--"
* 2. 通过 unix_to_sd2506() 转换为 SD2506 结构后格式化
* 3. 使用 snprintf 防止缓冲区溢出
*/
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, "----/--/-- --:--:--");
return buf;
}
sd2506_time_t sd;
unix_to_sd2506(unix_secs, &sd);
snprintf(buf, buf_size, "%04d-%02d-%02d %02d:%02d:%02d",
sd.year, sd.month, sd.day,
sd.hour, sd.minute, sd.second);
return buf;
}
/*
* 函数功能:设置(更新)系统墙钟时间基准,并可写回 SD2506 RTC
* 入口参数unix_secs - 新的 Unix 时间戳 uint32_t 0~0x7FFFFFFF
* sd_time - 对应的 RTC 时间结构指针 sd2506_time_t* 可为 NULL仅更新基准
* 出口参数:无
* 返回值0 - 成功(始终返回 0
* 限定条件unix_secs 为合法 Unix 时间戳
* 函数说明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)
{
/* Step 1: 更新墙钟基准 */
s_base_tick = HAL_GetTick();
s_base_unix_secs = unix_secs;
DBG_INFO("sys_clock_set: wall clock updated to %lu", (unsigned long)unix_secs);
/* Step 2: 写回 SD2506 RTC若提供了 sd_time */
if (sd_time != NULL) {
if (sd2506_set_time(sd_time) == SD2506_OK) {
DBG_INFO("sys_clock_set: SD2506 RTC written");
} else {
DBG_ERROR("sys_clock_set: SD2506 RTC write FAIL");
}
}
return 0;
}
int sys_clock_set_unix(uint32_t unix_secs)
{
sd2506_time_t sd;
/* 内部静态函数:将 Unix 秒数拆分为 SD2506 时间结构 */
unix_to_sd2506(unix_secs, &sd);
/* 更新运行期基准并写回 RTC实现掉电保持 */
return sys_clock_set(unix_secs, &sd);
}

75
App/sys_clock.h Normal file
View File

@@ -0,0 +1,75 @@
/*
* 模块名称System Clock — Wall Clock Time Maintenance
* 模块功能:基于 HAL Tick + SD2506 RTC 的系统墙钟时间维护
* 启动时从 SD2506 RTC 读取初始时间,运行期间以 HAL Tick 计数维持
* 适用平台STM32F407ZGTx (Cortex-M4 FPU, 168MHz)
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#ifndef __SYS_CLOCK_H
#define __SYS_CLOCK_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include "sd2506.h"
/*
* 函数功能:系统时钟初始化(从 SD2506 RTC 读取初始时间)
* 入口参数:无
* 返回值0 - 成功, -1 - 失败
* 限定条件sd2506_init() 已调用
* 函数说明:读取 SD2506 RTC 当前值转换为 Unix timestamp设为系统墙钟初始时间
*/
int sys_clock_init(void);
/*
* 函数功能获取当前系统墙钟时间Unix epoch seconds
* 入口参数:无
* 返回值Unix time (秒),失败返回 UINT32_MAX
* 限定条件sys_clock_init() 已调用
* 函数说明:基于 HAL Tick 计数 + 上次校准时刻的 Unix 秒数计算当前时间,
* 精度为毫秒级
*/
uint32_t sys_clock_get(void);
/*
* 函数功能:格式化系统墙钟时间为字符串
* 入口参数buf - 输出缓冲区(至少 20 字节)
* buf_size - 缓冲区大小
* 返回值buf 指针,失败返回 NULL
* 限定条件sys_clock_init() 已调用
* 函数说明:格式 "YYYY-MM-DD HH:MM:SS"
*/
char *sys_clock_get_str(char *buf, size_t buf_size);
/*
* 函数功能:设置系统墙钟时间
* 入口参数unix_secs - Unix epoch time (秒)
* sd_time - SD2506 RTC 时间结构体(可选,非 NULL 时同时写入 RTC
* 返回值0 - 成功, -1 - 失败
* 限定条件sys_clock_init() 已调用
* 函数说明:更新系统墙钟基准 + 写回 SD2506 RTC 保持长期准确性
*/
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time);
/*
* 函数功能:以 Unix 时间戳设置系统墙钟并写回 SD2506 RTC
* 入口参数unix_secs - Unix epoch time (秒,本地时间) uint32_t 0~0x7FFFFFFF
* 返回值0 - 成功
* 限定条件sys_clock_init() 已调用
* 函数说明:内部将 Unix 秒数转换为 sd2506_time_t 后调用 sys_clock_set()
* 便于时间同步模块(如 PC 推送)直接以时间戳校时
*/
int sys_clock_set_unix(uint32_t unix_secs);
#ifdef __cplusplus
}
#endif
#endif /* __SYS_CLOCK_H */

45
App/task/adc_task.c Normal file
View File

@@ -0,0 +1,45 @@
/*
* 模块名称ADC Sampling Task
* 模块功能TPAFE5160 8 通道同步采样,数据缓存与通知
* 适用平台STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#include "adc_task.h"
#include "cmsis_os.h"
#include "tpafe5160.h"
#define DBG_TAG "[ADC_TASK]"
#include "dbg_log.h"
/*
* 函数功能ADC 任务初始化
* 入口参数:无
* 返 回 值0 - 成功,-1 - 失败
* 限定条件TPAFE5160 硬件已上电
* 函数说明:配置采样参数,准备数据缓冲区
*/
int adc_task_init(void)
{
DBG_INFO("adc_task_init");
return 0;
}
/*
* 函数功能ADC 任务主函数
* 入口参数arg - FreeRTOS 传入参数(未使用)
* 返 回 值:无
* 限定条件adc_task_init() 已成功调用
* 函数说明:定时触发 TPAFE5160 同步采样并处理数据
*/
void adc_task_func(void const *arg)
{
(void)arg;
for (;;) {
osDelay(1000);
}
}

25
App/task/adc_task.h Normal file
View File

@@ -0,0 +1,25 @@
/*
* 模块名称ADC Sampling Task
* 模块功能TPAFE5160 8 通道同步采样,数据缓存与通知
* 适用平台STM32F407ZGTx + TPAFE5160
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#ifndef __ADC_TASK_H
#define __ADC_TASK_H
#ifdef __cplusplus
extern "C" {
#endif
int adc_task_init(void);
void adc_task_func(void const *arg);
#ifdef __cplusplus
}
#endif
#endif /* __ADC_TASK_H */

114
App/task/net_task.c Normal file
View File

@@ -0,0 +1,114 @@
/*
* 模块名称Network Application Task
* 模块功能CH395F 协议栈初始化 + 轮询 + TCP/UDP 业务处理
* 与 net_socket.h 配合,在 FreeRTOS 独立任务中运行
* 适用平台STM32F407ZGTx + CH395F
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
* v1.1 2026-07-19 王建锋 migrate from freertos.c StartNetTask
* v1.2 2026-07-21 王建锋 网络初始化移入本任务net_init + TCP 监听)
* v1.3 2026-07-22 王建锋 单连接模式重构,增加大文件/边界/KeepAlive 测试
*/
#include <string.h>
#include "net_task.h"
#include "cmsis_os.h"
#include "net_socket.h"
#include "net_select.h"
#include "ch395f.h"
#define DBG_TAG "[NET_TASK]"
#include "dbg_log.h"
/* 网络初始化完成标志(供 ch395f_test_task 等外部任务等待) */
volatile uint8_t g_net_ready = 0;
/*
* 函数功能CH395F 协议栈一次性初始化(两种构建模式共用)
* 入口参数:无
* 返 回 值0 - 成功,-1 - 失败
* 函数说明net_init → MAC 打印 → PHY 强制 100M 全双工 → 置 g_net_ready。
* NET 层模式由 netTask 调用;硬件层模式(阶段 1~5netTask 不创建,
* 由 ch395fTestTask 调用后独占 CH395F。
*/
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) {
DBG_ERROR("net init: FAIL");
return -1;
}
DBG_INFO("net init: OK");
uint8_t mac[6];
ch395f_get_mac_addr(mac);
DBG_INFO("MAC: %02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
uint8_t ipinf[20];
ch395f_get_ip_inf(ipinf);
DBG_INFO("IP_INF: IP=%d.%d.%d.%d GW=%d.%d.%d.%d MASK=%d.%d.%d.%d",
ipinf[0], ipinf[1], ipinf[2], ipinf[3],
ipinf[4], ipinf[5], ipinf[6], ipinf[7],
ipinf[8], ipinf[9], ipinf[10], ipinf[11]);
ch395f_set_phy(CH395F_PHY_100M_FULL);
HAL_Delay(3000);
uint8_t phy = ch395f_get_phy_status();
DBG_INFO("PHY: %s",
(phy == CH395F_PHY_100M_FULL) ? "100M FULL" :
(phy == CH395F_PHY_100M_HALF) ? "100M HALF" :
(phy == CH395F_PHY_10M_FULL) ? "10M FULL" :
(phy == CH395F_PHY_10M_HALF) ? "10M HALF" :
(phy == CH395F_PHY_DISCONN) ? "DISCONNECT" : "UNKNOWN");
g_net_ready = 1;
return 0;
}
/*
* 函数功能:网络任务初始化
* 入口参数:无
* 返 回 值0 - 成功,-1 - 失败
* 限定条件CH395F 协议栈已初始化
* 函数说明:创建 TCP Server 监听 Socket注册回调等
*/
int net_task_init(void)
{
DBG_INFO("net_task_init");
return 0;
}
/*
* 函数功能:网络任务主函数
* 入口参数arg - FreeRTOS 传入参数(未使用)
* 返 回 值:无
* 函数说明:启动后先调用 net_stack_bring_up() 初始化 CH395F 协议栈,
* 再进入轮询循环处理网络事件。
* 仅 NET 层测试模式(阶段 6~10会创建本任务
* 硬件层模式下 netTask 不创建CH395F 由 ch395fTestTask 独占。
*/
void net_task_func(void const *arg)
{
(void)arg;
osDelay(2000);
if (net_stack_bring_up() != 0) {
for (;;) {
osDelay(1000);
}
}
DBG_INFO("netTask: started (single-socket mode)");
/* ==================== 主轮询循环 ==================== */
for (;;) {
net_poll();
net_process_messages();
osDelay(1);
}
}

31
App/task/net_task.h Normal file
View File

@@ -0,0 +1,31 @@
/*
* 模块名称Network Application Task
* 模块功能网络协议栈初始化、轮询、TCP/UDP 业务处理
* 适用平台STM32F407ZGTx + CH395F
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
* v1.1 2026-08-24 王建锋 提取 net_stack_bring_up() 供硬件层/NET 层两种测试模式共用
*/
#ifndef __NET_TASK_H
#define __NET_TASK_H
#ifdef __cplusplus
extern "C" {
#endif
/* CH395F 协议栈一次性初始化net_init + MAC 打印 + PHY 配置 + g_net_ready 置位)。
* 返回 0 成功 / -1 失败。NET 层模式由 netTask 调用;硬件层模式(阶段 1~5
* netTask 不创建,由 ch395fTestTask 调用后独占 CH395F。 */
int net_stack_bring_up(void);
int net_task_init(void);
void net_task_func(void const *arg);
#ifdef __cplusplus
}
#endif
#endif /* __NET_TASK_H */

45
App/task/rs485_task.c Normal file
View File

@@ -0,0 +1,45 @@
/*
* 模块名称RS485 Communication Task
* 模块功能RS-485 轮询收发、协议帧解析与响应
* 适用平台STM32F407ZGTx
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#include "rs485_task.h"
#include "cmsis_os.h"
#include "rs485.h"
#define DBG_TAG "[RS485_TASK]"
#include "dbg_log.h"
/*
* 函数功能RS485 任务初始化
* 入口参数:无
* 返 回 值0 - 成功,-1 - 失败
* 限定条件UART 和 GPIO 已由 CubeMX 初始化
* 函数说明:配置 RS-485 通信参数,注册接收回调
*/
int rs485_task_init(void)
{
DBG_INFO("rs485_task_init");
return 0;
}
/*
* 函数功能RS485 任务主函数
* 入口参数arg - FreeRTOS 传入参数(未使用)
* 返 回 值:无
* 限定条件rs485_task_init() 已成功调用
* 函数说明:轮询接收缓冲区并解析协议帧
*/
void rs485_task_func(void const *arg)
{
(void)arg;
for (;;) {
osDelay(100);
}
}

25
App/task/rs485_task.h Normal file
View File

@@ -0,0 +1,25 @@
/*
* 模块名称RS485 Communication Task
* 模块功能RS-485 数据收发与协议解析
* 适用平台STM32F407ZGTx
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#ifndef __RS485_TASK_H
#define __RS485_TASK_H
#ifdef __cplusplus
extern "C" {
#endif
int rs485_task_init(void);
void rs485_task_func(void const *arg);
#ifdef __cplusplus
}
#endif
#endif /* __RS485_TASK_H */

138
App/time_sync.c Normal file
View File

@@ -0,0 +1,138 @@
/*
* 模块名称Time Sync — PC 本地时间推送接收TCP
* 模块功能:在独立任务 timeSyncTask 中以 TCP Server 方式监听端口,接收 PC 推送的本地时间包,
* 调用 sys_clock_set_unix() 更新系统墙钟并写回 SD2506 RTC
* 适用平台STM32F407ZGTx + CH395F
* 作者:王建锋
* 创建日期2026-08-27
* 修改记录:
* v1.0 2026-08-27 王建锋 创建初始版本UDP 推送netTask 内轮询)
* v1.1 2026-08-28 王建锋 改为 TCP Server迁入独立任务 timeSyncTask
* 所有 socket 操作经 net_socket 线程安全 API 走消息队列,
* netTask 仅负责 net_poll/net_process_messages多任务并发安全
*/
#include <string.h>
#include "time_sync.h"
#include "net_socket.h"
#include "sys_clock.h"
#define DBG_TAG "[TIME_SYNC]"
#include "dbg_log.h"
/* 监听端口PC 推送端TCP 客户端)需连到本端口 */
#define TIME_SYNC_PORT 8888
/*
* 函数功能:创建并监听 TCP 时间同步服务端口
* 入口参数:无
* 返回值:监听 Socket 描述符(>=0失败返回 -1
* 限定条件net_init() 已成功;可在非 netTask 上下文调用
* 函数说明net_socket/net_bind/net_listen 均为线程安全 API内部经消息队列由 netTask 串行处理。
* 单连接模式下监听 Socket 在收到连接后会转为数据通道,连接关闭后即被释放,
* 因此每次调用都会新建监听 Socket与 lftpd 同模式),失败自动重试。
*/
static int time_sync_open_listener(void)
{
int retry;
int s = -1;
for (retry = 0; retry < 5; retry++) {
s = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (s < 0) {
DBG_ERROR("time_sync: net_socket FAIL");
vTaskDelay(pdMS_TO_TICKS(500));
continue;
}
struct net_sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = NET_AF_INET;
addr.sin_port = net_htons(TIME_SYNC_PORT);
addr.sin_addr.s_addr = NET_INADDR_ANY;
if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
DBG_ERROR("time_sync: bind port %d FAIL", TIME_SYNC_PORT);
net_close(s);
vTaskDelay(pdMS_TO_TICKS(2000));
continue;
}
if (net_listen(s) != 0) {
DBG_ERROR("time_sync: listen FAIL");
net_close(s);
vTaskDelay(pdMS_TO_TICKS(2000));
continue;
}
DBG_INFO("time_sync: TCP listen on :%d (sock=%d)", TIME_SYNC_PORT, s);
return s;
}
return -1;
}
/*
* 函数功能:处理一个已建立的时间同步连接
* 入口参数conn_sock - 已建立的连接 Socket 描述符(单连接模式下即监听 Socket 自身)
* 返回值:无
* 函数说明:阻塞读取 PC 发来的时间包("TIME"+uint32 大端 本地时间),解析后写回 RTC然后关闭连接
*/
static void time_sync_handle_conn(int conn_sock)
{
uint8_t buf[16];
int len;
len = net_recv(conn_sock, buf, sizeof(buf), 0);
if (len >= 8) {
if (buf[0] == 'T' && buf[1] == 'I' && buf[2] == 'M' && buf[3] == 'E') {
uint32_t epoch = ((uint32_t)buf[4] << 24) |
((uint32_t)buf[5] << 16) |
((uint32_t)buf[6] << 8) |
((uint32_t)buf[7]);
sys_clock_set_unix(epoch);
DBG_INFO("time_sync: wall clock updated (epoch=%lu)", (unsigned long)epoch);
}
}
net_close(conn_sock);
}
/*
* 函数功能:时间同步任务主体(独立 FreeRTOS 任务)
* 入口参数argument - 未使用
* 返回值:无
* 限定条件net_init() 已由 netTask 完成netTask 正在处理消息队列
* 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> 关闭 -> 重建监听。
* 全部 socket 操作经消息队列由 netTask 串行执行,与本任务/其它网络任务并发安全。
*/
void timeSyncTask(void *argument)
{
(void)argument;
/* 等网络栈起来(与 StartFtpTask 的 7s 延迟同风格,确保 netTask 已开始处理消息) */
vTaskDelay(pdMS_TO_TICKS(8000));
for (;;) {
int ls = time_sync_open_listener();
if (ls < 0) {
DBG_ERROR("time_sync: open listener FAIL, retry later");
vTaskDelay(pdMS_TO_TICKS(2000));
continue;
}
/* 等待 PC 连接net_accept 在无连接时立即返回 -1循环重试同一监听 Socket */
int conn = -1;
do {
conn = net_accept(ls, NULL, NULL);
if (conn < 0) {
vTaskDelay(pdMS_TO_TICKS(200));
}
} while (conn < 0);
time_sync_handle_conn(conn);
/* conn单连接模式下即 ls已被 net_close 释放,下次循环重建监听 */
vTaskDelay(pdMS_TO_TICKS(50));
}
}

40
App/time_sync.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* 模块名称Time Sync — PC 本地时间推送接收TCP
* 模块功能:以 TCP Server 方式监听 TIME_SYNC_PORT接收 PC 推送的本地时间包,
* 更新系统墙钟并写回 SD2506 RTC
* 适用平台STM32F407ZGTx + CH395F
* 作者:王建锋
* 创建日期2026-08-27
* 修改记录:
* v1.0 2026-08-27 王建锋 创建初始版本UDP 推送)
* v1.1 2026-08-28 王建锋 改为 TCP Server迁入独立任务 timeSyncTask
*/
#ifndef __TIME_SYNC_H
#define __TIME_SYNC_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 监听端口PC 推送端TCP 客户端)需连到本端口 */
#define TIME_SYNC_PORT 8888
/*
* 函数功能:时间同步任务主体(独立 FreeRTOS 任务)
* 入口参数argument - FreeRTOS 传入参数(未使用)
* 返回值:无
* 限定条件net_init() 已由 netTask 完成;网络消息队列由 netTask 处理
* 函数说明:以 TCP Server 方式监听 TIME_SYNC_PORT接收 PC 推送的本地时间包,
* 经消息队列(由 netTask 串行执行)完成 socket 操作,更新系统墙钟。
* 该任务与 FTP 等其它网络任务并发安全。
*/
void timeSyncTask(void *argument);
#ifdef __cplusplus
}
#endif
#endif /* __TIME_SYNC_H */

122
App/util/crc.c Normal file
View File

@@ -0,0 +1,122 @@
/*
* 模块名称CRC Utility
* 模块功能CRC-16 Modbus (poly=0x8005, init=0xFFFF, refin=true, refout=true, xorout=0x0000)
* CRC-8 (poly=0x07, init=0x00, refin=false, refout=false, xorout=0x00)
* 适用平台:通用嵌入式平台
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#include "crc.h"
static const uint16_t s_crc16_table[256] = {
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040,
};
static const uint8_t s_crc8_table[256] = {
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3,
};
/*
* 函数功能:计算 CRC-16 Modbus
* 入口参数data - 数据指针
* len - 数据长度
* 返 回 值16 位 CRC 值
* 限定条件data 为非空指针
* 函数说明:多项式 0x8005初始值 0xFFFF结果异或 0x0000
*/
uint16_t crc16_modbus(const uint8_t *data, size_t len)
{
uint16_t crc = 0xFFFF;
size_t i;
for (i = 0; i < len; i++) {
crc = (crc >> 8) ^ s_crc16_table[(crc ^ data[i]) & 0xFF];
}
return crc;
}
/*
* 函数功能:计算 CRC-8
* 入口参数data - 数据指针
* len - 数据长度
* 返 回 值8 位 CRC 值
* 限定条件data 为非空指针
* 函数说明:多项式 0x07初始值 0x00
*/
uint8_t crc8(const uint8_t *data, size_t len)
{
uint8_t crc = 0;
size_t i;
for (i = 0; i < len; i++) {
crc = s_crc8_table[crc ^ data[i]];
}
return crc;
}

28
App/util/crc.h Normal file
View File

@@ -0,0 +1,28 @@
/*
* 模块名称CRC Utility
* 模块功能CRC-16 Modbus & CRC-8 查表计算
* 适用平台:通用嵌入式平台
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#ifndef __CRC_H
#define __CRC_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
uint16_t crc16_modbus(const uint8_t *data, size_t len);
uint8_t crc8(const uint8_t *data, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* __CRC_H */

164
App/util/ringbuf.c Normal file
View File

@@ -0,0 +1,164 @@
/*
* 模块名称Ring Buffer
* 模块功能字节环形缓冲区实现head == tail 为空head + 1 == tail 为满
* 适用平台:通用嵌入式平台
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#include "ringbuf.h"
/*
* 函数功能:初始化环形缓冲区
* 入口参数rb - 缓冲区控制块指针
* pool - 存储池指针
* size - 存储池大小(字节)
* 返 回 值:无
* 限定条件rb 和 pool 均为非空指针
* 函数说明size 保留一个空位用于区分空/满状态
*/
void ringbuf_init(ringbuf_t *rb, uint8_t *pool, size_t size)
{
rb->buf = pool;
rb->size = size;
rb->head = 0;
rb->tail = 0;
}
/*
* 函数功能:写入数据到环形缓冲区
* 入口参数rb - 缓冲区控制块指针
* data - 源数据指针
* len - 待写入长度
* 返 回 值:实际写入字节数
* 限定条件rb 和 data 均为非空指针
* 函数说明:空间不足时写入能容纳的最大字节数
*/
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;
cap = rb->size - 1 - ringbuf_avail(rb);
n = (len < cap) ? len : cap;
for (i = 0; i < n; i++) {
rb->buf[rb->head] = data[i];
rb->head = (rb->head + 1) % rb->size;
}
return n;
}
/*
* 函数功能:从环形缓冲区读取数据
* 入口参数rb - 缓冲区控制块指针
* data - 目标数据指针
* len - 期望读取长度
* 返 回 值:实际读取字节数
* 限定条件rb 和 data 均为非空指针
* 函数说明:读取后数据从缓冲区移除
*/
size_t ringbuf_get(ringbuf_t *rb, uint8_t *data, size_t len)
{
size_t n = 0;
size_t i = 0;
n = ringbuf_avail(rb);
n = (n < len) ? n : len;
for (i = 0; i < n; i++) {
data[i] = rb->buf[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
}
return n;
}
/*
* 函数功能:预读取数据(不移除)
* 入口参数rb - 缓冲区控制块指针
* data - 目标数据指针
* len - 期望读取长度
* 返 回 值:实际可读取字节数
* 限定条件rb 和 data 均为非空指针
* 函数说明:与 ringbuf_get 的区别是不移动 tail 指针
*/
size_t ringbuf_peek(const ringbuf_t *rb, uint8_t *data, size_t len)
{
size_t n = 0;
size_t i = 0;
size_t idx = 0;
n = ringbuf_avail(rb);
n = (n < len) ? n : len;
for (i = 0; i < n; i++) {
idx = (rb->tail + i) % rb->size;
data[i] = rb->buf[idx];
}
return n;
}
/*
* 函数功能:获取缓冲区中有效数据长度
* 入口参数rb - 缓冲区控制块指针
* 返 回 值:有效数据字节数
* 限定条件rb 为非空指针
*/
size_t ringbuf_avail(const ringbuf_t *rb)
{
if (rb->head >= rb->tail) {
return rb->head - rb->tail;
}
return rb->size - rb->tail + rb->head;
}
/*
* 函数功能:获取缓冲区剩余空间
* 入口参数rb - 缓冲区控制块指针
* 返 回 值:剩余空间字节数(保留一个空位)
* 限定条件rb 为非空指针
*/
size_t ringbuf_space(const ringbuf_t *rb)
{
return rb->size - 1 - ringbuf_avail(rb);
}
/*
* 函数功能:判断缓冲区是否为空
* 入口参数rb - 缓冲区控制块指针
* 返 回 值1 - 空0 - 非空
* 限定条件rb 为非空指针
*/
int ringbuf_is_empty(const ringbuf_t *rb)
{
return (rb->head == rb->tail) ? 1 : 0;
}
/*
* 函数功能:判断缓冲区是否为满
* 入口参数rb - 缓冲区控制块指针
* 返 回 值1 - 满0 - 非满
* 限定条件rb 为非空指针
*/
int ringbuf_is_full(const ringbuf_t *rb)
{
return (ringbuf_avail(rb) == rb->size - 1) ? 1 : 0;
}
/*
* 函数功能:重置缓冲区
* 入口参数rb - 缓冲区控制块指针
* 返 回 值:无
* 限定条件rb 为非空指针
*/
void ringbuf_reset(ringbuf_t *rb)
{
rb->head = 0;
rb->tail = 0;
}

42
App/util/ringbuf.h Normal file
View File

@@ -0,0 +1,42 @@
/*
* 模块名称Ring Buffer
* 模块功能:线程安全的字节环形缓冲区(单生产者-单消费者模型)
* 适用平台:通用嵌入式平台
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* v1.0 2026-07-19 王建锋 创建初始版本
*/
#ifndef __RINGBUF_H
#define __RINGBUF_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
typedef struct {
uint8_t *buf;
size_t size;
size_t head;
size_t tail;
} ringbuf_t;
void ringbuf_init(ringbuf_t *rb, uint8_t *pool, size_t size);
size_t ringbuf_put(ringbuf_t *rb, const uint8_t *data, size_t len);
size_t ringbuf_get(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 ringbuf_avail(const ringbuf_t *rb);
size_t ringbuf_space(const ringbuf_t *rb);
int ringbuf_is_empty(const ringbuf_t *rb);
int ringbuf_is_full(const ringbuf_t *rb);
void ringbuf_reset(ringbuf_t *rb);
#ifdef __cplusplus
}
#endif
#endif /* __RINGBUF_H */

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,33 @@ extern "C" {
/* 3 字节输入 + 数据输入socket + 长度 + 数据) */ /* 3 字节输入 + 数据输入socket + 长度 + 数据) */
#define CH395F_CMD_WRITE_SEND_BUF_SN 0x39U #define CH395F_CMD_WRITE_SEND_BUF_SN 0x39U
/*
* TCP MSSCH395F_CMD_SET_TCP_MSS 参数)
* 由 net_init 经 ch395f_set_tcp_mss() 下发;此处作为单一真值源,
* DMA 缓冲大小亦由此推导见下。当前选取依据8 Socket 共享 24KB 缓冲时,
* 各 Socket 接收缓冲 = 2×MSS故 MSS 取 1024 为最大值(详见 net_socket.c 分配注释)。
*/
#define CH395F_TCP_MSS 1024U
/* 内部缓冲 RAM 每块字节数(手册 §5.4548 块 × 512B = 24KB */
#define CH395F_RAM_BLOCK_SIZE 512U
/* 单 Socket 缓冲块数由 MSS 推导:接收 = 2×MSS满足手册 §9.2.8 接收 ≥ 2×MSS发送 = 1×MSS向上取整 */
#define CH395F_RECV_BLOCKS ((2U * CH395F_TCP_MSS + CH395F_RAM_BLOCK_SIZE - 1U) / CH395F_RAM_BLOCK_SIZE)
#define CH395F_SEND_BLOCKS ((CH395F_TCP_MSS + CH395F_RAM_BLOCK_SIZE - 1U) / CH395F_RAM_BLOCK_SIZE)
/* 单 Socket 硬件收发缓冲字节数(= 块数 × 块大小)。写入发送 FIFO 的单次长度严禁超过此值,
* 否则会写穿有限的发送缓冲(见 Trap 20。 */
#define CH395F_SEND_BUF_SIZE_BYTES (CH395F_SEND_BLOCKS * CH395F_RAM_BLOCK_SIZE)
#define CH395F_RECV_BUF_SIZE_BYTES (CH395F_RECV_BLOCKS * CH395F_RAM_BLOCK_SIZE)
/*
* SPI DMA 事务限制
* CH395F_SPI_DMA_MAX_PAYLOAD: 单次 DMA 事务最大有效载荷(总缓冲 - 4 字节命令头)
* 接收方向需一次性读空整个 Socket 接收缓冲(= 2×MSS避免 Trap 13 部分读导致数据不可见),
* 故总缓冲 = 2×MSS + 44 为命令头开销),有效载荷 = 2×MSS。
*/
#define CH395F_SPI_DMA_BUF_SIZE (CH395F_TCP_MSS * 2U + 4U)
#define CH395F_SPI_DMA_MAX_PAYLOAD (CH395F_SPI_DMA_BUF_SIZE - 4U)
/* 其他命令 */ /* 其他命令 */
#define CH395F_CMD_SET_TCP_MSS 0x50U #define CH395F_CMD_SET_TCP_MSS 0x50U
#define CH395F_CMD_SET_RECV_BUF 0x52U #define CH395F_CMD_SET_RECV_BUF 0x52U
@@ -520,13 +547,6 @@ uint16_t ch395f_get_recv_len(uint8_t sock);
* 函数声明区 - 中断状态 * 函数声明区 - 中断状态
*/ */
/*
* 函数功能:获取全局中断状态
* 返回值:中断状态字节 uint8_t
* 限定条件:芯片已初始化
*/
uint8_t ch395f_get_glob_int_status(void);
/* /*
* 函数功能获取全局中断状态2 字节版,支持 Socket 0~7 * 函数功能获取全局中断状态2 字节版,支持 Socket 0~7
* 返回值:中断状态 uint16_t低字节=Socket 0~3 + PHY/DHCP高字节=Socket 4~7 + DHCPv6 * 返回值:中断状态 uint16_t低字节=Socket 0~3 + PHY/DHCP高字节=Socket 4~7 + DHCPv6

View File

@@ -1,664 +0,0 @@
/*
* 模块名称CH395F Driver Test Suite
* 模块功能CH395F 驱动各功能模块的测试入口,分阶段组织测试用例
* 适用平台STM32F4 系列CH395F 以太网芯片)
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
* 2026-07-19 Phase 1/2/3 implemented
*/
/* 头文件包含区 */
#include "ch395f_test.h"
#include "ch395f.h"
#include "net_socket.h"
#define DBG_TAG "[CH395T]"
#include "dbg_log.h"
#include <string.h>
#include <stdio.h>
test_stats_t g_test_stats;
/*
* Phase 2 配置默认值
*/
#ifndef PHASE2_REMOTE_IP
#define PHASE2_REMOTE_IP "192.168.1.2"
#endif
#ifndef PHASE2_REMOTE_PORT
#define PHASE2_REMOTE_PORT 8081
#endif
#ifndef PHASE2_LOCAL_PORT
#define PHASE2_LOCAL_PORT 50000
#endif
#ifndef PHASE2_LOOP_COUNT
#define PHASE2_LOOP_COUNT 3
#endif
#ifndef PHASE2_TIMEOUT_MS
#define PHASE2_TIMEOUT_MS 10000
#endif
/*
* Phase 3 配置默认值
*/
#ifndef PHASE3_REMOTE_IP
#define PHASE3_REMOTE_IP "192.168.1.2"
#endif
#ifndef PHASE3_REMOTE_PORT
#define PHASE3_REMOTE_PORT 8082
#endif
#ifndef PHASE3_LOCAL_PORT
#define PHASE3_LOCAL_PORT 60000
#endif
#ifndef PHASE3_ECHO_COUNT
#define PHASE3_ECHO_COUNT 30
#endif
#ifndef PHASE3_TIMEOUT_MS
#define PHASE3_TIMEOUT_MS 30000
#endif
/*
* 恢复 TCP Server 多连接模式的数据 Socket 配置
*/
static void restore_data_sockets(uint16_t port)
{
int i;
for (i = NET_TCP_SERVER_DATA_SOCK_START; i < NET_MAX_SOCKETS; i++)
{
ch395f_set_proto_type((uint8_t)i, CH395F_PROTO_TYPE_TCP);
ch395f_set_sour_port((uint8_t)i, port);
}
DBG_INFO("data socks restored (port %d)", port);
}
/*
* ============================ Phase 1 ============================
*/
void ch395f_phase1_tests(void)
{
uint8_t i;
int ver;
DBG_INFO("=== CH395F Phase 1 Tests ===");
/* Test 1: GET_VERSION */
ver = ch395f_get_version();
TEST_CHECK(ver > 0, "GET_VERSION: 0x%02X (ver=%d)", ver, ver & 0x3F);
/* Test 2: GET_CMD_STATUS */
uint8_t cmd_st = ch395f_get_cmd_status();
TEST_CHECK(cmd_st == CH395F_ERR_SUCCESS,
"GET_CMD_STATUS: 0x%02X (%s)", cmd_st,
cmd_st == CH395F_ERR_SUCCESS ? "SUCCESS" :
cmd_st == CH395F_ERR_BUSY ? "BUSY" : "OTHER");
/* Test 3: GET_IP_INF */
uint8_t ip_info[20];
ch395f_get_ip_inf(ip_info);
DBG_INFO("GET_IP_INF:");
DBG_INFO(" IP: %d.%d.%d.%d", ip_info[0], ip_info[1], ip_info[2], ip_info[3]);
DBG_INFO(" GW: %d.%d.%d.%d", ip_info[4], ip_info[5], ip_info[6], ip_info[7]);
DBG_INFO(" MASK: %d.%d.%d.%d", ip_info[8], ip_info[9], ip_info[10], ip_info[11]);
DBG_INFO(" DNS1: %d.%d.%d.%d", ip_info[12], ip_info[13], ip_info[14], ip_info[15]);
DBG_INFO(" DNS2: %d.%d.%d.%d", ip_info[16], ip_info[17], ip_info[18], ip_info[19]);
TEST_CHECK(ip_info[0]==192 && ip_info[1]==168 && ip_info[2]==1 && ip_info[3]==100,
"IP match");
TEST_CHECK(ip_info[4]==192 && ip_info[5]==168 && ip_info[6]==1 && ip_info[7]==1,
"GW match");
TEST_CHECK(ip_info[8]==255 && ip_info[9]==255 && ip_info[10]==255 && ip_info[11]==0,
"MASK match");
/* Test 4: GINT_STATUS_ALL (2-byte version) */
uint16_t gint_all = ch395f_get_glob_int_status_all();
TEST_CHECK((gint_all & 0xFF) == 0x04 || (gint_all & 0xFF) == 0x00,
"GINT_STATUS_ALL: 0x%04X (lo=0x%02X hi=0x%02X)",
gint_all, gint_all & 0xFF, (gint_all >> 8) & 0xFF);
/* Test 5: GINT_STATUS (1-byte version) — read-clear check */
uint8_t gint_1b = ch395f_get_glob_int_status();
TEST_CHECK(gint_1b == 0x00,
"GINT_STATUS_1byte: 0x%02X (expect 0x00 after read-clear)", gint_1b);
/* Test 6: SOCK_STATUS(0) */
{
uint8_t st[2];
ch395f_get_socket_status(0, st);
TEST_CHECK(st[0] == 0x05 && st[1] == 0x01,
"SOCK_STATUS(0): sock=0x%02X(LISTEN), tcp=0x%02X(LISTEN)", st[0], st[1]);
}
/* Test 7: SET_ARP */
ch395f_set_arp(10, 5);
TEST_CHECK(1, "SET_ARP(period=10*100ms, cnt=5): OK");
/* Test 8: SET_TTL */
ch395f_set_ttl(0, 64);
TEST_CHECK(1, "SET_TTL(sock=0, ttl=64): OK");
/* Test 9: CLEAR_RECV_BUF + SOCK_INT poll */
ch395f_clear_recv_buf(0);
TEST_CHECK(1, "CLEAR_RECV_BUF(sock=0): OK");
for (i = 0; i < 8; i++)
{
uint8_t si = ch395f_get_sock_int_status(i);
TEST_CHECK(si == 0x00, "SOCK_INT(%d): 0x%02X", i, si);
}
TEST_REPORT("Phase 1");
}
/*
* ============================ Phase 2 ============================
*/
void ch395f_phase2_tests(void)
{
uint8_t sock;
uint8_t sock_int;
uint8_t rx_buf[300];
int i;
uint32_t tick;
uint16_t int_st;
int iter_pass;
DBG_INFO("=== CH395F Phase 2 Tests (TCP Client - Direct Driver) ===");
/* 解析目标 IP */
uint8_t ip_arr[4];
{
uint32_t a[4];
sscanf(PHASE2_REMOTE_IP, "%lu.%lu.%lu.%lu", &a[0], &a[1], &a[2], &a[3]);
ip_arr[0] = (uint8_t)a[0];
ip_arr[1] = (uint8_t)a[1];
ip_arr[2] = (uint8_t)a[2];
ip_arr[3] = (uint8_t)a[3];
}
DBG_INFO("Target: %s:%d", PHASE2_REMOTE_IP, PHASE2_REMOTE_PORT);
sock = (uint8_t)(NET_MAX_SOCKETS - 1);
/* 一次性配置 */
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP);
ch395f_set_send_buf(sock, 28, 2);
ch395f_set_recv_buf(sock, 30, 2);
ch395f_set_des_ip(sock, ip_arr);
ch395f_set_des_port(sock, PHASE2_REMOTE_PORT);
for (i = 0; i < PHASE2_LOOP_COUNT; i++)
{
DBG_INFO("--- Iteration %d/%d ---", i + 1, PHASE2_LOOP_COUNT);
iter_pass = 1;
ch395f_set_sour_port(sock, PHASE2_LOCAL_PORT + i);
if (ch395f_open_socket(sock) != CH395F_ERR_SUCCESS) { TEST_CHECK(0, "open socket"); iter_pass = 0; goto phase2_iter_done; }
if (ch395f_tcp_connect(sock) != CH395F_ERR_SUCCESS) { TEST_CHECK(0, "tcp connect"); iter_pass = 0; ch395f_close_socket(sock); goto phase2_iter_done; }
/* 等待 CONNECT */
tick = HAL_GetTick();
while (HAL_GetTick() - tick < PHASE2_TIMEOUT_MS)
{
int_st = ch395f_get_glob_int_status_all();
if (int_st & (1U << (sock + 4)))
{
sock_int = ch395f_get_sock_int_status(sock);
if (sock_int & CH395F_SINT_STAT_CONNECT) break;
}
HAL_Delay(10);
}
if (HAL_GetTick() - tick >= PHASE2_TIMEOUT_MS) { TEST_CHECK(0, "connect timeout"); iter_pass = 0; ch395f_close_socket(sock); goto phase2_iter_done; }
HAL_Delay(100);
/* 发送测试数据 */
{
uint8_t test_buf[64];
const char *msg = "HelloFromCH395F_TCP_Client! This is a 64-byte test message.";
int p, mlen = (int)strlen(msg);
memcpy(test_buf, msg, mlen);
for (p = mlen; p < 64; p++) test_buf[p] = (uint8_t)('0' + (p - mlen) % 10);
ch395f_write_send_buf(sock, test_buf, 64);
}
/* Echo 循环:收数据 → 回发 */
{
int got_echo = 0;
tick = HAL_GetTick();
while (HAL_GetTick() - tick < 10000)
{
uint16_t rlen = ch395f_get_recv_len(sock);
if (rlen > 0)
{
uint16_t read_len = (rlen > sizeof(rx_buf)) ? (uint16_t)sizeof(rx_buf) : rlen;
memset(rx_buf, 0, sizeof(rx_buf));
ch395f_read_recv_buf(sock, rx_buf, read_len);
DBG_INFO("recv: %d bytes [%.30s]", read_len, (char *)rx_buf);
ch395f_write_send_buf(sock, rx_buf, read_len);
got_echo = 1;
break;
}
int_st = ch395f_get_glob_int_status_all();
if (int_st & (1U << (sock + 4)))
{
sock_int = ch395f_get_sock_int_status(sock);
if (sock_int & CH395F_SINT_STAT_DISCONNECT) { DBG_INFO("DISCONNECT"); break; }
if (sock_int & CH395F_SINT_STAT_SOCK_TIMEOUT) { DBG_INFO("SOCK_TIMEOUT"); break; }
}
HAL_Delay(50);
}
if (!got_echo) { TEST_CHECK(0, "echo recv (iter %d)", i + 1); iter_pass = 0; }
}
phase2_iter_done:
if (iter_pass) TEST_CHECK(1, "Iteration %d PASS", i + 1);
/* 关闭 socket */
ch395f_close_socket(sock);
{
uint8_t st[2];
uint32_t wait = HAL_GetTick();
while (HAL_GetTick() - wait < 5000)
{
ch395f_get_socket_status(sock, st);
if (st[0] == CH395F_SOCKET_CLOSED) break;
HAL_Delay(50);
}
DBG_INFO("closed: sock=0x%02X tcp=0x%02X", st[0], st[1]);
}
HAL_Delay(200);
}
restore_data_sockets(8080);
TEST_REPORT("Phase 2");
}
/*
* ============================ Phase 3 ============================
*/
void ch395f_phase3_tests(void)
{
uint8_t sock;
uint8_t rx_buf[300];
uint32_t tick;
int i;
int echo_ok;
DBG_INFO("=== CH395F Phase 3 Tests (UDP) ===");
sock = (uint8_t)(NET_MAX_SOCKETS - 1);
/* UDP Server 模式DesIP=0xFFFFFFFF → 接受任意来源 */
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_UDP);
ch395f_set_send_buf(sock, 28, 2);
ch395f_set_recv_buf(sock, 30, 2);
ch395f_set_sour_port(sock, PHASE3_LOCAL_PORT);
{
uint8_t zero_ip[4] = {0xFF, 0xFF, 0xFF, 0xFF};
ch395f_set_des_ip(sock, zero_ip);
}
ch395f_set_des_port(sock, 0);
TEST_CHECK(ch395f_open_socket(sock) == CH395F_ERR_SUCCESS,
"UDP socket %d opened (port=%d)", sock, PHASE3_LOCAL_PORT);
HAL_Delay(200);
/* Echo 循环:收到 → 解析来源 → 回显 */
for (i = 0; i < PHASE3_ECHO_COUNT; i++)
{
tick = HAL_GetTick();
echo_ok = 0;
while (HAL_GetTick() - tick < PHASE3_TIMEOUT_MS)
{
uint16_t rlen = ch395f_get_recv_len(sock);
if (rlen > 8)
{
memset(rx_buf, 0, sizeof(rx_buf));
ch395f_read_recv_buf(sock, rx_buf, rlen);
uint16_t payload_len = rlen - 8;
uint16_t src_port = (uint16_t)rx_buf[2] | ((uint16_t)rx_buf[3] << 8);
uint8_t *src_ip = &rx_buf[4];
uint8_t *payload = &rx_buf[8];
DBG_INFO("UDP #%d: %dB from %d.%d.%d.%d:%d [%.32s]",
i + 1, payload_len,
src_ip[0], src_ip[1], src_ip[2], src_ip[3], src_port,
payload_len > 0 ? (char *)payload : "");
ch395f_set_des_ip(sock, src_ip);
ch395f_set_des_port(sock, src_port);
ch395f_write_send_buf(sock, payload, payload_len);
{
uint32_t swait = HAL_GetTick();
while (HAL_GetTick() - swait < 2000)
{
uint16_t gs = ch395f_get_glob_int_status_all();
if (gs & (1U << (sock + 4)))
{
if (ch395f_get_sock_int_status(sock) & CH395F_SINT_STAT_SENBUF_FREE)
break;
}
HAL_Delay(10);
}
}
echo_ok = 1;
break;
}
HAL_Delay(50);
}
TEST_CHECK(echo_ok, "UDP echo #%d/%d", i + 1, PHASE3_ECHO_COUNT);
if (!echo_ok) break;
}
ch395f_close_socket(sock);
TEST_CHECK(1, "UDP socket closed");
restore_data_sockets(8080);
TEST_REPORT("Phase 3");
}
/*
* ============================ Phase 4 ============================
* NET 层 TCP Echo在主循环中调用用 net_recv + net_send 回显
*/
#ifdef ENABLE_PHASE4_TESTS
static char s_phase4_buf[2048];
#endif
void ch395f_phase4_tests(void)
{
#ifdef ENABLE_PHASE4_TESTS
for (int i = NET_TCP_SERVER_DATA_SOCK_START; i < NET_MAX_SOCKETS; i++)
{
net_sock_t *sk = net_get_sock(i);
if (sk == NULL || !sk->in_use || sk->state != NET_SOCK_STATE_ESTABLISHED)
{
continue;
}
int n = net_recv(i, s_phase4_buf, sizeof(s_phase4_buf), NET_MSG_DONTWAIT);
if (n > 0)
{
net_send(i, s_phase4_buf, n, 0);
}
}
#endif
}
/*
* ============================ Phase 5 ============================
* NET 层 UDP EchonetTask 中运行,用 net_recvfrom + net_sendto 回显
*/
#ifndef PHASE5_LOCAL_PORT
#define PHASE5_LOCAL_PORT 60000
#endif
#ifdef ENABLE_PHASE5_TESTS
static int s_phase5_sock = -1;
static char s_phase5_buf[2048];
static uint32_t s_phase5_count = 0;
#endif
void ch395f_phase5_init(void)
{
#ifdef ENABLE_PHASE5_TESTS
struct net_sockaddr_in addr;
s_phase5_sock = net_socket(NET_AF_INET, NET_SOCK_DGRAM, 0);
if (s_phase5_sock < 0)
{
DBG_ERROR("Phase5: net_socket failed");
return;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = NET_AF_INET;
addr.sin_port = net_htons(PHASE5_LOCAL_PORT);
if (net_bind(s_phase5_sock, (struct net_sockaddr *)&addr, sizeof(addr)) < 0)
{
DBG_ERROR("Phase5: net_bind failed");
net_close(s_phase5_sock);
s_phase5_sock = -1;
return;
}
s_phase5_count = 0;
DBG_INFO("Phase5: UDP socket %d bound to port %d", s_phase5_sock, PHASE5_LOCAL_PORT);
#endif
}
void ch395f_phase5_tests(void)
{
#ifdef ENABLE_PHASE5_TESTS
if (s_phase5_sock < 0) return;
struct net_sockaddr_in src;
int addrlen = sizeof(src);
int n = net_recvfrom(s_phase5_sock, s_phase5_buf, sizeof(s_phase5_buf),
NET_MSG_DONTWAIT,
(struct net_sockaddr *)&src, &addrlen);
if (n > 0)
{
net_sendto(s_phase5_sock, s_phase5_buf, n, 0,
(struct net_sockaddr *)&src, sizeof(src));
s_phase5_count++;
TEST_CHECK(1, "Phase5: UDP echo #%lu (%d bytes)", s_phase5_count, n);
}
#endif
}
/*
* Phase 6 配置默认值
*/
#ifndef PHASE6_ANNOUNCE_PORT
#define PHASE6_ANNOUNCE_PORT 60001
#endif
#ifndef PHASE6_ANNOUNCE_SOCK
#define PHASE6_ANNOUNCE_SOCK 6
#endif
/*
* ============================ Phase 6 ============================
* DHCP 自动获取 IP 测试
*/
void ch395f_phase6_tests(void)
{
#ifdef ENABLE_PHASE6_TESTS
uint8_t status;
uint8_t ip_info[20];
uint32_t tick;
DBG_INFO("=== CH395F Phase 6 Tests (DHCP) ===");
/* 1. 启用 DHCP */
ch395f_set_dhcp(1);
TEST_CHECK(1, "DHCP enabled, waiting for IP...");
/* 2. 轮询等待 DHCP 完成 */
HAL_Delay(500);
tick = HAL_GetTick();
while (HAL_GetTick() - tick < 30000)
{
uint16_t gint = ch395f_get_glob_int_status_all();
if (gint & CH395F_GINT_STAT_DHCP)
{
DBG_INFO("DHCP interrupt");
}
status = ch395f_get_dhcp_status();
if (status == 0x00)
{
TEST_CHECK(1, "DHCP: SUCCESS");
break;
}
HAL_Delay(500);
}
TEST_CHECK(HAL_GetTick() - tick < 30000,
"DHCP completed within 30s (status=0x%02X)", status);
if (HAL_GetTick() - tick >= 30000)
{
goto phase6_end;
}
/* 3. 读取获取的 IP */
ch395f_get_ip_inf(ip_info);
DBG_INFO(" IP: %d.%d.%d.%d", ip_info[0], ip_info[1], ip_info[2], ip_info[3]);
DBG_INFO(" GW: %d.%d.%d.%d", ip_info[4], ip_info[5], ip_info[6], ip_info[7]);
DBG_INFO(" MASK: %d.%d.%d.%d", ip_info[8], ip_info[9], ip_info[10], ip_info[11]);
DBG_INFO(" DNS1: %d.%d.%d.%d", ip_info[12], ip_info[13], ip_info[14], ip_info[15]);
TEST_CHECK(ip_info[0] != 0 || ip_info[1] != 0 || ip_info[2] != 0 || ip_info[3] != 0,
"IP = %d.%d.%d.%d (non-zero)", ip_info[0], ip_info[1], ip_info[2], ip_info[3]);
/* 4. 广播宣告 + 等待 PC HELLO 确认 */
{
#define PHASE6_HELLO_PORT 60000
uint8_t anno_sock = PHASE6_ANNOUNCE_SOCK;
uint8_t bcast_ip[4] = {0xFF, 0xFF, 0xFF, 0xFF};
uint8_t rx_buf[64];
/* UDP Server 模式:本地端口 60000接收 HELLODesIP=0xFFFFFFFF */
ch395f_set_proto_type(anno_sock, CH395F_PROTO_TYPE_UDP);
ch395f_set_send_buf(anno_sock, 24, 2);
ch395f_set_recv_buf(anno_sock, 26, 2);
ch395f_set_sour_port(anno_sock, PHASE6_HELLO_PORT);
ch395f_set_des_ip(anno_sock, bcast_ip);
ch395f_set_des_port(anno_sock, 0);
if (ch395f_open_socket(anno_sock) == CH395F_ERR_SUCCESS)
{
/* 发送广播宣告(宣告端口 = 60001来源端口 = 60000 */
ch395f_set_des_ip(anno_sock, bcast_ip);
ch395f_set_des_port(anno_sock, PHASE6_ANNOUNCE_PORT);
ch395f_write_send_buf(anno_sock, ip_info, 4);
/* 等待 SENDBUF_FREE手册要求每次 write_send_buf 后必须等待) */
{
uint32_t swait = HAL_GetTick();
while (HAL_GetTick() - swait < 2000)
{
uint16_t gs = ch395f_get_glob_int_status_all();
if (gs & (1U << (anno_sock + 4)))
{
if (ch395f_get_sock_int_status(anno_sock) & CH395F_SINT_STAT_SENBUF_FREE)
break;
}
HAL_Delay(10);
}
}
DBG_INFO("DHCP announcement: IP=%d.%d.%d.%d (port %d->%d)",
ip_info[0], ip_info[1], ip_info[2], ip_info[3],
PHASE6_HELLO_PORT, PHASE6_ANNOUNCE_PORT);
TEST_CHECK(1, "DHCP announcement sent");
/* 重置 Server 模式,等待 PC 发送 HELLO */
ch395f_set_des_ip(anno_sock, bcast_ip);
ch395f_set_des_port(anno_sock, 0);
uint32_t wait = HAL_GetTick();
uint8_t hello_ok = 0;
while (HAL_GetTick() - wait < 5000)
{
/* 读取全局中断(刷新 socket 中断状态) */
ch395f_get_glob_int_status_all();
uint16_t rlen = ch395f_get_recv_len(anno_sock);
if (rlen > 8)
{
memset(rx_buf, 0, sizeof(rx_buf));
ch395f_read_recv_buf(anno_sock, rx_buf, rlen);
uint16_t payload_len = rlen - 8;
uint8_t *payload = &rx_buf[8];
if (payload_len == 5 && memcmp(payload, "HELLO", 5) == 0)
{
uint8_t *src_ip = &rx_buf[4];
uint16_t src_port = (uint16_t)rx_buf[2] | ((uint16_t)rx_buf[3] << 8);
ch395f_set_des_ip(anno_sock, src_ip);
ch395f_set_des_port(anno_sock, src_port);
ch395f_write_send_buf(anno_sock, payload, payload_len);
HAL_Delay(100);
DBG_INFO("HELLO response sent to %d.%d.%d.%d:%d",
src_ip[0], src_ip[1], src_ip[2], src_ip[3], src_port);
TEST_CHECK(1, "HELLO echo OK");
hello_ok = 1;
break;
}
}
HAL_Delay(50);
}
TEST_CHECK(hello_ok, "HELLO echo within 5s");
ch395f_close_socket(anno_sock);
}
else
{
TEST_CHECK(0, "DHCP announcement: open socket failed");
}
}
phase6_end:
TEST_REPORT("Phase 6");
#endif
}
/*
* ============================ Phase 7 ============================
* Select/Poll I/O 多路复用测试 — net_select() + net_recv/send echo
*/
#include "net_select.h"
#ifdef ENABLE_PHASE7_TESTS
static char s_phase7_buf[2048];
#endif
void ch395f_phase7_tests(void)
{
#ifdef ENABLE_PHASE7_TESTS
static uint32_t s_phase7_count = 0;
net_fd_set readfds;
net_timeval tv;
NET_FD_ZERO(&readfds);
for (int i = NET_TCP_SERVER_DATA_SOCK_START; i < NET_MAX_SOCKETS; i++)
{
net_sock_t *sk = net_get_sock(i);
if (sk && sk->in_use && sk->state == NET_SOCK_STATE_ESTABLISHED)
{
NET_FD_SET(i, &readfds);
}
}
tv.tv_sec = 0;
tv.tv_usec = 50000;
int n = net_select(NET_MAX_SOCKETS, &readfds, NULL, NULL, &tv);
if (n <= 0) return;
for (int i = 0; i < NET_MAX_SOCKETS; i++)
{
if (!NET_FD_ISSET(i, &readfds)) continue;
int len = net_recv(i, s_phase7_buf, sizeof(s_phase7_buf), NET_MSG_DONTWAIT);
if (len > 0)
{
net_send(i, s_phase7_buf, len, 0);
s_phase7_count++;
TEST_CHECK(1, "Phase7: select echo #%lu (sock=%d, %d bytes)", s_phase7_count, i, len);
}
}
#endif
}

View File

@@ -1,139 +0,0 @@
/*
* 模块名称CH395F Driver Test Suite
* 模块功能CH395F 驱动各功能模块的测试入口,分阶段组织测试用例,
* 覆盖 SPI 命令路径、协议栈配置、Socket 状态机等
* 适用平台STM32F4 系列CH395F 以太网芯片)
* 作者:王建锋
* 创建日期2026-07-19
* 修改记录:
*
* === 通过标准速查 ===
*
* Phase | 通过条件 | 失败特征
* ---------|-----------------------------------|--------------------------------
* Phase 1 | 9 项全部打印 OK/SUCCESS/YES/0x00 | 任何一项显示 FAIL、ERROR、MISMATCH
* Phase 2 | 3 轮 "PASS" 打印 | 任何一轮打印 "FAIL" 或超时
* Phase 3 | 20/20 回显 + PING 5/5 + 大包 10/10 | 任何回显超时或丢包
* Phase 4 | PC 脚本显示 10/10 sent/received | 断连、数据错乱
* Phase 5 | PC 脚本显示 10/10 sent/received | 断连、数据错乱
* Phase 6 | 日志打印 IP/GW/MASK 非零 IP | DHCP timeout 或 0.0.0.0
* Phase 7 | PC 脚本显示 10/10 sent/received | 断连、数据错乱
* Phase 8 | 70/70 成功率 100% | 任何连接失败或丢包
*/
#ifndef __CH395F_TEST_H
#define __CH395F_TEST_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/*
* 阶段使能开关(取消注释即启用对应阶段)
* 注意Phase 6 (DHCP) 会改变 IP建议独立运行
*/
//#define ENABLE_PHASE1_TESTS /* 寄存器读写 + 中断一致性 */
//#define ENABLE_PHASE2_TESTS /* TCP Client 收发 + 关闭重连 */
//#define ENABLE_PHASE3_TESTS /* UDP Server Echo */
#define ENABLE_PHASE4_TESTS /* NET 层 TCP EchonetTask 中运行) */
//#define ENABLE_PHASE5_TESTS /* NET 层 UDP EchonetTask 中运行) */
//#define ENABLE_PHASE6_TESTS /* DHCP 自动获取 IP会改 IP */
//#define ENABLE_PHASE7_TESTS /* Select/Poll I/O 多路复用netTask 中运行) */
/* 测试统计 */
typedef struct {
uint16_t total;
uint16_t passed;
uint16_t failed;
} test_stats_t;
extern test_stats_t g_test_stats;
#define TEST_CHECK(cond, fmt, ...) do { \
g_test_stats.total++; \
if (cond) { \
g_test_stats.passed++; \
DBG_INFO("[PASS] " fmt, ##__VA_ARGS__); \
} else { \
g_test_stats.failed++; \
DBG_ERROR("[FAIL] " fmt, ##__VA_ARGS__); \
} \
} while(0)
#define TEST_REPORT(name) do { \
DBG_INFO("=== %s: %d/%d PASSED (failed=%d) ===", \
name, g_test_stats.passed, g_test_stats.total, g_test_stats.failed); \
} while(0)
/*
* Phase 1: 底层寄存器测试
* 通过: 9 项全部打印 OK/SUCCESS/YES/0x00
* 失败: 任何一项 FAIL/ERROR/MISMATCH
*/
void ch395f_phase1_tests(void);
/*
* Phase 2: TCP Client 收发 + 关闭重连 ×3 轮
* 通过: 日志打印 3 轮 "PASS"
* 失败: 任何一轮打印 "FAIL" 或超时
* PC 端预启动:
* python test/ch395f_socket_test.py tcp_server --port 8081
*/
void ch395f_phase2_tests(void);
/*
* Phase 3: UDP Server Echo + PING + 大包
* 通过: 20/20 回显 + PING 5/5 + 大包 10/10
* 失败: 任何回显超时或丢包
* PC 端:
* ping 192.168.1.100
* python test/ch395f_socket_test.py udp_client --ip 192.168.1.100 --port 60000
*/
void ch395f_phase3_tests(void);
/*
* Phase 4: NET 层 TCP EchonetTask 中运行)
* 通过: PC 脚本 10/10 sent/received
* 失败: 断连或数据错乱
* PC 端:
* python test/ch395f_socket_test.py tcp_client --ip 192.168.1.100 --port 8080
*/
void ch395f_phase4_tests(void);
/*
* Phase 5: NET 层 UDP EchonetTask 中运行)
* 通过: PC 脚本 10/10 sent/received
* 失败: 断连或数据错乱
* 注init 在 USER CODE BEGIN 2 中调用echo 在 netTask 中运行
* PC 端:
* python test/ch395f_socket_test.py udp_client --ip 192.168.1.100 --port 60000
*/
void ch395f_phase5_init(void);
void ch395f_phase5_tests(void);
/*
* Phase 6: DHCP 自动获取 IP
* 通过: DHCP SUCCESS + IP 非零 + 广播宣告 + HELLO 回显
* 失败: DHCP timeout 或宣告/HELLO 失败
* 注意: 会改变 IP需独立运行
* PC 端:管理员权限
* python test/ch395f_socket_test.py dhcp_server --dhcp-iface 192.168.1.2 --ip 192.168.1.100
*/
void ch395f_phase6_tests(void);
/*
* Phase 7: Select/Poll 测试netTask 中运行)
* 通过: PC 脚本 10/10 sent/received
* 失败: 断连或数据错乱
* PC 端:
* python test/ch395f_socket_test.py tcp_client --ip 192.168.1.100 --port 8080
*/
void ch395f_phase7_tests(void);
#ifdef __cplusplus
}
#endif
#endif /* __CH395F_TEST_H */

View File

@@ -1,51 +0,0 @@
#ifndef __FAL_CFG_H
#define __FAL_CFG_H
/*
* 模块名称FAL 配置
* 模块功能:定义 FAL Flash 设备表和分区表
* 适用平台STM32F407ZGT6
* 作者:王建锋
* 创建日期2026-07-16
* 修改记录:
* 2026-07-16 王建锋 创建初始版本
*/
#ifdef __cplusplus
extern "C" {
#endif
/* FAL 调试开关0-关闭1-开启 */
#define FAL_DEBUG 0
/* 启用分区表配置 */
#define FAL_PART_HAS_TABLE_CFG
/* ======================== Flash 设备表 ======================== */
extern const struct fal_flash_dev gd5f2gq5ue_flash;
#define FAL_FLASH_DEV_TABLE \
{ \
&gd5f2gq5ue_flash, \
}
/* ======================== 分区表 ======================== */
/*
* 分区名称 设备名称 偏移 大小
* KVDB: 64MB @ 0
* TSDB: 64MB @ 64MB
*/
#ifdef FAL_PART_HAS_TABLE_CFG
#define FAL_PART_TABLE \
{ \
{FAL_PART_MAGIC_WORD, "fdb_kvdb1", "gd5f2gq5ue", 0, 64*1024*1024, 0}, \
{FAL_PART_MAGIC_WORD, "fdb_tsdb1", "gd5f2gq5ue", 64*1024*1024, 64*1024*1024, 0}, \
}
#endif
#ifdef __cplusplus
}
#endif
#endif /* __FAL_CFG_H */

View File

@@ -1,85 +0,0 @@
/*
* 模块名称FAL Flash 设备适配
* 模块功能:将 GD5F2GQ5UE 驱动接口适配到 FAL 框架
* 适用平台STM32F407ZGT6
* 作者:王建锋
* 创建日期2026-07-16
* 修改记录:
* 2026-07-16 王建锋 创建初始版本
*/
/* 头文件包含区 */
#include "fal_def.h"
#include "gd5f2gq5ue.h"
/* ======================== FAL 操作函数适配 ======================== */
/*
* 函数功能Flash 设备初始化适配
* 入口参数:无
* 返回值0 - 成功,其他 - 错误码
* 限定条件SPI 和 GPIO 已由 CubeMX 初始化完成
* 函数说明:调用底层驱动的初始化函数
*/
static int gd5f_fal_init(void)
{
return gd5f2gq5ue_init();
}
/*
* 函数功能Flash 读取适配
* 入口参数offset - 起始字节偏移 long
* p_buf - 数据缓冲区 uint8_t*
* size - 读取字节数 size_t
* 返回值0 - 成功,其他 - 错误码
* 限定条件gd5f_fal_init() 已成功调用
* 函数说明:直接转发到底层驱动的读取函数
*/
static int gd5f_fal_read(long offset, uint8_t *p_buf, size_t size)
{
return gd5f2gq5ue_read(offset, p_buf, size);
}
/*
* 函数功能Flash 写入适配
* 入口参数offset - 起始字节偏移 long
* p_buf - 数据缓冲区 const uint8_t*
* size - 写入字节数 size_t
* 返回值0 - 成功,其他 - 错误码
* 限定条件:目标区域已擦除
* 函数说明:直接转发到底层驱动的写入函数
*/
static int gd5f_fal_write(long offset, const uint8_t *p_buf, size_t size)
{
return gd5f2gq5ue_write(offset, p_buf, size);
}
/*
* 函数功能Flash 擦除适配
* 入口参数offset - 起始字节偏移 long
* size - 擦除字节数 size_t
* 返回值0 - 成功,其他 - 错误码
* 限定条件gd5f_fal_init() 已成功调用
* 函数说明:直接转发到底层驱动的擦除函数
*/
static int gd5f_fal_erase(long offset, size_t size)
{
return gd5f2gq5ue_erase(offset, size);
}
/* ======================== FAL Flash 设备定义 ======================== */
/* GD5F2GQ5UE FAL 设备实例总容量256MB块大小128KB */
const struct fal_flash_dev gd5f2gq5ue_flash = {
.name = "gd5f2gq5ue",
.addr = 0,
.len = GD5F_TOTAL_SIZE,
.blk_size = GD5F_BLOCK_SIZE,
.ops = {
.init = gd5f_fal_init,
.read = gd5f_fal_read,
.write = gd5f_fal_write,
.erase = gd5f_fal_erase,
},
.write_gran = 8,
};

View File

@@ -1,40 +0,0 @@
#ifndef __FDB_CFG_H
#define __FDB_CFG_H
/*
* 模块名称FlashDB 配置
* 模块功能:配置 FlashDB 数据库功能开关和参数
* 适用平台STM32F407ZGT6
* 作者:王建锋
* 创建日期2026-07-16
* 修改记录:
* 2026-07-16 王建锋 创建初始版本
*/
#ifdef __cplusplus
extern "C" {
#endif
/* 启用 KVDB键值数据库 */
#define FDB_USING_KVDB
/* KVDB 版本号变化时自动更新到最新默认值(默认关闭) */
/* #define FDB_KV_AUTO_UPDATE */
/* 启用 TSDB时序数据库 */
#define FDB_USING_TSDB
/* 使用 FAL 存储模式(非文件模式) */
#define FDB_USING_FAL_MODE
/* Flash 写入粒度单位bitSTM32F4 字节可编程 = 8 */
#define FDB_WRITE_GRAN 8
/* 调试输出使能 */
/*#define FDB_DEBUG_ENABLE*/
#ifdef __cplusplus
}
#endif
#endif /* __FDB_CFG_H */

View File

@@ -7,11 +7,12 @@
* 修改记录: * 修改记录:
* 2026-07-16 王建锋 创建初始版本 * 2026-07-16 王建锋 创建初始版本
* 2026-07-17 王建锋 切换硬件 SPI修正擦除地址参考 NuttX 驱动 * 2026-07-17 王建锋 切换硬件 SPI修正擦除地址参考 NuttX 驱动
* 2026-08-27 补充函数注释(代码规范 V1.0);修正工厂坏块扫描为首页;增加 BBT 持久化
*/ */
/* 头文件包含区 */ /* 头文件包含区 */
#include "gd5f2gq5ue.h"
#include <string.h> #include <string.h>
#include "gd5f2gq5ue.h"
/* /*
* 调试输出配置 * 调试输出配置
@@ -20,25 +21,72 @@
#include "dbg_log.h" #include "dbg_log.h"
/* 私有宏定义区 */ /* 私有宏定义区 */
#define GD5F_SPI_TIMEOUT 100 #define GD5F_SPI_TIMEOUT 100
#define GD5F_BBT_SIZE (GD5F_TOTAL_BLOCKS / 8)
/* BBT 静态数组1 bit 表示一个块0=好块 1=坏块) */
static uint8_t s_bbt[GD5F_BBT_SIZE];
/* BBT 持久化:保留块池(从 NAND 顶部向下挑选出厂好块,存储 BBT 副本) */
#define GD5F_BBT_HDR_SIZE 16
static uint32_t s_bbt_pool_blocks[GD5F_BBT_POOL_COUNT]; /* BBT 保留池块号列表(出厂好块) */
static uint32_t s_bbt_pool_count = 0; /* 实际可用池块数(<= GD5F_BBT_POOL_COUNT */
static uint32_t s_usable_blocks = GD5F_TOTAL_BLOCKS; /* dhara 可见块数(= 最低池块号) */
static uint32_t s_bbt_write_slot = 0; /* 下一写入槽(轮转指针) */
static uint32_t s_bbt_version = 0; /* 当前持久化 BBT 版本号 */
static uint8_t s_bbt_slot_dead = 0; /* 池块损坏位掩码 */
static uint8_t s_bbt_page_buf[GD5F_PAGE_SIZE]; /* BBT 读写页缓冲(静态,避免占栈) */
static uint8_t s_bbt_best[GD5F_BBT_SIZE]; /* 最高版本位图缓存 */
static const uint8_t s_bbt_magic[4] = { 'G', 'B', 'B', 'T' }; /* BBT 存储魔数 "GBBT" */
/* 外部 SPI 句柄声明 */ /* 外部 SPI 句柄声明 */
extern SPI_HandleTypeDef hspi1; extern SPI_HandleTypeDef hspi1;
/* DMA 完成标志ch395f.c 中的 HAL_SPI_TxRxCpltCallback 会设置此标志) */
volatile uint8_t g_spi1_dma_done;
/* DMA 阈值(字节数超过此值使用 DMA 传输) */
#define GD5F_DMA_THRESHOLD 32
/*
* 函数功能SPI1 TX DMA 完成回调PROGRAM_LOAD 数据发送用)
* 入口参数hspi - 触发回调的 SPI 句柄 SPI_HandleTypeDef*
* 返回值:无
* 限定条件:由 HAL DMA 中断调用
* 函数说明:仅当 SPI1 传输完成时置位 g_spi1_dma_done 标志
*/
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi->Instance == SPI1) {
g_spi1_dma_done = 1U;
}
}
/*
* 函数功能SPI1 RX DMA 完成回调READ_FROM_CACHE 数据接收用)
* 函数说明HAL_SPI_Receive_DMA() 在 2 线 master 模式下
* 内部走 HAL_SPI_TransmitReceive_DMA 但 state=BUSY_RX
* 完成回调是 HAL_SPI_RxCpltCallback 而非 TxRxCpltCallback
*/
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi->Instance == SPI1) {
g_spi1_dma_done = 1U;
}
}
/* ======================== 私有函数声明 ======================== */ /* ======================== 私有函数声明 ======================== */
static int gd5f_wait_busy(uint32_t timeout_ms);
static int gd5f_write_enable(void);
static int gd5f_read_status(uint8_t *p_status);
static int gd5f_page_read(uint32_t page_addr);
static int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
size_t size);
static int gd5f_page_program(uint32_t page_addr, uint16_t column, static int gd5f_page_program(uint32_t page_addr, uint16_t column,
const uint8_t *p_buf, size_t size); const uint8_t *p_buf, size_t size);
static int gd5f_set_feature(uint8_t addr, uint8_t data); static int gd5f_set_feature(uint8_t addr, uint8_t data);
static int gd5f_block_erase(uint32_t block_addr);
/* ======================== 私有函数定义 ======================== */ /* BBT 持久化内部函数 */
static int gd5f_private_bbt_locate_pool(void);
static uint32_t gd5f_private_crc32(const uint8_t *p_buf, uint32_t len);
static int gd5f_private_bbt_find_best(void);
static int gd5f_bbt_load(void);
static int gd5f_bbt_save(void);
/* ======================== SPI 原语定义 ======================== */
/* /*
* 函数功能:等待芯片操作完成(轮询 OIP 位) * 函数功能:等待芯片操作完成(轮询 OIP 位)
@@ -47,8 +95,7 @@ static int gd5f_block_erase(uint32_t block_addr);
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明:循环读取状态寄存器直到 OIP 位清零或超时 * 函数说明:循环读取状态寄存器直到 OIP 位清零或超时
*/ */
static int gd5f_wait_busy(uint32_t timeout_ms) int gd5f_wait_busy(uint32_t timeout_ms) {
{
uint8_t cmd = GD5F_CMD_GET_FEATURE; uint8_t cmd = GD5F_CMD_GET_FEATURE;
uint8_t addr = GD5F_REG_STATUS; uint8_t addr = GD5F_REG_STATUS;
uint8_t status = 0; uint8_t status = 0;
@@ -75,10 +122,8 @@ static int gd5f_wait_busy(uint32_t timeout_ms)
* 入口参数:无 * 入口参数:无
* 返回值0 - 成功 * 返回值0 - 成功
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明CS# 拉低后发 06h 命令再拉高 * 函数说明CS# 拉低后发 06h 命令再拉高 */
*/ int gd5f_write_enable(void) {
static int gd5f_write_enable(void)
{
uint8_t cmd = GD5F_CMD_WRITE_ENABLE; uint8_t cmd = GD5F_CMD_WRITE_ENABLE;
GD5F_CS_LOW(); GD5F_CS_LOW();
@@ -90,13 +135,11 @@ static int gd5f_write_enable(void)
/* /*
* 函数功能:读取状态寄存器 * 函数功能:读取状态寄存器
* 口参数p_status - 状态值输出指针 uint8_t* 不为 NULL * 口参数p_status - 状态值输出指针 uint8_t* 不为 NULL
* 返回值0 - 成功 * 返回值0 - 成功
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明:发 0Fh + C0h 地址后读取1字节状态 * 函数说明:发 0Fh + C0h 地址后读 1 字节状态 */
*/ int gd5f_read_status(uint8_t *p_status) {
static int gd5f_read_status(uint8_t *p_status)
{
uint8_t cmd = GD5F_CMD_GET_FEATURE; uint8_t cmd = GD5F_CMD_GET_FEATURE;
uint8_t addr = GD5F_REG_STATUS; uint8_t addr = GD5F_REG_STATUS;
@@ -111,14 +154,13 @@ static int gd5f_read_status(uint8_t *p_status)
/* /*
* 函数功能:页读取(将数据从存储阵列加载到内部缓存) * 函数功能:页读取(将数据从存储阵列加载到内部缓存)
* 入口参数page_addr - 页地址 uint32_t * 入口参数page_addr - 页地址 uint32_t 0 ~ GD5F_TOTAL_BLOCKS*64-1
* 返回值0 - 成功,其 - 错误 * 返回值0 - 成功,其 - 错误
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明:发 13h + 3字节行地址等待 OIP 清零 * 函数说明:发 13h + 3字节行地址等待 OIP 清零
*/ */
static int gd5f_page_read(uint32_t page_addr) int gd5f_page_read(uint32_t page_addr) {
{ uint8_t cmd[4] = {0};
uint8_t cmd[4];
cmd[0] = GD5F_CMD_PAGE_READ; cmd[0] = GD5F_CMD_PAGE_READ;
cmd[1] = (page_addr >> 16) & 0xFF; cmd[1] = (page_addr >> 16) & 0xFF;
@@ -135,16 +177,14 @@ static int gd5f_page_read(uint32_t page_addr)
/* /*
* 函数功能:从内部缓存读取数据 * 函数功能:从内部缓存读取数据
* 入口参数column - 列地址(页内偏移) uint16_t 0 - 2047 * 入口参数column - 列地址(页内偏移) uint16_t 0 - 2047
* p_buf - 数据输出缓冲区 uint8_t* 不为 NULL * size - 读取字节数 size_t > 0
* size - 读取字节数 size_t > 0 * 出口参数p_buf - 数据输出缓冲区 uint8_t* 不为 NULL
* 返回值0 - 成功 * 返回值0 - 成功
* 限定条件:必须先调用 gd5f_page_read 完成数据加载 * 限定条件:必须先调用 gd5f_page_read 完成数据加载
* 函数说明:发 0Bh + 2字节列地址 +1字节 dummy 后读取数据 * 函数说明:发 0Bh + 2字节列地址 +1字节 dummy 后读取数据 */
*/ int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
static int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, size_t size) {
size_t size) uint8_t cmd[4] = {0};
{
uint8_t cmd[4];
cmd[0] = GD5F_CMD_READ_FROM_CACHE; cmd[0] = GD5F_CMD_READ_FROM_CACHE;
cmd[1] = (column >> 8) & 0xFF; cmd[1] = (column >> 8) & 0xFF;
@@ -153,7 +193,24 @@ static int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
GD5F_CS_LOW(); GD5F_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, GD5F_SPI_TIMEOUT); HAL_SPI_Transmit(&hspi1, cmd, 4, GD5F_SPI_TIMEOUT);
HAL_SPI_Receive(&hspi1, p_buf, size, GD5F_SPI_TIMEOUT);
if (size > GD5F_DMA_THRESHOLD) {
uint32_t tick_start = HAL_GetTick();
g_spi1_dma_done = 0U;
if (HAL_SPI_Receive_DMA(&hspi1, p_buf, (uint16_t)size) != HAL_OK) {
GD5F_CS_HIGH();
return GD5F_ERROR;
}
while (g_spi1_dma_done == 0U) {
if ((HAL_GetTick() - tick_start) >= GD5F_SPI_TIMEOUT) {
HAL_SPI_DMAStop(&hspi1);
GD5F_CS_HIGH();
return GD5F_BUSY_TIMEOUT;
}
}
} else {
HAL_SPI_Receive(&hspi1, p_buf, size, GD5F_SPI_TIMEOUT);
}
GD5F_CS_HIGH(); GD5F_CS_HIGH();
return GD5F_OK; return GD5F_OK;
@@ -163,32 +220,73 @@ static int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
* 函数功能:页编程(将数据写入指定页) * 函数功能:页编程(将数据写入指定页)
* 入口参数page_addr - 页地址 uint32_t * 入口参数page_addr - 页地址 uint32_t
* column - 列地址 uint16_t 0 - 2047 * column - 列地址 uint16_t 0 - 2047
* p_buf - 数据缓冲区 const uint8_t* * p_buf - 数据缓冲区 const uint8_t*
* size - 写入字节数 size_t > 0 * size - 写入字节数 size_t > 0
* 返回值0 - 成功,其 - 错误 * 返回值0 - 成功,其 - 错误 * 限定条件:目标区域已擦除
* 限定条件:目标区域已擦除
* 函数说明1. 写使能 - 02h 加载数据 - 10h 执行编程 - 等待完成 * 函数说明1. 写使能 - 02h 加载数据 - 10h 执行编程 - 等待完成
* 2. 编程完成后检查 P_FAIL 位 * 2. 编程完成后检查 P_FAIL 位 */
*/
static int gd5f_page_program(uint32_t page_addr, uint16_t column, static int gd5f_page_program(uint32_t page_addr, uint16_t column,
const uint8_t *p_buf, size_t size) const uint8_t *p_buf, size_t size) {
{ int ret;
int ret = GD5F_OK;
uint8_t cmd[4];
uint8_t status = 0;
gd5f_write_enable(); gd5f_write_enable();
ret = gd5f_program_load(column, p_buf, size);
if (ret != GD5F_OK) return ret;
return gd5f_program_exec(page_addr);
}
/*
* 函数功能将数据加载到内部缓存PROGRAM LOAD
* 入口参数column - 列地址(页内偏移) uint16_t 0 - 2047
* p_buf - 待写入数据缓冲区 const uint8_t* 不为 NULL
* size - 写入字节数 size_t > 0
* 返回值0 - 成功,其它 - 错误
* 限定条件SPI 已初始化,目标区域已擦除
* 函数说明:发 02h + 2字节列地址后发送数据超过 DMA 阈值走 DMA
*/
int gd5f_program_load(uint16_t column, const uint8_t *p_buf, size_t size) {
uint8_t cmd[3];
cmd[0] = GD5F_CMD_PROGRAM_LOAD; cmd[0] = GD5F_CMD_PROGRAM_LOAD;
cmd[1] = (column >> 8) & 0xFF; cmd[1] = (uint8_t)(column >> 8);
cmd[2] = column & 0xFF; cmd[2] = (uint8_t)(column);
GD5F_CS_LOW(); GD5F_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 3, GD5F_SPI_TIMEOUT); HAL_SPI_Transmit(&hspi1, cmd, 3, GD5F_SPI_TIMEOUT);
HAL_SPI_Transmit(&hspi1, (uint8_t *)p_buf, size,
GD5F_SPI_TIMEOUT); if (size > GD5F_DMA_THRESHOLD) {
uint32_t tick_start = HAL_GetTick();
g_spi1_dma_done = 0U;
if (HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)p_buf, (uint16_t)size)
!= HAL_OK) {
GD5F_CS_HIGH();
return GD5F_ERROR;
}
while (g_spi1_dma_done == 0U) {
if ((HAL_GetTick() - tick_start) >= GD5F_SPI_TIMEOUT) {
HAL_SPI_DMAStop(&hspi1);
GD5F_CS_HIGH();
return GD5F_BUSY_TIMEOUT;
}
}
} else {
HAL_SPI_Transmit(&hspi1, (uint8_t *)p_buf, size, GD5F_SPI_TIMEOUT);
}
GD5F_CS_HIGH(); GD5F_CS_HIGH();
return GD5F_OK;
}
/*
* 函数功能执行页编程PROGRAM EXECUTE
* 入口参数page_addr - 目标页地址 uint32_t 0 ~ GD5F_TOTAL_BLOCKS*64-1
* 返回值0 - 成功,其它 - 错误GD5F_PROGRAM_FAIL 等)
* 限定条件:已先调用 gd5f_program_load 加载数据
* 函数说明:发 10h + 3字节页地址触发编程等待完成后检查 P_FAIL 位
*/
int gd5f_program_exec(uint32_t page_addr) {
int ret;
uint8_t cmd[4];
uint8_t status;
cmd[0] = GD5F_CMD_PROGRAM_EXEC; cmd[0] = GD5F_CMD_PROGRAM_EXEC;
cmd[1] = (page_addr >> 16) & 0xFF; cmd[1] = (page_addr >> 16) & 0xFF;
cmd[2] = (page_addr >> 8) & 0xFF; cmd[2] = (page_addr >> 8) & 0xFF;
@@ -199,28 +297,37 @@ static int gd5f_page_program(uint32_t page_addr, uint16_t column,
GD5F_CS_HIGH(); GD5F_CS_HIGH();
ret = gd5f_wait_busy(1000); ret = gd5f_wait_busy(1000);
if (ret != GD5F_OK) { if (ret != GD5F_OK) return ret;
return ret;
}
gd5f_read_status(&status); gd5f_read_status(&status);
if (status & GD5F_STATUS_P_FAIL) { if (status & GD5F_STATUS_P_FAIL) return GD5F_PROGRAM_FAIL;
return GD5F_PROGRAM_FAIL;
}
return GD5F_OK; return GD5F_OK;
} }
/*
* 函数功能:检查上次读操作的 ECC 状态
* 入口参数:无
* 返回值0 - 无 ECC 错误GD5F_ECC_ERROR - 存在不可纠正 ECC 错误
* 限定条件SPI 已初始化ECC 已使能
* 函数说明:读取状态寄存器 ECCS[5:4] 位0x02 表示不可纠正错误
*/
int gd5f_check_ecc(void) {
uint8_t status;
gd5f_read_status(&status);
uint8_t ecc = (status >> 4) & 0x03;
if (ecc == 0x02) return GD5F_ECC_ERROR;
return GD5F_OK;
}
/* /*
* 函数功能:设置 Feature 寄存器 * 函数功能:设置 Feature 寄存器
* 入口参数addr - 寄存器地址 uint8_t * 入口参数addr - 寄存器地址 uint8_t
* data - 写入数据 uint8_t * data - 写入数据 uint8_t
* 返回值0 - 成功 * 返回值0 - 成功
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明:先写使能,发 1Fh + 地址 + 数据,等待操作完成 * 函数说明:先写使能,发 1Fh + 地址 + 数据,等待操作完成 */
*/ static int gd5f_set_feature(uint8_t addr, uint8_t data) {
static int gd5f_set_feature(uint8_t addr, uint8_t data)
{
uint8_t cmd[3]; uint8_t cmd[3];
gd5f_write_enable(); gd5f_write_enable();
@@ -243,22 +350,20 @@ static int gd5f_set_feature(uint8_t addr, uint8_t data)
* 入口参数block_addr - 块编号 uint32_t 0 - 2047 * 入口参数block_addr - 块编号 uint32_t 0 - 2047
* 返回值0 - 成功,其他 - 错误码 * 返回值0 - 成功,其他 - 错误码
* 限定条件gd5f2gq5ue_init() 已调用 * 限定条件gd5f2gq5ue_init() 已调用
* 函数说明1. 写使能 - D8h + 3字节字节地址 - 等待完成 * 函数说明1. 写使能 - D8h + 3字节地址 - 等待完成
* 2. 擦除完成后检查 E_FAIL * 2. 擦除完成后检查 E_FAIL */
*/ int gd5f_block_erase(uint32_t block_addr) {
static int gd5f_block_erase(uint32_t block_addr)
{
int ret = GD5F_OK; int ret = GD5F_OK;
uint8_t cmd[4]; uint8_t cmd[4];
uint8_t status = 0; uint8_t status = 0;
uint32_t byte_addr = block_addr * GD5F_BLOCK_SIZE; uint32_t page_addr = block_addr * GD5F_PAGES_PER_BLOCK;
gd5f_write_enable(); gd5f_write_enable();
cmd[0] = GD5F_CMD_BLOCK_ERASE; cmd[0] = GD5F_CMD_BLOCK_ERASE;
cmd[1] = (byte_addr >> 16) & 0xFF; cmd[1] = (page_addr >> 16) & 0xFF;
cmd[2] = (byte_addr >> 8) & 0xFF; cmd[2] = (page_addr >> 8) & 0xFF;
cmd[3] = byte_addr & 0xFF; cmd[3] = page_addr & 0xFF;
GD5F_CS_LOW(); GD5F_CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, GD5F_SPI_TIMEOUT); HAL_SPI_Transmit(&hspi1, cmd, 4, GD5F_SPI_TIMEOUT);
@@ -277,6 +382,106 @@ static int gd5f_block_erase(uint32_t block_addr)
return GD5F_OK; return GD5F_OK;
} }
/*
* 函数功能:扫描所有块构建 BBT在 ECC 使能前调用)
* 入口参数:无
* 返回值0 - 成功
* 限定条件SPI 已初始化ECC 尚未使能
* 函数说明读取每个块首页page 0的 spare byte 0
* 若不为 0xFF 则为出厂坏块(符合 datasheet §12.4
*/
static int gd5f_bbt_scan(void) {
uint32_t block;
uint8_t spare[8];
uint32_t bad_count = 0;
memset(s_bbt, 0, GD5F_BBT_SIZE);
for (block = 0; block < GD5F_TOTAL_BLOCKS; block++) {
uint32_t page = block << 6;
gd5f_page_read(page);
gd5f_read_from_cache(GD5F_PAGE_SIZE, spare, 1);
if (spare[0] != 0xFF) {
s_bbt[block >> 3] |= (1 << (block & 7));
bad_count++;
}
}
DBG_INFO("BBT scan: %lu bad blocks found", bad_count);
return GD5F_OK;
}
/*
* 函数功能:清空 RAM 中的 BBT
* 入口参数:无
* 返回值:无
* 限定条件:无
* 函数说明:仅清空内存位图,不影响闪存中的持久化副本
*/
void gd5f2gq5ue_bbt_clear(void) {
memset(s_bbt, 0, GD5F_BBT_SIZE);
}
/*
* 函数功能:重建 BBT重新扫描出厂坏块并持久化覆盖运行时注入的坏块
* 入口参数:无
* 返回值GD5F_OK - 成功,其他 - 错误码
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:临时关闭 ECC 重新扫描每块 page0 spare[0] 的出厂坏块标记,覆盖 RAM BBT
* 再写回持久化 BBT 池(版本 +1掉电安全从而清除 TC-STO-03 等运行时
* 标记的坏块。调用后建议再执行 nand_ftl_format() 清空 dhara map。
*/
int gd5f2gq5ue_bbt_rebuild(void) {
DBG_INFO("Rebuilding BBT from factory scan...");
/* 1. 关闭 ECC 以读取原始出厂坏块标记(与 gd5f2gq5ue_init 的扫描前提一致) */
gd5f_set_feature(0xB0, 0x00);
gd5f_wait_busy(100);
/* 2. 重新扫描出厂坏块(覆盖 RAM BBT剔除运行时注入的坏块 */
gd5f_bbt_scan();
/* 3. 重新使能 ECC */
gd5f_set_feature(0xB0, 0x10);
gd5f_wait_busy(100);
/* 4. 持久化重建后的 BBT覆盖持久化区中的脏副本 */
if (gd5f_bbt_save() != GD5F_OK) {
DBG_ERROR("BBT rebuild: persist failed");
return GD5F_ERROR;
}
DBG_INFO("BBT rebuilt and persisted");
return GD5F_OK;
}
/*
* 函数功能:通过调试串口打印当前 BBT 统计与坏块列表
* 入口参数:无
* 返回值:无
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:最多打印前 32 个坏块块号与偏移
*/
void gd5f2gq5ue_print_bbt(void) {
uint32_t bad_count = 0;
for (uint32_t b = 0; b < GD5F_TOTAL_BLOCKS; b++) {
if ((s_bbt[b >> 3] >> (b & 7)) & 1) bad_count++;
}
DBG_INFO("BBT: %lu/%lu blocks bad", bad_count, GD5F_TOTAL_BLOCKS);
if (bad_count > 0) {
uint32_t shown = 0;
for (uint32_t b = 0; b < GD5F_TOTAL_BLOCKS && shown < 32; b++) {
if ((s_bbt[b >> 3] >> (b & 7)) & 1) {
DBG_INFO(" block %lu (offset 0x%08lX)", b, b * GD5F_BLOCK_SIZE);
shown++;
}
}
if (bad_count > 32) DBG_INFO(" ... and %lu more", bad_count - 32);
}
}
/* ======================== 公共函数定义 ======================== */ /* ======================== 公共函数定义 ======================== */
/* /*
@@ -286,11 +491,12 @@ static int gd5f_block_erase(uint32_t block_addr)
* 限定条件SPI1 和相关 GPIO 已由 CubeMX 初始化完成 * 限定条件SPI1 和相关 GPIO 已由 CubeMX 初始化完成
* 函数说明1. 发送复位命令并等待完成 * 函数说明1. 发送复位命令并等待完成
* 2. 读取芯片 ID 并校验 * 2. 读取芯片 ID 并校验
* 3. 使能内部 ECC (B0h bit4) * 3. 扫描出厂坏块构建 BBTECC 使能前,读每块首页 spare[0]
* 4. 解除所有块保护 (A0h = 0x00) * 4. 使能内部 ECC (B0h bit4)
* 5. 解除所有块保护 (A0h = 0x00)
* 6. 定位保留块池并加载持久化 BBTECC 使能后)
*/ */
int gd5f2gq5ue_init(void) int gd5f2gq5ue_init(void) {
{
int ret = GD5F_OK; int ret = GD5F_OK;
uint8_t mid = 0; uint8_t mid = 0;
uint8_t did = 0; uint8_t did = 0;
@@ -325,6 +531,9 @@ int gd5f2gq5ue_init(void)
return GD5F_ID_MISMATCH; return GD5F_ID_MISMATCH;
} }
DBG_INFO("Scanning bad blocks...");
gd5f_bbt_scan();
DBG_INFO("Enabling ECC..."); DBG_INFO("Enabling ECC...");
gd5f_set_feature(0xB0, 0x10); gd5f_set_feature(0xB0, 0x10);
gd5f_wait_busy(100); gd5f_wait_busy(100);
@@ -333,20 +542,25 @@ int gd5f2gq5ue_init(void)
gd5f_set_feature(GD5F_REG_PROTECT, 0x00); gd5f_set_feature(GD5F_REG_PROTECT, 0x00);
gd5f_wait_busy(100); gd5f_wait_busy(100);
DBG_INFO("Locating BBT pool...");
gd5f_private_bbt_locate_pool();
DBG_INFO("Loading persisted BBT...");
gd5f_bbt_load();
DBG_INFO("NAND init OK"); DBG_INFO("NAND init OK");
return GD5F_OK; return GD5F_OK;
} }
/* /*
* 函数功能:读取芯片 IDMID + DID * 函数功能:读取芯片 IDMID + DID
* 口参数mid - 制造商 ID 输出指针 uint8_t* 不为 NULL * 口参数:p_mid - 制造商 ID 输出指针 uint8_t* 不为 NULL
* did - 设备 ID 输出指针 uint8_t* 不为 NULL * p_did - 设备 ID 输出指针 uint8_t* 不为 NULL
* 返回值0 - 成功 * 返回值0 - 成功
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明:发 9Fh 命令后接收1个 dummy + MID + DID * 函数说明:发 9Fh 命令后接 1 字节 dummy + MID + DID
*/ */
int gd5f2gq5ue_read_id(uint8_t *mid, uint8_t *did) int gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did) {
{
uint8_t cmd = GD5F_CMD_READ_ID; uint8_t cmd = GD5F_CMD_READ_ID;
uint8_t id_buf[3] = {0}; uint8_t id_buf[3] = {0};
@@ -355,8 +569,8 @@ int gd5f2gq5ue_read_id(uint8_t *mid, uint8_t *did)
HAL_SPI_Receive(&hspi1, id_buf, 3, GD5F_SPI_TIMEOUT); HAL_SPI_Receive(&hspi1, id_buf, 3, GD5F_SPI_TIMEOUT);
GD5F_CS_HIGH(); GD5F_CS_HIGH();
*mid = id_buf[1]; *p_mid = id_buf[1];
*did = id_buf[2]; *p_did = id_buf[2];
return GD5F_OK; return GD5F_OK;
} }
@@ -364,16 +578,19 @@ int gd5f2gq5ue_read_id(uint8_t *mid, uint8_t *did)
/* /*
* 函数功能:从 NAND 读取数据(支持跨页) * 函数功能:从 NAND 读取数据(支持跨页)
* 入口参数offset - 起始字节偏移 long 0 ~ 总容量-1 * 入口参数offset - 起始字节偏移 long 0 ~ 总容量-1
* p_buf - 数据缓冲区 uint8_t* 不为 NULL * size - 读取字节数 size_t > 0
* size - 读取字节数 size_t > 0 * 出口参数p_buf - 数据缓冲区 uint8_t* 不为 NULL
* 返回值0 - 成功,其 - 错误 * 返回值0 - 成功,其 - 错误
* 限定条件gd5f2gq5ue_init() 已成功调用 * 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:自动处理跨页读取 * 函数说明:自动处理跨页读取offset+size 越界返回错误
*/ */
int gd5f2gq5ue_read(long offset, uint8_t *p_buf, size_t size) int gd5f2gq5ue_read(long offset, uint8_t *p_buf, size_t size) {
{
int ret = GD5F_OK; int ret = GD5F_OK;
if (offset < 0 || (uint32_t)offset + (uint32_t)size > GD5F_TOTAL_SIZE) {
return GD5F_ERROR;
}
while (size > 0) { while (size > 0) {
uint32_t page_addr = offset / GD5F_PAGE_SIZE; uint32_t page_addr = offset / GD5F_PAGE_SIZE;
uint16_t column = offset % GD5F_PAGE_SIZE; uint16_t column = offset % GD5F_PAGE_SIZE;
@@ -403,17 +620,20 @@ int gd5f2gq5ue_read(long offset, uint8_t *p_buf, size_t size)
/* /*
* 函数功能:向 NAND 写入数据(支持跨页) * 函数功能:向 NAND 写入数据(支持跨页)
* 入口参数offset - 起始字节偏移 long 0 ~ 总容量-1 * 入口参数offset - 起始字节偏移 long 0 ~ 总容量-1
* p_buf - 数据缓冲区 const uint8_t* 不为 NULL * p_buf - 数据缓冲区 const uint8_t* 不为 NULL
* size - 写入字节数 size_t > 0 * size - 写入字节数 size_t > 0
* 返回值0 - 成功,其 - 错误 * 返回值0 - 成功,其 - 错误
* 限定条件gd5f2gq5ue_init() 已成功调用,目标区域已擦除 * 限定条件gd5f2gq5ue_init() 已成功调用,目标区域已擦除
* 函数说明:自动处理跨页写入 * 函数说明:自动处理跨页写入offset+size 越界返回错误
*/ */
int gd5f2gq5ue_write(long offset, const uint8_t *p_buf, size_t size) int gd5f2gq5ue_write(long offset, const uint8_t *p_buf, size_t size) {
{
int ret = GD5F_OK; int ret = GD5F_OK;
if (offset < 0 || (uint32_t)offset + (uint32_t)size > GD5F_TOTAL_SIZE) {
return GD5F_ERROR;
}
while (size > 0) { while (size > 0) {
uint32_t page_addr = offset / GD5F_PAGE_SIZE; uint32_t page_addr = offset / GD5F_PAGE_SIZE;
uint16_t column = offset % GD5F_PAGE_SIZE; uint16_t column = offset % GD5F_PAGE_SIZE;
@@ -438,14 +658,13 @@ int gd5f2gq5ue_write(long offset, const uint8_t *p_buf, size_t size)
/* /*
* 函数功能:擦除块(按块擦除,最小单位 128KB * 函数功能:擦除块(按块擦除,最小单位 128KB
* 入口参数offset - 起始字节偏移 long 必须对齐 * 入口参数offset - 起始字节偏移 long 必须 GD5F_BLOCK_SIZE 对齐
* size - 擦除字节数 size_t 必须块大小整数倍 * size - 擦除字节数 size_t 必须 GD5F_BLOCK_SIZE 整数倍
* 返回值0 - 成功,其 - 错误码 * 返回值0 - 成功,其 - 错误码
* 限定条件gd5f2gq5ue_init() 已成功调用 * 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:擦除操作以块为单位 * 函数说明:循环擦除 [offset, offset+size) 覆盖的每个块,偏移/长度不对齐返回错误
*/ */
int gd5f2gq5ue_erase(long offset, size_t size) int gd5f2gq5ue_erase(long offset, size_t size) {
{
int ret = GD5F_OK; int ret = GD5F_OK;
if (offset % GD5F_BLOCK_SIZE != 0) { if (offset % GD5F_BLOCK_SIZE != 0) {
@@ -472,13 +691,11 @@ int gd5f2gq5ue_erase(long offset, size_t size)
/* /*
* 函数功能:复位芯片 * 函数功能:复位芯片
* 入口参数:无
* 返回值0 - 成功 * 返回值0 - 成功
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明:发 FFh 复位命令后等待 5ms * 函数说明:发 FFh 复位命令后等待 5ms
*/ */
int gd5f2gq5ue_reset(void) int gd5f2gq5ue_reset(void) {
{
uint8_t cmd = GD5F_CMD_RESET; uint8_t cmd = GD5F_CMD_RESET;
GD5F_CS_LOW(); GD5F_CS_LOW();
@@ -489,3 +706,257 @@ int gd5f2gq5ue_reset(void)
return GD5F_OK; return GD5F_OK;
} }
/*
* 函数功能:查询块是否坏块
* 入口参数block - 块编号 uint32_t 0 ~ GD5F_TOTAL_BLOCKS-1
* 返回值0 - 好块1 - 坏块
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:通过 BBT 查询
*/
int gd5f2gq5ue_is_block_bad(uint32_t block) {
if (block >= GD5F_TOTAL_BLOCKS) {
return 1;
}
return (s_bbt[block >> 3] >> (block & 7)) & 1;
}
/*
* 函数功能:标记块为坏块
* 入口参数block - 块编号 uint32_t 0 ~ GD5F_TOTAL_BLOCKS-1
* 返回值:无
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:更新 BBT
*/
void gd5f2gq5ue_mark_block_bad(uint32_t block) {
if (block >= GD5F_TOTAL_BLOCKS) {
return;
}
uint32_t byte = block >> 3;
uint8_t bit = (uint8_t)(1U << (block & 7));
if (s_bbt[byte] & bit) {
return; /* 已是坏块,幂等,无需重复持久化 */
}
s_bbt[byte] |= bit;
gd5f_bbt_save(); /* 持久化到保留块池(带版本号 + CRC */
}
/* ======================== BBT 持久化实现 ======================== */
/*
* 函数功能:从 NAND 顶部向下挑选出厂好块作为 BBT 保留池
* 入口参数:无
* 返回值0 - 成功
* 限定条件gd5f_bbt_scan() 已完成s_bbt 已就绪)
* 函数说明:保留池不交给 dhara 管理;最低池块号以上的块全部排除,
* 并将可用块数 s_usable_blocks 设为最低池块号
*/
static int gd5f_private_bbt_locate_pool(void) {
uint32_t cnt = 0;
uint32_t b = GD5F_TOTAL_BLOCKS;
while (b-- > 0 && cnt < GD5F_BBT_POOL_COUNT) {
if (((s_bbt[b >> 3] >> (b & 7)) & 1) == 0) {
s_bbt_pool_blocks[cnt++] = b;
}
}
s_bbt_pool_count = cnt;
if (cnt > 0) {
s_usable_blocks = s_bbt_pool_blocks[cnt - 1]; /* 最低池块 = dhara 可见上限 */
}
return GD5F_OK;
}
/*
* 函数功能CRC32 计算IEEE 802.3,多项式 0xEDB88320
* 入口参数p_buf - 数据缓冲区 const uint8_t* 不为 NULL
* len - 数据长度 uint32_t > 0
* 返回值32 位 CRC 校验值
* 限定条件:无
* 函数说明采用位级bit-by-bit实现初始值 0xFFFFFFFF
*/
static uint32_t gd5f_private_crc32(const uint8_t *p_buf, uint32_t len) {
uint32_t crc = 0xFFFFFFFFU;
for (uint32_t i = 0; i < len; i++) {
crc ^= p_buf[i];
for (int j = 0; j < 8; j++) {
uint32_t mask = (crc & 1U) ? 0xEDB88320U : 0U;
crc = (crc >> 1) ^ mask;
}
}
return ~crc;
}
/*
* 函数功能:在保留池中找出版本号最高的有效 BBT 副本
* 入口参数:无
* 返回值0 - 成功
* 限定条件gd5f_private_bbt_locate_pool() 已成功执行
* 函数说明:校验 magic / len / CRC结果缓存到 s_bbt_best / s_bbt_version
* 并设置下一轮转写入槽 s_bbt_write_slot
*/
static int gd5f_private_bbt_find_best(void) {
uint32_t best_ver = 0;
int best_slot = -1;
s_bbt_slot_dead = 0;
for (uint32_t i = 0; i < s_bbt_pool_count; i++) {
uint32_t block = s_bbt_pool_blocks[i];
uint32_t page = block * GD5F_PAGES_PER_BLOCK;
uint32_t ver, len, crc;
gd5f_page_read(page);
gd5f_read_from_cache(0, s_bbt_page_buf, GD5F_BBT_HDR_SIZE + GD5F_BBT_SIZE);
if (memcmp(s_bbt_page_buf, s_bbt_magic, 4) != 0) {
continue;
}
memcpy(&ver, s_bbt_page_buf + 4, 4);
memcpy(&len, s_bbt_page_buf + 8, 4);
memcpy(&crc, s_bbt_page_buf + 12, 4);
if (len != GD5F_BBT_SIZE) {
continue;
}
if (gd5f_private_crc32(s_bbt_page_buf + GD5F_BBT_HDR_SIZE, GD5F_BBT_SIZE) != crc) {
continue;
}
if (best_slot < 0 || (int32_t)ver > (int32_t)best_ver) {
best_ver = ver;
best_slot = (int)i;
memcpy(s_bbt_best, s_bbt_page_buf + GD5F_BBT_HDR_SIZE, GD5F_BBT_SIZE);
}
}
if (best_slot >= 0) {
s_bbt_version = best_ver;
s_bbt_write_slot = (uint32_t)(best_slot + 1) % s_bbt_pool_count;
} else {
s_bbt_write_slot = 0;
}
return GD5F_OK;
}
/*
* 函数功能:加载持久化 BBTOR 进 RAM BBT
* 入口参数:无
* 返回值0 - 成功
* 限定条件gd5f_private_bbt_locate_pool() 已成功执行
* 函数说明:工厂扫描已填好块,持久化副本补充运行期坏块
*/
static int gd5f_bbt_load(void) {
gd5f_private_bbt_find_best();
if (s_bbt_pool_count == 0) {
return GD5F_OK;
}
for (uint32_t i = 0; i < GD5F_BBT_SIZE; i++) {
s_bbt[i] |= s_bbt_best[i];
}
return GD5F_OK;
}
/*
* 函数功能:将当前 RAM BBT 持久化到保留池(轮转写入,掉电安全)
* 入口参数:无
* 返回值0 - 成功GD5F_ERROR - 写入失败
* 限定条件gd5f_private_bbt_locate_pool() 已成功执行
* 函数说明:版本号自增;先擦除目标块再编程;损坏块标记后跳到下一槽
*/
static int gd5f_bbt_save(void) {
if (s_bbt_pool_count == 0) {
return GD5F_OK;
}
uint32_t slot = s_bbt_write_slot;
while (s_bbt_slot_dead & (1U << slot)) {
slot = (slot + 1) % s_bbt_pool_count;
if (slot == s_bbt_write_slot) {
break; /* 所有池块均损坏 */
}
}
uint32_t block = s_bbt_pool_blocks[slot];
uint32_t page = block * GD5F_PAGES_PER_BLOCK;
uint32_t ver = s_bbt_version + 1;
uint32_t len = GD5F_BBT_SIZE;
uint32_t crc = gd5f_private_crc32(s_bbt, GD5F_BBT_SIZE);
if (gd5f_block_erase(block) != GD5F_OK) {
s_bbt_slot_dead |= (1U << slot);
s_bbt_write_slot = (slot + 1) % s_bbt_pool_count;
return GD5F_ERROR;
}
memcpy(s_bbt_page_buf, s_bbt_magic, 4);
memcpy(s_bbt_page_buf + 4, &ver, 4);
memcpy(s_bbt_page_buf + 8, &len, 4);
memcpy(s_bbt_page_buf + 12, &crc, 4);
memcpy(s_bbt_page_buf + GD5F_BBT_HDR_SIZE, s_bbt, GD5F_BBT_SIZE);
gd5f_write_enable();
gd5f_program_load(0, s_bbt_page_buf, GD5F_BBT_HDR_SIZE + GD5F_BBT_SIZE);
gd5f_program_exec(page);
gd5f_wait_busy(100);
s_bbt_version = ver;
s_bbt_write_slot = (slot + 1) % s_bbt_pool_count;
return GD5F_OK;
}
/*
* 函数功能:获取 dhara 可用块数(= 最低保留池块号)
* 入口参数:无
* 返回值dhara 可见的块数量(= 保留块池起始块号)
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:保留块池用于存储持久化 BBT不交给 dhara 管理
*/
uint32_t gd5f_get_usable_blocks(void) {
return s_usable_blocks;
}
/*
* 函数功能:从闪存读取当前生效的 BBT 位图(测试/调试用)
* 入口参数:无
* 出口参数p_bbt - 位图输出缓冲 uint8_t* 不为 NULL长度 >= GD5F_BBT_SIZE
* p_version - 版本号输出 uint32_t* 不为 NULL
* 返回值0 - 成功GD5F_ERROR - 参数非法
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:返回闪存中版本号最高的有效 BBT 拷贝(供测试验证持久化)
*/
int32_t gd5f_bbt_dump_flash(uint8_t *p_bbt, uint32_t *p_version) {
if (p_bbt == NULL || p_version == NULL) {
return GD5F_ERROR;
}
gd5f_private_bbt_find_best();
memcpy(p_bbt, s_bbt_best, GD5F_BBT_SIZE);
*p_version = s_bbt_version;
return GD5F_OK;
}
/*
* 函数功能:擦除保留块池(撤销持久化 BBT回到仅工厂扫描态
* 入口参数:无
* 返回值0 - 成功
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:仅供测试/出厂重置;调用方需自行还原 RAM BBT
*/
int32_t gd5f_bbt_wipe_pool(void) {
if (s_bbt_pool_count == 0) {
return GD5F_OK;
}
for (uint32_t i = 0; i < s_bbt_pool_count; i++) {
gd5f_block_erase(s_bbt_pool_blocks[i]);
}
s_bbt_write_slot = 0;
s_bbt_version = 0;
return GD5F_OK;
}
/*
* 函数功能:从保留块池重新加载持久化 BBT 到 RAM测试/调试用)
* 入口参数:无
* 返回值0 - 成功
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:将闪存中版本号最高的有效副本 OR 进 RAM BBT
*/
int32_t gd5f_bbt_reload(void) {
return gd5f_bbt_load();
}

View File

@@ -17,6 +17,7 @@ extern "C" {
#endif #endif
#include "main.h" #include "main.h"
#include <stdint.h>
/* ======================== 宏定义 ======================== */ /* ======================== 宏定义 ======================== */
@@ -60,6 +61,9 @@ extern "C" {
#define GD5F_TOTAL_BLOCKS 2048 #define GD5F_TOTAL_BLOCKS 2048
#define GD5F_TOTAL_SIZE (GD5F_TOTAL_BLOCKS * GD5F_BLOCK_SIZE) #define GD5F_TOTAL_SIZE (GD5F_TOTAL_BLOCKS * GD5F_BLOCK_SIZE)
/* 坏块表持久化:保留块池数量(从 NAND 顶部向下挑选出厂好块存储 BBT */
#define GD5F_BBT_POOL_COUNT 4
/* 制造商 ID 和设备 ID */ /* 制造商 ID 和设备 ID */
#define GD5F_MANUFACTURER_ID 0xC8 #define GD5F_MANUFACTURER_ID 0xC8
#define GD5F_DEVICE_ID 0x52 #define GD5F_DEVICE_ID 0x52
@@ -91,7 +95,7 @@ extern "C" {
* 3. 使能内部 ECC (B0h bit4) * 3. 使能内部 ECC (B0h bit4)
* 4. 解除所有块保护 (A0h = 0x00) * 4. 解除所有块保护 (A0h = 0x00)
*/ */
int gd5f2gq5ue_init(void); int32_t gd5f2gq5ue_init(void);
/* /*
* 函数功能:读取芯片 IDMID + DID * 函数功能:读取芯片 IDMID + DID
@@ -101,13 +105,13 @@ int gd5f2gq5ue_init(void);
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明:发送 9Fh 命令后接收1个 dummy + MID + DID * 函数说明:发送 9Fh 命令后接收1个 dummy + MID + DID
*/ */
int gd5f2gq5ue_read_id(uint8_t *mid, uint8_t *did); int32_t gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did);
/* /*
* 函数功能:从 NAND 读取数据(支持跨页) * 函数功能:从 NAND 读取数据(支持跨页)
* 入口参数offset - 起始字节偏移 long 0 - GD5F_TOTAL_SIZE-1 * 入口参数offset - 起始字节偏移 int32_t 0 - GD5F_TOTAL_SIZE-1
* buf - 数据缓冲区 uint8_t* 不为 NULL * buf - 数据缓冲区 uint8_t* 不为 NULL
* size - 读取字节数 size_t > 0 * size - 读取字节数 uint32_t > 0
* 返回值0 - 成功,其他 - 错误码 * 返回值0 - 成功,其他 - 错误码
* 限定条件gd5f2gq5ue_init() 已成功调用 * 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:自动处理跨页读取,每次读取不超过当前页剩余空间 * 函数说明:自动处理跨页读取,每次读取不超过当前页剩余空间
@@ -116,9 +120,9 @@ int gd5f2gq5ue_read(long offset, uint8_t *buf, size_t size);
/* /*
* 函数功能:向 NAND 写入数据(支持跨页) * 函数功能:向 NAND 写入数据(支持跨页)
* 入口参数offset - 起始字节偏移 long 0 - GD5F_TOTAL_SIZE-1 * 入口参数offset - 起始字节偏移 int32_t 0 - GD5F_TOTAL_SIZE-1
* buf - 数据缓冲区 uint8_t* 不为 NULL * buf - 数据缓冲区 uint8_t* 不为 NULL
* size - 写入字节数 size_t > 0 * size - 写入字节数 uint32_t > 0
* 返回值0 - 成功,其他 - 错误码 * 返回值0 - 成功,其他 - 错误码
* 限定条件gd5f2gq5ue_init() 已成功调用,目标区域已擦除 * 限定条件gd5f2gq5ue_init() 已成功调用,目标区域已擦除
* 函数说明:自动处理跨页写入,每次写入不超过当前页剩余空间 * 函数说明:自动处理跨页写入,每次写入不超过当前页剩余空间
@@ -127,8 +131,8 @@ int gd5f2gq5ue_write(long offset, const uint8_t *buf, size_t size);
/* /*
* 函数功能:擦除块(按块擦除,最小单位 128KB * 函数功能:擦除块(按块擦除,最小单位 128KB
* 入口参数offset - 起始字节偏移 long 必须 GD5F_BLOCK_SIZE 对齐 * 入口参数offset - 起始字节偏移 int32_t 必须 GD5F_BLOCK_SIZE 对齐
* size - 擦除字节数 size_t 必须 GD5F_BLOCK_SIZE 整数倍 * size - 擦除字节数 uint32_t 必须 GD5F_BLOCK_SIZE 整数倍
* 返回值0 - 成功,其他 - 错误码 * 返回值0 - 成功,其他 - 错误码
* 限定条件gd5f2gq5ue_init() 已成功调用 * 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明擦除操作以块为单位offset 和 size 必须块对齐 * 函数说明擦除操作以块为单位offset 和 size 必须块对齐
@@ -142,7 +146,90 @@ int gd5f2gq5ue_erase(long offset, size_t size);
* 限定条件SPI 已初始化 * 限定条件SPI 已初始化
* 函数说明:发送 FFh 复位命令后等待 5ms * 函数说明:发送 FFh 复位命令后等待 5ms
*/ */
int gd5f2gq5ue_reset(void); int32_t gd5f2gq5ue_reset(void);
/*
* 函数功能:查询块是否坏块
* 入口参数block - 块编号 uint32_t 0 ~ GD5F_TOTAL_BLOCKS-1
* 返回值0 - 好块1 - 坏块
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:通过 BBTBad Block Table查询BBT 在 init 阶段 ECC 使能前扫描构建
*/
int32_t gd5f2gq5ue_is_block_bad(uint32_t block);
/*
* 函数功能:标记块为坏块
* 入口参数block - 块编号 uint32_t 0 ~ GD5F_TOTAL_BLOCKS-1
* 返回值:无
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:更新 RAM 中的 BBT并持久化到保留块池带版本号 + CRC
* 轮转写入,掉电安全);仅在管理区内的块才纳入持久化 BBT
*/
void gd5f2gq5ue_mark_block_bad(uint32_t block);
void gd5f2gq5ue_bbt_clear(void);
void gd5f2gq5ue_print_bbt(void);
/*
* 函数功能:重建 BBT重新扫描出厂坏块并持久化覆盖运行时注入的坏块
* 入口参数:无
* 返回值GD5F_OK - 成功,其他 - 错误码
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:用于测试(如 TC-STO-03 注入坏块后)或出厂重置后恢复。
* 临时关闭 ECC 重新扫描每块 page0 spare[0] 的出厂坏块标记,覆盖 RAM BBT
* 再写回持久化 BBT 池(版本 +1掉电安全从而清除运行时 mark 的坏块。
* 调用后建议再执行 nand_ftl_format() 清空 dhara map。
*/
int gd5f2gq5ue_bbt_rebuild(void);
/*
* 函数功能:获取 dhara 可用块数(总块数减去保留块池)
* 入口参数:无
* 返回值dhara 可见的块数量(= 保留块池起始块号)
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:保留块池用于存储持久化 BBT不交给 dhara 管理
*/
uint32_t gd5f_get_usable_blocks(void);
/*
* 函数功能:从闪存读取当前生效的 BBT 位图(测试/调试用)
* 入口参数p_bbt - 位图输出缓冲 uint8_t* 不为 NULL长度 >= GD5F_BBT_SIZE
* p_version - 版本号输出 uint32_t* 不为 NULL
* 返回值0 - 成功,其他 - 错误码
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:返回闪存中版本号最高的有效 BBT 拷贝(供测试验证持久化)
*/
int32_t gd5f_bbt_dump_flash(uint8_t *p_bbt, uint32_t *p_version);
/*
* 函数功能:擦除保留块池(撤销持久化 BBT回到仅工厂扫描态
* 入口参数:无
* 返回值0 - 成功
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:仅供测试/出厂重置使用;擦除后下次启动 load 找不到副本,
* 仅保留 RAM 中工厂扫描结果(调用方需自行还原 RAM BBT
*/
int32_t gd5f_bbt_wipe_pool(void);
/*
* 函数功能:从保留块池重新加载持久化 BBT 到 RAM测试/调试用)
* 入口参数:无
* 返回值0 - 成功
* 限定条件gd5f2gq5ue_init() 已成功调用
* 函数说明:将闪存中版本号最高的有效副本 OR 进 RAM BBT
*/
int32_t gd5f_bbt_reload(void);
/* ==================== SPI 原语FTL 共享) ==================== */
int32_t gd5f_wait_busy(uint32_t timeout_ms);
int32_t gd5f_write_enable(void);
int32_t gd5f_read_status(uint8_t *p_status);
int32_t gd5f_page_read(uint32_t page_addr);
int32_t gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, uint32_t size);
int32_t gd5f_program_load(uint16_t column, const uint8_t *p_buf, uint32_t size);
int32_t gd5f_program_exec(uint32_t page_addr);
int32_t gd5f_block_erase(uint32_t block_addr);
int32_t gd5f_check_ecc(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -0,0 +1,403 @@
/*
* 模块名称NAND FTL 适配层dhara + FatFS diskio 胶水)
* 模块功能:实现 dhara NAND HAL 和 FatFS 磁盘 I/O连接 GD5F2GQ5UE 底层驱动
* 适用平台STM32F407ZGT6
* 作者:王建锋
* 创建日期2026-07-20
* 修改记录:
* 2026-07-20 王建锋 创建初始版本
*/
/* 头文件包含区 */
#include <string.h>
#include "gd5f2gq5ue.h"
#include "ff.h"
#include "diskio.h"
#include "nand.h"
#include "map.h"
/* FTL 分区起始地址(块 0偏移 0占用全部 256MB */
#define FTL_FATFS_OFFSET 0U
/* 调试输出配置 */
#define DBG_TAG "[NAND_FTL]"
#include "dbg_log.h"
/* 私有宏定义区 */
#define FTL_PAGE_SIZE GD5F_PAGE_SIZE
#define FTL_SECTORS_PER_PAGE (FTL_PAGE_SIZE / 512)
#define FTL_START_BLOCK (FTL_FATFS_OFFSET / GD5F_BLOCK_SIZE)
#define FTL_NUM_BLOCKS (GD5F_TOTAL_BLOCKS - FTL_START_BLOCK)
#define FTL_START_PAGE (FTL_START_BLOCK * GD5F_PAGES_PER_BLOCK)
/* dhara 逻辑地址到物理地址转换 */
static inline uint32_t ftl_phy_block(dhara_block_t b) { return b + FTL_START_BLOCK; }
static inline uint32_t ftl_phy_page(dhara_page_t p) { return p + FTL_START_PAGE; }
/* ======================== 私有变量 ======================== */
/* dhara 核心数据结构 */
static struct dhara_nand s_nand;
static struct dhara_map s_map;
static uint8_t s_page_buf[FTL_PAGE_SIZE]; /* dhara 内部页缓冲区 */
/* 页面缓存FatFS 读写缓存) */
static uint8_t s_cache_buf[FTL_PAGE_SIZE];
static dhara_sector_t s_cached_lpn;
static uint8_t s_cache_dirty;
/* 拷贝临时缓冲区 */
static uint8_t s_copy_buf[FTL_PAGE_SIZE];
/* 初始化状态 */
static uint8_t s_initialized;
/* ======================== 私有函数声明 ======================== */
static int ftl_flush_cache(void);
static int ftl_read_page(dhara_sector_t lpn);
/* SPI 原语从 gd5f2gq5ue.h 获取 */
/* ======================== 页面缓存管理 ======================== */
static int ftl_flush_cache(void) {
if (!s_cache_dirty) {
return GD5F_OK;
}
dhara_error_t err;
if (dhara_map_write(&s_map, s_cached_lpn, s_cache_buf, &err) < 0) {
DBG_ERROR("Cache flush failed: LPN=%lu err=%d", s_cached_lpn, err);
return GD5F_ERROR;
}
s_cache_dirty = 0;
return GD5F_OK;
}
static int ftl_read_page(dhara_sector_t lpn) {
dhara_error_t err;
if (dhara_map_read(&s_map, lpn, s_cache_buf, &err) < 0) {
DBG_ERROR("Page read failed: LPN=%lu err=%d", lpn, err);
return GD5F_ERROR;
}
s_cached_lpn = lpn;
s_cache_dirty = 0;
return GD5F_OK;
}
/* ======================== dhara NAND HAL ======================== */
int dhara_nand_is_bad(const struct dhara_nand *n, dhara_block_t b) {
(void)n;
return gd5f2gq5ue_is_block_bad(ftl_phy_block(b));
}
void dhara_nand_mark_bad(const struct dhara_nand *n, dhara_block_t b) {
(void)n;
gd5f2gq5ue_mark_block_bad(ftl_phy_block(b));
}
int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b, dhara_error_t *err) {
(void)n;
int ret = gd5f_block_erase(ftl_phy_block(b));
if (ret == GD5F_ERASE_FAIL || ret != GD5F_OK) {
dhara_nand_mark_bad(n, b);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
return -1;
}
return 0;
}
int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p, const uint8_t *data, dhara_error_t *err) {
int ret;
uint32_t phy_pg = ftl_phy_page(p);
gd5f_write_enable();
ret = gd5f_program_load(0, data, FTL_PAGE_SIZE);
if (ret != GD5F_OK) {
dhara_nand_mark_bad(n, p >> n->log2_ppb);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
return -1;
}
ret = gd5f_program_exec(phy_pg);
if (ret == GD5F_PROGRAM_FAIL || ret != GD5F_OK) {
dhara_nand_mark_bad(n, p >> n->log2_ppb);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
return -1;
}
return 0;
}
int dhara_nand_is_free(const struct dhara_nand *n, dhara_page_t p) {
(void)n;
uint32_t i;
uint8_t buf[64];
uint32_t phy_pg = ftl_phy_page(p);
if (gd5f_page_read(phy_pg) != GD5F_OK) {
return 0;
}
gd5f_read_from_cache(0, buf, sizeof(buf));
for (i = 0; i < sizeof(buf); i++) {
if (buf[i] != 0xFF) {
return 0;
}
}
return 1;
}
int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p, size_t offset, size_t length, uint8_t *data, dhara_error_t *err) {
(void)n;
uint32_t phy_pg = ftl_phy_page(p);
if (gd5f_page_read(phy_pg) != GD5F_OK) {
dhara_set_error(err, DHARA_E_ECC);
return -1;
}
if (gd5f_check_ecc() != GD5F_OK) {
dhara_set_error(err, DHARA_E_ECC);
return -1;
}
gd5f_read_from_cache((uint16_t)offset, data, length);
return 0;
}
int dhara_nand_copy(const struct dhara_nand *n, dhara_page_t src, dhara_page_t dst, dhara_error_t *err) {
(void)n;
if (dhara_nand_read(n, src, 0, FTL_PAGE_SIZE, s_copy_buf, err) < 0) {
return -1;
}
if (dhara_nand_prog(n, dst, s_copy_buf, err) < 0) {
return -1;
}
return 0;
}
/* ======================== FatFS 磁盘 I/O ======================== */
DSTATUS disk_initialize(BYTE pdrv) {
if (pdrv != 0) {
return STA_NOINIT;
}
if (s_initialized) {
return 0;
}
DBG_INFO("Initializing NAND FTL...");
s_nand.log2_page_size = 11;
s_nand.log2_ppb = 6;
/* dhara 可见块数 = 总块数 - 保留块池BBT 存储区不交给 dhara避免被擦写 */
s_nand.num_blocks = gd5f_get_usable_blocks();
dhara_map_init(&s_map, &s_nand, s_page_buf, 4);
memset(s_cache_buf, 0, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)-1; /* -1 表示缓存未加载LPN 0 合法,不能用 0 作哨兵 */
s_cache_dirty = 0;
{
dhara_error_t err;
if (dhara_map_resume(&s_map, &err) < 0) {
DBG_INFO("No valid map found, creating fresh (err=%d)", err);
dhara_map_clear(&s_map);
} else {
DBG_INFO("Map resumed: %lu/%lu sectors used",
dhara_map_size(&s_map), dhara_map_capacity(&s_map));
}
}
s_initialized = 1;
DBG_INFO("NAND FTL ready (capacity: %lu pages = %lu MB)",
dhara_map_capacity(&s_map),
(dhara_map_capacity(&s_map) * FTL_PAGE_SIZE) / (1024 * 1024));
return 0;
}
int nand_ftl_init(void)
{
DSTATUS sta = disk_initialize(0);
return (sta == 0) ? 0 : -1;
}
void nand_ftl_deinit(void)
{
/* 复位 FTL 状态,使下次 nand_ftl_init() 真正重新走 dhara_map_resume
用于测试"掉电恢复"与"坏块注入后重建"场景(模拟设备重启) */
s_initialized = 0;
s_cached_lpn = (dhara_sector_t)-1;
s_cache_dirty = 0;
}
DSTATUS disk_status(BYTE pdrv) {
if (pdrv != 0) {
return STA_NOINIT;
}
if (!s_initialized) {
return STA_NOINIT;
}
return 0;
}
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) {
if (pdrv != 0 || !buff) {
return RES_PARERR;
}
if (!s_initialized && disk_initialize(pdrv) != 0) {
return RES_NOTRDY;
}
while (count > 0) {
dhara_sector_t lpn = (dhara_sector_t)(sector / FTL_SECTORS_PER_PAGE);
uint32_t offset = (uint32_t)(sector % FTL_SECTORS_PER_PAGE) * 512;
uint32_t batch = FTL_SECTORS_PER_PAGE - (sector % FTL_SECTORS_PER_PAGE);
if (batch > count) {
batch = count;
}
if (lpn != s_cached_lpn) {
if (ftl_flush_cache() != GD5F_OK) {
return RES_ERROR;
}
if (ftl_read_page(lpn) != GD5F_OK) {
return RES_ERROR;
}
}
memcpy(buff, s_cache_buf + offset, batch * 512);
buff += batch * 512;
sector += batch;
count -= (UINT)batch;
}
return RES_OK;
}
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) {
if (pdrv != 0 || !buff) {
return RES_PARERR;
}
if (!s_initialized && disk_initialize(pdrv) != 0) {
return RES_NOTRDY;
}
while (count > 0) {
dhara_sector_t lpn = (dhara_sector_t)(sector / FTL_SECTORS_PER_PAGE);
uint32_t offset = (uint32_t)(sector % FTL_SECTORS_PER_PAGE) * 512;
uint32_t batch = FTL_SECTORS_PER_PAGE - (sector % FTL_SECTORS_PER_PAGE);
if (batch > count) {
batch = count;
}
if (lpn != s_cached_lpn) {
if (ftl_flush_cache() != GD5F_OK) {
return RES_ERROR;
}
if (ftl_read_page(lpn) != GD5F_OK) {
return RES_ERROR;
}
}
memcpy(s_cache_buf + offset, buff, batch * 512);
s_cache_dirty = 1;
if (offset == 0 && batch == FTL_SECTORS_PER_PAGE) {
if (ftl_flush_cache() != GD5F_OK) {
return RES_ERROR;
}
}
buff += batch * 512;
sector += batch;
count -= (UINT)batch;
}
return RES_OK;
}
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
if (pdrv != 0) {
return RES_PARERR;
}
if (!s_initialized) {
return RES_NOTRDY;
}
switch (cmd) {
case CTRL_SYNC:
if (ftl_flush_cache() != GD5F_OK) {
return RES_ERROR;
}
{
dhara_error_t err;
if (dhara_map_sync(&s_map, &err) < 0) {
DBG_ERROR("Map sync failed: %d", err);
return RES_ERROR;
}
}
return RES_OK;
case GET_SECTOR_COUNT: {
LBA_t *p_sectors = (LBA_t *)buff;
dhara_sector_t pages = dhara_map_capacity(&s_map);
*p_sectors = pages * FTL_SECTORS_PER_PAGE;
return RES_OK;
}
case GET_SECTOR_SIZE:
*(WORD *)buff = 512;
return RES_OK;
case GET_BLOCK_SIZE:
*(DWORD *)buff = 1;
return RES_OK;
default:
return RES_PARERR;
}
}
/* ======================== 公共函数定义 ======================== */
int nand_ftl_format(void) {
dhara_error_t err;
if (!s_initialized) {
if (disk_initialize(0) != 0) {
return GD5F_ERROR;
}
}
if (ftl_flush_cache() != GD5F_OK) {
return GD5F_ERROR;
}
dhara_map_clear(&s_map);
if (dhara_map_sync(&s_map, &err) < 0) {
DBG_ERROR("Map sync after clear failed: %d", err);
return GD5F_ERROR;
}
s_cache_dirty = 0;
s_cached_lpn = (dhara_sector_t)-1;
DBG_INFO("NAND FTL formatted, capacity: %lu pages", dhara_map_capacity(&s_map));
return GD5F_OK;
}

View File

@@ -0,0 +1,18 @@
#ifndef __NAND_FTL_H
#define __NAND_FTL_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
int nand_ftl_init(void);
int nand_ftl_format(void);
void nand_ftl_deinit(void);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Jason von Nieda <jason@vonnieda.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,675 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdbool.h>
#include <ctype.h>
#include "lftpd.h"
#include "private/lftpd_status.h"
#include "private/lftpd_inet.h"
#include "private/lftpd_log.h"
#include "private/lftpd_string.h"
#include "private/lftpd_io.h"
#include "net_socket.h"
#include "net_config.h"
#include "ff.h"
typedef struct {
const char *command;
int (*handler)(lftpd_client_t *client, const char *arg);
} command_t;
static int cmd_cwd(lftpd_client_t *client, const char *arg);
static int cmd_dele(lftpd_client_t *client, const char *arg);
static int cmd_epsv(lftpd_client_t *client, const char *arg);
static int cmd_feat(lftpd_client_t *client, const char *arg);
static int cmd_list(lftpd_client_t *client, const char *arg);
static int cmd_nlst(lftpd_client_t *client, const char *arg);
static int cmd_noop(lftpd_client_t *client, const char *arg);
static int cmd_pass(lftpd_client_t *client, const char *arg);
static int cmd_pasv(lftpd_client_t *client, const char *arg);
static int cmd_pwd(lftpd_client_t *client, const char *arg);
static int cmd_quit(lftpd_client_t *client, const char *arg);
static int cmd_retr(lftpd_client_t *client, const char *arg);
static int cmd_size(lftpd_client_t *client, const char *arg);
static int cmd_stor(lftpd_client_t *client, const char *arg);
static int cmd_syst(lftpd_client_t *client, const char *arg);
static int cmd_type(lftpd_client_t *client, const char *arg);
static int cmd_user(lftpd_client_t *client, const char *arg);
static command_t commands[] = {
{ "CWD", cmd_cwd },
{ "DELE", cmd_dele },
{ "EPSV", cmd_epsv },
{ "FEAT", cmd_feat },
{ "LIST", cmd_list },
{ "NLST", cmd_nlst },
{ "NOOP", cmd_noop },
{ "PASS", cmd_pass },
{ "PASV", cmd_pasv },
{ "PWD", cmd_pwd },
{ "QUIT", cmd_quit },
{ "RETR", cmd_retr },
{ "SIZE", cmd_size },
{ "STOR", cmd_stor },
{ "SYST", cmd_syst },
{ "TYPE", cmd_type },
{ "USER", cmd_user },
{ NULL, NULL },
};
static int send_response(int socket, int code, bool include_code,
bool multiline_start, const char *format, ...)
{
va_list args;
char message[256];
char response[512];
va_start(args, format);
vsnprintf(message, sizeof(message), format, args);
va_end(args);
if (include_code) {
if (multiline_start) {
snprintf(response, sizeof(response), "%d-%s%s", code, message, CRLF);
} else {
snprintf(response, sizeof(response), "%d %s%s", code, message, CRLF);
}
} else {
snprintf(response, sizeof(response), "%s%s", message, CRLF);
}
return lftpd_inet_write_string(socket, response);
}
#define send_simple_response(socket, code, format, ...) \
send_response(socket, code, true, false, format, ##__VA_ARGS__)
#define send_multiline_response_begin(socket, code, format, ...) \
send_response(socket, code, true, true, format, ##__VA_ARGS__)
#define send_multiline_response_line(socket, format, ...) \
send_response(socket, 0, false, false, format, ##__VA_ARGS__)
#define send_multiline_response_end(socket, code, format, ...) \
send_response(socket, code, true, false, format, ##__VA_ARGS__)
static int send_list(int socket, const char *path)
{
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s";
static const char *file_format = "-rw-rw-rw- 1 owner group %13lu Jan 01 1970 %s";
void *dp = lftpd_io_opendir(path);
if (dp == NULL) {
return -1;
}
char name[256];
uint32_t size;
int is_dir;
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
if (is_dir) {
send_multiline_response_line(socket, directory_format,
(unsigned long)size, name);
} else {
send_multiline_response_line(socket, file_format,
(unsigned long)size, name);
}
}
lftpd_io_closedir(dp);
return 0;
}
static int send_nlst(int socket, const char *path)
{
void *dp = lftpd_io_opendir(path);
if (dp == NULL) {
return -1;
}
char name[256];
uint32_t size;
int is_dir;
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
if (!is_dir) {
send_multiline_response_line(socket, name);
}
}
lftpd_io_closedir(dp);
return 0;
}
static int send_file(int socket, const char *path)
{
int fh = lftpd_io_open_read(path);
if (fh < 0) {
lftpd_log_error("failed to open file for read");
return -1;
}
unsigned char buffer[1024];
int read_len;
while ((read_len = lftpd_io_read(fh, buffer, sizeof(buffer))) > 0) {
unsigned char *p = buffer;
while (read_len > 0) {
int write_len = lftpd_inet_write(socket, p, read_len);
if (write_len < 0) {
lftpd_log_error("write error");
lftpd_io_close(fh);
return -1;
}
p += write_len;
read_len -= write_len;
}
}
lftpd_io_close(fh);
return 0;
}
static int receive_file(int socket, const char *path)
{
int fh = lftpd_io_open_write(path);
if (fh < 0) {
lftpd_log_error("failed to open file for write");
return -1;
}
unsigned char buffer[1024];
int err;
int total = 0;
while ((err = lftpd_inet_read(socket, buffer, sizeof(buffer))) > 0) {
total += err;
if (lftpd_io_write(fh, buffer, err) != err) {
err = -1;
break;
}
}
lftpd_io_close(fh);
if (err < 0) {
return err;
}
if (total == 0) {
lftpd_log_error("received 0 bytes");
lftpd_io_unlink(path);
return -1;
}
return 0;
}
static int cmd_cwd(lftpd_client_t *client, const char *arg)
{
if (arg == NULL || strlen(arg) == 0) {
send_simple_response(client->socket, 550, STATUS_550);
return -1;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
/* 根目录始终有效,跳过 stat 检查FatFS FF_FS_RPATH=0 下 stat 根目录可能失败) */
if (strcmp(path, "/") != 0) {
uint32_t size;
int is_dir;
if (lftpd_io_stat(path, &size, &is_dir) != 0 || !is_dir) {
send_simple_response(client->socket, 550, STATUS_550);
free(path);
return -1;
}
}
strncpy(client->directory, path, sizeof(client->directory) - 1);
client->directory[sizeof(client->directory) - 1] = '\0';
free(path);
send_simple_response(client->socket, 250, STATUS_250);
return 0;
}
static int cmd_dele(lftpd_client_t *client, const char *arg)
{
if (arg == NULL || strlen(arg) == 0) {
send_simple_response(client->socket, 550, STATUS_550);
return -1;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
uint32_t size;
int is_dir;
if (lftpd_io_stat(path, &size, &is_dir) != 0) {
send_simple_response(client->socket, 550, STATUS_550);
free(path);
return -1;
}
if (is_dir) {
send_simple_response(client->socket, 550, STATUS_550);
free(path);
return -1;
}
lftpd_io_unlink(path);
free(path);
send_simple_response(client->socket, 250, STATUS_250);
return 0;
}
static int cmd_epsv(lftpd_client_t *client, const char *arg)
{
if (client->data_socket >= 0) {
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
}
int listener_socket = lftpd_inet_listen(0);
if (listener_socket < 0) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
int port = lftpd_inet_get_socket_port(listener_socket);
send_simple_response(client->socket, 229, STATUS_229, port);
lftpd_log_debug("EPSV listener on port %d", port);
client->data_socket = listener_socket;
return 0;
}
static int cmd_feat(lftpd_client_t *client, const char *arg)
{
send_multiline_response_begin(client->socket, 211, STATUS_211);
send_multiline_response_line(client->socket, "EPSV");
send_multiline_response_line(client->socket, "PASV");
send_multiline_response_line(client->socket, "SIZE");
send_multiline_response_line(client->socket, "NLST");
send_multiline_response_end(client->socket, 211, STATUS_211);
return 0;
}
static int cmd_list(lftpd_client_t *client, const char *arg)
{
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
return -1;
}
int err = send_list(data_sock, client->directory);
lftpd_inet_close(data_sock);
client->data_socket = -1;
if (err == 0) {
send_simple_response(client->socket, 226, STATUS_226);
} else {
send_simple_response(client->socket, 550, STATUS_550);
}
return 0;
}
static int cmd_nlst(lftpd_client_t *client, const char *arg)
{
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
return -1;
}
int err = send_nlst(data_sock, client->directory);
lftpd_inet_close(data_sock);
client->data_socket = -1;
if (err == 0) {
send_simple_response(client->socket, 226, STATUS_226);
} else {
send_simple_response(client->socket, 550, STATUS_550);
}
return 0;
}
static int cmd_noop(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 200, STATUS_200);
return 0;
}
static int cmd_pass(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 230, STATUS_230);
return 0;
}
static int cmd_pasv(lftpd_client_t *client, const char *arg)
{
int listener_socket = -1;
int retry;
if (client->data_socket >= 0) {
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
}
for (retry = 0; retry < 3; retry++) {
listener_socket = lftpd_inet_listen(0);
if (listener_socket >= 0) break;
vTaskDelay(pdMS_TO_TICKS(200));
}
if (listener_socket < 0) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
int port = lftpd_inet_get_socket_port(listener_socket);
{
struct net_sockaddr_in local_addr;
int addrlen = sizeof(local_addr);
int err = net_getsockname(client->socket,
(struct net_sockaddr *)&local_addr, &addrlen);
if (err != 0) {
lftpd_log_error("error getting client IP info");
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(listener_socket);
return -1;
}
uint32_t ip = net_htonl(local_addr.sin_addr.s_addr);
send_simple_response(client->socket, 227, STATUS_227,
(ip >> 24) & 0xff,
(ip >> 16) & 0xff,
(ip >> 8) & 0xff,
(ip >> 0) & 0xff,
(port >> 8) & 0xff, (port >> 0) & 0xff);
}
lftpd_log_debug("PASV listener on port %d", port);
client->data_socket = listener_socket;
return 0;
}
static int cmd_pwd(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 257, "\"%s\"", client->directory);
return 0;
}
static int cmd_quit(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 221, STATUS_221);
return -1;
}
static int cmd_retr(lftpd_client_t *client, const char *arg)
{
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
return -1;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
lftpd_log_debug("send '%s'", path);
int err = send_file(data_sock, path);
lftpd_inet_close(data_sock);
client->data_socket = -1;
if (err == 0) {
lftpd_log_info("DOWNLOAD '%s' OK", path);
send_simple_response(client->socket, 226, STATUS_226);
} else {
lftpd_log_error("DOWNLOAD '%s' FAILED", path);
send_simple_response(client->socket, 450, STATUS_450);
}
free(path);
return 0;
}
static int cmd_size(lftpd_client_t *client, const char *arg)
{
if (arg == NULL) {
send_simple_response(client->socket, 550, STATUS_550);
return 0;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
lftpd_log_debug("size %s", path);
uint32_t size;
int is_dir;
if (lftpd_io_stat(path, &size, &is_dir) == 0 && !is_dir) {
send_simple_response(client->socket, 213, "%lu", (unsigned long)size);
} else {
send_simple_response(client->socket, 550, STATUS_550);
}
free(path);
return 0;
}
static int cmd_stor(lftpd_client_t *client, const char *arg)
{
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
}
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
return -1;
}
char *path = lftpd_io_canonicalize_path(client->directory, arg);
lftpd_log_debug("receive '%s'", path);
int err = receive_file(data_sock, path);
lftpd_inet_close(data_sock);
client->data_socket = -1;
if (err == 0) {
uint32_t size = 0;
int is_dir = 0;
lftpd_io_stat(path, &size, &is_dir);
lftpd_log_info("UPLOAD '%s' %lu bytes OK", path, (unsigned long)size);
send_simple_response(client->socket, 226, STATUS_226);
} else {
lftpd_log_error("UPLOAD '%s' FAILED", path);
send_simple_response(client->socket, 450, STATUS_450);
}
free(path);
return 0;
}
static int cmd_syst(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 215, "UNIX Type: L8");
return 0;
}
static int cmd_type(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 200, STATUS_200);
return 0;
}
static int cmd_user(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 230, STATUS_230);
return 0;
}
static int handle_control_channel(lftpd_client_t *client)
{
int err = send_simple_response(client->socket, 220, STATUS_220);
if (err != 0) {
lftpd_log_error("error sending welcome message");
goto cleanup;
}
char read_buffer[512];
while (err == 0) {
int line_len = lftpd_inet_read_line(client->socket, read_buffer,
sizeof(read_buffer));
if (line_len != 0) {
lftpd_log_error("error reading next command");
goto cleanup;
}
int index;
char *p = strchr(read_buffer, ' ');
if (p != NULL) {
index = (int)(p - read_buffer);
} else {
index = (int)strlen(read_buffer);
}
if (index >= 5) {
err = send_simple_response(client->socket, 500, STATUS_500);
continue;
}
char command_tmp[4 + 1];
memset(command_tmp, 0, sizeof(command_tmp));
memcpy(command_tmp, read_buffer, (size_t)index);
for (int i = 0; command_tmp[i]; i++) {
command_tmp[i] = (char)toupper((int)command_tmp[i]);
}
bool matched = false;
for (int i = 0; commands[i].command; i++) {
if (strcmp(commands[i].command, command_tmp) == 0) {
char arg_buf[512];
const char *arg = NULL;
if (index < (int)strlen(read_buffer)) {
strncpy(arg_buf, read_buffer + index + 1, sizeof(arg_buf) - 1);
arg_buf[sizeof(arg_buf) - 1] = '\0';
arg = lftpd_string_trim(arg_buf);
}
lftpd_log_info("CMD %s %s", command_tmp, arg ? arg : "");
err = commands[i].handler(client, arg);
matched = true;
break;
}
}
if (!matched) {
send_simple_response(client->socket, 502, STATUS_502);
}
}
cleanup:
lftpd_inet_close(client->socket);
return 0;
}
int lftpd_start(const char *directory, int port, lftpd_t *lftpd)
{
memset(lftpd, 0, sizeof(lftpd_t));
lftpd->directory = directory;
lftpd->port = port;
while (true) {
lftpd->server_socket = lftpd_inet_listen(port);
if (lftpd->server_socket < 0) {
lftpd_log_error("error creating listener");
vTaskDelay(pdMS_TO_TICKS(3000));
continue;
}
lftpd_log_info("waiting for connection...");
int client_socket = lftpd_inet_accept(lftpd->server_socket);
if (client_socket < 0) {
lftpd_log_error("error accepting client socket");
lftpd_inet_close(lftpd->server_socket);
continue;
}
{
char ip_str[16];
struct net_sockaddr_in addr;
int addrlen = sizeof(addr);
if (net_getsockname(client_socket,
(struct net_sockaddr *)&addr, &addrlen) == 0) {
net_inet_ntoa(addr.sin_addr.s_addr, ip_str);
int remote_port = lftpd_inet_get_socket_port(client_socket);
lftpd_log_info("connection received from %s:%d...",
ip_str, remote_port);
} else {
lftpd_log_info("connection received...");
}
}
lftpd_client_t client;
memset(&client, 0, sizeof(client));
strncpy(client.directory, directory, sizeof(client.directory) - 1);
client.directory[sizeof(client.directory) - 1] = '\0';
client.socket = client_socket;
client.data_socket = -1;
lftpd->client = &client;
handle_control_channel(&client);
lftpd->client = NULL;
/* standalone mode: socket was closed in handle_control_channel,
* loop back to create a new listener */
}
}
int lftpd_stop(lftpd_t *lftpd)
{
if (lftpd->server_socket >= 0) {
lftpd_inet_close(lftpd->server_socket);
}
if (lftpd->client) {
lftpd_inet_close(lftpd->client->socket);
}
return 0;
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <stdint.h>
typedef struct {
char directory[256];
int socket;
int data_socket;
} lftpd_client_t;
typedef struct {
const char *directory;
int port;
int server_socket;
lftpd_client_t *client;
} lftpd_t;
int lftpd_start(const char *directory, int port, lftpd_t *lftpd);
int lftpd_stop(lftpd_t *lftpd);

View File

@@ -0,0 +1,166 @@
#define DBG_TAG "[LFT_INET]"
#include "private/lftpd_inet.h"
#include "dbg_log.h"
#include "net_socket.h"
#include <string.h>
int lftpd_inet_listen(int port)
{
int retry;
int s;
for (retry = 0; retry < 5; retry++) {
s = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (s < 0) {
if (retry == 0) DBG_ERROR("create socket");
vTaskDelay(pdMS_TO_TICKS(500));
continue;
}
struct net_sockaddr_in addr;
addr.sin_family = NET_AF_INET;
addr.sin_port = net_htons((uint16_t)port);
addr.sin_addr.s_addr = 0;
if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) {
DBG_ERROR("bind port %d", port);
net_close(s);
return -1;
}
if (net_listen(s) >= 0) {
return s;
}
net_close(s);
if (retry < 4) {
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
DBG_ERROR("listen failed after 5 retries (total 10s wait)");
return -1;
}
int lftpd_inet_get_socket_port(int socket)
{
struct net_sockaddr_in addr;
int addrlen = sizeof(addr);
if (net_getsockname(socket, (struct net_sockaddr *)&addr, &addrlen) < 0) {
DBG_ERROR("error getting socket port number");
return -1;
}
return (int)net_ntohs(addr.sin_port);
}
int lftpd_inet_accept(int socket)
{
while (1) {
int s = net_accept(socket, NULL, NULL);
if (s >= 0) {
return s;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
int lftpd_inet_accept_timeout(int socket, int timeout_ms)
{
int elapsed = 0;
while (elapsed < timeout_ms) {
int s = net_accept(socket, NULL, NULL);
if (s >= 0) {
return s;
}
/* 数据连接已被对端关闭时提前退出,不等待超时 */
net_sock_t *p_sock = net_get_sock(socket);
if (p_sock == NULL || p_sock->state != NET_SOCK_STATE_LISTENING) {
DBG_ERROR("data socket closed, accept aborted");
return -1;
}
vTaskDelay(pdMS_TO_TICKS(100));
elapsed += 100;
}
DBG_ERROR("accept timeout (%dms)", timeout_ms);
return -1;
}
int lftpd_inet_read_line(int socket, char *buffer, size_t buffer_len)
{
memset(buffer, 0, buffer_len);
int total_read_len = 0;
while (total_read_len < (int)buffer_len) {
int read_len = net_recv(socket,
buffer + total_read_len,
buffer_len - total_read_len - 1,
0);
if (read_len == 0) {
return -1;
}
if (read_len < 0) {
return read_len;
}
total_read_len += read_len;
char *p = strstr(buffer, "\r\n");
if (p != NULL) {
*p = '\0';
return 0;
}
}
return -1;
}
int lftpd_inet_write_string(int socket, const char *message)
{
const char *p = message;
int length = (int)strlen(message);
while (length > 0) {
int write_len = net_send(socket, p, length, 0);
if (write_len < 0) {
DBG_ERROR("write error");
return write_len;
}
p += write_len;
length -= write_len;
}
return 0;
}
int lftpd_inet_write(int socket, const void *buf, int len)
{
const char *p = (const char *)buf;
int remaining = len;
while (remaining > 0) {
int n = net_send(socket, p, remaining, 0);
if (n < 0) {
return n;
}
p += n;
remaining -= n;
}
return len;
}
int lftpd_inet_read(int socket, void *buf, int len)
{
int n = net_recv(socket, buf, len, 0);
return n;
}
int lftpd_inet_close(int socket)
{
return net_close(socket);
}

View File

@@ -0,0 +1,236 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "ff.h"
#include "private/lftpd_io.h"
#define DBG_TAG "[LFT_IO]"
#include "dbg_log.h"
/*
* FatFS 在 FF_USE_LFN=0 时不接受 "/" 前缀路径。
* 剥离前导 "/" 后用 FatFS 兼容路径,根目录用 "0:"(驱动号格式)替代。
*/
static const char *to_fatfs_path(const char *path)
{
if (path == NULL || *path == '\0') {
return "0:";
}
if (*path == '/') {
path++;
if (*path == '\0') {
return "0:";
}
}
return path;
}
static int s_file_open = 0;
static FIL s_ftp_file;
char *lftpd_io_canonicalize_path(const char *base, const char *name)
{
if (base == NULL) {
base = "";
}
if (name == NULL) {
name = "";
}
char *path;
if (name[0] == '/') {
path = (char *)malloc(strlen(name) + 1);
if (path == NULL) return NULL;
strcpy(path, name);
} else {
size_t len = strlen(base) + 1 + strlen(name) + 1;
path = (char *)malloc(len);
if (path == NULL) return NULL;
snprintf(path, len, "%s/%s", base, name);
}
char *abs_path = (char *)malloc(strlen(path) + 2);
if (abs_path == NULL) {
free(path);
return NULL;
}
abs_path[0] = '\0';
char *p = path;
while (*p != '\0') {
while (*p == '/') p++;
if (*p == '\0') break;
char *seg_start = p;
while (*p != '\0' && *p != '/') p++;
char saved = *p;
*p = '\0';
if (strcmp(seg_start, ".") == 0) {
/* ignore */
} else if (strcmp(seg_start, "..") == 0) {
char *slash = strrchr(abs_path, '/');
if (slash != NULL) {
*slash = '\0';
}
} else {
strcat(abs_path, "/");
strcat(abs_path, seg_start);
}
*p = saved;
}
free(path);
if (strlen(abs_path) == 0) {
strcpy(abs_path, "/");
}
return abs_path;
}
int lftpd_io_open_read(const char *path)
{
if (s_file_open) {
DBG_ERROR("open_read '%s': already open", path);
return -1;
}
const char *fpath = to_fatfs_path(path);
FRESULT fr = f_open(&s_ftp_file, fpath, FA_READ);
if (fr != FR_OK) {
DBG_ERROR("open_read '%s': f_open err=%d", fpath, (int)fr);
return -1;
}
s_file_open = 1;
return 0;
}
int lftpd_io_open_write(const char *path)
{
if (s_file_open) {
DBG_ERROR("open_write '%s': already open", path);
return -1;
}
const char *fpath = to_fatfs_path(path);
FRESULT fr = f_open(&s_ftp_file, fpath, FA_WRITE | FA_CREATE_ALWAYS);
if (fr != FR_OK) {
DBG_ERROR("open_write '%s': f_open err=%d", fpath, (int)fr);
return -1;
}
s_file_open = 1;
return 0;
}
int lftpd_io_read(int handle, void *buf, int len)
{
(void)handle;
UINT br;
if (f_read(&s_ftp_file, buf, (UINT)len, &br) != FR_OK) {
return -1;
}
return (int)br;
}
int lftpd_io_write(int handle, const void *buf, int len)
{
(void)handle;
UINT bw;
if (f_write(&s_ftp_file, buf, (UINT)len, &bw) != FR_OK) {
return -1;
}
if ((int)bw != len) {
return -1;
}
return (int)bw;
}
void lftpd_io_close(int handle)
{
(void)handle;
if (s_file_open) {
f_close(&s_ftp_file);
s_file_open = 0;
}
}
int lftpd_io_stat(const char *path, uint32_t *p_size, int *p_is_dir)
{
FILINFO fno;
const char *fpath = to_fatfs_path(path);
if (f_stat(fpath, &fno) != FR_OK) {
return -1;
}
if (p_size != NULL) {
*p_size = (uint32_t)fno.fsize;
}
if (p_is_dir != NULL) {
*p_is_dir = (fno.fattrib & AM_DIR) ? 1 : 0;
}
return 0;
}
int lftpd_io_unlink(const char *path)
{
const char *fpath = to_fatfs_path(path);
if (f_unlink(fpath) == FR_OK) {
return 0;
}
return -1;
}
void *lftpd_io_opendir(const char *path)
{
DIR *dp = (DIR *)malloc(sizeof(DIR));
if (dp == NULL) {
return NULL;
}
const char *fpath = to_fatfs_path(path);
if (f_opendir(dp, fpath) != FR_OK) {
free(dp);
return NULL;
}
return (void *)dp;
}
int lftpd_io_readdir(void *dp, char *name, int name_max, uint32_t *p_size, int *p_is_dir)
{
FILINFO fno;
FRESULT fr;
while (1) {
fr = f_readdir((DIR *)dp, &fno);
if (fr != FR_OK || fno.fname[0] == 0) {
return -1;
}
if (fno.fname[0] == '.') {
continue;
}
break;
}
strncpy(name, fno.fname, (size_t)(name_max - 1));
name[name_max - 1] = '\0';
if (p_size != NULL) {
*p_size = (uint32_t)fno.fsize;
}
if (p_is_dir != NULL) {
*p_is_dir = (fno.fattrib & AM_DIR) ? 1 : 0;
}
return 0;
}
void lftpd_io_closedir(void *dp)
{
if (dp != NULL) {
f_closedir((DIR *)dp);
free(dp);
}
}

View File

@@ -0,0 +1,18 @@
#include "private/lftpd_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
void lftpd_log_internal(const char* level, const char* format, ...) {
char buffer[256];
va_list args;
va_start(args, format);
int err = vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
if (err >= sizeof(buffer)) {
return;
}
printf("%s %s\n", level, buffer);
}

View File

@@ -0,0 +1,16 @@
#include <string.h>
#include <ctype.h>
#include "private/lftpd_string.h"
char *lftpd_string_trim(char *s)
{
char *p = s;
for (int i = 0, len = (int)strlen(s); i < len && isspace((int)s[i]); i++) {
p++;
}
for (int i = (int)strlen(p); i >= 0 && isspace((int)p[i]); i--) {
p[i] = '\0';
}
return p;
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include <stdlib.h>
#include <stdint.h>
int lftpd_inet_listen(int port);
int lftpd_inet_get_socket_port(int socket);
int lftpd_inet_accept(int socket);
int lftpd_inet_accept_timeout(int socket, int timeout_ms);
int lftpd_inet_read_line(int socket, char *buffer, size_t buffer_len);
int lftpd_inet_write_string(int socket, const char *message);
int lftpd_inet_write(int socket, const void *buf, int len);
int lftpd_inet_read(int socket, void *buf, int len);
int lftpd_inet_close(int socket);

View File

@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
char *lftpd_io_canonicalize_path(const char *base, const char *name);
int lftpd_io_open_read(const char *path);
int lftpd_io_open_write(const char *path);
int lftpd_io_read(int handle, void *buf, int len);
int lftpd_io_write(int handle, const void *buf, int len);
void lftpd_io_close(int handle);
int lftpd_io_stat(const char *path, uint32_t *p_size, int *p_is_dir);
int lftpd_io_unlink(const char *path);
void *lftpd_io_opendir(const char *path);
int lftpd_io_readdir(void *dp, char *name, int name_max, uint32_t *p_size, int *p_is_dir);
void lftpd_io_closedir(void *dp);

View File

@@ -0,0 +1,7 @@
#pragma once
#include "dbg_log.h"
#define lftpd_log_error(format, ...) DBG_ERROR("[FTP] " format, ##__VA_ARGS__)
#define lftpd_log_info(format, ...) DBG_INFO("[FTP] " format, ##__VA_ARGS__)
#define lftpd_log_debug(format, ...)

View File

@@ -0,0 +1,42 @@
#pragma once
#define STATUS_110 "Restart marker reply."
#define STATUS_120 "Service ready in %d minutes."
#define STATUS_125 "Data connection already open; transfer starting."
#define STATUS_150 "File status okay; about to open data connection."
#define STATUS_200 "Command okay."
#define STATUS_202 "Command not implemented, superfluous at this site."
#define STATUS_211 "System status, or system help reply."
#define STATUS_212 "Directory status."
#define STATUS_213 "File status."
#define STATUS_214 "Help message."
#define STATUS_215 "%s system type."
#define STATUS_220 "Service ready for new user."
#define STATUS_221 "Service closing control connection."
#define STATUS_225 "Data connection open; no transfer in progress."
#define STATUS_226 "Closing data connection."
#define STATUS_227 "Entering Passive Mode (%d,%d,%d,%d,%d,%d)."
#define STATUS_229 "Entering Extended Passive Mode (|||%d|)."
#define STATUS_230 "User logged in, proceed."
#define STATUS_250 "Requested file action okay, completed."
#define STATUS_257 "\"%s\" created."
#define STATUS_331 "User name okay, need password."
#define STATUS_332 "Need account for login."
#define STATUS_350 "Requested file action pending further information."
#define STATUS_421 "Service not available, closing control connection."
#define STATUS_425 "Can't open data connection."
#define STATUS_426 "Connection closed; transfer aborted."
#define STATUS_450 "Requested file action not taken. File unavailable (e.g., file busy)."
#define STATUS_451 "Requested action aborted: local error in processing."
#define STATUS_452 "Requested action not taken. Insufficient storage space in system."
#define STATUS_500 "Syntax error, command unrecognized. This may include errors such as command line too long."
#define STATUS_501 "Syntax error in parameters or arguments."
#define STATUS_502 "Command not implemented."
#define STATUS_503 "Bad sequence of commands."
#define STATUS_504 "Command not implemented for that parameter."
#define STATUS_530 "Not logged in."
#define STATUS_532 "Need account for storing files."
#define STATUS_550 "Requested action not taken. File unavailable (e.g., file not found, no access)."
#define STATUS_551 "Requested action aborted: page type unknown."
#define STATUS_552 "Requested file action aborted. Exceeded storage allocation (for current directory or dataset)."
#define STATUS_553 "Requested action not taken. File name not allowed."

View File

@@ -0,0 +1,5 @@
#pragma once
#define CRLF "\r\n"
char* lftpd_string_trim(char* s);

View File

@@ -30,14 +30,6 @@ extern "C" {
#define NET_TCP_SERVER_LISTEN_SOCK 0 /* 监听 Socket 索引 */ #define NET_TCP_SERVER_LISTEN_SOCK 0 /* 监听 Socket 索引 */
#endif #endif
#ifndef NET_TCP_SERVER_DATA_SOCK_START
#define NET_TCP_SERVER_DATA_SOCK_START 1 /* 数据 Socket 起始索引 */
#endif
#ifndef NET_TCP_SERVER_MAX_CLIENTS
#define NET_TCP_SERVER_MAX_CLIENTS 7 /* 最大客户端数1~7 */
#endif
/* /*
* 缓冲区大小配置 * 缓冲区大小配置
*/ */
@@ -66,7 +58,7 @@ extern "C" {
#endif #endif
#ifndef NET_RECV_TIMEOUT_MS #ifndef NET_RECV_TIMEOUT_MS
#define NET_RECV_TIMEOUT_MS 0 /* 接收超时0=无限等待) */ #define NET_RECV_TIMEOUT_MS 30000 /* 接收超时(30秒0=无限等待) */
#endif #endif
/* /*
@@ -98,6 +90,13 @@ extern "C" {
#define NET_POLL_DELAY_MS 1 /* 非阻塞轮询延时 */ #define NET_POLL_DELAY_MS 1 /* 非阻塞轮询延时 */
#endif #endif
/*
* 消息队列配置
*/
#ifndef NET_MSG_QUEUE_LENGTH
#define NET_MSG_QUEUE_LENGTH 8 /* 消息队列深度 */
#endif
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -1,20 +1,29 @@
/* /*
* 模块名称Network Select Implementation * 模块名称Network Select Implementation
* 模块功能I/O 多路复用实现,包括 select 和 poll 模式, * 模块功能I/O 多路复用实现,包括 select 和 poll 两种模式,用于同时监控多个 Socket 的读写事件
* 用于同时监控多个 Socket 的读写事件 * 适用平台STM32F407ZGTx + CH395F 以太网芯片
* 适用平台STM32F4 系列CH395F 以太网芯片)
* 作者:王建锋 * 作者:王建锋
* 创建日期2026-07-18 * 创建日期2026-07-18
* 修改记录: * 修改记录:
* 2026-07-18 王建锋 创建初始版本
*/ */
#include <string.h>
#include <stdio.h>
#include "cmsis_os.h" /* CMSIS-RTOS2 API: osDelay() */
#include "FreeRTOS.h"
#include "task.h"
#include "net_select.h" #include "net_select.h"
#include "net_socket.h" #include "net_socket.h"
#include "ch395f.h" #include "ch395f.h"
#include "main.h"
/* /*
* 私有函数声明区 * 私有函数声明区
*/ */
static int check_socket_readable(int sockfd); static int check_socket_readable(int sockfd);
static int check_socket_writable(int sockfd); static int check_socket_writable(int sockfd);
static int check_socket_error(int sockfd); static int check_socket_error(int sockfd);
@@ -24,11 +33,24 @@ static int check_socket_error(int sockfd);
*/ */
/* /*
* 函数功能I/O 多路复用select 模式) * 函数功能I/O 多路复用select 模式),等待多个 Socket 的读写事件就绪
* 入口参数nfds - 最大文件描述符数 +1 int > 0
* readfds - 读事件集合指针 net_fd_set *(可为 NULL
* writefds - 写事件集合指针 net_fd_set *(可为 NULL
* exceptfds- 异常事件集合指针 net_fd_set *(可为 NULL
* timeout - 超时时间结构体指针 net_timeval *NULL=无限等待)
* 出口参数readfds/writefds/exceptfds - 仅保留就绪的 Socket 位 net_fd_set *
* 返回值:就绪的 Socket 数量(>= 0-1 表示参数无效 int
* 限定条件net_init() + MX_FREERTOS_Init() 已执行;可在任意任务上下文调用
* 函数说明1. 轮询 Socket 控制块RAM中的读/写/异常状态,判断事件是否就绪
* 2. 不在此直接访问 CH395F SPI——所有 ch395f 事务统一由 netTask 的 net_poll()
* 每 10ms 串行执行并刷新控制块状态send_ready / readable本函数仅做
* RAM 读取,避免与应用任务发起的 SPI 访问竞争(见 Trap 16
* 3. 阻塞等待直到有事件就绪或超时
* 4. 通过 osDelay(1) 让出 CPU平衡实时性与 CPU 利用率(状态刷新延迟 <=10ms
*/ */
int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds, int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
net_fd_set *exceptfds, net_timeval *timeout) net_fd_set *exceptfds, net_timeval *timeout) {
{
uint32_t start_tick; uint32_t start_tick;
uint32_t timeout_ms = 0; uint32_t timeout_ms = 0;
net_fd_set result_read = {0}; net_fd_set result_read = {0};
@@ -37,57 +59,50 @@ int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
int ready_count = 0; int ready_count = 0;
int i; int i;
/* 计算超时时间 */ /* 检查输入参数合法性 */
if (timeout != NULL) if ((0 >= nfds) || (NET_MAX_SOCKETS < nfds)) {
{ return -1;
}
/* 计算超时时间(毫秒) */
if (NULL != timeout) {
timeout_ms = (uint32_t)(timeout->tv_sec * 1000) + timeout_ms = (uint32_t)(timeout->tv_sec * 1000) +
(uint32_t)(timeout->tv_usec / 1000); (uint32_t)(timeout->tv_usec / 1000);
} }
start_tick = HAL_GetTick(); start_tick = HAL_GetTick();
while (1) while (1) {
{ /* 仅读取 netTask 的 net_poll() 已刷新的控制块状态(无 CH395F SPI 访问) */
/* 轮询所有 Socket */
net_poll();
ready_count = 0; ready_count = 0;
/* 检查哪 Socket 有事件 */ /* 检查哪 Socket 有事件 */
for (i = 0; i < nfds && i < NET_MAX_SOCKETS; i++) for (i = 0; i < nfds && i < NET_MAX_SOCKETS; i++) {
{ net_sock_t *p_sock = net_get_sock(i);
net_sock_t *sock = net_get_sock(i);
if (sock == NULL) if (NULL == p_sock) {
{
continue; continue;
} }
/* 检查可读事件 */ /* 检查可读事件 */
if (readfds != NULL && NET_FD_ISSET(i, readfds)) if ((NULL != readfds) && NET_FD_ISSET(i, readfds)) {
{ if (check_socket_readable(i)) {
if (check_socket_readable(i))
{
NET_FD_SET(i, &result_read); NET_FD_SET(i, &result_read);
ready_count++; ready_count++;
} }
} }
/* 检查可写事件 */ /* 检查可写事件 */
if (writefds != NULL && NET_FD_ISSET(i, writefds)) if ((NULL != writefds) && NET_FD_ISSET(i, writefds)) {
{ if (check_socket_writable(i)) {
if (check_socket_writable(i))
{
NET_FD_SET(i, &result_write); NET_FD_SET(i, &result_write);
ready_count++; ready_count++;
} }
} }
/* 检查异常事件 */ /* 检查异常事件 */
if (exceptfds != NULL && NET_FD_ISSET(i, exceptfds)) if ((NULL != exceptfds) && NET_FD_ISSET(i, exceptfds)) {
{ if (check_socket_error(i)) {
if (check_socket_error(i))
{
NET_FD_SET(i, &result_except); NET_FD_SET(i, &result_except);
ready_count++; ready_count++;
} }
@@ -95,36 +110,30 @@ int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
} }
/* 有事件就绪,返回 */ /* 有事件就绪,返回 */
if (ready_count > 0) if (0 < ready_count) {
{
break; break;
} }
/* 超时检 */ /* 超时检 */
if (timeout != NULL) if (NULL != timeout) {
{ if ((0 < timeout_ms) &&
if (timeout_ms > 0 && ((HAL_GetTick() - start_tick) >= timeout_ms)) {
(HAL_GetTick() - start_tick) >= timeout_ms)
{
break; /* 超时返回 0 */ break; /* 超时返回 0 */
} }
} }
/* 短暂延时避免忙等 */ /* 让出 CPUFreeRTOS osDelay() 会正确进入休眠,允许其他任务运行 */
HAL_Delay(NET_POLL_DELAY_MS); osDelay(1); /* 1ms 周期轮询,平衡实时性和 CPU 利用率 */
} }
/* 复制结果 */ /* 复制结果集合 */
if (readfds != NULL) if (NULL != readfds) {
{
readfds->fd_bits = result_read.fd_bits; readfds->fd_bits = result_read.fd_bits;
} }
if (writefds != NULL) if (NULL != writefds) {
{
writefds->fd_bits = result_write.fd_bits; writefds->fd_bits = result_write.fd_bits;
} }
if (exceptfds != NULL) if (NULL != exceptfds) {
{
exceptfds->fd_bits = result_except.fd_bits; exceptfds->fd_bits = result_except.fd_bits;
} }
@@ -132,100 +141,93 @@ int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
} }
/* /*
* 函数功能I/O 多路复用poll 模式) * 函数功能I/O 多路复用poll 模式),等待多个 Socket 的读写事件就绪
* 入口参数fds - pollfd 数组指针 net_pollfd *
* nfds - 数组元素数量 int > 0
* timeout - 超时时间(毫秒) int >= 0-1=无限等待,但本实现按 >=0 处理)
* 出口参数fds[i].revents - 填充实际发生的事件掩码 short
* 返回值:就绪的 fd 数量(>= 0-1 表示参数无效 int
* 限定条件net_init() + MX_FREERTOS_Init() 已执行;可在任意任务上下文调用
* 函数说明1. 轮询 Socket 控制块RAM中的读/写/错误/挂起状态,判断事件是否就绪
* 2. 不在此直接访问 CH395F SPI——状态由 netTask 的 net_poll() 每 10ms 刷新
* 3. 阻塞等待直到有事件就绪或超时
* 4. 通过 osDelay(1) 让出 CPU平衡实时性与 CPU 利用率(状态刷新延迟 <=10ms
*/ */
int net_poll_events(net_pollfd *fds, int nfds, int timeout) int net_poll_events(net_pollfd *fds, int nfds, int timeout) {
{
uint32_t start_tick; uint32_t start_tick;
int ready = 0; int ready = 0;
int i; int i;
if (fds == NULL || nfds <= 0) if ((NULL == fds) || (0 >= nfds)) {
{
return -1; return -1;
} }
start_tick = HAL_GetTick(); start_tick = HAL_GetTick();
while (1) while (1) {
{ /* 仅读取 netTask 的 net_poll() 已刷新的控制块状态(无 CH395F SPI 访问) */
/* 轮询 */
net_poll();
ready = 0; ready = 0;
for (i = 0; i < nfds; i++) for (i = 0; i < nfds; i++) {
{
fds[i].revents = 0; fds[i].revents = 0;
/* 检查 fd 有效性 */ /* 检查 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; fds[i].revents |= NET_POLLNVAL;
ready++; ready++;
continue; continue;
} }
net_sock_t *sock = net_get_sock(fds[i].fd); net_sock_t *p_sock = net_get_sock(fds[i].fd);
if (sock == NULL) if (NULL == p_sock) {
{
fds[i].revents |= NET_POLLNVAL; fds[i].revents |= NET_POLLNVAL;
ready++; ready++;
continue; continue;
} }
/* 检查可读 */ /* 检查可读 */
if (fds[i].events & NET_POLLIN) if (fds[i].events & NET_POLLIN) {
{ if (check_socket_readable(fds[i].fd)) {
if (check_socket_readable(fds[i].fd))
{
fds[i].revents |= NET_POLLIN; fds[i].revents |= NET_POLLIN;
ready++; ready++;
} }
} }
/* 检查可写 */ /* 检查可写 */
if (fds[i].events & NET_POLLOUT) if (fds[i].events & NET_POLLOUT) {
{ if (check_socket_writable(fds[i].fd)) {
if (check_socket_writable(fds[i].fd))
{
fds[i].revents |= NET_POLLOUT; fds[i].revents |= NET_POLLOUT;
ready++; ready++;
} }
} }
/* 检查错误 */ /* 检查错误 */
if (check_socket_error(fds[i].fd)) if (check_socket_error(fds[i].fd)) {
{
fds[i].revents |= NET_POLLERR; fds[i].revents |= NET_POLLERR;
ready++; ready++;
} }
/* 检查挂起(连接断开) */ /* 检查挂起(连接断开) */
if (sock->state == NET_SOCK_STATE_TCP_ACCEPT || if (NET_SOCK_STATE_CLOSED == p_sock->state) {
sock->state == NET_SOCK_STATE_CLOSED)
{
fds[i].revents |= NET_POLLHUP; fds[i].revents |= NET_POLLHUP;
ready++; ready++;
} }
} }
if (ready > 0) if (0 < ready) {
{
break; break;
} }
/* 超时检 */ /* 超时检 */
if (timeout >= 0) if (0 <= timeout) {
{ if ((HAL_GetTick() - start_tick) >= (uint32_t)timeout) {
if ((HAL_GetTick() - start_tick) >= (uint32_t)timeout)
{
break; break;
} }
} }
HAL_Delay(NET_POLL_DELAY_MS); /* 让出 CPUFreeRTOS osDelay() 会正确进入休眠,允许其他任务运行 */
osDelay(1); /* 1ms 周期轮询,平衡实时性和 CPU 利用率 */
} }
return ready; return ready;
@@ -236,58 +238,55 @@ int net_poll_events(net_pollfd *fds, int nfds, int timeout)
*/ */
/* /*
* 函数功能:检查 Socket 是否可读 * 函数功能:检查指定 Socket 当前是否可读
* 返回值1 可读0 不可读 * 入口参数sockfd - 待检查的 Socket 描述符 int 0 - NET_MAX_SOCKETS-1
* 出口参数:无
* 返回值1 - 可读监听态有新连接或已连接态接收缓冲非空0 - 不可读或描述符无效 int
* 限定条件net_init() 已调用
* 函数说明1. 监听态 Socket 视为可读(存在可 accept 的新连接)
* 2. 已建立连接且接收缓冲区长度大于 0 时视为可读
*/ */
static int check_socket_readable(int sockfd) static int check_socket_readable(int sockfd) {
{ net_sock_t *p_sock;
net_sock_t *sock;
uint16_t recv_len;
sock = net_get_sock(sockfd); p_sock = net_get_sock(sockfd);
if (sock == NULL) if (NULL == p_sock) {
{
return 0; return 0;
} }
/* 监听 Socket 有新连接时可读 */ /* 监听 Socket 有新连接时可读 */
if (sock->state == NET_SOCK_STATE_LISTENING) if (NET_SOCK_STATE_LISTENING == p_sock->state) {
{
return 1; return 1;
} }
/* 已建立连接的 Socket 有数据可读 */ /* 已建立连接或 UDP 等有数据可读readable 由 net_poll 在 RECV_OK 时置位、
if (sock->state == NET_SOCK_STATE_ESTABLISHED) * net_recv_locked 读空后清零。避免应用任务直接访问 CH395F SPI见 Trap 16 */
{ if (p_sock->readable) {
recv_len = ch395f_get_recv_len(sockfd); return 1;
if (recv_len > 0)
{
return 1;
}
} }
return 0; return 0;
} }
/* /*
* 函数功能:检查 Socket 是否可写 * 函数功能:检查指定 Socket 当前是否可写
* 返回值1 可写0 不可写 * 入口参数sockfd - 待检查的 Socket 描述符 int 0 - NET_MAX_SOCKETS-1
* 出口参数:无
* 返回值1 - 可写已连接且发送缓冲空闲0 - 不可写或描述符无效 int
* 限定条件net_init() 已调用
* 函数说明1. 仅已建立连接且 send_ready 标志置位的 Socket 视为可写
*/ */
static int check_socket_writable(int sockfd) static int check_socket_writable(int sockfd) {
{ net_sock_t *p_sock;
net_sock_t *sock;
sock = net_get_sock(sockfd); p_sock = net_get_sock(sockfd);
if (sock == NULL) if (NULL == p_sock) {
{
return 0; return 0;
} }
/* 已建立连接的 Socket 发送缓冲区空闲时可写 */ /* 已建立连接的 Socket 发送缓冲区空闲时可写 */
if (sock->state == NET_SOCK_STATE_ESTABLISHED) if (NET_SOCK_STATE_ESTABLISHED == p_sock->state) {
{ if (p_sock->send_ready) {
if (sock->send_ready)
{
return 1; return 1;
} }
} }
@@ -296,22 +295,24 @@ static int check_socket_writable(int sockfd)
} }
/* /*
* 函数功能:检查 Socket 是否有错误 * 函数功能:检查指定 Socket 是否存在错误状态
* 返回值1 有错误0 无错误 * 入口参数sockfd - 待检查的 Socket 描述符 int 0 - NET_MAX_SOCKETS-1
* 出口参数:无
* 返回值1 - 有错误(含描述符越界或 Socket 未分配0 - 无错误 int
* 限定条件:无
* 函数说明1. 描述符越界视为错误
* 2. 未分配net_get_sock 返回 NULL的 Socket 视为错误
*/ */
static int check_socket_error(int sockfd) static int check_socket_error(int sockfd) {
{ net_sock_t *p_sock;
net_sock_t *sock;
/* 无效 fd 视为错误 */ /* 无效 fd 视为错误 */
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) if ((0 > sockfd) || (NET_MAX_SOCKETS <= sockfd)) {
{
return 1; return 1;
} }
sock = net_get_sock(sockfd); p_sock = net_get_sock(sockfd);
if (sock == NULL) if (NULL == p_sock) {
{
return 1; /* 未使用的 Socket 视为错误 */ return 1; /* 未使用的 Socket 视为错误 */
} }

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,33 @@ extern "C" {
#endif #endif
#include "net_types.h" #include "net_types.h"
#include "FreeRTOS.h"
#include "task.h"
/*
* 消息队列类型定义
*/
typedef enum {
NET_MSG_SEND,
NET_MSG_RECV,
NET_MSG_CLOSE,
NET_MSG_CONNECT,
NET_MSG_LISTEN,
NET_MSG_ACCEPT,
} net_msg_type_t;
typedef struct {
net_msg_type_t type; /* 操作类型 */
int sockfd; /* Socket 描述符 */
void *buf; /* 数据缓冲区send/recv */
int len; /* 数据长度/缓冲区大小 */
int flags; /* 接收标志 */
TaskHandle_t caller; /* 调用方任务句柄 */
int result; /* 操作结果 */
/* 连接参数 */
struct net_sockaddr_in addr; /* 连接地址 */
int addrlen; /* 地址长度 */
} net_msg_t;
/* /*
* 网络初始化接口 * 网络初始化接口
@@ -49,11 +76,11 @@ int net_poll(void);
* 函数功能:创建 socket * 函数功能:创建 socket
* 入口参数domain - 地址族,仅支持 AF_INET * 入口参数domain - 地址族,仅支持 AF_INET
* type - SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP) * type - SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP)
* protocol - 通常为 0
* 返回值socket 描述符 (0~7),失败返回 -1 * 返回值socket 描述符 (0~7),失败返回 -1
* 限定条件net_init() 已调用 * 限定条件net_init() 已调用
* 函数说明:自动分配空闲 Socket 控制块
*/ */
int net_socket(int domain, int type, int protocol); int net_socket(int domain, int type);
/* /*
* 函数功能:绑定本地地址和端口 * 函数功能:绑定本地地址和端口
@@ -61,17 +88,19 @@ int net_socket(int domain, int type, int protocol);
* addr - 本地地址结构体指针 * addr - 本地地址结构体指针
* addrlen - 地址结构体长度 * addrlen - 地址结构体长度
* 返回值0 成功,-1 失败 * 返回值0 成功,-1 失败
* 限定条件socket 已创建且未绑定
* 函数说明:设置 CH395F 源端口
*/ */
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen); int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
/* /*
* 函数功能TCP Server 监听 * 函数功能TCP Server 监听
* 入口参数sockfd - socket 描述符 * 入口参数sockfd - socket 描述符
* backlog - 最大等待连接数(多连接模式下为数据 Socket 数量)
* 返回值0 成功,-1 失败 * 返回值0 成功,-1 失败
* 限定条件socket 已绑定 * 限定条件socket 已绑定
* 函数说明:内部调用 ch395f_tcp_listen监听 Socket 自身即为数据通道
*/ */
int net_listen(int sockfd, int backlog); int net_listen(int sockfd);
/* /*
* 函数功能TCP Server 接受连接 * 函数功能TCP Server 接受连接
@@ -104,7 +133,8 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen);
* len - 数据长度 * len - 数据长度
* flags - 通常为 0 * flags - 通常为 0
* 返回值:实际发送字节数,失败返回 -1 * 返回值:实际发送字节数,失败返回 -1
* 限定条件TCP 连接已建立 * 限定条件TCP 连接已建立,指针非空
* 函数说明:阻塞等待发送缓冲区空闲,拷贝数据后触发发送
*/ */
int net_send(int sockfd, const void *buf, int len, int flags); int net_send(int sockfd, const void *buf, int len, int flags);
@@ -124,13 +154,15 @@ int net_recv(int sockfd, void *buf, int len, int flags);
/* /*
* 函数功能:发送 UDP 数据 * 函数功能:发送 UDP 数据
* 入口参数sockfd - socket 描述符 * 入口参数sockfd - socket 描述符
* buf - 数据缓冲区 * buf - 数据缓冲区
* len - 数据长度 * len - 数据长度
* flags - 通常为 0 * flags - 通常为 0
* dest_addr - 目标地址 * dest_addr - 目标地址
* addrlen - 地址长度 * addrlen - 地址长度
* 返回值:实际发送字节数,失败返回 -1 * 返回值:实际发送字节数,失败返回 -1
* 限定条件socket 已创建,指针非空
* 函数说明:设置目标地址后调用 CH395F 发送
*/ */
int net_sendto(int sockfd, const void *buf, int len, int flags, int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest_addr, int addrlen); const struct net_sockaddr *dest_addr, int addrlen);
@@ -144,6 +176,8 @@ int net_sendto(int sockfd, const void *buf, int len, int flags,
* src_addr - 输出:发送方地址 * src_addr - 输出:发送方地址
* addrlen - 输入输出:地址长度 * addrlen - 输入输出:地址长度
* 返回值:实际接收字节数,失败返回 -1 * 返回值:实际接收字节数,失败返回 -1
* 限定条件socket 已创建,指针非空
* 函数说明:读取 CH395F 接收缓冲区,获取对端地址
*/ */
int net_recvfrom(int sockfd, void *buf, int len, int flags, int net_recvfrom(int sockfd, void *buf, int len, int flags,
struct net_sockaddr *src_addr, int *addrlen); struct net_sockaddr *src_addr, int *addrlen);
@@ -152,6 +186,8 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
* 函数功能:关闭 socket * 函数功能:关闭 socket
* 入口参数sockfd - socket 描述符 * 入口参数sockfd - socket 描述符
* 返回值0 成功,-1 失败 * 返回值0 成功,-1 失败
* 限定条件socket 已打开
* 函数说明:释放 Socket 控制块TCP 先断开连接
*/ */
int net_close(int sockfd); int net_close(int sockfd);
@@ -165,6 +201,8 @@ int net_close(int sockfd);
* cb - 回调函数 * cb - 回调函数
* arg - 用户参数 * arg - 用户参数
* 返回值0 成功,-1 失败 * 返回值0 成功,-1 失败
* 限定条件socket 已创建
* 函数说明:回调在 net_poll 上下文中触发
*/ */
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg); int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg);
@@ -181,7 +219,9 @@ int net_get_errno(void);
/* /*
* 函数功能:将 IP 地址字符串转为网络字节序 * 函数功能:将 IP 地址字符串转为网络字节序
* 入口参数cp - 点分十进制字符串 "192.168.1.100" * 入口参数cp - 点分十进制字符串 "192.168.1.100"
* 返回值:网络字节序 IP * 返回值:网络字节序 IP,失败返回 0
* 限定条件cp 指针非空
* 函数说明:不支持域名解析
*/ */
uint32_t net_inet_addr(const char *cp); uint32_t net_inet_addr(const char *cp);
@@ -190,29 +230,54 @@ uint32_t net_inet_addr(const char *cp);
* 入口参数addr - 网络字节序 IP * 入口参数addr - 网络字节序 IP
* buf - 输出缓冲区(至少 16 字节) * buf - 输出缓冲区(至少 16 字节)
* 返回值buf 指针 * 返回值buf 指针
* 限定条件buf 指针非空
*/ */
char *net_inet_ntoa(uint32_t addr, char *buf); char *net_inet_ntoa(uint32_t addr, char *buf);
/* /*
* 函数功能:端口字节序转换(主机序 -> 网络序) * 函数功能:端口字节序转换(主机序 -> 网络序)
* 入口参数hostshort - 主机序端口
* 返回值:网络序端口
* 限定条件:无
* 函数说明:小端 → 大端
*/ */
uint16_t net_htons(uint16_t hostshort); uint16_t net_htons(uint16_t hostshort);
/* /*
* 函数功能:端口字节序转换(网络序 -> 主机序) * 函数功能:端口字节序转换(网络序 -> 主机序)
* 入口参数netshort - 网络序端口
* 返回值:主机序端口
* 限定条件:无
* 函数说明:大端 → 小端
*/ */
uint16_t net_ntohs(uint16_t netshort); uint16_t net_ntohs(uint16_t netshort);
/* /*
* 函数功能IP 地址字节序转换(主机序 -> 网络序) * 函数功能IP 地址字节序转换(主机序 -> 网络序)
* 入口参数hostlong - 主机序 IP
* 返回值:网络序 IP
* 限定条件:无
* 函数说明:小端 → 大端
*/ */
uint32_t net_htonl(uint32_t hostlong); uint32_t net_htonl(uint32_t hostlong);
/* /*
* 函数功能IP 地址字节序转换(网络序 -> 主机序) * 函数功能IP 地址字节序转换(网络序 -> 主机序)
* 入口参数netlong - 网络序 IP
* 返回值:主机序 IP
* 限定条件:无
* 函数说明:大端 → 小端
*/ */
uint32_t net_ntohl(uint32_t netlong); uint32_t net_ntohl(uint32_t netlong);
/*
* 函数功能:处理消息队列中的请求(必须在 netTask 主循环中调用)
* 限定条件net_init() 已调用
* 函数说明:处理其他任务通过 net_send/net_recv 等接口发送的请求,
* 在 netTask 上下文中串行化执行所有 CH395F 操作
*/
void net_process_messages(void);
/* /*
* 函数功能:获取 Socket 控制块指针(内部使用) * 函数功能:获取 Socket 控制块指针(内部使用)
* 入口参数sockfd - socket 描述符 * 入口参数sockfd - socket 描述符
@@ -220,6 +285,57 @@ uint32_t net_ntohl(uint32_t netlong);
*/ */
net_sock_t *net_get_sock(int sockfd); net_sock_t *net_get_sock(int sockfd);
/*
* 函数功能:获取 Socket 本地地址IP + Port
* 入口参数sockfd - socket 描述符
* addr - 输出:本地地址
* addrlen - 输入输出:地址结构体长度
* 返回值0 成功,-1 失败
* 限定条件socket 已创建或已绑定
*/
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen);
/*
* 内部 API需在 netTask 上下文中调用,绕过消息队列)
*/
/*
* 函数功能:发送数据(内部版本,直接操作,不经过消息队列)
* 入口参数p_sock - Socket 控制块指针
* buf - 数据缓冲区
* len - 数据长度
* 返回值:实际发送字节数,失败返回 -1
* 限定条件:必须在 netTask 上下文中调用
*/
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
/*
* 函数功能:接收数据(内部版本,直接操作,不经过消息队列)
* 入口参数p_sock - Socket 控制块指针
* buf - 接收缓冲区
* len - 缓冲区大小
* flags - 通常为 0可设置 NET_MSG_DONTWAIT
* 返回值实际接收字节数0=对端关闭,-1=错误
* 限定条件:必须在 netTask 上下文中调用
*/
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
/*
* 函数功能:关闭 socket内部版本直接操作不经过消息队列
* 入口参数p_sock - Socket 控制块指针
* 返回值0 成功,-1 失败
* 限定条件:必须在 netTask 上下文中调用
*/
int net_close_sock(net_sock_t *p_sock);
/*
* 函数功能:启动 TCP Server 监听(内部版本,直接操作,不经过消息队列)
* 入口参数sockfd - Socket 描述符
* 返回值0 成功,-1 失败
* 限定条件:必须在 netTask 上下文中调用
*/
int net_listen_locked(int sockfd);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -114,7 +114,6 @@ typedef enum {
NET_SOCK_STATE_CONNECTING, /* TCP Client 连接中 */ NET_SOCK_STATE_CONNECTING, /* TCP Client 连接中 */
NET_SOCK_STATE_ESTABLISHED, /* TCP 已连接 */ NET_SOCK_STATE_ESTABLISHED, /* TCP 已连接 */
NET_SOCK_STATE_UDP_OPEN, /* UDP 已打开 */ NET_SOCK_STATE_UDP_OPEN, /* UDP 已打开 */
NET_SOCK_STATE_TCP_ACCEPT, /* TCP Server 数据 Socket等待分配连接 */
NET_SOCK_STATE_TCP_CLIENT_RECONNECTING, /* TCP Client 重连中 */ NET_SOCK_STATE_TCP_CLIENT_RECONNECTING, /* TCP Client 重连中 */
} net_sock_state_t; } net_sock_state_t;
@@ -150,19 +149,23 @@ typedef struct {
uint16_t local_port; /* 本地端口(主机序) */ uint16_t local_port; /* 本地端口(主机序) */
uint16_t remote_port; /* 远端端口(主机序) */ uint16_t remote_port; /* 远端端口(主机序) */
uint32_t remote_ip; /* 远端 IP网络字节序 */ uint32_t remote_ip; /* 远端 IP网络字节序 */
uint8_t remote_ip_arr[4]; /* 远端 IP 数组(低字节在前 */ uint8_t remote_ip_arr[4]; /* 远端 IP 数组(大端/网络字节序 */
/* 事件回调(可选) */ /* 事件回调(可选) */
net_event_cb_t event_cb; net_event_cb_t event_cb;
void *event_arg; void *event_arg;
/* TCP Server 多连接模式:监听 Socket 的数据 Socket 列表 */
int data_socks[NET_TCP_SERVER_MAX_CLIENTS];
int data_sock_count;
/* 发送就绪标志 */ /* 发送就绪标志 */
uint8_t send_ready; uint8_t send_ready;
/* 接收可读标志net_poll 检测到 RECV_OK 时置 1net_recv_locked 读空后清零。
* net_select 据此判断可读性,从而无需在应用/测试任务上下文直接访问 CH395F SPI
* (所有 ch395f 事务统一由 netTask 串行执行,避免与 net_poll 竞争,见 Trap 16。 */
uint8_t readable;
/* 单连接模式自动重监听:连接断开后由 net 层自动回到 LISTENBSD 风格监听常驻) */
uint8_t auto_relisten;
/* 统计信息 */ /* 统计信息 */
uint32_t recv_bytes; /* 累计接收字节数 */ uint32_t recv_bytes; /* 累计接收字节数 */
uint32_t send_bytes; /* 累计发送字节数 */ uint32_t send_bytes; /* 累计发送字节数 */

View File

@@ -1,133 +1,152 @@
/* /*
* 模块名称RS-485 半双工通信驱动 * 模块名称RS-485 半双工通信驱动
* 模块功能RS-485 半双工通信驱动实现,核心要点: * 模块功能RS-485 半双工通信驱动实现,封装任意 UART 外设实现 485 方向控制,
* 1. 发送前拉高 DE切换到发送态 * 提供阻塞发送、中断接收IDLE 空闲帧检测)。使用 DWT CYCCNT 提供
* 2. HAL_UART_Transmit 内部已等待 TC发送完成标志 * 微秒级延时,确保 RS485 收发器 DE 使能稳定。
* 3. 发送完毕后拉低 DE切回接收态
* 4. 使用 HAL_UARTEx_ReceiveToIdle_IT 实现变长帧接收
* 参考ST AN3070 应用笔记、controllerstech.com RS485 教程
* 适用平台STM32F4 系列 * 适用平台STM32F4 系列
* 作者:王建锋 * 作者:王建锋
* 创建日期2026-07-18 * 创建日期2026-07-18
* 修改记录: * 修改记录:
* 2026-07-18 王建锋 创建初始版本 * 2026-07-18 王建锋 创建初始版本
* 2026-07-20 王建锋 修复 DE 时序:添加 DWT 微秒延时替代 for 循环
* 2026-07-20 王建锋 清理中文注释乱码,对齐代码规范
*/ */
#include "rs485.h" #include "rs485.h"
/* ======================== 内部辅助函数 ======================== */
/* /*
* 内部辅助宏 - 方向控制 * 函数功能DWT 微秒级延时
* 入口参数us - 延时微秒数 uint32_t 1 - N
* 返回值:无
* 限定条件:无(首次调用自动初始化 DWT CYCCNT
* 函数说明:利用 Cortex-M4 DWT CYCCNT 实现精确微秒延时168MHz 下 1us = 168 计数。
* 首次调用时自动使能 DWT 和 CYCCNT后续只读 CYCCNT 寄存器,不影响其他模块。
*/ */
static void delay_us(uint32_t us)
{
if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000uL);
while ((DWT->CYCCNT - start) < ticks) { ; }
}
/* ======================== 方向控制宏 ======================== */
#define RS485_DIR_TX(h) HAL_GPIO_WritePin((h)->dir_port, (h)->dir_pin, GPIO_PIN_SET) #define RS485_DIR_TX(h) HAL_GPIO_WritePin((h)->dir_port, (h)->dir_pin, GPIO_PIN_SET)
#define RS485_DIR_RX(h) HAL_GPIO_WritePin((h)->dir_port, (h)->dir_pin, GPIO_PIN_RESET) #define RS485_DIR_RX(h) HAL_GPIO_WritePin((h)->dir_port, (h)->dir_pin, GPIO_PIN_RESET)
/* /* ======================== 公共函数实现 ======================== */
* 公共函数实现
*/
/* /*
* 函数功能:初始化 RS-485 句柄 * 函数功能:初始化 RS-485 句柄
* 入口参数handle - RS-485 句柄指针 * 入口参数:p_handle - RS-485 句柄指针 rs485_handle_t*(非空)
* huart - UART 外设句柄 * p_huart - UART 外设句柄指针(已由 CubeMX 初始化) UART_HandleTypeDef*(非空)
* dir_port - DE 引脚 GPIO 端口 * p_dir_port - 方向控制 GPIO 端口(如 GPIOD GPIO_TypeDef*(非空)
* dir_pin - DE 引脚 GPIO 编号 * dir_pin - 方向控制 GPIO 引脚号(如 GPIO_PIN_0 uint16_t
* 返回值:无 * 返回值:无
* 限定条件UART 和 GPIO 须先由 CubeMX 完成初始化 * 限定条件UART 和 GPIO 须先由 CubeMX 完成初始化
* 函数说明:初始化后立即将 DE 拉低,使 RS-485 收发器处于接收状态 * 函数说明:初始化后默认处于接收状态DE 引脚拉低)
*/ */
void rs485_init(rs485_handle_t *handle, void rs485_init(rs485_handle_t *p_handle,
UART_HandleTypeDef *huart, UART_HandleTypeDef *p_huart,
GPIO_TypeDef *dir_port, GPIO_TypeDef *p_dir_port,
uint16_t dir_pin) uint16_t dir_pin)
{ {
handle->huart = huart; p_handle->huart = p_huart;
handle->dir_port = dir_port; p_handle->dir_port = p_dir_port;
handle->dir_pin = dir_pin; p_handle->dir_pin = dir_pin;
handle->rx_size = 0; p_handle->rx_size = 0;
/* 默认进入接收状态 */ RS485_DIR_RX(p_handle);
RS485_DIR_RX(handle);
} }
/* /*
* 函数功能:阻塞方式发送数据 * 函数功能:阻塞方式发送数据
* 入口参数handle - RS-485 句柄指针 * 入口参数:p_handle - RS-485 句柄指针 rs485_handle_t*(非空)
* data - 待发送数据缓冲区 * p_data - 待发送数据缓冲区 uint8_t*(非空)
* len - 待发送字节数 * len - 待发送字节数 uint16_t 1 - N
* timeout - 发送超时ms传 0 使用默认值 * timeout - 发送超时ms传 0 使用 RS485_TIMEOUT_DEFAULT uint32_t 0 / 1 - N
* 返回值HAL_OK / HAL_TIMEOUT / HAL_ERROR * 返回值HAL_OK - 发送成功 HAL_StatusTypeDef
* 限定条件:须在主循环或 RTOS 任务中调用,不可在 UART 中断中调用 * HAL_TIMEOUT - 发送超时
* 函数说明: * HAL_ERROR - 发送错误
* 时序DE=HIGH → UART 发送 N 字节 → 等 TC → DE=LOW * 限定条件:须在主循环或 FreeRTOS 任务中调用,不可在 UART 中断中调用
* HAL_UART_Transmit 内部会等待 TXE发送寄存器空逐字节搬数据 * 函数说明:发送前拉高 DE等待 50us 确保 RS485 收发器驱动使能稳定后再启动 UART 发送。
* 最后还会等待 TC发送完成标志确保最后一字节的停止位已移出 * HAL_UART_Transmit 内部等待 TC发送完成标志确保最后一字节完全移出
* 因此返回后再拉低 DE 是安全的 * 再拉低 DE 切回接收状态。
*/ */
HAL_StatusTypeDef rs485_transmit(rs485_handle_t *handle, HAL_StatusTypeDef rs485_transmit(rs485_handle_t *p_handle,
const uint8_t *data, const uint8_t *p_data,
uint16_t len, uint16_t len,
uint32_t timeout) uint32_t timeout)
{ {
HAL_StatusTypeDef status; HAL_StatusTypeDef status = HAL_OK;
if (timeout == 0U) { if (timeout == 0U) {
timeout = RS485_TIMEOUT_DEFAULT; timeout = RS485_TIMEOUT_DEFAULT;
} }
/* 1. 切换到发送状态 */ RS485_DIR_TX(p_handle);
RS485_DIR_TX(handle);
/* 2. 阻塞发送(内部等待 TC */ delay_us(50);
status = HAL_UART_Transmit(handle->huart,
(uint8_t *)data, status = HAL_UART_Transmit(p_handle->huart,
(uint8_t *)p_data,
len, len,
timeout); timeout);
/* 3. 发送完毕或超时,切回接收状态 */ RS485_DIR_RX(p_handle);
RS485_DIR_RX(handle);
return status; return status;
} }
/* /*
* 函数功能启动中断方式接收IDLE 空闲帧检测) * 函数功能启动中断方式接收IDLE 空闲帧检测)
* 入口参数handle - RS-485 句柄指针 * 入口参数:p_handle - RS-485 句柄指针 rs485_handle_t*(非空)
* buf - 接收缓冲区 * p_buf - 接收缓冲区 uint8_t*(非空)
* buf_size - 缓冲区最大容量 * buf_size - 缓冲区大小 uint16_t
* 返回值HAL_OK / HAL_ERROR * 返回值HAL_OK - 启动成功 HAL_StatusTypeDef
* HAL_ERROR - 启动失败
* 限定条件UART 须已开启全局中断NVIC 使能) * 限定条件UART 须已开启全局中断NVIC 使能)
* 函数说明: * 函数说明:使用 HAL_UARTEx_ReceiveToIdle_IT 实现变长帧接收。
* 使用 HAL_UARTEx_ReceiveToIdle_IT 实现变长帧接收。 * 当 UART 总线空闲超过 1 个字符时间后硬件触发 IDLE 中断,
* 当 UART 总线空闲超过 1 个字符时间后,硬件触发 IDLE 中断, * HAL 自动调用 HAL_UARTEx_RxEventCallback 并返回已接收字节数。
* HAL 自动调用 HAL_UARTEx_RxEventCallback 并返回已接收字节数 * 用户须在回调中重新调用本函数重新开启接收
* 用户须在该回调中重新调用本函数重新开启接收
*/ */
HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *handle, HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *p_handle,
uint8_t *buf, uint8_t *p_buf,
uint16_t buf_size) uint16_t buf_size)
{ {
handle->rx_size = 0; p_handle->rx_size = 0;
return HAL_UARTEx_ReceiveToIdle_IT(handle->huart, buf, buf_size); return HAL_UARTEx_ReceiveToIdle_IT(p_handle->huart, p_buf, buf_size);
} }
/* /*
* 函数功能:缓存最近一次接收的字节数 * 函数功能:缓存最近一次接收的字节数
* 入口参数handle - RS-485 句柄指针 * 入口参数:p_handle - RS-485 句柄指针 rs485_handle_t*(非空)
* size - 本次接收到的字节数 * size - 本次接收到的字节数 uint16_t
* 返回值:无 * 返回值:无
* 限定条件:须在 HAL_UARTEx_RxEventCallback 中调用 * 限定条件:须在 HAL_UARTEx_RxEventCallback 中调用
* 函数说明:将 HAL 回调中接收到的数据大小回写到句柄的 rx_size 字段
*/ */
void rs485_rx_set_size(rs485_handle_t *handle, uint16_t size) void rs485_rx_set_size(rs485_handle_t *p_handle, uint16_t size)
{ {
handle->rx_size = size; p_handle->rx_size = size;
} }
/* /*
* 函数功能:获取最近一次接收的字节数 * 函数功能:获取最近一次接收的字节数
* 入口参数handle - RS-485 句柄指针 * 入口参数:p_handle - RS-485 句柄指针 const rs485_handle_t*(非空)
* 返回值:字节数 * 返回值:最近一次接收的字节数 uint16_t
* 限定条件:须在 HAL_UARTEx_RxEventCallback 触发后调用
* 函数说明:获取 rx_size 字段中缓存的接收字节数
*/ */
uint16_t rs485_rx_get_size(const rs485_handle_t *handle) uint16_t rs485_rx_get_size(const rs485_handle_t *p_handle)
{ {
return handle->rx_size; return p_handle->rx_size;
} }

View File

@@ -50,9 +50,9 @@ typedef struct {
* 限定条件UART 和 GPIO 须先由 CubeMX 完成初始化 * 限定条件UART 和 GPIO 须先由 CubeMX 完成初始化
* 函数说明初始化后默认处于接收状态DE 引脚拉低) * 函数说明初始化后默认处于接收状态DE 引脚拉低)
*/ */
void rs485_init(rs485_handle_t *handle, void rs485_init(rs485_handle_t *p_handle,
UART_HandleTypeDef *huart, UART_HandleTypeDef *p_huart,
GPIO_TypeDef *dir_port, GPIO_TypeDef *p_dir_port,
uint16_t dir_pin); uint16_t dir_pin);
/* /*
@@ -66,8 +66,8 @@ void rs485_init(rs485_handle_t *handle,
* 函数说明:发送前自动拉高 DE发送完成后等待 TC 标志再拉低 DE * 函数说明:发送前自动拉高 DE发送完成后等待 TC 标志再拉低 DE
* 确保最后一字节完全移出后再切换到接收状态 * 确保最后一字节完全移出后再切换到接收状态
*/ */
HAL_StatusTypeDef rs485_transmit(rs485_handle_t *handle, HAL_StatusTypeDef rs485_transmit(rs485_handle_t *p_handle,
const uint8_t *data, const uint8_t *p_data,
uint16_t len, uint16_t len,
uint32_t timeout); uint32_t timeout);
@@ -82,8 +82,8 @@ HAL_StatusTypeDef rs485_transmit(rs485_handle_t *handle,
* 收到完整帧后在 RxEventCallback 中通知用户。 * 收到完整帧后在 RxEventCallback 中通知用户。
* 用户须在回调中重新调用本函数重新开启接收 * 用户须在回调中重新调用本函数重新开启接收
*/ */
HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *handle, HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *p_handle,
uint8_t *buf, uint8_t *p_buf,
uint16_t buf_size); uint16_t buf_size);
/* /*
@@ -93,7 +93,7 @@ HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *handle,
* 限定条件:须在 HAL_UART_RxCpltCallback / HAL_UARTEx_RxEventCallback 中调用 * 限定条件:须在 HAL_UART_RxCpltCallback / HAL_UARTEx_RxEventCallback 中调用
* 函数说明:将 HAL 回调中接收到的数据大小回写到句柄的 rx_size 字段 * 函数说明:将 HAL 回调中接收到的数据大小回写到句柄的 rx_size 字段
*/ */
void rs485_rx_set_size(rs485_handle_t *handle, uint16_t size); void rs485_rx_set_size(rs485_handle_t *p_handle, uint16_t size);
/* /*
* 函数功能:获取最近一次接收的字节数 * 函数功能:获取最近一次接收的字节数
@@ -101,7 +101,7 @@ void rs485_rx_set_size(rs485_handle_t *handle, uint16_t size);
* 返回值:最近一次接收的字节数 * 返回值:最近一次接收的字节数
* 限定条件:须在 HAL_UARTEx_RxEventCallback 触发后调用 * 限定条件:须在 HAL_UARTEx_RxEventCallback 触发后调用
*/ */
uint16_t rs485_rx_get_size(const rs485_handle_t *handle); uint16_t rs485_rx_get_size(const rs485_handle_t *p_handle);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -1,39 +1,52 @@
/* /*
* 模块名称SD2506API-G RTC 实时时钟驱动 * 模块名称SD2506API-G RTC Driver
* 模块功能SD2506API-G 高精度温补实时时钟模块 I2C 驱动 * 模块功能SD2506API-G 高精度温补实时时钟驱动,I2C 接口
* 适用平台STM32F407ZGT6I2C1 接口 (PB6-SCL, PB7-SDA) * 适用平台STM32F407ZGTx + SD2506API-G 实时时钟芯片I2C1: PB6-SCL, PB7-SDA
* 作者:王建锋 * 作者:王建锋
* 创建日期2026-07-17 * 创建日期2026-07-17
* 修改记录: * 修改记录:
* 2026-07-17 王建锋 创建初始版本,参考 SD2506API-G Ver2.0 手册 * 2026-07-17 王建锋 创建初始版本,参考 SD2506API-G Ver2.0 手册
*/ */
#include <string.h>
#include <stdio.h>
#include "sd2506.h" #include "sd2506.h"
#include "i2c.h" #include "i2c.h"
#include <string.h>
extern I2C_HandleTypeDef hi2c1; extern I2C_HandleTypeDef hi2c1;
/* ======================== 内部辅助函数 ======================== */ /* ======================== 内部辅助函数 ======================== */
uint8_t sd2506_bcd_to_dec(uint8_t bcd) /*
{ * 函数功能BCD 码转十进制数
* 入口参数bcd - BCD 编码值 uint8_t 0x00 - 0x99
* 返回值:十进制数值 uint8_t
* 限定条件:输入为合法 BCD 编码(高 4 位和低 4 位均 <= 9
* 函数说明:高 4 位为十位,低 4 位为个位,分别乘 10 和加后返回
*/
uint8_t sd2506_bcd_to_dec(uint8_t bcd) {
return ((bcd >> 4) * 10) + (bcd & 0x0FU); return ((bcd >> 4) * 10) + (bcd & 0x0FU);
} }
uint8_t sd2506_dec_to_bcd(uint8_t dec) /*
{ * 函数功能:十进制转 BCD 码
* 入口参数dec - 十进制数 uint8_t 0 - 99 */
uint8_t sd2506_dec_to_bcd(uint8_t dec) {
return ((dec / 10) << 4) | (dec % 10); return ((dec / 10) << 4) | (dec % 10);
} }
/* /*
* 写单字节寄存器 * 函数功能:写单字节寄存器
* reg: 寄存器地址 (00H~79H) * 入口参数:reg - 寄存器地址 uint8_t 0x00 - 0x79
* val: 写入 * val - 写入数据 uint8_t 0x00 - 0xFF
* 返回: 0=成功, -2=I2C错误 * 返回0-成功2-I2C 错误 int
* 限定条件I2C 外设已初始化
* 函数说明:通过 I2C 向指定地址写入 1 字节数据
* 限定条件I2C 外设已初始化
* 函数说明:通过 I2C 向指定地址写入 1 字节
*/ */
static int sd2506_write_reg(uint8_t reg, uint8_t val) static int sd2506_write_reg(uint8_t reg, uint8_t val) {
{
if (HAL_I2C_Mem_Write(&hi2c1, SD2506_I2C_ADDR_WRITE, reg, if (HAL_I2C_Mem_Write(&hi2c1, SD2506_I2C_ADDR_WRITE, reg,
I2C_MEMADD_SIZE_8BIT, &val, 1, I2C_MEMADD_SIZE_8BIT, &val, 1,
SD2506_I2C_TIMEOUT_MS) != HAL_OK) { SD2506_I2C_TIMEOUT_MS) != HAL_OK) {
@@ -43,15 +56,17 @@ static int sd2506_write_reg(uint8_t reg, uint8_t val)
} }
/* /*
* 读单字节寄存器 * 函数功能:读单字节寄存器
* reg: 寄存器地址 (00H~79H) * 入口参数:reg - 寄存器地址 uint8_t 0x00 - 0x79
* val: 读取值指针 * p_val - 读取值指针 uint8_t*(非空)
* 返回: 0=成功, -2=I2C错误 * 返回0-成功2-I2C 错误 int
* 限定条件I2C 外设已初始化p_val 为非空指针
* 函数说明:通过 I2C 从指定地址读取 1 字节数据
* 限定条件I2C 外设已初始化val 为非空指针函数说明:通过 I2C 从指定地址读取 1 字节
*/ */
static int sd2506_read_reg(uint8_t reg, uint8_t *val) static int sd2506_read_reg(uint8_t reg, uint8_t *p_val) {
{
if (HAL_I2C_Mem_Read(&hi2c1, SD2506_I2C_ADDR_READ, reg, if (HAL_I2C_Mem_Read(&hi2c1, SD2506_I2C_ADDR_READ, reg,
I2C_MEMADD_SIZE_8BIT, val, 1, I2C_MEMADD_SIZE_8BIT, p_val, 1,
SD2506_I2C_TIMEOUT_MS) != HAL_OK) { SD2506_I2C_TIMEOUT_MS) != HAL_OK) {
return SD2506_I2C_ERROR; return SD2506_I2C_ERROR;
} }
@@ -59,16 +74,18 @@ static int sd2506_read_reg(uint8_t reg, uint8_t *val)
} }
/* /*
* 写多字节寄存器 (从 reg 开始连续写入 len 字节) * 函数功能:写多字节寄存器(连续写)
* reg: 起始寄存器地址 * 入口参数:reg - 起始寄存器地址 uint8_t 0x00 - 0x79
* data: 数据缓冲区 * p_data - 数据缓冲区指针 uint8_t*(非空)
* len: 数据长度 * len - 数据长度 uint8_t 1 - N
* 返回: 0=成功, -2=I2C错误 * 返回0-成功2-I2C 错误 int
* 限定条件I2C 外设已初始化p_data 为非空指针
* 函数说明:从 reg 开始连续写入 len 字节数据
* 限定条件I2C 外设已初始化data 为非空指针
*/ */
static int sd2506_write_regs(uint8_t reg, const uint8_t *data, uint8_t len) static int sd2506_write_regs(uint8_t reg, const uint8_t *p_data, uint8_t len) {
{
if (HAL_I2C_Mem_Write(&hi2c1, SD2506_I2C_ADDR_WRITE, reg, if (HAL_I2C_Mem_Write(&hi2c1, SD2506_I2C_ADDR_WRITE, reg,
I2C_MEMADD_SIZE_8BIT, (uint8_t *)data, len, I2C_MEMADD_SIZE_8BIT, (uint8_t *)p_data, len,
SD2506_I2C_TIMEOUT_MS) != HAL_OK) { SD2506_I2C_TIMEOUT_MS) != HAL_OK) {
return SD2506_I2C_ERROR; return SD2506_I2C_ERROR;
} }
@@ -76,16 +93,18 @@ static int sd2506_write_regs(uint8_t reg, const uint8_t *data, uint8_t len)
} }
/* /*
* 读多字节寄存器 (从 reg 开始连续读取 len 字节) * 函数功能:读多字节寄存器(连续读)
* reg: 起始寄存器地址 * 入口参数:reg - 起始寄存器地址 uint8_t 0x00 - 0x79
* data: 数据缓冲区 * p_data - 数据缓冲区指针 uint8_t*(非空)
* len: 数据长度 * len - 数据长度 uint8_t 1 - N
* 返回: 0=成功, -2=I2C错误 * 返回0-成功2-I2C 错误 int
* 限定条件I2C 外设已初始化p_data 为非空指针
* 函数说明:从 reg 开始连续读取 len 字节数据
* 限定条件I2C 外设已初始化data 为非空指针
*/ */
static int sd2506_read_regs(uint8_t reg, uint8_t *data, uint8_t len) static int sd2506_read_regs(uint8_t reg, uint8_t *p_data, uint8_t len) {
{
if (HAL_I2C_Mem_Read(&hi2c1, SD2506_I2C_ADDR_READ, reg, if (HAL_I2C_Mem_Read(&hi2c1, SD2506_I2C_ADDR_READ, reg,
I2C_MEMADD_SIZE_8BIT, data, len, I2C_MEMADD_SIZE_8BIT, p_data, len,
SD2506_I2C_TIMEOUT_MS) != HAL_OK) { SD2506_I2C_TIMEOUT_MS) != HAL_OK) {
return SD2506_I2C_ERROR; return SD2506_I2C_ERROR;
} }
@@ -93,38 +112,41 @@ static int sd2506_read_regs(uint8_t reg, uint8_t *data, uint8_t len)
} }
/* /*
* 开启写保护 (允许写入 00H~71H 寄存器) * 函数功能:开启写保护允许写入 00H~71H 寄存器
* 顺序: 先写 WRTC1=1, 再写 WRTC2=1 + WRTC3=1 * 入口参数:无
* 返回值0 - 成功 2 - I2C 错误
* 限定条件I2C 外设已初始化
* 函数说明:顺序:先写 WRTC1=1, 再写 WRTC2=1 + WRTC3=1
*/ */
static int sd2506_write_enable(void) static int sd2506_write_enable(void) {
{ int ret = 0;
int ret;
/* 先置 WRTC1=1 (bit6=1), 其它位参考手册: 0x84 */ /* 写允许顺序SD2506/SD24xx 手册要求):先置 WRTC1(CTR2 bit7)=1
ret = sd2506_write_reg(SD2506_REG_CTR1, 0x84U); * 再置 WRTC2(CTR1 bit2) + WRTC3(CTR1 bit7)=1。顺序反了芯片不会解开写保护。 */
ret = sd2506_write_reg(SD2506_REG_CTR2, 0x80U);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
/* 再置 WRTC2=1(bit5) + WRTC3=1(bit4): 0x8C */ ret = sd2506_write_reg(SD2506_REG_CTR1, 0x84U);
ret = sd2506_write_reg(SD2506_REG_CTR2, 0x8CU);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
return SD2506_OK; return SD2506_OK;
} }
/* /*
* 关闭写保护 (禁止写入 00H~71H 寄存器) * 函数功能:关闭写保护禁止写入 00H~71H 寄存器
* 顺序: 先写 WRTC2=0 + WRTC3=0, 再写 WRTC1=0 * 入口参数:无
* 返回值0 - 成功 2 - I2C 错误
* 限定条件I2C 外设已初始化
* 函数说明:顺序:先写 WRTC2=0 + WRTC3=0, 再写 WRTC1=0
*/ */
static int sd2506_write_disable(void) static int sd2506_write_disable(void) {
{ int ret = 0;
int ret;
/* 先清 WRTC2=0, WRTC3=0: 0x00 */ /* 写禁止顺序(手册要求):先清 WRTC2/WRTC3(CTR1),再清 WRTC1(CTR2) */
ret = sd2506_write_reg(SD2506_REG_CTR2, 0x00U); ret = sd2506_write_reg(SD2506_REG_CTR1, 0x00U);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
/* 再清 WRTC1=0, 同时 ARST=1 使能自动复位: 0x20 */ ret = sd2506_write_reg(SD2506_REG_CTR2, 0x00U);
ret = sd2506_write_reg(SD2506_REG_CTR1, SD2506_CTR1_WRITE_OFF);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
return SD2506_OK; return SD2506_OK;
@@ -132,28 +154,40 @@ static int sd2506_write_disable(void)
/* ======================== 公共 API 实现 ======================== */ /* ======================== 公共 API 实现 ======================== */
int sd2506_init(void) /*
{ * 函数功能:初始化 SD2506API-G RTC
int ret; * 入口参数:无
uint8_t id[8]; * 返回值0-成功2-I2C 错误 int
* 限定条件CubeMX 已完成 I2C1 初始化PB6-SCL, PB7-SDA
* 函数说明1. 读取芯片 ID 验证通信
* 2. 上电重置充电寄存器 (0x18=0x82)
* 3. 配置 24 小时制、开自动复位
* 限定条件CubeMX 已完成I2C1 初始化
* 函数说明:读取芯片 ID 验证通信
* 2. 上电重置充电寄存器18H=82H
* 3. 配置 24 小时制、开自动复位
*/
int sd2506_init(void) {
int ret = 0;
uint8_t a_id[8] = {0};
/* 验证 I2C 通信: 尝试读取 8 字节 ID */ /* 验证 I2C 通信: 尝试读取 8 字节 ID */
ret = sd2506_read_regs(SD2506_REG_ID_START, id, SD2506_ID_SIZE); ret = sd2506_read_regs(SD2506_REG_ID_START, a_id, SD2506_ID_SIZE);
if (ret != SD2506_OK) { if (ret != SD2506_OK) {
return ret; return ret;
} }
/* 上电重置充电寄存器 18H = 82H (开启充电, 5K电阻) /* 上电重置充电寄存器 18H = 82H (开启充电 5K电阻)
* 手册强烈建议每次上电时重置此 */ * 手册强烈建议每次上电时重置此寄存器 */
ret = sd2506_write_reg(SD2506_REG_CHARGE, 0x82U); ret = sd2506_write_reg(SD2506_REG_CHARGE, 0x82U);
if (ret != SD2506_OK) { if (ret != SD2506_OK) {
return ret; return ret;
} }
/* 配置: 24小时制, 自动复位使能 /* 配置: 24小时制,写允许状态下 0FH 的 WRTC 位必须为 1
* 0FH = 0x20 (bit5=ARST=1, 其它标志位清零) * 0FH = 0x20 (bit5=ARST=1, 其它标志位清零)
* 写入时需注意: 写允许状态下 0FH 的 WRTC 位必须为1 * 注意: 写入时需注意 0FH 的 WRTC 位必须为 1
* 此处直接写入 0x20 即可 (ARST=1, WRTC 位) */ * 此处直接写入 0x20 即可 (ARST=1, WRTC 位) */
ret = sd2506_write_reg(SD2506_REG_CTR1, SD2506_CTR1_WRITE_OFF); ret = sd2506_write_reg(SD2506_REG_CTR1, SD2506_CTR1_WRITE_OFF);
if (ret != SD2506_OK) { if (ret != SD2506_OK) {
return ret; return ret;
@@ -162,30 +196,38 @@ int sd2506_init(void)
return SD2506_OK; return SD2506_OK;
} }
int sd2506_set_time(const sd2506_time_t *time) /*
{ * 函数功能:设置 RTC 时间日期
int ret; * 入口参数p_time - 时间结构体指针,包含要设置的时间
uint8_t buf[7]; * 返回值0 - 成功2 - I2C 错误
* 限定条件sd2506_init() 已调用成功
* 函数说明1. 先开写保护
* 2. 一次性写入 7 字节时间数据 (00H~06H)
* 3. 关闭写保护
*/
int sd2506_set_time(const sd2506_time_t *p_time) {
int ret = 0;
uint8_t buf[7] = {0};
if (time == NULL) { if (p_time == NULL) {
return SD2506_ERROR; return SD2506_ERROR;
} }
/* 组装 7 字节时间数据 (BCD 码) */ /* 组装 7 字节时间数据 (BCD 码) */
buf[0] = sd2506_dec_to_bcd(time->second); /* 00H: 秒 */ buf[0] = sd2506_dec_to_bcd(p_time->second); /* 00H: 秒 */
buf[1] = sd2506_dec_to_bcd(time->minute); /* 01H: 分 */ buf[1] = sd2506_dec_to_bcd(p_time->minute); /* 01H: 分 */
buf[2] = sd2506_dec_to_bcd(time->hour) | 0x80U; /* 02H: 时 (bit7=1, 24小时制) */ buf[2] = sd2506_dec_to_bcd(p_time->hour) | 0x80U; /* 02H: 时(bit7=1, 24小时制) */
buf[3] = sd2506_dec_to_bcd(time->week); /* 03H: 星期 */ buf[3] = sd2506_dec_to_bcd(p_time->week); /* 03H: 星期 */
buf[4] = sd2506_dec_to_bcd(time->day); /* 04H: 日 */ buf[4] = sd2506_dec_to_bcd(p_time->day); /* 04H: 日 */
buf[5] = sd2506_dec_to_bcd(time->month); /* 05H: 月 */ buf[5] = sd2506_dec_to_bcd(p_time->month); /* 05H: 月 */
buf[6] = sd2506_dec_to_bcd(time->year - 2000U); /* 06H: 年 */ buf[6] = sd2506_dec_to_bcd(p_time->year - 2000U); /* 06H: 年 */
/* 开启写保护 */ /* 开启写保护 */
ret = sd2506_write_enable(); ret = sd2506_write_enable();
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
/* 一次性写入 7 字节时间数据 (00H~06H) /* 一次性写入 7 字节时间数据 (00H~06H)
* 手册要求: 不可单独写某一个时间寄存器,否则可能引起错误进 */ * 手册要求: 不可单独写某一个时间寄存器,否则可能引起错误进 */
ret = sd2506_write_regs(SD2506_REG_SEC, buf, 7); ret = sd2506_write_regs(SD2506_REG_SEC, buf, 7);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
@@ -196,39 +238,54 @@ int sd2506_set_time(const sd2506_time_t *time)
return SD2506_OK; return SD2506_OK;
} }
int sd2506_get_time(sd2506_time_t *time) /*
{ * 函数功能:读取 RTC 时间日期
int ret; * 入口参数p_time - 输出时间结构体指针 sd2506_time_t*(非空)
uint8_t buf[7]; * 返回值0-成功2-I2C 错误 int
* 限定条件sd2506_init() 已调用成功
* 函数说明:一次读取 7 字节时间数据 (00H~06H),所有实时数据被锁存避免错误
* note: sd2506_init() must be called first
*/
int sd2506_get_time(sd2506_time_t *p_time) {
int ret = 0;
uint8_t buf[7] = {0};
if (time == NULL) { if (p_time == NULL) {
return SD2506_ERROR; return SD2506_ERROR;
} }
/* 一次读取 7 字节时间数据 (00H~06H) /* 一次读取 7 字节时间数据 (00H~06H)
* 手册说明: 读取时所有实时数据被锁存,避免错 */ * 手册说明: 读取时所有实时数据被锁存,避免错 */
ret = sd2506_read_regs(SD2506_REG_SEC, buf, 7); ret = sd2506_read_regs(SD2506_REG_SEC, buf, 7);
if (ret != SD2506_OK) { if (ret != SD2506_OK) {
return ret; return ret;
} }
time->second = sd2506_bcd_to_dec(buf[0] & 0x7FU); /* 00H: 秒 */ p_time->second = sd2506_bcd_to_dec(buf[0] & 0x7FU); /* 00H: 秒 */
time->minute = sd2506_bcd_to_dec(buf[1] & 0x7FU); /* 01H: 分 */ p_time->minute = sd2506_bcd_to_dec(buf[1] & 0x7FU); /* 01H: 分 */
time->hour = sd2506_bcd_to_dec(buf[2] & 0x7FU); /* 02H: 屏蔽 bit7 (12/24标志) */ p_time->hour = sd2506_bcd_to_dec(buf[2] & 0x7FU); /* 02H: 屏蔽 bit7 (12/24标志) */
time->week = sd2506_bcd_to_dec(buf[3] & 0x07U); /* 03H: 星期 */ p_time->week = sd2506_bcd_to_dec(buf[3] & 0x07U); /* 03H: 星期 */
time->day = sd2506_bcd_to_dec(buf[4] & 0x3FU); /* 04H: 日 */ p_time->day = sd2506_bcd_to_dec(buf[4] & 0x3FU); /* 04H: 日 */
time->month = sd2506_bcd_to_dec(buf[5] & 0x1FU); /* 05H: 月 */ p_time->month = sd2506_bcd_to_dec(buf[5] & 0x1FU); /* 05H: 月 */
time->year = sd2506_bcd_to_dec(buf[6]) + 2000U; /* 06H: 年 */ p_time->year = sd2506_bcd_to_dec(buf[6]) + 2000U; /* 06H: 年 */
return SD2506_OK; return SD2506_OK;
} }
int sd2506_get_temperature(int8_t *temp) /*
{ * 函数功能:读取内部温度
int ret; * 入口参数p_temp - 输出温度值指针 int8_t*(非空)
uint8_t val; * 返回值0-成功2-I2C 错误 int
* 限定条件sd2506_init() 已调用成功
* 函数说明:读取寄存器 0x16bit7 为符号位
* return: 0=OK, -2=I2C error
* note: reads register 0x16
*/
int sd2506_get_temperature(int8_t *p_temp) {
int ret = 0;
uint8_t val = 0;
if (temp == NULL) { if (p_temp == NULL) {
return SD2506_ERROR; return SD2506_ERROR;
} }
@@ -239,52 +296,79 @@ int sd2506_get_temperature(int8_t *temp)
/* bit7 为符号位, 其余为温度值 */ /* bit7 为符号位, 其余为温度值 */
if (val & 0x80U) { if (val & 0x80U) {
/* 负温度: 取补码 */ /* 负温度 取补码 */
*temp = (int8_t)(val | 0xF0U); *p_temp = (int8_t)(val | 0xF0U);
} else { } else {
/* 正温度 */ /* 正温度 */
*temp = (int8_t)(val & 0x7FU); *p_temp = (int8_t)(val & 0x7FU);
} }
return SD2506_OK; return SD2506_OK;
} }
int sd2506_get_battery_voltage(uint16_t *voltage) /*
{ * 函数功能:读取电池电压(毫伏)
int ret; * 入口参数p_voltage - 输出电压指针 uint16_t*(非空)
uint8_t val_high, val_low; * 返回值0-成功2-I2C 错误 int
* 限定条件sd2506_init() 已调用成功
* 函数说明:组合 9 位值 (bit8=BAT8_VAL, bit7~0=BAT_VL),转换为毫伏
* return: 0=OK, -2=I2C error
* note: combines 9-bit value
*/
int sd2506_get_battery_voltage(uint16_t *p_voltage) {
int ret = 0;
uint8_t val_high = 0;
uint8_t val_low = 0;
if (voltage == NULL) { if (p_voltage == NULL) {
return SD2506_ERROR; return SD2506_ERROR;
} }
/* 读取 1AH bit7 (BAT8_VAL) 1BH (BAT_VL) */ /* 读取 1AH bit7 (BAT8_VAL) 1BH (BAT_VL) */
ret = sd2506_read_reg(SD2506_REG_CTR5, &val_high); ret = sd2506_read_reg(SD2506_REG_CTR5, &val_high);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
ret = sd2506_read_reg(SD2506_REG_BAT_VAL, &val_low); ret = sd2506_read_reg(SD2506_REG_BAT_VAL, &val_low);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
/* 组合 9 位: bit8=BAT8_VAL(1AH bit7), bit7~0=BAT_VL(1BH) /* 组合 9 位 bit8=BAT8_VAL(1AH bit7), bit7~0=BAT_VL(1BH)
* 1AH=80H, 1BH=30H => 电压 = 0x130 = 304 => 3.04V => 3040mV */ * 例: 1AH=80H, 1BH=30H => 电压 = 0x130 = 304 => 3.04V => 3040mV */
uint16_t raw = ((val_high & 0x80U) << 1) | val_low; uint16_t raw = ((val_high & 0x80U) << 1) | val_low;
*voltage = raw * 10; /* 转换为毫伏 (raw 单位 0.01V) */ *p_voltage = raw * 10; /* 转换为毫伏 (raw 单位 0.01V) */
return SD2506_OK; return SD2506_OK;
} }
int sd2506_get_id(uint8_t id[8]) /*
{ * 函数功能:读取 8 字节芯片 ID
if (id == NULL) { * 入口参数p_id - 输出缓冲区指针 uint8_t[8](非空)
* 返回值0-成功2-I2C 错误 int
* 限定条件sd2506_init() 已调用成功
* 函数说明:读取寄存器 0x72~0x79
* return: 0=OK, -2=I2C error
* note: reads registers 0x72-0x79
*/
int sd2506_get_id(uint8_t p_id[8]) {
if (p_id == NULL) {
return SD2506_ERROR; return SD2506_ERROR;
} }
return sd2506_read_regs(SD2506_REG_ID_START, id, SD2506_ID_SIZE); return sd2506_read_regs(SD2506_REG_ID_START, p_id, SD2506_ID_SIZE);
} }
int sd2506_read_sram(uint8_t addr, uint8_t *buf, uint8_t len) /*
{ * 函数功能:读取 SRAM 数据
if (buf == NULL || len == 0 || addr >= SD2506_SRAM_SIZE) { * 入口参数addr - 起始地址 uint8_t 0-69
* p_buf - 输出缓冲区指针 uint8_t*(非空)
* len - 读取长度 uint8_t 1-N
* 返回值0-成功,-1=参数错误,-2=I2C 错误 int
* 限定条件sd2506_init() 已调用成功addr+len <= SD2506_SRAM_SIZE(70)
* 函数说明SRAM 范围 0x2C~0x71
* return: 0=OK, -1=param err, -2=I2C err
* note: SRAM range 0x2C-0x71
*/
int sd2506_read_sram(uint8_t addr, uint8_t *p_buf, uint8_t len) {
if (p_buf == NULL || len == 0 || addr >= SD2506_SRAM_SIZE) {
return SD2506_ERROR; return SD2506_ERROR;
} }
@@ -292,12 +376,21 @@ int sd2506_read_sram(uint8_t addr, uint8_t *buf, uint8_t len)
return SD2506_ERROR; return SD2506_ERROR;
} }
return sd2506_read_regs(SD2506_REG_SRAM_START + addr, buf, len); return sd2506_read_regs(SD2506_REG_SRAM_START + addr, p_buf, len);
} }
int sd2506_write_sram(uint8_t addr, const uint8_t *buf, uint8_t len) /*
{ * 函数功能:写入 SRAM 数据
if (buf == NULL || len == 0 || addr >= SD2506_SRAM_SIZE) { * 入口参数addr - 起始地址 uint8_t 0-69
* p_buf - 输入数据指针 uint8_t*(非空)
* len - 写入长度 uint8_t 1-N
* 返回值0-成功,-1=参数错误,-2=I2C 错误 int
* 限定条件sd2506_init() 已调用成功addr+len <= SD2506_SRAM_SIZE(70)
* 函数说明SRAM 写入需走写保护流程(虽 SRAM 本身无需)
* return: 0=OK, -1=param err, -2=I2C err
*/
int sd2506_write_sram(uint8_t addr, const uint8_t *p_buf, uint8_t len) {
if (p_buf == NULL || len == 0 || addr >= SD2506_SRAM_SIZE) {
return SD2506_ERROR; return SD2506_ERROR;
} }
@@ -305,36 +398,44 @@ int sd2506_write_sram(uint8_t addr, const uint8_t *buf, uint8_t len)
return SD2506_ERROR; return SD2506_ERROR;
} }
/* SRAM 无需开写保护即可写入 (写保护仅对 00H~71H 有效, SRAM 2CH~71H) /* SRAM 无需开写保护即可写入 (写保护仅对 00H~71H 有效, SRAM 2CH~71H)
* 但为安全起见, SRAM 写入也走写保护流程 */ * 但为安全起见, SRAM 写入也走写保护流程 */
int ret; int ret = 0;
ret = sd2506_write_enable(); ret = sd2506_write_enable();
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
ret = sd2506_write_regs(SD2506_REG_SRAM_START + addr, buf, len); ret = sd2506_write_regs(SD2506_REG_SRAM_START + addr, p_buf, len);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
ret = sd2506_write_disable(); ret = sd2506_write_disable();
return ret; return ret;
} }
int sd2506_set_alarm(const sd2506_time_t *time, uint8_t mask) /*
{ * 函数功能:设置闹钟中断
int ret; * 入口参数p_time - 报警时间结构体指针 sd2506_time_t*(非空)
uint8_t buf[8]; * mask - 报警匹配掩码 uint8_t
* 返回值0-成功,-2=I2C 错误 int
* 限定条件sd2506_init() 已调用成功
* 函数说明:写入 8 字节报警数据 (07H~0EH),使能 CTR2.INTAE+INTS0+IM
* return: 0=OK, -2=I2C error
*/
int sd2506_set_alarm(const sd2506_time_t *p_time, uint8_t mask) {
int ret = 0;
uint8_t buf[8] = {0};
if (time == NULL) { if (p_time == NULL) {
return SD2506_ERROR; return SD2506_ERROR;
} }
/* 组装 8 字节报警数据 (07H~0EH) */ /* 组装 8 字节报警数据 (07H~0EH) */
buf[0] = sd2506_dec_to_bcd(time->second) & 0x7FU; /* 07H: 秒报 */ buf[0] = sd2506_dec_to_bcd(p_time->second) & 0x7FU; /* 07H: 秒报 */
buf[1] = sd2506_dec_to_bcd(time->minute) & 0x7FU; /* 08H: 分报 */ buf[1] = sd2506_dec_to_bcd(p_time->minute) & 0x7FU; /* 08H: 分报 */
buf[2] = sd2506_dec_to_bcd(time->hour) & 0x3FU; /* 09H: 时报警 (最高位始终为0) */ buf[2] = sd2506_dec_to_bcd(p_time->hour) & 0x3FU; /* 09H: 时报最高位始终为0 */
buf[3] = 0x00U; /* 0AH: 星期报警 */ buf[3] = 0x00U; /* 0AH: 星期报警 */
buf[4] = sd2506_dec_to_bcd(time->day) & 0x3FU; /* 0BH: 日报 */ buf[4] = sd2506_dec_to_bcd(p_time->day) & 0x3FU; /* 0BH: 日报 */
buf[5] = sd2506_dec_to_bcd(time->month) & 0x1FU; /* 0CH: 月报 */ buf[5] = sd2506_dec_to_bcd(p_time->month) & 0x1FU; /* 0CH: 月报 */
buf[6] = sd2506_dec_to_bcd(time->year - 2000U); /* 0DH: 年报 */ buf[6] = sd2506_dec_to_bcd(p_time->year - 2000U); /* 0DH: 年报 */
buf[7] = mask; /* 0EH: 报警允许 */ buf[7] = mask; /* 0EH: 报警允许 */
/* 开启写保护 */ /* 开启写保护 */
@@ -345,7 +446,7 @@ int sd2506_set_alarm(const sd2506_time_t *time, uint8_t mask)
ret = sd2506_write_regs(SD2506_REG_AL_SEC, buf, 8); ret = sd2506_write_regs(SD2506_REG_AL_SEC, buf, 8);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
/* 使能报警中断: CTR2 INTAE=1, INTS1=0, INTS0=1, IM=1 (周期) */ /* 使能报警中断: CTR2 INTAE=1, INTS1=0, INTS0=1, IM=1 (周期中断) */
ret = sd2506_write_reg(SD2506_REG_CTR2, ret = sd2506_write_reg(SD2506_REG_CTR2,
SD2506_CTR2_INTAE | SD2506_CTR2_INTS0 | SD2506_CTR2_IM); SD2506_CTR2_INTAE | SD2506_CTR2_INTS0 | SD2506_CTR2_IM);
if (ret != SD2506_OK) return ret; if (ret != SD2506_OK) return ret;
@@ -357,10 +458,18 @@ int sd2506_set_alarm(const sd2506_time_t *time, uint8_t mask)
return SD2506_OK; return SD2506_OK;
} }
int sd2506_clear_alarm(void) /*
{ * 函数功能:清除闹钟中断标志
int ret; * 入口参数:无
uint8_t val; * 返回值0-成功,-2=I2C 错误 int
* 限定条件sd2506_init() 已调用成功ARST=1 时自动清除 INTAF
* 函数说明:读取 CTR1 即可清除 INTAF 标志
* return: 0=OK, -2=I2C error
* note: reading CTR1 clears INTAF
*/
int sd2506_clear_alarm(void) {
int ret = 0;
uint8_t val = 0;
/* 读取 CTR1, ARST=1 时自动清除 INTAF */ /* 读取 CTR1, ARST=1 时自动清除 INTAF */
ret = sd2506_read_reg(SD2506_REG_CTR1, &val); ret = sd2506_read_reg(SD2506_REG_CTR1, &val);
@@ -371,11 +480,18 @@ int sd2506_clear_alarm(void)
return SD2506_OK; return SD2506_OK;
} }
int sd2506_read_ctr1(uint8_t *val) /*
{ * 函数功能:读取控制寄存器 1 (CTR1)
if (val == NULL) { * 入口参数p_val - 输出值指针 uint8_t*(非空)
* 返回值0-成功,-2=I2C 错误 int
* 限定条件sd2506_init() 已调用成功
* return: 0=OK, -2=I2C error
*/
int sd2506_read_ctr1(uint8_t *p_val) {
if (p_val == NULL) {
return SD2506_ERROR; return SD2506_ERROR;
} }
return sd2506_read_reg(SD2506_REG_CTR1, val); return sd2506_read_reg(SD2506_REG_CTR1, p_val);
} }

View File

@@ -50,7 +50,7 @@ extern "C" {
#define SD2506_REG_AL_EN 0x0EU /* 报警允许寄存器 */ #define SD2506_REG_AL_EN 0x0EU /* 报警允许寄存器 */
/* 控制寄存器 */ /* 控制寄存器 */
#define SD2506_REG_CTR1 0x0FH /* 控制寄存器1 */ #define SD2506_REG_CTR1 0x0FU /* 控制寄存器1 */
#define SD2506_REG_CTR2 0x10U /* 控制寄存器2 */ #define SD2506_REG_CTR2 0x10U /* 控制寄存器2 */
#define SD2506_REG_CTR3 0x11U /* 控制寄存器3 */ #define SD2506_REG_CTR3 0x11U /* 控制寄存器3 */
@@ -180,7 +180,7 @@ int sd2506_init(void);
* 3. 关闭写保护 * 3. 关闭写保护
* 注意:不可单独写某一个时间寄存器 * 注意:不可单独写某一个时间寄存器
*/ */
int sd2506_set_time(const sd2506_time_t *time); int sd2506_set_time(const sd2506_time_t *p_time);
/* /*
* 函数功能:读取 RTC 时间日期 * 函数功能:读取 RTC 时间日期
@@ -191,7 +191,7 @@ int sd2506_set_time(const sd2506_time_t *time);
* 2. BCD 转十进制 * 2. BCD 转十进制
* 3. 屏蔽小时 bit7 (12/24标志位) * 3. 屏蔽小时 bit7 (12/24标志位)
*/ */
int sd2506_get_time(sd2506_time_t *time); int sd2506_get_time(sd2506_time_t *p_time);
/* /*
* 函数功能:读取芯片内部温度 * 函数功能:读取芯片内部温度
@@ -200,7 +200,7 @@ int sd2506_get_time(sd2506_time_t *time);
* 限定条件sd2506_init() 已调用 * 限定条件sd2506_init() 已调用
* 函数说明:读取 16H 寄存器bit7 为符号位,范围 -40~+85 * 函数说明:读取 16H 寄存器bit7 为符号位,范围 -40~+85
*/ */
int sd2506_get_temperature(int8_t *temp); int sd2506_get_temperature(int8_t *p_temp);
/* /*
* 函数功能:读取电池电压 (毫伏) * 函数功能:读取电池电压 (毫伏)
@@ -210,7 +210,7 @@ int sd2506_get_temperature(int8_t *temp);
* 函数说明1. 读取 1AH bit7 (BAT8_VAL) 和 1BH (BAT_VL) * 函数说明1. 读取 1AH bit7 (BAT8_VAL) 和 1BH (BAT_VL)
* 2. 组合 9 位数据得到电压值 (如 0x135 = 309 = 3.09V) * 2. 组合 9 位数据得到电压值 (如 0x135 = 309 = 3.09V)
*/ */
int sd2506_get_battery_voltage(uint16_t *voltage); int sd2506_get_battery_voltage(uint16_t *p_voltage);
/* /*
* 函数功能:读取芯片 8 字节 ID * 函数功能:读取芯片 8 字节 ID
@@ -219,7 +219,7 @@ int sd2506_get_battery_voltage(uint16_t *voltage);
* 限定条件sd2506_init() 已调用 * 限定条件sd2506_init() 已调用
* 函数说明:读取 72H~79H 共 8 字节唯一 ID * 函数说明:读取 72H~79H 共 8 字节唯一 ID
*/ */
int sd2506_get_id(uint8_t id[8]); int sd2506_get_id(uint8_t p_id[8]);
/* /*
* 函数功能:读取用户 SRAM 数据 * 函数功能:读取用户 SRAM 数据
@@ -230,7 +230,7 @@ int sd2506_get_id(uint8_t id[8]);
* 限定条件sd2506_init() 已调用 * 限定条件sd2506_init() 已调用
* 函数说明SRAM 地址范围 0~69对应寄存器 2CH~71H * 函数说明SRAM 地址范围 0~69对应寄存器 2CH~71H
*/ */
int sd2506_read_sram(uint8_t addr, uint8_t *buf, uint8_t len); int sd2506_read_sram(uint8_t addr, uint8_t *p_buf, uint8_t len);
/* /*
* 函数功能:写入用户 SRAM 数据 * 函数功能:写入用户 SRAM 数据
@@ -242,7 +242,7 @@ int sd2506_read_sram(uint8_t addr, uint8_t *buf, uint8_t len);
* 函数说明SRAM 地址范围 0~69对应寄存器 2CH~71H * 函数说明SRAM 地址范围 0~69对应寄存器 2CH~71H
* SRAM 无需开写保护即可写入 * SRAM 无需开写保护即可写入
*/ */
int sd2506_write_sram(uint8_t addr, const uint8_t *buf, uint8_t len); int sd2506_write_sram(uint8_t addr, const uint8_t *p_buf, uint8_t len);
/* /*
* 函数功能:设置报警中断 * 函数功能:设置报警中断
@@ -255,7 +255,7 @@ int sd2506_write_sram(uint8_t addr, const uint8_t *buf, uint8_t len);
* 3. 使能报警中断 INTAE * 3. 使能报警中断 INTAE
* 4. 关写保护 * 4. 关写保护
*/ */
int sd2506_set_alarm(const sd2506_time_t *time, uint8_t mask); int sd2506_set_alarm(const sd2506_time_t *p_time, uint8_t mask);
/* /*
* 函数功能:清除报警中断标志 * 函数功能:清除报警中断标志
@@ -271,7 +271,7 @@ int sd2506_clear_alarm(void);
* 入口参数val - 输出值指针 * 入口参数val - 输出值指针
* 返回值0 - 成功,-2 - I2C 通信错误 * 返回值0 - 成功,-2 - I2C 通信错误
*/ */
int sd2506_read_ctr1(uint8_t *val); int sd2506_read_ctr1(uint8_t *p_val);
/* /*
* 函数功能BCD 码转十进制 * 函数功能BCD 码转十进制

View File

@@ -1,12 +1,9 @@
/* /*
* 模块名称TPAFE5160 16位8通道同步采样ADC驱动 * 模块名称TPAFE5160 16<EFBFBD>?通道同步采样ADC驱动
* 模块功能:提TPAFE5160 并行接口模式下的初始化、过采样设置、转换启动、数据读取接 * 模块功能:提<EFBFBD>?TPAFE5160 并行接口模式下的初始化、过采样设置、转换启动、数据读取接<EFBFBD>? * 适用平台STM32F407ZGT6<EFBC8C>?6位数据总线<E680BB>?GPIOG[15:0]
* 适用平台STM32F407ZGT6并行16位数据总线接 GPIOG[15:0] * 作者王建<EFBFBD>? * 创建日期<E697A5>?026-07-17
* 作者:王建锋 * 修改记录<EFBFBD>? * 2026-07-17 王建<E78E8B>? 创建初始版本<EFBC8C>?AD7606 并行驱动<E9A9B1>?TPAFE5160 手册
* 创建日期:2026-07-17 * 2026-07-17 王建<E78E8B>? 增加 EXTI 中断读取模式
* 修改记录:
* 2026-07-17 王建锋 创建初始版本,参考 AD7606 并行驱动及 TPAFE5160 手册
* 2026-07-17 王建锋 增加 EXTI 中断读取模式
*/ */
/* 头文件包含区 */ /* 头文件包含区 */
@@ -19,35 +16,31 @@
#define DBG_TAG "[ADC]" #define DBG_TAG "[ADC]"
#include "dbg_log.h" #include "dbg_log.h"
/* ======================== 私有宏定======================== */ /* ======================== 私有宏定<EFBFBD>?======================== */
/* /*
* 并行读取时序延时 (168MHz 主频1 NOP 5.95ns) * 并行读取时序延时 (168MHz 主频<EFBFBD>? NOP <EFBFBD>?5.95ns)
* *
* TPAFE5160 并行时序要求 (VDRIVE > 2.7V) * TPAFE5160 并行时序要求 (VDRIVE > 2.7V)<EFBFBD>? * t10 (RD 低脉<E4BD8E>? <20>?22ns
* t10 (RD 低脉宽) ≥ 22ns * t11 (RD 高脉<EFBFBD>? <20>?10ns
* t11 (RD 高脉宽) ≥ 10ns * t14 (数据访问时间) <20>?21ns (<28>?RD 下降沿算<E6B2BF>?
* t14 (数据访问时间) ≤ 21ns (RD 下降沿算起) * t15 (数据保持时间) <EFBFBD>?6ns (<EFBFBD>?RD 下降沿算<EFBFBD>?
* t15 (数据保持时间) ≥ 6ns (从 RD 下降沿算起)
* *
* GPIO 写操作本身约 1 AHB 周期 (~6ns) * GPIO 写操作本身约 1 <EFBFBD>?AHB 周期 (~6ns)<EFBFBD>? * 加上端口延迟 (~12-18ns)<EFBC8C>?RD 引脚翻转滞后<E6BB9E>?18-24ns<6E>? * 因此 NOP 延时只需覆盖数据建立时间即可<E58DB3>? */
* 加上端口延迟 (~12-18ns),实际 RD 引脚翻转滞后约 18-24ns。 #define TPAFE5160_NOP_5() __NOP(); __NOP(); __NOP(); __NOP(); __NOP()
* 因此 NOP 延时只需覆盖数据建立时间即可。
*/
#define TP_NOP_5() __NOP(); __NOP(); __NOP(); __NOP(); __NOP()
/* RD 低脉宽延时GPIO写(~6ns) + 5NOP(~30ns) > 22ns */ /* RD 低脉宽延时GPIO<EFBFBD>?~6ns) + 5NOP(~30ns) > 22ns */
#define TP_RD_LOW_DLY() do { TPAFE5160_RD_LOW(); TP_NOP_5(); } while (0) #define TPAFE5160_RD_LOW_DLY() do { TPAFE5160_RD_LOW(); TPAFE5160_NOP_5(); } while (0)
/* RD 高脉宽延时GPIO写(~6ns) + 3NOP(~18ns) > 10ns */ /* RD 高脉宽延时GPIO<EFBFBD>?~6ns) + 3NOP(~18ns) > 10ns */
#define TP_RD_HIGH_DLY() do { TPAFE5160_RD_HIGH(); __NOP(); __NOP(); __NOP(); } while (0) #define TPAFE5160_RD_HIGH_DLY() do { TPAFE5160_RD_HIGH(); __NOP(); __NOP(); __NOP(); } while (0)
/* CONVST 脉冲延时上升沿前需保证低电平5NOP 覆盖 t5 20ns */ /* CONVST 脉冲延时上升沿前需保证低电平5NOP 覆盖 t5 <EFBFBD>?20ns */
#define TP_CONVST_PULSE() do { \ #define TPAFE5160_CONVST_PULSE() do { \
TPAFE5160_CONVST_LOW(); \ TPAFE5160_CONVST_LOW(); \
TP_NOP_5(); \ TPAFE5160_NOP_5(); \
TPAFE5160_CONVST_HIGH(); \ TPAFE5160_CONVST_HIGH(); \
TP_NOP_5(); \ TPAFE5160_NOP_5(); \
} while (0) } while (0)
/* ======================== 私有函数声明 ======================== */ /* ======================== 私有函数声明 ======================== */
@@ -59,22 +52,20 @@ static void tpafe5160_read_channels(uint8_t count, int16_t *buf);
/* /*
* 函数功能:初始化 TPAFE5160设置过采样、等待就绪 * 函数功能:初始化 TPAFE5160设置过采样、等待就绪
* 入口参数:无 * 入口参数:无
* 返回值0 - 成功-2 - BUSY 超时 * 返回值0 - 成功<EFBFBD>?2 - BUSY 超时
* 限定条件CubeMX 已完GPIO 初始 * 限定条件CubeMX 已完<EFBFBD>?GPIO 初始<EFBFBD>? * 函数说明<E8AFB4>?. 设置过采样为无过采样 (000)
* 函数说明1. 设置过采样为无过采样 (000)
* 2. 确保 RD 为高、CONVST 为低 * 2. 确保 RD 为高、CONVST 为低
* 3. 等待 BUSY 释放(转换空闲) * 3. 等待 BUSY 释放(转换空闲)
*/ */
int tpafe5160_init(void) int tpafe5160_init(void) {
{
/* 设置默认过采样:无过采样 (OS[2:0] = 000) */ /* 设置默认过采样:无过采样 (OS[2:0] = 000) */
tpafe5160_set_os(TP_OS_NONE); tpafe5160_set_os(TPAFE5160_OS_NONE);
/* 确保控制引脚处于空闲状*/ /* 确保控制引脚处于空闲状<EFBFBD>?*/
TPAFE5160_RD_HIGH(); TPAFE5160_RD_HIGH();
TPAFE5160_CONVST_LOW(); TPAFE5160_CONVST_LOW();
/* 等待 BUSY 释放,确保上电后无残留转*/ /* 等待 BUSY 释放,确保上电后无残留转<EFBFBD>?*/
int ret = tpafe5160_wait_busy(TPAFE5160_CONV_TIMEOUT_MS); int ret = tpafe5160_wait_busy(TPAFE5160_CONV_TIMEOUT_MS);
if (ret != TPAFE5160_OK) { if (ret != TPAFE5160_OK) {
DBG_ERROR("Init wait BUSY timeout"); DBG_ERROR("Init wait BUSY timeout");
@@ -85,14 +76,10 @@ int tpafe5160_init(void)
} }
/* /*
* 函数功能:设置过采样 * 函数功能:设置过采样<EFBFBD>? * 入口参数os - 过采样率枚举<E69E9A>? tpafe5160_os_t
* 入口参数os - 过采样率枚举值 tpafe5160_os_t * 返回值<EFBFBD>? * 限定条件GPIO 已初始化
* 返回值:无 * 函数说明:通过 OS[2:0] 引脚设置过采样率在下一<E4B88B>?BUSY 下降沿锁存生<E5AD98>? */
* 限定条件GPIO 已初始化 void tpafe5160_set_os(tpafe5160_os_t os) {
* 函数说明:通过 OS[2:0] 引脚设置过采样率,在下一次 BUSY 下降沿锁存生效
*/
void tpafe5160_set_os(tpafe5160_os_t os)
{
TPAFE5160_OS0(os & 0x01); /* OS0 = bit0 */ TPAFE5160_OS0(os & 0x01); /* OS0 = bit0 */
TPAFE5160_OS1((os >> 1) & 0x01); /* OS1 = bit1 */ TPAFE5160_OS1((os >> 1) & 0x01); /* OS1 = bit1 */
TPAFE5160_OS2((os >> 2) & 0x01); /* OS2 = bit2 */ TPAFE5160_OS2((os >> 2) & 0x01); /* OS2 = bit2 */
@@ -101,27 +88,22 @@ void tpafe5160_set_os(tpafe5160_os_t os)
/* /*
* 函数功能启动一次转换CONVST 上升沿触发) * 函数功能启动一次转换CONVST 上升沿触发)
* 入口参数:无 * 入口参数:无
* 返回值: * 返回值:<EFBFBD>? * 限定条件GPIO 已初始化
* 限定条件GPIO 已初始化 * 函数说明<EFBFBD>?CONVST 脉冲上升沿启动全部8通道同步采样与转<E4B88E>? */
* 函数说明:产生 CONVST 脉冲上升沿启动全部8通道同步采样与转换 void tpafe5160_start_conv(void) {
*/ TPAFE5160_CONVST_PULSE();
void tpafe5160_start_conv(void)
{
TP_CONVST_PULSE();
} }
/* /*
* 函数功能:等待转换完成
* 入口参数timeout_ms - 超时时间 uint32_t > 0 * 函数功能等待转换完<E68DA2>? * 入口参数timeout_ms - 超时时间 uint32_t > 0
* 返回值0 - 转换完成-2 - 超时 * 返回值0 - 转换完成<EFBFBD>?2 - 超时
* 限定条件:已调用 tpafe5160_start_conv() * 限定条件:已调用 tpafe5160_start_conv()
* 函数说明:轮BUSY 引脚等待下降沿 * 函数说明:轮<EFBFBD>?BUSY 引脚等待下降<EFBFBD>? */
*/ int tpafe5160_wait_busy(uint32_t timeout_ms) {
int tpafe5160_wait_busy(uint32_t timeout_ms)
{
uint32_t tick_start = HAL_GetTick(); uint32_t tick_start = HAL_GetTick();
/* 等待 BUSY 释放(低电平表示空闲 */ /* 等待 BUSY 释放(低电平表示空闲<EFBFBD>?*/
while (TPAFE5160_BUSY_READ() == GPIO_PIN_SET) { while (TPAFE5160_BUSY_READ() == GPIO_PIN_SET) {
if ((HAL_GetTick() - tick_start) >= timeout_ms) { if ((HAL_GetTick() - tick_start) >= timeout_ms) {
return TPAFE5160_BUSY_TIMEOUT; return TPAFE5160_BUSY_TIMEOUT;
@@ -132,29 +114,26 @@ int tpafe5160_wait_busy(uint32_t timeout_ms)
} }
/* /*
* 函数功能:查询当前是否正在转 * 函数功能:查询当前是否正在转<EFBFBD>? * 入口参数:无
* 入口参数:无 * 返回值1 - 正在转换<E8BDAC>? - 空闲
* 返回值1 - 正在转换0 - 空闲
* 限定条件GPIO 已初始化 * 限定条件GPIO 已初始化
* 函数说明:读BUSY 引脚电平 * 函数说明:读<EFBFBD>?BUSY 引脚电平
*/ */
uint8_t tpafe5160_is_busy(void) uint8_t tpafe5160_is_busy(void) {
{
return (TPAFE5160_BUSY_READ() == GPIO_PIN_SET) ? 1 : 0; return (TPAFE5160_BUSY_READ() == GPIO_PIN_SET) ? 1 : 0;
} }
/* /*
* 函数功能:读取全部8通道转换结果 * 函数功能:读取全<EFBFBD>?通道转换结果
* 入口参数buf - 8个int16_t的输出缓冲区 int16_t* 不为 NULL * 入口参数buf - 8个int16_t的输出缓冲区 int16_t* 不为 NULL
* 返回值0 - 成功-2 - BUSY 超时 * 返回值0 - 成功<EFBFBD>?2 - BUSY 超时
* 限定条件GPIO 已初始化 * 限定条件GPIO 已初始化
* 函数说明1. 启动转换并等BUSY 释放 * 函数说明<EFBFBD>?. 启动转换并等<EFBFBD>?BUSY 释放
* 2. 连续8次拉低RD读取各通道数据 * 2. 连续8次拉低RD读取各通道数据
* 3. 通过 FRSTDATA 验证第一通道 * 3. 通过 FRSTDATA 验证第一通道
*/ */
int tpafe5160_read_all(int16_t *buf) int tpafe5160_read_all(int16_t *buf) {
{ int ret = 0;
int ret;
/* 启动转换 */ /* 启动转换 */
tpafe5160_start_conv(); tpafe5160_start_conv();
@@ -173,17 +152,15 @@ int tpafe5160_read_all(int16_t *buf)
} }
/* /*
* 函数功能:读取指定通道的转换结 * 函数功能:读取指定通道的转换结<EFBFBD>? * 入口参数channel - 通道<E9809A>? uint8_t 0 - 7
* 入口参数channel - 通道号 uint8_t 0 - 7
* value - 输出指针 int16_t* 不为 NULL * value - 输出指针 int16_t* 不为 NULL
* 返回值0 - 成功-1 - 通道号无效,-2 - BUSY 超时 * 返回值0 - 成功<EFBFBD>?1 - 通道号无效,-2 - BUSY 超时
* 限定条件GPIO 已初始化 * 限定条件GPIO 已初始化
* 函数说明:启动转换并等待完成后,连续读取至指定通道 * 函数说明:启动转换并等待完成后,连续读取至指定通道
*/ */
int tpafe5160_read_channel(uint8_t channel, int16_t *value) int tpafe5160_read_channel(uint8_t channel, int16_t *value) {
{ int ret = 0;
int ret; int16_t buf[TPAFE5160_CH_NUM] = {0};
int16_t buf[TPAFE5160_CH_NUM];
if (channel >= TPAFE5160_CH_NUM || value == NULL) { if (channel >= TPAFE5160_CH_NUM || value == NULL) {
return TPAFE5160_ERROR; return TPAFE5160_ERROR;
@@ -199,29 +176,23 @@ int tpafe5160_read_channel(uint8_t channel, int16_t *value)
} }
/* /*
* 函数功能原始ADC值转电压 * 函数功能原始ADC值转电压<EFBFBD>? * 入口参数raw - ADC原始<E58E9F>? int16_t 有符号补<E58FB7>? * 返回值电压<E794B5>? float 单位 V
* 入口参数raw - ADC原始值 int16_t 有符号补码
* 返回值:电压值 float 单位 V
* 限定条件:无 * 限定条件:无
* 函数说明:±5V量程LSB = 10V / 65536 152.59μV * 函数说明:<EFBFBD>?V量程<EFBFBD>?LSB = 10V / 65536 <EFBFBD>?152.59μV
* ±10V量程LSB = 20V / 65536 305.18μV * ±10V量程<EFBFBD>?LSB = 20V / 65536 <EFBFBD>?305.18μV
* 硬件 RANGE 引脚GND±5V 量程 * 硬件 RANGE 引脚<EFBFBD>?GND<EFBFBD>?±5V 量程
*/ */
float tpafe5160_to_voltage(int16_t raw) float tpafe5160_to_voltage(int16_t raw) {
{ /* RANGE <20>?GND <20>?±5V 量程,满量程 10V */
/* RANGE 接 GND → ±5V 量程,满量程 10V */
return (float)raw * (10.0f / 65536.0f); return (float)raw * (10.0f / 65536.0f);
} }
/* /*
* 函数功能:直接读取并行数据总线(不启动转换 * 函数功能:直接读取并行数据总线(不启动转换<EFBFBD>? * 入口参数:无
* 入口参数:无 * 返回值16位原始数<EFBFBD>? uint16_t
* 返回值16位原始数据 uint16_t * 限定条件RD 为低<E4B8BA>?CS <20>?RD 已拉<E5B7B2>? * 函数说明<EFBC9A>?GPIOG->IDR <20>?6位对应 DB[15:0]
* 限定条件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(); return TPAFE5160_READ_BUS();
} }
@@ -229,100 +200,82 @@ uint16_t tpafe5160_read_bus(void)
/* /*
* 函数功能:通过 RD 脉冲连续读取多个通道数据 * 函数功能:通过 RD 脉冲连续读取多个通道数据
* 入口参数count - 要读取的通道 uint8_t 1 - 8 * 入口参数count - 要读取的通道<EFBFBD>? uint8_t 1 - 8
* buf - 输出缓冲 int16_t* 不为 NULL * buf - 输出缓冲<EFBFBD>? int16_t* 不为 NULL
* 返回值: * 返回值:<EFBFBD>? * 限定条件转换已完成BUSY 为低RD 初始为高
* 限定条件转换已完成BUSY 为低RD 初始为高 * 函数说明<EFBFBD>?RD 下降沿输出一个通道数据按通道1~8顺序输出
* 函数说明:每次 RD 下降沿输出一个通道数据按通道1~8顺序输出 * DB[15:0] 直接<E79BB4>?GPIOG[15:0],通过 IDR 寄存器一次读<E6ACA1>? *
* DB[15:0] 直接接 GPIOG[15:0],通过 IDR 寄存器一次读取 * 时序关键<EFBFBD>?(VDRIVE > 2.7V, 168MHz 主频)<29>? * GPIO 写操<E58699>?~6ns + 端口延迟 ~18ns <20>?RD 引脚实际翻转<E7BFBB>?24ns <20>? * t14 数据建立 <20>?21ns <20>?数据<E695B0>?RD 下降沿后 21ns 内有<E58685>? * 5 <20>?NOP (~30ns) 覆盖建立时间
*
* 时序关键点 (VDRIVE > 2.7V, 168MHz 主频)
* GPIO 写操作 ~6ns + 端口延迟 ~18ns → RD 引脚实际翻转约 24ns 后
* 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; uint8_t i;
for (i = 0; i < count; i++) { for (i = 0; i < count; i++) {
/* RD 下降沿ADC 输出当前通道数据DB[15:0] */ /* RD 下降沿ADC 输出当前通道数据<EFBFBD>?DB[15:0] */
TP_RD_LOW_DLY(); TPAFE5160_RD_LOW_DLY();
/* 读取16位并行数*/ /* 读取16位并行数<EFBFBD>?*/
buf[i] = (int16_t)TPAFE5160_READ_BUS(); buf[i] = (int16_t)TPAFE5160_READ_BUS();
/* RD 上升沿:准备下一通道 */ /* RD 上升沿:准备下一通道 */
TP_RD_HIGH_DLY(); TPAFE5160_RD_HIGH_DLY();
} }
} }
/* ======================== 中断模式实现 ======================== */ /* ======================== 中断模式实现 ======================== */
/* 双缓冲区ISR s_buf_b主循环s_buf_a */ /* 双缓冲区ISR <EFBFBD>?s_buf_b主循环<EFBFBD>?s_buf_a */
static int16_t s_buf_a[TPAFE5160_CH_NUM]; static int16_t s_buf_a[TPAFE5160_CH_NUM];
static int16_t s_buf_b[TPAFE5160_CH_NUM]; static int16_t s_buf_b[TPAFE5160_CH_NUM];
static volatile uint8_t s_ready = 0; static volatile uint8_t s_ready = 0;
static volatile uint8_t s_buf_sel = 0; /* 0 = B / A, 1 = A / B */ static volatile uint8_t s_buf_sel = 0; /* 0 = <EFBFBD>?B / <EFBFBD>?A, 1 = <EFBFBD>?A / <EFBFBD>?B */
/* /*
* 函数功能:使BUSY EXTI 中断(运行时重使能用 * 函数功能:使<EFBFBD>?BUSY EXTI 中断(运行时重使能用<EFBFBD>? * 入口参数:无
* 入口参数:无 * 返回值<EFBFBD>? * 限定条件CubeMX 已完<E5B7B2>?GPIO <20>?NVIC 配置
* 返回值:无 * 函数说明正常启动流程无需调用<EFBFBD>?irq_disable() 后需要重新使能时使用
* 限定条件CubeMX 已完成 GPIO 和 NVIC 配置
* 函数说明:正常启动流程无需调用,仅在 irq_disable() 后需要重新使能时使用
*/ */
void tpafe5160_irq_enable(void) void tpafe5160_irq_enable(void) {
{
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
} }
/* /*
* 函数功能:关BUSY EXTI 中断 * 函数功能:关<EFBFBD>?BUSY EXTI 中断
* 入口参数:无 * 入口参数:无
* 返回值: * 返回值:<EFBFBD>? * 限定条件:已调用 tpafe5160_irq_enable()
* 限定条件:已调用 tpafe5160_irq_enable()
* 函数说明:仅禁用 NVIC 中断GPIO 配置保持 CubeMX 设定 * 函数说明:仅禁用 NVIC 中断GPIO 配置保持 CubeMX 设定
*/ */
void tpafe5160_irq_disable(void) void tpafe5160_irq_disable(void) {
{
HAL_NVIC_DisableIRQ(EXTI9_5_IRQn); HAL_NVIC_DisableIRQ(EXTI9_5_IRQn);
} }
/* /*
* 函数功能:启动转换(中断模式 * 函数功能:启动转换(中断模式<EFBFBD>? * 入口参数:无
* 入口参数:无 * 返回值<EFBFBD>? * 限定条件:已调用 tpafe5160_irq_enable()
* 返回值:无 * 函数说明<EFBFBD>?CONVST 脉冲转换完成后<E68890>?EXTI 中断自动读取 8 通道数据
* 限定条件:已调用 tpafe5160_irq_enable()
* 函数说明:产生 CONVST 脉冲,转换完成后由 EXTI 中断自动读取 8 通道数据
*/ */
void tpafe5160_start_conv_irq(void) void tpafe5160_start_conv_irq(void) {
{
s_ready = 0; s_ready = 0;
TP_CONVST_PULSE(); TPAFE5160_CONVST_PULSE();
} }
/* /*
* 函数功能:检查是否有新的转换数据 * 函数功能:检查是否有新的转换数据
* 入口参数:无 * 入口参数:无
* 返回值1 - 数据就绪0 - 无新数据 * 返回值1 - 数据就绪<EFBFBD>? - 无新数据
* 限定条件:中断模式已启用 * 限定条件:中断模式已启用
* 函数说明:在 EXTI 回调中置位,主循环读取后需调用 tpafe5160_clear_ready() 清除 * 函数说明:在 EXTI 回调中置位,主循环读取后需调用 tpafe5160_clear_ready() 清除
*/ */
uint8_t tpafe5160_data_ready(void) uint8_t tpafe5160_data_ready(void) {
{
return s_ready; return s_ready;
} }
/* /*
* 函数功能:清除数据就绪标 * 函数功能:清除数据就绪标<EFBFBD>? * 入口参数:无
* 入口参数:无 * 返回值<EFBFBD>? * 限定条件:中断模式已启用
* 返回值:无
* 限定条件:中断模式已启用
* 函数说明:主循环处理完数据后调用 * 函数说明:主循环处理完数据后调用
*/ */
void tpafe5160_clear_ready(void) void tpafe5160_clear_ready(void) {
{
s_ready = 0; s_ready = 0;
} }
@@ -330,23 +283,16 @@ void tpafe5160_clear_ready(void)
* 函数功能:获取数据缓冲区指针 * 函数功能:获取数据缓冲区指针
* 入口参数:无 * 入口参数:无
* 返回值int16_t[8] 数据缓冲区的 const 指针 * 返回值int16_t[8] 数据缓冲区的 const 指针
* 限定条件tpafe5160_data_ready() 返回 1 时调 * 限定条件tpafe5160_data_ready() 返回 1 时调<EFBFBD>? * 函数说明双缓冲切换ISR 写另一个缓冲区主循环安全读取当前缓冲<E7BC93>? */
* 函数说明双缓冲切换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; return (s_buf_sel == 0) ? s_buf_a : s_buf_b;
} }
/* /*
* 函数功能BUSY 下降沿 EXTI 处理(由 HAL_GPIO_EXTI_Callback 调用 * 函数功能BUSY 下降<EFBFBD>?EXTI 处理(由 HAL_GPIO_EXTI_Callback 调用<EFBFBD>? * 入口参数GPIO_Pin - 触发中断的引脚号
* 入口参数GPIO_Pin - 触发中断的引脚号 * 返回值<EFBFBD>? * 限定条件BUSY EXTI 已使<E5B7B2>? * 函数说明:转换完成后自动读取 8 通道数据到缓冲区耗时<E88097>?0.5µs (168MHz)
* 返回值:无
* 限定条件BUSY EXTI 已使能
* 函数说明:转换完成后自动读取 8 通道数据到缓冲区,耗时约 0.5µs (168MHz)
*/ */
void tpafe5160_exti_handler(uint16_t GPIO_Pin) void tpafe5160_exti_handler(uint16_t GPIO_Pin) {
{
if (GPIO_Pin != TP_BUSY_Pin) { if (GPIO_Pin != TP_BUSY_Pin) {
return; return;
} }
@@ -357,9 +303,9 @@ void tpafe5160_exti_handler(uint16_t GPIO_Pin)
/* 连续读取8个通道约 0.5µs @ 168MHz */ /* 连续读取8个通道约 0.5µs @ 168MHz */
uint8_t i; uint8_t i;
for (i = 0; i < TPAFE5160_CH_NUM; i++) { for (i = 0; i < TPAFE5160_CH_NUM; i++) {
TP_RD_LOW_DLY(); TPAFE5160_RD_LOW_DLY();
p_wr[i] = (int16_t)TPAFE5160_READ_BUS(); p_wr[i] = (int16_t)TPAFE5160_READ_BUS();
TP_RD_HIGH_DLY(); TPAFE5160_RD_HIGH_DLY();
} }
/* 切换缓冲区并标记就绪 */ /* 切换缓冲区并标记就绪 */

View File

@@ -48,20 +48,20 @@ extern "C" {
/* 过采样率枚举 (OS[2:0] 编码OS2为MSB, OS0为LSB) */ /* 过采样率枚举 (OS[2:0] 编码OS2为MSB, OS0为LSB) */
typedef enum { typedef enum {
TP_OS_NONE = 0, /* 000 — 无过采样350 kSPS */ TPAFE5160_OS_NONE = 0, /* 000 — 无过采样350 kSPS */
TP_OS_X2 = 1, /* 001 — 2倍过采样175 kSPS */ TPAFE5160_OS_X2 = 1, /* 001 — 2倍过采样175 kSPS */
TP_OS_X4 = 2, /* 010 — 4倍过采样87.5 kSPS */ TPAFE5160_OS_X4 = 2, /* 010 — 4倍过采样87.5 kSPS */
TP_OS_X8 = 3, /* 011 — 8倍过采样43.75 kSPS */ TPAFE5160_OS_X8 = 3, /* 011 — 8倍过采样43.75 kSPS */
TP_OS_X16 = 4, /* 100 — 16倍过采样21.875 kSPS */ TPAFE5160_OS_X16 = 4, /* 100 — 16倍过采样21.875 kSPS */
TP_OS_X32 = 5, /* 101 — 32倍过采样10.94 kSPS */ TPAFE5160_OS_X32 = 5, /* 101 — 32倍过采样10.94 kSPS */
TP_OS_X64 = 6, /* 110 — 64倍过采样5.47 kSPS */ TPAFE5160_OS_X64 = 6, /* 110 — 64倍过采样5.47 kSPS */
TP_OS_HBW = 7 /* 111 — 高带宽模式 (~30kHz)350 kSPS */ TPAFE5160_OS_HBW = 7 /* 111 — 高带宽模式 (~30kHz)350 kSPS */
} tpafe5160_os_t; } tpafe5160_os_t;
/* 输入量程枚举 */ /* 输入量程枚举 */
typedef enum { typedef enum {
TP_RANGE_5V = 0, /* ±5V (RANGE = LOW) */ TPAFE5160_RANGE_5V = 0, /* ±5V (RANGE = LOW) */
TP_RANGE_10V = 1 /* ±10V (RANGE = HIGH) */ TPAFE5160_RANGE_10V = 1 /* ±10V (RANGE = HIGH) */
} tpafe5160_range_t; } tpafe5160_range_t;
/* 返回值定义 */ /* 返回值定义 */

View File

@@ -8,6 +8,11 @@
* - 两者都不定义:所有 DBG_* 宏为空操作release 版本) * - 两者都不定义:所有 DBG_* 宏为空操作release 版本)
* *
* 用户可在本文件中直接控制,也可在编译器预定义中覆盖 * 用户可在本文件中直接控制,也可在编译器预定义中覆盖
* 适用平台:所有 BSP 驱动
* 作者:王建锋
* 创建日期2026-07-18
* 修改记录:
* 2026-07-18 王建锋 创建初始版本
*/ */
#ifndef __DBG_CFG_H #ifndef __DBG_CFG_H
@@ -28,6 +33,12 @@ extern "C" {
*/ */
/* #define DBG_DEBUG_ENABLE */ /* #define DBG_DEBUG_ENABLE */
/*
* 系统时间戳开关:取消注释在所有日志行首追加 [YYYY-MM-DD HH:MM:SS] 时间戳
* 依赖 sys_clock_init() 已调用SD2506 RTC 初始化完成)
*/
#define APP_TIMESTAMP_ENABLE
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -8,6 +8,10 @@
* #define DBG_TAG "[CH395]" * #define DBG_TAG "[CH395]"
* #include "dbg_log.h" * #include "dbg_log.h"
* 适用平台:所有 BSP 驱动 * 适用平台:所有 BSP 驱动
* 作者:王建锋
* 创建日期2026-07-18
* 修改记录:
* 2026-07-18 王建锋 创建初始版本
*/ */
#ifndef __DBG_LOG_H #ifndef __DBG_LOG_H
@@ -18,8 +22,13 @@ extern "C" {
#endif #endif
#include <stdio.h> #include <stdio.h>
#include <stddef.h>
#include "dbg_cfg.h" #include "dbg_cfg.h"
#ifdef APP_TIMESTAMP_ENABLE
#include "sys_clock.h"
#endif
/* /*
* 总开关DBG_ENABLE * 总开关DBG_ENABLE
* 定义 → 输出 DBG_ERROR / DBG_INFO若 DBG_DEBUG_ENABLE 也定义则输出 DBG_DEBUG * 定义 → 输出 DBG_ERROR / DBG_INFO若 DBG_DEBUG_ENABLE 也定义则输出 DBG_DEBUG
@@ -44,31 +53,76 @@ extern "C" {
#define DBG_TAG "" #define DBG_TAG ""
#endif #endif
#ifdef APP_TIMESTAMP_ENABLE
/* /*
* DBG_ERROR - 错误输出,受 DBG_ENABLE 控制 * 时间戳输出宏 — 在日志行首追加 [YYYY-MM-DD HH:MM:SS] 格式的系统墙钟时间
* sys_clock_get_str() 线程安全(内部使用 DWT US 计数,无锁)
*/
#define DBG_TS() sys_clock_get_str((char[32]){0}, 32)
/*
* DBG_ERROR - 错误输出,受 DBG_ENABLE + APP_TIMESTAMP_ENABLE 控制
*/
#define DBG_ERROR(fmt, ...) do { \
char __ts_buf[32]; \
(void)__ts_buf; \
DBG_PRINT("[%s]" "[ERR]" DBG_TAG " " fmt "\r\n", \
sys_clock_get_str(__ts_buf, sizeof(__ts_buf)), ##__VA_ARGS__); \
} while (0)
/*
* DBG_INFO - 信息输出,受 DBG_ENABLE + APP_TIMESTAMP_ENABLE 控制
*/
#define DBG_INFO(fmt, ...) do { \
char __ts_buf[32]; \
(void)__ts_buf; \
DBG_PRINT("[%s]" DBG_TAG " " fmt "\r\n", \
sys_clock_get_str(__ts_buf, sizeof(__ts_buf)), ##__VA_ARGS__); \
} while (0)
/*
* DBG_DEBUG - 调试输出,需 DBG_ENABLE + DBG_DEBUG_ENABLE + APP_TIMESTAMP_ENABLE 同时定义
*/
#ifdef DBG_DEBUG_ENABLE
#define DBG_DEBUG(fmt, ...) do { \
char __ts_buf[32]; \
(void)__ts_buf; \
DBG_PRINT("[%s]" DBG_TAG "(%s:%d) " fmt "\r\n", \
sys_clock_get_str(__ts_buf, sizeof(__ts_buf)), __func__, __LINE__, ##__VA_ARGS__); \
} while (0)
#else
#define DBG_DEBUG(fmt, ...)
#endif
#else /* APP_TIMESTAMP_ENABLE not defined */
/*
* DBG_ERROR - 错误输出,受 DBG_ENABLE 控制(无时间戳)
*/ */
#define DBG_ERROR(fmt, ...) do { \ #define DBG_ERROR(fmt, ...) do { \
DBG_PRINT("[ERR]" DBG_TAG " " fmt "\r\n", ##__VA_ARGS__); \ DBG_PRINT("[ERR]" DBG_TAG " " fmt "\r\n", ##__VA_ARGS__); \
} while(0) } while (0)
/* /*
* DBG_INFO - 信息输出,受 DBG_ENABLE 控制 * DBG_INFO - 信息输出,受 DBG_ENABLE 控制(无时间戳)
*/ */
#define DBG_INFO(fmt, ...) do { \ #define DBG_INFO(fmt, ...) do { \
DBG_PRINT(DBG_TAG " " fmt "\r\n", ##__VA_ARGS__); \ DBG_PRINT(DBG_TAG " " fmt "\r\n", ##__VA_ARGS__); \
} while(0) } while (0)
/* /*
* DBG_DEBUG - 调试输出,需 DBG_ENABLE + DBG_DEBUG_ENABLE 同时定义 * DBG_DEBUG - 调试输出,需 DBG_ENABLE + DBG_DEBUG_ENABLE 同时定义(无时间戳)
*/ */
#ifdef DBG_DEBUG_ENABLE #ifdef DBG_DEBUG_ENABLE
#define DBG_DEBUG(fmt, ...) do { \ #define DBG_DEBUG(fmt, ...) do { \
DBG_PRINT(DBG_TAG "(%s:%d) " fmt "\r\n", __func__, __LINE__, ##__VA_ARGS__); \ DBG_PRINT(DBG_TAG "(%s:%d) " fmt "\r\n", __func__, __LINE__, ##__VA_ARGS__); \
} while(0) } while (0)
#else #else
#define DBG_DEBUG(fmt, ...) #define DBG_DEBUG(fmt, ...)
#endif #endif
#endif /* APP_TIMESTAMP_ENABLE */
#else /* DBG_ENABLE not defined — all debug output disabled */ #else /* DBG_ENABLE not defined — all debug output disabled */
#define DBG_ERROR(fmt, ...) #define DBG_ERROR(fmt, ...)

View File

@@ -55,6 +55,7 @@ void DebugMon_Handler(void);
void DMA1_Stream3_IRQHandler(void); void DMA1_Stream3_IRQHandler(void);
void DMA1_Stream4_IRQHandler(void); void DMA1_Stream4_IRQHandler(void);
void EXTI9_5_IRQHandler(void); void EXTI9_5_IRQHandler(void);
void SPI1_IRQHandler(void);
void SPI2_IRQHandler(void); void SPI2_IRQHandler(void);
void USART1_IRQHandler(void); void USART1_IRQHandler(void);
void USART2_IRQHandler(void); void USART2_IRQHandler(void);
@@ -62,7 +63,9 @@ void USART3_IRQHandler(void);
void UART4_IRQHandler(void); void UART4_IRQHandler(void);
void UART5_IRQHandler(void); void UART5_IRQHandler(void);
void TIM7_IRQHandler(void); void TIM7_IRQHandler(void);
void DMA2_Stream0_IRQHandler(void);
void DMA2_Stream2_IRQHandler(void); void DMA2_Stream2_IRQHandler(void);
void DMA2_Stream3_IRQHandler(void);
void DMA2_Stream7_IRQHandler(void); void DMA2_Stream7_IRQHandler(void);
/* USER CODE BEGIN EFP */ /* USER CODE BEGIN EFP */

43
Lib/FatFs/diskio.h Normal file
View File

@@ -0,0 +1,43 @@
#ifndef _DISKIO_DEFINED
#define _DISKIO_DEFINED
#ifdef __cplusplus
extern "C" {
#endif
typedef BYTE DSTATUS;
typedef enum {
RES_OK = 0,
RES_ERROR,
RES_WRPRT,
RES_NOTRDY,
RES_PARERR
} DRESULT;
DSTATUS disk_initialize(BYTE pdrv);
DSTATUS disk_status(BYTE pdrv);
DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count);
DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count);
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff);
#define STA_NOINIT 0x01
#define STA_NODISK 0x02
#define STA_PROTECT 0x04
#define CTRL_SYNC 0
#define GET_SECTOR_COUNT 1
#define GET_SECTOR_SIZE 2
#define GET_BLOCK_SIZE 3
#define CTRL_TRIM 4
#define CTRL_POWER 5
#define CTRL_LOCK 6
#define CTRL_EJECT 7
#define CTRL_FORMAT 8
#ifdef __cplusplus
}
#endif
#endif

7084
Lib/FatFs/ff.c Normal file

File diff suppressed because it is too large Load Diff

429
Lib/FatFs/ff.h Normal file
View File

@@ -0,0 +1,429 @@
/*----------------------------------------------------------------------------/
/ FatFs - Generic FAT Filesystem module R0.15 /
/-----------------------------------------------------------------------------/
/
/ Copyright (C) 2022, ChaN, all right reserved.
/
/ FatFs module is an open source software. Redistribution and use of FatFs in
/ source and binary forms, with or without modification, are permitted provided
/ that the following condition is met:
/
/ 1. Redistributions of source code must retain the above copyright notice,
/ this condition and the following disclaimer.
/
/ This software is provided by the copyright holder and contributors "AS IS"
/ and any warranties related to this software are DISCLAIMED.
/ The copyright owner or contributors be NOT LIABLE for any damages caused
/ by use of this software.
/
/----------------------------------------------------------------------------*/
#ifndef FF_DEFINED
#define FF_DEFINED 80286 /* Revision ID */
#ifdef __cplusplus
extern "C" {
#endif
#include "ffconf.h" /* FatFs configuration options */
#if FF_DEFINED != FFCONF_DEF
#error Wrong configuration file (ffconf.h).
#endif
/* Integer types used for FatFs API */
#if defined(_WIN32) /* Windows VC++ (for development only) */
#define FF_INTDEF 2
#include <windows.h>
typedef unsigned __int64 QWORD;
#include <float.h>
#define isnan(v) _isnan(v)
#define isinf(v) (!_finite(v))
#elif (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__cplusplus) /* C99 or later */
#define FF_INTDEF 2
#include <stdint.h>
typedef unsigned int UINT; /* int must be 16-bit or 32-bit */
typedef unsigned char BYTE; /* char must be 8-bit */
typedef uint16_t WORD; /* 16-bit unsigned integer */
typedef uint32_t DWORD; /* 32-bit unsigned integer */
typedef uint64_t QWORD; /* 64-bit unsigned integer */
typedef WORD WCHAR; /* UTF-16 character type */
#else /* Earlier than C99 */
#define FF_INTDEF 1
typedef unsigned int UINT; /* int must be 16-bit or 32-bit */
typedef unsigned char BYTE; /* char must be 8-bit */
typedef unsigned short WORD; /* 16-bit unsigned integer */
typedef unsigned long DWORD; /* 32-bit unsigned integer */
typedef WORD WCHAR; /* UTF-16 character type */
#endif
/* Type of file size and LBA variables */
#if FF_FS_EXFAT
#if FF_INTDEF != 2
#error exFAT feature wants C99 or later
#endif
typedef QWORD FSIZE_t;
#if FF_LBA64
typedef QWORD LBA_t;
#else
typedef DWORD LBA_t;
#endif
#else
#if FF_LBA64
#error exFAT needs to be enabled when enable 64-bit LBA
#endif
typedef DWORD FSIZE_t;
typedef DWORD LBA_t;
#endif
/* Type of path name strings on FatFs API (TCHAR) */
#if FF_USE_LFN && FF_LFN_UNICODE == 1 /* Unicode in UTF-16 encoding */
typedef WCHAR TCHAR;
#define _T(x) L ## x
#define _TEXT(x) L ## x
#elif FF_USE_LFN && FF_LFN_UNICODE == 2 /* Unicode in UTF-8 encoding */
typedef char TCHAR;
#define _T(x) u8 ## x
#define _TEXT(x) u8 ## x
#elif FF_USE_LFN && FF_LFN_UNICODE == 3 /* Unicode in UTF-32 encoding */
typedef DWORD TCHAR;
#define _T(x) U ## x
#define _TEXT(x) U ## x
#elif FF_USE_LFN && (FF_LFN_UNICODE < 0 || FF_LFN_UNICODE > 3)
#error Wrong FF_LFN_UNICODE setting
#else /* ANSI/OEM code in SBCS/DBCS */
typedef char TCHAR;
#define _T(x) x
#define _TEXT(x) x
#endif
/* Definitions of volume management */
#if FF_MULTI_PARTITION /* Multiple partition configuration */
typedef struct {
BYTE pd; /* Physical drive number */
BYTE pt; /* Partition: 0:Auto detect, 1-4:Forced partition) */
} PARTITION;
extern PARTITION VolToPart[]; /* Volume - Partition mapping table */
#endif
#if FF_STR_VOLUME_ID
#ifndef FF_VOLUME_STRS
extern const char* VolumeStr[FF_VOLUMES]; /* User defied volume ID */
#endif
#endif
/* Filesystem object structure (FATFS) */
typedef struct {
BYTE fs_type; /* Filesystem type (0:not mounted) */
BYTE pdrv; /* Volume hosting physical drive */
BYTE ldrv; /* Logical drive number (used only when FF_FS_REENTRANT) */
BYTE n_fats; /* Number of FATs (1 or 2) */
BYTE wflag; /* win[] status (b0:dirty) */
BYTE fsi_flag; /* FSINFO status (b7:disabled, b0:dirty) */
WORD id; /* Volume mount ID */
WORD n_rootdir; /* Number of root directory entries (FAT12/16) */
WORD csize; /* Cluster size [sectors] */
#if FF_MAX_SS != FF_MIN_SS
WORD ssize; /* Sector size (512, 1024, 2048 or 4096) */
#endif
#if FF_USE_LFN
WCHAR* lfnbuf; /* LFN working buffer */
#endif
#if FF_FS_EXFAT
BYTE* dirbuf; /* Directory entry block scratchpad buffer for exFAT */
#endif
#if !FF_FS_READONLY
DWORD last_clst; /* Last allocated cluster */
DWORD free_clst; /* Number of free clusters */
#endif
#if FF_FS_RPATH
DWORD cdir; /* Current directory start cluster (0:root) */
#if FF_FS_EXFAT
DWORD cdc_scl; /* Containing directory start cluster (invalid when cdir is 0) */
DWORD cdc_size; /* b31-b8:Size of containing directory, b7-b0: Chain status */
DWORD cdc_ofs; /* Offset in the containing directory (invalid when cdir is 0) */
#endif
#endif
DWORD n_fatent; /* Number of FAT entries (number of clusters + 2) */
DWORD fsize; /* Number of sectors per FAT */
LBA_t volbase; /* Volume base sector */
LBA_t fatbase; /* FAT base sector */
LBA_t dirbase; /* Root directory base sector (FAT12/16) or cluster (FAT32/exFAT) */
LBA_t database; /* Data base sector */
#if FF_FS_EXFAT
LBA_t bitbase; /* Allocation bitmap base sector */
#endif
LBA_t winsect; /* Current sector appearing in the win[] */
BYTE win[FF_MAX_SS]; /* Disk access window for Directory, FAT (and file data at tiny cfg) */
} FATFS;
/* Object ID and allocation information (FFOBJID) */
typedef struct {
FATFS* fs; /* Pointer to the hosting volume of this object */
WORD id; /* Hosting volume's mount ID */
BYTE attr; /* Object attribute */
BYTE stat; /* Object chain status (b1-0: =0:not contiguous, =2:contiguous, =3:fragmented in this session, b2:sub-directory stretched) */
DWORD sclust; /* Object data start cluster (0:no cluster or root directory) */
FSIZE_t objsize; /* Object size (valid when sclust != 0) */
#if FF_FS_EXFAT
DWORD n_cont; /* Size of first fragment - 1 (valid when stat == 3) */
DWORD n_frag; /* Size of last fragment needs to be written to FAT (valid when not zero) */
DWORD c_scl; /* Containing directory start cluster (valid when sclust != 0) */
DWORD c_size; /* b31-b8:Size of containing directory, b7-b0: Chain status (valid when c_scl != 0) */
DWORD c_ofs; /* Offset in the containing directory (valid when file object and sclust != 0) */
#endif
#if FF_FS_LOCK
UINT lockid; /* File lock ID origin from 1 (index of file semaphore table Files[]) */
#endif
} FFOBJID;
/* File object structure (FIL) */
typedef struct {
FFOBJID obj; /* Object identifier (must be the 1st member to detect invalid object pointer) */
BYTE flag; /* File status flags */
BYTE err; /* Abort flag (error code) */
FSIZE_t fptr; /* File read/write pointer (Zeroed on file open) */
DWORD clust; /* Current cluster of fpter (invalid when fptr is 0) */
LBA_t sect; /* Sector number appearing in buf[] (0:invalid) */
#if !FF_FS_READONLY
LBA_t dir_sect; /* Sector number containing the directory entry (not used at exFAT) */
BYTE* dir_ptr; /* Pointer to the directory entry in the win[] (not used at exFAT) */
#endif
#if FF_USE_FASTSEEK
DWORD* cltbl; /* Pointer to the cluster link map table (nulled on open, set by application) */
#endif
#if !FF_FS_TINY
BYTE buf[FF_MAX_SS]; /* File private data read/write window */
#endif
} FIL;
/* Directory object structure (DIR) */
typedef struct {
FFOBJID obj; /* Object identifier */
DWORD dptr; /* Current read/write offset */
DWORD clust; /* Current cluster */
LBA_t sect; /* Current sector (0:Read operation has terminated) */
BYTE* dir; /* Pointer to the directory item in the win[] */
BYTE fn[12]; /* SFN (in/out) {body[8],ext[3],status[1]} */
#if FF_USE_LFN
DWORD blk_ofs; /* Offset of current entry block being processed (0xFFFFFFFF:Invalid) */
#endif
#if FF_USE_FIND
const TCHAR* pat; /* Pointer to the name matching pattern */
#endif
} DIR;
/* File information structure (FILINFO) */
typedef struct {
FSIZE_t fsize; /* File size */
WORD fdate; /* Modified date */
WORD ftime; /* Modified time */
BYTE fattrib; /* File attribute */
#if FF_USE_LFN
TCHAR altname[FF_SFN_BUF + 1];/* Alternative file name */
TCHAR fname[FF_LFN_BUF + 1]; /* Primary file name */
#else
TCHAR fname[12 + 1]; /* File name */
#endif
} FILINFO;
/* Format parameter structure (MKFS_PARM) */
typedef struct {
BYTE fmt; /* Format option (FM_FAT, FM_FAT32, FM_EXFAT and FM_SFD) */
BYTE n_fat; /* Number of FATs */
UINT align; /* Data area alignment (sector) */
UINT n_root; /* Number of root directory entries */
DWORD au_size; /* Cluster size (byte) */
} MKFS_PARM;
/* File function return code (FRESULT) */
typedef enum {
FR_OK = 0, /* (0) Succeeded */
FR_DISK_ERR, /* (1) A hard error occurred in the low level disk I/O layer */
FR_INT_ERR, /* (2) Assertion failed */
FR_NOT_READY, /* (3) The physical drive cannot work */
FR_NO_FILE, /* (4) Could not find the file */
FR_NO_PATH, /* (5) Could not find the path */
FR_INVALID_NAME, /* (6) The path name format is invalid */
FR_DENIED, /* (7) Access denied due to prohibited access or directory full */
FR_EXIST, /* (8) Access denied due to prohibited access */
FR_INVALID_OBJECT, /* (9) The file/directory object is invalid */
FR_WRITE_PROTECTED, /* (10) The physical drive is write protected */
FR_INVALID_DRIVE, /* (11) The logical drive number is invalid */
FR_NOT_ENABLED, /* (12) The volume has no work area */
FR_NO_FILESYSTEM, /* (13) There is no valid FAT volume */
FR_MKFS_ABORTED, /* (14) The f_mkfs() aborted due to any problem */
FR_TIMEOUT, /* (15) Could not get a grant to access the volume within defined period */
FR_LOCKED, /* (16) The operation is rejected according to the file sharing policy */
FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated */
FR_TOO_MANY_OPEN_FILES, /* (18) Number of open files > FF_FS_LOCK */
FR_INVALID_PARAMETER /* (19) Given parameter is invalid */
} FRESULT;
/*--------------------------------------------------------------*/
/* FatFs Module Application Interface */
/*--------------------------------------------------------------*/
FRESULT f_open (FIL* fp, const TCHAR* path, BYTE mode); /* Open or create a file */
FRESULT f_close (FIL* fp); /* Close an open file object */
FRESULT f_read (FIL* fp, void* buff, UINT btr, UINT* br); /* Read data from the file */
FRESULT f_write (FIL* fp, const void* buff, UINT btw, UINT* bw); /* Write data to the file */
FRESULT f_lseek (FIL* fp, FSIZE_t ofs); /* Move file pointer of the file object */
FRESULT f_truncate (FIL* fp); /* Truncate the file */
FRESULT f_sync (FIL* fp); /* Flush cached data of the writing file */
FRESULT f_opendir (DIR* dp, const TCHAR* path); /* Open a directory */
FRESULT f_closedir (DIR* dp); /* Close an open directory */
FRESULT f_readdir (DIR* dp, FILINFO* fno); /* Read a directory item */
FRESULT f_findfirst (DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern); /* Find first file */
FRESULT f_findnext (DIR* dp, FILINFO* fno); /* Find next file */
FRESULT f_mkdir (const TCHAR* path); /* Create a sub directory */
FRESULT f_unlink (const TCHAR* path); /* Delete an existing file or directory */
FRESULT f_rename (const TCHAR* path_old, const TCHAR* path_new); /* Rename/Move a file or directory */
FRESULT f_stat (const TCHAR* path, FILINFO* fno); /* Get file status */
FRESULT f_chmod (const TCHAR* path, BYTE attr, BYTE mask); /* Change attribute of a file/dir */
FRESULT f_utime (const TCHAR* path, const FILINFO* fno); /* Change timestamp of a file/dir */
FRESULT f_chdir (const TCHAR* path); /* Change current directory */
FRESULT f_chdrive (const TCHAR* path); /* Change current drive */
FRESULT f_getcwd (TCHAR* buff, UINT len); /* Get current directory */
FRESULT f_getfree (const TCHAR* path, DWORD* nclst, FATFS** fatfs); /* Get number of free clusters on the drive */
FRESULT f_getlabel (const TCHAR* path, TCHAR* label, DWORD* vsn); /* Get volume label */
FRESULT f_setlabel (const TCHAR* label); /* Set volume label */
FRESULT f_forward (FIL* fp, UINT(*func)(const BYTE*,UINT), UINT btf, UINT* bf); /* Forward data to the stream */
FRESULT f_expand (FIL* fp, FSIZE_t fsz, BYTE opt); /* Allocate a contiguous block to the file */
FRESULT f_mount (FATFS* fs, const TCHAR* path, BYTE opt); /* Mount/Unmount a logical drive */
FRESULT f_mkfs (const TCHAR* path, const MKFS_PARM* opt, void* work, UINT len); /* Create a FAT volume */
FRESULT f_fdisk (BYTE pdrv, const LBA_t ptbl[], void* work); /* Divide a physical drive into some partitions */
FRESULT f_setcp (WORD cp); /* Set current code page */
int f_putc (TCHAR c, FIL* fp); /* Put a character to the file */
int f_puts (const TCHAR* str, FIL* cp); /* Put a string to the file */
int f_printf (FIL* fp, const TCHAR* str, ...); /* Put a formatted string to the file */
TCHAR* f_gets (TCHAR* buff, int len, FIL* fp); /* Get a string from the file */
/* Some API fucntions are implemented as macro */
#define f_eof(fp) ((int)((fp)->fptr == (fp)->obj.objsize))
#define f_error(fp) ((fp)->err)
#define f_tell(fp) ((fp)->fptr)
#define f_size(fp) ((fp)->obj.objsize)
#define f_rewind(fp) f_lseek((fp), 0)
#define f_rewinddir(dp) f_readdir((dp), 0)
#define f_rmdir(path) f_unlink(path)
#define f_unmount(path) f_mount(0, path, 0)
/*--------------------------------------------------------------*/
/* Additional Functions */
/*--------------------------------------------------------------*/
/* RTC function (provided by user) */
#if !FF_FS_READONLY && !FF_FS_NORTC
DWORD get_fattime (void); /* Get current time */
#endif
/* LFN support functions (defined in ffunicode.c) */
#if FF_USE_LFN >= 1
WCHAR ff_oem2uni (WCHAR oem, WORD cp); /* OEM code to Unicode conversion */
WCHAR ff_uni2oem (DWORD uni, WORD cp); /* Unicode to OEM code conversion */
DWORD ff_wtoupper (DWORD uni); /* Unicode upper-case conversion */
#endif
/* O/S dependent functions (samples available in ffsystem.c) */
#if FF_USE_LFN == 3 /* Dynamic memory allocation */
void* ff_memalloc (UINT msize); /* Allocate memory block */
void ff_memfree (void* mblock); /* Free memory block */
#endif
#if FF_FS_REENTRANT /* Sync functions */
int ff_mutex_create (int vol); /* Create a sync object */
void ff_mutex_delete (int vol); /* Delete a sync object */
int ff_mutex_take (int vol); /* Lock sync object */
void ff_mutex_give (int vol); /* Unlock sync object */
#endif
/*--------------------------------------------------------------*/
/* Flags and Offset Address */
/*--------------------------------------------------------------*/
/* File access mode and open method flags (3rd argument of f_open) */
#define FA_READ 0x01
#define FA_WRITE 0x02
#define FA_OPEN_EXISTING 0x00
#define FA_CREATE_NEW 0x04
#define FA_CREATE_ALWAYS 0x08
#define FA_OPEN_ALWAYS 0x10
#define FA_OPEN_APPEND 0x30
/* Fast seek controls (2nd argument of f_lseek) */
#define CREATE_LINKMAP ((FSIZE_t)0 - 1)
/* Format options (2nd argument of f_mkfs) */
#define FM_FAT 0x01
#define FM_FAT32 0x02
#define FM_EXFAT 0x04
#define FM_ANY 0x07
#define FM_SFD 0x08
/* Filesystem type (FATFS.fs_type) */
#define FS_FAT12 1
#define FS_FAT16 2
#define FS_FAT32 3
#define FS_EXFAT 4
/* File attribute bits for directory entry (FILINFO.fattrib) */
#define AM_RDO 0x01 /* Read only */
#define AM_HID 0x02 /* Hidden */
#define AM_SYS 0x04 /* System */
#define AM_DIR 0x10 /* Directory */
#define AM_ARC 0x20 /* Archive */
#ifdef __cplusplus
}
#endif
#endif /* FF_DEFINED */

41
Lib/FatFs/ffconf.h Normal file
View File

@@ -0,0 +1,41 @@
#define FFCONF_DEF 80286
#define FF_FS_READONLY 0
#define FF_FS_MINIMIZE 0
#define FF_USE_FIND 0
#define FF_USE_MKFS 1
#define FF_USE_FASTSEEK 0
#define FF_USE_EXPAND 0
#define FF_USE_CHMOD 0
#define FF_USE_LABEL 0
#define FF_USE_FORWARD 0
#define FF_USE_STRFUNC 0
#define FF_PRINT_LLI 1
#define FF_PRINT_FLOAT 1
#define FF_STRF_ENCODE 3
#define FF_CODE_PAGE 437
#define FF_USE_LFN 0
#define FF_MAX_LFN 255
#define FF_LFN_UNICODE 0
#define FF_LFN_BUF 255
#define FF_SFN_BUF 12
#define FF_FS_RPATH 0
#define FF_VOLUMES 1
#define FF_STR_VOLUME_ID 0
#define FF_VOLUME_STRS "RAM","NAND","CF","SD","SD2","USB","USB2","USB3"
#define FF_MULTI_PARTITION 0
#define FF_MIN_SS 512
#define FF_MAX_SS 512
#define FF_LBA64 0
#define FF_MIN_GPT 0x10000000
#define FF_USE_TRIM 0
#define FF_FS_TINY 0
#define FF_FS_EXFAT 0
#define FF_FS_NORTC 1
#define FF_NORTC_MON 1
#define FF_NORTC_MDAY 1
#define FF_NORTC_YEAR 2026
#define FF_FS_NOFSINFO 0
#define FF_FS_LOCK 0
#define FF_FS_REENTRANT 0
#define FF_FS_TIMEOUT 1000

Submodule Lib/FlashDB deleted from 8236571f6e

50
Lib/dhara/bytes.h Normal file
View File

@@ -0,0 +1,50 @@
/* Dhara - NAND flash management layer
* Copyright (C) 2013 Daniel Beer <dlbeer@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef DHARA_BYTES_H_
#define DHARA_BYTES_H_
#include <stdint.h>
static inline uint16_t dhara_r16(const uint8_t *data)
{
return ((uint16_t)data[0]) |
(((uint16_t)data[1]) << 8);
}
static inline void dhara_w16(uint8_t *data, uint16_t v)
{
data[0] = v;
data[1] = v >> 8;
}
static inline uint32_t dhara_r32(const uint8_t *data)
{
return ((uint32_t)data[0]) |
(((uint32_t)data[1]) << 8) |
(((uint32_t)data[2]) << 16) |
(((uint32_t)data[3]) << 24);
}
static inline void dhara_w32(uint8_t *data, uint32_t v)
{
data[0] = v;
data[1] = v >> 8;
data[2] = v >> 16;
data[3] = v >> 24;
}
#endif

45
Lib/dhara/error.h Normal file
View File

@@ -0,0 +1,45 @@
/* Dhara - NAND flash management layer
* Copyright (C) 2013 Daniel Beer <dlbeer@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef DHARA_ERROR_H_
#define DHARA_ERROR_H_
typedef enum {
DHARA_E_NONE = 0,
DHARA_E_BAD_BLOCK,
DHARA_E_ECC,
DHARA_E_TOO_BAD,
DHARA_E_RECOVER,
DHARA_E_JOURNAL_FULL,
DHARA_E_NOT_FOUND,
DHARA_E_MAP_FULL,
DHARA_E_CORRUPT_MAP,
DHARA_E_MAX
} dhara_error_t;
/* Produce a human-readable error message. This function is kept in a
* separate compilation unit and can be omitted to reduce binary size.
*/
const char *dhara_strerror(dhara_error_t err);
/* Save an error */
static inline void dhara_set_error(dhara_error_t *err, dhara_error_t v)
{
if (err)
*err = v;
}
#endif

884
Lib/dhara/journal.c Normal file
View File

@@ -0,0 +1,884 @@
/* Dhara - NAND flash management layer
* Copyright (C) 2013 Daniel Beer <dlbeer@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <string.h>
#include "journal.h"
#include "bytes.h"
/************************************************************************
* Metapage binary format
*/
/* Does the page buffer contain a valid checkpoint page? */
static inline int hdr_has_magic(const uint8_t *buf)
{
return (buf[0] == 'D') &&
(buf[1] == 'h') &&
(buf[2] == 'a');
}
static inline void hdr_put_magic(uint8_t *buf)
{
buf[0] = 'D';
buf[1] = 'h';
buf[2] = 'a';
}
/* What epoch is this page? */
static inline uint8_t hdr_get_epoch(const uint8_t *buf)
{
return buf[3];
}
static inline void hdr_set_epoch(uint8_t *buf, uint8_t e)
{
buf[3] = e;
}
static inline dhara_page_t hdr_get_tail(const uint8_t *buf)
{
return dhara_r32(buf + 4);
}
static inline void hdr_set_tail(uint8_t *buf, dhara_page_t tail)
{
dhara_w32(buf + 4, tail);
}
static inline dhara_page_t hdr_get_bb_current(const uint8_t *buf)
{
return dhara_r32(buf + 8);
}
static inline void hdr_set_bb_current(uint8_t *buf, dhara_page_t count)
{
dhara_w32(buf + 8, count);
}
static inline dhara_page_t hdr_get_bb_last(const uint8_t *buf)
{
return dhara_r32(buf + 12);
}
static inline void hdr_set_bb_last(uint8_t *buf, dhara_page_t count)
{
dhara_w32(buf + 12, count);
}
/* Clear user metadata */
static inline void hdr_clear_user(uint8_t *buf, uint8_t log2_page_size)
{
memset(buf + DHARA_HEADER_SIZE + DHARA_COOKIE_SIZE, 0xff,
(1 << log2_page_size) - DHARA_HEADER_SIZE - DHARA_COOKIE_SIZE);
}
/* Obtain pointers to user data */
static inline size_t hdr_user_offset(uint8_t which)
{
return DHARA_HEADER_SIZE + DHARA_COOKIE_SIZE +
which * DHARA_META_SIZE;
}
/************************************************************************
* Page geometry helpers
*/
/* Is this page index aligned to N bits? */
static inline int is_aligned(dhara_page_t p, int n)
{
return !(p & ((1 << n) - 1));
}
/* Are these two pages from the same alignment group? */
static inline int align_eq(dhara_page_t a, dhara_page_t b,
int n)
{
return !((a ^ b) >> n);
}
/* What is the successor of this block? */
static dhara_block_t next_block(const struct dhara_nand *n, dhara_block_t blk)
{
blk++;
if (blk >= n->num_blocks)
blk = 0;
return blk;
}
static dhara_page_t next_upage(const struct dhara_journal *j,
dhara_page_t p)
{
p++;
if (is_aligned(p + 1, j->log2_ppc))
p++;
if (p >= (j->nand->num_blocks << j->nand->log2_ppb))
p = 0;
return p;
}
/* Calculate a checkpoint period: the largest value of ppc such that
* (2**ppc - 1) metadata blocks can fit on a page with one journal
* header.
*/
static int choose_ppc(int log2_page_size, int max)
{
const int max_meta = (1 << log2_page_size) -
DHARA_HEADER_SIZE - DHARA_COOKIE_SIZE;
int total_meta = DHARA_META_SIZE;
int ppc = 1;
while (ppc < max) {
total_meta <<= 1;
total_meta += DHARA_META_SIZE;
if (total_meta > max_meta)
break;
ppc++;
}
return ppc;
}
/************************************************************************
* Journal setup/resume
*/
/* Clear recovery status */
static void clear_recovery(struct dhara_journal *j)
{
j->recover_next = DHARA_PAGE_NONE;
j->recover_root = DHARA_PAGE_NONE;
j->recover_meta = DHARA_PAGE_NONE;
j->flags &= ~(DHARA_JOURNAL_F_BAD_META |
DHARA_JOURNAL_F_RECOVERY |
DHARA_JOURNAL_F_ENUM_DONE);
}
/* Set up an empty journal */
static void reset_journal(struct dhara_journal *j)
{
/* We don't yet have a bad block estimate, so make a
* conservative guess.
*/
j->epoch = 0;
j->bb_last = j->nand->num_blocks >> 6;
j->bb_current = 0;
j->flags = 0;
/* Empty journal */
j->head = 0;
j->tail = 0;
j->tail_sync = 0;
j->root = DHARA_PAGE_NONE;
/* No recovery required */
clear_recovery(j);
/* Empty metadata buffer */
memset(j->page_buf, 0xff, 1 << j->nand->log2_page_size);
}
static void roll_stats(struct dhara_journal *j)
{
j->bb_last = j->bb_current;
j->bb_current = 0;
j->epoch++;
}
void dhara_journal_init(struct dhara_journal *j,
const struct dhara_nand *n,
uint8_t *page_buf)
{
/* Set fixed parameters */
j->nand = n;
j->page_buf = page_buf;
j->log2_ppc = choose_ppc(n->log2_page_size, n->log2_ppb);
reset_journal(j);
}
/* Find the first checkpoint-containing block. If a block contains any
* checkpoints at all, then it must contain one in the first checkpoint
* location -- otherwise, we would have considered the block eraseable.
*/
static int find_checkblock(struct dhara_journal *j,
dhara_block_t blk, dhara_block_t *where,
dhara_error_t *err)
{
int i;
for (i = 0; (blk < j->nand->num_blocks) &&
(i < DHARA_MAX_RETRIES); i++) {
const dhara_page_t p =
(blk << j->nand->log2_ppb) |
((1 << j->log2_ppc) - 1);
if (!(dhara_nand_is_bad(j->nand, blk) ||
dhara_nand_read(j->nand, p,
0, 1 << j->nand->log2_page_size,
j->page_buf, err)) &&
hdr_has_magic(j->page_buf)) {
*where = blk;
return 0;
}
blk++;
}
dhara_set_error(err, DHARA_E_TOO_BAD);
return -1;
}
static dhara_block_t find_last_checkblock(struct dhara_journal *j,
dhara_block_t first)
{
dhara_block_t low = first;
dhara_block_t high = j->nand->num_blocks - 1;
while (low <= high) {
const dhara_block_t mid = (low + high) >> 1;
dhara_block_t found;
if ((find_checkblock(j, mid, &found, NULL) < 0) ||
(hdr_get_epoch(j->page_buf) != j->epoch)) {
if (!mid)
return first;
high = mid - 1;
} else {
dhara_block_t nf;
if (((found + 1) >= j->nand->num_blocks) ||
(find_checkblock(j, found + 1,
&nf, NULL) < 0) ||
(hdr_get_epoch(j->page_buf) != j->epoch))
return found;
low = nf;
}
}
return first;
}
/* Test whether a checkpoint group is in a state fit for reprogramming,
* but allow for the fact that is_free() might not have any way of
* distinguishing between an unprogrammed page, and a page programmed
* with all-0xff bytes (but if so, it must be ok to reprogram such a
* page).
*
* We used to test for an unprogrammed checkpoint group by checking to
* see if the first user-page had been programmed since last erase (by
* testing only the first page with is_free). This works if is_free is
* precise, because the pages are written in order.
*
* If is_free is imprecise, we need to check all pages in the group.
* That also works, because the final page in a checkpoint group is
* guaranteed to contain non-0xff bytes. Therefore, we return 1 only if
* the group is truly unprogrammed, or if it was partially programmed
* with some all-0xff user pages (which changes nothing for us).
*/
static int cp_free(struct dhara_journal *j, dhara_page_t first_user)
{
const int count = 1 << j->log2_ppc;
int i;
for (i = 0; i < count; i++)
if (!dhara_nand_is_free(j->nand, first_user + i))
return 0;
return 1;
}
static dhara_page_t find_last_group(struct dhara_journal *j,
dhara_block_t blk)
{
const int num_groups = 1 << (j->nand->log2_ppb - j->log2_ppc);
int low = 0;
int high = num_groups - 1;
/* If a checkpoint group is completely unprogrammed, everything
* following it will be completely unprogrammed also.
*
* Therefore, binary search checkpoint groups until we find the
* last programmed one.
*/
while (low <= high) {
int mid = (low + high) >> 1;
const dhara_page_t p = (mid << j->log2_ppc) |
(blk << j->nand->log2_ppb);
if (cp_free(j, p)) {
high = mid - 1;
} else if (((mid + 1) >= num_groups) ||
cp_free(j, p + (1 << j->log2_ppc))) {
return p;
} else {
low = mid + 1;
}
}
return blk << j->nand->log2_ppb;
}
static int find_root(struct dhara_journal *j, dhara_page_t start,
dhara_error_t *err)
{
const dhara_block_t blk = start >> j->nand->log2_ppb;
int i = (start & ((1 << j->nand->log2_ppb) - 1)) >> j->log2_ppc;
while (i >= 0) {
const dhara_page_t p = (blk << j->nand->log2_ppb) +
((i + 1) << j->log2_ppc) - 1;
if (!dhara_nand_read(j->nand, p,
0, 1 << j->nand->log2_page_size,
j->page_buf, err) &&
(hdr_has_magic(j->page_buf)) &&
(hdr_get_epoch(j->page_buf) == j->epoch)) {
j->root = p - 1;
return 0;
}
i--;
}
dhara_set_error(err, DHARA_E_TOO_BAD);
return -1;
}
static int find_head(struct dhara_journal *j, dhara_page_t start,
dhara_error_t *err)
{
j->head = next_upage(j, start);
if (!j->head)
roll_stats(j);
/* Starting from the last good checkpoint, find either:
*
* (a) the next free user-page in the same block
* (b) or, the first page of the next block
*
* The block we end up on might be bad, but that's ok -- we'll
* skip it when we go to prepare the next write.
*/
for (;;) {
/* How many free pages trail this checkpoint group? */
const unsigned int ppc = 1 << j->log2_ppc;
unsigned int n = 0;
dhara_page_t first = j->head & ~(dhara_page_t)(ppc - 1);
while (n < ppc &&
dhara_nand_is_free(j->nand, first + ppc - n - 1))
n++;
/* If we have some, then we've found our next free
* userpage.
*/
if (n > 1) {
j->head = first + ppc - n;
break;
}
/* Skip to the next checkpoint group */
j->head = first + ppc;
if (j->head >= (j->nand->num_blocks << j->nand->log2_ppb)) {
j->head = 0;
roll_stats(j);
}
/* If we hit the end of the block, we're done */
if (is_aligned(j->head, j->nand->log2_ppb)) {
/* Make sure we don't chase over the tail */
if (align_eq(j->head, j->tail, j->nand->log2_ppb))
j->tail = next_block(j->nand,
j->tail >> j->nand->log2_ppb) <<
j->nand->log2_ppb;
break;
}
}
return 0;
}
int dhara_journal_resume(struct dhara_journal *j, dhara_error_t *err)
{
dhara_block_t first, last;
dhara_page_t last_group;
/* Find the first checkpoint-containing block */
if (find_checkblock(j, 0, &first, err) < 0) {
reset_journal(j);
return -1;
}
/* Find the last checkpoint-containing block in this epoch */
j->epoch = hdr_get_epoch(j->page_buf);
last = find_last_checkblock(j, first);
/* Find the last programmed checkpoint group in the block */
last_group = find_last_group(j, last);
/* Perform a linear scan to find the last good checkpoint (and
* therefore the root).
*/
if (find_root(j, last_group, err) < 0) {
reset_journal(j);
return -1;
}
/* Restore settings from checkpoint */
j->tail = hdr_get_tail(j->page_buf);
j->bb_current = hdr_get_bb_current(j->page_buf);
j->bb_last = hdr_get_bb_last(j->page_buf);
hdr_clear_user(j->page_buf, j->nand->log2_page_size);
/* Perform another linear scan to find the next free user page */
if (find_head(j, last_group, err) < 0) {
reset_journal(j);
return -1;
}
j->flags = 0;
j->tail_sync = j->tail;
clear_recovery(j);
return 0;
}
/**************************************************************************
* Public interface
*/
dhara_page_t dhara_journal_capacity(const struct dhara_journal *j)
{
const dhara_block_t max_bad = j->bb_last > j->bb_current ?
j->bb_last : j->bb_current;
const dhara_block_t good_blocks = j->nand->num_blocks - max_bad - 1;
const int log2_cpb = j->nand->log2_ppb - j->log2_ppc;
const dhara_page_t good_cps = good_blocks << log2_cpb;
/* Good checkpoints * (checkpoint period - 1) */
return (good_cps << j->log2_ppc) - good_cps;
}
dhara_page_t dhara_journal_size(const struct dhara_journal *j)
{
/* Find the number of raw pages, and the number of checkpoints
* between the head and the tail. The difference between the two
* is the number of user pages (upper limit).
*/
dhara_page_t num_pages = j->head;
dhara_page_t num_cps = j->head >> j->log2_ppc;
if (j->head < j->tail_sync) {
const dhara_page_t total_pages =
j->nand->num_blocks << j->nand->log2_ppb;
num_pages += total_pages;
num_cps += total_pages >> j->log2_ppc;
}
num_pages -= j->tail_sync;
num_cps -= j->tail_sync >> j->log2_ppc;
return num_pages - num_cps;
}
int dhara_journal_read_meta(struct dhara_journal *j, dhara_page_t p,
uint8_t *buf, dhara_error_t *err)
{
/* Offset of metadata within the metadata page */
const dhara_page_t ppc_mask = (1 << j->log2_ppc) - 1;
const size_t offset = hdr_user_offset(p & ppc_mask);
/* Special case: buffered metadata */
if (align_eq(p, j->head, j->log2_ppc)) {
memcpy(buf, j->page_buf + offset, DHARA_META_SIZE);
return 0;
}
/* Special case: incomplete metadata dumped at start of
* recovery.
*/
if ((j->recover_meta != DHARA_PAGE_NONE) &&
align_eq(p, j->recover_root, j->log2_ppc))
return dhara_nand_read(j->nand, j->recover_meta,
offset, DHARA_META_SIZE,
buf, err);
/* General case: fetch from metadata page for checkpoint group */
return dhara_nand_read(j->nand, p | ppc_mask,
offset, DHARA_META_SIZE,
buf, err);
}
dhara_page_t dhara_journal_peek(struct dhara_journal *j)
{
if (j->head == j->tail)
return DHARA_PAGE_NONE;
if (is_aligned(j->tail, j->nand->log2_ppb)) {
dhara_block_t blk = j->tail >> j->nand->log2_ppb;
int i;
for (i = 0; i < DHARA_MAX_RETRIES; i++) {
if ((blk == (j->head >> j->nand->log2_ppb)) ||
!dhara_nand_is_bad(j->nand, blk)) {
j->tail = blk << j->nand->log2_ppb;
if (j->tail == j->head)
j->root = DHARA_PAGE_NONE;
return j->tail;
}
blk = next_block(j->nand, blk);
}
}
return j->tail;
}
static dhara_page_t wrap(dhara_page_t a, dhara_page_t b)
{
return a >= b ? (a - b) : a;
}
void dhara_journal_dequeue(struct dhara_journal *j)
{
if (j->head == j->tail)
return;
j->tail = next_upage(j, j->tail);
/* If the journal is clean at the time of dequeue, then this
* data was always obsolete, and can be reused immediately.
*/
if (!(j->flags & (DHARA_JOURNAL_F_DIRTY | DHARA_JOURNAL_F_RECOVERY)))
j->tail_sync = j->tail;
const dhara_page_t chip_size = j->nand->num_blocks << j->nand->log2_ppb;
const dhara_page_t raw_size = wrap(j->head + chip_size - j->tail,
chip_size);
const dhara_page_t root_offset = wrap(j->head + chip_size - j->root,
chip_size);
if (root_offset > raw_size)
j->root = DHARA_PAGE_NONE;
}
void dhara_journal_clear(struct dhara_journal *j)
{
j->tail = j->head;
j->root = DHARA_PAGE_NONE;
j->flags |= DHARA_JOURNAL_F_DIRTY;
hdr_clear_user(j->page_buf, j->nand->log2_page_size);
}
static int skip_block(struct dhara_journal *j, dhara_error_t *err)
{
const dhara_block_t next = next_block(j->nand,
j->head >> j->nand->log2_ppb);
/* We can't roll onto the same block as the tail */
if ((j->tail_sync >> j->nand->log2_ppb) == next) {
dhara_set_error(err, DHARA_E_JOURNAL_FULL);
return -1;
}
j->head = next << j->nand->log2_ppb;
if (!j->head)
roll_stats(j);
return 0;
}
/* Make sure the head pointer is on a ready-to-program page. */
static int prepare_head(struct dhara_journal *j, dhara_error_t *err)
{
const dhara_page_t next = next_upage(j, j->head);
int i;
/* We can't write if doing so would cause the head pointer to
* roll onto the same block as the last-synced tail.
*/
if (align_eq(next, j->tail_sync, j->nand->log2_ppb) &&
!align_eq(next, j->head, j->nand->log2_ppb)) {
dhara_set_error(err, DHARA_E_JOURNAL_FULL);
return -1;
}
j->flags |= DHARA_JOURNAL_F_DIRTY;
if (!is_aligned(j->head, j->nand->log2_ppb))
return 0;
for (i = 0; i < DHARA_MAX_RETRIES; i++) {
const dhara_block_t blk = j->head >> j->nand->log2_ppb;
if (!dhara_nand_is_bad(j->nand, blk))
return dhara_nand_erase(j->nand, blk, err);
j->bb_current++;
if (skip_block(j, err) < 0)
return -1;
}
dhara_set_error(err, DHARA_E_TOO_BAD);
return -1;
}
static void restart_recovery(struct dhara_journal *j, dhara_page_t old_head)
{
/* Mark the current head bad immediately, unless we're also
* using it to hold our dumped metadata (it will then be marked
* bad at the end of recovery).
*/
if ((j->recover_meta == DHARA_PAGE_NONE) ||
!align_eq(j->recover_meta, old_head, j->nand->log2_ppb))
dhara_nand_mark_bad(j->nand, old_head >> j->nand->log2_ppb);
else
j->flags |= DHARA_JOURNAL_F_BAD_META;
/* Start recovery again. Reset the source enumeration to
* the start of the original bad block, and reset the
* destination enumeration to the newly found good
* block.
*/
j->flags &= ~DHARA_JOURNAL_F_ENUM_DONE;
j->recover_next =
j->recover_root & ~((1 << j->nand->log2_ppb) - 1);
j->root = j->recover_root;
}
static int dump_meta(struct dhara_journal *j, dhara_error_t *err)
{
int i;
/* We've just begun recovery on a new erasable block, but we
* have buffered metadata from the failed block.
*/
for (i = 0; i < DHARA_MAX_RETRIES; i++) {
dhara_error_t my_err;
/* Try to dump metadata on this page */
if (!(prepare_head(j, &my_err) ||
dhara_nand_prog(j->nand, j->head,
j->page_buf, &my_err))) {
j->recover_meta = j->head;
j->head = next_upage(j, j->head);
if (!j->head)
roll_stats(j);
hdr_clear_user(j->page_buf, j->nand->log2_page_size);
return 0;
}
/* Report fatal errors */
if (my_err != DHARA_E_BAD_BLOCK) {
dhara_set_error(err, my_err);
return -1;
}
j->bb_current++;
dhara_nand_mark_bad(j->nand, j->head >> j->nand->log2_ppb);
if (skip_block(j, err) < 0)
return -1;
}
dhara_set_error(err, DHARA_E_TOO_BAD);
return -1;
}
static int recover_from(struct dhara_journal *j,
dhara_error_t write_err,
dhara_error_t *err)
{
const dhara_page_t old_head = j->head;
if (write_err != DHARA_E_BAD_BLOCK) {
dhara_set_error(err, write_err);
return -1;
}
/* Advance to the next free page */
j->bb_current++;
if (skip_block(j, err) < 0)
return -1;
/* Are we already in the middle of a recovery? */
if (dhara_journal_in_recovery(j)) {
restart_recovery(j, old_head);
dhara_set_error(err, DHARA_E_RECOVER);
return -1;
}
/* Were we block aligned? No recovery required! */
if (is_aligned(old_head, j->nand->log2_ppb)) {
dhara_nand_mark_bad(j->nand, old_head >> j->nand->log2_ppb);
return 0;
}
j->recover_root = j->root;
j->recover_next =
j->recover_root & ~((1 << j->nand->log2_ppb) - 1);
/* Are we holding buffered metadata? Dump it first. */
if (!is_aligned(old_head, j->log2_ppc) &&
dump_meta(j, err) < 0)
return -1;
j->flags |= DHARA_JOURNAL_F_RECOVERY;
dhara_set_error(err, DHARA_E_RECOVER);
return -1;
}
static void finish_recovery(struct dhara_journal *j)
{
/* We just recovered the last page. Mark the recovered
* block as bad.
*/
dhara_nand_mark_bad(j->nand,
j->recover_root >> j->nand->log2_ppb);
/* If we had to dump metadata, and the page on which we
* did this also went bad, mark it bad too.
*/
if (j->flags & DHARA_JOURNAL_F_BAD_META)
dhara_nand_mark_bad(j->nand,
j->recover_meta >> j->nand->log2_ppb);
/* Was the tail on this page? Skip it forward */
clear_recovery(j);
}
static int push_meta(struct dhara_journal *j, const uint8_t *meta,
dhara_error_t *err)
{
const dhara_page_t old_head = j->head;
dhara_error_t my_err;
const size_t offset =
hdr_user_offset(j->head & ((1 << j->log2_ppc) - 1));
/* We've just written a user page. Add the metadata to the
* buffer.
*/
if (meta)
memcpy(j->page_buf + offset, meta, DHARA_META_SIZE);
else
memset(j->page_buf + offset, 0xff, DHARA_META_SIZE);
/* Unless we've filled the buffer, don't do any IO */
if (!is_aligned(j->head + 2, j->log2_ppc)) {
j->root = j->head;
j->head++;
return 0;
}
/* We don't need to check for immediate recover, because that'll
* never happen -- we're not block-aligned.
*/
hdr_put_magic(j->page_buf);
hdr_set_epoch(j->page_buf, j->epoch);
hdr_set_tail(j->page_buf, j->tail);
hdr_set_bb_current(j->page_buf, j->bb_current);
hdr_set_bb_last(j->page_buf, j->bb_last);
if (dhara_nand_prog(j->nand, j->head + 1, j->page_buf, &my_err) < 0)
return recover_from(j, my_err, err);
j->flags &= ~DHARA_JOURNAL_F_DIRTY;
j->root = old_head;
j->head = next_upage(j, j->head);
if (!j->head)
roll_stats(j);
if (j->flags & DHARA_JOURNAL_F_ENUM_DONE)
finish_recovery(j);
if (!(j->flags & DHARA_JOURNAL_F_RECOVERY))
j->tail_sync = j->tail;
return 0;
}
int dhara_journal_enqueue(struct dhara_journal *j,
const uint8_t *data, const uint8_t *meta,
dhara_error_t *err)
{
dhara_error_t my_err;
int i;
for (i = 0; i < DHARA_MAX_RETRIES; i++) {
if (!(prepare_head(j, &my_err) ||
(data && dhara_nand_prog(j->nand, j->head, data,
&my_err))))
return push_meta(j, meta, err);
if (recover_from(j, my_err, err) < 0)
return -1;
}
dhara_set_error(err, DHARA_E_TOO_BAD);
return -1;
}
int dhara_journal_copy(struct dhara_journal *j,
dhara_page_t p, const uint8_t *meta,
dhara_error_t *err)
{
dhara_error_t my_err;
int i;
for (i = 0; i < DHARA_MAX_RETRIES; i++) {
if (!(prepare_head(j, &my_err) ||
dhara_nand_copy(j->nand, p, j->head, &my_err)))
return push_meta(j, meta, err);
if (recover_from(j, my_err, err) < 0)
return -1;
}
dhara_set_error(err, DHARA_E_TOO_BAD);
return -1;
}
dhara_page_t dhara_journal_next_recoverable(struct dhara_journal *j)
{
const dhara_page_t n = j->recover_next;
if (!dhara_journal_in_recovery(j))
return DHARA_PAGE_NONE;
if (j->flags & DHARA_JOURNAL_F_ENUM_DONE)
return DHARA_PAGE_NONE;
if (j->recover_next == j->recover_root)
j->flags |= DHARA_JOURNAL_F_ENUM_DONE;
else
j->recover_next = next_upage(j, j->recover_next);
return n;
}

256
Lib/dhara/journal.h Normal file
View File

@@ -0,0 +1,256 @@
/* Dhara - NAND flash management layer
* Copyright (C) 2013 Daniel Beer <dlbeer@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef DHARA_JOURNAL_H_
#define DHARA_JOURNAL_H_
#include <stdint.h>
#include "nand.h"
/* Number of bytes used by the journal checkpoint header. */
#define DHARA_HEADER_SIZE 16
/* Global metadata available for a higher layer. This metadata is
* persistent once the journal reaches a checkpoint, and is restored on
* startup.
*/
#define DHARA_COOKIE_SIZE 4
/* This is the size of the metadata slice which accompanies each written
* page. This is independent of the underlying page/OOB size.
*/
#define DHARA_META_SIZE 132
/* When a block fails, or garbage is encountered, we try again on the
* next block/checkpoint. We can do this up to the given number of
* times.
*/
#define DHARA_MAX_RETRIES 8
/* This is a page number which can be used to represent "no such page".
* It's guaranteed to never be a valid user page.
*/
#define DHARA_PAGE_NONE ((dhara_page_t)0xffffffff)
/* State flags */
#define DHARA_JOURNAL_F_DIRTY 0x01
#define DHARA_JOURNAL_F_BAD_META 0x02
#define DHARA_JOURNAL_F_RECOVERY 0x04
#define DHARA_JOURNAL_F_ENUM_DONE 0x08
/* The journal layer presents the NAND pages as a double-ended queue.
* Pages, with associated metadata may be pushed onto the end of the
* queue, and pages may be popped from the end.
*
* Block erase, metadata storage are handled automatically. Bad blocks
* are handled by relocating data to the next available non-bad page in
* the sequence.
*
* It's up to the user to ensure that the queue doesn't grow beyond the
* capacity of the NAND chip, but helper functions are provided to
* assist with this. If the head meets the tail, the journal will refuse
* to enqueue more pages.
*/
struct dhara_journal {
const struct dhara_nand *nand;
uint8_t *page_buf;
/* In the journal, user data is grouped into checkpoints of
* 2**log2_ppc contiguous aligned pages.
*
* The last page of each checkpoint contains the journal header
* and the metadata for the other pages in the period (the user
* pages).
*/
uint8_t log2_ppc;
/* Epoch counter. This is incremented whenever the journal head
* passes the end of the chip and wraps around.
*/
uint8_t epoch;
/* General purpose flags field */
uint8_t flags;
/* Bad-block counters. bb_last is our best estimate of the
* number of bad blocks in the chip as a whole. bb_current is
* the number of bad blocks in all blocks before the current
* head.
*/
dhara_block_t bb_current;
dhara_block_t bb_last;
/* Log head and tail. The tail pointer points to the last user
* page in the log, and the head pointer points to the next free
* raw page. The root points to the last written user page.
*/
dhara_page_t tail_sync;
dhara_page_t tail;
dhara_page_t head;
/* This points to the last written user page in the journal */
dhara_page_t root;
/* Recovery mode: recover_root points to the last valid user
* page in the block requiring recovery. recover_next points to
* the next user page needing recovery.
*
* If we had buffered metadata before recovery started, it will
* have been dumped to a free page, indicated by recover_meta.
* If this block later goes bad, we will have to defer bad-block
* marking until recovery is complete (F_BAD_META).
*/
dhara_page_t recover_next;
dhara_page_t recover_root;
dhara_page_t recover_meta;
};
/* Initialize a journal. You must supply a pointer to a NAND chip
* driver, and a single page buffer. This page buffer will be used
* exclusively by the journal, but you are responsible for allocating
* it, and freeing it (if necessary) at the end.
*
* No NAND operations are performed at this point.
*/
void dhara_journal_init(struct dhara_journal *j,
const struct dhara_nand *n,
uint8_t *page_buf);
/* Start up the journal -- search the NAND for the journal head, or
* initialize a blank journal if one isn't found. Returns 0 on success
* or -1 if a (fatal) error occurs.
*
* This operation is O(log N), where N is the number of pages in the
* NAND chip. All other operations are O(1).
*
* If this operation fails, the journal will be reset to an empty state.
*/
int dhara_journal_resume(struct dhara_journal *j, dhara_error_t *err);
/* Obtain an upper bound on the number of user pages storable in the
* journal.
*/
dhara_page_t dhara_journal_capacity(const struct dhara_journal *j);
/* Obtain an upper bound on the number of user pages consumed by the
* journal.
*/
dhara_page_t dhara_journal_size(const struct dhara_journal *j);
/* Obtain a pointer to the cookie data */
static inline uint8_t *dhara_journal_cookie(const struct dhara_journal *j)
{
return j->page_buf + DHARA_HEADER_SIZE;
}
/* Obtain the locations of the first and last pages in the journal.
*/
static inline dhara_page_t dhara_journal_root(const struct dhara_journal *j)
{
return j->root;
}
/* Read metadata associated with a page. This assumes that the page
* provided is a valid data page. The actual page data is read via the
* normal NAND interface.
*/
int dhara_journal_read_meta(struct dhara_journal *j, dhara_page_t p,
uint8_t *buf, dhara_error_t *err);
/* Advance the tail to the next non-bad block and return the page that's
* ready to read. If no page is ready, return DHARA_PAGE_NONE.
*/
dhara_page_t dhara_journal_peek(struct dhara_journal *j);
/* Remove the last page from the journal. This doesn't take permanent
* effect until the next checkpoint.
*/
void dhara_journal_dequeue(struct dhara_journal *j);
/* Remove all pages form the journal. This doesn't take permanent effect
* until the next checkpoint.
*/
void dhara_journal_clear(struct dhara_journal *j);
/* Append a page to the journal. Both raw page data and metadata must be
* specified. The push operation is not persistent until a checkpoint is
* reached.
*
* This operation may fail with the error code E_RECOVER. If this
* occurs, the upper layer must complete the assisted recovery procedure
* and then try again.
*
* This operation may be used as part of a recovery. If further errors
* occur during recovery, E_RECOVER is returned, and the procedure must
* be restarted.
*/
int dhara_journal_enqueue(struct dhara_journal *j,
const uint8_t *data, const uint8_t *meta,
dhara_error_t *err);
/* Copy an existing page to the front of the journal. New metadata must
* be specified. This operation is not persistent until a checkpoint is
* reached.
*
* This operation may fail with the error code E_RECOVER. If this
* occurs, the upper layer must complete the assisted recovery procedure
* and then try again.
*
* This operation may be used as part of a recovery. If further errors
* occur during recovery, E_RECOVER is returned, and the procedure must
* be restarted.
*/
int dhara_journal_copy(struct dhara_journal *j,
dhara_page_t p, const uint8_t *meta,
dhara_error_t *err);
/* Mark the journal dirty. */
static inline void dhara_journal_mark_dirty(struct dhara_journal *j)
{
j->flags |= DHARA_JOURNAL_F_DIRTY;
}
/* Is the journal checkpointed? If true, then all pages enqueued are now
* persistent.
*/
static inline int dhara_journal_is_clean(const struct dhara_journal *j)
{
return !(j->flags & DHARA_JOURNAL_F_DIRTY);
}
/* If an operation returns E_RECOVER, you must begin the recovery
* procedure. You must then:
*
* - call dhara_journal_next_recoverable() to obtain the next block
* to be recovered (if any). If there are no blocks remaining to be
* recovered, DHARA_JOURNAL_PAGE_NONE is returned.
*
* - proceed to the next checkpoint. Once the journal is clean,
* recovery will finish automatically.
*
* If any operation during recovery fails due to a bad block, E_RECOVER
* is returned again, and recovery restarts. Do not add new data to the
* journal (rewrites of recovered data are fine) until recovery is
* complete.
*/
static inline int dhara_journal_in_recovery(const struct dhara_journal *j)
{
return j->flags & DHARA_JOURNAL_F_RECOVERY;
}
dhara_page_t dhara_journal_next_recoverable(struct dhara_journal *j);
#endif

530
Lib/dhara/map.c Normal file
View File

@@ -0,0 +1,530 @@
/* Dhara - NAND flash management layer
* Copyright (C) 2013 Daniel Beer <dlbeer@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <string.h>
#include "bytes.h"
#include "map.h"
#define DHARA_RADIX_DEPTH (sizeof(dhara_sector_t) << 3)
static inline dhara_sector_t d_bit(int depth)
{
return ((dhara_sector_t)1) << (DHARA_RADIX_DEPTH - depth - 1);
}
/************************************************************************
* Metadata/cookie layout
*/
static inline void ck_set_count(uint8_t *cookie, dhara_sector_t count)
{
dhara_w32(cookie, count);
}
static inline dhara_sector_t ck_get_count(const uint8_t *cookie)
{
return dhara_r32(cookie);
}
static inline void meta_clear(uint8_t *meta)
{
memset(meta, 0xff, DHARA_META_SIZE);
}
static inline dhara_sector_t meta_get_id(const uint8_t *meta)
{
return dhara_r32(meta);
}
static inline void meta_set_id(uint8_t *meta, dhara_sector_t id)
{
dhara_w32(meta, id);
}
static inline dhara_page_t meta_get_alt(const uint8_t *meta, int level)
{
return dhara_r32(meta + 4 + (level << 2));
}
static inline void meta_set_alt(uint8_t *meta, int level, dhara_page_t alt)
{
dhara_w32(meta + 4 + (level << 2), alt);
}
/************************************************************************
* Public interface
*/
void dhara_map_init(struct dhara_map *m, const struct dhara_nand *n,
uint8_t *page_buf, uint8_t gc_ratio)
{
if (!gc_ratio)
gc_ratio = 1;
dhara_journal_init(&m->journal, n, page_buf);
m->gc_ratio = gc_ratio;
}
int dhara_map_resume(struct dhara_map *m, dhara_error_t *err)
{
if (dhara_journal_resume(&m->journal, err) < 0) {
m->count = 0;
return -1;
}
m->count = ck_get_count(dhara_journal_cookie(&m->journal));
return 0;
}
void dhara_map_clear(struct dhara_map *m)
{
if (m->count) {
m->count = 0;
dhara_journal_clear(&m->journal);
}
}
dhara_sector_t dhara_map_capacity(const struct dhara_map *m)
{
const dhara_sector_t cap = dhara_journal_capacity(&m->journal);
const dhara_sector_t reserve = cap / (m->gc_ratio + 1);
const dhara_sector_t safety_margin =
DHARA_MAX_RETRIES << m->journal.nand->log2_ppb;
if (reserve + safety_margin >= cap)
return 0;
return cap - reserve - safety_margin;
}
/* Trace the path from the root to the given sector, emitting
* alt-pointers and alt-full bits in the given metadata buffer. This
* also returns the physical page containing the given sector, if it
* exists.
*
* If the page can't be found, a suitable path will be constructed
* (containing PAGE_NONE alt-pointers), and DHARA_E_NOT_FOUND will be
* returned.
*/
static int trace_path(struct dhara_map *m, dhara_sector_t target,
dhara_page_t *loc, uint8_t *new_meta,
dhara_error_t *err)
{
uint8_t meta[DHARA_META_SIZE];
int depth = 0;
dhara_page_t p = dhara_journal_root(&m->journal);
if (new_meta)
meta_set_id(new_meta, target);
if (p == DHARA_PAGE_NONE)
goto not_found;
if (dhara_journal_read_meta(&m->journal, p, meta, err) < 0)
return -1;
while (depth < DHARA_RADIX_DEPTH) {
const dhara_sector_t id = meta_get_id(meta);
if (id == DHARA_SECTOR_NONE)
goto not_found;
if ((target ^ id) & d_bit(depth)) {
if (new_meta)
meta_set_alt(new_meta, depth, p);
p = meta_get_alt(meta, depth);
if (p == DHARA_PAGE_NONE) {
depth++;
goto not_found;
}
if (dhara_journal_read_meta(&m->journal, p,
meta, err) < 0)
return -1;
} else {
if (new_meta)
meta_set_alt(new_meta, depth,
meta_get_alt(meta, depth));
}
depth++;
}
if (loc)
*loc = p;
return 0;
not_found:
if (new_meta) {
while (depth < DHARA_RADIX_DEPTH)
meta_set_alt(new_meta, depth++, DHARA_SECTOR_NONE);
}
dhara_set_error(err, DHARA_E_NOT_FOUND);
return -1;
}
int dhara_map_find(struct dhara_map *m, dhara_sector_t target,
dhara_page_t *loc, dhara_error_t *err)
{
return trace_path(m, target, loc, NULL, err);
}
int dhara_map_read(struct dhara_map *m, dhara_sector_t s,
uint8_t *data, dhara_error_t *err)
{
const struct dhara_nand *n = m->journal.nand;
dhara_error_t my_err;
dhara_page_t p;
if (dhara_map_find(m, s, &p, &my_err) < 0) {
if (my_err == DHARA_E_NOT_FOUND) {
memset(data, 0xff, 1 << n->log2_page_size);
return 0;
}
dhara_set_error(err, my_err);
return -1;
}
return dhara_nand_read(n, p, 0, 1 << n->log2_page_size, data, err);
}
/* Check the given page. If it's garbage, do nothing. Otherwise, rewrite
* it at the front of the map. Return raw errors from the journal (do
* not perform recovery).
*/
static int raw_gc(struct dhara_map *m, dhara_page_t src,
dhara_error_t *err)
{
dhara_sector_t target;
dhara_page_t current;
dhara_error_t my_err;
uint8_t meta[DHARA_META_SIZE];
if (dhara_journal_read_meta(&m->journal, src, meta, err) < 0)
return -1;
/* Is the page just filler/garbage? */
target = meta_get_id(meta);
if (target == DHARA_SECTOR_NONE)
return 0;
/* Find out where the sector once represented by this page
* currently resides (if anywhere).
*/
if (trace_path(m, target, &current, meta, &my_err) < 0) {
if (my_err == DHARA_E_NOT_FOUND)
return 0;
dhara_set_error(err, my_err);
return -1;
}
/* Is this page still the most current representative? If not,
* do nothing.
*/
if (current != src)
return 0;
/* Rewrite it at the front of the journal with updated metadata */
ck_set_count(dhara_journal_cookie(&m->journal), m->count);
if (dhara_journal_copy(&m->journal, src, meta, err) < 0)
return -1;
return 0;
}
static int pad_queue(struct dhara_map *m, dhara_error_t *err)
{
dhara_page_t p = dhara_journal_root(&m->journal);
uint8_t root_meta[DHARA_META_SIZE];
ck_set_count(dhara_journal_cookie(&m->journal), m->count);
if (p == DHARA_PAGE_NONE)
return dhara_journal_enqueue(&m->journal, NULL, NULL, err);
if (dhara_journal_read_meta(&m->journal, p, root_meta, err) < 0)
return -1;
return dhara_journal_copy(&m->journal, p, root_meta, err);
}
/* Attempt to recover the journal */
static int try_recover(struct dhara_map *m, dhara_error_t cause,
dhara_error_t *err)
{
int restart_count = 0;
if (cause != DHARA_E_RECOVER) {
dhara_set_error(err, cause);
return -1;
}
while (dhara_journal_in_recovery(&m->journal)) {
dhara_page_t p = dhara_journal_next_recoverable(&m->journal);
dhara_error_t my_err;
int ret;
if (p == DHARA_PAGE_NONE)
ret = pad_queue(m, &my_err);
else
ret = raw_gc(m, p, &my_err);
if (ret < 0) {
if (my_err != DHARA_E_RECOVER) {
dhara_set_error(err, my_err);
return -1;
}
if (restart_count >= DHARA_MAX_RETRIES) {
dhara_set_error(err, DHARA_E_TOO_BAD);
return -1;
}
restart_count++;
}
}
return 0;
}
static int auto_gc(struct dhara_map *m, dhara_error_t *err)
{
int i;
if (dhara_journal_size(&m->journal) < dhara_map_capacity(m))
return 0;
for (i = 0; i <= m->gc_ratio; i++)
if (dhara_map_gc(m, err) < 0)
return -1;
return 0;
}
static int prepare_write(struct dhara_map *m, dhara_sector_t dst,
uint8_t *meta, dhara_error_t *err)
{
dhara_error_t my_err;
if (auto_gc(m, err) < 0)
return -1;
if (trace_path(m, dst, NULL, meta, &my_err) < 0) {
if (my_err != DHARA_E_NOT_FOUND) {
dhara_set_error(err, my_err);
return -1;
}
if (m->count >= dhara_map_capacity(m)) {
dhara_set_error(err, DHARA_E_MAP_FULL);
return -1;
}
m->count++;
}
ck_set_count(dhara_journal_cookie(&m->journal), m->count);
return 0;
}
int dhara_map_write(struct dhara_map *m, dhara_sector_t dst,
const uint8_t *data, dhara_error_t *err)
{
for (;;) {
uint8_t meta[DHARA_META_SIZE];
dhara_error_t my_err;
const dhara_sector_t old_count = m->count;
if (prepare_write(m, dst, meta, err) < 0)
return -1;
if (!dhara_journal_enqueue(&m->journal, data, meta, &my_err))
break;
m->count = old_count;
if (try_recover(m, my_err, err) < 0)
return -1;
}
return 0;
}
int dhara_map_copy_page(struct dhara_map *m, dhara_page_t src,
dhara_sector_t dst, dhara_error_t *err)
{
for (;;) {
uint8_t meta[DHARA_META_SIZE];
dhara_error_t my_err;
const dhara_sector_t old_count = m->count;
if (prepare_write(m, dst, meta, err) < 0)
return -1;
if (!dhara_journal_copy(&m->journal, src, meta, &my_err))
break;
m->count = old_count;
if (try_recover(m, my_err, err) < 0)
return -1;
}
return 0;
}
int dhara_map_copy_sector(struct dhara_map *m, dhara_sector_t src,
dhara_sector_t dst, dhara_error_t *err)
{
dhara_error_t my_err;
dhara_page_t p;
if (dhara_map_find(m, src, &p, &my_err) < 0) {
if (my_err == DHARA_E_NOT_FOUND)
return dhara_map_trim(m, dst, err);
dhara_set_error(err, my_err);
return -1;
}
return dhara_map_copy_page(m, p, dst, err);
}
static int try_delete(struct dhara_map *m, dhara_sector_t s,
dhara_error_t *err)
{
dhara_error_t my_err;
uint8_t meta[DHARA_META_SIZE];
dhara_page_t alt_page;
uint8_t alt_meta[DHARA_META_SIZE];
int level = DHARA_RADIX_DEPTH - 1;
int i;
if (trace_path(m, s, NULL, meta, &my_err) < 0) {
if (my_err == DHARA_E_NOT_FOUND)
return 0;
dhara_set_error(err, my_err);
return -1;
}
/* Select any of the closest cousins of this node which are
* subtrees of at least the requested order.
*/
while (level >= 0) {
alt_page = meta_get_alt(meta, level);
if (alt_page != DHARA_PAGE_NONE)
break;
level--;
}
/* Special case: deletion of last sector */
if (level < 0) {
m->count = 0;
dhara_journal_clear(&m->journal);
return 0;
}
/* Rewrite the cousin with an up-to-date path which doesn't
* point to the original node.
*/
if (dhara_journal_read_meta(&m->journal, alt_page, alt_meta, err) < 0)
return -1;
meta_set_id(meta, meta_get_id(alt_meta));
meta_set_alt(meta, level, DHARA_PAGE_NONE);
for (i = level + 1; i < DHARA_RADIX_DEPTH; i++)
meta_set_alt(meta, i, meta_get_alt(alt_meta, i));
meta_set_alt(meta, level, DHARA_PAGE_NONE);
ck_set_count(dhara_journal_cookie(&m->journal), m->count - 1);
if (dhara_journal_copy(&m->journal, alt_page, meta, err) < 0)
return -1;
m->count--;
return 0;
}
int dhara_map_trim(struct dhara_map *m, dhara_sector_t s, dhara_error_t *err)
{
for (;;) {
dhara_error_t my_err;
if (auto_gc(m, err) < 0)
return -1;
if (!try_delete(m, s, &my_err))
break;
if (try_recover(m, my_err, err) < 0)
return -1;
}
return 0;
}
int dhara_map_sync(struct dhara_map *m, dhara_error_t *err)
{
while (!dhara_journal_is_clean(&m->journal)) {
dhara_page_t p = dhara_journal_peek(&m->journal);
dhara_error_t my_err;
int ret;
if (p == DHARA_PAGE_NONE) {
ret = pad_queue(m, &my_err);
} else {
ret = raw_gc(m, p, &my_err);
if (!ret)
dhara_journal_dequeue(&m->journal);
}
if ((ret < 0) && (try_recover(m, my_err, err) < 0))
return -1;
}
return 0;
}
int dhara_map_gc(struct dhara_map *m, dhara_error_t *err)
{
if (!m->count)
return 0;
for (;;) {
dhara_page_t tail = dhara_journal_peek(&m->journal);
dhara_error_t my_err;
if (tail == DHARA_PAGE_NONE)
break;
if (!raw_gc(m, tail, &my_err)) {
dhara_journal_dequeue(&m->journal);
break;
}
if (try_recover(m, my_err, err) < 0)
return -1;
}
return 0;
}

114
Lib/dhara/map.h Normal file
View File

@@ -0,0 +1,114 @@
/* Dhara - NAND flash management layer
* Copyright (C) 2013 Daniel Beer <dlbeer@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef DHARA_MAP_H_
#define DHARA_MAP_H_
#include "journal.h"
/* The map is a journal indexing format. It maps virtual sectors to
* pages of data in flash memory.
*/
typedef uint32_t dhara_sector_t;
/* This sector value is reserved */
#define DHARA_SECTOR_NONE 0xffffffff
struct dhara_map {
struct dhara_journal journal;
uint8_t gc_ratio;
dhara_sector_t count;
};
/* Initialize a map. You need to supply a buffer for page metadata, and
* a garbage collection ratio. This is the ratio of garbage collection
* operations to real writes when automatic collection is active.
*
* Smaller values lead to faster and more predictable IO, at the
* expense of capacity. You should always initialize the same chip with
* the same garbage collection ratio.
*/
void dhara_map_init(struct dhara_map *m, const struct dhara_nand *n,
uint8_t *page_buf, uint8_t gc_ratio);
/* Recover stored state, if possible. If there is no valid stored state
* on the chip, -1 is returned, and an empty map is initialized.
*/
int dhara_map_resume(struct dhara_map *m, dhara_error_t *err);
/* Clear the map (delete all sectors). */
void dhara_map_clear(struct dhara_map *m);
/* Obtain the maximum capacity of the map. */
dhara_sector_t dhara_map_capacity(const struct dhara_map *m);
/* Obtain the current number of allocated sectors. */
static inline dhara_sector_t dhara_map_size(const struct dhara_map *m)
{
return m->count;
}
/* Find the physical page which holds the current data for this sector.
* Returns 0 on success or -1 if an error occurs. If the sector doesn't
* exist, the error is E_NOT_FOUND.
*/
int dhara_map_find(struct dhara_map *m, dhara_sector_t s,
dhara_page_t *loc, dhara_error_t *err);
/* Read from the given logical sector. If the sector is unmapped, a
* blank page (0xff) will be returned.
*/
int dhara_map_read(struct dhara_map *m, dhara_sector_t s,
uint8_t *data, dhara_error_t *err);
/* Write data to a logical sector. */
int dhara_map_write(struct dhara_map *m, dhara_sector_t s,
const uint8_t *data, dhara_error_t *err);
/* Copy any flash page to a logical sector. */
int dhara_map_copy_page(struct dhara_map *m, dhara_page_t src,
dhara_sector_t dst, dhara_error_t *err);
/* Copy one sector to another. If the source sector is unmapped, the
* destination sector will be trimmed.
*/
int dhara_map_copy_sector(struct dhara_map *m, dhara_sector_t src,
dhara_sector_t dst, dhara_error_t *err);
/* Delete a logical sector. You don't necessarily need to do this, but
* it's a useful hint if you no longer require the sector's data to be
* kept.
*
* If order is non-zero, it specifies that all sectors in the
* (2**order)-aligned group of s are to be deleted.
*/
int dhara_map_trim(struct dhara_map *m, dhara_sector_t s,
dhara_error_t *err);
/* Synchronize the map. Once this returns successfully, all changes to
* date are persistent and durable. Conversely, there is no guarantee
* that unsynchronized changes will be persistent.
*/
int dhara_map_sync(struct dhara_map *m, dhara_error_t *err);
/* Perform one garbage collection step. You can do this whenever you
* like, but it's not necessary -- garbage collection happens
* automatically and is interleaved with other operations.
*/
int dhara_map_gc(struct dhara_map *m, dhara_error_t *err);
#endif

105
Lib/dhara/nand.h Normal file
View File

@@ -0,0 +1,105 @@
/* Dhara - NAND flash management layer
* Copyright (C) 2013 Daniel Beer <dlbeer@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef DHARA_NAND_H_
#define DHARA_NAND_H_
#include <stdint.h>
#include <stddef.h>
#include "error.h"
/* Each page in a NAND device is indexed, starting at 0. It's required
* that there be a power-of-two number of pages in a eraseblock, so you can
* view a page number is being a concatenation (in binary) of a block
* number and the number of a page within a block.
*/
typedef uint32_t dhara_page_t;
/* Blocks are also indexed, starting at 0. */
typedef uint32_t dhara_block_t;
/* Each NAND chip must be represented by one of these structures. It's
* intended that this structure be embedded in a larger structure for
* context.
*
* The functions declared below are not implemented -- they must be
* provided and satisfy the documented conditions.
*/
struct dhara_nand {
/* Base-2 logarithm of the page size. If your device supports
* partial programming, you may want to subdivide the actual
* pages into separate ECC-correctable regions and present those
* as pages.
*/
uint8_t log2_page_size;
/* Base-2 logarithm of the number of pages within an eraseblock */
uint8_t log2_ppb;
/* Total number of eraseblocks */
unsigned int num_blocks;
};
/* Is the given block bad? */
int dhara_nand_is_bad(const struct dhara_nand *n, dhara_block_t b);
/* Mark bad the given block (or attempt to). No return value is
* required, because there's nothing that can be done in response.
*/
void dhara_nand_mark_bad(const struct dhara_nand *n, dhara_block_t b);
/* Erase the given block. This function should return 0 on success or -1
* on failure.
*
* The status reported by the chip should be checked. If an erase
* operation fails, return -1 and set err to E_BAD_BLOCK.
*/
int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b,
dhara_error_t *err);
/* Program the given page. The data pointer is a pointer to an entire
* page ((1 << log2_page_size) bytes). The operation status should be
* checked. If the operation fails, return -1 and set err to
* E_BAD_BLOCK.
*
* Pages will be programmed sequentially within a block, and will not be
* reprogrammed.
*/
int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p,
const uint8_t *data,
dhara_error_t *err);
/* Check that the given page is erased */
int dhara_nand_is_free(const struct dhara_nand *n, dhara_page_t p);
/* Read a portion of a page. ECC must be handled by the NAND
* implementation. Returns 0 on sucess or -1 if an error occurs. If an
* uncorrectable ECC error occurs, return -1 and set err to E_ECC.
*/
int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p,
size_t offset, size_t length,
uint8_t *data,
dhara_error_t *err);
/* Read a page from one location and reprogram it in another location.
* This might be done using the chip's internal buffers, but it must use
* ECC.
*/
int dhara_nand_copy(const struct dhara_nand *n,
dhara_page_t src, dhara_page_t dst,
dhara_error_t *err);
#endif

View File

@@ -338,9 +338,9 @@
<v6Rtti>0</v6Rtti> <v6Rtti>0</v6Rtti>
<VariousControls> <VariousControls>
<MiscControls></MiscControls> <MiscControls></MiscControls>
<Define>USE_HAL_DRIVER,STM32F407xx</Define> <Define>USE_HAL_DRIVER,STM32F407xx,NDEBUG</Define>
<Undefine></Undefine> <Undefine></Undefine>
<IncludePath>../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Lib/FlashDB/port/fal/inc;../Lib/FlashDB/inc;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F</IncludePath> <IncludePath>../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../App;../App/task;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test</IncludePath>
</VariousControls> </VariousControls>
</Cads> </Cads>
<Aads> <Aads>
@@ -752,6 +752,51 @@
</File> </File>
</Files> </Files>
</Group> </Group>
<Group>
<GroupName>App</GroupName>
<Files>
<File>
<FileName>app_main.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\app_main.c</FilePath>
</File>
<File>
<FileName>sys_clock.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\sys_clock.c</FilePath>
</File>
<File>
<FileName>time_sync.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\time_sync.c</FilePath>
</File>
<File>
<FileName>net_task.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\task\net_task.c</FilePath>
</File>
<File>
<FileName>adc_task.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\task\adc_task.c</FilePath>
</File>
<File>
<FileName>rs485_task.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\task\rs485_task.c</FilePath>
</File>
<File>
<FileName>ringbuf.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\util\ringbuf.c</FilePath>
</File>
<File>
<FileName>crc.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\util\crc.c</FilePath>
</File>
</Files>
</Group>
<Group> <Group>
<GroupName>Drivers/STM32F4xx_HAL_Driver</GroupName> <GroupName>Drivers/STM32F4xx_HAL_Driver</GroupName>
<Files> <Files>
@@ -1176,16 +1221,6 @@
<FileType>1</FileType> <FileType>1</FileType>
<FilePath>..\Drivers\BSP\CH395F\ch395f.c</FilePath> <FilePath>..\Drivers\BSP\CH395F\ch395f.c</FilePath>
</File> </File>
<File>
<FileName>ch395f_test.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\CH395F\ch395f_test.c</FilePath>
</File>
<File>
<FileName>fal_flash_gd5f2gq5ue.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\GD5F2GQ5UE\fal_flash_gd5f2gq5ue.c</FilePath>
</File>
<File> <File>
<FileName>gd5f2gq5ue.c</FileName> <FileName>gd5f2gq5ue.c</FileName>
<FileType>1</FileType> <FileType>1</FileType>
@@ -1216,45 +1251,30 @@
<FileType>1</FileType> <FileType>1</FileType>
<FilePath>..\Drivers\BSP\NET\net_socket.c</FilePath> <FilePath>..\Drivers\BSP\NET\net_socket.c</FilePath>
</File> </File>
</Files>
</Group>
<Group>
<GroupName>lib/FlashDB</GroupName>
<Files>
<File> <File>
<FileName>fdb.c</FileName> <FileName>nand_ftl.c</FileName>
<FileType>1</FileType> <FileType>1</FileType>
<FilePath>..\Lib\FlashDB\src\fdb.c</FilePath> <FilePath>..\Drivers\BSP\GD5F2GQ5UE\nand_ftl.c</FilePath>
</File> </File>
<File> <File>
<FileName>fdb_kvdb.c</FileName> <FileName>lftpd.c</FileName>
<FileType>1</FileType> <FileType>1</FileType>
<FilePath>..\Lib\FlashDB\src\fdb_kvdb.c</FilePath> <FilePath>..\Drivers\BSP\NET\lftpd\lftpd.c</FilePath>
</File> </File>
<File> <File>
<FileName>fdb_tsdb.c</FileName> <FileName>lftpd_inet.c</FileName>
<FileType>1</FileType> <FileType>1</FileType>
<FilePath>..\Lib\FlashDB\src\fdb_tsdb.c</FilePath> <FilePath>..\Drivers\BSP\NET\lftpd\lftpd_inet.c</FilePath>
</File> </File>
<File> <File>
<FileName>fdb_utils.c</FileName> <FileName>lftpd_io.c</FileName>
<FileType>1</FileType> <FileType>1</FileType>
<FilePath>..\Lib\FlashDB\src\fdb_utils.c</FilePath> <FilePath>..\Drivers\BSP\NET\lftpd\lftpd_io.c</FilePath>
</File> </File>
<File> <File>
<FileName>fal.c</FileName> <FileName>lftpd_string.c</FileName>
<FileType>1</FileType> <FileType>1</FileType>
<FilePath>..\Lib\FlashDB\port\fal\src\fal.c</FilePath> <FilePath>..\Drivers\BSP\NET\lftpd\lftpd_string.c</FilePath>
</File>
<File>
<FileName>fal_flash.c</FileName>
<FileType>1</FileType>
<FilePath>..\Lib\FlashDB\port\fal\src\fal_flash.c</FilePath>
</File>
<File>
<FileName>fal_partition.c</FileName>
<FileType>1</FileType>
<FilePath>..\Lib\FlashDB\port\fal\src\fal_partition.c</FilePath>
</File> </File>
</Files> </Files>
</Group> </Group>
@@ -1892,6 +1912,130 @@
</File> </File>
</Files> </Files>
</Group> </Group>
<Group>
<GroupName>lib/dhara</GroupName>
<GroupOption>
<CommonProperty>
<UseCPPCompiler>0</UseCPPCompiler>
<RVCTCodeConst>0</RVCTCodeConst>
<RVCTZI>0</RVCTZI>
<RVCTOtherData>0</RVCTOtherData>
<ModuleSelection>0</ModuleSelection>
<IncludeInBuild>2</IncludeInBuild>
<AlwaysBuild>2</AlwaysBuild>
<GenerateAssemblyFile>2</GenerateAssemblyFile>
<AssembleAssemblyFile>2</AssembleAssemblyFile>
<PublicsOnly>2</PublicsOnly>
<StopOnExitCode>11</StopOnExitCode>
<CustomArgument></CustomArgument>
<IncludeLibraryModules></IncludeLibraryModules>
<ComprImg>1</ComprImg>
</CommonProperty>
<GroupArmAds>
<Cads>
<interw>2</interw>
<Optim>0</Optim>
<oTime>2</oTime>
<SplitLS>2</SplitLS>
<OneElfS>2</OneElfS>
<Strict>2</Strict>
<EnumInt>2</EnumInt>
<PlainCh>2</PlainCh>
<Ropi>2</Ropi>
<Rwpi>2</Rwpi>
<wLevel>4</wLevel>
<uThumb>2</uThumb>
<uSurpInc>2</uSurpInc>
<uC99>2</uC99>
<uGnu>2</uGnu>
<useXO>2</useXO>
<v6Lang>0</v6Lang>
<v6LangP>0</v6LangP>
<vShortEn>2</vShortEn>
<vShortWch>2</vShortWch>
<v6Lto>2</v6Lto>
<v6WtE>2</v6WtE>
<v6Rtti>2</v6Rtti>
<VariousControls>
<MiscControls>--diag_suppress=177</MiscControls>
<Define></Define>
<Undefine></Undefine>
<IncludePath></IncludePath>
</VariousControls>
</Cads>
<Aads>
<interw>2</interw>
<Ropi>2</Ropi>
<Rwpi>2</Rwpi>
<thumb>2</thumb>
<SplitLS>2</SplitLS>
<SwStkChk>2</SwStkChk>
<NoWarn>2</NoWarn>
<uSurpInc>2</uSurpInc>
<useXO>2</useXO>
<ClangAsOpt>1</ClangAsOpt>
<VariousControls>
<MiscControls></MiscControls>
<Define></Define>
<Undefine></Undefine>
<IncludePath></IncludePath>
</VariousControls>
</Aads>
</GroupArmAds>
</GroupOption>
<Files>
<File>
<FileName>journal.c</FileName>
<FileType>1</FileType>
<FilePath>..\Lib\dhara\journal.c</FilePath>
</File>
<File>
<FileName>map.c</FileName>
<FileType>1</FileType>
<FilePath>..\Lib\dhara\map.c</FilePath>
</File>
</Files>
</Group>
<Group>
<GroupName>lib/FatFs</GroupName>
<Files>
<File>
<FileName>ff.c</FileName>
<FileType>1</FileType>
<FilePath>..\Lib\FatFs\ff.c</FilePath>
</File>
</Files>
</Group>
<Group>
<GroupName>Test</GroupName>
<Files>
<File>
<FileName>ch395f_test_task.c</FileName>
<FileType>1</FileType>
<FilePath>..\test\ch395f_test_task.c</FilePath>
</File>
<File>
<FileName>net_test_task.c</FileName>
<FileType>1</FileType>
<FilePath>..\test\net_test_task.c</FilePath>
</File>
<File>
<FileName>gd5f_test_task.c</FileName>
<FileType>1</FileType>
<FilePath>..\test\gd5f_test_task.c</FilePath>
</File>
<File>
<FileName>storage_test_task.c</FileName>
<FileType>1</FileType>
<FilePath>..\test\storage_test_task.c</FilePath>
</File>
<File>
<FileName>sd2506_test_task.c</FileName>
<FileType>1</FileType>
<FilePath>..\test\sd2506_test_task.c</FilePath>
</File>
</Files>
</Group>
<Group> <Group>
<GroupName>::CMSIS</GroupName> <GroupName>::CMSIS</GroupName>
</Group> </Group>

1
MDK-ARM/_build.cmd Normal file
View File

@@ -0,0 +1 @@
@echo off && cd /d "%~dp0" && C:\Keil_v5\UV4\UV4.exe -r "STM32F407-Demo.uvprojx" -t STM32F407-Demo -j0

View File

@@ -1,4 +1,76 @@
*** Using Compiler 'V5.06 update 7 (build 960)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin' *** Using Compiler 'V5.06 update 7 (build 960)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
Build target 'STM32F407-Demo' Rebuild target 'STM32F407-Demo'
assembling startup_stm32f407xx.s...
compiling crc.c...
compiling ringbuf.c...
compiling stm32f4xx_it.c...
compiling app_main.c...
compiling dma.c...
compiling stm32f4xx_hal_flash_ramfunc.c...
compiling i2c.c...
compiling sys_clock.c...
compiling stm32f4xx_hal_timebase_tim.c...
compiling stm32f4xx_hal_gpio.c...
compiling stm32f4xx_hal_dma_ex.c...
compiling stm32f4xx_hal_dma.c...
compiling stm32f4xx_hal_flash_ex.c...
compiling stm32f4xx_hal_pwr.c...
compiling spi.c...
compiling stm32f4xx_hal_msp.c...
compiling stm32f4xx_hal_rcc.c...
compiling stm32f4xx_hal_flash.c...
compiling gpio.c...
compiling usart.c...
compiling stm32f4xx_hal_tim_ex.c...
compiling net_task.c...
compiling freertos.c...
compiling stm32f4xx_hal_rcc_ex.c...
compiling rs485_task.c...
compiling main.c...
compiling adc_task.c...
compiling time_sync.c...
compiling lftpd_string.c...
compiling croutine.c...
compiling stm32f4xx_hal_tim.c...
compiling list.c...
compiling event_groups.c...
compiling queue.c...
compiling stream_buffer.c...
compiling timers.c...
compiling tasks.c...
compiling heap_4.c...
compiling port.c...
compiling stm32f4xx_hal_pwr_ex.c...
compiling journal.c...
compiling map.c...
compiling ff.c...
compiling stm32f4xx_hal.c...
compiling system_stm32f4xx.c...
compiling lftpd_io.c...
compiling stm32f4xx_hal_cortex.c...
compiling stm32f4xx_hal_i2c_ex.c...
compiling stm32f4xx_hal_exti.c...
compiling rs485.c...
compiling tpafe5160.c...
compiling sd2506.c...
compiling ch395f.c...
compiling nand_ftl.c...
compiling stm32f4xx_hal_spi.c...
compiling lftpd_inet.c...
compiling gd5f2gq5ue.c...
compiling net_select.c...
compiling stm32f4xx_hal_uart.c...
compiling lftpd.c...
compiling net_socket.c...
compiling stm32f4xx_hal_i2c.c...
compiling cmsis_os2.c...
compiling ch395f_test_task.c...
compiling net_test_task.c...
compiling gd5f_test_task.c...
compiling storage_test_task.c...
compiling sd2506_test_task.c...
linking...
Program Size: Code=64436 RO-data=3936 RW-data=420 ZI-data=51868
FromELF: creating hex file...
".\STM32F407-Demo\STM32F407-Demo.axf" - 0 Error(s), 0 Warning(s). ".\STM32F407-Demo\STM32F407-Demo.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:00 Build Time Elapsed: 00:00:14

0
MDK-ARM/err.txt Normal file
View File

0
MDK-ARM/out.txt Normal file
View File

View File

@@ -6,7 +6,29 @@ Dma.Request0=USART1_TX
Dma.Request1=USART1_RX Dma.Request1=USART1_RX
Dma.Request2=SPI2_RX Dma.Request2=SPI2_RX
Dma.Request3=SPI2_TX Dma.Request3=SPI2_TX
Dma.RequestsNb=4 Dma.Request4=SPI1_RX
Dma.Request5=SPI1_TX
Dma.RequestsNb=6
Dma.SPI1_RX.4.Direction=DMA_PERIPH_TO_MEMORY
Dma.SPI1_RX.4.FIFOMode=DMA_FIFOMODE_DISABLE
Dma.SPI1_RX.4.Instance=DMA2_Stream0
Dma.SPI1_RX.4.MemDataAlignment=DMA_MDATAALIGN_BYTE
Dma.SPI1_RX.4.MemInc=DMA_MINC_ENABLE
Dma.SPI1_RX.4.Mode=DMA_NORMAL
Dma.SPI1_RX.4.PeriphDataAlignment=DMA_PDATAALIGN_BYTE
Dma.SPI1_RX.4.PeriphInc=DMA_PINC_DISABLE
Dma.SPI1_RX.4.Priority=DMA_PRIORITY_LOW
Dma.SPI1_RX.4.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode
Dma.SPI1_TX.5.Direction=DMA_MEMORY_TO_PERIPH
Dma.SPI1_TX.5.FIFOMode=DMA_FIFOMODE_DISABLE
Dma.SPI1_TX.5.Instance=DMA2_Stream3
Dma.SPI1_TX.5.MemDataAlignment=DMA_MDATAALIGN_BYTE
Dma.SPI1_TX.5.MemInc=DMA_MINC_ENABLE
Dma.SPI1_TX.5.Mode=DMA_NORMAL
Dma.SPI1_TX.5.PeriphDataAlignment=DMA_PDATAALIGN_BYTE
Dma.SPI1_TX.5.PeriphInc=DMA_PINC_DISABLE
Dma.SPI1_TX.5.Priority=DMA_PRIORITY_LOW
Dma.SPI1_TX.5.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode
Dma.SPI2_RX.2.Direction=DMA_PERIPH_TO_MEMORY Dma.SPI2_RX.2.Direction=DMA_PERIPH_TO_MEMORY
Dma.SPI2_RX.2.FIFOMode=DMA_FIFOMODE_DISABLE Dma.SPI2_RX.2.FIFOMode=DMA_FIFOMODE_DISABLE
Dma.SPI2_RX.2.Instance=DMA1_Stream3 Dma.SPI2_RX.2.Instance=DMA1_Stream3
@@ -48,8 +70,9 @@ Dma.USART1_TX.0.PeriphInc=DMA_PINC_DISABLE
Dma.USART1_TX.0.Priority=DMA_PRIORITY_LOW Dma.USART1_TX.0.Priority=DMA_PRIORITY_LOW
Dma.USART1_TX.0.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode Dma.USART1_TX.0.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode
FREERTOS.FootprintOK=true FREERTOS.FootprintOK=true
FREERTOS.IPParameters=Tasks01,configENABLE_FPU,FootprintOK,configTOTAL_HEAP_SIZE FREERTOS.IPParameters=Tasks01,configENABLE_FPU,FootprintOK,configTOTAL_HEAP_SIZE,Queues01
FREERTOS.Tasks01=defaultTask,24,512,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;netTask,24,1024,StartNetTask,Default,NULL,Dynamic,NULL,NULL FREERTOS.Queues01=netMsgQueue,8,net_msg_t,0,Dynamic,NULL,NULL
FREERTOS.Tasks01=defaultTask,24,512,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;netTask,24,1024,StartNetTask,Default,NULL,Dynamic,NULL,NULL;ftpTask,24,1024,StartFtpTask,As weak,NULL,Dynamic,NULL,NULL;ch395fTestTask,16,4096,StartCh395fTestTask,As weak,NULL,Dynamic,NULL,NULL
FREERTOS.configENABLE_FPU=1 FREERTOS.configENABLE_FPU=1
FREERTOS.configTOTAL_HEAP_SIZE=30720 FREERTOS.configTOTAL_HEAP_SIZE=30720
File.Version=6 File.Version=6
@@ -141,7 +164,9 @@ MxDb.Version=DB.6.0.170
NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false
NVIC.DMA1_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true NVIC.DMA1_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
NVIC.DMA1_Stream4_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true NVIC.DMA1_Stream4_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
NVIC.DMA2_Stream0_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
NVIC.DMA2_Stream2_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true NVIC.DMA2_Stream2_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true
NVIC.DMA2_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
NVIC.DMA2_Stream7_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true NVIC.DMA2_Stream7_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true
NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false
NVIC.EXTI9_5_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true NVIC.EXTI9_5_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true
@@ -151,6 +176,7 @@ NVIC.MemoryManagement_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:
NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false
NVIC.PendSV_IRQn=true\:15\:0\:false\:false\:false\:true\:false\:false\:false NVIC.PendSV_IRQn=true\:15\:0\:false\:false\:false\:true\:false\:false\:false
NVIC.PriorityGroup=NVIC_PRIORITYGROUP_4 NVIC.PriorityGroup=NVIC_PRIORITYGROUP_4
NVIC.SPI1_IRQn=true\:7\:0\:true\:false\:true\:true\:true\:true\:true
NVIC.SPI2_IRQn=true\:7\:0\:true\:false\:true\:true\:true\:true\:true NVIC.SPI2_IRQn=true\:7\:0\:true\:false\:true\:true\:true\:true\:true
NVIC.SVCall_IRQn=true\:0\:0\:false\:false\:false\:false\:false\:false\:false NVIC.SVCall_IRQn=true\:0\:0\:false\:false\:false\:false\:false\:false\:false
NVIC.SavedPendsvIrqHandlerGenerated=true NVIC.SavedPendsvIrqHandlerGenerated=true
@@ -301,9 +327,10 @@ PCC.PartNumber=STM32F407ZGTx
PCC.Series=STM32F4 PCC.Series=STM32F4
PCC.Temperature=25 PCC.Temperature=25
PCC.Vdd=3.3 PCC.Vdd=3.3
PD0.GPIOParameters=GPIO_PuPd,GPIO_Label PD0.GPIOParameters=GPIO_Speed,GPIO_PuPd,GPIO_Label
PD0.GPIO_Label=ST_DIR4 PD0.GPIO_Label=ST_DIR4
PD0.GPIO_PuPd=GPIO_NOPULL PD0.GPIO_PuPd=GPIO_NOPULL
PD0.GPIO_Speed=GPIO_SPEED_FREQ_VERY_HIGH
PD0.Locked=true PD0.Locked=true
PD0.Signal=GPIO_Output PD0.Signal=GPIO_Output
PD1.GPIOParameters=GPIO_Label PD1.GPIOParameters=GPIO_Label
@@ -524,8 +551,8 @@ RCC.VCOOutputFreq_Value=336000000
RCC.VcooutputI2S=192000000 RCC.VcooutputI2S=192000000
SH.GPXTI7.0=GPIO_EXTI7 SH.GPXTI7.0=GPIO_EXTI7
SH.GPXTI7.ConfNb=1 SH.GPXTI7.ConfNb=1
SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_8 SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_2
SPI1.CalculateBaudRate=10.5 MBits/s SPI1.CalculateBaudRate=42.0 MBits/s
SPI1.Direction=SPI_DIRECTION_2LINES SPI1.Direction=SPI_DIRECTION_2LINES
SPI1.IPParameters=VirtualType,Mode,Direction,CalculateBaudRate,BaudRatePrescaler SPI1.IPParameters=VirtualType,Mode,Direction,CalculateBaudRate,BaudRatePrescaler
SPI1.Mode=SPI_MODE_MASTER SPI1.Mode=SPI_MODE_MASTER
@@ -551,3 +578,4 @@ VP_FREERTOS_VS_CMSIS_V2.Signal=FREERTOS_VS_CMSIS_V2
VP_SYS_VS_tim7.Mode=TIM7 VP_SYS_VS_tim7.Mode=TIM7
VP_SYS_VS_tim7.Signal=SYS_VS_tim7 VP_SYS_VS_tim7.Signal=SYS_VS_tim7
board=custom board=custom
rtos.0.ip=FREERTOS

View File

@@ -50,9 +50,15 @@ void MX_DMA_Init(void)
/* DMA1_Stream4_IRQn interrupt configuration */ /* DMA1_Stream4_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 7, 0); HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 7, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn); HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn);
/* DMA2_Stream0_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 7, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
/* DMA2_Stream2_IRQn interrupt configuration */ /* DMA2_Stream2_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 9, 0); HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 9, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn); HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
/* DMA2_Stream3_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 7, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
/* DMA2_Stream7_IRQn interrupt configuration */ /* DMA2_Stream7_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 9, 0); HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 9, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn); HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);

View File

@@ -25,11 +25,18 @@
/* Private includes ----------------------------------------------------------*/ /* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */ /* USER CODE BEGIN Includes */
#include "net_socket.h" #include <string.h>
#include "net_select.h" #include "net_task.h"
#include "ch395f.h" #include "ch395f.h"
#include "ch395f_test.h"
#include "dbg_log.h" #include "dbg_log.h"
#include "net_socket.h"
#include "usart.h"
#include "ff.h"
#include "gd5f2gq5ue.h"
#include "lftpd.h"
#include "../test/ch395f_test_task.h"
#include "../test/ch395f_test.h" /* ENABLE_NET_LAYER_TESTSnetTask 条件创建 */
#include "time_sync.h"
/* USER CODE END Includes */ /* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/
@@ -39,7 +46,6 @@
/* Private define ------------------------------------------------------------*/ /* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */ /* USER CODE BEGIN PD */
/* USER CODE END PD */ /* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/ /* Private macro -------------------------------------------------------------*/
@@ -49,9 +55,13 @@
/* Private variables ---------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */ /* USER CODE BEGIN Variables */
#ifdef ENABLE_PHASE4_TESTS /* 存储测试变量g_storage_stats / s_perf_buf已迁至 test/storage_test_task.c */
static uint8_t s_net_buf[1500]; osThreadId_t timeSyncTaskHandle;
#endif const osThreadAttr_t timeSyncTask_attributes = {
.name = "timeSyncTask",
.stack_size = 512 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* USER CODE END Variables */ /* USER CODE END Variables */
/* Definitions for defaultTask */ /* Definitions for defaultTask */
osThreadId_t defaultTaskHandle; osThreadId_t defaultTaskHandle;
@@ -67,6 +77,25 @@ const osThreadAttr_t netTask_attributes = {
.stack_size = 1024 * 4, .stack_size = 1024 * 4,
.priority = (osPriority_t) osPriorityNormal, .priority = (osPriority_t) osPriorityNormal,
}; };
/* Definitions for ftpTask */
osThreadId_t ftpTaskHandle;
const osThreadAttr_t ftpTask_attributes = {
.name = "ftpTask",
.stack_size = 1024 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for ch395fTestTask */
osThreadId_t ch395fTestTaskHandle;
const osThreadAttr_t ch395fTestTask_attributes = {
.name = "ch395fTestTask",
.stack_size = 4096 * 4,
.priority = (osPriority_t) osPriorityBelowNormal,
};
/* Definitions for netMsgQueue */
osMessageQueueId_t netMsgQueueHandle;
const osMessageQueueAttr_t netMsgQueue_attributes = {
.name = "netMsgQueue"
};
/* Private function prototypes -----------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */ /* USER CODE BEGIN FunctionPrototypes */
@@ -75,6 +104,8 @@ const osThreadAttr_t netTask_attributes = {
void StartDefaultTask(void *argument); void StartDefaultTask(void *argument);
void StartNetTask(void *argument); void StartNetTask(void *argument);
void StartFtpTask(void *argument);
void StartCh395fTestTask(void *argument);
void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */ void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
@@ -100,6 +131,10 @@ void MX_FREERTOS_Init(void) {
/* start timers, add new ones, ... */ /* start timers, add new ones, ... */
/* USER CODE END RTOS_TIMERS */ /* USER CODE END RTOS_TIMERS */
/* Create the queue(s) */
/* creation of netMsgQueue */
netMsgQueueHandle = osMessageQueueNew (8, sizeof(net_msg_t), &netMsgQueue_attributes);
/* USER CODE BEGIN RTOS_QUEUES */ /* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */ /* add queues, ... */
/* USER CODE END RTOS_QUEUES */ /* USER CODE END RTOS_QUEUES */
@@ -111,8 +146,22 @@ void MX_FREERTOS_Init(void) {
/* creation of netTask */ /* creation of netTask */
netTaskHandle = osThreadNew(StartNetTask, NULL, &netTask_attributes); netTaskHandle = osThreadNew(StartNetTask, NULL, &netTask_attributes);
/* creation of ftpTask */
ftpTaskHandle = osThreadNew(StartFtpTask, NULL, &ftpTask_attributes);
/* creation of ch395fTestTask */
ch395fTestTaskHandle = osThreadNew(StartCh395fTestTask, NULL, &ch395fTestTask_attributes);
/* USER CODE BEGIN RTOS_THREADS */ /* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */ #ifdef ENABLE_NET_LAYER_TESTS
/* NET 层测试模式(阶段 6~10完整网络栈netTask 负责轮询与消息处理 */
netTaskHandle = osThreadNew(StartNetTask, NULL, &netTask_attributes);
#endif
/* 本地时间同步任务PC 推送 TCP依赖 netTask 处理消息队列 */
timeSyncTaskHandle = osThreadNew(timeSyncTask, NULL, &timeSyncTask_attributes);
/* USER CODE END RTOS_THREADS */ /* USER CODE END RTOS_THREADS */
/* USER CODE BEGIN RTOS_EVENTS */ /* USER CODE BEGIN RTOS_EVENTS */
@@ -131,59 +180,85 @@ void MX_FREERTOS_Init(void) {
void StartDefaultTask(void *argument) void StartDefaultTask(void *argument)
{ {
/* USER CODE BEGIN StartDefaultTask */ /* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */ (void)argument;
for(;;)
{ /* 系统启动期挂载 NAND 上的 FatFS 卷,供 FTP 及其它模块访问。
osDelay(1); * f_mount(forced=1) 会触发 disk_initialize() → nand_ftl_init()
* dhara_map_resume。此前默认构建从未挂载导致 FTP 的
* LIST/STOR/RETR 全部返回 550f_opendir 失败)。
* 若卷为空FR_NO_FILESYSTEM如恢复/坏块重建后清空),则首次自动
* 格式化建卷MKFS_PARM 与存储测试一致FM_FAT32 / 无分区表)。 */
static FATFS default_fatfs;
FRESULT fr = f_mount(&default_fatfs, "", 1);
if (fr == FR_NO_FILESYSTEM) {
DBG_INFO("[FS] no FAT volume, formatting NAND...");
static uint8_t fs_work[FF_MAX_SS];
MKFS_PARM opts = { FM_FAT32, 0, 0, 0, 0 };
fr = f_mkfs("", &opts, fs_work, sizeof(fs_work));
if (fr == FR_OK) {
fr = f_mount(&default_fatfs, "", 1);
} else {
DBG_ERROR("[FS] f_mkfs failed (fr=%d)", (int)fr);
}
}
if (fr == FR_OK) {
DBG_INFO("[FS] FATFS mounted");
} else {
DBG_ERROR("[FS] f_mount failed (fr=%d)", (int)fr);
}
for (;;) {
osDelay(1000);
} }
/* USER CODE END StartDefaultTask */ /* USER CODE END StartDefaultTask */
} }
/* USER CODE BEGIN Header_StartNetTask */ /* USER CODE BEGIN Header_StartNetTask */
/** /**
* @brief Function implementing the netTask thread. * @brief Function implementing the netTask thread.
* @param argument: Not used * @param argument: Not used
* @retval None * @retval None
*/ */
/* USER CODE END Header_StartNetTask */ /* USER CODE END Header_StartNetTask */
void StartNetTask(void *argument) void StartNetTask(void *argument)
{ {
/* USER CODE BEGIN StartNetTask */ /* USER CODE BEGIN StartNetTask */
osDelay(2000); net_task_func(argument);
/* USER CODE END StartNetTask */
}
DBG_INFO("netTask: started"); /* USER CODE BEGIN Header_StartFtpTask */
/**
* @brief Function implementing the ftpTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartFtpTask */
__weak void StartFtpTask(void *argument)
{
/* USER CODE BEGIN StartFtpTask */
osDelay(7000);
lftpd_t ftp;
lftpd_start("/", 21, &ftp);
/* USER CODE END StartFtpTask */
}
/* USER CODE BEGIN Header_StartCh395fTestTask */
/**
* @brief Function implementing the ch395fTestTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartCh395fTestTask */
__weak void StartCh395fTestTask(void *argument)
{
/* USER CODE BEGIN StartCh395fTestTask */
/* Infinite loop */
for(;;) for(;;)
{ {
net_poll(); osDelay(1);
#ifdef ENABLE_PHASE4_TESTS
for (int i = 1; i < 8; i++)
{
net_sock_t *sk = net_get_sock(i);
if (sk == NULL || !sk->in_use || sk->state != NET_SOCK_STATE_ESTABLISHED)
{
continue;
}
int n = net_recv(i, s_net_buf, sizeof(s_net_buf), NET_MSG_DONTWAIT);
if (n > 0)
{
net_send(i, s_net_buf, n, 0);
}
}
#endif
#ifdef ENABLE_PHASE5_TESTS
ch395f_phase5_tests();
#endif
#ifdef ENABLE_PHASE7_TESTS
ch395f_phase7_tests();
#endif
osDelay(10);
} }
/* USER CODE END StartNetTask */ /* USER CODE END StartCh395fTestTask */
} }
/* Private application code --------------------------------------------------*/ /* Private application code --------------------------------------------------*/

View File

@@ -128,12 +128,12 @@ void MX_GPIO_Init(void)
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(CH395_SCS_GPIO_Port, &GPIO_InitStruct); HAL_GPIO_Init(CH395_SCS_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pin : ST_DIR4_Pin */ /*Configure GPIO pins : ST_DIR4_Pin TP_RD_Pin TP_CONVST_Pin */
GPIO_InitStruct.Pin = ST_DIR4_Pin; GPIO_InitStruct.Pin = ST_DIR4_Pin|TP_RD_Pin|TP_CONVST_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(ST_DIR4_GPIO_Port, &GPIO_InitStruct); HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
/*Configure GPIO pin : TP_FRSTDATA_Pin */ /*Configure GPIO pin : TP_FRSTDATA_Pin */
GPIO_InitStruct.Pin = TP_FRSTDATA_Pin; GPIO_InitStruct.Pin = TP_FRSTDATA_Pin;
@@ -141,13 +141,6 @@ void MX_GPIO_Init(void)
GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(TP_FRSTDATA_GPIO_Port, &GPIO_InitStruct); HAL_GPIO_Init(TP_FRSTDATA_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pins : TP_RD_Pin TP_CONVST_Pin */
GPIO_InitStruct.Pin = TP_RD_Pin|TP_CONVST_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
/*Configure GPIO pin : TP_BUSY_Pin */ /*Configure GPIO pin : TP_BUSY_Pin */
GPIO_InitStruct.Pin = TP_BUSY_Pin; GPIO_InitStruct.Pin = TP_BUSY_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;

View File

@@ -36,7 +36,8 @@
#include "rs485.h" #include "rs485.h"
#include "net_socket.h" #include "net_socket.h"
#include "net_select.h" #include "net_select.h"
#include "ch395f_test.h" #include "app_main.h"
#include "sys_clock.h"
/* USER CODE END Includes */ /* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/
@@ -62,8 +63,6 @@
rs485_handle_t g_rs485; rs485_handle_t g_rs485;
#define RS485_RX_BUF_SIZE 256 #define RS485_RX_BUF_SIZE 256
uint8_t g_rs485_rx_buf[RS485_RX_BUF_SIZE]; uint8_t g_rs485_rx_buf[RS485_RX_BUF_SIZE];
/* TCP Server Echo 测试 */
static int s_listen_sock = -1;
/* USER CODE END PV */ /* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/
@@ -118,26 +117,21 @@ int main(void)
/* USER CODE BEGIN 2 */ /* USER CODE BEGIN 2 */
HAL_Delay(100); HAL_Delay(100);
/* BSD Socket API 初始化(包含 CH395F 复位/配置/协议栈初始化/多连接模式) */ /*
DBG_INFO("net init: %s", * 时间子系统初始化(尽早完成,后续所有日志带时间戳)
(net_init("192.168.1.100", "255.255.255.0", "192.168.1.1") == 0) ? "OK" : "FAIL"); * sd2506_init() 从 RTC 读取初始时间
* sys_clock_init() 将 RTC 时间转为 Unix epoch建立墙钟基准
*/
app_main_init();
sd2506_init();
sys_clock_init();
/* 读取 MAC */ /* RS-485 初始化UART5 + PD0 方向控制) */
uint8_t mac[6]; rs485_init(&g_rs485, &huart5, ST_DIR4_GPIO_Port, ST_DIR4_Pin);
ch395f_get_mac_addr(mac); DBG_INFO("RS485 init: OK (UART5, DE=PD0)");
DBG_INFO("MAC: %02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
/* 强制 100M 全双工,建立 PHY 链路 */ /* 启动第一次 RS-485 接收 */
ch395f_set_phy(CH395F_PHY_100M_FULL); rs485_receive_start(&g_rs485, g_rs485_rx_buf, RS485_RX_BUF_SIZE);
HAL_Delay(3000);
uint8_t phy = ch395f_get_phy_status();
DBG_INFO("PHY: %s",
(phy == CH395F_PHY_100M_FULL) ? "100M FULL" :
(phy == CH395F_PHY_100M_HALF) ? "100M HALF" :
(phy == CH395F_PHY_10M_FULL) ? "10M FULL" :
(phy == CH395F_PHY_10M_HALF) ? "10M HALF" :
(phy == CH395F_PHY_DISCONN) ? "DISCONNECT" : "UNKNOWN");
/* GD5F 初始化测试 */ /* GD5F 初始化测试 */
int ret = gd5f2gq5ue_init(); int ret = gd5f2gq5ue_init();
@@ -155,12 +149,8 @@ int main(void)
DBG_INFO("ADC init: %s", DBG_INFO("ADC init: %s",
(tp_ret == TPAFE5160_OK) ? "OK" : "TIMEOUT"); (tp_ret == TPAFE5160_OK) ? "OK" : "TIMEOUT");
/* SD2506 RTC 初始化测试 */ /* SD2506 RTC 诊断信息 */
int sd_ret = sd2506_init(); {
DBG_INFO("SD2506 init: %s",
(sd_ret == SD2506_OK) ? "OK" : "I2C ERROR");
if (sd_ret == SD2506_OK) {
/* 读取 ID */ /* 读取 ID */
uint8_t sd_id[8]; uint8_t sd_id[8];
sd2506_get_id(sd_id); sd2506_get_id(sd_id);
@@ -188,56 +178,10 @@ int main(void)
sd_vbat / 1000, (sd_vbat % 1000) / 10); sd_vbat / 1000, (sd_vbat % 1000) / 10);
} }
/* RS-485 初始化UART5 + PD0 方向控制) */
rs485_init(&g_rs485, &huart5, ST_DIR4_GPIO_Port, ST_DIR4_Pin);
DBG_INFO("RS485 init: OK (UART5, DE=PD0)");
/* 启动第一次 RS-485 接收 */
rs485_receive_start(&g_rs485, g_rs485_rx_buf, RS485_RX_BUF_SIZE);
/* 创建 TCP Server Echo 测试 */
{
s_listen_sock = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0);
DBG_INFO("TCP listen socket: %d", s_listen_sock);
struct net_sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = NET_AF_INET;
serv_addr.sin_port = net_htons(8080);
serv_addr.sin_addr.s_addr = NET_INADDR_ANY;
if (net_bind(s_listen_sock, (struct net_sockaddr *)&serv_addr, sizeof(serv_addr)) == 0) {
DBG_INFO("TCP bind: OK (port 8080)");
} else {
DBG_ERROR("TCP bind: FAIL");
}
if (net_listen(s_listen_sock, NET_TCP_SERVER_MAX_CLIENTS) == 0) {
DBG_INFO("TCP listen: OK (max_clients=%d)", NET_TCP_SERVER_MAX_CLIENTS);
} else {
DBG_ERROR("TCP listen: FAIL");
}
}
/* 启动第一次转换CubeMX 已配置 BUSY EXTI 中断) */ /* 启动第一次转换CubeMX 已配置 BUSY EXTI 中断) */
tpafe5160_start_conv_irq(); tpafe5160_start_conv_irq();
/* CH395F 驱动测试 — 各阶段独立运行,注释 define 可跳过 */ /* CH395F 测试已全部移至 ch395fTestTask */
#ifdef ENABLE_PHASE1_TESTS
ch395f_phase1_tests();
#endif
#ifdef ENABLE_PHASE3_TESTS
ch395f_phase3_tests();
#endif
#ifdef ENABLE_PHASE2_TESTS
ch395f_phase2_tests();
#endif
#ifdef ENABLE_PHASE5_TESTS
ch395f_phase5_init();
#endif
#ifdef ENABLE_PHASE6_TESTS
ch395f_phase6_tests();
#endif
/* USER CODE END 2 */ /* USER CODE END 2 */
/* Init scheduler */ /* Init scheduler */
@@ -256,10 +200,8 @@ int main(void)
/* USER CODE END WHILE */ /* USER CODE END WHILE */
/* USER CODE BEGIN 3 */ /* USER CODE BEGIN 3 */
/* 网络轮询已移到 FreeRTOS netTask 中 */
;
/* USER CODE END 3 */
} }
/* USER CODE END 3 */
} }
/** /**

View File

@@ -26,6 +26,8 @@
SPI_HandleTypeDef hspi1; SPI_HandleTypeDef hspi1;
SPI_HandleTypeDef hspi2; SPI_HandleTypeDef hspi2;
DMA_HandleTypeDef hdma_spi1_rx;
DMA_HandleTypeDef hdma_spi1_tx;
DMA_HandleTypeDef hdma_spi2_rx; DMA_HandleTypeDef hdma_spi2_rx;
DMA_HandleTypeDef hdma_spi2_tx; DMA_HandleTypeDef hdma_spi2_tx;
@@ -47,7 +49,7 @@ void MX_SPI1_Init(void)
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
@@ -119,6 +121,46 @@ void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* SPI1 DMA Init */
/* SPI1_RX Init */
hdma_spi1_rx.Instance = DMA2_Stream0;
hdma_spi1_rx.Init.Channel = DMA_CHANNEL_3;
hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_rx.Init.Mode = DMA_NORMAL;
hdma_spi1_rx.Init.Priority = DMA_PRIORITY_LOW;
hdma_spi1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_spi1_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(spiHandle,hdmarx,hdma_spi1_rx);
/* SPI1_TX Init */
hdma_spi1_tx.Instance = DMA2_Stream3;
hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3;
hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_tx.Init.Mode = DMA_NORMAL;
hdma_spi1_tx.Init.Priority = DMA_PRIORITY_LOW;
hdma_spi1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_spi1_tx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(spiHandle,hdmatx,hdma_spi1_tx);
/* SPI1 interrupt Init */
HAL_NVIC_SetPriority(SPI1_IRQn, 7, 0);
HAL_NVIC_EnableIRQ(SPI1_IRQn);
/* USER CODE BEGIN SPI1_MspInit 1 */ /* USER CODE BEGIN SPI1_MspInit 1 */
/* USER CODE END SPI1_MspInit 1 */ /* USER CODE END SPI1_MspInit 1 */
@@ -215,6 +257,12 @@ void HAL_SPI_MspDeInit(SPI_HandleTypeDef* spiHandle)
*/ */
HAL_GPIO_DeInit(GPIOB, GD_SCLK_Pin|GD_SO_Pin|GD_SI_Pin); HAL_GPIO_DeInit(GPIOB, GD_SCLK_Pin|GD_SO_Pin|GD_SI_Pin);
/* SPI1 DMA DeInit */
HAL_DMA_DeInit(spiHandle->hdmarx);
HAL_DMA_DeInit(spiHandle->hdmatx);
/* SPI1 interrupt Deinit */
HAL_NVIC_DisableIRQ(SPI1_IRQn);
/* USER CODE BEGIN SPI1_MspDeInit 1 */ /* USER CODE BEGIN SPI1_MspDeInit 1 */
/* USER CODE END SPI1_MspDeInit 1 */ /* USER CODE END SPI1_MspDeInit 1 */

View File

@@ -55,8 +55,11 @@
/* USER CODE END 0 */ /* USER CODE END 0 */
/* External variables --------------------------------------------------------*/ /* External variables --------------------------------------------------------*/
extern DMA_HandleTypeDef hdma_spi1_rx;
extern DMA_HandleTypeDef hdma_spi1_tx;
extern DMA_HandleTypeDef hdma_spi2_rx; extern DMA_HandleTypeDef hdma_spi2_rx;
extern DMA_HandleTypeDef hdma_spi2_tx; extern DMA_HandleTypeDef hdma_spi2_tx;
extern SPI_HandleTypeDef hspi1;
extern SPI_HandleTypeDef hspi2; extern SPI_HandleTypeDef hspi2;
extern DMA_HandleTypeDef hdma_usart1_tx; extern DMA_HandleTypeDef hdma_usart1_tx;
extern DMA_HandleTypeDef hdma_usart1_rx; extern DMA_HandleTypeDef hdma_usart1_rx;
@@ -211,6 +214,20 @@ void EXTI9_5_IRQHandler(void)
/* USER CODE END EXTI9_5_IRQn 1 */ /* USER CODE END EXTI9_5_IRQn 1 */
} }
/**
* @brief This function handles SPI1 global interrupt.
*/
void SPI1_IRQHandler(void)
{
/* USER CODE BEGIN SPI1_IRQn 0 */
/* USER CODE END SPI1_IRQn 0 */
HAL_SPI_IRQHandler(&hspi1);
/* USER CODE BEGIN SPI1_IRQn 1 */
/* USER CODE END SPI1_IRQn 1 */
}
/** /**
* @brief This function handles SPI2 global interrupt. * @brief This function handles SPI2 global interrupt.
*/ */
@@ -309,6 +326,20 @@ void TIM7_IRQHandler(void)
/* USER CODE END TIM7_IRQn 1 */ /* USER CODE END TIM7_IRQn 1 */
} }
/**
* @brief This function handles DMA2 stream0 global interrupt.
*/
void DMA2_Stream0_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream0_IRQn 0 */
/* USER CODE END DMA2_Stream0_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_spi1_rx);
/* USER CODE BEGIN DMA2_Stream0_IRQn 1 */
/* USER CODE END DMA2_Stream0_IRQn 1 */
}
/** /**
* @brief This function handles DMA2 stream2 global interrupt. * @brief This function handles DMA2 stream2 global interrupt.
*/ */
@@ -323,6 +354,20 @@ void DMA2_Stream2_IRQHandler(void)
/* USER CODE END DMA2_Stream2_IRQn 1 */ /* USER CODE END DMA2_Stream2_IRQn 1 */
} }
/**
* @brief This function handles DMA2 stream3 global interrupt.
*/
void DMA2_Stream3_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream3_IRQn 0 */
/* USER CODE END DMA2_Stream3_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_spi1_tx);
/* USER CODE BEGIN DMA2_Stream3_IRQn 1 */
/* USER CODE END DMA2_Stream3_IRQn 1 */
}
/** /**
* @brief This function handles DMA2 stream7 global interrupt. * @brief This function handles DMA2 stream7 global interrupt.
*/ */

File diff suppressed because it is too large Load Diff

View File

@@ -1,264 +0,0 @@
# CH395F 测试使用指南
## 目录
1. [快速开始](#1-快速开始)
2. [各阶段测试](#2-各阶段测试)
- [Phase 1: 寄存器测试](#phase-1-寄存器测试)
- [Phase 2: TCP Client 收发+重连](#phase-2-tcp-client)
- [Phase 3: UDP Server Echo](#phase-3-udp-server-echo)
- [Phase 4: NET 层 TCP Echo](#phase-4-net-层-tcp-echo)
- [Phase 5: NET 层 UDP Echo](#phase-5-net-层-udp-echo)
- [Phase 6: DHCP](#phase-6-dhcp)
- [Phase 7: Select/Poll](#phase-7-selectpoll)
- [Phase 8: 多客户端并发](#phase-8-多客户端并发)
3. [Python 脚本参考](#3-python-脚本参考)
4. [常见问题](#4-常见问题)
5. [陷阱记录](#5-陷阱记录)
---
## 1. 快速开始
### 启用/禁用测试
编辑 `Drivers/BSP/CH395F/ch395f_test.h`,取消注释想要测试的阶段:
```c
//#define ENABLE_PHASE1_TESTS /* 取消注释即启用 */
//#define ENABLE_PHASE2_TESTS /* 默认关闭 */
//#define ENABLE_PHASE3_TESTS /* 默认关闭 */
#define ENABLE_PHASE4_TESTS /* 当前默认启用netTask TCP echo */
//#define ENABLE_PHASE5_TESTS /* 默认关闭 */
//#define ENABLE_PHASE6_TESTS /* 默认关闭(会改 IP */
//#define ENABLE_PHASE7_TESTS /* 默认关闭 */
```
### 测试拓扑
```
PC (192.168.1.2) ──── 交换机 ──── CH395F (192.168.1.100)
```
---
## 2. 各阶段测试
### Phase 1: 寄存器测试
| 项目 | 说明 |
|------|------|
| 类型 | 独立运行,无需 PC 配合 |
| 耗时 | ~1 秒 |
| 测试项 | 芯片版本、命令状态、IP/网关/掩码、全局中断一致性、Socket 中断、ARP、TTL |
**操作**
```
1. 只启用 ENABLE_PHASE1_TESTS
2. 编译烧录
3. 看串口输出
```
**通过标准**:串口打印 `=== Phase 1 Tests Complete ===`,中间无 ERROR。
---
### Phase 2: TCP Client
| 项目 | 说明 |
|------|------|
| 类型 | 需 PC 配合 |
| 耗时 | ~60 秒 |
| 测试项 | TCP Client 连接、收发 64 字节、关闭重连 ×3 轮 |
| 默认端口 | CH395F → PC:8081 |
**操作**
```
终端 1: python test/ch395f_socket_test.py tcp_server --port 8081 --timeout 180
终端 2: 编译烧录(只启用 ENABLE_PHASE2_TESTS
```
**通过标准**:串口显示 3 轮 `closed: sock=0x00`PC 显示 3 次连接且 PING echo 匹配。
---
### Phase 3: UDP Server Echo
| 项目 | 说明 |
|------|------|
| 类型 | 需 PC 配合 |
| 耗时 | ~30 秒 |
| 测试项 | UDP Server 模式、8 字节头解析、30 轮 echo含 HELLO + PING + 大包) |
| 默认端口 | CH395F:60000PC 发到 60000 |
**操作**
```
终端 1: 编译烧录(只启用 ENABLE_PHASE3_TESTS
终端 2: python test/ch395f_socket_test.py udp_client --ip 192.168.1.100 --port 60000 --timeout 30
```
**通过标准**PC 显示 30/30 回显匹配(含 HELLO ×1 + PING ×5 + 大包 ×20 + 其他)。
> **关键发现**CH395F UDP 分两种模式——DesIP=`255.255.255.255` 是 Server 模式(接受任意来源),否则只能收指定 IP:Port。每次 `write_send_buf` 后必须等 `SINT_STAT_SENDBUF_FREE`。
---
### Phase 4: NET 层 TCP Echo
| 项目 | 说明 |
|------|------|
| 类型 | 需 PC 配合netTask 中运行 |
| 耗时 | 按需 |
| 测试项 | `net_recv()` + `net_send()` 非阻塞 echo |
| 默认端口 | CH395F TCP Server:8080PC 连到 8080 |
> ⚠️ 该测试在 FreeRTOS netTask 中运行,需等调度器启动(`osKernelStart`)后约 2 秒。
**操作**
```
终端 1: 编译烧录(只启用 ENABLE_PHASE4_TESTS看到 "netTask: started"
终端 2: python test/ch395f_socket_test.py tcp_client --ip 192.168.1.100 --port 8080
```
**通过标准**10/10 回显匹配 + 大数据传输完整。
---
### Phase 5: NET 层 UDP Echo
| 项目 | 说明 |
|------|------|
| 类型 | 需 PC 配合netTask 中运行 |
| 耗时 | 按需 |
| 测试项 | `net_recvfrom()` + `net_sendto()` 非阻塞 echo |
| 默认端口 | CH395F UDP:60000 |
> ⚠️ 该测试在 FreeRTOS netTask 中运行,需等调度器启动后约 2 秒。
**操作**
```
终端 1: 编译烧录(只启用 ENABLE_PHASE5_TESTS看到 "netTask: started"
终端 2: python test/ch395f_socket_test.py udp_client --ip 192.168.1.100 --port 60000 --timeout 30
```
**通过标准**PING 5/5 + 大包 10/10。
---
### Phase 6: DHCP
| 项目 | 说明 |
|------|------|
| 类型 | 需 PC 配合 |
| 耗时 | ~15 秒 |
| 测试项 | DHCP 自动获取 IP、HELLO echo 验证连通性 |
> ⚠️ **DHCP 会改变 CH395F IP** 必须独立运行,不能与其他 Phase 同时启用。
PC 运行内置 DHCP Server完整 DISCOVER → OFFER → REQUEST → ACK 握手,然后发送 HELLO 验证 UDP 连通性。
```
终端 1: python test/ch395f_socket_test.py dhcp_server --dhcp-iface 192.168.1.2 --ip 192.168.1.100 --dhcp-mask 255.255.255.0 --dhcp-gw 192.168.1.1
终端 2: 编译烧录(只启用 ENABLE_PHASE6_TESTS
```
**通过标准**PC 显示 `DHCP 分配完成!` + 连通性验证通过。
> **关键:** 需管理员权限Windows 绑定端口 67。xid 偏移为 `data[4:8]`。`--dhcp-iface` 为 PC 网卡 IP用于区分网卡
**通过标准**PC 显示 `收到 CH395F 广播宣告` + 3 轮 HELLO echo 成功率 100%。
> **关键:** CH395F 每次 `write_send_buf` 后必须等 `SENDBUF_FREE` 中断,否则后续接收操作失败。
---
### Phase 7: Select/Poll
| 项目 | 说明 |
|------|------|
| 类型 | 需 PC 配合netTask 中运行 |
| 耗时 | 按需 |
| 测试项 | `net_select()` 多路复用检测可读 socket → echo |
| 默认端口 | CH395F TCP Server:8080 |
> ⚠️ 该测试在 FreeRTOS netTask 中运行,需等调度器启动后约 2 秒。
**操作**:同 Phase 4TCP Client 连 8080
**通过标准**:同 Phase 410/10 回显匹配)。
> Phase 4 和 Phase 7 功能相同但实现不同通常只启用一个。Phase 7 额外验证了 `NET_FD_SET/ISSET` 宏和 `net_select()`。
---
### Phase 8: 多客户端并发
| 项目 | 说明 |
|------|------|
| 类型 | 需 PC 配合,主循环运行 |
| 耗时 | ~2 秒 |
| 测试项 | 7 个 TCP 客户端并发,每客户端 10 轮 PING echo |
| 默认端口 | CH395F TCP Server:8080 |
**操作**
```
终端 1: 编译烧录(启用 ENABLE_PHASE4_TESTS 或 ENABLE_PHASE7_TESTS
终端 2: python test/ch395f_socket_test.py stress --ip 192.168.1.100 --port 8080 --count 10 --timeout 30
```
**通过标准**7 并发 × 10 轮 = 70/70 成功率 100%。
> **关键陷阱**`net_listen()` 中数据 Socket 1~7 的配置必须在监听 Socket 0 的 `OPEN + TCP_LISTEN` **之前**完成。CH395F 在 `TCP_LISTEN` 时刻一次性扫描可用 Socket若颠倒顺序则 Socket 4~7 不被识别,最多只接受 3 个并发连接。详见 `docs/CH395F_Trap_Records.md → Trap 01`。
---
## 3. Python 脚本参考
| 命令 | 说明 |
|------|------|
| `tcp_server --port 8081` | PC 作 TCP Server测试 CH395F TCP Client |
| `tcp_client --ip x.x.x.x --port 8080` | PC 作 TCP Client测试 CH395F TCP Server echo |
| `udp_client --ip x.x.x.x --port 60000 --timeout 30` | PC 发 UDP PING测试 CH395F UDP echo |
| `udp_server --port 8082 --timeout 60` | PC 作 UDP Server接收 CH395F 数据 |
| `stress --ip x.x.x.x --port 8080 --count 10` | 7 并发压力测试 |
| `dhcp_server --dhcp-iface IP --dhcp-mask MASK --dhcp-gw GW` | 内置 DHCP Server完整握手 + 连通性验证(需管理员权限) |
通用参数:
通用参数:
- `--ip`:目标 IP默认 192.168.1.100
- `--port`:端口(各模式默认不同)
- `--count`:测试次数(默认 10
- `--timeout`:超时秒数(默认 30
- `--size`:数据包大小(默认 1024
---
## 4. 常见问题
| 问题 | 原因 | 解决 |
|------|------|------|
| Phase 2 `open_socket: 0x20 (FAIL)` | socket 未完全关闭 | 已修复:先 closeRST不等 disconnect |
| Phase 2 收到 `\x00(\x00\x00...` 乱码 | socket 7`NET_MAX_SOCKETS-1`)默认无发送缓冲 | 在 `open_socket` 前调用 `set_send_buf(sock, 28, 2)` 分配 |
| Phase 3 UDP 收不到数据 | DesIP 设为具体 IP 而非 255.255.255.255 | 设 DesIP=`0xFFFFFFFF` 进入 Server 模式 |
| Phase 3 HELLO 收到但 PING 收不到 | 没等 `SENDBUF_FREE` 就第二次写 | 每次 `write_send_buf` 后轮询 `SINT_STAT_SENDBUF_FREE` |
| Python 报 `WinError 10054` | STM32 还没启动 | 等串口看到阶段启动信息再跑 Python |
| Phase 5 UDP 不工作 | NET 层 `net_bind()` 没打开 CH395F Socket | 已修复:`net_bind()` 对 DGRAM 类型自动打开 |
| `net_recvfrom()` 源地址错误 | UDP 头解析偏移错了 | 已修复:端口读 `header[2-3]`IP 读 `header[4-7]` |
| Phase 8 Socket 4~7 分配不到 | 数据 Socket 配置在 `TCP_LISTEN` 之后CH395F 在 listen 时扫描) | 先配数据 Socket 1~7 的缓冲/端口/协议,再配监听 Socket 0 |
---
## 5. 陷阱记录
详细陷阱记录(含根因分析、解决方案、发现时间)见 `docs/CH395F_Trap_Records.md`
| # | 陷阱 | 对应文件 |
|---|------|----------|
| 01 | Socket 4~7 自动分配不到(初始化顺序) | `net_socket.c``net_listen()` |
| 02 | 数据 Socket 缓冲区重叠 | `net_socket.c``net_listen()` |
| 03 | DHCP 包 xid 偏移错误 | `test/ch395f_socket_test.py` |
| 04 | UDP 发送未等待 SENDBUF_FREE | `ch395f.c` / `ch395f_test.c` |
| 05 | CH395F 与 RTL8305NBI 自动协商不兼容 | `ch395f.c` |
| 06 | TCP KeepAlive 参数非 500ms 倍数 | `net_socket.c``net_init()` |
| 07 | TCP 关闭误用 disconnect 导致 FIN_WAIT_2 | `ch395f.c` |
| 08 | RECV 中断电平触发无限循环 | `net_socket.c``net_poll()` |

View File

@@ -1,221 +0,0 @@
# CH395F 驱动陷阱记录
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
---
## Trap 01Socket 4~7 自动分配不到
### 现象
多连接模式下最多接受 3 个客户端Socket 1~3Socket 4~7 的 CONNECT 中断从不触发,新客户端 SYN 被静默丢弃。
### 根因
`net_listen()` 中数据 Socket 的配置顺序错误。CH395F 在 `TCP_LISTEN` 时刻**一次性扫描**当前所有可用 Socket 并建立内部查找表。如果数据 Socket 4~7 在 `TCP_LISTEN` **之后**才配置缓冲区/协议/端口,自动分配逻辑不认识它们。
### 正确顺序
1. 先配置数据 Socket 1~7`SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP`
2. 最后配置监听 Socket 0`SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN`
### 注意事项
- 数据 Socket 不调 `OPEN_SOCKET`,由 CH395F 在连接到达时自动打开
- DISCONNECT 后 CH395F 自动关闭 Socket但保留协议/端口/缓冲区配置,可被再次分配
- 手册 §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反)
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_listen()`
### 发现时间
Phase 8 压力测试2025-07
---
## Trap 02数据 Socket 缓冲区重叠
### 现象
多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。
### 根因
CH395F 默认只为 Socket 0~3 各分配 4 个缓冲区块(共 32 块用完Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31导致数据重叠。
### 解决方案
每个数据 Socket 独占 4 个缓冲区块2 发 + 2 收),按 `ds * 4` 基址分配,确保不重叠:
| Socket | 发送块 | 接收块 |
|--------|--------|--------|
| 1 | 4~5 | 6~7 |
| 2 | 8~9 | 10~11 |
| 3 | 12~13 | 14~15 |
| 4 | 16~17 | 18~19 |
| 5 | 20~21 | 22~23 |
| 6 | 24~25 | 26~27 |
| 7 | 28~29 | 30~31 |
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_listen()`
### 发现时间
Phase 8 压力测试2025-07
---
## Trap 03DHCP 包 xid 偏移错误
### 现象
Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid握手失败。
### 根因
错误使用了 `data[232:236]` 作为 xidWireshark 某旧版显示误导),实际 DHCP 标准偏移为 `data[4:8]`
### 正确偏移
- xid`data[4:8]`
- msg_type`data[242]`
- client_mac`data[28:34]`
- magic cookie`data[236:240]`
### 对应文件
`test/ch395f_socket_test.py` → DHCP server 模式
### 发现时间
Phase 6, 2025-07
---
## Trap 04UDP 发送后必须等待 SENDBUF_FREE
### 现象
UDP 发送后立即执行接收操作,接收长度始终为 0 或数据错误。
### 根因
CH395F 手册要求每次 `WRITE_SEND_BUF` 后必须等待 `SINT_STAT_SENBUF_FREE` 中断,否则下次写入或接收操作可能失败。
### 解决方案
每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查 `SINT_STAT_SENBUF_FREE` 标志。
### 对应文件
`Drivers/BSP/CH395F/ch395f.c` / test code in `ch395f_test.c`
### 发现时间
Phase 6 广播宣告模式2025-07
---
## Trap 05CH395F 与 RTL8305NBI 自动协商不兼容
### 现象
`ch395f_get_phy_status()` 始终返回 `PHY_DISCONN`,链路无法建立。
### 根因
CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败。
### 解决方案
初始化协议栈后强制设为 100M 全双工:
```c
ch395f_set_phy(CH395F_PHY_100M_FULL);
```
### 对应文件
`Drivers/BSP/CH395F/ch395f.c`
### 发现时间
项目初期硬件联调
---
## Trap 06TCP KeepAlive 参数必须为 500ms 倍数
### 现象
设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。
### 根因
CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。
`IDLE` 必须 > `INTVL`
### 正确值
```c
ch395f_set_keepalive_idle(60000); // 60s500 倍数)
ch395f_set_keepalive_intvl(5000); // 5s500 倍数)
ch395f_set_keepalive_cnt(3); // 3 次
```
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_init()`
### 发现时间
Phase 2 测试2025-07
---
## Trap 07TCP 关闭重连直接 close不要 disconnect
### 现象
`disconnect → close` 后 Socket 卡在 `FIN_WAIT_2` 数分钟,无法重新打开。
### 根因
`disconnect` 发送 FIN 将 Socket 推入 `FIN_WAIT_2`,此后 `close` 不再发 RST必须等远端发 FIN 才能关闭。
### 解决方案
直接调用 `ch395f_close_socket()`,在 ESTABLISHED 或 CLOSE_WAIT 下会发 RST 立即终止。
关闭后轮询 `ch395f_get_socket_status()` 等待 `sock=0x00``open_socket`
### 发现时间
Phase 2 TCP Client 测试2025-07
---
## Trap 08RECV 中断电平触发,避免无限循环
### 现象
`net_poll()` 中 RECV 中断无限触发主循环echo 等)永远得不到执行。
### 根因
CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就保持 `INT#` 低电平。
`do { ... } while(int_status != 0)` 会无限循环。
### 解决方案
`net_poll()` 使用 `do { ... } while(0)` 每次只处理一批中断,由主循环负责读取数据。
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_poll()`
---
## 附CH395F 驱动关键点
### 初始化顺序
- 必须按手册9.2.1节顺序:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`
- **IP/网关/掩码必须在 `INIT_CH395` 之前设置**INIT 会读取并锁定当前寄存器值到协议栈
- **`SET_PHY` 必须在 `INIT_CH395` 之后**,复位 MAC/PHY 建立物理链路,不影响已锁定的协议栈参数
- **`CMD_PING_ENABLE` 不需要显式调用**INIT 后默认可用
### SPI 通信
- 每次 SPI 事务需调用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹
- **大数据量收发使用 SPI2 DMA**`ch395f_write_send_buf()``ch395f_read_recv_buf()` 已改造为 DMA 批量传输DMA1_Stream3 RX, DMA1_Stream4 TX命令和配置操作仍使用逐字节轮询
- DMA 缓冲区:`s_spi2_dma_tx_buf[1500]` / `s_spi2_dma_rx_buf[1500]`4 字节对齐
### TCP 参数配置(必须在 INIT_CH395 之前)
- **重传参数**`SET_RETRAN_COUNT`默认12次最大20`SET_RETRAN_PERIOD`默认500ms最大1000ms总重传时间 = 次数 × 周期
- **KeepAlive 参数必须为 500ms 的倍数**`IDLE`默认20000ms`INTVL`默认15000ms且 IDLE > INTVL传入非 500 倍数的值导致 CH395F 内部定时器异常
- **KeepAlive 默认关闭**:需在 `SINT_STAT_CONNECT` 后调用 `ch395f_set_keepalive_enable(sock, 1)` 启用
### Socket 4~7 缓冲区
- CH395F 默认只给 Socket 0~3 各分配 4 个缓冲区块(共 32 块用完Socket 4~7 为零块
- 多连接模式下使用 Socket 4~7 时,**必须在 `open_socket` 之前**显式分配:
- `ch395f_set_send_buf(sock, start_block, count)`
- `ch395f_set_recv_buf(sock, start_block, count)`
- 不分配会导致发送数据为固定垃圾内容(`0x0028` 填充)、接收缓冲区无法存储数据
### TCP 关闭重连
- **直接调用 `ch395f_close_socket()`,不要先调 `ch395f_tcp_disconnect()`**
-`disconnect → close`disconnect 发 FIN 推入 FIN_WAIT_2close 不再发 RSTSocket 卡住数分钟
-`close` 直接:在 ESTABLISHED 或 CLOSE_WAIT 下 close 发 RST 立即终止,瞬间回到 CLOSED
- 关闭后需轮询 `ch395f_get_socket_status()` 等待 `sock=0x00``open_socket`
### SOCK_TIMEOUT 处理
- 长时间无数据时可能触发 `SINT_STAT_SOCK_TIMEOUT`**不应视为致命错误**——记录日志后继续操作即可,不要因此关闭 Socket
### UDP 模式区分
- **DesIP=0xFFFFFFFF → UDP Server 模式**:接受任意来源数据,接收数据前 8 字节为信息头(`reserved[2] src_port[2] src_ip[4]`),回发前需设置 `SET_DES_IP``SET_DES_PORT`
- **DesIP=具体 IP → UDP Client 模式**:只接收指定 IP:Port 的数据,接收数据无信息头
### UDP 发送缓冲
- 每次 `ch395f_write_send_buf()` 后必须等待 `SINT_STAT_SENBUF_FREE` 中断,否则下次写入会被 CH395F 静默丢弃

View File

@@ -1,400 +0,0 @@
# FlashDB 使用说明
## 1. 概述
本项目使用 [FlashDB](https://github.com/armink/FlashDB) 嵌入式数据库,提供两种数据库:
- **KVDB**(键值数据库):存储键值对,类似 Redis 简化版
- **TSDB**(时序数据库):存储带时间戳的日志记录,支持按时间范围查询
底层存储介质为 GD5F2GQ5UE SPI NAND Flash256MB通过 FALFlash Abstraction Layer抽象层访问。
## 2. 软件架构
```
┌─────────────────────────────────────────┐
│ 用户应用main.c
├──────────────┬──────────────────────────┤
│ fdb_kvdb │ fdb_tsdb │
│ (键值数据库) │ (时序数据库) │
├──────────────┴──────────────────────────┤
│ FlashDB 核心 │
│ Lib/FlashDB/src/ │
├─────────────────────────────────────────┤
│ FAL 抽象层 │
│ Lib/FlashDB/port/fal/src/ │
├─────────────────────────────────────────┤
│ FAL 设备适配层 │
│ fal_flash_gd5f2gq5ue.c │
├─────────────────────────────────────────┤
│ GD5F2GQ5UE SPI NAND 驱动 │
│ gd5f2gq5ue.c │
├─────────────────────────────────────────┤
│ SPI1 硬件外设 + HAL 驱动 │
└─────────────────────────────────────────┘
```
## 3. 文件清单
| 文件 | 说明 | 是否自编 |
|------|------|----------|
| `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h` | NAND 驱动头文件 | 是 |
| `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c` | NAND 驱动实现 | 是 |
| `Drivers/BSP/GD5F2GQ5UE/fal_flash_gd5f2gq5ue.c` | FAL 设备适配 | 是 |
| `Drivers/BSP/GD5F2GQ5UE/fal_cfg.h` | FAL 设备表 + 分区表 | 是 |
| `Drivers/BSP/GD5F2GQ5UE/fdb_cfg.h` | FlashDB 功能配置 | 是 |
| `Lib/FlashDB/src/` | FlashDB 核心源码 | 否 |
| `Lib/FlashDB/port/fal/src/` | FAL 硬件抽象框架源码 | 否 |
## 4. 硬件配置
### 4.1 Flash 芯片参数
| 参数 | 值 |
|------|-----|
| 型号 | GD5F2GQ5UEGigaDevice |
| 总容量 | 2Gbit = 256MB |
| 页大小 | 2048 字节 |
| Spare 区 | 64 字节 |
| 每块页数 | 64 页 |
| 块大小 | 128KB64 × 2048 |
| 总块数 | 2048 |
| SPI 模式 | Mode 0CPOL=0, CPHA=0 |
| SPI 时钟 | 42MHzAPB2=84MHz, 分频=2 |
| MID | 0xC8 |
| DID | 0x52 |
### 4.2 引脚连接
| 信号 | STM32 引脚 | 方向 | 说明 |
|------|-----------|------|------|
| CS# | PE0 | OUT | 片选,低有效 |
| SCLK | PB3 | OUT | SPI1 时钟 |
| MOSI | PB5 | OUT | SPI1 主出从入 |
| MISO | PB4 | IN | SPI1 主入从出 |
| WP# | PB8 | OUT | 写保护,低有效(拉高禁用) |
| HOLD# | PE1 | OUT | 保持,低有效(拉高禁用) |
### 4.3 GPIO 初始化
CubeMX 中需确保以下 GPIO 上电默认电平:
- **CS#** (PE0):上电输出 **高电平**(未选中)
- **HOLD#** (PE1):上电输出 **高电平**(不保持)
- **WP#** (PB8):上电输出 **高电平**(不禁用写保护)
> 若 GPIO 初始电平错误SPI 通信将完全无响应。
## 5. 分区规划
| 分区名 | 设备 | 偏移 | 大小 | 用途 |
|--------|------|------|------|------|
| fdb_kvdb1 | gd5f2gq5ue | 0 | 64MB | KVDB 键值数据库 |
| fdb_tsdb1 | gd5f2gq5ue | 64MB | 64MB | TSDB 时序数据库 |
| (未分配) | - | 128MB | 128MB | 剩余空间 |
共使用 128MB / 256MB剩余 128MB 可扩展。
## 6. 初始化流程
```
1. HAL_Init()
2. SystemClock_Config()
3. MX_GPIO_Init() ← CS#/HOLD#/WP# 初始电平
4. MX_USART1_UART_Init() ← 调试串口
5. MX_SPI2_Init() ← CH395F 以太网
6. MX_SPI1_Init() ← GD5F2GQ5UE NAND
7. gd5f2gq5ue_init() ← NAND 初始化
8. fdb_kvdb_init() ← FlashDB KVDB 初始化
```
### 6.1 gd5f2gq5ue_init() 内部步骤
```
1. CS#/WP#/HOLD# 拉高
2. 发送复位命令FFh等待 5ms
3. 读取芯片 ID9Fh校验 MID=0xC8, DID=0x52
4. 使能内部 ECCSET_FEATURE(B0h, 10h)
5. 解除块保护SET_FEATURE(A0h, 00h)
```
> **关键点**SET_FEATURE 命令前必须先发写使能06h否则设置不生效。
## 7. KVDB 使用方法
### 7.1 初始化
```c
#include "flashdb.h"
static struct fdb_kvdb kvdb;
/* path 参数对应分区表中的分区名 "fdb_kvdb1" */
/* default_kv 传 NULL 表示不使用默认键值 */
int ret = fdb_kvdb_init(&kvdb, "db", "fdb_kvdb1", NULL, NULL);
if (ret != FDB_NO_ERR) {
/* 初始化失败处理 */
}
```
### 7.2 字符串读写
```c
/* 写入字符串 */
fdb_kv_set(&kvdb, "device_name", "STM32F407-DTU");
/* 读取字符串(返回内部缓存指针,下次写入同一 key 后失效) */
char *val = fdb_kv_get(&kvdb, "device_name");
if (val) {
printf("device_name = %s\n", val);
}
```
### 7.3 二进制数据读写Blob
```c
#include <string.h>
/* 写入二进制数据 */
uint32_t temp = 2560;
fdb_kv_set_blob(&kvdb, "temperature",
fdb_blob_make(NULL, &temp, sizeof(temp)));
/* 读取二进制数据 */
uint32_t read_temp = 0;
fdb_kv_get_blob(&kvdb, "temperature",
fdb_blob_make(NULL, &read_temp, sizeof(read_temp)));
printf("temperature = %u\n", read_temp);
```
### 7.4 删除键
```c
fdb_kv_del(&kvdb, "device_name");
```
### 7.5 遍历所有键
```c
struct fdb_kv kv;
struct fdb_kv_iterator itr;
fdb_kv_iterator_init(&kvdb, &itr);
while (fdb_kv_iterate(&kvdb, &itr)) {
kv = itr.curr;
printf("key: %s\n", kv.name);
}
```
### 7.6 打印所有键值
```c
fdb_kv_print(&kvdb);
```
## 8. TSDB 使用方法
### 8.1 获取时间戳
FlashDB 需要用户提供时间戳获取函数。本项目使用 HAL_GetTick()(毫秒):
```c
static fdb_time_t get_time(void)
{
return (fdb_time_t)HAL_GetTick();
}
```
### 8.2 初始化
```c
static struct fdb_tsdb tsdb;
/* max_len 参数限制单条记录最大长度 */
int ret = fdb_tsdb_init(&tsdb, "tsdb", "fdb_tsdb1",
get_time, 256, NULL);
```
### 8.3 追加记录
```c
/* 写入二进制数据,自动附加当前时间戳 */
uint32_t sensor_val = 3200;
fdb_tsl_append(&tsdb,
fdb_blob_make(NULL, &sensor_val, sizeof(sensor_val)));
```
### 8.4 遍历记录
```c
/* 正序遍历 */
fdb_tsl_iter(&tsdb, tsl_cb_func, NULL);
/* 逆序遍历 */
fdb_tsl_iter_reverse(&tsdb, tsl_cb_func, NULL);
/* 按时间范围遍历 */
fdb_tsl_iter_by_time(&tsdb, from_time, to_time, tsl_cb_func, NULL);
```
回调函数原型:
```c
static void tsl_cb_func(struct fdb_tsl *tsl, void *arg)
{
struct fdb_blob blob;
uint32_t val;
/* 读取数据 */
fdb_blob_read((fdb_db_t)&tsl,
fdb_tsl_to_blob(tsl, fdb_blob_make(&blob, &val, sizeof(val))));
printf("ts=%lu, val=%lu\n", tsl->time, val);
}
```
### 8.5 查询记录数量
```c
size_t count = fdb_tsl_query_count(&tsdb, from_time, to_time,
FDB_TSL_STATUS_APPEND);
```
### 8.6 清理所有记录
```c
fdb_tsl_clean(&tsdb);
```
## 9. 配置文件说明
### 9.1 fdb_cfg.h
| 宏 | 说明 |
|----|------|
| `FDB_USING_KVDB` | 启用 KVDB |
| `FDB_USING_TSDB` | 启用 TSDB |
| `FDB_USING_FAL_MODE` | 使用 FAL 模式(非文件系统) |
| `FDB_WRITE_GRAN` | 写入粒度 8字节可编程 |
| `FDB_DEBUG_ENABLE` | 启用调试输出 |
| `FDB_KV_AUTO_UPDATE` | KVDB 版本变化时自动更新(默认关闭) |
### 9.2 fal_cfg.h
分区表定义,修改分区大小需同时调整偏移和大小:
```c
#define FAL_PART_TABLE \
{ \
{FAL_PART_MAGIC_WORD, "fdb_kvdb1", "gd5f2gq5ue", 0, 64*1024*1024, 0}, \
{FAL_PART_MAGIC_WORD, "fdb_tsdb1", "gd5f2gq5ue", 64*1024*1024, 64*1024*1024, 0}, \
}
```
字段顺序:`{魔数, 分区名, 设备名, 偏移, 大小, 标志}`
## 10. Keil 工程配置
### 10.1 头文件搜索路径
在 Keil 工程 Options → C/C++ → Include Paths 中添加:
```
../Drivers/BSP/GD5F2GQ5UE
../Lib/FlashDB/inc
../Lib/FlashDB/port/fal/inc
```
### 10.2 编译的源文件
需在 Keil 工程中添加以下源文件:
```
Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c
Drivers/BSP/GD5F2GQ5UE/fal_flash_gd5f2gq5ue.c
Lib/FlashDB/src/fdb.c
Lib/FlashDB/src/fdb_kvdb.c
Lib/FlashDB/src/fdb_tsdb.c
Lib/FlashDB/src/fdb_utils.c
Lib/FlashDB/port/fal/src/fal.c
Lib/FlashDB/port/fal/src/fal_flash.c
Lib/FlashDB/port/fal/src/fal_partition.c
```
### 10.3 全局宏定义
确保工程中定义了:
```
USE_HAL_DRIVER, STM32F407xx
```
## 11. 注意事项
### 11.1 NAND Flash 特性
1. **写前必须擦除**NAND 只能将 1→0不能 0→1。写入前目标块必须先擦除全部变为 0xFF
2. **擦除单位是块**:最小擦除单位 128KB不能按页擦除
3. **写入单位是页**单次写入不超过一页2048 字节),跨页需驱动层拆分
4. **Spare 区不可直接访问**:驱动已使能内部 ECCSpare 区由芯片硬件管理
### 11.2 驱动层注意事项
1. **每次 SPI 事务前**:必须 `CS_LOW()` 拉低片选
2. **每次 SPI 事务后**:必须 `CS_HIGH()` 拉高片选
3. **SET_FEATURE 前必须写使能**:先发 06h再发 1Fh + 地址 + 数据
4. **块擦除地址是字节地址**D8h 命令的参数 = 块编号 × 128KB不是块编号本身
5. **读取 ID 需跳过 dummy 字节**9Fh 返回 3 字节,第 0 字节无意义,第 1 字节 MID第 2 字节 DID
### 11.3 FlashDB 使用注意事项
1. **fdb_kv_get 返回值是内部缓存**:下次对同一 key 写入后,之前返回的指针失效
2. **TSDB 需要用户提供时间戳**:通过 `get_time` 回调,本项目使用 `HAL_GetTick()`
3. **fdb_kvdb_init 的 path 参数**:对应分区表中的分区名 `"fdb_kvdb1"`
4. **FlashDB 开启 FDB_DEBUG_ENABLE 后**:会通过 `fdb_print()` 输出调试信息,需确保有可用的输出(如 printf 重定向到串口)
5. **分区大小修改后**:需同步更新 `fal_cfg.h` 中的偏移和大小
### 11.4 常见问题
| 现象 | 可能原因 | 解决方法 |
|------|----------|----------|
| init 返回 -6 (ID_MISMATCH) | SPI 通信失败或芯片未上电 | 检查接线、GPIO 初始电平、虚焊 |
| KVDB init 失败 | 分区名不匹配 | 确认 `fdb_kvdb_init` 的 path 与 `fal_cfg.h` 一致 |
| 写入后读取为空 | 未擦除或写入失败 | 检查写入返回值,确认目标块已擦除 |
| 读取数据异常 | ECC 错误 | 检查 `gd5f2gq5ue_init` 是否成功使能 ECC |
| SET_FEATURE 未生效 | 缺少写使能命令 | 确认 `gd5f_set_feature` 中先调用 `gd5f_write_enable()` |
## 12. 调试方法
### 12.1 串口调试输出
FlashDB 调试输出通过 `fdb_print` 宏实现。在 `fdb_cfg.h` 中定义 `FDB_DEBUG_ENABLE`FlashDB 内部操作会自动输出到标准输出。
确保 Keil 工程中 `printf` 已重定向到 USART1PA9/PA10, 115200bps
### 12.2 NAND 驱动调试
可通过串口输出以下信息验证驱动工作正常:
```c
/* 读取芯片 ID */
uint8_t mid, did;
gd5f2gq5ue_read_id(&mid, &did);
printf("MID=0x%02X DID=0x%02X\n", mid, did);
/* 预期输出MID=0xC8 DID=0x52 */
/* 初始化测试 */
int ret = gd5f2gq5ue_init();
printf("init ret=%d\n", ret);
/* 预期输出init ret=0 */
```
### 12.3 FAL 设备注册验证
```c
#include "fal.h"
/* 检查 FAL 是否初始化成功 */
int ret = fal_init();
printf("fal_init ret=%d\n", ret);
/* 预期输出fal_init ret=0 */
```

View File

@@ -0,0 +1,558 @@
# STM32F4-Base 存储架构说明
## 1. 概述
本 项目在 GD5F2GQ5UE SPI NAND Flash256MB上实现了三层存储软件栈
```
┌─────────────────────────────────────────────────────────────┐
│ 应用程序 (main, freertos) │
├─────────────────────────────────────────────────────────────┤
│ │
│ FatFs (文件系统) │
│ │
├─────────────────────────────────────────────────────────────┤
│ dhara FTL │
│ (地址映射 · 磨损均衡 · 坏块管理 · 垃圾回收) │
├─────────────────────────────────────────────────────────────┤
│ GD5F2GQ5UE NAND Flash 驱动 (底层 SPI) │
│ 硬件 SPI · 页读写 · 块擦除 · BBT · 内部 ECC 使能 │
├─────────────────────────────────────────────────────────────┤
│ SPI1 硬件外设 (PB3 SCK, PB4 MISO, PB5 MOSI) │
│ 42MHz · Mode 0 · MSB First · DMA2 (S0-RX, S3-TX) │
│ 小包轮询(≤32B) · 页数据 DMA (>32B) │
└─────────────────────────────────────────────────────────────┘
```
```
┌─────────────────────────────────────────────────────────────┐
│ 物理分区布局 (256MB) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ftl_fatfs (256MB) │ │
│ │ Block 0~2047 │ │
│ │ dhara FTL + FatFS全部 2048 个块) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ 偏移: 0 256MB│
└─────────────────────────────────────────────────────────────┘
```
**数据路径:**
- **应用** → `FatFS``dhara FTL``gd5f2gq5ue 驱动`。FTL 提供 LBA 到物理页的映射、磨损均衡、坏块管理和垃圾回收FatFS 通过标准 `disk_*` 接口访问 FTL 提供的块设备。
**初始化顺序:**
```
HAL_Init → SystemClock_Config → MX_GPIO_Init → MX_USART1_UART_Init
→ MX_SPI2_Init → MX_SPI1_Init
→ gd5f2gq5ue_init() // NAND 底层驱动 (含 BBT 扫描 + ECC 使能)
→ app_main_init() // 应用层初始化
→ osKernelStart() // FreeRTOS 启动
├─ defaultTask → f_mount() → 存储测试 + 业务
├─ netTask → 网络协议栈轮询
├─ adcTask → ADC 采集
└─ rs485Task → RS485 通信
```
---
## 2. 硬件规格GD5F2GQ5UE
| 参数 | 值 |
|------|-----|
| 容量 | 256 MB (2 Gb) |
| 页大小 (主区) | 2048 bytes (2 KB) |
| 备校区 (Spare) | 64 bytes/页 |
| 每块页数 | 64 |
| 块大小 | **128 KB** (64 × 2 KB) |
| 总块数 | 2048 |
| 总页面数 | 131072 |
| 内部 ECC | 支持 (每页 8bit ECC) |
| 接口 | SPI (Mode 0, CPOL=0 CPHA=0) |
| 最高时钟 | 42 MHz |
| 读页延迟 | < 60 μs |
| 编程延迟 | < 600 μs |
| 块擦除延迟 | < 3 ms |
**SPI 引脚分配:**
| 信号 | GPIO | 说明 |
|------|------|------|
| CS# | PE0 | 片选 (低有效) |
| SCK | PB3 | SPI1 SCK |
| MISO | PB4 | SPI1 MISO (主入从出) |
| MOSI | PB5 | SPI1 MOSI (主出从入) |
| WP# | PB8 | 写保护 (本驱动恒拉高) |
| HOLD# | PE1 | 保持 (本驱动恒拉高) |
---
## 3. 分区布局
单个分区,配置在 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c`
```c
#define FTL_FATFS_OFFSET 0 // 偏移 0占用全部 256MB
```
### 3.1 分区对照表
| 分区名 | 偏移 | 大小 | 物理块范围 | 逻辑用途 |
|--------|------|------|-----------|----------|
| `ftl_fatfs` | 0 | 256 MB | Block 0~2047 | dhara FTL + FatFS |
### 3.2 配置联动
- **FTL** — `nand_ftl.c``FTL_START_BLOCK = 0``FTL_NUM_BLOCKS = 2048`
- **FatFS** — 通过 `disk_ioctl(GET_SECTOR_COUNT)` 获取 FTL 管理的扇区数
---
## 4. 驱动层gd5f2gq5ue.c/h
### 4.1 SPI 命令集
| 命令 | 编码 | 用途 |
|------|------|------|
| `WRITE_ENABLE` | `0x06` | 写使能 (每个写/擦除前必须发送) |
| `WRITE_DISABLE` | `0x04` | 写禁止 |
| `GET_FEATURE` | `0x0F` | 读状态/feature 寄存器 |
| `SET_FEATURE` | `0x1F` | 写 feature 寄存器 (前需写使能) |
| `READ_ID` | `0x9F` | 读芯片 ID |
| `PAGE_READ` | `0x13` | 将页数据从存储阵列载入内部缓存 |
| `READ_FROM_CACHE` | `0x0B` | 从内部缓存读取数据 |
| `PROGRAM_LOAD` | `0x02` | 将数据写入内部缓存 |
| `PROGRAM_EXEC` | `0x10` | 将缓存数据编程到存储阵列 |
| `BLOCK_ERASE` | `0xD8` | 块擦除 (128KB) |
| `RESET` | `0xFF` | 芯片复位 |
### 4.2 Feature 寄存器
| 地址 | 名称 | 说明 |
|------|------|------|
| `0xA0` | Protect | 块保护 (写入 `0x00` 解除全部保护) |
| `0xB0` | Feature | ECC 使能 (bit4=1 启用) |
| `0xC0` | Status | 状态标志 (OIP/WEL/E_FAIL/P_FAIL/ECC) |
| `0xD0` | Driver | 驱动强度 |
### 4.3 初始化序列
```
1. RESET (FFh)
└─ 等待 5ms
2. READ_ID (9Fh)
└─ 校验 MID=0xC8, DID=0x52
3. BBT 扫描
└─ 读每块第一页 (page 0) spare byte 0列地址 0x800手册 §12.4 初始坏块标记位)
└─ 非 0xFF 即出厂坏块,写入 s_bbt[]
4. SET_FEATURE (B0h=10h) — 使能内部 8bit ECC
5. SET_FEATURE (A0h=00h) — 解除全部块保护
```
### 4.4 读写擦除操作
**页读取**(任何字节偏移均可,驱动自动定位到页):
```
PAGE_READ (13h + 3字节行地址) → 等待 OIP 清零 → READ_FROM_CACHE (0Bh + 2字节列地址 + dummy)
```
**页编程**NAND 只能将 bit 从 1→0 翻转,编程前必须擦除):
```
WRITE_ENABLE → PROGRAM_LOAD (02h + 列地址 + 数据) → PROGRAM_EXEC (10h + 行地址) → 等待完成 → 检查 P_FAIL
```
**块擦除**(最小擦除单位 128KB参数是字节地址而非块编号
```
WRITE_ENABLE → BLOCK_ERASE (D8h + 3字节字节地址) → 等待完成 → 检查 E_FAIL
```
### 4.5 DMA 传输策略2026-07-21 新增)
SPI1 使用 DMA2 实现页数据级别的不阻塞传输,配置如下:
| 通道 | DMA | 流 | 通道 | 方向 | 优先级 | 模式 |
|------|-----|----|------|------|--------|------|
| SPI1_RX | DMA2 | Stream 0 | CH3 | 外设→内存 | LOW | NORMAL |
| SPI1_TX | DMA2 | Stream 3 | CH3 | 内存→外设 | LOW | NORMAL |
**传输策略:**
- **小包轮询(≤ 32 bytes**:命令字、地址、状态寄存器等短数据使用 `HAL_SPI_Transmit/Receive` 轮询模式,避免 DMA 初始化开销
- **页数据 DMA> 32 bytes**`READ_FROM_CACHE``PROGRAM_LOAD` 的页数据段使用 `HAL_SPI_Receive_DMA` / `HAL_SPI_Transmit_DMA`
**HAL 内部路由说明:**
`HAL_SPI_Receive_DMA()` 在 2 线 Master 模式内部调用 `HAL_SPI_TransmitReceive_DMA()`,但由于 SPI 状态设置为 `HAL_SPI_STATE_BUSY_RX`HAL 的完成回调为 `HAL_SPI_RxCpltCallback`(而非 `TxRxCpltCallback`),需确保该回调在 `gd5f2gq5ue.c` 中实现。
**完整回调链条:**
| 操作 | 触发回调 | 实现位置 |
|------|---------|---------|
| `PROGRAM_LOAD` TX DMA | `HAL_SPI_TxCpltCallback` | `gd5f2gq5ue.c` |
| `READ_FROM_CACHE` RX DMA | `HAL_SPI_RxCpltCallback` | `gd5f2gq5ue.c` |
| 任意外设错误 | `HAL_SPI_ErrorCallback` | `ch395f.c`(含 SPI1 分支) |
**性能(顺序 128KB 读写FatFS + FTL**
| 模式 | 写 | 读 |
|------|----|----|
| 轮询(改造前) | 330 KB/s | 547 KB/s |
| DMA改造后 | **831 KB/s** | **1855 KB/s** |
| 42MHz SPI 理论极限 | ~5.25 MB/s受 NAND tPROG ≈ 500μs/页 限制) | ~5.25 MB/s |
DMA 消除了轮询模式下 SPI 状态寄存器查检的逐字节 CPU 开销,读写性能分别提升 **2.5×****3.4×**。进一步优化需考虑 Cache Read 模式(重叠 NAND 内部 tR 延迟)或批量编程(减少 tPROG 次数)。
### 4.6 BBT (Bad Block Table)
- 初始化时扫描全部 2048 块第一页page 0的 spare byte 0列地址 0x800
- `s_bbt[256]` 位图数组1 bit 标识 1 个块 (0=好, 1=坏)
- `gd5f2gq5ue_is_block_bad(block)` — 查询坏块状态
- `gd5f2gq5ue_mark_block_bad(block)` — 标记坏块 (FTL 层在擦除/编程失败时调用)
- `gd5f2gq5ue_bbt_clear()` — 清空 BBT慎用仅在重建时使用
- `gd5f_bbt_scan()` — 在 `gd5f2gq5ue_init()` 阶段、ECC 使能前扫描构建 BBT运行时不再提供公开重扫接口见 GD5F2GQ5UE_Trap_Records.md
- `gd5f2gq5ue_print_bbt()` — 打印 BBT 摘要(调试用)
> **注意**`gd5f_private_bbt_scan()` 选择在 ECC 使能前扫描。按手册 §12.6ECC 使能后整段 spare 仍可读取(仅 0x840~0x87F 的 ECC 校验区禁止编程),因此读取 0x800spare byte 0即便 ECC 开启也安全;但扫描放在 ECC 禁用阶段是更稳妥的默认做法。运行时坏块只来源于真实擦除/编程失败,不要主动重新扫描。
### 4.7 页内寻址模型(行地址 / 列地址)
NAND 访问是**二维寻址**,两个地址含义不同:
| 维度 | 含义 | 由谁发出 | 决定什么 |
|------|------|----------|----------|
| 行地址(页地址 / page address | 第几页 | `gd5f_page_read(page)` | 把哪一页从存储阵列搬进芯片内部 cache 寄存器 |
| 列地址column address | 该页内第几个字节 | `gd5f_read_from_cache(column, ...)`(发送 `0Bh` + 2 字节列地址) | 从 cache 里偏移 `column` 处开始吐数据 |
**一页的内部布局**(手册 §12.6 Table 12-8一页 = 2KB 主数据 + 64B 备用区(spare) = 2112 字节,页内连续编址为 column 0 ~ 2111
```
page (2112B) = main(2048B) + spare(64B)
column: 0 2047 2048 2111
spare[0] / 坏块标记位 (0x800)
```
- `column 0 ~ 2047` → 主数据区main data
- `column 2048 ~ 2111` → 备用区spare其中 **20480x800就是 spare 的第 0 字节**
**为什么坏块标记的列地址是 2048**main 区占据了前 2048 字节spare 区从 2048 起算,故 spare byte 0 落在列地址 20480x800。驱动把 `GD5F_PAGE_SIZE`=2048作为 `column` 传给 `gd5f_read_from_cache()``gd5f2gq5ue.c:307-308` 将其拼成 2 字节列地址),芯片即从 spare[0] 开始回数据——这正是出厂坏块标记位(手册 §12.4 / Table 12-6First spare area location = Byte 2048
> 一句话:**page 选页column 选页内字节偏移main 占前 2048 字节spare 从 0x800 起算。**
### 4.8 BBT 持久化存储2026-08-27 新增)
**需求背景**:出厂坏块标记写在每块首页 `spare[0]`page 0但 NAND 的**块擦除会同时清空主区与 spare 区**,因此运行时坏块(擦除/编程失败产生)无法回写到原厂标记位——否则该块一旦被擦除,坏块标记即丢失。必须把运行期坏块信息存到独立的、不参与擦写的**保留区域**,并在每次上电时加载回来。
**保留块池BBT Pool**
- 从 NAND 顶部向下挑选 `GD5F_BBT_POOL_COUNT`=4个**出厂好块**作为 BBT 保留池,不交给 dhara 管理。
- `gd5f_private_bbt_locate_pool()` 在 init 阶段从 `GD5F_TOTAL_BLOCKS-1` 向下收集前 4 个 `s_bbt[b]==0`(出厂好)的块号,存入 `s_bbt_pool_blocks[]`,并把 `s_usable_blocks` 设为其中**最低的块号**。
- dhara 看到的可用块数 = `gd5f_get_usable_blocks()` = `s_usable_blocks`,即保留池以下的块;整个保留池(顶部 4 块)对文件系统不可见。文件系统可用块数由 2048 降为约 2044若顶部块含出厂坏块则略少
**闪存上的 BBT 记录格式**(每个保留块 page 0
```
偏移 长度 字段
0 4 magic = "GBBT"
4 4 version (uint32 LE, 单调递增)
8 4 len (uint32 LE, = GD5F_BBT_SIZE = 256)
12 4 crc32 (uint32, 对下方 bitmap 计算, 多项式 0xEDB88320)
16 256 bitmap (GD5F_BBT_SIZE 字节, 1 bit / 块, 与 RAM s_bbt 同布局)
────────────────────────────────────
合计 272 字节 (GD5F_BBT_HDR_SIZE + GD5F_BBT_SIZE)
```
**写入(掉电安全,轮转)— `gd5f_bbt_save()`**
- 版本号自增:`ver = s_bbt_version + 1`
- 轮转写入下一保留槽 `s_bbt_write_slot`(环形指针,遇损坏槽 `s_bbt_slot_dead` 跳过)
-`BLOCK_ERASE` 目标保留块,再 `PROGRAM_LOAD(0)` 写入整页header+bitmap最后 `PROGRAM_EXEC`
- 若某保留块自身擦除/编程失败,置 `s_bbt_slot_dead` 对应位并跳到下一槽
- 每次 `gd5f2gq5ue_mark_block_bad()` 标记新坏块后都会调用 `gd5f_bbt_save()`;写入极罕见(仅坏块事件触发),磨损可忽略
**加载 — `gd5f_bbt_load()` → `gd5f_private_bbt_find_best()`**
- 遍历全部保留块,校验 `magic` / `len` / `crc32`,丢弃损坏副本
- 选出**版本号最高**的有效副本,将其 bitmap `OR` 进 RAM `s_bbt`RAM 此时已含出厂坏块,`gd5f_bbt_scan` 先于本步执行)
- 记录 `s_bbt_version` 与下一轮转槽 `s_bbt_write_slot`
- 掉电保护:每次写入都是"擦除 + 整页编程"的完整新副本;若写入中途掉电,旧的高版本有效副本仍在,上电时按最高版本选取,不会读到半成品
**上电初始化顺序(与 §4.3 对应补充)**
```
1. RESET → READ_ID → 校验 MID/DID
2. gd5f_bbt_scan() // 出厂坏块扫描 (ECC 关闭), 填充 s_bbt
3. SET_FEATURE(0xB0,0x10) // 使能内部 8bit ECC
4. SET_FEATURE(保护,0x00) // 解除块保护
5. gd5f_private_bbt_locate_pool() // 选保留池, 设定 s_usable_blocks
6. gd5f_bbt_load() // 叠加持久化的运行期坏块
```
**调试 / 测试 API**
| 函数 | 说明 |
|------|------|
| `gd5f_bbt_dump_flash(p_bbt, p_version)` | 读取闪存中当前生效的 BBT 位图与版本(验证持久化用) |
| `gd5f_bbt_wipe_pool()` | 擦除保留块池(撤销持久化,回到仅出厂扫描态;出厂重置/测试用) |
| `gd5f_bbt_reload()` | 从保留池重新加载持久化 BBT 到 RAM |
**关键常量**
| 宏 / 变量 | 位置 | 值 | 说明 |
|-----------|------|----|------|
| `GD5F_BBT_POOL_COUNT` | `gd5f2gq5ue.h` | 4 | 保留块数量 |
| `GD5F_BBT_SIZE` | `gd5f2gq5ue.c` | 256 (`=TOTAL_BLOCKS/8`) | 位图字节数(覆盖 2048 块,每块 1 bit |
| `GD5F_BBT_HDR_SIZE` | `gd5f2gq5ue.c` | 16 | 头部长度magic+ver+len+crc |
| `s_bbt_magic` | `gd5f2gq5ue.c` | `"GBBT"` | BBT 存储魔数 |
---
## 5. dhara FTL (Flash Translation Layer)
### 5.1 概述
FTL 是 NAND Flash 上方最重要的组件,功能包括:
| 功能 | 说明 |
|------|------|
| **地址映射** | 将 FatFS 的逻辑扇区号 (LBA) 映射到 NAND 物理页 |
| **磨损均衡** | 均匀分布擦除次数,延长芯片寿命 |
| **垃圾回收** | 回收无效页面空间 (GC) |
| **坏块管理** | 擦除/编程失败时自动跳过并标记坏块 |
| **ECC 处理** | 检测和上报硬件 ECC 不可纠正错误 |
### 5.2 数据结构
```c
struct dhara_nand {
int log2_page_size; // 页大小对数 (2KB → 11)
int log2_ppb; // 每块页数对数 (64 → 6)
int num_blocks; // 管理的物理块数 (1024)
};
struct dhara_map {
// 内部状态GC 队列、journal、映射表等
};
```
### 5.3 NAND HAL (nand_ftl.c)
dhara 通过 7 个回调函数访问物理 NAND
| 回调 | 功能 | 对应底层函数 |
|------|------|-------------|
| `dhara_nand_is_bad` | 查询坏块 | `gd5f2gq5ue_is_block_bad(block + 1024)` |
| `dhara_nand_mark_bad` | 标记坏块 | `gd5f2gq5ue_mark_block_bad(block + 1024)` |
| `dhara_nand_erase` | 擦除块 | `nand_block_erase(block + 1024)` |
| `dhara_nand_prog` | 写页 | `nand_program_load + nand_program_exec` |
| `dhara_nand_read` | 读页 | `nand_page_read_to_cache + nand_read_from_cache` |
| `dhara_nand_is_free` | 检查页空闲 | 读前 64 字节判断全为 0xFF |
| `dhara_nand_copy` | 页拷贝 (GC 用) | read + prog 组合 |
所有回调自动将 dhara 逻辑块/页加 `FTL_START_BLOCK` 偏移转换为物理地址。
### 5.4 初始化流程 (disk_initialize)
```
1. 设置 nand 参数 (log2_page_size=11, log2_ppb=6, num_blocks=1024)
2. dhara_map_init(&s_map, &s_nand, s_page_buf, 4)
├─ s_page_buf: dhara 内部使用的 2KB 工作缓冲区
└─ 4: journal 页面数量 (影响 GC 效率, 增大可减少写入放大)
3. dhara_map_resume(&s_map)
├─ 成功: 加载已有映射表
└─ 失败: dhara_map_clear 创建空映射表
4. 页面缓存初始化 (s_cache_buf, s_cached_lpn, s_cache_dirty)
```
### 5.5 页面缓存策略
FTL 之上还有一个 **单页写回缓存 (write-back cache)**
- **读命中**: 直接返回 s_cache_buf 数据
- **读未命中**: 刷出脏页 → 读新页到缓存
- **写**: 写入缓存 → 标记脏
- **全页写入**: 直接刷出 (跳过缓存)
- **同步 (CTRL_SYNC)**: 刷出脏页 + dhara_map_sync
---
## 6. FatFS 集成
### 6.1 配置 (ffconf.h)
```c
#define FF_FS_READONLY 0 // 读写模式
#define FF_USE_MKFS 1 // 启用格式化
#define FF_MIN_SS 512 // 最小扇区大小
#define FF_MAX_SS 512 // 最大扇区大小
#define FF_VOLUMES 1 // 单卷
#define FF_FS_TINY 0 // 非 tiny 模式
#define FF_FS_NORTC 1 // 无 RTC (固定时间戳)
```
### 6.2 disk I/O 接口
| 函数 | 功能 | 关键实现 |
|------|------|----------|
| `disk_initialize` | 初始化 FTL | 见 7.4 节 |
| `disk_status` | 查询状态 | 返回初始化状态 |
| `disk_read` | 读扇区 | 通过 FTL 映射读物理页 |
| `disk_write` | 写扇区 | 通过 FTL 映射写 (缓存优化) |
| `disk_ioctl` | 控制命令 | GET_SECTOR_COUNT/SIZE/BLOCK_SIZE + CTRL_SYNC |
### 6.3 容量计算
```
FTL 管理块数 = 2048 (Block 0~2047)
每块页数 = 64
每页扇区数 (512B) = 4
总扇区数 = 2048 × 64 × 4 = 524288
总容量 = 524288 × 512 = 256MB (原始容量)
FTL 开销后 ≈ 186 MB (随 GC 和 journal 使用量波动)
```
---
## 7. 完整数据流
### 7.1 写文件流程
```
f_write("hello.txt")
└─ FatFS: 计算 LBA, 写扇区
└─ disk_write(0, data, sector=100, count=2)
├─ 计算 LPN = sector / 4 = 25
├─ 缓存未命中 → ftl_flush_cache() → ftl_read_page(25)
├─ 拷贝数据到 s_cache_buf → 标记脏
└─ 全页面写入 → ftl_flush_cache()
└─ dhara_map_write(&s_map, 25, s_cache_buf)
├─ 查找页映射 (或分配新页)
├─ dhara_nand_prog(pg, data) → 物理写
└─ 更新映射表
f_close → disk_ioctl(CTRL_SYNC)
└─ ftl_flush_cache() → dhara_map_sync()
└─ 写 journal 到 NAND (持久化映射表)
```
### 7.2 f_mkfs 格式化流程
```
f_mkfs("", &opts, work, size)
├─ disk_initialize(0) → FTL 初始化
├─ disk_write: 写入引导扇区 (MBR/PBR)
├─ disk_write: 写入 FAT 表
├─ disk_write: 创建根目录
└─ disk_ioctl(CTRL_SYNC) → FTL sync
```
---
## 8. API 参考
### 8.1 底层 NAND 驱动 (gd5f2gq5ue.h)
| 函数 | 说明 |
|------|------|
| `gd5f2gq5ue_init()` | 初始化 NAND (BBT 扫描 + ECC 使能 + 块保护解除) |
| `gd5f2gq5ue_read_id(p_mid, p_did)` | 读芯片 ID |
| `gd5f2gq5ue_read(offset, buf, size)` | 读数据 (支持跨页) |
| `gd5f2gq5ue_write(offset, buf, size)` | 写数据 (支持跨页) |
| `gd5f2gq5ue_erase(offset, size)` | 块擦除 (需块对齐) |
| `gd5f2gq5ue_reset()` | 复位芯片 |
| `gd5f2gq5ue_is_block_bad(block)` | 查询坏块 |
| `gd5f2gq5ue_mark_block_bad(block)` | 标记坏块 |
| `gd5f2gq5ue_bbt_clear()` | 清空 BBT |
| `gd5f_bbt_scan()` | init 时扫描构建 BBT内部静态 |
| `gd5f2gq5ue_print_bbt()` | 打印 BBT |
**SPI 原语FTL 和测试共享):**
| 函数 | 说明 |
|------|------|
| `gd5f_wait_busy(timeout_ms)` | 轮询等待 OIP 清零 |
| `gd5f_write_enable()` | 发送 06h 写使能 |
| `gd5f_read_status(p_status)` | 读状态寄存器 (C0h) |
| `gd5f_page_read(page_addr)` | 页读取 (13h) |
| `gd5f_read_from_cache(column, buf, size)` | 读内部缓存 (0Bh) |
| `gd5f_program_load(column, buf, size)` | PROGRAM LOAD (02h) |
| `gd5f_program_exec(page_addr)` | PROGRAM EXEC (10h) |
| `gd5f_block_erase(block_addr)` | 块擦除 (D8h) |
| `gd5f_check_ecc()` | 检查 ECC 状态位 |
### 8.2 FTL/FatFS (via nand_ftl.c / ff.h)
| 函数 | 说明 |
|------|------|
| `f_mount(&fs, "", 1)` | 挂载文件系统 (首次调用自动初始化 FTL) |
| `f_mkfs("", &opts, work, size)` | 格式化 FAT32 |
| `f_open/f_close/f_read/f_write` | 标准文件操作 |
| `f_unlink("test.txt")` | 删除文件 |
| `disk_initialize(0)` | 显式初始化 FTL (通常由 f_mount 自动调用) |
| `nand_ftl_format()` | 直接格式化 FTL (清空映射表, 慎用) |
---
## 9. 使用示例
### 9.1 FatFS 文件操作
```c
#include "ff.h"
static FATFS fs;
static int fatfs_init(void) {
FRESULT res = f_mount(&fs, "", 1);
if (res == FR_NO_FILESYSTEM) {
/* 首次使用需格式化 */
MKFS_PARM opts = {FM_FAT32, 0, 0, 0, 0};
uint8_t work[512];
res = f_mkfs("", &opts, work, sizeof(work));
if (res != FR_OK) return -1;
res = f_mount(&fs, "", 1);
}
return (res == FR_OK) ? 0 : -1;
}
static void fatfs_write_read(void) {
FIL fil;
UINT bw, br;
const char *msg = "Hello Storage!";
char buf[32];
/* 写文件 */
f_open(&fil, "data.txt", FA_CREATE_ALWAYS | FA_WRITE);
f_write(&fil, msg, strlen(msg), &bw);
f_close(&fil);
/* 读文件 */
f_open(&fil, "data.txt", FA_READ);
f_read(&fil, buf, sizeof(buf), &br);
buf[br] = '\0';
f_close(&fil);
}
```
### 9.2 FTL 格式化
```c
#include "nand_ftl.h"
/* 注意: 此操作将清空 FTL 分区全部数据, 慎用 */
if (nand_ftl_format() == 0) {
DBG_INFO("FTL formatted");
}
```
---
## 10. 注意事项
1. **擦除对齐**`gd5f2gq5ue_erase()` 的 offset 和 size 必须严格按 GD5F_BLOCK_SIZE (128KB) 对齐和整数倍。
2. **写前擦除** — NAND 不能原地覆写FTL 内部自动管理擦除,但直接调用 `gd5f2gq5ue_write()` 前必须确保目标块已擦除。
3. **FTL 首个扇区**`dhara_map_init` 的第 5 个参数 (journal 页数) 影响 GC 效率,当前为 4增大可减少写入放大但占用更多内存。
4. **坏块传播** — FTL 在擦除/编程失败后自动调用 `dhara_nand_mark_bad``gd5f2gq5ue_mark_block_bad`BBT 在 RAM 中更新并立即通过 `gd5f_bbt_save()` 持久化到保留块池;下次复位后 `gd5f_bbt_scan()` 重建出厂坏块,`gd5f_bbt_load()` 再叠加保留池中保存的运行期坏块,因此运行期坏块在掉电后依然有效(详见 §4.8)。
5. **功耗** — 擦除操作最大耗时约 5ms (驱动超时设为 5s),页编程约 600ms (超时 1s),读写操作快。在低功耗场景需注意合理安排操作时序。
6. **缓存一致性** — 单页缓存 (s_cache_buf) 仅对 FatFS 层可见,多任务读写同一文件需在应用层同步。

View File

@@ -0,0 +1,244 @@
# 嵌入式测试开发标准流程(基于 STM32F4 + CH395F 经验)
> 适用范围STM32 裸机/FreeRTOS 项目Keil MDK-ARM v5 (ARMCC),串口日志输出测试结果。
---
## 1. 测试规划(文档先行)
`docs/` 下创建测试规范文档,定义以下结构:
### 1.1 阶段概览表
| 阶段 ID | 类型 | 耗时 | 入口 | 出口 |
|---------|------|------|------|------|
| TM-PHY-01 | 独立运行 | ~1s | 宏启用 | 串口汇总 |
### 1.2 每个测试用例
| 字段 | 说明 |
|------|------|
| **ID** | `TC-NET-NNN`,唯一编号 |
| **优先级** | P0核心/ P1重要 |
| **类型** | 功能测试 / 边界测试 / 负向测试 / 压力测试 / 恢复测试 |
| **标题** | 一句话描述 |
| **前置条件** | 硬件状态、PC 端命令、依赖的其他用例 |
| **测试步骤** | 编号操作序列 |
| **成功标准** | 精确到"返回值 == 0x00"、"延迟 >= 60000ms" |
| **失败标准** | 每种失败对应的现象 |
| **覆盖原则** | 对应哪个设计原则 |
关键原则:**先写判定标准,再写测试代码**,避免"测了但不知道算不算过"。
---
## 2. 测试框架搭建
### 2.1 目录结构
```
test/
ch395f_test.h # 宏定义 + TEST_CHECK/TEST_REPORT + test_stats_t
ch395f_test_task.h # 仅导出 StartCh395fTestTask
ch395f_test_task.c # 全 Phase 实现
```
### 2.2 模板文件
#### `ch395f_test.h` — 宏+类型
```c
// 阶段使能开关(取消注释即启用)
#define ENABLE_PHASE1_TESTS
// 测试统计
typedef struct {
uint16_t total;
uint16_t passed;
uint16_t failed;
} test_stats_t;
extern test_stats_t g_test_stats;
#define TEST_CHECK(cond, fmt, ...) do { \
g_test_stats.total++; \
if (cond) { \
g_test_stats.passed++; \
DBG_INFO("[PASS] " fmt, ##__VA_ARGS__); \
} else { \
g_test_stats.failed++; \
DBG_ERROR("[FAIL] " fmt, ##__VA_ARGS__); \
} \
} while (0)
#define TEST_REPORT(name) do { \
DBG_INFO("=== %s: %d/%d PASSED (failed=%d) ===", \
name, g_test_stats.passed, g_test_stats.total, g_test_stats.failed); \
} while (0)
```
#### `ch395f_test_task.c` — 每个 Phase 的模板
```c
#ifdef ENABLE_PHASEX_TESTS
/*
* Phase X — 功能说明
*
* 测试目的:(概括)
* 前置条件:(硬件/软件/PC 端)
* 通过:(整体判定条件)
* 失败:(整体判定条件)
*/
static void phaseX_run(void) {
DBG_INFO("=== CH395F Phase X Tests ===");
/* ---- TC-NET-NNN: 用例标题 ---- */
{
// 原理说明
// 通过:精确条件
// 失败:精确条件
ret = some_api();
DBG_INFO(" expect: ...");
DBG_INFO(" actual: ...");
TEST_CHECK(ret == EXPECTED, "description");
}
TEST_REPORT("Phase X");
}
#endif
```
---
## 3. 编码规范
### 3.1 DBG_INFO 三行输出
每个判定点输出三行,一眼看出"期望什么、拿了什么、过没过"
```
[P1-01] ch395f_check_exist()
expect: 0x00 (~0x57 = 0xA8)
actual: 0x00
[PASS] P1-01 check_exist = 0x00 (expect 0x00)
```
### 3.2 硬编码值加注释
```c
// 好:
ch395f_status_t ret = ch395f_check_exist();
// SPI 写入 0x06 (CMD_CHECK_EXIST) + 0x57 (测试字节)
// 成功:回复 == ~0x57 == 0xA8CH395F_ERR_SUCCESS = 0x00
// 失败:回复 != 0xA8CH395F_STATUS_NOT_DETECTED = 0xFF
// 不好:
ch395f_status_t ret = ch395f_check_exist();
TEST_CHECK(ret == 0, "check_exist OK");
```
### 3.3 无中文字符串
ARMCC v5 不识别 UTF-8 多字节字符。`DBG_INFO`/`DBG_ERROR` 中只写 ASCII。注释可以写中文。
### 3.4 栈安全
大缓冲区(如 64KB`static` 全局,不放任务栈:
```c
#define TEST_BUF_SIZE 65536
static uint8_t s_rx_buf[TEST_BUF_SIZE];
```
### 3.5 条件编译消除未使用变量警告
```c
static uint8_t s_rx_buf[TEST_BUF_SIZE];
#if defined(ENABLE_PHASE5_TESTS) || defined(ENABLE_PHASE8_TESTS)
static uint8_t s_tx_buf[TEST_BUF_SIZE];
#endif
```
---
## 4. 统一入口
### 4.1 所有测试在 FreeRTOS 任务中串行执行
```
ch395fTestTask:
1. 等待 g_net_readynetTask 初始化完成)
2. 阻塞执行 Phase 1/2/3/6/9内部循环无需外部连接
3. 创建 TCP 监听 Socket端口 8080单连接模式
4. 事件驱动循环 Phase 4/5/7/8/10等待 PC 连接)
```
### 4.2 不要在 main.c 中直接调用测试
main.c 只做硬件初始化和启动 OS不包含任何测试逻辑。
### 4.3 测试文件的手动注册
新测试文件需:
1. 添加到 `test/` 目录
2. 修改 `MDK-ARM/STM32F407-Demo.uvprojx` 添加文件引用和 IncludePath
---
## 5. 文档与代码同步
- 文档中的入口函数名、宏名、流程描述必须与代码一致
- 代码变更后立即更新文档,否则文档两天内就会失效
- 测试用例的通过/失败标准在**文档和代码注释中都写清楚**
- 文档使用标准 Markdown 表格,避免复杂嵌套导致渲染异常
---
## 6. 编译验证
### 6.1 底线
**`0 Error(s), 0 Warning(s)`**,不可妥协。
### 6.2 常见警告处理
| 警告 | 原因 | 解决 |
|------|------|------|
| `#177-D: variable was declared but never referenced` | 条件编译导致 | 加 `#ifdef` 包裹变量声明 |
| `#870-D: invalid multibyte character sequence` | DBG_INFO 中有中文 | 改为纯 ASCII |
| 隐式类型转换 | 参数类型不匹配 | 加显式 `(uint8_t)` 等 cast |
### 6.3 构建命令
```bat
MDK-ARM\build.bat
:: 或项目根目录执行 @build
```
退出码0 = 成功 | 1 = 有警告(不通过)| 2+ = 错误
---
## 7. 迭代节奏
```
文档(规划 + 判定标准)
→ 代码(添加 Phase 实现)
→ @build0 Error(s), 0 Warning(s)
→ 烧录验证(串口观察 PASS/FAIL
→ 根据实际结果修正判定标准
→ 更新文档(保持同步)
→ 下一阶段
```
---
## 附录:本项目的测试文件清单
| 文件 | 作用 |
|------|------|
| `test/ch395f_test.h` | 测试宏、类型定义、Phase 使能开关 |
| `test/ch395f_test_task.h` | 导出 `StartCh395fTestTask` |
| `test/ch395f_test_task.c` | 全 10 个 Phase 实现 |
| `docs/CH395F_Test_Guide.md` | 测试规范、用例表格、通过/失败标准 |
| `docs/CH395F_Trap_Records.md` | 已知硬件/软件陷阱及修复 |

View File

@@ -0,0 +1,221 @@
# 测试文档模板规范
本文档定义本项目中测试文档的统一模板和编写规范确保各模块CH395F、GD5F2GQ5UE、TPAFE5160 等)的测试文档格式一致、可追溯。
---
## 1. 文档结构
每份测试文档包含以下章节:
```
# [模块名] 测试规范
## 1. 概述
- 测试范围
- 设计原则覆盖表DP-xx → 描述 → 验证用例)
- 测试环境(拓扑、工具链、硬件版本)
- 启用测试的方法
## 2~N. 阶段 x[阶段名]
- 阶段元信息表:阶段 ID、类型、耗时、入口、出口
- TC-NET-xxx: 测试用例(完整表格)
## N+1. 脚本参考(如有 PC 端工具)
## N+2. 已知陷阱与故障模式(引用 Traps 文档)
## 附录 A通过/失败汇总矩阵
## 附录 B测试覆盖 vs 设计原则
```
---
## 2. 阶段元信息表
每个阶段以表格开头,描述阶段的整体属性:
| 字段名 | 填写说明 |
|--------|----------|
| **阶段 ID** | `TM-模块缩写-两位数序号`,如 TM-PHY-01、TM-NAND-01 |
| **类型** | 独立运行 / 需 PC 配合 / 需物理操作 / 在 xxTask 中运行 |
| **耗时** | 预估执行时间 |
| **入口** | 代码入口函数或宏定义 |
| **出口** | 阶段结束标志(如串口输出内容) |
---
## 3. 测试用例表格模板
每个测试用例使用统一表格格式:
```
### TC-模块缩写-序号: 用例标题
| 字段 | 值 |
|------|-----|
| **ID** | TC-XXX-NNN全局唯一 |
| **优先级** | P0 / P1 |
| **类型** | 见下文 3.1 |
| **标题** | 一句话描述测试什么 |
| **前置条件** | 测试执行前必须满足的条件(编号列表) |
| **测试步骤** | 操作步骤(编号列表,动作具体到 API 或操作) |
| **预期结果** | 系统应表现的行为 |
| **通过标准** | 可量化的判定条件 |
| **覆盖原则** | DP-xx可选关联设计原则 |
```
### 3.1 类型枚举
| 类型 | 说明 | 适用场景 |
|------|------|----------|
| 功能测试 | 验证某项功能是否符合预期 | 正常的收发、初始化、配置等 |
| 负向测试 | 验证系统对非法/异常输入的处理 | 超时、断开、无效参数等 |
| 边界测试 | 验证系统在边界条件的表现 | 最大包长、最小超时、满队列等 |
| 压力测试 | 验证系统在高负载下的稳定性 | 多客户端、高频请求、长时运行等 |
| 恢复测试 | 验证系统从故障中恢复的能力 | 断线重连、PHY 重连、复位恢复等 |
| 合规测试 | 验证实现是否符合特定约束 | 中断处理位置、等待顺序、字节序等 |
| 稳定性测试 | 验证系统长时间运行的可靠性 | 长时间压力、反复循环等 |
### 3.2 优先级定义
| 优先级 | 定义 |
|--------|------|
| **P0** | 核心功能,必须通过。阻塞后续测试或影响系统基本可用性 |
| **P1** | 重要功能,建议通过。失败表明潜在缺陷但不阻塞基本功能 |
---
## 4. 命名规则
### 4.1 阶段 ID
```
TM-{MOD}-{NN}
```
- `TM` — Test Module
- `{MOD}` — 模块缩写(大写)
- `PHY` — CH395F 网络
- `NAND` — GD5F2GQ5UE NAND Flash
- `ADC` — TPAFE5160 ADC
- `RTC` — SD2506 RTC
- `RS485` — RS-485 通信
- `{NN}` — 两位序号,从 01 开始
示例:`TM-PHY-01`, `TM-NAND-03`
### 4.2 测试用例 ID
```
TC-{MOD}-{NNN}
```
- `TC` — Test Case
- `{MOD}` — 模块缩写(同上)
- `{NNN}` — 三位序号,从 001 开始
示例:`TC-PHY-101`, `TC-NAND-201`
### 4.3 设计原则 ID
```
DP-{NN}
```
- `DP` — Design Principle
- `{NN}` — 两位序号,从 01 开始
示例:`DP-01`, `DP-02`
---
## 5. 编写规范
### 5.1 表格格式
- 使用 GFM (GitHub Flavored Markdown) 表格
- 首列为字段名,加粗(`**字段**`
- 第二列为值,左对齐
- 多行内容使用 `<br>` 换行(保持表格可读性)
### 5.2 测试步骤与前置条件
- 使用有序列表(`1. 2. 3.`
- 每个步骤是一个完整的可执行动作
- 包含具体 API 名或操作名(如 `net_recv()``ch395f_open_socket()`
- 前置条件写明硬件状态、PC 端命令、代码配置等
### 5.3 语言
- 中文书写(技术标识保留英文)
- 保持客观、精确、可验证
- 避免模糊表述(如 "应该能正常工作" → "回显内容与发送完全一致"
### 5.4 通过标准
- 必须可量化验证
- 好的示例:`70/70 成功率100%``串口显示 "PHY_CHANGE: 0x01"``10/10 回显匹配`
- 差的示例:`功能正常``系统稳定`
### 5.5 引用
- 引用函数、宏、文件名使用反引号(`` `net_poll()` ``
- 引用其他文档使用相对路径:`docs/CH395F_Trap_Records.md`
---
## 6. 快速参考
### 6.1 新模块测试文档模板
```markdown
# [模块名] 测试规范
## 1. 概述
### 设计原则覆盖
| 原则 | 描述 | 验证用例 |
|------|------|----------|
| DP-01 | ... | TC-XXX-xxx |
### 测试环境
```
[硬件拓扑]
```
## 2. 阶段 1[阶段名]
| 阶段 ID | TM-XXX-01 |
|---------|------------|
| **类型** | ... |
| **耗时** | ... |
| **入口** | ... |
### TC-XXX-001: 用例标题
| 字段 | 值 |
|------|-----|
| **ID** | TC-XXX-001 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | ... |
| **前置条件** | 1. ...<br>2. ... |
| **测试步骤** | 1. ... |
| **预期结果** | ... |
| **通过标准** | ... |
```
### 6.2 附录模板
```markdown
## 附录 A通过/失败汇总矩阵
| 阶段 | TC ID | 优先级 | 类型 | 状态 |
|------|-------|--------|------|------|
## 附录 B测试覆盖 vs 设计原则
| 原则 | 覆盖用例 |
|------|----------|
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,168 @@
# FTP 功能测试指导
> 适用对象STM32F4-Base 上基于 `lftpd` 的 FTP Server 功能验证。
> 配套源码:`Drivers/BSP/NET/lftpd/`、`App/task/net_task.c`、`Src/freertos.c`StartFtpTask
> 关联文档:`CH395F_Test_Guide.md`(网络测试总说明·阶段 8、`NET_Socket_FTP_Support_Plan.md`PASV 改造方案)、`CH395F_Trap_Records.md`。
---
## 1. 概述
- FTP Server`lftpd`)在**默认固件构建中即常驻运行**`Src/freertos.c``StartFtpTask`(默认 `__weak` 实现)直接调用 `lftpd_start("/", 21, &ftp)`,与测试套件选择无关。
- 监听端口 **21**,根目录 `/`(即 NAND 上的 FatFS 卷),**匿名登录**`USER anonymous` / `PASS` 任意)。
- 仅支持 **1 个并发客户端**(控制通道 Socket0 + PASV 数据通道动态 Socket2backlog=1
- 测试分两类:
- **PC 端手测(推荐,覆盖真实 FTP 行为)**:用 `test/` 下 Python 脚本或 GUI 客户端。
- **固件自动化TEST_SUITE_CH395F 阶段 8**TC-NET-801/802/803 文件传输。
---
## 2. 测试环境
### 2.1 硬件与网络
- CH395F 网口经以太网连接到 PC 或同一台交换机。
- PC 网卡与固件 IP **同网段**。固件默认静态 IP **192.168.1.100**(掩码 255.255.255.0PC 可设 192.168.1.x如 192.168.1.77)。
- 启动时 `netTask` 完成 `net_init()` + PHY 稳定(约 >2s`ftpTask` 另有 `osDelay(7000)` 等待后才 `listen`;若 `open_socket` 失败会 `vTaskDelay(3000)` 重试(最多 3 次)。**客户端应在上电约 7s 后、串口出现 `[FTP] waiting for connection...` 再连接。**
### 2.2 软件
- Python 3.x脚本位于 `test/`)。
- 可选 GUI 客户端FileZilla / MobaXterm / lftp。
---
## 3. 编译与运行(固件侧)
- **默认构建即可提供 FTP Server**,无需切换测试套件。
- 任何改动后必须执行 `@build`(即 `MDK-ARM/build.bat`),确认 **0 Error(s) 0 Warning(s)** 再烧录(遵循 AGENTS.md 铁律)。
- 若需跑**自动化 FTP 文件传输用例**:在 `test/test_config.h` 选择 `TEST_SUITE_CH395F`,并在 `test/ch395f_test.h` 打开 `ENABLE_PHASE8_TESTS`(文件传输阶段),重新编译烧录。
- 烧录后打开串口115200bps观察
- `net_init` 完成、PHY link up
- `[FTP] waiting for connection...`(约 3 次重试足够 PHY 稳定);
- 客户端连接后进入 FTP 会话(控制通道 220 欢迎)。
---
## 4. PC 端脚本手测(主要手段)
所有脚本支持 `--host`(默认 `192.168.1.100`)、`--port`(默认 21`test/` 目录运行。
### 4.1 冒烟测试:`ftp_test.py`
- 用途:验证连接、登录、`PWD``PASV``LIST` 基本链路。
- 命令:`python ./test/ftp_test.py --host 192.168.1.100`
- 预期:
- 连接成功并读到 `220` 欢迎;
- `USER anonymous` / `PASS` 返回 `230`
- `PASV` 返回 `227` 并解析出数据端口(或回退 `EPSV`
- `LIST` 返回目录列表;
- `QUIT` 正常关闭。
- 判据:全程无 `[NO RESPONSE]` / `CONNECT FAILED`,且 LIST 返回 `226` 即 PASS。**空卷下 LIST 数据通道为 0 字节属正常**(新格式化卷无文件),不必要求有目录列表输出。
### 4.2 上传 / 下载 / 比对:`ftp_up_dl_test.py`
- 用途:验证 `STOR`/`RETR`/`DELE` 与数据一致性(`TYPE I` 二进制 + `PASV`)。
- 命令:`python ./test/ftp_up_dl_test.py --host 192.168.1.100`
- 流程:登录 → `TYPE I``STOR up_test.dat``LIST` 校验存在 → `RETR up_test.dat` → 逐字节比对 → `DELE` 清理 → `QUIT`
- 预期:`*** MATCH! Upload/Download success. ***`,且 LIST 中出现 `up_test.dat`
- 判据:上传/下载字节数相等且内容逐字节一致。
### 4.3 速度与大数据量:`ftp_speed_test.py`
- 用途:测量上传/下载吞吐,作回归基线,同时验证大文件不丢字节。
- 命令:`python ./test/ftp_speed_test.py --host 192.168.1.100 --size 512`(默认 512KB`--size` 可调)。
- 预期:上传/下载均返回 `226`,回读 `MATCH!`,打印 KB/s。
- 注意:脚本使用 8.3 文件名 `SPEEDTST.DAT`**FTP 仅支持 8.3 短文件名**,见 §8.3以回读比对MATCH为最终判据中间打印的 Sent 字段仅供参考。
- 判据size 一致 + MATCH + 速度稳定(受 SPI2 分频 8 限制,通常约 300~450 KB/s参见 `CH395F_Test_Guide.md` 阶段 5 吞吐基线)。
### 4.4 抓包辅助:`ftp_capture.py`
- 用途:连接/传输过程中抓包,分析 PASV 端口、命令/数据通道交互,用于疑难排查。
- 用法参见脚本内 `--help` / 注释。
---
## 5. GUI 客户端手测(可选)
- **FileZilla**:主机 `192.168.1.100`,端口 21用户 `anonymous`,密码任意;可拖拽上传/下载、浏览目录。
- 要点:使用二进制模式传输;文件名遵守 8.3**一次只连一个客户端**。
---
## 6. 自动化测试TEST_SUITE_CH395F 阶段 8
- 入口:`ENABLE_PHASE8_TESTS` → 文件传输阶段100KB 文件)。
- 用例:
- `TC-NET-801`100KB 文件上发MCU→PC
- `TC-NET-802`100KB 文件下发PC→MCU
- `TC-NET-803`round-trip可选
- 详见 `docs/CH395F_Test_Guide.md` 阶段 8。
---
## 7. 测试清单Checklist
- [ ] 默认构建烧录,串口见 `[FTP] waiting for connection`
- [ ] `ftp_test.py` 冒烟 PASS连接 / 登录 / PASV / LIST
- [ ] `ftp_up_dl_test.py` 上传下载比对 `MATCH`
- [ ] `ftp_speed_test.py` 大文件 `MATCH` + 记录速度基线。
- [ ] 可选GUI 客户端手动上传/下载成功。
- [ ] (可选)阶段 8 自动化 `TC-NET-801/802/803` PASS。
---
## 8. 常见失败与排查(基于已知陷阱)
### 8.1 连接被拒 / 长时间无欢迎
- 确认 PC 与固件同网段、IP 正确;上电后等待约 7sPHY 稳定 + `ftpTask` 延迟)再连。
- 若反复 `open_socket` 失败:见 **TRAP-09**PHY 未稳定;固件已做 3 次重试 + 7s 延迟,仍失败检查网线/交换机)。
### 8.2 PASV 失败 / 数据通道连不上
- 确认 PC 防火墙未阻断高端口PASV 动态端口)。
- **数据通道必须先 `connect` 再发命令**(如 `LIST`/`STOR`),顺序反了会失败(脚本已遵循)。
- **TRAP-11**`net_accept_locked``ESTABLISHED` 状态导致 PASV 失败——需校验 `ESTABLISHED`
### 8.3 STOR 报 FR_INVALID_NAME (err=6)
- **8.3 文件名限制**`upload_test.dat`11+3超过 8.3 → `FR_INVALID_NAME`。改用短名如 `UPLOAD.DAT` / `SPEEDTST.DAT`。这是 FatFS LFN/8.3 约束,非驱动 bug。
### 8.4 大文件传输中途 RST / 速度抖动
- **TRAP-13** / 设计约束CH395F `WRITE_SEND_BUF` 芯片侧吞吐限制,单帧 >1KB 才稳定TCP 背压会"堵一会儿",属正常流控,不丢数据。
- 接收侧 `recv_len` 首包可能较大、次包可能为 0**必须循环读直到连接关闭**(脚本已处理)。
- 若持续 RST检查 **SPI 分频 = 8**PCB 设计限制)、`net_poll()` 周期10ms是否正常、中断是否统一在 `net_poll()` 中经 `GET_GLOB_INT_STATUS` 处理。
### 8.5 路径问题
- **TRAP-12**FatFS 根路径 `/` 前缀由 `to_fatfs_path()` 统一处理;客户端用绝对路径(如 `/UPLOAD.DAT`)即可。
### 8.6 单客户端限制
- FTP 仅接受 1 个并发客户端;上一个会话未 `QUIT` 前,新连接会被拒。每次测完务必 `QUIT` / 断开。
### 8.7 LIST/STOR/RETR 全部 550卷未挂载 / 无 FAT 文件系统
- 根因FTP 服务的是 NAND 上的 FatFS 卷。固件在 `StartDefaultTask` 启动期执行 `f_mount(&fs, "", 1)`(会触发 `disk_initialize``nand_ftl_init`),使卷可用。
- **首启自动建卷**:若 `f_mount` 返回 `FR_NO_FILESYSTEM`(如坏块重建/恢复测试后卷被清空),固件会自动 `f_mkfs`FM_FAT32、无分区表参数与存储测试一致再挂载。该过程上电会多花数秒格式化串口可见 `[FS] no FAT volume, formatting NAND...``[FS] FATFS mounted`
- 若串口见 `[FS] f_mount failed (fr=...)` 且非首次(已建卷仍失败):检查 `f_mkfs` 是否报错(`[FS] f_mkfs failed`),或 NAND 是否存在物理/ECC 错误;必要时用 `TEST_SUITE_STORAGE` 跑一次确认磁盘健康。
---
## 9. 诊断手段(沿用前期测试经验)
- **串口日志 + `DBG_*`**:在 `lftpd.c` / `net_socket` 关键路径加 `DBG_ERROR/INFO` 打印(注意编码安全铁律——只用 Edit 改中文源文件,禁止用 `Get/Set-Content` 整文件重写)。
- **抓包**`ftp_capture.py` 或 Wireshark 过滤 `tcp.port==21`
- **编译验证**`@build` 必须 0/0 再烧录AGENTS.md 铁律)。
- **套件切换**`test/test_config.h``TEST_SUITE_CH395F` / `TEST_SUITE_GD5F` / `TEST_SUITE_STORAGE`(三选一,互斥)。
---
## 10. 参考文档
- `docs/CH395F_Test_Guide.md` —— 网络测试总说明(含阶段 8 文件传输、FTP 陷阱清单)
- `docs/NET_Socket_FTP_Support_Plan.md` —— FTP / PASV Socket 改造方案
- `docs/CH395F_Trap_Records.md` —— CH395F 已知陷阱
- `docs/BSD_Socket_API_使用指南.md` —— BSD Socket API 用法

View File

@@ -0,0 +1,741 @@
# GD5F2GQ5UE NAND Flash 测试规范
> 本规范参照 `docs/CH395F_Test_Guide.md` 结构编写,覆盖 `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c`
> 全部公开/私有函数,并向上延伸到 FTL`nand_ftl.c`)与 FatFS diskio。
> 详细芯片行为见 `docs/GD5F2GQ5UExxG.md`,已知陷阱见 `docs/GD5F2GQ5UE_Trap_Records.md`。
## 1. 概述
### 设计原则
- **同步 SPI 访问**GD5F 驱动全部为**同步阻塞** SPI 事务CS 拉低 → 命令/地址/数据 → CS 拉高),
由调用任务直接执行,**不经过消息队列**(与 CH395F 的 netTask 串行化模型不同)。
因此测试代码可在任意任务上下文调用,但要注意调用任务会被 SPI 传输阻塞数毫秒(擦除最久)。
- **先擦后写**NAND 只能将 1 写为 0**写操作前目标块必须已擦除**,否则数据不可预期(见边界条件 TC-GD5F-1003
- **ECC 默认开启**`gd5f2gq5ue_init()` 会置 `Feature(B0h).ECC_EN=1`,读/写均走内部 ECC。
- **坏块以 BBT 管理**出厂坏块标记在每块的第一页page 0spare byte 0列地址 0x800见手册 §12.4 / Table 12-6 / Note`init` 阶段扫描构建 RAM 中的 BBT。
### 芯片参数(关键参数)
| 参数 | 值 | 说明 |
|------|-----|------|
| 制造商/设备 ID | `0xC8` / `0x52` | `gd5f2gq5ue_read_id()` 校验依据 |
| 页数据大小 | 2048 B | `GD5F_PAGE_SIZE` |
| Spare 大小 | 64 B | `GD5F_SPARE_SIZE`ECC 开启时有效) |
| 单页总长 | 2112 B | `GD5F_TOTAL_PAGE_SIZE`ECC 开启) |
| 每块页数 | 64 | `GD5F_PAGES_PER_BLOCK` |
| 块大小 | 128 KB | `GD5F_BLOCK_SIZE = 64×2048` |
| 总块数 | 2048 | `GD5F_TOTAL_BLOCKS` |
| 总容量 | 256 MB | `GD5F_TOTAL_SIZE` |
| 最小擦除单位 | 1 块128 KB | 不可按页/字节擦除 |
| DMA 阈值 | 32 B | `GD5F_DMA_THRESHOLD`>32B 走 `HAL_SPI_*_DMA` |
### 寄存器与状态位
| 寄存器 | 地址 | 关键位 |
|--------|------|--------|
| Status | `0xC0` | OIP(b0) 忙标志、WEL(b1) 写使能、E_FAIL(b2)、P_FAIL(b3)、ECCS1/0(b5/b4) ECC 状态 |
| Feature | `0xB0` | ECC_EN(b4)、QE(b0) |
| Protect | `0xA0` | BP2/1/0 块保护init 后写为 `0x00` 解除保护 |
ECC 状态(`ECCS1:ECCS0``00`=无错;`01/10/11`=纠正 1/2/3 bit`10`ECCS1=1,ECCS0=0= 超出纠正能力(>4bit不可纠正
驱动 `gd5f_check_ecc()``(status>>4)&0x03 == 2` 时返回 `GD5F_ECC_ERROR`
### 测试环境
- **硬件**STM32F407ZGTx + GD5F2GQ5UESPI NAND
- **SPI 接口**SPI1CubeMX 初始化),引脚:
- CS=`PE0`SCK=`PB3`MISO=`PB4`MOSI=`PB5`WP=`PB8`HOLD=`PE1`
- **调试输出**USART1115200bps`[NAND]` 标签(`dbg_log.h`
- **擦除/编程耗时**参考驱动超时PAGE_READ 等待 100ms、PROGRAM_EXEC 等待 1000ms、BLOCK_ERASE 等待 5000ms
### 统一测试调度ch395fTestTask
所有测试套件CH395F / 存储 / GD5F统一收归 `ch395fTestTask` 线程(`test/ch395f_test_task.c`
`StartCh395fTestTask`),通过 `test/test_config.h` 中的 `TEST_SUITE_*` 宏在**编译期**选择要跑的套件。
选中 `TEST_SUITE_GD5F` 时,`test_config.h` 会自动 `#define ENABLE_GD5F_TESTS` 及全部
`ENABLE_GD5F_PHASE*_TESTS`,随后由 `gd5f_test_run()``test/gd5f_test_task.c`)按阶段串行执行。
> `defaultTask` **不再承担任何测试**(仅空闲);所有测试都只在 `ch395fTestTask` 中运行。
`test/test_config.h` 配置示例(三选一,互斥,多选触发 `#error`
```c
// #define TEST_SUITE_CH395F
// #define TEST_SUITE_STORAGE
#define TEST_SUITE_GD5F /* 选中后自动开启 gd5f 全部阶段宏 */
```
执行顺序(由 `gd5f_test_run()` 按阶段串行,与函数依赖一致):
`init/read_id/reset``BBT``页读写``块擦除``ECC``SPI 原语``DMA 边界``FTL``FatFS``边界条件`
### 启用测试
1.`test/test_config.h` 取消注释所需的 `TEST_SUITE_*` 宏(如 `TEST_SUITE_GD5F`
2. 编译(`@build`)确认 0 错误 0 警告;
3. 烧录后串口观察 `[NAND-TEST]` 日志与各 TC 的 `PASS/FAIL`,结尾打印
`=== GD5F2GQ5UE Tests: X/Y PASSED ===`
4. 不选任何 `TEST_SUITE_*` 时为正常产品构建(`ch395fTestTask` 空闲,不跑测试)。
---
## 2. 阶段 1初始化与 ID 识别
### TC-GD5F-101: gd5f2gq5ue_init 全流程
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-101 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | 初始化成功复位→ID 校验→BBT 扫描→ECC 使能→解除块保护) |
| **前置条件** | SPI1 与 GPIO 已由 CubeMX 初始化 |
| **测试步骤** | 1. 调用 `gd5f2gq5ue_init()`<br>2. 观察日志Resetting / Reading NAND ID / Scanning bad blocks / Enabling ECC / Unlocking block protection / NAND init OK |
| **通过标准** | 返回 `GD5F_OK`;日志无 `ID mismatch` / `Reset failed`BBT 扫描打印 bad count |
| **覆盖原则** | 初始化主路径 |
### TC-GD5F-102: gd5f2gq5ue_read_id 返回正确 ID
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-102 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | 读取 MID=0xC8、DID=0x52 |
| **前置条件** | `gd5f2gq5ue_init()` 已成功(或至少 SPI 已初始化) |
| **测试步骤** | 调用 `gd5f2gq5ue_read_id(&mid, &did)` 并比较 |
| **通过标准** | `mid==0xC8 && did==0x52` |
| **覆盖原则** | `gd5f2gq5ue_read_id` |
### TC-GD5F-103: gd5f2gq5ue_reset 成功
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-103 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | 软复位后状态寄存器 OIP 清零 |
| **前置条件** | SPI 已初始化 |
| **测试步骤** | 调用 `gd5f2gq5ue_reset()`;随后 `gd5f_read_status(&s)` 检查 `OIP==0` |
| **通过标准** | `reset` 返回 `GD5F_OK``s & GD5F_STATUS_OIP == 0` |
| **覆盖原则** | `gd5f2gq5ue_reset` / `gd5f_read_status` |
---
## 3. 阶段 2坏块管理BBT
> 注:`gd5f2gq5ue_mark_block_bad()` / `gd5f2gq5ue_bbt_clear()` **仅修改 RAM 中的 BBT 位图**
> 不会写回 Flash 物理标记(与头注释"尝试物理标记"不符,见陷阱节)。重启后由 `init` 重新扫描出厂标记。
### TC-GD5F-201: 出厂坏块扫描
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-201 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | init 后 BBT 扫描可完成并打印坏块数 |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | 观察 init 日志 `BBT scan: N bad blocks found`;调用 `gd5f2gq5ue_print_bbt()` |
| **通过标准** | 扫描完成N 为合理值,通常 0~数十);`print_bbt` 能列出坏块偏移 |
| **覆盖原则** | BBT 扫描(`gd5f_private_bbt_scan` |
### TC-GD5F-202: gd5f2gq5ue_is_block_bad 查询
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-202 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | 好块返回 0坏块返回 1 |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | 1. 对已知好块(如 block 0除非其为坏块`is_block_bad` 应返回 0<br>2. 对 `print_bbt` 列出的坏块 `is_block_bad` 应返回 1 |
| **通过标准** | 好块返回 0坏块返回 1越界 block 返回 1 |
| **覆盖原则** | `gd5f2gq5ue_is_block_bad` |
### TC-GD5F-203: mark_block_bad 与 is_block_bad 一致性
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-203 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | 标记后查询一致RAM BBT |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | 取一个好块 B`gd5f2gq5ue_mark_block_bad(B)``is_block_bad(B)==1`;再 `gd5f2gq5ue_bbt_clear()``is_block_bad(B)==0` |
| **通过标准** | mark 后查得 1clear 后查得 0RAM 行为符合预期) |
| **覆盖原则** | `gd5f2gq5ue_mark_block_bad` / `gd5f2gq5ue_bbt_clear` |
### TC-GD5F-204: 初始化 BBT 稳定读取(无需 rescan
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-204 |
| **优先级** | P2 |
| **类型** | 一致性测试 |
| **标题** | 初始化 BBT 可直接读取且稳定(无需 rescan |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | 记录 `print_bbt` 坏块数;再次 `print_bbt` 比较坏块数(验证 init BBT 可直接读取且稳定) |
| **通过标准** | 两次坏块数一致(出厂标记未变) |
| **覆盖原则** | `gd5f2gq5ue_is_block_bad` / `gd5f2gq5ue_bbt_clear` / `gd5f2gq5ue_mark_block_bad`init BBT 读取) |
---
## 4. 阶段 3页读写跨页
> 重要:写操作前目标区域必须已擦除(见阶段 4 / 边界条件 TC-GD5F-1003
### TC-GD5F-301: 整页写读
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-301 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | offset=0, size=2048 写入递增模式并读回比对 |
| **前置条件** | 目标块已擦除 |
| **测试步骤** | 1. 填 `buf[i]=i&0xFF`(或 `(offset+i)&0xFF`<br>2. `gd5f2gq5ue_write(0, buf, 2048)`<br>3. `gd5f2gq5ue_read(0, rbuf, 2048)` 逐字节比对 |
| **通过标准** | 读回与写入完全一致 |
| **覆盖原则** | `gd5f2gq5ue_write` / `gd5f2gq5ue_read`(单页路径) |
### TC-GD5F-302: 跨页写读
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-302 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | offset=1000, size=3000 跨越页边界 |
| **前置条件** | 涉及块已擦除 |
| **测试步骤** | 写入 3000 字节offset=1000 起),读回同窗口比对 |
| **通过标准** | 跨页数据连续正确(`gd5f2gq5ue_read/write` 自动分页) |
| **覆盖原则** | 跨页拆分逻辑 |
### TC-GD5F-303: 非对齐 offset 写读
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-303 |
| **优先级** | P1 |
| **类型** | 边界测试 |
| **标题** | offset=1, size=2047 |
| **前置条件** | 目标块已擦除 |
| **测试步骤** | 同 TC-GD5F-301但 offset=1 |
| **通过标准** | 读回与写入一致(列地址 = offset%2048 正确) |
| **覆盖原则** | 列地址计算 |
### TC-GD5F-304: 多页顺序写读
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-304 |
| **优先级** | P1 |
| **类型** | 压力测试 |
| **标题** | 连续 10 页offset=0, size=20480 |
| **前置条件** | 前 10 页所在块已擦除 |
| **测试步骤** | 写入 20480 字节递增模式,读回全量比对 |
| **通过标准** | 全部一致 |
| **覆盖原则** | 多页循环 |
### TC-GD5F-305: 随机单字节访问
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-305 |
| **优先级** | P2 |
| **类型** | 功能测试 |
| **标题** | 任意 offsetsize=1 写入/读回 |
| **前置条件** | 目标块已擦除 |
| **测试步骤** | 在若干随机 offset 写单字节并读回 |
| **通过标准** | 单字节值正确,且不影响同页其余字节(部分页编程约束内) |
| **覆盖原则** | 单字节 `program_load` 列偏移 |
---
## 5. 阶段 4块擦除
### TC-GD5F-401: 单块擦除后读全 0xFF
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-401 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | 擦除 block 0先确认非坏块读出应全 0xFF |
| **前置条件** | block 0 非坏块 |
| **测试步骤** | `gd5f2gq5ue_erase(0, 128*1024)`;读整块比对全 0xFF |
| **通过标准** | 返回 `GD5F_OK` 且整块读回全 0xFF |
| **覆盖原则** | `gd5f2gq5ue_erase` / `gd5f_block_erase` |
### TC-GD5F-402: 多块擦除
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-402 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | size=3 块384KB连续擦除 |
| **前置条件** | 对应块非坏块 |
| **测试步骤** | `gd5f2gq5ue_erase(0, 3*128*1024)`;读回比对全 0xFF |
| **通过标准** | 返回 `GD5F_OK`,范围全 0xFF |
| **覆盖原则** | 多块循环擦除 |
### TC-GD5F-403: 非块对齐应返回错误
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-403 |
| **优先级** | P0 |
| **类型** | 负向测试 |
| **标题** | offset 或 size 非 128KB 整数倍 |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | `gd5f2gq5ue_erase(100, 128*1024)`offset 不对齐);`gd5f2gq5ue_erase(0, 100)`size 不对齐) |
| **通过标准** | 两者均返回 `GD5F_ERROR` |
| **覆盖原则** | 对齐校验 |
### TC-GD5F-404: 擦除-写-读 完整循环
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-404 |
| **优先级** | P0 |
| **类型** | 集成测试 |
| **标题** | erase → write → read 闭环比对 |
| **前置条件** | 目标块非坏块 |
| **测试步骤** | 1. `erase` 目标块<br>2. `write` 已知模式(如 0xAA 填充/递增)<br>3. `read` 比对 |
| **通过标准** | 读回 == 写入(验证"先擦后写"链路) |
| **覆盖原则** | 完整 NAND 写流程 |
---
## 6. 阶段 5ECC 验证
### TC-GD5F-501: ECC 开启下写读正确
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-501 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | ECC 使能时数据正确ECC status 无错 |
| **前置条件** | `gd5f2gq5ue_init()`(已使能 ECC目标块已擦除 |
| **测试步骤** | 写随机/递增模式;`gd5f_page_read(page)` + `gd5f_read_from_cache` 读回;`gd5f_check_ecc()` 检查 |
| **通过标准** | 数据一致且 `gd5f_check_ecc()` 返回 `GD5F_OK`ECCS=00 无错) |
| **覆盖原则** | ECC 数据路径 |
### TC-GD5F-502: gd5f_check_ecc 干净读
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-502 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | 干净页 `gd5f_check_ecc()` 返回 OK |
| **前置条件** | 已 `gd5f_page_read` 加载页 |
| **测试步骤** | 读后调 `gd5f_check_ecc()` |
| **通过标准** | 返回 `GD5F_OK`(非 `GD5F_ECC_ERROR` |
| **覆盖原则** | `gd5f_check_ecc` |
### TC-GD5F-503: ECC 纠错能力(可选)
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-503 |
| **优先级** | P3 |
| **类型** | 可选/深入测试 |
| **标题** | 观察 ECCS 位在纠正场景下的变化 |
| **前置条件** | 已知好块ECC 开启 |
| **测试步骤** | 写入后读回,检查 `ECCS`00 无错 / 01~11 已纠正 bit人为制造位翻转较困难建议仅读状态位验证逻辑 |
| **通过标准** | `gd5f_check_ecc()` 在不可纠正时返回 `GD5F_ECC_ERROR`,否则 `GD5F_OK` |
| **覆盖原则** | ECC 状态位语义 |
---
## 7. 阶段 6SPI 原语FTL 共享)
> 下列函数为 `gd5f2gq5ue.c` 的私有/半公开原语FTL 直接复用;通过组合调用验证。
### TC-GD5F-601: page_read + read_from_cache 组合
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-601 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | 单页加载到 cache 并读出 |
| **前置条件** | 目标页有已编程数据 |
| **测试步骤** | `gd5f_page_read(page)``gd5f_read_from_cache(0, buf, 2048)` |
| **通过标准** | 数据正确,返回 `GD5F_OK` |
| **覆盖原则** | `gd5f_page_read` / `gd5f_read_from_cache` |
### TC-GD5F-602: program_load + program_exec 组合
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-602 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | 单页编程load→exec |
| **前置条件** | 目标块已擦除 |
| **测试步骤** | `gd5f_write_enable()``gd5f_program_load(0, buf, 2048)``gd5f_program_exec(page)`;读回比对 |
| **通过标准** | 编程后数据一致,无 `P_FAIL` |
| **覆盖原则** | `gd5f_program_load` / `gd5f_program_exec` |
### TC-GD5F-603: gd5f_block_erase 单块
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-603 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | 原语级块擦除(地址=块首页地址) |
| **前置条件** | SPI 已初始化 |
| **测试步骤** | `gd5f_block_erase(block)`(注意传**块编号**,内部转块首页地址,见陷阱 01 |
| **通过标准** | 返回 `GD5F_OK`,无 `E_FAIL` |
| **覆盖原则** | `gd5f_block_erase`Trap 01 修复点) |
### TC-GD5F-604: write_enable / read_statusWEL 位)
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-604 |
| **优先级** | P2 |
| **类型** | 功能测试 |
| **标题** | 写使能后 WEL 位置位 |
| **前置条件** | SPI 已初始化 |
| **测试步骤** | `gd5f_write_enable()``gd5f_read_status(&s)` 检查 `s & GD5F_STATUS_WEL` |
| **通过标准** | WEL 位为 1 |
| **覆盖原则** | `gd5f_write_enable` / `gd5f_read_status` |
### TC-GD5F-605: gd5f_wait_busy 正常返回
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-605 |
| **优先级** | P2 |
| **类型** | 功能测试 |
| **标题** | 操作完成后 wait_busy 在超时内返回 0 |
| **前置条件** | 执行一次读/写操作后 |
| **测试步骤** | 调 `gd5f_wait_busy(1000)`,检查返回值 |
| **通过标准** | 返回 `GD5F_OK`OIP 已清零) |
| **覆盖原则** | `gd5f_wait_busy` |
---
## 8. 阶段 7DMA 边界(阈值 32 字节)
> 驱动 `GD5F_DMA_THRESHOLD=32``size>32` 走 `HAL_SPI_*_DMA`,否则轮询。两路径结果必须一致。
### TC-GD5F-701: 阈值下界(轮询路径)
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-701 |
| **优先级** | P1 |
| **类型** | 边界测试 |
| **标题** | size=32 走非 DMA 路径,读写正确 |
| **前置条件** | 目标块已擦除 |
| **测试步骤** | 写/读 32 字节并比对 |
| **通过标准** | 数据一致 |
| **覆盖原则** | `GD5F_DMA_THRESHOLD` 下界 |
### TC-GD5F-702: 阈值上界DMA 路径)
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-702 |
| **优先级** | P1 |
| **类型** | 边界测试 |
| **标题** | size=33 走 DMA 路径,读写正确 |
| **前置条件** | 目标块已擦除 |
| **测试步骤** | 写/读 33 字节并比对 |
| **通过标准** | 数据一致 |
| **覆盖原则** | `GD5F_DMA_THRESHOLD` 上界 |
### TC-GD5F-703: 大块 DMA 一致性
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-703 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | size=2048 整页DMA 路径)读写正确 |
| **前置条件** | 目标块已擦除 |
| **测试步骤** | 同 TC-GD5F-301确认 DMA 路径全页正确 |
| **通过标准** | 数据一致 |
| **覆盖原则** | DMA 大数据路径 |
## 9. 阶段 8FTL 层nand_ftl
### TC-GD5F-801: nand_ftl_init 成功
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-801 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | FTL 初始化(在 driver init 之上建立映射) |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | 调用 `nand_ftl_init()` |
| **通过标准** | 返回 0日志无异常 |
| **覆盖原则** | `nand_ftl_init` |
### TC-GD5F-802: nand_ftl_format 成功
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-802 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | 格式化后可正常使用 |
| **前置条件** | `nand_ftl_init()` 已成功 |
| **测试步骤** | 调用 `nand_ftl_format()` |
| **通过标准** | 返回 0 |
| **覆盖原则** | `nand_ftl_format` |
---
## 10. 阶段 9FatFS diskio可选待 FTL 完成)
### TC-GD5F-901: 挂载 + 文件写读
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-901 |
| **优先级** | P2 |
| **类型** | 集成测试 |
| **标题** | FatFS 挂载 `/`,写文件后读回比对 |
| **前置条件** | FTL 已 init/formatFatFS diskio 已对接 |
| **测试步骤** | `f_mount``f_open` 写若干 KB → `f_close``f_open` 读回比对 |
| **通过标准** | 文件内容一致 |
| **覆盖原则** | FatFS diskio 链路 |
---
## 11. 边界条件
### TC-GD5F-1001: offset 越界
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-1001 |
| **优先级** | P1 |
| **类型** | 负向测试 |
| **标题** | offset >= TOTAL_SIZE 应返回错误 |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | `gd5f2gq5ue_read(GD5F_TOTAL_SIZE, buf, 1)` / `write` 同 |
| **通过标准** | 返回错误码(非 GD5F_OK |
| **覆盖原则** | 边界保护 |
### TC-GD5F-1002: size=0
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-1002 |
| **优先级** | P2 |
| **类型** | 负向测试 |
| **标题** | 零长度读写不崩溃 |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | `gd5f2gq5ue_read(0, buf, 0)` / `write(0, buf, 0)` |
| **通过标准** | 返回 GD5F_OK 或合理错误,无 HardFault |
| **覆盖原则** | 零长度处理 |
### TC-GD5F-1003: 未擦除区域写入
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-1003 |
| **优先级** | P0 |
| **类型** | 注意事项/负向 |
| **标题** | 写前未擦除 → 数据不可预期 |
| **前置条件** | 目标块含旧数据(未擦除) |
| **测试步骤** | 直接 `write` 新数据并 `read` 比对 |
| **通过标准** | **不通过比对**(验证 NAND "先擦后写" 特性;测试本身用于确认驱动不会误报成功) |
| **覆盖原则** | NAND 写约束 |
### TC-GD5F-1004: 写保护解除验证
| 字段 | 值 |
|------|-----|
| **ID** | TC-GD5F-1004 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | init 后 Protect 寄存器为 0x00可写 |
| **前置条件** | `gd5f2gq5ue_init()` 已成功 |
| **测试步骤** | `gd5f_private_set_feature` 读 Protect(0xA0);或间接由"可正常 program"推断 |
| **通过标准** | 块保护已解除,编程不返回 `GD5F_PROGRAM_FAIL` |
| **覆盖原则** | 块保护解除 |
---
---
## 12. 存储套件测试TEST_SUITE_STORAGE / storage_test_run
> 本套件独立于 GD5F 驱动套件,由 `test_config.h` 的 `TEST_SUITE_STORAGE` 选择,
> 入口 `storage_test_run()``test/storage_test_task.c`),统一在 `ch395fTestTask` 中调度。
> 当前 `storage_test_run()` 已覆盖 mkfs/mount/文件写读/性能(与阶段 9 FatFs 同类路径),
> 并新增下列三项**集成测试**TC-STO-01~03覆盖 FTL journal 掉电恢复、大文件/随机访问、坏块注入下的存储可用性。
> 三项均已于 STORAGE 套件实测通过(`=== Storage Tests: 11/11 PASSED (failed=0) ===`)。
>
> **已知陷阱(已修复)**FTL 页缓存用 `s_cached_lpn = 0` 表示"缓存未加载",但 LPN 0 是合法页FAT 引导扇区所在页)。
> resume/重新初始化后 `s_cached_lpn` 被复位为 0导致首次 `f_mount` 读 LBA0 时误判缓存命中、返回 `memset` 的全 0 缓存,
> 引导扇区签名变成 `0000`FatFs 报 `FR_NO_FILESYSTEM`。正常启动因 `f_mkfs` 兜底分支掩盖了此问题(第二次挂载才真正读盘)。
> 修复:将"缓存空"哨兵改为非法值 `(dhara_sector_t)-1`0xFFFFFFFF远超实际容量涉及 `disk_initialize` / `nand_ftl_deinit` / `nand_ftl_format`。
### TC-STO-01: FTL 掉电恢复journal 持久化)
| 字段 | 值 |
|------|-----|
| **ID** | TC-STO-01 |
| **优先级** | P1 |
| **类型** | 集成/可靠性测试 |
| **标题** | 写入文件并同步后,可经 `dhara_map_resume` 恢复映射,文件不丢 |
| **前置条件** | `gd5f2gq5ue_init()` + `nand_ftl_init()` 已成功FatFS 可挂载 |
| **测试步骤** | 1. `f_mount` → 写已知内容文件 `pl.dat`4KB 位置相关模式)→ `f_sync`/`f_close` 落盘<br>2. 模拟掉电:`f_mount("",0)` 卸载 → `nand_ftl_deinit()` 复位 FTL → `nand_ftl_init()` 重新走 `dhara_map_resume`<br>3. `f_mount("",1)` 重新挂载 → 打开 `pl.dat` 读回比对 |
| **通过标准** | 掉电模拟前后文件内容逐字节一致resume 后映射有效(无异常) |
| **覆盖原则** | `disk_initialize` / `dhara_map_resume` / FatFS 掉电安全 |
| **依赖** | `nand_ftl_deinit()`(清 `s_initialized`/`s_cached_lpn`,使下次 init 真正重新 resume |
| **实测** | PASSSTORAGE 套件resume 后 `pl.dat` 内容逐字节一致;修复 `s_cached_lpn` 哨兵 bug 后通过) |
### TC-STO-02: 大文件 / 多扇区 / 随机 seek
| 字段 | 值 |
|------|-----|
| **ID** | TC-STO-02 |
| **优先级** | P1 |
| **类型** | 功能/压力测试 |
| **标题** | 256KB 文件顺序写读 + 随机偏移 seek 读一致性 |
| **前置条件** | FTL 已初始化、FatFS 已挂载 |
| **测试步骤** | 1. 写 256KB 文件 `big.dat`,每字节按位置相关模式填充(便于任意偏移校验)<br>2. 顺序读回全量比对<br>3. `f_lseek` 到若干随机偏移(如 0/33KB/128KB/200KB/末段)读小块比对 |
| **通过标准** | 顺序与全部随机偏移读回内容均一致(验证 FTL 大范围映射 + GC + FatFS 随机访问) |
| **覆盖原则** | FTL 大范围 LBA 映射 / 垃圾回收 / FatFs `f_lseek` + `f_read` |
| **实测** | PASSSTORAGE 套件256KB 顺序 + 随机 seek 读回一致) |
### TC-STO-03: 坏块注入下存储写入
| 字段 | 值 |
|------|-----|
| **ID** | TC-STO-03 |
| **优先级** | P1 |
| **类型** | 负向/健壮性测试 |
| **标题** | 运行时注入坏块后FatFS 仍可正常写读且坏块未被占用 |
| **前置条件** | `gd5f2gq5ue_init()` 成功FTL 已复位 |
| **测试步骤** | 1. `nand_ftl_format()` 清空旧 map → 选若干出厂好块 `gd5f2gq5ue_mark_block_bad()` 注入(更新并持久化 BBT<br>2. `nand_ftl_deinit()` + `nand_ftl_init()` 使 dhara 经 `is_block_bad` 看到新坏块<br>3. `f_mkfs` 重建文件系统 → 正常文件写读 `bbt.dat` 并校验<br>4. 校验注入坏块仍 `gd5f2gq5ue_is_block_bad()==1` |
| **通过标准** | 文件内容完整;注入坏块未被 dhara 分配使用(仍报告坏),存储可用 |
| **注意事项** | 本例会**永久**将若干好块标记为坏并持久化(测试设备专用);如需恢复,运行 TC-STO-04启用 `ENABLE_STORAGE_RECOVERY_TESTS` 编译 STORAGE 套件一次)即可重建 BBT + 重新格式化 |
| **实测** | PASSSTORAGE 套件:注入 3 坏块后文件完整且坏块仍报告坏) |
### TC-STO-04: 重建 BBT + 重新格式化(恢复)
| 字段 | 值 |
|------|-----|
| **ID** | TC-STO-04 |
| **优先级** | P1 |
| **类型** | 恢复/清理测试 |
| **标题** | 重建 BBT重新扫描出厂坏块并持久化并清空 dhara map恢复设备干净态 |
| **前置条件** | `gd5f2gq5ue_init()` 成功;需启用 `ENABLE_STORAGE_RECOVERY_TESTS` 编译 |
| **测试步骤** | 1. `gd5f2gq5ue_bbt_rebuild()`(关 ECC → 重新扫描出厂坏块 → 开 ECC → `gd5f_bbt_save` 持久化,覆盖注入坏块)<br>2. `nand_ftl_format()` 清空 dhara map<br>3. `gd5f_bbt_dump_flash` 校验持久化 BBT 版本 ≥1 |
| **通过标准** | 重建返回 OK 且持久化 BBT 版本 ≥1设备坏块回到出厂集合注入坏块被清除 |
| **注意事项** | 运行一次即恢复;恢复后建议断电重启让 init 重新加载干净 BBT。本 TC 默认关闭,避免常规测试每次重置设备 |
| **依赖** | `gd5f2gq5ue_bbt_rebuild`(详见 §4.8 持久化设计) |
---
## 附录 B通过/失败汇总
| TC-ID | 标题 | 优先级 | 实测 |
|-------|------|--------|------|
| TC-GD5F-101 | init 全流程 | P0 | PASS |
| TC-GD5F-102 | read_id 正确 ID | P0 | PASS |
| TC-GD5F-103 | reset 成功 | P1 | PASS |
| TC-GD5F-201 | 出厂坏块扫描 | P0 | PASS |
| TC-GD5F-202 | is_block_bad 查询 | P0 | PASS |
| TC-GD5F-203 | mark/clear 一致性 | P1 | PASS |
| TC-GD5F-204 | init BBT 稳定(无需 rescan | P2 | PASS |
| TC-GD5F-205 | 可用块数 < 总块数(保留池生效) | P1 | PASS |
| TC-GD5F-206 | RAM 标记→闪存持久化bit+version | P1 | PASS |
| TC-GD5F-207 | 清 RAM→reload 恢复坏块 | P1 | PASS |
| TC-GD5F-301 | 整页写读 | P0 | PASS |
| TC-GD5F-302 | 跨页写读 | P0 | PASS |
| TC-GD5F-303 | 非对齐 offset | P1 | PASS |
| TC-GD5F-304 | 多页顺序写读 | P1 | PASS |
| TC-GD5F-305 | 随机单字节访问 | P2 | PASS |
| TC-GD5F-401 | 单块擦除读 0xFF | P0 | PASS |
| TC-GD5F-402 | 多块擦除 | P1 | PASS |
| TC-GD5F-403 | 非块对齐返回错误 | P0 | PASS |
| TC-GD5F-404 | 擦-写-读 闭环 | P0 | PASS |
| TC-GD5F-501 | ECC 开启写读 | P0 | PASS |
| TC-GD5F-502 | check_ecc 干净读 | P1 | PASS |
| TC-GD5F-503 | ECC 纠错(可选) | P3 | 未实现(可选) |
| TC-GD5F-601 | page_read+cache 组合 | P1 | PASS |
| TC-GD5F-602 | program_load+exec 组合 | P1 | PASS |
| TC-GD5F-603 | block_erase 原语 | P1 | PASS |
| TC-GD5F-604 | write_enable WEL 位 | P2 | PASS |
| TC-GD5F-605 | wait_busy 正常返回 | P2 | PASS |
| TC-GD5F-701 | DMA 阈值下界(32) | P1 | PASS |
| TC-GD5F-702 | DMA 阈值上界(33) | P1 | PASS |
| TC-GD5F-703 | 大块 DMA 一致性 | P1 | PASS |
| TC-GD5F-801 | nand_ftl_init | P0 | PASS |
| TC-GD5F-802 | nand_ftl_format | P1 | PASS |
| TC-GD5F-901 | FatFS 挂载写读 | P2 | PASS |
| TC-GD5F-1001 | offset 越界 | P1 | PASS |
| TC-GD5F-1002 | size=0 | P2 | PASS |
| TC-GD5F-1003 | 未擦除写入 | P0 | PASS |
| TC-GD5F-1004 | 写保护解除 | P1 | PASS |
| TC-STO-01 | FTL 掉电恢复journal | P1 | PASS |
| TC-STO-02 | 大文件/随机 seek | P1 | PASS |
| TC-STO-03 | 坏块注入下存储写入 | P1 | PASS |
| TC-STO-04 | BBT 重建 + 重新格式化(恢复) | P1 | 受 `ENABLE_STORAGE_RECOVERY_TESTS` 控制(默认关) |
> 当前实测:`TEST_SUITE_GD5F` 构建一次运行全部阶段,结尾输出 `=== GD5F2GQ5UE Tests: 65/65 PASSED (failed=0) ===`65 为各 TC 内部断言总数,含 TC-GD5F-205~207 BBT 持久化用例TC 覆盖见上表)。`TEST_SUITE_STORAGE` 套件结尾输出 `=== Storage Tests: 11/11 PASSED (failed=0) ===`TC-STO-01/02/03 各含若干内部断言,且 TC-STO-01 依赖的 `s_cached_lpn` 哨兵 bug 已修复)。两套件均 0 错误 0 警告通过 MDK 编译。
---
## 附录 C驱动函数覆盖
| 函数 | 覆盖 TC |
|------|---------|
| `gd5f2gq5ue_init` | TC-GD5F-101 |
| `gd5f2gq5ue_read_id` | TC-GD5F-102 |
| `gd5f2gq5ue_reset` | TC-GD5F-103 |
| `gd5f2gq5ue_read` | TC-GD5F-301/302/303/304/305 |
| `gd5f2gq5ue_write` | TC-GD5F-301/302/303/304/305 |
| `gd5f2gq5ue_erase` | TC-GD5F-401/402/403/404 |
| `gd5f2gq5ue_is_block_bad` | TC-GD5F-202 |
| `gd5f2gq5ue_mark_block_bad` | TC-GD5F-203 |
| `gd5f2gq5ue_bbt_clear` | TC-GD5F-203 |
| `gd5f2gq5ue_is_block_bad` / 初始化 BBT | TC-GD5F-204 |
| `gd5f2gq5ue_print_bbt` | TC-GD5F-201 |
| `gd5f_wait_busy` | TC-GD5F-605 |
| `gd5f_write_enable` | TC-GD5F-604 |
| `gd5f_read_status` | TC-GD5F-103/604 |
| `gd5f_page_read` | TC-GD5F-601 |
| `gd5f_read_from_cache` | TC-GD5F-601 |
| `gd5f_program_load` | TC-GD5F-602 |
| `gd5f_program_exec` | TC-GD5F-602 |
| `gd5f_block_erase` | TC-GD5F-603 |
| `gd5f_check_ecc` | TC-GD5F-501/502 |
| `nand_ftl_init` | TC-GD5F-801 |
| `nand_ftl_format` | TC-GD5F-802 |
| `nand_ftl_deinit` | TC-STO-01 / TC-STO-03 |
| `gd5f2gq5ue_bbt_rebuild` | TC-STO-04 |
| `storage_test_run`(存储套件) | TC-STO-01/02/03/04 |
---
## 附录 D参数速查
| 项 | 值 |
|----|-----|
| 页数据/Spare/总长 | 2048 / 64 / 2112 B |
| 每块页数 / 块大小 | 64 / 128 KB |
| 总块数 / 容量 | 2048 / 256 MB |
| ID | MID=0xC8, DID=0x52 |
| 擦除最小单位 | 1 块128 KB须对齐 |
| DMA 阈值 | 32 B>32 走 DMA |
| 状态寄存器 | 0xC0OIP=0, WEL=1, E_FAIL=2, P_FAIL=3, ECCS=4/5 |
| Feature 寄存器 | 0xB0ECC_EN=4, QE=0 |
| Protect 寄存器 | 0xA0init 后写 0x00 解除保护 |
| SPI 引脚 | CS=PE0, SCK=PB3, MISO=PB4, MOSI=PB5, WP=PB8, HOLD=PE1 |

View File

@@ -0,0 +1,105 @@
# SD2506_RTC 测试指南
> 对应驱动:`Drivers/BSP/SD2506/sd2506.c`
> 测试代码:`test/sd2506_test_task.c` + `test/sd2506_test.h`
## 1. 概述
本测试套件对 `sd2506.c` 做**黑盒功能自测**,只调用公共 API不触碰驱动内部函数。
每个用例通过 `SD2506_TEST_CHECK` 记录 pass/fail最终由 `SD2506_TEST_REPORT` 汇总。
历史背景:本驱动曾出现“时间写不进”问题,根因是寄存器宏写成 `0x0FH`KEIL/ARMCC
不识别 `H` 整数后缀,被编译为 0导致 `SD2506_REG_CTR1` 实际等于 0秒寄存器
`sd2506_write_enable()` 把 WRTC 写错地址、写保护永不解除。修正为 `0x0FU` 后恢复。
详见 `docs/SD2506_Trap_Records.md`
## 2. 启用与编译
`test/test_config.h` 中一次只能启用一个 `TEST_SUITE_*`(互斥):
```c
//#define TEST_SUITE_CH395F
//#define TEST_SUITE_STORAGE
//#define TEST_SUITE_GD5F
#define TEST_SUITE_RTC /* 启用 RTC 测试套件 */
```
`test/sd2506_test.h` 中控制各阶段(默认全开):
```c
#define ENABLE_RTC_BASIC_TESTS /* Phase 1: 基本读写 / 走时 */
#define ENABLE_RTC_API_TESTS /* Phase 2: 全功能 API 覆盖 */
```
编译(要求 0 错误 0 警告):
```bat
MDK-ARM\build.bat
```
烧录后在串口USART1, 115200bps观察 `[RTC_TEST]` 日志。
## 3. 测试用例
| 编号 | 目标 | 覆盖函数 | 判定 |
|------|------|----------|------|
| TC-RTC-001 | 初始化并读取当前时间 | `sd2506_init` / `sd2506_get_time` | init 返回 0get 返回 0 |
| TC-RTC-002 | 写固定时间立即回读一致 | `sd2506_set_time` / `sd2506_get_time` | set==readback允许 +1s 进位) |
| TC-RTC-003 | 走时验证 | `sd2506_get_time` | 延时 3s 后秒数前进 2~5s |
| TC-RTC-010 | BCD↔DEC 互转 | `sd2506_dec_to_bcd` / `sd2506_bcd_to_dec` | 10 组样本双向一致 |
| TC-RTC-011 | 内部温度读取 | `sd2506_get_temperature` | 返回 0值 ∈ [-40, 85] |
| TC-RTC-012 | 电池电压mV | `sd2506_get_battery_voltage` | 返回 0值 ∈ (2000, 5000) |
| TC-RTC-013 | 芯片 8 字节 ID | `sd2506_get_id` | 返回 0读出 8 字节 |
| TC-RTC-014 | 用户 SRAM 回环 | `sd2506_write_sram` / `sd2506_read_sram` | 写入 8 字节后回读一致 |
| TC-RTC-015 | 报警设置 + 清除 | `sd2506_set_alarm` / `sd2506_clear_alarm` | 两者均返回 0 |
## 4. 覆盖矩阵
| 公共 API | 是否覆盖 | 用例 |
|----------|----------|------|
| `sd2506_init` | ✅ | TC-RTC-001 |
| `sd2506_get_time` | ✅ | TC-RTC-001/002/003 |
| `sd2506_set_time` | ✅ | TC-RTC-002 |
| `sd2506_get_temperature` | ✅ | TC-RTC-011 |
| `sd2506_get_battery_voltage` | ✅ | TC-RTC-012 |
| `sd2506_get_id` | ✅ | TC-RTC-013 |
| `sd2506_read_sram` | ✅ | TC-RTC-014 |
| `sd2506_write_sram` | ✅ | TC-RTC-014 |
| `sd2506_set_alarm` | ✅ | TC-RTC-015 |
| `sd2506_clear_alarm` | ✅ | TC-RTC-015 |
| `sd2506_read_ctr1` | △(诊断) | run() 内打印 CTR1 |
| `sd2506_bcd_to_dec` / `sd2506_dec_to_bcd` | ✅ | TC-RTC-010 |
> 未覆盖(需专项验证,不在本自测范围):倒计时寄存器、温度报警历史、
> 跨重启持久化(原 Phase 3 已废弃,依赖实际掉电/上电,建议手动或另写用例)。
## 5. 运行与判定
- `sd2506_test_run()` 由测试调度器调用,先 init 并记录原始时间,最后用
`sd2506_set_time` 把板子时间**恢复**为测试前的值(不污染真实时钟)。
- 输出形如:
```
[RTC_TEST] === SD2506 Phase 1: basic read/write (public API) ===
[RTC_TEST] [PASS] sd2506_init() ret=0 (expect 0)
...
[RTC_TEST] === Phase 1 basic: 6/6 PASSED (failed=0) ===
[RTC_TEST] === SD2506 Phase 2: full API coverage ===
...
[RTC_TEST] === Phase 2 API: 9/9 PASSED (failed=0) ===
```
- 通过标准:所有 Phase 的 `failed=0`,且总量 `X/Y PASSED` 中 Y 与 PASS 数相等。
## 6. 失败排查
- **TC-RTC-002 FAILset≠readback**:优先查 `sd2506.h` 中各寄存器宏是否被写成
`0xNNH`H 后缀陷阱)。例如 `SD2506_REG_CTR1` 必须是 `0x0FU` 而非 `0x0FH`。
- **init/get/set 返回 -2I2C 错误)**:查 I2C1 接线PB6-SCL / PB7-SDA
上拉电阻、芯片供电与器件地址 `0x32`。
- **温度/电压越界**:多为芯片未上电或 I2C 读到全 0xFF按 I2C 链路排查。
## 7. 已知限制
- TC-RTC-015 会让 `sd2506_set_alarm` 置 `CTR2.INTAE=1`(报警中断允许)。
测试固件未挂 INT 中断处理,且 INT 引脚在板上未连接,对运行无影响;
若要完全复位,可手动再调用一次 `set_alarm(..., mask=0)`。
- SRAM 回环会在 `addr=0` 写入测试数据8 字节),属用户 SRAM 区域,不影响时间/控制寄存器。

View File

@@ -0,0 +1,348 @@
# 系统墙钟时间维护 设计说明
> 模块:`App/sys_clock.c` / `App/sys_clock.h`
> 依赖SD2506API-G RTC 驱动(`Drivers/BSP/SD2506/`、HAL`HAL_GetTick()`
> 适用平台STM32F407ZGTxCortex-M4168MHzI2C1 接 SD2506
> 作者王建锋 创建日期2026-07-19
---
## 1. 概述
系统需要一个"墙钟时间"wall clock即真实日历时间 `YYYY-MM-DD HH:MM:SS`),用于:
- 日志/调试信息打时间戳;
- 文件系统的文件创建/修改时间FatFS 需要);
- 业务逻辑中基于真实时间的判定(定时、超时、调度等);
- 对外接口(如 FTP `LIST` 返回的文件时间、SNTP/NTP 校时等)。
本模块在 **SD2506 硬件 RTC掉电保持****HAL 系统节拍(运行期计时)** 之间做桥接:
- 启动时从 SD2506 读取一次真实时间,换算为 Unix 时间戳,建立"墙钟基准"
- 运行期间不再频繁访问 I2C RTC而是以 **HAL Tick 流逝量** 推算当前时间,避免每次取时都走 I2C 总线;
- 仅在 `sys_clock_set()` 被调用如用户校时、SNTP 同步)时,才把时间写回 SD2506 实现掉电保持。
---
## 2. 角色与依赖
| 角色 | 提供方 | 说明 |
|------|--------|------|
| 硬件实时钟RTC | SD2506API-G | 温补晶振带备份电池掉电后继续走时I2C1PB6-SCL/PB7-SDA |
| 系统节拍 | `HAL_GetTick()` | 毫秒级计数HAL 时基由 TIM7 提供168MHz 主频派生,精度远高于走时需求) |
| Unix 时间戳 | 本模块计算 | 自 1970-01-01 00:00:00 UTC 起的秒数(`uint32_t` |
| 时间结构 | `sd2506_time_t` | SD2506 驱动定义,`year` 为**完整年份2000~2099**,含 `week` |
**调用顺序(见 `Src/main.c`**
```
app_main_init(); /* 外设/日志等前置初始化 */
sd2506_init(); /* 初始化 RTCI2C 通信、24h 制、充电配置) */
sys_clock_init(); /* 读取 RTC → 建立墙钟基准 */
```
> 必须在 `sd2506_init()` 之后调用 `sys_clock_init()`;其余 `sys_clock_*` 接口必须在 `sys_clock_init()` 之后使用。
---
## 3. 核心思想基准法drift-free 维持)
为什么不每次都读 RTC
- SD2506 走 I2C单次读取有总线开销与阻塞延迟
- RTC 自身已足够精确,无需运行时反复校准;
- 运行期真正变化的只是"过了多少时间",由 HAL Tick 提供。
因此模块只在初始化(`sys_clock_init`)与显式设置(`sys_clock_set`)时接触 RTC其余时刻用以下公式维持时间
```
当前 Unix 秒数 = s_base_unix_secs + (HAL_GetTick() - s_base_tick) / 1000
```
其中 `(now_tick - s_base_tick)`**unsigned 减法**,在 Tick 每约 49 天回绕一次时依然正确(只要两次采样间隔 < 2³² ms ≈ 49 天,单次运行必然满足),所以连续上电不超过 49 天的场景无回绕问题。
---
## 4. 内部数据结构
`App/sys_clock.c` 中定义两个静态变量作为墙钟基准:
```c
static volatile uint32_t s_base_tick = 0; /* HAL_GetTick() 基准(建立基准时的 Tick 值) */
static volatile uint32_t s_base_unix_secs = UINT32_MAX; /* 基准对应的 Unix 秒数;初始为 UINT32_MAX 表示"未建立" */
```
- `s_base_tick``sys_clock_init()` / `sys_clock_set()` 时采样一次。
- `s_base_unix_secs`:与 `s_base_tick` 同一时刻的 Unix 秒数;`sys_clock_get()` 以它为锚推算当前值。
- 二者均带 `volatile`,因为时间是跨任务共享的状态(虽更新在 nets/默认任务中,但读取可能发生在任意任务上下文)。
> 注:当前工程仅 `main.c` 调用了 `sys_clock_init()``sys_clock_get()` / `sys_clock_get_str()` / `sys_clock_set()` 是供应用层(日志、文件时间、校时协议等)调用的 API尚未被其他模块引用。
---
## 5. 对外 API
| 函数 | 功能 | 返回值 | 限定条件 |
|------|------|--------|----------|
| `int sys_clock_init(void)` | 从 SD2506 读取初始时间,建立墙钟基准(含可选校时) | `0`(始终成功) | `sd2506_init()` 已调用 |
| `uint32_t sys_clock_get(void)` | 获取当前 Unix 时间戳(秒) | 当前秒数;未初始化返回 `UINT32_MAX` | `sys_clock_init()` 已调用 |
| `char *sys_clock_get_str(char *buf, size_t buf_size)` | 获取格式化字符串 `YYYY-MM-DD HH:MM:SS` | `buf` 指针;未初始化输出 `----/--/-- --:--:--` | `buf` 有效且 `buf_size ≥ 20` |
| `int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time)` | 更新基准,可写回 SD2506 | `0`(始终成功) | `unix_secs` 为合法时间戳 |
> 头文件 `sys_clock.h` 中注释的返回值含 `-1` 失败码,但实际实现恒返回 `0`,以"成功"语义对外;调用方按 `0` 成功处理即可。
---
## 6. 初始化流程(`sys_clock_init`
```
Step 1 从 SD2506 读取初始时间 sd2506_get_time(&sd_time)
├─ 注意sd_time.year 已是完整年份(驱动内已 +2000
│ 后续换算【不得】再加 2000否则年份虚增 → Unix 秒数超 uint32 回绕
└─ 可选校时(见第 9 节 RTC_DEFAULT_INIT_* 宏)
Step 2 将 RTC 时间换算为 Unix epoch 秒数
├─ 累计 1970 至当前年份的整年天数(含闰年 366 天判定)
├─ 加上当年 1 月至当前月的累计天数
├─ 加当月已过天数、时/分/秒
└─ 用 int64_t 累加,避免中间溢出;结果截断存入 uint32_t受 2038 限制,见第 12 节)
Step 3 建立基准
s_base_tick = HAL_GetTick();
s_base_unix_secs = (uint32_t)unix_secs;
```
初始化后,所有后续取时都基于 Step 3 的基准推算,不再访问 RTC。
---
## 7. 时间获取与漂移维持(`sys_clock_get`
```c
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);
if (unix_secs < 0) return UINT32_MAX; /* 基准未建立(理论上不会发生) */
return (uint32_t)unix_secs;
```
要点:
-**整除 1000** 把毫秒 Tick 折算为秒,**不**做每毫秒累加,避免累加误差累积与频繁除法;
- `now_tick - s_base_tick` 为无符号减法,自动正确处理 Tick 回绕;
- 若基准未建立(`s_base_unix_secs` 仍为 `UINT32_MAX` 且未初始化),返回 `UINT32_MAX` 作为"无效"哨兵。
### 7.1 字符串格式化(`sys_clock_get_str`
内部先 `sys_clock_get()`,若无效则输出占位串 `----/--/-- --:--:--`;否则用 `unix_to_sd2506()` 转回 `sd2506_time_t` 后,以 `snprintf(buf, buf_size, "%04d-%02d-%02d %02d:%02d:%02d", ...)` 格式化(年份显示时 `+2000` 还原为完整年份)。使用 `snprintf` 防止缓冲区溢出,调用方需保证 `buf_size ≥ 20`
---
## 8. 时间设置与回写(`sys_clock_set`
```c
s_base_tick = HAL_GetTick();
s_base_unix_secs = unix_secs;
if (sd_time != NULL) {
sd2506_set_time(sd_time); /* 写回 RTC实现掉电保持 */
}
```
- 仅更新"基准",之后 `sys_clock_get()` 即以此为准;
- 若传入 `sd_time != NULL`,同步写回 SD2506使断电后时间不丢失
- `sd_time``NULL` 时只改运行期基准、不动 RTC适用于临时/网络校时尚未落盘的场景)。
典型应用SNTP/NTP 同步得到 Unix 秒数 → 转成 `sd2506_time_t``sys_clock_set(secs, &sd_time)` 一次性更新运行基准与掉电保持。
---
## 9. 开发期校时宏(`sys_clock.c` 顶部)
```c
#define RTC_DEFAULT_INIT_ENABLE 1
#define RTC_DEFAULT_INIT_FORCE 1
#define RTC_DEFAULT_YEAR 2026
#define RTC_DEFAULT_MONTH 8
#define RTC_DEFAULT_DAY 24
#define RTC_DEFAULT_HOUR 0
#define RTC_DEFAULT_MINUTE 0
#define RTC_DEFAULT_SECOND 0
```
行为(`sys_clock_init` 内,`#if defined(RTC_DEFAULT_INIT_ENABLE)` 包裹):
- 判定 RTC 时间是否"无效"`year` 越界(<2000 或 >2099`month` 越界、`day` 越界;
- `RTC_DEFAULT_INIT_FORCE == 1`:强制每次启动都用上述宏写入默认时间;
- 否则仅在 RTC 时间无效时写入;
- 写入后重新 `sd2506_get_time()` 读取校准后的值,再进入 Step 2 换算。
> ⚠️ **使用注意**`FORCE=1` 会**每次开机覆盖用户/网络已设的时间**,仅用于首次烧录或硬件时钟丢失时的开发期校时。校时完成后应将 `RTC_DEFAULT_INIT_FORCE` 置 `0`(或注释 `ENABLE`),否则会抹掉正常运行中通过 SNTP 等写入的正确时间。
---
## 10. 时间换算算法
### 10.1 Unix 秒 ↔ SD2506`unix_to_sd2506`
- 时分秒:对 `unix_secs` 连续 `%60 /60 /24` 拆分;
- 年月日:先算总天数 `unix_secs / 86400`,再从 1970 年起逐年减去平/闰年天数,定位到具体年;再按月累减定位月、日;
- 星期:由 Zeller 公式(见 10.2)计算,结果 `0=Sunday`
- SD2506 年份字段为 0~99+2000故回写时 `year - 2000`
### 10.2 星期计算Zeller 公式)
`rtc_calc_week()``unix_to_sd2506()` 内使用同一套算法:
```
week = (century_term + month_term + day_term) % 7 /* 0=Sunday */
```
- `1、2 月视作上一年的 13、14 月`(公式要求),对应代码里 `wm<=2``wy--, wm+=12`
- 计算与闰年无关,纯日历代数,结果稳定。
---
## 11. 集成与调用示例
`Src/main.c` 中的初始化顺序(节选):
```c
HAL_Delay(100);
app_main_init();
sd2506_init(); /* 先初始化 RTC */
sys_clock_init(); /* 再建立墙钟基准:所有后续日志即带正确时间戳 */
```
应用层取时间示例:
```c
char ts[24];
DBG_INFO("now: %s", sys_clock_get_str(ts, sizeof(ts)));
uint32_t now = sys_clock_get(); /* Unix 秒,可用于 FatFS 文件时间、超时判定等 */
```
---
## 12. 关键设计要点与陷阱
1. **`year` 已经 `+2000`,禁止二次加 2000**
`sd2506_get_time()` 返回的 `year` 是完整年份(如 2026`sys_clock_init` 的 Step 2 直接把它当完整年份参与 Unix 换算;若再 `+2000` 会得到 4026 年,换算出的 Unix 秒数远超 `uint32_t` 上限,发生截断/回绕,时间彻底错乱。代码中相关注释已明确标注。
2. **`uint32_t` Unix 时间戳的 2038 上限**
基准与返回值均为 `uint32_t`,时间范围约为 `1970-01-01 ~ 2038-01-19`。超过 2038 年换算会回绕。当前设备生命周期内可接受;若需长期运行,需改用 `uint64_t``time_t`
3. **Tick 回绕安全**
`(now_tick - s_base_tick)` 为无符号减法Tick 每 ~49 天回绕一次时计算依然正确,无需特殊处理。
4. **运行期不访问 RTC**
`sys_clock_get()` 无任何 I2C 操作,零阻塞、可随时在任意任务/中断上下文安全调用(仅读 `volatile` 基准)。
5. **校时宏的副作用**
见第 9 节 `FORCE=1` 会覆盖既有正确时间,仅限开发期使用。
---
## 13. 已知限制与可扩展方向
| 项目 | 现状 | 可扩展方向 |
|------|------|------------|
| 时间范围 | 受 `uint32_t` 限制至 2038 | 改用 `uint64_t`/`time_t` 扩展 |
| 运行期校准 | 仅初始化时读 RTC | 可定时(如每天)重新 `sd2506_get_time()` 校准,消除 RTC 与 Tick 的微小累积偏差 |
| 网络校时 | 已实现PC 推送 / TCP见第 15 节) | SNTP/NTP 公网校时仍可作为后续扩展 |
| 时区 | 按 RTC 本地时间处理 | 如需 UTC/时区,可在 API 层增加偏移参数 |
---
## 14. 相关文件
| 路径 | 说明 |
|------|------|
| `App/sys_clock.c` | 墙钟维护实现(基准法、换算、校时宏) |
| `App/sys_clock.h` | 对外 API 声明 |
| `Drivers/BSP/SD2506/sd2506.c` / `.h` | SD2506 RTC I2C 驱动(`sd2506_get_time` / `sd2506_set_time` |
| `Src/main.c` | 初始化调用顺序:`sd2506_init()``sys_clock_init()` |
| `App/time_sync.c` / `time_sync.h` | 时间同步实现TCP Server`timeSyncTask` |
| `test/time_sync_server.py` | PC 端时间推送客户端TCP |
---
---
## 15. 时间同步PC 推送 / TCP
除 RTC 自身走时与开发期校时宏外,系统支持由 **PC 通过 TCP 主动推送本地墙钟** 来同步板子时间,使板子时间与开发电脑一致(无需公网 NTP
### 15.1 模块与角色
| 角色 | 提供方 | 说明 |
|------|--------|------|
| 时间同步服务端(板子) | `App/time_sync.c` / `time_sync.h` | TCP Server监听 `TIME_SYNC_PORT`(默认 8888 |
| 同步任务 | `timeSyncTask`FreeRTOS 任务) | 独立任务,由 `Src/freertos.c` 创建(优先级 `osPriorityNormal`,栈 2KB |
| 时间推送端PC | `test/time_sync_server.py` | Python TCP 客户端,周期推送本机本地时间 |
| 落盘接口 | `sys_clock_set_unix()` | 收到时间包后调用,内部转 `sd2506_time_t` 并写回 SD2506 |
### 15.2 协议
PC 每轮发送固定 8 字节帧:
```
字节 0..3 : 魔术字 "TIME"0x54 0x49 0x4D 0x45
字节 4..7 : 本地 Unix 时间戳uint32大端PC 本地时间,含时区,与电脑显示一致)
```
板子 `time_sync_handle_conn()` 读取后校验魔术字,取大端 uint32 调用 `sys_clock_set_unix(epoch)` 更新运行基准并写回 RTC。
### 15.3 任务流程
`timeSyncTask` 启动后延迟 8s等 netTask 完成 `net_init` 与 PHY 协商),进入循环:
```c
for (;;) {
ls = time_sync_open_listener(); /* net_socket + net_bind + net_listen失败重试 */
conn = net_accept(ls, NULL, NULL); /* 阻塞等待 PC 连接(无连接立即返回 -1循环重试 */
time_sync_handle_conn(conn); /* net_recv 读到 8 字节 → sys_clock_set_unix → net_close */
/* conn单连接模式下即 ls被 net_close 释放,下次循环重建监听 */
}
```
> 单连接模式下监听 Socket 收到连接后即转为数据通道,连接关闭后被释放;因此每轮循环都**重新创建监听 Socket**(与 `lftpd` 同模式),与 FTP 不冲突。
### 15.4 为什么用 TCP 而非 UDP
- 所有 socket 操作(`net_socket` / `net_bind` / `net_listen` / `net_accept` / `net_recv` / `net_close`)均走 `net_socket` 的**线程安全 API**,内部经消息队列由 `netTask` 串行执行;
- netTask 因此只需做 `net_poll()` + `net_process_messages()`,不掺杂任何应用层网络逻辑;
- **TCP 的全部 API 已经是基于消息队列的**(已在 `net_socket.c` 实现),而 UDP 的 `net_bind` / `net_sendto` / `net_recvfrom` 绕过队列。选用 TCP 即可让时间同步作为独立任务运行,且**无需改动 `net_socket.c` 的 UDP 路径**。
### 15.5 Socket 分配与冲突
- `net_socket()` 从 0 开始分配第一个空闲 Socket 控制块(`alloc_socket()`),并把硬件 Socket 号设为该下标调用时的第三个参数0仅为协议占位不影响分配。
- 当前启动顺序FTP 任务(延迟 7s先监听 → 占用 **socket 0**(控制)+ **socket 1**(数据);时间同步任务(延迟 8s后监听 → 占用 **socket 1**(或下一个空闲)。二者不冲突。
- 单连接模式允许**任意** Socket 作为"监听 + 数据复用"通道,且总共 8 个 SocketFTP 用 2 个、时间同步用 1 个,余量充足。
### 15.6 客户端实现要点(避坑)
PC 端 `test/time_sync_server.py` 发送 8 字节后**必须保持连接打开**,等待板子读取(或短延时)后再 `close()`
- 若发送后立刻关闭CH395F 在收到 FIN 时会**丢弃接收缓冲里尚未被板子读出的 8 字节**
- 板子 `net_recv` 经消息队列异步执行(有 ~10ms+ 调度延迟),等它去读时连接已 DISCONNECT、缓冲已清空`net_recv` 走 CLOSED 分支返回 0时间包丢失、同步失败
- 客户端改为 `sendall``recv` 直到板子主动关闭EOF或 2s 超时,即可保证板子先读到数据。
### 15.7 验证
板子日志应出现:
```
[TIME_SYNC] time_sync: TCP listen on :8888 (sock=1)
[NET] sock1 standalone accept
[NET] recv sock1 len=8
[CLK] sys_clock_set: wall clock updated to <epoch>
[CLK] sys_clock_set: SD2506 RTC written
[TIME_SYNC] time_sync: wall clock updated (epoch=<epoch>)
```
PC 端 `python test/time_sync_server.py``interval` 秒推送一次epoch 随电脑本地时间递增即表示同步正常。
---
*文档依据 `App/sys_clock.c` / `App/time_sync.c` 实现与《嵌入式C语言代码规范V1.0)》整理。*

View File

@@ -120,7 +120,7 @@ GND
注: $" \mathbb { X } ^ { 3 1 }$ 表示随机值,可以为 0或者 1。 注: $" \mathbb { X } ^ { 3 1 }$ 表示随机值,可以为 0或者 1。
## 4.2 实时时钟数据寄存器 $( 0 0 mathsf { H } \widetilde { \mathsf { \Omega } } 0 6 \mathsf { H } )$ ## 4.2 实时时钟数据寄存器 (00H~06H)
实时时钟数据寄存器是 7字节的存储器它以 BCD码方式存贮包括年、月、日、星期、时、分、秒的数据。 实时时钟数据寄存器是 7字节的存储器它以 BCD码方式存贮包括年、月、日、星期、时、分、秒的数据。

View File

@@ -0,0 +1,538 @@
# CH395F 驱动陷阱记录
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
---
## Trap 01Socket 4~7 自动分配不到
### 现象
多连接模式下最多接受 3 个客户端Socket 1~3Socket 4~7 的 CONNECT 中断从不触发,新客户端 SYN 被静默丢弃。
### 根因
`net_listen()` 中数据 Socket 的配置顺序错误。CH395F 在 `TCP_LISTEN` 时刻**一次性扫描**当前所有可用 Socket 并建立内部查找表。如果数据 Socket 4~7 在 `TCP_LISTEN` **之后**才配置缓冲区/协议/端口,自动分配逻辑不认识它们。
### 正确顺序
1. 先配置数据 Socket 1~7`SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP`
2. 最后配置监听 Socket 0`SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN`
### 注意事项
- 数据 Socket 不调 `OPEN_SOCKET`,由 CH395F 在连接到达时自动打开
- DISCONNECT 后 CH395F 自动关闭 Socket但保留协议/端口/缓冲区配置,可被再次分配
- 手册 §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反)
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_listen()`
### 发现时间
Phase 8 压力测试2025-07
---
## Trap 02数据 Socket 缓冲区重叠
### 现象
多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。
### 根因
CH395F 默认只为 Socket 0~3 各分配独立收发缓冲(共 48 块 × 512B 用满,见手册 §5.45Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31导致数据重叠。
### 解决方案
每个数据 Socket 独占 4 个缓冲区块2 发 + 2 收),按 `ds * 4` 基址分配,确保不重叠:
| Socket | 发送块 | 接收块 |
|--------|--------|--------|
| 1 | 4~5 | 6~7 |
| 2 | 8~9 | 10~11 |
| 3 | 12~13 | 14~15 |
| 4 | 16~17 | 18~19 |
| 5 | 20~21 | 22~23 |
| 6 | 24~25 | 26~27 |
| 7 | 28~29 | 30~31 |
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_listen()`
### 发现时间
Phase 8 压力测试2025-07
---
## Trap 03DHCP 包 xid 偏移错误
### 现象
Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid握手失败。
### 根因
错误使用了 `data[232:236]` 作为 xidWireshark 某旧版显示误导),实际 DHCP 标准偏移为 `data[4:8]`
### 正确偏移
- xid`data[4:8]`
- msg_type`data[242]`
- client_mac`data[28:34]`
- magic cookie`data[236:240]`
### 对应文件
`test/ch395f_socket_test.py` → DHCP server 模式
### 发现时间
Phase 6, 2025-07
---
## Trap 04UDP 发送后必须等待 SENDBUF_FREE
### 现象
UDP 发送后立即执行接收操作,接收长度始终为 0 或数据错误。
### 根因
CH395F 手册要求每次 `WRITE_SEND_BUF` 后必须等待 `SINT_STAT_SENBUF_FREE` 中断,否则下次写入或接收操作可能失败。
### 解决方案
每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查 `SINT_STAT_SENBUF_FREE` 标志。
### 对应文件
`Drivers/BSP/CH395F/ch395f.c` / test code in `ch395f_test.c`
### 发现时间
Phase 6 广播宣告模式2025-07
---
## Trap 05CH395F 与 RTL8305NBI 自动协商不兼容
### 现象
`ch395f_get_phy_status()` 始终返回 `PHY_DISCONN`,链路无法建立。
### 根因
CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败。
### 解决方案
初始化协议栈后强制设为 100M 全双工:
```c
ch395f_set_phy(CH395F_PHY_100M_FULL);
```
### 对应文件
`Drivers/BSP/CH395F/ch395f.c`
### 发现时间
项目初期硬件联调
---
## Trap 06TCP KeepAlive 参数必须为 500ms 倍数
### 现象
设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。
### 根因
CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。
`IDLE` 必须 > `INTVL`
### 正确值
```c
ch395f_set_keepalive_idle(60000); // 60s500 倍数)
ch395f_set_keepalive_intvl(5000); // 5s500 倍数)
ch395f_set_keepalive_cnt(3); // 3 次
```
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_init()`
### 发现时间
Phase 2 测试2025-07
---
## Trap 07TCP 关闭重连直接 close不要 disconnect
### 现象
`disconnect → close` 后 Socket 卡在 `FIN_WAIT_2` 数分钟,无法重新打开。
### 根因
`disconnect` 发送 FIN 将 Socket 推入 `FIN_WAIT_2`,此后 `close` 不再发 RST必须等远端发 FIN 才能关闭。
### 解决方案
直接调用 `ch395f_close_socket()`,在 ESTABLISHED 或 CLOSE_WAIT 下会发 RST 立即终止。
关闭后轮询 `ch395f_get_socket_status()` 等待 `sock=0x00``open_socket`
### 发现时间
Phase 2 TCP Client 测试2025-07
---
## Trap 08RECV 中断电平触发,避免无限循环
### 现象
`net_poll()` 中 RECV 中断无限触发主循环echo 等)永远得不到执行。
### 根因
CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就保持 `INT#` 低电平。
`do { ... } while(int_status != 0)` 会无限循环。
### 解决方案
`net_poll()` 使用 `do { ... } while(0)` 每次只处理一批中断,由主循环负责读取数据。
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_poll()`
---
## 附CH395F 驱动关键点
### 初始化顺序
- 必须按手册9.2.1节顺序:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`
- **IP/网关/掩码必须在 `INIT_CH395` 之前设置**INIT 会读取并锁定当前寄存器值到协议栈
- **`SET_PHY` 必须在 `INIT_CH395` 之后**,复位 MAC/PHY 建立物理链路,不影响已锁定的协议栈参数
- **`CMD_PING_ENABLE` 不需要显式调用**INIT 后默认可用
### SPI 通信
- 每次 SPI 事务需调用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹
- **大数据量收发使用 SPI2 DMA**`ch395f_write_send_buf()``ch395f_read_recv_buf()` 已改造为 DMA 批量传输DMA1_Stream3 RX, DMA1_Stream4 TX命令和配置操作仍使用逐字节轮询
- DMA 缓冲区:`s_spi2_dma_tx_buf[1500]` / `s_spi2_dma_rx_buf[1500]`4 字节对齐
### TCP 参数配置(必须在 INIT_CH395 之前)
- **重传参数**`SET_RETRAN_COUNT`默认12次最大20`SET_RETRAN_PERIOD`默认500ms最大1000ms总重传时间 = 次数 × 周期
- **KeepAlive 参数必须为 500ms 的倍数**`IDLE`默认20000ms`INTVL`默认15000ms且 IDLE > INTVL传入非 500 倍数的值导致 CH395F 内部定时器异常
- **KeepAlive 默认关闭**:需在 `SINT_STAT_CONNECT` 后调用 `ch395f_set_keepalive_enable(sock, 1)` 启用
### Socket 4~7 缓冲区
- CH395F 默认只给 Socket 0~3 分配独立收发缓冲(共 48 块 × 512B 用满Socket 4~7 为零块
- 多连接模式下使用 Socket 4~7 时,**必须在 `open_socket` 之前**显式分配:
- `ch395f_set_send_buf(sock, start_block, count)`
- `ch395f_set_recv_buf(sock, start_block, count)`
- 不分配会导致发送数据为固定垃圾内容(`0x0028` 填充)、接收缓冲区无法存储数据
### TCP 关闭重连
- **直接调用 `ch395f_close_socket()`,不要先调 `ch395f_tcp_disconnect()`**
-`disconnect → close`disconnect 发 FIN 推入 FIN_WAIT_2close 不再发 RSTSocket 卡住数分钟
-`close` 直接:在 ESTABLISHED 或 CLOSE_WAIT 下 close 发 RST 立即终止,瞬间回到 CLOSED
- 关闭后需轮询 `ch395f_get_socket_status()` 等待 `sock=0x00``open_socket`
### SOCK_TIMEOUT 处理
- 长时间无数据时可能触发 `SINT_STAT_SOCK_TIMEOUT`**不应视为致命错误**——记录日志后继续操作即可,不要因此关闭 Socket
### UDP 模式区分
- **DesIP=0xFFFFFFFF → UDP Server 模式**:接受任意来源数据,接收数据前 8 字节为信息头(`reserved[2] src_port[2] src_ip[4]`),回发前需设置 `SET_DES_IP``SET_DES_PORT`
- **DesIP=具体 IP → UDP Client 模式**:只接收指定 IP:Port 的数据,接收数据无信息头
### UDP 发送缓冲
- 每次 `ch395f_write_send_buf()` 后必须等待 `SINT_STAT_SENBUF_FREE` 中断,否则下次写入会被 CH395F 静默丢弃
---
## Trap 09PHY link up 后立即 open_socket 失败
### 现象
`net_listen()` 在 PHY_CHANGE 中断同一时刻调用 `ch395f_open_socket()` 返回错误码(非 BUSY 超时),导致首次 listen 失败。3 秒后重试成功。
```
[NET] PHY_CHANGE: 0x08 ← PHY 刚 link up (100M FULL)
[ERR] error listening on socket ← 同一秒 open_socket 失败
[FTP] waiting for connection... ← 3秒后重试成功
```
### 根因
CH395F 的 PHY link up 后,内部 TCP/IP 协议栈需要额外时间完成初始化ARP 缓存、路由表等)。在 PHY_CHANGE 中断触发的同一 `net_poll()` 迭代内立即调用 `OPEN_SOCKET`,芯片可能返回 `ERR_BUSY` 或其他错误码。
### 解决方案
1. **应用层重试兜底**FTP Server (`lftpd_start`) 的 `while(1)` 循环中listen 失败后 `vTaskDelay(3000)` 重试3 秒足够 PHY 稳定
2. **启动延时消峰**FTP 任务启动时 `osDelay(7000)`,等 netTask 完成 `net_init() → PHY 协商 → 首轮 net_poll()` 后再发起 listen此时 PHY 已稳定 2 秒以上,首次就成功
### 注意事项
- 此问题在单连接模式 (`FUN_PARA=0x00`) 和多连接模式 (`0x02`) 下均存在,与 FUN_PARA 无关
- 如果多个应用任务同时启动 listen可能全部首次失败、全部重试成功——建议各任务错峰启动
---
## Trap 10消息队列值拷贝导致结果无法回传
### 现象
`net_send()` / `net_recv()` / `net_listen()` / `net_accept()` / `net_close()` 等线程安全 API通过消息队列委托 netTask 执行)永远返回 `-1`,即使 netTask 内部操作成功。
### 根因
CMSIS-RTOS v2 `osMessageQueue` 传递 `net_msg_t` 结构体是**值拷贝**。调用方 puts 后 `msg.result = -1`栈变量netTask 侧 `osMessageQueueGet` 拿到的是**队列中的副本**,修改副本的 `msg.result = 0``xTaskNotifyGive` 通知调用方。调用方的栈变量 `msg.result` 仍然是 `-1`,从未被更新。
```c
// 调用方ftpTask
msg.result = -1;
osMessageQueuePut(&msg); // 值拷贝到队列
xTaskNotifyWait(&notified); // 等待通知
return msg.result; // 永远是 -1
// 处理方netTask
osMessageQueueGet(&msg); // 拿到的是队列副本
msg.result = actual_result; // 修改的是副本
xTaskNotify(...); // 通知回去了,但值丢了
```
### 解决方案
不通过消息队列回传结果,改用 **FreeRTOS 通知值**notification value携带返回值
**ProducernetTask**
```c
xTaskNotify(msg.caller, (uint32_t)msg.result, eSetValueWithOverwrite);
```
**Consumer调用方**
```c
uint32_t notified;
xTaskNotifyWait(0, 0, &notified, portMAX_DELAY);
msg.result = (int)notified;
```
### 注意事项
- 指针成员(`msg.buf`不受值拷贝影响——netTask 拿到的是同一个指针,可以读写调用方的缓冲区
- `net_connect()` 不使用消息队列(在 netTask 内直接调用 `_locked` 版本),不受此 bug 影响
- 内核级 API`net_send_sock` / `net_recv_sock` / `net_listen_locked`)直接操作,同样不受影响
---
## Trap 11`net_accept_locked` 拒绝 standalone_accept 的 ESTABLISHED 状态
### 现象
FTP 客户端 TCP 连接成功standalone accept220 欢迎语不发送,`lftpd_inet_accept()` 死循环。客户端连接成功但永远收不到任何 FTP 响应。
### 根因
`net_accept_locked()` 入口检查:
```c
if (p_listen_sock->state != NET_SOCK_STATE_LISTENING) {
return -1; // ← 直接拒绝!
}
```
standalone accept 后 state 已变为 `ESTABLISHED`,永远到不了后续的 `standalone_accept` 检查——死锁。
### 解决方案
```c
if (p_listen_sock->state != NET_SOCK_STATE_LISTENING &&
!(p_listen_sock->standalone_accept && p_listen_sock->state == NET_SOCK_STATE_ESTABLISHED)) {
return -1;
}
```
允许 `standalone_accept + ESTABLISHED` 组合通过。
### 注意事项
- 不影响多连接模式(多连接监听 Socket 状态始终为 LISTENING由数据 Socket 承载连接)
- 仅影响 standalone_accept 模式PASV 数据通道、单连接 FTP
---
## Trap 12FatFS `FF_USE_LFN=0` + 路径 `/` 前缀 → `FR_INVALID_NAME`
### 现象
FTP STOR 上传文件时 `f_open("/upload_test.txt", FA_WRITE | FA_CREATE_ALWAYS)` 返回 `err=6`FR_INVALID_NAME`upload_test.txt` 文件名 11+3 字符 > 8.3 限制同样触发 err=6。
### 根因
1. `ffconf.h``FF_USE_LFN = 0`(长文件名禁用),最大文件名 8+3 字符
2. FatFS 不接受 `/file` 格式的路径(需要 `file``0:file`
3. `lftpd_io_canonicalize_path("/", "file")` 输出必然带 `/` 前缀
4. 根目录路径 `"/"` 剥离后为空字符串,`f_stat("")` 同样失败
### 解决方案
```c
/* 剥离 "/" 前缀,根目录用 "." 表示 */
static const char *to_fatfs_path(const char *path) {
if (path == NULL || *path == '\0') return ".";
if (*path == '/') {
path++;
if (*path == '\0') return ".";
}
return path;
}
```
所有 `lftpd_io_*` 函数调用 FatFS 前统一使用 `to_fatfs_path(path)`
### 注意事项
- 长文件名需设置 `FF_USE_LFN = 1` 并配置 `FF_LFN_UNICODE`,会显著增加 RAM 占用
- `CWD .``CWD /` 同样受此问题影响,修复后正常工作
---
## 附FTP 支持改造完整问题清单
| # | 问题 | 类型 | 修复位置 |
|---|------|------|----------|
| 1 | 消息队列值拷贝6 个 API 永远返回 -1 | Trap 10 | `net_socket.c` |
| 2 | PHY link up 后 open_socket 失败 | Trap 09 | `freertos.c` + `lftpd.c` |
| 3 | `net_accept_locked` 拒绝 ESTABLISHED | Trap 11 | `net_socket.c` |
| 4 | CH395F 多连接模式不支持多监听 | 架构 | `net_socket.c: fun_para=0x00` |
| 5 | PASV 端口 0net_bind 无自动分配) | 缺功能 | `net_socket.c: s_dynamic_port` |
| 6 | FatFS 路径 `/` 不兼容 + LFN 禁用 | Trap 12 | `lftpd_io.c: to_fatfs_path()` |
| 7 | `cmd_pasv` 连关连开 listen 失败 | Trap 09 子类 | `lftpd.c: 3次重试` |
| 8 | `ftpTask` 启动时 PHY 未稳定 | Trap 09 子类 | `freertos.c: osDelay(7000)` |
| 9 | 会话结束后立即重建监听失败 | Trap 09 子类 | `lftpd.c: vTaskDelay(200ms)` |
| 10 | `s_file_open` 单文件限制 | 已知限制 | `lftpd_io.c` (全局变量) |
| 11 | CH395F DMA buffer 太小,分批读导致 recv_len 归零 | Trap 13 | `ch395f.c + lftpd.c` |
| 12 | `malloc` 嵌入式环境失败 | Trap 13 子类 | `lftpd.c: static buffer` |
| 13 | `net_listen_locked` 缓冲区覆盖s0/s1 争用 block 0-3 | Trap 02 翻版 | `net_socket.c: 单连接用默认 buf` |
| 14 | CH395F TIMEOUT 立即触发(数据连接无数据) | Trap 09 子类 | `net_socket.c: CLOSED 状态处理` |
| 15 | 发送单次写 >1KB 写穿 1KB 硬件发送 FIFO | Trap 20 | `net_socket.c: NET_SEND_CHUNK_MAX` |
| 15 | `NET_RECV_TIMEOUT_MS=5s` 控制通道超时断开 | 配置 | `net_config.h: 30000` |
| 16 | `receive_file` 0 bytes 当作成功 | 逻辑 | `lftpd.c: total==0 则失败` |
| 17 | MobaXterm 连数据端口不发数据(客户端特殊行为) | 外部 | Python 抓包确认,超时恢复 |
---
## Trap 13CH395F `recv_len` 分批读取时归零
### 现象
MobaXterm 上传文件时 CH395F 收到数据(`s1 int=0x04` RECV_OK 持续触发),但 `ch395f_get_recv_len(1)` 第一次返回 2920读 1024 字节后第二次永远返回 0。`GINT=0x0020` 反复触发但无数据可读——死循环。
### 根因
`ch395f_read_recv_buf` 读部分数据后CH395F 的 `recv_len` 寄存器**被重置为 0**(非递减)。缓冲区中剩余的 1896 字节仍然存在RECV_OK 标志为真),但 `ch395f_get_recv_len` 报告 0。
同时 `CH395F_DMA_BUF_SIZE=1500`header 4 + max 1496 data无法一次读取完整的 4096 字节 CH395F 接收缓冲区。
### 解决方案
1. `CH395F_DMA_BUF_SIZE` 从 1500 增大到 4100支持一次读 4096 字节 header+data
2. `receive_file` 缓冲区从 1024 增大到 4096一次性读空 CH395F 接收缓冲区
3. 不能使用 `malloc`(嵌入式堆可能不可用),改用 `static unsigned char s_recv_buf[4096]`
### 注意事项
- CH395F 接收缓冲区默认 socket 0: 4096B, socket 1: 4096B。读空后客户端继续发数据recv_len 重新递增——正常
- 清理中断状态后 `handle_timeout_event` 中的 `ch395f_close_socket` 必须显式调用,确保下次 `open_socket` 成功
---
## Trap 14close 后 DISCONNECT 滞后锁存,污染下次会话首次中断读取
### 现象
`close_socket` → 轮询 `GET_SOCKET_STATUS` 已确认 CLOSED 后,紧接的下一会话(重新 open + connect在第一次读 `GET_INT_STATUS_SN` 时读到**上一会话遗留的 DISCONNECT 事件**0ms 即命中),导致负向判定/状态机误判。实测TC-202 三轮重连后立即跑 TC-203PC 端握手实际成功(连接被接受),固件却因陈旧 DISCONNECT 抢先判"未连接"。
### 根因
- `wait-CLOSED` 轮询用的 `GET_SOCKET_STATUS` 是**纯查询,不清中断**
- TCP 拆除过程中断DISCONNECT可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存;
- 该事件无人消费,跨会话存活到下一次 `open_socket` 之后。
### 解决
1. **预清**:新会话 OPEN 前读一次 `GET_GLOB_INT_STATUS_ALL` + `GET_INT_STATUS_SN` 丢弃陈旧事件;
2. **反核实**:依据中断做结论前,用 `GET_SOCKET_STATUS` 验证实际状态一致(如"负向事件 vs ESTABLISHED"矛盾时以状态为准)。
### 参考
`test/ch395f_test_task.c: phase2_tc203()`(双保险实现);同类思路见 Phase 1 入口的 PHY_CHANGE 预清。
---
## Trap 15DHCP 成功后 IP 寄存器提交滞后
### 现象
`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF`,回读到的仍是**旧静态配置**(如 192.168.1.100而芯片实际已按新租约192.168.1.77)正常收发——串口打印与 Socket6 广播宣告均带出过期 IP。2026-08-24 上板实测:固件各检查全 PASS6/6但 PC 侧 HELLO 发往租约地址才得到应答,三方数据矛盾构成假阳性。
### 根因
DHCP 状态位仅代表协商流程完成IP/MASK/GW 寄存器的提交与状态置位是异步的,存在数百毫秒级窗口(实测 ~1s 内完成翻转)。
### 解决
1. **稳定轮询**:连续 3 次读 `GET_IP_INF`(间隔 300ms上限 8s结果一致且非零才采信
2. **交叉比对**PC 端将收到的宣告 IP 与所分配租约比对,不一致即报错兜底。
### 参考
`test/ch395f_test_task.c: phase4_run()``docs/CH395F_Test_Guide.md` 阶段 4 风险表 #4
---
## Trap 16NET 层 TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT
### 现象
NET 层 TCP Client阶段 7`net_connect()` 返回 0`net_poll()``sock0 CONNECT TIMEOUT`,连接永远建立不起来;而硬件层 TCP Client阶段 2同一 `net_init`/FUN_PARA=0x08同一目标 192.168.1.2:8081完全正常。PC 同网段、防火墙关闭、服务端正常监听。
### 根因
`net_connect_locked()``sockaddr_in.sin_addr.s_addr`(网络字节序,`net_inet_addr` 返回)按 **低字节在前** 拆成 `remote_ip_arr[4]`,得到 `[2,1,168,192]`,再交给 `ch395f_set_des_ip()`。但 **CH395F 的目的 IP/端口寄存器是大端(网络字节序)**(阶段 2 用 `test_parse_ip("192.168.1.2")` 直接得到 `[192,168,1,2]` 即成功,可证)。于是芯片把 SYN 发往 `2.1.168.192`——一个跨网段、不可达的地址SYN 被网关丢弃 → 无 SYN-ACK → `CONNECT TIMEOUT`
**误导性排查**`CMD_GET_REMOT_IPP_SN` 回读的是**小端**字节序(`ch395f.c` 注释 "IP和端口均为低字节在前")。把写反的 `[2,1,168,192]` 读回再按小端解释,恰好又得到 `192.168.1.2`,使回读日志看起来"目的地址正确",掩盖了真实写反。
### 解决
1. `net_connect_locked()` 提取 `remote_ip_arr` 改为 **大端**`arr[0]=(ip>>24)&0xFF … arr[3]=ip&0xFF`ip 为网络字节序 s_addr
2. 同样修正 UDP 发送路径 `net_send_locked()``set_des_ip` 的目的 IP 数组(同样低字节在前 bug
3. 诊断回读 `GET_REMOT_IPP` 时按其小端语义还原:`dip=(rip[3]<<24)|…|rip[0]`,再按 `>>24…` 打印,方可显示芯片实际目的 IP。
### 注意事项
- `net_inet_addr` 返回网络字节序;`sin_addr.s_addr` 一律按网络字节序理解。
- 任何写 CH395F IP/端口寄存器set_des_ip / set_ip_addr / set_gwip_addr / set_mask_addr都传**大端数组**,与 `test_parse_ip` 产物一致。
- 回读类命令GET_REMOT_IPP / GET_IP_INF 等)若文档标注"低字节在前",打印时务必先按小端重组再格式化,否则会误判。
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_connect_locked` 提取 `remote_ip_arr` 大端;`net_send_locked` UDP 目的 IP 同样修正;`[CONN]` 回读按小端重组);`Drivers/BSP/NET/net_types.h``remote_ip_arr` 注释改为大端);`test/ch395f_test_task.c``test_parse_ip` 可作为大端构建范本)。
### 发现时间
阶段 7 NET 层 TCP Client 上板实测2026-08-25。
---
## Trap 17单连接模式 client 重连被上一条连接的残留 DISCONNECT 秒断
### 现象
阶段 7 TC701首次 TCP Client 连接 + 回显正常TC702close 后同 Socket 0 重连 ×3每轮不同本地端口每次都 `[CONN] openOK` 成功、但紧接着 `sock0 DISCONNECTED, releasing`连接被秒断PC 端看到"已建立连接但对端立即关闭、未收到数据"。阶段 6 的 `auto_relisten`/断线处理逻辑同源,但 `auto_relisten` 标志本身不是元凶client 该标志为 0
### 根因
单连接模式只有 Socket 0新旧连接复用它。`net_close_locked()``close_socket()` 后芯片要等对端 FIN/ACK 才把 Socket 置 `SOCK_CLOSED` 并上报 `DISCONNECT` 中断耗时数毫秒。TC702 几乎在 TC701 关闭同时复用 Socket 0 去 `tcp_connect`:旧连接的 `DISCONNECT` 中断在 TC702 已 `ESTABLISHED` 之后才到达,`net_poll()``handle_disconnect_event()` 把状态置 `CLOSED` → 新连接被误判为"刚建立就断开"。`GET_REMOT_IPP` 回读仅确认目的地址正确,掩盖不了此事件串扰。
### 解决
1. `net_close_locked()``close_socket()` 后轮询 `ch395f_get_socket_status()` 直到 `CH395F_SOCKET_CLOSED`,再读 `ch395f_get_sock_int_status()` / `ch395f_get_glob_int_status_all()` 清掉中断状态(上限 500msLAN 通常几毫秒),彻底排空上一条连接残留的 `DISCONNECT`
2. `net_connect_locked()``open_socket()` 成功后再次读空本 Socket 中断状态,作为双保险。
### 注意事项
- 任何"关闭→复用同一 Socket 0 重新建连"的场景都必须先排空旧连接的中断,否则旧 `DISCONNECT` 会串扰新连接。
- 若关闭后阻塞等待影响实时性(多连接模式另有 Socket应改为非阻塞的"代次/epoch"机制;单连接模式直接排空即可。
- `net_close_locked` 内阻塞 `osDelay` 仅在 netTask 上下文,单连接模式无其它并发 Socket安全。
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_close_locked` 排空残留 DISCONNECT`net_connect_locked` open 后清中断)。
### 发现时间
阶段 7 NET 层 TCP Client 上板实测2026-08-25。
## Trap 18多 Socket 模式切换 + 8 个 Socket 缓冲分配
### 现象
阶段 7 仅测了 Socket 0单连接模式只有 Socket 0 可用)。要验证 Socket 1~7 也能作为 client必须把 `FUN_PARA` 从单 Socket 模式切到多 Socket 模式,否则 `open_socket(1~7)` 被芯片拒绝/无响应1~7 完全不可用。
### 根因
`CMD_SET_FUN_PARA`**bit0** 决定 Socket 数量模式:`0`=单 Socket 模式(仅 Socket 08KB 缓冲全归它);`1`=多 Socket 模式Socket 0~7 全部可用,缓冲需自行分配)。之前 `net_init``0x08`bit0=0即单 Socket 模式,故 1~7 不存在。注意 bit1TCP Server 单/多连接)与 bit0 是两回事:本方案只翻 bit0多 Socketbit1 保持 0TCP Server 单连接),即"8 个 Socket 可用,但 Socket 0 作服务端时只接 1 客户端"。
### 解决
1. `net_init``ch395f_set_fun_para(0x09)`bit0=1, bit1=0, bit3=1
2. `ch395f_init()` 之后,循环 0~7 用 `ch395f_set_send_buf(sock, i*6+4, 2)` / `ch395f_set_recv_buf(sock, i*6, 4)` 给每个 Socket 分配独立收发缓冲。按手册 §5.4548 块 × 512B = 24KB+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB)`ch395f_set_tcp_mss(1024)`,恰好用尽 0~47 块、互不重叠。
3. MSS 选取依据 §9.2.8:接收缓冲(2KB) ≥ 2×MSS(1024) 满足建议、≥ MSS 满足必须;发送缓冲(1KB) ≤ 8KB 满足必须。MSS 取 1024 为 8-Socket 共享 24KB 时的最大值。
4. `net_init` 缓冲分配顺序遵循 Trap 01先配数据 Socket再配监听 Socket 0本 Net 层 Socket 按需 open故统一在 init 一次性配好。UDP 路径 `net_socket()` 中对 Socket 4~7 的兜底重设也同步为同一布局。
5. PC 端 `tcp_server` 改为每连接一线程的并发回显(`--max-conn` 为并发计数),否则 Phase 7 全量连接会触发累计计数上限而拒掉 4~7Trap 19
### 注意事项
- 4~7 的缓冲必须显式分配,否则其 CONNECT/RECV 中断不触发Trap 01/02
- 上述布局**以手册 §5.45 的 48×512B 缓冲几何为准**,并已由 Phase 7 TC70440/40 PASS实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误。Socket 4~7 分配与 8 路并发回显均验证通过。
- 多 Socket 模式下 TCP Server 单连接语义不变Socket 0 监听且连接停留在本 SocketSockets 1~7 由应用当作独立 client 使用。
- `net_poll``GET_GLOB_INT_STATUS_ALL`2 字节,支持 0~7并遍历 0~7Socket n 中断位即 `bit(n+4)`Socket 0~3→bit4~7Socket 4~7→bit8~11原代码正确无需改动。
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_init` FUN_PARA + 缓冲分配 + MSS`test/net_test_task.c`(新增 TC704 八 Socket 并发回显);`test/ch395f_socket_test.py``tcp_server` 并发 + `--max-conn`)。
### 发现时间
阶段 7 扩展 8 Socket 测试2026-08-25。
## Trap 20发送缓冲区单次写入超过硬件 FIFO 上限(写穿)
### 现象
大文件/批量发送(如阶段 8 的 100KB或 FTP 上传)时,数据在中间某处错乱、连接被对端 RST、或在 `WRITE_SEND_BUF` 后芯片不再产生 `SEND_OK`,表现为发送卡死或后续字节丢失。该问题只在单次发送长度 > 1KB 时稳定复现。
### 根因
`net_send_locked`TCP`net_sendto`UDP在分块时以 **`NET_DMA_MAX_PAYLOAD`(2KB) / `NET_SEND_BUF_SIZE`(4KB)** 作为单次写长度上限,但 CH395F 每个 Socket 的**硬件发送 FIFO 只有 `CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE` = 2×512 = 1KB**(由 MSS=1024 推导,见 `ch395f.h`)。当某次调用 `net_send/sendto` 传入 >1KB 数据,驱动会在 `send_ready` 置位后一次性 `WRITE_SEND_BUF(2KB/4KB)`,直接**写穿 1KB 发送 FIFO**(超出部分覆盖/丢失或破坏 FIFO 指针),导致后续数据错位。原代码误把"SPI DMA 缓冲上限(2KB)"或"软件 API 缓冲(4KB)"当成硬件发送缓冲上限。
### 解决
1. `ch395f.h` 新增硬件缓冲字节宏:`CH395F_SEND_BUF_SIZE_BYTES = CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE`=1KB并补充 `CH395F_RECV_BUF_SIZE_BYTES`
2. `net_socket.c` 新增 `NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES)`,即**以硬件发送 FIFO 为硬上限**(当前 1KB < 2KB DMA 载荷,故生效 1KB
3. `net_send_locked``net_sendto` 的单次写长度上限均由原来的 `NET_DMA_MAX_PAYLOAD`/`NET_SEND_BUF_SIZE` 改为 `NET_SEND_CHUNK_MAX``send_ready` 门控保证只有 FIFO 空闲时才写入,故写入 ≤1KB 安全(写满后等 `SEND_OK` 再写下一块)。
4. 接收方向不受影响:`net_recv_locked` 只读 `ch395f_get_recv_len`(≤ 接收 FIFO 2KB并受调用方 `len` 截断,不会写穿。
### 注意事项
- 该上限由 `CH395F_TCP_MSS` 推导,若将来调大 MSS 使发送缓冲 >2KB`NET_SEND_CHUNK_MAX` 自动跟随(仍取硬件缓冲与 DMA 载荷的较小者),无需改此处逻辑。
- 调用方(阶段 8、FTP `lftpd`)仍可按任意长度调用 `net_send`,实际分块由驱动保证 ≤ 硬件 FIFO。
### 对应文件
`Drivers/BSP/CH395F/ch395f.h``CH395F_SEND_BUF_SIZE_BYTES` 等);`Drivers/BSP/NET/net_socket.c``NET_SEND_CHUNK_MAX` + `net_send_locked`/`net_sendto` 分块上限)。
### 发现时间
阶段 8 大文件传输实现2026-08-26。

View File

@@ -0,0 +1,135 @@
# GD5F2GQ5UE 陷阱记录
## Trap 01 — 块擦除地址错误(有效)
**发现时间**2026-07-21
**现象**
`gd5f_block_erase()` 使用 `byte_addr = block × 128KB`(字节地址)作为 D8h 命令的入参,但 GD5F2GQ5UE 要求的是**页地址**`block × 64`)。
只有 block 0 的 byte_addr=0 与 page_addr=0 巧合一致,其余块全部指向了错误的物理块。导致:
- 验证流程从未擦除过大部分块,厂标坏块每次重启都恢复
- 基于错误擦除的实验结论全部无效
**根因**
- `gd5f_block_erase(block_addr)``block_addr × GD5F_BLOCK_SIZE` 计算地址
- 芯片 D8h 命令将 24-bit 地址作为页地址(行地址)解析:`RA<16:6>` 选块,`RA<5:0>` 选页内页
- 对于 block=1发送 `{0xD8, 0x00, 0x02, 0x00}` → 芯片视为 page=512 → 擦除 block=8
- 导致 block 1~7、9~15、17~23…等大量块从未被擦除
**修复**:改用 `page_addr = block_addr × GD5F_PAGES_PER_BLOCK`(块首页地址)
**验证结果**(修复后):
- 全部 2048 块正确擦除,厂标全部清除,重启后 BBT scan 为 0
- ECC 开启时 BBT rescan 正常工作
- ECC 校验码正确生成ECCS=0
- Block 0 无特殊行为
**经验教训**
- D8h 和 13h/10h 都使用 24-bit 行地址,格式一致
- 块擦除不使用 `block × block_size`(字节偏移),而用块首页地址
- 芯片手册的 memory mapping 必须严格遵守
## Trap 02 — 测试缓冲越界导致跨页比对误判(有效)
**发现时间**2026-08-26
**现象**
`gd5f_test_task.c``s_wbuf` / `s_rbuf` 按单页大小定义:
```c
static uint8_t s_wbuf[GD5F_PAGE_SIZE]; /* 2048 字节 */
static uint8_t s_rbuf[GD5F_PAGE_SIZE]; /* 2048 字节 */
```
TC-GD5F-302 跨页写读用 `gd5f2gq5ue_write(base + 1000, s_wbuf, 3000)` /
`read(..., s_rbuf, 3000)`。驱动实际把 3000 字节正确写入了 NAND但测试缓冲只有
2048 字节,发生**数组越界**
- 前 2048 字节写入合法区page0 全 + page1 前 1000 字节)→ 比对正确;
- page1 剩余 952 字节被写到 `s_rbuf[2048..2999]`(越界),落入相邻内存,**丢失**
- 比对时读 `s_rbuf[2048..2999]` 为越界垃圾 → 误判 `cross-page MISMATCH`
**根因**
测试缓冲未覆盖实际最大读写长度(跨页场景超过单页 2048 字节)。
**修复**
将缓冲放大到 ≥ 最大读写长度(如 `GD5F_PAGE_SIZE * 2`,即 4096
```c
static uint8_t s_wbuf[GD5F_PAGE_SIZE * 2];
static uint8_t s_rbuf[GD5F_PAGE_SIZE * 2];
```
**验证结果**
缓冲放大后 TC-GD5F-302 一次通过,不再误判。
**经验教训**
- 任何“跨页 / 大于单页”的读写测试,缓冲必须按**实际长度上限**而非单页大小分配
- 越界写会污染相邻全局变量,可能引起其它用例莫名失败,定位时优先怀疑缓冲尺寸
## Trap 03 — 驱动读写擦缺越界检查(有效 / 健壮性缺口)
**发现时间**2026-08-26
**现象**
TC-GD5F-1001 用 `offset = GD5F_TOTAL_SIZE`(恰好超出末尾 1 字节)做 read/write
期望驱动拒绝(返回错误),但 `gd5f2gq5ue_read` / `write` / `erase` 原实现**无任何边界校验**
直接计算 `page_addr = offset / PAGE_SIZE` 后继续操作,返回 `GD5F_OK` → 测试 FAIL。
同时这也意味着正常调用若传入越界参数,会静默访问到回绕后的非法页。
**根因**
`gd5f2gq5ue_read` / `write` / `erase` 入口未校验 `offset` / `size` 是否在
`[0, GD5F_TOTAL_SIZE]` 容量范围内。
**修复**
在三个函数入口统一加边界检查FTL/diskio 均在容量内访问,不受影响):
```c
if (offset < 0 || (unsigned long)offset + size > (unsigned long)GD5F_TOTAL_SIZE) {
return GD5F_ERROR;
}
```
`erase` 在原有的块对齐检查之后、循环之前加同一判断即可。
**验证结果**
- TC-GD5F-1001 read/write 越界均被拒绝ret=-1测试 PASS
- TC-GD5F-1002 size=0 仍返回 `GD5F_OK`offset 合法且 size=0 不越界),符合预期
- 该检查还顺带逮到 Trap 04 的测试越界擦除(见下)
**经验教训**
- 面向字节偏移的裸 NAND 接口必须做容量边界检查,调用方传错参数时尽早失败
- 无越界检查的驱动容易“静默回绕”,问题极难定位
## Trap 04 — gd5f_find_run 返回语义与多块擦除窗口错位(测试 bug
**发现时间**2026-08-26
**现象**
TC-GD5F-402 多块擦除(`gd5f_find_run(3)` 后擦 3 块)返回 `ret=-1`
经 Trap 03 的边界检查定位:实际要擦的块超出了设备末尾。
**根因**
`gd5f_find_run(n)` 原实现返回的是**连续好块的末尾块号** `top`
(校验 `top, top-1, ..., top-(n-1)` 共 n 块),但调用方把返回值当作**起始块号**使用:
```c
base3 = (long)blk3 * GD5F_BLOCK_SIZE;
gd5f2gq5ue_erase(base3, 3 * GD5F_BLOCK_SIZE); /* 实际擦 blk3, blk3+1, blk3+2 */
```
`gd5f_find_run(3)` 返回的末尾块贴到设备末尾(例如 2047即块 2045/2046/2047 为好块)
时,调用方实际要擦 `2047, 2048, 2049`,块 2048/2049 越界。
`n=1` 时首尾块号相同所以不暴露,仅 `n>1` 暴露。
**修复**
统一让 `gd5f_find_run` 返回**起始块号**,与 `n=1` 的用法一致,并仍避开块 0
```c
if (ok) {
return top - (uint32_t)(n - 1); /* 返回连续好块的起始块号 */
}
```
循环条件保持 `top >= (uint32_t)(n - 1) + 1`,保证起始块 ≥ 1。
**验证结果**
修复后 TC-GD5F-402 多块擦除ret=0及“擦后全 0xFF”均 PASS
全套用例最终 60/60 PASSED。
**经验教训**
- 返回“连续区间”的辅助函数,其返回值是起点还是终点必须在注释/命名中明确,
调用方与实现必须一致
- 这类错位 bug 往往只在“区间贴到设备边界”时才触发,正常情况能过,建议测试覆盖边界块

Some files were not shown because too many files have changed in this diff Show More