6 I2C读写EEPROM
6.1 RA Smart Configurator配置I2C
打开RA Smart Configurator,根据硬件连接,I2C使用的是I2C3,因此在配置界面里面依次打开“Pins->Peripherals->Connectivity:SCI>SCI3”配置SCI模块,选择开发板所用的I2C引脚,这里SCL和SDA分别接的是P408和P409引脚。

Figure 6‑1 I2C引脚设置
接下来就是添加I2C的stack。

Figure 6‑2 添加I2C的stack步骤
接下来需要配置I2C的参数。

Figure 6‑3 I2C参数设置
这里可以设置I2C的参数,我这里设置I2C的变量名、通道以及从机地址,I2C的编号和Channel编号是一一对应的,因此需要设置为3,回调函数依据C语言命名规范任意编译一个就行。
值得注意的是,这里的从机地址是7位,代码中自动左移了。
然后让软件自动生成配置代码即可。
6.2 基于I2C的EEPROM读写实现
R_SCI_I2C_Open()函数为执行IIC初始化,开启配置如下所示。
err = R_SCI_I2C_Open(&g_i2c3_ctrl, &g_i2c3_cfg);
assert(FSP_SUCCESS == err);
R_SCI_I2C_Write()函数是向IIC设备中写入数据,写入格式如下所示。
err = R_SCI_I2C_Write(&g_i2c_device_ctrl_1, &g_i2c_tx_buffer[0], I2C_BUFFER_SIZE_BYTES, false);
assert(FSP_SUCCESS == err);
R_SCI_I2C_Read ()函数是向IIC设备中写入数据,写入格式如下所示。
err = R_SCI_I2C_Read(&g_i2c_device_ctrl_1, &g_i2c_rx_buffer[0], I2C_BUFFER_SIZE_BYTES, false);
assert(FSP_SUCCESS == err);
sci_i2c_master_callback()回调函数用于数据是否发送完毕,可以查看是否获取到I2C_MASTER_EVENT_TX_COMPLETE字段。
i2c_master_event_t i2c_event = I2C_MASTER_EVENT_ABORTED;
void sci_i2c_master_callback(i2c_master_callback_args_t *p_args)
{
i2c_event = I2C_MASTER_EVENT_ABORTED;
if (NULL != p_args)
{
i2c_event = p_args->event;
}
}
主要配置I2C模式、低电平占空比、I2C寻址模式以及通信速率,最后使能I2C设备。
初始化完成后就是对AT24C02的读写操作,严格按照相应的时序操作就行。
6.2.1 字节写
在字节写模式下,向AT24C02中写数据时序如下:

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

