打印
[研电赛技术支持]

通信模拟 I2C

[复制链接]
4628|65
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
janewood|  楼主 | 2023-11-22 20:00 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
I²C的传输协议,如果是软件模拟I²C,需要依次实现每个步骤。因此,还需要知道每一步的具体细节,比如什么时候的数据有效,开始信号怎么表示。

数据有效性

I²C由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。SDA线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。数据有效性示意图如图 19.1.2 所示.。





开始信号和结束信号

I²C起始信号(S):当SCL高电平时,SDA由高电平向低电平转换;

I²C停止信号(P):当SCL高电平时,SDA由低电平向高电平转换;





应答信号

I²C每次传输的8位数据,每次传输后需要从机反馈一个应答位,以确认从机是否正常接收了数据。当主机发送了8位数据后,会再产生一个时钟,此时主机放开SDA的控制,读取SDA电平,在上拉电阻的影响下,此时SDA默认为高,必须从机拉低,以确认收到数据。





完整传输流程

I²C完整传输流程如下:

① SDA和SCL开始都为高,然后主机将SDA拉低,表示开始信号;

② 在接下来的8个时间周期里,主机控制SDA的高低,发送从机地址。其中第8位如果为0,表示接下来是写操作,即主机传输数据给从机;如果为1,表示接下来是读操作,即从机传输数据给主机;另外,数据传输是从最高位到最低位,因此传输方式为MSB(Most Significant Bit)。

③ 总线中对应从机地址的设备,发出应答信号;

④ 在接下来的8个时间周期里,如果是写操作,则主机控制SDA的高低;如果是读操作,则从机控制SDA的高低;

⑤ 每次传输完成,接收数据的设备,都发出应答信号; ⑥ 最后,在SCL为高时,主机由低拉高SDA,表示停止信号,整个传输结束;



19.1.2 EEPROM 介绍
EEPROM的全称是“电可擦除可编程只读存储器”,即Electrically Erasable Programmable Read-Only Memory。通常用于存放用户配置信息数据,比如在开发板首次运行时,需要屏幕校准,校准后的配置信息就可以保存在EEPROM里,开发板断电后配置信息不丢失,下次启动,开发板自动读取EEPROM的校准配置信息,就不需要重新校准。

EEPROM和Flash的本质上是一样的,Flash包括MCU内部的Flash和外部扩展的Flash,本开发板就有一个SPI接口的外部Flash(W25Q64),在后面SPI接口再讲解。从功能上,Flash通常存放运行代码,运行过程中不会修改,而EEPROM存放用户数据,可能会反复修改。从结构上,Flash按扇区操作,EEPROM通常按字节操作。两者区别这里不再过多赘述,读者理解EEPROM在嵌入式中扮演的角色即可。

结构组成
EEPROM类型众多,其中比较常见是AT24Cxx系列,从命名上看,AT24Cxx中xx的单位是K Bit,如AT24C08,其存储容量为8K Bit。本开发板上的EEPROM型号为AT24C02,其存储容量为2K Bit,2*1024=2048Bit。

对于AT24C01/02,每页大小为8 Byte,对于AT24C04/08/16,每页大小为16 Byte。如图 19.1.6 所示,AT24C02由32页(Page)组成,每一页由8个字节(Byte)组成,每个Byte由8位(Bit)组成,Bit为最小存储单位,存放1个0或1。





设备地址

I²C设备都会有一个设备地址,不同容量的AT24C02,设备地址定义会有所差异,由芯片数据手册《AT24Cxx.pdf》可知,如图 19.1.7 所示。





AT24C02的容量为2K,对应上图中的第一行,高四位固定为“1010”,中间三位由A2、A1、A0引脚的电平决定,比如A2~0引脚全接地,则值为“000”,最后的最低位为读写位,0代表写命令,1代表读命令。

A2、A1、A0引脚电平需要由原理图决定,假设全接电源地,则如果需要向AT24C02写数据,则发送地址“1010 0000”,如果需要向AT24C02读数据,则发送地址“1010 0001”。

