3、硬件架构设计:FPGA与Flash的接口设计、双镜像存储区布局、看门狗与回滚机制

好,咱们直接切入正题。这一章讲的是固件升级的硬件根基。说白了,你软件写得再漂亮,如果硬件架构没搭好,升级过程中一旦断电或者传错数据,板子就变砖了。我这些年见过太多这种案例了。

我个人习惯,在设计FPGA交易系统的固件升级方案时,先把硬件架构的骨架立起来。这个骨架就三块:FPGA怎么跟Flash通信Flash里两个镜像怎么摆出问题了怎么自动滚回去。咱们一个一个来拆。

3.1 FPGA与Flash的接口设计

FPGA跟Flash打交道,最常用的就是SPI接口。为什么?因为引脚少,布线简单,而且市面上大容量的NOR Flash基本都是SPI接口。你想想看,交易系统里FPGA的引脚本来就金贵,能省则省。

我一般推荐使用四线SPI(Quad SPI),也就是QSPI。相比标准SPI,它多了两根数据线,读速度能跑到几百兆赫兹。对于固件升级来说,这速度完全够用了。

接口设计上,有几个关键点要注意:

  • 时钟频率:别一味追求高频。Flash芯片有最高频率限制,PCB走线也有损耗。我建议先跑个50MHz,稳定了再往上调。
  • 片选信号:这个信号必须干净,不能有毛刺。否则Flash会误判,数据就全乱了。
  • 数据线:QSPI的四根数据线,长度要尽量等长。我在一个项目里吃过亏,两根线差了2厘米,结果高频下数据老是出错。

核心要点:FPGA内部要例化一个QSPI控制器,这个控制器负责产生时序、处理命令。我习惯用状态机来实现,简单可靠,不依赖任何IP核。

下面是我常用的QSPI控制器状态机框架:

// 简化版QSPI写操作状态机
typedef enum {
    IDLE,
    SEND_CMD,
    SEND_ADDR,
    SEND_DATA,
    WAIT_DONE
} qspi_state_t;

always @(posedge clk) begin
    case(state)
        IDLE: begin
            if(start_write) state <= SEND_CMD;
        end
        SEND_CMD: begin
            // 发送写使能或页编程命令
            if(bit_counter == 7) state <= SEND_ADDR;
        end
        SEND_ADDR: begin
            // 发送24位或32位地址
            if(addr_done) state <= SEND_DATA;
        end
        SEND_DATA: begin
            // 发送数据,注意页边界
            if(page_full) state <= WAIT_DONE;
        end
        WAIT_DONE: begin
            // 等待Flash内部编程完成
            if(busy_flag == 0) state <= IDLE;
        end
    endcase
end

小技巧:在FPGA里,我习惯把QSPI的时钟和数据信号都做一次同步处理,用FPGA的主时钟打两拍。这样可以有效避免跨时钟域的问题。

3.2 双镜像存储区布局

双镜像,说白了就是Flash里存两份固件。一份是当前正在跑的,另一份是待升级的。这样即使升级过程中断电,至少还有一份能启动。

Flash的存储区怎么划分?我一般这么干:

区域 起始地址 大小 内容
Bootloader 0x000000 64KB 启动加载程序,负责校验和跳转
镜像A(Golden) 0x010000 1MB 出厂固件,只读,永不修改
镜像B(Update) 0x110000 1MB 可升级固件,升级时写入
配置参数区 0x210000 64KB 存储启动次数、版本号、校验值

这个布局有什么讲究?

  • Bootloader放在最前面:FPGA上电后,先从0地址加载Bootloader。Bootloader负责决定加载哪个镜像。
  • 镜像A是Golden镜像:这个镜像在出厂时就烧好了,之后永远不擦除、不修改。它是最后的保底方案。
  • 镜像B是升级目标:每次升级,新固件都写到这个区域。如果升级成功,下次启动就加载它。
  • 配置参数区:这里存一些关键信息,比如当前应该启动哪个镜像、启动失败了几次。

注意:Flash的擦除操作是按扇区来的,一个扇区通常是4KB或64KB。所以分区时,每个区域的起始地址和大小都必须是扇区大小的整数倍。否则你会遇到擦除不干净的问题。

