☰
Nios II工程生成JIC全解析:SOF与ELF融合及EPCS烧写实战
2026/10/14 0:15:02 网站建设 项目流程

经常有开发者拿一个 Nios II 项目来问我:硬件编译出来是 xxx.sof,软件编译出来是 xxx.elf,怎么把它们变成一个 .jic 文件,一次性烧到板子的 EPCS/EPCQ 配置芯片里,让设备上电之后不接电脑就能自己跑起来?这个问题看起来简单,网上搜一下也能看到“sof+ELF=jic”这种说法,但真上手操作时,很多人会在 Quartus 的 Convert Programming Files 窗口里找不到“添加 ELF”的入口,甚至把 .elf 拖进去直接被提示格式不支持。

这不是你操作不对,而是因为你还不清楚 JIC 文件的生成逻辑:.jic 不是一个能直接把两个任意文件“打包”在一起的容器,它本质上是 JTAG 间接配置文件,里面的 SOF 和 ELF 各有各的存放方式和加载路径。本文就把这三类文件的关系讲透,然后分别用 GUI 和命令行两种方式演示 JIC 的完整生成过程,最后把我实际踩过的几个坑一并列出,给正在做 Nios II 软核开发的朋友做个参考。

1. JIC 到底是什么:SOF、ELF、JIC 三者关系

1.1 SOF 和 ELF 各自在扮演什么角色

先看最基础的两个文件。.sof 全称是 SRAM Object File,是 Quartus 对 FPGA 工程综合、布局布线之后生成的配置文件。它描述的是 FPGA 内部 LUT、寄存器、BRAM、PLL、IO 等资源如何布线、如何实现你写的 Verilog/VHDL 逻辑。这个文件有两个特点:一是必须通过 JTAG 直接下载到 FPGA 内部的 SRAM 配置区;二是掉电立即丢失,所以它一般只用于调试阶段。

.elf 是 Nios II 软核处理器运行的程序文件,也就是你写的 C 代码编译出来的可执行文件,里面包含机器指令、全局变量、常量、堆栈初始化内容等。Nios II 处理器本身是“软核”,它的逻辑就是 SOF 的一部分,但它要跑程序就必须有指令和数据,这些指令和数据不能凭空出现在 FPGA 内部,必须提前放到某个存储器里。这个存储器可以是 FPGA 片上的 on-chip RAM,也可以是板子上外挂的 SDRAM/DDR,还可以是配置芯片里专门划分出来的代码区。

关键点在于:SOF 决定 Nios II 这个“CPU”存不存在、长什么样;ELF 决定这个“CPU”跑什么程序。两者缺一不可。

1.2 JIC 的“间接”体现在哪里

JIC 全称是 JTAG Indirect Configuration File。这里的“Indirect”值得好好理解一下。FPGA 的 JTAG 接口正常情况下只能直接配置 FPGA 的 SRAM,不能直接往 EPCS/EPCQ 配置芯片里写数据,因为 JTAG 协议和 SPI 协议是两回事。那如何通过 JTAG 烧写 SPI 接口的配置芯片呢?

Quartus 的做法是:利用 FPGA 本身作为“翻译桥”。烧写 JIC 时,软件先把一个称为 Flash Loader 的小段逻辑通过 JTAG 下载到 FPGA 里,这个逻辑在 FPGA 内部实现了 SPI 主控制器,能够按照 EPCS/EPCQ 的编程时序向外发送命令。之后 JTAG 数据流就不再是配置 FPGA 的位流,而是通过这条“桥”间接控制配置芯片的擦除、页编程和校验。整个过程你把 JTAG 线插在板子上,但数据最终写进了配置芯片。

所以 JIC 文件并 .jic 不能用一个通用编程器直接烧到 EPCS 里,它必须在 Quartus Programmer 里、在 FPGA 的 JTAG 链上完成烧写。这一点很多人容易误会,以为 .jic 就是一个“镜像文件”,拿到什么烧录器都能用。

1.3 三类文件的加载路径对比

文件内容加载位置加载方式掉电后
.sofFPGA 配置位流FPGA 内部 SRAMJTAG 直接配置丢失
.elfNios II 程序指令与数据RAM 或外部存储器调试器加载/初始化丢失
.jicSOF + 可选数据区(来自 ELF)EPCS/EPCQ 配置芯片JTAG 间接烧写保留

