硬汉嵌入式论坛

 找回密码
 立即注册
查看: 18|回复: 1
收起左侧

[SPI/QSPI] 一个不需要任何CPU 死等的ospi_mdma_w25q512实现,大家可以提提意见

[复制链接]

7

主题

31

回帖

52

积分

初级会员

积分
52
发表于 昨天 22:48 | 显示全部楼层 |阅读模式
本帖最后由 VDVA 于 2026-6-4 22:50 编辑



一、概述
flash_task 是 W25Q512 Flash 芯片的 I/O Server,将底层异步中断驱动封装为线程安全的同步阻塞 API。

  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
  │ Scan_Task │  │Motor_Task│  │Shell_Cmd │  │Flash_Test│
  └─────┬────┘  └─────┬────┘  └─────┬────┘  └─────┬────┘
        │              │              │              │
        │ Flash_Read() │ Flash_Write()│ Flash_Erase()│ Flash_ReadID()
        ▼              ▼              ▼              ▼
  ┌─────────────────────────────────────────────────────────┐
  │              submit() — 同步阻塞入口                      │
  │  req.requester = osThreadGetId() ← 自动获取线程 ID       │
  │  投入队列 → 阻塞等 REPLY                                  │
  └──────────────────────────┬──────────────────────────────┘
                             ▼
  ┌─────────────────────────────────────────────────────────┐
  │                   Flash_Task (I/O Server)                │
  │  取请求 → 调用驱动 → 等中断 → 回写结果 → 唤醒请求者       │
  └──────────────────────────┬──────────────────────────────┘
                             ▼
  ┌─────────────────────────────────────────────────────────┐
  │         bsp_w25q512.c (底层驱动, 异步 MDMA)              │
  └─────────────────────────────────────────────────────────┘
二、线程 ID 传递机制
调用者不需要手动传线程 ID。 submit() 内部自动完成:

static flash_ret_t submit(flash_req_t *req)
{
    req->requester = osThreadGetId();  // ← 自动获取当前线程 ID
    // ...
}
工作原理
osThreadGetId() 是 CMSIS-OS2 标准 API,返回当前正在执行的线程句柄。 因为 submit() 在调用者的线程上下文中执行(尚未阻塞),所以返回的就是调用者的 ID。

Scan_Task 调用 Flash_Read()
  │
  ▼ submit() 在 Scan_Task 上下文中执行
  │
  │  osThreadGetId() → 返回 Scan_Task 的句柄
  │  存入 req->requester
  │  投入队列,阻塞...
  │
  ▼ (Scan_Task 挂起)

Flash_Task 取出请求
  │
  │  执行驱动操作...
  │  等待中断回调...
  │  回写 req->result
  │
  │  osThreadFlagsSet(req->requester, REPLY)
  │                        ▲
  │                        └── 就是 Scan_Task 的句柄,精准唤醒
  │
  ▼

Scan_Task 被唤醒,返回 FLASH_OK
任何任务调用方式完全相同
// scan_task.c
void Scan_Task(void *arg) {
    Flash_Read(buf, 0x1000, 256, 2000);   // 自动拿到 scan_task 线程 ID
}

// motor_task.c
void Motor_Task(void *arg) {
    Flash_Write(data, 0x2000, 100, 3000); // 自动拿到 motor_task 线程 ID
}

// shell_cmd.c
void cmd_flash_read(int argc, char **argv) {
    Flash_Read(buf, addr, len, 2000);     // 自动拿到 shell 线程 ID
}
三、初始化
自动注册,无需手动调用
// flash_task.c
TASK_REGISTER(flash_task_init, TASK_LEVEL_EARLY);
EARLY 级别:比其他 LATE 级别任务更早初始化
保证:当 flash_test_task、shell_task 等启动时,Flash 服务已就绪
初始化顺序
系统启动
  │
  ├─ TASK_LEVEL_EARLY
  │   └─ flash_task_init()
  │       ├─ 创建消息队列(深度 8)
  │       └─ 创建 Flash_Task 线程
  │           ├─ W25Q512_RegisterDoneCb()  ← 注册中断回调
  │           ├─ W25Q512_Init()            ← 初始化芯片
  │           └─ 进入 for(;;) 等待队列
  │
  ├─ TASK_LEVEL_NORMAL
  │   └─ scan_task_init(), motor_task_init(), ...
  │
  └─ TASK_LEVEL_LATE
      └─ shell_task_init(), flash_test_task_init(), ...