假设开发板有多个AT24C02挂在同一I²C总线上,通过这个规则,只需设计电路时,让A2、A1、A0引脚电平不同,即可区分两个AT24C02。

对于容量再大一点的AT24Cxx系列,比如AT24C04,器件地址由A2、A1引脚决定,数据空间有P0决定。比如对AT24C04的0-2K空间操作,则P0为0,对2K-4K空间操作,则P0为1。

写AT24Cxx
AT24Cxx支持字节写模式和页写模式。字节写模式是一个地址一个数据的写;页写模式是连续写数据,一个地址多个数据的写,但是页写模式不能自动跨页,如果超出一页长度,超出的数据会覆盖原先写入的数据。

如图 19.1.8 所示,为AT24Cxx字节写模式的时序,在MCU发出开始信号(Start)后,发出8 Bit的设备地址信息(图中读写位为低电平,即写数据),待收到AT24Cxx应答信号后,再发出要写的数据地址,再次等待AT24Cxx应答,最后发出8 Bit数据写数据,待AT24Cxx应答后,发出停止信号(Stop),完成一次单字节写数据。





AT24C02容量为2K,因此数据地址范围为0x000xFF,即0255,每个数据地址每次写1Byte,即8bit,也就刚好2568=2048Bit。对于1K容量的产品,数据地址范围为0x00~0x7F,最高位不会用到,因此图中数据地址的最高位为“”,意思是对于1K容量的产品,该位无需关心。





图 19.1.10 为AT24Cxx的页写模式时序,与字节写模式的差异在于,不是只发送1Byte数据,而是任意多个。需要注意,该模式不能跨页写,遇到跨页时,需要重新发送完整的时序。





值得一提的是,《AT24Cxx.pdf》里提到每次写完之后,再到下次写之前,需要间隔5ms时间,以确保上次写操作在芯片内部完成,如图 19.1.11 所示。





读AT24Cxx

AT24Cxx支持当前地址读模式、随机地址读模式和顺序读模式。当前地址读模式就是在上一次读/写操作之后的最后位置,继续读出数据,比如上次读/写在地址n,接下来可以直接从n+1处读出数据;随机地址读模式是指定数据地址,然后读出数据;顺序读模式是连续读出多个数据。在当前地址读模式下,无需发送数据地址,数据地址为上一次读/写操作之后的位置,时序如图 19.1.12所示,注意在结尾,主机接收数据后,无需产生应答信号。





在当前地址读模式下,需要先发送设备地址,待读的数据地址,接着再重新发出开始信号,设备地址,读出数据,时序如图 19.1.13 所示。





在顺序读模式下,需要先从当前地址读模式或随机地址读模式启动,随后便可连续读多个数据,时序如图 19.1.14 所示



19.2 硬件设计
如图 19.2.1 为开发板EEPROM部分的原理图,U4为AT24C02芯片,它的A0、A1、A2都接地,因此该设备地址为“1010 000X”,当读该设备时,X为1,写该设备时,X为0。

U4的7脚为写保护引脚(Write Protect,WP),当该引脚为高,则禁止写AT24C02,这里直接拉低WP,任何时候都可直接写AT24C02。

此外,I2C的两个脚SCL和SDA都进行了上拉处理,从而保证I2C总线空闲时,两根线都必须为高电平。如果没有上拉,在主机发送完数据后,放开SDA,此时SDA的电平状态不确定,可能为高,也可能为低,无法确定是从机拉低给出应答信号。

结合原理图可知,PB6作为了I2C1的SCL,PB7作为了I2C1的SDA。



19.3 软件设计
19.3.1 软件设计思路
实验目的:本实验通过GPIO模拟I2C总线时序,对EEPROM设备AT24C02进行读写操作。

