杰理 SDK 文档中心
首页
首页
  • 项目概览与快速开始

    • 项目概述与芯片支持
    • 环境搭建与工具链
    • 工程与构建系统
    • 烧录与升级工具
    • 文档与硬件资料
  • 系统架构与芯片平台

    • 芯片平台与启动流程
    • 预编译库与头文件体系
    • 消息、定时器与中断服务
    • 通用外设驱动
  • 存储与文件系统

    • 文件系统实现
    • 存储设备驱动
    • VM 参数存储系统
  • 音频处理

    • 音频解码器
    • 音频编码器
    • MIDI 合成与播放
    • 音效、变速变调与降噪
  • 语音玩具应用

    • 应用框架与状态机
    • 音乐播放与外部音源
    • MIDI 乐器模式
    • 录音应用
    • 待机、电源管理与 USB 从机
  • 小音箱应用

    • 应用框架与模式管理
    • 播放源:音乐、FM、录音与 LineIn
  • 应用层与示例工程

    • 通用 MCU 应用
  • 固件更新与补丁

    • 固件升级机制
    • AD14N 主动降噪补丁

存储设备驱动

本页介绍 SDK 中基于 SPI 的 NOR Flash 存储设备驱动:统一的设备抽象层(device_drive.h)、norflash 驱动的分区管理、软硬件 SPI 收发、读/写/擦除实现、4KB 写缓存机制以及设备操作注册(norflash_dev_ops / norfs_dev_ops)。

Purpose and Scope

本页覆盖以下内容:

  • 设备驱动抽象层:dev_open / dev_read / dev_write / dev_ioctl 等统一设备接口的用法与约定;
  • norflash 驱动(sdk/app/bsp/common/norflash/):WinBond W25X 系列命令集、分区模型、软硬件 SPI 切换、字节读写与页写入、扇区/块/芯片擦除、4KB 写缓存同步;
  • 设备操作表 norflash_dev_ops 与 norfs_dev_ops 的语义差异及其注册方式;
  • S25FL 系列 4 字节地址模式、掉电/唤醒等特殊功能。

以下主题属于其他目录页,本页不展开:

  • 上层文件系统(如 norfs、fatfs)的具体实现与挂载逻辑 —— 见文件系统相关页面;
  • 分区表与烧录布局(post_build 下的 dir_ex_flash、flash_wp_info.csv 等)—— 见构建/烧录相关页面;
  • 写保护(flash WP)功能 —— 见 flash_wp.h 相关页面。

Overview

在杰理 AD1NN 系列 MCU SDK 中,几乎所有外设(UART、SPI、SD、NOR Flash 等)都通过一套统一的设备抽象层暴露给上层应用。设备驱动需要实现 struct device_operations 中定义的回调(init、online、open、read、write、ioctl、close 等),然后以设备名(如 __EXT_FLASH_NANE、"S25FL256")注册到设备节点表中,上层通过 dev_open() 拿到 struct device * 句柄后即可使用统一的 dev_byte_read()、dev_byte_write()、dev_ioctl() 等接口访问存储介质。

NOR Flash 驱动位于 sdk/app/bsp/common/norflash/norflash.c,具有以下关键设计决策:

  1. 双操作表(ops):norflash_dev_ops 提供 512 字节块(bulk)粒度接口,norfs_dev_ops 提供 1 字节粒度接口且写前不检查、不擦除(擦除交给上层文件系统)。两种 ops 可以同时使用、服务于不同分区。
  2. 静态分区模型:最多 4 个分区(MAX_NORFLASH_PART_NUM),每个分区可注册为独立设备名,分区地址重叠或越界会触发 ASSERT()。
  3. 可裁剪的 SPI 层:通过 TCFG_FLASH_SPI_TYPE_SELECT 在硬件 SPI(spi1.h)与软件 SPI(spi_soft.h)之间切换,所有收发操作经由一组宏抽象,便于移植。
  4. 可选 4KB 写缓存:FLASH_CACHE_ENABLE 开启后,读路径可命中 4KB 缓存,写路径先落缓存、定时或关闭时再"擦除+写回",减少擦写次数、延长 Flash 寿命,同时缩短写操作耗时。
  5. 裸机互斥:NORFLASH_NO_SYS 模式下用自定义 flash_mutex(基于 jiffies 与看门狗)保证多任务/中断上下文访问串行化,并在等待期间喂狗避免复位。
