摘要:本文从电平信号与差分信号的区别讲起,系统梳理 I2C 通信协议的核心知识点,包括硬件接线、上拉电阻作用、起始/终止信号、数据传输时序、应答机制与寻址方式。随后以 STM32 为例,从零搭建软件 IIC 驱动,逐步实现微秒延时、起始信号、8bit 发送、应答检测及单字节/多字节读写函数,并对比硬件 IIC 的 HAL 库用法。最后结合 ENS160 多气体传感器,演示如何将 I2C 驱动落地到真实项目,帮助读者快速掌握从原理到实战的完整链路。
一、差分和电平信号
1.1简介
1. 电平信号:电平信号有一根是参考电平线,信号线的信号值由信号线和参考电平线之间的电压差决定。
2. 差分信号:差分信号没有参考电平线,传输一路数据需要两根信号线,信号线的信号值由两根信号线的电压差决定。
区别:
(1) 信号值的表示不同:电平信号的信号值是信号线和参考电平线的电压差决定;差分信号的信息值是由两条信号线的电压差决定。
(2) 传输相同路数的信号,电平信号用的数据线根数比差分信号少。比如传输 3 路信号,电平信号需要 1 根参考电平 + 3 根数据线;而差分信号需要 2x3=6 根数据线。
(3) 现在通信大部分是差分信号,因为差分信号的抗干扰性更强,可以在更短的周期传输一个数据。
1.2两种逻辑电平标准
1.2.1TTL
TTL电平:逻辑1: 2.4V--5V 逻辑0: 0V--0.5V
TTL电平下的串口协议
假设发送的数据为:01010101(由低位向高位发送)
RS232
电平标准:+3V~+15V对应0,
-3V~-15V对应1
RS232电平下的串口协议
假设发送的数据为:01010101
需要注意的是:
rs232 的逻辑电平和TTL 不一样但是协议一样。
1.3差分信号-RS485
RS-485仅是一个电气标准,描述了接口的物理层,像协议、时序、串行或并行数据以及链路全部由设计者或更高层协议定义。 RS-485定义的是使用平衡多点传输线的驱动器和接收器的电气特性。
RS-485能够进行远距离传输主要得益于使用差分信号进行传输,当有噪声干扰时仍可以使用线路上两者差值进行判断,使传输数据不受噪声干扰。
半双工、是电气协议(逻辑1:+2V–+6V 逻辑0: -6V— -2V)是二线制差分信号,也就是实际传输的数据是通过判断这两条信号线上的电压差来实现的,RS-485总线弥补了RS-232通信距离短,速率低的缺点,RS-485的速率可高达10Mbit/s,理论通讯距离可达1200米;RS-485和RS-232的单端传输不一样,是差分传输,使用一对双绞线
共模抑制
二、通信学习方式
1 了解电路接线
2 了解通信协议
3 使用代码实现该协议
4 硬件通信练习
三、硬件接线
同步串行半双工
半双工通信,输出口都必须是开漏,如果是推挽则容易在通信双方一个发1一个发0是产生短路从而烧毁引脚
开漏输出本身无法实现高电平输出,为了能够实现高电平输出,此时需要外接上拉电阻
四、上拉电阻的作用
4.1避免总线冲突和实现“线与”(Wire-AND)功能
这是最核心的原因。I²C协议支持多主多从(Multiple Masters and Slaves)的架构。
开漏输出:意味着芯片内部的输出级电路只有一个连接到地的MOSFET(像是一个开关),而没有直接连接到电源(VCC)的上拉部分。
当MOSFET关闭(开关断开)时,输出引脚为高阻态,不影响总线。
当MOSFET开启(开关闭合)时,输出引脚被强行拉低到地(GND),输出逻辑'0'。
“线与”逻辑:由于所有设备的总线(SDA和SCL)都是开漏结构并通过上拉电阻连接到VCC,任何一个设备只要主动拉低总线(输出'0'),整条总线就是低电平。
逻辑“与”关系:总线的最终状态 = 设备1的输出 AND 设备2的输出 AND ... 设备N的输出。只有所有设备都输出'1'(即释放总线,不拉低),总线才为高电平'1'。
如何避免冲突:这种“线与”特性使得总线天生就具备冲突检测(Arbitration) 机制。
假设两个主设备同时开始发送数据。它们一边发送,一边会读取总线上的实际电平,并与自己发送的电平进行比较。
如果某个主设备发送了一个高电平'1'(释放总线),但它检测到总线实际是低电平'0'(被另一个主设备拉低了),它就立刻知道自己失去了仲裁,会立即停止操作,转为从接收模式。
这个过程不会损坏硬件,也无需额外的冲突检测线路。这是推挽输出(Push-Pull)绝对无法实现的,如果两个推挽输出的设备一个输出高一个输出低,会造成电源对地短路,烧毁器件。
4.2电平转换的灵活性(兼容不同电压的设备)
这是另一个巨大的优势。由于总线上的高电平电压完全由上拉电阻所连接的电源(VCC) 决定,而与各个设备自身的工作电压(VDD)无关。
场景:一个3.3V的STM32(主设备)想要和一个5V的传感器(从设备)通信。
实现:
将I²C总线的上拉电阻连接到5V电源。
3.3V的STM32的I²C引脚设置为开漏模式,并且其耐压需要能承受5V(很多现代MCU的IO口是5V tolerant的)。
工作原理:
当STM32要输出'0'时,它内部MOSFET导通,将总线拉低到GND(0V)。对于5V的传感器来说,0V显然是逻辑'0'。
当STM32要输出'1'时,它释放总线(高阻态),上拉电阻将总线拉高到5V。STM32会读取到这个5V的高电平,虽然超过了其本身的3.3V供电,但只要引脚耐压足够就没有问题。同时,5V的传感器也正确地看到了5V的逻辑'1'。
好处:你可以在一条I²C总线上混用不同工作电压的芯片,而不需要任何额外的电平转换电路。只需将上拉电阻接到一个所有设备都能接受的最高电压即可。
4.3简化驱动电路
开漏结构非常简单,只需要一个MOSFET管来控制对地的导通,而不需要更复杂的推挽电路(既有上拉P-MOS,又有下拉N-MOS)。这使得IC内部的电路更简单,成本更低。
五、协议时序分析
5.1起始信号与终止信号
SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号;
SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号;
起始和终止信号都是由主机发出,起始信号产生后,总线就处于占用的态;
终止信号产生后,总线就处于空闲态。
5.2数据传输时序
I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定; 只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
在时钟为低电平期间,发送器向数据线上写入数据,因此数据线上的数据运行变化; 在时钟为高电平期间,接收器从数据线上读取数据,因此必须保持数据线上的数据稳定。
最终完成一个时钟周期内,发送器发生一个bit位数据,接收器接收一个bit位数据。
5.3字节传输和应答信号 8+1
每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。
在第9个时钟周期的低电平期间,接收器向数据线上写入数据。
在第9个时钟周期的高电平期间,发送器从数据线上读取数据。
如果读到的是低电平信号,表示应答信号。
如果读到的是高电平信号,表示非应答信号。
5.4 I2C寻址
I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
主机在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/W),用“0”表示主机发送数据(W),“1”表示主机接收数据(R)。 “w0我”
总线上的每个从机都将这7位地址码与自己的地址进行比较,如果相同,则认为自己被主机寻址,根据R/W位将自己定为发送器或接收器。
//IIC地址的表示方式一般使用两位16进制表示
//但是IIC地址只有7位,两位16进制表示了8位二进制
//此时会有两种分配方式
// 假设IIC地址为1100 111
//1从最低位补零的方式 1100 1110=0xCE 此种方式写的话地址可以直接用,而读时需要或等于0x01
//2从最高位补零 0110 0111=0x67 此种方式发送时需要先左移1位然后根据都或者写操作进行或等于或者与等于
5.4.1 主机给从机发送一个字节
5.4.2 主机给从机发送多个字节
5.4.3 主机从从机接收一个字节
5.4.4 主机从从机接收多个字节
六、IIC
6.1 软件IIC定位引脚
6.1.1 定位引脚
6.1.2 配置引脚为开漏,默认高阻态
6.1.3 给引脚添加标签
6.2 驱动框架搭建
6.2.1 添加驱动文件,进行编写
6.3 编写.h的框架
#ifndef __MY_IIC__ #define __MY_IIC__ #endif6.3.1 编写.h文件的宏定义
对时钟、数据线函数、引脚进行宏定义,方便后续引脚变化时调整代码,也方便使用者阅读
#ifndef __MY_IIC__ #define __MY_IIC__ #include "main.h" //声明函数 #define IIC_SCL_PORT GPIOA #define IIC_SCL_PIN GPIO_PIN_8 #define IIC_SDA_PORT GPIOC #define IIC_SDA_PIN GPIO_PIN_9 #define IIC_SCL_1() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,1) #define IIC_SCL_0() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,0) #define IIC_SDA_1() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,1) #define IIC_SDA_0() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,0) #endif6.4 编写.c 文件编写发送单个字节的完整时序函数
#include "myiic.h" /* 0 初始化IIC 1 写单个字节 2 写多个字节 3 读单个字节 4 读多个字节 */ /* 单片机发送单个字节给从机 param : 从机地址+W 寄存器地址 发送的内容 */ void Myiic_SendByte(uint8_t iic_addr,uint8_t reg_addr,uint8_t send_data) { //1 起始标识 //2 发送从机地址+写 //3 接收应答 //4 发送寄存器地址 //5 接收应答 //6 发送8为数据 //7 接收应答 //8 发送停止标识 }6.5 为了提高IIC的通信速度,需要用到微秒级延时,创建微秒级延时的驱动代码
mydelay.c
#include "mydelay.h" //使用滴答定时器实现微秒级延时函数 //编写思路:初始值与当前计数值相减, //数值大于等于我们设定的延时值,就跳出循环 void Delay_us(uint32_t us) { //1.定义变量来存储时间 uint32_t ticks;//需要的滴答数 uint32_t t_old,t_now,t_cnt = 0;//之前的时间、现在的时间、计数值 uint32_t reload = SysTick->LOAD;//将重载值放在变量reload里 //2.计算需要的滴答数 ticks = us * 100; t_old = SysTick->VAL;//将最开始的值赋给t_old //3.比较计时时间与设定时间大小 while(1) { t_now = SysTick->VAL;//获取当前计数值 if(t_now != t_old) { if(t_now < t_old) { t_cnt += t_old - t_now;//将两者差值累加 } else//VAL减到零重置时 { t_cnt += reload-t_now+t_old;//把重新装载之后减得数进行累加 } t_old = t_now;//更新原来的t_old值 if(t_cnt >= ticks)//计时时间大于设定时间,则跳出while { break; } } } } void Delay_ms(uint32_t ms) { for(int i =0;i<ms;i++) Delay_us(1000); }mydelay.h
#ifndef __MY_Delay__ #define __MY_Delay__ #include "main.h" //高精延时 void Delay_us(uint32_t us); void Delay_ms(uint32_t ms); #endif6.6 添加延迟
6.6.1 对延时函数添加宏定义
6.7 因为起始信号会被多次复用,所以封装成函数更易理解和模块化,编写起始函数
void Myiic_Start(void) { IIC_SCL_0(); IIC_SDA_1(); IIC_Delay(); IIC_SCL_1(); IIC_SDA_0(); IIC_Delay(); }6.8 封装一个发送8bit数据的函数
//发送8bit void Myiic_8Bit(uint8_t send_data) { for(int i= 0; i<8;i++) { /* 时钟线拉低 发送最高位 然后拉高 等待从机读取 再拉低 发送次高位*/ IIC_SCL_0(); if(send_data & 0x80) { IIC_SDA_1(); }else{ IIC_SDA_0(); } IIC_Delay(); //拉高时钟线,从机可以读取数据了 IIC_SCL_1(); IIC_Delay();//让从机有充分的时间来读取 send_data <<= 1; } }6.9 以此类推可完成写单个函数
#include "myiic.h" void Myiic_Start(void) { //防止发送起始标识之前,SDA处于低电平,需要提前拉高 IIC_SCL_0(); IIC_SDA_1(); IIC_Delay(); IIC_SCL_1(); IIC_SDA_0();//时钟线高电平时拉低数据线 IIC_Delay(); } void Myiic_Stop(void) { IIC_SCL_0(); IIC_SDA_0(); IIC_Delay(); IIC_SCL_1(); IIC_SDA_1(); IIC_Delay(); } void Myiic_8bit(uint8_t data) { //拉低时钟线准备发送 for(int i = 0; i<8;i++) { IIC_SCL_0(); //p7 p6 p5 p4 p3 p2 p1 p0 // 1 0 0 0 0 0 0 0 if(data &0x80)//0000 000p7 IIC_SDA_1(); else IIC_SDA_0(); IIC_Delay(); //拉高时钟线 从机可以读取数据 IIC_SCL_1(); IIC_Delay();//让高电平保持一段时间,给从机留时间读 data <<=1; } } int Myiic_WaitAck(void) { //拉低时钟线 让从机发送应答 IIC_SCL_0(); //松手 IIC_SDA_1(); //延迟 IIC_Delay(); //拉高时钟线 IIC_SCL_1(); //读信号线 //根据读到的高低电平判断有无应答 if(IIC_SDA_READ() == 1) { //无应答 //发送停止结束通信 IIC_SCL_0(); IIC_SDA_0(); IIC_Delay(); IIC_SCL_1(); IIC_SDA_1(); IIC_Delay(); return NACK; }else{ return ACK; } } /* 发送单个字节到从机 param: iic_addr 最低位为0 默认写的IIC地址 reg_addr 要写的寄存器地址 send_data 要发送的数据 return 0应答 1非应答 */ int Myiic_SendByte(uint8_t iic_addr,uint8_t reg_addr,uint8_t send_data) { //1 起始标识 Myiic_Start(); //2 发送8位数---从机地址+写 Myiic_8bit(iic_addr); //3 接收应答 if(Myiic_WaitAck() == NACK) { return NACK; } //4 发送8位数---寄存器地址 Myiic_8bit(reg_addr); //5 接收应答 if(Myiic_WaitAck() == NACK) { return NACK; } //6 发送8位数---要写的数据 Myiic_8bit(send_data); //7 接收应答 if(Myiic_WaitAck() == NACK) { return NACK; } //8 发送停止标识 Myiic_Stop(); }#ifndef __MY_IIC__ #define __MY_IIC__ #include "main.h" #include "mydelay.h" #define IIC_Delay() Delay_us(2) #define IIC_SCL_PORT GPIOA #define IIC_SCL_PIN GPIO_PIN_8 #define IIC_SDA_PORT GPIOC #define IIC_SDA_PIN GPIO_PIN_9 enum{ ACK=0, NACK }; #define IIC_SCL_1() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,1) #define IIC_SCL_0() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,0) #define IIC_SDA_1() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,1) #define IIC_SDA_0() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,0) #define IIC_SDA_READ() HAL_GPIO_ReadPin(IIC_SDA_PORT,IIC_SDA_PIN) #endif6.10 完整驱动
#include "myiic.h" void Myiic_Start(void) { //防止发送起始标识之前,SDA处于低电平,需要提前拉高 IIC_SCL_0(); IIC_SDA_1(); IIC_Delay(); IIC_SCL_1(); IIC_SDA_0();//时钟线高电平时拉低数据线 IIC_Delay(); } void Myiic_Stop(void) { IIC_SCL_0(); IIC_SDA_0(); IIC_Delay(); IIC_SCL_1(); IIC_SDA_1(); IIC_Delay(); } void Myiic_8bit(uint8_t data) { //拉低时钟线准备发送 for(int i = 0; i<8;i++) { IIC_SCL_0(); //p7 p6 p5 p4 p3 p2 p1 p0 // 1 0 0 0 0 0 0 0 if(data &0x80)//0000 000p7 IIC_SDA_1(); else IIC_SDA_0(); IIC_Delay(); //拉高时钟线 从机可以读取数据 IIC_SCL_1(); IIC_Delay();//让高电平保持一段时间,给从机留时间读 data <<=1; } } int Myiic_WaitAck(void) { //拉低时钟线 让从机发送应答 IIC_SCL_0(); //松手 IIC_SDA_1(); //延迟 IIC_Delay(); //拉高时钟线 IIC_SCL_1(); //读信号线 //根据读到的高低电平判断有无应答 if(IIC_SDA_READ() == 1) { //无应答 //发送停止结束通信 IIC_SCL_0(); IIC_SDA_0(); IIC_Delay(); IIC_SCL_1(); IIC_SDA_1(); IIC_Delay(); return NACK; }else{ return ACK; } } void Myiic_NACK(void) { //拉低时钟线 IIC_SCL_0(); //松手 IIC_SDA_1(); //延迟 IIC_Delay(); //拉高时钟线 IIC_SCL_1(); //延迟 通知从机读取信号线 IIC_Delay(); } void Myiic_ACK(void) { //拉低时钟线 IIC_SCL_0(); //拉低数据线 IIC_SDA_0(); //延迟 IIC_Delay(); //拉高时钟线 IIC_SCL_1(); //延迟 通知从机读取信号线 IIC_Delay(); IIC_SCL_0(); IIC_SDA_1(); } void Myiic_RECV8Bit(uint8_t *data) { int recv_data=0; //接收8位数据 for(int i=0;i<8;i++) { //拉低时钟线 延迟 让对方来发送 IIC_SCL_0(); IIC_Delay(); //拉高时钟线 IIC_SCL_1(); //读取当前线的电平,赋值recv_data第8位 recv_data <<= 1; recv_data |= IIC_SDA_READ(); //延迟 IIC_Delay(); } *data = recv_data; } /* 发送单个字节到从机 param: iic_addr 最低位为0 默认写的IIC地址 reg_addr 要写的寄存器地址 send_data 要发送的数据 return 0应答 1非应答 */ int Myiic_SendByte(uint8_t iic_addr,uint8_t reg_addr,uint8_t send_data) { //1 起始标识 Myiic_Start(); //2 发送8位数---从机地址+写 Myiic_8bit(iic_addr); //3 接收应答 if(Myiic_WaitAck() == NACK) { return NACK; } //4 发送8位数---寄存器地址 Myiic_8bit(reg_addr); //5 接收应答 if(Myiic_WaitAck() == NACK) { return NACK; } //6 发送8位数---要写的数据 Myiic_8bit(send_data); //7 接收应答 if(Myiic_WaitAck() == NACK) { return NACK; } //8 发送停止标识 Myiic_Stop(); } uint8_t Myiic_SendByte_len(uint8_t iic_addr,uint8_t reg_addr,uint8_t *send_data,uint8_t send_len) { //起始标识 Myiic_Start(); Myiic_8bit(iic_addr); if(Myiic_WaitAck() == NACK) { return NACK; } Myiic_8bit(reg_addr); if(Myiic_WaitAck() == NACK) { return NACK; } for(int i=0;i<send_len;i++) { Myiic_8bit(send_data[i]); if(Myiic_WaitAck() == NACK) { return NACK; } } Myiic_Stop(); } uint8_t Myiic_Read_1Byte(uint8_t iic_addr,uint8_t reg_addr,uint8_t *recv_data) { //起始标识 Myiic_Start(); //发送从机地址+写 Myiic_8bit(iic_addr); if(Myiic_WaitAck() == NACK) { return NACK; } Myiic_8bit(reg_addr); if(Myiic_WaitAck() == NACK) { return NACK; } //重新发送起始 Myiic_Start(); Myiic_8bit(iic_addr | 0x01); if(Myiic_WaitAck() == NACK) { return NACK; } Myiic_RECV8Bit(recv_data); //发送非应答 Myiic_NACK(); //发送结束信号 Myiic_Stop(); } /* iic主机接收多字节函数 param : 从机地址+W 寄存器地址 接收的变量地址 接收长度 */ uint8_t Myiic_Read_Byte_len(uint8_t iic_addr,uint8_t reg_addr,uint8_t *data,uint8_t recv_len) { //1起始标识 Myiic_Start(); //2 发送从机地址+写 Myiic_8bit(iic_addr); //3等待应答 if(Myiic_WaitAck() == NACK) { return NACK; } //4 发送寄存器地址 Myiic_8bit(reg_addr); //5等待应答 if(Myiic_WaitAck() == NACK) { return NACK; } //6发送起始标识 Myiic_Start(); //7发送从机地址+读 Myiic_8bit(iic_addr | 0X01); //8等待应答 if(Myiic_WaitAck() == NACK) { return NACK; } //9接收第1个字节,连读8位 //10发送应答 //。。重复9,10,当接收达到recv_len时,发送非应答 for(int i = 0;i<recv_len;i++) { Myiic_RECV8Bit(data+i); if(i != recv_len-1) { Myiic_ACK(); } } Myiic_NACK(); //发送停止标识 Myiic_Stop(); }#ifndef __MY_IIC__ #define __MY_IIC__ #include "main.h" #include "mydelay.h" #define IIC_Delay() Delay_us(2) #define IIC_SCL_PORT GPIOA #define IIC_SCL_PIN GPIO_PIN_8 #define IIC_SDA_PORT GPIOC #define IIC_SDA_PIN GPIO_PIN_9 enum{ ACK=0, NACK }; #define IIC_SCL_1() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,1) #define IIC_SCL_0() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,0) #define IIC_SDA_1() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,1) #define IIC_SDA_0() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,0) #define IIC_SDA_READ() HAL_GPIO_ReadPin(IIC_SDA_PORT,IIC_SDA_PIN) int Myiic_SendByte(uint8_t iic_addr,uint8_t reg_addr,uint8_t send_data); uint8_t Myiic_SendByte_len(uint8_t iic_addr,uint8_t reg_addr,uint8_t *send_data,uint8_t send_len); uint8_t Myiic_Read_1Byte(uint8_t iic_addr,uint8_t reg_addr,uint8_t *recv_data); uint8_t Myiic_Read_Byte_len(uint8_t iic_addr,uint8_t reg_addr,uint8_t *data,uint8_t recv_len); #endif七、 硬件IIC函数
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);hi2c: I2C 句柄指针(比如&hi2c1)。
DevAddress: 目标从设备的地址。
STM32 的 HAL 库要求这里传入的是包含读写位的 8 位地址,底层的代码会自动帮你把最低位变成 0(表示写操作)。
pData: 要发送的数据缓冲区的指针。
Size: 要发送的数据字节数。
Timeout: 超时时间(毫秒)。
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);hi2c: 同上,I2C 句柄指针。
DevAddress: 目标从设备的地址。同样需要把 7 位地址左移 1 位(DevAddress << 1)。底层代码会自动帮你把最低位变成 1(表示读操作)。
pData: 用来存放接收到的数据的缓冲区指针。
Size: 你期望读取的数据字节数。
Timeout: 超时时间(毫秒)。
八、总结特点
软件IIC :
通过CPU直接控制GPIO引脚去生成IIC时序
优点:引脚配置更加的灵活 ,使用起来更加稳定可控
缺点:需要自己写时序函数,速度上限不如硬件IIC
硬件IIC:使用硬件IIC外设实现,CPU只需要将要发送的数据传给IIC外设,时序由IIC外设借助GPIO引脚自动生成
优点是速度上限高,使用简单方便
缺点:引脚太固定了,IIC外设容易死机