3、驱动框架设计:分层架构设计

做嵌入式驱动开发这些年,我最大的体会就是:没有好的框架,代码迟早会变成一团乱麻。尤其是NOR Flash这种底层存储设备,操作时序复杂、不同厂商寄存器差异大,如果不做分层设计,后期维护起来真是欲哭无泪。

今天我们就来聊聊驱动框架的核心——分层架构。我个人习惯把NOR Flash驱动分成三层:HAL层、驱动层、应用层。每一层各司其职,互不干扰。

3.1 分层架构设计(HAL层、驱动层、应用层)

先看一张我画的架构图,这样你理解起来更直观:

NOR Flash 驱动分层架构 应用层(Application Layer) 文件系统接口(FATFS/LittleFS) 用户API:flash_read() / flash_write() / flash_erase() 驱动层(Driver Layer) 设备对象管理:初始化、读写、擦除、状态查询 状态机管理:IDLE → READY → BUSY → ERROR 错误处理与重试机制 HAL层(硬件抽象层) SPI/QSPI接口封装:spi_transfer() / qspi_read() GPIO控制:片选、写保护、复位 时序配置:时钟频率、模式设置 硬件层:NOR Flash芯片(W25Q64 / S25FL128 等)

这张图我画了好几次才满意。你看,从上到下是应用层 → 驱动层 → HAL层,每一层只跟相邻层打交道。这样做的好处是什么?说白了就是解耦

3.1.1 HAL层(硬件抽象层)

HAL层是最底层的代码,直接跟硬件寄存器打交道。我记得刚开始做驱动时,总喜欢把SPI读写操作散落在各个函数里,结果换了个MCU平台,改代码改到崩溃。

后来我学乖了——把所有硬件相关的操作全部封装在HAL层。比如:

/* HAL层接口定义 */
typedef struct {
    int (*spi_transfer)(uint8_t *tx, uint8_t *rx, uint32_t len);
    int (*gpio_set_cs)(uint8_t level);
    int (*delay_us)(uint32_t us);
} nor_flash_hal_t;

这样设计后,换平台只需要重新实现这3个函数。嗯,这就是抽象的力量。

3.1.2 驱动层(核心逻辑层)

驱动层是整块代码的心脏。它不关心底层是SPI还是QSPI,只关心NOR Flash的协议逻辑。

举个例子,NOR Flash的页编程操作:

/* 驱动层 - 页编程 */
int nor_flash_page_program(nor_flash_dev_t *dev, uint32_t addr, 
                           const uint8_t *data, uint32_t len) {
    int ret;
    
    /* 检查状态 */
    if (dev->state != NOR_STATE_READY) {
        return NOR_ERR_BUSY;
    }
    
    /* 写使能 */
    ret = nor_flash_write_enable(dev);
    if (ret != NOR_OK) return ret;
    
    /* 发送页编程命令 */
    dev->hal->spi_transfer(cmd_buf, NULL, 4);
    dev->hal->spi_transfer((uint8_t*)data, NULL, len);
    
    /* 等待完成 */
    ret = nor_flash_wait_ready(dev, 1000);
    
    return ret;
}

你看,驱动层只调用HAL层的spi_transfer,完全不关心底层实现。我在项目中遇到过好几次这样的情况:产品量产时发现某批次Flash芯片时序有差异,只需要微调HAL层的延时函数,驱动层代码一行都不用改。

3.1.3 应用层(用户接口层)

应用层是给上层软件(比如文件系统、OTA升级模块)调用的。它提供最简洁的API:

/* 应用层API */
int flash_read(uint32_t addr, uint8_t *buf, uint32_t size);
int flash_write(uint32_t addr, const uint8_t *buf, uint32_t size);
int flash_erase(uint32_t addr, uint32_t size);
int flash_ioctl(uint32_t cmd, void *arg);

应用层还会做一些逻辑校验,比如地址对齐检查、长度合法性判断。这些脏活累活,不应该让驱动层去操心。

3.2 抽象设备对象模型

做驱动开发,我最推崇的就是面向对象思想。虽然C语言没有class,但我们可以用结构体模拟。

来看一个典型的NOR Flash设备对象:

/* NOR Flash设备对象 */
typedef struct {
    /* 设备信息 */
    char name[16];
    uint32_t chip_id;
    uint32_t capacity;      /* 容量,单位字节 */
    uint32_t page_size;     /* 页大小 */
    uint32_t sector_size;   /* 扇区大小 */
    uint32_t block_size;    /* 块大小 */
    
    /* 运行时状态 */
    nor_state_t state;      /* 当前状态 */
    uint32_t error_count;   /* 错误计数 */
    uint32_t last_error;    /* 最后一次错误码 */
    
    /* 硬件接口 */
    nor_flash_hal_t *hal;   /* HAL层接口指针 */
    
    /* 回调函数 */
    void (*on_error)(struct nor_flash_dev *dev, uint32_t err);
    
    /* 私有数据 */
    void *priv;
} nor_flash_dev_t;

