在SPI接口的EEPROM的原理与使用中给大家介绍了怎样学习使用一颗SPI接口的EEPROM芯片,包括SPI接口EEPROM芯片的广泛特点,一般通用的操作命令,操作时序图等。在学习使用SPI协议控制EEPORM芯片,调试SPI controller控制程序时,下载程序到FPGA开发板上进行调试是一种常用的方法。在调试时,可以使用示波器, 或者Quartus, Vivado 的工具自带的调试工具(ILA,SignalTap)。
本文介绍一种方法,直接使用仿真工具验证SPI controller, 如果仿真正确,可以再进行编译,在线调试,这样会大大缩减开发周期。在对SPI EEPROM controller 进行仿真时, 在没有SPI EEPROM (slave device)的情况下,也就没有设备应答,因此难以验证controller 是否正确。 解决方法是绝大多数的SPI EEPROM厂家都会提供芯片Verilog模型的源码,开发者可以使用芯片Verilog模型来仿真验证controller。这样controller给仿真模块的芯片Verilog模型发送读写指令,芯片Verilog模型会进行应答。本文将介绍如何使用SPI EEPROM 芯片Verilog模型仿真,以及分析仿真结果的波形。
1. 选择芯片Verilog模型
之前学习了如何操作和使用SPI接口EEPROM芯片Microchip的AT25040B。本文为了验证SPI controller,首选的仿真模型应该是AT25040B。但是Microchip没有提供对应芯片的仿真Verilog模型。因此本文选择了相似的芯片M25AA040A/25LC040A模型进行仿真(SPI接口,容量为4 kbit的EEPROM芯片)。查找相应的仿真芯片模型如图1,图2所示。
图1
图2 芯片Verilog模型
下载解压后得到两个Verilog文件,如图3所示。

图3
文件下载:
[content_control]
[/content_control]
2. 建立仿真工程
将两个Verilog都加进Vivado工程的simulation source,并建立一个仿真的testbench(spi_eeprom_tb.v),如图4所示。

图4
在spi_eeprom_tb.v中设计了如下基本操作:
- 写使能锁存
- 读状态寄存器
- 写数据
- 读状态机寄存器 + 读数据
这里需要注意的是,在controller进行读操作,即 EEPROM进行写操作时,controller仍然需要提供稳定的时钟,片选也需要稳定为低电平。举例读状态寄存器时序图,如图5所示:

