diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\200\347\253\240/\347\254\254\344\270\200\347\253\240 \345\274\200\345\217\221\347\216\257\345\242\203\343\200\201BSP \351\205\215\347\275\256\344\270\216 RT-Thread \350\256\276\345\244\207\346\250\241\345\236\213.md" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\200\347\253\240/\347\254\254\344\270\200\347\253\240 \345\274\200\345\217\221\347\216\257\345\242\203\343\200\201BSP \351\205\215\347\275\256\344\270\216 RT-Thread \350\256\276\345\244\207\346\250\241\345\236\213.md" new file mode 100644 index 0000000000000000000000000000000000000000..8098a3e0aa6b9cb4b9291d57c513b17bab61eb61 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\200\347\253\240/\347\254\254\344\270\200\347\253\240 \345\274\200\345\217\221\347\216\257\345\242\203\343\200\201BSP \351\205\215\347\275\256\344\270\216 RT-Thread \350\256\276\345\244\207\346\250\241\345\236\213.md" @@ -0,0 +1,984 @@ +## 新建工程 +### 安装 RT-Thread Studio +#### IDE 简介 +RT-Thread Studio 是 RT-Thread 官方推出的集成开发环境,基于 Eclipse 平台深度定制。与传统的 MDK(Keil)/IAR 开发方式不同,RT-Thread Studio 针对 RT-Thread 开发流程做了大量优化: +- **工程管理**:可视化创建、配置、编译、下载 RT-Thread 项目 +- **图形化配置**:RT-Thread Settings 替代手动编辑 rtconfig.h,支持组件和软件包的可视化管理 +- **CubeMX 集成**:内置 STM32CubeMX 支持,可直接在 IDE 中配置引脚和外设 +- **软件包生态**:一键搜索、安装、管理 RT-Thread 软件包 +- **终端调试**:内置串口终端,直接与 FinSH 命令行交互 +- **一键下载**:集成 ST-Link、J-Link、DAP-Link 等多种调试器 +#### 下载与安装步骤 +**第一步:下载安装包** +访问[RT-Thread 下载页面](https://www.rt-thread.org/download.html) : +在下载页面中找到 **RT-Thread Studio** 部分,根据操作系统选择对应的安装包。 +- RT-Thread Studio 支持 Windows、Linux 和 macOS 三大平台,其中 Windows 平台使用最为广泛。 +**第二步:安装** +![install-studio](https://www.rt-thread.org/document/site/development-tools/rtthread-studio/um/figures/install-studio.png) +1. 双击安装包启动安装向导 +2. **关键提示**:安装路径必须选择**全英文且不含空格**的路径。推荐路径: + - ✅ `D:\RT-Thread\RT-Thread-Studio` + - ✅ `C:\RTThreadStudio` + - ❌ `D:\开发工具\RT-Thread Studio`(含中文和空格) + - ❌ `C:\Program Files\RT-Thread Studio`(含空格) +3. 保持其他选项为默认值,点击"安装" +4. 等待安装完成后,勾选"启动 RT-Thread Studio" +**第三步:首次启动** +首次启动 RT-Thread Studio 时,需要联网登录 RT-Thread 账号。如果没有账号,可以点击"注册"按钮创建新账号。登录后,RT-Thread Studio 会自动初始化工作空间,并检测和下载必要的工具链组件。 +![sign-in](https://www.rt-thread.org/document/site/development-tools/rtthread-studio/um/figures/sign-in.png) +#### 界面布局概览 +启动完成后,RT-Thread Studio 的主界面分为以下几个区域: + +| 区域 | 位置 | 功能 | +| ------------------ | -------- | ------------------- | +| 项目资源管理器 | 左侧 | 显示工程文件树,管理项目文件 | +| 代码编辑器 | 中间 | 编写和编辑代码,支持语法高亮和自动补全 | +| RT-Thread Settings | 双击工程下的节点 | 图形化配置内核、组件、驱动和软件包 | +| 控制台 | 下方 | 显示编译输出、下载日志和串口终端 | +| 工具栏 | 顶部 | 提供编译、下载、调试等快捷操作按钮 | +### 基于开发板创建工程 +#### 安装对应的SDK支持包 +1. 打开 RT-Thread Studio,点击菜单栏 **文件 → 包管理器**(或点击工具栏上的 SDK 管理器图标) +2. 在弹出的包管理器中,找到 **Board_Support_Packages** 分类 +3. 在搜索框中输入对应的开发板支持包 +4. 点击**安装**按钮,等待下载和安装完成 +SDK 安装完成后,在包管理器中可以看到已安装的 SDK 版本信息 +##### SDK 资源包包含的内容 + +| 内容 | 说明 | +| ----- | --------------------- | +| 芯片支持包 | 基础工程框架 | +| 板级支持包 | 开发板的 board.c/h 和驱动文件 | +| 示例代码 | 按学习阶段编号的示例项目 | +| 库文件 | STM32 HAL 库和 CMSIS 组件 | +#### 创建工程 +1. 在项目资源管理器窗口内点击右键,选择新建子菜单项目。 +2. 在弹出的新建项目向导对话框中选择 RT‑Thread 项目类型,然后点击下一步。 +3. 填写工程名,选择基于开发板创建工程,选择开发板型号,选择 BSP 版本号,选择 RT‑Thread 源码版本,选择调试器和调试接口,然后点击完成按钮。 +4. 点击完成后,等待工程创建过程。 +5. 工程创建成功后项目资源管理器窗口会出现刚创建的工程 test。 +### 基于芯片支持包创建工程 +#### 创建工程 +1. 点击菜单栏 **文件 → 新建 → RT-Thread 项目** +2. 在弹出的新建项目向导中,选择 **"基于芯片"** 选项卡 +3. 在芯片列表中展开对应的系列,找到并选择对应的芯片 +4. 在右侧填写项目信息: + - **项目名称** + - **项目位置**:选择工作空间路径(建议全英文路径) + - **RT-Thread 版本**: +1. 点击 **完成**,RT-Thread Studio 会自动生成工程文件 +创建完成后,在项目资源管理器中可以看到工程目录结构。 +#### 工程目录结构详解 +创建完成后,工程目录结构如下: + +``` +rsoc_chapter01/ # 工程根目录 +├── .settings/ # IDE 配置文件(自动生成) +├── applications/ # 应用层代码 +│ └── main.c # 主程序入口 +├── board/ # 板级支持包(BSP) +│ ├── board.c # 板级初始化(时钟、外设、内存) +│ ├── board.h # 板级头文件(引脚定义) +│ ├── board_linker/ # 链接脚本 +│ │ └── link.lds # 内存布局定义 +│ ├── CubeMX_Config/ # CubeMX 配置 +│ │ ├── CubeMX_Config.ioc # CubeMX 工程文件 +│ │ └── Src/ # CubeMX 生成的 HAL 初始化代码 +│ │ ├── main.c # HAL 库初始化 +│ │ └── stm32f4xx_hal_msp.c # 外设 MSP 初始化 +│ └── ports/ # 板级驱动(针对具体外设) +├── rt-thread/ # RT-Thread 内核源码 +│ ├── src/ # 内核源码 +│ ├── include/ # 内核头文件 +│ ├── components/ # 组件源码 +│ │ ├── finsh/ # FinSH 命令行组件 +│ │ ├── dfs/ # 虚拟文件系统 +│ │ └── drivers/ # 驱动框架 +│ └── libcpu/ # 处理器架构相关代码 +│ └── arm/cortex-m4/ # ARM Cortex-M4 移植代码 +├── libraries/ # 芯片厂商库 +│ └── HAL_Drivers/ # STM32 HAL 库 +│ ├── Inc/ # HAL 库头文件 +│ └── Src/ # HAL 库源码 +├── rtconfig.h # RT-Thread 内核配置(自动生成) +├── rtconfig_preinc.h # 预编译配置 +├── SConscript # SCons 构建脚本 +├── SConstruct # SCons 顶层构建脚本 +└── template.uvprojx # 可选的 MDK 工程模板 +``` + +**关键目录说明:** + +| 目录/文件 | 作用 | 是否需要修改 | +| --------------------------------------- | ------------------- | ----------------------------- | +| `applications/main.c` | 用户应用入口,编写业务逻辑 | ✅ 经常修改 | +| `board/board.h` | 引脚定义,外设宏定义 | ✅ 按需修改 | +| `board/board.c` | 系统时钟配置,板级初始化 | ✅ 按需修改 | +| `board/CubeMX_Config/CubeMX_Config.ioc` | CubeMX 工程,用于引脚和时钟配置 | ✅ 按需修改 | +| `rtconfig.h` | RT-Thread 内核配置头文件 | ⚠️ 通过 RT-Thread Settings 间接修改 | +| `rt-thread/` | RT-Thread 内核源码 | ❌ 一般不修改 | +| `libraries/` | HAL 库源码 | ❌ 一般不修改 | +#### 编译与下载验证 +**第一步:编译工程** +右键工程名称 → **重新构建项目**(或点击工具栏的锤子图标)。 +编译成功后,控制台输出类似以下内容: + +``` +... +arm-none-eabi-objcopy -O ihex rtthread.elf rtthread.hex +arm-none-eabi-size rtthread.elf + text data bss dec hex filename + 42345 1032 4304 47681 ba41 rtthread.elf +``` + +- `text`:代码段大小(Flash 占用) +- `data`:已初始化数据段大小 +- `bss`:未初始化数据段大小(RAM 占用) +- `dec`:总大小(十进制)= text + data + bss +**第二步:连接硬件** +1. 使用 USB 线连接开发板的 ST-Link 接口到电脑 +2. 使用另一个 USB 转 TTL 串口模块连接开发板的 UART1 引脚(PA9 TX / PA10 RX) +3. 确保 ST-Link 驱动已安装 +**第三步:下载程序** +右键工程 → **调试方式 → ST-Link 下载**(或点击工具栏的下载图标)。 +**第四步:串口验证** +打开串口终端(波特率 115200,数据位 8,停止位 1,无校验),按下开发板复位键,应看到 RT-Thread 的启动 Logo: + +``` + \ | / +- RT - Thread Operating System + / | \ 4.1.1 build Aug 24 2026 + 2006 - 2023 Copyright by RT-Thread team +msh > +``` + +看到 `msh >` 提示符,说明工程创建成功、编译下载正常、RT-Thread 系统正常运行。 +### 启动流程分析 +理解 RT-Thread 的启动流程,有助于后续的 BSP 配置和驱动开发。 +- 从上电到 `msh >` 提示符的完整启动链路: + +``` +上电复位 + │ + ▼ +Reset_Handler (startup_stm32f407xx.s) + │ └── SystemInit() -- 系统初始化(FPU、时钟源选择) + │ └── __main() -- C 运行时初始化(BSS 清零、数据段拷贝) + │ + ▼ +main() [board/CubeMX_Config/Src/main.c] + │ └── HAL_Init() -- HAL 库初始化 + │ └── SystemClock_Config() -- 时钟树配置(168MHz) + │ + ▼ +rtthread_startup() [rt-thread/src/components.c] + │ └── rt_hw_board_init() -- 板级硬件初始化 [board/board.c] + │ └── 时钟初始化 + │ └── 内存初始化 + │ └── 中断初始化 + │ └── 控制台初始化 + │ └── rt_show_version() -- 打印 RT-Thread Logo + │ └── rt_system_timer_init() -- 系统定时器初始化 + │ └── rt_system_scheduler_init() -- 调度器初始化 + │ └── rt_application_init() -- 应用初始化(创建 main 线程) + │ └── rt_system_timer_thread_init() -- 定时器线程初始化 + │ └── rt_thread_idle_init() -- 空闲线程初始化 + │ └── rt_system_scheduler_start() -- 启动调度器 + │ + ▼ +main 线程入口 [applications/main.c] + │ └── main() -- 用户 main 函数 + │ └── 用户初始化代码 + │ + ▼ +调度器运行 + │ └── FinSH 线程启动 + │ └── msh > 提示符出现 +``` + +>`applications/main.c` 中的 `main()` 函数并不是系统启动的入口,而是 RT-Thread 调度器启动后,在 main 线程中执行的用户入口函数。 +>真正的系统启动入口在 `components.c` 的 `rtthread_startup()` 中。 +## RT-Thread Setting 的配置 +### 配置界面概览 +RT-Thread Settings 是 RT-Thread Studio 提供的图形化配置工具,它本质上是 `rtconfig.h` 的可视化编辑器。双击工程资源管理器中的 **RT-Thread Settings** 节点,即可打开配置界面。 +![image-20210127181544774](https://www.rt-thread.org/document/site/development-tools/rtthread-studio/figures/image-20210127181325681.png) + +| 标签页 | 功能 | 对应关系 | +|--------|------|---------| +| **内核** | 内核功能裁剪:线程、信号量、内存管理、IPC 等 | 对应 `rtconfig.h` 中的 `RT_USING_*` 宏 | +| **组件** | 组件启用/禁用:FinSH、DFS、网络、驱动框架等 | 对应 `rtconfig.h` 中的组件配置宏 | +| **软件包** | 在线搜索、安装、管理 RT-Thread 软件包 | 对应 `packages/` 目录和 `rtconfig.h` 中的软件包宏 | +| **硬件** | 芯片和外设的硬件配置 | 对应 `board.h` 和 HAL 库配置 | +### 内核配置 +**内核配置决定了 RT-Thread 内核的基本行为。** +#### 内核基础配置 + +| 配置项 | 说明 | 建议值 | 备注 | +| ------------------------------ | ------------- | ---- | ------------------------- | +| 系统 Tick 频率(RT_TICK_PER_SECOND) | 每秒的系统节拍数 | 1000 | 值为 1000 时,每个 Tick = 1ms | +| 线程优先级数量 | 可用的优先级级别数 | 32 | 数值越小优先级越高,0 为最高优先级 | +| 最大线程名称长度 | 线程名称字符串的最大长度 | 8 | 包括 `\0` 结束符 | +| 空闲线程栈大小 | 空闲线程的栈空间大小 | 256 | 空闲线程只做简单操作,256 字节足够 | +| 主线程栈大小 | main 线程的栈空间大小 | 2048 | 如果 main 函数中创建了大量局部变量,需要增大 | +#### Tick +系统 Tick 是 RT-Thread 内核的心跳。以 `RT_TICK_PER_SECOND = 1000` 为例: + +``` +时间单位换算: + 1 Tick = 1ms = 1/1000 秒 + 1 秒 = 1000 Tick + +API 中的延时单位: + rt_thread_mdelay(500) → 延时 500ms(500 Tick) + rt_thread_sleep(10) → 休眠 10 Tick(10ms) + rt_tick_get() → 获取从启动到现在的 Tick 计数 +``` + +> Tick 频率越高,系统实时性越好,但上下文切换的开销也越大。 +#### 内存管理配置 + +| 配置项 | 说明 | 建议值 | +| -------- | ---------------------------------- | ------------ | +| 使用动态内存管理 | 启用 `rt_malloc`/`rt_free` 等动态内存分配函数 | ✅ 启用 | +| 堆大小 | 动态内存堆的总大小 | 16384(16KB) | +| 内存管理算法 | 小内存管理算法(small memory)或 SLAB 算法 | small memory | +> **堆大小设置建议**:基于芯片创建工程时,默认堆大小通常为 16KB。如果后续需要启用网络功能(如 LWIP),需要增加到 64KB 以上。可以在 `board.c` 的 `rt_hw_board_init()` 函数中找到 `HEAP_BEGIN` 和 `HEAP_END` 的定义。 +#### 线程间通信(IPC)配置 + +| 配置项 | 说明 | 建议值 | +|--------|------|--------| +| 使用信号量 | 线程间同步机制 | ✅ 启用 | +| 使用互斥量 | 保护共享资源 | ✅ 启用 | +| 使用事件集 | 一对多的事件通知机制 | ✅ 启用 | +| 使用邮箱 | 线程间消息传递 | ✅ 启用 | +| 使用消息队列 | 线程间流式数据传递 | 按需启用 | +### 组件配置 +#### FinSH 命令行组件 +FinSH 是 RT-Thread 的命令行组件,提供了类似 Linux Shell 的交互式调试接口。这是开发过程中**最重要的调试工具**之一。 + +| 配置项 | 说明 | 建议值 | +|--------|------|--------| +| 启用 FinSH | 开启 FinSH 命令行组件 | ✅ 启用 | +| FinSH 线程优先级 | FinSH 线程的优先级 | 21 | +| FinSH 线程栈大小 | FinSH 线程的栈空间 | 4096 | +| FinSH 历史命令数量 | 可通过 ↑↓ 键回显的历史命令数 | 5 | +| 使用 msh 模式 | 使用增强的 msh 模式(支持自动补全) | ✅ 启用 | +| 使用串口方式 | 通过串口与 FinSH 交互 | ✅ 启用 | +| 串口设备名称 | FinSH 使用的串口设备名 | uart1 | +> 启用 FinSH 后,可以在串口终端中输入 `list_device`(查看设备列表)、`ps`(查看线程状态)、`free`(查看内存使用)、`help`(查看所有命令)等命令,实时监控系统运行状态。 + +#### 设备驱动框架 +切换到"组件 → 驱动"标签页,这里是驱动框架的配置核心。驱动框架采用"按需启用"原则——**需要什么外设,就启用什么驱动框架**。 + +| 驱动框架 | 配置项 | 说明 | 本章建议 | +|---------|--------|------|---------| +| GPIO | 使用 GPIO 驱动 | PIN 设备驱动框架 | ✅ 启用 | +| UART | 使用 UART 驱动 | 串口设备驱动框架 | ✅ 启用 | +| SPI | 使用 SPI 驱动 | SPI 总线设备驱动框架 | 后续章节按需启用 | +| I2C | 使用 I2C 驱动 | I2C 总线设备驱动框架 | 后续章节按需启用 | +| ADC | 使用 ADC 驱动 | 模数转换器驱动框架 | 后续章节按需启用 | +| DAC | 使用 DAC 驱动 | 数模转换器驱动框架 | 后续章节按需启用 | +| RTC | 使用 RTC 驱动 | 实时时钟驱动框架 | 后续章节按需启用 | +| WDT | 使用 WDT 驱动 | 看门狗驱动框架 | 后续章节按需启用 | +| HWTIMER | 使用 HWTIMER 驱动 | 硬件定时器驱动框架 | 后续章节按需启用 | +| PWM | 使用 PWM 驱动 | 脉冲宽度调制驱动框架 | 后续章节按需启用 | +| CAN | 使用 CAN 驱动 | CAN 总线驱动框架 | 后续章节按需启用 | +| SDIO | 使用 SDIO 驱动 | SDIO 接口驱动框架 | 后续章节按需启用 | +#### 其他常用组件 + +| 组件 | 说明 | 本章建议 | +|------|------|---------| +| DFS | 虚拟文件系统 | 不启用(第十八章启用) | +| 网络 | 网络协议栈(LWIP) | 不启用(第二十二章启用) | +| ulog | 日志组件 | 可选启用 | +| utest | 单元测试框架 | 不启用 | +### 软件包管理 +#### 软件包搜索与安装 +**操作步骤:** +1. 在软件包标签页的搜索框中输入关键词 +2. 浏览搜索结果,点击软件包名称查看详细信息 +3. 点击 **安装** 按钮,RT-Thread Studio 自动下载并集成到工程中 +安装完成后,软件包源码会出现在 `packages/` 目录下,同时 `rtconfig.h` 中会自动添加对应的配置宏。 +### 配置保存与生效 +当在 RT-Thread Settings 中修改配置后,点击 **保存** 按钮,RT-Thread Studio 会执行以下操作: + +``` +点击"保存" + │ + ├── 1. 更新 rtconfig.h 中的宏定义 + │ └── 例如:启用 GPIO 驱动 → #define RT_USING_PIN + │ + ├── 2. 更新 packages/Kconfig 和 packages/ 目录 + │ └── 例如:安装 multibutton → 下载源码到 packages/multibutton-xxx/ + │ + └── 3. 触发 SCons 重新生成构建配置 + └── 更新编译依赖关系 +``` + +## board.c/h 的配置(包括引脚的初始化,配合 CubeMX) +### board.h — 引脚定义与宏配置 +`board.h` 是板级头文件,主要负责三件事:**引脚宏定义**、**外设宏定义**和**板级参数配置**。 +#### 引脚宏定义(使用 GET_PIN 宏) +RT-Thread 使用 `GET_PIN(port, pin)` 宏将物理引脚编号转换为内部编号,实现硬件无关的引脚访问: + +```c +/* 语法:GET_PIN(端口字母, 引脚编号) */ +#define LED_R_PIN GET_PIN(F, 9) /* PF9 → 红色 LED */ +#define LED_B_PIN GET_PIN(F, 10) /* PF10 → 蓝色 LED */ +#define KEY0_PIN GET_PIN(C, 0) /* PC0 → 用户按键 KEY0 */ +#define KEY1_PIN GET_PIN(C, 1) /* PC1 → 用户按键 KEY1 */ +#define WK_UP_PIN GET_PIN(A, 0) /* PA0 → 唤醒按键 */ +#define BEEP_PIN GET_PIN(B, 2) /* PB2 → 蜂鸣器 */ +``` + +#### GET_PIN 宏的原理 +`GET_PIN` 宏将 GPIO 端口和引脚编号组合成一个 16 位的整数: + +```c +#define GET_PIN(PORT, PIN) ((uint16_t)(((uint16_t)(PORT - 'A') & 0x0F) << 8) | ((uint16_t)PIN & 0xFF)) + +/* 编码公式: + * 高 8 位:端口编号(PORT - 'A'),取值 0~15(对应 GPIOA~GPIOP) + * 低 8 位:引脚编号,取值 0~15 + * + * 示例: + * GET_PIN(F, 9) → ((5) << 8) | 9 → (0x0500) | 0x0009 → 0x0509 + * GET_PIN(A, 0) → ((0) << 8) | 0 → (0x0000) | 0x0000 → 0x0000 + * GET_PIN(B, 2) → ((1) << 8) | 2 → (0x0100) | 0x0002 → 0x0102 + */ +``` + +#### 星火 1 号完整引脚定义 +基于星火 1 号原理图,以下是 `board.h` 中推荐添加的完整引脚定义(按功能分组,便于维护): + +```c +/* ==================== 星火 1 号 board.h 引脚定义 ==================== */ + +/* --- LED 指示灯 --- */ +#define LED_R_PIN GET_PIN(F, 9) /* 红色 LED(低电平亮) */ +#define LED_B_PIN GET_PIN(F, 10) /* 蓝色 LED(低电平亮) */ + +/* --- 按键 --- */ +#define KEY0_PIN GET_PIN(C, 0) /* 用户按键 KEY0(按下低电平) */ +#define KEY1_PIN GET_PIN(C, 1) /* 用户按键 KEY1(按下低电平) */ +#define WK_UP_PIN GET_PIN(A, 0) /* 唤醒按键 WK_UP(按下高电平) */ + +/* --- 蜂鸣器 --- */ +#define BEEP_PIN GET_PIN(B, 2) /* 蜂鸣器 */ + +/* --- 串口 --- */ +#define CONSOLE_TX GET_PIN(A, 9) /* UART1 TX(调试控制台) */ +#define CONSOLE_RX GET_PIN(A, 10) /* UART1 RX(调试控制台) */ +#define RS485_TX GET_PIN(D, 5) /* UART2 TX(RS485) */ +#define RS485_RX GET_PIN(D, 6) /* UART2 RX(RS485) */ +#define RS485_RTS GET_PIN(D, 4) /* RS485 方向控制 */ + +/* --- I2C 总线 --- */ +#define I2C1_SCL GET_PIN(B, 6) /* I2C1 SCL(AHT10/AP3216C) */ +#define I2C1_SDA GET_PIN(B, 7) /* I2C1 SDA */ +#define I2C2_SCL GET_PIN(B, 10) /* I2C2 SCL(ES8388 音频) */ +#define I2C2_SDA GET_PIN(B, 11) /* I2C2 SDA */ + +/* --- SPI 总线 --- */ +#define SPI1_SCK GET_PIN(A, 5) /* SPI1 SCK(ICM20608) */ +#define SPI1_MISO GET_PIN(A, 6) /* SPI1 MISO */ +#define SPI1_MOSI GET_PIN(A, 7) /* SPI1 MOSI */ +#define SPI1_CS GET_PIN(A, 4) /* SPI1 NSS(ICM20608 片选) */ +#define SPI2_SCK GET_PIN(B, 13) /* SPI2 SCK(W25Q64 / RW007) */ +#define SPI2_MISO GET_PIN(B, 14) /* SPI2 MISO */ +#define SPI2_MOSI GET_PIN(B, 15) /* SPI2 MOSI */ +#define SPI2_FLASH_CS GET_PIN(B, 12) /* W25Q64 片选 */ +#define SPI2_WIFI_CS GET_PIN(C, 4) /* RW007 片选 */ +#define SPI3_SCK GET_PIN(B, 3) /* SPI3 SCK(ST7789 LCD) */ +#define SPI3_MOSI GET_PIN(B, 5) /* SPI3 MOSI */ +#define SPI3_LCD_CS GET_PIN(A, 15) /* ST7789 片选 */ +#define SPI3_LCD_DC GET_PIN(B, 4) /* ST7789 数据/命令 */ +#define SPI3_LCD_RST GET_PIN(A, 8) /* ST7789 复位 */ +#define SPI3_LCD_BLK GET_PIN(C, 8) /* ST7789 背光 */ + +/* --- CAN 总线 --- */ +#define CAN1_TX GET_PIN(D, 1) /* CAN1 TX */ +#define CAN1_RX GET_PIN(D, 0) /* CAN1 RX */ + +/* --- USB --- */ +#define USB_DP GET_PIN(A, 12) /* USB D+ */ +#define USB_DM GET_PIN(A, 11) /* USB D- */ + +/* --- SDIO --- */ +#define SDIO_D0 GET_PIN(C, 8) /* SDIO D0(TF 卡) */ +#define SDIO_D1 GET_PIN(C, 9) /* SDIO D1 */ +#define SDIO_D2 GET_PIN(C, 10) /* SDIO D2 */ +#define SDIO_D3 GET_PIN(C, 11) /* SDIO D3 */ +#define SDIO_CLK GET_PIN(C, 12) /* SDIO CLK */ +#define SDIO_CMD GET_PIN(D, 2) /* SDIO CMD */ + +/* --- I2S 音频 --- */ +#define I2S2_WS GET_PIN(B, 12) /* I2S2 WS(ES8388) */ +#define I2S2_CK GET_PIN(B, 13) /* I2S2 CK */ +#define I2S2_SD GET_PIN(C, 3) /* I2S2 SD */ +#define I2S2_MCK GET_PIN(C, 6) /* I2S2 MCK */ + +/* --- 红外 --- */ +#define IR_IN_PIN GET_PIN(A, 1) /* 红外接收 */ + +/* --- 可编程彩灯 --- */ +#define SK6805_PIN GET_PIN(F, 11) /* SK6805 数据引脚 */ +``` + +### board.c — 时钟配置与板级初始化 +`board.c` 是板级初始化文件,负责系统时钟配置、内存初始化、外设初始化和控制台配置。 +#### 时钟配置函数 +`board.c` 中的时钟配置核心是 `SystemClock_Config()` 函数,它通过 HAL 库配置 STM32 的时钟树: + +```c +/** + * 系统时钟配置 + * 目标:HCLK = 168MHz, PCLK1 = 42MHz, PCLK2 = 84MHz + */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + + /* 1. 配置 HSE(外部高速晶振) */ + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; /* 启用 HSE */ + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; /* 启用 PLL */ + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* PLL 时钟源 = HSE */ + RCC_OscInitStruct.PLL.PLLM = 8; /* 分频因子 M = 8 */ + RCC_OscInitStruct.PLL.PLLN = 336; /* 倍频因子 N = 336 */ + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; /* 分频因子 P = 2 */ + RCC_OscInitStruct.PLL.PLLQ = 7; /* 分频因子 Q = 7 */ + HAL_RCC_OscConfig(&RCC_OscInitStruct); + + /* 2. 配置系统时钟源、AHB/APB 总线和 Flash 等待周期 */ + RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | + RCC_CLOCKTYPE_SYSCLK | + RCC_CLOCKTYPE_PCLK1 | + RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; /* 系统时钟 = PLL */ + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; /* AHB = SYSCLK / 1 */ + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; /* APB1 = AHB / 4 */ + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; /* APB2 = AHB / 2 */ + HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5); +} +``` + +**STM32F407 时钟树计算:** + +``` +HSE = 8MHz(外部晶振) + │ + ├── PLLM = 8 → PLL 输入 = 8MHz / 8 = 1MHz + │ + ├── PLLN = 336 → VCO 输出 = 1MHz × 336 = 336MHz + │ + ├── PLLP = 2 → SYSCLK = 336MHz / 2 = 168MHz + │ + ├── AHB (HCLK) = SYSCLK / 1 = 168MHz + │ ├── APB1 (PCLK1) = 168MHz / 4 = 42MHz(低速外设:UART2-5, I2C, SPI2-3) + │ └── APB2 (PCLK2) = 168MHz / 2 = 84MHz(高速外设:UART1, SPI1, ADC) + │ + └── PLLQ = 7 → 48MHz(USB OTG / SDIO 时钟) +``` + +#### 板级初始化函数 +`rt_hw_board_init()` 是 RT-Thread 启动时调用的板级初始化函数,位于 `board.c` 中: + +```c +void rt_hw_board_init(void) +{ + /* 1. HAL 库初始化 */ + HAL_Init(); + + /* 2. 系统时钟配置 */ + SystemClock_Config(); + + /* 3. 系统 Tick 定时器初始化(SysTick 或专用定时器) */ + rt_hw_systick_init(); + + /* 4. 板级硬件初始化 */ + /* - 引脚时钟使能 */ + /* - 外设初始化 */ + /* - 控制台串口初始化 */ + + /* 5. 内存堆初始化 */ +#if defined(RT_USING_HEAP) + rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END); +#endif + + /* 6. 中断向量表配置 */ + /* 7. 控制台设备注册 */ + rt_console_set_device(RT_CONSOLE_DEVICE_NAME); +} +``` + +#### 内存堆配置 +在 `board.c` 中,`HEAP_BEGIN` 和 `HEAP_END` 定义了动态内存堆的起止地址: + +```c +#define HEAP_BEGIN ((void *)&__bss_end) /* 堆起始地址 = BSS 段末尾 */ +#define HEAP_END ((void *)(0x20000000 + 0x20000)) /* 堆结束地址 = SRAM 末尾 */ +``` + +### 配合 STM32CubeMX 进行引脚配置 +#### CubeMX 在 RT-Thread 开发中的角色 +在 RT-Thread 的开发流程中,CubeMX 负责**引脚分配和硬件配置**,RT-Thread Settings 负责**软件配置**。两者分工明确: + +| 配置工具 | 负责内容 | 输出文件 | +|---------|---------|---------| +| CubeMX | 引脚复用、时钟树、外设参数(波特率、SPI 模式等) | `CubeMX_Config.ioc` → `main.c` / `stm32f4xx_hal_msp.c` | +| RT-Thread Settings | 内核裁剪、组件启用、驱动框架、软件包 | `rtconfig.h` | + +#### 打开 CubeMX 工程 +在 RT-Thread Studio 中直接打开 CubeMX: +1. 在项目资源管理器中,展开 `board/CubeMX_Config/` 目录 +2. 双击 `CubeMX_Config.ioc` 文件 +3. RT-Thread Studio 会自动启动 STM32CubeMX 并加载工程 +#### 引脚配置步骤 +以配置 LED 引脚(PF9、PF10)为例: +**第一步:进入引脚配置界面** +在 CubeMX 主界面中,点击 **Pinout & Configuration** 标签页,在右侧的芯片引脚图中找到目标引脚。 +**第二步:配置引脚功能** +点击 PF9 引脚,在弹出的功能列表中选择 **GPIO_Output**。同样的方法配置 PF10。 +**第三步:配置引脚属性** +在左侧的 **System Core → GPIO** 列表中,找到 PF9 和 PF10,配置以下属性: + +| 属性 | 值 | 说明 | +|------|-----|------| +| GPIO output level | High | 初始输出高电平(LED 熄灭) | +| GPIO mode | Output Push Pull | 推挽输出模式 | +| GPIO Pull-up/Pull-down | No pull-up and no pull-down | 无上下拉 | +| Maximum output speed | Low | 低速输出(LED 控制不需要高速) | +| User Label | LED_R / LED_B | 自定义标签(便于代码可读性) | + +**第四步:配置时钟树** +切换到 **Clock Configuration** 标签页,确认时钟树配置正确: + +``` +HSE: 8 MHz → PLLM:/8 → PLLN:×336 → PLLP:/2 → SYSCLK: 168 MHz + → AHB: 168 MHz + → APB1: 42 MHz + → APB2: 84 MHz +``` + +**第五步:生成代码** +点击工具栏的 **GENERATE CODE** 按钮(齿轮图标),CubeMX 会重新生成 `board/CubeMX_Config/Src/main.c` 和 `stm32f4xx_hal_msp.c`。 +**第六步:同步到 RT-Thread 工程** +CubeMX 生成代码后,RT-Thread Studio 会自动检测到变更并提示是否同步。点击 **是** 完成同步。 + +> **注意**:CubeMX 生成的 `main.c` 中的 `main()` 函数和 `SystemClock_Config()` 函数会被 RT-Thread 的 `board.c` 引用和调用。不要直接修改 CubeMX 生成的文件中的 `main()` 函数,所有用户代码都应该写在 `applications/main.c` 中。 +## 相关框架的说明(RT-Thread I/O 设备模型) +### 为什么需要设备模型? +在传统的裸机开发中,操作外设的方式是直接调用 HAL 库函数: + +```c +/* 裸机方式:操作 GPIO */ +HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_RESET); /* 点亮 LED */ +HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_SET); /* 熄灭 LED */ + +/* 裸机方式:发送串口数据 */ +HAL_UART_Transmit(&huart1, data, len, 1000); /* 阻塞发送 */ +``` + +这种方式的**痛点**: +1. **代码不通用**:换一个引脚、换一个芯片型号,代码就要大量修改 +2. **接口不统一**:GPIO 用 `HAL_GPIO_WritePin`,UART 用 `HAL_UART_Transmit`,SPI 用 `HAL_SPI_Transmit`——每个外设的 API 都不同 +3. **缺乏抽象**:上层应用直接依赖底层硬件,耦合度高,移植困难 +4. **并发访问困难**:多个线程同时操作同一个外设时,需要自己实现互斥机制 +RT-Thread 的 I/O 设备模型正是为了解决这些问题而设计的。它提供了一套**统一的设备访问接口**,让开发者使用相同的 API 操作不同类型的外设,同时由框架层负责并发控制和资源管理。 +### 设备模型的四层架构 +RT-Thread 的 I/O 设备模型采用分层架构设计,从应用层到硬件层共分为四层: +![I/O 设备模型框架](https://www.rt-thread.org/document/site/rt-thread-version/rt-thread-standard/programming-manual/device/figures/io-dev.png) + +**各层职责:** + +| 层次 | 职责 | 示例 | +|------|------|------| +| 应用层 | 使用统一的设备接口操作设备 | `rt_device_write(dev, data, len)` | +| 驱动框架层 | 定义设备类型、提供类型专属的扩展接口 | `rt_pin_write(pin, value)` | +| 设备驱动层 | 实现框架层定义的接口,调用 HAL 库操作硬件 | `stm32_pin_write()` → `HAL_GPIO_WritePin()` | +| 硬件层 | 芯片厂商提供的底层库 | STM32 HAL 库 / CMSIS | +### 核心数据结构:rt_device +`rt_device` 是 RT-Thread 设备模型的核心数据结构,所有设备对象都必须包含它(通常通过继承的方式): + +```c +struct rt_device +{ + struct rt_object parent; /* 内核对象基类(包含名称、类型等) */ + enum rt_device_class_type type; /* 设备类型 */ + rt_uint16_t flag; /* 设备标志(读写/打开/关闭方式) */ + rt_uint16_t open_flag; /* 设备打开标志(由 open 函数设置) */ + rt_uint8_t ref_count; /* 引用计数(多线程安全) */ + rt_uint8_t device_id; /* 设备 ID(0-255) */ + + /* 设备操作接口(函数指针) */ + rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size); /* 接收回调 */ + rt_err_t (*tx_complete)(rt_device_t dev, void *buffer); /* 发送完成回调 */ + + /* 标准操作接口 */ + rt_err_t (*init) (rt_device_t dev); /* 初始化 */ + rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag); /* 打开 */ + rt_err_t (*close) (rt_device_t dev); /* 关闭 */ + rt_size_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size); /* 读取 */ + rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size); /* 写入 */ + rt_err_t (*control)(rt_device_t dev, int cmd, void *args); /* 控制 */ + + void *user_data; /* 用户自定义数据 */ +}; +``` + +- **关键字段说明:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `parent` | `rt_object` | 继承自内核对象基类,包含设备名称(`name`)和内核对象类型 | +| `type` | `enum` | 设备类型(字符设备、块设备、网络设备等,共 17 种) | +| `flag` | `rt_uint16_t` | 设备读写标志:`RT_DEVICE_FLAG_RDONLY`(只读)、`RT_DEVICE_FLAG_WRONLY`(只写)、`RT_DEVICE_FLAG_RDWR`(读写) | +| `ref_count` | `rt_uint8_t` | 引用计数:每调用一次 `rt_device_open()`,计数 +1;每调用一次 `rt_device_close()`,计数 -1。计数为 0 时才能安全关闭设备 | +| `init` / `open` / `close` | 函数指针 | 设备生命周期管理接口 | +| `read` / `write` | 函数指针 | 数据收发接口 | +| `control` | 函数指针 | 设备控制接口(配置参数、设置模式等) | +### 面向对象设计:设备继承链 +RT-Thread 在 C 语言中实现了面向对象的继承机制。以 PIN 设备为例: + +```c +/* 1. 设备模型基类:rt_device */ +struct rt_device +{ + struct rt_object parent; + enum rt_device_class_type type; + /* ... 其他基类成员 */ +}; + +/* 2. PIN 设备子类:继承 rt_device */ +struct rt_device_pin +{ + struct rt_device parent; /* 继承 rt_device */ + const struct rt_pin_ops *ops; /* 引脚操作接口 */ + /* ... PIN 设备专属成员 */ +}; + +/* 3. PIN 设备操作接口 */ +struct rt_pin_ops +{ + void (*pin_mode)(struct rt_device *device, rt_base_t pin, rt_base_t mode); + void (*pin_write)(struct rt_device *device, rt_base_t pin, rt_base_t value); + int (*pin_read)(struct rt_device *device, rt_base_t pin); + rt_err_t (*pin_attach_irq)(struct rt_device *device, rt_int32_t pin, + rt_uint32_t mode, void (*hdr)(void *args), void *args); + rt_err_t (*pin_detach_irq)(struct rt_device *device, rt_int32_t pin); + rt_err_t (*pin_irq_enable)(struct rt_device *device, rt_base_t pin, rt_uint32_t enabled); +}; +``` + +**继承链的访问方式:** + +```c +/* 父类指针 → 子类对象 */ +struct rt_device_pin *pin_dev = (struct rt_device_pin *)rt_device_find("pin"); + +/* 子类 → 父类 */ +rt_device_t dev = &pin_dev->parent; + +/* 通过父类指针调用子类实现的接口 */ +rt_device_write(dev, 0, &pin_value, sizeof(pin_value)); /* 多态调用 */ +``` + +这种设计使得所有的设备操作都可以通过 `rt_device` 基类指针进行统一调用,而具体的实现由子类完成。这就是 C 语言中的**多态**。 +### 设备类型分类 +RT-Thread 定义了 17 种设备类型,覆盖了嵌入式开发中几乎所有常见的外设: + +| 类型常量 | 数值 | 设备类型 | 说明 | +|---------|------|---------|------| +| `RT_Device_Class_Char` | 0 | 字符设备 | 串口、控制台等流式设备 | +| `RT_Device_Class_Block` | 1 | 块设备 | SD 卡、Flash 等块存储设备 | +| `RT_Device_Class_NetIf` | 2 | 网络接口 | 以太网、Wi-Fi 网络接口 | +| `RT_Device_Class_MTD` | 3 | 内存技术设备 | NOR/NAND Flash 底层设备 | +| `RT_Device_Class_CAN` | 4 | CAN 设备 | CAN 总线控制器 | +| `RT_Device_Class_RTC` | 5 | RTC 设备 | 实时时钟 | +| `RT_Device_Class_Sound` | 6 | 声音设备 | 音频播放/录制 | +| `RT_Device_Class_Graphic` | 7 | 图形设备 | LCD 显示、GPU | +| `RT_Device_Class_I2CBUS` | 8 | I2C 总线 | I2C 总线控制器 | +| `RT_Device_Class_SPIBUS` | 9 | SPI 总线 | SPI 总线控制器 | +| `RT_Device_Class_SPIDevice` | 10 | SPI 设备 | 挂载在 SPI 总线上的从设备 | +| `RT_Device_Class_SDIO` | 11 | SDIO 设备 | SDIO 接口控制器 | +| `RT_Device_Class_PM` | 12 | 电源管理 | 电源管理框架 | +| `RT_Device_Class_Pipe` | 13 | 管道设备 | 线程间管道通信 | +| `RT_Device_Class_PHY` | 14 | PHY 设备 | 以太网 PHY 芯片 | +| `RT_Device_Class_Miscellaneous` | 15 | 杂项设备 | PIN 设备、ADC、DAC、PWM 等 | +| `RT_Device_Class_Timer` | 16 | 定时器设备 | 硬件定时器 | +> **注意**:PIN 设备(GPIO)、ADC 设备、DAC 设备、PWM 设备、WDT 设备等都属于 `RT_Device_Class_Miscellaneous`(杂项设备)类型。这是因为它们虽然功能不同,但在设备模型中共享相同的注册和访问模式。 +### 统一设备访问接口 +RT-Thread 设备模型的核心价值在于**统一接口**。无论是串口、SPI Flash 还是传感器,上层应用都使用相同的 API 进行访问: +#### 设备查找:rt_device_find() +![简单 I/O 设备使用序列图](https://www.rt-thread.org/document/site/rt-thread-version/rt-thread-standard/programming-manual/device/figures/io-call.png) + +```c +/** + * 通过设备名称查找设备对象 + * @param name 设备名称(如 "pin", "uart1", "spi10") + * @return 设备句柄(rt_device_t),查找失败返回 RT_NULL + */ +rt_device_t rt_device_find(const char *name); +``` + +**使用示例:** + +```c +rt_device_t dev; + +/* 查找 PIN 设备 */ +dev = rt_device_find("pin"); +if (dev == RT_NULL) +{ + rt_kprintf("PIN device not found!\n"); + return -RT_ERROR; +} + +/* 查找 UART 设备 */ +dev = rt_device_find("uart1"); +``` + +#### 设备打开/关闭:rt_device_open() / rt_device_close() + +```c +/** + * 打开设备(增加引用计数) + * @param dev 设备句柄 + * @param oflag 打开标志(RT_DEVICE_FLAG_RDONLY / WRONLY / RDWR) + * @return RT_EOK 成功,其他值失败 + */ +rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflag); + +/** + * 关闭设备(减少引用计数) + * @param dev 设备句柄 + * @return RT_EOK 成功,其他值失败 + */ +rt_err_t rt_device_close(rt_device_t dev); +``` + +**使用示例:** + +```c +rt_device_t uart_dev = rt_device_find("uart1"); + +/* 以读写模式打开串口 */ +rt_device_open(uart_dev, RT_DEVICE_FLAG_RDWR); + +/* ... 使用设备 ... */ + +/* 使用完毕后关闭 */ +rt_device_close(uart_dev); +``` + +#### 数据读写:rt_device_read() / rt_device_write() + +```c +/** + * 从设备读取数据 + * @param dev 设备句柄 + * @param pos 读取位置(块设备使用,字符设备通常为 0) + * @param buffer 数据缓冲区 + * @param size 读取大小 + * @return 实际读取的字节数 + */ +rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size); + +/** + * 向设备写入数据 + * @param dev 设备句柄 + * @param pos 写入位置 + * @param buffer 数据缓冲区 + * @param size 写入大小 + * @return 实际写入的字节数 + */ +rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size); +``` + +#### 设备控制:rt_device_control() + +```c +/** + * 向设备发送控制命令 + * @param dev 设备句柄 + * @param cmd 控制命令 + * @param args 命令参数 + * @return RT_EOK 成功,其他值失败 + */ +rt_err_t rt_device_control(rt_device_t dev, int cmd, void *args); +``` + +这是设备模型中**最灵活**的接口。不同设备类型有不同的控制命令,例如: + +| 设备类型 | 控制命令示例 | 说明 | +|---------|------------|------| +| UART | `RT_DEVICE_CTRL_CONFIG` | 配置波特率、数据位等 | +| PIN | `RT_DEVICE_CTRL_CONFIG` | 配置引脚模式 | +| RTC | `RT_DEVICE_CTRL_RTC_GET_TIME` | 获取当前时间 | +| WDT | `RT_DEVICE_CTRL_WDT_KEEPALIVE` | 喂狗 | +| PWM | `RT_DEVICE_CTRL_PWM_SET_PERIOD` | 设置 PWM 周期 | +### PIN 设备框架详解 +PIN 设备是 RT-Thread 中最基础的设备类型,负责 GPIO 的管理和操作。它使用的是 **PIN 设备框架专属的 API**,而非通用的 `rt_device_read/write`: +#### PIN 设备 API + +```c +/* 设置引脚模式 */ +void rt_pin_mode(rt_base_t pin, rt_base_t mode); + +/* 设置引脚输出电平 */ +void rt_pin_write(rt_base_t pin, rt_base_t value); + +/* 读取引脚输入电平 */ +int rt_pin_read(rt_base_t pin); + +/* 绑定引脚中断回调 */ +rt_err_t rt_pin_attach_irq(rt_int32_t pin, rt_uint32_t mode, + void (*hdr)(void *args), void *args); + +/* 解绑引脚中断回调 */ +rt_err_t rt_pin_detach_irq(rt_int32_t pin); + +/* 使能/禁止引脚中断 */ +rt_err_t rt_pin_irq_enable(rt_base_t pin, rt_uint32_t enabled); +``` + +#### 引脚模式定义 + +| 模式常量 | 说明 | 应用场景 | +|---------|------|---------| +| `PIN_MODE_OUTPUT` | 推挽输出 | 控制 LED、蜂鸣器等输出设备 | +| `PIN_MODE_OUTPUT_OD` | 开漏输出 | I2C、单总线等需要外部上拉的场景 | +| `PIN_MODE_INPUT` | 浮空输入 | 读取外部数字信号 | +| `PIN_MODE_INPUT_PULLUP` | 上拉输入 | 按键检测(默认高电平,按下低电平) | +| `PIN_MODE_INPUT_PULLDOWN` | 下拉输入 | 按键检测(默认低电平,按下高电平) | +| `PIN_MODE_OUTPUT_PP` | 推挽输出(别名) | 同 `PIN_MODE_OUTPUT` | +#### 引脚电平定义 + +| 电平常量 | 说明 | +|---------|------| +| `PIN_LOW` | 低电平(0V) | +| `PIN_HIGH` | 高电平(3.3V) | +#### PIN 设备使用示例 + +```c +#include +#include + +/* 使用 board.h 中定义的引脚宏 */ +#define LED_PIN GET_PIN(F, 9) /* 红色 LED */ +#define KEY_PIN GET_PIN(C, 0) /* 用户按键 KEY0 */ + +/* 按键回调函数 */ +static void key_callback(void *args) +{ + rt_kprintf("KEY0 pressed!\n"); +} + +int pin_demo(void) +{ + /* 1. 配置 LED 引脚为输出模式 */ + rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT); + + /* 2. 点亮 LED */ + rt_pin_write(LED_PIN, PIN_LOW); /* 低电平点亮 */ + + /* 3. 配置按键引脚为上拉输入模式 */ + rt_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP); + + /* 4. 绑定按键中断(下降沿触发) */ + rt_pin_attach_irq(KEY_PIN, PIN_IRQ_MODE_FALLING, + key_callback, RT_NULL); + + /* 5. 使能按键中断 */ + rt_pin_irq_enable(KEY_PIN, PIN_IRQ_ENABLE); + + return RT_EOK; +} +``` + +### UART 设备框架详解 +UART(串口)设备是 RT-Thread 中使用最频繁的通信设备之一,FinSH 控制台就是通过 UART 设备实现的。 +#### UART 设备配置 +在 RT-Thread Settings 中启用 UART 驱动后,还需要在 `board.h` 中配置串口参数: + +```c +/* board.h 中的 UART 配置 */ +#define BSP_USING_UART1 /* 启用 UART1 */ +#define BSP_UART1_TX_PIN "PA9" /* UART1 TX 引脚 */ +#define BSP_UART1_RX_PIN "PA10" /* UART1 RX 引脚 */ + +#define BSP_USING_UART2 /* 启用 UART2 */ +#define BSP_UART2_TX_PIN "PD5" /* UART2 TX 引脚 */ +#define BSP_UART2_RX_PIN "PD6" /* UART2 RX 引脚 */ +``` + +#### UART 设备访问模式 +UART 设备支持三种访问模式: + +| 模式 | 说明 | 适用场景 | +|------|------|---------| +| 轮询模式 | CPU 主动查询数据状态 | 简单测试、非实时场景 | +| 中断模式 | 数据到达时触发中断通知 CPU | 一般数据收发 | +| DMA 模式 | DMA 控制器自动搬运数据,零 CPU 开销 | 大数据量收发 | +#### UART 设备使用示例 + +```c +#include + +/* 串口接收缓冲区 */ +static rt_uint8_t uart_rx_buffer[256]; + +/* 串口接收回调 */ +static rt_err_t uart_rx_callback(rt_device_t dev, rt_size_t size) +{ + /* 读取接收到的数据 */ + rt_device_read(dev, 0, uart_rx_buffer, size); + rt_kprintf("Received %d bytes: %s\n", size, uart_rx_buffer); + return RT_EOK; +} + +int uart_demo(void) +{ + rt_device_t uart_dev; + struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; + + /* 1. 查找串口设备 */ + uart_dev = rt_device_find("uart2"); + if (uart_dev == RT_NULL) + { + rt_kprintf("UART2 not found!\n"); + return -RT_ERROR; + } + + /* 2. 配置串口参数 */ + config.baud_rate = BAUD_RATE_115200; + config.data_bits = DATA_BITS_8; + config.stop_bits = STOP_BITS_1; + config.parity = PARITY_NONE; + rt_device_control(uart_dev, RT_DEVICE_CTRL_CONFIG, &config); + + /* 3. 打开串口 */ + rt_device_open(uart_dev, RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX); + + /* 4. 设置接收回调 */ + rt_device_set_rx_indicate(uart_dev, uart_rx_callback); + + /* 5. 发送数据 */ + rt_device_write(uart_dev, 0, "Hello UART!\r\n", 13); + + return RT_EOK; +} +``` + +### 设备模型的设计思想 + +| 设计思想 | 实现方式 | 实际价值 | +| --------- | ----------------------------------------------- | -------------------- | +| **统一接口** | 所有设备通过 `find/open/read/write/control` 五个 API 访问 | 降低学习成本,一套 API 操作所有外设 | +| **分层架构** | 应用层 → 框架层 → 驱动层 → 硬件层 | 各层职责清晰,修改一层不影响其他层 | +| **面向对象** | `rt_device` 基类 + 子类继承扩展 | 代码复用,扩展方便 | +| **引用计数** | `ref_count` 字段 | 多线程安全,防止设备被意外关闭 | +| **自动初始化** | `INIT_DEVICE_EXPORT` 等宏 | 驱动自动注册,无需手动调用 | + diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828132951.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828132951.png" new file mode 100644 index 0000000000000000000000000000000000000000..06472ad7b8e413fd7e0d83f31aef7656ac08675d Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828132951.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235517.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235517.png" new file mode 100644 index 0000000000000000000000000000000000000000..dd5ad0f5cf2d7c2e8091b78566e176f520a724b7 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235517.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235737.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235737.png" new file mode 100644 index 0000000000000000000000000000000000000000..8e1247360bbfa45064b938e1cd59e7e84df6611f Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235737.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235802.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235802.png" new file mode 100644 index 0000000000000000000000000000000000000000..370fd0472d509cd8f1a5f4de9658a53f9eb1ac35 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235802.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235826.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235826.png" new file mode 100644 index 0000000000000000000000000000000000000000..6d4dd4cd837d47afd571445b0e3186ccc19c47c8 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260828235826.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260829134756.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260829134756.png" new file mode 100644 index 0000000000000000000000000000000000000000..e1bf6115ee08f8d302d9f2faaba955815dfd225d Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260829134756.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260830135550.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260830135550.png" new file mode 100644 index 0000000000000000000000000000000000000000..0fa0c6376a34df2b47c037124539b37f4552b6fd Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/figures/Pasted image 20260830135550.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/\347\254\254\344\270\211\347\253\240 UART \344\270\262\345\217\243\350\256\276\345\244\207\357\274\232\350\275\256\350\257\242\343\200\201\344\270\255\346\226\255\345\222\214 DMA \346\225\260\346\215\256\346\224\266\345\217\221.md" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/\347\254\254\344\270\211\347\253\240 UART \344\270\262\345\217\243\350\256\276\345\244\207\357\274\232\350\275\256\350\257\242\343\200\201\344\270\255\346\226\255\345\222\214 DMA \346\225\260\346\215\256\346\224\266\345\217\221.md" new file mode 100644 index 0000000000000000000000000000000000000000..10b2b0142c9cda1f1c29aaa560463e45bc045976 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\270\211\347\253\240/\347\254\254\344\270\211\347\253\240 UART \344\270\262\345\217\243\350\256\276\345\244\207\357\274\232\350\275\256\350\257\242\343\200\201\344\270\255\346\226\255\345\222\214 DMA \346\225\260\346\215\256\346\224\266\345\217\221.md" @@ -0,0 +1,584 @@ +# 1.  新建工程 +**Step 1:打开 RT-Thread Studio,选择"基于芯片创建工程"** +点击菜单栏 `文件 → 新建 → RT-Thread 项目`,在弹出的对话框中选择 **"基于芯片"** 选项卡。 +**Step 2:配置芯片参数** +![[贡献周/第三章/figures/Pasted image 20260829134756.png]] +**Step 3:点击"完成"创建工程** +工程创建完成后,RT-Thread Studio 会自动生成基础工程框架,包含: + +``` +项目名称/ +├── applications/ # 用户应用代码 +│ └── main.c # 主程序入口 +├── board/ # 板级支持包 +│ ├── board.c # 板级初始化(时钟配置等) +│ ├── board.h # 板级头文件 +│ ├── CubeMX_Config/ # CubeMX 配置文件 +│ └── linker_scripts/ # 链接脚本 +├── libraries/ # 库文件 +│ ├── HAL_Drivers/ # HAL 驱动层 +│ └── Board_Drivers/ # 板级驱动 +├── rt-thread/ # RT-Thread 内核源码 +├── Kconfig # 内核配置菜单 +├── rtconfig.h # RT-Thread 配置头文件 +├── .config # 内核配置文件 +└── SConscript # SCons 构建脚本 +``` + +# 2.  RT-Thread setting的配置 +## 使能DMA +![[贡献周/第三章/figures/Pasted image 20260828132951.png]] + +# 3.  board.c/h 的配置(包括引脚的初始化,配合cubemx) +### board.h 配置 + +`board.h` 文件定义了芯片的 SRAM 和 Flash 地址范围,以及堆内存起止地址: + +```c +#ifndef __BOARD_H__ +#define __BOARD_H__ + +#include +#include +#include "drv_common.h" +#include "drv_gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define STM32_SRAM_SIZE (128) +#define STM32_SRAM_END (0x20000000 + STM32_SRAM_SIZE * 1024) + +#define STM32_FLASH_START_ADRESS ((uint32_t)0x08000000) +#define STM32_FLASH_SIZE (1024 * 1024) +#define STM32_FLASH_END_ADDRESS ((uint32_t)(STM32_FLASH_START_ADRESS + STM32_FLASH_SIZE)) + +#if defined(__ARMCC_VERSION) +extern int Image$$RW_IRAM1$$ZI$$Limit; +#define HEAP_BEGIN ((void *)&Image$$RW_IRAM1$$ZI$$Limit) +#elif __ICCARM__ +#pragma section="CSTACK" +#define HEAP_BEGIN (__segment_end("CSTACK")) +#else +extern int __bss_end; +#define HEAP_BEGIN ((void *)&__bss_end) +#endif + +#define HEAP_END STM32_SRAM_END + +void SystemClock_Config(void); + +#ifdef __cplusplus +} +#endif + +#endif +``` + +| 宏定义 | 值 | 说明 | +| -------------------------- | ------------------ | ------------- | +| `STM32_FLASH_START_ADRESS` | `0x08000000` | 片内 Flash 起始地址 | +| `STM32_FLASH_SIZE` | `1024 * 1024`(1MB) | 片内 Flash 总容量 | +| `STM32_SRAM_SIZE` | `128`(128KB) | 片内 SRAM 大小 | +| `HEAP_BEGIN` | bss 段末尾 | 堆内存起始地址 | +| `HEAP_END` | `STM32_SRAM_END` | 堆内存结束地址 | + +### board.c 配置(系统时钟初始化) + +`board.c` 中的 `SystemClock_Config()` 函数负责配置系统时钟,使用 CubeMX 生成的 HAL 库代码。本例使用 **HSE(外部 8MHz 晶振)+ PLL**,系统时钟 168MHz: + +```c +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; + + __HAL_RCC_PWR_CLK_ENABLE(); + __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); + + /* 配置时钟源:HSE + LSE + LSI */ + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_HSE + |RCC_OSCILLATORTYPE_LSE; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.LSEState = RCC_LSE_ON; + RCC_OscInitStruct.LSIState = RCC_LSI_ON; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLM = 4; + RCC_OscInitStruct.PLL.PLLN = 168; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; + RCC_OscInitStruct.PLL.PLLQ = 7; + HAL_RCC_OscConfig(&RCC_OscInitStruct); + + /* 配置系统时钟树 */ + RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK + |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; + HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5); + + /* 配置 RTC 时钟源为 LSE */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; + PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; + HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct); +} +``` + +#### 时钟树总结: + +|时钟域|时钟源|频率| +|---|---|---| +|HSE|外部 8MHz 晶振|8 MHz| +|PLL|HSE × PLLN / PLLM / PLLP|168 MHz (SYSCLK)| +|HCLK|SYSCLK / 1|168 MHz| +|APB1|HCLK / 4|42 MHz| +|APB2|HCLK / 2|84 MHz| +# 4.  UART 设备框架说明 +### 什么是 UART + +- `UART`(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是嵌入式开发中最常用的外设之一,用于实现设备间的全双工异步串行通信。 +- 它通过两根信号线(TX 发送、RX 接收)完成数据传输,**无需时钟线,双方约定好波特率即可通信。** +- 在 STM32F407 开发板上,**UART1** 通过板载 USB 转串口芯片连接到 PC,是调试和日志输出的主要通道。 +### RT-Thread UART 设备驱动框架 +RT-Thread 将 UART 抽象为标准的 I/O 设备,开发者通过统一的设备操作接口访问串口: + +``` +应用程序 + │ + ▼ +rt_device_find / rt_device_open / rt_device_read / rt_device_write + │ + ▼ +RT-Thread I/O 设备管理层 + │ + ▼ +串口设备驱动(drv_usart.c) + │ + ▼ +HAL 库(STM32 HAL UART) + │ + ▼ +硬件(UART 外设) +``` + +这种分层设计使得上层应用代码与底层硬件解耦,更换 MCU 时只需替换底层驱动,应用层代码无需修改。 +### 三种数据接收模式 + +| 模式 | 原理 | 优点 | 缺点 | 适用场景 | +| ---------- | ------------------------- | -------------- | ------------------ | ------------- | +| **轮询模式** | 主循环中不断调用 `rt_device_read` | 实现简单,无需中断 | 阻塞 CPU,可能丢数据 | 数据量极小、对实时性无要求 | +| **中断模式** | 硬件接收中断触发 `rx_indicate` 回调 | 不阻塞 CPU,响应及时 | 频繁中断,高波特率下 CPU 负载高 | 常规数据收发,数据量适中 | +| **DMA 模式** | DMA 控制器自动搬运数据到内存 | CPU 零负载,适合大数据量 | 依赖 DMA 通道,配置稍复杂 | 高频大数据量传输 | +### RT-Thread 设备驱动 API 总结 + +|API 函数|功能|说明| +|---|---|---| +|`rt_device_find(name)`|根据名称查找设备|返回设备句柄,找不到返回 `RT_NULL`| +|`rt_device_open(dev, flags)`|打开设备|flags 决定读写模式(轮询/中断/DMA)| +|`rt_device_read(dev, pos, buf, size)`|从设备读取数据|返回实际读取的字节数| +|`rt_device_write(dev, pos, buf, size)`|向设备写入数据|返回实际写入的字节数| +|`rt_device_set_rx_indicate(dev, cb)`|注册接收回调|中断/DMA 模式下,数据到达时触发| +|`rt_device_close(dev)`|关闭设备|释放设备资源| +# 5.  例程测试 +### 完整代码 +本例程实现了一个 UART 串口回显(echo)功能,并且通过宏 `USE_MODE` 可以在**轮询、中断、DMA** 三种模式之间切换: + +```c +#include +#include +#include + +#define UART_NAME "uart1" +#define BUF_SIZE 256 + +//切换模式: 0=轮询 1=中断 2=DMA +#define USE_MODE 0 + +static rt_device_t serial = RT_NULL; +static rt_sem_t rx_sem = RT_NULL; +static char rx_buf[BUF_SIZE]; + +static rt_err_t rx_ind(rt_device_t dev, rt_size_t size) +{ + rt_kprintf(">> irq, size=%d\n", size); + rt_sem_release(rx_sem); + return RT_EOK; +} + +static void uart_thread(void *p) +{ + serial = rt_device_find(UART_NAME); + if (!serial) return; + + rx_sem = rt_sem_create("rxs", 0, RT_IPC_FLAG_FIFO); + + rt_uint16_t flag = RT_DEVICE_FLAG_RDWR; + if (USE_MODE == 0 || USE_MODE == 1) + flag |= RT_DEVICE_FLAG_INT_RX; + if (USE_MODE == 2) + flag |= RT_DEVICE_FLAG_DMA_RX; + + rt_device_open(serial, flag); + rt_err_t ret = rt_device_open(serial, flag); + rt_kprintf("uart open ret = %d , flag=0x%04X\r\n", ret, flag); + + if (USE_MODE == 1 || USE_MODE == 2) + rt_device_set_rx_indicate(serial, rx_ind); + + rt_device_write(serial, 0, "UART ready\r\n", 12); + + while (1) + { + if (USE_MODE == 0) + { + char ch; + if (rt_device_read(serial, 0, &ch, 1) == 1) + rt_device_write(serial, 0, &ch, 1); + else + rt_thread_mdelay(10); + } + else + { + rt_sem_take(rx_sem, RT_TICK_PER_SECOND / 20); + rt_size_t len = rt_device_read(serial, 0, rx_buf, BUF_SIZE - 1); + if (len > 0) + rt_device_write(serial, 0, rx_buf, len); + } + } +} + +static int uart_init(void) +{ + rt_thread_t t = rt_thread_create("uart", uart_thread, RT_NULL, 1024, 25, 10); + if (t) rt_thread_startup(t); + return RT_EOK; +} +INIT_APP_EXPORT(uart_init); +``` + +### 代码结构分析 +整个例程由五个部分组成: + +| 模块 | 内容说明 | +| ---------------------- | ------------------------------------------------------------------------------- | +| ① 头文件与宏定义 | `#include ` / ``

