硬件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;//100MHz GPIO_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 high delay_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();//发送nACK else IIC_Ack(); //发送ACK return receive; }
综上所述,硬件I2C和软件I2C各有其优势和适用场景。在选择时,需要根据具体的应用需求和系统资源来权衡利弊,选择最合适的实现方式。