FPGA基础入门【8】开发板外部存储器SPI flash访问

前两篇教程利用数码管project介绍了chipscope和各种烧写开发板的方式,这篇开始继续探索开发板,这次关注外置存储器的控制,外置指的是芯片外部,不是开发板外部。板子上的外置存储器有DDR2和SPI flash,本次先写如何读写SPI flash。

开发板中的外置存储器

开发板的具体介绍参考此链接:NEXYS 4 DDR Manual PDF

开发板上有两种外置存储器,一种是DDR2,另一种就是上一篇博文中用来保存mcs文件的SPI flash。DDR2已经比较过时了,只因为NEXYS4板本身就比较老,现在新的板子比较多的使用DDR4。SPI flash容量为16M,开发板的配置文件只需要4M以内,因此开发板实际上还可以利用剩下的近12M空间,由于SPI接口不需要调用IP,我们优先介绍SPI flash。

Nexys 4 DDR开发板比起Nexys 4开发板,将原本的Cellular RAM换成了Micron MT47H64M16HR-25:H DDR2存储器,将容量从16M提升到了128M。我们可以使用Vivado的MIG(Memory Interface Generator)来生成和DDR的接口,这一部分下次再写。
DDR2
开发板上另外一个存储设备SPI flash序列号是S25FL128S,使用SPI接口和FPGA连接,接口如下
flash

存储器细节

NEXYS 4 DDR开发板上的SPI flash使用的是S25FL128S芯片,它的文档可以在这里下载:S25FL128S Datasheet

引脚配置

Flash Block Diagram
从图中可以看出有几个引脚是复用的,这是教程第一次用到FPGA以外的芯片,做点详细的分析。

  • CS#是Chip Select (negative),低电平触发的芯片选择
  • SCK是Serial Clock,串口时钟;SI是Serial Input,串行输入,在双通道或者四通道模式中复用为IO0接口
  • SO是Serial Output,串行输出,在双通道或者四通道模式中复用为IO1接口
  • WP#是Write Protect (negative),低电平触发的写保护,触发时无法配置控制寄存器,在双通道或者四通道模式中复用为IO2接口
  • HOLD#是Hold,低电平触发后可以在不结束CS触发或不停止SCK的情况下暂停串行通信,在双通道或者四通道模式中复用为IO3接口
  • RESET#是低电平触发的复位

这篇教程追求简易实现,暂时不深究如何使用复用接口,只选择初始功能

指令集

不少芯片都用指令访问,具体指令配置要看文档。

指令集有几种不同的形式,不同的时序图,把文档中和指令时序图摘出几个:

  • 独立指令
    standalone
  • 单字节输入指令
    single byte
  • 单字节输出指令
    single byte out
  • 寻址单字节输出指令
    Flash wave

观察这几种基本指令的规律,可以把它分为几部分

  • 指令instruction都是8位,必然在最先输出
  • 地址address可以是32位或者24位,有些指令不需要地址。理论上来说16MB只需要24位地址,但内部有头地址配置,可以用多枚芯片组成最高4GB,32位地址的存储空间。由于板子上只有一枚,这篇教程只使用24位地址
  • 数据data可以是写入flash的数据,也可以是从flash读出的数据,这个数据以字节为单位,可长可短

因此写指令状态机时需要配置的是:指令本身、是否有地址、地址、读还是写数据、读写数据长度(0表示不需要数据)。这在之后的Verilog code中要用

控制寄存器

S25FL128S的寄存器如下
CSR
第一个最重要,其他的看看就好。第一个SR1定义如下
SR1

  • 第7位是状态寄存器写锁定,1表示在WP#引脚拉低时将状态寄存器锁定,无法被修改,0表示不锁定
  • 第6位是烧写错误位,1表示烧写有错误
  • 第5位是擦除错误位,1表示擦除有错误
  • 第4到2位是块保护,1表示将相应存储块锁定,不可擦除
  • 第1位是写使能位,只有将这位写到1才能进行烧写擦除操作,否则会被保护,指令写使能WREN和写去能WRDI可以修改这一位
  • 第0位是忙碌位,1表示设备正忙,忽略新进来的指令(除了读取状态,和写暂停等少数指令),0表示设备准备好执行下一个指令

