目录
文章传送门
一、添加串口
二、上板验证
三、总结与思考
文章目录
RISC-V处理器的设计与实现(一)—— 基本指令集_Patarw_Li的博客-CSDN博客
RISC-V处理器的设计与实现(二)—— CPU框架设计_Patarw_Li的博客-CSDN博客
RISC-V处理器的设计与实现(三)—— 上板验证_Patarw_Li的博客-CSDN博客
RISC-V处理器设计(四)—— Verilog 代码设计-CSDN博客
RISC-V处理器设计(五)—— 在 RISC-V 处理器上运行 C 程序-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里面的内容进行修改。
下面是串口烧录模块的Verilog代码,文件为FPGA\rtl\debug\uart_debug.v。
- uart_rx:用于接收bit数据。
- debug_en_i:用于使能串口模块(否则会和uart.v模块冲突),该引脚绑定到板子上的key1,在烧录程序时按住key1,烧录完后松开即可。
- rib_wr_req_o:请求使用总线传递数据。
- mem_wr_en_o:memory写使能信号
- mem_wr_addr_o:memory写地址。
- mem_wr_data_o:memory写数据。
// 串口模块,目前只用于下载程序到memory中,波特率为9600,系统时钟频率为50MHz,传输一位需要5208个时钟周期
module uart_debug(
input wire clk ,
input wire rst_n ,
input wire debug_en_i , // 模块使能信号
input wire uart_rx ,
output reg rib_wr_req_o , // 总线请求信号
output reg mem_wr_en_o , // mem写使能信号
output reg[`INST_ADDR_BUS] mem_wr_addr_o , // mem写地址信号
output reg[`INST_DATA_BUS] mem_wr_data_o // mem写数据信号
);
parameter BAUD_CNT_MAX = `CLK_FREQ / `UART_BPS;
parameter IDLE = 4'd0,
BEGIN= 4'd1,
SEND_BYTE = 4'd2,
END = 4'd3;
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; // 数据就绪标志位
reg[3:0] bit_cnt; // 比特计数
// 将输入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
// rib_wr_req_o
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rib_wr_req_o <= 1'b0;
end
else if(debug_en_i == 1'b0) begin
rib_wr_req_o <= 1'b0;
end
else if(data_rd_flag == 1'b1) begin
rib_wr_req_o <= 1'b1;
end
else begin
rib_wr_req_o <= 1'b0;
end
end
// baud_cnt计数
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
baud_cnt <= 13'd0;
end
else if(debug_en_i == 1'b0) 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(debug_en_i == 1'b0) 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(debug_en_i == 1'b0) 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(debug_en_i == 1'b0) begin
wr_data_reg <= 32'd0;
end
else if(uart_state == END && byte_cnt != 3'd0 && baud_cnt == 13'd1) begin
wr_data_reg <= {byte_data, wr_data_reg[31:8]};
end
else begin
wr_data_reg <= wr_data_reg;
end
end
// mem_wr_en_o,mem_wr_data_o
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
mem_wr_en_o <= 1'b0;
mem_wr_data_o <= 32'd0;
end
else if(debug_en_i == 1'b0) begin
mem_wr_en_o <= 1'b0;
mem_wr_data_o <= 32'd0;
end
else if(data_rd_flag == 1'b1) begin
mem_wr_en_o <= 1'b1;
mem_wr_data_o <= wr_data_reg;
end
else begin
mem_wr_en_o <= 1'b0;
mem_wr_data_o <= mem_wr_data_o;
end
end
// mem_wr_addr_o
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
mem_wr_addr_o <= 32'd0;
end
else if(debug_en_i == 1'b0) begin
mem_wr_addr_o <= 32'd0;
end
// 待数据写入后,地址+4
else if(mem_wr_en_o == 1'b1) begin
mem_wr_addr_o <= mem_wr_addr_o + 3'd4;
end
else begin
mem_wr_addr_o <= mem_wr_addr_o;
end
end
// uart_state状态机
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_state <= IDLE;
byte_data <= 8'd0;
bit_cnt <= 4'd0;
end
else if(debug_en_i == 1'b0) begin
uart_state <= IDLE;
byte_data <= 8'd0;
bit_cnt <= 4'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 <= SEND_BYTE;
end
else begin
uart_state <= uart_state;
end
end
SEND_BYTE: begin
if(bit_cnt == 4'd7 && baud_cnt == BAUD_CNT_MAX - 1) begin
bit_cnt <= 4'd0;
uart_state <= END;
end
else 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
bit_cnt <= bit_cnt + 1'b1;
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
bit_cnt <= 4'd0;
byte_data <= 8'd0;
uart_state <= IDLE;
end
endcase
end
end
endmodule
二、上板验证
可以到我的仓库里面下载整个项目的代码:cpu_prj: 一个基于RISC-V指令集的CPU实现
配套的简易操作系统程序也在更新中:riscv_os: 一个RISC-V上的简易操作系统
进入到FPGA目录下,使用quartus打开工程(因为我现在手上只有altera的板子)。
首先绑定引脚:
- clk:系统时钟。
- rst_n:复位信号,低电平有效。
- gpio_pins:目前只用于控制led,绑定板子上的四个led(gpio_data寄存器位于地址0x30000004处,控制其低四位值即可控制led灯)。
- uart_debug_pin:串口烧录模块使能信号,绑定板子上的key1。
- uart_tx、uart_rx:串口发送和接收引脚,绑定你们板子上的串口引脚即可(这里要注意,不同板子串口使用的接口标准和波特率不一样,需要相应的修改,我这里接口规范是RS232,波特率为19200)
引脚绑定完后进行编译, 连好板子烧录程序:
接下来就是去写一个C程序了,下面是一个简单的求和程序,计算结果sum为15,后面会把sum结果写入gpio_data寄存器,这样gpio_data寄存器的内容的低四位即为1111,会让板子上的led灯全亮起来:
int main(){
int n = 5;
int sum = 0;
for (int i = 1; i <= n; ++i) {
sum = sum + i;
}
int* gpio_data = (int*) 0x30000004; // gpio_data寄存器的地址
*gpio_data = sum; // 将gpio_data寄存器的内容改为sum值
return 0;
}
如何配置交叉编译工具链和烧录到板子上可以参考我的这篇文章 :
开发一个RISC-V上的操作系统(一)_Patarw_Li的博客-CSDN博客
将串口连接PC,执行Python串口发送程序 serial_send.py 烧录编译生成的.bin文件(烧录前一定先把上面的博客看完!):
再按一下复位键,可以发现四个灯亮起:
既然可以执行C程序了,并且可以用C来控制led灯,那么我们用C语言来实现一个流水灯程序来看看把:
int main(){
int sum1 = 1; // 0001
int sum2 = 2; // 0010
int sum3 = 4; // 0100
int sum4 = 8; // 1000
int* gpio_data = (int*) 0x30000004;
*gpio_data = sum1;
while(1){
// 第一个灯亮起
*gpio_data = sum1;
for(int i = 0; i < 1000000; i++); // delay
// 第二个灯亮起
*gpio_data = sum2;
for(int i = 0; i < 1000000; i++); // delay
// 第三个灯亮起
*gpio_data = sum3;
for(int i = 0; i < 1000000; i++); // delay
// 第四个灯亮起
*gpio_data = sum4;
for(int i = 0; i < 1000000; i++); // delay
}
return 0;
}
还是和上面步骤一样,烧录程序到板子上后,按下复位键,可以发现板子上的led交替闪烁,我们用C写的流水灯程序就实现啦!
三、总结与思考
这一次我们完成了将我们做的处理器移植到板子上,并且在我们的处理器上运行C语言实现的流水灯程序,并且成功运行。这是不是意味着。。。。我们也能在我们的处理器上跑一个简易的操作系统!接下来我会研究如何到我们的处理器上跑起来一个简易的操作系统,之后也会更新相关的文章~文章来源:https://www.toymoban.com/news/detail-514193.html
如果遇到问题也欢迎加群 892873718 交流~文章来源地址https://www.toymoban.com/news/detail-514193.html
到了这里,关于RISC-V处理器的设计与实现(三)—— 上板验证(基于野火征途Pro开发板)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!