017、I2C实战:读写EEPROM(AT24Cxx)、温湿度传感器(SHT30)、加速度计(MPU6050)
2026/6/16 3:24:50 网站建设 项目流程

017 I2C实战:读写EEPROM(AT24Cxx)、温湿度传感器(SHT30)、加速度计(MPU6050)

一、从一次凌晨三点的I2C死锁说起

去年做一款工业数据记录仪,凌晨三点被测试电话叫醒——设备运行12小时后,温湿度数据全部卡死在上一帧。示波器抓SCL/SDA波形,发现SDA被拉低后永远不释放。当时第一反应是“从机没释放总线”,但换掉SHT30模块后问题依旧。最后查到是主控在异常复位时,I2C状态机卡在了发送ACK的中间态,SDA被主控自己锁死。

这个坑让我养成了一个习惯:所有I2C外设初始化前,先发一个GPIO级别的总线复位——把SCL和SDA对应的IO口切到普通GPIO模式,手动拉低SCL 9个时钟周期,再拉高SDA释放总线。这个操作在AT24Cxx和MPU6050上同样有效,尤其是热插拔场景。

二、AT24Cxx:EEPROM的“写死循环”陷阱

AT24Cxx系列(AT24C02/04/08/16)是I2C接口最经典的存储芯片,但它的写操作有个反直觉的机制:每写入一页(通常8字节或16字节)后,芯片会进入内部编程周期,此时不响应任何I2C命令。新手最容易犯的错误是连续写入多页时,不等待内部编程完成就直接发下一帧数据。

2.1 页写入的正确姿势

// 伪代码,实际工程中需要加超时保护uint8_teeprom_write_page(uint16_taddr,uint8_t*data,uint8_tlen){// 这里踩过坑:AT24C02页大小是8字节,AT24C16是16字节// 如果len超过页边界,必须拆分成多次写入if(len>EEPROM_PAGE_SIZE)returnERROR;i2c_start();i2c_send_byte(DEV_ADDR_W);// 设备地址+写位// 别这样写:直接发数据地址,忘记检查ACKif(i2c_wait_ack()!=ACK){i2c_stop();returnERROR;}i2c_send_byte((uint8_t)(addr>>8));// 高位地址i2c_wait_ack();i2c_send_byte((uint8_t)(addr&0xFF));// 低位地址i2c_wait_ack();for(uint8_ti=0;i<len;i++){i2c_send_byte(data[i]);// 这里有个坑:如果从机NACK,必须立即停止if(i2c_wait_ack()!=ACK){i2c_stop();returnERROR;}}i2c_stop();// 关键:等待内部编程完成(轮询从机应答)// 别这样写:用固定delay(5ms),不同批次芯片时间不同uint32_ttimeout=10000;// 10ms超时while(timeout--){i2c_start();i2c_send_byte(DEV_ADDR_W);if(i2c_wait_ack()==ACK){i2c_stop();returnOK;}i2c_stop();delay_us(1);// 微秒级延时,别用毫秒}returnTIMEOUT;}

个人经验:AT24Cxx的写周期典型值是5ms,但工业级芯片在高温下可能延长到10ms。轮询等待时,如果连续5次NACK,建议做一次总线复位再重试。另外,写操作前务必关全局中断,否则中断服务函数里的I2C操作会打乱时序。

2.2 连续读的“地址自动递增”特性

读操作比写简单,但有个细节:AT24Cxx支持连续读,地址会自动递增。如果你只读一个字节,发完地址后直接发停止位即可;如果要读多个字节,主控需要在每个字节后发ACK(除了最后一个字节发NACK)。

// 读多个字节,最后一个字节发NACKuint8_teeprom_read_buf(uint16_taddr,uint8_t*buf,uint8_tlen){i2c_start();i2c_send_byte(DEV_ADDR_W);if(i2c_wait_ack()!=ACK){i2c_stop();returnERROR;}i2c_send_byte((uint8_t)(addr>>8));i2c_wait_ack();i2c_send_byte((uint8_t)(addr&0xFF));i2c_wait_ack();i2c_start();// 重复起始条件i2c_send_byte(DEV_ADDR_R);i2c_wait_ack();for(uint8_ti=0;i<len;i++){buf[i]=i2c_read_byte();if(i==len-1){i2c_send_nack();// 最后一个字节发NACK}else{i2c_send_ack();}}i2c_stop();returnOK;}

踩坑记录:某次用AT24C16,地址是2字节(A0-A10),但手册上写的是“高5位地址通过器件地址引脚设定”。实际测试发现,AT24C16的地址线A0/A1/A2是悬空的,地址完全由I2C从机地址的A0/A1/A2位决定。如果你用AT24C02的驱动去读AT24C16,地址会错乱。

三、SHT30:温湿度传感器的“时钟延展”噩梦

SHT30是Sensirion的经典数字温湿度传感器,I2C接口,精度高但有个坑:它支持时钟延展(Clock Stretching)。当传感器正在测量时,如果主控发读命令,SHT30会把SCL拉低,直到测量完成才释放。很多MCU的硬件I2C外设不支持时钟延展,直接卡死。