我曾经在一个项目里,因为分区没对齐,导致升级时把Bootloader给擦掉了。那叫一个惨,最后只能拆机用编程器重烧。所以,分区对齐这事,千万别马虎。

3.3 看门狗与回滚机制

看门狗,就是系统的一个「监工」。它盯着FPGA,如果FPGA在规定时间内没给它「喂狗」,它就认为FPGA死机了,然后强制复位。

在固件升级场景里,看门狗的作用尤其重要。你想啊,如果新固件有bug,FPGA加载后直接跑飞了,怎么办?看门狗就会复位,然后Bootloader检测到启动失败,自动切回旧镜像。

具体怎么实现?我一般分三步:

  1. 硬件看门狗:在PCB上放一个独立的看门狗芯片,比如MAX706。它的输出接到FPGA的复位引脚。FPGA正常工作时要定期拉高一个GPIO来喂狗。
  2. 软件看门狗:在FPGA逻辑里也实现一个软看门狗。这个软看门狗监控固件升级流程,如果某个步骤超时了,就主动触发复位。
  3. 启动计数器:在配置参数区里存一个启动计数器。每次系统启动,计数器加1。如果启动成功,计数器清零。如果计数器连续几次都大于某个阈值(比如3次),就说明新固件有问题,自动回滚。

回滚的流程是这样的:

// 伪代码:Bootloader启动流程
void bootloader_main() {
    // 1. 读取配置参数
    boot_count = read_config(BOOT_COUNT_ADDR);
    boot_target = read_config(BOOT_TARGET_ADDR);
    
    // 2. 判断是否需要回滚
    if (boot_count > MAX_RETRY) {
        // 启动失败次数过多,回滚到Golden镜像
        boot_target = GOLDEN_IMAGE;
        write_config(BOOT_TARGET_ADDR, GOLDEN_IMAGE);
        boot_count = 0;
        write_config(BOOT_COUNT_ADDR, 0);
    }
    
    // 3. 启动计数器加1
    boot_count++;
    write_config(BOOT_COUNT_ADDR, boot_count);
    
    // 4. 加载目标镜像
    load_image(boot_target);
    
    // 5. 如果加载成功,应用程序会清零启动计数器
    // 如果加载失败,看门狗会复位,重新执行本流程
}

关键点:看门狗的喂狗时间要设置合理。太短了,正常流程还没跑完就被复位了。太长了,死机了要等半天才恢复。我一般设成1秒,对于FPGA来说,1秒足够完成所有初始化工作了。

嗯,这里还要提一个坑。我曾经遇到过一个问题:看门狗复位后,FPGA重新加载固件,但Flash里的配置参数区因为频繁写入而损坏了。后来我加了ECC校验,每次写入都计算校验和,读取时校验。这样即使个别bit翻转,也能纠正过来。

最后,我用一张图来总结整个硬件架构的逻辑:

FPGA固件升级硬件架构图 FPGA QSPI控制器 软看门狗 启动逻辑 NOR Flash Bootloader (64KB) 镜像A - Golden (1MB) 镜像B - Update (1MB) 配置参数区 (64KB) 启动计数器 版本号 校验值 硬件看门狗 MAX706 QSPI (CLK, CS, IO0-IO3) 喂狗信号 复位信号 升级流程: 1. 新固件通过QSPI写入镜像B区域 2. 写入完成后,更新配置参数区,标记下次启动镜像B 3. 系统复位,Bootloader加载镜像B 4. 如果启动失败,看门狗复位,回滚到镜像A

这张图把整个硬件架构串起来了。FPGA通过QSPI跟Flash通信,Flash里分好了四个区域。硬件看门狗盯着FPGA,一旦出问题就复位。Bootloader根据配置参数区的信息,决定加载哪个镜像。

说白了,这套架构的核心思想就是:永远给自己留一条后路。Golden镜像就是你的救命稻草,看门狗就是你的安全网。只要这两样东西在,系统就死不了。

个人经验:我建议在调试阶段,把看门狗的喂狗时间设长一点,比如5秒。这样方便你用调试器单步执行。等产品稳定了,再改回1秒。别问我怎么知道的,问就是吃过亏。

好了,硬件架构这块就聊到这儿。记住,设计时多想想「如果这里出错了怎么办」,把各种异常场景都考虑进去。这样你的系统才能真正做到可靠升级。