系列文章目录
寄存器表单,寄存器设计代码,寄存器ral文件
概述
在SOC系统中,每个子系统或模块往往包含一组控制端口,通过控制端口可以配置模块中的寄存器,模块功能可以依据寄存器的值改变,这组控制端口就是寄存器配置总线,常见的寄存器配置总线有APB/AHB/OCP总线等等。一个简单的模块代码如下:
module dut(
input clk,
input rst_n,
input bus_cmd_valid,
input bus_op,
input [31:0] bus_addr,
input [31:0] bus_wr_data,
output reg [31:0] bus_rd_data,
input [7:0] rxd,
input rx_dv,
output reg [7:0] txd,
output reg tx_en
);
reg invert;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
txd <= 8'b0;
tx_en <= 1'b0;
end
else if(invert) begin
txd <= ~rxd;
tx_en <= rx_dv;
end
else begin
txd <= rxd;
tx_en <= rx_dv;
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
invert <= 1'b0;
else if(bus_cmd_valid && bus_op) begin
case(bus_addr)
32'h8: invert <= bus_wr_data[0];
default: ;
endcase
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
bus_rd_data <= 32'b0;
else if(bus_cmd_valid && !bus_op) begin
case(bus_addr)
32'h8: bus_rd_data <= {31'b0, invert};
default: bus_rd_data <= 32'b0;
endcase
end
end
endmodule
上述代码中,第4-8行为寄存器配置总线。
第32-44行,通过寄存器配置总线写寄存器的值,模块中只有一个变量叫invert。
invert变量会控制模块的功能,当invert为零时,输出txd与输入rxd的值相等;当invert为一时,输出txd与输入rxd的值相反。
除了invert变量这种可以改变模块功能,还可以表示模块内部的一些状态,对上述代码做一些小小的改变:
module dut(
input clk,
//第1-32行代码保持不变
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
tx_en_cnt <= 32'd0;
else if(rx_dv)
tx_en_cnt <= tx_en_cnt + 32'd1;
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
invert <= 1'b0;
else if(bus_cmd_valid && bus_op) begin
case(bus_addr)
32'h8: invert <= bus_wr_data[0];
default: ;
endcase
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
bus_rd_data <= 32'b0;
else if(bus_cmd_valid && !bus_op) begin
case(bus_addr)
32'h8: bus_rd_data <= {31'b0, invert};
32'hC: bus_rd_data <= tx_en_cnt;
default:
bus_rd_data <= 32'b0;
endcase
end
end
);
上述代码中新增tx_en_cnt,表示模块已经发送了多少次txd数据。配置总线只能读不能写。
上述代码中配置总线位宽为32bit,invert只用了1bit,如果新增加一个sum表示txd的输出是输入数据bit位的总和,且sum和invert在同一个配置总线地址上,对上述代码再做一些小小的改变:
module dut(
input clk,
//第1-32行代码保持不变
reg invert;
reg sum;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
txd <= 8'b0;
tx_en <= 1'b0;
end
else if(invert) begin
txd <= ~rxd;
tx_en <= rx_dv;
end
else if(sum) begin
txd <= rxd[0] + rxd[1] + rxd[2] + rxd[3];
tx_en <= rx_dv;
end
else begin
txd <= rxd;
tx_en <= rx_dv;
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
invert <= 1'b0;
else if(bus_cmd_valid && bus_op) begin
case(bus_addr)
32'h8: begin
invert <= bus_wr_data[0];
sum <= bus_wr_data[1];
end
default: ;
endcase
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
bus_rd_data <= 32'b0;
else if(bus_cmd_valid && !bus_op) begin
case(bus_addr)
32'h8: bus_rd_data <= {30'b0,sum,invert};
default: bus_rd_data <= 32'b0;
endcase
end
end
上述代码中称sum和invert为寄存器域,而32’h8称为一个寄存器,由于配置总线数据位宽为32bit,占据四个地址,所以从地址32’h8-32’hC称为一个寄存器。
每个寄存器可以由多bit组成,每个域也可以由多bit组成。域是寄存器的最小粒度,每个域代表不同的含义和功能。比如:
uart_data就是一个寄存器,parity_error就是一个域,data也是一个域。parity_error域代表UART模块发生了校验位检测错误,CPU读取uart_data寄存器,一看parity_error域值为1就知道UART模块发生了校验位检测错误。
data域代表待发送的UART帧数据,CPU通过写uart_data寄存器就能将想发送的数据发送给UART模块,由UART模块组UART帧进行发送。
寄存器设计代码
为了更好的理解寄存器的功能,我们用一个实例来说明:
在UART模块中,使用D触发器实现parity_mode寄存器域段,用2bit表示,verilog代码如下:
always@(posedge clk or negedge rst_n) begin
if(~rst_n)
parity_mode <= 2'd0;
else if(psel & penable & pwrite & paddr[15:0] == 16'h0008)
parity_mode <= pwdata[2:1];
end
第四行代码含义是CPU可以SOC总线(APB总线)来访问寄存器,当APB总线psel且penable有效且是写操作,且地址低16bit为0x0008时,写数据的bit2-bit1写入到parity_mode中。暂且先不用关心地址为什么是0x0008。
UART模块可以依据CPU配置的parity_mode值决定校验位生成模式,从而实现不同的逻辑功能。
中心化寄存器管理
在项目中,寄存器数据量有时候会很庞大,为了统一方便管理,通常首先用excel表单来记录寄存器相关信息,如下:
确认excel寄存器表单无误后,通过脚本生成Verilog设计代码及验证所需的寄存器ral模型。
寄存器表单
寄存器表单地址
寄存器表单中与地址相关的概念有:base_addr,addr_width,data_width,offset。
首先先了解一下SOC系统中有关CPU地址空间的概念:
CPU在SOC总线中是通过不同的地址空间去进行访问不同的模块,比如一个简单的地址空间划分如下。CPU如果想访问UART模块,就需要保证发起的总线请求(如AHB请求)地址范围必须在0x4000_0000 - 0x4000_0FFF。
模块名称 | 地址范围 |
---|---|
DDR | 0x1000_0000 - 0x4000_0000 |
UART | 0x4000_0000 - 0x4000_0FFF |
SPI | 0x4000_1000 - 0x4000_1FFF |
0x4000_0000 - 0x4000_0FFF是分配给UART的地址空间,我们可以将这段地址空间分配给寄存器,MEM等等,且寄存器,MEM的起始地址,地址大小可以自由分配,只要保证不超过0x4000_0000 - 0x4000_0FFF这段地址空间。具体怎么分配需要由实际的业务决定。
寄存器表单中base_addr即表示寄存器的起始点,如下图寄存器从0x4000_8000开始。offset表示相对于base_addr的偏移。偏移量具体用多少位来表示,就记录在addr_sel信息中。上述寄存器表单中,addr_sel为16bit,对应offset中也用16bit来表示。
addr_width通常与SOC总线地址位宽一致,data_width通常与SOC总线数据位宽一致。
在UART_APB寄存器表单中data_width为32,那么APB请求访问uart_data寄存器时,一次性读写32bit,32bit占位4个地址空间。所以baud_div寄存器offset为0x0004
寄存器表单嵌套
上述例子中寄存器表单属于UART,如verilog中模块嵌套一样,寄存器表单也有嵌套的层次结构。