Verilog实现二线制I2C CMOS串行EEPROM的读写操作
- 1,二线制I2C CMOS串行EEPROM的简单介绍
- 2,I2C总线特征介绍
- 3,二线制I2C、CMOS串行EEPROM的读写操作
- 4,EEPROM的Verilog HDL程序
- 4.1,EEPROM的行为模型思路如下:eeprom.v RTL设计代码
- 4.2,EEPROM_WR的行为模型,RTL设计代码:eeprom_wr.v
- 4.3,下面的程序就是把这个信号源的Verilog HDL模型和顶层模块。RTL代码设计模型是:signal.v
- 4.4,顶层模块的RTL代码设计模型:tb_top.v
- 5,SIM输出波
- 6,总结
1,二线制I2C CMOS串行EEPROM的简单介绍
概述:
在设计复杂的数字电路过程中,可通过仿真调试来改进源代码,使其完全符合设计要求。
介绍一个经过实际运行、验证并可综合到各种FPGA和ASIC工艺的串行EEPROM读写器件的设计过程。该器件能把并行数据和地址信号转变为串行EEPROM能识别的串行码,并把数据写入相应的地址,或根据并行的地址信号从EEPROM相应的地址读取数据并把相应的串行码转换成并行的数据放到并行地址总线上。
二进制I2C CMOS 串行EEPROM AT24C02/4/8/16是一种采用CMOS工艺制成的串行可用电擦除可编程随机读写存储器。串行EEPROM一般具有两种写入方式:
1, 字节写入方式;
2,页写入方式,其允许在一个写周期内同时对一个字节到一页的若干字节进行编程写入。 一页的大小取决于芯片内页寄存器的大小,不同公司的同一种型号存储器内的页寄存器可能是不一样的。
本项目只编写串行EEPROM的一个字节都写入和读出方式的Verilog HDL行为模型代码,串行EEPROM读写器的Verilog HDL模型也只是字节读写方式的可综合模型,对于页写入和读出方式,建议读者参考有关书籍,通过自己改写串行EEPROM的行为模型和串行EEPROM读写器的可综合模型,进而真正掌握本项目的内容。
2,I2C总线特征介绍
I2C(inter integrated circuit)双向二进制串行总线协议定义是:
只有在总线处于“非忙”状态时,数据传输才能开始。在数据传输期间,只要时钟线是高电平,数据线都必须保持稳定,否则数据线上的任何变化都被当作“启动”或“停止”信号。图16.1是被定义的总线状态。
以下介绍A、B、C、D段的工作状态。
(1)总线非忙状态(A段):
该段内的数据线(SDA)和时钟线(SCL)都保持高电平。
(2)启动数据传输(B段):
当时钟线(SCL)是高电平状态时,数据线(SDA)由高电平变为低电平的下降沿被认为是“启动”信号。只有出现“启动”信号后,其他的命令才有效。
(3)停止数据传输(C段):
当时钟线(SCL)是高电平状态时,数据线(SDA)由低电平变为高电平的上升沿被认为是“停止”信号。随着“停止”信号的出现,所有的外部操作都结束。
(4)数据有效(D段):
在出现“启动”信号以后,在时钟线(SCL)是高电平状态时,数据线是稳定的,这时数据线的状态就是要传送的数据。数据线(SDA)上数据的改变必须在时钟线是低电平期间完成,每位数据占用一个时钟脉冲。每个数据传输都是由“启动”信号开始,结束于“停止”信号。
(5)应答信号:
每个正在接收数据的EEPROM在接到一个字节的数据后,通常需要发出一个应答信号。而每个正在发送数据的EEPROM在发出一个字节的数据后,通常需要接收一个应答信号。EEPROM读写控制器必须产生一个与这个应答位相联系的额外的时钟脉冲。在EEPROM的读操作中,EEPROM读写控制器对EEPROM完成的最后一个字节不产生应答位,但是应该给EEPROM一个结束信号。
3,二线制I2C、CMOS串行EEPROM的读写操作
1,EEPROM的写操作(字节编程方式):所谓EEPROM的写操作(字节编程方式)就是通过读写控制器把一个字节数据发送到EEPROM中指定地址的存储单元。其过程如下:
(1)EEPROM读写控制器发出“启动”信号后,紧跟着送4位I2C总线器件特征编码1010和3位EEPROM芯片地址/页地址XXX,以及写状态的R/W位(=0)到总线上。
(2)这一字节表示在接收到被寻址的EEPROM产生的一个应答位后,读写控制器将跟着发送1个字节的EEPROM存储单元地址和要写入的1个字节数据。
(3)EEPROM在接收到存储单元地址后,又一次产生应答位,使读写控制器才发送数据字节,并把数据写入被寻址的存储单元。
(4)EEPROM再一次发出应答信号,读写控制器收到此应答信号后,便产生“停止”信号。AT24C02/4/8/16字节写入帧格式如图2所示。
2,二线制I2C、CMOS串行EEPROM的读操作,所谓EEPROM的读操作是通过读写控制器读取EEPROM中指定地址的存储单元中的一个字节数据。串行EEPROM的读操作分两步进行:
(1)读写器首先发送一个“启动”信号和控制字节(包括页面地址和写控制位)到 EEPROM;
(2)再通过写操作设置EEPROM存储单元地址(注意:虽然这是读操作,但需要先写入地址指针的值),在此期间EEPROM会产生必要的应答位。
接着读写器重新发送另一个“启动”信号和控制字节(包括页面地址和读控制位R/W=1),EEPROM收到后发出应答信号,然后,要寻址存储单元的数据就从SDA线上输出。读操作有3种:
(1)读当前地址存储单元的数据;
(2)读指定地址存储单元的数据;
(3)读连续存储单元的数据。
读指定地址存储单元数据的帧格式如图3所示:
4,EEPROM的Verilog HDL程序
要设计一个串行EEPROM读写器件,不仅要编写EEPROM读写器件的可综合Verilog HDL的代码,而且要编写相应的测试代码以及EEPROM的行为模型。EEPROM的读写电路及其测试电路如图4所示。
(1)EEPROM的行为模型:为了设计一个电路,首先要设计一个EEPROM的Verilog HDL模型。而设计这样一个模型需要仔细地阅读和分析EEPROM器件的说明书,因为EEPROM不是要设计的对象,而要要验证设计对象所需要的器件。所以,只需设计一个EEPROM的行为模型,而不需要可综合风格的模型,这就大大简化了设计过程。下面的Verilog HDL程序就是这个EEPROM(AT24C02/4/8/16)能完成一个字节数据读写的部分行为模型,请读者查阅AT24C02/4/8/16说明书,并对照Verilog HDL程序理解设计的要点。
这里只对在操作中用到的信号线进行模拟,对于没有用到的信号线就略去了。对EEPROM用于基本总线操作的引脚SCL和SDA说明如下,SCL为串行时钟端,这个信号用于对输入和输出数据的同步,而写入串行EEPROM的数据用其上升沿同步,输出数据用其下降沿同步,SDA是串行数据(/地址)输入/输出总线端。
4.1,EEPROM的行为模型思路如下:eeprom.v RTL设计代码
//
/* eeprom.v
用于模拟真实的EEPROM(AT24C02/4/8/16)的随机读写功能。对于符合AT24C02/4/8/16
要求的 scl 和 sda 随机读/写信号能根据I2C协议,分析其含义并进行相应的 读/写 操作
本模块是行为模块,不可综合为 门级网表。
*/
`define timeslice 100
module eeprom(scl, sda);
input scl; // 串行时钟线
inout sda; // 串行数据线
reg out_flag; // SDA 数据输出的控制信号
reg [7:0] memory [2047:0];
reg [10:0] address;
reg [7:0] memory_buf;
reg [7:0] sda_buf; // SDA数据输出寄存器
reg [7:0] shift; // SDA数据输入寄存器
reg [7:0] addr_byte; // EEPROM 存储单元地址寄存器
reg [7:0] ctrl_byte; // 控制字寄存器
reg [1:0] state; // 状态寄存器
integer i;
//
parameter r7=8'b1010_1111, w7=8'b1010_1110, // main7
r6=8'b1010_1101, w6=8'b1010_1100, // main6
r5=8'b1010_1011, w5=8'b1010_1010, // main5
r4=8'b1010_1001, w4=8'b1010_1000, // main4
r3=8'b1010_0111, w3=8'b1010_0110, // main3
r2=8'b1010_0101, w2=8'b1010_0100, // main2
r1=8'b1010_0011, w1=8'b1010_0010, // main1
r0=8'b1010_0001, w0=8'b1010_0000; // main0
//
assign sda = (out_flag == 1) ? sda_buf[7] : 1'bz;
// 寄存器和存储器初始化
initial
begin
addr_byte = 0;
ctrl_byte = 0;
out_flag = 0;
sda_buf = 0;
state = 2'b00;
memory_buf= 0;
address = 0;
shift = 0;
for(i=0; i<=2047; i=i+1)
memory[i] = 0;
end
// 启动信号
always@(negedge sda)
if(scl == 1)
begin
state = state + 1; // 注意:Modelsim6.1以上版本,认为从高阻态到1是负跳变沿
if(state == 2'b11)
disable write_to_eeprm; // 禁用名:write_to_eeprm 的过程或任务
end
// 主状态机
always@(posedge sda)
if(scl == 1) // 停止操作
stop_W_R;
else
begin
casex(state)
2'b01: begin
// 注意:老版书上为 2'b01,因为Modelsim 6.0 以下版本 不认为从高阻态到1是跳变沿,
// 而Modelsim 6.1以上版本,在RTL仿真时,认为从高阻态到1是负跳变沿,所以写
// EEPROM操作从 2'b10 状态开始。
// 而做布线后仿真时,Modelsim 6.1以上版本,并不认为高阻态到1是 负跳变沿,所以
// 应该将进入转态2'b10,改为与老版书一致,即2'b01.
// 不同的仿真工具在处理高阻和不定态时有所不同,必须引起设计者的注意
read_in;
if(ctrl_byte == w7 || ctrl_byte == w6 || ctrl_byte == w5
|| ctrl_byte == w4 || ctrl_byte == w3 || ctrl_byte == w2
|| ctrl_byte == w1 || ctrl_byte == w0)
begin
state = 2'b10;
write_to_eeprm; // 写操作
end
else
state = 2'b00;
end
2'b11:
read_from_eeprm; // 读操作
default: state = 2'b00;
endcase
end
// 主状态机结束
// 操作停止
task stop_W_R;
begin
state = 2'b00; // 状态返回为初始状态
addr_byte = 0;
ctrl_byte = 0;
out_flag = 0;
sda_buf = 0;
end
endtask
// 读进控制字和存储单元地址
task read_in;
begin
shift_in(ctrl_byte);
shift_in(addr_byte);
end
endtask
// EEPROM的写操作
task write_to_eeprm;
begin
shift_in(memory_buf);
address = {ctrl_byte[3:1], addr_byte};
memory[address] = memory_buf;
// $dispaly("eeprm---memory[%0h] = %0h", address, memory[address]);
state = 2'b00; // 回到 0 状态
end
endtask
// EEPROM的读操作
task read_from_eeprm;
begin
shift_in(ctrl_byte);
if(ctrl_byte == r7 || ctrl_byte == r6 || ctrl_byte == r5 || ctrl_byte == r4
|| ctrl_byte == r3 || ctrl_byte == r2 || ctrl_byte == r1 || ctrl_byte == r0)
begin
address = {ctrl_byte[3:1],addr_byte};
sda_buf = memory[address];
shift_out;
state = 2'b00;
end
end
endtask
// SDA数据线上的数据存入寄存器,数据在 SCL 的高电平有效
task shift_in;
output [7:0] shift;
begin
@(posedge scl) shift[7] = sda;
@(posedge scl) shift[6] = sda;
@(posedge scl) shift[5] = sda;
@(posedge scl) shift[4] = sda;
@(posedge scl) shift[3] = sda;
@(posedge scl) shift[2] = sda;
@(posedge scl) shift[1] = sda;
@(posedge scl) shift[0] = sda;
@(negedge scl) begin
#`timeslice;
out_flag = 1; // 应答信号输出
sda_buf = 0;
end
@(negedge scl)
#`timeslice out_flag = 0;
end
endtask
// EEPROM存储器中的数据通过 SDA 数据线输出,数据在 SCL 低电平时变化
task shift_out;
begin
out_flag = 1;
for(i=6; i>=0; i=i-1)
begin
@(negedge scl);
#`timeslice;
sda_buf = sda_buf << 1;
end
@(negedge scl) #`timeslice sda_buf[7] = 1; // 非应答信号输出
@(negedge scl) #`timeslice out_flag = 0;
end
endtask
endmodule
(2)EEPROM读写器的可综合的Verilog HDL模型,下面的Verilog程序是一个可综合的EEPROM读写器模型,它接收来自信号源模型产生的读信号、写信号、并行地址信号和并行数据信号,并把它们转换为相应的串行信号发送到串行EEPROM(AT24C02/4/8/16)的行为模型中去。它海发送应答信号(ACK)到信号源模型,以便让信号源来调节发送或接收数据的速度,以配合EEPROM读写器模型从EEPROM行为模型发送(写)和接收(读)数据。因为它是我们的设计对象,所以它的仿真不但要正确无误,还需要能综合成门级网表。
这个程序基本上由两部分组成,开关组合电路和控制时序电路,如下图5所示。开关电路在控制时序电路的控制下,按照设计的要求有节奏地打开或闭合,这样SDA可以按I2C数据总线的格式输出或输入,使SDA和SCL一起完成EEPROM的读写操作。
电路最终用同步有限状态机(FSM)的设计方法实现。程序实则上是一个嵌套的状态机,由主状态机和从状态机通过由控制线启动的总线在不同的输入信号情况下构成不同功能的较复杂的有限状态机,这个有限状态机只有唯一的驱动时钟CLK。根据串行EEPROM的读写操作时序可知,用5个状态时钟可以完成写操作,用7个状态时钟可以完成读操作。由于读写操作的状态中有几个状态是一致的,用一个嵌套的状态机即可。状态转移如下图6所示。程序由一个读写大任务和若干个较小的任务所组成,其状态机采用独热编码,若需改变状态编码,只需改变程序中的parameter定义即可。
读者可通过模仿这一程序来编写较复杂的可综合Verilog HDL模块程序。这个设计已通过后仿真,并可在FPGA上实现布局布线。
4.2,EEPROM_WR的行为模型,RTL设计代码:eeprom_wr.v
// 模块名称:EEPROM_WR
// 模块功能:EEPROM读写器根据 MCU 的并行数据、地址线 和 读/写控制线对 EEPROM(AT24C02/4/8/16)
// 的行为模块进行随机读写操作,而且本模块为教学要求做了许多简化,
// 但本模块只能做随机的读写操作,功能不完整。
// 本模块为可综合模块,可综合为 门级网表
module eeprom_wr(
SDA, SCL, ACK, RESET, CLK, WR, RD, ADDR, DATA
);
output SCL; // 串行时钟线
output ACK; // 读写一个周期的应答信号
input RESET; // 复位信号
input CLK; // 时钟信号输入
input WR, RD; // 读写信号
input [10:0] ADDR; // 地址线
inout SDA; // 串行数据线
inout [7:0] DATA; // 并行数据线
reg ACK;
reg SCL;
reg WF, RF; // 读写操作标志
reg FF; // 标志寄存器
reg [1:0] head_buf; // 启动信号寄存器
reg [1:0] stop_buf; // 停止信号寄存器
reg [7:0] sh8out_buf; // EEPROM写寄存器
reg [8:0] sh8out_state; // EEPROM写状态寄存器
reg [9:0] sh8in_state; // EEPROM读状态寄存器
reg [2:0] head_state; // 启动状态寄存器
reg [2:0] stop_state; // 停止状态寄存器
reg [10:0] main_state; // 主状态寄存器
reg [7:0] data_from_rm; // EEPROM读寄存器
reg link_sda; // SDA数据输入EEPROM开关
reg link_read; // EEPROM读操作开关
reg link_head; // 启动信号开关
reg link_write; // EEPROM写操作开关
reg link_stop; // 停止信号开关
wire sda1, sda2, sda3, sda4;
// 串行数据在开关控制下有秩序的输出或输入
assign sda1 = (link_head) ? head_buf[1] : 1'b0;
assign sda2 = (link_write) ? sh8out_buf[7] : 1'b0;
assign sda3 = (link_stop) ? stop_buf[1] : 1'b0;
assign sda4 = (sda1 | sda2 | sda3);
assign SDA = (link_sda) ? sda4 : 1'bz;
assign DATA = (link_read) ? data_from_rm : 8'hzz;
// 主状态机状态定义
parameter
Idle = 11'b000_0000_0001,
Ready = 11'b000_0000_0010,
Write_start = 11'b000_0000_0100,
Ctrl_write = 11'b000_0000_1000,
Addr_write = 11'b000_0001_0000,
Data_write = 11'b000_0010_0000,
Read_start = 11'b000_0100_0000,
Ctrl_read = 11'b000_1000_0000,
Data_read = 11'b001_0000_0000,
Stop = 11'b010_0000_0000,
Ackn = 11'b100_0000_0000;
// 并行数据串行输出状态
parameter
sh8out_bit7 = 9'b0_0000_0001,
sh8out_bit6 = 9'b0_0000_0010,
sh8out_bit5 = 9'b0_0000_0100,
sh8out_bit4 = 9'b0_0000_1000,
sh8out_bit3 = 9'b0_0001_0000,
sh8out_bit2 = 9'b0_0010_0000,
sh8out_bit1 = 9'b0_0100_0000,
sh8out_bit0 = 9'b0_1000_0000,
sh8out_end = 9'b1_0000_0000;
// 串行数据并行输出状态
parameter
sh8in_begin = 10'b00_0000_0001,
sh8in_bit7 = 10'b00_0000_0010,
sh8in_bit6 = 10'b00_0000_0100,
sh8in_bit5 = 10'b00_0000_1000,
sh8in_bit4 = 10'b00_0001_0000,
sh8in_bit3 = 10'b00_0010_0000,
sh8in_bit2 = 10'b00_0100_0000,
sh8in_bit1 = 10'b00_1000_0000,
sh8in_bit0 = 10'b01_0000_0000,
sh8in_end = 10'b10_0000_0000;
// 启动状态
parameter
head_begin = 3'b001,
head_bit = 3'b010,
head_end = 3'b100,
// 停止状态
stop_begin = 3'b001,
stop_bit = 3'b010,
stop_end = 3'b100;
parameter
YES = 1,
NO = 0;
// 产生串行时钟 SCL,为输入时钟的2分频
always@(negedge CLK)
if(RESET)
SCL <= 0;
else
SCL <= ~SCL;
// 主状态机程序
always@(posedge CLK)
if(RESET) begin
link_read <= NO;
link_write <= NO;
link_head <= NO;
link_stop <= NO;
link_sda <= NO;
ACK <= 0;
RF <= 0;
WF <= 0;
FF <= 0;
main_state <= Idle;
end
else begin
casex(main_state)
Idle: begin
link_read <= NO;
link_write <= NO;
link_head <= NO;
link_stop <= NO;
link_sda <= NO;
if(WR) begin
WF <= 1;
main_state <= Ready;
end
else if(RD) begin
RF <= 1;
main_state <= Ready;
end
else begin
WF <= 0;
RF <= 0;
main_state <= Idle;
end
end
Ready: begin
link_read <= NO;
link_write <= NO;
link_stop <= NO;
link_head <= YES;
link_sda <= YES;
head_buf[1:0] <= 2'b10;
stop_buf[1:0] <= 2'b01;
head_state <= head_begin;
FF <= 0;
ACK <= 0;
main_state <= Write_start;
end
Write_start:
if(FF == 0)
shift_head;
else begin
sh8out_buf[7:0] <= {1'b1, 1'b0, 1'b1, 1'b0, ADDR[10:8], 1'b0};
link_head <= NO;
link_write <= YES;
FF <= 0;
sh8out_state <= sh8out_bit6;
main_state <= Ctrl_write;
end
Ctrl_write:
if(FF == 0)
shift8_out;
else begin
sh8out_state <= sh8out_bit7;
sh8out_buf[7:0] <= ADDR[7:0];
FF <= 0;
main_state <= Addr_write;
end
Addr_write:
if(FF == 0)
shift8_out;
else begin
FF <= 0;
if(WF) begin
sh8out_state <= sh8out_bit7;
sh8out_buf[7:0] <= DATA;
main_state <= Data_write;
end
else if(RF) begin
head_buf <= 2'b10;
head_state <= head_begin;
main_state <= Read_start;
end
end
Data_write:
if(FF == 0)
shift8_out;
else begin
stop_state <= stop_begin;
main_state <= Stop;
link_write <= NO;
FF <= 0;
end
Read_start:
if(FF == 0)
shift_head;
else begin
sh8out_buf <= {1'b1, 1'b0, 1'b1, 1'b0, ADDR[10:8], 1'b1};
link_head <= NO;
link_sda <= YES;
link_write <= YES;
FF <= 0;
sh8out_state <= sh8out_bit6;
main_state <= Ctrl_read;
end
Ctrl_read:
if(FF == 0)
shift8_out;
else begin
link_sda <= NO;
link_write <= NO;
FF <= 0;
sh8in_state <= sh8in_begin;
main_state <= Data_read;
end
Data_read:
if(FF == 0)
shift8in;
else begin
link_stop <= YES;
link_sda <= YES;
stop_state <= stop_bit;
FF <= 0;
main_state <= Stop;
end
Stop:
if(FF == 0)
shift_stop;
else begin
ACK <= 1;
FF <= 0;
main_state <= Ackn;
end
Ackn:
begin
ACK <= 0;
WF <= 0;
RF <= 0;
main_state <= Idle;
end
default:
main_state <= Idle;
endcase
end
// 串行数据转换为并行数据任务
task shift8in; // in
begin
casex(sh8in_state)
sh8in_begin:
sh8in_state <= sh8in_bit7;
sh8in_bit7: if(SCL) begin
data_from_rm[7] <= SDA;
sh8in_state <= sh8in_bit6;
end
else
sh8in_state <= sh8in_bit7;
sh8in_bit6: if(SCL) begin
data_from_rm[6] <= SDA;
sh8in_state <= sh8in_bit5;
end
else
sh8in_state <= sh8in_bit6;
sh8in_bit5: if(SCL) begin
data_from_rm[5] <= SDA;
sh8in_state <= sh8in_bit4;
end
else
sh8in_state <= sh8in_bit5;
sh8in_bit4: if(SCL) begin
data_from_rm[4] <= SDA;
sh8in_state <= sh8in_bit3;
end
else
sh8in_state <= sh8in_bit4;
sh8in_bit3: if(SCL) begin
data_from_rm[3] <= SDA;
sh8in_state <= sh8in_bit2;
end
else
sh8in_state <= sh8in_bit3;
sh8in_bit2: if(SCL) begin
data_from_rm[2] <= SDA;
sh8in_state <= sh8in_bit1;
end
else
sh8in_state <= sh8in_bit2;
sh8in_bit1: if(SCL) begin
data_from_rm[1] <= SDA;
sh8in_state <= sh8in_bit0;
end
else
sh8in_state <= sh8in_bit1;
sh8in_bit0: if(SCL) begin
data_from_rm[0] <= SDA;
sh8in_state <= sh8in_end;
end
else
sh8in_state <= sh8in_bit0;
sh8in_end: if(SCL) begin
link_read <= YES;
FF <= 1;
sh8in_state <= sh8in_bit7;
end
else
sh8in_state <= sh8in_end;
default: begin
link_read <= NO;
sh8in_state <= sh8in_bit7;
end
endcase
end
endtask
// 并行数据转换为串行数据任务
task shift8_out;
begin
casex(sh8out_state)
sh8out_bit7: // 7
if(!SCL)
begin
link_sda <= YES;
link_write <= YES;
sh8out_state <= sh8out_bit6;
end
else
sh8out_state <= sh8out_bit7;
sh8out_bit6: // 6
if(!SCL) begin
link_sda <= YES;
link_write <= YES;
sh8out_state <= sh8out_bit5;
sh8out_buf <= sh8out_buf << 1;
end
else
sh8out_state <= sh8out_bit6;
sh8out_bit5: // 5
if(!SCL) begin
sh8out_state <= sh8out_bit4;
sh8out_buf <= sh8out_buf << 1;
end
else
sh8out_state <= sh8out_bit5;
sh8out_bit4: // 4
if(!SCL) begin
sh8out_state <= sh8out_bit3;
sh8out_buf <= sh8out_buf << 1;
end
else
sh8out_state <= sh8out_bit4;
sh8out_bit3: // 3
if(!SCL) begin
sh8out_state <= sh8out_bit2;
sh8out_buf <= sh8out_buf << 1;
end
else
sh8out_state <= sh8out_bit3;
sh8out_bit2: // 2
if(!SCL) begin
sh8out_state <= sh8out_bit1;
sh8out_buf <= sh8out_buf << 1;
end
else
sh8out_state <= sh8out_bit2;
sh8out_bit1: // 1
if(!SCL) begin
sh8out_state <= sh8out_bit0;
sh8out_buf <= sh8out_buf << 1;
end
else
sh8out_state <= sh8out_bit1;
sh8out_bit0: // 0
if(!SCL) begin
sh8out_state <= sh8out_end;
sh8out_buf <= sh8out_buf << 1;
end
else
sh8out_state <= sh8out_bit0;
sh8out_end: // end
if(!SCL) begin
link_sda <= NO;
link_write <= NO;
FF <= 1;
end
else
sh8out_state <= sh8out_end;
endcase
end
endtask
// 输出启动任务
task shift_head;
begin
casex(head_state)
head_begin:
if(!SCL) begin
link_write <= NO;
link_sda <= YES;
link_head <= YES;
head_state <= head_bit;
end
else
head_state <= head_begin;
head_bit:
if(SCL) begin
FF <= 1;
head_buf <= head_buf << 1;
head_state <= head_end;
end
else
head_state <= head_bit;
head_end:
if(!SCL) begin
link_head <= NO;
link_write <= YES;
end
else
head_state <= head_end;
endcase
end
endtask
// 输出停止信号任务
task shift_stop;
begin
casex(stop_state)
stop_begin:
if(!SCL) begin
link_sda <= YES;
link_write <= NO;
link_stop <= YES;
stop_state <= stop_bit;
end
else
stop_state <= stop_begin;
stop_bit:
if(SCL) begin
stop_buf <= stop_buf << 1;
stop_state <= stop_end;
end
else
stop_state <= stop_bit;
stop_end:
if(!SCL) begin
link_head <= NO;
link_stop <= NO;
link_sda <= NO;
FF <= 1;
end
else
stop_state <= stop_end;
endcase
end
endtask
endmodule
说明:
eeprom_wr.v,程序模块最终通过各种EDA软件的综合,并在多种系列的FPGA上实现布局布线,最终布线后仿真验证。
(3)EEPROM的信号源模块和顶层模块:完成串行EEPROM读写器件的设计后,我们还需要做EEPROM读写器件的仿真。仿真可以分为前仿真和后仿真,前仿真是Verilog HDL的功能仿真,后仿真是Verilog HDL代码经过综合、布局布线后的时序仿真。为此,我们还要编写用于EEPROM读写器件的仿真测试的信号源程序。这个信号源能产生相应的读信号、写信号、并行地址信号和并行数据信号,并能接收串行EEPROM读写器件的应答信号(ACK),为此来调节发送或接收数据的速度。在这个程序中,为了保证串行EEPROM读写器件的正确性,可以进行完整的测试。写操作时输入的地址信号和数据信号的数据通过系统命令
readmemh
从addr.dat和data.dat文件中取得,而在addr.dat和data.dat文件中可以存放任意数据。读操作时从EEPROM读出的数据存入文件eeprom.dat。对比3个文件的数据就可以验证程序的正确性。readmemh
和fopen
等系统命令读者可以参考Verilog HDL的语法部分。最后我们把信号源、EEPROM和EEPROM读写器用顶层模块连接在一起。
4.3,下面的程序就是把这个信号源的Verilog HDL模型和顶层模块。RTL代码设计模型是:signal.v
注意:
系统命令:
//
/* 模块功能:
用于产生测试信号,对所设计的 EEPROM_WR 模块进行测试。signal模块能对被测试模块产生的 ack 信号
产生响应,发出模仿 MCU 的数据、地址信号和 读/写信号;被测试的模块在接收到信号后会发出
写/读 EEPROM虚拟模块的信号。
模块说明:
本模块是行为模块,不可综合为门级网表。而且本模块为教学目的做了许多简化,功能不完整。
*/
// 信号源的Verilog HDL模型: signal.v
`define timeslice 200
module signal(RESET, CLK, RD, WR, ADDR, ACK, DATA);
output RESET; // 复位信号
output CLK; // 时钟信号
output RD, WR; // 读写信号
output [10:0] ADDR; // 11 位地址信号
input ACK; // 读写周期的应答信号
inout [7:0] DATA; // 数据线
reg RESET;
reg CLK;
reg RD, WR;
reg W_R; // 低位,写操作; 高位,读操作
reg [10:0] ADDR;
reg [7:0] data_to_eeprom;
reg [10:0] addr_mem [0:255];
reg [7:0] data_mem [0:255];
reg [7:0] ROM [1:2047];
integer i, j;
integer OUTFILE;
parameter test_number = 50;
assign DATA = (W_R) ? 8'bz : data_to_eeprom;
// 时钟信号输入
always #(`timeslice/2) CLK = ~CLK;
// 读写信号输入
initial begin
RESET = 1;
i = 0;
j = 0;
W_R = 0;
CLK = 0;
RD = 0;
WR = 0;
#1000;
RESET = 0;
repeat(test_number) // 连续写 test_number 次数据,测试成功后可以增加到全部地址并覆盖测试
begin
#(5 * `timeslice);
WR = 1;
#(`timeslice);
WR = 0;
@(posedge ACK); // EEPROM_WR 转换模块请求写数据
end
#(10 * `timeslice);
W_R = 1; // 开始读操作
repeat(test_number) // 开始读 test_number 次数据
begin
#(5 * `timeslice);
RD = 1;
#(`timeslice)
RD = 0;
@(posedge ACK); // EEPROM_WR 转换模块请求读数据
end
end
// 写操作
initial begin
$display("writing-----writing-----writing-----writing");
#(2 * `timeslice);
for(i = 0; i <= test_number; i = i+1) begin
ADDR = addr_mem[i]; // 输出写操作的地址
data_to_eeprom = data_mem[i]; // 输出需要转换的平行数据
$fdisplay(OUTFILE, "@%0h %0h", ADDR, data_to_eeprom);
// 把输出的地址和数据记录在已经打开的 eeprom.dat 文件中
@(posedge ACK); // EEPROM_WR 转换模块请求写数据
end
end
// 读操作
initial
@(posedge W_R)
begin
ADDR = addr_mem[0];
$fclose(OUTFILE); // 关闭已经打开的 eeprom.dat 文件
$readmemh("./eeprom.dat", ROM); // 把数据文件的数据读到 ROM 中去
$display("Begin READING --- READING --- READING --- READING");
for(j = 0; j <= test_number; j = j+1) begin
ADDR = addr_mem[j];
@(posedge ACK)
if(DATA == ROM[ADDR]) // 比较并显示发送的数据和接收到的数据是否一致
$display("DATA %0h == ROM[%0h] --- READ RIGHT", DATA, ADDR);
else
$display("DATA %0h != ROM[%0h] --- READ WRONG", DATA, ADDR);
end
end
initial begin
OUTFILE = $fopen("./eeprom.dat"); // 打开一个名是 eeprom.dat 的文件备用
$readmemh("E:/vivado_xia_verilog/16_eeprom/addr.dat", addr_mem); // 把地址数据存入地址存储器
$readmemh("E:/vivado_xia_verilog/16_eeprom/data.dat", data_mem); // 把准备写入 EEPROM 的数据存入数据存储器
end
endmodule
4.4,顶层模块的RTL代码设计模型:tb_top.v
/*
顶层模块: top.v
模块功能:
用于把产生测试信号的模块(signal)与设计的具体模块(EEPROM_WR)以及EEPROM虚拟模块连接
起来的模块,用于全面测试。
模块说明:
本模块是行为模块,不可综合为门级网表。
但其中 EEPROM_WR 可以综合为 门级网表,所以可以对所设计的 EEPROM 读写器
进行门级仿真。
*/
`define timeslice 200
module tb_top;
wire RESET;
wire CLK;
wire RD, WR;
wire ACK;
wire [10:0] ADDR;
wire [7:0] DATA;
wire SCL;
wire SDA;
parameter test_numbers = 7'd123;
// initial #(`timeslice * 180 * test_numbers) $stop;
// 例化
signal #(
test_numbers) u1_signal(
.RESET (RESET ),
.CLK (CLK ),
.RD (RD ),
.WR (WR ),
.ADDR (ADDR ),
.ACK (ACK ),
.DATA (DATA )
);
eeprom_wr u1_eeprom_wr(
.RESET (RESET ),
.SDA (SDA ),
.SCL (SCL ),
.ACK (ACK ),
.CLK (CLK ),
.WR (WR ),
.RD (RD ),
.ADDR (ADDR ),
.DATA (DATA )
);
eeprom u1_eeprom(
.sda (SDA ),
.scl (SCL )
);
endmodule
5,SIM输出波
通过前后仿真可以验证程序的正确性。这里给出的是EEPROM读写时序的前仿真波形,如图7和图8所示。后仿真波形除SCL和SDA与CLK有些延迟外,信号的逻辑关系与前仿真一致。
图7. EEPROM的写程序
图8. EEPROM的读时序
图9. EEPROM.dat的数据位置
说明:以上代码已通过RTL仿真、综合、布局布线、和布线后仿真验证。
6,总结
用Verilog设计可实现的数字逻辑电路时,必须对它的电路结构的总体有一个明确的想法。
例如本项目I2C CMOS EEPROM模块的设计,必须要对如何控制sda串行总线有深入细致的认识。Sda总线既用于输出,又用于输入,它必须通过开关网络(组合逻辑电路)与寄存器发生联系。Sda有时与某个寄存器的输出连接,有时又与另一个寄存器的输入连接。而这些连接过程与MCU发过来的命令和数据有关。发出的或接收的信号流又与串行通信协议有关,这些关系都体现在一个或几个状态机中。状态机是这些开关逻辑正确协调操作的指挥者。为了设计好这些状态机必须要搞清楚信号数据的流动和协议。对接口时序的要求必须明确在具体电路模块的设计中体现,也必须在与其连接通信的虚拟模块中明确地用表示行为的Verilog语句体现。只有这样,才能在仿真中帮助我们及时发现复杂状态机编写的漏洞,从而设计出正确无误的电路系统。