引脚初始化:GPIO端口时钟使能、GPIO引脚设置为输入/输出模式(PB6、PB7);
封装I2C每个环节的时序函数:起始信号、响应信号、读写数据、停止信号;
使用I2C协议函数,实现对AT24C02的读写;
主函数,每按一次按键,写一次AT24C02,接着读出来验证是否和写的数据一致;
本实验配套代码位于“5_程序源码\11_通信—模拟I2C\”。
19.3.2 软件设计讲解
GPIO选择与接口定义
首先定义SCL和SDA引脚,引脚的高低电平宏定义,如代码段 19.3.1 所示。

代码段 19.3.1 模拟 I2C 引脚相关定义(driver_i2c.h)

/************************* I2C 硬件相关定义 *************************/
#define ACK (0)
#define NACK (1)
#define SCL_PIN GPIO_PIN_6
#define SCL_PORT GPIOB
#define SCL_PIN_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()
#define SDA_PIN GPIO_PIN_7
#define SDA_PORT GPIOB
#define SDA_PIN_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()
#define SCL_H() HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET)
#define SCL_L() HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_RESET)
#define SCL_INPUT() HAL_GPIO_ReadPin(SCL_PORT, SCL_PIN)
#define SDA_H() HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET)
#define SDA_L() HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_RESET)
#define SDA_INPUT() HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN)

接着将两个GPIO引脚初始化,使能引脚时钟,先默认设置为输出模式。SCL引脚为时钟信号,始终为输出模式,SDA引脚为数据引脚,可能输出或者输入,因此还需要编写函数实现输入、输出的切换,如代码段19.3.2 所示。

代码段 19.3.2 I2C 引脚初始化(driver_i2c.c)

/*
* 函数名:void I2C_Init(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:初始化模拟 I2C 的引脚为输出状态且 SCL/SDA 都初始为高电平
*/
void I2C_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
SCL_PIN_CLK_EN();
SDA_PIN_CLK_EN();
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pin = SCL_PIN;
HAL_GPIO_Init(SCL_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = SDA_PIN;
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
SCL_H();
SDA_H(); }
/*
* 函数名:static void I2C_SDA_OUT(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:配置 SDA 引脚为输出
*/
static void I2C_SDA_OUT(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pin = SDA_PIN;
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct); }
/*
* 函数名:static void I2C_SDA_IN(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:配置 SDA 引脚为输入
*/
static void I2C_SDA_IN(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pin = SDA_PIN;
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct); }


I2C时序函数
开始信号/结束信号
参考前面图 19.1.3 所示的开始信号和结束信号编写程序。对于开始信号,首先将SDA和SCL都拉高,随后SDA拉低,再SCL拉低。对于结束信号,首先拉低SDA,拉高SCL,再拉低SDA,代码如代码段 19.3.3所示。

代码段 19.3.3 I2C 开始信号和结束信号(driver_i2c.c)

/*
* 函数名:void I2C_Start(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:I2C 开始信号
*/
void I2C_Start(void) {
I2C_SDA_OUT();
SCL_H();
I2C_Delay();
SDA_H();
I2C_Delay();
SDA_L();
I2C_Delay();
SCL_L();
I2C_Delay(); }
/*
* 函数名:void I2C_Stop(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:I2C 停止信号
*/
void I2C_Stop(void) {
I2C_SDA_OUT();
SDA_L();
I2C_Delay();
SCL_H();
I2C_Delay();
SDA_H();
I2C_Delay(); }

信号/非应答信号/等待应答信号
参考前面图 19.1.4 所示,编译应答信号,如代码段 19.3.4 所示。

代码段 19.3.4 应答/非应答/等待应答信号(driver_i2c.c)