从这个表能看出,.jic 的真正价值不是替代 .sof,而是把“FPGA 配置”和“程序存储”两件事合并成一次烧写动作。上电时配置芯片主动配置 FPGA,Nios II 的逻辑先建立起来;随后处理器从指定地址开始取指执行,这个地址上的代码可能来自配置芯片内部的数据区、来自片上 RAM 的初始化内容,或者由 bootloader 从配置芯片搬运到外部 SDRAM。

2. 生成 JIC 之前:硬件工程和软件工程各要满足什么条件

2.1 硬件工程侧的基础要求

不是随便一个带 Nios II 的工程都能生成有效的 JIC。硬件工程里至少要满足以下条件:

第一,Platform Designer(以前叫 Qsys)系统中必须有一个 Nios II 处理器,且处理器的 reset vector 和 exception vector 指向正确的存储器。如果程序放在外部 SDRAM,reset vector 通常指向片上 RAM 中的 bootloader;如果程序直接放在片上 RAM,reset vector 直接指向该 RAM。

第二,如果 ELF 数据计划存放在配置芯片(EPCS/EPCQ)里,那么硬件系统里必须有对应的串行 Flash 控制器 IP,比如 EPCS Serial Flash Controller 或 Serial Flash Controller。否则 Nios II 无法在上电后读取配置芯片里的数据。

第三,整个硬件工程要能正常编译出 .sof。这一步是基础,很多人在 Nios II 开发中改了硬件后忘了重新编译,拿着旧的 .sof 去配新的 .elf,结果逻辑和程序对不上,烧进去跑不起来。

2.2 软件工程侧需要准备什么

软件工程在 Nios II SBT for Eclipse 或者新版 Nios II Command-Line Tools 里编译,最终得到 application.elf。生成 JIC 前,需要明确你的 ELF 最终要放到哪里:

  • 如果程序要放到片上 RAM,且希望上电时 RAM 里已经有程序,那 ELF 需要先转成 hex/mif,然后在 Platform Designer 的 on-chip memory IP 里指定为初始化文件,重新编译硬件,ELF 内容就被嵌进 SOF 了。
  • 如果程序要放到 SDRAM/DDR 等外部存储器,ELF 本身不会自动出现在外部存储器里。你需要 bootloader 在启动时从配置芯片读出 ELF 数据并拷贝到外部 RAM,或者在调试阶段直接用 JTAG 加载。

这两种场景对应的 JIC 内容形态是不一样的,我在第 5 部分专门展开。这里你只需要先记住:准备生成 JIC 时,手边除了 .sof 和 .elf,很可能还需要一个由 .elf 转换来的 .hex 或 .mif 文件。

2.3 把 ELF 转成 HEX:最常用的两条命令

如果你要走“ELF 作为附加数据放进 JIC”的路线,第一步就是把 .elf 转成 Quartus 能识别的 hex 格式。Nios II 工具链里提供了 GNU objcopy,用法很直接:

nios2-elf-objcopy -O ihex application.elf application.hex

这条命令会把 ELF 转成 Intel HEX 格式,里面包含了代码段、数据段、堆栈段等所有需要被加载到内存的内容。转出来的 .hex 可以直接在 Convert Programming Files 里作为附加数据添加,也可以作为 on-chip memory 的初始化文件。

另一种情况,如果你在 Platform Designer 里给 on-chip memory 指定初始化文件,通常需要 .mif 或 .hex。此时可以用 Nios II SBT 里自带的 elf2hex 工具:

elf2hex application.elf 0 0x00000000 0x00007fff init.hex

参数依次是输入 ELF、存储段名称、起始地址、结束地址、输出文件。不同版本的 elf2hex 参数略有差异,建议先看下版本帮助。如果嫌命令行麻烦,在 Nios II SBT for Eclipse 的工程菜单里也有 Generate Memory Initialization File 之类的图形化入口。

提示:直接拿 .elf 往 Convert Programming Files 里拖通常会报“unsupported format”,这不是你操作姿势不对,而是 Quartus 的转换工具根本不直接消费 ELF 文件。先转成 hex/mif 再添加是正路。

