一、I2C协议
I2C物理层两条线,SCL和SDA。
SCL(serial clock):时钟线,传输CLK信号,一般是I2C主设备向从设备提供时钟的通道。
SDA(serial data):数据线,通信数据都通过SDA线传输。
I2C通信可以一对一(一个主设备对1个从设备),也可以一对多(一个主设备对多个从设备)。
一个Master可以对应多个Slave。

1.1 数据传输格式

当数据传输结束时,发送一个停止传输信号 (P),表示不再传输数据。
1.2 信号解析
- 起始信号[Start]:在SCL保持高电平时,SDA下降沿。
- 读写位:【0写1读】
- 数据位:SCL低电平时,SDA可变化;SCL高电平,SDA不变
- 终止信号:在SCL高电平时,SDA上升沿。
- 回应信号[ACK]:SDA低电平,低电平

二、RT1064 I2C
我们可以用两个GPIO引脚,分别用作SCL和SDA,模拟I2C的协议,这被称之为“软件模拟协议”方式。
RT1064提供了LPI2C(Low power I2C,低功耗I2C)片上外设专门负责实现 I2C 通讯协议,这种由硬件外设处理 I2C 协议的方式被称之为“硬件协议”方式。这种方式只需要配置好外设,软件设计更加的简单。
RT1064 I2C的框架图:

RT1064提供了4个I2C。官方已经提供了封装好的库函数,也有例程,其中很多细节不需要太关心。
2.1 初始化配置结构体
typedef struct _lpi2c_master_config
{
bool enableMaster; /*!< Whether to enable master mode. */
bool enableDoze; /*!< Whether master is enabled in doze mode. */
bool debugEnable; /*!< Enable transfers to continue when halted in debug mode. */
bool ignoreAck; /*!< Whether to ignore ACK/NACK. */
lpi2c_master_pin_config_t pinConfig; /*!< The pin configuration option. */
uint32_t baudRate_Hz; /*!< Desired baud rate in Hertz. */
uint32_t busIdleTimeout_ns; /*!< Bus idle timeout in nanoseconds. Set to 0 to disable. */
uint32_t pinLowTimeout_ns; /*!< Pin low timeout in nanoseconds. Set to 0 to disable. */
uint8_t sdaGlitchFilterWidth_ns; /*!< Width in nanoseconds of glitch filter on SDA pin. Set to 0 to disable. */
uint8_t sclGlitchFilterWidth_ns; /*!< Width in nanoseconds of glitch filter on SCL pin. Set to 0 to disable. */
struct
{
bool enable; /*!< Enable host request. */
lpi2c_host_request_source_t source; /*!< Host request source. */
lpi2c_host_request_polarity_t polarity; /*!< Host request pin polarity. */
} hostRequest; /*!< Host request options. */
} lpi2c_master_config_t;使用的时候和其他外设一样,先使用LPI2C_MasterGetDefaultConfig函数获取默认值,再对默认值进行修改。
I2C的默认值设置如下,通常是不需要修改的,只需要根据实际情况修改 I2C通讯的波特率即可。
void LPI2C_MasterGetDefaultConfig(lpi2c_master_config_t *masterConfig)
{
/* Initializes the configure structure to zero. */
(void)memset(masterConfig, 0, sizeof(*masterConfig));
masterConfig->enableMaster = true;
masterConfig->debugEnable = false;
masterConfig->enableDoze = true;
masterConfig->ignoreAck = false;
masterConfig->pinConfig = kLPI2C_2PinOpenDrain;
masterConfig->baudRate_Hz = 100000U;
masterConfig->busIdleTimeout_ns = 0U;
masterConfig->pinLowTimeout_ns = 0U;
masterConfig->sdaGlitchFilterWidth_ns = 0U;
masterConfig->sclGlitchFilterWidth_ns = 0U;
masterConfig->hostRequest.enable = false;
masterConfig->hostRequest.source = kLPI2C_HostRequestExternalPin;
masterConfig->hostRequest.polarity = kLPI2C_HostRequestPinActiveHigh;
}三、EEPROM CAT24C256
本文例子中所用的EEPROM是CAT24C256,这也是一个很常用的EEPROM, 网上很多例子用的是AT24C02,和它是一个系列的,大致上差不多,但也有一些细微的差异。
CAT24C256的引脚,其中A0,A1,A2是设备地址,WP是写保护。

