文章目录
RISC-V处理器的设计与实现(一)—— 基本指令集_Patarw_Li的博客-CSDN博客
RISC-V处理器的设计与实现(二)—— CPU框架设计_Patarw_Li的博客-CSDN博客
RISC-V处理器的设计与实现(三)—— 上板验证_Patarw_Li的博客-CSDN博客
前面我们用Verilog实现了一个简易的RISC-V处理器,并且写了一个简易的C程序,把它编译成机器指令后放到我们的处理器中运行,运行结果也是正确的。这次我会把我们的处理器移植到板子上(板子是野火家的征途Pro,型号为EP4CE10F17C8),并实现用串口给rom烧录程序(C语言编译后的机器指令),方便我们的测试。
一、添加串口
串口(UART)又名异步收发传输器(Universal Asynchronous Receiver/Transmitter),是一种通用的数据通信协议,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将串行数据转换成并行数据。
串口包括RS232、RS499、RS423等接口标准规范,我们这里使用的是RS232:
上图为串口的通信方式,可以同时收发(全双工通信)。其中rx负责接收,tx负责发送,每次发送10bit数据(起始位+8bit数据+停止位),从最低位开始发送。
使用串口的目的是为了给我们在板子上的处理器烧录可执行程序,因为我们处理器的rom是使用寄存器资源模拟出来的,所以移植到板子上后rom里面的内容就无法更改了,为了避免每次修改程序都要重新移植,我们直接使用串口对rom里面的内容进行修改。
下面是串口程序的代码,其中rx和tx用于接收和传输bit数据;rom_erase_en_o是为了在指令写入rom之前,对rom进行全擦除;rom_wr_en_o、rom_wr_addr_o、rom_wr_data_o分别是写使能、写地址、写数据信号,用于给rom写入数据。
// 串口模块,目前只用于下载程序到rom中,波特率为9600,系统时钟频率为50MHz,传输一位需要5208个时钟周期
module uart(
input wire clk ,
input wire rst_n ,
input wire uart_rx ,
output wire uart_tx ,
output reg rom_erase_en_o , // rom全擦除使能信号
output reg rom_wr_en_o , // rom写使能信号
output reg[`INST_ADDR_BUS] rom_wr_addr_o , // rom写地址信号
output reg[`INST_DATA_BUS] rom_wr_data_o // rom写数据信号
);
parameter BAUD_CNT_MAX = `CLK_FREQ / `UART_BPS;
parameter IDLE = 4'd0,
BEGIN= 4'd1,
BIT0 = 4'd2,
BIT1 = 4'd3,
BIT2 = 4'd4,
BIT3 = 4'd5,
BIT4 = 4'd6,
BIT5 = 4'd7,
BIT6 = 4'd8,
BIT7 = 4'd9,
END = 4'd10;
wire uart_rx_temp;
reg uart_rx_delay; // 延迟后的rx输入
reg[12:0] baud_cnt; // 计数器
reg[2:0] byte_cnt; // 接收到的字节数
reg[3:0] uart_state; // 状态机
reg[7:0] byte_data; // 接收到的字节数据
reg[`INST_DATA_BUS] wr_data_reg; // 字节数据拼接成的32位数据
reg data_rd_flag; // 数据就绪标志位
// 将输入rx延迟4个时钟周期,减少亚稳态的影响
delay_buffer #(
.DEPTH(4),
.DATA_WIDTH(1)
) u_delay_buffer(
.clk (clk), // Master Clock
.data_i (uart_rx), // Data Input
.data_o (uart_rx_temp) // Data Output
);
always @ (posedge clk) begin
uart_rx_delay <= uart_rx_temp;
end
// baud_cnt计数
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
baud_cnt <= 13'd0;
end
else if(uart_state == IDLE || baud_cnt == BAUD_CNT_MAX - 1) begin
baud_cnt <= 13'd0;
end
else begin
baud_cnt <= baud_cnt + 1'b1;
end
end
// byte_cnt计数
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
byte_cnt <= 3'd0;
end
else if(byte_cnt == 3'd4) begin
byte_cnt <= 3'd0;
end
else if(uart_state == END && baud_cnt == 13'd0) begin
byte_cnt <= byte_cnt + 1'b1;
end
else begin
byte_cnt <= byte_cnt;
end
end
// data_rd_flag
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
data_rd_flag <= 1'b0;
end
else if(byte_cnt == 3'd4) begin
data_rd_flag <= 1'd1;
end
else begin
data_rd_flag <= 1'b0;
end
end
// wr_data_reg
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_data_reg <= 32'd0;
end
else if(uart_state == END && byte_cnt != 3'd0 && baud_cnt == 13'd1) begin
wr_data_reg <= {wr_data_reg[23:0], byte_data};
end
else begin
wr_data_reg <= wr_data_reg;
end
end
// rom_wr_en_o,rom_wr_data_o
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rom_wr_en_o <= 1'b0;
rom_wr_data_o <= 32'd0;
end
else if(data_rd_flag == 1'b1) begin
rom_wr_en_o <= 1'b1;
rom_wr_data_o <= wr_data_reg;
end
else begin
rom_wr_en_o <= 1'b0;
rom_wr_data_o <= rom_wr_data_o;
end
end
// rom_wr_addr_o
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rom_wr_addr_o <= 32'd0;
end
// 待数据写入后,地址+4
else if(rom_wr_en_o == 1'b1) begin
rom_wr_addr_o <= rom_wr_addr_o + 3'd4;
end
else begin
rom_wr_addr_o <= rom_wr_addr_o;
end
end
// rom_erase_en_o
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rom_erase_en_o <= 1'b0;
end
else if(uart_state == BEGIN && baud_cnt == 13'd0 && byte_cnt == 3'd0 && rom_wr_addr_o == 32'd0) begin
rom_erase_en_o <= 1'b1;
end
else begin
rom_erase_en_o <= 1'b0;
end
end
// uart_state状态机
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_state <= IDLE;
byte_data <= 8'd0;
end
else begin
case(uart_state)
IDLE: begin
if(uart_rx_temp == 1'b0 && uart_rx_delay == 1'b1) begin
uart_state <= BEGIN;
end
else begin
uart_state <= uart_state;
end
end
BEGIN: begin
if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= BIT0;
end
else begin
uart_state <= uart_state;
end
end
BIT0: begin
if(baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
byte_data <= {uart_rx_delay, byte_data[7:1]};
end
else if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= BIT1;
end
else begin
uart_state <= uart_state;
end
end
BIT1: begin
if(baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
byte_data <= {uart_rx_delay, byte_data[7:1]};
end
else if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= BIT2;
end
else begin
uart_state <= uart_state;
end
end
BIT2: begin
if(baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
byte_data <= {uart_rx_delay, byte_data[7:1]};
end
else if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= BIT3;
end
else begin
uart_state <= uart_state;
end
end
BIT3: begin
if(baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
byte_data <= {uart_rx_delay, byte_data[7:1]};
end
else if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= BIT4;
end
else begin
uart_state <= uart_state;
end
end
BIT4: begin
if(baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
byte_data <= {uart_rx_delay, byte_data[7:1]};
end
else if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= BIT5;
end
else begin
uart_state <= uart_state;
end
end
BIT5: begin
if(baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
byte_data <= {uart_rx_delay, byte_data[7:1]};
end
else if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= BIT6;
end
else begin
uart_state <= uart_state;
end
end
BIT6: begin
if(baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
byte_data <= {uart_rx_delay, byte_data[7:1]};
end
else if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= BIT7;
end
else begin
uart_state <= uart_state;
end
end
BIT7: begin
if(baud_cnt == BAUD_CNT_MAX / 2 - 1) begin
byte_data <= {uart_rx_delay, byte_data[7:1]};
end
else if(baud_cnt == BAUD_CNT_MAX - 1) begin
uart_state <= END;
end
else begin
uart_state <= uart_state;
end
end
END: begin
if(baud_cnt == 2) begin
uart_state <= IDLE;
end
else begin
uart_state <= uart_state;
end
end
default: begin
byte_data <= 8'd0;
uart_state <= IDLE;
end
endcase
end
end
endmodule
目前该串口只用于往rom中写入指令,之后会增加一些其他的功能。
二、上板验证
可以到我的仓库里面下载整个项目的代码:cpu_prj: 一个基于RISC-V指令集的CPU实现
进入到FPGA目录下,使用quartus打开工程(因为我现在手上只有altera的板子)。
首先绑定引脚:
clk为系统时钟,绑定你板子对应的时钟引脚即可;rst_n为复位信号,低电平有效;uart_rx和uart_tx为串口的接收和发送引脚,绑定你们板子上的串口引脚即可(这里要注意,不同板子串口使用的接口标准和波特率不一样,需要相应的修改,我这里接口规范是RS232,波特率为9600);res_data为ram中地址为0x00000000位置的数据,等下编写C程序会把结果存放到这个位置,绑定的引脚为我板子上的四个led灯:
如果程序计算结果为15,即1111,那么四个灯全亮,如果为3,则只亮右边两个灯。
引脚绑定完后进行编译, 连好板子烧录程序:
接下来就是去写一个C程序了,下面是一个简单的求和程序,计算结果为15,处理器执行完程序会让四个led灯全亮:
int main(){
int n = 5;
int sum = 0;
for (int i = 1; i <= n; ++i) {
sum = sum + i;
}
int* point;
point = (int*) 0x00000000;
*point = sum;
return 0;
}
然后使用ripes把程序编译成二进制代码(不知道怎么使用配置ripes的可以看我上一篇文章:RISC-V处理器的设计与实现(二)—— CPU框架设计_Patarw_Li的博客-CSDN博客):
复制右边的二进制指令到matlab文件夹下的instructions.txt文件中:
然后用matlab打开b2h文件,点击运行,把二进制指令转成16进制(因为野火的串口发送工具只能发送16进制):
将串口连接PC:
打开野火的串口发送工具,配置如下:
加载我们之前生成的16进制txt文件:
先按一下板子上的复位键,然后打开串口,点击发送数据:
可以发现还没有变化:
再按一下复位键,可以发现四个灯亮起:
既然可以执行C程序了,并且可以用C来控制led灯,那么我们用C语言来实现一个流水灯程序来看看把:
int main(){
int* point;
int sum1 = 1; // 0001
int sum2 = 2; // 0010
int sum3 = 4; // 0100
int sum4 = 8; // 1000
point = (int*) 0x00000000;
*point = sum1;
while(1){
// 第一个灯亮起
*point = sum1;
for(int i = 0; i < 1000000; i++); // delay
// 第二个灯亮起
*point = sum2;
for(int i = 0; i < 1000000; i++); // delay
// 第三个灯亮起
*point = sum3;
for(int i = 0; i < 1000000; i++); // delay
// 第四个灯亮起
*point = sum4;
for(int i = 0; i < 1000000; i++); // delay
}
return 0;
}
还是和上面步骤一样,转成16进制后,先按复位键,然后点击发送:
等待一会之后,按下复位键,可以发现板子上的led交替闪烁,我们用C写的流水灯程序就实现啦!
三、总结与思考
这一次我们完成了将我们做的处理器移植到板子上,并且在使用我们的处理器运行C语言实现的流水灯程序,并且成功运行。这是不是意味着。。。。我们也能在我们的处理器上跑一个简易的操作系统!接下来我会研究如何到我们的处理器上跑起来一个简易的操作系统,之后也会更新相关的文章~