3. GUI 方式生成 JIC:分步操作全流程

3.1 打开转换工具并按需求选参数

在 Quartus 菜单栏选择 File -> Convert Programming Files,会弹出一个很大的转换窗口。顶部 Output programming file 区域是关键:Programming file type 一栏下拉选择JTAG Indirect Configuration File (.jic)。

选择之后,下面几个框会联动改变。Configuration device 一栏要选择你板子上实际使用的配置芯片型号,常见的有 EPCS4、EPCS16、EPCQ64、EPCQ128 等。选错型号会导致生成的 JIC 容量计算、地址分配都跟着错,后续烧写时 Programmer 也会报设备不匹配。

Mode 一栏一般选择Active Serial(AS)。如果板子上的配置芯片是多 bit 数据线连接方式,比如 EPCQ 接成 x4,也可以选 AS x4 模式。拿不准的时候打开板子原理图确认一下,或者直接用开发板默认配置。Mode 选错是烧写成功但上电无法启动的常见原因之一。

3.2 添加 SOF 文件

Input files to convert 区域点击 Add File,选择你的硬件工程编译出来的 .sof。添加之后,列表里会出现一行 SOF 记录,并自动分配一个起始地址,一般从 0x000000 开始。

这里可以点中这一行,然后看下面的 Properties 按钮。不同 Quartus 版本弹出的属性对话框内容不一样,有的版本里可以设置 SOF 是否启用压缩(Compression),有的版本里还能配置是否允许加密位流。如果你的配置芯片容量比较紧张,可以勾选压缩;但要注意,压缩后的配置数据在 FPGA 配置阶段需要额外的解码逻辑,部分老器件或者特殊模式下不支持,建议先查一下目标器件手册。

3.3 添加 ELF 转出来的 HEX 文件

这是最容易卡住的步骤。正确做法不是点 Add File,而是点旁边的Add Additional Data按钮,在弹窗里选择你从 ELF 转出来的 .hex 文件。添加之后,列表里会多出一行 HEX 记录,同时有一个可编辑的起始地址。

有的 Quartus 版本里,Add Additional Data 弹窗会让你选择地址来源:可以手动输入,也可以从文件中自动读取。如果 hex 文件里包含了完整的地址信息(Intel HEX 本身是带地址的),工具会自动识别每个数据块的地址并生成对应的地址映射。如果地址不对,可以在列表里双击该行或选中后点 Properties 手动改。

3.4 检查输出文件和地址,点击 Generate

在 Output programming file 区域填好输出文件名和路径,建议放到一个专门的 output 目录里。生成前再确认三处:配置芯片型号、Mode、以及 SOF/HEX 两行的起始地址是否有重叠。确认无问题后点击 Generate,等待 Console 输出提示成功。

生成的 .jic 文件建议顺手保存一份 .cof 转换配置文件。GUI 底部一般有 Save 按钮,保存下来的 .cof 是一个文本文件,之后命令行生成或换电脑复现都会非常方便,第 4 部分会细讲。

3.5 GUI 操作完成后的最小验证

生成成功后不要急着断电,先把 .jic 烧进去做一次完整上电验证。完整烧写步骤在第 7 部分。这里强调一句:GUI 生成 JIC 只是第一步,真正能证明 JIC 有效的是烧写后重新上电,Nios II 能按预期运行起来。如果你在生成这一步反复出错,先排查是不是 .hex 文件本身有问题,可以尝试用文本编辑器打开 .hex 看一眼,里面应该是成排的十六进制文本行,而不是二进制乱码。

4. 命令行方式生成 JIC:适合脚本化批量操作

4.1 quartus_cpf 一行命令

GUI 能做的事,命令行基本都能做。Quartus 自带的quartus_cpf就是 Convert Programming Files 的命令行版本。生成一个最基础的 JIC 可以这样写:

quartus_cpf -c --jic -d EPCS16 -m AS input.sof output.jic

其中-c表示转换操作,--jic指定输出类型为 JIC,-d指定配置芯片型号,-m指定模式。这条命令在没有附加数据时足够用了,对应的就是“只把 SOF 打进 JIC”的场景。

但如果我们想同时把 HEX 数据加进去,命令行写法要稍微复杂一些。实际项目中更推荐用 COF 文件方式,这样可读性和可维护性都更好,出错概率也低。

