IIC(Inter-Integrated Circuit)其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主多从架构。它有两根线,一根时钟线SCL,一根数据线SDA,半双工串行同步通信。
设备空闲状态:高电平 因为高电平可以检测设备的好坏。
起始位:时钟线高的时候,数据线拉低;停止位:时钟线高的时候,数据线拉高。在起始信号产生之后,总线就处于被占用的状态,在终止信号产生之后,总线就处于空闲状态。
低电平改变数据,高电平的时候稳定。时钟信号SCL为高电平期间,数据线SDA上的数据必须保持稳定,只有在时钟线SCL上的信号为低电平期间,数据线SDA上的高电平或低电平状态才允许变化。
IIC有应答机制,0为有效应答,1为无效应答,接收端收到有效数据后向对方响应的信号,发送端每发送一个字节(8位)数据,在第9个时钟周期释放数据线去接收对方的应答。每当发送器传输完一个字节的数据之后,发送端会等待一定的时间,等接收方的应答信号
发送数据以字节为单位,也就是一次数据最多8bit,再加一个ACK位,就是9bit。
时钟频率:IIC时钟频率在标准模式下可达100kHz,快速模式下可达400kHz,高速模式下可达3.4MHz。
AT24C64是一款EEPROM储存器,存储器容量为64Kbit,电源电压范围为1.8V ~3.6V。以AT24C64这颗芯片为例,查阅手册可以了解用IIC总线驱动AT24C64的读写的大致流程。
以字节写为例:
主机先发送一个设备地址,这里设备地址的高位是固定的1010,后三位由硬件设置。主机发送地址后,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/T位将自己确定为发送器和接收器。
在从机回复应答位ACK之后,主机发送写地址,即往这个设备的哪个地址写入,为13bit,因为IIC的数据线上一次数据最多8bit,所以要分成两个帧来发送,前一帧发送高5bit,缺位补0,后一帧发送低8bit。收到应答后主机向该地址发送数据。每一帧数据后面都有一位应答位。
以上是AT24C64字节写操作的时序图,除了字节写,还有页写,读操作有随机读,顺序读等,都可以查阅手册得知,都有相应的时序图。
当多个IIC设备通过IIC总线接在一起,这就要求IIC设备间可以实现线与,而芯片的IIC引脚是开漏输出的话就能很好的实现这个线与。只要有一个IIC设备的引脚电平是低电平,那么相应的SCL或SDA总线也会成为一个低电平。 如果IIC设备引脚为推挽输出,多个IIC设备接在一条总线上很容易烧坏芯片。因为IIC芯片的SDA和SCL的引脚是开漏输出,就只有一个MOS管,而推挽输出有两个MOS管。
当芯片SDA和SCL的引脚输出MOS管导通,IIC信号线电平为低电平。当芯片SDA和SCL的引脚输出MOS管关闭,如果没有上拉电阻,IIC信号线是处于一个高阻状态,电平是未知的,开漏输出是没有高电平的输出能力的。所以加上上拉电阻后,当芯片SDA和SCL的引脚输出MOS管关闭,IIC信号线上的电平就是一个确切的高电平。
IIC信号上拉电阻取值常用的值就是4.7K,一般小于10K,大于1K,如果IIC总线比较长,从设备比较多,可以适当降低电阻。
总线上挂有多个设备,设备于总线以高阻的形式连接。这样在设备不占用总线时自动释放总线,以方便其他设备获得总线的使用权。如果你的设备端口要挂在一个总线上,必须通过三态缓冲器。因为在一个总线上同时只能有一个端口作输出,这时其他端口必须在高阻态,同时可以输入这个输出端口的数据。所以还需要有总线控制管理, 访问到哪个端口,那个端口的三态缓冲器才可以转入输出状态,这是典型的三态门应用。
I2C中SDA因为SDA数据传输线是一个发送和接收数据共用的信号线,即FPGA的一个IO口既需要输出数据,又需要接收数据inout类型,那么则需要三态开漏模式去控制实现什么时候作为输出,什么时候作为输入;而且输出的时候不受输入的影响,输入的时候不受输出影响。
高阻态,指的是电路的一种输出状态,既不是高电平也不是低电平,如果高阻态再输入下一级电路的话,对下级电路无任何影响,和没接一样。当门电路的输出上拉管导通而下拉管截止时,输出为高电平;反之就是低电平;如上拉管和下拉管都截止时,输出端就相当于浮空(没有电流流动),其电平随外部电平高低而定,即该门电路放弃对输出端电路的控制 。
我们可以用两种方法实现三态门的控制。第一种可以用两行assign实现:
//三态门
assign io_iic_sda = r_iic_sda_ctrl ? ro_iic_sda : 1'bz;//输出数据,如果想输出数据,就把ctrl拉高,然后给ro这个寄存器赋值,然后就从io口输出出去了,io口就作为output口了
assign w_iic_sda = !r_iic_sda_ctrl ? io_iic_sda : 1'b0;//读取输入的数据,就是ctrl拉低,上面那个就是高阻态,然后下面这个就是把io口的数据发到w_iic_sda上,io口作为input
其中io_iic_sda为inout接口,r_iic_sda_ctrl为三态门控制寄存器,可以用时序逻辑来控制这个寄存器什么时候为1,从而控制数据流向。
第二种可以用vivado自带的原语iobuf。在vivado中,连接的管脚的信号一般都会自动添加OBUF或IBUF。但是对于inout类型的接口,不会主动添加IOBUF,因为in/out切换需要控制信号,需要用户自己分配好。
在Language Template中能找到IOBUF的标准实例并例化,端口名字与上面一样:
IOBUF #(
.DRIVE (12), // Specify the output drive strength
.IBUF_LOW_PWR("TRUE"), // Low Power - "TRUE", High Performance = "FALSE"
.IOSTANDARD ("DEFAULT"), // Specify the I/O standard
.SLEW ("SLOW") // Specify the output slew rate
) IOBUF_inst (
.O (ro_iic_sda), // Buffer output
.IO (io_iic_sda), // Buffer inout port (connect directly to top-level port)
.I (w_iic_sda), // Buffer input
.T (!r_iic_sda_ctrl) // 3-state enable input, high=input, low=output
);
原语中的O/I都是针对这个BUF来说的,不是针对管脚,务必注意。我们把 .IO() 端口当成pad管脚一侧,那么需要输出到io的内部信号填入到 .I(),通过OBUF缓冲输出到.IO()管脚;从.IO()管脚输入进来的信号经过IBUF缓冲到 .O()内部信号。输入信号想要正确,那么这个时候的OBUF必须是高阻z,也就是 .T()要有效。所以 .T() 填管脚input的使能条件,即让输出无效。