📚 I2C 技术系列 · 持续更新中
本系列从 OD 开漏物理层一路打到 Linux i2c子系统,把"两根线"背后那套电气/协议/仲裁/恢复/驱动全拧成一根线。
👉 想系统追更,戳这里查看完整目录:
【I2C 技术系列 00】总目录
本篇定位:理论(01-11)攒齐,本篇落地到具体器件——EEPROM(24C)、传感器(BMP280/MPU6050)、OLED(SSD1306)、PMIC(RK8xx)。每种给裸机 + Linux 两套代码,讲清多设备共总线设计和 RTOS 集成。读完你能给任何 I2C 器件写驱动。
目录
- 一、EEPROM(24C)读写驱动
- 1.1 24C04 特性
- 1.2 裸机驱动(DesignWare 风格)
- 1.3 ACK 轮询(替代 delay)
- 1.4 Linux 驱动(at24 框架)
- 二、传感器(BMP280/MPU6050)驱动
- 2.1 BMP280(温压传感器)
- 2.2 MPU6050(六轴 IMU)
- 2.3 Linux 驱动
- 三、OLED(SSD1306)显示驱动
- 3.1 SSD1306 特性
- 3.2 裸机驱动
- 3.3 Linux 驱动(ssd1307fb)
- 四、PMIC(RK8xx)配置
- 4.1 RK817 特性
- 4.2 读版本
- 4.3 调压
- 4.4 Linux 驱动
- 五、多设备共总线设计
- 5.1 总线规划
- 5.2 分总线策略
- 5.3 TCA9548A 多段
- 六、RTOS 集成与互斥
- 6.1 互斥保护
- 6.2 任务级缓冲
- 6.3 中断 + 信号量(09 篇)
- 七、关键问题 Q&A
- Q1:EEPROM 页写为什么不能跨页?
- Q2:BMP280 读数据前必须读校准系数吗?
- Q3:OLED 刷屏用 I2C 会不会太慢?
- Q4:PMIC 调压失败怎么办?
- Q5:多任务共 I2C,mutex 死锁怎么办?
- Q6:Linux 下怎么自测 I2C 器件?
- 八、实战案例
- 案例 1:EEPROM 跨页写数据错乱
- 案例 2:BMP280 读温度恒为 0
- 案例 3:MPU6050 唤醒失败
- 案例 4:OLED 显示花屏
- 案例 5:多任务抢 I2C,总线错乱
- 案例 6:Linux BMP280 probe 失败
- 九、本篇小结与速查
- 核心要点
- 速查表
- 代码模板速查
- 延伸阅读
- 参考资料
一、EEPROM(24C)读写驱动
1.1 24C04 特性
- 容量 4Kbit = 512 字节,分 32 页 × 16 字节
- 地址:1010 + A2A1A0(3 位引脚配,最多 8 片)
- 页写:一次最多 16 字节,跨页要分次
- 写周期:5ms(写后要等,或用 ACK 轮询)
- 读:当前地址读 / 随机读(Sr)/ 顺序读
1.2 裸机驱动(DesignWare 风格)
#defineEEPROM_ADDR0x50// 写一字节(随机写)inteeprom_write_byte(uint8_taddr,uint8_tdata){uint8_tbuf[2]={addr,data};intret=i2c_master_write(EEPROM_ADDR,buf,2);if(ret<0)returnret;delay_ms(5);// 写周期return0;}// 页写(最多 16 字节,不跨页)inteeprom_write_page(uint8_taddr,uint8_t*data,intlen){uint8_tbuf[17];// 1 地址 + 16 数据if(len>16)return-EINVAL;// 检查不跨页if((addr&0x0F)+len>16)return-EINVAL;buf[0]=addr;memcpy(&buf[1],data,len);intret=i2c_master_write(EEPROM_ADDR,buf,len+1);if(ret<0)returnret;delay_ms(5);returnlen;}// 随机读(Sr 经典)inteeprom_read(uint8_taddr,uint8_t*buf,intlen){// 1. 写地址(设定 EEPROM 内部指针)intret=i2c_master_write(EEPROM_ADDR,&addr,1);if(ret<0)returnret;// 2. Sr + 读ret=i2c_master_read(EEPROM_ADDR,buf,len);returnret;}1.3 ACK 轮询(替代 delay)
// EEPROM 写周期内不 ACK,写完后 ACK。用这个判断写完成voideeprom_wait_write_done(uint8_tdev_addr){while(1){// 发地址 + 读,看是否 ACKif(i2c_master_write(dev_addr,NULL,0)==0)break;// ACK 了// 还在写周期,继续}}- 优势:不用固定 delay 5ms,写完成即返回(可能 3ms)
- 注意:发"空写"要控制器支持
1.4 Linux 驱动(at24 框架)
// Linux 已有 at24 驱动,设备树配置即可// dts:&eeprom@50{compatible="atmel,24c04";reg=<0x50>;pagesize=<16>;size=<512>;read-only;// 可选};// 用户态访问// /sys/bus/i2c/devices/0-0050/eeprom 或 nvmemcat/sys/bus/i2c/devices/0-0050/eeprom|xxd ddif=/sys/bus/i2c/devices/0-0050/eeprom bs=1count=16- Linux at24 驱动封装好,nvmem 子系统暴露
- 不用自己写驱动,配设备树即可
二、传感器(BMP280/MPU6050)驱动
2.1 BMP280(温压传感器)
- 地址:1110 11X(X=SDO 引脚,0x76/0x77)
- 关键寄存器:0xD0(ID)、0xF4(ctrl_meas)、0xF5(config)、0xF7~0xFC(数据)
- 校准系数:0x88~0xA1(读出后软件补偿)
#defineBMP280_ADDR0x76#defineBMP280_REG_ID0xD0#defineBMP280_REG_CTRL0xF4#defineBMP280_REG_DATA0xF7intbmp280_init(void){// 1. 读 ID 验证uint8_tid;i2c_read_reg(BMP280_ADDR,BMP280_REG_ID,&id,1);if(id!=0x58)return-ENODEV;// 2. 读校准系数(11 个 16 位 + 1 个 16 位)uint8_tcalib[24];i2c_read_reg(BMP280_ADDR,0x88,calib,24);// 解析 dig_T1..dig_T3, dig_P1..dig_P9// 3. 配置:温度×1, 压力×4, 正常模式uint8_tctrl=(1<<5)|(4<<2)|3;// osrs_t=1, osrs_p=4, mode=normali2c_write_reg(BMP280_ADDR,BMP280_REG_CTRL,ctrl);return0;}intbmp280_read(int*temperature,int*pressure){uint8_tdata[6];i2c_read_reg(BMP280_ADDR,BMP280_REG_DATA,data,6);int32_traw_t=(data[0]<<12)|(data[1]<<4)|(data[2]>>4);int32_traw_p=(data[3]<<12)|(data[4]<<4)|(data[5]>>4);// 用校准系数补偿(BMP280 datasheet 公式)*temperature=bmp280_compensate_t(raw_t);*pressure=bmp280_compensate_p(raw_p);return0;}2.2 MPU6050(六轴 IMU)
- 地址:110 100X(X=AD0,0x68/0x69)
- 关键寄存器:0x75(WHO_AM_I)、0x6B(PWR_MGMT)、0x1B/1C(量程)、0x3B~(数据)
#defineMPU6050_ADDR0x68#defineMPU_REG_WHOAMI0x75#defineMPU_REG_PWR0x6B#defineMPU_REG_ACCEL0x3Bintmpu6050_init(void){uint8_twhoami;i2c_read_reg(MPU6050_ADDR,MPU_REG_WHOAMI,&whoami,1);if(whoami!=0x68)return-ENODEV;// 唤醒(写 0 到 PWR_MGMT)i2c_write_reg(MPU6050_ADDR,MPU_REG_PWR,0x00);delay_ms(100);// 设量程:加速度 ±2g,陀螺 ±250dpsi2c_write_reg(MPU6050_ADDR,0x1C,0x00);// ACCEL_CONFIGi2c_write_reg(MPU6050_ADDR,0x1B,0x00);// GYRO_CONFIGreturn0;}intmpu6050_read(short*accel,short*gyro){uint8_tdata[14];i2c_read_reg(MPU6050_ADDR,MPU_REG_ACCEL,data,14);accel[0]=(data[0]<<8)|data[1];// AXaccel[1]=(data[2]<<8)|data[3];// AYaccel[2]=(data[4]<<8)|data[5];// AZ// data[6..7] 温度gyro[0]=(data[8]<<8)|data[9];// GXgyro[1]=(data[10]<<8)|data[11];// GYgyro[2]=(data[12]<<8)|data[13];// GZreturn0;}2.3 Linux 驱动
- BMP280:drivers/iio/pressure/bmp280-i2c.c(已主线)
- MPU6050:drivers/iio/imu/inv_mpu6050/(已主线)
- 设备树配置 + sysfs 读
&i2c0 { bmp280@76 { compatible = "bosch,bmp280"; reg = <0x76>; }; };cat/sys/bus/iio/devices/iio:device0/in_temp_inputcat/sys/bus/iio/devices/iio:device0/in_pressure_input三、OLED(SSD1306)显示驱动
3.1 SSD1306 特性
- 128×64 OLED 控制器,I2C 接口
- 地址:0111 10SA0(0x3C/0x3D)
- 显存:128×64/8 = 1024 字节,页模式(8 页 × 128 列)
- 控制字节:Co(继续)+ D/C(0=命令,1=数据)
3.2 裸机驱动
#defineOLED_ADDR0x3Cvoidoled_cmd(uint8_tcmd){uint8_tbuf[2]={0x00,cmd};// 控制字节=0(命令)i2c_master_write(OLED_ADDR,buf,2);}voidoled_data(uint8_tdata){uint8_tbuf[2]={0x40,data};// 控制字节=0x40(数据)i2c_master_write(OLED_ADDR,buf,2);}voidoled_init(void){oled_cmd(0xAE);// display offoled_cmd(0x20);oled_cmd(0x00);// 页寻址oled_cmd(0xB0);// 页 0oled_cmd(0xC8);// 反向扫描oled_cmd(0x81);oled_cmd(0xFF);// 对比度oled_cmd(0xA1);// segment remapoled_cmd(0xA4);// 正常显示oled_cmd(0xA6);// 正常(非反色)oled_cmd(0xAF);// display on}voidoled_set_pos(uint8_tpage,uint8_tcol){oled_cmd(0xB0+page);// 页oled_cmd(0x00+(col&0x0F));// 列低oled_cmd(0x10+(col>>4));// 列高}// 刷一页(128 字节)voidoled_flush_page(uint8_tpage,uint8_t*data){oled_set_pos(page,0);uint8_tbuf[129];buf[0]=0x40;// 数据memcpy(&buf[1],data,128);i2c_master_write(OLED_ADDR,buf,129);// 一次写 129 字节}3.3 Linux 驱动(ssd1307fb)
&i2c0 { oled@3c { compatible = "solomon,ssd1306fb-i2c"; reg = <0x3c>; width = <128>; height = <64>; page-offset = <0>; }; };- Linux 有 ssd1307fb 帧缓冲驱动
- /dev/fb0 出现,可写屏
四、PMIC(RK8xx)配置
4.1 RK817 特性
- 地址:0x5A(可配)
- 集成 DCDC/LDO/RTC/音频/ADC
- 关键寄存器:0x00(版本)、各路电压调节、开/关机
4.2 读版本
uint8_tver;i2c_read_reg(0x5A,0x00,&ver,1);// ver 应为 RK817 芯片 ID4.3 调压
// 设 DCDC1 输出 1.2V(寄存器值按 datasheet 查表)i2c_write_reg(0x5A,RK817_DCDC1_VOL,0x0C);// 例:0x0C 对应 1.2V4.4 Linux 驱动
&i2c0 { pmic@5a { compatible = "rockchip,rk817"; reg = <0x5a>; interrupt-parent = <&gpio0>; interrupts = <3 IRQ_TYPE_LEVEL_LOW>; regulators { dcdc1: DCDC_REG1 { regulator-name = "vdd_logic"; regulator-min-microvolt = <950000>; regulator-max-microvolt = <1350000>; regulator-boot-on; }; // ... }; }; };- Linux regulator 框架管理,sysfs 可调压
cat /sys/class/regulator/*/microvolts
五、多设备共总线设计
5.1 总线规划
I2C0(400k): ├── 0x50 EEPROM(配置存储) ├── 0x76 BMP280(温压) ├── 0x68 MPU6050(IMU) └── 0x3C OLED(显示)- 地址不冲突,共一条总线
- 算电容:4 器件 × 10pF + 走线 = ~80pF,400k OK
5.2 分总线策略
I2C0(高速 1M):OLED(刷屏要带宽) I2C1(400k):传感器(BMP280/MPU6050) I2C2(100k):EEPROM + PMIC(慢,关键)- 按速度/重要性分总线
- 高速设备独占快总线
- 关键设备(PMIC)独占,避免被其他拖累
5.3 TCA9548A 多段
I2C0 ─── TCA9548A ─── 段0:EEPROM ├── 段1:传感器组 └── 段2:显示组- 选通某段后访问,各段独立电容/地址
- 软件选通:写 TCA9548A 控制寄存器选通道
voidtca9548a_select(uint8_tchannel){uint8_tmask=1<<channel;i2c_master_write(0x70,&mask,1);// TCA9548A 地址 0x70}六、RTOS 集成与互斥
6.1 互斥保护
SemaphoreHandle_t i2c_mutex;voidi2c_init(void){i2c_mutex=xSemaphoreCreateMutex();}inti2c_read_sensor(uint8_taddr,uint8_treg,uint8_t*buf,intlen){xSemaphoreTake(i2c_mutex,portMAX_DELAY);intret=i2c_read_reg(addr,reg,buf,len);xSemaphoreGive(i2c_mutex);returnret;}- 多任务访问 I2C,用 mutex 串行化
- 防止:任务 A 读 EEPROM 中途,任务 B 切来读 BMP280,总线错乱
6.2 任务级缓冲
// 传感器任务:周期采集,存缓冲voidsensor_task(void*arg){while(1){xSemaphoreTake(i2c_mutex,portMAX_DELAY);bmp280_read(&temp,&press);xSemaphoreGive(i2c_mutex);// 存入队列/缓冲xQueueSend(data_queue,&sample,0);vTaskDelay(pdMS_TO_TICKS(100));// 10Hz}}- I2C 操作在 mutex 内,快速释放
- 数据处理在 mutex 外,不占总线
6.3 中断 + 信号量(09 篇)
// ISR 完成 I2C 传输后给信号量,任务层等voidi2c_isr(void){// 处理中断xSemaphoreGiveFromISR(i2c_done_sem,&hpw);portYIELD_FROM_ISR(hpw);}inti2c_xfer_rtos(...){// 启动传输xSemaphoreTake(i2c_done_sem,portMAX_DELAY);returnret;}七、关键问题 Q&A
Q1:EEPROM 页写为什么不能跨页?
A:24C04 页边界是 16 字节对齐(0x00, 0x10, 0x20…)。一次页写内部地址低位回卷:写到页末尾,地址不递增到下一页,而是回卷到该页开头。跨页写,后面的数据覆盖页开头,数据错乱。解决:跨页要分多次页写,每次不跨页边界。
Q2:BMP280 读数据前必须读校准系数吗?
A:必须。BMP280 输出原始 ADC 值,要用出厂校准系数(dig_T1…dig_P9)补偿才是真实温度/压力。不补偿读出的是无意义数。校准系数存 0x88~0xA1,开机读一次缓存。
Q3:OLED 刷屏用 I2C 会不会太慢?
A:128×64/8 = 1024 字节,400k 传 1024 字节约 25ms,刷屏 40fps 上限。1M 模式约 10ms,85fps。要高刷屏率用 SPI OLED(推挽,几 Mbps)。I2C OLED 适合静态/慢刷新显示。
Q4:PMIC 调压失败怎么办?
A:
- 确认 I2C 通信(读版本号验证)
- 确认寄存器值(查 datasheet 电压对应表)
- 确认 regulator 是否使能(部分要先 enable)
- 确认电压范围(min/max 限制)
- 确认未锁定(部分 PMIC 有写保护)
Q5:多任务共 I2C,mutex 死锁怎么办?
A:
- mutex 内不放耗时操作(只 I2C 读写)
- mutex take 用超时(portMAX_DELAY 改具体值),超时告警
- 不要在 mutex 内再 take 其他锁(防 AB-BA)
- ISR 内不 take mutex(用信号量)
Q6:Linux 下怎么自测 I2C 器件?
A:
i2cdetect -y N看地址i2cdump -y N 0x50dump 寄存器i2cget/i2cset读写- 看 dmesg 驱动 probe 日志
- 看 sysfs(/sys/bus/i2c/devices/)
八、实战案例
案例 1:EEPROM 跨页写数据错乱
现象:写 20 字节到 0x0C,读回发现 0x0C~0x0F 对,0x10~0x1F 是错的数据。
分析:页边界 0x10,从 0x0C 写 20 字节跨页。前 4 字节(0x0C~0x0F)写对,后 16 字节本应到 0x10~0x1F,但页内回卷到 0x00~0x0F,覆盖了页开头。
解决:分两次写——0x0C 写 4 字节,0x10 写 16 字节。
案例 2:BMP280 读温度恒为 0
现象:BMP280 读 0xF7~0xFC,温度恒 0。
分析:未读校准系数或未补偿。直接用原始 ADC 值当温度,显示 0 或异常值。
解决:开机读 0x88~0xA1 校准系数,按 datasheet 公式补偿。
案例 3:MPU6050 唤醒失败
现象:MPU6050 读 WHO_AM_I 对,但读数据全 0。
分析:MPU6050 默认休眠(PWR_MGMT=0x40),不唤醒数据不更新。
解决:写 0x00 到 0x6B 唤醒,delay 100ms 稳定。
案例 4:OLED 显示花屏
现象:OLED 刷屏后显示乱码。
分析:页地址/列地址设错,数据写到错误位置。
解决:每次刷页前正确 set_pos(page, col),确认寻址模式(页模式/水平模式)。
案例 5:多任务抢 I2C,总线错乱
现象:RTOS 下两任务都读 I2C,偶发数据错。
分析:无 mutex,任务 A 读 EEPROM 中途,任务 B 切来读 BMP280,从机地址切换时序错。
解决:加 mutex,所有 I2C 操作在 mutex 内。
案例 6:Linux BMP280 probe 失败
现象:BMP280 驱动 probe 报 -ENODEV。
排查:
- i2cdetect 看 0x76 是否在
- i2cget 读 0xD0,应为 0x58
- 确认设备树 compatible = “bosch,bmp280”
- 确认 reg = <0x76>(不是 0xEC)
- dmesg 看 probe 日志
九、本篇小结与速查
核心要点
- EEPROM:页写不跨页,写周期 5ms,随机读用 Sr,ACK 轮询替代 delay
- BMP280:读校准系数补偿,ctrl 配置过采样/模式
- MPU6050:唤醒(PWR_MGMT=0),设量程,14 字节连读
- OLED:页寻址,控制字节区分命令/数据,刷页一次写 129 字节
- PMIC:Linux regulator 框架管理,sysfs 调压
- 多设备共总线:地址不冲突,按速度/重要性分总线,TCA9548A 多段
- RTOS:mutex 串行化访问,ISR + 信号量,任务级缓冲
- Linux:多数器件有主线驱动(iio/hwmon/regulator/fb),配设备树即可
速查表
| 器件 | 地址 | 关键寄存器 | 注意 |
|---|---|---|---|
| 24C04 | 0x50 | — | 页写不跨页,写周期 5ms |
| BMP280 | 0x76/77 | 0xD0 ID, 0xF4 ctrl, 0xF7 data | 校准系数补偿 |
| MPU6050 | 0x68/69 | 0x75 ID, 0x6B PWR, 0x3B data | 唤醒后用 |
| SSD1306 | 0x3C/3D | 控制字节 0x00/0x40 | 页寻址刷屏 |
| RK817 | 0x5A | 0x00 ver, 各电压 | regulator 框架 |
代码模板速查
| 操作 | API |
|---|---|
| 裸机读寄存器 | i2c_write(addr)+i2c_read(Sr) |
| 裸机写寄存器 | i2c_write(addr+data) |
| Linux 读寄存器 | i2c_smbus_read_byte_data(client, reg) |
| Linux 多 msg | i2c_transfer(adap, msgs, num) |
| Linux regmap | regmap_read/write |
| RTOS 互斥 | xSemaphoreTake/Give(i2c_mutex) |
| 用户态 | i2cget/i2cset/i2cdump |
延伸阅读
- 【I2C 技术系列 04】I2C 协议与时序详解 —— Sr 用在读寄存器
- 【I2C 技术系列 09】I2C 传输模式:轮询 / 中断 / DMA —— 实战的传输模式
- 【I2C 技术系列 10】I2C 错误处理与总线恢复 —— 器件异常恢复
- 【I2C 技术系列 11】I2C Linux 驱动框架 —— Linux 驱动结构
- [[13-I2C调试与排查]] —— 器件调试
- [[FreeRTOS_设计思想与功能模块讲解]] —— RTOS 互斥
参考资料
- 24C04 Datasheet
- BMP280 Datasheet(校准公式)
- MPU6050 Register Map
- SSD1306 Datasheet
- RK817 Datasheet
- Linux drivers/iio/pressure/bmp280.c
- Linux drivers/video/fbdev/ssd1307fb.c
【I2C 技术系列 00】总目录 · 上一篇:【I2C 技术系列 11】I2C Linux 驱动框架 ·下一篇:[[13-I2C调试与排查]]
✨ 感谢你的阅读 ✨
技术之路漫漫,分享是为了更好地交流。
如果本文的内容对你有启发,希望能得到你的支持:
👍 点赞|⭐ 收藏|👀 关注
如果你在调试过程中遇到了其他问题,
欢迎在评论区 💬留言,我们一起探讨。
关注我,一起交流底层开发的那些事儿!🚀