4.2 用 COF 文件固化转换配置

在 GUI 中点击 Save 生成的 .cof 文件,本质上就是一个文本配置文件。典型内容大致如下:

[Quartus Prime] OutputFile=output.jic OutputFileType=JIC ConfigurationDevice=EPCS16 Mode=AS [InputFiles] SOF=hardware/output.sof HEX=software/application.hex

不同版本 Quartus 生成的 COF 字段名会有些差异,例如ConfigurationDevice在某些版本里写的是Device,OutputFileType可能是FileType。所以最稳妥的方式是先你在 GUI 里手动配置一次、保存 COF,然后再用文本编辑器去修改路径和参数,而不是凭记忆手写整个 COF。

拿到 COF 之后,命令行只需要执行:

quartus_cpf -c output.cof

Quartus 会读取 COF 里所有设置并生成对应的 .jic。这个方式最大的好处是:软件每次更新编译出新的 ELF,转成 HEX 后,只需要把 COF 里的 HEX 路径替换一下,重新执行这一条命令即可,整个过程可以很自然地写进一个 build 脚本里。

#!/bin/bash nios2-elf-objcopy -O ihex application.elf application.hex quartus_cpf -c jic_config.cof

这样每次生成 JIC 只需要跑一下脚本,省去打开 GUI 反复点按钮的时间。我在维护多套固件版本的时候深有体会:手动点 GUI 不仅慢,还容易在某个下拉框上选错配置芯片。

4.3 命令行方式的常见报错

命令行方式确实高效,但报错信息也更直接。常见的一个坑是quartus_cpf不在 PATH 环境变量里,导致命令找不到。Quartus Prime 安装目录下的bin64或bin目录需要加入环境变量,或者直接使用完整路径调用。

另一个常见报错是“Error: Can't open HEX file”,多半是路径写错或者 HEX 文件没生成成功。建议脚本里在调用quartus_cpf之前加上文件存在性检查。

5. ELF 在 JIC 里的两种形态:内嵌 SOF 和独立数据区

5.1 形态一:ELF 被初始化进片上 RAM,随 SOF 一起下载

这种形态下,ELF 不是作为独立文件出现在 JIC 里,而是已经被“融化”进了 SOF。操作路径是这样的:在 Platform Designer 中打开 on-chip memory IP,把它的初始化文件设置为由 ELF 转出来的 hex/mif,然后重新编译 Quartus 工程。综合时 Quartus 会把初始化文件里的数据写入 FPGA 内部 RAM 的初始值逻辑,最终生成的 SOF 里就包含了 Nios II 程序的所有指令和数据。

用这种方式生成 JIC 时,Convert Programming Files 里只需要添加 SOF 一个文件,不需要再额外添加 HEX。烧写后,FPGA 配置完成的瞬间,片上 RAM 已经带有全部程序内容,Nios II 复位之后直接从 reset vector 指定的地址取指执行,启动过程非常干净。

优点是结构简单、启动快、不容易出错;缺点是片上 RAM 容量有限,程序稍微大一点就放不下,而且每次修改软件都需要重新生成 HEX、重新在 Platform Designer 里指定、重新全编译硬件工程,效率比较低。

5.2 形态二:ELF 对应的 HEX 作为独立数据区,和 SOF 并列存进配置芯片

这种形态下,SOF 和 HEX 在配置芯片里各占一块区域,JIC 中二者是并列关系。上电后,FPGA 先从配置芯片的 0x0 区域加载 SOF,建立起 Nios II 硬件;随后 Nios II 的 bootloader 通过串行 Flash 控制器读取配置芯片后面区域的 HEX 数据,把它拷贝到 SDRAM/DDR 等程序运行区,再跳转执行。

在 GUI 中的操作对应第 3 部分讲过的流程:先添加 SOF,再通过 Add Additional Data 添加 HEX,两者地址错开。命令行里通过 COF 同时写入 SOF 和 HEX 两项。Nios II 这一端必须在 BSP 设置里使能对应的 bootloader,比如boot_loader_epcs或boot_loader_cfi,否则处理器上电后不知道该去哪里找程序,跑不起来。

