跳转至

实验八 静态存储器访问实验

实验目的

  1. 完成对静态随机读写存储器 SRAM 的访问;
  2. 进一步掌握时序逻辑电路的基本分析和设计方法;

实验内容

设计一个静态存储器读写模块,具体功能如下:

(1)写数据:由开关输入地址,对该地址写入数据,数据值为当前地址 +1;

(2)读数据:由开关输入地址,读出该地址的数据到发光二极管上;

设计需求

  1. 设计一个静态存储器的读写模块,通过模式开关来决定是写数据还是读数据,复位后通过时钟控制状态机完成完成的读或写操作。
  2. 使用时钟模块上的复位开关 CLK 作为手动时钟输入,复位开关 RST 作为复位输入。
  3. 使用一个开关模块上的四个开关作地址输入 address[3:0];另一个开关模块上的一个开关作为读写模式选择 rw
  4. 使用开关模块上的发光二极管作为读出的数据指示灯 out_data 和当前状态指示灯 state_light
  5. 写数据时,首先复位,然后模块通过时钟控制状态机将 address[3:0] 输入的地址写入数据 address[3:0] + 1, 比如 address[3:0] = 0110 时,将内存的 0110 地址写入数据 0111
  6. 读数据时,首先复位,然后模块通过时钟控制状态机将 address[3:0] 输入的地址上的数据读出,然后输出到数据指示灯 out_data
  7. 为了方便调试,将状态机的当前状态输出到当前状态指示灯 state_light
  8. FPGA 与静态存储器 sram 之间为信号为内存地址 sram_address,内存数据 sram_data,芯片使能 sram_ce_n,输出使能 sram_oe_n ,写使能 sram_we_n

这样就可用通过读写模块对静态存储器的不同地址进行读写,并通过数据指示灯 out_data 来查看写入是否正确。

注意

由于开关数量有限,实验中只使用sram的低4位地址,其他地址均可以设成任意值,比如全 0

设计框图

flowchart LR

  clk[复位开关clk]:::periph -- clk ---> sram_rw;

  rst[复位开关rst]:::periph -- rst ---> sram_rw;

  rw[拨动开关rw]:::periph -- rw ---> sram_rw;

  address[拨动开关address]:::periph -- address ---> sram_rw;

  subgraph FPGA
    direction BT
    sram_rw[内存读写sram_rw]:::seq;    
  end

  sram_rw -- sram_address --> sram;
  sram_rw -- sram_data <--> sram;
  sram_rw -- sram_ce_n --> sram;
  sram_rw -- sram_oe_n --> sram;
  sram_rw -- sram_we_n --> sram;

  sram[内存sram]:::seq;    

  sram_rw -- out_data --> out_data[发光二极管]:::periph;

  sram_rw -- state_light --> state_light[发光二极管]:::periph;

  classDef comb fill:#e8f5e9,stroke:#43a047;   %% green
  classDef seq fill:#ffecb3,stroke:#ffa000;    %% orange
  classDef periph fill:#e3f2fd,stroke:#2196f3; %% blue
  linkStyle default opacity:1.0

原理图

静态随机访问存储器 sram

本实验中的存储器为静态随机访问存储器 sram,对于存储器芯片的访问,首先要熟悉存储器芯片的访问时序,并了解存储器芯片与 FPGA 芯片的具体连接方式,也就是说,如何在 FPGA 上具体实现对 RAM 芯片的访问时序。

实验模块使用了 1 片 sram(Static Random Access Memory)作为主要存储器,sram 是一种具有静态存取功能的内存,不需要动态刷新电路即能保存它内部存储的数据,其特点是访问速度快,访问操作、管理方式简单方便,不需要额外的内存刷新电路。

实验用到的 sram 芯片存储容量为 256Kx8bit,此芯片配置 8 位的数据管脚,18 位的地址管脚,以及内存使能信号、内存读写信号、内存输出使能信号,这些管脚都已经预先连接到 FPGA 的不同引脚上,具体请参见可编程模块介绍部分。