Figure 6‑5 页写时序
代码很简单,和字节写不同的是,数据会一直发,直到主机发送停止信号。
void BSP_I2C_EE_Writepage(unsigned char* ptr_write , unsigned char WriteAddr, unsigned char len)
{
unsigned char send_buffer[9] = {};
send_buffer[0] = WriteAddr;
for(unsigned char i = 0;i<len;i++)
{
send_buffer[1+i] = *(ptr_write+i);
}
R_SCI_I2C_Write(&g_i2c3_ctrl, &send_buffer[0], len+1 , false);
while ((I2C_MASTER_EVENT_TX_COMPLETE != i2c_event) && timeout_ms)
{
R_BSP_SoftwareDelay(1U, BSP_DELAY_UNITS_MILLISECONDS);
timeout_ms--;
}
timeout_ms = 500;
BSP_I2C_EE_WaitState();
}
6.2.3 任意写
在实际过程中,我们经常需要任意写数据,这里就调用页写的操作,来实现任意字节的写操作。
void BSP_I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
Addr = WriteAddr % EEPROM_PAGESIZE;
count = EEPROM_PAGESIZE - Addr;
NumOfPage = (uint8_t)(NumByteToWrite / EEPROM_PAGESIZE);
NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;
if (Addr == 0)
{
if (NumOfPage == 0)
{
BSP_I2C_EE_Writepage(pBuffer, WriteAddr, NumOfSingle);
}
else
{
while (NumOfPage--)
{
BSP_I2C_EE_Writepage(pBuffer, WriteAddr, EEPROM_PAGESIZE);
WriteAddr += EEPROM_PAGESIZE;
pBuffer += EEPROM_PAGESIZE;
}
if (NumOfSingle!=0)
{
BSP_I2C_EE_Writepage(pBuffer, WriteAddr, NumOfSingle);
}
}
}
else
{
if (NumOfPage== 0)
{
BSP_I2C_EE_Writepage(pBuffer, WriteAddr, NumOfSingle);
}
else
{
NumByteToWrite -= count;
NumOfPage = (uint8_t)(NumByteToWrite / EEPROM_PAGESIZE);
NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;
if (count != 0)
{
BSP_I2C_EE_Writepage(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
}
while (NumOfPage--)
{
BSP_I2C_EE_Writepage(pBuffer, WriteAddr, EEPROM_PAGESIZE);
WriteAddr += EEPROM_PAGESIZE;
pBuffer += EEPROM_PAGESIZE;
}
if (NumOfSingle != 0)
{
BSP_I2C_EE_Writepage(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
主要分为两种情况,写的地址正好是一页的开始,另外一种是在一页的中间。不管如何,始终遵循的原则就是最大智能写一页,可以从一页的中间开始。
6.2.4 读字节
读操作允许主器件对寄存器的任意字节进行读操作,主器件首先通过发送起始信号、从器件地址和它想读取的字节数据的地址执行一个写操作。在AT24Cxx应答之后,主器件重新发送起始信号和从器件地址,此时R/W位置1,AT24Cxx响应并发送应答信号,然后输出所要求的一个8位字节数据,主器件不发送应答信号但产生一个停止信号。

Figure 6‑6 读字节
读取字节的时序如下:
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)停止总线。
6.2.5 顺序读
在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 6‑7 顺序读时序
我们常用的方式就是连续读取,代码很简单。
void BSP_I2C_EE_BufferRead(unsigned char* ptr_read,unsigned char address,unsigned char byte)
{
unsigned char send_buffer[2] = {};
send_buffer[0] = address;
R_SCI_I2C_Write(&g_i2c3_ctrl, &send_buffer[0], 1, true);
while ((I2C_MASTER_EVENT_TX_COMPLETE != i2c_event) && timeout_ms)
{
R_BSP_SoftwareDelay(400U, BSP_DELAY_UNITS_MICROSECONDS);
timeout_ms--;
}
timeout_ms = 500;
R_BSP_SoftwareDelay(250U, BSP_DELAY_UNITS_MICROSECONDS);
R_SCI_I2C_Read(&g_i2c3_ctrl, ptr_read, byte, false);
}
最后看下hal_entry()函数吧。
void hal_entry(void)
{
R_SCI_UART_Open (g_uart9.p_ctrl, g_uart9.p_cfg);
R_SCI_I2C_Open(&g_i2c3_ctrl, &g_i2c3_cfg);
BSP_I2C_Test();
while(1)
{
R_BSP_SoftwareDelay(500, BSP_DELAY_UNITS_MILLISECONDS);
}
#if BSP_TZ_SECURE_BUILD
R_BSP_NonSecureEnter();
#endif
}
首先对I2C初始化,然后就进行EEPROM的读写测试。
BSP_I2C_Test()函数如下:
uint8_t BSP_I2C_Test(void)
{
uint8_t i;
uint8_t DATA_Size = 16;
uint8_t I2c_Buf_Write[32] = {};
uint8_t I2c_Buf_Read[32] = {};
printf("Write data:\r\n");
for ( i=0; i<DATA_Size; i++ )
{
I2c_Buf_Write[i] = i;
printf("0x%02X ", I2c_Buf_Write[i]);
if (i%16 == 15)
{
printf("\r\n");
}
}
BSP_I2C_EE_BufferWrite( I2c_Buf_Write, 0x00 , DATA_Size);
printf("\r\nRead data:\r\n");
R_BSP_SoftwareDelay(10U, BSP_DELAY_UNITS_MILLISECONDS);
BSP_I2C_EE_BufferRead(I2c_Buf_Read, 0x00 , DATA_Size);
R_BSP_SoftwareDelay(10U, BSP_DELAY_UNITS_MILLISECONDS);
for (i=0; i<DATA_Size; i++)
{
if (I2c_Buf_Read[i] != I2c_Buf_Write[i])
{
printf("0x%02X \r\n", I2c_Buf_Read[i]);
printf("error : the data written and read are inconsistent\r\n");
printf("%d\n",i);
return 0;
}
printf("0x%02X ", I2c_Buf_Read[i]);
if (i%16 == 15)
{
printf("\r\n");
}
}
printf("\r\nI2C(AT24C02)Read and write test succeeded \r\n");
return 1;
}
6.3 实现现象
接上AT24C02模块,打开串口助手,打印信息如下:

Figure 6‑8 实验现象
从打印信息可以看出,I2C读写AT24C02成功。