重要指令

  1. 读取芯片型号READ_ID,指令0x90h,地址24’h000000,读取两个byte,分别是厂商ID和型号ID,S25FL128S的厂商ID为01h,型号ID为17h
    READ_ID
  2. 读取芯片信息RDID,指令0x9Fh,不需要地址,读取81个byte
    RDID
  3. 读取状态寄存器1 RDSR1,指令0x05h,不需要地址,可以持续读取,不固定读取长度
    RDSR1
  4. 写使能WREN,指令0x06h,无需地址,将写入使能寄存器拉高
    WREN
  5. 擦除一个sector SE,指令0x20h,需要24位地址,在拉高CS后会将该地址所在的sector都设置成高电平,一个sector有64kB,该命令需要写使能WREN后才有效
    SE
  6. 读取数据READ,指令0x03h,需要24位地址,返回第一个byte是相应地址的数据,在拉高停止CS或者停止时钟SCK之前,SO会不停的输出下一个地址的数据,直到最高地址,然后再回到地址0循环往复
    READ
  7. 写入数据Page Program(PP),指令0x02h,需要24位地址,之后跟上一个page的数据,在这里是512byte。如果地址最低9位位非0,则在一个page写完后自动写到下一个page,这个命令只有在写使能WREN之后才有效。如果输入的数据不到512byte,则只从起始地址开始输入相应数据。PP命令只能将相应位置的数据从1写成0,而不能将0拉高成1,因此要写入新数据应该先调用擦除命令,比如SE
    PP

逻辑设计

状态机配置

状态机,又称作有限状态机finite-state machine (FSM),是FPGA逻辑设计中常见的结构,相比起一般的数字逻辑,它可以比较容易的在FPGA中实现类似软件逻辑。

可以看出SPI flash的配置更偏向软件逻辑,根据不同的instruction依次配置,具体设计如下
FSM

测试读写

伪代码设计大致如下:

READ_ID
RDID
RDSR1
RDCR
READ 0x800000 32
WREN
RDSR1
SE 0x800000
RDSR1
READ 0x800000 32
WREN
PP 0x800000 0x5a5a5a5a....
while(RDSR1 return busy) {RDSR1}
READ 0x800000 32

代码设计

module flash(
    input       clk,
    input       rst,
	output reg  led, // used to debug
    
    // Ports for SPI Flash
    output reg  cs_n,
    input       sdi,
    output reg  sdo,
    output      wp_n,
    output      hld_n
);

assign wp_n  = 1'b1;
assign hld_n = 1'b1;

顶层设计中,通用部分依旧是时钟、复位和LED测试输出;其他引脚和SPI flash相连

你可能注意到应该如下所示有6个引脚,但我们只写了5个。原因是SCK引脚所在的E9引脚是系统占用的,因为SPI flash可以作为FPGA初始化配置的来源,具体做法可以参考我的上一篇教程

要使用E9引脚,则需要使用一个Xilinx提供的特殊IP STARTUPE2。它能在FPGA初始化完成之后把这个引脚的控制权交给你,具体用法之后会有
FPGA_flash

parameter IDLE       = 4'b0000;
parameter START      = 4'b0001;
parameter INST_OUT   = 4'b0010;
parameter ADDR1_OUT  = 4'b0011;
parameter ADDR2_OUT  = 4'b0100;
parameter ADDR3_OUT  = 4'b0101;
parameter WRITE_DATA = 4'b0110;
parameter READ_DATA  = 4'b0111;
parameter ENDING     = 4'b1000;