这个结构体包含了设备属性、运行时状态、硬件接口、回调机制。我习惯把每个Flash芯片都实例化成一个这样的对象。如果板子上有2片Flash,就创建2个实例,互不干扰。

我的小技巧:priv指针里存放一些平台相关的数据,比如SPI句柄、DMA通道号。这样即使同一个驱动要适配不同MCU,也只需要改priv指向的内容。

3.3 错误处理与状态机设计

NOR Flash操作过程中,错误处理是重中之重。我曾经在一个项目中,因为没处理好写操作超时,导致整个系统卡死,产品在客户现场频繁死机。那次教训太深刻了。

所以我设计了一个有限状态机来管理设备状态:

/* 状态定义 */
typedef enum {
    NOR_STATE_UNINIT = 0,   /* 未初始化 */
    NOR_STATE_IDLE,         /* 空闲 */
    NOR_STATE_READY,        /* 就绪 */
    NOR_STATE_BUSY,         /* 忙 */
    NOR_STATE_ERROR,        /* 错误 */
    NOR_STATE_SUSPEND       /* 挂起(用于低功耗) */
} nor_state_t;

状态转换图如下:

UNINIT IDLE READY BUSY ERROR SUSPEND init() probe() 操作开始 超时/校验错 恢复成功 操作完成 suspend() resume()

状态机的核心逻辑:

/* 状态机处理 */
int nor_flash_state_machine(nor_flash_dev_t *dev) {
    switch (dev->state) {
        case NOR_STATE_UNINIT:
            /* 执行初始化 */
            if (nor_flash_hw_init(dev) == NOR_OK) {
                dev->state = NOR_STATE_IDLE;
            }
            break;
            
        case NOR_STATE_BUSY:
            /* 检查是否完成 */
            if (nor_flash_check_ready(dev) == NOR_OK) {
                dev->state = NOR_STATE_READY;
            } else if (dev->timeout_count++ > MAX_TIMEOUT) {
                dev->state = NOR_STATE_ERROR;
                dev->last_error = NOR_ERR_TIMEOUT;
                /* 触发错误回调 */
                if (dev->on_error) {
                    dev->on_error(dev, NOR_ERR_TIMEOUT);
                }
            }
            break;
            
        case NOR_STATE_ERROR:
            /* 尝试恢复 */
            if (nor_flash_reset(dev) == NOR_OK) {
                dev->state = NOR_STATE_IDLE;
            }
            break;
            
        default:
            break;
    }
    return NOR_OK;
}
注意: 状态机一定要有超时保护。NOR Flash在极端情况下(比如电压不稳)可能会一直处于BUSY状态。没有超时机制,系统就死锁了。我吃过这个亏,现在每个状态转换都加了超时判断。

3.4 驱动初始化流程

驱动初始化看似简单,但顺序搞错了,后面全白搭。我总结了一套标准初始化流程

/* 驱动初始化 */
int nor_flash_driver_init(nor_flash_dev_t *dev, nor_flash_hal_t *hal) {
    int ret;
    
    /* 1. 清空设备对象 */
    memset(dev, 0, sizeof(nor_flash_dev_t));
    
    /* 2. 绑定HAL接口 */
    dev->hal = hal;
    
    /* 3. 硬件初始化(HAL层) */
    ret = dev->hal->gpio_set_cs(1);  /* 片选拉高 */
    if (ret != NOR_OK) return ret;
    
    /* 4. 复位Flash芯片 */
    ret = nor_flash_software_reset(dev);
    if (ret != NOR_OK) return ret;
    
    /* 5. 读取芯片ID */
    ret = nor_flash_read_id(dev, &dev->chip_id);
    if (ret != NOR_OK) return ret;
    
    /* 6. 根据ID配置参数 */
    ret = nor_flash_configure(dev);
    if (ret != NOR_OK) return ret;
    
    /* 7. 设置初始状态 */
    dev->state = NOR_STATE_IDLE;
    dev->error_count = 0;
    
    return NOR_OK;
}

这里有个细节:先复位再读ID。为什么?因为芯片上电后可能处于未知状态,直接读ID可能读到错误数据。我在项目中遇到过,某批次芯片上电后默认进入4字节地址模式,读ID指令被解析错误,导致初始化失败。加上复位操作后,问题就解决了。

初始化检查清单:

  • ✅ HAL接口指针非空检查
  • ✅ 硬件复位时序正确
  • ✅ 芯片ID读取并校验
  • ✅ 根据ID自动配置页大小、扇区大小
  • ✅ 状态机初始化为IDLE
  • ✅ 错误计数清零

好了,以上就是驱动框架设计的核心内容。分层架构让代码清晰,设备对象模型让管理方便,状态机让错误处理可靠,初始化流程让启动稳健。这些经验都是我在实际项目中一点点积累起来的,希望能帮到你。


公众号:蓝海资料掘金营,微信deep3321