问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

I2C通信模块的设计和“AT24C64 型号的EEPROM 芯片通信”实践

创作时间:
作者:
@小白创作中心

I2C通信模块的设计和“AT24C64 型号的EEPROM 芯片通信”实践

引用
与非网
1.
https://www.eefocus.com/article/1750309.html

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。

主机读写数据时序

主机写数据

  1. 主机操作命令寄存器,使能开始命令,使I2C总线发送开始信号。
  2. 主机操作发送数据寄存器,写入从机地址+读写位,决定访问哪个从机。这是一个8位的数据,其中高7位是从机地址,最后1位是读写位。1表示读操作,0表示写操作(对主机而言)。这里读写位为0。
  3. 主机操作命令寄存器,使能写命令,使I2C总线开始传输数据。
  4. 主机读取状态寄存器的TIP位,以确保命令执行完毕。
  5. 主机操作发送数据寄存器,写入从机存储地址,决定待发送数据存储在从机哪里。
  6. 主机操作命令寄存器,使能写命令,使I2C总线开始传输数据。
  7. 主机读取状态寄存器的TIP位,以确保命令执行完毕。
  8. 主机操作发送数据寄存器,写入8bit的待发送数据。
  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. 主机操作接收数据寄存器,读取接收到的8bit的数据。
  14. 重复步骤12到13,不断从从机读数据。
  15. 当主机需要停止从从机读数据时,操作命令寄存器,使能读命令,但不使能应答命令,读取最后一个字节的数据。

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通信。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号