硬件说明
CPU与SOC简单介绍
这个cpu采用的是lkx的工作 http://os.cs.tsinghua.edu.cn/oscourse/OS2017spring/projects/u1
该cpu是经典的5级流水线架构,支持m态和s态特权级,并且具有mmu单元与tlb,无cache设计。
soc则基于《自己动手写CPU》(雷思磊)结构,采用的是wishbone总线,其中sdram使用的是开源代码,uart自己重写(为了节省资源,只支持发送功能,且只有一个端口,波特率写死,无ready位),片外flash目前尚不清楚如何工作,故采用片内rom,使用intel的.hex文件进行例化。总线交互也采用的是开源代码。
一些背景知识
五级流水架构
这只是一个简单的示意图,如果没有相关的基础知识理解起来还是比较困难的,我这里推荐一本书,名字是《计算机组成与设计——硬件软件接口》作者是Patterson和Hennessy,里面有详细的过于MIPS架构的介绍,事实上MIPS和RISCV从架构上讲都属于精简指令集,而且很多结构都比较相似,所以不必拘泥于一定要找riscv的书。另外如果接触源码的话,之前推荐的雷思磊写的是一本不错的书,不过学习过程不能只是抄源码,还是要多去理解才有更大的收获的。
在这里我结合本工程的CPU简单介绍一下一个指令的执行过程,对于一条指令来讲,在硬件执行的过程中被拆分成了5个阶段,这五个阶段在没有异常和暂停的时候是并行进行的。对于一个指令来说首先在IF阶段更新pc值并从指令ROM中取数据,这个时候由于数据可能需要若干周期,流水线就暂停了,等到请求完毕后将去回来的指令送往ID级,ID级的工作顾名思义就是decode的过程,接下来进入的是EXE也就是执行级,这里面一般会进行一些加减乘除移位的运算操作,然后将结果送往mem级,如果有访存的指令,那么就访问数据ram即可,同理,请求ram可能也需要多周期,此时流水线也停下。执行完毕后数据送往WB级,将结果写回寄存器堆。
需要注意的是,图中并没有标注处理中断异常的特权级,mips中是cp0,riscv中是csr,这个阶段一般都放在mem,一般来讲地址都需要进行mmu转换,一个访问ram的地址,需要先经过csr的一些判断,然后送往tlb,tlb出来的地址再送往(或者cache)内存。针对一些异常的情况,csr这个模块会及时地发出清流水线的命令,并进行相关处理。
riscv的官方手册也是一个很好的帮助,另外国人也有一版电子书(翻译)介绍riscv架构的,在此也一并推荐 http://crva.io/documents/RISC-V-Reader-Chinese-v2p1.pdf
FPGA平台
小脚丫STEP-CYC10是一款基于Intel Cyclone10设计的FPGA开发板,芯片型号是10CL016YU256C8G。另外,板卡上集成了USB Blaster编程器、SDRAM、FLASH等多种外设。板上预留了PCIE子卡插座,可方便进行扩展。其板载资源如下:
资源种类 | 数量 | 资源种类 | 数量 |
---|---|---|---|
LE资源 | 16000 | 可扩展 STEP-PCIE接口 | 1个 |
片上存储空间 | 504Kbit | 集成 USB Blaster编程器 | 1个 |
DSP blocks | 56个 | SDRAM | 64Mbit |
PLL | 4路 | Flash | 64Mbit |
Micro USB接口 | 2路 | 三轴加速度计 ADXL345 | 1个 |
数码管 | 4位 | USB转Uart桥接芯片 CP2102 | 1个 |
RGB 三色LED | 2个 | 12M与50M双路时钟源 | 1个 |
5向按键 | 1路 | LED | 8路 |
5向按键(?)我当成普通按键处理
SOC整体框图
CONFIG模块对应于 verilog
中 config_string
模块,这个模块存在的目的是为了兼容BBL,其中保存了一些硬件信息供BBL查询设置,另外根据BBL要求,timer与cmp的地址也是通过内存地址访问的,这里也一并归于此模块中。当timer达到cmp的数值时会触发一个定时器中断,直接送往CPU。
硬件地址空间分配
基于以上资源,我将测试环境下的CPU地址分配如下:
设备 | 地址分布 | 大小 |
---|---|---|
ROM | 0x0001_0000~0x0001_c000 | 48KB |
SDRAM | 0x0010_0000~0x0050_0000 | 4MB |
串口 | 0x0200_0000~0x0200_0020 | 32B |
LED | 0x3000_0000~0x0300_0010 | 16B |
CONFIG | 0x0000_1000~0x0000_0100 0x4000_0000~0x4000_0010 0x4000_0000~0x4000_0004 |
256B 16B 4B |
该部分可以在 ./wishbone_cyc10/phy_bus_addr_conv.v
中找到对应的verilog语句及宏定义,只需修改其中的数值即可。举个例子,如想修改RAM的地址分配,只需要修改以下两个宏即可,其余不需更改。
`define RAM_PHYSICAL_ADDR_BEGIN 34'h00010_0000
`define RAM_PHYSICAL_ADDR_LEN 34'h00040_0000
ROM控制器
相关代码位于 ./wishbone_cyc10/rom_wishbone.v
,相关控制比较简单,不赘述,单举一个需要注意的事项,这里我的ROM里面调用了一个已经封装好的IP核,这个IP核的配置为 深度=16384,宽度=32bits,这样算起来一共有64kB,与之前的48KB不符。这是因为IP核深度只能配置为16384/8192,即64KB/32KB,没有中间选项,所以只好如此,但并不影响结果,只要你保证真正用到的rom不超过48KB即可。或者配置为8192也可以,这样程序限制在小于32KB。
仿真环境的RAM控制器
通过开启或关闭 位于./wishbone_cyc10/cpu/defines.v
中的宏`define Simulation
可以开启或关闭仿真环境,在仿真环境下使用的是如下定义的ram。
reg [`WishboneDataBus] mem[0:`DataMemNum-1];
这里面 DataMemNum 是一个很大的数,所以综合必定失败,但是由于我们只是用来仿真,所以不必要求综合。但在进行联合仿真的时候 Tools -> Run Simulation Tool -> RTL Simulation,有时系统会报错,大意是必须先进行sythesis再仿真,这时我的做法是把ram中的DataMemNum数值调小,先保证综合成功,然后仿真的时候再改回原来的大数值就可以了。
真实环境的SDRAM控制器
SDRAM结构
SDRAM控制器
我在使用SDRAM的时候使用了两种方法,但最后都以失败告终,在此记录,如果可能可以帮助到后来者。
方法一、 使用手把手中的sdram开源文件
过程见附录所述,结果是只能使用极其有限的一部分sdram空间可能只有几十KB。
注意:这个配置并不能跑起整个SDRAM,虽然也确实可以通过rv32ui的官方测例
方法二、 使用qsys中的sdram
感谢贺清同学的帮助,目前问题是运行一分钟后SDRAM可以正常使用,一开始还是有乱码
这种方法参考了lxs中的实现,在它的soc中,全部环境都采用的是200MHz的频率,并且通过quartus的qsys直接搭建,简便明了。使用qsys的SDRAMIP核从他的实验中验证是可以行的通的,那么理论在我这里加一个总线转换桥也是可以跑的。
为什么直接在IP Catalog中搜不到sdram的ip核,我猜测还是由于总线的原因,但至少通过我接下来说的这种方式是可以间接用它的IP核的
打开qsys界面后,只需假如SDRAM的IP核即可,然后将avalon接口和物理的sdram的接口引出,图中对应信号avalon_sdram
和sdram
,双击一下就可以修改名称,这里必须引出,因为需要和我们的wishbone总线交互。
参数如表,另外需要注意的是左侧信号,sdram
对应的信号为zs_xxx
,这里和实际的物理sdram接口对应没有问题,而对应的avalon
总线,注意地址是[21:0],数据是[15:0],那么也就是说这里面总线的最小寻址单元是1个16bits的半字,所以我们这个转换桥,还需要做一个32位到16位的工作,实际上这个avalon是32位的更方便,因为这样就不要我们转换桥做额外的工作了,但是当我设定16位宽的时候,sdram和总线接口都被固定为16位,不能修改。设定完这些后保存,在.bb
中找到所有接口信号明确的位宽
转换桥
WISHBONE总线与AVALON总线:
WISHBONE | 位宽 | 作用 | AVALON | 位宽 | 作用 |
---|---|---|---|---|---|
CLK | 1 | 时钟输入 | CLK | 1 | 时钟输入 |
ADDR_O | 32 | 地址线 | ADDRESS | 22 | 地址线 |
DATA_O | 32 | 数据线(输出) | WRITEDATA | 16 | 数据线(输出) |
DATA_I | 32 | 数据线(输入) | READDATA | 16 | 数据线(输入) |
WE_O | 1 | 写使能 | WRITE | 1 | 写使能 |
SEL_O | 4 | 选通 | BYTEENABLE | 4 | 选通 |
STB_O | 1 | 使能 | CHIPSELECT | 1 | 片选 |
ACK_I | 1 | 确认 | READ | 1 | 读使能 |
CYC_O | 1 | 使能 | WRITREQUEST | 1 | 等待 |
READVAILD | 1 | 读确认 |
两种总线读写示意图:
WISHBONE协议最关键的信号是CYC、STB与ACK, CYC和STB同时拉高时表示请求开始,在整个过程中,保持高电平,一直等到slave响应ACK拉高后的下一周期,CYC,STB和ACK拉低,至此一个请求结束。
对于AVALON协议而言,关键的几个信号是READ、WRITE、WAIT和READVALID。当READ/WRITE拉高代表读或写的请求,但是与WISHBONE不同的是,这个请求一只保持到WAIT变低,在WAIT为高时,从机处理请求,对于写请求来说,只需等待WAIT变为低电平就可以,而对于读请求来说还需要等待READVALID变为高电平,才表明总线交互结束。
有了这部分之后,下面开始总线转换桥的编写,参见wb32_avalon16
代码,这里不具体分析,因为我也不确定是否完全正确,大致的思路是构建一个状态机,当wishbone
总线上有请求时,也就是cyc
和stb
都为1,那么就开始进行转换工作,在开始之前,我有一个等待cnt的操作,出于担心时序的影响,因为setup time
小于0,所以又等待了十几个sdram频率的周期。这里SDRAM主频150MHz,总线20MHz。
如果发现仍然有cyc
和stb
都为1,根据总线中we使能情况看是写请求还是读请求,对于读请求和写请求来说,都是通过先低16位后高16位的操作,对于avalon
总线来说,对于写请求等待waitrequest
拉低就可以,而读请求需要等readdatavalid
拉高才可以。事实上应该有更快的响应方式,但此处我这里采用这种基本的握手规则。
需要注意的是avalon
总线中关于read
,write
,和byteenable
都是低有效的,准确的说应该是对于qsys中这个sdram是这样规定(低有效)的,所以处理的时候需要多加小心。
这里采用有限状态机的思路设计转换桥的过程,整体思想就是根据握手信号的变化进入到不同的状态,然后在这个状态中根据另外一些信号再改变到新的状态,循环往复。 总得说来需要这样几个状态: (1)IDLE: 这个状态代表一开始初始化,所有关于握手信号都要置低,以免进行不必要的请求 (2)写请求: 这个状态是当Wishbone总线收到CPU发送来的写使能信号时进入的状态,此时转换桥发送给Avalon一个信号。 (3)写等待: 这个状态是上一个状态马上进入到这个状态,表明转换桥在等待从机的确认信号 (4)完成: 这个状态表明请求已经被从机响应,整个请求已经完成了。 (5)、(6)读请求与读等待同理 另外在实现中,由于需要从32位转换到16位,所以每个读写状态又分为高位低位,所以一共有10个状态,初始1个,完成1个,写和读各占4个。
整体转换状态简图如下所示:
时钟相位 lxs的工作里面sdram给出来了两个时钟,1个角度为0,一个角度为-68,这里配置的原因请参考IP核手册,我在查阅资料的时候发现确实需要相位不同,需要调整,这里直接采用lxs中的数值。
CONFIG控制器
正如前面所提到的,这里的CONFIG是为了与BBL兼容,里面包含了
- 硬件信息
- timer中断
其中硬件信息通过如下方式嵌入到fpga中,这里的config_string_rom通过 ./wishbone_cyc10/config_string_rom
生成,已写好相关makeifle(感谢lkx等人的工作),通过脚本把生成的指令转换成verilog语句。实际上当上电的时刻,cpu执行的第一条指令是 config_string_rom
里面的一条跳转指令,跳转到ROM地址即0x0001_0000,和x86的FFFF_0000的跳转有异曲同工之妙。
wire[`WishboneDataBus] mem[0:`DataMemNum-1];
`include"config_string_rom/config_string_rom"
定时器中断实际上会有两个寄存器,一个是当前的cycle保存寄存器,这里面是 mtime
,另一个是阈值寄存器,超过这个阈值就会触发一个中断,这里面叫 mtimecmp
,该模块包含了这两个寄存器的读写功能,以及触发timer中断的相关设置。
uart控制器
uart帧格式比较简单,在本工程下,停止位1位,无校验位,波特率115200。
为了节约资源,目前的uart控制器只包含发送功能,且波特率硬件写死为115200(不符合UART16550协议),(在仿真环境下为了加快速度,调成250_0000)。需要注意的是,uart在仿真的时候需要在接受端模拟一个串口。位于simulation/modelsim/wishbone_soc.vt
,这个代码修改自lkx的工作,大体的意思是在每个比特发送的中间时刻进行采样,最后形成字节,并使用$write("%c", rx_byte);
从而回显在modelsim上。
如果需要修改波特率,除了在SOC中修改波特率外,仿真条件下,在测试文件也需要进行修改,主要是以下两个常量:
localparam CfgDivider = 25000000/2500000;
localparam CfgDividerMid = CfgDivider/2;
在下板的时候,可以用putty等软件进行串口回显。putty是一个轻量级的软件,简单好用,推荐
PLL控制器
在本工程中,使用quartus IP核配置进行配置,说明如下
信号名称 | 时钟频率 | 用途 |
---|---|---|
clk | 12MHz | 板载晶振输入时钟 |
wishbone_clk | 25MHz | 系统总线时钟 |
cpu_clk | 10MHz | cpu时钟 |
这里的时钟频率并不是很高,主要是为了布线时消除时序违约,实际上可能可以再快一点也不会有WNS错误。
另外,PLL出来有一个 lock
信号,将它和复位信号进行与运算 assign reset_n = lock & rst_n;
可以避免一些时序问题。
注意没当修改总线频率之后,相应的uart模块中传递的参数也要修改(如下部分),否则可能uart工作不正常。
wishbone_uart_lite #(
.ClkFreq(25000000),
`ifdef Simulation
.BoundRate(2500000)
`else
.BoundRate(115200)
`endif
)