优点是程序大小基本不受限,外部 SDRAM 多大就能用多大;软件更新时硬件工程不用重新编译,只要替换 ELF 转出的 HEX、重新生成 JIC、烧写配置芯片即可,迭代效率高很多。缺点是启动链路复杂,bootloader、地址配置、SDRAM 初始化时序任何一个环节有问题,现象都差不多——上电后完全没反应。

5.3 两种形态怎么选

维度形态一(ELF 内嵌 SOF)形态二(ELF 独立数据区)
程序容量受限于片上 RAM受限于外部 RAM / 配置芯片容量
启动速度快,配置即就绪略慢,需 bootloader 搬运
软件更新需重新编译硬件工程只需替换 HEX 和 JIC
结构复杂度简单较复杂,依赖 bootloader
适用场景小规模裸机程序、原型验证较大应用、产品量产

所以当有人简单概括“sof+ELF=jic”时,他可能指的是形态一——ELF 已经被嵌入 SOF,最终 JIC 里虽然只有一个文件输入,但内容上确实是 SOF 加 ELF 的组合;也可能指的是形态二——需要显式地把 HEX 加进去。搞清楚两种形态,你才能判断自己到底该用哪种。

6. 地址分配、容量估算与实测踩坑

6.1 配置芯片的地址空间怎么分

EPCS/EPCQ 的地址空间是按扇区组织的,不同型号扇区大小不同,有些是 4KB 扇区,有些是 64KB 扇区。JIC 在烧写时会把 SOF 和 HEX 各放在一把地址对齐的区域内。最常见的分配方式是 SOF 起始地址 0x0,HEX 紧随其后,且起始地址按扇区边界对齐。

假设你的 SOF 是 500KB,HEX 是 128KB,配置芯片是 EPCS16(16Mbit,即 2MB 容量)。那么你可以把 SOF 放在 0x000000 到 0x07D000 附近,然后 HEX 起始地址设为 0x080000,这样两者不相交,且都满足扇区对齐。如果懒得手算,Convert Programming Files 里会依据文件长度自动给出建议地址,你只需要确认没有重叠即可。

规模更大的程序要特别注意配置芯片容量。容量估算公式很简单:SOF 大小 + HEX 大小 + 少量余量。如果容量超了,生成 JIC 时 Quartus 会直接报错,提示配置芯片空间不足。此时要么换更大容量的 EPCQ,要么对 SOF 启用压缩,要么把软件里不用的段裁掉。

6.2 我实测中遇到过的四个典型问题

第一个问题:把 .elf 直接添加到 Convert Programming Files,弹窗提示格式不支持。这不是软件 bug,而是 ELF 确实不是 Quartus 转换工具可识别的输入格式。我一开始也纳闷“标题不是说 sof+ELF=jic 吗”,实际做的时候才知道要先转成 hex。这条前面已经反复强调,但还是值得单独列出来。

第二个问题:配置芯片型号选了 EPCS16,但板子上实际是 EPCQ64。生成的 JIC 烧写时,Programmer 会尝试先通过 JTAG 识别链路上的设备,识别结果和 JIC 里的配置芯片型号对不上,直接报错。这个坑我踩了不止一次,现在我在做任何转换之前都会先打开板卡原理图确认配置芯片丝印。

第三个问题:SOF 和 HEX 的地址重叠。曾经在一次转换中,Quartus 没有给你报错,因为 HEX 文件里的地址起始由文件内部决定,而我手动填的起始地址和 SOF 数据区撞了。结果烧写后上电,FPGA 可以被配置,但 bootloader 读出来的程序数据是乱码,行为非常怪异。排查到最后才发现是地址重叠覆盖了部分配置数据。所以生成后最好在 Console 里看地址映射信息,或者在 COF 里核对两个文件的地址区间。

第四个问题:Mode 选成了 AS x4,但板子上的配置芯片只接了单条数据线。烧写时 Programmer 可能不报错,但断电重启后 FPGA 根本没被配置,上电后板上无任何反应。原因是 x4 模式要求四根数据线同时连接,硬件不满足时配置过程会失败。这个问题表面上看是软件配置问题,本质上是硬件设计检查没做到位。

6.3 生成完 JIC 后自己先检查一遍

有些问题在生成阶段看不出来,需要你主动检查:

