第3章:固件架构设计:模块化分层设计、主循环与中断架构、状态机设计模式
好,咱们直接进入正题。固件架构这事儿,说白了就是给代码搭骨架。骨架搭得好,后面写代码就像搭积木一样顺畅。骨架搭歪了,后期改一个bug能牵出一串问题。我在项目里见过太多“屎山”代码,都是因为一开始没想清楚架构。
3.1 模块化分层设计
我个人习惯,把存储芯片固件分成三层:硬件抽象层(HAL)、核心逻辑层(Core)、应用接口层(API)。你想想看,这样分有什么好处?
- 硬件抽象层:直接跟寄存器打交道。比如NAND Flash的读写时序、ECC引擎的配置、DMA的启动。这一层只做一件事:把硬件操作封装成函数。
- 核心逻辑层:处理FTL(闪存转换层)、坏块管理、垃圾回收。这一层不关心具体怎么操作硬件,它只调用HAL提供的接口。
- 应用接口层:给上层Host用的。比如读扇区、写扇区、擦除、获取状态。这一层负责解析命令、校验参数、调用Core层。
关键原则:每一层只能调用它的下一层,不能跨层调用。Core层不能直接写寄存器,API层不能直接操作硬件。这是铁律。
我在项目中遇到过一件事:有个同事图省事,在API层直接调用了HAL的NAND读写函数。结果后来换了一颗Flash芯片,HAL层接口变了,他得把API层所有相关代码全改一遍。嗯,从那以后,我再也不敢允许跨层调用了。
3.2 主循环与中断架构
存储芯片固件,本质上是个事件驱动的系统。Host发来命令,固件处理,返回结果。那怎么组织这个处理流程呢?
我常用的方案是:主循环 + 中断。主循环负责轮询和处理耗时任务,中断负责响应紧急事件。
// 伪代码示例:主循环架构
void main_loop(void) {
while(1) {
// 1. 检查是否有新命令
if (cmd_queue_not_empty()) {
process_command(cmd_dequeue());
}
// 2. 后台任务:垃圾回收、磨损均衡
if (background_task_needed()) {
do_background_task();
}
// 3. 看门狗喂狗
watchdog_refresh();
// 4. 低功耗处理
if (system_idle()) {
enter_low_power_mode();
}
}
}
中断呢?主要处理三类事情:
- 命令到达中断:Host通过接口(比如UFS、eMMC、NVMe)发来命令,中断里把命令塞进队列,然后唤醒主循环。
- DMA完成中断:数据搬完了,通知Core层可以开始处理了。
- 错误中断:比如ECC纠错失败、电源异常。这些需要立即响应,不能等主循环慢慢轮询。
我的经验:中断里尽量少做事。只做标记、塞队列、清中断标志。真正耗时的处理,放到主循环里做。否则中断嵌套能把系统搞崩。
为什么会这样?因为中断服务程序(ISR)执行时间太长,会影响其他中断的响应。我曾经在一个项目里,ISR里做了NAND的读操作,结果读一次要几十微秒,导致UFS接口的协议超时。嗯,后来我把读操作挪到了主循环,问题就解决了。
3.3 状态机设计模式
存储芯片固件里,状态机无处不在。比如:
- NAND Flash操作状态机:空闲 → 编程 → 读状态 → 完成/失败
- 命令处理状态机:接收命令 → 解析参数 → 执行操作 → 返回结果
- 电源管理状态机:正常 → 空闲 → 睡眠 → 唤醒
我建议用有限状态机(FSM)来建模。别小看这个,很多bug都是因为状态跳转没考虑全。
// 状态机示例:NAND编程操作
typedef enum {
STATE_IDLE,
STATE_PROGRAM_SETUP,
STATE_PROGRAM_EXEC,
STATE_PROGRAM_VERIFY,
STATE_PROGRAM_DONE,
STATE_PROGRAM_FAIL
} nand_program_state_t;
nand_program_state_t current_state = STATE_IDLE;
void nand_program_fsm(void) {
switch(current_state) {
case STATE_IDLE:
if (program_request_pending()) {
setup_program_parameters();
current_state = STATE_PROGRAM_SETUP;
}
break;
case STATE_PROGRAM_SETUP:
send_program_cmd();
start_dma_transfer();
current_state = STATE_PROGRAM_EXEC;
break;
case STATE_PROGRAM_EXEC:
if (dma_complete()) {
start_verify_read();
current_state = STATE_PROGRAM_VERIFY;
}
break;
case STATE_PROGRAM_VERIFY:
if (verify_pass()) {
current_state = STATE_PROGRAM_DONE;
} else {
if (retry_count < MAX_RETRIES) {
retry_count++;
current_state = STATE_PROGRAM_SETUP;
} else {
current_state = STATE_PROGRAM_FAIL;
}
}
break;
case STATE_PROGRAM_DONE:
notify_host_success();
current_state = STATE_IDLE;
break;
case STATE_PROGRAM_FAIL:
mark_block_bad();
notify_host_fail();
current_state = STATE_IDLE;
break;
}
}
注意:状态机一定要有超时处理。我曾经遇到过NAND卡在编程状态,因为硬件没响应,状态机就死在那了。后来我加了个超时定时器,超时后强制跳转到失败状态,问题就解决了。
状态机设计还有个要点:状态转移表。我习惯用表格来梳理所有可能的转移路径,避免遗漏。
| 当前状态 | 触发事件 | 下一状态 | 动作 |
|---|---|---|---|
| IDLE | 编程请求 | PROGRAM_SETUP | 设置参数 |
| PROGRAM_SETUP | DMA完成 | PROGRAM_EXEC | 发送编程命令 |
| PROGRAM_EXEC | 状态寄存器就绪 | PROGRAM_VERIFY | 启动校验读 |
| PROGRAM_VERIFY | 校验通过 | PROGRAM_DONE | 通知成功 |
| PROGRAM_VERIFY | 校验失败且重试次数未超 | PROGRAM_SETUP | 重试 |
| PROGRAM_VERIFY | 校验失败且重试次数超限 | PROGRAM_FAIL | 标记坏块 |
| PROGRAM_DONE | 通知完成 | IDLE | 清状态 |
| PROGRAM_FAIL | 通知失败 | IDLE | 清状态 |
你看,这样一列,所有路径都清清楚楚。写代码的时候照着表来,基本不会漏状态。
3.4 三种架构如何配合
模块化分层、主循环+中断、状态机,这三者不是孤立的。它们要配合起来用。
举个例子:Host发来一个写命令。
- 中断层:UFS接口收到命令,触发中断。ISR里把命令塞进队列,然后退出。
- 主循环:轮询到队列里有新命令,调用API层的命令处理函数。
- API层:解析命令,校验参数,然后调用Core层的写函数。
- Core层:启动一个写状态机,状态机里调用HAL层的NAND编程函数。
- HAL层:操作寄存器,启动DMA,等待中断或轮询完成。
整个过程,每一层各司其职。中断只做最紧急的事,主循环做耗时的事,状态机管理复杂的操作流程。这样设计出来的固件,可读性强、可维护性好、bug也少。
最后说一句:架构设计没有银弹。不同的存储芯片(NAND、NOR、SSD、UFS),对实时性、吞吐量、功耗的要求不一样,架构也要相应调整。但模块化、分层、状态机这三个思想,是通用的。你把这个骨架搭好了,后面写代码就轻松多了。