AS32X601的I2C模块操作EEPROM详解

国科安芯推出的AS32X601系列MCU芯片内置的I2C模块提供了符合工业标准的两线串行制接口,可用于MCU和外部I2C设备的通讯。I2C总线使用两条串行线:串行数据线SDA和串行时钟线SCL。 I2C接口模块实现了I2C协议的标准模式和快速模式,支持多主机I2C总线架构。其标准模式为100K,快速模式400K。而EEPROM,作为一种支持字节级单独擦写、数据掉电不丢失的存储器,其存储容量(从几字节到数百千字节)恰好满足了大量嵌入式应用对中小规模非易失性数据存储的需求。将EEPROM与并行地址/数据总线相连的传统方式会占用大量I/O口,在引脚资源紧张的微控制器(如众多8位、32位MCU)上显得笨重且不经济,因此,AS32X601系列开发板搭载了一块24C02 eeprom。本文旨在系统阐述I2C EEPROM的工作原理与核心操作流程。内容将涵盖I2C通信的基本框架,EEPROM的器件寻址方式,以及针对字节写入等关键流程

一、硬件设计

二、I2C时序

①Start开始信号、Stop停止信号:

这两个信号由主机产生,不属于数据域交互:

在SCL的高电平时,主机将SDA的电平由 高–>低是Start信号(下降沿);

在SCL的高电平时,主机将SDA的电平由 低–>高是Stop信号(上升沿);

②7位寻址

AS32X601的I2C只支持7位寻址模式,配置过程中从机地址需要左移1位才为实际地址。

③数据方向

0写/1读

④应答ACK、非应答NACK

在SCL的一个时钟周期内,从机在SCL的高电平时,将SDA的电平由高拉低(或者继续保持低电平状态) 则是ACK信号;

从机在SCL的高电平时,如果SDA的电平一直是 高电平 则是NACK信号;

三、时钟

I2C0、I2C1时钟来自APB0,I2C2、I2C3时钟来自ABP1。具体配置可见I2C_CTLR寄存器。

四、I2C初始化

1.配置I2Cx需要的GPIO为复用功能。

2.通过配置I2C_INITSTRUCT初始化I2Cx,包括时钟分频,从机地址,ACK,高低电平时间等

3.按需求配置中断,并配置IRQ_HANDLER;

4.调用收发接口,并处理数据

五、如何操作EEPROM

5.1按字节写入函数

FlagStatus I2C_MEEPROMWriteByte(I2C_TypeDef* I2Cx, uint8_t addr, uint16_t reg, uint16_t data, uint32_t timeout)

