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检测到启动失败,自动切回旧镜像。
具体怎么实现?我一般分三步:
- 硬件看门狗:在PCB上放一个独立的看门狗芯片,比如MAX706。它的输出接到FPGA的复位引脚。FPGA正常工作时要定期拉高一个GPIO来喂狗。
- 软件看门狗:在FPGA逻辑里也实现一个软看门狗。这个软看门狗监控固件升级流程,如果某个步骤超时了,就主动触发复位。
- 启动计数器:在配置参数区里存一个启动计数器。每次系统启动,计数器加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通过QSPI跟Flash通信,Flash里分好了四个区域。硬件看门狗盯着FPGA,一旦出问题就复位。Bootloader根据配置参数区的信息,决定加载哪个镜像。
说白了,这套架构的核心思想就是:永远给自己留一条后路。Golden镜像就是你的救命稻草,看门狗就是你的安全网。只要这两样东西在,系统就死不了。
个人经验:我建议在调试阶段,把看门狗的喂狗时间设长一点,比如5秒。这样方便你用调试器单步执行。等产品稳定了,再改回1秒。别问我怎么知道的,问就是吃过亏。
好了,硬件架构这块就聊到这儿。记住,设计时多想想「如果这里出错了怎么办」,把各种异常场景都考虑进去。这样你的系统才能真正做到可靠升级。