四、API 详解
4.1 返回值
typedef enum {
    FLASH_OK = 0,     // 操作成功
    FLASH_ERR,        // 硬件/驱动错误
    FLASH_TIMEOUT,    // 等待超时
} flash_ret_t;
4.2 Flash_Read — 读取
flash_ret_t Flash_Read(uint8_t *buf, uint32_t addr, uint32_t size, uint32_t timeout_ms);
参数        说明        约束
buf        目标缓冲区        必须 32 字节对齐
addr        Flash 起始地址        0x000000 ~ 0x3FFFFFF
size        读取字节数        ≥ 1
timeout_ms        超时(ms)        建议 ≥ 2000
// ✅ 正确:32 字节对齐
uint8_t buf[512] __attribute__((aligned(32)));
Flash_Read(buf, 0x100000, 512, 2000);

// ❌ 错误:未对齐 → D-Cache 破坏相邻数据
uint8_t buf[512];
Flash_Read(buf, 0x100000, 512, 2000);
4.3 Flash_Write — 写入
flash_ret_t Flash_Write(const uint8_t *buf, uint32_t addr, uint32_t size, uint32_t timeout_ms);
参数        说明        约束
buf        数据源        无需特殊对齐
addr        Flash 起始地址        目标区域须已擦除
size        写入字节数        任意大小,自动跨页拆分
timeout_ms        每页超时(ms)        建议 ≥ 3000
自动跨页拆分示例:

Flash_Write(data, 0x10F0, 600, 3000);

// 内部自动拆分为:
//   页 1: addr=0x10F0, size=16   (填满到 0x10FF)
//   页 2: addr=0x1100, size=256
//   页 3: addr=0x1200, size=256
//   页 4: addr=0x1300, size=72   (剩余数据)
4.4 Flash_EraseSector — 4KB 扇区擦除
flash_ret_t Flash_EraseSector(uint32_t addr, uint32_t timeout_ms);
参数        说明        约束
addr        扇区首地址        必须 4KB 对齐 (addr & 0xFFF == 0)
timeout_ms        超时(ms)        建议 ≥ 5000
4.5 Flash_EraseBlock64K — 64KB 块擦除
flash_ret_t Flash_EraseBlock64K(uint32_t addr, uint32_t timeout_ms);
参数        说明        约束
addr        块首地址        必须 64KB 对齐 (addr & 0xFFFF == 0)
timeout_ms        超时(ms)        建议 ≥ 10000
4.6 Flash_EraseChip — 整片擦除
flash_ret_t Flash_EraseChip(uint32_t timeout_ms);
参数        说明        约束
timeout_ms        超时(ms)        建议 ≥ 600000(10 分钟)
4.7 Flash_ReadID — 读取 JEDEC ID
uint32_t Flash_ReadID(void);
返回值        说明
0xEF4020        正常(W25Q512)
0        通信失败
五、典型使用场景
5.1 保存/加载配置参数
#define CONFIG_ADDR  0x3F0000  /* 芯片末尾 */

typedef struct {
    float target_flow;
    uint8_t mode;
    uint32_t checksum;
} device_config_t;

void save_config(const device_config_t *cfg)
{
    Flash_EraseSector(CONFIG_ADDR, 5000);
    Flash_Write((const uint8_t *)cfg, CONFIG_ADDR,
                sizeof(device_config_t), 3000);
}

bool load_config(device_config_t *cfg)
{
    uint8_t tmp[sizeof(device_config_t)] __attribute__((aligned(32)));
    if (Flash_Read(tmp, CONFIG_ADDR, sizeof(tmp), 2000) != FLASH_OK)
        return false;
    memcpy(cfg, tmp, sizeof(*cfg));
    return true;
}
5.2 日志存储(循环写入)
static uint32_t log_addr = 0x000000;

