第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发来一个写命令。

  1. 中断层:UFS接口收到命令,触发中断。ISR里把命令塞进队列,然后退出。
  2. 主循环:轮询到队列里有新命令,调用API层的命令处理函数。
  3. API层:解析命令,校验参数,然后调用Core层的写函数。
  4. Core层:启动一个写状态机,状态机里调用HAL层的NAND编程函数。
  5. HAL层:操作寄存器,启动DMA,等待中断或轮询完成。

整个过程,每一层各司其职。中断只做最紧急的事,主循环做耗时的事,状态机管理复杂的操作流程。这样设计出来的固件,可读性强、可维护性好、bug也少。

最后说一句:架构设计没有银弹。不同的存储芯片(NAND、NOR、SSD、UFS),对实时性、吞吐量、功耗的要求不一样,架构也要相应调整。但模块化、分层、状态机这三个思想,是通用的。你把这个骨架搭好了,后面写代码就轻松多了。