十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ARM汇编学习笔记(八):I²C 通信详解:推挽/开漏、通信时序与代码实现

ARM汇编学习笔记(八):I²C 通信详解:推挽/开漏、通信时序与代码实现 一、GPIO 输出模式推挽 vs 开漏I²C 总线的物理层核心是开漏输出 上拉电阻。理解这两种输出模式是理解 I²C 电气特性的前提。1.1 推挽输出Push-Pull结构由两个 MOS 管上管 mos1 接 VCC下管 mos2 接 GND组成同一时刻只导通一个。mos1mos2pad 电平1导通0截止3.3V强高0截止1导通GND强低00高阻态悬空11禁止短路烧管特点可以主动输出高电平和低电平驱动能力强。不能多个设备同时挂在一根线上否则一个输出高、一个输出低会短路。适用于单独驱动 LED、蜂鸣器、SPI 的 MOSI/SCLK 等独占型信号。1.2 开漏输出Open-Drain结构只有下管 mos2上管 mos1 永远不导通。mos1mos2pad 电平103.3V01GND00高阻态悬空11禁止特点只能主动拉低无法主动输出高电平。高电平状态需依赖外部上拉电阻将总线拉到 VCC。输出“1”实际上是释放总线高阻态输出“0”是拉低。支持“线与”只要有一个设备拉低总线就是低只有全部释放总线才是高。1.3 为什么 I²C 必须用开漏因为 I²C 是多主多从总线同一根 SDA/SCL 上可能挂多个设备如果使用推挽A 输出高、B 输出低 →电源到地短路器件烧毁。使用开漏A 输出高实际是释放、B 输出低拉低→ 总线被 B 拉低不会短路实现“线与”。上拉电阻典型值4.7kΩ ~ 10kΩ阻值越小上升沿越快但功耗越大阻值越大越省电但边沿变缓高速通信时可能采不到数据。1.4 电平转换应用开漏结构还便于电平转换3.3V 的 chipA 与 5V 的 chipB 共挂 I²CSDA/SCL 各自上拉到自己的 VCC。任意一方拉低另一方都能检测到低电平释放时各自被拉回各自的高电平天然实现 3.3V↔5V 互通。二、I²C 通信时序详解2.1 空闲状态SCL 和 SDA 均被上拉电阻拉为高电平总线空闲。2.2 起始信号STARTSCL 为高电平时SDA 由高变低。此时所有从机检测到起始条件开始准备接收地址。2.3 停止信号STOPSCL 为高电平时SDA 由低变高。主设备发出结束本次通信释放总线。2.4 数据位传输规则MSB 优先先发最高位 bit7最后发 bit0。SCL 低电平期间发送方可以改变 SDA 电平准备数据。SCL 高电平期间SDA 必须保持稳定接收方在此时采样。每个字节 8 位数据 1 位应答 9 个时钟周期。2.5 应答位ACK / NACK第 9 个时钟周期接收方控制 SDA类型SDA 电平含义ACK低0接收成功继续NACK高1接收失败 / 结束读取2.6 完整写时序帧结构START → [从机地址7位 W(0)] → ACK → [寄存器地址8位] → ACK → [数据8位] → ACK → ... → STOP2.7 完整读时序帧结构START → [从机地址7位 W(0)] → ACK → [寄存器地址8位] → ACK → RESTART → [从机地址7位 R(1)] → ACK → [数据8位] → ACK(主机发) → ... → NACK → STOP2.8 时序图以写 0xAC 为例┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ SCL ───┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └─── 1 2 3 4 5 6 7 8 9 SDA ──┐ 1 ┌─0─┐ 1 ┌─0─┐ 1 ┌─1─┐ 0 ┌─0─┐ ACK └───┘ └───┘ └───┘ └───┘ └────── MSB LSB三、EEPROM 读写代码实现软件模拟 I²C以 AT24C027 位地址0xA01 0x50为例使用 GPIO 软件模拟 I²C。3.1 宏定义与基础函数#includestm32f10x.h#defineI2C_GPIOGPIOB#defineI2C_SCL_PINGPIO_Pin_6#defineI2C_SDA_PINGPIO_Pin_7#defineSCL_H()GPIO_SetBits(I2C_GPIO,I2C_SCL_PIN)#defineSCL_L()GPIO_ResetBits(I2C_GPIO,I2C_SCL_PIN)#defineSDA_H()GPIO_SetBits(I2C_GPIO,I2C_SDA_PIN)#defineSDA_L()GPIO_ResetBits(I2C_GPIO,I2C_SDA_PIN)#defineSDA_READ()GPIO_ReadInputDataBit(I2C_GPIO,I2C_SDA_PIN)/* 简单延时适配 100kHz 标准模式 */staticvoidi2c_delay(void){for(volatileinti0;i10;i);}/* 初始化SCL/SDA 配置为开漏输出 */voidI2C_Init(void){GPIO_InitTypeDef gpio;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);gpio.GPIO_PinI2C_SCL_PIN|I2C_SDA_PIN;gpio.GPIO_ModeGPIO_Mode_Out_OD;// ★ 开漏输出gpio.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(I2C_GPIO,gpio);SCL_H();SDA_H();// 空闲态}3.2 起始 / 停止信号/* START: SCL 高时 SDA 下降沿 */voidI2C_Start(void){SDA_H();SCL_H();i2c_delay();SDA_L();// 高电平下拉低 SDAi2c_delay();SCL_L();// 钳住总线准备发送i2c_delay();}/* STOP: SCL 高时 SDA 上升沿 */voidI2C_Stop(void){SDA_L();SCL_H();i2c_delay();SDA_H();// 高电平下释放 SDAi2c_delay();}3.3 发送一个字节 等待应答/* 发送 1 字节返回 0ACK1NACK */uint8_tI2C_SendByte(uint8_tbyte){for(uint8_ti0;i8;i){SCL_L();if(byte0x80)SDA_H();// MSB 优先elseSDA_L();byte1;i2c_delay();SCL_H();// 高电平期间从机采样i2c_delay();}/* 第 9 个时钟接收 ACK */SCL_L();SDA_H();// 主机释放 SDAi2c_delay();SCL_H();i2c_delay();uint8_tackSDA_READ();// 0ACK, 1NACKSCL_L();i2c_delay();returnack;}3.4 读取一个字节 发送应答/* ack0 主机发 ACK继续读ack1 主机发 NACK结束读 */uint8_tI2C_ReadByte(uint8_tack){uint8_tbyte0;SDA_H();// 主机释放 SDA交给从机驱动for(uint8_ti0;i8;i){SCL_L();i2c_delay();SCL_H();// 高电平期间采样i2c_delay();byte1;if(SDA_READ())byte|0x01;}/* 第 9 个时钟主机发 ACK/NACK */SCL_L();if(ack)SDA_H();// NACKelseSDA_L();// ACKi2c_delay();SCL_H();i2c_delay();SCL_L();SDA_H();// 释放总线returnbyte;}3.5 EEPROM 写一个字节#defineEEPROM_ADDR0xA0// 7位地址 0x50 左移 1 位 W0voidEEPROM_WriteByte(uint8_treg,uint8_tdata){I2C_Start();I2C_SendByte(EEPROM_ADDR|0);// 从机地址 写I2C_SendByte(reg);// 寄存器内存地址I2C_SendByte(data);// 写入数据I2C_Stop();/* AT24C02 需要 5ms 内部写周期 */for(volatileuint32_ti0;i50000;i);}3.6 EEPROM 读一个字节uint8_tEEPROM_ReadByte(uint8_treg){uint8_tdata;/* 第一阶段假写定位寄存器地址 */I2C_Start();I2C_SendByte(EEPROM_ADDR|0);// 写I2C_SendByte(reg);/* 第二阶段RESTART切换到读 */I2C_Start();// RESTARTI2C_SendByte(EEPROM_ADDR|1);// 读dataI2C_ReadByte(1);// 读 1 字节并回 NACKI2C_Stop();returndata;}3.7 页写与连续读进阶/* 页写AT24C02 一页 8 字节跨页会回卷注意地址对齐 */voidEEPROM_WritePage(uint8_treg,uint8_t*buf,uint8_tlen){I2C_Start();I2C_SendByte(EEPROM_ADDR|0);I2C_SendByte(reg);for(uint8_ti0;ilen;i)I2C_SendByte(buf[i]);I2C_Stop();for(volatileuint32_ti0;i50000;i);}/* 连续读地址自动递增 */voidEEPROM_ReadSeq(uint8_treg,uint8_t*buf,uint8_tlen){I2C_Start();I2C_SendByte(EEPROM_ADDR|0);I2C_SendByte(reg);I2C_Start();I2C_SendByte(EEPROM_ADDR|1);for(uint8_ti0;ilen;i){uint8_tack(ilen-1)?1:0;// 最后一个 NACKbuf[i]I2C_ReadByte(ack);}I2C_Stop();}四、工程调试要点问题原因解决从机无 ACK地址算错 / 器件没上电确认 7 位地址左移 1 位后拼 R/W数据全 0xFF上拉缺失 / SDA 一直高检查 SDA、SCL 是否都接 4.7k 上拉偶发通信失败上升沿太慢减小上拉阻值 / 降低速率EEPROM 写后读不出未等写周期加 5ms 延时或 ACK 轮询高电平被拉不上去误配为推挽GPIO 必须配Open-Drain五、总结物理层开漏 上拉 → 线与逻辑 → 多设备安全共存且天然支持电平转换。协议层START/STOP 由 SCL 高电平期间 SDA 跳变产生数据 SCL 高时采样、低时改变每字节跟随一位 ACK/NACK。代码层软件模拟的关键是**严格按“SCL 低改变 SDASCL 高采样 SDA”**的顺序起始、停止、ACK 三步最易写错。读取 EEPROM必须使用RESTART而不是 STOP才能保持对同一从机的连续操作。掌握上面这三点推挽/开漏电气基础、时序规则、代码实现就能独立完成绝大多数 I²C 外设EEPROM、SHT30、MPU6050、OLED 等的驱动开发。
返回列表