reg         sck;
reg  [3:0]  state;
reg  [3:0]  next_state;
(* dont_touch = "true" *)reg  [7:0]   instruction;
(* dont_touch = "true" *)reg  [7:0]   datain_shift;
(* dont_touch = "true" *)reg  [7:0]   datain;
reg  [7:0]  dataout;
reg         sck_en;
reg  [2:0]  sck_en_d;
reg  [2:0]  cs_n_d;

(* dont_touch = "true" *)reg  [7:0]  inst_count;
reg         temp;
reg  [3:0]  sdo_count;
reg  [15:0] page_count;
reg  [7:0]  wait_count;
reg  [23:0] addr;
reg         wrh_rdl;  // High indicates write, low indicates read
reg         addr_req;  // Address writing requested
reg  [15:0] wr_cnt;  // Number of bytes to be written
reg  [15:0] rd_cnt;  // Number of bytes to be read

资源定义,parameter是给状态机用的名称。

其中的(* dont_touch = “true” *)是Xilinx定义的资源特质Attribute,在寄存器register定义前加上这个后,编译器就不会把它重新配置来优化布局,这么做是为了方便ChipScope找到相应的寄存器。如果最后逻辑正确,可以将这个Attribute去掉重新编译,对逻辑本身没有影响,仿真也会忽视它。

// State machine
always @(posedge clk or posedge rst) begin
	if(rst) begin
		state <= IDLE;
	end
	else begin
		state <= next_state;
	end
end

