当前位置:   article > 正文

基于AHB协议的SRAM控制器的Verilog实现(详解代码)(一)——sram_core的实现_sram协议

sram协议

目录

 一、sramc(sram_controller)的总框架和模块划分

Features of sramc

结构框图和信号描述

二、sram_core的Verilog实现

动手开敲前的构思

Verilog代码和代码分析

三、sram_core的仿真验证

下载链接


 一、sramc(sram_controller)的总框架和模块划分

sram因为其高读写速度常常作为CPU和嵌入式IC的缓存,是一个数字系统中必不可少的存在,因此能够支持各种总线协议的sram控制器也是必不可少的。本系列文章将完成一个支持AHB总线协议的sram控制器的Verilog实现,并对Verilog代码进行详细分析。本篇文章主要完成了sramc的一个子模块——sram_core的Verilog实现。

Features of sramc

思路清晰,先说功能:

  • 能按照AHB协议的读写时序进行数据传输;
  • 能够实现流水线形式的单周期读写;
  • 支持8、16、32位数据传输;
  • 对于一次传输中用不到的sram块,不对其使能;

结构框图和信号描述

将sramc划分为两个子模块:ahb_sramc_if和sram_core。ahb_sramc_if是sram与AHB总线的接口,负责根据协议时序为sram_core提供使能读写控制信号,并对总线地址haddr进行处理提供给sram_core;sram_core负责管理各sram bank的使能信号,并处理写入数据和读出数据。

signalI/Owidthdescriptions
hrstninput1总线复位信号,同步复位,低有效
hclkinput1总线时钟
hsel_sraminput1sramc选中信号
hreadyinput1总线可用信号,从机只能在hready为1时从总线采样
hwriteinput1读写控制信号1写0读
htransinput2传输类型,详见AHB协议
hsizeinput3传输位宽,详见AHB协议
haddrinputAHB_ADDR_WIDTH总线地址
hwdatainputAHB_DATA_WIDTH总线写数据
hreadyoutoutput1从机传输是否完成
hrespoutput1从机传输状态应答信号
hrdataoutputAHB_DATA_WIDTH读数据

二、sram_core的Verilog实现

首先要进一步细化sram_core要实现的功能:

  • 支持8、16、32位的数据传输;
  • 对于一次传输中用不到的sram块,不对其使能;
  • 对sramc的私有地址(private_addr)进行处理,确保每个sram bank收到的地址是sram bank中与private_addr对应的地址;
  • 对要写入的数据进行位重排列,确保正确的数据写入正确的sram bank里;
  • 对读出的数据进行位排列,确保从相应sram bank读出的数据位于输出hrdata的正确位上

乍一看可能会懵,不知道这些功能具体指什么。但听我说,先别急,后面我会尽可能的展开说明。

动手开敲前的构思

首先最多要支持32位的数据传输,并且要支持单周期传输 ,而sram一般是8位的,这就决定了sram_core最少要包含4个sram bank。出于设计难度的考虑,本设计决定使用了4个sram bank,但如果sram比较大,则需增加bank数,使得每个sram bank的大小不超过一定水平。

既然要使用多个sram bank,那就不得不考虑怎样对每个sram bank进行地址分配。同样由于最多要支持32位传输,所以会出现4个bank同时工作的情况,这就决定了只能按照这样的方式分配地址:private_addr 0是sram bank0的地址0,private_addr 1是sram bank1的地址0,private_addr 2是sram bank2的地址0,private_addr 3是sram bank3的地址0,private_addr 4是sram bank0的地址1,private_addr 5是sram bank1的地址1......以此类推。

既然要进行地址分配,那就不得不对private_addr进行处理,确保每个sram bank收到的地址是sram bank中按照上述地址分配方式与private_addr对应的地址。所以要有一个地址处理逻辑块。

