Compare commits
22 Commits
9ed4c22022
...
research/n
| Author | SHA1 | Date | |
|---|---|---|---|
| 78eff31de7 | |||
| 565f23ea18 | |||
| 02a05a7a63 | |||
| 4e9ac0d9c3 | |||
| 615a9b13e9 | |||
| cf33171c8f | |||
| 88160a5134 | |||
| 0a707034cc | |||
| d6bc3f6253 | |||
| 4375873083 | |||
| fc88bc8752 | |||
| a7f367f6a3 | |||
| c646df5c2a | |||
| 038a2542af | |||
| 46a74f24f3 | |||
| 75cf4c9fbf | |||
| 6d404d136e | |||
| d48be81478 | |||
| 66a1e8fd6a | |||
| f50b9c9e66 | |||
| fbe0726991 | |||
| e06d849007 |
14
.opencode/command/build.md
Normal file
14
.opencode/command/build.md
Normal 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
284
AGENTS.md
@@ -1,230 +1,94 @@
|
||||
# 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/ # CubeMX 生成的外设初始化 + main
|
||||
├── Inc/ # CubeMX 生成的头文件
|
||||
├── 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 CMSIS(CubeMX 生成)
|
||||
├── Lib/
|
||||
│ └── FlashDB/ # FlashDB 数据库库(v2.2.99)
|
||||
│ ├── src/ # FlashDB 核心源码
|
||||
│ ├── inc/ # FlashDB 头文件
|
||||
│ └── port/fal/ # FAL 抽象层
|
||||
├── MDK-ARM/ # Keil MDK 工程文件
|
||||
├── docs/ # 参考文档
|
||||
└── STM32F407-Demo.ioc # STM32CubeMX 项目源文件
|
||||
Src/main.c → 外设初始化 → app_main_init → gd5f2gq5ue_init → tpafe5160_init
|
||||
→ osKernelInitialize → MX_FREERTOS_Init → osKernelStart
|
||||
→ [FreeRTOS tasks: defaultTask, netTask, ftpTask]
|
||||
```
|
||||
|
||||
- **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 0(TCP LISTEN)
|
||||
- **新文件需手动添加到 Keil MDK 工程**才能编译
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 路径 | 说明 |
|
||||
|---|---|
|
||||
| `Src/main.c` | 程序入口,初始化序列及主循环 |
|
||||
| `Drivers/BSP/CH395F/ch395f.c/h` | CH395F 以太网芯片 SPI 驱动 |
|
||||
| `Drivers/BSP/NET/net_socket.c/h` | BSD Socket API 网络抽象层 |
|
||||
| `Drivers/BSP/NET/net_select.c/h` | select/poll I/O 多路复用 |
|
||||
| `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c/h` | GD5F2GQ5UE NAND Flash SPI 驱动 |
|
||||
| `Drivers/BSP/GD5F2GQ5UE/fal_flash_gd5f2gq5ue.c` | FAL 设备适配层 |
|
||||
| `Drivers/BSP/TPAFE5160/tpafe5160.c/h` | TPAFE5160 ADC 并行接口驱动 |
|
||||
| `Src/freertos.c` | FreeRTOS task 创建和任务函数(CubeMX 生成 + 手写) |
|
||||
| `Inc/FreeRTOSConfig.h` | FreeRTOS 内核配置 |
|
||||
| `MDK-ARM/STM32F407-Demo.uvprojx` | Keil MDK 项目文件 |
|
||||
| `STM32F407-Demo.ioc` | STM32CubeMX 项目源文件 |
|
||||
| `Src/main.c` | 入口,CubeMX 生成 + USER CODE 区域 |
|
||||
| `Src/freertos.c` | FreeRTOS task 创建,CubeMX 生成 + USER CODE 区域 |
|
||||
| `App/task/net_task.c` | netTask 主循环(网络初始化 + 轮询) |
|
||||
| `Drivers/BSP/NET/` | BSD Socket API 网络抽象层 |
|
||||
| `Drivers/BSP/CH395F/` | CH395F SPI 驱动 |
|
||||
| `Drivers/BSP/GD5F2GQ5UE/` | NAND Flash 驱动 + dhara FTL + FatFS diskio |
|
||||
| `Drivers/BSP/TPAFE5160/` | ADC 并行接口驱动 |
|
||||
| `Drivers/BSP/SD2506/` | RTC 驱动 |
|
||||
| `Drivers/BSP/RS485/` | RS-485 通信(UART5 + PD0 DE) |
|
||||
|
||||
## 构建
|
||||
## 驱动关键点
|
||||
|
||||
仅支持 Keil MDK-ARM v5 (ARMCC)。打开 `MDK-ARM/STM32F407-Demo.uvprojx` 编译。
|
||||
|
||||
- 编译器:ARMCC V5.06 update 7
|
||||
- 优化等级:`-O4` (项目级),`spi.c`/`usart.c` / HAL 源文件使用 `-O0`
|
||||
- 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"`
|
||||
详见 `docs/` 目录:
|
||||
- `CH395F_Trap_Records.md` — CH395F 已知陷阱
|
||||
- `GD5F2GQ5UE_Trap_Records.md` — GD5F2GQ5UE 已知陷阱
|
||||
- `CH395F_Test_Guide.md` — 网络测试说明
|
||||
|
||||
## 注意
|
||||
|
||||
- `Inc/` 和 `Src/` 中 CubeMX 生成的文件带有 `USER CODE BEGIN`/`END` 标记,自定义代码应写在这些区域之间
|
||||
- `ch395f.c/h`、`gd5f2gq5ue.c/h`、`fal_flash_gd5f2gq5ue.c` 为纯手工代码,不受 CubeMX 保护
|
||||
- `tpafe5160.c/h` 为纯手工代码,不受 CubeMX 保护
|
||||
- CH395F 每次 SPI 事务需调用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹
|
||||
- GD5F2GQ5UE 的 `gd5f2gq5ue.c` 中声明了 `extern SPI_HandleTypeDef hspi1`,需确保 SPI1 已初始化
|
||||
- FlashDB 使用 FAL 模式,`fdb_cfg.h` 中定义 `FDB_USING_FAL_MODE`,`fal_cfg.h` 中定义分区表
|
||||
- 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
|
||||
- `Inc/` 和 `Src/` 中 CubeMX 生成的文件带有 `USER CODE BEGIN`/`END` 标记,自定义代码写在这些区域之间
|
||||
- `Drivers/BSP/` 下的驱动文件为纯手工代码,不受 CubeMX 保护
|
||||
- CH395F 每次 SPI 事务需用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹
|
||||
- CH395F SPI PCB设计问题,现在分频系数必须为 8,速度相对较低
|
||||
- FreeRTOS V10.3.1 via CMSIS-RTOS V2,HAL 时基用 TIM7(非 SysTick)
|
||||
- NVIC 优先级分组 4 位,外设中断优先级 ≥ 5(`configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`)
|
||||
|
||||
## CH395F 驱动关键点
|
||||
## 源码编辑与编码安全(铁律)
|
||||
|
||||
驱动初始化顺序、SPI/DMA 通信、TCP/UDP 参数配置、Socket 缓冲区分配等完整关键点见 `docs/CH395F_Trap_Records.md` → 附:CH395F 驱动关键点。
|
||||
本工程源码含大量中文注释,曾因错误的文本重写方式导致整文件乱码(`U+FFFD` 替换符,不可逆)。以下规则必须遵守:
|
||||
|
||||
## 测试说明
|
||||
|
||||
- **测试代码文件**:`Drivers/BSP/CH395F/ch395f_test.h` / `ch395f_test.c`
|
||||
- **PC 端测试脚本**:`test/ch395f_socket_test.py`,支持 Python 3.7+
|
||||
- **调用入口**:`Src/main.c` 的 `USER CODE BEGIN 2` 区域
|
||||
- **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 0(CPOL=0, CPHA=0),时钟 42MHz
|
||||
- 初始化必须按顺序:复位 → 读 ID → 使能 ECC(B0h=10h)→ 解除块保护(A0h=00h)
|
||||
- SET_FEATURE 命令前必须先发写使能(06h)
|
||||
- 块擦除(D8h)参数是字节地址(块编号 × 128KB),不是块编号
|
||||
- 读取 ID(9Fh)返回 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µs,64倍过采样时 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 中断电平触发无限循环
|
||||
- **绝不用 `Get-Content` / `Set-Content` / `cmd` 文本重写含中文的源码文件**。这类"整文件重写"会按系统默认编码重新解码再写回,多字节中文必被 `U+FFFD` 替换且不可逆。
|
||||
- **改代码只用 Edit 工具**(精确子串替换,不重新编解码整个文件,安全);批量替换用 Edit 的 `replaceAll`。
|
||||
- 必须脚本级替换时,使用显式且一致的编码(如 Python `open(p, encoding="utf-8")` 同编码回写),并先确认工程编码(Keil 工程常用 GBK)——但**最稳的是别碰**,只用 Edit / git。
|
||||
- **发现乱码先别急着 `git checkout`**:`git checkout` 会丢弃所有未提交工作,使损失扩大。先评估能否用 Edit 仅改坏掉的那几行。
|
||||
- 验证是否真损坏用字节级检查(统计 `EF BF BD` 出现次数),不要依赖终端/Read 渲染判断。
|
||||
|
||||
36
App/app_cfg.h
Normal file
36
App/app_cfg.h
Normal 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 - TIM2(32-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
42
App/app_main.c
Normal 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
27
App/app_main.h
Normal 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
0
App/protocol/.gitkeep
Normal file
369
App/sys_clock.c
Normal file
369
App/sys_clock.c
Normal 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 congruence,0=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
75
App/sys_clock.h
Normal 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
45
App/task/adc_task.c
Normal 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
25
App/task/adc_task.h
Normal 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
114
App/task/net_task.c
Normal 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~5)netTask 不创建,
|
||||
* 由 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
31
App/task/net_task.h
Normal 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
45
App/task/rs485_task.c
Normal 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
25
App/task/rs485_task.h
Normal 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
138
App/time_sync.c
Normal 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
40
App/time_sync.h
Normal 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
122
App/util/crc.c
Normal 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
28
App/util/crc.h
Normal 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
164
App/util/ringbuf.c
Normal 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
42
App/util/ringbuf.h
Normal 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
@@ -110,6 +110,33 @@ extern "C" {
|
||||
/* 3 字节输入 + 数据输入(socket + 长度 + 数据) */
|
||||
#define CH395F_CMD_WRITE_SEND_BUF_SN 0x39U
|
||||
|
||||
/*
|
||||
* TCP MSS(CH395F_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.45:48 块 × 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 + 4(4 为命令头开销),有效载荷 = 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_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)
|
||||
* 返回值:中断状态 uint16_t(低字节=Socket 0~3 + PHY/DHCP,高字节=Socket 4~7 + DHCPv6)
|
||||
|
||||
@@ -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 Echo:netTask 中运行,用 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(接收 HELLO),DesIP=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
|
||||
}
|
||||
@@ -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 Echo(netTask 中运行) */
|
||||
//#define ENABLE_PHASE5_TESTS /* NET 层 UDP Echo(netTask 中运行) */
|
||||
//#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 Echo(netTask 中运行)
|
||||
* 通过: 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 Echo(netTask 中运行)
|
||||
* 通过: 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 */
|
||||
@@ -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 */
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 写入粒度,单位:bit,STM32F4 字节可编程 = 8 */
|
||||
#define FDB_WRITE_GRAN 8
|
||||
|
||||
/* 调试输出使能 */
|
||||
/*#define FDB_DEBUG_ENABLE*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __FDB_CFG_H */
|
||||
@@ -7,11 +7,12 @@
|
||||
* 修改记录:
|
||||
* 2026-07-16 王建锋 创建初始版本
|
||||
* 2026-07-17 王建锋 切换硬件 SPI,修正擦除地址,参考 NuttX 驱动
|
||||
* 2026-08-27 补充函数注释(代码规范 V1.0);修正工厂坏块扫描为首页;增加 BBT 持久化
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include "gd5f2gq5ue.h"
|
||||
#include <string.h>
|
||||
#include "gd5f2gq5ue.h"
|
||||
|
||||
/*
|
||||
* 调试输出配置
|
||||
@@ -20,25 +21,72 @@
|
||||
#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 句柄声明 */
|
||||
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,
|
||||
const uint8_t *p_buf, size_t size);
|
||||
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 位)
|
||||
@@ -47,8 +95,7 @@ static int gd5f_block_erase(uint32_t block_addr);
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:循环读取状态寄存器直到 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 addr = GD5F_REG_STATUS;
|
||||
uint8_t status = 0;
|
||||
@@ -75,10 +122,8 @@ static int gd5f_wait_busy(uint32_t timeout_ms)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 成功
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:CS# 拉低后发送 06h 命令再拉高
|
||||
*/
|
||||
static int gd5f_write_enable(void)
|
||||
{
|
||||
* 函数说明:CS# 拉低后发 06h 命令再拉高 */
|
||||
int gd5f_write_enable(void) {
|
||||
uint8_t cmd = GD5F_CMD_WRITE_ENABLE;
|
||||
|
||||
GD5F_CS_LOW();
|
||||
@@ -90,13 +135,11 @@ static int gd5f_write_enable(void)
|
||||
|
||||
/*
|
||||
* 函数功能:读取状态寄存器
|
||||
* 入口参数:p_status - 状态值输出指针 uint8_t* 不为 NULL
|
||||
* 出口参数:p_status - 状态值输出指针 uint8_t* 不为 NULL
|
||||
* 返回值:0 - 成功
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:发送 0Fh + C0h 地址后读取1字节状态值
|
||||
*/
|
||||
static int gd5f_read_status(uint8_t *p_status)
|
||||
{
|
||||
* 函数说明:发 0Fh + C0h 地址后读 1 字节状态 */
|
||||
int gd5f_read_status(uint8_t *p_status) {
|
||||
uint8_t cmd = GD5F_CMD_GET_FEATURE;
|
||||
uint8_t addr = GD5F_REG_STATUS;
|
||||
|
||||
@@ -111,14 +154,13 @@ static int gd5f_read_status(uint8_t *p_status)
|
||||
|
||||
/*
|
||||
* 函数功能:页读取(将数据从存储阵列加载到内部缓存)
|
||||
* 入口参数:page_addr - 页地址 uint32_t
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* 入口参数:page_addr - 页地址 uint32_t 0 ~ GD5F_TOTAL_BLOCKS*64-1
|
||||
* 返回值:0 - 成功,其它 - 错误
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:发送 13h + 3字节行地址,等待 OIP 清零
|
||||
* 函数说明:发 13h + 3字节行地址,等待 OIP 清零
|
||||
*/
|
||||
static int gd5f_page_read(uint32_t page_addr)
|
||||
{
|
||||
uint8_t cmd[4];
|
||||
int gd5f_page_read(uint32_t page_addr) {
|
||||
uint8_t cmd[4] = {0};
|
||||
|
||||
cmd[0] = GD5F_CMD_PAGE_READ;
|
||||
cmd[1] = (page_addr >> 16) & 0xFF;
|
||||
@@ -135,16 +177,14 @@ static int gd5f_page_read(uint32_t page_addr)
|
||||
/*
|
||||
* 函数功能:从内部缓存读取数据
|
||||
* 入口参数:column - 列地址(页内偏移) uint16_t 0 - 2047
|
||||
* p_buf - 数据输出缓冲区 uint8_t* 不为 NULL
|
||||
* size - 读取字节数 size_t > 0
|
||||
* size - 读取字节数 size_t > 0
|
||||
* 出口参数:p_buf - 数据输出缓冲区 uint8_t* 不为 NULL
|
||||
* 返回值:0 - 成功
|
||||
* 限定条件:必须先调用 gd5f_page_read 完成数据加载
|
||||
* 函数说明:发送 0Bh + 2字节列地址 +1字节 dummy 后读取数据
|
||||
*/
|
||||
static int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
|
||||
size_t size)
|
||||
{
|
||||
uint8_t cmd[4];
|
||||
* 函数说明:发 0Bh + 2字节列地址 +1字节 dummy 后读取数据 */
|
||||
int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
|
||||
size_t size) {
|
||||
uint8_t cmd[4] = {0};
|
||||
|
||||
cmd[0] = GD5F_CMD_READ_FROM_CACHE;
|
||||
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();
|
||||
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();
|
||||
|
||||
return GD5F_OK;
|
||||
@@ -163,32 +220,73 @@ static int gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
|
||||
* 函数功能:页编程(将数据写入指定页)
|
||||
* 入口参数:page_addr - 页地址 uint32_t
|
||||
* column - 列地址 uint16_t 0 - 2047
|
||||
* p_buf - 数据缓冲区 const uint8_t*
|
||||
* size - 写入字节数 size_t > 0
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* 限定条件:目标区域已擦除
|
||||
* p_buf - 数据缓冲区 const uint8_t*
|
||||
* size - 写入字节数 size_t > 0
|
||||
* 返回值:0 - 成功,其它 - 错误 * 限定条件:目标区域已擦除
|
||||
* 函数说明:1. 写使能 - 02h 加载数据 - 10h 执行编程 - 等待完成
|
||||
* 2. 编程完成后检查 P_FAIL 位
|
||||
*/
|
||||
* 2. 编程完成后检查 P_FAIL 位 */
|
||||
static int gd5f_page_program(uint32_t page_addr, uint16_t column,
|
||||
const uint8_t *p_buf, size_t size)
|
||||
{
|
||||
int ret = GD5F_OK;
|
||||
uint8_t cmd[4];
|
||||
uint8_t status = 0;
|
||||
|
||||
const uint8_t *p_buf, size_t size) {
|
||||
int ret;
|
||||
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[1] = (column >> 8) & 0xFF;
|
||||
cmd[2] = column & 0xFF;
|
||||
cmd[1] = (uint8_t)(column >> 8);
|
||||
cmd[2] = (uint8_t)(column);
|
||||
|
||||
GD5F_CS_LOW();
|
||||
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();
|
||||
|
||||
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[1] = (page_addr >> 16) & 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();
|
||||
|
||||
ret = gd5f_wait_busy(1000);
|
||||
if (ret != GD5F_OK) {
|
||||
return ret;
|
||||
}
|
||||
if (ret != GD5F_OK) return ret;
|
||||
|
||||
gd5f_read_status(&status);
|
||||
if (status & GD5F_STATUS_P_FAIL) {
|
||||
return GD5F_PROGRAM_FAIL;
|
||||
}
|
||||
if (status & GD5F_STATUS_P_FAIL) return GD5F_PROGRAM_FAIL;
|
||||
|
||||
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 寄存器
|
||||
* 入口参数:addr - 寄存器地址 uint8_t
|
||||
* data - 写入数据 uint8_t
|
||||
* 返回值:0 - 成功
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:先写使能,发送 1Fh + 地址 + 数据,等待操作完成
|
||||
*/
|
||||
static int gd5f_set_feature(uint8_t addr, uint8_t data)
|
||||
{
|
||||
* 函数说明:先写使能,发 1Fh + 地址 + 数据,等待操作完成 */
|
||||
static int gd5f_set_feature(uint8_t addr, uint8_t data) {
|
||||
uint8_t cmd[3];
|
||||
|
||||
gd5f_write_enable();
|
||||
@@ -243,22 +350,20 @@ static int gd5f_set_feature(uint8_t addr, uint8_t data)
|
||||
* 入口参数:block_addr - 块编号 uint32_t 0 - 2047
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* 限定条件:gd5f2gq5ue_init() 已调用
|
||||
* 函数说明:1. 写使能 - D8h + 3字节字节地址 - 等待完成
|
||||
* 2. 擦除完成后检查 E_FAIL 位
|
||||
*/
|
||||
static int gd5f_block_erase(uint32_t block_addr)
|
||||
{
|
||||
* 函数说明:1. 写使能 - D8h + 3字节页地址 - 等待完成
|
||||
* 2. 擦除完成后检查 E_FAIL */
|
||||
int gd5f_block_erase(uint32_t block_addr) {
|
||||
int ret = GD5F_OK;
|
||||
uint8_t cmd[4];
|
||||
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();
|
||||
|
||||
cmd[0] = GD5F_CMD_BLOCK_ERASE;
|
||||
cmd[1] = (byte_addr >> 16) & 0xFF;
|
||||
cmd[2] = (byte_addr >> 8) & 0xFF;
|
||||
cmd[3] = byte_addr & 0xFF;
|
||||
cmd[1] = (page_addr >> 16) & 0xFF;
|
||||
cmd[2] = (page_addr >> 8) & 0xFF;
|
||||
cmd[3] = page_addr & 0xFF;
|
||||
|
||||
GD5F_CS_LOW();
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:扫描所有块构建 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 初始化完成
|
||||
* 函数说明:1. 发送复位命令并等待完成
|
||||
* 2. 读取芯片 ID 并校验
|
||||
* 3. 使能内部 ECC (B0h bit4)
|
||||
* 4. 解除所有块保护 (A0h = 0x00)
|
||||
* 3. 扫描出厂坏块构建 BBT(ECC 使能前,读每块首页 spare[0])
|
||||
* 4. 使能内部 ECC (B0h bit4)
|
||||
* 5. 解除所有块保护 (A0h = 0x00)
|
||||
* 6. 定位保留块池并加载持久化 BBT(ECC 使能后)
|
||||
*/
|
||||
int gd5f2gq5ue_init(void)
|
||||
{
|
||||
int gd5f2gq5ue_init(void) {
|
||||
int ret = GD5F_OK;
|
||||
uint8_t mid = 0;
|
||||
uint8_t did = 0;
|
||||
@@ -325,6 +531,9 @@ int gd5f2gq5ue_init(void)
|
||||
return GD5F_ID_MISMATCH;
|
||||
}
|
||||
|
||||
DBG_INFO("Scanning bad blocks...");
|
||||
gd5f_bbt_scan();
|
||||
|
||||
DBG_INFO("Enabling ECC...");
|
||||
gd5f_set_feature(0xB0, 0x10);
|
||||
gd5f_wait_busy(100);
|
||||
@@ -333,20 +542,25 @@ int gd5f2gq5ue_init(void)
|
||||
gd5f_set_feature(GD5F_REG_PROTECT, 0x00);
|
||||
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");
|
||||
return GD5F_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取芯片 ID(MID + DID)
|
||||
* 入口参数:mid - 制造商 ID 输出指针 uint8_t* 不为 NULL
|
||||
* did - 设备 ID 输出指针 uint8_t* 不为 NULL
|
||||
* 出口参数:p_mid - 制造商 ID 输出指针 uint8_t* 不为 NULL
|
||||
* p_did - 设备 ID 输出指针 uint8_t* 不为 NULL
|
||||
* 返回值:0 - 成功
|
||||
* 限定条件: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 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);
|
||||
GD5F_CS_HIGH();
|
||||
|
||||
*mid = id_buf[1];
|
||||
*did = id_buf[2];
|
||||
*p_mid = id_buf[1];
|
||||
*p_did = id_buf[2];
|
||||
|
||||
return GD5F_OK;
|
||||
}
|
||||
@@ -364,16 +578,19 @@ int gd5f2gq5ue_read_id(uint8_t *mid, uint8_t *did)
|
||||
/*
|
||||
* 函数功能:从 NAND 读取数据(支持跨页)
|
||||
* 入口参数:offset - 起始字节偏移 long 0 ~ 总容量-1
|
||||
* p_buf - 数据缓冲区 uint8_t* 不为 NULL
|
||||
* size - 读取字节数 size_t > 0
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* size - 读取字节数 size_t > 0
|
||||
* 出口参数:p_buf - 数据缓冲区 uint8_t* 不为 NULL
|
||||
* 返回值:0 - 成功,其它 - 错误
|
||||
* 限定条件: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;
|
||||
|
||||
if (offset < 0 || (uint32_t)offset + (uint32_t)size > GD5F_TOTAL_SIZE) {
|
||||
return GD5F_ERROR;
|
||||
}
|
||||
|
||||
while (size > 0) {
|
||||
uint32_t page_addr = 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 写入数据(支持跨页)
|
||||
* 入口参数:offset - 起始字节偏移 long 0 ~ 总容量-1
|
||||
* p_buf - 数据缓冲区 const uint8_t* 不为 NULL
|
||||
* size - 写入字节数 size_t > 0
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* 入口参数:offset - 起始字节偏移 long 0 ~ 总容量-1
|
||||
* p_buf - 数据缓冲区 const uint8_t* 不为 NULL
|
||||
* size - 写入字节数 size_t > 0
|
||||
* 返回值:0 - 成功,其它 - 错误
|
||||
* 限定条件: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;
|
||||
|
||||
if (offset < 0 || (uint32_t)offset + (uint32_t)size > GD5F_TOTAL_SIZE) {
|
||||
return GD5F_ERROR;
|
||||
}
|
||||
|
||||
while (size > 0) {
|
||||
uint32_t page_addr = 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)
|
||||
* 入口参数:offset - 起始字节偏移 long 必须块对齐
|
||||
* size - 擦除字节数 size_t 必须块大小整数倍
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* 入口参数:offset - 起始字节偏移 long 必须 GD5F_BLOCK_SIZE 对齐
|
||||
* size - 擦除字节数 size_t 必须 GD5F_BLOCK_SIZE 整数倍
|
||||
* 返回值:0 - 成功,其它 - 错误码
|
||||
* 限定条件: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;
|
||||
|
||||
if (offset % GD5F_BLOCK_SIZE != 0) {
|
||||
@@ -472,13 +691,11 @@ int gd5f2gq5ue_erase(long offset, size_t size)
|
||||
|
||||
/*
|
||||
* 函数功能:复位芯片
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 成功
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:发送 FFh 复位命令后等待 5ms
|
||||
* 函数说明:发 FFh 复位命令后等待 5ms
|
||||
*/
|
||||
int gd5f2gq5ue_reset(void)
|
||||
{
|
||||
int gd5f2gq5ue_reset(void) {
|
||||
uint8_t cmd = GD5F_CMD_RESET;
|
||||
|
||||
GD5F_CS_LOW();
|
||||
@@ -489,3 +706,257 @@ int gd5f2gq5ue_reset(void)
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:加载持久化 BBT(OR 进 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
#include "main.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/* ======================== 宏定义 ======================== */
|
||||
|
||||
@@ -60,6 +61,9 @@ extern "C" {
|
||||
#define GD5F_TOTAL_BLOCKS 2048
|
||||
#define GD5F_TOTAL_SIZE (GD5F_TOTAL_BLOCKS * GD5F_BLOCK_SIZE)
|
||||
|
||||
/* 坏块表持久化:保留块池数量(从 NAND 顶部向下挑选出厂好块存储 BBT) */
|
||||
#define GD5F_BBT_POOL_COUNT 4
|
||||
|
||||
/* 制造商 ID 和设备 ID */
|
||||
#define GD5F_MANUFACTURER_ID 0xC8
|
||||
#define GD5F_DEVICE_ID 0x52
|
||||
@@ -91,7 +95,7 @@ extern "C" {
|
||||
* 3. 使能内部 ECC (B0h bit4)
|
||||
* 4. 解除所有块保护 (A0h = 0x00)
|
||||
*/
|
||||
int gd5f2gq5ue_init(void);
|
||||
int32_t gd5f2gq5ue_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取芯片 ID(MID + DID)
|
||||
@@ -101,13 +105,13 @@ int gd5f2gq5ue_init(void);
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:发送 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 读取数据(支持跨页)
|
||||
* 入口参数:offset - 起始字节偏移 long 0 - GD5F_TOTAL_SIZE-1
|
||||
* 入口参数:offset - 起始字节偏移 int32_t 0 - GD5F_TOTAL_SIZE-1
|
||||
* buf - 数据缓冲区 uint8_t* 不为 NULL
|
||||
* size - 读取字节数 size_t > 0
|
||||
* size - 读取字节数 uint32_t > 0
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* 限定条件:gd5f2gq5ue_init() 已成功调用
|
||||
* 函数说明:自动处理跨页读取,每次读取不超过当前页剩余空间
|
||||
@@ -116,9 +120,9 @@ int gd5f2gq5ue_read(long offset, uint8_t *buf, size_t size);
|
||||
|
||||
/*
|
||||
* 函数功能:向 NAND 写入数据(支持跨页)
|
||||
* 入口参数:offset - 起始字节偏移 long 0 - GD5F_TOTAL_SIZE-1
|
||||
* 入口参数:offset - 起始字节偏移 int32_t 0 - GD5F_TOTAL_SIZE-1
|
||||
* buf - 数据缓冲区 uint8_t* 不为 NULL
|
||||
* size - 写入字节数 size_t > 0
|
||||
* size - 写入字节数 uint32_t > 0
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* 限定条件:gd5f2gq5ue_init() 已成功调用,目标区域已擦除
|
||||
* 函数说明:自动处理跨页写入,每次写入不超过当前页剩余空间
|
||||
@@ -127,8 +131,8 @@ int gd5f2gq5ue_write(long offset, const uint8_t *buf, size_t size);
|
||||
|
||||
/*
|
||||
* 函数功能:擦除块(按块擦除,最小单位 128KB)
|
||||
* 入口参数:offset - 起始字节偏移 long 必须 GD5F_BLOCK_SIZE 对齐
|
||||
* size - 擦除字节数 size_t 必须 GD5F_BLOCK_SIZE 整数倍
|
||||
* 入口参数:offset - 起始字节偏移 int32_t 必须 GD5F_BLOCK_SIZE 对齐
|
||||
* size - 擦除字节数 uint32_t 必须 GD5F_BLOCK_SIZE 整数倍
|
||||
* 返回值:0 - 成功,其他 - 错误码
|
||||
* 限定条件:gd5f2gq5ue_init() 已成功调用
|
||||
* 函数说明:擦除操作以块为单位,offset 和 size 必须块对齐
|
||||
@@ -142,7 +146,90 @@ int gd5f2gq5ue_erase(long offset, size_t size);
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:发送 FFh 复位命令后等待 5ms
|
||||
*/
|
||||
int gd5f2gq5ue_reset(void);
|
||||
int32_t gd5f2gq5ue_reset(void);
|
||||
|
||||
/*
|
||||
* 函数功能:查询块是否坏块
|
||||
* 入口参数:block - 块编号 uint32_t 0 ~ GD5F_TOTAL_BLOCKS-1
|
||||
* 返回值:0 - 好块,1 - 坏块
|
||||
* 限定条件:gd5f2gq5ue_init() 已成功调用
|
||||
* 函数说明:通过 BBT(Bad 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
|
||||
}
|
||||
|
||||
403
Drivers/BSP/GD5F2GQ5UE/nand_ftl.c
Normal file
403
Drivers/BSP/GD5F2GQ5UE/nand_ftl.c
Normal 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;
|
||||
}
|
||||
18
Drivers/BSP/GD5F2GQ5UE/nand_ftl.h
Normal file
18
Drivers/BSP/GD5F2GQ5UE/nand_ftl.h
Normal 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
|
||||
21
Drivers/BSP/NET/lftpd/LICENSE.txt
Normal file
21
Drivers/BSP/NET/lftpd/LICENSE.txt
Normal 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.
|
||||
675
Drivers/BSP/NET/lftpd/lftpd.c
Normal file
675
Drivers/BSP/NET/lftpd/lftpd.c
Normal 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;
|
||||
}
|
||||
19
Drivers/BSP/NET/lftpd/lftpd.h
Normal file
19
Drivers/BSP/NET/lftpd/lftpd.h
Normal 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);
|
||||
166
Drivers/BSP/NET/lftpd/lftpd_inet.c
Normal file
166
Drivers/BSP/NET/lftpd/lftpd_inet.c
Normal 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);
|
||||
}
|
||||
236
Drivers/BSP/NET/lftpd/lftpd_io.c
Normal file
236
Drivers/BSP/NET/lftpd/lftpd_io.c
Normal 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);
|
||||
}
|
||||
}
|
||||
18
Drivers/BSP/NET/lftpd/lftpd_log.c
Normal file
18
Drivers/BSP/NET/lftpd/lftpd_log.c
Normal 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);
|
||||
}
|
||||
16
Drivers/BSP/NET/lftpd/lftpd_string.c
Normal file
16
Drivers/BSP/NET/lftpd/lftpd_string.c
Normal 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;
|
||||
}
|
||||
14
Drivers/BSP/NET/lftpd/private/lftpd_inet.h
Normal file
14
Drivers/BSP/NET/lftpd/private/lftpd_inet.h
Normal 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);
|
||||
18
Drivers/BSP/NET/lftpd/private/lftpd_io.h
Normal file
18
Drivers/BSP/NET/lftpd/private/lftpd_io.h
Normal 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);
|
||||
7
Drivers/BSP/NET/lftpd/private/lftpd_log.h
Normal file
7
Drivers/BSP/NET/lftpd/private/lftpd_log.h
Normal 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, ...)
|
||||
42
Drivers/BSP/NET/lftpd/private/lftpd_status.h
Normal file
42
Drivers/BSP/NET/lftpd/private/lftpd_status.h
Normal 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."
|
||||
5
Drivers/BSP/NET/lftpd/private/lftpd_string.h
Normal file
5
Drivers/BSP/NET/lftpd/private/lftpd_string.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define CRLF "\r\n"
|
||||
|
||||
char* lftpd_string_trim(char* s);
|
||||
@@ -30,14 +30,6 @@ extern "C" {
|
||||
#define NET_TCP_SERVER_LISTEN_SOCK 0 /* 监听 Socket 索引 */
|
||||
#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
|
||||
|
||||
#ifndef NET_RECV_TIMEOUT_MS
|
||||
#define NET_RECV_TIMEOUT_MS 0 /* 接收超时(0=无限等待) */
|
||||
#define NET_RECV_TIMEOUT_MS 30000 /* 接收超时(30秒,0=无限等待) */
|
||||
#endif
|
||||
|
||||
/*
|
||||
@@ -98,6 +90,13 @@ extern "C" {
|
||||
#define NET_POLL_DELAY_MS 1 /* 非阻塞轮询延时 */
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 消息队列配置
|
||||
*/
|
||||
#ifndef NET_MSG_QUEUE_LENGTH
|
||||
#define NET_MSG_QUEUE_LENGTH 8 /* 消息队列深度 */
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
/*
|
||||
* 模块名称:Network Select Implementation
|
||||
* 模块功能:I/O 多路复用实现,包括 select 和 poll 模式,
|
||||
* 用于同时监控多个 Socket 的读写事件
|
||||
* 适用平台:STM32F4 系列(CH395F 以太网芯片)
|
||||
* 模块功能:I/O 多路复用实现,包括 select 和 poll 两种模式,用于同时监控多个 Socket 的读写事件
|
||||
* 适用平台:STM32F407ZGTx + CH395F 以太网芯片
|
||||
* 作者:王建锋
|
||||
* 创建日期: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_socket.h"
|
||||
#include "ch395f.h"
|
||||
#include "main.h"
|
||||
|
||||
/*
|
||||
* 私有函数声明区
|
||||
*/
|
||||
|
||||
static int check_socket_readable(int sockfd);
|
||||
static int check_socket_writable(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,
|
||||
net_fd_set *exceptfds, net_timeval *timeout)
|
||||
{
|
||||
net_fd_set *exceptfds, net_timeval *timeout) {
|
||||
uint32_t start_tick;
|
||||
uint32_t timeout_ms = 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 i;
|
||||
|
||||
/* 计算超时时间 */
|
||||
if (timeout != NULL)
|
||||
{
|
||||
/* 检查输入参数合法性 */
|
||||
if ((0 >= nfds) || (NET_MAX_SOCKETS < nfds)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 计算超时时间(毫秒) */
|
||||
if (NULL != timeout) {
|
||||
timeout_ms = (uint32_t)(timeout->tv_sec * 1000) +
|
||||
(uint32_t)(timeout->tv_usec / 1000);
|
||||
}
|
||||
|
||||
start_tick = HAL_GetTick();
|
||||
|
||||
while (1)
|
||||
{
|
||||
/* 轮询所有 Socket */
|
||||
net_poll();
|
||||
|
||||
while (1) {
|
||||
/* 仅读取 netTask 的 net_poll() 已刷新的控制块状态(无 CH395F SPI 访问) */
|
||||
ready_count = 0;
|
||||
|
||||
/* 检查哪些 Socket 有事件 */
|
||||
for (i = 0; i < nfds && i < NET_MAX_SOCKETS; i++)
|
||||
{
|
||||
net_sock_t *sock = net_get_sock(i);
|
||||
/* 检查哪个 Socket 有事件 */
|
||||
for (i = 0; i < nfds && i < NET_MAX_SOCKETS; i++) {
|
||||
net_sock_t *p_sock = net_get_sock(i);
|
||||
|
||||
if (sock == NULL)
|
||||
{
|
||||
if (NULL == p_sock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 检查可读事件 */
|
||||
if (readfds != NULL && NET_FD_ISSET(i, readfds))
|
||||
{
|
||||
if (check_socket_readable(i))
|
||||
{
|
||||
if ((NULL != readfds) && NET_FD_ISSET(i, readfds)) {
|
||||
if (check_socket_readable(i)) {
|
||||
NET_FD_SET(i, &result_read);
|
||||
ready_count++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 检查可写事件 */
|
||||
if (writefds != NULL && NET_FD_ISSET(i, writefds))
|
||||
{
|
||||
if (check_socket_writable(i))
|
||||
{
|
||||
if ((NULL != writefds) && NET_FD_ISSET(i, writefds)) {
|
||||
if (check_socket_writable(i)) {
|
||||
NET_FD_SET(i, &result_write);
|
||||
ready_count++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 检查异常事件 */
|
||||
if (exceptfds != NULL && NET_FD_ISSET(i, exceptfds))
|
||||
{
|
||||
if (check_socket_error(i))
|
||||
{
|
||||
if ((NULL != exceptfds) && NET_FD_ISSET(i, exceptfds)) {
|
||||
if (check_socket_error(i)) {
|
||||
NET_FD_SET(i, &result_except);
|
||||
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;
|
||||
}
|
||||
|
||||
/* 超时检查 */
|
||||
if (timeout != NULL)
|
||||
{
|
||||
if (timeout_ms > 0 &&
|
||||
(HAL_GetTick() - start_tick) >= timeout_ms)
|
||||
{
|
||||
/* 超时检测 */
|
||||
if (NULL != timeout) {
|
||||
if ((0 < timeout_ms) &&
|
||||
((HAL_GetTick() - start_tick) >= timeout_ms)) {
|
||||
break; /* 超时返回 0 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 短暂延时避免忙等 */
|
||||
HAL_Delay(NET_POLL_DELAY_MS);
|
||||
/* 让出 CPU:FreeRTOS osDelay() 会正确进入休眠,允许其他任务运行 */
|
||||
osDelay(1); /* 1ms 周期轮询,平衡实时性和 CPU 利用率 */
|
||||
}
|
||||
|
||||
/* 复制结果 */
|
||||
if (readfds != NULL)
|
||||
{
|
||||
/* 复制结果集合 */
|
||||
if (NULL != readfds) {
|
||||
readfds->fd_bits = result_read.fd_bits;
|
||||
}
|
||||
if (writefds != NULL)
|
||||
{
|
||||
if (NULL != writefds) {
|
||||
writefds->fd_bits = result_write.fd_bits;
|
||||
}
|
||||
if (exceptfds != NULL)
|
||||
{
|
||||
if (NULL != exceptfds) {
|
||||
exceptfds->fd_bits = result_except.fd_bits;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
int ready = 0;
|
||||
int i;
|
||||
|
||||
if (fds == NULL || nfds <= 0)
|
||||
{
|
||||
if ((NULL == fds) || (0 >= nfds)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
start_tick = HAL_GetTick();
|
||||
|
||||
while (1)
|
||||
{
|
||||
/* 轮询 */
|
||||
net_poll();
|
||||
|
||||
while (1) {
|
||||
/* 仅读取 netTask 的 net_poll() 已刷新的控制块状态(无 CH395F SPI 访问) */
|
||||
ready = 0;
|
||||
|
||||
for (i = 0; i < nfds; i++)
|
||||
{
|
||||
for (i = 0; i < nfds; i++) {
|
||||
fds[i].revents = 0;
|
||||
|
||||
/* 检查 fd 有效性 */
|
||||
if (fds[i].fd < 0 || fds[i].fd >= NET_MAX_SOCKETS)
|
||||
{
|
||||
if ((0 > fds[i].fd) || (NET_MAX_SOCKETS <= fds[i].fd)) {
|
||||
fds[i].revents |= NET_POLLNVAL;
|
||||
ready++;
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
ready++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 检查可读 */
|
||||
if (fds[i].events & NET_POLLIN)
|
||||
{
|
||||
if (check_socket_readable(fds[i].fd))
|
||||
{
|
||||
if (fds[i].events & NET_POLLIN) {
|
||||
if (check_socket_readable(fds[i].fd)) {
|
||||
fds[i].revents |= NET_POLLIN;
|
||||
ready++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 检查可写 */
|
||||
if (fds[i].events & NET_POLLOUT)
|
||||
{
|
||||
if (check_socket_writable(fds[i].fd))
|
||||
{
|
||||
if (fds[i].events & NET_POLLOUT) {
|
||||
if (check_socket_writable(fds[i].fd)) {
|
||||
fds[i].revents |= NET_POLLOUT;
|
||||
ready++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 检查错误 */
|
||||
if (check_socket_error(fds[i].fd))
|
||||
{
|
||||
if (check_socket_error(fds[i].fd)) {
|
||||
fds[i].revents |= NET_POLLERR;
|
||||
ready++;
|
||||
}
|
||||
|
||||
/* 检查挂起(连接断开) */
|
||||
if (sock->state == NET_SOCK_STATE_TCP_ACCEPT ||
|
||||
sock->state == NET_SOCK_STATE_CLOSED)
|
||||
{
|
||||
if (NET_SOCK_STATE_CLOSED == p_sock->state) {
|
||||
fds[i].revents |= NET_POLLHUP;
|
||||
ready++;
|
||||
}
|
||||
}
|
||||
|
||||
if (ready > 0)
|
||||
{
|
||||
if (0 < ready) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* 超时检查 */
|
||||
if (timeout >= 0)
|
||||
{
|
||||
if ((HAL_GetTick() - start_tick) >= (uint32_t)timeout)
|
||||
{
|
||||
/* 超时检测 */
|
||||
if (0 <= timeout) {
|
||||
if ((HAL_GetTick() - start_tick) >= (uint32_t)timeout) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
HAL_Delay(NET_POLL_DELAY_MS);
|
||||
/* 让出 CPU:FreeRTOS osDelay() 会正确进入休眠,允许其他任务运行 */
|
||||
osDelay(1); /* 1ms 周期轮询,平衡实时性和 CPU 利用率 */
|
||||
}
|
||||
|
||||
return ready;
|
||||
@@ -236,58 +238,55 @@ int net_poll_events(net_pollfd *fds, int nfds, int timeout)
|
||||
*/
|
||||
|
||||
/*
|
||||
* 函数功能:检查 Socket 是否可读
|
||||
* 返回值:1 可读,0 不可读
|
||||
* 函数功能:检查指定 Socket 当前是否可读
|
||||
* 入口参数: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)
|
||||
{
|
||||
net_sock_t *sock;
|
||||
uint16_t recv_len;
|
||||
static int check_socket_readable(int sockfd) {
|
||||
net_sock_t *p_sock;
|
||||
|
||||
sock = net_get_sock(sockfd);
|
||||
if (sock == NULL)
|
||||
{
|
||||
p_sock = net_get_sock(sockfd);
|
||||
if (NULL == p_sock) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 监听 Socket 有新连接时可读 */
|
||||
if (sock->state == NET_SOCK_STATE_LISTENING)
|
||||
{
|
||||
if (NET_SOCK_STATE_LISTENING == p_sock->state) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 已建立连接的 Socket 有数据可读 */
|
||||
if (sock->state == NET_SOCK_STATE_ESTABLISHED)
|
||||
{
|
||||
recv_len = ch395f_get_recv_len(sockfd);
|
||||
if (recv_len > 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
/* 已建立连接或 UDP 等有数据可读:readable 由 net_poll 在 RECV_OK 时置位、
|
||||
* net_recv_locked 读空后清零。避免应用任务直接访问 CH395F SPI(见 Trap 16) */
|
||||
if (p_sock->readable) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:检查 Socket 是否可写
|
||||
* 返回值:1 可写,0 不可写
|
||||
* 函数功能:检查指定 Socket 当前是否可写
|
||||
* 入口参数:sockfd - 待检查的 Socket 描述符 int 0 - NET_MAX_SOCKETS-1
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 可写(已连接且发送缓冲空闲);0 - 不可写或描述符无效 int
|
||||
* 限定条件:net_init() 已调用
|
||||
* 函数说明:1. 仅已建立连接且 send_ready 标志置位的 Socket 视为可写
|
||||
*/
|
||||
static int check_socket_writable(int sockfd)
|
||||
{
|
||||
net_sock_t *sock;
|
||||
static int check_socket_writable(int sockfd) {
|
||||
net_sock_t *p_sock;
|
||||
|
||||
sock = net_get_sock(sockfd);
|
||||
if (sock == NULL)
|
||||
{
|
||||
p_sock = net_get_sock(sockfd);
|
||||
if (NULL == p_sock) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 已建立连接的 Socket 发送缓冲区空闲时可写 */
|
||||
if (sock->state == NET_SOCK_STATE_ESTABLISHED)
|
||||
{
|
||||
if (sock->send_ready)
|
||||
{
|
||||
if (NET_SOCK_STATE_ESTABLISHED == p_sock->state) {
|
||||
if (p_sock->send_ready) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -296,22 +295,24 @@ static int check_socket_writable(int sockfd)
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:检查 Socket 是否有错误
|
||||
* 返回值:1 有错误,0 无错误
|
||||
* 函数功能:检查指定 Socket 是否存在错误状态
|
||||
* 入口参数: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)
|
||||
{
|
||||
net_sock_t *sock;
|
||||
static int check_socket_error(int sockfd) {
|
||||
net_sock_t *p_sock;
|
||||
|
||||
/* 无效 fd 视为错误 */
|
||||
if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS)
|
||||
{
|
||||
if ((0 > sockfd) || (NET_MAX_SOCKETS <= sockfd)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
sock = net_get_sock(sockfd);
|
||||
if (sock == NULL)
|
||||
{
|
||||
p_sock = net_get_sock(sockfd);
|
||||
if (NULL == p_sock) {
|
||||
return 1; /* 未使用的 Socket 视为错误 */
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,33 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
#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
|
||||
* 入口参数:domain - 地址族,仅支持 AF_INET
|
||||
* type - SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP)
|
||||
* protocol - 通常为 0
|
||||
* 返回值:socket 描述符 (0~7),失败返回 -1
|
||||
* 限定条件: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 - 本地地址结构体指针
|
||||
* addrlen - 地址结构体长度
|
||||
* 返回值:0 成功,-1 失败
|
||||
* 限定条件:socket 已创建且未绑定
|
||||
* 函数说明:设置 CH395F 源端口
|
||||
*/
|
||||
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP Server 监听
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
* backlog - 最大等待连接数(多连接模式下为数据 Socket 数量)
|
||||
* 返回值:0 成功,-1 失败
|
||||
* 限定条件:socket 已绑定
|
||||
* 函数说明:内部调用 ch395f_tcp_listen,监听 Socket 自身即为数据通道
|
||||
*/
|
||||
int net_listen(int sockfd, int backlog);
|
||||
int net_listen(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP Server 接受连接
|
||||
@@ -104,7 +133,8 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen);
|
||||
* len - 数据长度
|
||||
* flags - 通常为 0
|
||||
* 返回值:实际发送字节数,失败返回 -1
|
||||
* 限定条件:TCP 连接已建立
|
||||
* 限定条件:TCP 连接已建立,指针非空
|
||||
* 函数说明:阻塞等待发送缓冲区空闲,拷贝数据后触发发送
|
||||
*/
|
||||
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 数据
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
* buf - 数据缓冲区
|
||||
* len - 数据长度
|
||||
* flags - 通常为 0
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
* buf - 数据缓冲区
|
||||
* len - 数据长度
|
||||
* flags - 通常为 0
|
||||
* dest_addr - 目标地址
|
||||
* addrlen - 地址长度
|
||||
* addrlen - 地址长度
|
||||
* 返回值:实际发送字节数,失败返回 -1
|
||||
* 限定条件:socket 已创建,指针非空
|
||||
* 函数说明:设置目标地址后调用 CH395F 发送
|
||||
*/
|
||||
int net_sendto(int sockfd, const void *buf, int len, int flags,
|
||||
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 - 输出:发送方地址
|
||||
* addrlen - 输入输出:地址长度
|
||||
* 返回值:实际接收字节数,失败返回 -1
|
||||
* 限定条件:socket 已创建,指针非空
|
||||
* 函数说明:读取 CH395F 接收缓冲区,获取对端地址
|
||||
*/
|
||||
int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||||
struct net_sockaddr *src_addr, int *addrlen);
|
||||
@@ -152,6 +186,8 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||||
* 函数功能:关闭 socket
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
* 返回值:0 成功,-1 失败
|
||||
* 限定条件:socket 已打开
|
||||
* 函数说明:释放 Socket 控制块,TCP 先断开连接
|
||||
*/
|
||||
int net_close(int sockfd);
|
||||
|
||||
@@ -165,6 +201,8 @@ int net_close(int sockfd);
|
||||
* cb - 回调函数
|
||||
* arg - 用户参数
|
||||
* 返回值:0 成功,-1 失败
|
||||
* 限定条件:socket 已创建
|
||||
* 函数说明:回调在 net_poll 上下文中触发
|
||||
*/
|
||||
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg);
|
||||
|
||||
@@ -181,7 +219,9 @@ int net_get_errno(void);
|
||||
/*
|
||||
* 函数功能:将 IP 地址字符串转为网络字节序
|
||||
* 入口参数:cp - 点分十进制字符串 "192.168.1.100"
|
||||
* 返回值:网络字节序 IP
|
||||
* 返回值:网络字节序 IP,失败返回 0
|
||||
* 限定条件:cp 指针非空
|
||||
* 函数说明:不支持域名解析
|
||||
*/
|
||||
uint32_t net_inet_addr(const char *cp);
|
||||
|
||||
@@ -190,29 +230,54 @@ uint32_t net_inet_addr(const char *cp);
|
||||
* 入口参数:addr - 网络字节序 IP
|
||||
* buf - 输出缓冲区(至少 16 字节)
|
||||
* 返回值:buf 指针
|
||||
* 限定条件:buf 指针非空
|
||||
*/
|
||||
char *net_inet_ntoa(uint32_t addr, char *buf);
|
||||
|
||||
/*
|
||||
* 函数功能:端口字节序转换(主机序 -> 网络序)
|
||||
* 入口参数:hostshort - 主机序端口
|
||||
* 返回值:网络序端口
|
||||
* 限定条件:无
|
||||
* 函数说明:小端 → 大端
|
||||
*/
|
||||
uint16_t net_htons(uint16_t hostshort);
|
||||
|
||||
/*
|
||||
* 函数功能:端口字节序转换(网络序 -> 主机序)
|
||||
* 入口参数:netshort - 网络序端口
|
||||
* 返回值:主机序端口
|
||||
* 限定条件:无
|
||||
* 函数说明:大端 → 小端
|
||||
*/
|
||||
uint16_t net_ntohs(uint16_t netshort);
|
||||
|
||||
/*
|
||||
* 函数功能:IP 地址字节序转换(主机序 -> 网络序)
|
||||
* 入口参数:hostlong - 主机序 IP
|
||||
* 返回值:网络序 IP
|
||||
* 限定条件:无
|
||||
* 函数说明:小端 → 大端
|
||||
*/
|
||||
uint32_t net_htonl(uint32_t hostlong);
|
||||
|
||||
/*
|
||||
* 函数功能:IP 地址字节序转换(网络序 -> 主机序)
|
||||
* 入口参数:netlong - 网络序 IP
|
||||
* 返回值:主机序 IP
|
||||
* 限定条件:无
|
||||
* 函数说明:大端 → 小端
|
||||
*/
|
||||
uint32_t net_ntohl(uint32_t netlong);
|
||||
|
||||
/*
|
||||
* 函数功能:处理消息队列中的请求(必须在 netTask 主循环中调用)
|
||||
* 限定条件:net_init() 已调用
|
||||
* 函数说明:处理其他任务通过 net_send/net_recv 等接口发送的请求,
|
||||
* 在 netTask 上下文中串行化执行所有 CH395F 操作
|
||||
*/
|
||||
void net_process_messages(void);
|
||||
|
||||
/*
|
||||
* 函数功能:获取 Socket 控制块指针(内部使用)
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
@@ -220,6 +285,57 @@ uint32_t net_ntohl(uint32_t netlong);
|
||||
*/
|
||||
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
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -114,7 +114,6 @@ typedef enum {
|
||||
NET_SOCK_STATE_CONNECTING, /* TCP Client 连接中 */
|
||||
NET_SOCK_STATE_ESTABLISHED, /* TCP 已连接 */
|
||||
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_t;
|
||||
|
||||
@@ -150,19 +149,23 @@ typedef struct {
|
||||
uint16_t local_port; /* 本地端口(主机序) */
|
||||
uint16_t remote_port; /* 远端端口(主机序) */
|
||||
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;
|
||||
void *event_arg;
|
||||
|
||||
/* TCP Server 多连接模式:监听 Socket 的数据 Socket 列表 */
|
||||
int data_socks[NET_TCP_SERVER_MAX_CLIENTS];
|
||||
int data_sock_count;
|
||||
|
||||
/* 发送就绪标志 */
|
||||
uint8_t send_ready;
|
||||
|
||||
/* 接收可读标志:net_poll 检测到 RECV_OK 时置 1,net_recv_locked 读空后清零。
|
||||
* net_select 据此判断可读性,从而无需在应用/测试任务上下文直接访问 CH395F SPI
|
||||
* (所有 ch395f 事务统一由 netTask 串行执行,避免与 net_poll 竞争,见 Trap 16)。 */
|
||||
uint8_t readable;
|
||||
|
||||
/* 单连接模式自动重监听:连接断开后由 net 层自动回到 LISTEN(BSD 风格监听常驻) */
|
||||
uint8_t auto_relisten;
|
||||
|
||||
/* 统计信息 */
|
||||
uint32_t recv_bytes; /* 累计接收字节数 */
|
||||
uint32_t send_bytes; /* 累计发送字节数 */
|
||||
|
||||
@@ -1,133 +1,152 @@
|
||||
/*
|
||||
* 模块名称:RS-485 半双工通信驱动
|
||||
* 模块功能:RS-485 半双工通信驱动实现,核心要点:
|
||||
* 1. 发送前拉高 DE(切换到发送态)
|
||||
* 2. HAL_UART_Transmit 内部已等待 TC(发送完成)标志
|
||||
* 3. 发送完毕后拉低 DE(切回接收态)
|
||||
* 4. 使用 HAL_UARTEx_ReceiveToIdle_IT 实现变长帧接收
|
||||
* 参考:ST AN3070 应用笔记、controllerstech.com RS485 教程
|
||||
* 模块功能:RS-485 半双工通信驱动实现,封装任意 UART 外设实现 485 方向控制,
|
||||
* 提供阻塞发送、中断接收(IDLE 空闲帧检测)。使用 DWT CYCCNT 提供
|
||||
* 微秒级延时,确保 RS485 收发器 DE 使能稳定。
|
||||
* 适用平台:STM32F4 系列
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-07-18
|
||||
* 修改记录:
|
||||
* 2026-07-18 王建锋 创建初始版本
|
||||
* 2026-07-20 王建锋 修复 DE 时序:添加 DWT 微秒延时替代 for 循环
|
||||
* 2026-07-20 王建锋 清理中文注释乱码,对齐代码规范
|
||||
*/
|
||||
|
||||
#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_RX(h) HAL_GPIO_WritePin((h)->dir_port, (h)->dir_pin, GPIO_PIN_RESET)
|
||||
|
||||
/*
|
||||
* 公共函数实现
|
||||
*/
|
||||
/* ======================== 公共函数实现 ======================== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化 RS-485 句柄
|
||||
* 入口参数:handle - RS-485 句柄指针
|
||||
* huart - UART 外设句柄
|
||||
* dir_port - DE 引脚 GPIO 端口
|
||||
* dir_pin - DE 引脚 GPIO 编号
|
||||
* 入口参数:p_handle - RS-485 句柄指针 rs485_handle_t*(非空)
|
||||
* p_huart - UART 外设句柄指针(已由 CubeMX 初始化) UART_HandleTypeDef*(非空)
|
||||
* p_dir_port - 方向控制 GPIO 端口(如 GPIOD) GPIO_TypeDef*(非空)
|
||||
* dir_pin - 方向控制 GPIO 引脚号(如 GPIO_PIN_0) uint16_t
|
||||
* 返回值:无
|
||||
* 限定条件:UART 和 GPIO 须先由 CubeMX 完成初始化
|
||||
* 函数说明:初始化后立即将 DE 拉低,使 RS-485 收发器处于接收状态
|
||||
* 函数说明:初始化后默认处于接收状态(DE 引脚拉低)
|
||||
*/
|
||||
void rs485_init(rs485_handle_t *handle,
|
||||
UART_HandleTypeDef *huart,
|
||||
GPIO_TypeDef *dir_port,
|
||||
void rs485_init(rs485_handle_t *p_handle,
|
||||
UART_HandleTypeDef *p_huart,
|
||||
GPIO_TypeDef *p_dir_port,
|
||||
uint16_t dir_pin)
|
||||
{
|
||||
handle->huart = huart;
|
||||
handle->dir_port = dir_port;
|
||||
handle->dir_pin = dir_pin;
|
||||
handle->rx_size = 0;
|
||||
p_handle->huart = p_huart;
|
||||
p_handle->dir_port = p_dir_port;
|
||||
p_handle->dir_pin = dir_pin;
|
||||
p_handle->rx_size = 0;
|
||||
|
||||
/* 默认进入接收状态 */
|
||||
RS485_DIR_RX(handle);
|
||||
RS485_DIR_RX(p_handle);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:阻塞方式发送数据
|
||||
* 入口参数:handle - RS-485 句柄指针
|
||||
* data - 待发送数据缓冲区
|
||||
* len - 待发送字节数
|
||||
* timeout - 发送超时(ms),传 0 使用默认值
|
||||
* 返回值:HAL_OK / HAL_TIMEOUT / HAL_ERROR
|
||||
* 限定条件:须在主循环或 RTOS 任务中调用,不可在 UART 中断中调用
|
||||
* 函数说明:
|
||||
* 时序:DE=HIGH → UART 发送 N 字节 → 等 TC → DE=LOW
|
||||
* HAL_UART_Transmit 内部会等待 TXE(发送寄存器空)逐字节搬数据,
|
||||
* 最后还会等待 TC(发送完成)标志,确保最后一字节的停止位已移出。
|
||||
* 因此返回后再拉低 DE 是安全的
|
||||
* 入口参数:p_handle - RS-485 句柄指针 rs485_handle_t*(非空)
|
||||
* p_data - 待发送数据缓冲区 uint8_t*(非空)
|
||||
* len - 待发送字节数 uint16_t 1 - N
|
||||
* timeout - 发送超时(ms),传 0 使用 RS485_TIMEOUT_DEFAULT uint32_t 0 / 1 - N
|
||||
* 返回值:HAL_OK - 发送成功 HAL_StatusTypeDef
|
||||
* HAL_TIMEOUT - 发送超时
|
||||
* HAL_ERROR - 发送错误
|
||||
* 限定条件:须在主循环或 FreeRTOS 任务中调用,不可在 UART 中断中调用
|
||||
* 函数说明:发送前拉高 DE,等待 50us 确保 RS485 收发器驱动使能稳定后再启动 UART 发送。
|
||||
* HAL_UART_Transmit 内部等待 TC(发送完成)标志,确保最后一字节完全移出后
|
||||
* 再拉低 DE 切回接收状态。
|
||||
*/
|
||||
HAL_StatusTypeDef rs485_transmit(rs485_handle_t *handle,
|
||||
const uint8_t *data,
|
||||
HAL_StatusTypeDef rs485_transmit(rs485_handle_t *p_handle,
|
||||
const uint8_t *p_data,
|
||||
uint16_t len,
|
||||
uint32_t timeout)
|
||||
{
|
||||
HAL_StatusTypeDef status;
|
||||
HAL_StatusTypeDef status = HAL_OK;
|
||||
|
||||
if (timeout == 0U) {
|
||||
timeout = RS485_TIMEOUT_DEFAULT;
|
||||
}
|
||||
|
||||
/* 1. 切换到发送状态 */
|
||||
RS485_DIR_TX(handle);
|
||||
RS485_DIR_TX(p_handle);
|
||||
|
||||
/* 2. 阻塞发送(内部等待 TC) */
|
||||
status = HAL_UART_Transmit(handle->huart,
|
||||
(uint8_t *)data,
|
||||
delay_us(50);
|
||||
|
||||
status = HAL_UART_Transmit(p_handle->huart,
|
||||
(uint8_t *)p_data,
|
||||
len,
|
||||
timeout);
|
||||
|
||||
/* 3. 发送完毕或超时,切回接收状态 */
|
||||
RS485_DIR_RX(handle);
|
||||
RS485_DIR_RX(p_handle);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:启动中断方式接收(IDLE 空闲帧检测)
|
||||
* 入口参数:handle - RS-485 句柄指针
|
||||
* buf - 接收缓冲区
|
||||
* buf_size - 缓冲区最大容量
|
||||
* 返回值:HAL_OK / HAL_ERROR
|
||||
* 入口参数:p_handle - RS-485 句柄指针 rs485_handle_t*(非空)
|
||||
* p_buf - 接收缓冲区 uint8_t*(非空)
|
||||
* buf_size - 缓冲区大小 uint16_t
|
||||
* 返回值:HAL_OK - 启动成功 HAL_StatusTypeDef
|
||||
* HAL_ERROR - 启动失败
|
||||
* 限定条件:UART 须已开启全局中断(NVIC 使能)
|
||||
* 函数说明:
|
||||
* 使用 HAL_UARTEx_ReceiveToIdle_IT 实现变长帧接收。
|
||||
* 当 UART 总线空闲超过 1 个字符时间后,硬件触发 IDLE 中断,
|
||||
* HAL 自动调用 HAL_UARTEx_RxEventCallback 并返回已接收字节数。
|
||||
* 用户须在该回调中重新调用本函数重新开启接收
|
||||
* 函数说明:使用 HAL_UARTEx_ReceiveToIdle_IT 实现变长帧接收。
|
||||
* 当 UART 总线空闲超过 1 个字符时间后硬件触发 IDLE 中断,
|
||||
* HAL 自动调用 HAL_UARTEx_RxEventCallback 并返回已接收字节数。
|
||||
* 用户须在回调中重新调用本函数重新开启接收。
|
||||
*/
|
||||
HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *handle,
|
||||
uint8_t *buf,
|
||||
HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *p_handle,
|
||||
uint8_t *p_buf,
|
||||
uint16_t buf_size)
|
||||
{
|
||||
handle->rx_size = 0;
|
||||
return HAL_UARTEx_ReceiveToIdle_IT(handle->huart, buf, buf_size);
|
||||
p_handle->rx_size = 0;
|
||||
return HAL_UARTEx_ReceiveToIdle_IT(p_handle->huart, p_buf, buf_size);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:缓存最近一次接收的字节数
|
||||
* 入口参数:handle - RS-485 句柄指针
|
||||
* size - 本次接收到的字节数
|
||||
* 入口参数:p_handle - RS-485 句柄指针 rs485_handle_t*(非空)
|
||||
* size - 本次接收到的字节数 uint16_t
|
||||
* 返回值:无
|
||||
* 限定条件:须在 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;
|
||||
}
|
||||
|
||||
@@ -50,9 +50,9 @@ typedef struct {
|
||||
* 限定条件:UART 和 GPIO 须先由 CubeMX 完成初始化
|
||||
* 函数说明:初始化后默认处于接收状态(DE 引脚拉低)
|
||||
*/
|
||||
void rs485_init(rs485_handle_t *handle,
|
||||
UART_HandleTypeDef *huart,
|
||||
GPIO_TypeDef *dir_port,
|
||||
void rs485_init(rs485_handle_t *p_handle,
|
||||
UART_HandleTypeDef *p_huart,
|
||||
GPIO_TypeDef *p_dir_port,
|
||||
uint16_t dir_pin);
|
||||
|
||||
/*
|
||||
@@ -66,8 +66,8 @@ void rs485_init(rs485_handle_t *handle,
|
||||
* 函数说明:发送前自动拉高 DE,发送完成后等待 TC 标志再拉低 DE,
|
||||
* 确保最后一字节完全移出后再切换到接收状态
|
||||
*/
|
||||
HAL_StatusTypeDef rs485_transmit(rs485_handle_t *handle,
|
||||
const uint8_t *data,
|
||||
HAL_StatusTypeDef rs485_transmit(rs485_handle_t *p_handle,
|
||||
const uint8_t *p_data,
|
||||
uint16_t len,
|
||||
uint32_t timeout);
|
||||
|
||||
@@ -82,8 +82,8 @@ HAL_StatusTypeDef rs485_transmit(rs485_handle_t *handle,
|
||||
* 收到完整帧后在 RxEventCallback 中通知用户。
|
||||
* 用户须在回调中重新调用本函数重新开启接收
|
||||
*/
|
||||
HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *handle,
|
||||
uint8_t *buf,
|
||||
HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *p_handle,
|
||||
uint8_t *p_buf,
|
||||
uint16_t buf_size);
|
||||
|
||||
/*
|
||||
@@ -93,7 +93,7 @@ HAL_StatusTypeDef rs485_receive_start(rs485_handle_t *handle,
|
||||
* 限定条件:须在 HAL_UART_RxCpltCallback / 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);
|
||||
|
||||
/*
|
||||
* 函数功能:获取最近一次接收的字节数
|
||||
@@ -101,7 +101,7 @@ void rs485_rx_set_size(rs485_handle_t *handle, uint16_t size);
|
||||
* 返回值:最近一次接收的字节数
|
||||
* 限定条件:须在 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
|
||||
}
|
||||
|
||||
@@ -1,39 +1,52 @@
|
||||
/*
|
||||
* 模块名称:SD2506API-G RTC 实时时钟驱动
|
||||
* 模块功能:SD2506API-G 高精度温补实时时钟模块 I2C 驱动
|
||||
* 适用平台:STM32F407ZGT6,I2C1 接口 (PB6-SCL, PB7-SDA)
|
||||
* 模块名称:SD2506API-G RTC Driver
|
||||
* 模块功能:SD2506API-G 高精度温补实时时钟驱动,I2C 接口
|
||||
* 适用平台:STM32F407ZGTx + SD2506API-G 实时时钟芯片(I2C1: PB6-SCL, PB7-SDA)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-07-17
|
||||
* 修改记录:
|
||||
* 2026-07-17 王建锋 创建初始版本,参考 SD2506API-G Ver2.0 手册
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "sd2506.h"
|
||||
#include "i2c.h"
|
||||
#include <string.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/*
|
||||
* 写单字节寄存器
|
||||
* reg: 寄存器地址 (00H~79H)
|
||||
* val: 写入值
|
||||
* 返回: 0=成功, -2=I2C错误
|
||||
* 函数功能:写单字节寄存器
|
||||
* 入口参数:reg - 寄存器地址 uint8_t 0x00 - 0x79
|
||||
* val - 写入数据 uint8_t 0x00 - 0xFF
|
||||
* 返回值: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,
|
||||
I2C_MEMADD_SIZE_8BIT, &val, 1,
|
||||
SD2506_I2C_TIMEOUT_MS) != HAL_OK) {
|
||||
@@ -43,15 +56,17 @@ static int sd2506_write_reg(uint8_t reg, uint8_t val)
|
||||
}
|
||||
|
||||
/*
|
||||
* 读单字节寄存器
|
||||
* reg: 寄存器地址 (00H~79H)
|
||||
* val: 读取值指针
|
||||
* 返回: 0=成功, -2=I2C错误
|
||||
* 函数功能:读单字节寄存器
|
||||
* 入口参数:reg - 寄存器地址 uint8_t 0x00 - 0x79
|
||||
* p_val - 读取值指针 uint8_t*(非空)
|
||||
* 返回值: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,
|
||||
I2C_MEMADD_SIZE_8BIT, val, 1,
|
||||
I2C_MEMADD_SIZE_8BIT, p_val, 1,
|
||||
SD2506_I2C_TIMEOUT_MS) != HAL_OK) {
|
||||
return SD2506_I2C_ERROR;
|
||||
}
|
||||
@@ -59,16 +74,18 @@ static int sd2506_read_reg(uint8_t reg, uint8_t *val)
|
||||
}
|
||||
|
||||
/*
|
||||
* 写多字节寄存器 (从 reg 开始连续写入 len 字节)
|
||||
* reg: 起始寄存器地址
|
||||
* data: 数据缓冲区
|
||||
* len: 数据长度
|
||||
* 返回: 0=成功, -2=I2C错误
|
||||
* 函数功能:写多字节寄存器(连续写)
|
||||
* 入口参数:reg - 起始寄存器地址 uint8_t 0x00 - 0x79
|
||||
* p_data - 数据缓冲区指针 uint8_t*(非空)
|
||||
* len - 数据长度 uint8_t 1 - N
|
||||
* 返回值: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,
|
||||
I2C_MEMADD_SIZE_8BIT, (uint8_t *)data, len,
|
||||
I2C_MEMADD_SIZE_8BIT, (uint8_t *)p_data, len,
|
||||
SD2506_I2C_TIMEOUT_MS) != HAL_OK) {
|
||||
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: 起始寄存器地址
|
||||
* data: 数据缓冲区
|
||||
* len: 数据长度
|
||||
* 返回: 0=成功, -2=I2C错误
|
||||
* 函数功能:读多字节寄存器(连续读)
|
||||
* 入口参数:reg - 起始寄存器地址 uint8_t 0x00 - 0x79
|
||||
* p_data - 数据缓冲区指针 uint8_t*(非空)
|
||||
* len - 数据长度 uint8_t 1 - N
|
||||
* 返回值: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,
|
||||
I2C_MEMADD_SIZE_8BIT, data, len,
|
||||
I2C_MEMADD_SIZE_8BIT, p_data, len,
|
||||
SD2506_I2C_TIMEOUT_MS) != HAL_OK) {
|
||||
return SD2506_I2C_ERROR;
|
||||
}
|
||||
@@ -93,38 +112,41 @@ static int sd2506_read_regs(uint8_t reg, uint8_t *data, uint8_t len)
|
||||
}
|
||||
|
||||
/*
|
||||
* 开启写保护 (允许写入 00H~71H 寄存器)
|
||||
* 顺序: 先写 WRTC1=1, 再写 WRTC2=1 + WRTC3=1
|
||||
* 函数功能:开启写保护(允许写入 00H~71H 寄存器)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 成功 2 - I2C 错误
|
||||
* 限定条件:I2C 外设已初始化
|
||||
* 函数说明:顺序:先写 WRTC1=1, 再写 WRTC2=1 + WRTC3=1
|
||||
*/
|
||||
static int sd2506_write_enable(void)
|
||||
{
|
||||
int ret;
|
||||
static int sd2506_write_enable(void) {
|
||||
int ret = 0;
|
||||
|
||||
/* 先置 WRTC1=1 (bit6=1), 其它位参考手册: 0x84 */
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR1, 0x84U);
|
||||
/* 写允许顺序(SD2506/SD24xx 手册要求):先置 WRTC1(CTR2 bit7)=1,
|
||||
* 再置 WRTC2(CTR1 bit2) + WRTC3(CTR1 bit7)=1。顺序反了芯片不会解开写保护。 */
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR2, 0x80U);
|
||||
if (ret != SD2506_OK) return ret;
|
||||
|
||||
/* 再置 WRTC2=1(bit5) + WRTC3=1(bit4): 0x8C */
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR2, 0x8CU);
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR1, 0x84U);
|
||||
if (ret != SD2506_OK) return ret;
|
||||
|
||||
return SD2506_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 关闭写保护 (禁止写入 00H~71H 寄存器)
|
||||
* 顺序: 先写 WRTC2=0 + WRTC3=0, 再写 WRTC1=0
|
||||
* 函数功能:关闭写保护(禁止写入 00H~71H 寄存器)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 成功 2 - I2C 错误
|
||||
* 限定条件:I2C 外设已初始化
|
||||
* 函数说明:顺序:先写 WRTC2=0 + WRTC3=0, 再写 WRTC1=0
|
||||
*/
|
||||
static int sd2506_write_disable(void)
|
||||
{
|
||||
int ret;
|
||||
static int sd2506_write_disable(void) {
|
||||
int ret = 0;
|
||||
|
||||
/* 先清 WRTC2=0, WRTC3=0: 0x00 */
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR2, 0x00U);
|
||||
/* 写禁止顺序(手册要求):先清 WRTC2/WRTC3(CTR1),再清 WRTC1(CTR2) */
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR1, 0x00U);
|
||||
if (ret != SD2506_OK) return ret;
|
||||
|
||||
/* 再清 WRTC1=0, 同时 ARST=1 使能自动复位: 0x20 */
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR1, SD2506_CTR1_WRITE_OFF);
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR2, 0x00U);
|
||||
if (ret != SD2506_OK) return ret;
|
||||
|
||||
return SD2506_OK;
|
||||
@@ -132,28 +154,40 @@ static int sd2506_write_disable(void)
|
||||
|
||||
/* ======================== 公共 API 实现 ======================== */
|
||||
|
||||
int sd2506_init(void)
|
||||
{
|
||||
int ret;
|
||||
uint8_t id[8];
|
||||
/*
|
||||
* 函数功能:初始化 SD2506API-G RTC
|
||||
* 入口参数:无
|
||||
* 返回值: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 */
|
||||
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) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* 上电重置充电寄存器 18H = 82H (开启充电, 5K电阻)
|
||||
* 手册强烈建议每次上电时重置此值 */
|
||||
/* 上电重置充电寄存器 18H = 82H (开启充电 5K电阻)
|
||||
* 手册强烈建议每次上电时重置此寄存器 */
|
||||
ret = sd2506_write_reg(SD2506_REG_CHARGE, 0x82U);
|
||||
if (ret != SD2506_OK) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* 配置: 24小时制, 自动复位使能
|
||||
/* 配置: 24小时制,写允许状态下 0FH 的 WRTC 位必须为 1
|
||||
* 0FH = 0x20 (bit5=ARST=1, 其它标志位清零)
|
||||
* 写入时需注意: 写允许状态下 0FH 的 WRTC 位必须为1
|
||||
* 此处直接写入 0x20 即可 (ARST=1, 无 WRTC 位) */
|
||||
* 注意: 写入时需注意 0FH 的 WRTC 位必须为 1
|
||||
* 此处直接写入 0x20 即可 (ARST=1, 的 WRTC 位) */
|
||||
ret = sd2506_write_reg(SD2506_REG_CTR1, SD2506_CTR1_WRITE_OFF);
|
||||
if (ret != SD2506_OK) {
|
||||
return ret;
|
||||
@@ -162,30 +196,38 @@ int sd2506_init(void)
|
||||
return SD2506_OK;
|
||||
}
|
||||
|
||||
int sd2506_set_time(const sd2506_time_t *time)
|
||||
{
|
||||
int ret;
|
||||
uint8_t buf[7];
|
||||
/*
|
||||
* 函数功能:设置 RTC 时间日期
|
||||
* 入口参数:p_time - 时间结构体指针,包含要设置的时间
|
||||
* 返回值: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;
|
||||
}
|
||||
|
||||
/* 组装 7 字节时间数据 (BCD 码) */
|
||||
buf[0] = sd2506_dec_to_bcd(time->second); /* 00H: 秒 */
|
||||
buf[1] = sd2506_dec_to_bcd(time->minute); /* 01H: 分 */
|
||||
buf[2] = sd2506_dec_to_bcd(time->hour) | 0x80U; /* 02H: 时 (bit7=1, 24小时制) */
|
||||
buf[3] = sd2506_dec_to_bcd(time->week); /* 03H: 星期 */
|
||||
buf[4] = sd2506_dec_to_bcd(time->day); /* 04H: 日 */
|
||||
buf[5] = sd2506_dec_to_bcd(time->month); /* 05H: 月 */
|
||||
buf[6] = sd2506_dec_to_bcd(time->year - 2000U); /* 06H: 年 */
|
||||
buf[0] = sd2506_dec_to_bcd(p_time->second); /* 00H: 秒 */
|
||||
buf[1] = sd2506_dec_to_bcd(p_time->minute); /* 01H: 分 */
|
||||
buf[2] = sd2506_dec_to_bcd(p_time->hour) | 0x80U; /* 02H: 时(bit7=1, 24小时制) */
|
||||
buf[3] = sd2506_dec_to_bcd(p_time->week); /* 03H: 星期 */
|
||||
buf[4] = sd2506_dec_to_bcd(p_time->day); /* 04H: 日 */
|
||||
buf[5] = sd2506_dec_to_bcd(p_time->month); /* 05H: 月 */
|
||||
buf[6] = sd2506_dec_to_bcd(p_time->year - 2000U); /* 06H: 年 */
|
||||
|
||||
/* 开启写保护 */
|
||||
ret = sd2506_write_enable();
|
||||
if (ret != SD2506_OK) return ret;
|
||||
|
||||
/* 一次性写入 7 字节时间数据 (00H~06H)
|
||||
* 手册要求: 不可单独写某一个时间寄存器,否则可能引起错误进位 */
|
||||
* 手册要求: 不可单独写某一个时间寄存器,否则可能引起错误进入 */
|
||||
ret = sd2506_write_regs(SD2506_REG_SEC, buf, 7);
|
||||
if (ret != SD2506_OK) return ret;
|
||||
|
||||
@@ -196,39 +238,54 @@ int sd2506_set_time(const sd2506_time_t *time)
|
||||
return SD2506_OK;
|
||||
}
|
||||
|
||||
int sd2506_get_time(sd2506_time_t *time)
|
||||
{
|
||||
int ret;
|
||||
uint8_t buf[7];
|
||||
/*
|
||||
* 函数功能:读取 RTC 时间日期
|
||||
* 入口参数:p_time - 输出时间结构体指针 sd2506_time_t*(非空)
|
||||
* 返回值: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;
|
||||
}
|
||||
|
||||
/* 一次读取 7 字节时间数据 (00H~06H)
|
||||
* 手册说明: 读取时所有实时数据被锁存,避免错读 */
|
||||
* 手册说明: 读取时所有实时数据被锁存,避免错误 */
|
||||
ret = sd2506_read_regs(SD2506_REG_SEC, buf, 7);
|
||||
if (ret != SD2506_OK) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
time->second = sd2506_bcd_to_dec(buf[0] & 0x7FU); /* 00H: 秒 */
|
||||
time->minute = sd2506_bcd_to_dec(buf[1] & 0x7FU); /* 01H: 分 */
|
||||
time->hour = sd2506_bcd_to_dec(buf[2] & 0x7FU); /* 02H: 屏蔽 bit7 (12/24标志) */
|
||||
time->week = sd2506_bcd_to_dec(buf[3] & 0x07U); /* 03H: 星期 */
|
||||
time->day = sd2506_bcd_to_dec(buf[4] & 0x3FU); /* 04H: 日 */
|
||||
time->month = sd2506_bcd_to_dec(buf[5] & 0x1FU); /* 05H: 月 */
|
||||
time->year = sd2506_bcd_to_dec(buf[6]) + 2000U; /* 06H: 年 */
|
||||
p_time->second = sd2506_bcd_to_dec(buf[0] & 0x7FU); /* 00H: 秒 */
|
||||
p_time->minute = sd2506_bcd_to_dec(buf[1] & 0x7FU); /* 01H: 分 */
|
||||
p_time->hour = sd2506_bcd_to_dec(buf[2] & 0x7FU); /* 02H: 屏蔽 bit7 (12/24标志) */
|
||||
p_time->week = sd2506_bcd_to_dec(buf[3] & 0x07U); /* 03H: 星期 */
|
||||
p_time->day = sd2506_bcd_to_dec(buf[4] & 0x3FU); /* 04H: 日 */
|
||||
p_time->month = sd2506_bcd_to_dec(buf[5] & 0x1FU); /* 05H: 月 */
|
||||
p_time->year = sd2506_bcd_to_dec(buf[6]) + 2000U; /* 06H: 年 */
|
||||
|
||||
return SD2506_OK;
|
||||
}
|
||||
|
||||
int sd2506_get_temperature(int8_t *temp)
|
||||
{
|
||||
int ret;
|
||||
uint8_t val;
|
||||
/*
|
||||
* 函数功能:读取内部温度
|
||||
* 入口参数:p_temp - 输出温度值指针 int8_t*(非空)
|
||||
* 返回值:0-成功,2-I2C 错误 int
|
||||
* 限定条件:sd2506_init() 已调用成功
|
||||
* 函数说明:读取寄存器 0x16,bit7 为符号位
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -239,52 +296,79 @@ int sd2506_get_temperature(int8_t *temp)
|
||||
|
||||
/* bit7 为符号位, 其余为温度值 */
|
||||
if (val & 0x80U) {
|
||||
/* 负温度: 取补码 */
|
||||
*temp = (int8_t)(val | 0xF0U);
|
||||
/* 负温度 取补码 */
|
||||
*p_temp = (int8_t)(val | 0xF0U);
|
||||
} else {
|
||||
/* 正温度 */
|
||||
*temp = (int8_t)(val & 0x7FU);
|
||||
*p_temp = (int8_t)(val & 0x7FU);
|
||||
}
|
||||
|
||||
return SD2506_OK;
|
||||
}
|
||||
|
||||
int sd2506_get_battery_voltage(uint16_t *voltage)
|
||||
{
|
||||
int ret;
|
||||
uint8_t val_high, val_low;
|
||||
/*
|
||||
* 函数功能:读取电池电压(毫伏)
|
||||
* 入口参数:p_voltage - 输出电压指针 uint16_t*(非空)
|
||||
* 返回值: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;
|
||||
}
|
||||
|
||||
/* 读取 1AH bit7 (BAT8_VAL) 和 1BH (BAT_VL) */
|
||||
/* 读取 1AH bit7 (BAT8_VAL) 与 1BH (BAT_VL) */
|
||||
ret = sd2506_read_reg(SD2506_REG_CTR5, &val_high);
|
||||
if (ret != SD2506_OK) return ret;
|
||||
|
||||
ret = sd2506_read_reg(SD2506_REG_BAT_VAL, &val_low);
|
||||
if (ret != SD2506_OK) return ret;
|
||||
|
||||
/* 组合 9 位: bit8=BAT8_VAL(1AH bit7), bit7~0=BAT_VL(1BH)
|
||||
* 如 1AH=80H, 1BH=30H => 电压 = 0x130 = 304 => 3.04V => 3040mV */
|
||||
/* 组合 9 位值 bit8=BAT8_VAL(1AH bit7), bit7~0=BAT_VL(1BH)
|
||||
* 例: 1AH=80H, 1BH=30H => 电压 = 0x130 = 304 => 3.04V => 3040mV */
|
||||
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;
|
||||
}
|
||||
|
||||
int sd2506_get_id(uint8_t id[8])
|
||||
{
|
||||
if (id == NULL) {
|
||||
/*
|
||||
* 函数功能:读取 8 字节芯片 ID
|
||||
* 入口参数: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_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)
|
||||
{
|
||||
if (buf == NULL || len == 0 || addr >= SD2506_SRAM_SIZE) {
|
||||
/*
|
||||
* 函数功能:读取 SRAM 数据
|
||||
* 入口参数: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;
|
||||
}
|
||||
|
||||
@@ -292,12 +376,21 @@ int sd2506_read_sram(uint8_t addr, uint8_t *buf, uint8_t len)
|
||||
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)
|
||||
{
|
||||
if (buf == NULL || len == 0 || addr >= SD2506_SRAM_SIZE) {
|
||||
/*
|
||||
* 函数功能:写入 SRAM 数据
|
||||
* 入口参数: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;
|
||||
}
|
||||
|
||||
@@ -305,36 +398,44 @@ int sd2506_write_sram(uint8_t addr, const uint8_t *buf, uint8_t len)
|
||||
return SD2506_ERROR;
|
||||
}
|
||||
|
||||
/* SRAM 无需开写保护即可写入 (写保护仅对 00H~71H 有效, SRAM 是 2CH~71H)
|
||||
/* SRAM 无需开写保护即可写入 (写保护仅对 00H~71H 有效, SRAM 为 2CH~71H)
|
||||
* 但为安全起见, SRAM 写入也走写保护流程 */
|
||||
int ret;
|
||||
int ret = 0;
|
||||
ret = sd2506_write_enable();
|
||||
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;
|
||||
|
||||
ret = sd2506_write_disable();
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sd2506_set_alarm(const sd2506_time_t *time, uint8_t mask)
|
||||
{
|
||||
int ret;
|
||||
uint8_t buf[8];
|
||||
/*
|
||||
* 函数功能:设置闹钟中断
|
||||
* 入口参数:p_time - 报警时间结构体指针 sd2506_time_t*(非空)
|
||||
* 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;
|
||||
}
|
||||
|
||||
/* 组装 8 字节报警数据 (07H~0EH) */
|
||||
buf[0] = sd2506_dec_to_bcd(time->second) & 0x7FU; /* 07H: 秒报警 */
|
||||
buf[1] = sd2506_dec_to_bcd(time->minute) & 0x7FU; /* 08H: 分报警 */
|
||||
buf[2] = sd2506_dec_to_bcd(time->hour) & 0x3FU; /* 09H: 时报警 (最高位始终为0) */
|
||||
buf[0] = sd2506_dec_to_bcd(p_time->second) & 0x7FU; /* 07H: 秒报 */
|
||||
buf[1] = sd2506_dec_to_bcd(p_time->minute) & 0x7FU; /* 08H: 分报 */
|
||||
buf[2] = sd2506_dec_to_bcd(p_time->hour) & 0x3FU; /* 09H: 时报(最高位始终为0) */
|
||||
buf[3] = 0x00U; /* 0AH: 星期报警 */
|
||||
buf[4] = sd2506_dec_to_bcd(time->day) & 0x3FU; /* 0BH: 日报警 */
|
||||
buf[5] = sd2506_dec_to_bcd(time->month) & 0x1FU; /* 0CH: 月报警 */
|
||||
buf[6] = sd2506_dec_to_bcd(time->year - 2000U); /* 0DH: 年报警 */
|
||||
buf[4] = sd2506_dec_to_bcd(p_time->day) & 0x3FU; /* 0BH: 日报 */
|
||||
buf[5] = sd2506_dec_to_bcd(p_time->month) & 0x1FU; /* 0CH: 月报 */
|
||||
buf[6] = sd2506_dec_to_bcd(p_time->year - 2000U); /* 0DH: 年报 */
|
||||
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);
|
||||
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,
|
||||
SD2506_CTR2_INTAE | SD2506_CTR2_INTS0 | SD2506_CTR2_IM);
|
||||
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;
|
||||
}
|
||||
|
||||
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 */
|
||||
ret = sd2506_read_reg(SD2506_REG_CTR1, &val);
|
||||
@@ -371,11 +480,18 @@ int sd2506_clear_alarm(void)
|
||||
return SD2506_OK;
|
||||
}
|
||||
|
||||
int sd2506_read_ctr1(uint8_t *val)
|
||||
{
|
||||
if (val == NULL) {
|
||||
/*
|
||||
* 函数功能:读取控制寄存器 1 (CTR1)
|
||||
* 入口参数: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_read_reg(SD2506_REG_CTR1, val);
|
||||
return sd2506_read_reg(SD2506_REG_CTR1, p_val);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ extern "C" {
|
||||
#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_CTR3 0x11U /* 控制寄存器3 */
|
||||
|
||||
@@ -180,7 +180,7 @@ int sd2506_init(void);
|
||||
* 3. 关闭写保护
|
||||
* 注意:不可单独写某一个时间寄存器
|
||||
*/
|
||||
int sd2506_set_time(const sd2506_time_t *time);
|
||||
int sd2506_set_time(const sd2506_time_t *p_time);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 RTC 时间日期
|
||||
@@ -191,7 +191,7 @@ int sd2506_set_time(const sd2506_time_t *time);
|
||||
* 2. BCD 转十进制
|
||||
* 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() 已调用
|
||||
* 函数说明:读取 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)
|
||||
* 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
|
||||
@@ -219,7 +219,7 @@ int sd2506_get_battery_voltage(uint16_t *voltage);
|
||||
* 限定条件:sd2506_init() 已调用
|
||||
* 函数说明:读取 72H~79H 共 8 字节唯一 ID
|
||||
*/
|
||||
int sd2506_get_id(uint8_t id[8]);
|
||||
int sd2506_get_id(uint8_t p_id[8]);
|
||||
|
||||
/*
|
||||
* 函数功能:读取用户 SRAM 数据
|
||||
@@ -230,7 +230,7 @@ int sd2506_get_id(uint8_t id[8]);
|
||||
* 限定条件:sd2506_init() 已调用
|
||||
* 函数说明: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 数据
|
||||
@@ -242,7 +242,7 @@ int sd2506_read_sram(uint8_t addr, uint8_t *buf, uint8_t len);
|
||||
* 函数说明:SRAM 地址范围 0~69,对应寄存器 2CH~71H
|
||||
* 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
|
||||
* 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 - 输出值指针
|
||||
* 返回值:0 - 成功,-2 - I2C 通信错误
|
||||
*/
|
||||
int sd2506_read_ctr1(uint8_t *val);
|
||||
int sd2506_read_ctr1(uint8_t *p_val);
|
||||
|
||||
/*
|
||||
* 函数功能:BCD 码转十进制
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
/*
|
||||
* 模块名称:TPAFE5160 16位8通道同步采样ADC驱动
|
||||
* 模块功能:提供 TPAFE5160 并行接口模式下的初始化、过采样设置、转换启动、数据读取接口
|
||||
* 适用平台:STM32F407ZGT6,并行16位数据总线接 GPIOG[15:0]
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-07-17
|
||||
* 修改记录:
|
||||
* 2026-07-17 王建锋 创建初始版本,参考 AD7606 并行驱动及 TPAFE5160 手册
|
||||
* 2026-07-17 王建锋 增加 EXTI 中断读取模式
|
||||
* 模块名称:TPAFE5160 16<EFBFBD>?通道同步采样ADC驱动
|
||||
* 模块功能:提<EFBFBD>?TPAFE5160 并行接口模式下的初始化、过采样设置、转换启动、数据读取接<EFBFBD>? * 适用平台:STM32F407ZGT6,并<EFBC8C>?6位数据总线<E680BB>?GPIOG[15:0]
|
||||
* 作者:王建<EFBFBD>? * 创建日期<E697A5>?026-07-17
|
||||
* 修改记录<EFBFBD>? * 2026-07-17 王建<E78E8B>? 创建初始版本,参<EFBC8C>?AD7606 并行驱动<E9A9B1>?TPAFE5160 手册
|
||||
* 2026-07-17 王建<E78E8B>? 增加 EXTI 中断读取模式
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -19,35 +16,31 @@
|
||||
#define DBG_TAG "[ADC]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* ======================== 私有宏定义 ======================== */
|
||||
/* ======================== 私有宏定<EFBFBD>?======================== */
|
||||
|
||||
/*
|
||||
* 并行读取时序延时 (168MHz 主频,1 NOP ≈ 5.95ns)
|
||||
* 并行读取时序延时 (168MHz 主频<EFBFBD>? NOP <EFBFBD>?5.95ns)
|
||||
*
|
||||
* TPAFE5160 并行时序要求 (VDRIVE > 2.7V):
|
||||
* t10 (RD 低脉宽) ≥ 22ns
|
||||
* t11 (RD 高脉宽) ≥ 10ns
|
||||
* t14 (数据访问时间) ≤ 21ns (从 RD 下降沿算起)
|
||||
* t15 (数据保持时间) ≥ 6ns (从 RD 下降沿算起)
|
||||
* TPAFE5160 并行时序要求 (VDRIVE > 2.7V)<EFBFBD>? * t10 (RD 低脉<E4BD8E>? <20>?22ns
|
||||
* t11 (RD 高脉<EFBFBD>? <20>?10ns
|
||||
* t14 (数据访问时间) <20>?21ns (<28>?RD 下降沿算<E6B2BF>?
|
||||
* t15 (数据保持时间) <EFBFBD>?6ns (<EFBFBD>?RD 下降沿算<EFBFBD>?
|
||||
*
|
||||
* GPIO 写操作本身约 1 个 AHB 周期 (~6ns),
|
||||
* 加上端口延迟 (~12-18ns),实际 RD 引脚翻转滞后约 18-24ns。
|
||||
* 因此 NOP 延时只需覆盖数据建立时间即可。
|
||||
*/
|
||||
#define TP_NOP_5() __NOP(); __NOP(); __NOP(); __NOP(); __NOP()
|
||||
* GPIO 写操作本身约 1 <EFBFBD>?AHB 周期 (~6ns)<EFBFBD>? * 加上端口延迟 (~12-18ns),实<EFBC8C>?RD 引脚翻转滞后<E6BB9E>?18-24ns<6E>? * 因此 NOP 延时只需覆盖数据建立时间即可<E58DB3>? */
|
||||
#define TPAFE5160_NOP_5() __NOP(); __NOP(); __NOP(); __NOP(); __NOP()
|
||||
|
||||
/* RD 低脉宽延时:GPIO写(~6ns) + 5NOP(~30ns) > 22ns */
|
||||
#define TP_RD_LOW_DLY() do { TPAFE5160_RD_LOW(); TP_NOP_5(); } while (0)
|
||||
/* RD 低脉宽延时:GPIO<EFBFBD>?~6ns) + 5NOP(~30ns) > 22ns */
|
||||
#define TPAFE5160_RD_LOW_DLY() do { TPAFE5160_RD_LOW(); TPAFE5160_NOP_5(); } while (0)
|
||||
|
||||
/* RD 高脉宽延时:GPIO写(~6ns) + 3NOP(~18ns) > 10ns */
|
||||
#define TP_RD_HIGH_DLY() do { TPAFE5160_RD_HIGH(); __NOP(); __NOP(); __NOP(); } while (0)
|
||||
/* RD 高脉宽延时:GPIO<EFBFBD>?~6ns) + 3NOP(~18ns) > 10ns */
|
||||
#define TPAFE5160_RD_HIGH_DLY() do { TPAFE5160_RD_HIGH(); __NOP(); __NOP(); __NOP(); } while (0)
|
||||
|
||||
/* CONVST 脉冲延时:上升沿前需保证低电平,5NOP 覆盖 t5 ≥ 20ns */
|
||||
#define TP_CONVST_PULSE() do { \
|
||||
/* CONVST 脉冲延时:上升沿前需保证低电平,5NOP 覆盖 t5 <EFBFBD>?20ns */
|
||||
#define TPAFE5160_CONVST_PULSE() do { \
|
||||
TPAFE5160_CONVST_LOW(); \
|
||||
TP_NOP_5(); \
|
||||
TPAFE5160_NOP_5(); \
|
||||
TPAFE5160_CONVST_HIGH(); \
|
||||
TP_NOP_5(); \
|
||||
TPAFE5160_NOP_5(); \
|
||||
} while (0)
|
||||
|
||||
/* ======================== 私有函数声明 ======================== */
|
||||
@@ -59,22 +52,20 @@ static void tpafe5160_read_channels(uint8_t count, int16_t *buf);
|
||||
/*
|
||||
* 函数功能:初始化 TPAFE5160(设置过采样、等待就绪)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 成功,-2 - BUSY 超时
|
||||
* 限定条件:CubeMX 已完成 GPIO 初始化
|
||||
* 函数说明:1. 设置过采样为无过采样 (000)
|
||||
* 返回值:0 - 成功<EFBFBD>?2 - BUSY 超时
|
||||
* 限定条件:CubeMX 已完<EFBFBD>?GPIO 初始<EFBFBD>? * 函数说明<E8AFB4>?. 设置过采样为无过采样 (000)
|
||||
* 2. 确保 RD 为高、CONVST 为低
|
||||
* 3. 等待 BUSY 释放(转换空闲)
|
||||
*/
|
||||
int tpafe5160_init(void)
|
||||
{
|
||||
int tpafe5160_init(void) {
|
||||
/* 设置默认过采样:无过采样 (OS[2:0] = 000) */
|
||||
tpafe5160_set_os(TP_OS_NONE);
|
||||
tpafe5160_set_os(TPAFE5160_OS_NONE);
|
||||
|
||||
/* 确保控制引脚处于空闲状态 */
|
||||
/* 确保控制引脚处于空闲状<EFBFBD>?*/
|
||||
TPAFE5160_RD_HIGH();
|
||||
TPAFE5160_CONVST_LOW();
|
||||
|
||||
/* 等待 BUSY 释放,确保上电后无残留转换 */
|
||||
/* 等待 BUSY 释放,确保上电后无残留转<EFBFBD>?*/
|
||||
int ret = tpafe5160_wait_busy(TPAFE5160_CONV_TIMEOUT_MS);
|
||||
if (ret != TPAFE5160_OK) {
|
||||
DBG_ERROR("Init wait BUSY timeout");
|
||||
@@ -85,14 +76,10 @@ int tpafe5160_init(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:设置过采样率
|
||||
* 入口参数:os - 过采样率枚举值 tpafe5160_os_t
|
||||
* 返回值:无
|
||||
* 限定条件:GPIO 已初始化
|
||||
* 函数说明:通过 OS[2:0] 引脚设置过采样率,在下一次 BUSY 下降沿锁存生效
|
||||
*/
|
||||
void tpafe5160_set_os(tpafe5160_os_t os)
|
||||
{
|
||||
* 函数功能:设置过采样<EFBFBD>? * 入口参数:os - 过采样率枚举<E69E9A>? tpafe5160_os_t
|
||||
* 返回值:<EFBFBD>? * 限定条件:GPIO 已初始化
|
||||
* 函数说明:通过 OS[2:0] 引脚设置过采样率,在下一<E4B88B>?BUSY 下降沿锁存生<E5AD98>? */
|
||||
void tpafe5160_set_os(tpafe5160_os_t os) {
|
||||
TPAFE5160_OS0(os & 0x01); /* OS0 = bit0 */
|
||||
TPAFE5160_OS1((os >> 1) & 0x01); /* OS1 = bit1 */
|
||||
TPAFE5160_OS2((os >> 2) & 0x01); /* OS2 = bit2 */
|
||||
@@ -101,27 +88,22 @@ void tpafe5160_set_os(tpafe5160_os_t os)
|
||||
/*
|
||||
* 函数功能:启动一次转换(CONVST 上升沿触发)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:GPIO 已初始化
|
||||
* 函数说明:产生 CONVST 脉冲上升沿,启动全部8通道同步采样与转换
|
||||
*/
|
||||
void tpafe5160_start_conv(void)
|
||||
{
|
||||
TP_CONVST_PULSE();
|
||||
* 返回值:<EFBFBD>? * 限定条件:GPIO 已初始化
|
||||
* 函数说明:产<EFBFBD>?CONVST 脉冲上升沿,启动全部8通道同步采样与转<E4B88E>? */
|
||||
void tpafe5160_start_conv(void) {
|
||||
TPAFE5160_CONVST_PULSE();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:等待转换完成
|
||||
* 入口参数:timeout_ms - 超时时间 uint32_t > 0
|
||||
* 返回值:0 - 转换完成,-2 - 超时
|
||||
|
||||
* 函数功能:等待转换完<E68DA2>? * 入口参数:timeout_ms - 超时时间 uint32_t > 0
|
||||
* 返回值:0 - 转换完成<EFBFBD>?2 - 超时
|
||||
* 限定条件:已调用 tpafe5160_start_conv()
|
||||
* 函数说明:轮询 BUSY 引脚等待下降沿
|
||||
*/
|
||||
int tpafe5160_wait_busy(uint32_t timeout_ms)
|
||||
{
|
||||
* 函数说明:轮<EFBFBD>?BUSY 引脚等待下降<EFBFBD>? */
|
||||
int tpafe5160_wait_busy(uint32_t timeout_ms) {
|
||||
uint32_t tick_start = HAL_GetTick();
|
||||
|
||||
/* 等待 BUSY 释放(低电平表示空闲) */
|
||||
/* 等待 BUSY 释放(低电平表示空闲<EFBFBD>?*/
|
||||
while (TPAFE5160_BUSY_READ() == GPIO_PIN_SET) {
|
||||
if ((HAL_GetTick() - tick_start) >= timeout_ms) {
|
||||
return TPAFE5160_BUSY_TIMEOUT;
|
||||
@@ -132,29 +114,26 @@ int tpafe5160_wait_busy(uint32_t timeout_ms)
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询当前是否正在转换
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 正在转换,0 - 空闲
|
||||
* 函数功能:查询当前是否正在转<EFBFBD>? * 入口参数:无
|
||||
* 返回值:1 - 正在转换<E8BDAC>? - 空闲
|
||||
* 限定条件: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;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取全部8通道转换结果
|
||||
* 函数功能:读取全<EFBFBD>?通道转换结果
|
||||
* 入口参数:buf - 8个int16_t的输出缓冲区 int16_t* 不为 NULL
|
||||
* 返回值:0 - 成功,-2 - BUSY 超时
|
||||
* 返回值:0 - 成功<EFBFBD>?2 - BUSY 超时
|
||||
* 限定条件:GPIO 已初始化
|
||||
* 函数说明:1. 启动转换并等待 BUSY 释放
|
||||
* 函数说明<EFBFBD>?. 启动转换并等<EFBFBD>?BUSY 释放
|
||||
* 2. 连续8次拉低RD读取各通道数据
|
||||
* 3. 通过 FRSTDATA 验证第一通道
|
||||
*/
|
||||
int tpafe5160_read_all(int16_t *buf)
|
||||
{
|
||||
int ret;
|
||||
int tpafe5160_read_all(int16_t *buf) {
|
||||
int ret = 0;
|
||||
|
||||
/* 启动转换 */
|
||||
tpafe5160_start_conv();
|
||||
@@ -173,17 +152,15 @@ int tpafe5160_read_all(int16_t *buf)
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取指定通道的转换结果
|
||||
* 入口参数:channel - 通道号 uint8_t 0 - 7
|
||||
* 函数功能:读取指定通道的转换结<EFBFBD>? * 入口参数:channel - 通道<E9809A>? uint8_t 0 - 7
|
||||
* value - 输出指针 int16_t* 不为 NULL
|
||||
* 返回值:0 - 成功,-1 - 通道号无效,-2 - BUSY 超时
|
||||
* 返回值:0 - 成功<EFBFBD>?1 - 通道号无效,-2 - BUSY 超时
|
||||
* 限定条件:GPIO 已初始化
|
||||
* 函数说明:启动转换并等待完成后,连续读取至指定通道
|
||||
*/
|
||||
int tpafe5160_read_channel(uint8_t channel, int16_t *value)
|
||||
{
|
||||
int ret;
|
||||
int16_t buf[TPAFE5160_CH_NUM];
|
||||
int tpafe5160_read_channel(uint8_t channel, int16_t *value) {
|
||||
int ret = 0;
|
||||
int16_t buf[TPAFE5160_CH_NUM] = {0};
|
||||
|
||||
if (channel >= TPAFE5160_CH_NUM || value == NULL) {
|
||||
return TPAFE5160_ERROR;
|
||||
@@ -199,29 +176,23 @@ int tpafe5160_read_channel(uint8_t channel, int16_t *value)
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:原始ADC值转电压值
|
||||
* 入口参数:raw - ADC原始值 int16_t 有符号补码
|
||||
* 返回值:电压值 float 单位 V
|
||||
* 函数功能:原始ADC值转电压<EFBFBD>? * 入口参数:raw - ADC原始<E58E9F>? int16_t 有符号补<E58FB7>? * 返回值:电压<E794B5>? float 单位 V
|
||||
* 限定条件:无
|
||||
* 函数说明:±5V量程时 LSB = 10V / 65536 ≈ 152.59μV
|
||||
* ±10V量程时 LSB = 20V / 65536 ≈ 305.18μV
|
||||
* 硬件 RANGE 引脚接 GND,默认 ±5V 量程
|
||||
* 函数说明:<EFBFBD>?V量程<EFBFBD>?LSB = 10V / 65536 <EFBFBD>?152.59μV
|
||||
* ±10V量程<EFBFBD>?LSB = 20V / 65536 <EFBFBD>?305.18μV
|
||||
* 硬件 RANGE 引脚<EFBFBD>?GND,默<EFBFBD>?±5V 量程
|
||||
*/
|
||||
float tpafe5160_to_voltage(int16_t raw)
|
||||
{
|
||||
/* RANGE 接 GND → ±5V 量程,满量程 10V */
|
||||
float tpafe5160_to_voltage(int16_t raw) {
|
||||
/* RANGE <20>?GND <20>?±5V 量程,满量程 10V */
|
||||
return (float)raw * (10.0f / 65536.0f);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:直接读取并行数据总线(不启动转换)
|
||||
* 入口参数:无
|
||||
* 返回值:16位原始数据 uint16_t
|
||||
* 限定条件:RD 为低或 CS 与 RD 已拉低
|
||||
* 函数说明:读取 GPIOG->IDR 低16位,对应 DB[15:0]
|
||||
* 函数功能:直接读取并行数据总线(不启动转换<EFBFBD>? * 入口参数:无
|
||||
* 返回值:16位原始数<EFBFBD>? uint16_t
|
||||
* 限定条件:RD 为低<E4B8BA>?CS <20>?RD 已拉<E5B7B2>? * 函数说明:读<EFBC9A>?GPIOG->IDR <20>?6位,对应 DB[15:0]
|
||||
*/
|
||||
uint16_t tpafe5160_read_bus(void)
|
||||
{
|
||||
uint16_t tpafe5160_read_bus(void) {
|
||||
return TPAFE5160_READ_BUS();
|
||||
}
|
||||
|
||||
@@ -229,100 +200,82 @@ uint16_t tpafe5160_read_bus(void)
|
||||
|
||||
/*
|
||||
* 函数功能:通过 RD 脉冲连续读取多个通道数据
|
||||
* 入口参数:count - 要读取的通道数 uint8_t 1 - 8
|
||||
* buf - 输出缓冲区 int16_t* 不为 NULL
|
||||
* 返回值:无
|
||||
* 限定条件:转换已完成(BUSY 为低),RD 初始为高
|
||||
* 函数说明:每次 RD 下降沿输出一个通道数据,按通道1~8顺序输出
|
||||
* DB[15:0] 直接接 GPIOG[15:0],通过 IDR 寄存器一次读取
|
||||
*
|
||||
* 时序关键点 (VDRIVE > 2.7V, 168MHz 主频):
|
||||
* GPIO 写操作 ~6ns + 端口延迟 ~18ns → RD 引脚实际翻转约 24ns 后
|
||||
* t14 数据建立 ≤ 21ns → 数据在 RD 下降沿后 21ns 内有效
|
||||
* 5 个 NOP (~30ns) 覆盖建立时间
|
||||
* 入口参数:count - 要读取的通道<EFBFBD>? uint8_t 1 - 8
|
||||
* buf - 输出缓冲<EFBFBD>? int16_t* 不为 NULL
|
||||
* 返回值:<EFBFBD>? * 限定条件:转换已完成(BUSY 为低),RD 初始为高
|
||||
* 函数说明:每<EFBFBD>?RD 下降沿输出一个通道数据,按通道1~8顺序输出
|
||||
* DB[15:0] 直接<E79BB4>?GPIOG[15:0],通过 IDR 寄存器一次读<E6ACA1>? *
|
||||
* 时序关键<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) 覆盖建立时间
|
||||
*/
|
||||
static void tpafe5160_read_channels(uint8_t count, int16_t *buf)
|
||||
{
|
||||
static void tpafe5160_read_channels(uint8_t count, int16_t *buf) {
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
/* RD 下降沿:ADC 输出当前通道数据到 DB[15:0] */
|
||||
TP_RD_LOW_DLY();
|
||||
/* RD 下降沿:ADC 输出当前通道数据<EFBFBD>?DB[15:0] */
|
||||
TPAFE5160_RD_LOW_DLY();
|
||||
|
||||
/* 读取16位并行数据 */
|
||||
/* 读取16位并行数<EFBFBD>?*/
|
||||
buf[i] = (int16_t)TPAFE5160_READ_BUS();
|
||||
|
||||
/* 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_b[TPAFE5160_CH_NUM];
|
||||
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 中断(运行时重使能用)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:CubeMX 已完成 GPIO 和 NVIC 配置
|
||||
* 函数说明:正常启动流程无需调用,仅在 irq_disable() 后需要重新使能时使用
|
||||
* 函数功能:使<EFBFBD>?BUSY EXTI 中断(运行时重使能用<EFBFBD>? * 入口参数:无
|
||||
* 返回值:<EFBFBD>? * 限定条件:CubeMX 已完<E5B7B2>?GPIO <20>?NVIC 配置
|
||||
* 函数说明:正常启动流程无需调用,仅<EFBFBD>?irq_disable() 后需要重新使能时使用
|
||||
*/
|
||||
void tpafe5160_irq_enable(void)
|
||||
{
|
||||
void tpafe5160_irq_enable(void) {
|
||||
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:关闭 BUSY EXTI 中断
|
||||
* 函数功能:关<EFBFBD>?BUSY EXTI 中断
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:已调用 tpafe5160_irq_enable()
|
||||
* 返回值:<EFBFBD>? * 限定条件:已调用 tpafe5160_irq_enable()
|
||||
* 函数说明:仅禁用 NVIC 中断,GPIO 配置保持 CubeMX 设定
|
||||
*/
|
||||
void tpafe5160_irq_disable(void)
|
||||
{
|
||||
void tpafe5160_irq_disable(void) {
|
||||
HAL_NVIC_DisableIRQ(EXTI9_5_IRQn);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:启动转换(中断模式)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:已调用 tpafe5160_irq_enable()
|
||||
* 函数说明:产生 CONVST 脉冲,转换完成后由 EXTI 中断自动读取 8 通道数据
|
||||
* 函数功能:启动转换(中断模式<EFBFBD>? * 入口参数:无
|
||||
* 返回值:<EFBFBD>? * 限定条件:已调用 tpafe5160_irq_enable()
|
||||
* 函数说明:产<EFBFBD>?CONVST 脉冲,转换完成后<E68890>?EXTI 中断自动读取 8 通道数据
|
||||
*/
|
||||
void tpafe5160_start_conv_irq(void)
|
||||
{
|
||||
void tpafe5160_start_conv_irq(void) {
|
||||
s_ready = 0;
|
||||
TP_CONVST_PULSE();
|
||||
TPAFE5160_CONVST_PULSE();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:检查是否有新的转换数据
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 数据就绪,0 - 无新数据
|
||||
* 返回值:1 - 数据就绪<EFBFBD>? - 无新数据
|
||||
* 限定条件:中断模式已启用
|
||||
* 函数说明:在 EXTI 回调中置位,主循环读取后需调用 tpafe5160_clear_ready() 清除
|
||||
*/
|
||||
uint8_t tpafe5160_data_ready(void)
|
||||
{
|
||||
uint8_t tpafe5160_data_ready(void) {
|
||||
return s_ready;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:清除数据就绪标志
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:中断模式已启用
|
||||
* 函数功能:清除数据就绪标<EFBFBD>? * 入口参数:无
|
||||
* 返回值:<EFBFBD>? * 限定条件:中断模式已启用
|
||||
* 函数说明:主循环处理完数据后调用
|
||||
*/
|
||||
void tpafe5160_clear_ready(void)
|
||||
{
|
||||
void tpafe5160_clear_ready(void) {
|
||||
s_ready = 0;
|
||||
}
|
||||
|
||||
@@ -330,23 +283,16 @@ void tpafe5160_clear_ready(void)
|
||||
* 函数功能:获取数据缓冲区指针
|
||||
* 入口参数:无
|
||||
* 返回值:int16_t[8] 数据缓冲区的 const 指针
|
||||
* 限定条件:tpafe5160_data_ready() 返回 1 时调用
|
||||
* 函数说明:双缓冲切换,ISR 写另一个缓冲区,主循环安全读取当前缓冲区
|
||||
*/
|
||||
const int16_t* tpafe5160_get_buf(void)
|
||||
{
|
||||
* 限定条件:tpafe5160_data_ready() 返回 1 时调<EFBFBD>? * 函数说明:双缓冲切换,ISR 写另一个缓冲区,主循环安全读取当前缓冲<E7BC93>? */
|
||||
const int16_t* tpafe5160_get_buf(void) {
|
||||
return (s_buf_sel == 0) ? s_buf_a : s_buf_b;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:BUSY 下降沿 EXTI 处理(由 HAL_GPIO_EXTI_Callback 调用)
|
||||
* 入口参数:GPIO_Pin - 触发中断的引脚号
|
||||
* 返回值:无
|
||||
* 限定条件:BUSY EXTI 已使能
|
||||
* 函数说明:转换完成后自动读取 8 通道数据到缓冲区,耗时约 0.5µs (168MHz)
|
||||
* 函数功能:BUSY 下降<EFBFBD>?EXTI 处理(由 HAL_GPIO_EXTI_Callback 调用<EFBFBD>? * 入口参数:GPIO_Pin - 触发中断的引脚号
|
||||
* 返回值:<EFBFBD>? * 限定条件:BUSY EXTI 已使<E5B7B2>? * 函数说明:转换完成后自动读取 8 通道数据到缓冲区,耗时<E88097>?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) {
|
||||
return;
|
||||
}
|
||||
@@ -357,9 +303,9 @@ void tpafe5160_exti_handler(uint16_t GPIO_Pin)
|
||||
/* 连续读取8个通道,约 0.5µs @ 168MHz */
|
||||
uint8_t 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();
|
||||
TP_RD_HIGH_DLY();
|
||||
TPAFE5160_RD_HIGH_DLY();
|
||||
}
|
||||
|
||||
/* 切换缓冲区并标记就绪 */
|
||||
|
||||
@@ -48,20 +48,20 @@ extern "C" {
|
||||
|
||||
/* 过采样率枚举 (OS[2:0] 编码,OS2为MSB, OS0为LSB) */
|
||||
typedef enum {
|
||||
TP_OS_NONE = 0, /* 000 — 无过采样,350 kSPS */
|
||||
TP_OS_X2 = 1, /* 001 — 2倍过采样,175 kSPS */
|
||||
TP_OS_X4 = 2, /* 010 — 4倍过采样,87.5 kSPS */
|
||||
TP_OS_X8 = 3, /* 011 — 8倍过采样,43.75 kSPS */
|
||||
TP_OS_X16 = 4, /* 100 — 16倍过采样,21.875 kSPS */
|
||||
TP_OS_X32 = 5, /* 101 — 32倍过采样,10.94 kSPS */
|
||||
TP_OS_X64 = 6, /* 110 — 64倍过采样,5.47 kSPS */
|
||||
TP_OS_HBW = 7 /* 111 — 高带宽模式 (~30kHz),350 kSPS */
|
||||
TPAFE5160_OS_NONE = 0, /* 000 — 无过采样,350 kSPS */
|
||||
TPAFE5160_OS_X2 = 1, /* 001 — 2倍过采样,175 kSPS */
|
||||
TPAFE5160_OS_X4 = 2, /* 010 — 4倍过采样,87.5 kSPS */
|
||||
TPAFE5160_OS_X8 = 3, /* 011 — 8倍过采样,43.75 kSPS */
|
||||
TPAFE5160_OS_X16 = 4, /* 100 — 16倍过采样,21.875 kSPS */
|
||||
TPAFE5160_OS_X32 = 5, /* 101 — 32倍过采样,10.94 kSPS */
|
||||
TPAFE5160_OS_X64 = 6, /* 110 — 64倍过采样,5.47 kSPS */
|
||||
TPAFE5160_OS_HBW = 7 /* 111 — 高带宽模式 (~30kHz),350 kSPS */
|
||||
} tpafe5160_os_t;
|
||||
|
||||
/* 输入量程枚举 */
|
||||
typedef enum {
|
||||
TP_RANGE_5V = 0, /* ±5V (RANGE = LOW) */
|
||||
TP_RANGE_10V = 1 /* ±10V (RANGE = HIGH) */
|
||||
TPAFE5160_RANGE_5V = 0, /* ±5V (RANGE = LOW) */
|
||||
TPAFE5160_RANGE_10V = 1 /* ±10V (RANGE = HIGH) */
|
||||
} tpafe5160_range_t;
|
||||
|
||||
/* 返回值定义 */
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
* - 两者都不定义:所有 DBG_* 宏为空操作(release 版本)
|
||||
*
|
||||
* 用户可在本文件中直接控制,也可在编译器预定义中覆盖
|
||||
* 适用平台:所有 BSP 驱动
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-07-18
|
||||
* 修改记录:
|
||||
* 2026-07-18 王建锋 创建初始版本
|
||||
*/
|
||||
|
||||
#ifndef __DBG_CFG_H
|
||||
@@ -28,6 +33,12 @@ extern "C" {
|
||||
*/
|
||||
/* #define DBG_DEBUG_ENABLE */
|
||||
|
||||
/*
|
||||
* 系统时间戳开关:取消注释在所有日志行首追加 [YYYY-MM-DD HH:MM:SS] 时间戳
|
||||
* 依赖 sys_clock_init() 已调用(SD2506 RTC 初始化完成)
|
||||
*/
|
||||
#define APP_TIMESTAMP_ENABLE
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
* #define DBG_TAG "[CH395]"
|
||||
* #include "dbg_log.h"
|
||||
* 适用平台:所有 BSP 驱动
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-07-18
|
||||
* 修改记录:
|
||||
* 2026-07-18 王建锋 创建初始版本
|
||||
*/
|
||||
|
||||
#ifndef __DBG_LOG_H
|
||||
@@ -18,8 +22,13 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
#include "dbg_cfg.h"
|
||||
|
||||
#ifdef APP_TIMESTAMP_ENABLE
|
||||
#include "sys_clock.h"
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 总开关:DBG_ENABLE
|
||||
* 定义 → 输出 DBG_ERROR / DBG_INFO;若 DBG_DEBUG_ENABLE 也定义则输出 DBG_DEBUG
|
||||
@@ -44,31 +53,76 @@ extern "C" {
|
||||
#define DBG_TAG ""
|
||||
#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 { \
|
||||
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 { \
|
||||
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
|
||||
#define DBG_DEBUG(fmt, ...) do { \
|
||||
DBG_PRINT(DBG_TAG "(%s:%d) " fmt "\r\n", __func__, __LINE__, ##__VA_ARGS__); \
|
||||
} while(0)
|
||||
} while (0)
|
||||
#else
|
||||
#define DBG_DEBUG(fmt, ...)
|
||||
#endif
|
||||
|
||||
#endif /* APP_TIMESTAMP_ENABLE */
|
||||
|
||||
#else /* DBG_ENABLE not defined — all debug output disabled */
|
||||
|
||||
#define DBG_ERROR(fmt, ...)
|
||||
|
||||
@@ -55,6 +55,7 @@ void DebugMon_Handler(void);
|
||||
void DMA1_Stream3_IRQHandler(void);
|
||||
void DMA1_Stream4_IRQHandler(void);
|
||||
void EXTI9_5_IRQHandler(void);
|
||||
void SPI1_IRQHandler(void);
|
||||
void SPI2_IRQHandler(void);
|
||||
void USART1_IRQHandler(void);
|
||||
void USART2_IRQHandler(void);
|
||||
@@ -62,7 +63,9 @@ void USART3_IRQHandler(void);
|
||||
void UART4_IRQHandler(void);
|
||||
void UART5_IRQHandler(void);
|
||||
void TIM7_IRQHandler(void);
|
||||
void DMA2_Stream0_IRQHandler(void);
|
||||
void DMA2_Stream2_IRQHandler(void);
|
||||
void DMA2_Stream3_IRQHandler(void);
|
||||
void DMA2_Stream7_IRQHandler(void);
|
||||
/* USER CODE BEGIN EFP */
|
||||
|
||||
|
||||
43
Lib/FatFs/diskio.h
Normal file
43
Lib/FatFs/diskio.h
Normal 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
7084
Lib/FatFs/ff.c
Normal file
File diff suppressed because it is too large
Load Diff
429
Lib/FatFs/ff.h
Normal file
429
Lib/FatFs/ff.h
Normal 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
41
Lib/FatFs/ffconf.h
Normal 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
50
Lib/dhara/bytes.h
Normal 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
45
Lib/dhara/error.h
Normal 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
884
Lib/dhara/journal.c
Normal 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
256
Lib/dhara/journal.h
Normal 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
530
Lib/dhara/map.c
Normal 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, ¤t, 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
114
Lib/dhara/map.h
Normal 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
105
Lib/dhara/nand.h
Normal 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
|
||||
@@ -338,9 +338,9 @@
|
||||
<v6Rtti>0</v6Rtti>
|
||||
<VariousControls>
|
||||
<MiscControls></MiscControls>
|
||||
<Define>USE_HAL_DRIVER,STM32F407xx</Define>
|
||||
<Define>USE_HAL_DRIVER,STM32F407xx,NDEBUG</Define>
|
||||
<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>
|
||||
</Cads>
|
||||
<Aads>
|
||||
@@ -752,6 +752,51 @@
|
||||
</File>
|
||||
</Files>
|
||||
</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>
|
||||
<GroupName>Drivers/STM32F4xx_HAL_Driver</GroupName>
|
||||
<Files>
|
||||
@@ -1176,16 +1221,6 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\CH395F\ch395f.c</FilePath>
|
||||
</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>
|
||||
<FileName>gd5f2gq5ue.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
@@ -1216,45 +1251,30 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NET\net_socket.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
<GroupName>lib/FlashDB</GroupName>
|
||||
<Files>
|
||||
<File>
|
||||
<FileName>fdb.c</FileName>
|
||||
<FileName>nand_ftl.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Lib\FlashDB\src\fdb.c</FilePath>
|
||||
<FilePath>..\Drivers\BSP\GD5F2GQ5UE\nand_ftl.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>fdb_kvdb.c</FileName>
|
||||
<FileName>lftpd.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Lib\FlashDB\src\fdb_kvdb.c</FilePath>
|
||||
<FilePath>..\Drivers\BSP\NET\lftpd\lftpd.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>fdb_tsdb.c</FileName>
|
||||
<FileName>lftpd_inet.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Lib\FlashDB\src\fdb_tsdb.c</FilePath>
|
||||
<FilePath>..\Drivers\BSP\NET\lftpd\lftpd_inet.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>fdb_utils.c</FileName>
|
||||
<FileName>lftpd_io.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Lib\FlashDB\src\fdb_utils.c</FilePath>
|
||||
<FilePath>..\Drivers\BSP\NET\lftpd\lftpd_io.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>fal.c</FileName>
|
||||
<FileName>lftpd_string.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Lib\FlashDB\port\fal\src\fal.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>
|
||||
<FilePath>..\Drivers\BSP\NET\lftpd\lftpd_string.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
@@ -1892,6 +1912,130 @@
|
||||
</File>
|
||||
</Files>
|
||||
</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>
|
||||
<GroupName>::CMSIS</GroupName>
|
||||
</Group>
|
||||
|
||||
1
MDK-ARM/_build.cmd
Normal file
1
MDK-ARM/_build.cmd
Normal file
@@ -0,0 +1 @@
|
||||
@echo off && cd /d "%~dp0" && C:\Keil_v5\UV4\UV4.exe -r "STM32F407-Demo.uvprojx" -t STM32F407-Demo -j0
|
||||
@@ -1,4 +1,76 @@
|
||||
*** 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).
|
||||
Build Time Elapsed: 00:00:00
|
||||
Build Time Elapsed: 00:00:14
|
||||
|
||||
0
MDK-ARM/err.txt
Normal file
0
MDK-ARM/err.txt
Normal file
0
MDK-ARM/out.txt
Normal file
0
MDK-ARM/out.txt
Normal file
@@ -6,7 +6,29 @@ Dma.Request0=USART1_TX
|
||||
Dma.Request1=USART1_RX
|
||||
Dma.Request2=SPI2_RX
|
||||
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.FIFOMode=DMA_FIFOMODE_DISABLE
|
||||
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.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode
|
||||
FREERTOS.FootprintOK=true
|
||||
FREERTOS.IPParameters=Tasks01,configENABLE_FPU,FootprintOK,configTOTAL_HEAP_SIZE
|
||||
FREERTOS.Tasks01=defaultTask,24,512,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;netTask,24,1024,StartNetTask,Default,NULL,Dynamic,NULL,NULL
|
||||
FREERTOS.IPParameters=Tasks01,configENABLE_FPU,FootprintOK,configTOTAL_HEAP_SIZE,Queues01
|
||||
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.configTOTAL_HEAP_SIZE=30720
|
||||
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.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.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_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.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
|
||||
@@ -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.PendSV_IRQn=true\:15\:0\:false\:false\:false\:true\:false\:false\:false
|
||||
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.SVCall_IRQn=true\:0\:0\:false\:false\:false\:false\:false\:false\:false
|
||||
NVIC.SavedPendsvIrqHandlerGenerated=true
|
||||
@@ -301,9 +327,10 @@ PCC.PartNumber=STM32F407ZGTx
|
||||
PCC.Series=STM32F4
|
||||
PCC.Temperature=25
|
||||
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_PuPd=GPIO_NOPULL
|
||||
PD0.GPIO_Speed=GPIO_SPEED_FREQ_VERY_HIGH
|
||||
PD0.Locked=true
|
||||
PD0.Signal=GPIO_Output
|
||||
PD1.GPIOParameters=GPIO_Label
|
||||
@@ -524,8 +551,8 @@ RCC.VCOOutputFreq_Value=336000000
|
||||
RCC.VcooutputI2S=192000000
|
||||
SH.GPXTI7.0=GPIO_EXTI7
|
||||
SH.GPXTI7.ConfNb=1
|
||||
SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_8
|
||||
SPI1.CalculateBaudRate=10.5 MBits/s
|
||||
SPI1.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_2
|
||||
SPI1.CalculateBaudRate=42.0 MBits/s
|
||||
SPI1.Direction=SPI_DIRECTION_2LINES
|
||||
SPI1.IPParameters=VirtualType,Mode,Direction,CalculateBaudRate,BaudRatePrescaler
|
||||
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.Signal=SYS_VS_tim7
|
||||
board=custom
|
||||
rtos.0.ip=FREERTOS
|
||||
|
||||
@@ -50,9 +50,15 @@ void MX_DMA_Init(void)
|
||||
/* DMA1_Stream4_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 7, 0);
|
||||
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 */
|
||||
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 9, 0);
|
||||
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 */
|
||||
HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 9, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);
|
||||
|
||||
167
Src/freertos.c
167
Src/freertos.c
@@ -25,11 +25,18 @@
|
||||
|
||||
/* Private includes ----------------------------------------------------------*/
|
||||
/* USER CODE BEGIN Includes */
|
||||
#include "net_socket.h"
|
||||
#include "net_select.h"
|
||||
#include <string.h>
|
||||
#include "net_task.h"
|
||||
#include "ch395f.h"
|
||||
#include "ch395f_test.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_TESTS:netTask 条件创建 */
|
||||
#include "time_sync.h"
|
||||
/* USER CODE END Includes */
|
||||
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
@@ -39,7 +46,6 @@
|
||||
|
||||
/* Private define ------------------------------------------------------------*/
|
||||
/* USER CODE BEGIN PD */
|
||||
|
||||
/* USER CODE END PD */
|
||||
|
||||
/* Private macro -------------------------------------------------------------*/
|
||||
@@ -49,9 +55,13 @@
|
||||
|
||||
/* Private variables ---------------------------------------------------------*/
|
||||
/* USER CODE BEGIN Variables */
|
||||
#ifdef ENABLE_PHASE4_TESTS
|
||||
static uint8_t s_net_buf[1500];
|
||||
#endif
|
||||
/* 存储测试变量(g_storage_stats / s_perf_buf)已迁至 test/storage_test_task.c */
|
||||
osThreadId_t timeSyncTaskHandle;
|
||||
const osThreadAttr_t timeSyncTask_attributes = {
|
||||
.name = "timeSyncTask",
|
||||
.stack_size = 512 * 4,
|
||||
.priority = (osPriority_t) osPriorityNormal,
|
||||
};
|
||||
/* USER CODE END Variables */
|
||||
/* Definitions for defaultTask */
|
||||
osThreadId_t defaultTaskHandle;
|
||||
@@ -67,6 +77,25 @@ const osThreadAttr_t netTask_attributes = {
|
||||
.stack_size = 1024 * 4,
|
||||
.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 -----------------------------------------------*/
|
||||
/* USER CODE BEGIN FunctionPrototypes */
|
||||
@@ -75,6 +104,8 @@ const osThreadAttr_t netTask_attributes = {
|
||||
|
||||
void StartDefaultTask(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) */
|
||||
|
||||
@@ -100,6 +131,10 @@ void MX_FREERTOS_Init(void) {
|
||||
/* start timers, add new ones, ... */
|
||||
/* 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 */
|
||||
/* add queues, ... */
|
||||
/* USER CODE END RTOS_QUEUES */
|
||||
@@ -111,8 +146,22 @@ void MX_FREERTOS_Init(void) {
|
||||
/* creation of netTask */
|
||||
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 */
|
||||
/* 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 BEGIN RTOS_EVENTS */
|
||||
@@ -131,59 +180,85 @@ void MX_FREERTOS_Init(void) {
|
||||
void StartDefaultTask(void *argument)
|
||||
{
|
||||
/* USER CODE BEGIN StartDefaultTask */
|
||||
/* Infinite loop */
|
||||
for(;;)
|
||||
{
|
||||
osDelay(1);
|
||||
(void)argument;
|
||||
|
||||
/* 系统启动期挂载 NAND 上的 FatFS 卷,供 FTP 及其它模块访问。
|
||||
* f_mount(forced=1) 会触发 disk_initialize() → nand_ftl_init()
|
||||
* (dhara_map_resume)。此前默认构建从未挂载,导致 FTP 的
|
||||
* LIST/STOR/RETR 全部返回 550(f_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 BEGIN Header_StartNetTask */
|
||||
/**
|
||||
* @brief Function implementing the netTask thread.
|
||||
* @param argument: Not used
|
||||
* @retval None
|
||||
*/
|
||||
* @brief Function implementing the netTask thread.
|
||||
* @param argument: Not used
|
||||
* @retval None
|
||||
*/
|
||||
/* USER CODE END Header_StartNetTask */
|
||||
void StartNetTask(void *argument)
|
||||
{
|
||||
/* 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(;;)
|
||||
{
|
||||
net_poll();
|
||||
|
||||
#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);
|
||||
osDelay(1);
|
||||
}
|
||||
/* USER CODE END StartNetTask */
|
||||
/* USER CODE END StartCh395fTestTask */
|
||||
}
|
||||
|
||||
/* Private application code --------------------------------------------------*/
|
||||
|
||||
15
Src/gpio.c
15
Src/gpio.c
@@ -128,12 +128,12 @@ void MX_GPIO_Init(void)
|
||||
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
|
||||
HAL_GPIO_Init(CH395_SCS_GPIO_Port, &GPIO_InitStruct);
|
||||
|
||||
/*Configure GPIO pin : ST_DIR4_Pin */
|
||||
GPIO_InitStruct.Pin = ST_DIR4_Pin;
|
||||
/*Configure GPIO pins : ST_DIR4_Pin TP_RD_Pin TP_CONVST_Pin */
|
||||
GPIO_InitStruct.Pin = ST_DIR4_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_LOW;
|
||||
HAL_GPIO_Init(ST_DIR4_GPIO_Port, &GPIO_InitStruct);
|
||||
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
|
||||
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
|
||||
|
||||
/*Configure GPIO pin : TP_FRSTDATA_Pin */
|
||||
GPIO_InitStruct.Pin = TP_FRSTDATA_Pin;
|
||||
@@ -141,13 +141,6 @@ void MX_GPIO_Init(void)
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
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 */
|
||||
GPIO_InitStruct.Pin = TP_BUSY_Pin;
|
||||
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
|
||||
|
||||
96
Src/main.c
96
Src/main.c
@@ -36,7 +36,8 @@
|
||||
#include "rs485.h"
|
||||
#include "net_socket.h"
|
||||
#include "net_select.h"
|
||||
#include "ch395f_test.h"
|
||||
#include "app_main.h"
|
||||
#include "sys_clock.h"
|
||||
/* USER CODE END Includes */
|
||||
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
@@ -62,8 +63,6 @@
|
||||
rs485_handle_t g_rs485;
|
||||
#define RS485_RX_BUF_SIZE 256
|
||||
uint8_t g_rs485_rx_buf[RS485_RX_BUF_SIZE];
|
||||
/* TCP Server Echo 测试 */
|
||||
static int s_listen_sock = -1;
|
||||
/* USER CODE END PV */
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
@@ -118,26 +117,21 @@ int main(void)
|
||||
/* USER CODE BEGIN 2 */
|
||||
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 */
|
||||
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]);
|
||||
/* RS-485 初始化(UART5 + PD0 方向控制) */
|
||||
rs485_init(&g_rs485, &huart5, ST_DIR4_GPIO_Port, ST_DIR4_Pin);
|
||||
DBG_INFO("RS485 init: OK (UART5, DE=PD0)");
|
||||
|
||||
/* 强制 100M 全双工,建立 PHY 链路 */
|
||||
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");
|
||||
/* 启动第一次 RS-485 接收 */
|
||||
rs485_receive_start(&g_rs485, g_rs485_rx_buf, RS485_RX_BUF_SIZE);
|
||||
|
||||
/* GD5F 初始化测试 */
|
||||
int ret = gd5f2gq5ue_init();
|
||||
@@ -155,12 +149,8 @@ int main(void)
|
||||
DBG_INFO("ADC init: %s",
|
||||
(tp_ret == TPAFE5160_OK) ? "OK" : "TIMEOUT");
|
||||
|
||||
/* SD2506 RTC 初始化测试 */
|
||||
int sd_ret = sd2506_init();
|
||||
DBG_INFO("SD2506 init: %s",
|
||||
(sd_ret == SD2506_OK) ? "OK" : "I2C ERROR");
|
||||
|
||||
if (sd_ret == SD2506_OK) {
|
||||
/* SD2506 RTC 诊断信息 */
|
||||
{
|
||||
/* 读取 ID */
|
||||
uint8_t sd_id[8];
|
||||
sd2506_get_id(sd_id);
|
||||
@@ -188,56 +178,10 @@ int main(void)
|
||||
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 中断) */
|
||||
tpafe5160_start_conv_irq();
|
||||
|
||||
/* CH395F 驱动测试 — 各阶段独立运行,注释 define 可跳过 */
|
||||
#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
|
||||
/* CH395F 测试已全部移至 ch395fTestTask */
|
||||
/* USER CODE END 2 */
|
||||
|
||||
/* Init scheduler */
|
||||
@@ -256,10 +200,8 @@ int main(void)
|
||||
/* USER CODE END WHILE */
|
||||
|
||||
/* USER CODE BEGIN 3 */
|
||||
/* 网络轮询已移到 FreeRTOS netTask 中 */
|
||||
;
|
||||
/* USER CODE END 3 */
|
||||
}
|
||||
/* USER CODE END 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
50
Src/spi.c
50
Src/spi.c
@@ -26,6 +26,8 @@
|
||||
|
||||
SPI_HandleTypeDef hspi1;
|
||||
SPI_HandleTypeDef hspi2;
|
||||
DMA_HandleTypeDef hdma_spi1_rx;
|
||||
DMA_HandleTypeDef hdma_spi1_tx;
|
||||
DMA_HandleTypeDef hdma_spi2_rx;
|
||||
DMA_HandleTypeDef hdma_spi2_tx;
|
||||
|
||||
@@ -47,7 +49,7 @@ void MX_SPI1_Init(void)
|
||||
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
|
||||
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
|
||||
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.TIMode = SPI_TIMODE_DISABLE;
|
||||
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
|
||||
@@ -119,6 +121,46 @@ void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
|
||||
GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
|
||||
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 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);
|
||||
|
||||
/* 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 END SPI1_MspDeInit 1 */
|
||||
|
||||
@@ -55,8 +55,11 @@
|
||||
/* USER CODE END 0 */
|
||||
|
||||
/* 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_tx;
|
||||
extern SPI_HandleTypeDef hspi1;
|
||||
extern SPI_HandleTypeDef hspi2;
|
||||
extern DMA_HandleTypeDef hdma_usart1_tx;
|
||||
extern DMA_HandleTypeDef hdma_usart1_rx;
|
||||
@@ -211,6 +214,20 @@ void EXTI9_5_IRQHandler(void)
|
||||
/* 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.
|
||||
*/
|
||||
@@ -309,6 +326,20 @@ void TIM7_IRQHandler(void)
|
||||
/* 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.
|
||||
*/
|
||||
@@ -323,6 +354,20 @@ void DMA2_Stream2_IRQHandler(void)
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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:60000,PC 发到 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:8080,PC 连到 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 4(TCP Client 连 8080)。
|
||||
|
||||
**通过标准**:同 Phase 4(10/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 未完全关闭 | 已修复:先 close(RST)不等 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()` |
|
||||
@@ -1,221 +0,0 @@
|
||||
# CH395F 驱动陷阱记录
|
||||
|
||||
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
|
||||
|
||||
---
|
||||
|
||||
## Trap 01:Socket 4~7 自动分配不到
|
||||
|
||||
### 现象
|
||||
多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 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 03:DHCP 包 xid 偏移错误
|
||||
|
||||
### 现象
|
||||
Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid,握手失败。
|
||||
|
||||
### 根因
|
||||
错误使用了 `data[232:236]` 作为 xid(Wireshark 某旧版显示误导),实际 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 04:UDP 发送后必须等待 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 05:CH395F 与 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 06:TCP KeepAlive 参数必须为 500ms 倍数
|
||||
|
||||
### 现象
|
||||
设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。
|
||||
|
||||
### 根因
|
||||
CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。
|
||||
且 `IDLE` 必须 > `INTVL`。
|
||||
|
||||
### 正确值
|
||||
```c
|
||||
ch395f_set_keepalive_idle(60000); // 60s(500 倍数)
|
||||
ch395f_set_keepalive_intvl(5000); // 5s(500 倍数)
|
||||
ch395f_set_keepalive_cnt(3); // 3 次
|
||||
```
|
||||
|
||||
### 对应文件
|
||||
`Drivers/BSP/NET/net_socket.c` → `net_init()`
|
||||
|
||||
### 发现时间
|
||||
Phase 2 测试,2025-07
|
||||
|
||||
---
|
||||
|
||||
## Trap 07:TCP 关闭重连直接 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 08:RECV 中断电平触发,避免无限循环
|
||||
|
||||
### 现象
|
||||
`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_2,close 不再发 RST,Socket 卡住数分钟
|
||||
- ✅ `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 静默丢弃
|
||||
@@ -1,400 +0,0 @@
|
||||
# FlashDB 使用说明
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本项目使用 [FlashDB](https://github.com/armink/FlashDB) 嵌入式数据库,提供两种数据库:
|
||||
|
||||
- **KVDB**(键值数据库):存储键值对,类似 Redis 简化版
|
||||
- **TSDB**(时序数据库):存储带时间戳的日志记录,支持按时间范围查询
|
||||
|
||||
底层存储介质为 GD5F2GQ5UE SPI NAND Flash(256MB),通过 FAL(Flash 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 芯片参数
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 型号 | GD5F2GQ5UE(GigaDevice) |
|
||||
| 总容量 | 2Gbit = 256MB |
|
||||
| 页大小 | 2048 字节 |
|
||||
| Spare 区 | 64 字节 |
|
||||
| 每块页数 | 64 页 |
|
||||
| 块大小 | 128KB(64 × 2048) |
|
||||
| 总块数 | 2048 |
|
||||
| SPI 模式 | Mode 0(CPOL=0, CPHA=0) |
|
||||
| SPI 时钟 | 42MHz(APB2=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. 读取芯片 ID(9Fh),校验 MID=0xC8, DID=0x52
|
||||
4. 使能内部 ECC:SET_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 区不可直接访问**:驱动已使能内部 ECC,Spare 区由芯片硬件管理
|
||||
|
||||
### 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` 已重定向到 USART1(PA9/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 */
|
||||
```
|
||||
558
docs/STM32F4-Base存储架构说明.md
Normal file
558
docs/STM32F4-Base存储架构说明.md
Normal file
@@ -0,0 +1,558 @@
|
||||
# STM32F4-Base 存储架构说明
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本 项目在 GD5F2GQ5UE SPI NAND Flash(256MB)上实现了三层存储软件栈:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 应用程序 (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.6,ECC 使能后整段 spare 仍可读取(仅 0x840~0x87F 的 ECC 校验区禁止编程),因此读取 0x800(spare 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),其中 **2048(0x800)就是 spare 的第 0 字节**
|
||||
|
||||
**为什么坏块标记的列地址是 2048**:main 区占据了前 2048 字节,spare 区从 2048 起算,故 spare byte 0 落在列地址 2048(0x800)。驱动把 `GD5F_PAGE_SIZE`(=2048)作为 `column` 传给 `gd5f_read_from_cache()`(`gd5f2gq5ue.c:307-308` 将其拼成 2 字节列地址),芯片即从 spare[0] 开始回数据——这正是出厂坏块标记位(手册 §12.4 / Table 12-6:First 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 层可见,多任务读写同一文件需在应用层同步。
|
||||
244
docs/测试开发标准流程.md
Normal file
244
docs/测试开发标准流程.md
Normal 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 == 0xA8(CH395F_ERR_SUCCESS = 0x00)
|
||||
// 失败:回复 != 0xA8(CH395F_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_ready(netTask 初始化完成)
|
||||
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 实现)
|
||||
→ @build(0 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` | 已知硬件/软件陷阱及修复 |
|
||||
221
docs/测试文档模板规范.md
Normal file
221
docs/测试文档模板规范.md
Normal 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 设计原则
|
||||
|
||||
| 原则 | 覆盖用例 |
|
||||
|------|----------|
|
||||
```
|
||||
1190
docs/测试说明/CH395F_Test_Guide.md
Normal file
1190
docs/测试说明/CH395F_Test_Guide.md
Normal file
File diff suppressed because it is too large
Load Diff
168
docs/测试说明/FTP_Test_Guide.md
Normal file
168
docs/测试说明/FTP_Test_Guide.md
Normal 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 数据通道动态 Socket2,backlog=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.0);PC 可设 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 正确;上电后等待约 7s(PHY 稳定 + `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 用法
|
||||
741
docs/测试说明/GD5F2GQ5UE_Test_Guide.md
Normal file
741
docs/测试说明/GD5F2GQ5UE_Test_Guide.md
Normal 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 0)spare 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 + GD5F2GQ5UE(SPI NAND)
|
||||
- **SPI 接口**:SPI1(CubeMX 初始化),引脚:
|
||||
- CS=`PE0`,SCK=`PB3`,MISO=`PB4`,MOSI=`PB5`,WP=`PB8`,HOLD=`PE1`
|
||||
- **调试输出**:USART1(115200bps),`[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 后查得 1,clear 后查得 0(RAM 行为符合预期) |
|
||||
| **覆盖原则** | `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 |
|
||||
| **类型** | 功能测试 |
|
||||
| **标题** | 任意 offset,size=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. 阶段 5:ECC 验证
|
||||
|
||||
### 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. 阶段 6:SPI 原语(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_status(WEL 位)
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| **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. 阶段 7:DMA 边界(阈值 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. 阶段 8:FTL 层(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. 阶段 9:FatFS diskio(可选,待 FTL 完成)
|
||||
|
||||
### TC-GD5F-901: 挂载 + 文件写读
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| **ID** | TC-GD5F-901 |
|
||||
| **优先级** | P2 |
|
||||
| **类型** | 集成测试 |
|
||||
| **标题** | FatFS 挂载 `/`,写文件后读回比对 |
|
||||
| **前置条件** | FTL 已 init/format;FatFS 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) |
|
||||
| **实测** | PASS(STORAGE 套件: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` |
|
||||
| **实测** | PASS(STORAGE 套件: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 + 重新格式化 |
|
||||
| **实测** | PASS(STORAGE 套件:注入 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) |
|
||||
| 状态寄存器 | 0xC0;OIP=0, WEL=1, E_FAIL=2, P_FAIL=3, ECCS=4/5 |
|
||||
| Feature 寄存器 | 0xB0;ECC_EN=4, QE=0 |
|
||||
| Protect 寄存器 | 0xA0;init 后写 0x00 解除保护 |
|
||||
| SPI 引脚 | CS=PE0, SCK=PB3, MISO=PB4, MOSI=PB5, WP=PB8, HOLD=PE1 |
|
||||
|
||||
105
docs/测试说明/SD2506_Test_Guide.md
Normal file
105
docs/测试说明/SD2506_Test_Guide.md
Normal 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 返回 0;get 返回 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 FAIL(set≠readback)**:优先查 `sd2506.h` 中各寄存器宏是否被写成
|
||||
`0xNNH`(H 后缀陷阱)。例如 `SD2506_REG_CTR1` 必须是 `0x0FU` 而非 `0x0FH`。
|
||||
- **init/get/set 返回 -2(I2C 错误)**:查 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 区域,不影响时间/控制寄存器。
|
||||
348
docs/系统墙钟时间维护说明.md
Normal file
348
docs/系统墙钟时间维护说明.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# 系统墙钟时间维护 设计说明
|
||||
|
||||
> 模块:`App/sys_clock.c` / `App/sys_clock.h`
|
||||
> 依赖:SD2506API-G RTC 驱动(`Drivers/BSP/SD2506/`)、HAL(`HAL_GetTick()`)
|
||||
> 适用平台:STM32F407ZGTx(Cortex-M4,168MHz),I2C1 接 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 | 温补晶振,带备份电池,掉电后继续走时;I2C1(PB6-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(); /* 初始化 RTC(I2C 通信、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 个 Socket,FTP 用 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)》整理。*
|
||||
@@ -120,7 +120,7 @@ GND
|
||||
|
||||
注: $" \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码方式存贮包括年、月、日、星期、时、分、秒的数据。
|
||||
|
||||
538
docs/问题记录/CH395F_Trap_Records.md
Normal file
538
docs/问题记录/CH395F_Trap_Records.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# CH395F 驱动陷阱记录
|
||||
|
||||
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
|
||||
|
||||
---
|
||||
|
||||
## Trap 01:Socket 4~7 自动分配不到
|
||||
|
||||
### 现象
|
||||
多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 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.45),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 03:DHCP 包 xid 偏移错误
|
||||
|
||||
### 现象
|
||||
Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid,握手失败。
|
||||
|
||||
### 根因
|
||||
错误使用了 `data[232:236]` 作为 xid(Wireshark 某旧版显示误导),实际 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 04:UDP 发送后必须等待 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 05:CH395F 与 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 06:TCP KeepAlive 参数必须为 500ms 倍数
|
||||
|
||||
### 现象
|
||||
设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。
|
||||
|
||||
### 根因
|
||||
CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。
|
||||
且 `IDLE` 必须 > `INTVL`。
|
||||
|
||||
### 正确值
|
||||
```c
|
||||
ch395f_set_keepalive_idle(60000); // 60s(500 倍数)
|
||||
ch395f_set_keepalive_intvl(5000); // 5s(500 倍数)
|
||||
ch395f_set_keepalive_cnt(3); // 3 次
|
||||
```
|
||||
|
||||
### 对应文件
|
||||
`Drivers/BSP/NET/net_socket.c` → `net_init()`
|
||||
|
||||
### 发现时间
|
||||
Phase 2 测试,2025-07
|
||||
|
||||
---
|
||||
|
||||
## Trap 07:TCP 关闭重连直接 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 08:RECV 中断电平触发,避免无限循环
|
||||
|
||||
### 现象
|
||||
`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_2,close 不再发 RST,Socket 卡住数分钟
|
||||
- ✅ `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 09:PHY 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(¬ified); // 等待通知
|
||||
return msg.result; // 永远是 -1!
|
||||
|
||||
// 处理方(netTask)
|
||||
osMessageQueueGet(&msg); // 拿到的是队列副本
|
||||
msg.result = actual_result; // 修改的是副本
|
||||
xTaskNotify(...); // 通知回去了,但值丢了
|
||||
```
|
||||
|
||||
### 解决方案
|
||||
不通过消息队列回传结果,改用 **FreeRTOS 通知值**(notification value)携带返回值:
|
||||
|
||||
**Producer(netTask)**:
|
||||
```c
|
||||
xTaskNotify(msg.caller, (uint32_t)msg.result, eSetValueWithOverwrite);
|
||||
```
|
||||
|
||||
**Consumer(调用方)**:
|
||||
```c
|
||||
uint32_t notified;
|
||||
xTaskNotifyWait(0, 0, ¬ified, 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 accept),220 欢迎语不发送,`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 12:FatFS `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 端口 0(net_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 13:CH395F `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 14:close 后 DISCONNECT 滞后锁存,污染下次会话首次中断读取
|
||||
|
||||
### 现象
|
||||
`close_socket` → 轮询 `GET_SOCKET_STATUS` 已确认 CLOSED 后,紧接的下一会话(重新 open + connect)在第一次读 `GET_INT_STATUS_SN` 时读到**上一会话遗留的 DISCONNECT 事件**(0ms 即命中),导致负向判定/状态机误判。实测:TC-202 三轮重连后立即跑 TC-203,PC 端握手实际成功(连接被接受),固件却因陈旧 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 15:DHCP 成功后 IP 寄存器提交滞后
|
||||
|
||||
### 现象
|
||||
`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF`,回读到的仍是**旧静态配置**(如 192.168.1.100),而芯片实际已按新租约(192.168.1.77)正常收发——串口打印与 Socket6 广播宣告均带出过期 IP。2026-08-24 上板实测:固件各检查全 PASS(6/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 16:NET 层 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 连接 + 回显)正常;TC702(close 后同 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()` 清掉中断状态(上限 500ms,LAN 通常几毫秒),彻底排空上一条连接残留的 `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 0,8KB 缓冲全归它);`1`=多 Socket 模式(Socket 0~7 全部可用,缓冲需自行分配)。之前 `net_init` 用 `0x08`(bit0=0)即单 Socket 模式,故 1~7 不存在。注意 bit1(TCP Server 单/多连接)与 bit0 是两回事:本方案只翻 bit0(多 Socket),bit1 保持 0(TCP 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.45(48 块 × 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~7(Trap 19)。
|
||||
|
||||
### 注意事项
|
||||
- 4~7 的缓冲必须显式分配,否则其 CONNECT/RECV 中断不触发(Trap 01/02)。
|
||||
- 上述布局**以手册 §5.45 的 48×512B 缓冲几何为准**,并已由 Phase 7 TC704(40/40 PASS)实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误)。Socket 4~7 分配与 8 路并发回显均验证通过。
|
||||
- 多 Socket 模式下 TCP Server 单连接语义不变:Socket 0 监听且连接停留在本 Socket,Sockets 1~7 由应用当作独立 client 使用。
|
||||
- `net_poll` 用 `GET_GLOB_INT_STATUS_ALL`(2 字节,支持 0~7)并遍历 0~7,Socket n 中断位即 `bit(n+4)`(Socket 0~3→bit4~7,Socket 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。
|
||||
135
docs/问题记录/GD5F2GQ5UE_Trap_Records.md
Normal file
135
docs/问题记录/GD5F2GQ5UE_Trap_Records.md
Normal 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
Reference in New Issue
Block a user