本帖最后由 DKENNY 于 2023-8-10 09:38 编辑
1. 背景
大多数嵌入式系统和单片机都提供了GPIO引脚,而I2C硬件接口可能并不总是可用或可用的GPIO引脚数量有限。通过使用GPIO模拟I2C,可以在没有硬件I2C接口的情况下实现I2C通信。使用软件IIC具有以下优点:
(1) 适应性和灵活性:在某些应用中,可能需要在不同的GPIO引脚上实现I2C通信,以适应特定的硬件布局或连接要求。使用GPIO模拟I2C可以提供更大的灵活性。
(2)自定义时序和速率:对于一些特殊需求的应用,可能需要自定义I2C通信的时序和速率。通过使用GPIO模拟I2C,可以通过软件控制时序和速率,以满足特定的需求。
(3)教育和学习目的:使用GPIO模拟I2C可以帮助学生和初学者更好地理解和学习I2C通信协议。通过手动控制GPIO引脚的状态和时序,他们可以更深入地了解I2C的工作原理。
(4)原型开发和快速迭代:在原型开发和快速迭代阶段,使用GPIO模拟I2C可以更快地验证和测试I2C设备的功能和交互。这样可以加快开发进度并节省成本。
总之,使用GPIO模拟I2C的背景主要涉及硬件限制、适应性、自定义需求、教育学习和原型开发等方面。通过使用GPIO模拟I2C,可以克服硬件限制并提供更大的灵活性和控制能力。
2. IIC简介
IIC是一种短距离通信协议,物理实现上,I2C总线由两根信号线(SDA与SCL)和一根地线组成,两根信号线为双向传输的。 两根信号线,SCL时钟线、SDA数据线。由SCL为SDA提供时序,SDA串行发送/接收数据。 SCL、SDA这两根信号线均为双向。 两个系统使用IIC总线通信时共用同一根地线。 如下图为IIC功能结构图。
2.1 IIC协议概述
数据以帧的形式传输,每一帧中由 1 个字节(8 位)组成。 在 SCL 的上升沿阶段,SDA 需要保持稳定,SDA 在 SCL 为低期间作出改变。 除了数据帧,I2C 总线还有起始信号,停止信号,应答信号。 起始位:在 SCL 为稳定的高电平期间,SDA 的一个下降沿启动传输。 停止位:在 SCL 为稳定的高电平期间,SDA 的一个上升沿停止传输。 应答位:用于表示一个字节传输成功。总线发送器(无论主机还是从机), 在发送 8 个位的数据后,SDA 将释放(由输出变为输入),在第九个时钟脉冲期间,接收器将 SDA 拉低,来应答接收到了数据。
I2C通信读写过程
如下为主机写数据至从机图。
如下为主机由从机读取数据图。
备注:(IIC发送8位命令,最后一位代表读写位) (1)
:此数据由主机传输到从机 (2) S:起始信号 (3) SLAVE ADDRESS:从机地址 (4)
:此数据由从机传输到主机 (5) R/W :传输方向选择位 (6) 1 为读取 (7) 0 为写入 (8) P:停止信号
起始信号产生后,所有从机都将等待主机发送的从机地址信号,I2C 总线中,每个设备的地址都是唯一的,当地址信号与设备地址匹配后,从机将被选中,没被选中的从机将忽略以后的数据信号。
主机方向为写数据时 广播完地址后,接收到应答信号,主机向从机发传输数据,数据长度为一个字节,主机每次发完一个字节数据后,都需等待从机发送的应答信号,当传输的所有字节完成后,主机向从机发送一个停止信号(STOP),表示为传输完成。 主机方向为读数据时 广播完地址后,接收到应答信号,从机开始向主机传输数据,数据包的大小为 8位,从机每发送完一个字节数据,都要等待主机的应答信号,当主机想停止接收数据时,需要向从机返回一个非应答信号,则从机自动停止数据传输。
2.2 IIC时序 I2C在传输数据过程中共3条信号,分别是开始信号,结束信号和应答信号。 2.2.1 IIC开始信号(START) SATRT信号定义为:时钟线SCL保持高电平期间,数据线SDA电平拉低,标志着一次数据传输的开始。启动信号是一种电平跳转的时序信号,而不是一种电平信号。该信号由主设备发出,在建立该信号之前,IIC总线必须处于空闲状态。如下为START时序图。
2.2.2 IIC停止信号(STOP) STOP信号定义为:时钟线SCL保持高电平期间,数据线SDA释放,返回高电平,标志着一次数据传输的结束。停止信号也是一种电平跳转的时序信号,而不是一种电平信号。该信号由主设备发出,建立该信号之后,IIC返回空闲状态。如下为STOP时序图。
2.2.3 IIC应答信号(ACK) IIC总线上的所有数据都是以8位字节传送的,IIC通信时,发送器每发送一次数据,接收器都会反馈一个应答信号。 应答信号为低电平时,规定为有效应答(ACK);应答信号为高电平时,规定为非有效应答(NACK)。 反馈有效应答信号的要求:接收器在接收第9个脉冲前,将SDA线拉低,确保当SCL线为高电平时,SDA线输出稳定的低电平。 如果接收器是主设备,则当其收到从设备发出的最后一个字节时,会发送一个NACK信号,以通知从设备停止数据传送,并释放SDA线,以便主设备发送一个停止信号。如下为ACK和NACK时序图。
3. AT24C02模块 AT24C02是一款串行EEPROM(Electrically Erasable Programmable Read-Only Memory)芯片,由Atmel(现在是Microchip Technology)公司生产。它是AT24C系列芯片中的一员,具有2K位(256字节)的存储容量。 如下是AT24C02的性能特点。 (1)采用I2C(Inter-Integrated Circuit)总线接口,这是一种常用的串行通信协议,可以在多个设备之间进行数据传输。它支持标准模式(100 KHz)和快速模式 (400 KHz)的I2C通信速率。 (2)具有可擦写和可编程的功能,可以通过电源供电进行数据的读取和写入。它采用8位地址寻址,可以连接多个AT24C02芯片,使得系统能够扩展更大的存储容量。 (3)数据存储是非易失性的,即使在断电情况下,数据仍然可以保持。它还具有内部写保护功能,可以通过硬件或软件方式来保护存储的数据,防止误写或擦除。 (4)广泛应用于各种电子设备中,例如存储配置信息、校准数据、序列号、日志记录等。由于其容量适中、接口简单、低功耗等特点,它在嵌入式系统和小型电子设备中得到了广泛应用。总体而言,AT24C02芯片具有较小的存储容量、简单的接口、良好的可靠性和低功耗等特点,适用于许多嵌入式系统和小型电子设备中的数据存储需求。 如下为AT24C02的通信时序图。
4.硬件设计 本次设计使用APM32F103的PB6、PB7引脚进行GPIO模拟IIC,将AT24C02的SCL和SDA分别连接在APM32F103的PB6、PB7上,连接关系如图所示。
5.软件设计 本章主要介绍GPIO模拟IIC所实现的代码,以及APM32F103使用软件IIC与AT24C02通信。其工作流程图如下所示。
5.1 SDA_IN_OUT //SDA设置为输入模式
void SDA_IN(void)
{
GPIOB->CFGLOW &= 0X0FFFFFFF;
GPIOB->CFGLOW |= (uint32_t)8<<28;
}
//SDA设置为输出模式
void SDA_OUT(void)
{
GPIOB->CFGLOW &= 0X0FFFFFFF;
GPIOB->CFGLOW |= (uint32_t)3<<28;
}
参照APM32F103用户手册关于CFGLOW寄存器的说明,SDA_IN()函数将GPIOB中的CFGLOW寄存器的高4位配置成了“1000”,对应设置PB7引脚为输入模式;SDA_OUT()函数将GPIOB中的CFGLOW寄存器高4位配置成了“0011”,对应PB7引脚为推挽输出模式。
5.2 IIC_Init//PB6(SCL)、PB7(SDA)初始化
void IIC_Init(void)
{
GPIO_Config_T gpioConfig;
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOB);
gpioConfig.pin = IIC_PIN_SCL|IIC_PIN_SDA;
gpioConfig.mode = GPIO_MODE_OUT_PP ; //推挽输出
gpioConfig.speed = GPIO_SPEED_50MHz;
GPIO_Config(GPIOB, &gpioConfig);
GPIO_SetBit(GPIOB,IIC_PIN_SCL|IIC_PIN_SDA);
}
IIC_Init()函数配置了GPIOB的时钟,将PB6(SCL)、PB7(SDA)配置成了推挽输出模式,由于IIC时序开始时为高电平,故将PB6、PB7默认置为高电平。
5.3 IIC_Start//IIC 开始时序
void IIC_Start(void)
{
SDA_OUT(); //SDA输出
IIC_SDA_SET;
IIC_SCL_SET;
Delay_us(4);
IIC_SDA_RESET; //START:当CLK为高电平时,DATA从高电平变为低电平
Delay_us(4);
IIC_SCL_RESET; //钳住I2C总线,准备发送或接收数据
}
根据IIC开始时序(START)图,当SCL线为稳定的高电平时,将SDA线拉低代表IIC时序的开始,故先将IIC_SDA_RESET,延时一段时间后,再将IIC_SCL_RESET,即可模拟IIC的START时序。
5.4 IIC_Stop//IIC 停止时序
void IIC_Stop(void)
{
SDA_OUT(); //SDA线输出
IIC_SCL_RESET;
IIC_SDA_RESET; //STOP:当CLK为高电平时,DATA从低电平变为高电平
Delay_us(4);
IIC_SCL_SET;
IIC_SDA_SET; //发送I2C总线结束信号
Delay_us(4);
}
根据IIC停止时序(STOP)图,当SCL线为稳定的高电平时,将SDA线拉高代表IIC时序的停止,故先将IIC_SCL_SET,再将 IIC_SDA_SET,即可模拟IIC的STOP时序。
5.5 IIC_Wait_Ack// 等待ACK应答
uint8_t IIC_Wait_Ack(void)
{
uint8_t ucErrTime = 0;
SDA_IN(); //SDA设置为输入
IIC_SDA_SET;
Delay_us(1);
IIC_SCL_SET;
Delay_us(1);
while(IIC_READ_SDA)
{
ucErrTime++;
if(ucErrTime > 250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL_RESET; //时钟输出0
return 0;
}
IIC_Wait_Ack()函数中,将SDA设置为输入模式,以读取SDA电平。将SCL和SDA拉高,若SDA线识别到低电平,则代表收到了ACK,返回0;若250次之后未收到ACK则返回1,同时中断此次IIC传输。 在IIC_Wait_Ack()函数中,成功接收到ACK后,会执行IIC_SCL_RESET这一段代码,其会将SCL拉低一段时间,这个时间被称为“时钟拉低期”。拉低SCL的作用为: 1. 稳定数据:拉低 SCK 线可以确保数据线(SDA)上的数据稳定,避免在时钟转换期间发生数据的变化。 2. 适应从设备速度:从设备可能由于处理数据的速度较慢而需要更多时间来处理接收到的数据。通过拉低 SCK 线,主设备可以等待从设备完成数据处理,使得数据传输的速度适应 从设备的处理速度。 3. 时钟同步:时钟拉低期可以确保主设备和从设备之间的时钟同步,使得数据传输的时序正确。 总的来说,将 SCK 线拉低一段时间是为了稳定数据传输、适应从设备的速度并确保时钟同步。这样可以保证在 I2C 通信中数据的准确传输和正确处理。
5.6 IIC_Ack//GPIO模拟产生ACK时序
void IIC_Ack(void)
{
IIC_SCL_RESET;
SDA_OUT();
IIC_SDA_RESET;
Delay_us(2);
IIC_SCL_SET;
Delay_us(2);
IIC_SCL_RESET;
}
根据IIC应答时序(ACK)图,当SCL发生脉冲变化时,SDA一直处于低电平,确保当SCL为稳定的高电平时,SDA一直为低电平。故先将IIC_SDA_RESET,而后SCL产生一次脉冲,SCL由“0”跳变为“1”,再由“1”跳变为“0”,模拟IIC时序的ACK信号。
5.7 IIC_NAck//GPIO模拟产生NACK时序
void IIC_NAck(void)
{
IIC_SCL_RESET;
SDA_OUT();
IIC_SDA_SET;
Delay_us(2);
IIC_SCL_SET;
Delay_us(2);
IIC_SCL_RESET;
}
根据IIC非应答时序(NACK)图,当SCL发生脉冲变化时,SDA一直处于高电平,确保当SCL为稳定的高电平时,SDA一直为高电平。故先将IIC_SDA_RESET,而后SCL产生一次脉冲,SCL由“0”跳变为“1”,再由“1”跳变为“0”,模拟IIC时序的NACK信号。
5.8 IIC_Send_Byte//GPIO模拟IIC:发送一个字节
void IIC_Send_Byte(uint8_t txd)
{
uint8_t t;
SDA_OUT();
IIC_SCL_RESET; //拉低时钟开始数据传输
for (t = 0; t < 8; t++)
{
IIC_SDA_WRITE((txd&0x80)>>7);
txd <<= 1;
Delay_us(2);
IIC_SCL_SET;
Delay_us(2);
IIC_SCL_RESET;
Delay_us(2);
}
}
由于IIC必须在SCL为低电平时,SDA线才能跳变,故现将IIC_SCL_RESET,而后调用IIC_SDA_WRITE()函数逐位写入对应被控设备的地址,写完一位数据后,将SCL拉高,确保此次传输数据的稳定性,而后IIC_SCL_RESET,准备下一次的数据传输。
5.9 IIC_Read_Byte//GPIO模拟IIC:读取一个字节
uint8_t IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN(); //SDA设置为输入
for (i = 0; i < 8; i++ )
{
IIC_SCL_RESET;
Delay_us(2);
IIC_SCL_SET;
receive <<= 1;
if(IIC_READ_SDA)
{
receive++;
}
Delay_us(1);
}
if (!ack)
{
IIC_NAck(); //发送NACK
}
else
{
IIC_Ack(); //发送ACK
}
return receive;
}
IIC_Read_Byte()函数中,SDA设置为输入模式,以便对IO口进行读取操作。随后给予SCL一个上升沿,以此触发数据的采样,函数中定义了一个“receive”变量,若IIC_READ_SDA为0,则代表读取到的数据为“0”,则recvice=0;相反,则receive=1。“receive”变量用来存储读取的数据。“ack”变量的作用是提示是否需要被控设备发送ACK应答信号,若“ack=1”,则代表控制设备的数据还未传输完,需要返回被控设备返回ACK应答;若“ack=0”,则代表控制设备的数据已经传输完,无需ACK应答,需要被控设备返回NACK应答。
5.10 AT24CXX_ReadOneByteuint8_t AT24CXX_ReadOneByte(uint16_t ReadAddr)
{
uint8_t temp=0;
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr>>8);//发送高地址
IIC_Wait_Ack();
}
else
{
IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0XA1); //进入读模式
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop(); //产生一个停止条件
return temp;
}
AT24CXX_ReadOneByte(),读取一个字节,“ReadAddr”代表AT24C02模块要读取数据对应的地址。AT24C02在发送读命令之前,需要先发送写命令,设置要读取设备的地址或寄存器,而后发送读命令,才能对设备进行读操作。
5.11 AT24CXX_WriteOneByte
void AT24CXX_WriteOneByte(uint16_t WriteAddr,uint8_t DataToWrite)
{
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr>>8);//发送高地址
}
else
{
IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送字节
IIC_Wait_Ack();
IIC_Stop();//产生一个停止条件
Delay_ms(10);
}
AT24CXX_WriteOneByte ()函数中,“WriteAddr”代表要写入数据的地址,“DataToWrite”代表要写入的数据。对于AT24C02模块的写操作没有读操作那么复杂,在发送写命令后,再发送要写入设备的地址和数据,即可完成模块的写入操作。
6. 实验现象 在代码编译成功后,我们通过下载代码到APM32F103开发板上,将AT24C02模块的SCL、SDA分别与APM32F103的PB6、PB7连接,将PA9、PA10作为串口线输出,通过按键KEY1写入数据,然后按键KEY2读取数据,现象如图所示。
7. 代码及相关资料附件
AT24C02.pdf
(484.85 KB)
I2C_Software _AT24C02_Demo.zip
(277.2 KB)
欢迎大家下载学习,如有问题,也请在评论区指出,谢谢!
|