在Xilinx FPGA上搭建SoC
背景
之前出于科研需求和龙芯杯比赛需求都自己搭建过SoC来运行CPU软核,整理一下我的搭建SoC的流程。以下的讨论基于MIPS和RISC-V两种软核ISA为例。
大家也可以参考我搭建的两个SoC以及对应的软件:
- cyysoc (RISC-V Rocket+Coherent DMA)
- pblaze_soc
- 某大船SSD上的xc7k325t-2 FPGA+2G 72bit DDR3 ECC
- 用了某个PCI-E引出到SFP接口的扩展板,连接光模块/电口模块提供网络
- PCI-E和SFP的IO在FPGA上都是GTX
- 用了某个该板子的IO接口转USB UART的扩展板
- 扩展板上面还有rgmii PHY,但我能用SFP上网就没折腾了
- 实际上该SSD的JTAG口还有2pin也连接着FPGA可以当串口,但我焊接技术不好。
- Vivado 2022.1
- Coherent DMA由Rocket-Chip提供L2 fronted bus
- uboot-cyysoc
- OpenSBI与Linux可直接使用uboot仓库中的同款设备树。CONFIG中需打开必要的驱动。
- pblaze_soc
- CDIM SOC(MIPS+纯MMIO)
- CDIM-SoC
- 龙芯杯实验箱的xc7a200t-2 FPGA + 128M DDR3+100M Ethernet PHY+RS232 UART
- 同时也移植了Nexys 4 DDR的版本,位于n4ddr_porting分支
- Vivado 2019.2
- uboot-cdim
- 基于u-boot-trivialmips与u-boot-megasoc大量摘樱桃(git cherry-pick)
- 需要注意配置处理器核缓存行大小,以便正确维护Cache一致性。
- linux-cdim
- 就是写个设备树,开启该开的config。
- Linux从CP0的Config中读取缓存行大小,需要确保CP0中缓存行大小填写正确。
- CDIM-SoC
了解你的SoC上需要的设备
我之前搭SoC的需求比较简单,就是在FPGA上运行CPU软核跑Linux,因此对外设的需求其实不高,只需要有一个串口+网卡即可。
而运行Linux时,rootfs文件系统可以有两种方式存放,一种是放在网络上使用nfs挂载,一种是在Linux Kernel编译时直接作为initrd。
我目前只实现了以下设备的驱动:
- DDR内存(使用Memory Interface Generator IP)
- 网卡(使用AXI Ethernet (lite) IP)
- 串口(使用AXI UARTLite或者AXI UART16550,或者用龙芯杯团队赛资料包中的UART IP)
需要注意的是,AXI UART 16550 IP每个寄存器间隔4字节,龙芯杯资料包中的APB AXI+UART每个寄存器间隔1字节,运行Bootloader和操作系统需要注意设备树的修改。当时为了直接运行uCore而不必修改代码,我选择了直接把龙芯杯资料包的APB AXI+UART拿出来用。
AXI UART 16550还有一些信号用于RTS/CTS,如果开发板硬件没有,可以读User Guide去接0或者1。
DMA规划
无DMA
如果你使用AXI Ethernet Lite网卡IP,那么不需要考虑DMA问题,因为该网卡是纯MMIO。但缺点是该网卡最高只能支持100M网络,且由于没有DMA,每次对网卡进行MMIO访问时效率较低,实测在我搭建的SoC上接入100MHz的CQU Dual Issue Machine只能达到17Mbps左右的速度(基于U-Boot的tftp),而在Linux中使用nc接收TCP数据到/dev/null大约在12Mbps左右。
而如果你使用AXI Ethernet(不带Lite),能够支持1G/2.5G网速,但是需要DMA支持。
Coherent DMA
如果你的CPU核支持缓存一致性,可以引出一个带缓存一致性的AXI Slave接口,这样DMA访问直接通过该接口实现缓存一致。当DMA读写修改数据时会自动Invalidate CPU各级数据缓存。
Non-coherent DMA
对于大多数学生的CPU核,Cache一致性协议过于复杂了,因此可能不会实现,这种时候就需要CPU核提供其ISA的Cache操作指令支持,或者是在软件分配DMA Buffer时将DMA页面设置上Uncached属性。
对于MIPS,我们可以直接在Linux Kernel的CONFIG中打开DMA_NONCOHERENT
,Kernel会使用Cache指令或者Uncached方式完成DMA操作。
对于RISC-V,或许可以期待下Linux 6.0合并的Zicbom支持。毕竟SBI DMA Sync已经Dropped,基于Svpbmt的方案个人不喜欢,因为把DMA都作为Uncached访问性能倒闭,特别是在没有AXI System Cache的情况下访问DDR的MIG每次都是几十个周期的时间。
Bootloader的选择与存放
对于Bootloader,我选择使用U-Boot。因为它能够支持常见的UART(包括UARTLite和UART 16550),且能够支持常见的网卡IP,AXI Ethernet与AXI Ethernet Lite均有支持。
由于U-Boot关闭了不必要的选项后能够将自身大小控制在256KB内,因此我们也可以将它直接放在一块Block Memory中,然后使用AXI Block Memory Controller连接到AXI总线上,如果FPGA开发板上有SPI Flash,也可以将它存放在SPI Flash中节省BRAM资源。
对于U-Boot可以用以下命令完成elf转bin再转coe的过程:
objcopy -O binary u-boot u-boot.bin # objcopy需要自己替换为交叉编译工具链
bin2coe -i u-boot.bin -w 32 -o u-boot.coe # 通过pip3 install bin2coe安装
如果放在BRAM中,我们可以参考UG1580的方法修改软件,避免每次修改coe后重新综合。
此外,对于RISC-V平台,如果CPU支持S-Mode,我们还需要考虑使用S-Mode U-Boot还是M-Mode U-Boot的问题。我个人选择使用M-Mode,这样可以自由切换OpenSBI、RustSBI、BBL等SBI实现。同时调试M-Mode的软件。
了解你的FPGA开发板的IO
相信来看这篇文章的读者都是用的成品FPGA开发板,这种情况下我们需要知道各个IO对应的FPGA引脚。
我们有以下两种方式获得这些资料:
- 找一个对于该开发板已经能跑的SoC工程直接抄
- 例如龙芯杯竞赛资料包提供的soc_up工程
- 找开发板提供的约束文件、文档、电路图。
- 例如我自己学校计算机组成原理课程使用的Nexys 4 DDR开发板,可以在Diligent网站上找到相关资料,我们需要它的约束文件以及它的DDR UCF文件。
为什么使用Vivado Block Design
原因如下:
- AXI一线连,不用自己定义一堆Wire并考虑宽度问题
- DDR、MII、MDIO多线接口可以合并为单个接口,在IO Planning时一根根接上即可
- 3-state buffer接口自动转换为inout类型,不需要手动定义IOBUF。(我们在接网卡PHY的MDIO时会遇到该接口)
但Block Design也有一些坑,可以看我之前的博客。
至此,读者可以使用Vivado新建一个自己FPGA芯片的空项目,并创建一个Block Design,开始操作了。
添加DDR控制器(MIG)
我们首先在Vivado的Block Design中添加Memory Interface Generator IP。
选择DDR
然后开始配置DDR。这时候我们应该开始边走向导边读MIG的User Guide了(如果你的FPGA不是7 Series需要找到对应的User Guide)。
前面跳过了DDR类型的选择,这里大家需要自己看芯片型号或者看开发板资料。
这里我们主要要做的事情就是选择Memory Part以及Data Width。Data Width取决于你的开发板上单颗DDR的数据宽度 * 你的芯片数量。例如你的开发板上单颗芯片是8bit,共9颗芯片,那么这里就需要将Data Width设置为72。而龙芯杯开发板是单颗芯片16bit的设计,因此选择16即可。
而Number of Bank Machines需要自己trade off资源占用与效率,一般保持默认即可。
至于内存频率,个人建议选择200MHz的倍数,这样一定可以选用200MHz作为MIG的输入时钟频率。至于200MHz会带来方便的原因将在后续讨论。此外,DDR是Double Data Rate,在上升沿和下降沿都可以进行数据传输,因此这里的400MHz对应到我们购买内存条时常说的Double后的有效频率需要*2,也就是800MHz。
在设置频率后,PHY to Controller Clock Ratio就是内存的分频,也就是AXI控制器的频率。例如这里分频比为4:1,频率设置为400MHz,那么我们得到的AXI接口的频率就将是100MHz。
注:当Data Width为72bit时,会将其中64bit作为数据,余下8bit作为ECC校验,开启ECC后刚train完的DDR没有写入直接读取会因为ECC校验失败出现SLVERR,这是我踩过的一个大坑,详见本文。
配置AXI参数
这一页里的Data Width与ID Width以及Narrow Burst Support我相信大家自己会配置CPU核AXI肯定看得懂。Arbitration Scheme影响不同的ID访问的调度策略,大家根据需求选择。
配置DDR选项
这里两个RZQ通常保持默认即可,如果遇到内存不稳定的情况再适度调高。
我们重点需要关注的是Controller Chip Select Pin与Input Clock Period。
CS Pin这个设置取决于你的FPGA开发板是否存在CS Pin。这个时候可以查看开发板的ucf文件是否存在CS,若存在而这个选项必须为Disabled时(例如开启ECC的情况),你需要手动接一个const将CS拉为0,否则会导致内存training跑不过。
Input Clock Period建议选200MHz,因为MIG设备无论如何都需要一个200MHz时钟,因此选择200MHz的一个好处就是MIG不需要连接两个时钟,可以简化SoC。
配置FPGA选项
这里对于System Clock,如果是FPGA内部产生(例如MMCM和PLL),可以选择No Buffer。对于外部时钟根据类型选择。
当System Clock为200MHz时,Reference Clock可以直接使用System Clock,这就非常方便。
Reset Polarity根据自己之后接的reset信号选择。
Internal Vref建议关闭来提高稳定性,如果关闭后无法通过Validate(例如龙芯杯开发板),再打开。
其他设置建议保持默认。
端接阻抗
这个作者表示不懂,但我两块开发板保持默认50欧都能用。
IO规划
对于我们购买的FPGA,只能选择Fixed Pin Out(除非你自己画板才可以选New Design),然后导入所购买开发板的ucf文件,Validate后即可。
System signals
可以全部No connect,这样我们可以自己引出来自己用。
引出DDR物理接口
直接在MIG的DDR上点击Make External即可,因为我们已经通过ucf配置过DDR的引脚,因此不需要在约束文件中添加相关引脚。
Debug
创建完MIG后,我们需要留意几个信号:
- mmcm_locked:表示DDR时钟是否锁上,若它为0需要检查输入时钟是否正确。
- 推荐连接到ila或者板上LED,除非你有足够自信认为你的DDR配置一定正确。
- init_calib_compelete:表示内存初始化(training and calibration)通过。可以说明DDR基本工作了。
连接SoC其他设备
- ui_clk:用于AXI的时钟,可以将它用于整个SoC时钟,或连接一个AXI Clock Converter。
- ui_clk_sync_rst:可以作为soc其他元件的reset,也可以取反后连接到AXI Clock Converter的reset。
添加网卡
直接调用AXI 1G/2.5G Ethernet Subsystem或AXI Ethernet Lite IP。
AXI Ethernet Lite
AXI Ethernet Lite只支持MII接口,并可选MDIO。这一块比较简单,将对应的接口(MII与MDIO)Make External,修改开发板中约束文件的接口名与引出的格式相同即可(可以去综合后的IO Planning看)。
如果你的网卡接口不是MII,例如我们学校的计算机组成原理实验板的Nexys 4 DDR使用的RMII,可以从Vivado 2018.3及之前的版本找到RMII转MII的IP核,导出到新版Vivado中使用。而如果是RGMII、GMII等接口或许需要自己寻找对应的转换IP核。但在RGMII中,工作在10Mbps/100Mbps速率时,数据传输不会使用DDR,因此转换起来相对简单。
AXI 1G/2.5G Ethernet Subsystem
AXI 1G/2.5G Ethernet Subsystem支持多种MII,还可以支持SFP接口的1000BaseX与2500BaseX。
DMA连接
建议自己参考我的pblaze_soc。
SFP接口
需要注意的是,SFP使用不同的速率所需要连接的时钟不同,这里请大家自行查阅User Guide,这里可以给出一个数据是千兆网使用125MHz时钟。且这个时钟需要由外部差分产生,许多开发板提供了一个拨码开关来切换这个时钟频率,请大家自行阅读有关说明。
中断
以上两种IP核产生的中断类型均为Rising Edge,如果CPU只支持电平中断需要连接一个AXI Interrupt Controller将Rising Edge中断转换为Level中断。
搭建完整SoC
这里我们根据需要添加UART IP、AXI Crossbar、CPU核,并添加必要的AXI转换器。
最终搭建的SoC硬件如下所示(以CDIM-SoC为例):
分配地址
我们在Block Design中打开Memory Editor,分配各设备在AXI总线上的地址范围。这样AXI Interconnect/AXI Crossbar就能够根据分配的地址将不同地址的请求发送给不同的Slave设备。
软件移植
软件移植主要就是编写设备树。具体设备的设备树编写可以参考Linux的文档。
这里需要特别注意的是,如果串口采用UART 16550,则分频需要软件知道UART设备的输入时钟频率,因此设备树中的时钟频率需要正确编写,否则会导致串口波特率偏移从而出现乱码。而UARTLite由于是固定波特率,因此不存在此问题。
U-Boot还支持Debug串口(包括UART 16550和UARTLite均支持),在.config中填写的时钟频率从其他SoC移植时也不要忘了更改。这里大家直接对着我的commit记录看即可。
仿真SoC用于软件移植
这里大家就可以用我的SoC-Simulator完成,避免软件硬件一起灵车。
调试Tips
- 使用System ILA抓AXI波形,它会将AXI请求变得非常直观。如果你自己写了AXI Master/Slave设备需要验证,也可以打开System ILA中的AXI-MM/Stream Protocol Checker来确定你的AXI实现是否符合规范。
- 板子按钮不够可以添加VIO IP核。例如我那块没有按钮的SSD当FPGA用就可以用VIO进行reset。
- 添加ILA可以直接在Block Design中在对应的Wire上点右键,选择Debug。
- 如果VIO\ILA没有出现在Hardware Manager中,多刷新几次,如果依然未出现通常原因是它所连接的时钟没有产生。
- 具体问题多读UG/PG。