3.1 单次测量模式 vs 周期测量模式

SHT30有两种工作模式,我强烈建议用单次测量模式,因为周期模式下传感器会持续占用总线。

// 单次测量命令:0x2C 0x06(高重复性)uint8_tsht30_single_measure(float*temp,float*humi){uint8_tcmd[2]={0x2C,0x06};uint8_traw[6]={0};i2c_start();i2c_send_byte(0x44<<1|0);// SHT30默认地址0x44if(i2c_wait_ack()!=ACK){i2c_stop();returnERROR;}i2c_send_byte(cmd[0]);i2c_wait_ack();i2c_send_byte(cmd[1]);i2c_wait_ack();i2c_stop();// 这里踩过坑:测量时间最长15ms,但时钟延展可能更长// 别这样写:直接delay(20ms),浪费CPUdelay_ms(20);// 保守等待,实际可优化为轮询状态i2c_start();i2c_send_byte(0x44<<1|1);// 读if(i2c_wait_ack()!=ACK){i2c_stop();returnERROR;}for(uint8_ti=0;i<6;i++){raw[i]=i2c_read_byte();if(i==5)i2c_send_nack();elsei2c_send_ack();}i2c_stop();// 校验CRC(SHT30每个数据包后跟一个CRC)if(!sht30_crc_check(raw,2,raw[2]))returnCRC_ERROR;if(!sht30_crc_check(raw+3,2,raw[5]))returnCRC_ERROR;// 温度转换:-45 + 175 * raw / 65535*temp=-45.0f+175.0f*(float)((raw[0]<<8)|raw[1])/65535.0f;*humi=100.0f*(float)((raw[3]<<8)|raw[4])/65535.0f;returnOK;}

个人经验:SHT30的CRC校验必须做,否则数据偶尔会跳变。我遇到过一批芯片,温度读数在25℃和-40℃之间来回跳,最后发现是CRC校验没做,总线噪声导致数据错误。另外,SHT30的I2C地址可以通过ADDR引脚修改,默认0x44,如果接地是0x45,别焊错了。

3.2 时钟延展的软件处理

如果你的MCU硬件I2C不支持时钟延展,可以用软件I2C模拟。核心逻辑是:在读取每个字节前,先检测SCL是否被拉低,如果被拉低则等待释放。

// 软件I2C读字节,带时钟延展处理uint8_tsw_i2c_read_byte(uint8_tack){uint8_tdata=0;// 别这样写:直接读SDA,忽略SCL状态for(uint8_ti=0;i<8;i++){// 等待SCL被从机释放(时钟延展)uint32_ttimeout=1000;while(GPIO_ReadPin(SCL_PIN)==0){if(--timeout==0)return0xFF;// 超时返回}GPIO_SetPin(SCL_PIN,1);// 主控拉高SCLdelay_us(1);data=(data<<1)|GPIO_ReadPin(SDA_PIN);GPIO_SetPin(SCL_PIN,0);// 拉低SCL,准备下一个位delay_us(1);}// 发送ACK/NACKGPIO_SetPin(SDA_PIN,ack?0:1);GPIO_SetPin(SCL_PIN,1);delay_us(1);GPIO_SetPin(SCL_PIN,0);GPIO_SetPin(SDA_PIN,0);returndata;}

踩坑记录:某次用STM32的硬件I2C读SHT30,开启时钟延展支持后,发现读回来的数据全是0xFF。查手册发现STM32的I2C外设时钟延展超时时间默认是25个SCL周期,而SHT30的测量时间可能超过这个值。解决方案:要么关掉硬件超时,要么用软件I2C。

四、MPU6050:加速度计的“寄存器读错位”问题

MPU6050是六轴惯性测量单元,I2C接口,寄存器地址是8位,但数据是16位(高8位和低8位分别存储)。新手最容易犯的错误是:读加速度数据时,只读了一个字节

4.1 正确的16位数据读取

MPU6050的加速度计输出寄存器从0x3B开始,每个轴占2个字节(高字节在前)。

// 读加速度计X轴数据int16_tmpu6050_read_accel_x(void){uint8_treg=0x3B;// ACCEL_XOUT_Huint8_thigh,low;i2c_start();i2c_send_byte(0x68<<1|0);// MPU6050默认地址0x68i2c_wait_ack();i2c_send_byte(reg);i2c_wait_ack();i2c_start();// 重复起始i2c_send_byte(0x68<<1|1);i2c_wait_ack();high=i2c_read_byte();i2c_send_ack();// 这里要发ACK,因为还要读下一个字节low=i2c_read_byte();i2c_send_nack();i2c_stop();// 别这样写:直接返回(high << 8) | low,忘记考虑符号位return(int16_t)((high<<8)|low);}

