3、驱动框架设计:分层架构设计
做嵌入式驱动开发这些年,我最大的体会就是:没有好的框架,代码迟早会变成一团乱麻。尤其是NOR Flash这种底层存储设备,操作时序复杂、不同厂商寄存器差异大,如果不做分层设计,后期维护起来真是欲哭无泪。
今天我们就来聊聊驱动框架的核心——分层架构。我个人习惯把NOR Flash驱动分成三层:HAL层、驱动层、应用层。每一层各司其职,互不干扰。
3.1 分层架构设计(HAL层、驱动层、应用层)
先看一张我画的架构图,这样你理解起来更直观:
这张图我画了好几次才满意。你看,从上到下是应用层 → 驱动层 → 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;
状态转换图如下:
状态机的核心逻辑:
/* 状态机处理 */
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;
}
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