对 sram 进行读的时序如下图所示:

从读时序上看,读取数据时要在地址线上提前准备好要读取的地址,经过一定时间延时后就能够在数据线上读取到数据,因此访问的时候需要保证这个延时已经达到,在具体操作过程中就可以在前一个时钟上沿给出地址,接着在下一个时钟上沿读出数据线上的数据(时钟周期>读延迟),需要注意的是由于数据线是双向的,就需要 FPGA 在给出地址的同时将数据线设置成高阻,这样 sram 就能把数据输出到数据线上。在整个读的过程中芯片使能信号、输出使能信号都为 0 (有效),读写信号为 1

对 sram 进行写的时序如下图所示:

写时序稍微复杂一些,简化来看,主要是 3 个步骤,(1)在地址线和数据线上分别发送要写入的地址和数据,此时读写信号为 1;(2)经过一定时间后,将读写信号变成 0;(3)在经过一定时间后,再将读写信号变回 1,此时数据才真正写入 sram 中。可以看出,在整个写过程中地址线和数据线上的值需要保持不变,芯片使能信号、输出使能信号都为 0 (有效)。实际实现的时候,可以在每个时钟上沿处理一个步骤,保证各个步骤之间的延时要求,构成一个状态机。

提示

实验模块上sram的地址线的低8位和数据线都直接连接了发光二极管,就在模块的左上角,标记为 AddressData, 可以用来查看当前的信号线上的值;同时输出使能信号、芯片使能信号和读写信号也连接了发光二极管,标记为 OE CE WE,需要注意的是这三个都是低有效信号,对应的放光二极管也采用的是上拉驱动,因此只有当信号为 0 的时候相应的放光二极管才变亮。

重要

关于双向数据线,当读的时候需要设置成三态,这个该如何处理呢,请参见以下文档。

三态门

或者参考计算机组成原理课程中关于双向总线和三态的介绍

双向总线该如何处理

其实这两个文档都一样

下面代码仅供参考

module sram(
    clk, rst, rw, address, 
    sram_address, sram_ce_n, sram_oe_n, sram_we_n, sram_data, 
    out_data, state_light
);

input clk; // 时钟
input rst; // 复位
input rw; // 读写, 0 为写,1 为读
input [3:0] address; // 要写入的地址

output [17:0] sram_address; // 内存地址
output reg sram_ce_n, sram_oe_n, sram_we_n; // 内存使能
inout [7:0] sram_data; // 内存数据

output reg [7:0]out_data; // 输出数据
output [1:0]state_light; // 输出状态

reg[7:0] sram_data_o;
wire[7:0] sram_data_i;
reg sram_data_t;

assign sram_data = sram_data_t ? 8'bz : sram_data_o;        //双向总线设置
assign sram_data_i = sram_data;

reg [1:0]state; // 状态机
parameter s0 = 2'b00, s1 = 2'b01, s2 = 2'b10, s3 = 2'b11; 

assign state_light = state;