/*
* 函数名:void I2C_ACK(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:I2C 发出应答信号
*/
void I2C_ACK(void) {
I2C_SDA_OUT();
SCL_L();
I2C_Delay();
SDA_L();
I2C_Delay();
SCL_H();
I2C_Delay();
SCL_L();
I2C_Delay(); }
/*
* 函数名:void I2C_NACK(void)
* 输入参数:
* 输出参数:无
* 返回值:无
* 函数作用:I2C 发出非应答信号
*/
void I2C_NACK(void) {
I2C_SDA_OUT();
SCL_L();
I2C_Delay();
SDA_H();
I2C_Delay();
SCL_H();
I2C_Delay();
SCL_L();
I2C_Delay(); }
/*
* 函数名:uint8_t I2C_GetACK(void)
* 输入参数:
* 输出参数:无
* 返回值:1 无应答,0 有应答
* 函数作用:I2C 等待从机的应答信号
*/
uint8_t I2C_GetACK(void) {
uint8_t time = 0;
I2C_SDA_IN();
SCL_L();
I2C_Delay();
SDA_H();
I2C_Delay();
SCL_H();
I2C_Delay();
while(SDA_INPUT()) {
time++;
if(time>250) {
SCL_L();
return 1; } }
SCL_L();
return 0; }

8~23行:应答信号,在一个SDA时钟周期里,将SCL拉低;
32~47行:非应答信号,在一个SDA时钟周期里,将SCL拉高;
56~82行:等待应答信号,拉高SDA后放开SDA,读取SDA是否被拉低,如果拉低返回0,否则返回1;
发送/接收函数
最后还剩发送/接收函数,如代码段 19.3.5 所示。对于发送函数,控制SDA产生8个时钟周期,每个时钟周期里控制SDA高低电平发送1位数据。对于接收函数,控制SDA产生8个时钟周期,每个时钟周期里读取SDA高低电平接收1位数据。

代码段 19.3.5 发送/接收函数(driver_i2c.c)