flowchart TD
    subgraph sg_App["应用层"]
        FS["文件系统 / 上层应用"]
        Test["驱动测试代码<br/>norflash_test / s25_flash_test"]
    end

    subgraph sg_DevLayer["设备抽象层 (device_drive.h)"]
        API["dev_open / dev_byte_read<br/>dev_byte_write / dev_ioctl"]
        Ops["struct device_operations"]
    end

    subgraph sg_Norflash["NOR Flash 驱动 (norflash.c)"]
        Ops2["norflash_dev_ops<br/>norfs_dev_ops"]
        Part["分区管理<br/>norflash_partition × 4"]
        Cache["4KB 写缓存<br/>FLASH_CACHE_ENABLE"]
        Mutex["flash_mutex 互斥"]
        Cmd["W25X 命令集<br/>读/写/擦除"]
    end

    subgraph sg_Spi["SPI 层"]
        SpiHw["硬件 SPI (spi1.h)"]
        SpiSoft["软件 SPI (spi_soft.h)"]
    end

    subgraph sg_Hw["硬件"]
        Flash[("NOR Flash 芯片<br/>W25Q / S25FL 系列")]
        Gpio["CS 引脚 GPIO"]
    end

    FS --> API
    Test --> API
    API --> Ops
    Ops --> Ops2
    Ops2 --> Part
    Ops2 --> Cache
    Ops2 --> Mutex
    Ops2 --> Cmd
    Cmd --> SpiHw
    Cmd --> SpiSoft
    SpiHw --> Flash
    SpiSoft --> Flash
    Cmd --> Gpio
    Gpio --> Flash

核心数据结构

设备操作表

norflash.h 声明了两个外部设备操作表,由 norflash.c 实现:

extern const struct device_operations norflash_dev_ops;
extern const struct device_operations norfs_dev_ops;

Source: norflash.h

norfs_dev_ops 的注册内容(norflash.c 末尾):

const struct device_operations norfs_dev_ops = {
    .init   = norflash_dev_init,
    .online = norflash_dev_online,
    .open   = norflash_dev_open,
    .read   = norflash_byte_read,//按字节(byte)读取
    .write  = norflash_byte_write,//按字节写入,写前不进行检查与擦除
    .bulk_read   = NULL,
    .bulk_write  = NULL,
    .ioctl  = norflash_byte_ioctl,
    .close  = norflash_dev_close,
};

Source: norflash.c

关键语义(来自源码注释):

  • 外部调用时地址与长度以 1 字节 为单位;驱动 write 不自己处理擦除,使用本 ops 时擦除由上层文件系统负责;
  • 若希望驱动内部处理擦除,需将 FLASH_CACHE_ENABLE 置 1,并且 norfs_dev_read()/norfs_dev_write() 调用 _norflash_read() 时 cache 实参填 1;
  • norfs_dev_ops 可被多个设备名注册,每个设备名被视为不同分区,需填不同起始地址与大小;分区重叠或最大结束地址大于 Flash 容量会触发 ASSERT();
  • IOCTL_GET_CAPACITY:多分区时返回分区大小;仅 1 个分区且起始地址为 0 时返回 Flash 容量;
  • 本文件内的各个 ops 可以同时使用。

平台数据

struct norflash_dev_platform_data {
    s8 spi_hw_num;         //只支持SPI1或SPI2
    u8 spi_cs_port;        //cs的引脚
    u8 spi_read_width;     //flash读数据的线宽
    const struct spi_platform_data *spi_pdata;
    u32 start_addr;         //分区起始地址
    u32 size;               //分区大小,若只有1个分区,则这个参数可以忽略
};

#define NORFLASH_DEV_PLATFORM_DATA_BEGIN(data) \
	const struct norflash_dev_platform_data data = {

#define NORFLASH_DEV_PLATFORM_DATA_END()  \
};

Source: norflash.h

spi_hw_num 只支持 SPI1 或 SPI2;spi_read_width 控制读数据线宽(1/2/4 BIT);start_addr/size 描述该设备名对应的分区范围。NORFLASH_DEV_PLATFORM_DATA_BEGIN/END 宏用于在 board 配置文件中声明平台数据实例。

分区管理与内部状态

驱动使用一个全局单例 _norflash 保存所有运行期状态:

struct norflash_info {//last
    u32 flash_id;
    u32 flash_capacity;
    s8 spi_num;
    u8 spi_cs_io;
    u8 spi_r_width;
    u8 part_num;
    u8 open_cnt;
    flash_mutex mutex;
    struct norflash_partition *const part_list;
    u32 max_end_addr;
};

static struct norflash_info _norflash = {
    .spi_num = (s8) - 1,
    .part_list = nor_part,
};

Source: norflash.c

分区表是静态数组,最多 4 个分区:

#define MAX_NORFLASH_PART_NUM       4

struct norflash_partition {
    const char *name;
    u32 start_addr;
    u32 size;
    struct device device;
};
static struct norflash_partition nor_part[MAX_NORFLASH_PART_NUM];

Source: norflash.c

分区的生命周期由三个静态函数管理:

  • norflash_find_part(name):线性扫描分区表,按名字匹配返回分区指针;
  • norflash_new_part(name, addr, size):在空槽位创建分区,并维护 max_end_addr(所有分区结束地址的最大值,用于 open 时与 Flash 容量比对);
  • norflash_verify_part(p):校验地址重叠 —— 若某分区起始地址落在其他分区范围内且名字不同,返回 -1 拒绝注册。

设计意图:分区信息在编译期/初始化期静态登记,运行期不做动态分配,避免了内存碎片与分配失败路径;同时通过 verify_part 与 open 时的 ASSERT(max_end_addr <= flash_capacity) 双重防护,从根上杜绝分区越界访问。

