增加文件系统
This commit is contained in:
@@ -4,15 +4,16 @@
|
||||
|
||||
1. [概述](#1-概述)
|
||||
2. [架构说明](#2-架构说明)
|
||||
3. [快速开始](#3-快速开始)
|
||||
4. [TCP 编程](#4-tcp-编程)
|
||||
5. [UDP 编程](#5-udp-编程)
|
||||
6. [I/O 多路复用](#6-io-多路复用)
|
||||
7. [事件回调机制](#7-事件回调机制)
|
||||
8. [字节序处理](#8-字节序处理)
|
||||
9. [API 参考](#9-api-参考)
|
||||
10. [完整示例](#10-完整示例)
|
||||
11. [常见问题](#11-常见问题)
|
||||
3. [线程安全设计](#3-线程安全设计)
|
||||
4. [快速开始](#4-快速开始)
|
||||
5. [TCP 编程](#5-tcp-编程)
|
||||
6. [UDP 编程](#6-udp-编程)
|
||||
7. [I/O 多路复用](#7-io-多路复用)
|
||||
8. [事件回调机制](#8-事件回调机制)
|
||||
9. [字节序处理](#9-字节序处理)
|
||||
10. [API 参考](#10-api-参考)
|
||||
11. [完整示例](#11-完整示例)
|
||||
12. [常见问题](#12-常见问题)
|
||||
|
||||
---
|
||||
|
||||
@@ -46,32 +47,42 @@
|
||||
|
||||
## 2. 架构说明
|
||||
|
||||
### 2.1 分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 应用层 (App) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ net_socket.h (BSD Socket API) │ ← 用户接口
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ net_socket.c + net_select.c (状态机+select) │ ← 核心逻辑
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ch395f.c/h (底层驱动) │ ← 硬件驱动
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ CH395F 硬件 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 应用层任务 (多个 FreeRTOS Task) │
|
||||
│ net_send() / net_recv() / net_close() (线程安全封装) │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ 消息队列 (osMessageQueue) │
|
||||
│ 由 CubeMX 生成,在 MX_FREERTOS_Init() 中创建 │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ netTask (网络任务线程) │
|
||||
│ net_process_messages() + net_poll() — 串行化执行 │
|
||||
│ net_send_sock() / net_recv_sock() / net_close_sock() │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ net_socket.c (BSD Socket API 核心) │
|
||||
│ Socket 状态机 + CH395F 命令封装 │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ ch395f.c/h (底层 SPI 驱动) │
|
||||
│ 所有 SPI 事务必须由 netTask 发起 │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ CH395F 硬件 (SPI2) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.1 文件说明
|
||||
### 2.2 文件说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) |
|
||||
| `net_types.h` | 类型定义(地址结构、Socket 控制块等) |
|
||||
| `net_socket.h` | BSD Socket API 头文件 |
|
||||
| `net_socket.c` | API 核心实现 |
|
||||
| `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 |
|
||||
| `net_socket.c` | API 核心实现,含消息队列处理、线程安全封装、内部 `_sock()` 版本 |
|
||||
| `net_select.h` | select/poll API 头文件 |
|
||||
| `net_select.c` | select/poll 实现 |
|
||||
|
||||
### 2.1a 底层驱动改进(v2.1)
|
||||
### 2.3 底层驱动改进(v2.1)
|
||||
|
||||
ch395f.c/h 已完成以下优化:
|
||||
|
||||
@@ -84,7 +95,7 @@ ch395f.c/h 已完成以下优化:
|
||||
| 新增函数 | `ch395f_set_arp()` / `ch395f_set_ttl()` / `ch395f_get_unreach_info()` / `ch395f_poll_cmd_status()` |
|
||||
| KeepAlive | 参数单位为毫秒(非秒),必须为 500 的倍数 |
|
||||
|
||||
### 2.2 Socket 状态机
|
||||
### 2.4 Socket 状态机
|
||||
|
||||
```
|
||||
net_socket()
|
||||
@@ -120,16 +131,86 @@ ch395f.c/h 已完成以下优化:
|
||||
|
||||
---
|
||||
|
||||
## 3. 快速开始
|
||||
## 3. 线程安全设计
|
||||
|
||||
### 3.1 包含头文件
|
||||
### 3.1 设计背景
|
||||
|
||||
NET 层(`net_socket.c` + `ch395f.c`)不是线程安全的:
|
||||
- Socket 控制块(`s_net_socks[]`)无互斥保护
|
||||
- CH395F SPI 事务不重入(STM32 HAL SPI 无内部锁)
|
||||
- 状态机在多个任务中并发访问会崩溃
|
||||
|
||||
引入消息队列将所有的 CH395F 操作串行化到 `netTask` 中执行。
|
||||
|
||||
### 3.2 消息队列模式
|
||||
|
||||
```
|
||||
App Task A App Task B netTask
|
||||
| | |
|
||||
net_send() net_recv() net_process_messages()
|
||||
| | + net_poll()
|
||||
▼ ▼ │
|
||||
┌─────────┐ ┌─────────┐ │
|
||||
│send msg │ │recv msg │ │
|
||||
└────┬────┘ └────┬────┘ │
|
||||
│ │ │
|
||||
└─────────┬─────────┘ │
|
||||
▼ ▼
|
||||
┌──────────────────┐ ┌──────────────┐
|
||||
│ 消息队列 │──────────│ 取出消息 │
|
||||
│ osMessageQueue │ dequeue │ 执行操作 │
|
||||
└──────────────────┘ │ 写结果 │
|
||||
▲ │ 通知调用方 │
|
||||
│ └──────┬───────┘
|
||||
│ ulTaskNotifyTake() │
|
||||
│ (阻塞等待结果) │ xTaskNotifyGive()
|
||||
│ │
|
||||
┌─────┴──────┐ │
|
||||
│ 返回结果给 │◄──────────────────────┘
|
||||
│ 调用方 │
|
||||
└────────────┘
|
||||
```
|
||||
|
||||
### 3.3 关键规则
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| **`netTask` 外调用** | 使用 `net_send()` / `net_recv()` / `net_close()` 公共 API |
|
||||
| **`netTask` 内调用** | 使用 `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` 内部 API |
|
||||
| **创建/绑定/监听** | 在 `main()` 初始化阶段完成(`osKernelStart()` 之前),无需队列 |
|
||||
| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环中调用 |
|
||||
|
||||
### 3.4 内部 API 说明
|
||||
|
||||
内部 API(带 `_sock` 后缀)直接操作 Socket 控制块和 CH395F,不经过消息队列。用于 `netTask` 中的网络业务逻辑,避免向自身发送消息造成死锁:
|
||||
|
||||
```c
|
||||
/* 在 netTask 中直接操作 Socket,绕过消息队列 */
|
||||
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
|
||||
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
|
||||
int net_close_sock(net_sock_t *p_sock);
|
||||
```
|
||||
|
||||
### 3.5 注意事项
|
||||
|
||||
- **消息队列由 CubeMX 创建**:在 `MX_FREERTOS_Init()`(`osKernelInitialize()` 之后)中调用 `osMessageQueueNew(8, sizeof(net_msg_t), NULL)` 创建
|
||||
- **队列句柄全局可见**:通过 `extern osMessageQueueId_t netMsgQueueHandle` 引用
|
||||
- **队列满时**:`net_send()` / `net_recv()` 会阻塞等待,直到 `netTask` 处理完消息腾出空间
|
||||
- **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本
|
||||
- **`net_socket()` / `net_bind()` / `net_listen()` 无需线程安全**:这些仅在初始化阶段调用,消息队列仅保护数据收发和连接关闭
|
||||
|
||||
---
|
||||
|
||||
## 4. 快速开始
|
||||
|
||||
### 4.1 包含头文件
|
||||
|
||||
```c
|
||||
#include "net_socket.h"
|
||||
#include "net_select.h"
|
||||
```
|
||||
|
||||
### 3.2 初始化网络
|
||||
### 4.2 初始化网络
|
||||
|
||||
```c
|
||||
int main(void)
|
||||
@@ -154,29 +235,55 @@ int main(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 主循环要求
|
||||
### 4.3 主循环要求
|
||||
|
||||
**重要**:`net_poll()` 必须在主循环中定期调用,它负责:
|
||||
- 轮询 CH395F INT# 引脚电平(GPIO 轮询,无需 EXTI 中断)
|
||||
- 通过 `GET_GLOB_INT_STATUS_ALL`(2字节版)读取所有 8 个 Socket 的中断状态
|
||||
- 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT)
|
||||
- 触发事件回调
|
||||
- 处理数据接收
|
||||
**重要**:本项目使用 FreeRTOS 多任务架构,`net_poll()` 和 `net_process_messages()` 均在 `netTask`(FreeRTOS 线程)中定期调用,而非主循环。
|
||||
|
||||
- **主循环**:系统初始化完成后进入空循环,等待 `osKernelStart()` 启动调度器
|
||||
- **netTask**:在任务函数中持续运行,每 10ms 调用一次 `net_poll()` + `net_process_messages()`,负责:
|
||||
- 轮询 CH395F INT# 引脚电平(GPIO 轮询,无需 EXTI 中断)
|
||||
- 通过 `GET_GLOB_INT_STATUS_ALL`(2字节版)读取所有 8 个 Socket 的中断状态
|
||||
- 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT)
|
||||
- 触发事件回调
|
||||
- 处理数据接收
|
||||
|
||||
```c
|
||||
while (1)
|
||||
int main(void)
|
||||
{
|
||||
net_poll(); /* 建议至少每 10ms 调用一次 */
|
||||
/* 外设初始化... */
|
||||
|
||||
/* 其他应用代码... */
|
||||
/* BSD Socket API 初始化 */
|
||||
net_init("192.168.1.100", "255.255.255.0", "192.168.1.1");
|
||||
|
||||
/* 创建 Socket、绑定、监听... */
|
||||
|
||||
osKernelInitialize();
|
||||
MX_FREERTOS_Init(); /* 创建 netTask 等 FreeRTOS 对象 */
|
||||
osKernelStart(); /* 启动调度器 */
|
||||
|
||||
while (1) { } /* 空循环,控制流已交给调度器 */
|
||||
}
|
||||
|
||||
/* netTask 中(freertos.c): */
|
||||
void StartNetTask(void *argument)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
net_poll(); /* CH395F 中断轮询 + Socket 状态更新 */
|
||||
net_process_messages(); /* 处理其他任务发送的网络操作请求 */
|
||||
|
||||
/* 应用层网络业务逻辑... */
|
||||
|
||||
osDelay(10); /* 10ms 周期 */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. TCP 编程
|
||||
## 5. TCP 编程
|
||||
|
||||
### 4.1 TCP Server(单连接)
|
||||
### 5.1 TCP Server(单连接)
|
||||
|
||||
单连接模式下,1 个 Socket 既做监听又做数据通信:
|
||||
|
||||
@@ -258,7 +365,7 @@ void tcp_server_single(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 TCP Server(多连接)
|
||||
### 5.2 TCP Server(多连接)
|
||||
|
||||
多连接模式使用 1 个监听 Socket + N 个数据 Socket,支持并发连接:
|
||||
|
||||
@@ -348,7 +455,7 @@ void tcp_server_multi(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 TCP Client
|
||||
### 5.3 TCP Client
|
||||
|
||||
```c
|
||||
#include "net_socket.h"
|
||||
@@ -413,9 +520,9 @@ void tcp_client(void)
|
||||
|
||||
---
|
||||
|
||||
## 5. UDP 编程
|
||||
## 6. UDP 编程
|
||||
|
||||
### 5.1 UDP Client
|
||||
### 6.1 UDP Client
|
||||
|
||||
```c
|
||||
#include "net_socket.h"
|
||||
@@ -472,7 +579,7 @@ void udp_client(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 UDP Server
|
||||
### 6.2 UDP Server
|
||||
|
||||
```c
|
||||
#include "net_socket.h"
|
||||
@@ -531,9 +638,9 @@ void udp_server(void)
|
||||
|
||||
---
|
||||
|
||||
## 6. I/O 多路复用
|
||||
## 7. I/O 多路复用
|
||||
|
||||
### 6.1 select 使用
|
||||
### 7.1 select 使用
|
||||
|
||||
`net_select()` 可以同时监控多个 Socket 的读写事件:
|
||||
|
||||
@@ -578,7 +685,7 @@ void select_example(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 poll 使用
|
||||
### 7.2 poll 使用
|
||||
|
||||
`net_poll_events()` 使用 `pollfd` 结构体数组:
|
||||
|
||||
@@ -616,7 +723,7 @@ void poll_example(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 事件掩码说明
|
||||
### 7.3 事件掩码说明
|
||||
|
||||
| 事件 | 说明 |
|
||||
|------|------|
|
||||
@@ -628,11 +735,11 @@ void poll_example(void)
|
||||
|
||||
---
|
||||
|
||||
## 7. 事件回调机制
|
||||
## 8. 事件回调机制
|
||||
|
||||
可以通过注册回调函数,异步获取 Socket 事件通知:
|
||||
|
||||
### 7.1 定义回调函数
|
||||
### 8.1 定义回调函数
|
||||
|
||||
```c
|
||||
#include "net_socket.h"
|
||||
@@ -669,7 +776,7 @@ void my_event_callback(int sockfd, net_event_t event, void *arg)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 注册回调
|
||||
### 8.2 注册回调
|
||||
|
||||
```c
|
||||
void event_callback_example(void)
|
||||
@@ -691,11 +798,11 @@ void event_callback_example(void)
|
||||
|
||||
---
|
||||
|
||||
## 8. 字节序处理
|
||||
## 9. 字节序处理
|
||||
|
||||
网络协议使用**大端序**(网络字节序),而 STM32 使用**小端序**(主机字节序)。发送和接收数据时需要进行字节序转换。
|
||||
|
||||
### 8.1 端口号转换
|
||||
### 9.1 端口号转换
|
||||
|
||||
```c
|
||||
uint16_t port = 8080;
|
||||
@@ -707,7 +814,7 @@ uint16_t net_port = net_htons(port);
|
||||
uint16_t host_port = net_ntohs(net_port);
|
||||
```
|
||||
|
||||
### 8.2 IP 地址转换
|
||||
### 9.2 IP 地址转换
|
||||
|
||||
```c
|
||||
/* 字符串转网络序 IP */
|
||||
@@ -718,7 +825,7 @@ char ip_str[16];
|
||||
net_inet_ntoa(ip, ip_str); /* ip_str = "192.168.1.100" */
|
||||
```
|
||||
|
||||
### 8.3 填充地址结构体
|
||||
### 9.3 填充地址结构体
|
||||
|
||||
```c
|
||||
struct net_sockaddr_in addr;
|
||||
@@ -733,7 +840,7 @@ addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
```
|
||||
|
||||
### 8.4 读取地址信息
|
||||
### 9.4 读取地址信息
|
||||
|
||||
```c
|
||||
struct net_sockaddr_in client_addr;
|
||||
@@ -757,9 +864,9 @@ if (client_sock >= 0)
|
||||
|
||||
---
|
||||
|
||||
## 9. API 参考
|
||||
## 10. API 参考
|
||||
|
||||
### 9.1 核心 API
|
||||
### 10.1 核心 API
|
||||
|
||||
#### net_init - 网络初始化
|
||||
|
||||
@@ -898,7 +1005,41 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||||
int net_close(int sockfd);
|
||||
```
|
||||
|
||||
### 9.2 辅助 API
|
||||
### 10.2 消息队列 API
|
||||
|
||||
#### net_process_messages - 处理消息队列
|
||||
|
||||
```c
|
||||
void net_process_messages(void);
|
||||
```
|
||||
|
||||
处理其他任务通过 `net_send()` / `net_recv()` / `net_close()` 发送到消息队列的请求。必须在 `netTask` 主循环中调用。
|
||||
|
||||
#### net_send_sock - 发送数据(内部版本)
|
||||
|
||||
```c
|
||||
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
|
||||
```
|
||||
|
||||
直接操作 Socket 控制块进行发送,不经过消息队列。**只能在 `netTask` 中调用**,用于 `netTask` 内的网络业务逻辑。
|
||||
|
||||
#### net_recv_sock - 接收数据(内部版本)
|
||||
|
||||
```c
|
||||
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
|
||||
```
|
||||
|
||||
直接操作 Socket 控制块进行接收,不经过消息队列。**只能在 `netTask` 中调用**。
|
||||
|
||||
#### net_close_sock - 关闭 Socket(内部版本)
|
||||
|
||||
```c
|
||||
int net_close_sock(net_sock_t *p_sock);
|
||||
```
|
||||
|
||||
直接操作 Socket 控制块关闭连接,不经过消息队列。**只能在 `netTask` 中调用**。
|
||||
|
||||
### 10.3 辅助 API
|
||||
|
||||
#### net_set_event_cb - 注册事件回调
|
||||
|
||||
@@ -918,7 +1059,7 @@ net_sock_t *net_get_sock(int sockfd);
|
||||
int net_get_errno(void);
|
||||
```
|
||||
|
||||
### 9.3 字节序转换
|
||||
### 10.4 字节序转换
|
||||
|
||||
```c
|
||||
uint16_t net_htons(uint16_t hostshort); /* 主机序 -> 网络序 */
|
||||
@@ -929,7 +1070,7 @@ uint32_t net_inet_addr(const char *cp); /* 字符串 -> 网络序 IP */
|
||||
char *net_inet_ntoa(uint32_t addr, char *buf); /* 网络序 IP -> 字符串 */
|
||||
```
|
||||
|
||||
### 9.4 select/poll
|
||||
### 10.5 select/poll
|
||||
|
||||
```c
|
||||
int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
|
||||
@@ -938,7 +1079,7 @@ int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
|
||||
int net_poll_events(net_pollfd *fds, int nfds, int timeout);
|
||||
```
|
||||
|
||||
### 9.5 fd_set 操作宏
|
||||
### 10.6 fd_set 操作宏
|
||||
|
||||
```c
|
||||
NET_FD_ZERO(fdset) /* 清空集合 */
|
||||
@@ -949,9 +1090,9 @@ NET_FD_ISSET(fd, fdset) /* 检查 fd 是否在集合中 */
|
||||
|
||||
---
|
||||
|
||||
## 10. 完整示例
|
||||
## 11. 完整示例
|
||||
|
||||
### 10.1 TCP Echo Server(多连接)
|
||||
### 11.1 TCP Echo Server(多连接)
|
||||
|
||||
```c
|
||||
#include "net_socket.h"
|
||||
@@ -1047,7 +1188,7 @@ void tcp_echo_server(void)
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 TCP Client 自动重连
|
||||
### 11.2 TCP Client 自动重连
|
||||
|
||||
```c
|
||||
#include "net_socket.h"
|
||||
@@ -1135,22 +1276,22 @@ void tcp_client_reconnect(void)
|
||||
|
||||
---
|
||||
|
||||
## 11. 常见问题
|
||||
## 12. 常见问题
|
||||
|
||||
### 11.1 net_poll() 必须调用吗?
|
||||
### 12.1 net_poll() 必须调用吗?
|
||||
|
||||
**是的**。`net_poll()` 负责轮询 CH395F 的中断状态并更新所有 Socket 的状态。如果不调用,连接状态不会更新,数据也无法接收。
|
||||
|
||||
建议至少每 10ms 调用一次。
|
||||
|
||||
### 11.2 Socket 数量有限制吗?
|
||||
### 12.2 Socket 数量有限制吗?
|
||||
|
||||
是的,CH395F 最多支持 8 个 Socket(索引 0~7)。其中:
|
||||
- TCP Server 多连接模式:1 个监听 Socket + 最多 7 个数据 Socket
|
||||
- TCP Client:最多 8 个
|
||||
- UDP:最多 8 个
|
||||
|
||||
### 11.3 TCP Server 多连接模式有什么要求?
|
||||
### 12.3 TCP Server 多连接模式有什么要求?
|
||||
|
||||
**模式工作原理:**
|
||||
- CH395F 多连接模式下,Socket 0 专职监听,Socket 1~7 由芯片自动分配
|
||||
@@ -1160,13 +1301,13 @@ void tcp_client_reconnect(void)
|
||||
**硬件要求:**
|
||||
- CH395F 固件版本 >= 0x44(支持 Socket 4~7)
|
||||
|
||||
### 11.4 发送数据有什么限制?
|
||||
### 12.4 发送数据有什么限制?
|
||||
|
||||
- 每次发送的数据长度不能超过 `NET_SEND_BUF_SIZE`(默认 4096 字节)
|
||||
- TCP 需要等待发送缓冲区空闲(`send_ready` 标志)
|
||||
- UDP 单次发送最大 1460 字节
|
||||
|
||||
### 11.5 如何处理连接断开?
|
||||
### 12.5 如何处理连接断开?
|
||||
|
||||
**方式一:阻塞接收**
|
||||
```c
|
||||
@@ -1197,7 +1338,7 @@ void callback(int sockfd, net_event_t event, void *arg)
|
||||
}
|
||||
```
|
||||
|
||||
### 11.6 select 和 poll 的区别?
|
||||
### 12.6 select 和 poll 的区别?
|
||||
|
||||
| 特性 | select | poll |
|
||||
|------|--------|------|
|
||||
@@ -1207,7 +1348,7 @@ void callback(int sockfd, net_event_t event, void *arg)
|
||||
|
||||
两者功能相同,推荐使用 `net_select()`,更接近 Linux 编程习惯。
|
||||
|
||||
### 11.7 错误码说明
|
||||
### 12.7 错误码说明
|
||||
|
||||
| 错误码 | 宏定义 | 说明 |
|
||||
|--------|--------|------|
|
||||
@@ -1224,7 +1365,7 @@ void callback(int sockfd, net_event_t event, void *arg)
|
||||
| -10 | NET_ERR_NOSPACE | 发送缓冲区无空间 |
|
||||
| -11 | NET_ERR_BUSY | 操作未完成(BUSY) |
|
||||
|
||||
### 11.8 CH395F 命令执行状态码
|
||||
### 12.8 CH395F 命令执行状态码
|
||||
|
||||
CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_poll_cmd_status()`):
|
||||
|
||||
@@ -1249,7 +1390,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po
|
||||
| 0x5F | CH395F_CMD_RET_ABORT | 命令中止 |
|
||||
| 0xFA | CH395F_ERR_UNKNOW | 未知错误 |
|
||||
|
||||
### 11.9 故障排查
|
||||
### 12.9 故障排查
|
||||
|
||||
**问题:TCP Client 连接超时,无 CONNECT 中断**
|
||||
- 检查多连接模式下数据 Socket(1~7)是否设置了 `SET_PROTO_TCP` + `SET_SOUR_PORT`(与监听端口相同)
|
||||
@@ -1266,3 +1407,21 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po
|
||||
**问题:CH395F 初始化超时**
|
||||
- 芯片 INIT 需要约 200ms,轮询间隔应为 20ms(过短的查询可能干扰内部处理)
|
||||
- 超时值建议设为 4 秒以上
|
||||
|
||||
### 12.10 线程安全问题
|
||||
|
||||
**问题:`net_send()` / `net_recv()` / `net_close()` 可以在任意任务中调用吗?**
|
||||
|
||||
可以。这些公共 API 通过消息队列将请求委托给 `netTask` 串行执行,自身阻塞等待结果,是线程安全的。
|
||||
|
||||
**问题:`netTask` 内可以直接调用 `net_send()` / `net_recv()` 吗?**
|
||||
|
||||
不可以。`netTask` 中必须使用 `net_send_sock()` / `net_recv_sock()` 等内部版本,否则会死锁(任务向自身发送消息,永远无人处理)。内部版本直接操作 Socket 控制块,不经过消息队列。
|
||||
|
||||
**问题:`net_socket()` / `net_bind()` / `net_listen()` 线程安全吗?**
|
||||
|
||||
这些函数仅在系统初始化阶段(`osKernelStart()` 之前)调用,不存在并发访问。消息队列仅保护数据收发和连接关闭操作。
|
||||
|
||||
**问题:消息队列由谁创建?在哪里创建?**
|
||||
|
||||
由 CubeMX 在 `MX_FREERTOS_Init()` 中创建(`osMessageQueueNew(8, sizeof(net_msg_t), NULL)`),在 `osKernelInitialize()` 之后执行,确保 FreeRTOS 内核已就绪。
|
||||
|
||||
611
docs/STM32F4-Base存储架构说明.md
Normal file
611
docs/STM32F4-Base存储架构说明.md
Normal file
@@ -0,0 +1,611 @@
|
||||
# STM32F4-Base 存储架构说明
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本项目在 GD5F2GQ5UE SPI NAND Flash(256MB)上实现了四层存储软件栈:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 应用程序 (main, freertos) │
|
||||
├──────────────────────┬──────────────────┬──────────────────┤
|
||||
│ │ │ │
|
||||
│ FlashDB KVDB │ FlashDB TSDB │ FatFs │
|
||||
│ (键值数据库) │ (时序数据库) │ (文件系统) │
|
||||
│ │ │ │
|
||||
├──────────┬───────────┴──────┬───────────┴──────────────────┤
|
||||
│ │ │ │
|
||||
│ FAL 抽象层 (分区访问) │ dhara FTL │
|
||||
│ (Flash Abstraction Layer)│ (地址映射 · 磨损均衡 │
|
||||
│ 直接转发到底层驱动) │ 坏块管理 · 垃圾回收) │
|
||||
│ │ │ │
|
||||
├──────────┴──────────────────┴──────────────────────────────┤
|
||||
│ 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) │
|
||||
│ │
|
||||
│ ┌──────────────────────┬──────────────────┬───────────────┐ │
|
||||
│ │ fdb_kvdb1 (64MB) │ fdb_tsdb1 (64MB) │ ftl_fatfs │ │
|
||||
│ │ Block 0~511 │ Block 512~1023 │ Block 1024~2047│ │
|
||||
│ │ FlashDB 键值数据库 │ FlashDB 时序数据库│ dhara + FatFS │ │
|
||||
│ └──────────────────────┴──────────────────┴───────────────┘ │
|
||||
│ 偏移: 0 64MB 128MB 256MB│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**两条独立路径:**
|
||||
- **FlashDB 路径**:`FlashDB → FAL → gd5f2gq5ue 驱动`,FAL 是薄封装层,将 FAL 读写请求直接转发给底层 NAND 驱动。FlashDB 在 FAL 之上自行管理磨损均衡和掉电安全。
|
||||
- **FatFS 路径**:`FatFS → dhara FTL → gd5f2gq5ue 驱动`,FTL 提供 LBA 到物理页的映射、磨损均衡、垃圾回收和坏块管理,FatFS 通过标准 `disk_*` 接口访问 FTL 提供的块设备。
|
||||
|
||||
两个路径共享最底层 NAND 驱动,但各自管理不同的物理分区(FlashDB 管理 Block 0~1023,FTL 管理 Block 1024~2047),互不干扰。
|
||||
|
||||
**初始化顺序:**
|
||||
```
|
||||
SPI1_Init → gd5f2gq5ue_init() → fdb_kvdb_init() [可选,FlashDB 路径]
|
||||
→ fdb_tsdb_init() [可选,FlashDB 路径]
|
||||
→ f_mount() [触发 FTL 初始化,FatFS 路径]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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/fal_cfg.h:18-30`:
|
||||
|
||||
```c
|
||||
#define FDB_KVDB1_OFFSET 0
|
||||
#define FDB_KVDB1_SIZE (64 * 1024 * 1024) // 64MB
|
||||
#define FDB_TSDB1_OFFSET (FDB_KVDB1_OFFSET + FDB_KVDB1_SIZE)
|
||||
#define FDB_TSDB1_SIZE (64 * 1024 * 1024) // 64MB
|
||||
#define FTL_FATFS_OFFSET (FDB_TSDB1_OFFSET + FDB_TSDB1_SIZE) // 128MB
|
||||
#define FTL_FATFS_SIZE (128 * 1024 * 1024) // 128MB
|
||||
```
|
||||
|
||||
### 3.1 分区对照表
|
||||
|
||||
| 分区名 | 偏移 | 大小 | 物理块范围 | 逻辑用途 |
|
||||
|--------|------|------|-----------|----------|
|
||||
| `fdb_kvdb1` | 0 | 64 MB | Block 0~511 | FlashDB 键值数据库 (KVDB) |
|
||||
| `fdb_tsdb1` | 64 MB | 64 MB | Block 512~1023 | FlashDB 时序数据库 (TSDB) |
|
||||
| `ftl_fatfs` | 128 MB | 128 MB | Block 1024~2047 | dhara FTL + FatFS |
|
||||
|
||||
### 3.2 配置联动
|
||||
|
||||
所有分区边界在 `fal_cfg.h` 定义后,自动传播到各子模块:
|
||||
|
||||
- **FAL 分区表** — `FAL_PART_TABLE` 宏直接引用 `FDB_KVDB1_OFFSET/SIZE`、`FDB_TSDB1_OFFSET/SIZE`
|
||||
- **FTL** — `nand_ftl.c` 通过 `#include "fal_cfg.h"` 引用 `FTL_FATFS_OFFSET / GD5F_BLOCK_SIZE` 计算 `FTL_START_BLOCK = 1024`
|
||||
- **FatFS** — 通过 `disk_ioctl(GET_SECTOR_COUNT)` 获取 FTL 管理的扇区数
|
||||
|
||||
> 调整分区大小时只需修改 `fal_cfg.h` 顶部 6 个宏,所有下游模块自动适配。
|
||||
|
||||
---
|
||||
|
||||
## 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 63) spare byte 0
|
||||
└─ 非 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 块最后一页的 spare byte 0
|
||||
- `s_bbt[256]` 位图数组,1 bit 标识 1 个块 (0=好, 1=坏)
|
||||
- `gd5f2gq5ue_is_block_bad(block)` — 查询坏块状态
|
||||
- `gd5f2gq5ue_mark_block_bad(block)` — 标记坏块 (FTL 层在擦除/编程失败时调用)
|
||||
|
||||
---
|
||||
|
||||
## 5. FAL 抽象层
|
||||
|
||||
FAL (Flash Abstraction Layer) 是 FlashDB 自带的 flash 抽象层,**仅 FlashDB 路径使用**。它是一个薄封装层,将 FAL 的 read/write/erase 请求直接转发给底层 NAND 驱动,不做地址映射或磨损均衡(这些由 FlashDB 自身在 FAL 之上完成)。
|
||||
|
||||
FAL 不参与 FatFS/FTL 路径。FTL 有自己的 HAL 回调直接对接底层 NAND 驱动,与 FAL 无关。
|
||||
|
||||
```
|
||||
FlashDB ──→ FAL ──→ gd5f2gq5ue_read/write/erase (直接透传)
|
||||
FatFs ──→ dhara FTL ──→ dhara_nand_* 回调 ──→ gd5f2gq5ue_* (独立路径)
|
||||
```
|
||||
|
||||
### 5.1 设备注册 (fal_flash_gd5f2gq5ue.c)
|
||||
|
||||
```c
|
||||
const struct fal_flash_dev g_gd5f2gq5ue_flash = {
|
||||
.name = "gd5f2gq5ue",
|
||||
.addr = 0,
|
||||
.len = GD5F_TOTAL_SIZE, // 256MB
|
||||
.blk_size = GD5F_BLOCK_SIZE, // 128KB
|
||||
.ops = { .init, .read, .write, .erase },
|
||||
.write_gran = 8, // byte programmable
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 分区表 (fal_cfg.h)
|
||||
|
||||
```c
|
||||
#define FAL_PART_TABLE \
|
||||
{ \
|
||||
{FAL_PART_MAGIC_WORD, "fdb_kvdb1", "gd5f2gq5ue", FDB_KVDB1_OFFSET, FDB_KVDB1_SIZE, 0}, \
|
||||
{FAL_PART_MAGIC_WORD, "fdb_tsdb1", "gd5f2gq5ue", FDB_TSDB1_OFFSET, FDB_TSDB1_SIZE, 0}, \
|
||||
}
|
||||
```
|
||||
|
||||
FlashDB 通过分区名 (`"fdb_kvdb1"`, `"fdb_tsdb1"`) 绑定到 FAL 分区。
|
||||
|
||||
---
|
||||
|
||||
## 6. FlashDB 数据库
|
||||
|
||||
### 6.1 配置 (fdb_cfg.h)
|
||||
|
||||
```c
|
||||
#define FDB_USING_KVDB // 启用键值数据库
|
||||
#define FDB_USING_TSDB // 启用时序数据库
|
||||
#define FDB_USING_FAL_MODE // 使用 FAL 存储模式
|
||||
#define FDB_WRITE_GRAN 8 // 写粒度 8bit (字节可编程)
|
||||
```
|
||||
|
||||
### 6.2 查询接口
|
||||
|
||||
FlashDB 通过 FAL 分区名查找对应的分区设备,无需 FlashDB 驱动额外的设备注册。
|
||||
|
||||
### 6.3 KVDB 键值数据库
|
||||
|
||||
适用于存储配置参数、运行状态等少量键值对。
|
||||
|
||||
**特点:**
|
||||
- 掉电安全 (写操作带 CRC 校验)
|
||||
- 支持 blob (二进制数据块)
|
||||
- 自动磨损均衡
|
||||
- 支持默认值 (首次启动自动写入)
|
||||
|
||||
### 6.4 TSDB 时序数据库
|
||||
|
||||
适用于存储采样数据、日志记录等时间序列数据。
|
||||
|
||||
**特点:**
|
||||
- 固定长度记录
|
||||
- 时间戳索引 (需提供 `get_time` 回调)
|
||||
- 自动擦除 (新数据覆盖最旧数据)
|
||||
- 支持按时间范围查询
|
||||
|
||||
---
|
||||
|
||||
## 7. dhara FTL (Flash Translation Layer)
|
||||
|
||||
### 7.1 概述
|
||||
|
||||
FTL 是 NAND Flash 上方最重要的组件,功能包括:
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **地址映射** | 将 FatFS 的逻辑扇区号 (LBA) 映射到 NAND 物理页 |
|
||||
| **磨损均衡** | 均匀分布擦除次数,延长芯片寿命 |
|
||||
| **垃圾回收** | 回收无效页面空间 (GC) |
|
||||
| **坏块管理** | 擦除/编程失败时自动跳过并标记坏块 |
|
||||
| **ECC 处理** | 检测和上报硬件 ECC 不可纠正错误 |
|
||||
|
||||
### 7.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、映射表等
|
||||
};
|
||||
```
|
||||
|
||||
### 7.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` 偏移转换为物理地址。
|
||||
|
||||
### 7.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)
|
||||
```
|
||||
|
||||
### 7.5 页面缓存策略
|
||||
|
||||
FTL 之上还有一个 **单页写回缓存 (write-back cache)**:
|
||||
|
||||
- **读命中**: 直接返回 s_cache_buf 数据
|
||||
- **读未命中**: 刷出脏页 → 读新页到缓存
|
||||
- **写**: 写入缓存 → 标记脏
|
||||
- **全页写入**: 直接刷出 (跳过缓存)
|
||||
- **同步 (CTRL_SYNC)**: 刷出脏页 + dhara_map_sync
|
||||
|
||||
---
|
||||
|
||||
## 8. FatFS 集成
|
||||
|
||||
### 8.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 (固定时间戳)
|
||||
```
|
||||
|
||||
### 8.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 |
|
||||
|
||||
### 8.3 容量计算
|
||||
|
||||
```
|
||||
FTL 管理块数 = 1024 (Block 1024~2047)
|
||||
每块页数 = 64
|
||||
每页扇区数 (512B) = 4
|
||||
总扇区数 = 1024 × 64 × 4 = 262144
|
||||
总容量 = 262144 × 512 = 128MB (原始容量)
|
||||
FTL 开销后 ≈ 93 MB (随 GC 和 journal 使用量波动)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 完整数据流
|
||||
|
||||
### 9.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 (持久化映射表)
|
||||
```
|
||||
|
||||
### 9.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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. API 参考
|
||||
|
||||
### 10.1 底层 NAND 驱动 (gd5f2gq5ue.h)
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `gd5f2gq5ue_init()` | 初始化 NAND (含 ECC 使能、块保护解除) |
|
||||
| `gd5f2gq5ue_read_id(mid, 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)` | 标记坏块 |
|
||||
|
||||
### 10.2 FAL 接口 (via fal_flash_gd5f2gq5ue.c)
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `g_gd5f2gq5ue_flash` | FAL 设备实例 |
|
||||
| `fal_flash_device_find("gd5f2gq5ue")` | 查找 Flash 设备 |
|
||||
| `fal_partition_find("fdb_kvdb1")` | 查找分区 |
|
||||
| `fal_partition_read/write/erase` | 分区读写擦除 |
|
||||
|
||||
### 10.3 FlashDB (via FlashDB 库)
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `fdb_kvdb_init(&db, "kvdb1", "fdb_kvdb1", NULL, NULL)` | 初始化 KVDB |
|
||||
| `fdb_kvdb_set(&db, "key", "value")` | 写键值 (字符串) |
|
||||
| `fdb_kvdb_get(&db, "key", &data)` | 读键值 |
|
||||
| `fdb_kvdb_del(&db, "key")` | 删除键值 |
|
||||
| `fdb_kvdb_set_blob(&db, "key", &blob)` | 写二进制 blob |
|
||||
| `fdb_tsdb_init(&db, "tsdb1", "fdb_tsdb1", get_time, 1024, NULL)` | 初始化 TSDB |
|
||||
| `fdb_tsl_append(&db, data)` | 追加时序记录 |
|
||||
| `fdb_tsl_iter_by_time(&db, from, to, cb, cb_arg)` | 时间范围查询 |
|
||||
|
||||
### 10.4 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 (清空映射表, 慎用) |
|
||||
|
||||
---
|
||||
|
||||
## 11. 使用示例
|
||||
|
||||
### 11.1 KVDB 初始化与使用
|
||||
|
||||
```c
|
||||
#include "flashdb.h"
|
||||
#include "fdb_cfg.h"
|
||||
|
||||
/* KVDB 实例 */
|
||||
static fdb_kvdb_t g_kvdb;
|
||||
|
||||
static int kvdb_init(void) {
|
||||
fdb_err_t ret = fdb_kvdb_init(&g_kvdb, "kvdb1", "fdb_kvdb1", NULL, NULL);
|
||||
return (ret == FDB_NO_ERR) ? 0 : -1;
|
||||
}
|
||||
|
||||
static void kvdb_example(void) {
|
||||
/* 写字符串 */
|
||||
fdb_kvdb_set(&g_kvdb, "device_id", "STM32F407-001");
|
||||
|
||||
/* 读字符串 */
|
||||
char buf[64];
|
||||
fdb_kvdb_get(&g_kvdb, "device_id", buf, sizeof(buf));
|
||||
|
||||
/* 写二进制 blob */
|
||||
struct fdb_blob blob;
|
||||
uint32_t value = 42;
|
||||
fdb_blob_make(&blob, &value, sizeof(value));
|
||||
fdb_kvdb_set_blob(&g_kvdb, "counter", &blob);
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 TSDB 初始化与使用
|
||||
|
||||
```c
|
||||
static fdb_tsdb_t g_tsdb;
|
||||
|
||||
static time_t get_timestamp(void) {
|
||||
return sys_clock_now(); // 从 RTC 获取 Unix 时间戳
|
||||
}
|
||||
|
||||
static int tsdb_init(void) {
|
||||
fdb_err_t ret = fdb_tsdb_init(&g_tsdb, "tsdb1", "fdb_tsdb1",
|
||||
get_timestamp, 1024, NULL);
|
||||
return (ret == FDB_NO_ERR) ? 0 : -1;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
float voltage;
|
||||
float current;
|
||||
float temperature;
|
||||
} sensor_data_t;
|
||||
|
||||
static void tsdb_example(void) {
|
||||
sensor_data_t data = {3.3f, 0.5f, 25.6f};
|
||||
fdb_tsl_append(&g_tsdb, &data);
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 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);
|
||||
}
|
||||
```
|
||||
|
||||
### 11.4 FTL 格式化
|
||||
|
||||
```c
|
||||
#include "nand_ftl.h"
|
||||
|
||||
/* 注意: 此操作将清空 FTL 分区全部数据, 慎用 */
|
||||
if (nand_ftl_format() == 0) {
|
||||
DBG_INFO("FTL formatted");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 注意事项
|
||||
|
||||
1. **擦除对齐** — `gd5f2gq5ue_erase()` 的 offset 和 size 必须严格按 GD5F_BLOCK_SIZE (128KB) 对齐和整数倍。
|
||||
2. **写前擦除** — NAND 不能原地覆写,FTL 和 FlashDB 内部自动管理擦除,但直接调用 `gd5f2gq5ue_write()` 前必须确保目标块已擦除。
|
||||
3. **FTL 首个扇区** — `dhara_map_init` 的第 5 个参数 (journal 页数) 影响 GC 效率,当前为 4,增大可减少写入放大但占用更多内存。
|
||||
4. **坏块传播** — FTL 在擦除/编程失败后自动调用 `dhara_nand_mark_bad` → `gd5f2gq5ue_mark_block_bad`,BBT 在 RAM 中更新,下次复位后重新扫描出厂坏块并叠加运行时坏块。
|
||||
5. **功耗** — 擦除操作最大耗时约 5ms (驱动超时设为 5s),页编程约 600ms (超时 1s),读写操作快。在低功耗场景需注意合理安排操作时序。
|
||||
6. **分区隔离** — 三个分区物理隔离,FlashDB 操作不会影响 FTL 数据,反之亦然。修改分区布局时需同步更新 `fal_cfg.h` 和确认 FTL 宏自动适配。
|
||||
7. **缓存一致性** — 单页缓存 (s_cache_buf) 仅对 FatFS 层可见,多任务读写同一文件需在应用层同步。
|
||||
Reference in New Issue
Block a user