EEPROM芯片的设备地址一共有7位,其中高4位固定为:1010,低3位则由A0/A1/A2信号线的电平决定。

本文所用的EEPROM,A2和A1为0,A0为1,所以 EEPROM的7位设备地址是:1010001。由于 I2C通讯时常常是地址跟读写方向连在一起构成一个8位数,且当 R/W 位为0时,表示写方向,所以加上7位地址,其值为“0xA2”,常称该值为I2C 设备的“写地址”;当 R/W位为1时,表示读方向,加上7位地址,其值为“0xA3”, 常称该值为“读地址”。
这里的地址和硬件电路相关,要根据具体的电路图来设置。
#define EEPROM_ADDRESS_7_BIT (0xA2>>1)
#define EEPROM_WRITE_ADDRESS_8_BIT (0xA2)
#define EEPROM_READ_ADDRESS_8_BIT (0xA3)
3.1初始化
需要初始化的是SCL、SDA和WP。WP是普通的GPIO,照前文所说的配置即可。SCL和SDA根据电路图,使用的是I2C3。
相应的宏定义如下:
#define EEPROM_I2C_MASTER_BASE (LPI2C3_BASE)
#define EEPROM_I2C_MASTER ((LPI2C_Type *)EEPROM_I2C_MASTER_BASE)
#define EEPROM_WP_GPIO GPIO3
#define EEPROM_WP_GPIO_PIN (0U)
#define WP_HIGH() GPIO_PinWrite(EEPROM_WP_GPIO, EEPROM_WP_GPIO_PIN, 1U)
#define WP_LOW() GPIO_PinWrite(EEPROM_WP_GPIO, EEPROM_WP_GPIO_PIN, 0U)SCL和SDA还需要使用配置工具进行配置。
特别要注意的是调用IOMUXC_SetPinMux 时的第二个参数必须设置为“1”以使能引脚的SION 功能。正常设置完第二个参数为0,需要手工修改pin_mux.c里的代码。
IOMUXC_SetPinMux(IOMUXC_GPIO_EMC_21_LPI2C3_SDA,1U);
IOMUXC_SetPinMux(IOMUXC_GPIO_EMC_22_LPI2C3_SCL, 1U);设置完引脚后,I2C的初始化函数很简单,和其他外设一样,设置时钟,获取默认配置,修改配置,初始化I2C,具体代码如下。
void EEPROM_I2C_ModeInit(void)
{
lpi2c_master_config_t masterConfig;
/*Clock setting for LPI2C*/
CLOCK_SetMux(kCLOCK_Lpi2cMux, LPI2C_CLOCK_SOURCE_SELECT);
CLOCK_SetDiv(kCLOCK_Lpi2cDiv, LPI2C_CLOCK_SOURCE_DIVIDER);
/*
* masterConfig->enableMaster = true;
* masterConfig->debugEnable = false;
* masterConfig->ignoreAck = false;
* masterConfig->pinConfig = kLPI2C_2PinOpenDrain;
* masterConfig->baudRate_Hz = 100000U;
* masterConfig->busIdleTimeout_ns = 0U;
* masterConfig->pinLowTimeout_ns = 0U;
* masterConfig->sdaGlitchFilterWidth_ns = 0U;
* masterConfig->sclGlitchFilterWidth_ns = 0U;
* masterConfig->hostRequest.enable = false;
* masterConfig->hostRequest.source = kLPI2C_HostRequestExternalPin;
* masterConfig->hostRequest.polarity = kLPI2C_HostRequestPinActiveHigh;
*/
LPI2C_MasterGetDefaultConfig(&masterConfig);
/* Change the default baudrate configuration */
masterConfig.baudRate_Hz = EEPROM_I2C_BAUDRATE;
/* Initialize the LPI2C master peripheral */
LPI2C_MasterInit(EEPROM_I2C_MASTER, &masterConfig, LPI2C_CLOCK_FREQUENCY);
}其中的时钟设置:
调用库函数 CLOCK_SetMux 选择LPI2C根时钟LPI2C_CLK_ROOT的时钟来源,它的可选值为 0(PLL3的8分频,即USB1 PLL的8分频)或1(OSC,即外部晶振)。在本例子中,LPI2C_CLOCK_SOURCE_SELECT的值为0,也就是说PLL3 的频率为 480MHz,其8分频为60MHz。
调用库函数CLOCK_SetDiv 设置时钟分频因子,其第二参数值设为5, 也就是说对时钟的分频为 6(即5+1)。
最终,LPI2C 根时钟的频率 为fPLL3/8/( LPI2C_CLOCK_SOURCE_DIVIDER+1) = 480/8/(5+1)=10MHz。
/* Select USB1 PLL (480 MHz) as master lpi2c clock source : 480/8 = 60MHz */
#define LPI2C_CLOCK_SOURCE_SELECT (0U)
/* Clock divider for master lpi2c clock source */
#define LPI2C_CLOCK_SOURCE_DIVIDER (5U)
/* Get frequency of lpi2c clock LPI2C_CLK_ROOT = 60/(5+1) = 10MHz */
#define LPI2C_CLOCK_FREQUENCY ((CLOCK_GetFreq(kCLOCK_Usb1PllClk) / 8) /(LPI2C_CLOCK_SOURCE_DIVIDER + 1U))EEPROM_I2C_ModeInit函数只是初始化了I2C,还需要初始化WP引脚。
初始化时将其置为高,写保护。等需要写的时候再拉低,写数据,写完数据再拉高。
void EEPROM_GPIO_Init(void)
{
gpio_pin_config_t wp_config = {kGPIO_DigitalOutput, 0, kGPIO_NoIntmode};
GPIO_PinInit(EEPROM_WP_GPIO, EEPROM_WP_GPIO_PIN, &wp_config);
WP_HIGH();
}
LPI2C_MasterStart函数发送START信号和从机地址 (SLAVE_ADDRESS)。如果有设备地址和这个地址相同,就会返回一个应答(ACK)信号。这个函数被用来检测设备是否连接上。
uint8_t EEPROM_CheckOk(void)
{
status_t lpi2c_status;
lpi2c_status = LPI2C_MasterStart(EEPROM_I2C_MASTER, EEPROM_ADDRESS_7_BIT, kLPI2C_Write);
if (lpi2c_status != kStatus_Success)
{
return 1;
}
return 0;
}
uint8_t InitEEPROM(void)
{
uint8_t reVal;
EEPROM_GPIO_Init();
EEPROM_I2C_ModeInit();
reVal = EEPROM_CheckOk();
return reVal;
}3.2 写数据
CAT24C256 一次最多只能写一页,也就是64bytes。CAT24C256存储容量为 32768 个字节,512页,每页64字节。
写时序如下图所示,I2C设备地址后,先发送内部存储单元的首地址(Word Address),再后面的才是数据。CAT24C256的Word Address为2位。

如果只写入一个字节,在Word Address后跟一个字节即可。

写数据的代码如下:
#define EEPROM_INER_ADDRESS_SIZE 0x02
uint32_t I2C_EEPROM_Page_Write( uint8_t ClientAddr, uint16_t WriteAddr, uint8_t* pBuffer,uint8_t NumByteToWrite)
{
lpi2c_master_transfer_t masterXfer = {0};
status_t reVal = kStatus_Fail;
if(NumByteToWrite>EEPROM_PAGE_SIZE)
{
PRINTF("NumByteToWrite>EEPROM_PageSize\r\n");
return 1;
}
/* subAddress = WriteAddr, data = pBuffer
* start + slave address(w) + tx data buffer + stop
*/
masterXfer.slaveAddress = (ClientAddr>>1);
masterXfer.direction = kLPI2C_Write;
masterXfer.subaddress = WriteAddr;
masterXfer.subaddressSize = EEPROM_INER_ADDRESS_SIZE;
masterXfer.data = pBuffer;
masterXfer.dataSize = NumByteToWrite;
masterXfer.flags = kLPI2C_TransferDefaultFlag;
reVal = LPI2C_MasterTransferBlocking(EEPROM_I2C_MASTER, &masterXfer);
if (reVal != kStatus_Success)
{
return 1;
}
return 0;
}这个函数只写一次,所以数据的最大大小是64字节,并且不能跨页。
这个函数写的比较简单,主要是对lpi2c_master_transfer_t 结构体类型的变量masterXfer赋值,然后调用库函数LPI2C_MasterTransferBlocking发送数据。
传输结构体的定义: subaddress就是前面说的Word Address,这里要将subaddressSize的值设为2。
struct _lpi2c_master_transfer
{
uint32_t flags; /*!< Bit mask of options for the transfer. See enumeration #_lpi2c_master_transfer_flags for
available options. Set to 0 or #kLPI2C_TransferDefaultFlag for normal transfers. */
uint16_t slaveAddress; /*!< The 7-bit slave address. */
lpi2c_direction_t direction; /*!< Either #kLPI2C_Read or #kLPI2C_Write. */
uint32_t subaddress; /*!< Sub address. Transferred MSB first. */
size_t subaddressSize; /*!< Length of sub address to send in bytes. Maximum size is 4 bytes. */
void *data; /*!< Pointer to data to transfer. */
size_t dataSize; /*!< Number of bytes to transfer. */
};3.3 写入后的状态等待
如果写入的内容比较多,EEPROM擦写需要的时间比较长,这期间EEPROM不会响应主机的任何访问,所以要先等待EEPROM内部擦写完毕后再进行后续访问。
代码如下:
static uint32_t I2C_Timeout_Callback(uint8_t errorCode)
{
PRINTF("I2C Timeout\n");
return 0xFF;
}
uint8_t I2C_EEPROM_WaitStandbyState(uint8_t ClientAddr)
{
status_t lpi2c_status;
uint32_t delay_count = I2CT_LONG_TIMEOUT;
do
{
/*Clears the LPI2C master status flag state*/
LPI2C_MasterClearStatusFlags(EEPROM_I2C_MASTER, kLPI2C_MasterNackDetectFlag);
lpi2c_status = LPI2C_MasterStart(EEPROM_I2C_MASTER, (ClientAddr>>1), kLPI2C_Write);
SDK_DelayAtLeastUs(40, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY);
}while(EEPROM_I2C_MASTER->MSR & kLPI2C_MasterNackDetectFlag && delay_count-- );
LPI2C_MasterClearStatusFlags(EEPROM_I2C_MASTER, kLPI2C_MasterNackDetectFlag);
lpi2c_status = LPI2C_MasterStop(EEPROM_I2C_MASTER);
SDK_DelayAtLeastUs(10, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY);
if(delay_count == 0 || lpi2c_status != kStatus_Success)
{
I2C_Timeout_Callback(3);
return 1;
}
return 0;
}LPI2C_MasterClearStatusFlags用来清除标志位。
LPI2C_MasterStart:发送起始信号
LPI2C_MasterStop : 发送停止信号
I2C_EEPROM_WaitStandbyState函数就是向I2C发送EEPROM的设备地址并检测是否有响应,若检测到EEPROM返回应答信号,就表明EEPROM写时序完成,否则重复以上过程等待至有应答或等待超时。
3.4 读数据
EEPROM的读数据包含了一个写过程和一个读过程。


实现的代码和写数据类似,都是填充传输结构体masterXfer。读数据不需要等待EEPROM的内部写时序,也没有读取数据个数的限制。
uint32_t I2C_EEPROM_BufferRead( uint8_t ClientAddr,uint16_t ReadAddr,uint8_t* pBuffer,uint16_t NumByteToRead)
{
lpi2c_master_transfer_t masterXfer = {0};
status_t reVal = kStatus_Fail;
/* subAddress = ReadAddr, data = pBuffer
* start + slaveaddress(w) + subAddress + repeated start + slaveaddress(r) + rx data buffer + stop
*/
masterXfer.slaveAddress = (ClientAddr>>1);
masterXfer.direction = kLPI2C_Read;
masterXfer.subaddress = (uint32_t)ReadAddr;
masterXfer.subaddressSize = EEPROM_INER_ADDRESS_SIZE;
masterXfer.data = pBuffer;
masterXfer.dataSize = NumByteToRead;
masterXfer.flags = kLPI2C_TransferDefaultFlag;
reVal = LPI2C_MasterTransferBlocking(EEPROM_I2C_MASTER, &masterXfer);
if (reVal != kStatus_Success)
{
return 1;
}
return 0;
}