Skip to content

第一百四十二章:I2C

合道期

涉及内核源码:

林小源从设备树的高处走下来,来到山脚的一条小溪旁。溪水很浅,只没过脚踝,但溪面上架着一座奇特的桥——桥面只有两根绳索,一根是绿色的,一根是蓝色的。

"这是什么桥?"林小源问。

"I2C 总线。"一个细小的声音从桥头传来。

林小源低头一看,桥头坐着一个身材矮小的老者,穿着绿色的袍子,袍子上绣着 两个字母。

"两根线就够了?"林小源有些惊讶。

"够了。"老者说,声音虽然细小但很清晰,"SDA 是数据线,SCL 是时钟线。一根传数据,一根传时钟。主设备拉低 SDA 发起通信,然后通过时钟线同步数据传输。就这么简单。"

老者站起身,带着林小源走上了桥。桥面上刻着一个个地址——0x030x480x500x680x77

"每个地址代表一个 I2C 设备。"老者说,"I2C 总线上的每个设备都有唯一的 7 位地址。主机通过地址选择要通信的设备——就像点名一样,叫到谁谁就应答。"

"但 Linux 里的 I2C 驱动分两层看。"老者把小桥下的水脉拨开,露出 adapterclient 两块石碑,"i2c_adapter 代表主机控制器,真正驱动硬件发时钟; 代表挂在某个地址上的从设备。设备驱动通常不创建 client,它在 里拿到 client,保存设备私有数据,再通过 SMBus 或 I2C transfer 访问芯片。"

"client 从哪里来?"

"来自设备树、ACPI、板级表,或者总线驱动明确调用 i2c_new_client_device()。盲目扫描地址是旧办法,新驱动要谨慎;I2C 协议没有标准方式证明某个地址上到底是什么芯片,误探测可能把读操作变成别的设备眼里的写操作。"

破关试炼

双碑初试

Linux I2C 中,代表主机控制器的是 i2c_adapter;代表某个地址上设备的结构是什么?

答对后才能继续滑动和进入下一章。
c
/*
 * I2C 的特点:
 *
 *   两根线: SDA (数据) + SCL (时钟)
 *   主从模式: 主机发起通信
 *   地址寻址: 每个设备有唯一地址
 *   速度: 100K/400K/3.4M
 *
 * I2C 驱动:
 *   struct i2c_driver:
 *     probe — 初始化
 *     remove — 移除
 *     id_table — 匹配表
 *
 * I2C 通信:
 *   i2c_master_send — 发送数据
 *   i2c_master_recv — 接收数据
 *   i2c_transfer — 传输消息
 *
 * 设备树描述:
 *   sensor@48 {
 *     compatible = "vendor,sensor";
 *     reg = <0x48>;
 *   };
 */

printf("=== I2C — 低速串行总线 ===\n\n");

printf("I2C 的特点:\n");
printf("  两根线: SDA + SCL\n");
printf("  主从模式\n");
printf("  地址寻址\n");
printf("  速度: 100K/400K/3.4M\n\n");

printf("--- I2C 驱动 ---\n");
printf("struct i2c_driver my_drv = {\n");
printf("  .probe = my_probe,\n");
printf("  .remove = my_remove,\n");
printf("  .id_table = my_ids,\n");
printf("  .driver = {\n");
printf("    .name = \"my_sensor\",\n");
printf("    .of_match_table = my_of_ids,\n");
printf("  },\n");
printf("};\n\n");

printf("--- I2C 通信 ---\n");
printf("发送数据:\n");
printf("  i2c_master_send(client, buf, len)\n\n");
printf("接收数据:\n");
printf("  i2c_master_recv(client, buf, len)\n\n");
printf("传输消息:\n");
printf("  i2c_transfer(adapter, msgs, num)\n\n");

printf("--- 设备树描述 ---\n");
printf("sensor@48 {\n");
printf("  compatible = \"vendor,sensor\";\n");
printf("  reg = <0x48>;  // I2C 地址\n");
printf("};\n\n");