{

unsigned int num;

/*等待总线释放*/

while (!I2C_CheckStatus(I2Cx, I2C_BUS_IDLE))

{

I2C_StartClear(I2Cx);

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

if ((timeout--) == 0)

{

return RESET;

}

delay_ms(1);

}

I2C_GenerateStart(I2Cx);

/*等待启动信号完成*/

while (!I2C_CheckStatus(I2Cx, MASTER_START_READY))

{

if ((timeout--) == 0)

{

return RESET;

}

delay_ms(1);

}

I2C_Send7bitAddress(I2Cx, addr, I2C_WRITE);

I2C_StartClear(I2Cx);

I2C_ClearITPendingBit(I2Cx);

/*等待从机接收完成地址并发送ack*/

while (!I2C_CheckStatus(I2Cx, MSEND_WADDR_ACK))

{

if ((timeout--) == RESET)

{

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

I2C_SendData(I2Cx, (uint8_t)(reg >> 0));

I2C_ClearITPendingBit(I2Cx);

/*等待从机接收完成数据并发送ack*/

while (!I2C_CheckStatus(I2Cx, MSEND_DATA_ACK))

{

if ((timeout--) == 0)

{

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

I2C_SendData(I2Cx, data);

I2C_ClearITPendingBit(I2Cx);

/*等待从机接收完成数据并发送ack*/

while (!I2C_CheckStatus(I2Cx, MSEND_DATA_ACK))

{

if ((timeout--) == 0)

{

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return 1;

}

代码执行流程详细解释如下:

等待总线空闲:函数首先进入一个循环,反复检查I2C总线是否处于空闲(I2C_BUS_IDLE)状态。如果总线被占用(忙状态),它会尝试通过调用I2C_StartClear和I2C_GenerateStop来清除可能的异常状态并发送停止信号,试图释放总线。每次循环都会递减超时计数器timeout并延迟1毫秒。如果timeout减到0,函数会返回RESET。这个步骤确保了本次传输开始时总线是可用的。

发起起始条件:确认总线空闲后,函数调用I2C_GenerateStart在I2C总线上产生一个起始条件(Start Condition),这标志着一次传输序列的开始。

等待起始条件完成:紧接着,函数进入另一个循环,等待起始条件成功发出的状态(MASTER_START_READY)。同样,这里也有超时检查和1ms延迟,防止程序死锁。超时则返回失败。

发送从机地址(写模式):起始条件成功后,函数调用I2C_Send7bitAddress,将参数addr(EEPROM的7位设备地址)和写操作位(I2C_WRITE,通常值为0)组合成一个8位字节发送出去。随后清除相关状态和中断标志。

等待从机地址应答:函数循环等待从设备(EEPROM)对收到地址的应答信号(MSEND_WADDR_ACK)。如果EEPROM存在于总线上并识别出自己的地址,它会拉低SDA线作为应答(ACK)。函数检测到这个状态才能继续。此处有一个代码瑕疵:超时判断写成了(timeout--) == RESET,虽然RESET很可能定义为0,但不如其他地方的== 0直观统一。超时或失败会发送停止条件并返回失败。

发送EEPROM内部存储地址(存在严重错误):地址应答后,函数准备发送要写入的EEPROM内部单元地址reg。这是一个关键错误。对于16位地址的EEPROM(如reg是uint16_t),需要发送两个字节:先发送高8位,再发送低8位。但代码中I2C_SendData(I2Cx, (uint8_t)(reg >> 0))的reg >> 0等于reg本身,所以它只发送了reg的低8位,完全遗漏了高8位。这会导致写入到错误的EEPROM位置。

等待内部地址字节应答:发送(不完整的)地址字节后,循环等待EEPROM对此数据字节的应答(MSEND_DATA_ACK)。有超时处理。

发送要写入的数据:收到地址字节应答后,调用I2C_SendData(I2Cx, data)发送数据。这里有一个潜在问题:参数data是uint16_t类型,但函数被命名为WriteByte,且I2C_SendData通常发送一个字节。这里发生了隐式截断,只有data的低8位被发送出去。函数意图和参数类型不匹配。

等待数据字节应答:再次循环等待EEPROM对收到数据字节的应答。有超时处理。

结束传输:数据成功发送并得到应答后,函数调用I2C_GenerateStop产生停止条件(Stop Condition),结束本次I2C通信。然后清除中断标志。

5.2读函数

FlagStatus I2C_MEEPROMRead(I2C_TypeDef* I2Cx, uint8_t addr, uint16_t reg, uint8_t* pData, uint32_t Size, uint32_t timeout)

{

uint32_t num = 0x00;

/*等待总线释放*/

while (!I2C_CheckStatus(I2Cx, I2C_BUS_IDLE))

{

I2C_StartClear(I2Cx);

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

if ((timeout--) == 0)

{

return RESET;

}

delay_ms(1);

}

I2C_AcknowledgeConfig(I2Cx, I2C_IICAA_ACK);

I2C_GenerateStart(I2Cx);

/*等待启动信号完成*/

while (!I2C_CheckStatus(I2Cx, MASTER_START_READY))

{

if ((timeout--) == 0)

{

I2C_StartClear(I2Cx);

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

I2C_Send7bitAddress(I2Cx, addr, I2C_WRITE);

I2C_StartClear(I2Cx);

I2C_ClearITPendingBit(I2Cx);

/*等待从机接收完成地址并发送ack*/

while (!I2C_CheckStatus(I2Cx, MSEND_WADDR_ACK))

{

if ((timeout--) == 0)

{

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

I2C_SendData(I2Cx, (uint8_t)(reg >> 8));

I2C_ClearITPendingBit(I2Cx);

/*等待从机接收完成数据并发送ack*/

while (!I2C_CheckStatus(I2Cx, MSEND_DATA_ACK))

{

if ((timeout--) == 0)

{

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return 0;

}

delay_ms(1);

}

I2C_SendData(I2Cx, (uint8_t)(reg >> 0));

I2C_ClearITPendingBit(I2Cx);

/*等待从机接收完成数据并发送ack*/

while (!I2C_CheckStatus(I2Cx, MSEND_DATA_ACK))

{

if ((timeout--) == 0)

{

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

I2C_GenerateStart(I2Cx);

I2C_ClearITPendingBit(I2Cx);

/*等待从机接收完成数据并发送ack*/

while (!I2C_CheckStatus(I2Cx, MASTER_START_REPEAT))

{

if ((timeout--) == 0)

{

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

I2C_Send7bitAddress(I2Cx, addr, I2C_READ);

I2C_ClearITPendingBit(I2Cx);

/*等待从机接收完成地址并发送ack*/

while (!I2C_CheckStatus(I2Cx, MSEND_RADDR_ACK))

{

if ((timeout--) == 0)

{

I2C_StartClear(I2Cx);

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

for (num = 0; num < Size; num++)

{

if (num == (Size - 1))

{

/* IIC sends NACK */

I2C_AcknowledgeConfig(I2Cx, I2C_IICAA_NACK);

}

else

{

I2C_AcknowledgeConfig(I2Cx, I2C_IICAA_ACK);

}

I2C_StartClear(I2Cx);

I2C_ClearITPendingBit(I2Cx);

/* Wait for the slave to send the completed data, and the host will send an ack */

while (!(I2C_CheckStatus(I2Cx, MREAD_DATA_ACK) || I2C_CheckStatus(I2Cx, MREAD_DATA_NACK)))

{

if ((Timeout--) == 0)

{

I2C_StartClear(I2Cx);

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return RESET;

}

delay_ms(1);

}

*pData++ = I2C_ReceiveData(I2Cx);

}

I2C_StartClear(I2Cx);

I2C_GenerateStop(I2Cx);

I2C_ClearITPendingBit(I2Cx);

return SET;

}

代码执行流程详细解释如下:

函数参数说明:

I2Cx: I2C外设指针

addr: EEPROM设备地址(7位)

reg: EEPROM内部起始地址(16位)

pData: 指向接收数据缓冲区的指针

Size: 要读取的字节数

timeout: 超时计数值(注意:函数内部有一处拼写错误写成了Timeout)

代码执行流程详细解释:

等待总线空闲:函数首先检查I2C总线是否空闲(I2C_BUS_IDLE)。如果总线忙,执行清理操作(I2C_StartClear)并发送停止信号(I2C_GenerateStop),尝试释放总线。每次循环都递减超时计数器并延迟1ms,超时则返回RESET。

配置应答:调用I2C_AcknowledgeConfig(I2Cx, I2C_IICAA_ACK)使能主设备的数据应答功能,这是为后续接收数据做准备。

发起起始条件:生成起始条件(I2C_GenerateStart)开始传输,并等待起始条件成功(MASTER_START_READY)。超时则清理总线并返回失败。

发送设备地址(写模式):发送EEPROM的7位地址和写方向位(I2C_WRITE),因为EEPROM读取操作需要先发送要读取的内部地址,这相当于一个"伪写"操作。清除相关状态后,等待EEPROM应答地址(MSEND_WADDR_ACK)。

发送重复起始条件:为了从写操作切换到读操作,需要发送一个重复起始条件(Repeated Start)。调用I2C_GenerateStart,然后等待重复起始条件完成(MASTER_START_REPEAT)。这是I2C协议中在不释放总线的情况下改变数据传输方向的标准做法。

发送设备地址(读模式):再次发送EEPROM的7位地址,但这次带读方向位(I2C_READ)。等待EEPROM对此读地址的应答(MSEND_RADDR_ACK)。

循环接收数据:这是函数的核心部分,循环接收Size个字节的数据:

在接收倒数第二个字节时(num == (Size - 1)),将主设备的应答配置为不应答(I2C_IICAA_NACK),这是I2C协议规定的:主设备在接收最后一个字节前发送不应答信号,通知从设备停止发送。

对于其他字节,使能应答(I2C_IICAA_ACK)。

等待从设备发送数据完成的状态(MREAD_DATA_ACK或MREAD_DATA_NACK)。这里使用了逻辑或||,表示等待任意一种接收完成状态。

从I2C数据寄存器读取数据(I2C_ReceiveData(I2Cx))并存储到pData指向的缓冲区,然后指针递增。

结束传输:所有数据接收完成后,生成停止条件(I2C_GenerateStop)结束本次I2C通信,清除相关状态。

六、下板验证

我们操作I2C写入0~0x3f数据,结果如下:

操作波形如图:

读取完最后一个数据后发送NACK:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值