前几篇博文中注释了RISC-V的内核CPU部分,从这篇开始来介绍RISC-V SoC的外设部分。
另外,在最后一个章节中会上传额外添加详细注释的工程代码,完全开源,如有需要可自行下载。
目录
0 RISC-V SoC注解系列文章目录
1. 结构
2. GPIO模块
2.1 输入和输出端口
2.2 代码注解
2.3 GPIO功能实现
0 RISC-V SoC注解系列文章目录
零、RISC-V SoC软核笔记详解——前言
一、RISC-V SoC内核注解——取指
二、RISC-V SoC内核注解——译码
三、RISC-V SoC内核注解——执行
四、RISC-V SoC内核注解——除法(试商法)
五、RISC-V SoC内核注解——中断
六、RISC-V SoC内核注解——通用寄存器
七、RISC-V SoC内核注解——总线
八、RISC-V SoC外设注解——GPIO
九、RISC-V SoC外设注解——SPI接口
十、RISC-V SoC外设注解——timer定时器
十一、RISC-V SoC外设注解——UART模块(终篇)
1. 结构
如下图,我们之前介绍的RISC-V内核部分,是图中左上角的RISC-V处理器核。而内核和所有的外设都挂载在总线上,内核通过总线和外设进行数据交互。这六个外设中,RAM和ROM外设已经在之前的博文中进行了解析,因此不再赘述。现在我们来介绍外设中的GPIO外设。
2. GPIO模块
2.1 输入和输出端口
input wire clk,
input wire rst,
input wire we_i, //总线写使能
input wire[31:0] addr_i, //总线 配置IO口寄存器地址
input wire[31:0] data_i, //总线 写数据(用来配置IO口相关寄存器)
input wire[1:0] io_pin_i, //输入模式下,IO口的输入逻辑电平
output wire[31:0] reg_ctrl, //IO口控制寄存器数据 0: 高阻,1:输出,2:输入
output wire[31:0] reg_data, //IO口数据寄存器数据
output reg[31:0] data_o // 总线读IO口寄存器数据
2.2 代码注解
Step1:先设计两个寄存器:gpio_ctrl(控制GPIO的输入和输出模式); gpio_data(存放GPIO的输入或输出数据)。
// 每2位控制1个IO的模式,最多支持16个IO
// 0: 高阻,1:输出,2:输入
reg[31:0] gpio_ctrl;
// 输入输出数据
reg[31:0] gpio_data;
assign reg_ctrl = gpio_ctrl;
assign reg_data = gpio_data;
Step2:给这两个寄存器规划地址。
// GPIO控制寄存器的地址
localparam GPIO_CTRL = 4'h0;
// GPIO数据寄存器的地址
localparam GPIO_DATA = 4'h4;
Step3:通过寄存器寻址来,对上述定义的两个寄存器进行写操作,通过配置gpio_ctrl寄存器来实现GPIO的输入输出。
// 写寄存器
always @ (posedge clk) begin
if (rst == 1'b0) begin
gpio_data <= 32'h0;
gpio_ctrl <= 32'h0;
end else begin
if (we_i == 1'b1) begin
case (addr_i[3:0]) //寄存器寻址
GPIO_CTRL: begin
gpio_ctrl <= data_i; //通过配置寄存器gpio_ctrl来实现GPIO的输入输出
end
GPIO_DATA: begin
gpio_data <= data_i;
end
endcase
end else begin //如果IO口是输入模式,则将输入的逻辑电平存放到gpio_data寄存器中
if (gpio_ctrl[1:0] == 2'b10) begin
gpio_data[0] <= io_pin_i[0];
end
if (gpio_ctrl[3:2] == 2'b10) begin
gpio_data[1] <= io_pin_i[1];
end
end
end
end
以下代码是通过总线来读IO口相关寄存器数据。
// 读寄存器
always @ (*) begin
if (rst == 1'b0) begin
data_o = 32'h0;
end else begin
case (addr_i[3:0])
GPIO_CTRL: begin
data_o = gpio_ctrl;
end
GPIO_DATA: begin
data_o = gpio_data;
end
default: begin
data_o = 32'h0;
end
endcase
end
end
2.3 GPIO功能实现
在顶层tinyriscv_soc_top.v模块中,
// io0
assign gpio[0] = (gpio_ctrl[1:0] == 2'b01)? gpio_data[0]: 1'bz;
assign io_in[0] = gpio[0];
// io1
assign gpio[1] = (gpio_ctrl[3:2] == 2'b01)? gpio_data[1]: 1'bz;
assign io_in[1] = gpio[1];
// gpio模块例化
gpio gpio_0(
.clk(clk),
.rst(rst),
.we_i(s4_we_o),
.addr_i(s4_addr_o),
.data_i(s4_data_o),
.data_o(s4_data_i),
.io_pin_i(io_in),
.reg_ctrl(gpio_ctrl),
.reg_data(gpio_data)
);
上述代码中:
gpio[0] = (gpio_ctrl[1:0] == 2'b01)? gpio_data[0]: 1'bz;
是说当配置寄存器gpio_ctrl[1:0]为1时,说明GPIO为输出模式,将gpio_data[0]输出至对应的IO口,如果gpio_ctrl[1:0]不为1,即为0或2,对应高阻态和输入模式,都将GPIO设置为高阻态,原因如下:
高阻态是一个数字电路里常见的术语,指的是电路的一种输出状态,既不是高电平也不是低电平,如果高阻态再输入下一级电路的话,对下级电路无任何影响,和没接一样,如果用万用表测的话有可能是高电平也有可能是低电平,随它后面接的东西而定。
参考:
从零开始写RISC-V处理器 | liangkangnan的博客 (gitee.io)
tinyriscv: 一个从零开始写的极简、非常易懂的RISC-V处理器核。 (gitee.com)