SPI 抽象层

驱动通过一组宏把底层 SPI 访问完全抽象化,TCFG_FLASH_SPI_TYPE_SELECT 决定使用硬件 SPI 还是软件 SPI:

#if(TCFG_FLASH_SPI_TYPE_SELECT)
#define spi_read_byte()             spi_recv_byte(_norflash.spi_num, NULL)
#define spi_write_byte(x)           spi_send_byte(_norflash.spi_num, x)
#define spi_dma_read(x, y)          spi_dma_recv(_norflash.spi_num, x, y)
#define spi_dma_write(x, y)         spi_dma_send(_norflash.spi_num, x, y)
#define spi_set_width(x)            spi_set_bit_mode(_norflash.spi_num, x)
#define spi_init()                  spi_open(_norflash.spi_num)
#define spi_closed()                spi_close(_norflash.spi_num)
#define spi_suspend()               hw_spi_suspend(_norflash.spi_num)
#define spi_resume()                hw_spi_resume(_norflash.spi_num)
#else
#define spi_read_byte()     soft_spi_recv_byte(_norflash.spi_num, NULL)
#define spi_write_byte(x)   soft_spi_send_byte(_norflash.spi_num, x)
#define spi_dma_read(x, y)  soft_spi_dma_recv(_norflash.spi_num, x, y)
#define spi_dma_write(x, y) soft_spi_dma_send(_norflash.spi_num, x, y)
#define spi_set_width(x)    soft_spi_set_bit_mode(_norflash.spi_num, x)
#define spi_init()          soft_spi_open(_norflash.spi_num)
#define spi_closed()        soft_spi_close(_norflash.spi_num)
#define spi_suspend()       soft_spi_suspend(_norflash.spi_num)
#define spi_resume()        soft_spi_resume(_norflash.spi_num)
#endif

Source: norflash.c

CS 引脚通过 GPIO 位带操作直接控制(不经过 SPI 控制器),并配套初始化/去初始化宏:

#define spi_cs_init() \
    do { \
        gpio_set_die(_norflash.spi_cs_io, 1); \
        gpio_set_direction(_norflash.spi_cs_io, 0); \
        gpio_write(_norflash.spi_cs_io, 1); \
    } while (0)

#define spi_cs_uninit() \
    do { \
        gpio_set_die(_norflash.spi_cs_io, 0); \
        gpio_set_direction(_norflash.spi_cs_io, 1); \
        gpio_set_pull_up(_norflash.spi_cs_io, 0); \
        gpio_set_pull_down(_norflash.spi_cs_io, 0); \
    } while (0)
#define spi_cs_h()                  gpio_write(_norflash.spi_cs_io, 1)
#define spi_cs_l()                  gpio_write(_norflash.spi_cs_io, 0)

Source: norflash.c

设计意图:软硬件 SPI 通过编译期宏切换而非运行期函数指针,保证零额外开销;CS 独立于 SPI 控制器便于复用 SD 卡的 SPI IO(见 SPI_SD_IO_REUSE)。

读 / 写 / 擦除实现

等待空闲(_norflash_wait_ok)

每次命令后轮询状态寄存器 1 的 BUSY 位(BIT(0)),超时约 8 万次轮询(8 * 1000 * 1000 / 100),期间每次 delay(100):

static int _norflash_wait_ok()
{
    u32 timeout = 8 * 1000 * 1000 / 100;
    u8 reg_1 = 0;
    while (timeout--) {
        spi_cs_l();
        spi_write_byte(WINBOND_READ_SR1);
        reg_1 = spi_read_byte();
        spi_cs_h();
        if (!(reg_1 & BIT(0))) {
            break;
        }
        delay(100);
    }
    if (timeout == 0) {
        log_error("norflash_wait_ok timeout!\r\n");
        return 1;
    }
    ...
}

Source: norflash.c

读取(_norflash_read)

读取路径优先尝试命中 4KB 缓存(FLASH_CACHE_ENABLE 且 cache 参数非 0),未命中则走 SPI:先发 WINBOND_FAST_READ_DATA(0x0B),2BIT 线宽时改用 WINBOND_FAST_READ_DUAL_OUTPUT(0x3B)并切换 SPI_MODE_UNIDIR_2BIT 后 DMA 收数;4BIT 在 SPI1 上不支持(打印错误日志):

    spi_cs_l();
#if(TCFG_FLASH_SPI_TYPE_SELECT)//soft spi only have 1BIT
    if (_norflash.spi_r_width == SPI_MODE_UNIDIR_2BIT) {
        spi_write_byte(WINBOND_FAST_READ_DUAL_OUTPUT);
        _norflash_send_addr(addr);
        spi_write_byte(0);
        spi_set_width(SPI_MODE_UNIDIR_2BIT);
        spi_dma_read(buf, len);
        spi_set_width(SPI_MODE_BIDIR_1BIT);
    } else if (_norflash.spi_r_width == SPI_MODE_UNIDIR_4BIT) {
        log_error("flash write error!SPI1 don't support 4BIT!");
    } else
