I2C通信模块的设计和“AT24C64 型号的EEPROM 芯片通信”实践
I2C通信模块的设计和“AT24C64 型号的EEPROM 芯片通信”实践
I2C(Inter-Integrated Circuit)总线是一种串行通信协议,广泛应用于嵌入式系统中,用于连接微控制器和各种外设。本文将详细介绍I2C通信协议的基本原理、FPGA模块设计以及与AT24C64 EEPROM芯片的通信实践。
I2C通信协议及FPGA模块设计
I2C总线使用两条线在主控制器和从机之间进行数据通信:SCL(串行时钟线)和SDA(串行数据线)。这两条线都需要接上拉电阻。I2C通信是半双工的,因为仅有一根数据线。
I2C总线有标准模式(100kb/s)和快速模式(400kb/s)两种。在一个I2C总线中,支持多个从设备,每个从设备都有不同的器件地址,这样I2C主控制器就可以通过I2C设备的器件地址访问指定的I2C设备。
I2C协议基本术语
- 起始信号:在SCL为高电平期间,SDA出现下降沿就表示产生起始信号。起始信号产生后总线处于占用状态。
- 停止信号:在SCL为高电平期间,SDA出现上升沿就表示为停止信号。停止信号产生后总线被释放,处于空闲状态。
- 数据传输:I2C总线在进行数据传输时要保证在SCL高电平期间,SDA上的数据稳定,因此SDA上的数据变化只能在SCL低电平期间发生。数据传送时,先传送最高位,后传送低位。
- 应答信号:当I2C主机发送完8bit数据后会将SDA设置为输入状态,等待I2C从机应答。从机通过将SDA拉低来表示发出应答信号(ACK,低有效),表示通信成功,否则表示通信失败(NACK)。
FPGA模块设计要求
设计一个I2C模块,需要满足以下要求:
- 支持总线仲裁丢失检测
- 支持总线忙状态检测
- 支持不同的I2C通信模式:标准模式(100kHz)和快速模式(400kHz)
- 支持产生起始、终止、重复起始和应答信息
- 支持起始、终止和重复起始检测
- 支持7位寻址模式
- 支持中断
主要包括8个8位宽的寄存器:
- I2C分频值低字节寄存器
- I2C分频值高字节寄存器
- I2C控制寄存器
- I2C发送数据寄存器
- I2C接受数据寄存器
- I2C命令寄存器
- I2C状态寄存器
- I2C总线死锁时间寄存器
根据I2C协议,需要设计一个状态机,对应I2C的通信过程。每个状态都需要持续多个周期,所以针对每个状态细分了几个子状态。
I2C有两个外部接口:I2C_SCL和I2C_SDA。对于I2C_SCL,这里只设计I2C主模式,所以对于FPGA来说,I2C_SCL始终是输出。对于I2C_SDA,在主模式下,在接收响应的状态下是输入,其他情况下为输出。为了便于接口管理,将I2C_SCL和I2C_SDA都设计为IOBUF。
主机读写数据时序
主机写数据
- 主机操作命令寄存器,使能开始命令,使I2C总线发送开始信号。
- 主机操作发送数据寄存器,写入从机地址+读写位,决定访问哪个从机。这是一个8位的数据,其中高7位是从机地址,最后1位是读写位。1表示读操作,0表示写操作(对主机而言)。这里读写位为0。
- 主机操作命令寄存器,使能写命令,使I2C总线开始传输数据。
- 主机读取状态寄存器的TIP位,以确保命令执行完毕。
- 主机操作发送数据寄存器,写入从机存储地址,决定待发送数据存储在从机哪里。
- 主机操作命令寄存器,使能写命令,使I2C总线开始传输数据。
- 主机读取状态寄存器的TIP位,以确保命令执行完毕。
- 主机操作发送数据寄存器,写入8bit的待发送数据。
- 主机操作命令寄存器,使能写命令,使I2C总线开始传输数据。
- 主机读取状态寄存器的TIP位,以确保命令执行完毕。
- 重复步骤8到10,不断向从机写数据。
- 主机操作命令寄存器,使能结束命令,使I2C总线结束传输数据。
- 每次传输结束后需要延时,保证下次能正常开始传输。
主机读数据
- 主机操作命令寄存器,使能开始命令,使I2C总线发送开始信号。
- 主机操作发送数据寄存器,写入从机地址+读写位,决定访问哪个从机。这是一个8位的数据,其中高7位是从机地址,最后1位是读写位。1表示读操作,0表示写操作(对主机而言)。这里读写位为0。
- 主机操作命令寄存器,使能写命令,使I2C总线开始传输数据。
- 主机读取状态寄存器的TIP位,以确保命令执行完毕。
- 主机操作发送数据寄存器,写入从机存储地址,主机将会从该地址读取数据。
- 主机操作命令寄存器,使能写命令,使I2C总线开始传输数据。
- 主机读取状态寄存器的TIP位,以确保命令执行完毕。
- 主机操作命令寄存器,使能开始命令(这种情况是重复起始),使I2C总线发送开始信号。
- 主机操作发送数据寄存器,写入从机地址+读写位。这里读写位为1。
- 主机操作命令寄存器,使能写命令,使I2C总线开始传输数据。
- 主机读取状态寄存器的TIP位,以确保命令执行完毕。
- 主机操作命令寄存器,使能读命令和应答命令,使I2C总线开始接收数据。
- 主机操作接收数据寄存器,读取接收到的8bit的数据。
- 重复步骤12到13,不断从从机读数据。
- 当主机需要停止从从机读数据时,操作命令寄存器,使能读命令,但不使能应答命令,读取最后一个字节的数据。
APB接口设计
可以将I2C模块作为一个APB外设,挂在APB总线上。在config.h文件中定义了9路APB地址,默认使用APB0作为GPIO,现在为I2C模块分配APB5,对应地址为0xbfe90000。
//APB0
`define APB_SLV0_ADDR_BASE 32'hbfeb0000 //APB0 base address
`define APB_SLV0_ADDR_LEN 32'h0000ffff //APB0 length
//APB1
`define APB_SLV1_ADDR_BASE 32'hbfec0000 //APB1 base address
`define APB_SLV1_ADDR_LEN 32'h0000ffff //APB1 length
//APB2
`define APB_SLV2_ADDR_BASE 32'hbfed0000 //APB2 base address
`define APB_SLV2_ADDR_LEN 32'h0000ffff //APB3 length
//APB3
`define APB_SLV3_ADDR_BASE 32'hbfea0000 //APB3 base address
`define APB_SLV3_ADDR_LEN 32'h0000ffff //APB3 length
//APB4
`define APB_SLV4_ADDR_BASE 32'hbfe88000 //APB4 base address
`define APB_SLV4_ADDR_LEN 32'h00000fff //APB4 length
//APB5
`define APB_SLV5_ADDR_BASE 32'hbfe90000 //APB5 base address
`define APB_SLV5_ADDR_LEN 32'h0000ffff //APB5 length
最后实现的结构框图如下所示:
在顶层文件godson_mcu_top.v中例化设计的模块,并在约束文件中将例化好的I2C的输出引脚与原理图上的合适引脚进行连接。
软件设计:基于I2C模块与AT24C64的EEPROM芯片通信
硬件I2C模块设计过程中为APB分配的地址是0xbfe90000,并且I2C相关寄存器的偏移地址是0x00 0x01 0x02 0x03 0x04,软件上需要对应好。一个不错的方法是用结构体指针来访问寄存器。由于这个结构体指针使用频率很高,所以通过宏定义进行重命名(I2C)。
I2C读写AT24C64函数实现
写一个字节
void AT24CXX_WriteByte(uint16_t u16Addr, uint8_t u8Data)
{
soc_I2C_GenerateSTART(ENABLE); // 起始信号
soc_I2C_SendData(DEV_ADDR | WRITE_CMD); // 器件寻址+读/写选择
soc_I2C_wait();
soc_I2C_SendData((uint8_t)((u16Addr >> 8) & 0xFF));
soc_I2C_wait();
soc_I2C_SendData((uint8_t)(u16Addr & 0xFF));
soc_I2C_wait();
soc_I2C_SendData(u8Data);
soc_I2C_wait();
soc_I2C_GenerateSTOP(ENABLE); // 停止信号
soc_I2C_delay(20); // 需要延时 2u ,保证下次能正常开始传输
}
读一个字节
uint8_t AT24CXX_ReadByte(uint16_t u16Addr)
{
uint8_t u8Data = 0;
soc_I2C_GenerateSTART(ENABLE); // 起始信号
soc_I2C_SendData(DEV_ADDR | WRITE_CMD); // 器件寻址+读/写选择
soc_I2C_wait();
soc_I2C_SendData((uint8_t)((u16Addr >> 8) & 0xFF));
soc_I2C_wait();
soc_I2C_SendData((uint8_t)(u16Addr & 0xFF));
soc_I2C_wait();
soc_I2C_GenerateSTART(ENABLE); // 重复起始信号
soc_I2C_SendData(DEV_ADDR | READ_CMD); // 器件寻址+读/写选择
soc_I2C_wait();
u8Data = soc_I2C_ReceiveData();
soc_I2C_wait();
soc_I2C_GenerateSTOP(ENABLE); // 停止信号
return u8Data;
}
这里需要注意的是,对芯片完成单字节写入或者页面写入命令后需要延时10ms。因为设备在此期间将不会对新的命令作出响应。延时之后再进行读命令才可以读到刚写入的数据。
可以在main.c中调用相应函数验证I2C通信。