文章目录
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里面的内容进行修改。
下面是串口接收程序的Verilog代码,其中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) beginuart_rx_delay <= uart_rx_temp;end// baud_cnt计数always @ (posedge clk or negedge rst_n) beginif(!rst_n) begin baud_cnt <= 13'd0;endelse if(uart_state == IDLE || baud_cnt == BAUD_CNT_MAX - 1) beginbaud_cnt <= 13'd0;endelse beginbaud_cnt <= baud_cnt + 1'b1;endend// byte_cnt计数always @ (posedge clk or negedge rst_n) beginif(!rst_n) begin byte_cnt <= 3'd0;endelse if(byte_cnt == 3'd4) beginbyte_cnt <= 3'd0;endelse if(uart_state == END && baud_cnt == 13'd0) beginbyte_cnt <= byte_cnt + 1'b1;endelse beginbyte_cnt <= byte_cnt;end end// data_rd_flagalways @ (posedge clk or negedge rst_n) beginif(!rst_n) begin data_rd_flag <= 1'b0;endelse if(byte_cnt == 3'd4) begindata_rd_flag <= 1'd1;endelse begindata_rd_flag <= 1'b0;end end// wr_data_regalways @ (posedge clk or negedge rst_n) beginif(!rst_n) begin wr_data_reg <= 32'd0;endelse if(uart_state == END && byte_cnt != 3'd0 && baud_cnt == 13'd1) beginwr_data_reg <= {byte_data, wr_data_reg[31:8]};endelse beginwr_data_reg <= wr_data_reg;end end// rom_wr_en_o,rom_wr_data_oalways @ (posedge clk or negedge rst_n) beginif(!rst_n) begin rom_wr_en_o <= 1'b0;rom_wr_data_o <= 32'd0;endelse if(data_rd_flag == 1'b1) beginrom_wr_en_o <= 1'b1;rom_wr_data_o <= wr_data_reg;endelse beginrom_wr_en_o <= 1'b0;rom_wr_data_o <= rom_wr_data_o;end end// rom_wr_addr_oalways @ (posedge clk or negedge rst_n) beginif(!rst_n) begin rom_wr_addr_o <= 32'd0;end// 待数据写入后,地址+4else if(rom_wr_en_o == 1'b1) beginrom_wr_addr_o <= rom_wr_addr_o + 3'd4;endelse beginrom_wr_addr_o <= rom_wr_addr_o;end end// rom_erase_en_oalways @ (posedge clk or negedge rst_n) beginif(!rst_n) begin rom_erase_en_o <= 1'b0;endelse if(uart_state == BEGIN && baud_cnt == 13'd0 && byte_cnt == 3'd0 && rom_wr_addr_o == 32'd0) beginrom_erase_en_o <= 1'b1;endelse beginrom_erase_en_o <= 1'b0;end end// uart_state状态机always @ (posedge clk or negedge rst_n) beginif(!rst_n) beginuart_state <= IDLE;byte_data <= 8'd0;endelse begincase(uart_state)IDLE: beginif(uart_rx_temp == 1'b0 && uart_rx_delay == 1'b1) beginuart_state <= BEGIN; endelse beginuart_state <= uart_state;endendBEGIN: beginif(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= BIT0; endelse beginuart_state <= uart_state;endendBIT0: beginif(baud_cnt == BAUD_CNT_MAX / 2 - 1) beginbyte_data <= {uart_rx_delay, byte_data[7:1]};endelse if(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= BIT1; endelse beginuart_state <= uart_state;endendBIT1: beginif(baud_cnt == BAUD_CNT_MAX / 2 - 1) beginbyte_data <= {uart_rx_delay, byte_data[7:1]};endelse if(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= BIT2; endelse beginuart_state <= uart_state;endendBIT2: beginif(baud_cnt == BAUD_CNT_MAX / 2 - 1) beginbyte_data <= {uart_rx_delay, byte_data[7:1]};endelse if(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= BIT3; endelse beginuart_state <= uart_state;endendBIT3: beginif(baud_cnt == BAUD_CNT_MAX / 2 - 1) beginbyte_data <= {uart_rx_delay, byte_data[7:1]};endelse if(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= BIT4; endelse beginuart_state <= uart_state;endendBIT4: beginif(baud_cnt == BAUD_CNT_MAX / 2 - 1) beginbyte_data <= {uart_rx_delay, byte_data[7:1]};endelse if(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= BIT5; endelse beginuart_state <= uart_state;endendBIT5: beginif(baud_cnt == BAUD_CNT_MAX / 2 - 1) beginbyte_data <= {uart_rx_delay, byte_data[7:1]};endelse if(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= BIT6; endelse beginuart_state <= uart_state;endendBIT6: beginif(baud_cnt == BAUD_CNT_MAX / 2 - 1) beginbyte_data <= {uart_rx_delay, byte_data[7:1]};endelse if(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= BIT7; endelse beginuart_state <= uart_state;endendBIT7: beginif(baud_cnt == BAUD_CNT_MAX / 2 - 1) beginbyte_data <= {uart_rx_delay, byte_data[7:1]};endelse if(baud_cnt == BAUD_CNT_MAX - 1) beginuart_state <= END; endelse beginuart_state <= uart_state;endendEND: beginif(baud_cnt == 2) beginuart_state <= IDLE; endelse beginuart_state <= uart_state;endenddefault: beginbyte_data <= 8'd0;uart_state <= IDLE;endendcaseendendendmodule
目前该串口只用于往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;
}
如何配置交叉编译工具链和烧录到板子上可以参考我的这篇文章 :
开发一个RISC-V上的操作系统(一)_Patarw_Li的博客-CSDN博客
将串口连接PC,使用Python串口发送程序烧录编译生成的.bin文件:
再按一下复位键,可以发现四个灯亮起:
既然可以执行C程序了,并且可以用C来控制led灯,那么我们用C语言来实现一个流水灯程序来看看把:
int main(){int* point;int sum1 = 1; // 0001int sum2 = 2; // 0010int sum3 = 4; // 0100int sum4 = 8; // 1000point = (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;
}
还是和上面步骤一样,烧录程序到板子上等待一会之后,按下复位键,可以发现板子上的led交替闪烁,我们用C写的流水灯程序就实现啦!
三、总结与思考
这一次我们完成了将我们做的处理器移植到板子上,并且在我们的处理器上运行C语言实现的流水灯程序,并且成功运行。这是不是意味着。。。。我们也能在我们的处理器上跑一个简易的操作系统!接下来我会研究如何到我们的处理器上跑起来一个简易的操作系统,之后也会更新相关的文章~