UART_NAME / BUF_SIZE / USE_MODE | +| ② 全局变量 | serial (设备句柄) /rx_sem (信号量) /rx_buf | +| ③ rx_ind () 接收回调 | 中断 / DMA 模式下,数据到达时被硬件 ISR 调用 | +| ④ uart_thread () 主逻辑线程 | 打开设备 → 设置回调 → 循环读写 | +| ⑤ uart_init () 自动初始化入口 | INIT_APP_EXPORT 宏,自动创建线程 | +### 关键代码 +#### ① 头文件与宏定义 + +```c +#include // RT-Thread 内核头文件:线程、信号量、rt_kprintf 等 +#include // RT-Thread 设备框架头文件:rt_device_* 系列 API +#include // 标准 C 库字符串操作 + +#define UART_NAME "uart1" // 设备名称,对应 rtconfig.h 中 BSP_USING_UART1 +#define BUF_SIZE 256 // 接收缓冲区大小 + +#define USE_MODE 0 // 0=轮询 1=中断 2=DMA +``` + +#### ② 全局变量 + +```c +static rt_device_t serial = RT_NULL; // 设备句柄,后续指向 "uart1" 设备 +static rt_sem_t rx_sem = RT_NULL; // 信号量,用于中断/DMA 模式下的线程同步 +static char rx_buf[BUF_SIZE]; // 数据接收缓冲区 +``` + +#### ③ 接收回调函数 `rx_ind()` + +```c +static rt_err_t rx_ind(rt_device_t dev, rt_size_t size) +{ + rt_kprintf(">> irq, size=%d\n", size); // 打印提示信息,显示本次触发接收到的数据量 + rt_sem_release(rx_sem); // 释放信号量,唤醒等待线程 + return RT_EOK; +} +``` + +这是**中断模式和 DMA 模式的核心**。当硬件接收到数据时,底层驱动会调用这个回调函数,通知上层"有数据来了"。回调中释放信号量,主线程被唤醒后执行 `rt_device_read` 读取数据。 + +> 回调函数运行在**中断上下文**中,不能做耗时操作(如 `rt_thread_mdelay`),也不能调用可能阻塞的 API。 +> 这里只做两件事:打印日志 + 释放信号量,都是安全的。`rt_kprintf` 在中断中使用需谨慎,生产环境建议去掉或替换为更轻量的日志机制。 + +#### ④ 主逻辑线程 `uart_thread()` + +线程函数分为三个阶段:**初始化 → 模式配置 → 循环读写**。 + +**阶段一:初始化** + +```c +serial = rt_device_find(UART_NAME); // 查找名为 "uart1" 的设备 +if (!serial) return; // 找不到则退出 + +rx_sem = rt_sem_create("rxs", 0, RT_IPC_FLAG_FIFO); // 创建信号量,初始值 0 +``` + +**阶段二:模式配置** + +```c +rt_uint16_t flag = RT_DEVICE_FLAG_RDWR; // 基本标志:可读可写 + +if (USE_MODE == 0 || USE_MODE == 1) // 轮询和中断模式都添加 INT_RX 标志 + flag |= RT_DEVICE_FLAG_INT_RX; +if (USE_MODE == 2) // DMA 模式添加 DMA_RX 标志 + flag |= RT_DEVICE_FLAG_DMA_RX; + +rt_device_open(serial, flag); // 第一次打开设备 +rt_err_t ret = rt_device_open(serial, flag); // 第二次打开(注意:设备已打开,此处会返回 -RT_EBUSY) +rt_kprintf("uart open ret = %d , flag=0x%04X\r\n", ret, flag); + +if (USE_MODE == 1 || USE_MODE == 2) + rt_device_set_rx_indicate(serial, rx_ind); // 注册接收回调(中断/DMA 模式需要) +``` + +> 1. 代码中 `rt_device_open` 被连续调用了两次。第一次打开成功后,第二次调用会返回 `-RT_EBUSY`(设备已忙)。实际开发中应只调用一次,并将返回值用于判断打开是否成功。 +> 2. 当 `USE_MODE == 0`(轮询模式)时,代码仍然设置了 `RT_DEVICE_FLAG_INT_RX` 标志。严格来说,纯轮询模式不需要此标志,但加上也不会影响基本功能——底层驱动会根据实际使用情况处理。 + +![[贡献周/第三章/figures/Pasted image 20260828235517.png]] + +- `0x0103 = 0x003 | 0x100` → **INT_RX 中断接收模式** +- `0x0203 = 0x003 | 0x200` → **DMA_RX 接收模式** + + +| USE_MODE | flag | rx_ind 回调 | 特点 | +| -------- | ------ | -------------------- | ---------------------------------- | +| 0 | 0x0103 | ❌ 不注册,无 `>>irq` | 底层中断 ringbuffer,上层线程轮询 read,回显正常 | +| 1 | 0x0103 | ✅ 注册,多次触发 `>>irq` | 每来若干字节触发一次回调,分片处理 | +| 2 | 0x0203 | ✅ 注册,一帧只触发一次 `>>irq` | DMA + IDLE 空闲中断,整帧结束才通知,size 为整帧长度 | + +**阶段三:循环读写** + +```c +while (1) +{ + if (USE_MODE == 0) + { + // === 轮询模式 === + char ch; + if (rt_device_read(serial, 0, &ch, 1) == 1) // 尝试读 1 字节 + rt_device_write(serial, 0, &ch, 1); // 读到了就回显 + else + rt_thread_mdelay(10); // 没读到就休眠 10ms + } + else + { + // === 中断/DMA 模式 === + rt_sem_take(rx_sem, RT_TICK_PER_SECOND / 20); // 等待信号量,超时 50ms + rt_size_t len = rt_device_read(serial, 0, rx_buf, BUF_SIZE - 1); + if (len > 0) + rt_device_write(serial, 0, rx_buf, len); // 回显收到的数据 + } +} +``` + +**两种模式的核心区别:** + +| 对比维度 | 轮询模式 | 中断/DMA 模式 | +| ---------- | ----------------------------------------- | ------------------------------------- | +| **等待方式** | 不断轮询 `rt_device_read`,无数据时主动 `mdelay(10)` | 信号量阻塞 `rt_sem_take`,超时 50ms 后自动唤醒检查 | +| **CPU 占用** | 高(即使没数据也在循环) | 低(无数据时线程挂起,CPU 可执行其他任务) | +| **读取粒度** | 逐字节读取和回显 | 批量读取(最多 `BUF_SIZE-1` 字节)后回显 | +| **实时性** | 取决于轮询间隔(10ms) | 取决于中断响应速度(微秒级) | +| **信号量超时** | 不适用 | 50ms(`RT_TICK_PER_SECOND / 20`),非永久阻塞 | +#### ⑤ 自动初始化入口 + +```c +static int uart_init(void) +{ + rt_thread_t t = rt_thread_create("uart", uart_thread, RT_NULL, 1024, 25, 10); + if (t) rt_thread_startup(t); + return RT_EOK; +} +INIT_APP_EXPORT(uart_init); // 在系统启动的"应用初始化"阶段自动调用 +``` + +`INIT_APP_EXPORT` 是 RT-Thread 的自动初始化宏,它在系统启动流程中自动调用 `uart_init()`,无需在 `main()` 中手动调用。 +### 编译与下载 +1. 修改 `USE_MODE` 宏选择测试模式(0,1,2) +2. 编译工程:点击 RT-Thread Studio 工具栏的 **构建** 按钮(锤子图标) +3. 下载到开发板:点击 **下载** 按钮 +### 预期输出 +下载后打开串口终端(波特率 **115200**),复位开发板,可以看到: + +```text +UART ready +``` + +#### 轮询模式测试(USE_MODE = 0) +在串口终端输入任意字符,开发板会逐字节回显: + +``` +UART ready +hello world +``` + +![[贡献周/第三章/figures/Pasted image 20260828235737.png]] +> 轮询模式下,每次读取 1 字节并立即回显,无数据时线程休眠 10ms。 +#### 中断模式测试(USE_MODE = 1) + +修改 `USE_MODE` 为 1,重新编译下载。在串口终端输入数据: +![[贡献周/第三章/figures/Pasted image 20260828235802.png]] + +> 中断模式下,每次硬件中断触发时打印 `>> irq` 提示,然后批量读取并回显数据。 +#### DMA 模式测试(USE_MODE = 2) + +修改 `USE_MODE` 为 2,重新编译下载。在串口终端输入数据: +![[贡献周/第三章/figures/Pasted image 20260828235826.png]] + +> DMA 模式下,DMA 控制器自动将数据搬运到内存,完成后触发中断,回调函数打印 `>> irq` 并释放信号量。 + +### 三种模式切换对比 + +| 测试项 | 轮询模式 | 中断模式 | DMA 模式 | +| ---------- | ---------------- | ------------------------- | ------------------------- | +| `USE_MODE` | 0 | 1 | 2 | +| 打开标志 | `RDWR \| INT_RX` | `RDWR \| INT_RX` | `RDWR \| DMA_RX` | +| 是否注册回调 | ❌ | ✅ | ✅ | +| 等待方式 | 主动轮询 + `mdelay` | `rt_sem_take` 阻塞(50ms 超时) | `rt_sem_take` 阻塞(50ms 超时) | +| 读取粒度 | 1 字节 | 批量 | 批量 | +| CPU 占用 | 高 | 低 | 最低 | +| 适用场景 | 简单调试 | 常规通信 | 高速大数据 | +# 6.  应用 +### 场景描述 +在实际项目中,UART 常用于接收上位机指令来控制外设。下面演示如何扩展本章例程,实现**通过串口发送指令控制 LED**。 +### 扩展代码 + +在 `uart_thread` 的循环中,对接收到的数据进行解析: + +```c +#include +#include +#include + +#include + +#define UART_NAME "uart1" +#define BUF_SIZE 256 + +#define LED_PIN GET_PIN(F,11) + + +//切换模式: 0=轮询 1=中断 2=DMA +#define USE_MODE 0 + +static rt_device_t serial = RT_NULL; +static rt_sem_t rx_sem = RT_NULL; +static char rx_buf[BUF_SIZE]; + +static rt_err_t rx_ind(rt_device_t dev, rt_size_t size) +{ + rt_kprintf(">> irq, size=%d\n", size); + rt_sem_release(rx_sem); + return RT_EOK; +} + +static void uart_thread(void *p) +{ + serial = rt_device_find(UART_NAME); + if (!serial) return -RT_ERROR; + + rx_sem = rt_sem_create("rxs", 0, RT_IPC_FLAG_FIFO); + + rt_uint16_t flag = RT_DEVICE_FLAG_RDWR; + if (USE_MODE == 0 ||USE_MODE == 1) + flag |= RT_DEVICE_FLAG_INT_RX; + if (USE_MODE == 2) + flag |= RT_DEVICE_FLAG_DMA_RX; + + rt_device_open(serial, flag); + rt_err_t ret = rt_device_open(serial, flag); + rt_kprintf("uart open ret = %d , flag=0x%04X\r\n", ret, flag); + + if (USE_MODE == 1 || USE_MODE == 2) + rt_device_set_rx_indicate(serial, rx_ind); + + rt_device_write(serial, 0, "UART ready\r\n", 12); + + + + while (1) + { + if (USE_MODE == 0) + { + char ch; + if (rt_device_read(serial, 0, &ch, 1) == 1) + rt_device_write(serial, 0, &ch, 1); + else + rt_thread_mdelay(10); + } + else + { + rt_sem_take(rx_sem, RT_TICK_PER_SECOND / 20); + rt_size_t len = rt_device_read(serial, 0, rx_buf, BUF_SIZE - 1); + if (len > 0) + rt_device_write(serial, 0, rx_buf, len); + } + + /* 解析指令 */ + + rt_sem_take(rx_sem, RT_TICK_PER_SECOND / 20); + rt_size_t len = rt_device_read(serial, 0, rx_buf, BUF_SIZE - 1); + if (len > 0) + { + if (rt_strstr(rx_buf, "led on")) + { + rt_pin_write(LED_PIN, PIN_LOW); // 点亮 LED(假设低电平点亮) + rt_device_write(serial, 0, "LED ON\r\n", 8); + } + else if (rt_strstr(rx_buf, "led off")) + { + rt_pin_write(LED_PIN, PIN_HIGH); // 熄灭 LED + rt_device_write(serial, 0, "LED OFF\r\n", 9); + } + else if (rt_strstr(rx_buf, "led toggle")) + { + rt_pin_write(LED_PIN, !rt_pin_read(LED_PIN)); + rt_device_write(serial, 0, "LED TOGGLE\r\n", 12); + } + else + { + rt_device_write(serial, 0, "Unknown command\r\n", 17); + } + } + } + +} + +static int uart_init(void) +{ + rt_thread_t t = rt_thread_create("uart", uart_thread, RT_NULL, 1024, 25, 10); + if (t) rt_thread_startup(t); + return RT_EOK; +} +INIT_APP_EXPORT(uart_init); + +``` + +### 测试结果 +![[贡献周/第三章/figures/Pasted image 20260830135550.png]] +# 7.  结论 +本章通过一个统一的 UART 串口例程,展示了 RT-Thread 下三种数据接收模式——**轮询、中断、DMA**——的实现方法。 + +| 要点 | 说明 | +| ---------- | --------------------------------------------------- | +| **轮询模式** | 代码最简单,但 CPU 占用高,适合调试或极低数据量场景 | +| **中断模式** | 通过 `rx_indicate` 回调 + 信号量实现事件驱动,平衡了复杂度和效率 | +| **DMA 模式** | CPU 零参与接收过程,适合高波特率、大数据量传输 | +| **统一框架** | 三种模式共用 `rt_device_*` API,只需修改 `open` 的 flag 和是否注册回调 | +| **自动初始化** | `INIT_APP_EXPORT` 让驱动代码自包含,无需改动 `main()` | +在实际项目选型时,建议遵循以下原则: +- **波特率 ≤ 115200,数据量小** → 中断模式即可满足需求 +- **波特率 ≥ 921600,或频繁突发大数据** → 优先考虑 DMA 模式 +- **快速验证、临时调试** → 轮询模式最省事 + + + + diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260827164410.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260827164410.png" new file mode 100644 index 0000000000000000000000000000000000000000..1dd4425195a3eafb05bcddb39ae8300cc8d5766c Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260827164410.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828112637.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828112637.png" new file mode 100644 index 0000000000000000000000000000000000000000..b4ddd7228fe6b0e535eb707e75eac0dfecf0631f Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828112637.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828121853.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828121853.png" new file mode 100644 index 0000000000000000000000000000000000000000..c2699cdcdb7742edb2e49b3264f5ba0b7c481ad5 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828121853.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828121924.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828121924.png" new file mode 100644 index 0000000000000000000000000000000000000000..4ff4761a247e67f9c35f2feecfba863f2d19e2b5 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828121924.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828122726.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828122726.png" new file mode 100644 index 0000000000000000000000000000000000000000..6ea1750d1285716dc90bcaf65de750f95d9f027d Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828122726.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828135224.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828135224.png" new file mode 100644 index 0000000000000000000000000000000000000000..a58234c03637b70a8510a1cb9c306351420aca4a Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/figures/Pasted image 20260828135224.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/\347\254\254\344\272\214\345\215\201\347\253\240 Flash \346\225\260\346\215\256\346\214\201\344\271\205\345\214\226\357\274\232FAL \345\210\206\345\214\272\344\270\212\347\232\204\346\226\207\344\273\266\347\263\273\347\273\237\344\270\216 EasyFlash KV \345\255\230\345\202\250.md" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/\347\254\254\344\272\214\345\215\201\347\253\240 Flash \346\225\260\346\215\256\346\214\201\344\271\205\345\214\226\357\274\232FAL \345\210\206\345\214\272\344\270\212\347\232\204\346\226\207\344\273\266\347\263\273\347\273\237\344\270\216 EasyFlash KV \345\255\230\345\202\250.md" new file mode 100644 index 0000000000000000000000000000000000000000..f17f14fcf5498c5e1d13c6092d34dfe334bd3a85 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\344\272\214\345\215\201\347\253\240/\347\254\254\344\272\214\345\215\201\347\253\240 Flash \346\225\260\346\215\256\346\214\201\344\271\205\345\214\226\357\274\232FAL \345\210\206\345\214\272\344\270\212\347\232\204\346\226\207\344\273\266\347\263\273\347\273\237\344\270\216 EasyFlash KV \345\255\230\345\202\250.md" @@ -0,0 +1,584 @@ +# 1.  新建工程 +### 基于芯片创建工程 +本例程需要使用 **RT-Thread Studio** 基于芯片创建工程,具体步骤如下: +**Step 1:打开 RT-Thread Studio,选择"基于芯片创建工程"** +点击菜单栏 `文件 → 新建 → RT-Thread 项目`,在弹出的对话框中选择 **"基于芯片"** 选项卡。 +**Step 2:配置芯片参数** +![[贡献周/第二十章/figures/Pasted image 20260827164410.png]] +**Step 3:点击"完成"创建工程** +工程创建完成后,RT-Thread Studio 会自动生成基础工程框架,包含: + +``` +项目名称/ +├── applications/ # 用户应用代码 +│ └── main.c # 主程序入口 +├── board/ # 板级支持包 +│ ├── board.c # 板级初始化(时钟配置等) +│ ├── board.h # 板级头文件 +│ ├── CubeMX_Config/ # CubeMX 配置文件 +│ └── linker_scripts/ # 链接脚本 +├── libraries/ # 库文件 +│ ├── HAL_Drivers/ # HAL 驱动层 +│ └── Board_Drivers/ # 板级驱动 +├── rt-thread/ # RT-Thread 内核源码 +├── Kconfig # 内核配置菜单 +├── rtconfig.h # RT-Thread 配置头文件 +├── .config # 内核配置文件 +└── SConscript # SCons 构建脚本 +``` +# 2.  RT-Thread setting的配置 +### 开启 MSH(FinSH)命令行外壳组件 + +![[贡献周/第十九章/figures/Pasted image 20260827125945.png]] +### 使能 FAL 及其相关驱动 + +![[贡献周/第十九章/figures/Pasted image 20260827090733.png]] + +![[贡献周/第十九章/figures/Pasted image 20260827090932.png]] + +![[贡献周/第十九章/figures/Pasted image 20260827091252.png]] +### 添加EasyFlash 软件包 + +![[贡献周/第二十章/figures/Pasted image 20260828112637.png]] +### 保存配置 + +点击保存按钮,RT-Thread Studio 会自动更新 `rtconfig.h` 和 `.config` 文件。 + +- 配置完成后,`rtconfig.h` 中应包含以下关键宏定义: +```c +/* FAL */ +#define RT_USING_FAL +#define FAL_DEBUG 0 +#define FAL_PART_HAS_TABLE_CFG +#define FAL_USING_SFUD_PORT +#define FAL_USING_NOR_FLASH_DEV_NAME "norflash0" + +/* SPI & SFUD */ +#define RT_USING_SPI +#define RT_USING_SFUD +#define RT_SFUD_USING_SFDP +#define RT_SFUD_USING_FLASH_INFO_TABLE +#define RT_SFUD_SPI_MAX_HZ 50000000 + +/* EasyFlash */ +#define PKG_USING_EASYFLASH +#define PKG_EASYFLASH_ENV +#define PKG_EASYFLASH_ERASE_GRAN 4096 +#define PKG_EASYFLASH_WRITE_GRAN_1BIT +#define PKG_EASYFLASH_WRITE_GRAN 1 +#define PKG_EASYFLASH_START_ADDR 0 +#define PKG_EASYFLASH_DEBUG +#define PKG_USING_EASYFLASH_V410 +#define PKG_EASYFLASH_VER_NUM 0x40100 + +/* 板级配置 */ +#define BSP_USING_SPI_FLASH +#define BSP_USING_FAL +#define BSP_USING_EASYFLASH +#define BSP_USING_ON_CHIP_FLASH +#define BSP_USING_SPI2 + +``` + +# 3.  board.c/h 的配置(包括引脚的初始化,配合cubemx) +## 3.1 board.h 配置 +`board.h` 文件定义了芯片的 SRAM 和 Flash 地址范围,以及堆内存起止地址: + +```c +#ifndef __BOARD_H__ +#define __BOARD_H__ + +#include +#include +#include "drv_common.h" +#include "drv_gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define STM32_SRAM_SIZE (128) +#define STM32_SRAM_END (0x20000000 + STM32_SRAM_SIZE * 1024) + +#define STM32_FLASH_START_ADRESS ((uint32_t)0x08000000) +#define STM32_FLASH_SIZE (1024 * 1024) +#define STM32_FLASH_END_ADDRESS ((uint32_t)(STM32_FLASH_START_ADRESS + STM32_FLASH_SIZE)) + +#if defined(__ARMCC_VERSION) +extern int Image$$RW_IRAM1$$ZI$$Limit; +#define HEAP_BEGIN ((void *)&Image$$RW_IRAM1$$ZI$$Limit) +#elif __ICCARM__ +#pragma section="CSTACK" +#define HEAP_BEGIN (__segment_end("CSTACK")) +#else +extern int __bss_end; +#define HEAP_BEGIN ((void *)&__bss_end) +#endif + +#define HEAP_END STM32_SRAM_END + +void SystemClock_Config(void); + +#ifdef __cplusplus +} +#endif + +#endif +``` + +| 宏定义 | 值 | 说明 | +| -------------------------- | ------------------ | ------------- | +| `STM32_FLASH_START_ADRESS` | `0x08000000` | 片内 Flash 起始地址 | +| `STM32_FLASH_SIZE` | `1024 * 1024`(1MB) | 片内 Flash 总容量 | +| `STM32_SRAM_SIZE` | `128`(128KB) | 片内 SRAM 大小 | +| `HEAP_BEGIN` | bss 段末尾 | 堆内存起始地址 | +| `HEAP_END` | `STM32_SRAM_END` | 堆内存结束地址 | +## 3.2 board.c 配置(系统时钟初始化) + +`board.c` 中的 `SystemClock_Config()` 函数负责配置系统时钟,使用 CubeMX 生成的 HAL 库代码。本例使用 **HSE(外部 8MHz 晶振)+ PLL**,系统时钟 168MHz: + +```c +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; + + __HAL_RCC_PWR_CLK_ENABLE(); + __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); + + /* 配置时钟源:HSE + LSE + LSI */ + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_HSE + |RCC_OSCILLATORTYPE_LSE; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.LSEState = RCC_LSE_ON; + RCC_OscInitStruct.LSIState = RCC_LSI_ON; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLM = 4; + RCC_OscInitStruct.PLL.PLLN = 168; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; + RCC_OscInitStruct.PLL.PLLQ = 7; + HAL_RCC_OscConfig(&RCC_OscInitStruct); + + /* 配置系统时钟树 */ + RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK + |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; + HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5); + + /* 配置 RTC 时钟源为 LSE */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; + PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; + HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct); +} +``` + +#### 时钟树总结: + +|时钟域|时钟源|频率| +|---|---|---| +|HSE|外部 8MHz 晶振|8 MHz| +|PLL|HSE × PLLN / PLLM / PLLP|168 MHz (SYSCLK)| +|HCLK|SYSCLK / 1|168 MHz| +|APB1|HCLK / 4|42 MHz| +|APB2|HCLK / 2|84 MHz| +## CubeMX 引脚配置 +SPI2 用于连接片外 W25Q64 Nor Flash,引脚配置如下(通过 CubeMX 设置): + +|引脚|功能|说明| +|---|---|---| +|PB12|SPI2_NSS|SPI2 片选信号| +|PB13|SPI2_SCK|SPI2 时钟信号| +|PB14|SPI2_MISO|SPI2 主入从出| +|PB15|SPI2_MOSI|SPI2 主出从入| + +![[贡献周/第十九章/figures/Pasted image 20260827092431.png]] + +>在 RT-Thread 工程中用 CubeMX 改完时钟/引脚后,**引脚与外设的初始化代码(MSP 回调、`HAL_MspInit`、各外设 MSP 函数)放在 CubeMX 生成目录下的 `Src/stm32f4xx_msp.c`文件里**;而**系统时钟配置函数(如 `SystemClock_Config`)应从 CubeMX 生成的 `Src/main.c`中拷出,放到 RT-Thread 工程的 `drivers/board.c`中**。 + +具体分工 +- `qmax/Src/`里只保留两个文件: + - `stm32f4xx_msp.c`—— 引脚复用、时钟使能等 MSP 初始化 + - `stm32f4xx_it.c`—— 中断向量 + - 其他(含 `main.c`)都可删,RT 工程不用 CubeMX 的 main +- 时钟初始化:从 CubeMX 生成的 `main.c`里把 `SystemClock_Config`等拷到 `drivers/board.c` 对应位置,外设时钟使能也归到这里 +- CubeMX 生成目录里 `Drivers/`、`EWARM/`、`*.frc`、`*.nc`等冗余文件全部删掉,避免和 RT 工程自带的 HAL 库冲突 +**引脚/MSP → `stm32f4xx_msp.c`; +系统时钟 → `drivers/board.c`**。 +![[贡献周/第十九章/figures/Pasted image 20260827093200.png]] + +# 4.  EasyFlash 框架说明 +## EasyFlash 概述 + +**EasyFlash** 是一款开源的轻量级嵌入式 Flash 存储器库,主要为 MCU 提供便捷、通用的上层应用接口。该库目前提供三大实用功能: + +|功能|说明| +|---|---| +|**Env(环境变量)**|快速保存产品参数,支持写平衡(磨损平衡)及掉电保护模式| +|**IAP(在线升级)**|封装 IAP 常用接口,支持 CRC32 校验,支持 Bootloader 及 Application 升级| +|**Log(日志存储)**|无需文件系统,日志可直接存储在 Flash 上| +### 架构图 + + +```mermaid +graph TD + A["应用层
ef_get_env / ef_set_env / ..."] --> B["EasyFlash 核心"] + subgraph EasyFlash核心模块 + B1["ENV环境变量"] + B2["IAP升级"] + B3["Log日志"] + end + B --> B1 & B2 & B3 + + B1 & B2 & B3 --> C["移植层 ef_port.c"] + C --> D["FAL Flash抽象层"] + D --> E["片内Flash"] + D --> F["片外W25Q64"] +```` + + +## 4.2 EasyFlash 核心 API + +### 环境变量操作 + +| API 函数 | 功能说明 | +| ------------------------ | --------------- | +| `easyflash_init()` | 初始化 EasyFlash | +| `ef_get_env(key)` | 读取环境变量(返回字符串指针) | +| `ef_set_env(key, value)` | 设置环境变量(内存操作) | +| `ef_save_env()` | 保存环境变量到 Flash | +| `ef_del_env(key)` | 删除环境变量 | +| `ef_reset_env()` | 复位环境变量为默认值 | +| `ef_print_env()` | 打印所有环境变量 | +### ENV 工作原理 + +``` +setenv → 写入 RAM 缓存(不写 Flash,掉电丢失) +saveenv → 将 RAM 缓存写入 Flash 分区(掉电保护) +上电初始化 → 从 Flash 分区读取 ENV 到 RAM 缓存 +``` + +## EasyFlash 移植说明 +EasyFlash 基于 FAL 的移植主要分为 3 步: +### Step 1:复制移植文件 +从 `packages/EasyFlash-v4.1.0/ports/ef_fal_port.c` 复制到 `libraries/Board_Drivers/ef_fal_port.c`。 +### Step 2:修改分区名 +在 `ef_fal_port.c` 中,将 `FAL_EF_PART_NAME` 宏定义为存储环境变量的分区名: + +```c +/* EasyFlash partition name on FAL partition table */ +#define FAL_EF_PART_NAME "easyflash" +``` +### Step 3:修改默认环境变量 +在 `ef_fal_port.c` 中,修改 `default_env_set` 数组,定义默认环境变量: +```c +static const ef_env default_env_set[] = { + {"iap_need_copy_app", "0"}, + {"iap_need_crc32_check", "0"}, + {"iap_copy_app_size", "0"}, + {"stop_in_bootloader", "0"}, +}; +``` + +> 本例程只记录开机次数(`boot_times`),实际在 `main.c` 中通过 `ef_set_env` 动态添加,默认数组保留 IAP 相关变量。 + +## FAL 分区配置 +分区表定义在 `libraries/Board_Drivers/fal/fal_cfg.h` 中: +### Flash 设备列表 + +```c +#define FAL_FLASH_DEV_TABLE +{ + &stm32_onchip_flash_128k, + &w25q64, +} +``` + +两个 Flash 设备: +- **stm32_onchip_flash_128k**:STM32F4 片内 Flash,擦除块 128KB +- **w25q64**:片外 SPI Nor Flash,容量 8MB,擦除块 4KB + +### 分区表(无 Bootloader) + +```c +#define FAL_PART_TABLE +{ + {FAL_PART_MAGIC_WROD, "app", "onchip_flash_128k", 0, 384 * 1024, 0}, + {FAL_PART_MAGIC_WROD, "param", "onchip_flash_128k", 384 * 1024, 640 * 1024, 0}, + {FAL_PART_MAGIC_WROD, "easyflash", "W25Q64", 0, 512 * 1024, 0}, + {FAL_PART_MAGIC_WROD, "download", "W25Q64", 512 * 1024, 1024 * 1024, 0}, + {FAL_PART_MAGIC_WROD, "wifi_image", "W25Q64", (512 + 1024) * 1024, 512 * 1024, 0}, + {FAL_PART_MAGIC_WROD, "font", "W25Q64", (512 + 1024 + 512) * 1024, 3 * 1024 * 1024, 0}, + {FAL_PART_MAGIC_WROD, "filesystem", "W25Q64", (512 + 1024 + 512 + 3 * 1024) * 1024, 3 * 1024 * 1024, 0}, +} +``` + +> `easyflash` 分区分配在片外 W25Q64 上(512KB),这是因为 W25Q64 的擦除粒度(4KB)比片内 Flash(128KB)更小,更适合 KV 存储的频繁写入。同时也能避免频繁擦除片内 Flash 影响固件寿命。 + +## EasyFlash 配置参数(ef_cfg.h) + +```c +/* 环境变量配置 */ +#define EF_USING_ENV // 启用 ENV 功能 +#define EF_ENV_AUTO_UPDATE // 版本号变化时自动追加新环境变量 + +/* Flash 物理参数 */ +#define EF_ERASE_MIN_SIZE 4096 // 最小擦除单元 4KB(对齐 W25Q64 扇区) +#define EF_WRITE_GRAN 1 // 写入粒度 1 bit(Nor Flash 特性) + +/* 存储区域 */ +#define EF_START_ADDR 0 // 分区内起始地址 +#define ENV_AREA_SIZE (EF_ERASE_MIN_SIZE * 2) // ENV 区域 8KB(至少 2 个扇区供 GC) +``` + +# 5.  例程测试 +## 例程代码(main.c) + +本例程的核心功能是通过 EasyFlash 记录开机次数,每次上电自动 +1 并保存到 Flash: + +```c +#include +#include +#include +#include + +#define DBG_TAG "main" +#define DBG_LVL DBG_LOG +#include + +static void test_env(void); + +int main(void) +{ + fal_init(); // ① 初始化 FAL + + if (easyflash_init() == EF_NO_ERR) // ② 初始化 EasyFlash + { + test_env(); // ③ 演示环境变量功能 + } + + return 0; +} + +static void test_env(void) +{ + uint32_t i_boot_times = 0; + char *c_old_boot_times, c_new_boot_times[11] = {0}; + + /* 从环境变量中获取启动次数 */ + c_old_boot_times = ef_get_env("boot_times"); + if (c_old_boot_times == RT_NULL) + c_old_boot_times[0] = '0'; + + i_boot_times = atol(c_old_boot_times); + i_boot_times++; // ④ 启动次数 +1 + + LOG_D("==============================================="); + LOG_D("The system now boot %ld times", i_boot_times); + LOG_D("==============================================="); + + sprintf(c_new_boot_times, "%ld", i_boot_times); + ef_set_env("boot_times", c_new_boot_times); // ⑤ 写入内存 + ef_save_env(); // ⑥ 保存到 Flash(掉电保护) +} +``` + +### 执行流程详解 + +|步骤|函数|说明| +|---|---|---| +|①|`fal_init()`|初始化 FAL,加载分区表| +|②|`easyflash_init()`|初始化 EasyFlash,从 Flash 读取已有 ENV 到 RAM| +|③|`test_env()`|演示 KV 读写| +|④|`ef_get_env("boot_times")`|从 RAM 缓存读取 boot_times| +|⑤|`ef_set_env(...)`|更新 RAM 缓存中的值| +|⑥|`ef_save_env()`|将 RAM 缓存持久化到 Flash| +## 5.2 预期输出 + +编译下载后,通过串口终端(115200 波特率)可以看到如下输出: + +![[贡献周/第二十章/figures/Pasted image 20260828121853.png]] + +按下复位按键,可以看到开机次数加一,**掉电后重新上电,次数不会丢失**: + +![[贡献周/第二十章/figures/Pasted image 20260828121924.png]] +## FinSH/MSH 命令行测试 + +EasyFlash 自带了 5 个 FinSH 测试命令: + +| 命令 | 功能 | 示例 | +| ---------------------- | ------------- | --------------------- | +| `printenv` | 打印所有环境变量 | `printenv` | +| `getvalue ` | 获取某个 key 的值 | `getvalue boot_times` | +| `setenv ` | 设置环境变量(内存) | `setenv boot_times 5` | +| `saveenv` | 保存环境变量到 Flash | `saveenv` | +| `resetenv` | 复位环境变量为默认值 | `resetenv` | +### MSH 命令演示x + +![[贡献周/第二十章/figures/Pasted image 20260828122726.png]] + +|操作|行为| +|---|---| +|`setenv` 后不 `saveenv`|只修改内存,掉电丢失| +|`setenv` 后 `saveenv`|写入 Flash,掉电不丢失| +|`resetenv`|恢复默认值并自动保存| +|`setenv ` 不带 value|删除该环境变量| +# 6. 应用——配合传感器软件包:存储校准参数 + +下面以 **aht10 温湿度传感器软件包** 为例,演示如何将 EasyFlash 和第三方软件包配合使用,实现传感器校准参数的持久化存储。 +## 场景描述 +aht10 传感器出厂时是校准好的,但实际使用中可能因为安装位置、供电电压差异等原因需要微调。我们希望把校准偏移量存到 Flash 里,掉电后还能保留,下次开机自动加载。 +## 操作步骤 + +### 第一步:添加 aht10 软件包 + +在 RT-Thread Studio 中打开 **RT-Thread Settings**,点击「添加软件包」,搜索 **AHT21**,选择 **Enable AHT21(i2c3)** 并添加。 + +![[贡献周/第二十章/figures/Pasted image 20260828135224.png]] + +> 添加后 RT-Thread Studio 会自动下载软件包到 `packages/` 目录并更新 `rtconfig.h`。 + +保存后,`rtconfig.h` 中会新增: + +```c +#define PKG_USING_AHT10 +``` +### 第二步:修改 `ef_fal_port.c`,添加默认环境变量 + +打开 `libraries/Board_Drivers/ef_fal_port.c`,找到 `default_env_set` 数组,在末尾的 `{NULL, NULL}` 之前**追加两行**校准参数的默认值: + +```c +static const ef_env default_env_set[] = { + // ... 原有的 iap_need_copy_app 等 ... + + /* 新增:传感器校准偏移量 */ + {"temp_offset", "0.0"}, // 温度偏移,默认 0 + {"humi_offset", "0.0"}, // 湿度偏移,默认 0 + + // ... 其余保持不变 ... +}; +``` + +> 新增环境变量后,必须把 `ef_cfg.h` 中的 `PKG_EASYFLASH_ENV_VER_NUM` 版本号 **+1**,这样 EasyFlash 的自动更新机制才会把新变量写进去。 +### 第三步:修改 `main.c`,编写传感器 + 校准逻辑 + +在 `main.c` 中,需要做三件事: + +**① 添加头文件:** + +在文件顶部的 `#include` 区域,新增: + +```c +#include +#include "aht10.h" // 传感器驱动头文件 +``` + +**② 在 `main()` 中初始化设备并加载校准值:** + +在 `easyflash_init()` 成功之后,添加: + +```c +/* 初始化 aht10 传感器(I2C1 总线) */ +aht10_device_t aht10_dev = aht10_init("i2c1"); + +/* 从 EasyFlash 环境变量加载校准偏移量 */ +float temp_offset = 0.0f, humi_offset = 0.0f; +char *str = ef_get_env("temp_offset"); +if (str != RT_NULL) { + temp_offset = atof(str); // 字符串 → 浮点数 +} +str = ef_get_env("humi_offset"); +if (str != RT_NULL) { + humi_offset = atof(str); +} +``` + +**③ 读取传感器数据时应用校准:** + +```c +/* 读取原始数据 */ +float temperature, humidity; +aht10_read_temperature(aht10_dev, &temperature); +aht10_read_humidity(aht10_dev, &humidity); + +/* 应用校准偏移 */ +temperature += temp_offset; +humidity += humi_offset; + +LOG_D("Temp: %.1f°C, Humi: %.1f%%", temperature, humidity); +``` + +### 第六步:通过 MSH 命令校准 + +添加一个自定义 MSH 命令,方便你在运行时调整校准值。在 `main.c` 中添加: + +```c +#include // for atof + +static int cmd_calib(int argc, char **argv) +{ + if (argc != 3) { + rt_kprintf("Usage: calib \n"); + rt_kprintf(" e.g. calib -0.5 2.0\n"); + return -1; + } + + ef_set_env("temp_offset", argv[1]); + ef_set_env("humi_offset", argv[2]); + ef_save_env(); + + rt_kprintf("Calib saved: temp_offset=%s, humi_offset=%s\n", + argv[1], argv[2]); + return 0; +} +MSH_CMD_EXPORT(calib, set sensor calibration offsets); +``` + +**编译 → 下载 → 串口测试:** + +``` +msh >calib -0.5 2.0 +Calib saved: temp_offset=-0.5, humi_offset=2.0 +msh >printenv +===== Env on easyflash ===== +boot_times=12 +temp_offset=-0.5 +humi_offset=2.0 +msh > + +/* 断电重启后,校准值仍然有效 */ +``` + +## 关键要点 + +| 要点 | 说明 | +|------|------| +| **软件包配合** | EasyFlash 负责存储,aht10 负责传感,各司其职 | +| **新增环境变量** | 在 `ef_fal_port.c` 的 `default_env_set` 数组里添加,版本号 +1 | +| **数据类型** | EasyFlash 只存字符串,`atof()` 把字符串转成浮点数用 | +| **MSH 命令** | 用 `MSH_CMD_EXPORT` 注册自定义命令,运行时调校准 | +| **掉电保护** | `ef_save_env()` 写入 Flash,校准值断电不丢 | + +# 7.  结论 + +本章通过 EasyFlash 软件包,基于 FAL(Flash Abstraction Layer)实现了 **环境变量(KV)读写与掉电保护** 功能。 + +|要点|说明| +|---|---| +|**EasyFlash 功能**|环境变量(ENV)、IAP 升级、日志存储三大功能| +|**ENV 存储机制**|内存操作 + Flash 持久化两步走,`setenv` 只改内存,`saveenv` 才写 Flash| +|**掉电保护**|写入时采用"先写新扇区,再擦旧扇区"策略,断电也能恢复旧数据| +|**磨损平衡**|不在固定位置反复擦写,通过 GC 机制在多个扇区间轮转| +|**底层依赖**|FAL 提供统一 Flash 分区接口,EasyFlash 不关心底层是片内还是片外 Flash| +|**Shell 调试**|自带 5 个 MSH 命令,支持运行时查看/修改/保存/复位环境变量| +|**存储位置**|`easyflash` 分区(W25Q64 上 512KB),擦除粒度 4KB| + +通过本章的学习,开发者可以: +1. **掌握 EasyFlash 的移植方法**:复制 `ef_fal_port.c` → 改分区名 → 改默认环境变量 +2. **理解 `setenv`/`saveenv` 的两步机制**,避免只 `setenv` 不 `saveenv` 导致掉电丢失 +3. **使用 MSH 命令** 在运行时调试环境变量 +4. **将 EasyFlash 应用到实际项目**:存储设备参数、记录运行日志、配合 IAP 升级 \ No newline at end of file diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827090733.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827090733.png" new file mode 100644 index 0000000000000000000000000000000000000000..0b79b4ee6bdbd929bde1117c08bf2af17dc39986 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827090733.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827090932.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827090932.png" new file mode 100644 index 0000000000000000000000000000000000000000..b2cced61e33e03453dc0ef06d7bbd2d7eb6fefc2 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827090932.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827091252.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827091252.png" new file mode 100644 index 0000000000000000000000000000000000000000..45cceaf4f7535586eb77bd222a6234d2224cf2d9 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827091252.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827092431.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827092431.png" new file mode 100644 index 0000000000000000000000000000000000000000..49c33ea6322d81e04624ffe9ffe3f236b36c7726 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827092431.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827093200.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827093200.png" new file mode 100644 index 0000000000000000000000000000000000000000..40ab815c7219187a23473825578f37d2d279c46d Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827093200.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827125945.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827125945.png" new file mode 100644 index 0000000000000000000000000000000000000000..89230253381cb2cf4da9b01763d1cc00030b8c75 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827125945.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827134451.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827134451.png" new file mode 100644 index 0000000000000000000000000000000000000000..10c5417aadc012c976c4213aa913ec4da25042fd Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827134451.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827145145.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827145145.png" new file mode 100644 index 0000000000000000000000000000000000000000..5d3f4770a2f8203948aac0e7303623d1e03deb64 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827145145.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827145436.png" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827145436.png" new file mode 100644 index 0000000000000000000000000000000000000000..d3ecbdc1008083dd406734268e831338486905f3 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/figures/Pasted image 20260827145436.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/\347\254\254\345\215\201\344\271\235\347\253\240 Flash \350\256\276\345\244\207\344\270\216 FAL\357\274\232\347\211\207\345\206\205 Flash\343\200\201W25Q64\343\200\201SFUD \345\222\214\345\210\206\345\214\272\347\256\241\347\220\206.md" "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/\347\254\254\345\215\201\344\271\235\347\253\240 Flash \350\256\276\345\244\207\344\270\216 FAL\357\274\232\347\211\207\345\206\205 Flash\343\200\201W25Q64\343\200\201SFUD \345\222\214\345\210\206\345\214\272\347\256\241\347\220\206.md" new file mode 100644 index 0000000000000000000000000000000000000000..3c324bdc61cf16ed566bf92c379e3796ec27f089 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\346\230\223\350\216\271\350\216\271/\350\264\241\347\214\256\345\221\250/\347\254\254\345\215\201\344\271\235\347\253\240/\347\254\254\345\215\201\344\271\235\347\253\240 Flash \350\256\276\345\244\207\344\270\216 FAL\357\274\232\347\211\207\345\206\205 Flash\343\200\201W25Q64\343\200\201SFUD \345\222\214\345\210\206\345\214\272\347\256\241\347\220\206.md" @@ -0,0 +1,538 @@ +# 1. 创建工程 +### 基于芯片创建工程 + +本例程需要使用 **RT-Thread Studio** 基于芯片创建工程,具体步骤如下: +**Step 1:打开 RT-Thread Studio,选择"基于芯片创建工程"** +点击菜单栏 `文件 → 新建 → RT-Thread 项目`,在弹出的对话框中选择 **"基于芯片"** 选项卡。 +**Step 2:配置芯片参数** +![[贡献周/第十九章/figures/Pasted image 20260827134451.png]] +**Step 3:点击"完成"创建工程** +工程创建完成后,RT-Thread Studio 会自动生成基础工程框架,包含: + +``` +项目名称/ +├── applications/ # 用户应用代码 +│ └── main.c # 主程序入口 +├── board/ # 板级支持包 +│ ├── board.c # 板级初始化(时钟配置等) +│ ├── board.h # 板级头文件 +│ ├── CubeMX_Config/ # CubeMX 配置文件 +│ └── linker_scripts/ # 链接脚本 +├── libraries/ # 库文件 +│ ├── HAL_Drivers/ # HAL 驱动层 +│ └── Board_Drivers/ # 板级驱动 +├── rt-thread/ # RT-Thread 内核源码 +├── Kconfig # 内核配置菜单 +├── rtconfig.h # RT-Thread 配置头文件 +├── .config # 内核配置文件 +└── SConscript # SCons 构建脚本 +``` +# 2. RT-Thread Setting 的配置 +### 开启 MSH(FinSH)命令行外壳组件 + +![[贡献周/第十九章/figures/Pasted image 20260827125945.png]] +### 使能 FAL 及其相关驱动 + +![[贡献周/第十九章/figures/Pasted image 20260827090733.png]] + +![[贡献周/第十九章/figures/Pasted image 20260827090932.png]] + +![[贡献周/第十九章/figures/Pasted image 20260827091252.png]] +### 保存配置 + +点击保存按钮,RT-Thread Studio 会自动更新 `rtconfig.h` 和 `.config` 文件。 + +- 配置完成后,`rtconfig.h` 中应包含以下关键宏定义: +```c +#define RT_USING_FAL +#define FAL_DEBUG_CONFIG +#define FAL_DEBUG 1 +#define FAL_PART_HAS_TABLE_CFG +#define FAL_USING_SFUD_PORT +#define RT_USING_SPI +#define RT_USING_SFUD +``` +# 3. board.c/h 的配置(包括引脚的初始化,配合 CubeMX) +## board.h 配置 + +`board.h` 文件定义了芯片的 SRAM 和 Flash 地址范围,以及堆内存起止地址: + +```c +#ifndef __BOARD_H__ +#define __BOARD_H__ + +#include +#include +#include "drv_common.h" +#include "drv_gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define STM32_SRAM_SIZE (128) +#define STM32_SRAM_END (0x20000000 + STM32_SRAM_SIZE * 1024) + +#define STM32_FLASH_START_ADRESS ((uint32_t)0x08000000) +#define STM32_FLASH_SIZE (1024 * 1024) +#define STM32_FLASH_END_ADDRESS ((uint32_t)(STM32_FLASH_START_ADRESS + STM32_FLASH_SIZE)) + +#if defined(__ARMCC_VERSION) +extern int Image$$RW_IRAM1$$ZI$$Limit; +#define HEAP_BEGIN ((void *)&Image$$RW_IRAM1$$ZI$$Limit) +#elif __ICCARM__ +#pragma section="CSTACK" +#define HEAP_BEGIN (__segment_end("CSTACK")) +#else +extern int __bss_end; +#define HEAP_BEGIN ((void *)&__bss_end) +#endif + +#define HEAP_END STM32_SRAM_END + +void SystemClock_Config(void); + +#ifdef __cplusplus +} +#endif + +#endif +``` + +|宏定义|值|说明| +|---|---|---| +|`STM32_FLASH_START_ADRESS`|`0x08000000`|片内 Flash 起始地址| +|`STM32_FLASH_SIZE`|`1024 * 1024`(1MB)|片内 Flash 总容量| +|`STM32_SRAM_SIZE`|`128`(128KB)|片内 SRAM 大小| +|`HEAP_BEGIN`|bss 段末尾|堆内存起始地址| +|`HEAP_END`|`STM32_SRAM_END`|堆内存结束地址| +## board.c 配置(系统时钟初始化) + +`board.c` 中的 `SystemClock_Config()` 函数负责配置系统时钟,使用 CubeMX 生成的 HAL 库代码: + +```c +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; + + __HAL_RCC_PWR_CLK_ENABLE(); + __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); + + /* 配置时钟源:HSE + LSE + LSI */ + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_HSE + |RCC_OSCILLATORTYPE_LSE; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.LSEState = RCC_LSE_ON; + RCC_OscInitStruct.LSIState = RCC_LSI_ON; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLM = 4; + RCC_OscInitStruct.PLL.PLLN = 168; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; + RCC_OscInitStruct.PLL.PLLQ = 7; + HAL_RCC_OscConfig(&RCC_OscInitStruct); + + /* 配置系统时钟树 */ + RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK + |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; + HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5); + + /* 配置 RTC 时钟源为 LSE */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; + PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; + HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct); +} +``` +#### 时钟树总结: + +|时钟域|时钟源|频率| +|---|---|---| +|HSE|外部 8MHz 晶振|8 MHz| +|PLL|HSE × PLLN / PLLM / PLLP|168 MHz (SYSCLK)| +|HCLK|SYSCLK / 1|168 MHz| +|APB1|HCLK / 4|42 MHz| +|APB2|HCLK / 2|84 MHz| +## CubeMX 引脚配置 +SPI2 用于连接片外 W25Q64 Nor Flash,引脚配置如下(通过 CubeMX 设置): + +|引脚|功能|说明| +|---|---|---| +|PB12|SPI2_NSS|SPI2 片选信号| +|PB13|SPI2_SCK|SPI2 时钟信号| +|PB14|SPI2_MISO|SPI2 主入从出| +|PB15|SPI2_MOSI|SPI2 主出从入| + +![[贡献周/第十九章/figures/Pasted image 20260827092431.png]] + +在 RT-Thread 工程中用 CubeMX 改完时钟/引脚后,**引脚与外设的初始化代码(MSP 回调、`HAL_MspInit`、各外设 MSP 函数)放在 CubeMX 生成目录下的 `Src/stm32f4xx_msp.c`文件里**;而**系统时钟配置函数(如 `SystemClock_Config`)应从 CubeMX 生成的 `Src/main.c`中拷出,放到 RT-Thread 工程的 `drivers/board.c`中**。 + +具体分工 +- `qmax/Src/`里只保留两个文件: + - `stm32f4xx_msp.c`—— 引脚复用、时钟使能等 MSP 初始化 + - `stm32f4xx_it.c`—— 中断向量 + - 其他(含 `main.c`)都可删,RT 工程不用 CubeMX 的 main +- 时钟初始化:从 CubeMX 生成的 `main.c`里把 `SystemClock_Config`等拷到 `drivers/board.c` 对应位置,外设时钟使能也归到这里 +- CubeMX 生成目录里 `Drivers/`、`EWARM/`、`*.frc`、`*.nc`等冗余文件全部删掉,避免和 RT 工程自带的 HAL 库冲突 +**引脚/MSP → `stm32f4xx_msp.c`; +系统时钟 → `drivers/board.c`**。 +![[贡献周/第十九章/figures/Pasted image 20260827093200.png]] +# 4. FAL 框架说明 +### FAL 概述 +**FAL (Flash Abstraction Layer)** 即 Flash 抽象层,是 RT-Thread 的一个核心组件,用于对 Flash 及基于 Flash 的分区进行统一管理和操作。FAL 向上层应用(如文件系统、OTA、KV 存储等)提供统一的 API 接口,屏蔽底层不同 Flash 设备的差异。 +**FAL 架构图:** +![FAL framework](https://www.rt-thread.org/document/site/rt-thread-version/rt-thread-standard/programming-manual/fal/figures/fal_framework.png) +### FAL 核心特性 +- **支持静态可配置分区表**:可关联多个 Flash 设备 +- **分区表自动装载**:避免多固件项目中分区表被多次定义 +- **代码精简,无 OS 依赖**:可运行于裸机平台(如 bootloader) +- **统一的操作接口**:保证文件系统、OTA、NVM 等组件的底层驱动可重用性 +- **自带 Finsh/MSH 测试命令**:可通过 Shell 按字节操作 Flash,方便调试 +### FAL 核心数据结构 +#### Flash 设备结构体 `fal_flash_dev` + +```c +struct fal_flash_dev +{ + char name[FAL_DEV_NAME_MAX]; // Flash 设备名称(最大 23 字符) + uint32_t addr; // Flash 设备起始地址 + size_t len; // Flash 设备容量(字节) + size_t blk_size; // 最小擦除块大小(字节) + + struct { + int (*init)(void); // 初始化函数 + int (*read)(long offset, uint8_t *buf, size_t size); // 读函数 + int (*write)(long offset, const uint8_t *buf, size_t size); // 写函数 + int (*erase)(long offset, size_t size); // 擦除函数 + } ops; + + size_t write_gran; // 写入最小粒度(位) +}; +``` + +#### 分区结构体 `fal_partition` + +```c +struct fal_partition +{ + uint32_t magic_word; // 魔法数(系统使用) + char name[FAL_DEV_NAME_MAX]; // 分区名称 + char flash_name[FAL_DEV_NAME_MAX]; // 所属 Flash 设备名称 + long offset; // 分区在 Flash 中的偏移地址 + size_t len; // 分区大小(字节) + uint8_t reserved; // 保留项 +}; +``` + +### FAL 核心 API + +|API 函数|功能说明| +|---|---| +|`fal_init()`|初始化 FAL 组件| +|`fal_flash_device_find(name)`|按名称查找 Flash 设备| +|`fal_partition_find(name)`|按名称查找分区| +|`fal_partition_read(part, offset, buf, size)`|从分区读取数据| +|`fal_partition_write(part, offset, buf, size)`|向分区写入数据| +|`fal_partition_erase(part, offset, size)`|擦除分区指定区域| +|`fal_partition_erase_all(part)`|擦除整个分区| +### 分区表配置(fal_cfg.h) + +分区表定义在 `libraries/Board_Drivers/fal/fal_cfg.h` 中,本例程的分区表如下: + +#### Flash 设备列表 + +```c +#define FAL_FLASH_DEV_TABLE \ +{ \ + &stm32_onchip_flash_128k, \ + &w25q64, \ +} +``` + +两个 Flash 设备: +- **stm32_onchip_flash_128k**:STM32F4 片内 Flash,擦除块 128KB +- **w25q64**:片外 SPI Nor Flash,容量 8MB,擦除块 4KB +#### 分区表 + +```c +#define FAL_PART_TABLE \ +{ \ +{FAL_PART_MAGIC_WORD, "app", "onchip_flash_128k", 0, 384 * 1024, 0}, \ +{FAL_PART_MAGIC_WORD, "param", "onchip_flash_128k", 384 * 1024, 640 * 1024, 0}, \ +{FAL_PART_MAGIC_WORD, "easyflash", "W25Q64", 0, 512 * 1024, 0}, \ +{FAL_PART_MAGIC_WORD, "download", "W25Q64", 512 * 1024, 1024 * 1024, 0}, \ +{FAL_PART_MAGIC_WORD, "wifi_image", "W25Q64", (512 + 1024) * 1024, 512 * 1024, 0}, \ +{FAL_PART_MAGIC_WORD, "font", "W25Q64", (512 + 1024 + 512) * 1024, 3 * 1024 * 1024, 0}, \ +{FAL_PART_MAGIC_WORD, "filesystem", "W25Q64", (512 + 1024 + 512 + 3 * 1024) * 1024, 3 * 1024 * 1024, 0}, \ +} +``` + +### Flash 设备对接说明 + +#### 片内 Flash 对接 + +片内 Flash 设备实例定义在 `libraries/HAL_Drivers/drv_flash/drv_flash_f4.c` 中: + +```c +const struct fal_flash_dev stm32_onchip_flash_128k = +{ + "onchip_flash_128k", + STM32_FLASH_START_ADRESS_128K, // 起始地址 0x08000000 + FLASH_SIZE_GRANULARITY_128K, // 容量 + (128 * 1024), // 擦除块大小 128KB + { + NULL, // 无需初始化 + fal_flash_read_128k, // 读接口 + fal_flash_write_128k, // 写接口 + fal_flash_erase_128k, // 擦除接口 + }, + 8, // 写入粒度 8 位 +}; +``` +#### 片外 Nor Flash 对接(SFUD) + +片外 W25Q64 通过 SFUD 框架对接,定义在 `libraries/Board_Drivers/fal/fal_spi_flash_sfud_port.c` 中: + +```c +struct fal_flash_dev w25q64 = +{ + .name = "W25Q64", + .addr = 0, + .len = 8 * 1024 * 1024, // 8MB + .blk_size = 4096, // 擦除块大小 4KB + .ops = {init, read, write, erase}, // 基于 SFUD 的操作接口 + .write_gran = 1 // 写入粒度 1 位 +}; +``` + +> **SFUD** (Serial Flash Universal Driver) 是一款开源的串行 SPI Flash 通用驱动库,覆盖了市面上绝大多数串行 Flash 型号,无需手动开发驱动即可操作 Flash。 + +# 5. 例程测试与讲解 + +## 例程代码(main.c) + +本例程的核心功能是通过调用 FAL 接口,对指定分区进行完整的擦除、写入、读取测试,验证 Flash 驱动和 FAL 组件的正确性。 + +```c +#include +#include + +#define DBG_TAG "main" +#define DBG_LVL DBG_LOG +#include + +#define BUF_SIZE 1024 + +static int fal_test(const char *partiton_name); + +int main(void) +{ + fal_init(); // 初始化 FAL 组件 + + if (fal_test("param") == 0) + LOG_I("Fal partition (%s) test success!", "param"); + else + LOG_E("Fal partition (%s) test failed!", "param"); + + if (fal_test("download") == 0) + LOG_I("Fal partition (%s) test success!", "download"); + else + LOG_E("Fal partition (%s) test failed!", "download"); + + return 0; +} +``` + +### 分区测试函数详解 + +`fal_test()` 函数对指定分区执行完整的测试流程: +#### Step 1:查找分区和 Flash 设备 + +```c +partition = fal_partition_find(partiton_name); +flash_dev = fal_flash_device_find(partition->flash_name); +``` + +通过分区名称查找分区信息,再通过分区关联的 Flash 设备名称找到对应的 Flash 设备。 +#### Step 2:擦除整个分区 + +```c +ret = fal_partition_erase_all(partition); +``` + +调用 `fal_partition_erase_all()` 擦除分区上的全部数据,擦除后所有数据变为 **0xFF**。 +#### Step 3:校验擦除结果 + +```c +for (i = 0; i < partition->len;) +{ + rt_memset(buf, 0x00, BUF_SIZE); + len = (partition->len - i) > BUF_SIZE ? BUF_SIZE : (partition->len - i); + ret = fal_partition_read(partition, i, buf, len); + // 校验每个字节是否都为 0xFF + for(j = 0; j < len; j++) + { + if (buf[j] != 0xFF) + { + LOG_E("The erase operation did not really succeed!"); + return -1; + } + } + i += len; +} +``` + +循环读取整个分区的数据,逐字节校验是否为 0xFF。若全部为 0xFF,说明擦除操作成功。 +#### Step 4:写入整个分区 + +```c +for (i = 0; i < partition->len;) +{ + rt_memset(buf, 0x00, BUF_SIZE); // 填充 0x00 + len = (partition->len - i) > BUF_SIZE ? BUF_SIZE : (partition->len - i); + ret = fal_partition_write(partition, i, buf, len); + i += len; +} +``` + +循环写入数据 0x00 到整个分区,每次写入 1024 字节。 +#### Step 5:校验写入结果 + +```c +for (i = 0; i < partition->len;) +{ + rt_memset(buf, 0xFF, BUF_SIZE); + len = (partition->len - i) > BUF_SIZE ? BUF_SIZE : (partition->len - i); + ret = fal_partition_read(partition, i, buf, len); + // 校验每个字节是否都为 0x00 + for(j = 0; j < len; j++) + { + if (buf[j] != 0x00) + { + LOG_E("The write operation did not really succeed!"); + return -1; + } + } + i += len; +} +``` + +循环读取并校验数据是否为步骤 4 写入的 0x00,校验通过则说明写操作正常。 +### 预期输出 + +编译下载后,通过串口终端(115200 波特率)可以看到如下输出: + +![[贡献周/第十九章/figures/Pasted image 20260827145145.png]] +### Finsh/MSH 命令行测试 +FAL 自带了 Finsh/MSH 测试命令,可以通过 Shell 进行操作: +![[贡献周/第十九章/figures/Pasted image 20260827145436.png]] +**常用 FAL Shell 命令:** + +|命令|说明| +|---|---| +|`fal probe `|选择分区| +|`fal read `|从分区读取指定长度数据| +|`fal write `|向分区写入数据| +|`fal erase `|擦除分区指定区域| +|`fal bench `|分区性能测试| +# 6. 如何应用 + +### 应用场景一:配合 EasyFlash 实现 KV 存储 + +EasyFlash 是一款开源的轻量级嵌入式 Flash 存储器库,适合存储配置参数和日志。FAL 为其提供了底层 Flash 分区操作能力。 +**步骤:** +1. 在 `RT-Thread Settings` 中使能 **EasyFlash** 软件包 +2. 在分区表中预留 `easyflash` 分区(本例程已在 W25Q64 上预留 512KB) +3. EasyFlash 将通过 FAL 接口操作该分区,实现键值存储 + +```c +#include + +// 初始化 EasyFlash(内部使用 FAL 操作 easyflash 分区) +easyflash_init(); + +// 存储配置 +ef_set_env("device_id", "STM32F407-001"); +ef_set_env("baud_rate", "115200"); + +// 读取配置 +char *device_id = ef_get_env("device_id"); +``` + +### 应用场景二:OTA 固件升级 + +使用 `download` 分区作为 OTA 固件下载区,配合 OTA 软件包实现固件升级: + +```c +#include + +// 下载固件到 download 分区 +const struct fal_partition *dl_part = fal_partition_find("download"); +fal_partition_erase_all(dl_part); +fal_partition_write(dl_part, 0, firmware_data, firmware_size); + +// 校验后跳转到 app 分区执行 +``` + +### 应用场景三:文件系统挂载 + +在 `filesystem` 分区上挂载文件系统(如 LittleFS 或 FatFS): + +```c +#include +#include + +// 使用 FAL 分区作为块设备挂载文件系统 +fal_init(); + +// 在 RT-Thread 中通过 fal 创建块设备,然后挂载 +dfs_mount("filesystem", "/", "elm", 0, 0); +``` + +### 自定义 Flash 设备对接 + +如需添加新的 Flash 设备(如其他型号的 SPI Flash),只需实现 `fal_flash_dev` 结构体中的 ops 接口并注册到 Flash 设备列表中: + +```c +struct fal_flash_dev my_flash = +{ + .name = "my_flash", + .addr = 0, + .len = 16 * 1024 * 1024, // 16MB + .blk_size = 4096, + .ops = {my_init, my_read, my_write, my_erase}, + .write_gran = 1 +}; + +// 在 FAL_FLASH_DEV_TABLE 中添加 +#define FAL_FLASH_DEV_TABLE \ +{ \ + &stm32_onchip_flash_128k, \ + &w25q64, \ + &my_flash, \ +} +``` + +--- + +# 7. 结论 + +本章通过 FAL(Flash Abstraction Layer)组件,实现了对 STM32F407 的 **片内 Flash** 和 **片外 W25Q64 Nor Flash** 的统一管理。 + +|要点|说明| +|---|---| +|**FAL 架构**|Flash 设备层 + 分区管理层,向上提供统一 API| +|**双 Flash 管理**|同时管理片内 Flash(1MB)和片外 W25Q64(8MB)| +|**分区表设计**|7 个分区,合理分配用于 app、参数、文件系统、OTA 等| +|**SFUD 框架**|零代码驱动 W25Q64,自动识别 Flash 型号| +|**测试验证**|完整的擦除→校验→写入→校验测试流程| +|**Shell 调试**|FAL 自带 Finsh 命令,支持按字节读写 Flash| +FAL 组件是 RT-Thread 生态中 OTA 升级、EasyFlash KV 存储、文件系统等高级功能的基础。通过本章的学习,开发者可以: +1. **理解 FAL 的分层架构**,掌握 Flash 设备注册和分区表配置方法 +2. **使用 SFUD 框架**快速驱动 SPI Nor Flash,无需手动编写驱动 +3. **通过 FAL API 和 Shell 命令**对 Flash 进行读写擦操作