#endif
    {
        spi_write_byte(WINBOND_FAST_READ_DATA);
        _norflash_send_addr(addr);
        spi_write_byte(0);
        spi_dma_read(buf, len);
    }
    spi_cs_h();

Source: norflash.c

页写入(_norflash_write_pages)

NOR Flash 页编程命令(WINBOND_PAGE_PROGRAM 0x02)一次最多写 256 字节。实现先算首页剩余长度(256 - addr % 256),然后逐页发送 写使能(0x06)→ 拉低 CS → 发命令与地址 → DMA 写数据 → 拉高 CS → 等待 BUSY 的完整序列:

static int _norflash_write_pages(u32 addr, u8 *buf, u32 len)
{
    int reg;
    u32 first_page_len = 256 - (addr % 256);
    first_page_len = len > first_page_len ? first_page_len : len;
    _norflash_send_write_enable();
    spi_cs_l();
    spi_write_byte(WINBOND_PAGE_PROGRAM);
    _norflash_send_addr(addr) ;
    spi_dma_write(buf, first_page_len);
    spi_cs_h();
    reg = _norflash_wait_ok();
    if (reg) {
        return 1;
    }
    addr += first_page_len;
    buf += first_page_len;
    len -= first_page_len;
    while (len) {
        u32 cnt = len > 256 ? 256 : len;
        _norflash_send_write_enable();
        spi_cs_l();
        spi_write_byte(WINBOND_PAGE_PROGRAM);
        _norflash_send_addr(addr) ;
        spi_dma_write(buf, cnt);
        spi_cs_h();
        reg = _norflash_wait_ok();
        if (reg) {
            return 1;
        }
        addr += cnt;
        buf += cnt;
        len -= cnt;
    }
    return 0;
}

Source: norflash.c

设计意图:每一页写前都必须单独发写使能命令,这是 W25X 系列芯片的硬件约束;先处理"首页非对齐"再按整页循环,保证任意起始地址、任意长度的写请求都能被正确切页。

擦除粒度

norflash.h 定义四种擦除粒度,与命令一一对应:

enum {
    FLASH_PAGE_ERASER,     // 0x81 WINBOND_PAGE_ERASE
    FLASH_SECTOR_ERASER,   // 0x20 WINBOND_SECTOR_ERASE
    FLASH_BLOCK_ERASER,    // 0xD8 WINBOND_BLOCK_ERASE
    FLASH_CHIP_ERASER,     // 0xC7 WINBOND_CHIP_ERASE
};

Source: norflash.h

_norflash_eraser(eraser, addr) 根据粒度选择命令并执行"写使能 → 发命令/地址 → 等待 BUSY"序列,供 ioctl 与缓存同步调用。

4KB 写缓存机制(FLASH_CACHE_ENABLE)

缓存是驱动最重要的优化手段之一,其状态变量:

#define    FLASH_READ_NO_USE_CACHE  0
#define    FLASH_READ_OCCUPY_CACHE  1
#define    FLASH_READ_PREPARE_USE_CACHE  2
static volatile u8 flash_cache_sync_enter_critical_dis = 0;
static u8 flash_read_use_cache = FLASH_READ_NO_USE_CACHE;
static u32 flash_cache_addr;
static u8 *flash_cache_buf = NULL; //缓存4K的数据,与flash里的数据一样。
static u8 norfs_cache_buf[4096]  __attribute__((aligned(4)))SEC(.norflash_cache_buf);
static u8 flash_cache_is_dirty;
static volatile u8 flash_cache_timer;

Source: norflash.c

工作方式:

  • 读:_norflash_read(addr, buf, len, cache=1) 时若 flash_cache_addr 是 4KB 对齐且请求范围落在缓存窗口内,直接从 flash_cache_buf 拷贝,完全绕开 SPI;超出部分回退到真读。
  • 写:上层写入先修改缓存(脏标记 flash_cache_is_dirty),不立即擦写 Flash。
  • 同步:_norflash_cache_sync_timer(sync_step) 在定时器中检查脏标记,满足步进(NORFLASH_NO_SYS 下 idle_cnt >= sync_step,即 N×100ms)后,进入临界区抢占互斥锁,执行"擦除扇区 → 整页写回 4096 字节":
    • 若 SPI_SD_IO_REUSE(与 SD 卡复用 SPI IO),同步前先 sd_io_reuse_suspend() 抢占总线,完成后 spi_flash_io_suspend() 再 sd_io_reuse_resume() 归还;
    • 互斥锁获取失败(query_flash_mutex_pend < 0)则本次放弃同步,等下一个周期。
  • 关闭:_norflash_close() 在 open_cnt 归零时若缓存仍脏,先擦除再写回,然后释放缓存指针、关闭 SPI 并 spi_cs_uninit()。

设计意图:NOR Flash 擦除时间长(扇区擦除可达数十毫秒)、擦写寿命有限。缓存把"每次小写"合并为"4KB 整扇区写回",显著减少擦除次数与总耗时;脏数据定时回写则兼顾了掉电数据安全与响应延迟。flash_cache_sync_enter_critical_dis 允许某些场景(如正在擦写期间)临时关闭临界区保护,避免死锁。