/*
* 函数名:void I2C_SendByte(uint8_t data)
* 输入参数:data->发送的数据
* 输出参数:无
* 返回值:无
* 函数作用:I2C 发送一个字节
*/
void I2C_SendByte(uint8_t data)
{
uint8_t cnt = 0;
I2C_SDA_OUT();
for(cnt=0; cnt<8; cnt++) {
SCL_L();
I2C_Delay();
if(data & 0x80) {
SDA_H(); }
else
{
SDA_L(); }
data = data<<1;
SCL_H();
I2C_Delay(); }
SCL_L();
I2C_Delay();
I2C_GetACK(); }
/*
* 函数名:uint8_t I2C_ReadByte(uint8_t ack)
* 输入参数:ack->发送的应答标志,1 应答,0 非应答
* 输出参数:无
* 返回值:返回读到的字节
* 函数作用:I2C 读出一个字节
*/
uint8_t I2C_ReadByte(uint8_t ack)
{
uint8_t cnt;
uint8_t data = 0xFF;
SCL_L();
I2C_Delay();
for(cnt=0; cnt<8; cnt++) {
SCL_H(); //SCL 高(读取数据)
I2C_Delay();
data <<= 1;
if(SDA_INPUT()) {
data |= 0x01; //SDA 高(数据为 1) }
SCL_L();
I2C_Delay(); }
//发送应答信号,为低代表应答,高代表非应答
if(ack == 0) {
I2C_ACK(); }
else
{
I2C_NACK(); }
return data; //返回数据
}

14~31行:循环8次,每次循环:
– 16行:先拉低SCL;
– 19~26行:将输入的数据data与0x08且运算,得到最高位的值,从而控制SDA输出对应的高、低电平;
– 27行:将data左移一位,得到次高位;
– 29行:拉高SCL,让SDA处于稳定期,从设备即可获取SDA的值;
35行:等待从设备的应答信号;
53~65行:循环8次,每次循环:
– 55行:先拉高SCL,此时认为从设备控制SDA电平,处于稳定期;
– 58行:将data左移1位,以确保收到数据按最高位在前存放;
– 59~62行:读取SDA电平,如果为高,保存到data当前最低位,否则data最低位默认为0;
– 63行:SCL拉低,此时从设备继续控制SDA电平变化
66~74行:根据传入的参数,决定是否发送应答信号;
整个I2C协议函数中,经常用到“ I2C_Delay()”来实现SCL时钟周期。对于AT24Cxx,由其芯片手册可知,时钟脉冲宽度(Clock Pulse Width)需要大于5us,也就是SCL如果刚变为高电平,需要等待至少5us才能变为低电平,因此定义“ I2C_Delay()”为5us以上即可。

#define I2C_Delay() us_timer_delay(5) // Clock Pulse Width >5us
1.
这里的“us_timer_delay()”可以由定时器提供,也可以使用循环提供,前者精度更高,效果更好。定时器的介绍在后面章节,本章不作分析,延时函数的两者方式如代码段 19.3.6 所示。

代码段 19.3.6 延时函数的实现(driver_timer.c)

#if 0
/*
* 函数名:void us_timer_delay(uint16_t t)
* 输入参数:t-延时时间 us
* 输出参数:无
* 返回值:无
* 函数作用:定时器实现的延时函数,延时时间为 t us,为了缩短时间,函数体使用寄存器操作,用户可对照手册查看每个寄存器每一位的意义
*/
void us_timer_delay(uint16_t t)
{
uint16_t counter = 0;
__HAL_TIM_SET_AUTORELOAD(&htim, t);
__HAL_TIM_SET_COUNTER(&htim, counter);
HAL_TIM_Base_Start(&htim);
while(counter != t)
{
counter = __HAL_TIM_GET_COUNTER(&htim);
}
HAL_TIM_Base_Stop(&htim);
}
#else
/*
* 函数名:void us_timer_delay(uint16_t t)
* 输入参数:t-延时时间 us
* 输出参数:无
* 返回值:无
* 函数作用:延时粗略实现的延时函数,延时时间为 t us
*/
void us_timer_delay(uint16_t t)
{
uint16_t counter = 0;
while(t--) {
counter=10;
while(counter--) ; } }
#endif

AT24C02读写函数
编写好I2C协议函数后,参考AT24C02手册编写读写数据函数,如代码段 19.3.7 所示。

代码段 19.3.7 读写 AT24C02 一字节数据(driver_eeprom.c)

/*
* 函数名:uint8_t EEPROM_WriteByte(uint16_t addr, uint8_t data)
* 输入参数:addr -> 写一个字节的 EEPROM 初始地址
* data -> 要写的数据
* 输出参数:无
* 返回值:无
* 函数作用:EEPROM 写一个字节
*/
void EEPROM_WriteByte(uint16_t addr, uint8_t data)
{
/* 1. Start */
I2C_Start();
/* 2. Write Device Address */
I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_WR );
/* 3. Data Address */
if(EEPROM_WORD_ADDR_SIZE==0x08) {
I2C_SendByte( (uint8_t)(addr & 0x00FF) ); }
else
{
I2C_SendByte( (uint8_t)(addr>>8) );
I2C_SendByte( (uint8_t)(addr & 0x00FF) ); }
/* 4. Write a byte */
I2C_SendByte(data);
/* 5. Stop */
I2C_Stop(); }
/*
* 函数名:uint8_t EEPROM_ReadByte(uint16_t addr, uint8_t *pdata)
* 输入参数:addr -> 读一个字节的 EEPROM 初始地址
* data -> 要读的数据指针
* 输出参数:无
* 返回值:无
* 函数作用:EEPROM 读一个字节
*/
void EEPROM_ReadByte(uint16_t addr, uint8_t *pdata)
{
/* 1. Start */
I2C_Start();
/* 2. Write Device Address */
I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_WR );
/* 3. Data Address */
if(EEPROM_WORD_ADDR_SIZE==0x08) {
I2C_SendByte( (uint8_t)(addr & 0x00FF) ); }
else
{
I2C_SendByte( (uint8_t)(addr>>8) );
I2C_SendByte( (uint8_t)(addr & 0x00FF) ); }
/* 4. Start Again */
I2C_Start();
/* 5. Write Device Address Read */
I2C_SendByte( EEPROM_DEV_ADDR | EEPROM_RD );
/* 6.Read a byte */
*pdata = I2C_ReadByte(NACK);
/* 7. Stop */
I2C_Stop(); }

参加前面图 19.2.1 和图 19.1.13 所示的介绍时序,编写AT24C02一字节读写程序。

