☰
IIC总线时序详解与软硬件实现取舍
2026/10/15 5:26:52 网站建设 项目流程

简介:一份面向嵌入式开发者的IIC通信协议详解文档,特别适合刚接触单片机和总线协议的读者。文档从IIC两线式串行总线的基本概念出发,说明SDA数据线与SCL时钟线的作用,并比较软件IIC与硬件IIC的异同:前者用普通IO口模拟时序,灵活但效率较低;后者依赖芯片I2C外设,速度更快但管脚固定。随后详细分析了通信过程中的开始、停止、发送、响应、接收五个环节,包括空闲状态下SDA和SCL同时为高电平、开始信号与停止信号的电平跳变规则,以及应答信号ACK/NACK的判断方法。同时强调IIC为半双工通信,在SCL高电平期间SDA须保持稳定,只有SCL拉低时数据才能变化。文中还提供软件模拟IIC的起始、停止、应答、发送字节等示例代码,并提及高速IIC总线一般可达400kbs以上,方便理解时序、传输速率和实际编写驱动。资源包内为1个docx文件,大小约81KB。这份文档已有1069人学习下载,内容逐步展开、结构清晰,可作为协议学习笔记和开发时的速查参考。

1. IIC 两线式串行总线:读懂时序纪律比背代码更关键

IIC 两线式串行总线,最容易被低估的是时序纪律:SCL 高电平期间 SDA 必须保持稳定,数据只能在 SCL 低电平时改变。很多人背得下这句话,真到示波器上抓起始信号时还是心里没底。这份 IIC 通信协议详解把 CPU 与被控 IC 之间用 SDA 和 SCL 双向传输的完整流程拆成五个环节:开始、停止、应答、发送、接收,并为每个环节给出可直接改用的 C 驱动。对刚接触嵌入式的开发者,它解决的是“寄存器配置都对,却读不回传感器数据”的常见困境;对想从软件模拟切到硬件外设的熟手,它也把两种实现的工作方式和适用边界讲清楚了。适合所有和 I2C 设备打交道的人案头备一份。

2. 软件IIC与硬件IIC二选一:算力、管脚与实时性的取舍

选择软件 IIC 还是硬件 IIC,不是哪个“高级”选哪个。从接口灵活性看,软件 IIC 只占两个普通 GPIO,想换引脚就换引脚;从 CPU 效率看,硬件 IIC 由芯片内部的 I2C 外设直接驱动波形,CPU 负担小。常见错误是一上来就用硬件外设,结果被引脚复用表卡住,调试时感觉像对着黑匣子;也有反过来的,软件 IIC 跑得很顺,换了一批高速设备后速度跟不上。先弄清两者的机制再做选择,能少走很多弯路。

2.1 硬件IIC:专用外设与寄存器配置的优势

硬件 IIC 对应芯片上的 I2C 外设,SDA 和 SCL 是专用管脚,外设内部有驱动电路和移位寄存器。CPU 只需要配置外设、向寄存器写入数据、等待标志位;发送时外设自己产生 SCL 脉冲,并在第 9 个时钟周期检测从机应答。高速 IIC 总线一般可达 400kbps 以上,若配合中断或 DMA,一次连续读几十字节几乎不占 CPU,适合数据采集频率高的场景。

代价是配置成本。硬件 IIC 需要翻芯片手册确认引脚复用功能,初始化外设时要设置时钟频率、占空比、地址模式等。不同芯片的寄存器命名差异很大,厂家提供的初始化库也不完全一样,移植代码时经常卡在引脚复用表上。

硬件 IIC 有个经典现象:总线上发生异常时序时,外设状态机可能进入忙状态,之后发数据不响应。多数芯片需要复位外设甚至重新初始化才能恢复;遇到这种情况只能重新配置外设,现场排错难度比软件 IIC 大。

2.2 软件IIC:GPIO模拟的灵活性与代价

软件 IIC 用两个通用 GPIO 模拟 IIC 波形,完全绕开 I2C 外设。发送位时拉高拉低 SCL 和 SDA,接收位时读引脚电平,全部由代码控制。优点是引脚不受限制,缺引脚时可以用任意 IO;调试时分步执行,可以清楚观察到每个信号变化,学习协议时优先推荐软件模拟。

