SoC开发关于AXI的一些记录
在开发SoC系统时关于AXI总线的一些记录
ARM, SoC, FPGA, AXI, DE-10, Cyclone V
--by Captdam @ Jan 14, 2021反正是个人blog我就不用书面语了_(:3_| <)__
AXI总线
首先说一下这个AXI总线到底是个什么东西。传统的FPGA是只有可编程逻辑的,输入信号和输出型号直接走芯片的外部引脚就OK了;后来,我们觉得有必要在FPGA芯片上加个CPU上去,CPU负责CPU擅长的东西,FPGA负责FPGA擅长的东西,比如CPU负责运行操作系统以及资料抓取,资料处理则交给FPGA。那么问题就来了,怎么让FPGA和CPU通信,于是ARM就搞了这么一个AXI总线来make my life hard。
虽然我们可以非常简单地让CPU读写外部引脚,同时把这个外部引脚连接到FPGA的引脚上,但是这加长了信号线,让我们的最大信号频率降低;而且引脚也可以看作一根天线,这会导致电磁干扰(虽然效果不是很强)。这个AXI总线呢,他是在芯片内部的,所以我们就可以用非常高的频率进行FPGA和CPU的通信了。
这个AXI总线它提供了3个桥,h2f_bridge,h2f_lw_bridge,f2h_bridge,f是FPGA,h是HPS(硬件CPU),lw是lightweight的意思,三个桥分别干嘛的看名字就知道了,这就不用具体解释了吧_(:3_| <)__
除了这三个FPGA-HPS桥,AXI还提供一个FPGA-SDRAM桥,用来连接FPGA和板载的DDR内存。
配置FPGA
当然,这玩意要看不少资料。很不幸,玩FPGA的社区很小,愿意不用模板把每个细节搞明白的人更少,或者说他们只是不怎么喜欢在网上写东西。网上很难找到比较好的资料,最要命的是有不少过期资料和不完全资料。这里把我走的弯路写出来,一是为了记录与吐槽,二是为了给后来者参考,三是为了亚马逊多收我几刀服务器网费(你这小破站的流量网费不足半刀,(╯‵□′)╯︵┻━┻)
如果看到“往/dev/fpga0”写FPGA配置文件的,直接关掉,linux内核更新了,现在用的是device tree了。
还有一次看到一个资料说“open('/dev/mem');mmap(0xff203000)”,我做了,很快呀,系统就死了,kernel就挂那了。后来才知道,FPGA先要配置,而且0xFF203000已经不在桥的地址了。这个地址后面我会再提。
首先,我们要配置这个FPGA才能使用AXI总线。要通过AXI收发信息需要三个部分协同工作。首先是kernel,这个我们不需要管,厂家提供的kernel image直接用就行。然后是我们的HPS部分需要有AXI的硬件,这个我们不用管,也管不了,这个是ARM已经设计好,芯片厂已经把这个硬件光刻再芯片上了。最后是FPGA部分,这个我们就要管了。我们需要手动往我们的FPGA里添加AXI IP,他是一个用来读取HPS发过来的信号或是往HPS发数据的接口。
我一开始觉得,“哎,又要啃资料和信号时序图了”。我也做好了准备。俺寻思俺先不需要搞什么复杂的东西,只要把FPGA的LED输出连在HPS上(注意HPS上面,不是AXI接口上),要是HPS方面有任何操作,我的LED就会有所显示,可能是显示地址,可能是显示数据,可能只是乱闪烁,但是只要有任何变化就说明我成功地连接了FPGA和HPS,之后再慢慢研究怎么把地址和数据正确地拿出来。后来打开pin planner,发现根本找不到任何HPS AXI相关的接口让我连接我的FPGA module。然后我就先放弃了。
写下来我就看网上关于HPS的软件的资料。首先用open这个system call打开/dev/mem来获得物理内存的接口(因为接下来我们要读写特殊硬件寄存器,所以要使用物理内存;不然,我们就会使用到虚拟内存,读写的就只是普通的用户空间数据内存了)。接下来我们mmpa到0xFF020000+0x3000这个地址,0xFF020000是FPGA-HPS-lightweight-bridge的地址,而0x3000是电路板上LED的offset address。我照做了,compile时甚至不需要手动连接任何库,就compile成功了。然后我就执行,然后就死了,ctrl+c都没用,所以不是程序的问题,估计是系统底层挂了。后来我觉得,应该是因为软件写了数据,但是没有硬件读到这个数据,所以系统一直在等待ACK。这只是猜测,需要验证。
后来继续啃资料看论坛,原来是需要配置FPGA。我们需要再FPGA里面有一个硬件逻辑来和我们的AXI对接,不然肯定会死机。要搞这个FPGA里面的AXI接口,需要使用Quartus带的Qsys(现在叫platform designer)。打开这个东西的时候我是懵逼的,这啥呀?咋用呀?后来看了些intel发布的教程,入门了就简单了。
这个platform designer怎么用我这就不写了,网上资料一大堆。我就说说一些自己琢啊哈的东西。
首先一打开,会自带一个叫clk_0的模组,这个是用来把主clock信号和reset信号连接到其他模块的,没别的用处,就是为了你不需要在你的verilog/vhdl文件里多写一行wire把所有模组的clk与reset连在一起。
最简单的配置是一个hps模块(我们需要的AXI接口)与数个PIO模块。这个PIO模块可以看作是在芯片内部的pin引脚,我们在逻辑里把我们自己的模块连接在PIO的external_connection上面就好。然后我们把PIO的s1连接到hps的*master或*slave,这样我们的这个PIO就被加入AXI总线上了。PIO的s1后面的地址就是我们访问被连接的模块的地址。比如上面的图,这个PIO被连接在h2f_lw_axi_master,地址是0x00000000-0x0000000f,那么我们在软件就使用h2f_lw的基础地址+0x0000-0x000F这个区间来访问它。
接下来,验证,生成VHDL/verilog文件与IP。这里,Platform designer就会生成一大堆文件,这些文件包含IP。IP是闭源的,我们没办法用正常办法看到它的内部逻辑,但是我们可以正常使用它(黑盒),有点类似于在软件里使用编译好的库。我们需要把这些生成的文件导入Quartus工程,怎么做网上教程很多。这里我是参考的ryerson大学的实验课资料https://www.ee.ryerson.ca/~courses/coe838/labs/lab3.pdf。非常手把手了,不需要下载它的课程文件,直接空白文件参考着做就没问题。
按照我的习惯,我一般喜欢写好verilog/VHDL文件后先compile一遍,没问题的话就pin planner然后再compile。于是,我写完了硬件逻辑,compile直接报错了。后来发现,居然必须要使用platform designer生成的tcl文件做完pin passignment后才能编译。我:……(说不出话)
AXI bridge
接下来说说这三个桥,h2f_bridge,h2f_lw_bridge,f2h_bridge。顾名思义,h2f是HPS(CPU上面运行的软件)给FPGA发数据的,f2h就是FPGA给HPS发数据的,light weight就是轻量级的。所有的桥都是单向的。一般来说,h2f_lw_bridge用来控制FPGA上面的模块,HPS通过h2f_bridge把数据发给FPGA,或是通过f2h_bridge取数据。
Datasheet里面HPS-FPGA Bridge章节写的最清楚最详细:https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/hb/cyclone-v/cv_5v4.pdf。这里还有个比较往底层专研的人写的东西可以参考,但是我还没有实际验证https://zipcpu.com/blog/2019/04/27/axi-addr.html
软件
配置好了AXI,FPGA逻辑也写好了,接下来就该试试我们到底能不能成功地用软件通过AXI控制FPGA了。测试很简单,我们的HPS运行着Linux,我们同C写了个小程序,控制连接在FPGA上面的LED闪烁几次。电路板上有两组LED,一组再HPS侧,显示系统状态,比如硬盘读写(好吧,是SD卡读写);另一组再FPGA侧,HPS无法直接读写的。
然后,他就成功了,led闪烁了。我就不放图片了,反正也没有炫酷的效果,总之闪烁了就是了,之后做了更有意思的东西再放图吧。
当然,这还没完,还记得前面说的还有一次看到一个资料说“open('/dev/mem');mmap(0xff203000)”吧,0xFF20000000是h2f_lw_bridge的地址,后面的0x3000是FPGA module的地址。看最上面的Platform designer里我的PIO的地址是0x0000。我应该使用mmap(0xff200000),但是我忘了修改我的代码,出乎意料mmap(0xff203000)居然也能够控制LED闪烁(╯‵□′)╯︵┻━┻。后来又做了实验:0xFF200000, 0xFF201000, 0xFF202000, 0XFF203000都可以控制LED,但是比如0xFF200001, 0xFF201800就会出现segment fault。后来再参考资料,h2f_lw_bridge的ID宽度是12bit,也就是说,这个地址的mask是0x00000FFF,这也就解释了为什么0xFF200000, 0xFF201000, 0xFF202000, 0XFF203000都能写到LED。我们可以看做这个是partial decoding。再深入地说,这个地址的最后12bit是作为h2f lightweight axi bus的ID,但是CPU不止分配了12bit空间给它,否者读写0xFF201000将会读写到其他硬件。如果我们去看device tree的话,我们可以看见下面的信息:
bridge@0xc0000000 { compatible = "altr,bridge-16.0", "simple-bus"; reg = <0xc0000000 0x20000000 0xff200000 0x200000>; reg-names = "axi_h2f", "axi_h2f_lw"; clocks = <0x2 0x2 0x2>; clock-names = "h2f_axi_clock", "h2f_lw_axi_clock", "f2h> #address-cells = <0x2>; #size-cells = <0x1>; };
我们可以看见,device tree中,axi_h2f_lw的起始地址为0xFF200000,分配的空间有0x200000,比硬件所能支持的空间要大。大概是为了软件的兼容性(可能其他芯片的axi_h2f_lw能支持的ID更大?或是为了以后可能扩展?),我们将高位直接砍掉了。当然,在编写系统时,我们需要知道实际ID只有12bit,写0xFF201000将会覆盖0xFF200000的内容。