9~33行:写AT24C02一字节数据;
– 12行:发送I2C开始信号;
– 15行:发送AT24C02的设备地址,最后一位表示写操作;
– 18~26行:根据EEPROM型号,调用不同的数据地址长度设置函数(AT24C01/02为8位,AT24C04/08/16
为16位);
– 29行:发送数据;
– 32行:发送I2C停止信号;
43~73行:读AT24C02一字节数据;
– 46行:发送I2C开始信号;
– 49行:发送AT24C02的设备地址,最后一位表示写操作(接下来要写数据地址);
– 52~60行:根据EEPROM型号,调用不同的数据地址长度设置函数(AT24C01/02为8位,AT24C04/08/16
为16位);
– 63行:再次发送I2C开始信号;
– 66行:发送AT24C02的设备地址,最后一位表示读操作;
– 69行:读取AT24C02数据,且无需ACK;
– 72行:发送I2C停止信号;
实现了对AT24C02单字节的读写,还需要实现多字节的读写。多字节读写可以通过AT24Cxx的页写模式和顺序读模式,实现多个数据的连续读写。在页写模式时,需要程序上设置,不能跨页写,这里简单处理,直接多次调用前面的单次读写即可,如代码段 19.3.8 所示。

代码段 19.3.8 读写 AT24C02 多字节数据(driver_eeprom.c)

/*
* 函数名:void EEPROM_Write_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
* 输入参数:addr -> 写一个字节的 EEPROM 初始地址
* data -> 要写的数据指针
* sz -> 要写的字节个数
* 输出参数:无
* 返回值:无
* 函数作用:EEPROM 写 N 个字节
*/
void EEPROM_Write_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
{
uint16_t i = 0;
for(i=0; i<sz; i++)

EEPROM_WriteByte(addr, pdata[i]);
addr++;
HAL_Delay(10); // Write Cycle Time 5ms
} }
/*
* 函数名:void EEPROM_Read_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
* 输入参数:addr -> 读一个字节的 EEPROM 初始地址
* data -> 要读的数据指针
* sz -> 要读的字节个数
* 输出参数:无
* 返回值:无
* 函数作用:EEPROM 读 N 个字节
*/
void EEPROM_Read_NBytes(uint16_t addr, uint8_t *pdata, uint16_t sz)
{
uint16_t i = 0;
for(i=0; i<sz; i++) {
EEPROM_ReadByte(addr, &pdata[i]);
addr++; } }

需要注意的是,AT24Cxx每次写操作后,有一个写间隔,需要间隔5ms以上,因此在写多个字节时,每次写完都需要延时5ms以上。

主函数控制逻辑
在主函数里,每按一下按键,调用“EEPROM_Write_Nbytes()”对AT24C02写一串数据,再调用“EEPROM_Read_Nbytes()”读出该数据,如代码段 19.3.9 所示。

代码段 19.3.9 主函数控制逻辑(main.c)

// 初始化 I2C
I2C_Init();
while(1) {
if(key_flag) // 按键按下
{
key_flag = 0;
printf("\n\r");
printf("Start write and read eeprom.\n\r");
// 读写一串字符,并打印
EEPROM_Write_NBytes(0, tx_buffer, sizeof(tx_buffer)); // 写数据
HAL_Delay(1);
EEPROM_Read_NBytes(0, rx_buffer, sizeof(tx_buffer)); // 读数据
HAL_Delay(1);
printf("EEPROM Write: %s\n\r", tx_buffer);
printf("EEPROM Read : %s\n\r", rx_buffer);
memset((uint8_t*)rx_buffer, 0, sizeof(rx_buffer)); // 清空接收的数据
} }

https://blog.51cto.com/weidongshan/6609372

使用特权

评论回复
沙发
tpgf| | 2024-1-2 14:11 | 只看该作者
模拟之后的iic的通讯速度会降低多少呢

使用特权

评论回复
板凳
nawu| | 2024-1-2 14:40 | 只看该作者
一般来说是固有的运行稳定 还是模拟的运行稳定呢

使用特权

评论回复
地板
gwsan| | 2024-1-2 15:33 | 只看该作者
iic的通讯过程是不是已经非常的简单可靠了啊