由于要使用多个sram bank且要按照上述方式进行地址分配,所以不能将hwdata按照从高到低位直接接入到各sram bank=中,对于hrdata同样如此。考虑下这种情况:总线上传来了对地址2的16位的读请求,sram_core给出的正确数据应该是{16’h0,sram3, sram2},但如果将各sram bank的读出数据直接按照位从高到低接到hrdata上,那么sram_core给出的数据将会是{sram3,sram2,sram1,sram0}。所以要有写数据位重排逻辑和读数据位排列逻辑来处理hwdata和从各sram bank中读出的数据。

另外对于一次传输中用不到的sram块,不对其使能,所以还要有一个sram块使能控制逻辑。

经过以上构思可大致确定sram_core的框架,其框图和信号描述如下所示:

signalI/Owidthdescriptions
hclkinput1总线时钟
hsel_sraminput1sram_core使能信号
writeinput1写控制信号,1有效
readinput1读控制信号,1有效
hsizeinput3传输位宽,详见AHB协议
private_addrinputPRIVATE_ADDR_WIDTH

sramc私有地址

private_addr_reg1inputPRIVATE_ADDR_WIDTH寄存一拍的private_addr
hwdatainputAHB_DATA_WIDTH总线写数据
hrdataoutputAHB_DATA_WIDTH读数据

Verilog代码和代码分析