代价是每位数据都要 CPU 亲自操作。发送一个字节要循环 8 次,每次更新 SDA、拉高 SCL、再拉低 SCL;接收时每读一位也要等时钟。系统里如果有频繁中断,延时函数会被打断,波形周期就飘了。

软件 IIC 的速率和延时强相关。通常用 delay_us 提供微秒级延时,延时太短,从机采样时 SDA 还没稳定;延时太长,速率上不去。常见做法是先按手册时序要求设一个起始值,再用逻辑分析仪确认。IIC 是半双工通信,同一时刻只能一个方向,软件模拟时最容易在方向切换上出错:写地址阶段 SDA 是输出,读数据阶段必须改成输入。调试时读到全 0xFF 或全 0x00,先检查是不是忘了切换。

2.3 选型:我一般会按这三个条件判断

在具体项目里,我一般先看三个条件。引脚是否富余:只要 GPIO 够用,优先软件 IIC,因为好移植、好调试。数据量是不是持续高速采集:如果是,硬件 IIC 配合 DMA 更合适。现场环境是否复杂:如果经常有热插拔或干扰,软件 IIC 在总线上恢复时可以靠代码强制释放,硬件 IIC 可能卡在外设状态里。

对比维度软件 IIC硬件 IIC
管脚占用任意两个 GPIO固定 I2C 专用引脚
CPU 占用每位数据都需 CPU 操作外设处理,配合 DMA 很低
速率上限跟随延时设置,受主频与控制精度制约可达 400kbps 以上
配置复杂度低,宏和延时函数即可需要查引脚复用与寄存器
调试可见性分步执行看得见过程外设内部过程像黑匣子
适用场景原型验证、引脚受限高速连续采集、低 CPU 占用

所以我的习惯是:新芯片第一次调 IIC,先用软件模拟把通信打通,确认从机地址和寄存器地址没问题后,再切到硬件外设。这样一来,后续遇到通信问题,至少能判断是电气层还是配置层的问题。

3. 时序驱动拆解:起始、停止、应答与收发字节的C实现

这一章把原资源里的 C 驱动逐段拆开。每段代码都是软件 IIC 的核心,硬件 IIC 寄存器操作虽然不同,但波形层面的要求完全一致。

3.1 空闲状态:SDA 与 SCL 同时为高

I2C 总线的空闲状态定义是 SDA 和 SCL 同时为高。此时所有设备的输出级场效应管都截止,两条信号线由上拉电阻拉高。工程上检查空闲状态很简单:不上设备时量引脚对地电压,应接近 VCC;如果量到低电平,先逐个断开可疑设备。这是所有时序流程的起点,也是排查总线卡死的第一反应。

3.2 起始与停止信号:找跳变沿,不找电平

起始信号是 SCL 为高期间 SDA 由高到低的跳变;停止信号是 SCL 为高期间 SDA 由低到高的跳变。两者都是跳变时序,而不是稳定电平。用逻辑分析仪看波形的时候,要盯的是边沿位置,而不是只看高电平。

