STM32 IIC通信实战:从时序图到软件模拟与硬件外设详解

📅 2026/8/7 15:05:36
STM32 IIC通信实战:从时序图到软件模拟与硬件外设详解
1. 项目概述深入理解STM32的IIC通信搞嵌入式开发尤其是用STM32IIC总线绝对是绕不开的一道坎。它只有两根线硬件连接简单但时序逻辑和软件配置却常常让新手感到头疼。网上资料很多但要么是照搬协议文档要么是代码片段零散真正能把“时序图”和“软件源码”结合起来讲清楚每一步为什么这么做的实战分享并不多。今天我就结合自己这些年调试各种传感器从EEPROM到OLED屏的经验把STM32的IIC通信从硬件时序到软件实现彻底拆解一遍。无论你是刚接触STM32还是在调试IIC设备时遇到了通信失败、应答异常等问题这篇文章都能给你提供一套清晰的排查思路和可直接复用的代码框架。IIC也叫I2C是一种同步、半双工、多主多从的串行通信总线。它的精髓全在那两根线上SDA数据线和SCL时钟线。所有设备都挂在这两根线上靠地址来区分。理解IIC核心就是理解它的时序图——那就像一份交通规则规定了启动、停止、发送数据、接收应答的每一个脉冲高低电平的顺序。而软件源码就是我们在MCU上模拟或控制硬件去严格遵守这份规则的过程。下面我们就从最根本的时序规则开始一步步深入到STM32的软件实现。2. IIC协议核心与时序图深度解析很多教程一上来就讲STM32的IIC库函数怎么用但如果不明白底层时序一旦出问题根本无从下手。所以我们必须先成为“交通规则”的专家。2.1 IIC总线的基本时序单元IIC总线的所有操作都是由几个最基本的时序单元组合而成的。我们可以把它想象成一套严谨的“电报”协议。起始条件S与停止条件P这是通信的开始和结束标志。起始条件是当SCL线为高电平时SDA线发生一个从高到低的下降沿。停止条件则相反是当SCL为高电平时SDA线发生一个从低到高的上升沿。这里有个关键点在SCL高电平期间SDA的变化通常用于表示起始和停止在数据传输期间SDA的变化必须发生在SCL为低电平时。这个规则一定要记牢它是保证数据稳定的基础。数据传输与应答起始信号之后就开始传输数据。每个字节8位的数据传输后都必须跟一个应答位ACK或非应答位NACK。数据位在SCL高电平期间必须保持稳定发送方主设备在SCL低电平时可以改变SDA的数据。传输完8个比特后主设备会释放SDA线将其设置为高电平并在第9个时钟脉冲期间检测SDA线是否被从设备拉低。如果被拉低表示从设备成功接收并给出了应答ACK如果保持高电平则表示非应答NACK可能是从设备未就绪、地址错误或通信故障。寻址字节起始信号后的第一个字节一定是寻址字节。这个字节的高7位是从设备的硬件地址最低位是读写控制位0表示主设备要向从设备写入数据1表示主设备要从从设备读取数据。例如一个7位地址为0x50的EEPROM进行写操作时主设备发出的寻址字节就是0xA0(0x50 1 | 0)读操作时就是0xA1(0x50 1 | 1)。2.2 关键时序参数与硬件设计要点时序图不仅仅是高低电平的顺序还包含了严格的时间要求。如果MCU的IO口翻转速度不满足这些要求通信就会失败。以下是几个最关键的参数SCL时钟频率标准模式为100kHz快速模式为400kHz高速模式可达3.4MHz。STM32的硬件IIC外设可以方便配置。如果是软件模拟GPIO模拟则需要通过延时函数来控制SCL的频率。建立时间和保持时间这是最容易出问题的地方。数据建立时间tSU;DAT指的是SDA数据线在SCL上升沿到来之前必须保持稳定的最短时间。数据保持时间tHD;DAT指的是SCL下降沿之后SDA数据线还必须保持稳定的最短时间。在软件模拟IIC时我们需要在SCL变化前后插入足够的延时以满足这些时间要求特别是当总线上有多个设备、存在容性负载时信号边沿会变缓更需要留足余量。注意很多初学者用软件模拟IIC通信失败问题就出在延时不够或逻辑顺序错误上。例如在改变SDA数据后没有等待一个短暂的稳定时间就拉高了SCL这可能导致从设备在SCL上升沿采样到的是不稳定数据。总线空闲状态IIC总线规定空闲时SCL和SDA线都必须通过上拉电阻保持高电平。这个上拉电阻的阻值选择有讲究通常在4.7kΩ到10kΩ之间。阻值太小电流大功耗高阻值太大上升沿太慢在高速模式下可能无法满足上升时间要求。如果总线上设备较多、走线较长等效电容大可能需要减小上拉电阻值如用2.2kΩ来加快上升速度。3. STM32的IIC实现方案硬件外设与软件模拟对比在STM32上实现IIC主要有两种方式使用硬件IIC外设或者用普通GPIO口进行软件模拟。两种方式各有优劣选择哪种取决于项目需求。3.1 硬件IIC外设以STM32F1/F4的I2C为例STM32芯片内部集成了I2C外设它能够自动处理起始、停止、应答、时钟拉伸等底层时序大大减轻CPU负担。以STM32标准外设库或HAL库为例其配置流程通常是结构化的。配置步骤简述时钟使能开启对应I2C总线和所用GPIO口的时钟。GPIO配置将对应的SCL和SDA引脚如STM32F103的PB6/PB7配置为复用开漏输出模式。必须选择开漏模式并配合外部上拉电阻这样才能实现“线与”功能多个设备才能挂在同一总线上。I2C参数初始化设置时钟频率如100kHz或400kHz、自身设备地址主模式通常不设、应答使能、地址模式7位/10位等。使能I2C外设。硬件IIC的优缺点优点效率高不占用CPU时间进行延时等待稳定性好由硬件确保时序精确支持中断和DMA适合大数据量传输。缺点不同系列STM32的I2C外设行为可能有差异特别是早期的F1系列I2C外设在某些错误状态下容易卡死需要复杂的错误恢复机制比如先执行一遍软件复位。调试起来相对复杂。3.2 软件模拟IICGPIO模拟时序软件模拟就是用两个普通的GPIO口分别模拟SCL和SDA线通过代码控制它们的高低电平变化和延时来“画”出符合IIC协议的时序图。这是最灵活、最通用、也最有助于理解协议本质的方法。软件模拟的核心函数通常需要编写以下几个最基础的函数IIC_Start()产生起始条件。IIC_Stop()产生停止条件。IIC_SendByte(uint8_t byte)发送一个字节。IIC_ReadByte(uint8_t ack)读取一个字节参数ack用于决定读取后返回的是ACK还是NACK。IIC_Wait_Ack()等待从机应答。IIC_Ack()/IIC_NAck()产生应答或非应答信号。软件模拟的优缺点优点移植性极强任何有GPIO的MCU都能用时序完全可控便于调试和排查问题不受特定芯片I2C外设Bug的影响。缺点占用CPU资源通信期间CPU被阻塞在延时循环里时序精度受系统主频和中断影响在高速或实时性要求高的场合不适用。实操心得对于大多数应用如读取温湿度传感器、配置外围芯片参数等通信频率不高100kHz数据量不大我强烈推荐使用软件模拟IIC。它简单、可靠、易调试。只有当需要高速400kHz以上或大量连续数据传输如读取图像传感器时才考虑使用硬件IICDMA的方案。4. 软件模拟IIC源码逐行解析与实战下面我以一个通用的、针对STM32的软件模拟IIC驱动代码为例进行逐行解析。假设我们使用PB6作为SCLPB7作为SDA。4.1 引脚初始化与基础宏定义首先我们需要定义引脚和配置基本的输入输出模式。// iic_sw.h #ifndef __IIC_SW_H #define __IIC_SW_H #include stm32f1xx_hal.h // 根据你的芯片系列包含对应头文件 // 引脚定义 #define IIC_SCL_PORT GPIOB #define IIC_SCL_PIN GPIO_PIN_6 #define IIC_SDA_PORT GPIOB #define IIC_SDA_PIN GPIO_PIN_7 // 电平操作宏提高代码可读性和执行效率 #define IIC_SCL_H() HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET) #define IIC_SCL_L() HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET) #define IIC_SDA_H() HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET) #define IIC_SDA_L() HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_RESET) #define IIC_SDA_READ() HAL_GPIO_ReadPin(IIC_SDA_PORT, IIC_SDA_PIN) // 微小延时函数用于控制时序。延时时间需根据主频调整。 #define IIC_Delay() for(volatile int i0; i10; i) // 示例实际需校准 // 函数声明 void IIC_Init(void); void IIC_Start(void); void IIC_Stop(void); void IIC_SendByte(uint8_t byte); uint8_t IIC_ReadByte(uint8_t ack); uint8_t IIC_Wait_Ack(void); void IIC_Ack(void); void IIC_NAck(void); #endif// iic_sw.c #include iic_sw.h // 初始化函数配置引脚为开漏输出并置高释放总线 void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; // 使能GPIO时钟如果之前未使能 __HAL_RCC_GPIOB_CLK_ENABLE(); // 配置SCL和SDA为开漏输出模式初始状态置高 GPIO_InitStruct.Pin IIC_SCL_PIN | IIC_SDA_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出是关键 GPIO_InitStruct.Pull GPIO_NOPULL; // 外部已接上拉电阻内部不使能上下拉 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; // 高速模式翻转更快 HAL_GPIO_Init(IIC_SCL_PORT, GPIO_InitStruct); // 释放总线让SCL和SDA处于空闲高电平状态 IIC_SCL_H(); IIC_SDA_H(); IIC_Delay(); }关键点解析GPIO_MODE_OUTPUT_OD开漏输出这是必须的。开漏模式下MCU只能将引脚拉低输出0或释放高阻态由外部上拉电阻拉高。这完美实现了IIC总线的“线与”逻辑避免了多个设备同时输出高电平时可能产生的冲突。初始状态置高初始化后立即将两根线拉高确保总线处于空闲状态这是IIC协议规定的。IIC_Delay()宏这是一个需要根据你的系统主频如72MHz进行精确校准的延时。太短可能不满足从设备时序要求太长则降低通信速率。可以通过示波器观察波形来调整循环次数使其满足目标频率如100kHz对应周期10us下的建立和保持时间。4.2 核心时序函数实现接下来是实现最核心的时序函数。请结合前面的时序图理解每一行代码。// 产生起始信号SCL高电平期间SDA产生下降沿 void IIC_Start(void) { IIC_SDA_H(); IIC_SCL_H(); IIC_Delay(); // 确保SDA和SCL稳定在高电平一段时间 IIC_SDA_L(); // SDA拉低产生下降沿 IIC_Delay(); IIC_SCL_L(); // 随后拉低SCL为后续发送数据做准备 IIC_Delay(); }// 产生停止信号SCL高电平期间SDA产生上升沿 void IIC_Stop(void) { IIC_SDA_L(); // 先确保SDA是低电平 IIC_SCL_H(); IIC_Delay(); IIC_SDA_H(); // SDA拉高产生上升沿 IIC_Delay(); // 停止信号后总线进入空闲 }// 发送一个字节8位数据 void IIC_SendByte(uint8_t byte) { uint8_t i; for (i 0; i 8; i) { if (byte 0x80) { // 判断最高位MSB first IIC_SDA_H(); } else { IIC_SDA_L(); } IIC_Delay(); IIC_SCL_H(); // 拉高SCL从设备在此时钟上升沿采样SDA IIC_Delay(); // 保持SCL高电平确保采样稳定 IIC_SCL_L(); // 拉低SCL允许SDA在低电平期间变化 IIC_Delay(); byte 1; // 左移准备发送下一位 } // 发送完8位后释放SDA线准备接收应答 IIC_SDA_H(); }// 等待从机应答 // 返回值0-收到ACK1-收到NACK或超时 uint8_t IIC_Wait_Ack(void) { uint8_t timeout 0; IIC_SDA_H(); // 主机释放SDA线设置为输入模式实际上开漏输出下写1即释放 IIC_Delay(); IIC_SCL_H(); // 拉高SCL IIC_Delay(); // 读取SDA线状态如果从机应答会将其拉低 while (IIC_SDA_READ() GPIO_PIN_SET) { timeout; if (timeout 250) { // 简单超时机制防止死等 IIC_Stop(); // 超时则产生停止信号 return 1; // 返回NACK } } IIC_SCL_L(); // 收到ACK拉低SCL结束应答周期 IIC_Delay(); return 0; // 返回ACK }// 读取一个字节 // 参数ack0-读取后发送ACK通知从机继续发1-读取后发送NACK通知从机停止发送 uint8_t IIC_ReadByte(uint8_t ack) { uint8_t i, byte 0; IIC_SDA_H(); // 确保主机释放SDA让从机控制 for (i 0; i 8; i) { byte 1; // 先左移第一次左移0无影响 IIC_SCL_H(); IIC_Delay(); if (IIC_SDA_READ() GPIO_PIN_SET) { byte | 0x01; // 读取到高电平对应位为1 } // 注意这里读取到的是SDA在SCL高电平期间的稳定值 IIC_SCL_L(); IIC_Delay(); } // 发送应答位 if (ack) { IIC_NAck(); // 发送NACK } else { IIC_Ack(); // 发送ACK } return byte; } // 产生应答信号ACK void IIC_Ack(void) { IIC_SDA_L(); // 主机拉低SDA表示ACK IIC_Delay(); IIC_SCL_H(); IIC_Delay(); IIC_SCL_L(); IIC_Delay(); IIC_SDA_H(); // 释放SDA恢复为可输入状态 } // 产生非应答信号NACK void IIC_NAck(void) { IIC_SDA_H(); // 主机保持SDA为高表示NACK IIC_Delay(); IIC_SCL_H(); IIC_Delay(); IIC_SCL_L(); IIC_Delay(); }4.3 实战应用读写AT24Cxx系列EEPROM有了上面的基础函数我们就可以组合它们来完成具体的设备操作。以常见的AT24C02256字节EEPROM为例其设备地址是0xA0写/0xA1读。向EEPROM指定地址写入一个字节/** * brief 向AT24Cxx指定地址写入一个字节 * param addr: 要写入的EEPROM内部地址 (0-255 for AT24C02) * param data: 要写入的数据 * retval 0: 成功, 1: 失败未收到应答 */ uint8_t AT24Cxx_WriteByte(uint16_t addr, uint8_t data) { IIC_Start(); if (IIC_SendByte(0xA0) IIC_Wait_Ack()) { // 发送设备地址写 IIC_Stop(); return 1; // 设备无应答 } // 发送要写入的存储地址对于AT24C02是8位地址 if (IIC_SendByte((uint8_t)addr) IIC_Wait_Ack()) { IIC_Stop(); return 1; } // 发送要写入的数据 if (IIC_SendByte(data) IIC_Wait_Ack()) { IIC_Stop(); return 1; } IIC_Stop(); // 重要EEPROM写入需要一定时间页写周期典型5ms // 必须等待写入完成期间发送查询ACK直到设备应答 HAL_Delay(5); // 简单延时等待更优做法是发送Start设备地址查询ACK // 更健壮的做法 // uint8_t timeout 200; // 约200ms超时 // while(timeout--) { // IIC_Start(); // if (IIC_SendByte(0xA0) 0) { // 发送设备地址如果收到ACK // IIC_Stop(); // break; // 写入完成跳出循环 // } // IIC_Stop(); // HAL_Delay(1); // } // if(timeout 0) return 1; // 超时错误 return 0; // 成功 }从EEPROM指定地址读取一个字节/** * brief 从AT24Cxx指定地址读取一个字节 * param addr: 要读取的EEPROM内部地址 * retval 读取到的数据 */ uint8_t AT24Cxx_ReadByte(uint16_t addr) { uint8_t data 0; // 先执行一个“哑写”操作发送要读取的地址 IIC_Start(); if (IIC_SendByte(0xA0) IIC_Wait_Ack()) { // 发送设备地址写 IIC_Stop(); return 0xFF; // 错误返回0xFF } if (IIC_SendByte((uint8_t)addr) IIC_Wait_Ack()) { // 发送内存地址 IIC_Stop(); return 0xFF; } // 重新启动总线发送读命令 IIC_Start(); if (IIC_SendByte(0xA1) IIC_Wait_Ack()) { // 发送设备地址读 IIC_Stop(); return 0xFF; } // 读取数据并发送NACK表示读取结束 data IIC_ReadByte(1); // 参数1表示读取后发送NACK IIC_Stop(); return data; }5. 硬件IIC外设配置流程与HAL库应用虽然软件模拟通用性强但了解硬件IIC的配置也很有必要尤其是在需要高效率的场景。这里以STM32CubeMX生成HAL库代码为例简述流程。5.1 CubeMX图形化配置在Pinout Configuration界面找到I2C模块如I2C1。将模式设置为I2C。右侧会自动分配SCL和SDA引脚如PB6, PB7。检查确认。在Configuration标签页的I2C参数设置中Clock Speed选择通信速度如Standard Mode (100kHz)。Duty Cycle快速模式400kHz下才有用保持默认。Own Address 1如果STM32也要作为从机被访问才需要设置。主模式通常设为0。Address Width选择7-bit大多数设备都是7位地址。General Call Callback一般禁用。生成代码。5.2 HAL库关键函数解析生成的代码会初始化好I2C外设。我们主要使用以下几个HAL函数阻塞式传输最简单HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);DevAddress从设备地址注意HAL库要求传入左移一位后的地址即0x50 1。pData数据缓冲区指针。Size数据大小。Timeout超时时间毫秒。示例向地址0x50的EEPROM写入一个字节data到地址mem_addr。uint8_t buffer[2] {mem_addr, data}; HAL_I2C_Master_Transmit(hi2c1, 0xA0, buffer, 2, 100); // 0xA0 (0x50 1) | 0带内存地址的读写更常用HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);MemAddress从设备内部寄存器或存储地址。MemAddSize内部地址大小I2C_MEMADD_SIZE_8BIT或I2C_MEMADD_SIZE_16BIT。示例从地址0x50的EEPROM的mem_addr地址读取一个字节。uint8_t rx_data; HAL_I2C_Mem_Read(hi2c1, 0xA0, mem_addr, I2C_MEMADD_SIZE_8BIT, rx_data, 1, 100);注意事项HAL库的硬件I2C函数虽然方便但在某些复杂错误如总线被意外拉低后可能会进入错误状态锁死。一个常见的补救措施是在调用传输函数后检查返回值如果多次返回HAL_ERROR或HAL_TIMEOUT可以考虑调用HAL_I2C_DeInit()和HAL_I2C_Init()重新初始化I2C外设或者先执行__HAL_I2C_CLEAR_FLAG()清除错误标志。6. IIC通信调试与常见问题排查实录调试IIC通信逻辑分析仪或者示波器几乎是必备的。它能让你直观地看到SDA和SCL线上的波形对照时序图一切问题都无所遁形。下面是我总结的几个典型问题场景和排查步骤。6.1 问题一通信完全无响应ACK总是失败现象发送起始信号和设备地址后始终等不到ACKIIC_Wait_Ack超时。排查步骤检查硬件连接这是第一步也是最容易出错的一步。用万用表测量SCL和SDA线对地电压空闲时是否都为高电平约3.3V上拉电阻是否焊接正确线路是否虚焊或短路确认从设备地址仔细核对芯片数据手册。很多设备有多个地址选择引脚如A0, A1, A2这些引脚接高电平VCC还是低电平GND决定了地址的最后几位。例如AT24C02的地址是1010xxx如果A2,A1,A0都接地写地址就是0xA0。一个常见的错误是忽略了地址左移一位和读写位的组合。用示波器/逻辑分析仪抓取波形看起始信号是否标准SCL高时SDA下降沿。看发送的地址字节数据是否正确每个比特的波形是否清晰。看在第9个时钟周期ACK位SDA线是否被从设备拉低。如果一直为高说明从设备没响应。检查电源和复位从设备是否已正常上电复位引脚电平是否正确有些传感器需要特定的初始化序列。检查软件模拟时序如果用的是软件模拟重点检查IIC_Delay()的时长。延时太短可能不满足从设备的最小建立/保持时间要求。可以用示波器测量SCL周期调整延时使其接近100kHz。6.2 问题二能收到ACK但读写数据错误现象地址应答正常但读取或写入的数据是乱码或者写入后读回的数据不对。排查步骤检查数据字节的波形用逻辑分析仪查看发送和接收的每一个数据位。是否在SCL高电平期间SDA数据有毛刺或不稳定这可能是GPIO速度设置或外部干扰导致的。检查ACK/NACK的发送时机在读取多个字节时除了最后一个字节主机在读取每个字节后都应发送ACK拉低SDA。读取最后一个字节后应发送NACK保持SDA高然后发送停止信号。顺序错误会导致从设备行为异常。检查从设备的页写限制像EEPROM这类设备有“页写”功能一次连续写入不能跨页。例如一页是8字节如果你从地址0开始连续写入10个字节后2个字节会从本页开头地址0覆盖写入而不是写到地址8和9。必须注意分页写入。检查写入后的等待时间EEPROM、Flash等存储器件在写入后需要一个“写周期”时间t~WR~通常是5ms才能进行下一次操作。在这期间发送命令设备不会应答。必须在写入后加入足够延时或者使用“查询ACK”的方式等待写入完成如前面代码注释所示。6.3 问题三通信不稳定时好时坏现象大部分时间通信正常偶尔会失败复位后可能又好了。排查步骤检查总线竞争和干扰总线上是否有多个主设备软件上是否有竞争确保通信过程是原子的不被中断打断。可以在关键的IIC通信函数前后加上关中断/开中断操作。void IIC_WriteBytes(...) { __disable_irq(); // 关中断 // ... IIC通信代码 ... __enable_irq(); // 开中断 }检查电源稳定性用示波器观察MCU和从设备的电源引脚是否有大的毛刺或跌落不稳定的电源会导致逻辑错误。检查上拉电阻和布线总线长度是否过长线间是否有平行走线引入串扰可以尝试减小上拉电阻值如从10kΩ换成4.7kΩ增强驱动能力加快上升沿。检查软件模拟的时序容错性适当增加IIC_Delay()中的延时给总线状态变化留出更多稳定时间。特别是在起始、停止和ACK/NACK位这些关键位置。6.4 调试工具与技巧速查表工具/方法用途关键观察点逻辑分析仪首选工具可视化分析时序。1. 起始/停止信号是否标准。2. 地址字节、数据字节的每一位是否正确。3. ACK/NACK位电平是否正确。4. SCL频率是否符合预期。示波器观察信号质量测量边沿时间、电压幅值。1. 信号上升/下降沿是否陡峭有无过缓。2. 高电平是否达到VCC低电平是否接近0V。3. 有无过冲、振铃或毛刺干扰。万用表快速检查静态电平、短路、断路。1. 空闲时SCL/SDA是否为高电平约VCC。2. 上拉电阻两端电压是否正常。软件调试单步跟踪检查变量和返回值。1. 发送的地址、数据值是否正确。2.IIC_Wait_Ack()等函数的返回值。3. 超时计数器是否触发。我个人在调试中最深刻的体会是90%的IIC通信问题都能通过逻辑分析仪抓到波形后对照数据手册的时序图找到原因。剩下10%可能是电源、复位或者芯片本身损坏的问题。所以投资一个简单的逻辑分析仪几十块钱的就行对于嵌入式开发来说绝对是事半功倍。最后再分享一个软件模拟IIC的小技巧把IIC_Delay()函数做成一个可调节的参数比如定义一个全局变量IIC_Delay_Time然后在需要延时的地方循环这个变量次数。这样你可以在不同主频的MCU上或者调试不同速度要求的设备时通过修改这一个参数来快速调整整个通信的时序而不用去改每一处的延时循环次数非常方便。