由于我搞不定Memory compiler所以我就自己写了个sram,对于Verilog行为级设计也够用了,虽然有write和read端,但是个单端口sram,不能同时读写。先附上sram.v:

  1. `timescale 1 ns/1 ns
  2. module sram
  3. #(parameter SRAM_ADDR_WIDTH = 6)
  4. (
  5. clk,
  6. ce,
  7. write,
  8. read,
  9. addr,
  10. wrdata,
  11. rddata
  12. );
  13. localparam SRAM_DEPTH = 1 << SRAM_ADDR_WIDTH;
  14. input clk;
  15. input ce;
  16. input write;
  17. input read;
  18. input[SRAM_ADDR_WIDTH-1:0] addr;
  19. input[7:0] wrdata;
  20. output reg[7:0] rddata;
  21. reg[7:0] mem[0:SRAM_DEPTH-1];
  22. always@(posedge clk) begin //写
  23. if(ce&&write)
  24. mem[addr] = wrdata;
  25. end
  26. always@(posedge clk) begin //读
  27. if(ce&&read)
  28. rddata = mem[addr];
  29. end
  30. endmodule

sram_core.v如下所示:

  1. `timescale 1 ns/1 ns
  2. module sram_core
  3. #(parameter AHB_ADDR_WIDTH = 32, AHB_DATA_WIDTH = 32, BANK_NUM = 4, SRAM_ADDR_WIDTH = 6)
  4. (
  5. clk,
  6. hsel_sram,
  7. write,
  8. read,
  9. hsize,
  10. private_addr,
  11. private_addr_reg1,
  12. hwdata,
  13. hrdata
  14. );
  15. input clk;
  16. input hsel_sram;
  17. input write;
  18. input read;
  19. input[2:0] hsize;
  20. input[AHB_ADDR_WIDTH-1:0] private_addr;
  21. input[AHB_ADDR_WIDTH-1:0] private_addr_reg1;//对于写操作,写入是在地址周期的下一周期发生,另外读出的数据需要维持一个周期,所以地址要寄存一拍
  22. input[AHB_DATA_WIDTH-1:0] hwdata;
  23. output[AHB_DATA_WIDTH-1:0] hrdata;
  24. wire[AHB_ADDR_WIDTH-1:0] sram_addr;
  25. reg[BANK_NUM-1:0] ce; //各SRAM BANK的使能信号
  26. wire[AHB_DATA_WIDTH-1:0] rddata_raw; //直接从各SRAM BANK中出来的读数据,其位顺序需要经过处理
  27. reg[AHB_DATA_WIDTH-1:0] wrdata_processed; //由hwdata经过处理得到,可直接按照从高到低位写入各BANK
  28. reg[AHB_DATA_WIDTH-1:0] rddata_processed; //由rddata_raw处理得到,直接作为输出读数据
  29. assign hrdata = rddata_processed;
  30. assign sram_addr = write ? private_addr_reg1 : private_addr;
  31. /********************各BANK使能控制逻辑*******************/
  32. always@(*) begin
  33. if(!hsel_sram)
  34. ce = 0;
  35. else begin
  36. case(hsize)
  37. 3'b000: begin //如果传输位宽是8位,一次只使能一个BANK
  38. case(sram_addr[1:0])
  39. 2'b00: ce = 4'b0001;
  40. 2'b01: ce = 4'b0010;
  41. 2'b10: ce = 4'b0100;
  42. 2'b11: ce = 4'b1000;
  43. endcase
  44. end
  45. 3'b001: begin //如果传输位宽是16位,一次使能2个BANK
  46. case(sram_addr[1:0])
  47. 2'b00: ce = 4'b0011;
  48. 2'b10: ce = 4'b1100;
  49. default: ce = 4'b0000;
  50. endcase
  51. end
  52. 3'b010: ce = 4'b1111;//如果传输位宽是32位,一次使能4个BANK
  53. default: ce = 4'b0000;
  54. endcase
  55. end
  56. end
  57. /********************写入数据和从各SRAM BANK读出的数据位重排列组合逻辑*******************/
  58. always@(*) begin
  59. if(hsel_sram) begin
  60. case(hsize)
  61. 3'b000: begin
  62. case(private_addr_reg1[1:0])//对于写操作,写入是在地址周期的下一周期发生,另外读出的数据需要维持一个周期,所以地址要寄存一拍
  63. 2'b00: begin
  64. if(write)
  65. wrdata_processed[7:0] = hwdata[7:0];
  66. if(read)
  67. rddata_processed = {24'd0 ,rddata_raw[7:0]};
  68. end
  69. 2'b01: begin
  70. if(write)
  71. wrdata_processed[15:8] = hwdata[7:0];
  72. if(read)
  73. rddata_processed = {24'd0, rddata_raw[15:8]};
  74. end
  75. 2'b10: begin
  76. if(write)
  77. wrdata_processed[23:16] = hwdata[7:0];
  78. if(read)
  79. rddata_processed = {24'd0, rddata_raw[23:16]};
  80. end
  81. 2'b11: begin
  82. if(write)
  83. wrdata_processed[31:24] = hwdata[7:0];
  84. if(read)
  85. rddata_processed = {24'd0, rddata_raw[31:24]};
  86. end
  87. endcase
  88. end
  89. 3'b001: begin
  90. case(private_addr_reg1[1:0])
  91. 2'b00: begin
  92. if(write)
  93. wrdata_processed[15:0] = hwdata[15:0];
  94. if(read)
  95. rddata_processed = {16'd0, rddata_raw[15:0]};
  96. end
  97. 2'b10: begin
  98. if(write)
  99. wrdata_processed[31:16] = hwdata[15:0];
  100. if(read)
  101. rddata_processed = {16'd0, rddata_raw[31:16]};
  102. end
  103. endcase
  104. end
  105. 3'b010: begin
  106. if(write)
  107. wrdata_processed = hwdata;
  108. if(read)
  109. rddata_processed = rddata_raw;
  110. end
  111. endcase
  112. end
  113. end
  114. genvar i;
  115. generate
  116. for(i=0; i<BANK_NUM; i=i+1) begin: sram_instance
  117. sram #(.SRAM_ADDR_WIDTH(SRAM_ADDR_WIDTH))
  118. sram(
  119. .clk(clk),
  120. .ce(ce[i]),
  121. .write(write),
  122. .read(read),
  123. .addr(sram_addr[SRAM_ADDR_WIDTH+1:2]),
  124. //总共有4个BANK,且必须要采用开头描述的地址分配,另外ahb协议有硬性要求,
  125. //传输地址必须与位宽对齐,所以总地址的去掉最低2位就可直接作为各BANK的地址
  126. .wrdata(wrdata_processed[8*i+7:8*i]),
  127. .rddata(rddata_raw[8*i+7:8*i])
  128. );
  129. end
  130. endgenerate
  131. endmodule

sram.v就是一个很基础的memory,没啥好说的,直接来看sram_core。

  1. input[AHB_ADDR_WIDTH-1:0] private_addr;
  2. input[AHB_ADDR_WIDTH-1:0] private_addr_reg1;

先说这两个地址信号,了解过微机原理都知道,一个单片机是需要对其上挂载的所有从机分配地址的,大部分情况下总线地址是不能直接用的,需要转化为从机的私有地址,private_addr就是sramc的私有地址,private_addr寄存一拍得到private_addr_reg1。这些工作决定交给ahb_sramc_if来做。

assign sram_addr = write ? private_addr_reg1 : private_addr;	

sram_addr是直接传给sram bank的地址,为什么写操作要用寄存一拍的地址呢?上时序图: 

 

 上图是ahb协议的基础的读写时序,可以看出ahb协议的数据传输是有两个阶段的:第一个地址周期要提供有效的控制和地址信号,第二个数据周期要进行数据采样。对于读操作,从机采样到数据和控制信号后就要立即开始给出并维持住数据,直到下一个上升沿总线采样完成,所以读操作直接用当前地址即可。而对于写操作,写入数据在从机采样地址的下一个周期才提供,写入操作真正发生在图中的第三个上升沿,而由于流水线工作方式,总线地址已经发生改变,所以写操作要用寄存一拍的地址。

  1. /********************各BANK使能控制逻辑*******************/
  2. always@(*) begin
  3. if(!hsel_sram)
  4. ce = 0;
  5. else begin
  6. case(hsize)
  7. 3'b000: begin //如果传输位宽是8位,一次只使能一个BANK
  8. case(sram_addr[1:0])
  9. 2'b00: ce = 4'b0001;
  10. 2'b01: ce = 4'b0010;
  11. 2'b10: ce = 4'b0100;
  12. 2'b11: ce = 4'b1000;
  13. endcase
  14. end
  15. 3'b001: begin //如果传输位宽是16位,一次使能2个BANK
  16. case(sram_addr[1:0])
  17. 2'b00: ce = 4'b0011;
  18. 2'b10: ce = 4'b1100;
  19. default: ce = 4'b0000;
  20. endcase
  21. end
  22. 3'b010: ce = 4'b1111;//如果传输位宽是32位,一次使能4个BANK
  23. default: ce = 4'b0000;
  24. endcase
  25. end
  26. end

接下来是各sram bank的使能控制逻辑,实现这个功能的思路简单直白,就是根据地址和传输位宽使能对应的sram bank,没什么好说的。注意这里只使用了sram_addr[1:0],因为只用了4个bank,所以只需地址的最低两位就足以判断此次传输要用到那些bank。

现在考虑一下这种情况:总线发起了一个对地址3的16位的传输请求。看一下我们的使能控制逻辑,发现在这种情况下竟然是所有sram bank都不使能,这岂不是一个大BUG?听我说,先别急,ahb协议里有这样一句话:

 意思就是传输地址必须要与传输位宽对齐,比如说16位传输的传输地址的最低位必须是0。所以说上述情况是不应该出现的,如果出现的话应当视作错误来处理,这个任务应该交给ahb_sramc_if来处理。大家可以思考一下为什么ahb协议要这样规定。我在敲代码时的感受就是这个规定最起码简化了设计。

  1. /********************写入数据和从各SRAM BANK读出的数据位重排列组合逻辑*******************/
  2. always@(*) begin
  3. if(hsel_sram) begin
  4. case(hsize)
  5. 3'b000: begin
  6. case(private_addr_reg1[1:0])//对于写操作,写入是在地址周期的下一周期发生,另外读出的数据需要维持一个周期,所以地址要寄存一拍
  7. 2'b00: begin
  8. if(write)
  9. wrdata_processed[7:0] = hwdata[7:0];
  10. if(read)
  11. rddata_processed = {24'd0 ,rddata_raw[7:0]};
  12. end
  13. 2'b01: begin
  14. if(write)
  15. wrdata_processed[15:8] = hwdata[7:0];
  16. if(read)
  17. rddata_processed = {24'd0, rddata_raw[15:8]};
  18. end
  19. 2'b10: begin
  20. if(write)
  21. wrdata_processed[23:16] = hwdata[7:0];
  22. if(read)
  23. rddata_processed = {24'd0, rddata_raw[23:16]};
  24. end
  25. 2'b11: begin
  26. if(write)
  27. wrdata_processed[31:24] = hwdata[7:0];
  28. if(read)
  29. rddata_processed = {24'd0, rddata_raw[31:24]};
  30. end
  31. endcase
  32. end
  33. 3'b001: begin
  34. case(private_addr_reg1[1:0])
  35. 2'b00: begin
  36. if(write)
  37. wrdata_processed[15:0] = hwdata[15:0];
  38. if(read)
  39. rddata_processed = {16'd0, rddata_raw[15:0]};
  40. end
  41. 2'b10: begin
  42. if(write)
  43. wrdata_processed[31:16] = hwdata[15:0];
  44. if(read)
  45. rddata_processed = {16'd0, rddata_raw[31:16]};
  46. end
  47. endcase
  48. end
  49. 3'b010: begin
  50. if(write)
  51. wrdata_processed = hwdata;
  52. if(read)
  53. rddata_processed = rddata_raw;
  54. end
  55. endcase
  56. end
  57. end

接下来是写入数据和读出数据处理逻辑,看着很长一段,但其实同样简单直白,就是根据地址和传输位宽将对应数据放到对应位置上。需要注意的是,这里直接就使用了寄存一拍的地址private_addr_reg1而不是sram_addr,对于写操作这样处理可以理解,为什么读操作也要这样呢?还记得前面在分析ahb读写时序时说过:读数据要维持到总线数据采样结束。如果在这里使用sram_addr我们看看会发生什么: 

可以看到,本应该维持一个周期的正确hrdata竟然变成了一闪而过的毛刺,观察此时的private_addr可找到原因:在110时刻的上升沿地址是0,数据处理逻辑按照地址0来处理数据,但由于流水线机制,这个上升沿之后地址变成了1,而数据处理逻辑是个组合逻辑,会马上按照地址1处理数据,所以地址0的hrdata就会一闪而过。使用private_addr_reg1就能避免这种情况。

  1. genvar i;
  2. generate
  3. for(i=0; i<BANK_NUM; i=i+1) begin: sram_instance
  4. sram #(.SRAM_ADDR_WIDTH(SRAM_ADDR_WIDTH))
  5. sram(
  6. .clk(clk),
  7. .ce(ce[i]),
  8. .write(write),
  9. .read(read),
  10. .addr(sram_addr[SRAM_ADDR_WIDTH+1:2]),
  11. //总共有4个BANK,且必须要采用开头描述的地址分配,另外ahb协议有硬性要求,
  12. //传输地址必须与位宽对齐,所以总地址的去掉最低2位就可直接作为各BANK的地址
  13. .wrdata(wrdata_processed[8*i+7:8*i]),
  14. .rddata(rddata_raw[8*i+7:8*i])
  15. );
  16. end
  17. endgenerate

最后是sram的例化,此处还有一点要说明:注意传给sram的addr是sram_addr去掉了最低两位。还记得我们采用的地址分配方式吗,因为有4个bank,所以每个sram bank中与私有地址对应的地址就是sram_addr1掉最低两位。

三、sram_core的仿真验证

先附上仿真代码:

  1. `timescale 1 ns/1 ns
  2. module sram_core_test();
  3. parameter ADDR_WIDTH = 32, DATA_WIDTH = 32;
  4. reg clk, hsel_sram, write, read;
  5. reg[2:0] hsize;
  6. reg[ADDR_WIDTH-1:0] private_addr;
  7. reg[ADDR_WIDTH-1:0] private_addr_reg1;
  8. reg[DATA_WIDTH-1:0] hwdata;
  9. wire[DATA_WIDTH-1:0] hrdata;
  10. integer i;
  11. sram_core sram_core1 (
  12. .clk(clk),
  13. .hsel_sram(hsel_sram),
  14. .write(write),
  15. .read(read),
  16. .hsize(hsize),
  17. .private_addr(private_addr),
  18. .private_addr_reg1(private_addr_reg1),
  19. .hwdata(hwdata),
  20. .hrdata(hrdata)
  21. );
  22. always #5 clk = ~clk;
  23. always@(posedge clk)
  24. private_addr_reg1 <= private_addr;
  25. initial begin
  26. clk=1; write=1; read=0; hsel_sram=1; hsize=3'd0;
  27. #11 write=1; read=0; hsel_sram=1; hsize=0;
  28. private_addr=32'h0000_0000; hwdata=32'h0000_0000;
  29. for(i=0; i<8; i=i+1) begin: write_8bytes_0
  30. #10 private_addr = private_addr + 1; hwdata = hwdata + 1;
  31. end
  32. #10 write=0; read=1; hsel_sram=1; hsize=3'd0;
  33. private_addr=32'h0000_0000;
  34. for(i=0; i<8; i=i+1) begin: read_8bytes
  35. #10 private_addr = private_addr + 1;
  36. end
  37. #10 write=1; read=0; hsel_sram=1; hsize=3'd1;
  38. private_addr=32'd0000_0000; hwdata=32'h0000_ffff;
  39. for(i=0; i<4; i=i+1) begin: write_4halfwords_1
  40. #10 private_addr = private_addr + 2; hwdata = hwdata + 1;
  41. end
  42. #10 write=0; read=1; hsel_sram=1; hsize=3'd1;
  43. private_addr=32'h0000_0000;
  44. for(i=0; i<4; i=i+1) begin: read_4halfwords
  45. #10 private_addr = private_addr + 2;
  46. end
  47. #10 write=1; read=0; hsel_sram=1; hsize=3'd2;
  48. private_addr=32'h0000_0000; hwdata=32'h0000_0000;
  49. for(i=0; i<2; i=i+1) begin: write_2words_0
  50. #10 private_addr = private_addr + 4; hwdata = hwdata + 1;
  51. end
  52. #10 write=0; read=1; hsel_sram=1; hsize=3'd2;
  53. private_addr=32'h0000_0000;
  54. for(i=0; i<2; i=i+1) begin: read_2words
  55. #10 private_addr = private_addr + 4;
  56. end
  57. #100 $finish;
  58. end
  59. initial begin
  60. $fsdbDumpfile("sramc_test.fsdb");
  61. $fsdbDumpvars(0);
  62. end
  63. endmodule

仿真代码就是地址从0开始,先依次进行8个8位的写操作,然后再依次进行8个8位的读操作,接着是16位先写后读,最后是32位的先写后读。这一个验证肯定不够,我其实做了很多仿真,但没有保存仿真代码,另外我没深入学习过验证,仿真代码不成体系,可以看到我连个task都没写,所以就不班门弄斧的讲仿真代码了。

 这是仿真结果,你们看起来可能会迷,但我很清楚是没问题的。

下载链接

最后附上代码和ahb协议手册下载链接,在设计基于ahb协议的模块时,一定要先仔细看协议手册

https://download.csdn.net/download/qq_44055255/86479698

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/829165
推荐阅读
相关标签
  

闽ICP备14008679号