I2C诞生于上世纪80年代初,由飞利浦(现在的恩智浦NXP)为解决微控制器与外围芯片之间繁琐的连接问题而设计。
仅仅两根线——SCL(时钟线)和SDA(数据线),就能实现多设备间的双向通信。
I2C通信协议可以用普通GPIO引脚进行软件模拟。
I2C接口主要用于通讯速率要求不高,以及多个器件之间通信的应用场景。
1. I2C介绍
1.1 特点
- 双线制总线:I2C仅使用两条线——串行数据线(SDA)和串行时钟线(SCL)进行通信,有效降低了连接复杂性。
- 多主多从:I2C支持多个主设备和多个从设备连接到同一总线上。每个设备都有唯一的地址。总线仲裁机制保证同一时刻只有一个主设备控制总线。
- 不同的传输速率:I2C总线支持不同的速率模式,如标准模式(100kbps)、快速模式(400kbps)和高速模式(3.4Mbps)。
- 同步通信:I2C是一种同步通信协议,数据传输由时钟信号(SCL)来控制(区别于UART异步通信)。
- 简单的连接:I2C通信对硬件的要求比较低,只需两根线和上拉电阻。
- 地址唯一:每个I2C设备都通过一个7位或10位的地址来识别(常用7位),主设备通过地址识别并选择具体的从设备进行通信。
- 半双工通信:数据线为双向线,但在同一时刻只能单向传输数据。
- 支持广播和点对点通信:可以向所有设备广播命令,也可以指定单个设备通信。
- 阻塞传输:I2C支持阻塞传输机制,即主设备可以在传输过程中控制总线,防止其他设备发送数据。
- 通信协议简单:包括起始信号、地址帧、读写位、应答位、数据帧、停止信号。
- 应用广泛:由于其简单和灵活性,I2C被广泛应用于各种电子产品中,如传感器、LCD显示器、EEPROM等。
- 总线仲裁和冲突检测:在多主模式下,I2C能够处理多个主设备同时尝试控制总线的情况。
- 低功耗:I2C总线的设计使其成为低功耗的通信方式,适用于电池供电的设备。
物理连接,注意看5V和上拉电阻:
两个信号线特点:
这两条线都是 双向 的,并且采用 开漏(Open-Drain) 或 开集(Open-Collector) 输出。
当配置为开漏输出(Open-drain Output)时,单片机可以输出「低电平」(GPIO_PIN_RESET)或「高阻态」(GPIO_PIN_SET),高阻态下,相当于断开。这意味着任何设备都只能将信号线拉低到GND,但不能主动输出高电平。为了使信号线能呈现高电平,SCL和SDA线路上都必须连接一个 上拉电阻(Pull-up Resistor) 到VCC,且设备只能释放总线,让线通过外部上拉电阻被拉到高电平。
1.2 为什么必须使用上拉电阻
- 保护电路:通过开漏结构,允许多个设备同时连接且不会因为某个设备输出高低电平冲突而损坏电路。
- 线与(Wired-AND)逻辑: 当多个设备同时连接到总线上时,只要有一个设备将线路拉低,整个线路就呈现低电平。只有当所有设备都“释放”线路(即高阻态)时,线路才会在上拉电阻的作用下恢复到高电平。
- 多主控仲裁: 如果两个主设备同时试图控制总线并发送数据,它们会监测SDA线。当一个主设备发送高电平(释放总线拉高电平)而另一个发送低电平(拉低总线)时,发送高电平的主设备会检测到SDA线与自己发送的电平不符,便会知道总线上存在冲突,并立即放弃对总线的控制,从而保证了数据传输的完整性。
1.3 I2C协议时序
起始条件 (START): 主设备发起通信。SCL保持
高电平
期间,SDA从高电平
变为低电平
。停止条件 (STOP): 当SCL保持
高电平
期间,SDA从低电平
跳变为高电平
。同样,只有主设备才能产生停止条件。
位数据传输: I²C对数据传输的时序有严格规定。在SCL为
高电平
期间,SDA上的数据必须保持稳定,不能有任何变化。SDA的电平变化(即数据位的翻转)只能在SCL为 低电平 时进行。每字节8位数据,最高位先发。
应答 (ACK) 与非应答 (NACK): 这是I²C通信中的“确认”与“拒绝”机制。
- ACK (Acknowledge): 发送方每发送8位(一个字节)数据后,接收方会在第9个时钟周期将SDA线拉低,表示“我收到了,请继续!”。
- NACK (Not Acknowledge): 接收方在第9个时钟周期保持SDA线为高电平,表示“我没收到”、“我忙着呢”、“数据不对劲”或者“我读完了,不用再发了”。这个NACK信号在主机读数据时尤为重要,它告诉从机“我不要更多数据了”。
传输的数据帧结构:
写入流程如下,比较简单,先写地址,再写数据即可:
读取一个地址的数据流程稍显复杂,因为中间会经历一次写/读的转换:
REPEATED START (重复起始条件): 这是读操作的精髓!在不发送STOP信号的情况下,主机再次发送一个START信号。这个操作非常关键,它允许主机在不释放总线的情况下,改变数据传输方向(从写变为读)。
简单来说读过程就是主机发送写请求将要读的地址告知从机,然后再发送读请求,从机开始回复数据。
这两个时序图是符合I2C标准的,EEPROM需符合该标准。
1.4 I2C多主机的仲裁机制
I2C是支持多主机通信的,多主机通信必须有仲裁机制,保证某个主机通信的完整性。
多主机同时发送起始信号时,通过监测SDA线电平实现位级仲裁。
主机发送1时发现SDA线为0时,会停止发送,等待下次机会。如果线为1,即使有其他主机在发送,也认为不冲突,等待下个位时再检测
1.5 时钟拉伸
什么是时钟拉伸?
在I2C通信中,SCL线由主机控制时钟信号。
有时从机处理数据较慢,不能马上准备好下一位数据。
从机可以主动拉低SCL线(即拉伸时钟),阻止主机继续产生时钟上升沿,从而延长当前时钟周期。
主机必须等待SCL释放(变高),才能继续通信。
2. 代码实践
使用野火-指南者F103进行测试,读写EEPROM。
2.1 硬件原理图
可以看到,SCL和SDA分别对应PB6和PB7,对应I2C1。
从设备地址:
高 4 位固定为:1010 b;
按照我们此处的连接,A0/A1/A2 均为 0,所以 EEPROM 的 7 位设备地址是:1010 000b ,即 0x50。
2.2 MX配置
Master Features(主机特性)
- I2C Speed Mode:Fast Mode
表示I2C总线速度模式为“快速模式”,通信速率最高可达400kHz。
这里只支持Standard Mode(100kHz)、Fast Mode(400kHz)。 - I2C Clock Speed (Hz):400000
主机产生的I2C时钟频率,单位赫兹。这里为400kHz,符合Fast Mode标准。 - Fast Mode Duty Cycle:Duty cycle Tlow/Thigh = 2
指时钟信号中低电平和高电平的占空比比值。
例如:Tlow = 2 × Thigh,意味着时钟低电平持续时间是高电平的两倍。默认即可。
- I2C Speed Mode:Fast Mode
Slave Features(从机特性)
Clock No Stretch Mode:Disabled
“时钟不拉伸模式”禁用。禁用此选项表示允许从机拉伸时钟。
在I2C协议中,从机可以通过拉低SCL线“拉伸时钟”来延长时钟周期,给自己时间处理数据。Primary Address Length Selection:7-bit
从机主地址使用7位地址模式。
I2C支持7位和10位地址两种寻址方式,这里选择7位地址。Dual Address Acknowledged:Disabled
禁用双地址应答功能。从机只响应一个主地址,若启用则可以设置两个不同地址响应。Primary Slave Address:0x50,即上面确定的EEPROM从机地址。
General Call Address Detection:Disabled
禁用“通用呼叫”地址检测。
通用呼叫地址(0x00)是主机向所有从机广播的地址,启用此功能时从机会响应通用呼叫。
2.3 代码生成
在i2c.c中创建了下面的关键代码:
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
【注意】这里`hi2c1.Init.OwnAddress1和OwnAddress2都被设置为了0,这两个是给主设备即STM32本身设置地址,一般只需要设置OwnAddress1即可,且只需要保证和EEPROM从设备地址不一样即可。
如果MX中设置为0x50,这个OwnAddress1最低位为读写位,实际代码中赋的值是0x50左移一位,即0xA0。
2.4 读写操作
为了简化验证,读写函数都放到main.c中了。
#define EE_ADDR (0x50 << 1) //注意要左移一位
#define EEPROM_PAGE_SIZE 8 // AT24C02页大小8字节
#define EEPROM_MAX_TRIALS 100
#define EEPROM_TIMEOUT 1000 // I2C操作超时时间(ms)
//I2C写入一段连续缓存
int EE_I2C_WriteBuffer(uint16_t memAddress, uint8_t *pData, uint16_t size)
{
//AT24C02的页大小为8字节,即一次写操作最多写入8字节(一个页)。
//当写入数据超过一页大小时,地址会自动回绕(wrap-around),覆盖这一页内的前面字节,导致数据错乱。
//因此,写入buffer时必须分块写,每块最大8字节,且不能"跨页"写。
//每次写入后需等待写周期完成(约5~10 ms)
//写入的地址可以是任意的,但为了保证不跨页,也得分多次写入。比如写入地址05,写4个字节,那么05 06 07写前三个字节,08单独写最后一个字节。
HAL_StatusTypeDef status;
uint16_t bytesWritten = 0;
while (bytesWritten < size) {
// 计算当前地址所在页剩余空间(这两句覆盖了写入地址在页中间和页头两种情况)
uint8_t page_offset = memAddress % EEPROM_PAGE_SIZE;
uint8_t page_space = EEPROM_PAGE_SIZE - page_offset;
// 本次写入长度,不能超过页剩余空间,也不能超过剩余数据长度
uint16_t write_len = (size - bytesWritten) < page_space ? (size - bytesWritten) : page_space;
// 执行写操作 &pData[bytesWritten]相当于pData + bytesWritten
status = HAL_I2C_Mem_Write(&hi2c1,EE_ADDR,memAddress,I2C_MEMADD_SIZE_8BIT,&pData[bytesWritten],write_len,EEPROM_TIMEOUT);
if (status != HAL_OK) {
return -1;
}
// 等待写完成
// 关键步骤1, 等待总线空闲,传输完成。因为上一步写入是异步操作
while(HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
{
}
// 关键步骤2, 等待EE内部写入FLASH完成
while(HAL_I2C_IsDeviceReady(&hi2c1, EE_ADDR, EEPROM_MAX_TRIALS, EEPROM_TIMEOUT) == HAL_TIMEOUT)
{
}
//关键步骤3, 再次确认I2C总线空闲
while(HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
{
}
// 更新地址和已写入字节数
memAddress += write_len;
bytesWritten += write_len;
}
return 0;
}
//I2C读取一段数据缓存
int EE_I2C_ReadBuffer(uint8_t *pbuffer, uint16_t addrRead, uint16_t readByteNum)
{
//读操作没有页限制,可以一次读取任意长度数据。
HAL_StatusTypeDef stat = HAL_I2C_Mem_Read(&hi2c1, EE_ADDR, addrRead, I2C_MEMADD_SIZE_8BIT, pbuffer, readByteNum, 1000);
if (stat != HAL_OK)
{
return -1;
}
return 0;
}
写入时的注意事项已经在代码中添加了。这里再强调一遍:
- AT24C02的页大小为8字节,即一次写操作最多写入8字节(一个页)。
- 当写入数据超过一页大小时,地址会自动回绕(wrap-around),覆盖这一页内的前面字节,导致数据错乱。
- 因此,写入buffer时必须分块写,每块最大8字节,且不能"跨页"写。
- 每次写入后需等待写周期完成(约5~10 ms)
- 写入的地址可以是任意的,但为了保证不跨页,也得分多次写入。比如写入地址05,写4个字节,那么05 06 07写前三个字节,08单独写最后一个字节。 读取非常简单。
//I2C写入测试
uint8_t buffer[10] = {1,2,3,4,5,6,7,8,9,10};
EE_I2C_WriteBuffer(5, buffer, sizeof(buffer)); //写入起始地址在页中间
uint8_t buffer_r[10];
EE_I2C_ReadBuffer(buffer_r, 5, sizeof(buffer));
测试通过。
DMA写入和串口DMA写入很类似,也是在中断中判定写入完成再可以写下一个。
3. 经验总结
本节上往上信息的摘录总结。
3.1 上拉电阻问题
- 阻值过大: 如果上拉电阻阻值过大,SCL和SDA线从低电平恢复到高电平的时间会变长(RC充电时间常数)。在高波特率(比如400kHz甚至1MHz)下,这可能导致信号上升沿缓慢,违反I²C的时序要求,从而导致通信失败或数据错误。
- 阻值过小: 如果上拉电阻阻值过小,当设备将线路拉低时,流过上拉电阻的电流会过大。这可能超过设备I/O引脚的灌电流能力,甚至可能损坏设备。
- 选型: 上拉电阻的阻值通常在1kΩ到10kΩ之间。具体选择多少,取决于总线电容(连接的设备数量、PCB走线长度)、工作电压和通信速率。一般来说,速率越高,总线电容越大,上拉电阻就需要越小。经验法则是先从4.7kΩ或2.2kΩ开始尝试。
I2C上升时间主要由RC时间常数决定,上拉电阻 * 总线电容,成反比关系。
先用4.7kΩ作为起点测试。
如果出现信号上升慢、通信不稳定,尝试减小阻值到2.2kΩ或1kΩ。
如果功耗过大或信号电平偏低,尝试增大阻值。
结合示波器测试信号波形,确保满足I2C时序。
3.2 地址错误
这是最常见的错误之一。
- 7位 vs 8位地址: 很多数据手册提供的是7位从机地址,但有些I²C驱动或库函数要求你传入8位地址(即7位地址左移1位,并包含读写位)。务必仔细阅读数据手册和驱动文档,确认传入的地址格式是否正确。
- 地址冲突: 如果总线上有两个从设备使用了相同的地址,那么通信就会混乱。
- 设备未上电/未初始化: 从设备没有正确上电或没有完成初始化,自然不会响应I²C地址。
3.3 ACK/NACK问题
传输中途出现NACK,通常意味着从机没有正确响应。
- 原因: 可能是地址错误(从机根本没收到地址)、从机正忙(内部操作未完成)、从机未正常工作(硬件故障)、或者数据格式不正确导致从机无法解析。
- 调试: 使用逻辑分析仪查看NACK发生在哪个字节之后,这能帮助你定位问题。
3.4 总线被锁死
这是I²C的“僵尸模式”。当某个设备在I²C传输过程中突然复位、掉电或出现异常,它可能将SDA线永久拉低,导致整个I²C总线被“锁死”,任何通信都无法进行。
- 解决方法: 主机可以尝试发送9个时钟脉冲(在SCL线上产生9个时钟周期,但不关心SDA),让从机完成当前字节传输并释放SDA线。如果SDA线仍然被拉低,则可能需要复位从设备或整个系统。有些I²C控制器有硬件复位总线的功能。
3.5 逻辑分析仪
调试I²C问题的“上帝视角”!如果你在I²C通信上遇到了“玄学”问题,一个逻辑分析仪(哪怕是几十块钱的USB逻辑分析仪)都能帮你“拨云见日”。它能清晰地捕获SCL和SDA上的波形,并自动解析出START/STOP、地址、数据和ACK/NACK,让你一目了然地看到通信的每一个细节,是定位时序问题、数据错误、ACK/NACK异常的终极神器。但是必要的时候(上升下降时间、波形电平)还是需要使用示波器进行波形抓取。
3.6 速度受限
相较于SPI(串行外设接口)等协议,I²C的速度较慢。常见的标准模式为100kbps,快速模式为400kbps,高速模式可达3.4Mbps,但仍不如SPI快。对于需要高速数据传输的应用(如显示屏),I²C可能不是最佳选择。
3.7 距离受限
由于总线电容效应,I²C的通信距离有限,通常仅限于同一块PCB板内或短距离连接。长距离传输需要额外的总线缓冲器或转换芯片。
3.8协议开销
每个字节的数据传输都需要一个额外的ACK/NACK位,这增加了协议的开销。在传输大量小数据包时,效率会受到一定影响。
3.9 布线考虑:
• 尽量保持SCL和SDA走线平行、等长
• 避免与高速信号或电源走线并行
• 加入适当的去耦电容
3.10 I²C支持多主控吗?如何实现仲裁?
A: I²C支持多主控。当多个主设备同时尝试控制总线时,I²C通过“时钟同步”和“数据仲裁”机制来解决冲突。
• 时钟同步: 多个主设备会同步它们的SCL信号,只要有一个主设备拉低SCL,SCL就保持低电平。
• 数据仲裁: 这是关键。当一个主设备发送高电平(释放SDA)而另一个主设备发送低电平(拉低SDA)时,发送高电平的主设备会检测到SDA线与自己发送的电平不符。它会立即意识到总线冲突,并主动放弃对总线的控制,进入监听模式。这种“谁先拉低谁赢”的机制保证了数据传输的完整性。
3.11 如何为我的I2C总线选择合适的上拉电阻值?
A: 这是一个权衡。阻值太大,拉高电流小,信号上升时间长,高速率下会出问题;阻值太小,灌电流大,可能超出GPIO引脚的承受能力,且功耗增加。一个经验法则是:对于100kbps速率,使用4.7kΩ;对于400kbps速率,使用1.8kΩ到2.2kΩ。最科学的方法是根据总线电容、电压和速率,查阅I2C官方规范(UM10204)中的公式进行计算。
3.12 为什么I²C总线会被锁死?如何避免和解决?
A: I²C总线锁死通常发生在从设备在通信过程中异常复位、掉电或程序跑飞,导致SDA线被永久拉低。因为I²C是线与逻辑,SDA被拉低后,总线就无法恢复高电平,所有通信都会停止。
- 确保从设备的电源稳定,软件逻辑健壮,避免死循环或异常中断。
- 软件复位: 主机尝试发送9个时钟脉冲(在SCL上产生9个时钟,不关心SDA),让从机有机会释放SDA。
- 硬件复位: 如果软件复位无效,可能需要通过控制从设备的复位引脚来强制复位从设备。
- 总线复位: 某些I²C控制器有硬件总线复位功能,可以直接复位I²C总线状态。
- 电源复位: 最“暴力”但有效的方法是给从设备断电再上电。
3.13 我的从设备总是返回NACK,最快的排查步骤是什么?
A: 遵循“由硬到软”原则:
- 测电压: 确认从设备VCC和GND都已正确连接。
- 查上拉: 确认SCL和SDA上拉电阻存在且阻值合适。
- 核对地址: 用i2cdetect或逻辑分析仪确认你代码中的地址和设备实际地址是否一致(注意7位/8位格式)。
- 简化总线: 只保留一个主机和一个从机,排除其他设备的干扰。
- 检查时序: 用逻辑分析仪检查你的START信号、时钟频率和时序是否符合规范。
3.14 什么是“时钟拉伸”(Clock Stretching),它有什么用?
A: “时钟拉伸”是I2C协议中一个非常重要的特性。它允许从设备在需要更多时间处理数据时(比如正在进行一次内部ADC转换),主动将SCL线拉低,从而“暂停”时钟。主机检测到SCL被拉低后,会耐心等待,直到从机处理完任务并释放SCL线,通信才会继续。这赋予了从设备一定的主动权,确保了在主快从慢的情况下数据不会丢失,大大增强了系统的健壮性。