文章目录
前言
这几天总算是将I2C通信协议的verilog设计写好了,并成功完成了与EEPROM的数据交互。虽说Vivado中有AXI_IIC模块,但博主认为IIC通信协议是用来训练自己逻辑的再好不过的选择了,因此自己手写一个硬件IIC还是有必要的。在设计过程中遇到了一个非常奇怪的问题,就是Vivado对于模块中内嵌例化的三态门无法综合的问题,简单来说就是无法综合inout定义的信号,故开篇来记录下解决的过程。
一、代码下载链接
废话不多说,这里贴出该I2C接口的verilog源码及软件驱动函数库链接:
https://hihii11.github.io/verilog_AXI_IIC.html
同样这也是我在github上的个人博客,欢迎大家访问!
二、I2C通信协议简介
在开始之前首先对I2C通信协议进行简要的介绍,有基础的读者可以直接跳过了。
1.I2C协议简介
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。需要注意的是I2C是一种半双工通信,也就是说在同一时刻无法同时发送或接收数据。
在I2C设备通信中,分为主机和从机,主机决定I2C传输的开始与结束,并产生I2C通信的时钟。从机则以主机产生的时钟步调,发送数据或接收由主机发出的数据。
一次完整的I2C通信大致有以下几个过程。
1. 主机发送I2C起始信号。 从机在接收到起始信号后,准备接收主机发送的地址。
2. 主机发送从机地址和读写标志位。 从机在接收到主机发送的数据后与自身地址相比较,一致则响应此次I2C数据传输,并判断此次操作是读还是写。
3. 从机返回一个应答信号,告知主机已接收到地址数据。
4. 主机将需要发送的数据一位一位Push到数据线上。
5. 从机返回一个应答信号,告知主机已接收到数据。
6. 主机发送I2C结束信号。
一般来说,现在大部分I2C器件都能进行地址递增的连续数据传输,所以在第4步主机发送数据也可一次性发送多个字节。
2.I2C接口
I2C接口比较简单,在通信设备之间只需要两根线就可以实现通信,分别是时钟线SCL和数据线SDA。如图2.0所示
时钟线传输由主机发出的时钟脉冲,是一个有一定频率,固定占空比(通常50%)的方波。在SDA数据线上传输数据。
这里需要注意的是,由于在该协议中,数据交互是双向的,所以SDA端口的类型应当是inout双向口类型。
图2.0 I2C接口
3.I2C通信时序
这里采用某I2C器件的时序图做下简单的讲解。如图2.1所示。
我们可以看到这张图还是很复杂的,但实际上,我们对它做下简单的拆分,就会好理解的多。
图2.1 I2C时序
I2C起始信号与结束信号:
如图2.2所示,起始信号即在SCL为高电平时,SDA有高电平变为低电平。结束信号为在SCL为高电平时,SDA由低电平变为高电平。
图2.2 I2C起始信号与结束信号
I2C数据传输与应答信号:
图2.3展示了I2C数据传输与应答信号。
在I2C数据传输过程中,要求在SCL为高电平时,SDA上的数据不允许发生改变,必须保持稳定,即只有在SCL为低电平时,SDA数据才允许发生改变。
应答信号本质和发送数据一样,但它是在发送完一个字节数据后的额外的一比特数据。
若该比特位为低电平,则发出的是一个响应应答信号,若是高电平,则是非应答信号。
图2.3 I2C数据传输与应答信号
三、I2C硬件接口设计
1.硬件I2C设计方案
虽然看上去I2C协议似乎比较复杂,但综合起来看,其实也就几种SCL与SDA数据的组合,下面我将这些不同的组合成为I2C事件,如表3.0所示。
表3.0 I2C事件划分
I2C事件 | 事件描述 |
---|---|
起始事件 | 标志着一次I2C通信的开始 |
结束事件 | 标志着一次I2C通信的结束 |
发送事件 | 串行发送一个字节数据 |
接收事件 | 串行接收一个字节数据 |
响应事件 | 发送一个应答或非应答信号 |
检测响应事件 | 检测一个应答或非应答信号 |
这样拆分的好处是,可以将任何一次I2C通信,都拆分成若干个I2C事件的组合,在硬件设计上,可以通过配置寄存器来设置本次执行的事件,在加以一个触发信号使得器件执行本次事件,在器件完成后反馈一个结束信号或一个中断信号。
对于每一个事件的实现,可以通过状态机来实现。
图3.1为整体接口的架构。
图3.1 硬件I2C架构
从左往右介绍,首先用户在使用时通过AXI总线对接口中寄存器进行读写,可读写的寄存器如下表:
表3.1 I2C寄存器说明
寄存器名称 | 寄存器功能 |
---|---|
Div0 Register | 分频器的分频系数,控制整个I2C接口的输入时钟 |
Div1 Register | 该分频器控制发送一比特数据时,SCL的脉冲宽度 |
Cmd Register | 指令寄存器,指示I2C当前事件 |
Status Register | 状态寄存器。寄存当前I2C接口状态 |
Rx Buffer | 接收缓冲区,寄存接收数据 |
Tx Buffer | 发送缓冲区,寄存发送数据 |
在配置完Cmd Register后,发出一个触发信号,I2C状态机就会根据Cmd Register中的指令来执行相应的事件,在执行完成后返回一个中断信号,同时状态寄存器中的完成标志位会置位。
2.重要程序设计
I2C模块信号:
module IIC_Master_2(
input clk,//system clk input
input iic_en,//enable signal
input iic_rst,//reset signal
input iic_sda_i,//IIC SDA port
output reg iic_sda_o,
output reg iic_sda_out,
output reg iic_scl,//IIC SCL port
input [7:0] iic_send_data,//IIC send data input
output reg [7:0] iic_rec_data,//IIC_receive data output
input iic_locked,//IIC div lock signal
input [15:0] clk_div,//IIC clk div
input [15:0] step_t,//IIC step time
input [7:0] iic_sel,//IIC event select
input iic_event_start,//to start an iic event
output reg [7:0] iic_IFG,//IIC event finish flag
output reg iic_busy,//IIC busy flag
output reg iic_ack,//when iic_ack = 1 ,an ack signal was checked ,otherwise an no-ack signal was checked
output reg iic_INT,
output reg iic_qvld //IIC event finish signal output
);
这边需要注意的是,sda信号并不是写成inout的形式,而是写成三态缓冲的形式,这是由于在Vivado中若直接用inout语句,则会出现综合错误,这个错误会在接下来介绍。
I2C状态机:
always @ (posedge iic_qvld , posedge iic_event_start)
begin
if(iic_event_start)
begin
iic_IFG <= 8'd0;
end
else
begin
if(iic_en)
begin
case(iic_sel)
8'b0000_0000:begin iic_IFG <= 8'b0000_0000;end
8'b0000_0001:begin iic_IFG <= 8'b0000_0001;end
8'b0000_0010:begin iic_IFG <= 8'b0000_0010;end
8'b0000_0100:begin iic_IFG <= 8'b0000_0100;end
8'b0000_1000:begin iic_IFG <= 8'b0000_1000;end
8'b0001_0000:begin iic_IFG <= 8'b0001_0000;end
8'b0010_0000:begin iic_IFG <= 8'b0010_0000;end
8'b0100_0000:begin iic_IFG <= 8'b0100_0000;end
default:iic_IFG <= 8'd0;
endcase
end
else
begin
iic_IFG <= iic_IFG;
end
end
end
//IIC State Machine
always@(posedge IIC_clk , posedge iic_rst)
begin
if(iic_rst)
begin
iic_state <= IDLE0;
iic_t_cnt <= 16'd0;
iic_rec_data <= 8'd0;
iic_sda_out <= 1'b1;
iic_sda_o <= 1'b1;
iic_scl <= 1'b1;
iic_qvld <= 1'b0;
iic_ack <= 1'b0;
iic_start_IFG<=1'b0;
iic_busy <= 1'b0;
iic_send_buff <= 8'd0;
iic_recv_buff <= 8'd0;
iic_bit_cnt <= 4'd0;
iic_INT <= 1'b0;
end
else
begin
if(iic_en)
begin
case(iic_state)
IDLE0://wait start signal
begin
iic_INT <= 1'b0;
if(iic_event_start)
begin
iic_state <= IDLE1;
iic_qvld <= 1'b0;
iic_busy <= 1'b1;
end
else
begin
iic_bit_cnt <= 4'd0;
iic_busy <= 1'b0;
iic_state <= IDLE0;
iic_t_cnt <= 16'd0;
iic_sda_out <= 1'b0;
iic_sda_o <= 1'b1;
iic_send_buff <= 8'd0;
iic_recv_buff <= 8'd0;
if(iic_start_IFG) iic_scl <= 1'b0;
else iic_scl <= 1'b1;
end
end
IDLE1://wait start signal finish
begin
case(iic_sel)
8'b0000_0000:begin iic_state <= IDLE0;end
8'b0000_0001:begin iic_state <= START;iic_sda_out <= 1'b0;end
8'b0000_0010:begin iic_state <= SENDBYTE;iic_sda_out <= 1'b0;
iic_send_buff <= iic_send_data;iic_scl<=1'b0;end
8'b0000_0100:begin iic_state <= SENDACK;iic_sda_out <= 1'b0;iic_scl<=1'b0;end
8'b0000_1000:begin iic_state <= SENDNACK;iic_sda_out <= 1'b0;iic_scl<=1'b0;end
8'b0001_0000:begin iic_state <= STOP;iic_sda_out <= 1'b0;iic_scl<=1'b0;end
8'b0010_0000:begin iic_state <= RECVBYTE;iic_sda_out <= 1'b1;iic_scl<=1'b0;end//set sda pin as input
8'b0100_0000:begin iic_state <= CHECKACK0;iic_sda_out <= 1'b1;iic_scl<=1'b0;end//set sda pin as input
default:begin iic_state <= IDLE0;iic_sda_out <= 1'b0;iic_scl<=1'b0; end
endcase
end
START://IIC send a start signal
begin
if(iic_t_cnt == 16'd0) begin iic_sda_o <= 1'b1;iic_scl <= 1'b1; end
else if(iic_t_cnt == step_time/2) begin iic_sda_o <= 1'b0;iic_scl <= 1'b1; end
else if(iic_t_cnt == step_time) begin iic_scl <= 1'b0;iic_state <= FINISH;iic_start_IFG<=1'b1; end
else iic_state <= START;
iic_t_cnt <= iic_t_cnt+16'd1;
end
SENDBYTE://IIC send a byte data
begin
if(iic_bit_cnt != 4'd8)
begin
if(iic_t_cnt == 16'd0) begin iic_sda_o <= iic_send_buff[7];iic_scl <= 1'b0; end
else if(iic_t_cnt == step_time/2) begin iic_scl <= 1'b1;end//set scl pin high
else if(iic_t_cnt == step_time) begin iic_scl <= 1'b0;iic_send_buff <= iic_send_buff<<1;end//set scl pin high
if(iic_t_cnt != step_time) begin iic_t_cnt <= iic_t_cnt+16'b1;end
else begin iic_t_cnt <= 16'd0;iic_bit_cnt<= iic_bit_cnt+16'd1; end
end
else
begin
iic_bit_cnt <= 4'd0;iic_state <= FINISH;iic_start_IFG<=1'b1;
end
end
SENDACK://IIC set an ack signal
begin
if(iic_t_cnt == 16'd0) begin iic_sda_o <= 1'b0;iic_scl <= 1'b0; end
else if(iic_t_cnt == step_time/2) begin iic_sda_o <= 1'b0;iic_scl <= 1'b1;end//set scl pin high
else if(iic_t_cnt == step_time) begin iic_sda_o <= 1'b0;iic_scl <= 1'b0;end//set scl pin high
if(iic_t_cnt != step_time) begin iic_t_cnt <= iic_t_cnt+16'b1;end
else begin iic_t_cnt <= 16'd0;iic_state <= FINISH;iic_start_IFG<=1'b1; end
end
SENDNACK:
begin
if(iic_t_cnt == 16'd0) begin iic_sda_o <= 1'b1;iic_scl <= 1'b0; end
else if(iic_t_cnt == step_time/2) begin iic_sda_o <= 1'b1;iic_scl <= 1'b1;end//set scl pin high
else if(iic_t_cnt == step_time) begin iic_sda_o <= 1'b1;iic_scl <= 1'b0;end//set scl pin high
if(iic_t_cnt != step_time) begin iic_t_cnt <= iic_t_cnt+16'b1;end
else begin iic_t_cnt <= 16'd0;iic_state <= FINISH;iic_start_IFG<=1'b1; end
end
STOP:
begin
if(iic_t_cnt == 16'd0) begin iic_sda_o <= 1'b0;iic_scl <= 1'b0; end
else if(iic_t_cnt == step_time/2) begin iic_sda_o <= 1'b0;iic_scl <= 1'b1;end//set scl pin high
else if(iic_t_cnt == step_time) begin iic_sda_o <= 1'b1;iic_scl <= 1'b1;end//set scl pin high
if(iic_t_cnt != step_time) begin iic_t_cnt <= iic_t_cnt+16'd1;end
else begin iic_t_cnt <= 16'd0;iic_state <= FINISH;iic_start_IFG<=1'b0; end
end
RECVBYTE://IIC receive a byte data
begin
if(iic_bit_cnt != 4'd8)
begin
if(iic_t_cnt == 16'd0)
begin
iic_scl <= 1'b0;
iic_recv_buff <= iic_recv_buff<<1;
end//set iic sel pin low
else if(iic_t_cnt == step_time/2)
begin
iic_scl <= 1'b1;
end//set iic sel pin high
else if(iic_t_cnt == ((step_time/4)*3))
begin
iic_scl <= 1'b1;
iic_recv_buff<=iic_recv_buff|iic_sda_i;
end//read iic sda pin
else if(iic_t_cnt == step_time)
begin
iic_scl <= 1'b0;
end
else
begin
iic_recv_buff<=iic_recv_buff;
end
if(iic_t_cnt != step_time)
begin
iic_t_cnt<= iic_t_cnt+16'd1;
end
else
begin
iic_t_cnt<= 16'd0;iic_bit_cnt<= iic_bit_cnt+16'd1;
end
end
else
begin
iic_rec_data <= iic_recv_buff;
iic_recv_buff <= 8'd0;
iic_bit_cnt <= 4'd0;
iic_state <= FINISH;
iic_start_IFG<=1'b1;
end
end
CHECKACK0://IIC check ack signal from slave
begin
if(iic_t_cnt == 16'd0) begin iic_scl <= 1'b0; end
else if(iic_t_cnt == step_time/2) //set IIC scl pin high
begin
iic_scl <= 1'b1;
end
if(iic_t_cnt != step_time)begin iic_t_cnt <= iic_t_cnt + 16'd1; end
else begin iic_t_cnt <= 16'd0;iic_state <= CHECKACK1; end
end
CHECKACK1:
begin
if(iic_sda_i == 1'b0)//check if sda pin is low
begin
iic_scl <= 1'b0;
iic_ack <= 1'b1; //has checked an ack signal
iic_state <= FINISH;
iic_start_IFG<=1'b1;
end
else
begin
iic_scl <= 1'b1;
iic_ack <= 1'b0;
iic_t_cnt <= iic_t_cnt + 16'd1;
if(iic_t_cnt == error_time)//has checked an no-ack signal
begin
iic_ack <= 1'b0;
iic_t_cnt <= 16'd0;
iic_state <= FINISH;
iic_start_IFG<=1'b1;
end
end
end
FINISH://set qvld high
begin
if(iic_event_start)
begin
iic_state <= FINISH;
end
else
begin
iic_state <= IDLE0;
end
iic_qvld <= 1'b1;
iic_INT <= 1'b1;
end
default:iic_state <= IDLE0;
endcase
end
else
begin
iic_bit_cnt <= iic_bit_cnt;
iic_state <= iic_state ;
iic_t_cnt <= iic_t_cnt ;
iic_rec_data <=iic_rec_data;
iic_sda_out <= iic_sda_out;
iic_sda_o <= iic_sda_o;
iic_scl <= iic_scl;
iic_qvld <= iic_qvld;
iic_ack <= iic_ack;
iic_send_buff <= iic_send_buff;
iic_recv_buff <= iic_recv_buff;
iic_INT <= iic_INT;
end
end
end
3.遇到的问题
在设计中,遇到的最头疼也是最奇怪的问题就是inout端口无法综合的问题,接下来写一下该问题的解决过程。
IIC属于半双工通信总线,所以对于SDA数据线既要能够作为输出发送数据,又要作为输入接收数据。这就涉及到了输入输出端口的设计,代码如下。
module tri_io(
input tri_i,
input tri_t,
output tri_o,
inout tri_p
);
assign tri_p = (tri_t == 1'b1) ? tri_i : 1'bz;
assign tri_o = tri_p;
endmodule
tri_t为三态门的输入输出方向控制信号,当tri_t=1’b1时,tri_p的值将等于tri_i,此时tri_p作为输出。当tri_t = 1’b0时,tri_p的值为高阻态,即此时tri_p值由外接设备决定,并将该值通过tri_o输出。
以上代码综合出的电路按理如下。
但在Vivado综合后,烧录进fpga的电路在设置该端口为输入情况下,无法进行高低电平的识别,甚至将IO口直接拉至电源电压,也无法识别高电平,该端口的读数永远为低电平。
在百思不得其解的情况下,我列出了如下几个可能原因一一排查
1.三态门代码错误。
2.IIC设备SDA总线需要上拉,但在约束时未进行上下拉进行约束。
3.Vivado无法综合inout逻辑。
对于第一种可能性,我将代码在quartus上进行实现,并烧录调试,发现是可以实现输入检测的。说明代码是没有错误的。
对于第二种可能性,我在约束文件中添加如下上拉约束。
set_property PULLUP true [get_ports iic_sda]
通过漫长的编译后下载到zynq中,结果显而易见,仍然为低电平。
那就剩下第三种可能性了,如果Vivado无法综合inout逻辑,那这个三态门综合出的结果是什么呢? 打开RTL原理图后,发现三态门被综合成下面这个样子:
没错,vivado将三态门综合成了LUT资源,图中的LUT1。
对于这种综合结果,与三态门相差甚远,但也可以解释为什么读数总为0了。为了解决这个综合错误问题,我尝试将三态门的逻辑放置到顶层top中,具体做法是将三态门单独封装成一个IP核,并在bd设计中调用,自以为这种直接连接IO口的逻辑,应该不会综合错误了吧。但结果令人大跌眼镜,Vivado综合出的结果如下。
这种综合结果仅在“形状”上与三态门相似,功能相差了十万八千里。也就是说在输入时,高阻逻辑被综合成了逻辑0。这就基本确定,Vivado无法综合inout逻辑。这让我突然想到前几个月在xilinx开发板上移植CPU时,总线错误问题,大概率和这个是一个原因了。
那既然vivado无法综合三态门,难道真的无法设计输入输出口了吗,答案肯定是行的,如果无法综合inout逻辑,那三态门肯定是在FPGA上的既定的资源。问题是如何去使用。
具体操作方法就是点击页面的加号在Interface Definition中找到gpio_rtl,再将信号一一绑定即可。重新设计完IP核后,进行综合和RTL分析,总算得到了心心念念的三态门。
至此,三态门综合错误问题解决。总结一下,Vivado是无法综合inout逻辑的,三态门作为既定的资源,也只能对真正存在的物理IO使用,具体方法就是通过Interface进行总线绑定,通过三态缓冲器驱动三态门。最后再放一张修改后的IP核图。
其实除了上述这种方法,还有一种方法是直接例化一个三态门IP核。如下:
IOBUF#(
)
IOBUF_inst7(
.T( tri_t[7]),
.I( tri_i[7]),
.O( tri_o[7]),
.IO(tri_io[7])
);
这样也能得到三态门。
四、软件驱动设计
软件层面实际上就是通过AXI总线向相关寄存器读写数据完成配置的过程,由于篇幅过长,这里就放一个发送字节的函数讲解下读写思路。
I2C接口初始化
/*-------------------------------------------------------------------
* 【 Name 】: <IIC_Master_send_byte>
* 【 Describe】: <This function start a processing of sending a byte data.>
* 【Parameter】:<Base_addr: The base address of IIC_Master device.>
* 【 Example 】:<IIC_Master_send_byte(IIC_Master_0);
* The example will let IIC_Master device send a byte data.>
*------------------------------------------------------------------*/
void IIC_Master_send_byte(AXI_BaseAddress_Data_Type Base_addr,uint8 data)
{
Xil_Out32(Base_addr|IIC_Master_SEL0, IIC_Master_SEL0_SENDBYTE);
while(((Xil_In32(Base_addr|IIC_Master_IFG))&(IIC_Master_BUSYIFG))==IIC_Master_BUSYIFG);
Xil_Out32(Base_addr|IIC_Master_SEDBUFF, data);
AXI_Register_Data_Type CTL0_status = Xil_In32(Base_addr|IIC_Master_CTL0);
CTL0_status |= IIC_Master_START;
Xil_Out32(Base_addr|IIC_Master_CTL0,CTL0_status);
while(((Xil_In32(Base_addr|IIC_Master_IFG))&(IIC_Master_BUSYIFG))==0x00);
CTL0_status &= ~IIC_Master_START;
Xil_Out32(Base_addr|IIC_Master_CTL0,CTL0_status);
while(((Xil_In32(Base_addr|IIC_Master_IFG))&(IIC_Master_EVENTIFG))==0x00);
}
首先配置指令寄存器IIC_Master_SEL0为发送数据,然后通过一个while去检测状态寄存器,查看I2C接口此时是否有正在进行的事件,然后向发送缓冲区IIC_Master_SEDBUFF写入发送数据,然后将控制寄存器CTL0中的start控制位拉高,触发一次发送事件。最后等待事件执行完成。
五、测试效果
通过逻辑分析仪显示的I2C事件:
驱动I2C OLED屏幕:
文字:
图片: