开发环境:
IDE:MounRiver Studio
MCU:CH585
4 硬件I2C
4.1 具体代码实现
首先看看I2C的初始化。这有两部分。
一部分是I2C的GPIO初始化。
static void I2C_GPIO_Config(void)
{
GPIOB_ModeCfg(GPIO_Pin_12 | GPIO_Pin_13, GPIO_ModeIN_PU);
}
另外一部分就是配置I2C的参数。
static void I2C_Mode_Config(void)
{
I2C_Init(I2C_Mode_I2C, 100000, I2C_DutyCycle_16_9, I2C_Ack_Enable, I2C_AckAddr_7bit, MASTER_ADDR);
while(I2C_GetFlagStatus(I2C_FLAG_BUSY) != RESET);
}
主要配置I2C模式、低电平占空比、I2C寻址模式以及通信速率,最后使能I2C设备。
初始化完成后就是对AT24C02的读写操作,严格按照相应的时序操作就行。
在字节写模式下,向AT24C02中写数据时序如下:

Figure 4‑1 字节写操作
操作时序如下:
1.MCU先发送一个开始信号(START)启动总线
2.接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
3.等待应答信号(ACK)
4.发送数据的存储地址。24C02一共有256个字节的存储空间,地址从0x00~0xFF,想把数据存储在哪个位置,此刻写的就是哪个地址。
5.发送要存储的数据,在写数据的过程中,AT24C02会回应一个“应答位0”,则表明写AT24C02数据成功,如果没有回应答位,说明写入不成功。
6.发送结束信号(STOP)停止总线。
代码很简单,跟着时序来就行。
void I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)
{
I2C_GenerateSTART(ENABLE);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(EEPROM_ADDRESS, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(WriteAddr);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(*pBuffer);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(ENABLE);
}
用页写,AT24C01可一次写入8 个字节数据,AT24C02/04/08/16可以一次写入16个字节的数据。__页写操作的启动和字节写一样,不同在于传送了一字节数据后并不产生停止信号。__每发送一个字节数据后AT24Cxx产生一个应答位并将字节地址低位加1,高位保持不变。
如果在发送停止信号之前主器件发送超过P+1个字节,地址计数器将自动翻转,先前写入的数据被覆盖。
接收到P+1字节数据和主器件发送的停止信号后,AT24Cxx启动内部写周期将数据写到数据区。所有接收的数据在一个写周期内写入AT24Cxx。

Figure 4‑2 页写时序
代码很简单,和字节写不同的是,数据会一直发,直到主机发送停止信号。
void I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite)
{
while(I2C_GetFlagStatus(I2C_FLAG_BUSY));
I2C_GenerateSTART(ENABLE);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(EEPROM_ADDRESS, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(WriteAddr);
while(! I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED));
while(NumByteToWrite--)
{
I2C_SendData(*pBuffer);
pBuffer++;
while (!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
I2C_GenerateSTOP(ENABLE);
}
在实际过程中,我们经常需要任意写数据,这里就调用页写的操作,来实现任意字节的写操作。
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
Addr = WriteAddr % I2C_PageSize;
count = I2C_PageSize - Addr;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
if(Addr == 0)
{
if(NumOfPage == 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
else
{
while(NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
if(NumOfSingle!=0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
else
{
if(NumOfPage== 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
else
{
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
if(count != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
I2C_EE_WaitEepromStandbyState();
WriteAddr += count;
pBuffer += count;
}
while(NumOfPage--)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
if(NumOfSingle != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
}
主要分为两种情况,写的地址正好是一页的开始,另外一种是在一页的中间。不管如何,始终遵循的原则就是最大智能写一页,可以从一页的中间开始。
读操作允许主器件对寄存器的任意字节进行读操作,主器件首先通过发送起始信号、从器件地址和它想读取的字节数据的地址执行一个写操作。在AT24Cxx应答之后,主器件重新发送起始信号和从器件地址,此时R/W位置1,AT24Cxx响应并发送应答信号,然后输出所要求的一个8位字节数据,主器件不发送应答信号但产生一个停止信号。

Figure 4‑3 读取字节的时序
读取字节的时序如下:
1.MCU先发送一个开始信号(START)启动总线
2.接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
注意:这里写操作是为了要把所要读的数据的存储地址先写进去,告诉AT24Cxx要读取哪个地址的数据。
3.发送要读取内存的地址(WORD ADDRESS),通知AT24Cxx读取要哪个地址的信息。
4.重新发送开始信号(START)。
5.发送设备读操作地址(DEVICE ADDRESS)对AT24Cxx进行读操作 (0xA1)。
6.AT24Cxx会自动向主机发送数据,主机读取从器件发回的数据,在读一个字节后,MCU会回应一个应答信号(ACK)。
7.发送一个“非应答位NAK(1)”。发送结束信号(STOP)停止总线。
在AT24Cxx发送完一个8位字节数据后,主器件产生一个应答信号来响应,告知AT24Cxx主器件要求更多的数据,对应每个主机产生的应答信号AT24Cxx将发送一个8位数据字节。当主器件不发送应答信号而发送停止位时结束此操作。
从AT24Cxx输出的数据按顺序由N到N+1输出。读操作时地址计数器在AT24Cxx整个地址内增加,这样整个寄存器区域可在一个读操作内全部读出,当读取的字节超过E(对于24WC01,E=127;对24C02,E=255;对24C04,E=511;对24C08,E=1023;对24C16,E=2047)计数器将翻转到零并继续输出数据字节。

Figure 4‑4 顺序读时序
我们常用的方式就是连续读取,代码很简单。
void I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead)
{
while(I2C_GetFlagStatus(I2C_FLAG_BUSY));
I2C_GenerateSTART(ENABLE);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(EEPROM_ADDRESS, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_Cmd(ENABLE);
I2C_SendData(ReadAddr);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTART(ENABLE);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(EEPROM_ADDRESS, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
while(NumByteToRead)
{
if(NumByteToRead == 1)
{
I2C_AcknowledgeConfig(DISABLE);
I2C_GenerateSTOP(ENABLE);
}
if(I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_RECEIVED))
{
*pBuffer = I2C_ReceiveData();
pBuffer++;
NumByteToRead--;
}
}
I2C_AcknowledgeConfig(ENABLE);
}
最后看下主函数吧。
/*********************************************************************
* @fn main
*
* @brief 主函数
*
* [url=home.php?mod=space&uid=1141835]@Return[/url] none
*/
int main()
{
HSECFG_Capacitance(HSECap_18p);
SetSysClock(CLK_SOURCE_HSE_PLL_62_4MHz);
BSP_UART_Init();
printf("I2C Test\r\n");
/* I2C 外设初(AT24C02)始化 */
I2C_EE_Init();
I2C_Test();
while(1)
{
}
}
很简单,往AT24C02中写入数据,然后再读取数据,读写测试的函数如下:
void I2C_Test(void)
{
uint16_t i;
printf("Write data\r\n");
for ( i=0; i<=255; i++ )
{
I2c_Buf_Write[i] = i;
printf("0x%02X ", I2c_Buf_Write[i]);
if(i%16 == 15)
{
printf("\n\r");
}
}
I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 256);
printf("\r\nWrite success\r\n");
printf("\r\nRead data\r\n");
I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256);
for (i=0; i<256; i++)
{
if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
{
printf("0x%02X ", I2c_Buf_Read[i]);
printf("Error: Inconsistent data between I2C EEPROM writing and reading\n\r");
return;
}
printf("0x%02X ", I2c_Buf_Read[i]);
if(i%16 == 15)
{
printf("\r\n");
}
}
printf("I2C(AT24C02) Read and write test successful\r\n");
}
4.2 实验现象
下载好程序后,打开串口助手,可以看到如下信息。

Figure 4‑5 硬件I2C实现现象
最后,我们使用逻辑分析来查看数据。

Figure 4‑6 I2C具体时序
我这里使用的100kHz的速率,可以看到数据的写操作和前面分析的时序是一样的,完全吻合。