printf("--- I2C 设备 ---\n");
printf("传感器: 温度、湿度、加速度\n");
printf("EEPROM: 小容量存储\n");
printf("RTC: 实时时钟\n");
printf("显示屏: OLED、LCD\n");

"速度怎么样?"林小源问,"只有两根线,能快到哪去?"

老者苦笑了一下:"标准模式 100Kbps,快速模式 400Kbps,高速模式 3.4Mbps。跟 PCI、SPI 比起来,确实慢。"

"那为什么还有人用 I2C?"

"因为简单。"老者说,"两根线就够了,节省引脚。你想想,一个 SoC 芯片的引脚是有限的——如果每个低速设备都用四根线(像 SPI),引脚根本不够用。I2C 两根线就能挂十几个设备,每个设备一个地址,通过地址寻址。"

老者指着桥面上的那些地址:"温度传感器 0x48、EEPROM 0x50、RTC 0x68——它们共用两根线,互不干扰。主机要跟谁说话,先发地址,只有地址匹配的设备才会应答。"

"半双工是什么意思?"

"同一时刻只能一个方向传输。"老者说,"主机发数据的时候,从机只能收;从机发数据的时候,主机只能收。不能同时收发——因为只有一根数据线。"

林小源点了点头:I2C 的优势不在速度,而在简洁。两根线、十几个设备、地址寻址——对于传感器、EEPROM、RTC 这些低速设备来说,已经足够了。

"还有一个旁支叫 SMBus。"老者说,"它和 I2C 很像,但有自己的事务类型和限制。很多 PC 硬件监控芯片走的是 SMBus 风格,所以驱动里常见 i2c_smbus_read_byte_data()i2c_smbus_write_word_data() 这类 helper。能用简单寄存器读写时,不必自己拼复杂消息。"

破关试炼

简桥之试

I2C 驱动访问寄存器式设备时,常用的一类 SMBus helper 前缀是什么?

答对后才能继续滑动和进入下一章。

"I2C 驱动怎么写?"林小源问。

"跟 platform 驱动类似。"老者说,"定义一个 结构体,填好 probe、remove 和匹配表。匹配可以用 (传统方式),也可以用 (设备树方式)。"

"probe 里做什么?"

"初始化设备。"老者说,"通过 结构体访问设备—— 发数据, 收数据。如果需要更复杂的操作,用 发送多个消息。"

老者从桥头拿起一块水晶,递给林小源。水晶内部浮现出一个温度传感器的画面——传感器通过 I2C 总线向主机报告温度数据,每次传输只有几个字节。

"驱动名字也不能随便起。"老者补充,"i2c_driver.driver.name 不应含空格,通常要和模块名相配,方便热插拔/冷插拔自动加载。每个 client 还可以用 i2c_set_clientdata() 保存私有状态,remove 或 probe 失败时,核心会清理这块指针。"

"I2C 就是这么用的。"老者说,"数据量小、速度要求不高、引脚要省——选 I2C 准没错。但如果你要传大量数据,比如刷屏幕、读 Flash,那得用 SPI。"

破关试炼

私藏之试

I2C client 上保存驱动私有数据的 helper 是 i2c_set_clientdata;读取它的 helper 是什么?

答对后才能继续滑动和进入下一章。

道藏笔记

内核启示

I2C 是连接低速设备的串行总线。

I2C 的特点:

  • 两根线(SDA、SCL)
  • 主从模式
  • 地址寻址
  • 速度:100K/400K/3.4M

I2C 驱动:

  • i2c_master_send/recv — 发送/接收数据
  • — 传输消息
  • i2c_adapter — 主机控制器
  • — 某地址上的设备
  • i2c_smbus_* — 常见寄存器式 SMBus/I2C helper
  • 新代码避免不可靠的盲目地址探测

I2C 是"简单"的总线——两根线连接多个设备。


破关试炼

I2C 之试

I2C 设备进行一次或多次消息传输时,本章对应的核心调用是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道