void append_log(const uint8_t *entry, uint32_t len)
{
    /* 当前扇区写满?擦除下一个 */
    if ((log_addr % W25Q512_SECTOR_SIZE) == 0 && log_addr > 0)
    {
        Flash_EraseSector(log_addr, 5000);
    }

    if (Flash_Write(entry, log_addr, len, 3000) == FLASH_OK)
    {
        log_addr += len;
    }
}
5.3 连通性自检
bool flash_self_check(void)
{
    uint32_t id = Flash_ReadID();
    if (id != W25Q512_JEDEC_ID) return false;

    uint8_t w[256], r[256] __attribute__((aligned(32)));
    for (int i = 0; i < 256; i++) w = (uint8_t)i;

    Flash_EraseSector(0x3FFF00, 5000);
    Flash_Write(w, 0x3FFF00, 256, 3000);
    Flash_Read(r, 0x3FFF00, 256, 2000);

    return (memcmp(w, r, 256) == 0);
}
5.4 OTA 固件写入
flash_ret_t ota_write_firmware(const uint8_t *fw, uint32_t fw_size)
{
    #define FW_ADDR  0x100000  /* 固件存储区 */

    /* 擦除固件区(按 64KB 块擦除,效率更高) */
    uint32_t blocks = (fw_size + W25Q512_BLOCK64K_SIZE - 1) / W25Q512_BLOCK64K_SIZE;
    for (uint32_t i = 0; i < blocks; i++)
    {
        flash_ret_t r = Flash_EraseBlock64K(FW_ADDR + i * W25Q512_BLOCK64K_SIZE, 10000);
        if (r != FLASH_OK) return r;
    }

    /* 写入固件(自动跨页) */
    return Flash_Write(fw, FW_ADDR, fw_size, 5000);
}
六、超时值选取参考
操作        典型耗时        最大耗时        建议超时
Flash_Read        < 1 ms        ~10 ms        2000 ms
Flash_Write (单页 256B)        0.4~3 ms        ~10 ms        3000 ms
Flash_Write (4KB 扇区)        ~50 ms        ~200 ms        5000 ms
Flash_EraseSector (4KB)        45~400 ms        ~800 ms        5000 ms
Flash_EraseBlock64K (64KB)        150~2000 ms        ~3000 ms        10000 ms
Flash_EraseChip (64MB)        100~300 s        ~600 s        600000 ms
七、注意事项
7.1 只能在任务中调用
// &#9989; 任务上下文: 可以阻塞
void My_Task(void *arg) {
    Flash_Write(data, addr, 256, 3000);  // OK
}

// &#10060; 中断上下文: 会崩溃!
void EXTI_IRQHandler(void) {
    Flash_Write(data, addr, 256, 3000);  // osThreadFlagsWait → 断言失败
}

// &#10060; 驱动回调中: 会崩溃!
void my_done_cb(w25q_evt_t evt) {
    Flash_Read(buf, addr, 256, 2000);    // 死锁 + 崩溃
}
7.2 读缓冲区必须对齐
// &#9989; 32 字节对齐
static uint8_t buf[4096] __attribute__((aligned(32)));
Flash_Read(buf, addr, 4096, 2000);

// &#10060; 未对齐 → D-Cache Invalidate 破坏相邻内存
uint8_t buf[4096];
Flash_Read(buf, addr, 4096, 2000);  // 危险!
7.3 写前必须擦除
Flash 物理特性:只能 1→0,不能 0→1。

// &#10060; 未擦除就写入 → 数据不正确
Flash_Write(data, 0x5000, 256, 3000);

// &#9989; 先擦除再写入
Flash_EraseSector(0x5000, 5000);     // 0x5000 必须 4KB 对齐
Flash_Write(data, 0x5000, 256, 3000);
7.4 地址对齐
Flash_EraseSector(0x1000, 5000);     // &#9989; 4KB 对齐
Flash_EraseSector(0x1234, 5000);     // &#10060; 未对齐

Flash_EraseBlock64K(0x10000, 10000); // &#9989; 64KB 对齐
Flash_EraseBlock64K(0x5000, 10000);  // &#10060; 未对齐
7.5 队列深度
消息队列深度为 8。最多同时排队 8 个请求。 若第 9 个请求到达,调用者会在 osMessageQueuePut 处阻塞(等队列有空位)。 正常情况下不会发生——Flash_Task 串行处理请求,每个请求完成后队列立即空出一个位置。

