硬件I2C和软件I2C是两种不同的实现I2C(Inter-Integrated Circuit,集成电路间)通信协议的方式,它们在实现方式、性能特点以及应用场景上存在显著差异。
一、实现方式
- 硬件I2C:通过专门的硬件电路实现,这些电路通常由微控制器或其他集成电路上的硬件模块提供支持。硬件I2C可以直接调用内部寄存器进行配置,利用芯片中的硬件I2C外设,自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,从而减轻CPU的负担。
- 软件I2C:通过软件控制GPIO(通用输入输出)管脚来模拟I2C协议的时序。这通常涉及到在程序中控制SCL(串行时钟)和SDA(串行数据)线的电平状态,以模拟I2C通信的起始、停止、数据发送和接收等过程。
二、性能特点
-
硬件I2C:
- 高速传输:由于使用专门的硬件电路,硬件I2C可以实现较高的数据传输速度,通常可以达到400kHz或更高。
- 低占用率:传输过程由硬件电路完成,不需要CPU的直接参与,因此可以释放CPU资源,降低系统负载。
- 稳定性高:时序控制由硬件电路完成,不易受到外部干扰的影响,具有较高的通信稳定性。
- 占用CPU资源少:由于硬件I2C的传输过程不需要CPU的干预,因此可以显著减少CPU的占用率。
- 可靠性高:硬件I2C的实现符合I2C标准,具有较高的可靠性。
然而,硬件I2C也存在一些缺点,如外设数量限制和异常处理难度较大。
-
软件I2C:
- 灵活性高:可以使用任意的GPIO管脚来实现,适应不同的硬件平台和需求。
- 可移植性强:不依赖于特定的硬件电路,可以在不同的平台上进行移植和使用。
- 适用范围广:在没有硬件I2C支持的情况下,软件I2C可以作为替代方案,也可以用于扩展硬件I2C的功能。
-
软件I2C通常也被称为模拟I2C:这是因为软件I2C是通过软件编程来控制GPIO(通用输入输出)引脚来模拟I2C通信协议的时序和信号。它不使用微控制器或其他集成电路上的硬件I2C模块,而是通过编写代码来手动控制SCL(串行时钟)和SDA(串行数据)线的电平状态,从而模拟出I2C通信的起始条件、停止条件、数据发送和接收等过程。
模拟I2C(即软件I2C)的优点在于其灵活性和可移植性,因为它不依赖于特定的硬件电路,可以在任何具有GPIO引脚的微控制器或处理器上实现。然而,与硬件I2C相比,模拟I2C的通信速度可能较慢,且由于需要CPU的参与来生成时序,因此会占用较多的CPU资源。此外,模拟I2C的实现可能不如硬件I2C稳定,因为它更容易受到外部干扰和程序错误的影响。
然而,软件I2C的速度和稳定性可能不如硬件I2C,且需要CPU的参与,因此会占用较多的CPU资源。此外,软件I2C的实现可能不符合I2C标准,导致可靠性较低。
三、应用场景
- 硬件I2C:适用于对传输速度和稳定性要求较高的场景,如高速数据传输、实时性要求较高的系统等。
-
/** @description : 初始化I2C,波特率100KHZ* @param - base : 要初始化的IIC设置* @return : 无*/ void i2c_init(I2C_Type *base) {/* 1、配置I2C */base->I2CR &= ~(1 << 7); /* 要访问I2C的寄存器,首先需要先关闭I2C *//* 设置波特率为100K* I2C的时钟源来源于IPG_CLK_ROOT=66Mhz* IC2 时钟 = PERCLK_ROOT/dividison(IFDR寄存器)* 设置寄存器IFDR,IFDR寄存器参考IMX6UL参考手册P1260页,表29-3,* 根据表29-3里面的值,挑选出一个还是的分频数,比如本例程我们* 设置I2C的波特率为100K, 因此当分频值=66000000/100000=660.* 在表29-3里面查找,没有660这个值,但是有640,因此就用640,* 即寄存器IFDR的IC位设置为0X15*/base->IFDR = 0X15 << 0;/** 设置寄存器I2CR,开启I2C* bit[7] : 1 使能I2C,I2CR寄存器其他位其作用之前,此位必须最先置1*/base->I2CR |= (1<<7); }/** @description : 发送重新开始信号* @param - base : 要使用的IIC* @param - addrss : 设备地址* @param - direction : 方向* @return : 0 正常 其他值 出错*/ unsigned char i2c_master_repeated_start(I2C_Type *base, unsigned char address, enum i2c_direction direction) {/* I2C忙并且工作在从模式,跳出 */if(base->I2SR & (1 << 5) && (((base->I2CR) & (1 << 5)) == 0)) return 1;/** 设置寄存器I2CR* bit[4]: 1 发送* bit[2]: 1 产生重新开始信号*/base->I2CR |= (1 << 4) | (1 << 2);/** 设置寄存器I2DR* bit[7:0] : 要发送的数据,这里写入从设备地址* 参考资料:IMX6UL参考手册P1249*/ base->I2DR = ((unsigned int)address << 1) | ((direction == kI2C_Read)? 1 : 0);return 0; }/** @description : 发送开始信号* @param - base : 要使用的IIC* @param - addrss : 设备地址* @param - direction : 方向* @return : 0 正常 其他值 出错*/ unsigned char i2c_master_start(I2C_Type *base, unsigned char address, enum i2c_direction direction) {if(base->I2SR & (1 << 5)) /* I2C忙 */return 1;/** 设置寄存器I2CR* bit[5]: 1 主模式* bit[4]: 1 发送*/base->I2CR |= (1 << 5) | (1 << 4);/** 设置寄存器I2DR* bit[7:0] : 要发送的数据,这里写入从设备地址* 参考资料:IMX6UL参考手册P1249*/ base->I2DR = ((unsigned int)address << 1) | ((direction == kI2C_Read)? 1 : 0);return 0; }/** @description : 检查并清除错误* @param - base : 要使用的IIC* @param - status : 状态* @return : 状态结果*/ unsigned char i2c_check_and_clear_error(I2C_Type *base, unsigned int status) {/* 检查是否发生仲裁丢失错误 */if(status & (1<<4)){base->I2SR &= ~(1<<4); /* 清除仲裁丢失错误位 */base->I2CR &= ~(1 << 7); /* 先关闭I2C */base->I2CR |= (1 << 7); /* 重新打开I2C */return I2C_STATUS_ARBITRATIONLOST;} else if(status & (1 << 0)) /* 没有接收到从机的应答信号 */{return I2C_STATUS_NAK; /* 返回NAK(No acknowledge) */}return I2C_STATUS_OK; }/** @description : 停止信号* @param - base : 要使用的IIC* @param : 无* @return : 状态结果*/ unsigned char i2c_master_stop(I2C_Type *base) {unsigned short timeout = 0xffff;/** 清除I2CR的bit[5:3]这三位*/base->I2CR &= ~((1 << 5) | (1 << 4) | (1 << 3));/* 等待忙结束 */while((base->I2SR & (1 << 5))){timeout--;if(timeout == 0) /* 超时跳出 */return I2C_STATUS_TIMEOUT;}return I2C_STATUS_OK; }/** @description : 发送数据* @param - base : 要使用的IIC* @param - buf : 要发送的数据* @param - size : 要发送的数据大小* @param - flags : 标志* @return : 无*/ void i2c_master_write(I2C_Type *base, const unsigned char *buf, unsigned int size) {/* 等待传输完成 */while(!(base->I2SR & (1 << 7))); /*这里判断base->I2SR的bit7,可以理解为i2c有没有被占用,手册1467中说这个位由最后一个字节传输的第9个时钟的下降沿设置,完成为一。我们要考虑当传输多个字节时,每传输完一个字节(不是最后一个字节),bit7并不会被置一,而bit1每传输完一个字节(包括最后一个字节)都会被置一,因此在要开始传输时判断bit7也就是i2c有没有被占用,在传输多个字节的过程中每传输完一个字节要判断bit1并置0。*/base->I2SR &= ~(1 << 1); /* 清除标志位 */base->I2CR |= 1 << 4; /* 发送数据 */while(size--){base->I2DR = *buf++; /* 将buf中的数据写入到I2DR寄存器 */while(!(base->I2SR & (1 << 1))); /* 等待传输完成 */ base->I2SR &= ~(1 << 1); /* 清除标志位 *//* 检查ACK */if(i2c_check_and_clear_error(base, base->I2SR))break;}base->I2SR &= ~(1 << 1);i2c_master_stop(base); /* 发送停止信号 */ }/** @description : 读取数据* @param - base : 要使用的IIC* @param - buf : 读取到数据* @param - size : 要读取的数据大小* @return : 无*/ void i2c_master_read(I2C_Type *base, unsigned char *buf, unsigned int size) {volatile uint8_t dummy = 0;dummy++; /* 防止编译报错 *//* 等待传输完成 */while(!(base->I2SR & (1 << 7))); base->I2SR &= ~(1 << 1); /* 清除中断挂起位 */base->I2CR &= ~((1 << 4) | (1 << 3)); /* 接收数据 *//* 如果只接收一个字节数据的话发送NACK信号 */if(size == 1)base->I2CR |= (1 << 3);dummy = base->I2DR; /* 假读:假读触发下一个字节的传输 */while(size--){while(!(base->I2SR & (1 << 1))); /* 等待传输完成 */ base->I2SR &= ~(1 << 1); /* 清除标志位 */if(size == 0){i2c_master_stop(base); /* 发送停止信号 */}if(size == 1){base->I2CR |= (1 << 3);}*buf++ = base->I2DR;} }/** @description : I2C数据传输,包括读和写* @param - base: 要使用的IIC* @param - xfer: 传输结构体* @return : 传输结果,0 成功,其他值 失败;*/ unsigned char i2c_master_transfer(I2C_Type *base, struct i2c_transfer *xfer) {unsigned char ret = 0;enum i2c_direction direction = xfer->direction; base->I2SR &= ~((1 << 1) | (1 << 4)); /* 清除标志位 *//* 等待传输完成 */while(!((base->I2SR >> 7) & 0X1)){}; /* 如果是读的话,要先发送寄存器地址,所以要先将方向改为写 */if ((xfer->subaddressSize > 0) && (xfer->direction == kI2C_Read)){direction = kI2C_Write;}ret = i2c_master_start(base, xfer->slaveAddress, direction); /* 发送开始信号 */if(ret){ return ret;}while(!(base->I2SR & (1 << 1))){}; /* 等待传输完成 */ret = i2c_check_and_clear_error(base, base->I2SR); /* 检查是否出现传输错误 */if(ret){i2c_master_stop(base); /* 发送出错,发送停止信号 */return ret;}/* 发送寄存器地址 */if(xfer->subaddressSize){do{base->I2SR &= ~(1 << 1); /* 清除标志位 */xfer->subaddressSize--; /* 地址长度减一 */base->I2DR = ((xfer->subaddress) >> (8 * xfer->subaddressSize)); //向I2DR寄存器写入子地址while(!(base->I2SR & (1 << 1))); /* 等待传输完成 *//* 检查是否有错误发生 */ret = i2c_check_and_clear_error(base, base->I2SR);if(ret){i2c_master_stop(base); /* 发送停止信号 */return ret;} } while ((xfer->subaddressSize > 0) && (ret == I2C_STATUS_OK));if(xfer->direction == kI2C_Read) /* 读取数据 */{base->I2SR &= ~(1 << 1); /* 清除中断挂起位 */i2c_master_repeated_start(base, xfer->slaveAddress, kI2C_Read); /* 发送重复开始信号和从机地址 */while(!(base->I2SR & (1 << 1))){};/* 等待传输完成 *//* 检查是否有错误发生 */ret = i2c_check_and_clear_error(base, base->I2SR);if(ret){ret = I2C_STATUS_ADDRNAK;i2c_master_stop(base); /* 发送停止信号 */return ret; }}} /* 发送数据 */if ((xfer->direction == kI2C_Write) && (xfer->dataSize > 0)){i2c_master_write(base, xfer->data, xfer->dataSize);}/* 读取数据 */if ((xfer->direction == kI2C_Read) && (xfer->dataSize > 0)){i2c_master_read(base, xfer->data, xfer->dataSize);}return 0; }
- 软件I2C:适用于没有硬件I2C支持或需要扩展硬件I2C功能的场景,如低成本、低功耗的嵌入式系统、小型设备等。
-
//初始化IIC void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure;RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);//使能GPIOB时钟//GPIOB8,B9初始化设置GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHzGPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化IIC_SCL=1;IIC_SDA=1; } //产生IIC起始信号 void IIC_Start(void) {SDA_OUT(); //sda线输出IIC_SDA=1; IIC_SCL=1;delay_us(4);IIC_SDA=0;//START:when CLK is high,DATA change form high to low delay_us(4);IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 } //产生IIC停止信号 void IIC_Stop(void) {SDA_OUT();//sda线输出IIC_SCL=0;IIC_SDA=0;//STOP:when CLK is high DATA change form low to highdelay_us(4);IIC_SCL=1; IIC_SDA=1;//发送I2C总线结束信号delay_us(4); } //等待应答信号到来 //返回值:1,接收应答失败 // 0,接收应答成功 u8 IIC_Wait_Ack(void) {u8 ucErrTime=0;SDA_IN(); //SDA设置为输入 IIC_SDA=1;delay_us(1); IIC_SCL=1;delay_us(1); while(READ_SDA){ucErrTime++;if(ucErrTime>250){IIC_Stop();return 1;}}IIC_SCL=0;//时钟输出0 return 0; } //产生ACK应答 void IIC_Ack(void) {IIC_SCL=0;SDA_OUT();IIC_SDA=0;delay_us(2);IIC_SCL=1;delay_us(2);IIC_SCL=0; } //不产生ACK应答 void IIC_NAck(void) {IIC_SCL=0;SDA_OUT();IIC_SDA=1;delay_us(2);IIC_SCL=1;delay_us(2);IIC_SCL=0; } //IIC发送一个字节 //返回从机有无应答 //1,有应答 //0,无应答 void IIC_Send_Byte(u8 txd) { u8 t; SDA_OUT(); IIC_SCL=0;//拉低时钟开始数据传输for(t=0;t<8;t++){ IIC_SDA=(txd&0x80)>>7;txd<<=1; delay_us(2); //对TEA5767这三个延时都是必须的IIC_SCL=1;delay_us(2); IIC_SCL=0; delay_us(2);} } //读1个字节,ack=1时,发送ACK,ack=0,发送nACK u8 IIC_Read_Byte(unsigned char ack) {unsigned char i,receive=0;SDA_IN();//SDA设置为输入for(i=0;i<8;i++ ){IIC_SCL=0; delay_us(2);IIC_SCL=1;receive<<=1;if(READ_SDA)receive++; delay_us(1); } if (!ack)IIC_NAck();//发送nACKelseIIC_Ack(); //发送ACK return receive; }
综上所述,硬件I2C和软件I2C各有其优势和适用场景。在选择时,需要根据具体的应用需求和系统资源来权衡利弊,选择最合适的实现方式。