核心流程

设备打开与关闭流程

norflash_dev_open 的执行顺序(_norflash_open):

  1. 首次打开时初始化 CS 引脚与 SPI(spi_cs_init / spi_init);
  2. 发 WINBOND_JEDEC_ID(0x9F)读取 Flash ID 与容量,若 ID 为 0 或 0xffffff 则报错返回 -EFAULT;
  3. 对 S25FL 系列执行 4 字节地址模式切换(S25FL512_enter_4byte_addr)或配置寄存器写回(_S25FLxxx_read_id);
  4. FLASH_CACHE_ENABLE 时初始化缓存指针与地址;
  5. ASSERT(max_end_addr <= flash_capacity) 校验分区边界;
  6. open_cnt++ 记录引用计数 —— 多任务共享同一设备时只有最后一个 close 才真正关 SPI。
sequenceDiagram
    participant App as 上层应用/文件系统
    participant Dev as 设备抽象层
    participant Nf as norflash 驱动
    participant Spi as SPI 层
    participant Chip as NOR Flash 芯片

    App->>Dev: dev_open("EXT_FLASH", 0)
    Dev->>Nf: norflash_dev_open()
    Nf->>Spi: spi_cs_init / spi_init
    Nf->>Chip: JEDEC_ID (0x9F)
    Chip-->>Nf: flash_id / capacity
    Nf->>Nf: 校验分区边界 ASSERT
    Nf-->>Dev: struct device *
    Dev-->>App: 设备句柄

    App->>Dev: dev_byte_read(buf, addr, len)
    Dev->>Nf: norflash_byte_read()
    Nf->>Nf: 4KB 缓存命中?
    alt 命中
        Nf-->>App: memcpy 直接返回
    else 未命中
        Nf->>Spi: FAST_READ (0x0B) + DMA
        Spi->>Chip: 读数据
        Chip-->>Nf: 数据
        Nf-->>App: 返回数据
    end

    App->>Dev: dev_byte_write(buf, addr, len)
    Dev->>Nf: norflash_byte_write()
    Nf->>Nf: 更新 4KB 缓存, 置脏标记
    Nf-->>App: 立即返回

    Note over Nf: 定时器触发 _norflash_cache_sync_timer
    Nf->>Nf: 抢互斥锁, sd_io_reuse_suspend (可选)
    Nf->>Chip: SECTOR_ERASE (0x20)
    Nf->>Chip: PAGE_PROGRAM ×16 (写回 4KB)
    Nf->>Nf: 清脏标记, 释放总线

    App->>Dev: dev_close()
    Dev->>Nf: norflash_dev_close()
    Nf->>Nf: open_cnt--, 缓存同步(若脏)
    Nf->>Spi: spi_closed / spi_cs_uninit

读缓存命中判定

_norflash_read 中命中判定要求 flash_cache_addr 4KB 对齐,且请求区间 [addr, addr+len) 不越过缓存窗口末尾;窗口剩余长度 r_len = 4096 - (addr % 4096) 内的部分直接拷贝,超出部分继续走 SPI 真读。

使用示例

