# 期望通过本文掌握I2C模块的FPGA和软件设计和实践 #
1 I2C(Inter-Integrated Circuit)通信协议及FPGA模块设计
I2C
是很常见的一种总线协议,使用两条线在主控制器和从机之间进行数据通信。一 条是 SCL(
串行时钟线
)
,另外一条是
SDA(
串行数据线
)
。这两条线都需要接上拉电 阻。因为仅有一根数据线,所以I2C
通信是半双工的。
I2C
总线有标准模式
(100kb/s)
和快速模式
(400kb/s)
两种。“总线
”
指多个设备共用的信号线。在一个
I2C
总线中,支持多个从设备。不同的从设备有不同的器件地址,这样 I2C
主控制器就可以通过
I2C
设备的器件地址访问指定的I2C 设备了,一个
I2C
总线连接多个
I2C
设备,如下所示,

I2C
协议基本术语
起始信号:
I2C
通信起始标志。主机告诉从机,要开始进行
I2C
通信了。在SCL 为高电平期间,
SDA
出现下降沿就表示产生起始信号。起始信号产生后总线处于占用状态。

停止信号:
I2C
停止通信的标志。在
SCL
为高电平期间,
SDA
出现上升沿就表
示为停止信号。停止信号产生后总线被释放,处于空闲状态。

数据传输:
I2C
总线在进行数据传输时要保证在
SCL
高电平期间,
SDA
上的数据稳定,因此 SDA
上的数据变化只能在
SCL
低电平期间发生。数据传送时,先传送最高位,后传送低位。

应答信号:当
I2C
主机发送完
8bit
数据后会将
SDA
设置为输入状态,等待
I2C从机应答,也就是等到 I2C
从机告诉主机它接收到
8bit
数据了。
应答信号是由从机发出的,主机需要提供应答信号所需的时钟。主机发送完数据以后的下一个时钟信号就是给应答信号使用的。从机通过将 SDA
拉低来表示发出应答信号(ACK
,低有效
)
,表示通信成功,否则表示通信失败
(NACK)
。
设计一个
I2C
模块,要求如下所示,
支持总线仲裁丢失检测;
支持总线忙状态检测;
支持不同的 I2C
通信模式:
标准模式(100kHz);
快速模式(400kHz);
支持产生起始、终止、重复起始和应答信息;
支持起始、终止和重复起始检测;
支持 7
位寻址模式;
支持中断;
主要包括 8
个
8
位宽的寄存器,如下所示
I2C 分频值低字节寄存器
I2C 分频值高字节寄存器
I2C 控制寄存器
I2C 发送数据寄存器
I2C 接受数据寄存器
I2C
命令寄存器
I2C 状态寄存器
I2C 总线死锁时间寄存器

根据
I2C
协议,我们需要设计一个状态机,对应
I2C
的通信过程,各个状态如下所
示,

根据
I2C
寄存器的配置以及状态完成标志进行上述状态的切换,如下所示
(
仅展示部
分代码
)
,

每个状态都需要持续多个周期,所以针对每个状态细分了几个子状态,如下所示,

I2C
有
2
个外部接口,分别是
I2C_SCL I2C_SDA
。对于
I2C_SCL
,这里我们只设计 I2C
主模式,所以对于
FPGA
来说,
I2C_SCL
始终是输出。对于
I2C_SDA
,在主模式下,在接收响应的状态下是输入,其他情况下为输出。为了便于接口管理,我们将 I2C_SCL I2C_SDA
都设计为
IOBUF
。如下所示,