图5 读状态寄存器
完整的spi_eeprom_tb.v代码如下所示,其中repeat的用法可以在文章Verilog repeat 循环语句中复习:
`timescale 1ns / 1ps
module spi_eeprom_tb(
);
localparam CLK_PERIOD = 140;
reg SI = 0; // serial data input
wire SO; // serial data output
reg SCK = 0; // serial data clock
reg CS_N = 1; // chip select - active low
reg WP_N = 1; // write protect pin - active low
reg HOLD_N = 1; // interface suspend - active low
reg reset = 0; // model reset/power-on reset
// -------- Command -----------
reg [7:0] RDSR_CMD = 8'h05;
reg [7:0] WREN_CMD = 8'h06;
reg [7:0] WRITE_CMD = 8'h02;
reg [7:0] WRITE_ADDR = 8'h80;
reg [7:0] WRITE_DATA = 8'h00;
reg [7:0] READ_CMD = 8'h03;
reg [7:0] READ_ADDR = 8'h80;
// ------------------------------
reg [7:0] DATA = 0;
reg valid = 0;
integer i = 0;
initial
begin
reset = 1;
SI = 0;
SCK = 0;
CS_N = 1;
#200;
reset = 0;
#800;
//----------------- WRITE WR_EN ----------------
#300;
i = 0;
SI = 0;
SCK = 0;
CS_N = 0;
#CLK_PERIOD;
repeat(8)
begin
SI = WREN_CMD[7-i];
SCK = 0;
#(CLK_PERIOD/2);
SCK = 1;
i = i + 1;
#(CLK_PERIOD/2);
end
SI = 0;
SCK = 0;
CS_N = 0;
#CLK_PERIOD;
CS_N = 1;
//---------- READ SR REGISTER --------------
#300;
i = 0;
SI = 0;
SCK = 0;
CS_N = 0;
#CLK_PERIOD;
repeat(8)
begin
SI = RDSR_CMD[7-i];
SCK = 0;
#(CLK_PERIOD/2);
SCK = 1;
i = i + 1;
#(CLK_PERIOD/2);
end
SCK = 0;
i = 0;
#CLK_PERIOD;
repeat(8)
begin
SCK = 0;
#(CLK_PERIOD/2);
DATA[7-i] = SO;
SCK = 1;
i = i + 1;
#(CLK_PERIOD/2);
end
SI = 0;
valid = 1;
SCK = 0;
#CLK_PERIOD;
valid = 0;
CS_N = 1;
//--------------- WRITE data-----------------
#300;
i = 0;
SI = 0;
SCK = 0;
CS_N = 0;
#CLK_PERIOD;
repeat(8)
begin
SI = WRITE_CMD[7-i];
SCK = 0;
#(CLK_PERIOD/2);
i = i + 1;
SCK = 1;
#(CLK_PERIOD/2);
end
SI = 1;
SCK = 0;
CS_N = 0;
i = 0;
#300;
repeat(8)
begin
SI = WRITE_ADDR[7-i];
SCK = 0;
#(CLK_PERIOD/2);
i = i + 1;
SCK = 1;
#(CLK_PERIOD/2);
end
repeat (8)
begin
i = 0;
SI = 0;
SCK = 0;
#300;
repeat(8)
begin
SI = WRITE_DATA[7-i];
SCK = 0;
#(CLK_PERIOD/2);
i = i + 1;
SCK = 1;
#(CLK_PERIOD/2);
end
WRITE_DATA = WRITE_DATA + 1;
end
SCK = 0;
#CLK_PERIOD;
CS_N = 1;
i = 0;
DATA = 8'h01;
//----------- READ SR REGISTER -------------------
while(DATA[0] == 1)
begin
#2000;
i = 0;
SI = 0;
SCK = 0;
CS_N = 0;
#70;
repeat(8)
begin
SI = RDSR_CMD[7-i];
SCK = 0;
#(CLK_PERIOD/2);
i = i + 1;
SCK = 1;
#(CLK_PERIOD/2);
end
SCK = 0;
i = 0;
#300;
valid = 0;
repeat(8)
begin
SCK = 0;
#(CLK_PERIOD/2);
DATA[7-i] = SO;
SCK = 1;
i = i + 1;
#(CLK_PERIOD/2);
end
valid = 1;
SCK = 0;
#CLK_PERIOD;
valid = 0;
CS_N = 1;
end
//----------------- read data-------------
#300;
i = 0;
SI = 0;
SCK = 0;
CS_N = 0;
#CLK_PERIOD;
repeat(8)
begin
SI = READ_CMD[7-i];
SCK = 0;
#(CLK_PERIOD/2);
i = i + 1;
SCK = 1;
#(CLK_PERIOD/2);
end
SI = 0;
SCK = 0;
i = 0;
#300;
repeat(8)
begin
SI = READ_ADDR[7-i];
SCK = 0;
#(CLK_PERIOD/2);
i = i + 1;
SCK = 1;
#(CLK_PERIOD/2);
end
repeat (8)
begin
i = 0;
SI = 0;
SCK = 0;
#300;
valid = 0;
repeat(8)
begin
SCK = 0;
#(CLK_PERIOD/2);
SCK = 1;
DATA[7-i] = SO;
i = i + 1;
#(CLK_PERIOD/2);
end
valid = 1;
SCK = 0;
#CLK_PERIOD;
valid = 0;
end
SCK = 0;
#100;
CS_N = 1;
#2000;
$stop;
end
// 可以使用任一芯片Verilog模型进行仿真
M25AA040A SPI_DEVICE_inst
//M25LC040A SPI_DEVICE_inst
(
.SI (SI),
.SO (SO),
.SCK (SCK),
.CS_N (CS_N),
.WP_N (WP_N),
.HOLD_N (HOLD_N),
.RESET (reset)
);
endmodule
3. 观察仿真结果
复习SPI接口的EEPROM的原理与使用中指令表如表1所示:
表1
| 命令/Instruction name | 命令格式/Instruction formt | 操作/operation | 命令描述 |
| WREN | 0000X110 | Write enable Set
write enable latch |
写使能锁存 |
| WRDI | 0000X100 | Write disable instruction
Reset write enable latch |
复位写使能 |
| RDSR | 0000X101 | Read status Register | 读状态寄存器 |
| WRSR | 0000X001 | Write status register | 写状态寄存器 |
| READ | 0000A011 | Read data
from memory array |
读数据A表示AT25040的第9位地址 |
| WRITE | 0000A010 | Write data to
memory array |
写数据A表示AT25040的第9位地址为0或1 |
- 写使能锁存

图6 写使能锁存仿真
注意图6中片选在SCK时钟波形前后都是稳定的低电平。因为写使能锁存是master发给slave的指令,所以SI根据SCK输出8’h06,而SO保持高阻。对比芯片的写使能时序图,如图7所示,证明仿真波形正确。

图7 写使能锁存时序图
- 读状态寄存器
状态寄存器格式如表2所示,注意bit 1表示的是写使能:1’b1表示写使能;1’b0表示没有写使能。
表2:

图8所示为读状态寄存器的时序图。

图8读状态寄存器时序图

图9 读状态寄存器仿真
从图9的仿真中可以看出来CS_N信号在 “master读状态机寄存器 + slave输出状态寄存器” 的整个过程中都保持为低电平。SI首先输出8’h05命令,接着SCK停顿了一个时钟周期(这里是因为时钟频率比较高,给芯片内部一点缓冲时间),再进行翻转,SO输出8’02,标志前一个写使能已经打开。
读状态寄存器经常会在写使能锁存之后使用,主要是为了确保芯片的写使能已经打开。
- 写数据
图10所示为写一个字节数据,在仿真中实际上写了8个字节的数据,如图11所示。

图10 写数据时序图

图11 写数据
图11所示为SI输出写指令(8’h02)+ 字节地址({1’b0,8’h80}) + 数据(8’h00) + 数据(8’h01) + 数据(8’h02) + 数据(8’h03) + 数据(8’h04) + 数据(8’h05) + 数据(8’h06) + 数据(8’h07)。
- 读状态机寄存器 + 读数据
为了确保读出准确的数据,需要先读出状态寄存器,如果bit 0为0,即表示上一笔写操作已经成功完成,否则需要等待芯片内部完成写操作。在仿真中将读出的状态寄存器存在DATA中,可以看到 图12中将近5ms的时间DATA的值都是8’h03,表示芯片当前正在内部写数据。

图12 写数据时间约为5ms
在图13红色方框处显示读出状态寄存器的值为8’h00(bit 0的值为0),表示终于可以进行读操作。读操作的命令是8’h03,读地址是8’h80。SO返回读出的数据8’h00,8’h01,8’h02,8’h03,8’h04,8’h05,8’h06,8’h07。对比芯片的读数据时序图13,仿真结果是正确的。

图13 读数据仿真

图14 读数据时序图
4. 仿真总结
从以上的仿真结果可以看出,Microchip提供的芯片Verilog模型可以配合controller测试一些SPI的基本操作,且仿真图与对应的时序图一致,包括芯片编程等待的时间5 ms也很好的模拟了出来。总的来说,使用芯片Verilog模型仿真是一种很好的验证手段,可以在没有真实EEPROM器件的时候,验证controller 的正确性。
仿真工程下载:
[content_control]
[/content_control]