以下代码来自 norflash.c 末尾的测试函数(默认被 #if 0 关闭,仅作验证/示例用途),展示了块(bulk)接口与字节(byte)接口两种用法。

块操作示例(norflash_dev_ops 风格)

void norflash_test()
{
    u32 i = 0;
    struct device *device;
    for (i = 0; i < n_n * 512; i++) {
        w25q_write_buf[i] = i % 26 + 'a';
    }
    devices_init();
    device = (struct device *)dev_open(__EXT_FLASH_NANE, 0);
    if (device == NULL) {
        log_error("w25q32 open fail!\n");
    } else {
        log_info("w25q32 open ok!\n");
#if 0  //块(512byte)操作测试---norflash_dev_ops
        log_info("flash dev bulk write/read test!");
        dev_bulk_write(device, w25q_write_buf, 0, n_n);
        delay(10000);
        memset(w25q_read_buf, 48, 512);
        dev_bulk_read(device, w25q_read_buf, 0, n_n);
        log_info("w25q bulk read buf:");
        log_info_hexdump(w25q_read_buf, n_n * 512);
#else  //字节(1byte)操作测试---norfs_dev_ops
        log_info("flash dev byte write/read test!");
        if (dev_ioctl(device, IOCTL_ERASE_SECTOR, 0)) { //返回0:擦出正常;1:擦出等待失败
            log_error("erase err\n");
            while (1);
        }
        dev_byte_read(device, w25q_read_buf, 0, n_n * 512);
        dev_byte_write(device, w25q_write_buf, 0, n_n * 512);
        delay(10000);
        dev_byte_read(device, w25q_read_buf, 0, n_n * 512);
        log_info(":%s\n", w25q_read_buf);
#endif
    }
}

Source: norflash.c

要点:dev_open(__EXT_FLASH_NANE, 0) 拿到句柄后,先 dev_ioctl(IOCTL_ERASE_SECTOR, 0) 擦除目标扇区,再写后读验证;IOCTL_ERASE_SECTOR 返回 0 表示成功,1 表示等待 Flash 空闲失败。

大容量 Flash(S25FL256)示例:按地址选择擦除粒度

void s25_flash_test()
{
    u32 s25fl_start_addr = 0x1020020;
    u8 erase_sector = 0;
    struct device *device;
    devices_init();
    device = (struct device *)dev_open("S25FL256", 0);
    if (device == NULL) {
        log_error("S25FL256 open fail!\n");
    } else {
        s25fl_start_addr = 0x1020020;//32个4k 扇区:0-0x20000; 510个64k扇区:0x20000-0x1ffffff
        //手动擦出扇区
        if (s25fl_start_addr < 0x20000) {
            erase_sector = IOCTL_ERASE_SECTOR;
        } else if (s25fl_start_addr < 0x1ffffff) {
            erase_sector = IOCTL_ERASE_BLOCK;
        } else while (1);
        if (dev_ioctl(device, erase_sector, s25fl_start_addr)) {
            log_error("erase err\n");
            while (1);
        }
        dev_byte_read(device, s25_read_buf, s25fl_start_addr, srw_len);
        dev_byte_write(device, s25_write_buf, s25fl_start_addr, srw_len);
        delay(10000);
        dev_byte_read(device, s25_read_buf, s25fl_start_addr, srw_len);
    }
}

Source: norflash.c

要点:S25FL256 低地址区是 4KB 小扇区、高地址区是 64KB 大扇区,因此调用方需按目标地址选择 IOCTL_ERASE_SECTOR 还是 IOCTL_ERASE_BLOCK;注释明确说明"扇区为 256KB 时(如 S25FL512)没有擦除相关程序",即超大扇区芯片不支持该擦除命令。源码注释还注明"两种 flash 不能同时运行",同一编译单元内只能启用其中一种测试。

配置选项

以下宏/字段决定驱动行为,均在 norflash.h、norflash.c 或板级配置(app_config.h、board 文件)中定义:

配置项类型默认值说明
EXT_FLASH_EN宏—置 1 时定义 TCFG_NORFLASH_DEV_ENABLE,启用 norflash 驱动编译(norflash.c 整体被 #if defined(TCFG_NORFLASH_DEV_ENABLE) 包裹)
TCFG_FLASH_SPI_TYPE_SELECT宏—1:使用硬件 SPI(spi1.h);0:使用软件 SPI(spi_soft.h)。软件 SPI 仅支持 1BIT
FLASH_CACHE_ENABLE宏—1:启用 4KB 读写缓存(norfs_cache_buf[4096] 置于 .norflash_cache_buf 段);0:直读直写
MALLOC_EN宏01:缓存缓冲区用 malloc 动态分配;0:使用静态 norfs_cache_buf
NORFLASH_NO_SYS宏11:裸机模式,用自定义 flash_mutex(基于 jiffies+喂狗);0:使用操作系统互斥量
SPI_SD_IO_REUSE宏—1:SPI IO 与 SD 卡复用,访问 Flash 前需 sd_io_reuse_suspend() 抢占、结束后归还总线
MAX_NORFLASH_PART_NUM宏4静态分区表容量上限
spi_hw_num(平台数据)s8—SPI 控制器编号,只支持 SPI1 或 SPI2
spi_cs_port(平台数据)u8—CS 片选引脚
spi_read_width(平台数据)u8—读数据线宽(SPI_MODE_BIDIR_1BIT / SPI_MODE_UNIDIR_2BIT 等),4BIT 在 SPI1 上不支持
start_addr / size(平台数据)u32—该设备名对应的分区起始地址与大小
_norflash_wait_ok 超时u3280000 次轮询8 * 1000 * 1000 / 100,每次 delay(100),超时返回 1 并打印 norflash_wait_ok timeout!
sync_step(_norflash_cache_sync_timer)u32—缓存同步步进,N 表示 N×100ms 才回写一次脏缓存

命令集常量(norflash.h)汇总:

命令值用途
WINBOND_WRITE_ENABLE0x06写使能(每次写/擦除前必须发送)
WINBOND_READ_SR1 / READ_SR20x05 / 0x35读状态寄存器 1/2(BUSY 位检测)
WINBOND_WRITE_SR1 / WRITE_SR20x01 / 0x31写状态寄存器
WINBOND_READ_DATA0x03普通读
WINBOND_FAST_READ_DATA0x0B快速读(默认路径)
WINBOND_FAST_READ_DUAL_OUTPUT0x3B双线快读(2BIT 模式)
WINBOND_PAGE_PROGRAM0x02页编程(256B/页)
WINBOND_PAGE_ERASE0x81页擦除(FLASH_PAGE_ERASER)
WINBOND_SECTOR_ERASE0x20扇区擦除(FLASH_SECTOR_ERASER)
WINBOND_BLOCK_ERASE0xD8块擦除(FLASH_BLOCK_ERASER)
WINBOND_CHIP_ERASE0xC7全片擦除(FLASH_CHIP_ERASER)
WINBOND_JEDEC_ID0x9F读 JEDEC ID 与容量
WINBOND_POWER_DOWN0xB9进入掉电模式(_norflash_power_down)
WINBOND_RELEASE_POWER_DOWN0xAB释放掉电模式(_norflash_release_power_down)

Source: norflash.h

API 参考

int _norflash_read(u32 addr, u8 *buf, u32 len, u8 cache)

按绝对地址从 Flash 读取数据。

参数:

  • addr (u32):Flash 绝对地址;
  • buf (u8 *):接收缓冲区;
  • len (u32):读取长度(字节);
  • cache (u8):FLASH_CACHE_ENABLE 时传 1 启用 4KB 缓存命中检查,0 强制真读。

返回: 0 成功(当前实现恒返回 0,SPI 层错误通过 NULL 参数丢弃)。

Source: norflash.c

static int _norflash_write_pages(u32 addr, u8 *buf, u32 len)

按绝对地址分页写入,自动切页、逐页发写使能。

参数: 同 _norflash_read(无 cache 参数)。

返回: 0 成功;1 某页编程后等待 BUSY 超时。

Source: norflash.c

int _norflash_eraser(u8 eraser, u32 addr)

执行指定粒度的擦除(页/扇区/块/芯片)。

参数:

  • eraser (u8):FLASH_PAGE_ERASER / FLASH_SECTOR_ERASER / FLASH_BLOCK_ERASER / FLASH_CHIP_ERASER;
  • addr (u32):擦除地址(芯片擦除时忽略)。

返回: 0 成功;非 0 等待失败。

设备接口(上层通过设备抽象层调用)

接口对应 ops 回调说明
dev_open(name, arg)norflash_dev_open打开设备,返回 struct device *;失败返回 NULL
dev_byte_read(dev, buf, addr, len)norflash_byte_read以 1 字节为单位的读
dev_byte_write(dev, buf, addr, len)norflash_byte_write以 1 字节为单位的写(写前不擦除、不检查)
dev_bulk_read(dev, buf, blk, cnt)norflash_bulk_read以 512 字节块为单位的读
dev_bulk_write(dev, buf, blk, cnt)norflash_bulk_write以 512 字节块为单位的写
dev_ioctl(dev, cmd, arg)norflash_byte_ioctlIOCTL_ERASE_SECTOR / IOCTL_ERASE_BLOCK / IOCTL_ERASE_CHIP / IOCTL_GET_CAPACITY 等
dev_close(dev)norflash_dev_close关闭设备,引用计数归零时真正关 SPI

IOCTL_GET_CAPACITY 语义(源码注释):多分区注册时返回分区大小;仅注册 1 个分区且起始地址为 0 时返回 Flash 总容量;起始地址非 0 时返回分区大小。返回长度以 1 字节为单位。

故障模式、边界情况与并发

互斥与并发访问

NORFLASH_NO_SYS(裸机)模式下,驱动用自定义信号量实现互斥:

typedef volatile u8 flash_mutex;
static inline void flash_mutex_create(flash_mutex *sem, u8 count) { *sem = count; }
static inline void flash_mutex_post(flash_mutex *sem) { (*sem) = 1; }
static inline s8 flash_mutex_pend(flash_mutex *sem, u32 timeout)// 当为timeout=0时,死等
{
    u32 _timeout = timeout + jiffies;
    extern void wdt_clear();
    while (1) {
        if (*sem) { (*sem) = 0; break; }
        if ((timeout != 0) && (_timeout < jiffies)) {
            return -1;
        }
        wdt_clear();
    }
    return 0;
}
static inline s8 query_flash_mutex_pend(flash_mutex *sem, u32 timeout)
{
    while (1) {
        if (*sem) { (*sem) = 0; break; }
        return -1;
    }
    return 0;
}

Source: norflash.h

要点与隐患:

  • flash_mutex_pend 在等待期间主动 wdt_clear() 喂狗,避免长时间等锁导致看门狗复位;timeout=0 时死等。
  • query_flash_mutex_pend 是非阻塞版本(拿不到立即返回 -1),被缓存同步定时器使用:拿不到锁就放弃本轮同步,而不是阻塞定时器。
  • _norflash_read / _norflash_write_pages 中的互斥调用被注释掉(/* flash_mutex_pend(...) */),说明当前字节接口的串行化主要依赖上层调用方与缓存同步的临界区;_norflash_cache_sync_timer 在同步前用 OS_ENTER_CRITICAL() + flash_mutex_pend 双重保护。
  • flash_cache_sync_enter_critical_dis 可临时关闭临界区进入,用于避免在关中断的上下文中调用同步逻辑造成死锁——这是一处需要调用方与驱动配合的约定。

等待超时

_norflash_wait_ok 轮询 BUSY 位有固定超时;超时返回 1,上层应视为该操作失败。若擦除/编程期间掉电或芯片异常,BUSY 一直为 1,驱动会打印 norflash_wait_ok timeout! 并返回错误,不会死循环。

分区校验

  • 地址重叠:norflash_verify_part 拒绝"起始地址落在其他分区范围内且名字不同"的分区;
  • 越界:open 时 ASSERT(_norflash.max_end_addr <= _norflash.flash_capacity),分区结束地址超过 Flash 容量直接触发断言(调试期即暴露配置错误)。

Flash ID 异常

open 时若 ID 为 0 或 0xffffff(读不到芯片 / 片选错误 / SPI 配置错误),返回 -EFAULT(errno-base.h)并打印 re-open norflash id error!。norflash_dev_ioctl 在 SPI_SD_IO_REUSE 下若 sd_io_reuse_suspend() 失败(SD 正忙),打印 sd io suspend fail 并直接返回错误,避免与 SD 卡控制器争抢总线。

掉电与脏缓存

脏缓存数据在关闭设备(_norflash_close)或定时同步时才回写。若在回写前掉电,最近写入的数据会丢失("擦除→写回"不是原子的)。这是"写合并提升寿命与性能"与"掉电一致性"之间的取舍,需要上层文件系统配合(如 journal、幂等写)。_norflash_close 中同步失败只打日志不阻塞返回(注释明确"错了不能 return,只提示出错")。

大容量 Flash 的 4 字节地址

S25FL 系列 >16MB 需要 4 字节地址模式:S25FL512_enter_4byte_addr 通过读/写 Bank Register(0x16/0x17)切换地址模式并回读验证,失败返回 1;_S25FLxxx_read_id 还通过配置寄存器(0x01 命令写 0x80)设置读时钟限制(<104MHz)并回读 0x35 验证。这些步骤必须在任何读写前完成,否则大容量地址访问越界。

性能与运维考虑

  • 读路径:默认走 WINBOND_FAST_READ_DATA(0x0B)+ DMA,2BIT 模式启用双线快读;命中 4KB 缓存时零 SPI 开销。注意 4BIT 在 SPI1 上不支持,代码会打印错误并回退。
  • 写路径:页编程每页都等待 BUSY(页编程典型数百微秒);FLASH_CACHE_ENABLE 把小写合并为整扇区擦写,写性能与寿命显著提升。sync_step 可调同步频率(N×100ms),频率越低写放大越小、但掉电丢数据窗口越大。
  • 定时同步抢占:缓存同步在定时器上下文执行且可能占用 SPI 总线数百毫秒(擦除+16 页写回),SPI_SD_IO_REUSE 时需先抢占 SD 总线;若 SD 繁忙则本轮放弃。对实时性敏感的任务需评估该阻塞窗口。
  • 日志:驱动使用 LOG_TAG "[flash]",LOG_ERROR_ENABLE / LOG_INFO_ENABLE 已开启;open success、close done、wait_ok timeout、sd io suspend fail 等日志是排查 Flash 问题的主要线索。
  • 内存占用:静态缓存 norfs_cache_buf[4096](4KB)放在 .norflash_cache_buf 段,MALLOC_EN=0 时零动态分配;分区表、norflash_info 均为静态,适合小内存 MCU。

扩展点

  • 新增 Flash 型号适配:在 _norflash_open 的 ID 分支中扩展(现有 S25FL 系列 4 字节地址/配置寄存器初始化即为此模式),或参照 _S25FLxxx_read_id 增加型号专用初始化序列。
  • 新增分区/设备名:复用 norfs_dev_ops 注册新的设备名并填写不同 start_addr/size(平台数据),即可把同一物理 Flash 切分为多个逻辑设备(如存储区 + 升级区)。
  • 新增 ops 变体:源码注释明确"如果需要驱动自己处理擦除……新建一个符合需求的 ops",即复制 norfs_dev_ops 的结构,修改 read/write 回调中的 cache 实参即可实现"驱动内擦写"语义。
  • 软硬件 SPI 切换:TCFG_FLASH_SPI_TYPE_SELECT 仅影响宏映射,驱动核心逻辑不变;新增 SPI 控制器时只需补全对应宏映射。

测试覆盖

norflash.c 内置两个测试函数(默认 #if 0 关闭,且"两种 flash 不能同时运行"):

  • norflash_test():覆盖 dev_open(__EXT_FLASH_NANE)、dev_bulk_write/read(512B 块)、dev_ioctl(IOCTL_ERASE_SECTOR)、dev_byte_read/write(1B 粒度)及 log_info_hexdump 校验;
  • s25_flash_test():覆盖 dev_open("S25FL256")、按地址选择 IOCTL_ERASE_SECTOR/IOCTL_ERASE_BLOCK 的擦除路径与字节读写回读校验。

上层文件系统的读写压力测试、掉电测试需在对应文件系统目录中验证,本驱动页不赘述。

Related Links

  • norflash.h(命令集、平台数据、互斥锁)
  • norflash.c(驱动实现)
  • ini_area_norflash.c(norflash 初始化区域)
  • flash_wp.h(Flash 写保护)
  • 文件系统层(norfs/fatfs 挂载与读写逻辑)——见文件系统目录页
  • 构建与烧录布局(post_build/*/dir_ex_flash、flash_write_protect/flash_wp_info.csv)——见构建/烧录目录页
Prev
文件系统实现
Next
VM 参数存储系统