主机读写数据的时序操作如下所示,
主机写数据
1.
主机操作命令寄存器,使能开始命令,使
I2C
总线发送开始信号。
2.
主机操作发送数据寄存器,写入从机地址
+
读写位,决定访问哪个从机。这是
一个8
位的数据,其中高
7
位是从机地址,最后
1
位是读写位。
1
表示读操作,
0 表示写操作(对主机而言)。这里读写位为
0
。
3.
主机操作命令寄存器,使能写命令,使
I2C
总线开始传输数据。
4.
主机读取状态寄存器的
TIP
位,以确保命令执行完毕。
5.
主机操作发送数据寄存器,写入从机存储地址,决定待发送数据存储在从机哪
里。
6.
主机操作命令寄存器,使能写命令,使
I2C
总线开始传输数据。
7.
主机读取状态寄存器的
TIP
位,以确保命令执行完毕。
8.
主机操作发送数据寄存器,写入
8 bit
的待发送数据。
9.
主机操作命令寄存器,使能写命令,使
I2C
总线开始传输数据。
10.
主机读取状态寄存器的
TIP
位,以确保命令执行完毕。
11.
重复步骤
8
到
10
,不断向从机写数据;
12.
主机操作命令寄存器,使能结束命令,使
I2C
总线结束传输数据。
13.
每次传输结束后需要延时,保证下次能正常开始传输。
主机读数据
1.
主机操作命令寄存器,使能开始命令,使
I2C
总线发送开始信号。
2.
主机操作发送数据寄存器,写入从机地址
+
读写位,决定访问哪个从机。这是一个8
位的数据,其中高
7
位是从机地址,最后
1
位是读写位。
1
表示读操作,
0 表示写操作(对主机而言)。这里读写位为
0
。
3. 主机操作命令寄存器,使能写命令,使
I2C
总线开始传输数据。
4.
主机读取状态寄存器的
TIP
位,以确保命令执行完毕。
5.
主机操作发送数据寄存器,写入从机存储地址,主机将会从该地址读取数据。
6.
主机操作命令寄存器,使能写命令,使
I2C
总线开始传输数据。
7.
主机读取状态寄存器的
TIP
位,以确保命令执行完毕。
8.
主机操作命令寄存器,使能开始命令(这种情况是重复起始),使
I2C
总线发送开始信号。
9.
主机操作发送数据寄存器,写入从机地址
+
读写位。这里读写位为
1
。
10.
主机操作命令寄存器,使能写命令,使
I2C
总线开始传输数据。
11.
主机读取状态寄存器的
TIP
位,以确保命令执行完毕。
12.
主机操作命令寄存器,使能读命令和应答命令,使
I2C
总线开始接收数据。
13.
主机操作接收数据寄存器,读取接收到的
8 bit
的数据。
14.
重复步骤
12
到
13
,不断从从机读数据;
15. 当主机需要停止从从机读数据时,操作命令寄存器,使能读命令,但不使能应答命令,读取最后一个字节的数据。
我们可以将
I2C
模块作为一个
APB
外设,挂在
APB
总线上。那么就需要设计一个APB 接口来对寄存器进行读写操作。我们需要对每个
APB
接口分配一个地址,这样才能通过译码电路区分开来不同的 APB
。在
config.h
文件中定了
9
路
APB
地址,默认使用 APB0
作为
GPIO
,现在我们为
I2C
模块分配
APB5
,对应地址为
0xbfe90000
,如下所示

最后实现的结构框图如下所示,

在顶层文件
godson_mcu_top.v
中例化我们设计的模块,如下所示,

在约束文件中,将例化好的
I2C
的输出引脚与原理图上的合适引脚进行连接即可,如
下所示,

2. 软件设计,基于软 I2C 模块与模 AT24C64型号的EEPROM芯片通信
既然已经设计好了硬件电路,我们就可以进行软件程序的编写了。在硬件
I2C
模块设
计过程中我们为
APB
分配的地址是
0xbfe90000
,并且
I2C
相关寄存器的偏移地址
是
0x00 0x01 0x02 0x03 0x04
,所以软件上需要对应好。一个不错的方法是用结构体
指针来访问寄存器。由于这个结构体指针使用频率很高,所以通过宏定义进行重命名
(I2C)
,如下所示,

编写的函数声明如下所示,

然后可以基于
I2C
相函数编写
I2C
读写
AT24C64
的函数,如下所示,
/****************************************************************
*
函数名:
AT24CXX_WriteByte
*
功 能:写一个字节
*
参 数:
u16Addr
要写入的地址
u8Data
要写入的数据
*
返回值:无
*
说 明:无
****************************************************************/

/****************************************************************
*
函数名:
x24Cxx_ReadByte
*
功 能:读一个字节
*
参 数:
u16Addr
要读取的地址
*
返回值:
u8Data
读出的数据
*
说 明:无
****************************************************************/


这里需要注意的是,对芯片完成单字节写入或者页面写入命令后需要延时
10ms
。因为设备在此期间将不会对新的命令作出响应。延时之后再进行读命令才可以读到刚写入的数据。
我们可以在
main.c
中调用相应函数验证
I2C
通信,如下所示,