个人经验:MPU6050的寄存器地址是自动递增的,所以读加速度计三个轴时,可以从0x3B开始连续读6个字节,一次性读完。但要注意,陀螺仪寄存器从0x43开始,别读串了。

4.2 配置寄存器的“写后读验证”

MPU6050有个坑:写配置寄存器后,必须读回来验证。因为某些寄存器(如电源管理寄存器0x6B)的复位值不是0,直接写可能不生效。

// 配置MPU6050为正常模式(唤醒)uint8_tmpu6050_wakeup(void){uint8_treg_val;// 写0x6B寄存器,清除SLEEP位i2c_start();i2c_send_byte(0x68<<1|0);i2c_wait_ack();i2c_send_byte(0x6B);// 电源管理寄存器i2c_wait_ack();i2c_send_byte(0x00);// 清除SLEEP位i2c_wait_ack();i2c_stop();// 读回来验证i2c_start();i2c_send_byte(0x68<<1|0);i2c_wait_ack();i2c_send_byte(0x6B);i2c_wait_ack();i2c_start();i2c_send_byte(0x68<<1|1);i2c_wait_ack();reg_val=i2c_read_byte();i2c_send_nack();i2c_stop();// 这里踩过坑:如果reg_val & 0x40不为0,说明SLEEP位没清除if(reg_val&0x40){returnERROR;// 唤醒失败}returnOK;}

踩坑记录:某次量产,10%的MPU6050初始化失败,读回来的寄存器值全是0xFF。排查发现是I2C总线电容过大,导致SCL上升沿变缓。解决方案:在SCL和SDA上加4.7kΩ上拉电阻,并把I2C速率从400kHz降到100kHz。

五、I2C调试的“三板斧”

5.1 逻辑分析仪是必备工具

别信示波器,I2C调试必须用逻辑分析仪。我常用的是Saleae逻辑分析仪,设置采样率24MHz,触发条件选“起始条件”。抓波形时重点关注:

  • 起始条件:SCL高电平时,SDA从高变低
  • 停止条件:SCL高电平时,SDA从低变高
  • ACK/NACK:第9个SCL时钟,SDA被拉低是ACK,保持高是NACK
  • 时钟延展:SCL被从机拉低超过一个时钟周期

5.2 总线复位函数

每个I2C驱动里都应该有一个总线复位函数,在初始化时调用:

voidi2c_bus_reset(void){// 把SCL和SDA配置为普通GPIO,开漏输出GPIO_Init(SCL_PIN,GPIO_MODE_OUT_OD);GPIO_Init(SDA_PIN,GPIO_MODE_OUT_OD);// 拉高SDAGPIO_SetPin(SDA_PIN,1);delay_us(5);// 发送9个时钟脉冲for(uint8_ti=0;i<9;i++){GPIO_SetPin(SCL_PIN,0);delay_us(5);GPIO_SetPin(SCL_PIN,1);delay_us(5);}// 发送停止条件GPIO_SetPin(SDA_PIN,0);delay_us(5);GPIO_SetPin(SCL_PIN,1);delay_us(5);GPIO_SetPin(SDA_PIN,1);delay_us(5);// 恢复I2C外设模式// ...}

5.3 超时保护是底线

所有I2C操作都必须加超时保护,尤其是等待ACK和等待时钟延展。我习惯用硬件定时器做超时,而不是软件循环计数,因为软件循环在中断关闭时会失效。

六、个人经验总结

  1. I2C速率不是越高越好:400kHz在短距离(<10cm)没问题,但板间连接或线缆超过20cm,建议降到100kHz。我见过100kHz下稳定的系统,升到400kHz后每天随机死机一次。

  2. 上拉电阻选型:3.3V系统用4.7kΩ,5V系统用2.2kΩ。如果总线电容大(比如接了多个从机),可以并联两个上拉电阻。别用10kΩ,上升沿太慢。

  3. 从机地址冲突:AT24Cxx和SHT30的默认地址都是0x50(左移后),如果同时使用,必须通过引脚修改地址。MPU6050的AD0引脚接地是0x68,接VCC是0x69。

  4. 写操作前关中断:EEPROM的页写入和SHT30的测量命令,执行期间如果被中断打断,可能导致I2C状态机错乱。我习惯在写操作前关全局中断,写完后开中断,但注意关中断时间不要超过100μs。

  5. CRC校验不是可选项:SHT30和MPU6050都提供了CRC校验,别偷懒。我见过因为没做CRC,数据偶尔跳变导致设备误报警的案例。

  6. 硬件I2C vs 软件I2C:如果MCU的硬件I2C支持时钟延展和DMA,优先用硬件。否则用软件I2C更可控。我个人的经验是:STM32的硬件I2C有bug(尤其是F1系列),建议用软件I2C或者用F4/F7系列。

最后说一句:I2C调试时,逻辑分析仪比示波器好用一百倍。别问我怎么知道的——当年用示波器抓I2C波形,抓了三天没找到问题,换逻辑分析仪十分钟定位到是ACK时序不对。

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

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

立即咨询