void IIC_Start(void) { SDA_OUT(); // SDA 设为输出 IIC_SDA = 1; // 拉高 SDA,准备空闲 IIC_SCL = 1; // 拉高 SCL,进入空闲状态 delay_us(4); IIC_SDA = 0; // SCL 高期间,SDA 由高变低,产生起始信号 delay_us(4); IIC_SCL = 0; // 钳住总线,准备发送数据 }

IIC_Start 的逻辑顺序不能换。SDA_OUT() 先把 GPIO 切到输出模式;SDA 和 SCL 先同时拉高,确保总线空闲;然后只拉低 SDA,形成下降沿。这个下降沿必须在 SCL 高电平区间内,所以要先拉高 SCL 再拉低 SDA。最后拉低 SCL,是因为后续发送数据位时,SDA 只能在 SCL 低电平期间变化。delay_us(4) 是软件延时,用来保证信号电平稳定,具体数值取决于主频和从设备要求。

void IIC_Stop(void) { SDA_OUT(); // SDA 设为输出 IIC_SCL = 0; IIC_SDA = 0; delay_us(4); IIC_SCL = 1; // SCL 先回到高 IIC_SDA = 1; // SDA 在 SCL 高电平期间由低变高,产生停止信号 delay_us(4); }

停止信号的顺序同样有讲究。先把 SCL 拉低,此时允许 SDA 变化;再把 SDA 拉低;接着拉高 SCL;最后拉高 SDA。SDA 的上升沿出现在 SCL 高电平区间内,从机看到这个边沿就知道数据传输结束。如果把 SDA 拉高放在 SCL 拉高之前,上升沿落在 SCL 低电平区间,从机不会认为传输结束。

3.3 数据位传输:SCL 低电平改SDA,高电平保持稳定

IIC 对每一位数据的采样规则是:SCL 高电平期间,从机采样 SDA。因此主机必须在 SCL 低电平期间改变 SDA,到高电平区间保持 SDA 不变。如果违反这条纪律,在 SCL 高电平期间出现 SDA 跳变,从机可能误判成起始或停止信号。

void IIC_Send_Byte(uint8_t txd) { uint8_t t; SDA_OUT(); IIC_SCL = 0; // 拉低时钟,允许改变 SDA for (t = 0; t < 8; t++) { if ((txd & 0x80) >> 7) IIC_SDA = 1; // 最高位为 1,SDA 拉高 else IIC_SDA = 0; txd <<= 1; // 左移一位,准备下一位 delay_us(2); IIC_SCL = 1; // 拉高 SCL,从机采样 SDA delay_us(2); IIC_SCL = 0; // 拉低 SCL,允许改变 SDA delay_us(2); } }

发送一个字节从最高位开始。txd & 0x80 取出最高位,>>7 变成 0 或 1,决定 SDA 电平;txd <<= 1 把下一位移到最高位。循环 8 次,每次一个 SCL 脉冲传一位。关键在三个 delay_us(2):第一次在 SDA 设置后等待电平稳定;第二次在 SCL 上升沿前等待,保证 SDA 已稳定;第三次在 SCL 下降沿后等待,给下一位的 SDA 改变留出时间。原文档里特别提到对某些设备这三个延时缺一不可,因为没有足够延时,SDA 在 SCL 高电平时还没稳定,采样就会出错。

3.4 应答信号:第9个时钟周期决定数据去留

主机每发完 8 个数据位,第 9 个时钟周期用来传应答位。从机拉低 SDA 表示 ACK,说明字节已成功接收;SDA 保持高表示 NACK,说明接收失败或设备忙。应答时钟仍由主机提供,很多初学者以为应答位是从机自己产生的时钟,其实从机只是在主机给出的脉冲里拉低或释放 SDA。

uint8_t IIC_Wait_Ack(void) { uint8_t ucErrTime = 0; SDA_IN(); // SDA 设为输入,等待从机拉低 IIC_SDA = 1; // 释放 SDA,由上拉电阻拉高 delay_us(1); IIC_SCL = 1; delay_us(1); while (READ_SDA) { // 读到高电平,说明没有 ACK ucErrTime++; if (ucErrTime > 250) { IIC_Stop(); // 超时,停止总线 return 1; // 返回无应答 } } IIC_SCL = 0; return 0; // 读到低电平,应答成功 }

IIC_Wait_Ack 用在主机发完地址或数据之后。SDA_IN() 把 SDA 切到输入,外部上拉电阻会把 SDA 拉高;如果从机成功接收字节,它会在第 9 个时钟前的低电平区间把 SDA 拉低。while 循环轮询 SDA 电平,ucErrTime 软件计数,超过 250 次则认为从机无应答,调用 IIC_Stop() 终止通信。这个超时计数值不是固定要求,根据实际运行时钟调整即可;如果系统主频高,计数循环跑得快,数值要相应增大。

主机作为接收方时也要发出应答。读最后一个字节前,主机要发 NACK 通知从机“不用再发了”:

void IIC_Ack(void) { IIC_SCL = 0; SDA_OUT(); IIC_SDA = 0; // 拉低 SDA,表示有效应答 delay_us(2); IIC_SCL = 1; delay_us(2); IIC_SCL = 0; } void IIC_NAck(void) { IIC_SCL = 0; SDA_OUT(); IIC_SDA = 1; // 保持高电平,表示非应答 delay_us(2); IIC_SCL = 1; delay_us(2); IIC_SCL = 0; }

这两个函数是“主机读从机”时用的。连续读多个字节时,每个字节后发 ACK,最后一个字节后发 NACK。IIC_NAck 让 SDA 保持高电平,从机看到非应答就不再发送下一个字节,主机随后可以发出停止信号。

3.5 读取字节:边采样边移位

接收数据和发送数据一样,一位一位收进来,最后拼成一个字节:

uint8_t 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; // SCL 高电平,从机驱动 SDA receive <<= 1; // 左移一位,腾出最低位 if (READ_SDA) receive++; // 读到 1,最低位置 1 delay_us(1); } if (!ack) IIC_NAck(); // 最后一个字节,发 NACK else IIC_Ack(); // 还需要继续读,发 ACK return receive; }

函数入口先 SDA_IN(),这是读阶段最容易漏的一步。SCL 拉高后主控制器读 SDA 引脚电平,读到的 0/1 依次移入 receive:先左移一位,再判断要不要加 1,结果形成高位在前的字节。读完 8 位后根据参数 ack 决定发 ACK 还是 NACK。参数 ack 是调用方控制的,比如连续读 4 个字节,前 3 次传 1,最后 1 次传 0。

4. 完整数据帧:把字节串成一次可靠传输

单独的字节函数只是砖块,真正交给从设备的是一整段帧。一次完整传输包含起始、从机地址、读写位、寄存器地址、数据、应答和停止。读寄存器时还要插入重复起始并切换 SDA 方向。这一章按读写两种场景把帧拆开。

4.1 从设备地址与读写位:先分清7位和8位写法

I2C 设备有一个 7 位地址,主机通信时发送地址字节,由 7 位地址加 1 位读写位组成。读写位为 0 表示写,为 1 表示读。代码里常见写法是:

#define DEV_ADDR 0x3C // 手册上的 7 位地址 uint8_t addr_w = (DEV_ADDR << 1) | 0; // 写地址 uint8_t addr_r = (DEV_ADDR << 1) | 1; // 读地址

把 7 位地址左移一位,最低位留给读写位。但有些器件手册直接给出 8 位地址,例如 0x78 这样的写地址,这时候代码里就不能再左移。实践中最快的确认方式:先看驱动库或官方例程里地址是怎么写的,再对照手册。

注意:很多芯片手册直接给出带读写位的 8 位地址,再左移一次就会寻址错误;确认地址格式比调代码更重要。

4.2 写寄存器:最简单的完整帧

写寄存器是 I2C 最常用的操作之一。向传感器配置寄存器写入 0x3C 的完整帧为:

帧段内容说明
StartSDA 在 SCL 高时由高到低起始条件
地址字节DEV_ADDR<<1,R/W=0寻址并声明写操作
ACK从机拉低 SDA地址匹配成功
寄存器地址内部寄存器编号后续数据写入目标
ACK从机拉低 SDA寄存器地址接收成功
数据字节要写入的值写入目标寄存器
ACK从机拉低 SDA数据接收成功
StopSDA 在 SCL 高时由低到高释放总线

驱动代码按顺序调用:

IIC_Start(); IIC_Send_Byte(addr_w); if (IIC_Wait_Ack()) { IIC_Stop(); return; } IIC_Send_Byte(reg_addr); if (IIC_Wait_Ack()) { IIC_Stop(); return; } IIC_Send_Byte(value); IIC_Wait_Ack(); IIC_Stop();

每发一个字节都等待 ACK,这样能尽早发现问题。地址字节 ACK 失败,通常是设备地址不对、设备没上电、SDA/SCL 缺少上拉;寄存器地址 ACK 失败,可能是该寄存器不存在或者从机正忙。停下来给出错误返回,比继续往下发强得多。

4.3 读寄存器:重复起始与方向切换

读数据比写多一步。主机必须先发设备地址+写位,再发寄存器地址,告诉从机“我要读哪个寄存器”。接着在从机返回数据之前,主机要重新发出一个起始信号,这个信号称为重复起始(Repeated START)。它不先发 STOP,而是直接把总线拉回起始状态,再发设备地址+读位。

IIC_Start(); IIC_Send_Byte(addr_w); IIC_Wait_Ack(); IIC_Send_Byte(reg_addr); IIC_Wait_Ack(); IIC_Start(); // 重复起始信号 IIC_Send_Byte(addr_r); IIC_Wait_Ack(); data = IIC_Read_Byte(0); // 最后一个字节发 NACK IIC_Stop();

代码里的第二次 IIC_Start() 就是重复起始。从机收到重复起始后,会认为当前传输重新开始,然后进入发送模式。此时主机把 SDA 切到输入,等待从机逐位发送数据。IIC_Read_Byte(0) 的参数 0 表示读完后发 NACK,告诉从机“这是最后一个字节,不用再发了”。如果这里误传 1 发 ACK,从机会继续发送下一字节,通信就会乱。

连续读多个字节时,前几个字节用 IIC_Read_Byte(1) 发 ACK,最后一个字节用 IIC_Read_Byte(0) 发 NACK:

for (i = 0; i < len; i++) { data[i] = IIC_Read_Byte(i < len - 1); // 最后一个传 0 } IIC_Stop();

这正是原文档中“主控接收器收到最后一个字节后发送 NACK,通知被控发送器结束数据发送,并释放 SDA 线”的落地写法。

4.4 主从状态机:角色切换是总线上最容易出的错

整个 IIC 通信过程里,主机会在发送者与接收者之间切换,SDA 引脚方向必须跟着切换。写阶段是输出,读阶段是输入。用软件模拟时,方向切换最容易漏,漏掉后的典型症状就是读回全 0xFF 或全 0x00。

通信阶段SDA 方向时钟提供者
起始、发送地址/数据输出主机
等待应答输入主机
读取数据输入主机
发送 ACK/NACK输出主机

时钟永远由主机提供,这条贯穿协议始终。从机只是拉高或拉低 SDA,SCL 始终由主机驱动。看波形时如果不知道哪根线该由谁驱动,按这个表去对照就能缩小范围。

停止信号之后的内部处理时间也不要忽略。很多传感器在收到 STOP 后会有一段内部处理时间,立刻发下一帧可能导致配置丢失。常见做法是 STOP 后加 1ms 以上延时,具体数值参考器件数据手册。

5. 避坑排查:IIC调试中的五个典型翻车现场

下面五条是实际调试中重复出现的问题。每条按现象、原因、解决展开,对照定位比反复猜快得多。

5.1 波形上升沿太缓:忘记上拉电阻或阻值过大

现象:示波器上 SCL 和 SDA 不是方波,上升沿拖得很长,数据偶尔对偶尔错,提高总线速率后失败的频率明显上升。

原因:IIC 是开漏结构,高电平不是由驱动器推上去的,而是靠上拉电阻把引脚拉到 VCC。如果上拉电阻没接或者阻值太大,RC 充电常数大,上升沿变缓;SCL 高电平期间 SDA 尚未稳定,从机采样就会得到错误电平。

解决:在 SCL 和 SDA 上各接一个上拉电阻到电源。3.3V 系统常用 2.2k~4.7k,5V 系统常用 4.7k~10k;总线设备数量多或速度要求高时适当减小阻值。上拉电阻不是玄学,是 RC 充放电的计算问题,算不清就先从 4.7k 开始试,再用波形确认。

5.2 从机一直不ACK:地址写法和设备地址对不上

现象:IIC_Wait_Ack() 一直返回 1,程序只能走 IIC_Stop() 退出,从机毫无反应。

原因:多半是设备地址搞混。手册给的是 8 位地址,代码却按 7 位地址左移了一位;或者地址本来就没对上,设备不在总线上。另外也要排除设备没上电、引脚虚焊等问题。

解决:先查手册确认地址是 7 位还是 8 位。如果手册写“0xA0 写入”,这是 8 位写地址,直接用;如果写“7 位地址 0x50”,代码里要左移一位再拼读写位。再用万用表量总线空闲电平,确认 SCL/SDA 有没有被拉低。

5.3 读数据全0xFF:SDA方向没切换

现象:写寄存器正常,读寄存器时返回的数据永远是 0xFF。

原因:IIC_Read_Byte() 之前没有把 SDA 设为输入。SDA 还处于输出模式,读引脚时读到的是自己驱动的电平,输出高就是读到 1,所以全是 0xFF。

解决:每个读函数入口调用 SDA_IN(),每个发送函数入口调用 SDA_OUT()。我一般把方向切换放到函数第一行,这样调用方不用额外操心。这个坑几乎每个写软件 IIC 的人都会踩一次,踩完之后就长记性了。

5.4 总线被拉死:SDA或SCL持续为低

现象:逻辑分析仪显示 SDA 一直为低,主机执行 STOP 也拉不上去,总线整个卡住。

原因:某个从机状态机异常,把 SDA 拉住了;或者地址错误导致从机一直等待后续数据,始终不肯释放总线。

解决:第一步断开受控设备电源,确认释放能不能恢复。第二步尝试“时钟恢复”技巧:让主机持续输出 9 个 SCL 脉冲(不发送数据),让卡死的从机走完自己的接收流程,从而释放 SDA。做这个操作时 SDA 要切到输入,避免主机把 SDA 拉死。这个方法并不能保证所有器件都适用,但比直接复位的代价小,值得先试。

5.5 降速才稳定:400k速率下的时序余量不足

现象:100kHz 通信正常,把总线速率提升到 400kHz 后偶尔丢字节,ACK 错乱。

原因:总线上挂的设备多,总线电容变大;上拉电阻偏大,上升沿受限;软件 IIC 的延时又调得太短,SDA 还没稳定 SCL 就拉高了。

解决:先用 100kHz 验证功能逻辑,再逐步提速。必须跑 400kHz 时,减小上拉电阻、缩短排线长度、减少总线从设备数量,或者改用硬件 IIC + DMA。软件 IIC 提速不能只改 SCL 周期,SDA 变化后到 SCL 上升沿之间的稳定时间也要留够,逐段延时用逻辑分析仪确认。

6. 验证方法:逻辑分析仪抓时序的三个习惯

拿到新的 IIC 驱动,我先不上业务逻辑,直接挂逻辑分析仪抓波形。IIC 时序对刚上手的人像个黑匣子:程序跑完不知道从机有没有正确应答,只知道结果不对。逻辑分析仪不贵,采样率 20MHz 以上就足够看 100kHz 和 400kHz 的总线。SCL 接一个通道,SDA 接另一个通道,共地之后抓一次从 START 到 STOP 的完整帧。

抓完波形我固定检查三处。第一,总线空闲时两根线都应该是高,说明上拉正常;第二,START 下降沿和 STOP 上升沿都发生在 SCL 高电平期间;第三,第 9 个 SCL 脉冲上 SDA 是低还是高,低是 ACK,高是 NACK。数据字节的位序也从解析窗口确认,通常地址在前、数据高位在前。

检查点正常结果
总线空闲SDA 和 SCL 均为高
STARTSCL 高时 SDA 下降沿
地址字节8 个 SCL 脉冲,地址+读写位
ACK 位第 9 个脉冲 SDA 为低
数据位高位先发,SCL 高时 SDA 不变
STOPSCL 高时 SDA 上升沿

从那以后,我每次接新传感器都会强迫自己先抓一帧完整波形再写业务代码。写过软件 IIC 的人都知道,驱动跑不通,十有八九是时序问题而不是传感器坏了。最折腾的一次,我查了两天发现只是上拉电阻用了 10k,还配了一根长杜邦线,换上 2.2k 后所有问题消失。从那以后我的习惯变成了:硬件先量、波形先抓、代码再写。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询