硬件说明

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模块对应于 verilogconfig_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_sdramsdram,双击一下就可以修改名称,这里必须引出,因为需要和我们的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总线上有请求时,也就是cycstb都为1,那么就开始进行转换工作,在开始之前,我有一个等待cnt的操作,出于担心时序的影响,因为setup time小于0,所以又等待了十几个sdram频率的周期。这里SDRAM主频150MHz,总线20MHz。

如果发现仍然有cycstb都为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
    )

results matching ""

    No results matching ""

    results matching ""

      No results matching ""