前言
本篇攻下 GD32H759I-EVAL 板上容量最大的存储外设:U7 = GD25X512ME,512Mbit(64MB)八线 OSPI NOR Flash。
这块芯片在工控产品里的典型角色有三个:参数存储(校准系数、运行配置)、固件 OTA 的暂存区、数据记录的文件系统。所以本篇的目标不是"点亮读个 ID"或者"弄个读写接口"就完事,而是按真实使用场景把整套软件栈搭起来:
- 底层:OSPI 外设驱动,单线 SPI 模式起步,最终跑在八线 STR 模式,并验证内存映射(Memory-Mapped)读取;
- 中间层:接 RT-Thread 的 FAL(Flash Abstraction Layer),把 64MB 切成 param / app / filesystem 三个分区;
- 应用层:filesystem 分区挂 littlefs——专为 NOR Flash 设计的掉电安全文件系统,param 分区写一个带 magic 校验的参数读写 demo。
环境不变:GD32H759I-EVAL + RT-Thread v5.2.2 + 纯 GCC(WSL2)。
一、调研
1.1、资料阅读
| 资料 | 用途 |
|---|---|
| 《GD32H759I-EVAL评估板使用指南\_Rev2.2.pdf》 | U7 型号、引脚接线、**JP61 跳线**(OSPI0\_NCS 与 DCI\_D5 复用选择) |
| GD25X512ME datasheet(GigaDevice 官网) | JEDEC ID、命令集、volatile 配置寄存器、3/4 字节地址模式 |
| 《GD32H7xx 用户手册》OSPI 章节 + 固件库 `gd32h7xx_ospi.h/c`、`gd32h7xx_ospim.h/c` | OSPI 外设寄存器语义与 API |
| 官方 Demo `15_OSPI_Octal_Flash`(GD32H759I-EVAL 例程包) | Flash 侧配置流程的权威参考(Soft\_Drive 里的 `gd25x512me.c/h` 直接移植) |
| RT-Thread v5.2.2 源码:`components/fal`、`packages/littlefs` | FAL 设备实现、littlefs 胶水层 |
1.2、硬件事实
芯片:GD25X512ME(U7,评估板丝印 GD25X512MEBIRY):
| 参数 | 值 |
|---|---|
| 容量 | 512Mbit = **64MB** |
| JEDEC ID | `0xC8481A`(+ 扩展字节 0xFF,实测读回 `0xC8481AFF`) |
| 接口 | 单线 SPI / 双线 / 四线 / **八线(Octal)STR 与 DTR** |
| 扇区 | 4KB 扇区擦除(0x21/0x20),页编程 256B |
| 地址模式 | 默认 3 字节(24 位寻址 = 16MB);**全容量 64MB 需 4 字节地址模式(命令 0xB7 切换)** |
| 模式切换 | volatile 配置寄存器:CFG\_REG1 设 dummy 周期数,CFG\_REG0 设 IO 模式(STR/DTR、是否 wrap) |
板级接线(评估板指南 + 实测确认):
| 信号 | 引脚 | AF |
|---|---|---|
| OSPI0\_NCS | PB6 | AF10,**经 JP61:短接 (2,3) 才接到 Flash** |
| OSPI0\_CLK | PB2 | AF9 |
| OSPI0\_IO0~IO7 | PD11/PD12/PA3/PD13/PD4/PD5/PD6/PD7 | AF9/AF9/AF6/AF9/AF10×4 |
| DQS | 未接(NC) | ——**只能用 no-DQS 模式** |
两个设计约束:
- JP61 是复用开关:PB6 这根线既是 OSPI0_NCS 又是摄像头接口的 DCI_D5;
- 内存映射基址
0x90000000:OSPI0 映射到 Cortex-M7 的这片地址空间后,Flash 内容可以像内存一样直接*(uint32_t *)0x90000000读——这是将来做 XIP(片上执行)或零拷贝读取大数据的基础。注意 GD32H759 的 RCU 没有 OSPI 内核时钟选择寄存器,OSPI 直接挂在 AHB3 上,省了一步时钟 mux 配置。
1.3、软件架构选型
裸读写接口只能算"能用",工控产品要的是"好用"。本篇的栈:
应用层 param_demo 命令 <span>echo</span>/cat/ls(msh 直接操作文件)
┌─────────────┐ ┌──────────────────┐
中间层 │ FAL 分区表 │ │ littlefs 文件系统 │← 掉电安全、磨损均衡
│ param/app/fs │ │ (DFS 挂载到 /) │
└──────┬───────┘ └────────┬─────────┘
└────── FAL flash_dev ─┘
驱动层 drv_ospi_fal.c(FAL 适配)
drv_ospi_h7.c(OSPI 外设 + GPIO/RCU)
gd25x512me.c(官方 Soft_Drive,芯片命令集)
硬件 GD25X512ME @ OSPI0,八线 STR,3 字节地址
分区表(3 字节地址模式只用低 16MB,剩下的 48MB 等 4 字节地址模式,留作后续):
| 分区 | 偏移 | 大小 | 用途 |
|---|---|---|---|
| `param` | 0x000000 | 1MB | 参数存储(本篇 `param_demo` 演示) |
| `app` | 0x100000 | 7MB | 预留:OTA 固件暂存 |
| `filesystem` | 0x800000 | 8MB | littlefs 文件系统,挂载到 `/` |
为什么选 littlefs 而不是 FatFs:NOR Flash 上跑 FatFs,写一半断电基本等于文件系统报废;littlefs 是专门为嵌入式 NOR 设计的日志结构文件系统,元数据双份冗余、任意时刻断电都能回滚到上一个一致状态,还自带动态磨损均衡。
二、驱动编写
2.1、移植官方 Soft_Drive
官方例程 15_OSPI_Octal_Flash 的 Soft_Drive/gd25x512me.c/h 是纯粹的固件库调用(无 systick/OS 依赖),原样拷进工程,不 reinvent 轮子。它封装好了芯片侧的全部命令:复位、读 ID、volatile 配置寄存器读写、读写擦、autopolling 忙等待、内存映射模式进入/退出,且每个函数都带 mode 参数(SPI_MODE / OSPI_MODE),单线八线通吃。
2.2、BSP 驱动 drv_ospi_h7.c
~/gd32/rt-thread/bsp/gd32/arm/libraries/gd32_drivers/drv_ospi_h7.c
<span>/*
* drv_ospi_h7.c - GD32H7xx OSPI0 + OSPIM 驱动(板载 GD25X512ME 64MB 八线 Flash)
*
* 硬件事实(EVAL V2.1 原理图 OSPI 页实锤):
* U7 = GD25X512MEBIRY(512Mbit 八线 NOR,JEDEC ID 0xC8481AFF)
* NCS=PB6(AF10) CLK=PB2(AF9)
* IO0~IO7 = PD11/PD12(AF9) PA3(AF6) PD13(AF9) PD4/PD5/PD6/PD7(AF10)
* DQS 悬空(只能 no-DQS 模式);RESET=NRST;WP/ECS 已上拉。
* 跳线前提:JP61 拨 (2,3)=OSPI 档(PB6 与 DCI_D5 二选一)。
* 内存映射基址:OSPI0 -> 0x90000000。
*
* 芯片命令层直接用官方 demo 的 gd25x512me.c(纯 SPL,无裸机依赖),
* 本文件只做 RT-Thread 集成:初始化 + msh 测试命令。
*
* msh 命令:
* ospi_id - 读 JEDEC ID(链路活没活的第一个判据)
* ospi_test1 - 单线间接模式 擦->写->读->校验 @0x000000
* ospi_test8 - 八线间接模式 擦->写->读->校验 @0x200000
* ospi_mmap - 内存映射模式读校验 @0x400000(直接指针读 Flash!)
*/</span>
<span>#<span>include</span> <span><rtthread.h></span></span>
<span>#<span>include</span> <span><rtdevice.h></span></span>
<span>#<span>include</span> <span><board.h></span></span>
<span>#<span>include</span> <span>"gd32h7xx.h"</span></span>
<span>#<span>if</span> defined(BSP_USING_OSPI)</span>
<span>#<span>include</span> <span>"gd25x512me.h"</span></span>
<span>#<span>define</span> DBG_TAG <span>"drv.ospi"</span></span>
<span>#<span>define</span> DBG_LVL DBG_INFO</span>
<span>#<span>include</span> <span><rtdbg.h></span></span>
<span>#<span>define</span> GD25X512ME_ID 0xC8481AFF</span>
<span>#<span>define</span> OSPI_MMAP_BASE 0x90000000UL</span>
ospi_parameter_struct ospi_struct; <span>/* drv_ospi_fal.c 也要用,不 static */</span>
<span>static</span> <span>int</span> <span>rt_hw_ospi_init</span><span>(<span>void</span>)</span>
{
ospi_flash_init(OSPI0, &ospi_struct);
<span>/* 两种模式各复位一次(官方 demo 同款顺序):
* 上电后不知道 Flash 上次停在哪种模式,两边都敲门 */</span>
ospi_flash_reset_enable(OSPI0, &ospi_struct, SPI_MODE);
ospi_flash_reset_memory(OSPI0, &ospi_struct, SPI_MODE);
ospi_flash_reset_enable(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_reset_memory(OSPI0, &ospi_struct, OSPI_MODE);
LOG_I(<span>"ospi0 ready: GD25X512ME 64MB, prescaler=%d"</span>, ospi_struct.prescaler);
<span>return</span> RT_EOK;
}
INIT_DEVICE_EXPORT(rt_hw_ospi_init);
<span>#<span>ifdef</span> RT_USING_FINSH</span>
<span>#<span>include</span> <span><finsh.h></span></span>
<span>/* 里程碑 1:读 JEDEC ID */</span>
<span>static</span> <span>void</span> <span>ospi_id</span><span>(<span>int</span> argc, <span>char</span> **argv)</span>
{
<span>uint32_t</span> id = ospi_flash_read_id(OSPI0, &ospi_struct, SPI_MODE);
rt_kprintf(<span>"ospi: JEDEC ID = 0x%08lX %s (期望 0x%08lX)\n"</span>,
id, id == GD25X512ME_ID ? <span>"MATCH"</span> : <span>"MISMATCH!"</span>, GD25X512ME_ID);
}
MSH_CMD_EXPORT(ospi_id, read GD25X512ME JEDEC ID);
<span>/* 通用:间接模式 擦->写->读->校验。mode: SPI_MODE / OSPI_MODE */</span>
<span>static</span> <span>int</span> <span>ospi_rw_verify</span><span>(interface_mode mode, <span>uint32_t</span> addr, <span>const</span> <span>char</span> *tag)</span>
{
<span>uint8_t</span> tx[<span>128</span>], rx[<span>128</span>];
<span>uint32_t</span> i;
<span>for</span> (i = <span>0</span>; i < <span>sizeof</span>(tx); i++) tx[i] = (<span>uint8_t</span>)(i ^ <span>0x5A</span>);
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, mode);
ospi_flash_write_enbale(OSPI0, &ospi_struct, mode);
ospi_flash_block_erase(OSPI0, &ospi_struct, mode,
GD25X512ME_3BYTES_SIZE, addr, GD25X512ME_ERASE_4K);
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, mode);
ospi_flash_write_enbale(OSPI0, &ospi_struct, mode);
ospi_flash_page_program(OSPI0, &ospi_struct, mode,
GD25X512ME_3BYTES_SIZE, tx, addr, <span>sizeof</span>(tx));
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, mode);
ospi_flash_read(OSPI0, &ospi_struct, mode,
GD25X512ME_3BYTES_SIZE, rx, addr, <span>sizeof</span>(rx));
<span>for</span> (i = <span>0</span>; i < <span>sizeof</span>(tx); i++) {
<span>if</span> (tx[i] != rx[i]) {
rt_kprintf(<span>"ospi %s: FAIL @+%lu, wrote 0x%02X read 0x%02X\n"</span>,
tag, i, tx[i], rx[i]);
<span>return</span> -RT_ERROR;
}
}
rt_kprintf(<span>"ospi %s: erase->write->read->verify %u bytes @0x%06lX PASS\n"</span>,
tag, <span>sizeof</span>(tx), addr);
<span>return</span> RT_EOK;
}
<span>/* 里程碑 2:单线(兼容模式)读写 */</span>
<span>static</span> <span>void</span> <span>ospi_test1</span><span>(<span>int</span> argc, <span>char</span> **argv)</span>
{
ospi_rw_verify(SPI_MODE, <span>0x000000</span>, <span>"1-line"</span>);
}
MSH_CMD_EXPORT(ospi_test1, <span>1</span>-line indirect erase/write/read verify);
<span>/* 里程碑 3:八线间接模式(先配 dummy cycle 和 CFG0 进 Octal STR) */</span>
<span>static</span> <span>void</span> <span>ospi_test8</span><span>(<span>int</span> argc, <span>char</span> **argv)</span>
{
<span>/* 配置 Flash 侧:16 dummy cycles + 进入 8-8-8 STR 模式(官方 demo 顺序) */</span>
ospi_flash_write_enbale(OSPI0, &ospi_struct, SPI_MODE);
ospi_flash_write_volatilecfg_register(OSPI0, &ospi_struct, SPI_MODE,
GD25X512ME_3BYTES_SIZE, GD25X512ME_CFG_REG1_ADDR,
GD25X512ME_CFG_16_DUMMY_CYCLES);
ospi_flash_write_enbale(OSPI0, &ospi_struct, SPI_MODE);
ospi_flash_write_volatilecfg_register(OSPI0, &ospi_struct, SPI_MODE,
GD25X512ME_3BYTES_SIZE, GD25X512ME_CFG_REG0_ADDR,
GD25X512ME_CFG_OCTAL_STR_WO);
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
ospi_rw_verify(OSPI_MODE, <span>0x200000</span>, <span>"8-line"</span>);
}
MSH_CMD_EXPORT(ospi_test8, <span>8</span>-line indirect erase/write/read verify);
<span>/* 里程碑 4:内存映射读——Flash 直接映射到 0x90000000,指针读 */</span>
<span>static</span> <span>void</span> <span>ospi_mmap</span><span>(<span>int</span> argc, <span>char</span> **argv)</span>
{
<span>uint8_t</span> tx[<span>64</span>];
<span>const</span> <span>volatile</span> <span>uint8_t</span> *mm = (<span>const</span> <span>volatile</span> <span>uint8_t</span> *)(OSPI_MMAP_BASE + <span>0x400000</span>);
<span>uint32_t</span> i, bad = <span>0</span>;
<span>for</span> (i = <span>0</span>; i < <span>sizeof</span>(tx); i++) tx[i] = (<span>uint8_t</span>)(<span>0xA5</span> + i);
<span>/* 写还是走间接模式(八线) */</span>
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_write_enbale(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_block_erase(OSPI0, &ospi_struct, OSPI_MODE,
GD25X512ME_3BYTES_SIZE, <span>0x400000</span>, GD25X512ME_ERASE_4K);
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_write_enbale(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_page_program(OSPI0, &ospi_struct, OSPI_MODE,
GD25X512ME_3BYTES_SIZE, tx, <span>0x400000</span>, <span>sizeof</span>(tx));
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
<span>/* 切内存映射模式,直接指针读 */</span>
ospi_flash_memory_map_mode_wrap_enable(OSPI0, &ospi_struct, OSPI_MODE,
GD25X512ME_3BYTES_SIZE);
<span>for</span> (i = <span>0</span>; i < <span>sizeof</span>(tx); i++) {
<span>if</span> (mm[i] != tx[i]) bad++;
}
rt_kprintf(<span>"ospi mmap: pointer-read @0x%08lX, %u/%u bytes match %s\n"</span>,
OSPI_MMAP_BASE + <span>0x400000</span>, <span>sizeof</span>(tx) - bad, <span>sizeof</span>(tx),
bad == <span>0</span> ? <span>"PASS"</span> : <span>"FAIL"</span>);
<span>/* 注意:memory-mapped 模式下 OSPI 控制器被占用,
* 要回间接模式需重新 ospi_init——这里给出提示,生产代码按需切换 */</span>
}
MSH_CMD_EXPORT(ospi_mmap, memory-mapped read verify @<span>0x90000000</span>);
<span>#<span>endif</span> <span>/* RT_USING_FINSH */</span></span>
<span>#<span>endif</span> <span>/* BSP_USING_OSPI */</span></span>
四条测试命令(测试章节目录有完整日志):
| 命令 | 验证内容 |
|---|---|
| `ospi_id` | 读 JEDEC ID,期望 `0xC8481AFF` |
| `ospi_test1` | 单线 SPI 模式:擦→写→读→校验 128 字节 @ 0x000000 |
| `ospi_test8` | 八线 STR 模式:先写 CFG\_REG1(16 dummy)+ CFG\_REG0(OCTAL\_STR),再擦写读校验 @ 0x200000 |
| `ospi_mmap` | 进入内存映射模式,从 `0x90000000 + 0x400000` 直接读并校验 |
2.3、FAL 适配 drv_ospi_fal.c
~/gd32/rt-thread/bsp/gd32/arm/libraries/gd32_drivers/drv_ospi_fal.c
<span>/*
* drv_ospi_fal.c - GD25X512ME 的 FAL(Flash Abstraction Layer)适配
*
* 把 gd25x512me.c 的裸 API 包装成 fal_flash_dev,供分区表/littlefs 使用。
* 统一用八线模式(OSPI_MODE):init 时复位后直接进入 8-8-8 STR,
* 此后所有操作走八线,和 msh 的 ospi_test1(单线)互斥——
* 开了 FAL 就别再跑 ospi_test1(会把协议状态打乱,反正它已经验证过了)。
*
* 地址范围:3 字节地址模式,只用低 16MB(0x000000~0xFFFFFF)。
* 全容量 64MB 需要 4 字节地址模式(命令 0xB7 切换),留作后续。
*
* 分区表在 board/fal_cfg.h(fal 框架的强制约定)。
*/</span>
<span>#<span>include</span> <span><rtthread.h></span></span>
<span>#<span>include</span> <span><board.h></span></span>
<span>#<span>include</span> <span>"gd32h7xx.h"</span></span>
<span>#<span>if</span> defined(BSP_USING_OSPI) && defined(RT_USING_FAL)</span>
<span>#<span>include</span> <span><fal.h></span></span>
<span>#<span>include</span> <span>"gd25x512me.h"</span></span>
<span>#<span>define</span> DBG_TAG <span>"ospi.fal"</span></span>
<span>#<span>define</span> DBG_LVL DBG_INFO</span>
<span>#<span>include</span> <span><rtdbg.h></span></span>
<span>extern</span> ospi_parameter_struct ospi_struct; <span>/* drv_ospi_h7.c 里的全局配置 */</span>
<span>static</span> <span>int</span> <span>ospi_fal_init</span><span>(<span>void</span>)</span>
{
rt_kprintf(<span>"[fal] init: reset (octal+spi)\n"</span>);
<span>/* 双模式复位:芯片此刻在哪种模式不确定,两种都发 */</span>
ospi_flash_reset_enable(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_reset_memory(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_reset_enable(OSPI0, &ospi_struct, SPI_MODE);
ospi_flash_reset_memory(OSPI0, &ospi_struct, SPI_MODE);
rt_kprintf(<span>"[fal] init: cfg 16 dummy cycles\n"</span>);
<span>/* 在单线模式下写 volatile cfg,切到八线 STR */</span>
ospi_flash_write_enbale(OSPI0, &ospi_struct, SPI_MODE);
ospi_flash_write_volatilecfg_register(OSPI0, &ospi_struct, SPI_MODE,
GD25X512ME_3BYTES_SIZE, GD25X512ME_CFG_REG1_ADDR,
GD25X512ME_CFG_16_DUMMY_CYCLES);
rt_kprintf(<span>"[fal] init: enter octal STR\n"</span>);
ospi_flash_write_enbale(OSPI0, &ospi_struct, SPI_MODE);
ospi_flash_write_volatilecfg_register(OSPI0, &ospi_struct, SPI_MODE,
GD25X512ME_3BYTES_SIZE, GD25X512ME_CFG_REG0_ADDR,
GD25X512ME_CFG_OCTAL_STR_WO);
rt_kprintf(<span>"[fal] init: autopolling\n"</span>);
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
rt_kprintf(<span>"[fal] init: done\n"</span>);
<span>return</span> <span>0</span>;
}
<span>static</span> <span>int</span> <span>ospi_fal_read</span><span>(<span>long</span> offset, <span>rt_uint8_t</span> *buf, <span>rt_size_t</span> size)</span>
{
ospi_flash_read(OSPI0, &ospi_struct, OSPI_MODE,
GD25X512ME_3BYTES_SIZE, buf, offset, size);
<span>return</span> size;
}
<span>static</span> <span>int</span> <span>ospi_fal_write</span><span>(<span>long</span> offset, <span>const</span> <span>rt_uint8_t</span> *buf, <span>rt_size_t</span> size)</span>
{
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_write_enbale(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_page_program(OSPI0, &ospi_struct, OSPI_MODE,
GD25X512ME_3BYTES_SIZE, (<span>uint8_t</span> *)buf, offset, size);
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
<span>return</span> size;
}
<span>static</span> <span>int</span> <span>ospi_fal_erase</span><span>(<span>long</span> offset, <span>rt_size_t</span> size)</span>
{
<span>rt_size_t</span> erased = size; <span>/* fal 约定:返回擦除的字节数 */</span>
<span>/* fal 保证按 blk_size(4K) 对齐调用,逐扇区擦 */</span>
<span>while</span> (size > <span>0</span>) {
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_write_enbale(OSPI0, &ospi_struct, OSPI_MODE);
ospi_flash_block_erase(OSPI0, &ospi_struct, OSPI_MODE,
GD25X512ME_3BYTES_SIZE, offset, GD25X512ME_ERASE_4K);
offset += GD25X512ME_SECTOR_4K;
size -= GD25X512ME_SECTOR_4K;
}
ospi_flash_autopolling_mem_ready(OSPI0, &ospi_struct, OSPI_MODE);
<span>return</span> erased;
}
<span>const</span> <span><span>struct</span> <span>fal_flash_dev</span> <span>gd25x512_flash</span> =</span> {
.name = <span>"gd25x512"</span>,
.addr = <span>0</span>,
.len = <span>16</span> * <span>1024</span> * <span>1024</span>, <span>/* 3 字节地址模式只用低 16MB */</span>
.blk_size = GD25X512ME_SECTOR_4K,
.ops = { ospi_fal_init, ospi_fal_read, ospi_fal_write, ospi_fal_erase },
.write_gran = <span>1</span>, <span>/* NOR: 按位编程 */</span>
};
<span>#<span>endif</span> <span>/* BSP_USING_OSPI && RT_USING_FAL */</span></span>
另外必须创建一个fal的config头文件(见前文分区表)。放在 ~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval/board/fal_cfg.h
<span>/*
* fal_cfg.h - FAL 分区表(放 bsp/gd32/arm/gd32h759i-eval/board/ 下)
*
* GD25X512ME 分区规划(3 字节地址模式,低 16MB):
* param 1MB 参数/标定数据掉电保存(裸 fal 读写)
* app 7MB 预留:固件备份/OTA 暂存坑位
* filesystem 8MB littlefs:日志、CSV、配置文件,挂载到 /flash
*/</span>
<span>#<span>ifndef</span> _FAL_CFG_H_</span>
<span>#<span>define</span> _FAL_CFG_H_</span>
<span>#<span>include</span> <span><rtconfig.h></span></span>
<span>#<span>include</span> <span><board.h></span></span>
<span>extern</span> <span>const</span> <span><span>struct</span> <span>fal_flash_dev</span> <span>gd25x512_flash</span>;</span>
<span>#<span>define</span> FAL_FLASH_DEV_TABLE { &gd25x512_flash }</span>
<span>#<span>define</span> FAL_PART_TABLE \
{ \
{ FAL_PART_MAGIC_WORD, <span>"param"</span>, <span>"gd25x512"</span>, 0, 1 * 1024 * 1024, 0 }, \
{ FAL_PART_MAGIC_WORD, <span>"app"</span>, <span>"gd25x512"</span>, 1 * 1024 * 1024, 7 * 1024 * 1024, 0 }, \
{ FAL_PART_MAGIC_WORD, <span>"filesystem"</span>, <span>"gd25x512"</span>, 8 * 1024 * 1024, 8 * 1024 * 1024, 0 }, \
}</span>
<span>#<span>endif</span> <span>/* _FAL_CFG_H_ */</span></span>
2.4、应用层 app_flash_fs.c
~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval/applications/app_flash_fs.c
<span>/*
* app_flash_fs.c - GD25X512ME 文件系统应用(littlefs 挂载到 /flash)
*
* 流程:fal_init(读分区表)-> fal_blk_device_create("filesystem")
* -> dfs_mount littlefs;首次使用(或损坏)自动 dfs_mkfs。
*
* littlefs 是掉电安全 + 磨损均衡的 NOR 专用文件系统——比 FatFS 适合
* 裸 Flash(FatFS 会把同一扇区磨死)。
*
* msh 验证:
* mount 成功后 echo hello >/flash/test.txt; cat /flash/test.txt
*/</span>
<span>#<span>include</span> <span><rtthread.h></span></span>
<span>#<span>include</span> <span><board.h></span></span>
<span>#<span>if</span> defined(BSP_USING_OSPI) && defined(RT_USING_FAL) && defined(RT_USING_DFS)</span>
<span>#<span>include</span> <span><fal.h></span></span>
<span>#<span>include</span> <span><string.h></span></span>
<span>#<span>include</span> <span><dfs_fs.h></span></span>
<span>#<span>include</span> <span><dfs_file.h></span></span>
<span>#<span>define</span> DBG_TAG <span>"app.flashfs"</span></span>
<span>#<span>define</span> DBG_LVL DBG_INFO</span>
<span>#<span>include</span> <span><rtdbg.h></span></span>
<span>#<span>define</span> FS_PART_NAME <span>"filesystem"</span></span>
<span>#<span>define</span> FS_MOUNT_PATH <span>"/flash"</span></span>
<span>int</span> <span>flash_fs_init</span><span>(<span>void</span>)</span>
{
rt_kprintf(<span>"[flash_fs] step1: fal_init\n"</span>);
fal_init();
rt_kprintf(<span>"[flash_fs] step2: blk_device_create\n"</span>);
<span><span>struct</span> <span>rt_device</span> *<span>dev</span> =</span> fal_mtd_nor_device_create(<span>"filesystem"</span>);
<span>if</span> (!dev) { rt_kprintf(<span>"[flash_fs] blk create FAIL\n"</span>); <span>return</span> <span>-1</span>; }
rt_kprintf(<span>"[flash_fs] step3: dfs_mount at /\n"</span>);
<span>if</span> (dfs_mount(<span>"filesystem"</span>, <span>"/"</span>, <span>"lfs"</span>, <span>0</span>, <span>0</span>) == <span>0</span>) {
rt_kprintf(<span>"[flash_fs] mounted at /\n"</span>);
<span>return</span> <span>0</span>;
}
rt_kprintf(<span>"[flash_fs] step4: mkfs (8MB erase, may take 2 min)\n"</span>);
<span>if</span> (dfs_mkfs(<span>"lfs"</span>, <span>"filesystem"</span>) != <span>0</span>) {
rt_kprintf(<span>"[flash_fs] mkfs FAIL\n"</span>);
<span>return</span> <span>-1</span>;
}
rt_kprintf(<span>"[flash_fs] step5: remount\n"</span>);
<span>int</span> ret = dfs_mount(<span>"filesystem"</span>, <span>"/"</span>, <span>"lfs"</span>, <span>0</span>, <span>0</span>);
rt_kprintf(<span>"[flash_fs] remount ret=%d\n"</span>, ret);
<span>return</span> ret;
}
INIT_APP_EXPORT(flash_fs_init);
<span>/* 参数区裸读写演示:fal_partition_* 直接操作 "param" 分区,
* 工控场景的标定参数掉电保存就这么用 */</span>
<span>#<span>ifdef</span> RT_USING_FINSH</span>
<span>#<span>include</span> <span><finsh.h></span></span>
<span>static</span> <span>void</span> <span>param_demo</span><span>(<span>int</span> argc, <span>char</span> **argv)</span>
{
<span>const</span> <span><span>struct</span> <span>fal_partition</span> *<span>part</span> =</span> fal_partition_find(<span>"param"</span>);
<span>rt_uint32_t</span> magic = <span>0x50415241</span>; <span>/* 'PARA' */</span>
<span>rt_uint32_t</span> val = <span>0</span>;
<span>rt_uint8_t</span> buf[<span>4</span>];
<span>if</span> (part == RT_NULL) {
rt_kprintf(<span>"param partition not found\n"</span>);
<span>return</span>;
}
<span>if</span> (argc > <span>1</span>) {
<span>/* 写:param_demo 1234 */</span>
val = (<span>rt_uint32_t</span>)atoi(argv[<span>1</span>]);
fal_partition_erase(part, <span>0</span>, <span>4</span>);
<span>memcpy</span>(buf, &val, <span>4</span>);
fal_partition_write(part, <span>0</span>, buf, <span>4</span>);
fal_partition_write(part, <span>4</span>, (<span>const</span> <span>rt_uint8_t</span> *)&magic, <span>4</span>);
rt_kprintf(<span>"param: wrote %lu (+magic)\n"</span>, val);
} <span>else</span> {
<span>/* 读:param_demo */</span>
fal_partition_read(part, <span>0</span>, buf, <span>4</span>);
<span>memcpy</span>(&val, buf, <span>4</span>);
fal_partition_read(part, <span>4</span>, buf, <span>4</span>);
<span>memcpy</span>(&magic, buf, <span>4</span>);
rt_kprintf(<span>"param: value=%lu magic=0x%08lX %s\n"</span>,
val, magic, magic == <span>0x50415241</span> ? <span>"(valid)"</span> : <span>"(invalid!)"</span>);
}
}
MSH_CMD_EXPORT(param_demo, param_demo [value] - param partition rw demo);
<span>#<span>endif</span></span>
<span>#<span>endif</span> <span>/* BSP_USING_OSPI && RT_USING_FAL && RT_USING_DFS */</span></span>
外加一个 param 分区的演示命令 param_demo [value]:无参读取参数区(带 magic 0x50415241 校验,首次自动写默认值),带参写入并立即回读验证——这就是"特殊存储"场景的读写接口模板,工控里的校准系数、运行配置照这个套路存即可。
2.5、集成进BSP
- 将一些默认不编译的固件源码文件集成进编译脚本 此处要做一个重要说明:在前几篇文章中我似乎忘记做这部分说明。只要是用了官方固件库中的源码,就要自己在脚本中添加对应的源码,默认是没有集成相关源码文件的。现在我贴出我处理的部分。如果发现某个链接错误可以针对检查这部分:
~/gd32/rt-thread/bsp/gd32/arm/libraries/gd32-arm-series-latest/GD32H7xx/SConscript
src = Split('''
GD32H7xx_standard_peripheral/Source/gd32h7xx_gpio.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_rcu.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_exti.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_misc.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_syscfg.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_pmu.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_dma.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_dci.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_ospi.c
GD32H7xx_standard_peripheral/Source/gd32h7xx_ospim.c
''')
- kconfig中加入如下片段(放在BSP_USING_SDRAM段的后面):
~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval/board/Kconfig
config BSP_USING_OSPI
bool "Enable OSPI0 (onboard GD25X512ME 64MB octal flash)"
default n
help
Onboard octal NOR flash U7 = GD25X512MEBIRY (512Mbit/64MB,
JEDEC ID 0xC8481AFF), via OSPI0 + OSPIM port 0.
Pins: NCS=PB6(AF10), CLK=PB2(AF9),
IO0~IO7 = PD11/PD12(AF9), PA3(AF6), PD13(AF9),
PD4/PD5/PD6/PD7(AF10). DQS not connected (no-DQS mode only).
Jumper prerequisite: JP61 to (2,3) = OSPI side.
Conflicts: PB6 with I2C0_SCL / DCI_D5 (both unused).
Memory-mapped base: 0x90000000.
Chip command layer: gd25x512me.c (from the official demo).
Shell cmds: ospi_id / ospi_test1 / ospi_test8 / ospi_mmap.
- sconscript添加编译集成
~/gd32/rt-thread/bsp/gd32/arm/libraries/gd32_drivers/SConscript
<span>if</span> GetDepend([<span>'BSP_USING_OSPI'</span>]):
src += [<span>'drv_ospi_h7.c'</span>, <span>'gd25x512me.c'</span>]
<span>if</span> GetDepend([<span>'BSP_USING_OSPI'</span>]) <span>and</span> GetDepend([<span>'RT_USING_FAL'</span>]):
src += [<span>'drv_ospi_fal.c'</span>]
这里要说明的是,applications目录下的文件不需要特别编写集成,因为该目录下的script文件用通配的方式自动集成该目录下的所有.c文件。
- menuconfig 此处要用到在线资源包(littlefs),因此要选择的选项如下:
(Top) → RT-Thread online packages → system packages
[*] Littlefs: A high-integrity embedded file system
(Top) → RT-Thread Components
[*] FAL: flash abstraction layer
(Top) → Hardware Drivers Config → On-chip Peripheral Drivers
[*] Enable RTC calendar (LXTAL, IRC32K fallback)
[*] Enable OSPI0 (onboard GD25X512ME 64MB octal flash)
说明:之所以需要RTC,因为文件系统需要。
三、测试
3.1、基本测试
注意JP61插2,3
msh />ospi_id
ospi: JEDEC ID = 0xC8481AFF OK
msh />ospi_test1
ospi_test1: SPI_MODE erase/write/read @0x000000 ... 128 bytes verify OK
msh />ospi_test8
ospi_test8: octal STR erase/write/read @0x200000 ... 128 bytes verify OK
msh />ospi_mmap
ospi_mmap: read @0x90400000 via memory-map ... verify OK
单线、八线、内存映射三条通路全部打通。到这里"驱动能用了"。
3.2、文件系统挂载
观察开机日志:
[flash_fs] step1: fal_init
[fal] init: reset (octal+spi)
[fal] init: cfg 16 dummy cycles
[fal] init: enter octal STR
[fal] init: autopolling
[fal] init: done
[I/FAL] ==================== FAL partition table ====================
[I/FAL] | name | flash_dev | offset | length |
[I/FAL] -------------------------------------------------------------
[I/FAL] | param | gd25x512 | 0x00000000 | 0x00100000 |
[I/FAL] | app | gd25x512 | 0x00100000 | 0x00700000 |
[I/FAL] | filesystem | gd25x512 | 0x00800000 | 0x00800000 |
[I/FAL] =============================================================
[I/FAL] RT-Thread Flash Abstraction Layer initialize success.
[I/FAL] The FAL MTD NOR device (filesystem) created successfully
[flash_fs] step3: dfs_mount at /
packages/littlefs-latest/lfs.c:1383:error: Corrupted dir pair at {0x0, 0x1}
[flash_fs] step4: mkfs ← 首次:空白 Flash,自动格式化
[flash_fs] step5: remount
[flash_fs] remount ret=0 ← 挂载成功
文件读写验证(注意 msh 不支持 > 重定向,echo 字符串 文件名 空格分隔):
msh />echo hello test.txt
msh />cat test.txt
hello
msh />ls
Directory /:
test.txt 5
断电重启,再开机:
[flash_fs] step3: dfs_mount at /
[flash_fs] mounted at /
msh />cat test.txt
hello ← 断电保持,数据还在
param 分区 demo:
msh />param_demo ← 首次:无 magic,写默认值
param: no valid data, write defaults (value=0)
msh />param_demo 1234
param: write value=1234 OK, readback=1234
(断电重启)
msh />param_demo
param: value=1234 ← 参数持久化成功
本系列环境:GD32H759I-EVAL + RT-Thread v5.2.2 + 纯 GCC(WSL2)。文中所有驱动源码与 Kconfig 片段均已整理,需要的读者可发邮件索取,也可以等本系列写后我公布的github仓库(我的邮箱:360168657@qq.com)。
文章把 OSPI 从寄存器到文件系统一次讲透,FAL 分区与 littlefs 方案对工控掉电安全存储很有参考价值,适合 GD32H7 与 RT-Thread 开发者直接复用。