用 Quartus Programmer 打开 JIC 文件时,能看到里面记录的目标配置芯片型号和文件长度。第一步先确认型号和板子一致;第二步看文件长度是否接近配置芯片容量限制;第三步,如果板上有串口,优先输出一些启动信息,方便判断 Nios II 有没有跑到你的程序入口。这些检查都通过后,才适合进入批量烧写阶段。

7. 烧写 JIC 到 EPCS/EPCQ 及上电验证

7.1 Quartus Programmer 的烧写步骤

打开 Quartus Programmer,先做 Hardware Setup,选择连接的下载器(比如 USB-Blaster),确保能够扫描到 JTAG 链上的设备。然后点击 Add File,选择上一步生成的 .jic 文件。这时 Programmer 的设备列表里会出现两种设备:FPGA 本身和配置芯片 EPCS/EPCQ。烧写时需要在配置芯片那一行勾选 Program/Configure,FPGA 那一行通常不勾。

点击 Start 后,Programmer 会先往 FPGA 里临时加载 Flash Loader 逻辑,然后通过它操作配置芯片的擦除、写入和校验。整个过程取决于配置芯片容量,从几十秒到几分钟不等。进度条显示成功之后,不要急着收工,先给板子断电再重新上电,这一步是验证 JIC 是否真正可用的关键。

7.2 上电验证的三种方式

第一种方式,看 FPGA 是否被成功配置。很多板子上有配置完成 LED,或者你可以在程序里驱动某个 IO 拉高电平。如果 SOF 本身有逻辑,配置成功后这个现象立刻能观察出来。

第二种方式,看 Nios II 程序是否运行起来了。如果程序里有串口打印,上电后终端应该能看到输出信息;如果程序控制 LED 闪烁,那 LED 状态会按预期变化。什么都没看到时,先检查串口波特率、调试串口引脚是不是被外设占用,这些低级问题往往比 JIC 本身更容易让人迷惑。

第三种方式,用 Quartus Programmer 的 Read Back 功能或者直接在 Nios II SBT 里通过 JTAG 连接处理器,确认 Nios II 是否在运行。连接成功后可以查看 PC 寄存器停在哪个地址,判断它是卡在 bootloader 里、还是没有正确跳转主函数。

7.3 烧写成功但上电无反应的排查思路

最让人头疼的就是“烧写成功但上电完全没反应”。按照我自己的排查习惯,从硬件到软件逐个排除。首先确认配置芯片型号和 Mode 是否与板子一致,这是最容易被忽略的硬件匹配问题。其次确认复位向量:在 BSP 设置里看一下 reset vector 指向哪里,如果指向了外部 SDRAM,而外部 SDRAM 在上电瞬间是空的,代码根本不存在,自然跑不起来。再次确认 bootloader 是否使能,以及 HEX 数据在配置芯片中的地址和 bootloader 读取的地址是否一致。这几步走完,绝大多数启动失败问题都能定位。

8. 最后分享一点我的个人操作习惯

这个流程我反复做过很多次之后,已经把日常操作固化成了一个固定套路:第一次用 GUI 把配置芯片型号、Mode、地址全部调整好,生成 JIC 之后顺手 Save COF 文件;之后每次软件更新,只跑两步——第一步用 objcopy 把新的 ELF 转成 HEX,第二步替换 COF 里的 HEX 路径,然后执行一行quartus_cpf -c xxx.cof。整个过程加起来不到一分钟,比每次都打开 GUI 点来点去稳定太多。

另外还想强调一句:JIC 文件和 SOF 文件在调试阶段的定位完全不同。SOF 适合开发期反复改逻辑、临时跑功能验证;JIC 适合确定了配置芯片型号和启动方案之后,面向烧写量产。不要在还没搞清楚 ELF 该怎么加载时就去生成 JIC,那样只会把一个还不成熟的状态固化到配置芯片里,后续改一次就要重新擦写一次,浪费时间。

如果你正在做 Nios II 相关的项目,建议把这篇文章里的两种 ELF 形态都亲手试一遍。第一次用片上 RAM 内嵌 ELF 的方式跑通最小系统,第二次再用外部 SDRAM 加 bootloader 的方式跑通大程序加载。两条路都走通了,你对“sof+ELF=jic”这句话背后的设计思路也就真正理解了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询