使用特权

评论回复
5
tfqi| | 2024-1-2 16:14 | 只看该作者
为什么现在的外部存储芯片大部分都是iic或者spi通讯方式的呢

使用特权

评论回复
6
zljiu| | 2024-1-2 23:04 | 只看该作者
这个只要完全按照iic的时序动作就可以了

使用特权

评论回复
7
aoyi| | 2024-1-2 23:36 | 只看该作者
模拟iic需要吧引脚配置成什么模拟呢

使用特权

评论回复
8
pentruman| | 2024-1-4 17:42 | 只看该作者
需要初始化I2C通信的相关参数,如时钟速度、设备地址等。

使用特权

评论回复
9
sheflynn| | 2024-1-4 17:53 | 只看该作者
这种方法通常用于在没有I2C控制器或需要移植I2C协议到其他微控制器或处理器的情况下。

使用特权

评论回复
10
nomomy| | 2024-1-4 18:24 | 只看该作者
模拟I2C的实现方式有多种,可以使用GPIO(通用输入输出)口模拟I2C时序,也可以使用专门的I2C模拟库或驱动程序。使用GPIO模拟时序需要手动控制SDA和SCL线的电平变化,而使用库或驱动程序可以简化实现过程。

使用特权

评论回复
11
earlmax| | 2024-1-5 09:44 | 只看该作者
许多现代MCU内置了I2C控制器,可以方便地实现I2C通信。但是,对于那些没有内置I2C控制器的MCU,可以通过软件模拟I2C时序来实现通信。

使用特权

评论回复
12
timfordlare| | 2024-1-5 09:59 | 只看该作者
I2C通信协议允许多个主控制器设备存在于同一通信系统中,这使得通信更加灵活。

使用特权

评论回复
13
jackcat| | 2024-1-5 16:28 | 只看该作者
在模拟I2C通信过程中,发送方需要将数据一位一位地串行发送到SDA线上,并通过SCL线提供时钟信号。接收方则需要根据SCL时钟信号从SDA线上接收数据。

使用特权

评论回复
14
fengm| | 2024-1-5 16:45 | 只看该作者
为了实现多主节点通信,需要软件来处理仲裁机制,确保在多个主节点同时尝试通信时,只有一个节点能够成功发送数据。

使用特权

评论回复
15
backlugin| | 2024-1-5 17:01 | 只看该作者
在某些情况下,当硬件I2C资源不足或调试不便时,可以使用模拟I2C作为替代方案。

使用特权

评论回复
16
elsaflower| | 2024-1-5 17:13 | 只看该作者
硬件I2C控制器还可以提供额外的功能和优化,例如数据校验、地址过滤等。

使用特权

评论回复
17
10299823| | 2024-1-5 17:52 | 只看该作者
要使用GPIO模拟I2C,开发者需要按照I2C协议规范控制GPIO引脚的高低电平变化来模拟

使用特权

评论回复
18
earlmax| | 2024-1-5 18:05 | 只看该作者
在开始一次I2C通信时,需要发送一个起始条件,即SDA线从高电平跳变到低电平,SCL线保持高电平。

使用特权

评论回复
19
benjaminka| | 2024-1-5 18:24 | 只看该作者
I2C通信协议基础
空闲状态:当SDA和SCL都处于高电平时,总线被认为是空闲的。
起始信号:当SCL为高电平时,SDA从高到低的变化表示开始信号。
停止信号:当SCL为低电平时,SDA从低到高的变化表示停止信号。
应答信号:在数据传输过程中,接收方会在SCL为高电平时,通过将SDA拉低一个时钟周期来应答。
数据有效性:数据在SCL的上升沿被采样,在下降沿被发送。

使用特权

评论回复
20
ccook11| | 2024-1-5 18:41 | 只看该作者
I2C通信协议只需要两根信号线,分别是数据线(SDA)和时钟线(SCL)。这使得连接非常简单,节省了硬件资源。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

58

主题

1276

帖子

1

粉丝