HDL4SE:软件工程师学习Verilog语言(十五)
Posted 饶先宏
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HDL4SE:软件工程师学习Verilog语言(十五)相关的知识,希望对你有一定的参考价值。
15 RISC-V CPU的FPGA实现
前面我们用软件实现了一个RISC-V的CPU,虽然是用HDL4SE建模实现,但是仍然不是RTL的,没法直接在硬件上运行,充其量算是RISC-V CPU的CModel。本次我们实现一个用verilog写的RISC-V CPU,能够在FPGA上跑起来。然而重点是在于介绍从软件CModel到FPGA中间的开发过程,可以看到,整个过程比直接写verilog还是要强,很多开发设计迭代过程在软件建模中实现了,软件做到RTL时,再用硬件语言实现就水到渠成了。反过来,如果直接看最后的verilog代码,其实很难想象能够直接这么设计出来。
15.1 目标
15.1.1 FPGA开发板
要做FPGA应用开发,需要准备很多前置知识,除了verilog语言的学习之外,还有一些硬件相关的内容。我们选用一个硬件方面要求比较低的FPGA开发板DE1-SOC,它采用了Altera(Intel)的Cyclone V(SOC) FPGA芯片,其中有一个ARM,不过本次我们不使用它,我们只使用它的FPGA部分。
DE1-SOC板子有CLK(50MHz外部输入),GPIO(两组共70个,inout类型),KEY(input 4个,按下松开后自动恢复),SW(input10个,二值输入,开关接入),7段数码管(共阳极,6个),LED灯(10个),64MB SDRAM,视频输入,视频输出(VGA DAC),还有PS/2,IR,AUDIO等接口,CPU那边还能接网络,USB,SD卡等。软件上还有一个自动建立工程的软件,省了很多配置FPGA管脚的工作,比较适合软件工程师入门,应该说做CPU的开发板时比较合适的。
我们这次的目标是在这个FPGA开发板上把RISC-V CPU跑起来,并实现软件的计数器,将计数结果输出到LED上,通过KEY或者SW来控制计数器的动作。
它的外部接口示意图如下(来自友terasIC官网):
我们用DE1-Soc自带的SystemBuilder工具生成FPGA的顶层模块及Altera的工程文件:
生成的工程文件中已经配置好了FPGA的管脚,顶层模型文件如下:
//=======================================================
// This code is generated by Terasic System Builder
//=======================================================
module de1_riscv(
ADC //
output ADC_CONVST,
output ADC_DIN,
input ADC_DOUT,
output ADC_SCLK,
Audio //
input AUD_ADCDAT,
inout AUD_ADCLRCK,
inout AUD_BCLK,
output AUD_DACDAT,
inout AUD_DACLRCK,
output AUD_XCK,
CLOCK //
input CLOCK2_50,
input CLOCK3_50,
input CLOCK4_50,
input CLOCK_50,
SDRAM //
output [12:0] DRAM_ADDR,
output [1:0] DRAM_BA,
output DRAM_CAS_N,
output DRAM_CKE,
output DRAM_CLK,
output DRAM_CS_N,
inout [15:0] DRAM_DQ,
output DRAM_LDQM,
output DRAM_RAS_N,
output DRAM_UDQM,
output DRAM_WE_N,
I2C for Audio and Video-In //
output FPGA_I2C_SCLK,
inout FPGA_I2C_SDAT,
SEG7 //
output [6:0] HEX0,
output [6:0] HEX1,
output [6:0] HEX2,
output [6:0] HEX3,
output [6:0] HEX4,
output [6:0] HEX5,
IR //
input IRDA_RXD,
output IRDA_TXD,
KEY //
input [3:0] KEY,
LED //
output [9:0] LEDR,
PS2 //
inout PS2_CLK,
inout PS2_CLK2,
inout PS2_DAT,
inout PS2_DAT2,
SW //
input [9:0] SW,
Video-In //
input TD_CLK27,
input [7:0] TD_DATA,
input TD_HS,
output TD_RESET_N,
input TD_VS,
VGA //
output VGA_BLANK_N,
output [7:0] VGA_B,
output VGA_CLK,
output [7:0] VGA_G,
output VGA_HS,
output [7:0] VGA_R,
output VGA_SYNC_N,
output VGA_VS,
GPIO_0, GPIO_0 connect to GPIO Default //
inout [35:0] GPIO
);
endmodule
我们先修改它,验证板子能够正确运行:
module de1_riscv(
/*
端口部分省略......
*/
wire wClk = CLOCK_50;
wire nwReset = KEY[3];
reg [6:0] led0;
reg [6:0] led1;
reg [6:0] led2;
reg [6:0] led3;
reg [6:0] led4;
reg [6:0] led5;
assign HEX0 = ~led0;
assign HEX1 = ~led1;
assign HEX2 = ~led2;
assign HEX3 = ~led3;
assign HEX4 = ~led4;
assign HEX5 = ~led5;
always @(posedge wClk) begin
if (!nwReset) begin
led0 <= 8'h3f;
led1 <= 8'h3f;
led2 <= 8'h3f;
led3 <= 8'h3f;
led4 <= 8'h3f;
led5 <= 8'h3f;
end else begin
if (SW[8]) begin
led0 <= 8'h06;
led1 <= 8'h06;
led2 <= 8'h06;
led3 <= 8'h07;
led4 <= 8'h07;
led5 <= 8'h07;
end
else if (SW[9]) begin
led0 <= 8'h3f;
led1 <= 8'h06;
led2 <= 8'h5b;
led3 <= 8'h4f;
led4 <= 8'h66;
led5 <= 8'h6d;
end
end
end
endmodule
这段代码用Quartus II综合后下载到FPGA板子中(这些操作不详细说明了),能够在数码管显示数字,按键KEY[3],显示000000,松开后拨动开关SW[9]和SW[8],分别显示54321和777111,这样就表示FPGA板子正常运行起来了。
15.1.2 设计目标
我们这次的目标是,将前面的RISC-V CPU核改写为verilog语言实现,在FPGA开发板上跑起来,运行前面的计数器软件,能够将计数值显示在数码管上,并读出按键信息,控制计数的行为(清零,暂停,继续)。数码管作为一个硬件设备挂在CPU的外部读写口上,通过写地址0xF0000010和0xF0000014来控制数码管显示,读0xF0000000得到输入信息。计数器软件代码如下:
const unsigned int segcode[10] =
{
0x3F,
0x06,
0x5B,// 8'b01011011,
0x4F,// 8'b01001111,
0x66,// 8'b01100110,
0x6d,// 8'b01101101,
0x7d,// 8'b01111101,
0x07,// 8'b00000111,
0x7f,// 8'b01111111,
0x6f,// 8'b01101111,
};
unsigned int num2seg(unsigned int num)
{
return segcode[num % 10];
}
int main(int argc, char* argv[])
{
unsigned long long count, ctemp;
int countit = 1;
unsigned int* ledkey = (unsigned int*)0xF0000000;
unsigned int* leddata = (unsigned int*)0xf0000010;
count = 0;
leddata[0] = 0x6f7f077d;
leddata[1] = 0x6d664f5b;
do {
unsigned int key;
key = *ledkey;
if (key & 1) {
count = 0;
}
else if (key & 2) {
countit = 0;
}
else if (key & 4) {
countit = 1;
}
if (countit)
count++;
ctemp = count;
leddata[0] = num2seg(ctemp) |
((num2seg(ctemp / 10ll)) << 8) |
((num2seg(ctemp / 100ll)) << 16) |
((num2seg(ctemp / 1000ll)) << 24);
ctemp /= 10000ll;
leddata[1] = num2seg(ctemp) |
((num2seg(ctemp / 10ll)) << 8) |
((num2seg(ctemp / 100ll)) << 16) |
((num2seg(ctemp / 1000ll)) << 24);
ctemp /= 10000ll;
leddata[2] = num2seg(ctemp) |
((num2seg(ctemp / 10ll)) << 8);
} while (1);
return 1;
}
这段代码用前面准备的RISC-V工具链编译连接后,生成一个ELF文件,通过工具链中的objcopy生成FPGA能够读的格式(本来支持ihex格式,但是不知道怎么回事,Altera ModelSim仿真时总是读不对,于是就用verilog格式然后在软件仿真开始时转换为MIF文件)。我们打算把代码和数据都放在FPGA的RAM中,利用FPGA的RAM IP能够用数据初始化的功能,将ELF文件生成的数据文件放在FPGA的一个RAM中。
这个过程中还涉及到ELF文件生成过程中的内存映象部署的问题,默认的工具链连接时,把运行起始点放在0x00010074开始的地方,前面64KB就空出来了,我们做这个应用时,希望占用的FPGA资源尽可能少,比如用8KB的RAM,就可以支持这个应用运行,其中4KB是代码和只读数据,4KB是程序的数据区和堆栈(当然这个应用没有调用诸如malloc之类的动态内存管理方面的API,因此,堆空间没有实现)。
为此,我们修改了默认的链接脚本,让程序从0x00000000开始,这样就可以在8KB的地址空间内完成运行。具体连接脚本的修改结果请看git文件库。
15.2 CModel模式改到RTL
跟SystemC一样,HDL4SE用来对数字电路建模,最大的好处是能够利用c/c++的资源,这会给建模带来很多方便,能够快速把数字电路的模型建立起来,并且能够仿真运行,可以验证软件工具链以及实现的算法等。我们做这个事情的时候,假定计算资源和存储资源都是不受限制的,而且可以用c/c++的一些表达方式进行算法描述。这样做出来的模型,往往不是RTL的,只能作为数字电路的CModel来用。从CModel改到RTL,其实就是将建模过程中用过的c/c++的表达方式,逐步改为全部用HDL4SE的建模方式实现。具体到RISC-V的这个模型,我们分几步来完成。
15.2.1 存储器实现
在前面实现RISC-V时,内存是用c的指针实现的,这是前面一节中的模型定义:
MODULE_DECLARE(riscv_core)
unsigned int *ram;
unsigned int regs[32];
unsigned int ramsize;
unsigned int dstreg;
unsigned int dstvalue;
unsigned int ramdstaddr;
unsigned int ramdstvalue;
unsigned int ramdstwidth; /* 1, 2, 4 */
END_MODULE_DECLARE(riscv_core)
......
MODULE_INIT(riscv_core)
int i;
pobj->ramsize = RAMSIZE * 4;
pobj->ram = malloc(pobj->ramsize);
loadExecImage(pobj->ram, pobj->ramsize);
......
可以看到其中的ram是用c语言的指针实现的,这样做无法对应到硬件实现,因此我们第一步就是把ram移到模型的外部,也通过模型的read/write系列接口来访问。
具体做法是,用Altera的IP生成工具生成RAM,字长32位,总共2048个字(8KB),生成的RAM只有一个读写口,它的接口如下:
module ram8kb (
address,
byteena,
clock,
data,
wren,
q);
input [10:0] address;
input [3:0] byteena;
input clock;
input [31:0] data;
input wren;
output [31:0] q;
endmodule
具体的用法见Altera的相关文档,为了在HDL4SE系统中进行仿真,我们照这个接口建立HDL4SE模型:
#define riscv_ram_MODULE_VERSION_STRING "0.4.0-20210825.0610 RISCV RAM cell"
#define riscv_ram_MODULE_CLSID CLSID_HDL4SE_RISCV_RAM
#define M_ID(id) riscv_ram##id
IDLIST
VID(address),
VID(byteena),
VID(clock),
VID(data),
VID(wren),
VID(q),
VID(lastaddr),
END_IDLIST
MODULE_DECLARE(riscv_ram)
unsigned int* ram;
unsigned int ramaddr;
unsigned int ramwrdata;
unsigned int ramwren;
unsigned int rambyteena;
END_MODULE_DECLARE(riscv_ram)
DEFINE_FUNC(riscv_ram_gen_q, "address, byteena, data, wren, lastaddr") {
unsigned int lastaddr;
lastaddr = vget(lastaddr);
if (lastaddr < RAMSIZE)
vput(q, pobj->ram[vget(lastaddr)]);
else
vput(q, 0xdeadbeef);
} END_DEFINE_FUNC
DEFINE_FUNC(riscv_ram_clktick, "") {
pobj->ramwren = vget(wren);
pobj->ramwrdata = vget(data);
pobj->rambyteena = vget(byteena);
pobj->ramaddr = vget(address);
vput(lastaddr, vget(address));
} END_DEFINE_FUNC
DEFINE_FUNC(riscv_ram_deinit, "") {
if (pobj->ram != NULL)
free(pobj->ram);
} END_DEFINE_FUNC
DEFINE_FUNC(riscv_ram_setup, "") {
if (pobj->ramwren) {
unsigned int mask =
(pobj->rambyteena & 1 ? 0x000000ff : 0)
| (pobj->rambyteena & 2 ? 0x0000ff00 : 0)
| (pobj->rambyteena & 4 ? 0x00ff0000 : 0)
| (pobj->rambyteena & 8 ? 0xff000000 : 0);
pobj->ram[pobj->ramaddr] = (pobj->ram[pobj->ramaddr] & (~mask))
| (pobj->ramwrdata & mask);
}
pobj->ramwren = 0;
} END_DEFINE_FUNC
static int loadExecImage(unsigned char* data, int maxlen)
{
....
}
MODULE_INIT(riscv_ram)
pobj->ram = malloc(RAMSIZE * 4);
loadExecImage(pobj->ram, RAMSIZE * 4);
pobj->ramwren = 0;
PORT_IN(clock, 1);
PORT_IN(wren, 1);
PORT_IN(address, 11);
PORT_IN(data, 32);
PORT_IN(byteena, 4);
GPORT_OUT(q, 32, riscv_ram_gen_q);
REG(lastaddr, 11);
CLKTICK_FUNC(riscv_ram_clktick);
SETUP_FUNC(riscv_ram_setup);
DEINIT_FUNC(riscv_ram_deinit);
END_MODULE_INIT(riscv_ram)
这个模型当然也是大量用了c/c++的描述,不过因为这是FPGA的IP,在FPGA应用时是由Altera生成的,因此这里的描述仅供HDL4SE仿真使用,所以也就无所谓了。
当然我们面临的问题是,这里的存储器访问在读的时候有1拍的延时,而且读写不能同时进行,这样前面建模中使用c/c++在同一个周期中读写的方式要进行修改,这点我们后面的寄存器文件修改后再一起描述。
15.2.2 寄存器文件
RISC-V中有32个32位寄存器,一个PC和31个通用寄存器,这些寄存器当然可以使用HDL4SE的寄存器实现,但是要实现按照寄存器号读写寄存器,其实是一个多路选择电路,为了简化电路,我们把寄存器也用1个端口的RAM实现,当然也放在CPU外面实现(当然,PC寄存器还是放在核内用寄存器实现),这样我们也为寄存器访问增加相应的接口。寄存器文件实际就是一个ram,AlteraIP工具生成的接口如下:
module regfile (
address,
byteena,
clock,
data,
wren,
q);
input [4:0] address;
input [3:0] byteena;
input clock;
input [31:0] data;
input wren;
output [31:0] q;
endmodule
为了HDL4SE仿真运行,我们同样用HDL4SE建模语言建模如下,我们特别增加了每个寄存器的访问接口,可以在仿真时在VCD文件中记录每个寄存器的值:
#define riscv_regfile_MODULE_VERSION_STRING "0.4.0-20210825.1540 RISCV REGFILE cell"
#define riscv_regfile_MODULE_CLSID CLSID_HDL4SE_RISCV_REGFILE
#define M_ID(id) riscv_regfile##id
IDLIST
VID(address),
VID(byteena),
VID(clock),
VID(data),
VID(wren),
VID(q),
VID(lastaddr),
VID(x1),
......
VID(x31),
END_IDLIST
#define REGCOUNT 32
MODULE_DECLARE(riscv_regfile)
unsigned int ram[REGCOUNT];
unsigned int ramaddr;
unsigned int ramwrdata;
unsigned int ramwren;
unsigned int rambyteena;
END_MODULE_DECLARE(riscv_regfile)
DEFINE_FUNC(riscv_regfile_gen_q, "address, byteena, data, wren, lastaddr") {
unsigned int lastaddr;
lastaddr = vget(lastaddr);
if (lastaddr == 0)
vput(q, 0);
else
if (lastaddr < REGCOUNT)
vput(q, pobj->ram[vget(lastaddr)]);
else {
printf("We have %d registers only, but you want to read %d\\n", REGCOUNT, lastaddr);
}
} END_DEFINE_FUNC
DEFINE_FUNC(riscv_regfile_clktick, "") {
pobj->ramwren = vget(wren);
pobj->ramwrdata = vget(data);
pobj->rambyteena = vget(byteena);
pobj->ramaddr = vget(address);
vput(lastaddr, vget(address));
} END_DEFINE_FUNC
DEFINE_FUNC(riscv_regfile_setup, "") {
if (pobj->ramwren以上是关于HDL4SE:软件工程师学习Verilog语言(十五)的主要内容,如果未能解决你的问题,请参考以下文章