always @(posedge clk or posedge rst) begin
	if(rst) begin
		next_state  <= IDLE;
		sck_en      <= 1'b0;
		cs_n_d[0]   <= 1'b1;
		dataout     <= 8'd0;
		sdo_count   <= 4'd0;
		sdo         <= 1'b0;
		datain      <= 8'd0;
        inst_count  <= 8'd0;
        temp        <= 1'b0;
        page_count  <= 16'd0;
        wait_count  <= 8'd0;
	end
	else begin
		case(state)
		IDLE: 
		begin	// IDLE state
			next_state <= START;
            wait_count <= 8'd0;
		end
		START:
		begin	// enable SCK and CS
			sck_en <= 1'b1;
			cs_n_d[0]  <= 1'b0;
			next_state <= INST_OUT;
		end
		INST_OUT:
		begin	// send out instruction
			if(sdo_count == 4'd1) begin
				{sdo, dataout[6:0]} <= instruction;
			end
			else if(sdo_count[0]) begin
				{sdo, dataout[6:0]} <= {dataout[6:0],1'b0};
			end
			
			if(sdo_count != 4'd15) begin
				sdo_count <= sdo_count + 4'd1;
			end
			else begin
				sdo_count  <= 4'd0;
				next_state <= (addr_req) ?  ADDR1_OUT : ((wrh_rdl) ? ((wr_cnt==16'd0) ? ENDING : WRITE_DATA) : ((rd_cnt==16'd0) ? ENDING : READ_DATA));
			end
		end
		ADDR1_OUT:
		begin	// send out address[23:16]
			if(sdo_count == 4'd1) begin
				{sdo, dataout[6:0]} <= addr[23:16];
			end
			else if(sdo_count[0]) begin
				{sdo, dataout[6:0]} <= {dataout[6:0],1'b0};
			end
			
			if(sdo_count != 4'd15) begin
				sdo_count <= sdo_count + 4'd1;
			end
			else begin
				sdo_count  <= 4'd0;
				next_state <= ADDR2_OUT;
			end
		end
		ADDR2_OUT:
		begin	// send out address[15:8]
			if(sdo_count == 4'd1) begin
				{sdo, dataout[6:0]} <= addr[15:8];
			end
			else if(sdo_count[0]) begin
				{sdo, dataout[6:0]} <= {dataout[6:0],1'b0};
			end
			
			if(sdo_count != 4'd15) begin
				sdo_count <= sdo_count + 4'd1;
			end
			else begin
				sdo_count  <= 4'd0;
				next_state <= ADDR3_OUT;
			end
		end
		ADDR3_OUT:
		begin	// send out address[7:0]
			if(sdo_count == 4'd1) begin
				{sdo, dataout[6:0]} <= addr[7:0];
			end
			else if(sdo_count[0]) begin
				{sdo, dataout[6:0]} <= {dataout[6:0],1'b0};
			end
			
			if(sdo_count != 4'd15) begin
				sdo_count <= sdo_count + 4'd1;
			end
			else begin
				sdo_count  <= 4'd0;
				next_state <= (wrh_rdl) ? ((wr_cnt==16'd0) ? ENDING : WRITE_DATA) : ((rd_cnt==16'd0) ? ENDING : READ_DATA);
                page_count <= 16'd0;
			end
		end
		WRITE_DATA:
		begin	// send testing data out to flash
			if(sdo_count == 4'd1) begin
				{sdo, dataout[6:0]} <= 8'h5A;
			end
			else if(sdo_count[0]) begin
				{sdo, dataout[6:0]} <= {dataout[6:0],1'b0};
			end
			
			if(sdo_count != 4'd15) begin
				sdo_count <= sdo_count + 4'd1;
			end
			else begin
                page_count <= page_count + 16'd1;
				sdo_count  <= 4'd0;
				next_state <= (page_count < (wr_cnt-16'd1)) ? WRITE_DATA : ENDING;
			end
		end
		READ_DATA:
		begin	// get the first data from flash
            if(~sdo_count[0]) begin
                datain_shift <= {datain_shift[6:0],sdi};
            end
            
            if(sdo_count == 4'd1) begin
                datain <= {datain_shift, sdi};
            end
            
			if(sdo_count != 4'd15) begin
				sdo_count <= sdo_count + 4'd1;
			end
			else begin
                page_count <= page_count + 16'd1;
				sdo_count  <= 4'd0;
				next_state <= (page_count < (rd_cnt-16'd1)) ? READ_DATA : ENDING;
			end
		end
		ENDING:
		begin	//disable SCK and CS, wait for 32 clock cycles
            if(wait_count != 8'd64) begin
                wait_count <= wait_count + 8'd1;
                next_state <= ENDING;
            end
            else begin
                if(instruction == 8'h05 && datain[0]) begin // If in RDSR1, wait until the process ended
                    {inst_count,temp} <= {inst_count,temp};
                end
                else begin
                    {inst_count,temp} <= {inst_count,temp} + 9'd1;
                end
                next_state <= IDLE;
            end
			sck_en <= 1'b0;
			cs_n_d[0] <= 1'b1;
            sdo_count <= 4'd0;
            page_count <= 16'd0;
		end
		endcase
	end
end

SPI接口逻辑部分,就像前面画的逻辑图一样。在READ_DATA阶段每一个byte的数据进入寄存器datain。

最后在ENDING部分,如果判断是RDSR1读取状态寄存器指令,则会自动识别最后一位数据,判断系统是否正在忙着处理其他指令,等到系统忙完再执行下一个指令,否则就停在RDSR1指令

// SCK generator, 50MHz output
always @(posedge clk) begin
    sck_en_d <= {sck_en_d[1:0],sck_en};
end

always @(posedge clk or posedge rst) begin
	if(rst) begin
		sck <= 1'b0;
	end
	else if(sck_en_d[2] & sck_en) begin
		sck <= ~sck;
	end
    else begin
        sck <= 1'b0;
    end
end

always @(posedge clk or posedge rst) begin
    if(rst) begin
        {cs_n,cs_n_d[2:1]} <= 3'h7;
    end
    else begin
        {cs_n,cs_n_d[2:1]} <= cs_n_d;
    end
end

STARTUPE2
#(
.PROG_USR("FALSE"),
.SIM_CCLK_FREQ(10.0)
)
STARTUPE2_inst
(
  .CFGCLK     (),
  .CFGMCLK    (),
  .EOS        (),
  .PREQ       (),
  .CLK        (1'b0),
  .GSR        (1'b0),
  .GTS        (1'b0),
  .KEYCLEARB  (1'b0),
  .PACK       (1'b0),
  .USRCCLKO   (sck),      // First three cycles after config ignored, see AR# 52626
  .USRCCLKTS  (1'b0),     // 0 to enable CCLK output
  .USRDONEO   (1'b1),     // Shouldn't matter if tristate is high, but generates a warning if tied low.
  .USRDONETS  (1'b1)      // 1 to tristate DONE output
);

这部分在处理输出时钟信号sck和片选信号cs_n。片选信号比较简单,只要延迟两个时钟后,在需要的时候输出0即可。

对于时钟输出sck,如果仔细阅读文档,可以发现每个指令都有时钟频率限制:
freq_limit
从这里看出只要保证SCK在50MHz及以下就可以让所有指令顺利运行。而查阅NEXYS4的文档,板载时钟的频率是100MHz,因此我们只需要将其二分。加上一个时钟使能信号sck_en,让它只在需要的时候输出即可。

记得前面说过,sck引脚是和芯片初始化共用的,为了能在芯片初始化完成后使用这个引脚,NEXYS4的文档告诉我们使用Xilinx提供的IP STARTUPE2。这个IP的介绍在Xilinx的文档UG953 Vivado 7series libraries的第421页可以找到:
STARTUPE2_1
STARTUPE2_2
它的简介和引脚介绍告诉我们,这个IP是用来访问特定的引脚的。从网上抄了一个最简单的配置方法,只要把sck连到USRCCLKO上,其他配置连到固定值上即可。

// ROM for instructions
always @(posedge clk) begin
	case(inst_count)
	8'd0 : begin 
    instruction <= 8'h90; wrh_rdl <= 1'b0; addr_req <= 1'b1; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd2; end  // READ_ID
    8'd1 : begin 
    instruction <= 8'h9F; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd81; end  // RDID
	8'd2 : begin 
    instruction <= 8'h05; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd2; end  // RDSR1
    8'd3 : begin 
    instruction <= 8'h35; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd2; end  // RDCR
	8'd4 : begin 
    instruction <= 8'h03; wrh_rdl <= 1'b0; addr_req <= 1'b1; 
    addr <= 24'h800000; wr_cnt <= 16'd0; rd_cnt <= 16'd32; end  // READ
    8'd5 : begin 
    instruction <= 8'h06; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd0; end  // WREN
    8'd6 : begin 
    instruction <= 8'h05; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd2; end  // RDSR1
	8'd7 : begin 
    instruction <= 8'hd8; wrh_rdl <= 1'b0; addr_req <= 1'b1; 
    addr <= 24'h800000; wr_cnt <= 16'd0; rd_cnt <= 16'd64; end  // SE
    8'd8 : begin 
    instruction <= 8'h05; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd2; end  // RDSR1
    8'd9 : begin 
    instruction <= 8'h03; wrh_rdl <= 1'b0; addr_req <= 1'b1; 
    addr <= 24'h800000; wr_cnt <= 16'd0; rd_cnt <= 16'd32; end  // READ
    8'd10: begin 
    instruction <= 8'h06; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd0; end  // WREN
	8'd11: begin 
    instruction <= 8'h02; wrh_rdl <= 1'b1; addr_req <= 1'b1; 
    addr <= 24'h800000; wr_cnt <= 16'd32; rd_cnt <= 16'd0; end  // PP
    8'd12: begin 
    instruction <= 8'h05; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd2; end  // RDSR1
	8'd13: begin 
    instruction <= 8'h03; wrh_rdl <= 1'b0; addr_req <= 1'b1; 
    addr <= 24'h800000; wr_cnt <= 16'd0; rd_cnt <= 16'd32; end  // READ
	default : begin 
    instruction <= 8'h05; wrh_rdl <= 1'b0; addr_req <= 1'b0; 
    addr <= 24'h000000; wr_cnt <= 16'd0; rd_cnt <= 16'd2; end  // RDSR1
	endcase
end

// Debug LED port
always @(posedge clk or posedge rst) begin
	if(rst) begin
		led <= 1'b0;
	end
	else if(instruction == 8'h03) begin
		led <= (datain == 8'h5a) ? 1'b1 : 1'b0;
	end
end

endmodule

伪代码以ROM的形式存在,现在还只是测试flash,不打算做出一个可以在线控制的模块。在指令流程中定义每一个指令的读写模式,是否需要地址,地址值,读长度和写长度。

有些实际不需要读取数据的指令定义了读长度,因为想让它延迟几个时钟。

最后在读取指令(0x03h)时,如果读到自己写入的数据0x5a,则LED输出为高。看到LED亮起就意味着这个测试完成了。

模拟仿真

testbench

仿真testbench tb_flash.v设计如下:

`timescale 1ns/1ns

module tb_flash;

reg clock;
reg reset;
wire led;
wire cs_n;
reg sdi;
wire sdo;
wire wp_n;
wire hld_n;

initial begin
    clock = 1'b0;
    reset = 1'b0;
    sdi = 1'b0;
    // Reset for 1us
    #100 
    reset = 1'b1;
    #1000
    reset = 1'b0;
end

// Generate 50MHz clock signal
always #10 clock <= ~clock;

always @(posedge flash.sck) begin
    sdi <= ~sdi;
end

flash flash(
    .clk(clock),
    .rst(reset),
	.led(led),
    .cs_n(cs_n),
    .sdi(sdi),
    .sdo(sdo),
    .wp_n(wp_n),
    .hld_n(hld_n)
);

endmodule

在这里不对sdi输入做特殊处理,只是随便输入一个信号,只观察SPI接口操作是否正常。

实际上网上可以下载到S25FL128S的仿真模块,可以进行更接近真实的仿真,但图了个方便没做。

库文件调用

在上面说到SCK端口的位置不是通用IO口,而是FPGA初始配置时会使用的引脚,为了在FPGA初始配置完成之后使用这个引脚,要调用Xilinx的基础模块STARTUPE2。为了使仿真可以进行,需要调用Xilinx的library。实现方式如下:
lib1
打开Vivado,无需打开任何project,选择Tools -> Compile Simulation Libraries
lib2
如图选择,选择编译好的library存储路径Compiled library location到自己想要的位置,之后选择自己的ModelSim执行文件路径。下方生成相应的命令:

compile_simlib -simulator modelsim -simulator_exec_path {C:/intelFPGA_pro/18.1/modelsim_ase/win32aloem} -family all -language verilog -library unisim -dir {C:/Users/[account]/xilinx_lib} -force

这个命令的含义是调用相应的ModelSim,将Xilinx的code编译到unisim这个library里,如此ModelSim就可以调用它。理解这个操作后,可以尝试另一种做法,那就是找到xilinx相应代码的位置,然后一起编译。

如果是用默认C盘路径安装的Xilinx Vivado,那么你可以在这个路径找到相应的代码:

C:\Xilinx\Vivado\2018.3\data\verilog\src\unisims\STARTUPE2.v

另外,在使用Xilinx的IP时,都需要编译一个叫做glbl.v的文件用来初始化系统引脚。这个文件可以在安装路径找到,然后拷贝到仿真的根目录中:

C:\Xilinx\Vivado\2018.3\data\verilog\src\glbl.v

仿真脚本

写个脚本sim.do放在仿真根目录方便调试:

vlib work
vlog ../src/flash.v ./tb_flash.v ./glbl.v
vsim -L xpm -L secureip -L unisims_ver -L unimacro_ver -L unifast_ver -L simprims_ver work.tb_flash work.glbl -voptargs=+acc +notimingchecks
log -depth 7 /tb_flash/*
do wave.do
run 1ms

查阅ModelSim的指令集可以看到

  • vlib是创建library
  • vlog是编译
  • vsim是仿真,-L是调用库,这里调用的都是Xilinx基础库文件,-voptargs是定义信号检查的量,如果不定义这些,它只会显示少量的信号
  • wave.do在保存好的波形脚本,在第一次仿真时保存一个,之后就能自动帮你囊括想要的信号,以及想要的形式,方便重复调试
  • run 1ms开始跑,以1ms为时长

为了让ModelSim能找到你在上一步编译好的Xilinx库文件,需要在使用仿真脚本前执行下面的命令。具体路径要参考你把Xilinx的库编译到哪:

vmap xpm C:/Users/[account]/xilinx_lib/xpm
vmap secureip C:/Users/[account]/xilinx_lib/secureip
vmap unisims_ver C:/Users/[account]/xilinx_lib/unisims_ver
vmap unimacro_ver C:/Users/[account]/xilinx_lib/unimacro_ver
vmap unifast_ver C:/Users/[account]/xilinx_lib/unifast_ver
vmap simprims_ver C:/Users/[account]/xilinx_lib/simprims_ver

仿真结果

waveform
仿真波形里可以看出一步一步执行指令

编译实现

新建一个叫flash的project,初始配置可以参考之前的教程。添加代码文件flash.v。

下一步加入约束constraint文件flash.xdc,同样这是用标准模板取自己需要部分修改出来的(NEXYS 4 DDR Master XDC):

## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project

## Clock signal
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports clk]
create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk]

##Switches

set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]

## LEDs

set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports led]

##Quad SPI Flash

set_property -dict {PACKAGE_PIN K17 IOSTANDARD LVCMOS33} [get_ports sdo]
set_property -dict {PACKAGE_PIN K18 IOSTANDARD LVCMOS33} [get_ports sdi]
set_property -dict {PACKAGE_PIN L14 IOSTANDARD LVCMOS33} [get_ports wp_n]
set_property -dict {PACKAGE_PIN M14 IOSTANDARD LVCMOS33} [get_ports hld_n]
set_property -dict {PACKAGE_PIN L13 IOSTANDARD LVCMOS33} [get_ports cs_n]

到这里可以点击 Run Synthesis做综合,几秒钟完成后用Set Up Debug配置ChipScope:
setup debug
设置观察长度为8192,因为持续时间会比较长。下面就可以Run Implementation和Generate Bitstream生成配置文件了。

和前面的教程一样,USB线连接NEXYS4板子,开启Hardware Manager,然后auto连接上板子,Program Device烧写进程序,注意Debug probes file有对应的ltx文件,完成后应该看到最低位有绿色LED亮起:
result
观察ChipScope可以获得更清晰的流程。观察的方式是先设置trigger,最好的trigger是inst_count,配置在其为0x01h时开始捕获,并且在Settings -> Capture Mode Settings -> Trigger position in window中设置为256(或者相对于8192较小的数字),这样捕获出来的波形大部分是在trigger之后的。先把板子上最低位开关拨到0进入reset,再在ChipScope中点击Run,最后拨回开关,这样可以获得Reset之后的波形,如果有什么问题,应该第一个周期就出现了,人工是很难捕捉到的:
READ_ID
第一个READ_ID指令是判断连接是否正确的,从波形图中看出sdo正确输出了0x90h指令和24位地址0x000000h,然后从sdi中
读回的数据是0x17h,是S25FL128S文档中显示的128M芯片传回的正确数据

chipscope wave1
chipscope wave2
接下来的指令一切正常,RDID读回81个byte的芯片数据,RDSR1在WREN后变成0x02,SE擦除了地址0x800000h所在sector的所有数据到1,PP又将制定的数据0x5a写入了flash中,不停读取RDSR1直到烧写完成后再读出,确认数据正确最终将LED点亮

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值