八、内部请求流程(调试参考)
8.1 请求结构体
typedef struct {
    req_op_t        op;            // 操作类型
    uint8_t        *buf;           // 数据缓冲区
    uint32_t        addr;          // Flash 地址
    uint32_t        size;          // 数据大小
    uint32_t        timeout_ms;    // 超时(ms)
    osThreadId_t    requester;     // ★ 自动填充的调用者线程 ID ★
    volatile flash_ret_t result;   // 操作结果(Flash_Task 写回)
} flash_req_t;
8.2 标志位
标志位          值     设到谁            何时设置
─────────────────────────────────────────────────────
FLASH_FLAG_DONE  0x01  Flash_Task        ISR: 驱动操作成功
FLASH_FLAG_ERR   0x02  Flash_Task        ISR: 驱动操作出错
FLASH_FLAG_REPLY 0x04  请求者线程        Flash_Task: 请求处理完成
8.3 写操作完整时序
Scan_Task                    Flash_Task                   驱动/硬件
─────────                    ──────────                   ─────────
Flash_Write(buf, 0x10F0, 600, 3000)

├─ req = {WRITE, buf, 0x10F0, 600, 3000}
├─ req.requester = osThreadGetId()
│   (Scan_Task 的句柄)
├─ Queue.put(&req) ──────&#9658; Queue.get(&req)
│                               │
├─ FlagsWait(REPLY)             ├─ exec_write(req)
│   (阻塞, CPU 让出)            │
│                               ├─ 页1: WritePage_DMA(buf, 0x10F0, 16)
│                               │        │
│                               │        │  MDMA 传输...
│                               │        │  Flash 内部编程...
│                               │        │  ISR: drv_done_cb(DONE)
│                               │        ▼
│                               │   FlagsSet(Flash_Task, DONE)
│                               │        │
│                               │        ▼
│                               │   FlagsWait → 收到 DONE
│                               │
│                               ├─ 页2: WritePage_DMA(buf+16, 0x1100, 256)
│                               │   ... (同上)
│                               │
│                               ├─ 页3: WritePage_DMA(buf+272, 0x1200, 256)
│                               │   ... (同上)
│                               │
│                               ├─ 页4: WritePage_DMA(buf+528, 0x1300, 72)
│                               │   ... (同上)
│                               │
│                               ├─ req->result = FLASH_OK
│                               │
│&#9668;── FlagsSet(Scan_Task, REPLY)─┤
│                               │
├─ 返回 FLASH_OK                │ 继续取下一个请求
│   读 req->result

九、架构分层总结
┌────────────────────────────────────────────────────────────────┐
│  应用层 (scan_task / motor_task / shell_cmd / flash_test)       │
│  调用 Flash_Read / Write / Erase 同步 API                       │
├────────────────────────────────────────────────────────────────┤
│  flash_task.h / flash_task.c (I/O Server)                      │
│  消息队列接收请求 → 驱动操作 → 中断等待 → 回复请求者             │
│  线程 ID 自动获取(osThreadGetId),调用者无感                     │
├────────────────────────────────────────────────────────────────┤
│  bsp_w25q512.h / bsp_w25q512.c (底层驱动, 异步)                │
│  MDMA + AutoPolling + 中断回调 → ThreadFlagsSet(DONE/ERR)       │
├────────────────────────────────────────────────────────────────┤
│  HAL_OSPI + MDMA (STM32 HAL 库)                                │
├────────────────────────────────────────────────────────────────┤
│  W25Q512 芯片 (OCTOSPI / SPI Flash)                            │
└────────────────────────────────────────────────────────────────┘
十、API 速查表
函数        用途        关键约束
Flash_Read(buf, addr, size, ms)        读取数据        buf 须 32B 对齐
Flash_Write(buf, addr, size, ms)        写入数据(自动跨页)        须先擦除
Flash_EraseSector(addr, ms)        擦除 4KB        addr 须 4KB 对齐
Flash_EraseBlock64K(addr, ms)        擦除 64KB        addr 须 64KB 对齐
Flash_EraseChip(ms)        整片擦除        耗时 ~100-300s
Flash_ReadID()        读 JEDEC ID        正常返回 0xEF4020

bsp_w25q512.c

31.21 KB, 下载次数: 0

bsp_w25q512.h

13.42 KB, 下载次数: 0

flash_task.c

19.38 KB, 下载次数: 0

flash_task.h

12.22 KB, 下载次数: 0

mdma.c

1.65 KB, 下载次数: 0

octospi.c

7.16 KB, 下载次数: 0

回复

使用道具 举报

9

主题

110

回帖

137

积分

初级会员

积分
137
发表于 昨天 22:55 | 显示全部楼层
ospi感觉还是用得比较少,看看硬汉哥后面v8的板子会不会考虑整一个?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2026-6-5 01:49 , Processed in 0.221496 second(s), 25 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表