案例采用明德扬设计思想完成。IIC协议是非常常用的接口协议,在电子类岗位招聘要求中经常出现它的身影。关于IIC协议这里只做简要介绍,详细信息请自行百度或查阅相关Datasheet,网上资料非常多。该篇博文主要讲如何使用verilog来描述IIC协议,以读写EEPROM为例带领大家了解下明德扬四段式状态机规范和优势,另外还有一些自己在设计过程中总结的经验技巧。
IIC协议时序格式以Datasheet中时序图的形式供大家参考。IIC协议有一条时钟线SCL和一条双线数据总线SDA。SDA在SCL高电平时保持稳定,否则视为开始或结束条件。
发送端发送1byte数据后,接收端在下一个SCL高电平期间拉低总线表示应答,即接收数据成功。
以下分别是器件地址为1字节的EEPROM的单字节写和读操作,需要注意的是DEVICE ADDRESS段中前四位固定是4'b1010,后三位根据EEPROM地址信号电平决定(本次实验地址信号引脚均接地,因此后三位为000),最后一位是读写标志位,低电平写。
好了,有了以上五张时序图我们便知道要干什么了,就是实现这些时序嘛!对于这种串行时序,时间有先后且操作差异较大的要用状态机实现。每种类型操作定义在一个状态中,状态内部需要多个操作则配合计数器实现。整体设计思路如下:先构造时钟信号SCL,这里频率定义为200KHz,而系统时钟有频率为200MHz差分晶振提供,显然需要用到分频计数器。由于SCL高电平期间数据要保持稳定,所以我们在分频计数器计数到1/4处拉高SCL,3/4处拉低SCL,这样做的好处是在结束计数时正好处于SCL低电平中间点,此处作为数据变化的时刻再合适不过。
有了时钟信号,下一步就是通过不同的状态实现SDA信号线满足上述时序要求。我们先来划分状态(实际上时序图中都给我们标识好了),很明显综合考虑写和读两种时序,状态应定义为:初始状态、开始、写控制、响应1、写地址、响应2、写数据、响应3、重新开始、读控制、响应4、读数据、不响应、停止。这里写控制和读控制即是DEVICE ADDRESS阶段,唯一的区别在于读写标志位不同。能看出以上状态划分包括写流程分支和读流程分支,可以根据指令用一个标志位加以区分。
定义状态参数并采用独热码进行编码:
IIC协议中每次SCL高电平期间视为一次操作,因此为了让每个状态都有整数个SCL周期(完整分频计数周期),对每个状态进行比特计数,写控制、地址、写数据、读控制、读数据阶段计数周期是8,其他为1。另外为保证代码的“健壮性”,也就是即使发送1byte数据后没有响应也不至于挂死在等待响应阶段,设定在每次等待响应阶段若响应才进入下一操作,否则回到初始状态。由此得到状态转移图(只包括主要流程,转移条件及未响应回到IDLE状态未画出):
至此所有的设计工作都已经完成,接下来就是根据上述分析编写代码。在编写代码之前简要介绍明德扬四段式状态机的设计思想和代码规范:四段式状态机实质上是在三段式状态机基础上单独提出状态转移条件定义的结构。目的是让设计者一个时间段只专注于一件事情,也就是说当设计状态机的时候先把状态转移流程确定,而条件用不同的信号名代替,等状态转移流程确定后再定义转移条件。这样做的另一个好处是作为条件的信号名可以很方便的在后续时序逻辑中使用。其中用于代替条件的信号名要遵循类似如下格式:<state_c>2<state_n>。<>处用状态名代替。
整体代码如下:
`timescale 1ns / 1ps module i2c_interface#(parameter SCL_CYC = )//200KHz
(
input clk,
input rst_n, //用户侧接口
input write_en,//写指令
input read_en, //读指令
input [:]share_addr, //读写复用地址
input [:] wri_data,//代写入数据
input wri_data_vld, output reg busy,//总线忙信号
output reg [:] rd_data,//读回数据
output reg rd_data_vld, //仿真用接口
output reg [:] state_c, //eeprom侧接口
output reg scl, //时钟
input sda_in,
output reg sda_en,
output reg sda_reg ); reg [:] div_cnt;
reg high_middle,low_middle;
reg [:] bit_cnt;
reg [:] N;
//(*keep = "true"*)reg [13:0] state_c;
reg [:] state_n;
reg [:] wri_byte;
reg rd_flag;
reg [:] rd_buf;
reg [:] state_c_tmp;
reg [:] device_addr_wr_shift; wire add_bit_cnt,end_bit_cnt;
wire add_div_cnt,end_div_cnt;
wire idle2start,start2wri_ctrl,wri_ctrl2ack1,ack12addr,addr2ack2,ack22wri_data;
wire wri_data2ack3,ack32stop,ack22re_start,re_start2rd_ctrl,rd_ctrl2ack4;
wire ack42rd_data,rd_data2nack,nack2stop,stop2idle,ack2idle;
reg ack_valid,ack_invalid;
wire [:] cs;
wire wri_vld;
wire [:] device_addr_rd,device_addr_wr;
wire [:] word_addr;
wire ack_state; //状态编码
localparam IDLE = 'b00_0000_0000_0001,//1
START = 'b00_0000_0000_0010,//2
WRI_CTRL = 'b00_0000_0000_0100,//4
ACK1 = 'b00_0000_0000_1000,//8
ADDR = 'b00_0000_0001_0000,//10
ACK2 = 'b00_0000_0010_0000,//20
WRI_DATA = 'b00_0000_0100_0000,//40
ACK3 = 'b00_0000_1000_0000,//80
RE_START = 'b00_0001_0000_0000,//100
RD_CTRL = 'b00_0010_0000_0000,//200
ACK4 = 'b00_0100_0000_0000,//400
RD_DATA = 'b00_1000_0000_0000,//800
NACK = 'b01_0000_0000_0000,//1000
STOP = 'b10_0000_0000_0000;//2000 //分频计数器 在响应操作直到完成或退出到IDLE中间都计数
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
div_cnt <= ;
else if(add_div_cnt)begin
if(end_div_cnt)
div_cnt <= ;
else
div_cnt <= div_cnt + 'b1;
end
else
div_cnt <= ;
end assign add_div_cnt = busy == ;
assign end_div_cnt = add_div_cnt && div_cnt == SCL_CYC - ; //比特计数器
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
bit_cnt <= ;
else if(add_bit_cnt)begin
if(end_bit_cnt)
bit_cnt <= ;
else
bit_cnt <= bit_cnt + 'b1;
end
end assign add_bit_cnt = end_div_cnt;
assign end_bit_cnt = add_bit_cnt && bit_cnt == N - ; always@(*)begin
case(state_c)
WRI_CTRL:N = ;
ADDR:N = ;
WRI_DATA:N = ;
RD_CTRL:N = ;
RD_DATA:N = ;
default:N = ;
endcase
end //---------------------iic时序四段式状态机部分------------------------- //时序逻辑描述状态转移
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
state_c <= IDLE;
else
state_c <= state_n;
end //组合逻辑描述状态转移条件
always@(*)begin
case(state_c)
IDLE:begin //空闲状态
if(idle2start)
state_n = START;
else
state_n = state_c;
end START:begin //产生开始条件 即SCL高电平期间SDA拉低
if(start2wri_ctrl)
state_n = WRI_CTRL;
else
state_n = state_c;
end WRI_CTRL:begin //写器件地址和写标志位
if(wri_ctrl2ack1)
state_n = ACK1;
else
state_n = state_c;
end ACK1:begin //等待响应
if(ack12addr)
state_n = ADDR;
else if(ack2idle)
state_n = IDLE;
else
state_n = state_c;
end ADDR:begin //写存储单元地址
if(addr2ack2)
state_n = ACK2;
else
state_n = state_c;
end ACK2:begin //等待响应2
if(ack22wri_data) //写操作
state_n = WRI_DATA;
else if(ack22re_start)//读操作
state_n = RE_START;
else if(ack2idle)
state_n = IDLE;
else
state_n = state_c;
end WRI_DATA:begin //写数据 8bit
if(wri_data2ack3)
state_n = ACK3;
else
state_n = state_c;
end ACK3:begin //等待响应3
if(ack32stop)
state_n = STOP;
else if(ack2idle)
state_n = IDLE;
else
state_n = state_c;
end RE_START:begin //若为读操作在响应2后再次构造开始条件
if(re_start2rd_ctrl)
state_n = RD_CTRL;
else
state_n = state_c;
end RD_CTRL:begin //写入存储单元地址和读标志位
if(rd_ctrl2ack4)
state_n = ACK4;
else
state_n = state_c;
end ACK4:begin //等待响应4
if(ack42rd_data)
state_n = RD_DATA;
else if(ack2idle)
state_n = IDLE;
else
state_n = state_c;
end RD_DATA:begin //读数据 8bit
if(rd_data2nack)
state_n = NACK;
else
state_n = state_c;
end NACK:begin //不响应 无操作即可
if(nack2stop)
state_n = STOP;
else
state_n = state_c;
end STOP:begin //构造停止条件
if(stop2idle)
state_n = IDLE;
else
state_n = state_c;
end default:
state_n = IDLE;
endcase
end //连续赋值语句定义状态转移条件
assign idle2start = state_c == IDLE && (write_en || read_en);
assign start2wri_ctrl = state_c == START && end_bit_cnt;
assign wri_ctrl2ack1 = state_c == WRI_CTRL && end_bit_cnt;
assign ack12addr = state_c == ACK1 && ack_valid && end_bit_cnt;
assign addr2ack2 = state_c == ADDR && end_bit_cnt;
assign ack22wri_data = state_c == ACK2 && ack_valid && !rd_flag && end_bit_cnt;
assign wri_data2ack3 = state_c == WRI_DATA && end_bit_cnt;
assign ack32stop = state_c == ACK3 && ack_valid && end_bit_cnt;
assign ack22re_start = state_c == ACK2 && ack_valid && rd_flag && end_bit_cnt;
assign re_start2rd_ctrl = state_c == RE_START && end_bit_cnt;
assign rd_ctrl2ack4 = state_c == RD_CTRL && end_bit_cnt;
assign ack42rd_data = state_c == ACK4 && ack_valid && end_bit_cnt;
assign rd_data2nack = state_c == RD_DATA && end_bit_cnt;
assign nack2stop = state_c == NACK && end_bit_cnt;
assign stop2idle = state_c == STOP && end_bit_cnt;
assign ack2idle = ack_state && ack_invalid; always@(posedge clk or negedge rst_n)begin
if(!rst_n)
ack_valid <= ;
else if(ack12addr || ack22wri_data || ack32stop || ack22re_start || ack42rd_data || ack2idle)
ack_valid <= ;
else if(ack_state && high_middle && !sda_en && !sda_in)
ack_valid <= ;
end assign ack_state = state_c == ACK1 || state_c == ACK2 || state_c == ACK3 || state_c == ACK4; always@(posedge clk or negedge rst_n)begin
if(!rst_n)
ack_invalid <= ;
else if(state_c == NACK && high_middle && !sda_en && sda_in)
ack_invalid <= ;
else if(end_bit_cnt)
ack_invalid <= ;
end //时序逻辑描述状态输出 //scl时钟信号
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
scl <= ;
else if(add_div_cnt && div_cnt == SCL_CYC/ - )
scl <= ;
else if(add_div_cnt && div_cnt == SCL_CYC/ + SCL_CYC/ - )
scl <= ;
end //找到scl高低电平中间点
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
high_middle <= ;
else if(add_div_cnt && div_cnt == SCL_CYC/ - )
high_middle <= ;
else
high_middle <= ;
end //三态门输出使能
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
sda_en <= ;
else if(idle2start || ack12addr || ack22wri_data || ack32stop || ack22re_start || nack2stop|| rd_data2nack)
sda_en <= ;
else if(wri_ctrl2ack1 || addr2ack2 || wri_data2ack3 || rd_ctrl2ack4 || ack2idle || stop2idle)
sda_en <= ;
end //数据总线输出寄存器
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
sda_reg <= ;
else if(idle2start)
sda_reg <= ;
else if((state_c == START || state_c == RE_START) && high_middle)
sda_reg <= ;
else if(state_c == WRI_CTRL)
sda_reg <= device_addr_wr[-bit_cnt];
else if(state_c == ADDR)
sda_reg <= word_addr[ - bit_cnt];
else if(state_c == WRI_DATA)
sda_reg <= wri_data[ - bit_cnt];
else if(state_c == STOP && high_middle)
sda_reg <= ;
else if(ack22re_start)
sda_reg <= ;
else if(state_c == RE_START && high_middle)
sda_reg <= ;
else if(state_c == RD_CTRL)
sda_reg <= device_addr_rd[- bit_cnt];
else if(ack_state)
sda_reg <= ;
else if(rd_data2nack)
sda_reg <= ;
else if(nack2stop)
sda_reg <= ;
end assign device_addr_wr = {'b1010,cs,1'b0};
assign cs = 'b000;
assign word_addr = share_addr;
assign device_addr_rd = {'b1010,cs,1'b1}; //读取数据缓存
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
rd_buf <= ;
else if(state_c == RD_DATA && high_middle)
rd_buf <= {rd_buf[:],sda_in};
end //读数据有效指示
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
rd_data_vld <= ;
else if(rd_data2nack)
rd_data_vld <= ;
else
rd_data_vld <= ;
end //读数据输出
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
rd_data <= ;
else
rd_data <= rd_buf;
end //读标志位
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
rd_flag <= ;
else if(read_en)
rd_flag <= ;
else if(rd_flag && (stop2idle || state_c == IDLE))
rd_flag <= ;
end //总线忙信号
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
busy <= ;
else if(write_en || read_en)
busy <= ;
else if(busy == &&(stop2idle || state_c == IDLE))
busy <= ;
end endmodule
i2c_interface.v
可以看出状态机部分依次分为:时序逻辑描述状态转移,组合逻辑描述状态转移条件,连续赋值定义状态转移条件以及时序逻辑描述状态相关输出。并且至始至终使用state_c和state_n两个信号表示现态和次态,使逻辑更加清晰。接口部分为了方便仿真和调试,加入状态信号state_c。这里涉及到一个双向端口sda,用三个信号:输出使能sda_en,输出寄存器sda_reg和输入缓存sda_in表示。在顶层模块中使用这三个信号通过三态门的形式给出,关于三态门的使用细节和仿真方式稍后讲述。
先设计其他模块和顶层模块,之后对顶层模块进行仿真测试,这时观察各个模块中信号数值分析排查问题。有了时序接口模块,在正确无误情况下,已经可以实现对EEPROM的读写操作。现在明确设计目的,我们要实现EEPROM的一字节数据读写,因此可以通过按键发送指令向EEPROM中某地址中写入任意一个数据,之后用另一个按键发送读指令将刚写入地址中数据读出的方式验证读写操作是否正常工作。编写控制模块(控制模块仅实现IIC总线空闲时才响应操作,实际上用按键方式犹豫时间间隔较长,不会出现多个指令抢占总线的情况,这里设计控制模块是为了适应其他场合或功能扩展用途)
`timescale 1ns / 1ps module iic_ctrl(
input clk,
input rst_n,
input local_rd,
input local_wr, input iic_busy,
output reg com_rd,
output reg com_wr
); wire ready; assign ready = !iic_busy; //写命令
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
com_wr <= ;
else if(local_wr && ready)//iic总线空闲时才响应操作
com_wr <= ;
else
com_wr <= ;
end //读命令
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
com_rd <= ;
else if(local_rd && ready)
com_rd <= ;
else
com_rd <= ;
end endmodule
剩下只需加入按键消抖模块,并把按键消抖模块,控制模块还有时序接口模块都例化在顶层文件中即可。按键消抖模块在之前的博文中有讲述,这里使用计数器配合状态标志位的方式实现。需要说明的是多个按键使用一个按键消抖模块的设计方式:只需将信号位宽定义为可变参数。
`timescale 1ns / 1ps module key_filter
#(parameter DATA_W = ,
KEY_W = ,
TIME_20MS = 4_000_000)
(
input clk ,
input rst_n ,
input [KEY_W- :] key_in , //按键 按下为低电平
output reg [KEY_W- :] key_vld
); reg [DATA_W-:] cnt;
reg flag;
reg [KEY_W- :] key_in_ff1;
reg [KEY_W- :] key_in_ff0; wire add_cnt,end_cnt; //延时计数器
always @(posedge clk or negedge rst_n)begin
if(rst_n=='b0)
cnt <= ;
else if(add_cnt)begin
if(end_cnt)
cnt <= ;
else
cnt <= cnt + 'b1;
end
else
cnt <= ;
end
//按下状态才计数,松手清零
assign add_cnt = flag == 'b0 && (key_in_ff1 != 2'b11);
assign end_cnt = add_cnt && cnt == TIME_20MS - ; //计数标志位,0有效 为了只计数一个周期
always @(posedge clk or negedge rst_n)begin
if(rst_n=='b0)begin
flag <= 'b0;
end
else if(end_cnt)begin
flag <= 'b1;
end
else if(key_in_ff1 == 'b11)begin//松手重新清零
flag <= 'b0;
end
end //同步处理
always @(posedge clk or negedge rst_n)begin
if(rst_n=='b0)begin
key_in_ff0 <= ;
key_in_ff1 <= ;
end
else begin
key_in_ff0 <= key_in ;
key_in_ff1 <= key_in_ff0;
end
end //输出有效
always @(posedge clk or negedge rst_n)begin
if(rst_n=='b0)begin
key_vld <= ;
end
else if(end_cnt)begin
key_vld <= ~key_in_ff1;
end
else begin
key_vld <= ;
end
end endmodule
顶层模块例化子模块:
`timescale 1ns / 1ps module eeprom_top( input sys_clk_p,
input sys_clk_n,
input rst_n,
input [:] key,
//仿真接口
output sda_en,
output [:] state_c, //EEPROM接口
output scl,
inout sda
); wire sys_clk_ibufg;
(*keep = "true"*)wire busy;
(*keep = "true"*)wire read,write;
wire [:] rd_data;
wire rd_data_vld;
(*keep = "true"*)wire sda_reg,sda_in;
(*keep = "true"*)wire [:] key_vld;
//(*keep = "true"*)wire sda_en;
//(*keep = "true"*)wire [13:0] state_c;
wire [:] probe0; IBUFGDS #
(
.DIFF_TERM ("FALSE"),
.IBUF_LOW_PWR ("FALSE")
)
u_ibufg_sys_clk
(
.I (sys_clk_p), //差分时钟的正端输入,需要和顶层模块的端口直接连接
.IB (sys_clk_n), // 差分时钟的负端输入,需要和顶层模块的端口直接连接
.O (sys_clk_ibufg) //时钟缓冲输出
); key_filter
#(.DATA_W(),
.KEY_W(),
.TIME_20MS(4_000_000))
key_filter
(
.clk (sys_clk_ibufg) ,
.rst_n(rst_n) ,
.key_in (key), //按键 按下为低电平
.key_vld(key_vld)
); iic_ctrl iic_ctrl(
.clk(sys_clk_ibufg),
.rst_n(rst_n),
.local_wr(key_vld[]),
.local_rd(key_vld[]), .iic_busy(busy),
.com_rd(read),
.com_wr(write)
); iic_interface
#(.SCL_CYC())
iic_interface(
.clk(sys_clk_ibufg),
.rst_n(rst_n), //用户侧接口
.write_en(write), //写指令
.read_en(read), //读指令
.share_addr('h15),//读写复用地址
.wri_data('h32), //待写入数据
.wri_data_vld('b1),
.busy(busy), //总线忙信号
.rd_data(rd_data), //读回数据
.rd_data_vld(rd_data_vld),
//仿真接口
.state_c(state_c),
//eeprom侧接口
.scl(scl), //时钟
.sda_in(sda_in),
.sda_en(sda_en),
.sda_reg(sda_reg)
); //三态门
assign sda = sda_en ? sda_reg : 'bz;
assign sda_in = sda; ila_0 ila_0 (
.clk(sys_clk_ibufg), // input wire clk
.probe0(probe0) // input wire [39:0] probe0
); assign probe0[:] = state_c; //14bit
assign probe0[] = busy;
assign probe0[] = scl;
assign probe0[] = sda_en;
assign probe0[] = sda_reg;
assign probe0[] = sda_in;
assign probe0[] = write;
assign probe0[] = read;
assign probe0[:] = ; endmodule
看一下软件分析出的原理图结构(ILA IP核是之后添加的):
此处详细说明下双向端口使用:顶层模块中建立三态门结构,在输出使能有效时作为输出端口,无效是呈现高阻态,此时作为输入端口,由sda_in信号读取数值。那双向端口如何仿真呢?很简单,在测试文件中也构造一个三态门结构,而输出使能信号为设计中输出使能信号的相反值,这样在设计中该端口呈现高阻态时,正好在测试文件中相应端口作为输出的阶段。可以注意到我在顶层模块中加入了两个仿真接口:state_c和sda_en,方便在测试文件中找到给出响应的位置。测试文件如下:
`timescale 1ns / 1ps module eeprom_top_tb; reg sys_clk_p,sys_clk_n;
reg rst_n;
reg [:] key; wire scl;
wire sda;
wire sda_en;//高电平时待测试文件为输出 reg [:] myrand;
reg sda_tb_out;
wire [:] state_c; eeprom_top eeprom_top(
.sys_clk_p(sys_clk_p),
.sys_clk_n(sys_clk_n),
.rst_n(rst_n),
.key(key),
.sda_en(sda_en),
.state_c(state_c),
.scl(scl),
.sda(sda)
); assign sda = (!sda_en) ? sda_tb_out : 'bz; parameter CYC = ,
RST_TIME = ; defparam eeprom_top.key_filter.TIME_20MS = ; initial begin
sys_clk_p = ;
forever #(CYC/) sys_clk_p = ~sys_clk_p;
end initial begin
sys_clk_n = ;
forever #(CYC/) sys_clk_n = ~sys_clk_n;
end localparam IDLE = 'b00_0000_0000_0001,
START = 'b00_0000_0000_0010,
WRI_CTRL = 'b00_0000_0000_0100,
ACK1 = 'b00_0000_0000_1000,
ADDR = 'b00_0000_0001_0000,
ACK2 = 'b00_0000_0010_0000,
WRI_DATA = 'b00_0000_0100_0000,
ACK3 = 'b00_0000_1000_0000,
RE_START = 'b00_0001_0000_0000,
RD_CTRL = 'b00_0010_0000_0000,
ACK4 = 'b00_0100_0000_0000,
RD_DATA = 'b00_1000_0000_0000,
NACK = 'b01_0000_0000_0000,
STOP = 'b10_0000_0000_0000; initial begin
rst_n = ;
#;
rst_n = ;
#(CYC*RST_TIME);
rst_n = ;
end initial begin
#;
key = 'b11;
#(CYC*RST_TIME);
#(CYC*); press_key_wr;
#120_000;
press_key_rd;
#80_000;
$stop;
end //构造响应条件
always@(*)begin
if(state_c == ACK1 || state_c == ACK2 || state_c == ACK3 || state_c == ACK4)
sda_tb_out <= ;
else
sda_tb_out <= ;
end task press_key_wr;
begin
repeat()begin//模拟抖动过程
myrand = {$random}%;
#myrand key[] = ~key[];
end
key[] = ;
#;
repeat()begin
myrand = {$random}%;
#myrand key[] = ~key[];
end
key[] = ;
#;
end
endtask task press_key_rd;
begin
repeat()begin//模拟抖动过程
myrand = {$random}%;
#myrand key[] = ~key[];
end
key[] = ;
#;
repeat()begin
myrand = {$random}%;
#myrand key[] = ~key[];
end
key[] = ;
#;
end
endtask endmodule
我的开发板使用差分晶振作为系统时钟,在测试文件中也要以差分信号的形式给出时钟。与单端时钟唯一的区别在于给出两个初始值不同周期相同的时钟信号。其中为了找到响应位置,引入状态编码,并在需要给出响应的时刻拉低总线。运行行为仿真:
整体结构:
写操作:
读操作:
读写操作过程中状态转移、比特计数器、sda 、scl这些核心信号数据正常,仿真通过。实际上这是设计过程中遇到些小问题,修改代码后的结果。下一步要在线调试了,这里是本篇博文最后一个重点要说明的内容。以往我会使用添加属性的方式(*mark_debug = "true"*)标志要观察的信号,再在综合后使用debug设置向导引入调试IP核。经过实验发现调试核的引入是通过添加约束的方式实现的,而且当要观察别的信号时该约束部分必须改动否则报错,所以这里使用IP核例化调试探测流程,直接在IP catalog中生成ILA IP核。这里有一个小技巧:生成IP核是只使用一个探针信号,并把位宽设置的较大,且使用OOC方式。在例化IP核后使用这个信号的不同位宽部分连接需要在线观察的信号。这样可以避免在反复综合、布局布线的过程中重新编译ILA IP核部分,节约时间。
打开硬件管理器,下载bit流后自动打开调试界面。设置触发条件观察波形,这里可以很方便的利用状态信号的不同状态设置触发条件。
写操作:
读操作:
写入数据定义为8'h32,读取bit依次是0011_0010,即为32,说明正确将写入数据读出。大家可以在本次实验基础上扩展,比如实现页写模式,或是使用串口来发送读写指令并读回数据等。经过本次博文,掌握了IIC协议的四段式状态机实现方式,双向端口的三态门结构及仿真方法,并能够灵活运用ILA IP核进行在线调试。希望大家和我一样收获很多。欢迎交流~