assign sram_address = {14'b0, address};         //地址线输出

assign sram_ce_n = 1'b0;
assign sram_oe_n = 1'b0;

always @(posedge clk, posedge rst)
begin
    if(rst) begin
        state <= s0;
        sram_we_n <= 1'b1;
        sram_data_t <= 1'b0;
    end else if(!rw) begin         //写内存
        sram_data_t <= 1'b0;
        case(state)
            s0: begin          //发送地址和数据
                sram_we_n <= 1'b1;
                sram_data_o <= address + 1;
                state <= s1;
            end
            s1: begin           //发送写信号
                sram_we_n <= 1'b0;
                state <= s2;
            end
            default: begin          //完成写入
                sram_we_n <= 1'b1;
                state <= s2;
            end
        endcase
    end else begin          //读内存
        sram_data_t <= 1'b1;        //输出设置成高阻
        sram_we_n <= 1'b1;
        case(state)
            s0: begin
                state <= s1;
            end
            s1: begin
                out_data <= sram_data_i;
                state <= s1;
            end
            default: begin
                state <= s0;
            end
        endcase
    end
end

endmodule

管脚绑定

除了可编程模块上已经预先连接好的 sram 信号需要绑定,还需要使用接插孔连接开关和发光数码管,其中 CLK 作为时钟输入 clk,IO0 作为复位输入 rst,IO16 ~ IO19 作为地址输入 address[3:0],IO20 作为读写控制 rw,IO1 ~ IO4 IO6 ~ IO9 作为数据指示灯 out_data[7:0], IO11 ~Io12 作为 当前状态指示灯 state_light[1:0],由于发光二极管不够用,可以使用不带译码的七段数码管模块替代。

具体约束文件如下:

#CLK input
set_property -dict {PACKAGE_PIN J19 IOSTANDARD LVCMOS33} [get_ports clk];     #CLK接插孔

#RST input
set_property -dict {PACKAGE_PIN K18 IOSTANDARD LVCMOS33} [get_ports rst];     #IO0接插孔

#mode W/R
set_property -dict {PACKAGE_PIN AA21 IOSTANDARD LVCMOS33} [get_ports rw];     #IO20接插孔

#input address
set_property -dict {PACKAGE_PIN AA18 IOSTANDARD LVCMOS33} [get_ports address[3]];     #IO16接插孔
set_property -dict {PACKAGE_PIN AB18 IOSTANDARD LVCMOS33} [get_ports address[2]];     #IO17接插孔
set_property -dict {PACKAGE_PIN AA20 IOSTANDARD LVCMOS33} [get_ports address[1]];     #IO18接插孔
set_property -dict {PACKAGE_PIN AB21 IOSTANDARD LVCMOS33} [get_ports address[0]];     #IO19接插孔

#data out 
set_property -dict {PACKAGE_PIN M21 IOSTANDARD LVCMOS33} [get_ports out_data[7]];     #IO1接插孔
set_property -dict {PACKAGE_PIN N20 IOSTANDARD LVCMOS33} [get_ports out_data[6]];     #IO2接插孔
set_property -dict {PACKAGE_PIN N22 IOSTANDARD LVCMOS33} [get_ports out_data[5]];     #IO3接插孔
set_property -dict {PACKAGE_PIN P21 IOSTANDARD LVCMOS33} [get_ports out_data[4]];     #IO4接插孔
set_property -dict {PACKAGE_PIN T21 IOSTANDARD LVCMOS33} [get_ports out_data[3]];     #IO6接插孔
set_property -dict {PACKAGE_PIN U21 IOSTANDARD LVCMOS33} [get_ports out_data[2]];     #IO7接插孔
set_property -dict {PACKAGE_PIN R21 IOSTANDARD LVCMOS33} [get_ports out_data[1]];     #IO8接插孔
set_property -dict {PACKAGE_PIN R22 IOSTANDARD LVCMOS33} [get_ports out_data[0]];     #IO9接插孔

#state
set_property -dict {PACKAGE_PIN W21 IOSTANDARD LVCMOS33} [get_ports state_light[1]];     #IO11接插孔
set_property -dict {PACKAGE_PIN W22 IOSTANDARD LVCMOS33} [get_ports state_light[0]];     #IO12接插孔

#SRAM address 
set_property -dict {PACKAGE_PIN P1 IOSTANDARD LVCMOS33} [get_ports sram_address[17]]; 
set_property -dict {PACKAGE_PIN N2 IOSTANDARD LVCMOS33} [get_ports sram_address[16]]; 
set_property -dict {PACKAGE_PIN P2 IOSTANDARD LVCMOS33} [get_ports sram_address[15]]; 
set_property -dict {PACKAGE_PIN M2 IOSTANDARD LVCMOS33} [get_ports sram_address[14]]; 
set_property -dict {PACKAGE_PIN R2 IOSTANDARD LVCMOS33} [get_ports sram_address[13]]; 
set_property -dict {PACKAGE_PIN M1 IOSTANDARD LVCMOS33} [get_ports sram_address[12]]; 
set_property -dict {PACKAGE_PIN U2 IOSTANDARD LVCMOS33} [get_ports sram_address[11]]; 
set_property -dict {PACKAGE_PIN J1 IOSTANDARD LVCMOS33} [get_ports sram_address[10]]; 
set_property -dict {PACKAGE_PIN U1 IOSTANDARD LVCMOS33} [get_ports sram_address[9]];  
set_property -dict {PACKAGE_PIN T1 IOSTANDARD LVCMOS33} [get_ports sram_address[8]];  
set_property -dict {PACKAGE_PIN C2 IOSTANDARD LVCMOS33} [get_ports sram_address[7]];  
set_property -dict {PACKAGE_PIN B1 IOSTANDARD LVCMOS33} [get_ports sram_address[6]];  
set_property -dict {PACKAGE_PIN B2 IOSTANDARD LVCMOS33} [get_ports sram_address[5]];  
set_property -dict {PACKAGE_PIN A1 IOSTANDARD LVCMOS33} [get_ports sram_address[4]];  
set_property -dict {PACKAGE_PIN L4 IOSTANDARD LVCMOS33} [get_ports sram_address[3]];  
set_property -dict {PACKAGE_PIN L1 IOSTANDARD LVCMOS33} [get_ports sram_address[2]];  
set_property -dict {PACKAGE_PIN K2 IOSTANDARD LVCMOS33} [get_ports sram_address[1]];  
set_property -dict {PACKAGE_PIN K1 IOSTANDARD LVCMOS33} [get_ports sram_address[0]];  

#SRAM controll 
set_property -dict {PACKAGE_PIN H2 IOSTANDARD LVCMOS33} [get_ports sram_ce_n]; 
set_property -dict {PACKAGE_PIN J2 IOSTANDARD LVCMOS33} [get_ports sram_oe_n]; 
set_property -dict {PACKAGE_PIN R1 IOSTANDARD LVCMOS33} [get_ports sram_we_n]; 

#SRAM data
set_property -dict {PACKAGE_PIN G2 IOSTANDARD LVCMOS33} [get_ports sram_data[7]]; 
set_property -dict {PACKAGE_PIN G1 IOSTANDARD LVCMOS33} [get_ports sram_data[6]]; 
set_property -dict {PACKAGE_PIN F3 IOSTANDARD LVCMOS33} [get_ports sram_data[5]]; 
set_property -dict {PACKAGE_PIN F1 IOSTANDARD LVCMOS33} [get_ports sram_data[4]]; 
set_property -dict {PACKAGE_PIN E2 IOSTANDARD LVCMOS33} [get_ports sram_data[3]]; 
set_property -dict {PACKAGE_PIN E1 IOSTANDARD LVCMOS33} [get_ports sram_data[2]]; 
set_property -dict {PACKAGE_PIN D2 IOSTANDARD LVCMOS33} [get_ports sram_data[1]]; 
set_property -dict {PACKAGE_PIN D1 IOSTANDARD LVCMOS33} [get_ports sram_data[0]]; 

# required if touch button used as manual clock source
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets clk_IBUF]

set_property CFGBVS VCCO [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]

实验步骤

  1. 使用 vivado2019.2 新建一个工程,编写 system verilog 代码,并进行综合;
  2. 使用时钟模块,开关模块和可编程模块,进行电路连接;
  3. 管脚绑定,编译并下载,观察实验结果。

提高要求

不需要!!有兴趣的话可以仿真一下,没兴趣的不要碰这个。

实验报告

  1. 整理出完整、清晰的代码,并详细添加注释说明电路的工作原理。
  2. 说明电路功能测试的结果。
  3. 总结调试中所遇到的问题及解决方法。

最后更新: 2024年2月28日
作者:cuibst (0.71%), 李山山 (95.04%), Jiajie Chen (4.26%)