在这里插入图片描述

[TOC]

drivers/i2c/i2c-core-smbus.c I2C/SMBus核心协议(I2C/SMBus Core Protocol) 实现SMBus事务处理

历史与背景

这项技术是为了解决什么特定问题而诞生的?

drivers/i2c/i2c-core-smbus.c 文件实现了对**系统管理总线(System Management Bus, SMBus)**协议的支持。SMBus是基于I2C(Inter-Integrated Circuit)协议演变而来的一种子集或变体,专为系统和电源管理而设计。
这项技术旨在解决以下问题:

  • 标准化系统管理:为计算机系统中的各种低速设备(如电池、温度传感器、风扇控制器、电源管理芯片)提供一个标准的、低速、两线式的通信接口。
  • 事务可靠性:SMBus在I2C的基础上增加了协议层面的可靠性检查,例如,大多数SMBus命令都包含**PEC(Packet Error Checking,数据包错误校验)**机制,以确保数据传输的完整性。
  • 简化驱动开发:提供了一套预定义的、原子性的事务类型(如快速命令、读取字节、发送/接收块数据等),驱动程序无需关注I2C底层的START/STOP信号和ACK/NACK握手,只需调用高级的SMBus函数即可完成整个命令序列。

它的发展经历了哪些重要的里程碑或版本迭代?

SMBus最早由Intel在1995年提出,主要用于对PC进行系统管理。

  • I2C基础:I2C协议在1982年由飞利浦(NXP)提出,是所有后续发展的基础。SMBus是在I2C的基础上增加了额外的协议层规则、时序要求(如更长的超时时间)和数据格式。
  • Linux I2C子系统成熟:Linux的I2C子系统起初主要关注通用的I2C通信。随着各种电源管理芯片和传感器(尤其是笔记本电池)的普及,对SMBus特定事务的需求日益增长。
  • 核心SMBus实现drivers/i2c/i2c-core-smbus.c 的核心代码被引入,以统一的方式为所有支持SMBus的主控制器(Adapter)提供高层协议支持。适配器(Adapter)驱动只需要实现底层I2C读写函数,并将SMBus事务映射到底层I2C消息序列即可。
  • 演进:随着时间的推移,新的I2C总线驱动(如PCI总线上的控制器)被添加进来,它们都通过 i2c-core-smbus.c 提供的接口来支持SMBus通信,确保了协议的兼容性和一致性。

目前该技术的社区活跃度和主流应用情况如何?

i2c-core-smbus.c 是I2C子系统的一个稳定且关键的组成部分。

  • 广泛应用:SMBus在PC、服务器和嵌入式系统中有着广泛的应用,特别是在电源管理领域。例如,服务器的基板管理控制器(BMC)经常使用SMBus进行通信,笔记本电脑的智能电池(Smart Battery)也依赖SMBus协议。
  • 稳定维护:该文件代码非常稳定,主要的社区活动集中在确保新的I2C适配器驱动正确地实现 i2c_adapter 结构体中的 .smbus_xfer.master_xfer 回调,以兼容SMBus协议。

核心原理与设计

它的核心工作原理是什么?

i2c-core-smbus.c 的核心原理是将复杂的、预定义的多步SMBus事务(Transaction)映射到一系列底层的I2C消息(Message)序列上,并统一处理PEC校验。

  1. 分层架构:SMBus机制建立在I2C总线之上。

    • 上层:是设备驱动(Client Driver),它们调用 i2c_smbus_read_byte_data() 等高级函数。
    • 中层:由 i2c-core-smbus.c 负责。它接收高级请求,选择正确的SMBus命令类型,并构造底层的I2C消息序列(struct i2c_msg)。如果硬件不支持SMBus事务,i2c-core-smbus.c 会尝试使用纯I2C消息模拟SMBus事务。
    • 下层:是I2C适配器驱动(Adapter Driver),它负责实际的硬件操作(如位操作或寄存器写入),将I2C消息序列转换为物理总线上的电信号。
  2. 事务类型:SMBus定义了多种标准事务,例如:

    • Send Byte / Receive Byte:发送或接收单个字节。
    • Write Byte / Word Data:写入字节或字数据。
    • Read Byte / Word Data:读取字节或字数据。
    • Block Read / Block Write:以块(最多32字节)为单位传输数据。
    • Process Call:发送一个字数据,并立即接收一个字数据。
  3. PEC校验:对于支持PEC的事务,i2c-core-smbus.c 会在发送数据的末尾计算并添加一个PEC字节(基于CRC-8),并在接收数据时校验收到的PEC字节是否正确,如果校验失败,事务将被拒绝。

  4. 硬件支持的回落(Fallback):理想情况下,I2C适配器驱动会实现 .smbus_xfer 回调,允许硬件在单个原子操作中执行完整的SMBus事务。如果适配器驱动没有实现该回调,i2c-core-smbus.c 就会回退(fallback)到使用通用的 .master_xfer 接口,通过发送一系列独立的I2C消息来模拟完整的SMBus事务。

它的主要优势体现在哪些方面?

  • 可靠性高:PEC机制显著提高了在嘈杂环境中的数据传输可靠性。
  • 原子性:SMBus事务通常被设计为原子操作,保证了整个命令序列的完整执行。
  • 驱动简化:设备驱动无需关心底层I2C的复杂细节和PEC计算,只需调用高级API。
  • 统一接口:为所有支持SMBus的适配器提供了统一的软件接口。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 速度限制:SMBus协议定义了比I2C更严格的时序,其最高时钟频率通常限制在100 KHz(某些版本支持400 KHz),远低于现代I2C可达到的1 MHz或更高速度。
  • 事务受限:SMBus只支持其协议规范中预定义的几种事务类型。对于复杂的、自定义的I2C消息序列,必须直接使用I2C核心接口(i2c_transfer())。
  • 总线保持:SMBus要求更长的超时时间,在总线被锁定或出现故障时,可能会导致系统更长时间地等待总线恢复。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 电源管理:这是SMBus最主要的用途。例如,**智能电池驱动(Smart Battery Driver)**使用SMBus的“读取字数据”(Read Word Data)命令来获取电池的剩余容量、电压、温度和循环次数等信息。
  • 温度与风扇控制:用于读取系统内部的温度传感器(如LM75, TMP421等)或控制PWM风扇的转速。
  • 基板管理控制器(BMC)通信:在服务器领域,BMC与主板上的各种传感器和电源芯片通信时,经常依赖SMBus协议。
  • PMBus(Power Management Bus):PMBus是基于SMBus发展起来的更专业的电源管理协议,它也依赖于i2c-core-smbus.c提供的底层事务支持。

是否有不推荐使用该技术的场景?为什么?

  • 高速数据传输:不适用于需要高带宽数据传输的场景(如高分辨率摄像头、高速外部存储)。SMBus的低速限制和额外的PEC开销使其效率低下。
  • 非标准I2C通信:如果设备需要执行I2C规范中未定义的、定制化的消息序列,或者需要处理超过32字节的块传输(SMBus的块传输限制),则必须绕过SMBus接口,直接使用 i2c_transfer() 接口。

对比分析

请将其 与 其他相似技术 进行详细对比。

该技术是I2C协议族的组成部分,因此将其与通用的I2C通信和SPI总线进行对比。

特性 SMBus (通过 i2c-core-smbus.c) I2C (通用 i2c_transfer()) SPI (Serial Peripheral Interface)
协议层级 高级协议,定义了固定事务格式。 低级协议,只定义START/STOP和基本消息结构。 协议松散,依赖片选信号。
速度 (通常最高100 KHz)。 中速到高速(100 KHz至数 MHz)。 高速(数十 MHz)。
可靠性 。内置PEC(CRC-8)错误校验。 。依赖上层软件实现校验。 。依赖上层软件实现校验。
设备寻址 主机发起,从设备地址寻址。 主机发起,从设备地址寻址。 主机发起,通过独立片选线(CS)寻址。
总线连接数 最少2根线(SDA, SCL),多主多从。 2根线(SDA, SCL),多主多从。 4根线(MOSI, MISO, CLK, CS),单主多从。
适用场景 电源管理、传感器、系统状态监控。 通用外设、EEPROM、ADC/DAC等。 闪存、LCD控制、需要高吞吐量的传感器。
事务原子性 事务在内核层面被抽象为原子操作。 仅依赖硬件适配器实现原子性。 依赖片选信号的起止。

SMBus协议软件模拟:通用I2C事务的构建与执行

本代码片段实现了i2c_smbus_xfer_emulated函数,它是Linux I2C核心层中的一个关键“回退”机制。其核心功能是:当一个I2C适配器(即硬件控制器)不提供对某个特定SMBus协议的原生硬件支持时,此函数能够在软件层面,通过组合一个或多个基本的I2C消息(struct i2c_msg),来模拟出该SMBus协议的行为。它将高级、标准化的SMBus事务请求,翻译成底层i2c_transfer接口可以理解的、最基础的I2C读写序列。

实现原理分析

该函数的实现原理是一个“协议翻译器”。它接收一个高级的SMBus操作请求,然后根据该请求的类型(由size参数定义),构建一个包含一到两个struct i2c_msg消息的数组。struct i2c_msg是Linux内核中描述一次基础I2C传输(一个START信号到STOP信号之间)的最小单元。

  1. 消息结构初始化: 函数首先定义了两个本地缓冲区(msgbuf0, msgbuf1)和两个i2c_msg结构体。默认情况下,它假设事务由两个消息组成:第一个消息是写操作(用于发送SMBus命令字节),第二个是读操作(用于接收数据)。

  2. 协议翻译 (switch语句): 函数的核心是一个巨大的switch语句,它根据size参数(代表不同的SMBus协议)来精确地配置i2c_msg数组:

    • 配置消息数量 (nmsgs): 对于纯写操作,nmsgs设为1;对于读操作或读写复合操作,设为2。
    • 配置消息长度 (len): 根据协议,设置每个消息需要传输的数据字节数。例如,对于I2C_SMBUS_WORD_DATA写操作,第一个消息的长度是3(命令字节 + 2字节数据)。
    • 配置消息标志 (flags): 设置读/写方向(I2C_M_RD),以及一些特殊标志如I2C_M_RECV_LEN(用于SMBus块读取,让底层驱动先读取一个长度字节)。
    • 填充数据缓冲区 (buf): 对于写操作,将命令字节和待发送数据从data联合体复制到msgbuf0中。
  3. PEC(包错误校验)处理: 如果请求了PEC,函数会负责计算并附加PEC字节到写消息的末尾,或者在读消息的预期长度上加1以接收PEC字节。

  4. 执行基础I2C传输: 构建好i2c_msg数组后,函数调用__i2c_transfer(adapter, msg, nmsgs)。这个函数会调用底层适配器驱动提供的master_xfer方法,该方法负责将这些基础的I2C消息序列通过硬件发送出去。

  5. 数据提取: 如果是读操作,并且__i2c_transfer成功返回,函数会进入另一个switch语句。这次是根据协议类型,从接收缓冲区(msgbuf1msgbuf0)中解析出数据,并将其存入调用者提供的data联合体中。

  6. 资源清理: 最后,释放为DMA传输可能分配的任何动态内存。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/*
* 使用I2C协议模拟一个SMBus命令。
* 不进行参数检查!
*/
static s32 i2c_smbus_xfer_emulated(struct i2c_adapter *adapter, u16 addr,
unsigned short flags,
char read_write, u8 command, int size,
union i2c_smbus_data *data)
{
/*
* 我们需要生成一系列i2c_msg。写操作时,我们只需要一个消息;
* 读操作时,需要两个。我们用合理的默认值初始化大部分内容,
* 以使下面的代码更简洁。
*/
// 消息缓冲区,大小足以容纳SMBus块传输的最大数据量及额外字节。
unsigned char msgbuf0[I2C_SMBUS_BLOCK_MAX+3];
unsigned char msgbuf1[I2C_SMBUS_BLOCK_MAX+2];
// 根据读写操作,确定消息数量,默认为读操作(2个)或写操作(1个)。
int nmsgs = read_write == I2C_SMBUS_READ ? 2 : 1;
u8 partial_pec = 0; // 用于读写复合操作的部分PEC计算。
int status;
// 定义一个包含两个消息的数组。
struct i2c_msg msg[2] = {
{ // 消息0: 通常用于写命令字节。
.addr = addr,
.flags = flags,
.len = 1,
.buf = msgbuf0,
}, { // 消息1: 通常用于读/写数据。
.addr = addr,
.flags = flags | I2C_M_RD, // 默认为读操作。
.len = 0,
.buf = msgbuf1,
},
};
// 判断是否需要进行PEC(包错误校验)。
bool wants_pec = ((flags & I2C_CLIENT_PEC) && size != I2C_SMBUS_QUICK
&& size != I2C_SMBUS_I2C_BLOCK_DATA);

// 将命令字节放入第一个消息的缓冲区。
msgbuf0[0] = command;
// 根据不同的SMBus协议类型(size)来配置消息。
switch (size) {
case I2C_SMBUS_QUICK: // 快传命令
msg[0].len = 0; // 没有数据负载。
// 特殊情况:读写标志位被用作数据。
msg[0].flags = flags | (read_write == I2C_SMBUS_READ ?
I2C_M_RD : 0);
nmsgs = 1;
break;
case I2C_SMBUS_BYTE: // 读/写 字节
if (read_write == I2C_SMBUS_READ) {
// 特殊情况:这是一个纯读命令,没有前置的写命令。
msg[0].flags = I2C_M_RD | flags;
nmsgs = 1;
}
break;
case I2C_SMBUS_BYTE_DATA: // 读/写 字节数据
if (read_write == I2C_SMBUS_READ)
msg[1].len = 1; // 期望读取1字节。
else {
msg[0].len = 2; // 写入2字节 (命令+数据)。
msgbuf0[1] = data->byte;
}
break;
case I2C_SMBUS_WORD_DATA: // 读/写 字数据
if (read_write == I2C_SMBUS_READ)
msg[1].len = 2; // 期望读取2字节。
else {
msg[0].len = 3; // 写入3字节 (命令+2字节数据)。
msgbuf0[1] = data->word & 0xff; // 低字节
msgbuf0[2] = data->word >> 8; // 高字节
}
break;
case I2C_SMBUS_PROC_CALL: // 过程调用
nmsgs = 2; // 特殊情况,总是写后读。
read_write = I2C_SMBUS_READ;
msg[0].len = 3; // 写入3字节 (命令+2字节数据)。
msg[1].len = 2; // 读取2字节。
msgbuf0[1] = data->word & 0xff;
msgbuf0[2] = data->word >> 8;
break;
case I2C_SMBUS_BLOCK_DATA: // 读/写 块数据
if (read_write == I2C_SMBUS_READ) {
// 对于读操作,设置I2C_M_RECV_LEN标志,让底层驱动先读长度。
msg[1].flags |= I2C_M_RECV_LEN;
msg[1].len = 1; // 初始长度为1,用于接收块长度字节。
i2c_smbus_try_get_dmabuf(&msg[1], 0); // 尝试使用DMA缓冲区。
} else {
msg[0].len = data->block[0] + 2; // 长度 = 数据长度+长度字节+命令字节
if (msg[0].len > I2C_SMBUS_BLOCK_MAX + 2) {
dev_err(&adapter->dev,
"Invalid block write size %d\n",
data->block[0]);
return -EINVAL;
}

i2c_smbus_try_get_dmabuf(&msg[0], command); // 尝试使用DMA缓冲区。
// 将块数据(包括长度字节)拷贝到消息缓冲区。
memcpy(msg[0].buf + 1, data->block, msg[0].len - 1);
}
break;
case I2C_SMBUS_BLOCK_PROC_CALL: // 块过程调用
nmsgs = 2; // 另一个特殊情况,写后读。
read_write = I2C_SMBUS_READ;
if (data->block[0] > I2C_SMBUS_BLOCK_MAX) {
dev_err(&adapter->dev,
"Invalid block write size %d\n",
data->block[0]);
return -EINVAL;
}

msg[0].len = data->block[0] + 2;
i2c_smbus_try_get_dmabuf(&msg[0], command);
memcpy(msg[0].buf + 1, data->block, msg[0].len - 1);

msg[1].flags |= I2C_M_RECV_LEN;
msg[1].len = 1;
i2c_smbus_try_get_dmabuf(&msg[1], 0);
break;
case I2C_SMBUS_I2C_BLOCK_DATA: // I2C块数据(与SMBus块不同,长度固定)
if (data->block[0] > I2C_SMBUS_BLOCK_MAX) {
dev_err(&adapter->dev, "Invalid block %s size %d\n",
str_read_write(read_write == I2C_SMBUS_READ),
data->block[0]);
return -EINVAL;
}

if (read_write == I2C_SMBUS_READ) {
msg[1].len = data->block[0]; // 长度由调用者预先指定。
i2c_smbus_try_get_dmabuf(&msg[1], 0);
} else {
msg[0].len = data->block[0] + 1; // 长度 = 数据长度+命令字节。
i2c_smbus_try_get_dmabuf(&msg[0], command);
// 将数据拷贝到缓冲区。注意这里源是block+1,因为长度在block[0]。
memcpy(msg[0].buf + 1, data->block + 1, data->block[0]);
}
break;
default:
dev_err(&adapter->dev, "Unsupported transaction %d\n", size);
return -EOPNOTSUPP;
}

if (wants_pec) { // 如果需要PEC
// 如果第一个消息是写操作,为其计算PEC。
if (!(msg[0].flags & I2C_M_RD)) {
if (nmsgs == 1) // 纯写操作
i2c_smbus_add_pec(&msg[0]); // 添加PEC到消息末尾。
else // 写后读操作
// 先计算写消息的PEC,保存下来。
partial_pec = i2c_smbus_msg_pec(0, &msg[0]);
}
// 如果最后一个消息是读操作,将其长度加1以接收PEC字节。
if (msg[nmsgs - 1].flags & I2C_M_RD)
msg[nmsgs - 1].len++;
}

// 调用I2C核心传输函数,发送构建好的消息。
status = __i2c_transfer(adapter, msg, nmsgs);
if (status < 0)
goto cleanup;
// 检查返回的消息数是否与预期相符。
if (status != nmsgs) {
status = -EIO;
goto cleanup;
}
status = 0; // 成功

// 如果需要PEC且最后一个消息是读操作,则检查接收到的PEC是否正确。
if (wants_pec && (msg[nmsgs - 1].flags & I2C_M_RD)) {
status = i2c_smbus_check_pec(partial_pec, &msg[nmsgs - 1]);
if (status < 0)
goto cleanup;
}

// 如果是读操作,将从缓冲区接收到的数据拷贝到data联合体中。
if (read_write == I2C_SMBUS_READ)
switch (size) {
case I2C_SMBUS_BYTE:
data->byte = msgbuf0[0];
break;
case I2C_SMBUS_BYTE_DATA:
data->byte = msgbuf1[0];
break;
case I2C_SMBUS_WORD_DATA:
case I2C_SMBUS_PROC_CALL:
data->word = msgbuf1[0] | (msgbuf1[1] << 8);
break;
case I2C_SMBUS_I2C_BLOCK_DATA:
memcpy(data->block + 1, msg[1].buf, data->block[0]);
break;
case I2C_SMBUS_BLOCK_DATA:
case I2C_SMBUS_BLOCK_PROC_CALL:
if (msg[1].buf[0] > I2C_SMBUS_BLOCK_MAX) { // 检查返回的块长度是否有效。
dev_err(&adapter->dev,
"Invalid block size returned: %d\n",
msg[1].buf[0]);
status = -EPROTO;
goto cleanup;
}
memcpy(data->block, msg[1].buf, msg[1].buf[0] + 1);
break;
}

cleanup:
// 如果为DMA分配了内存,在这里释放。
if (msg[0].flags & I2C_M_DMA_SAFE)
kfree(msg[0].buf);
if (msg[1].flags & I2C_M_DMA_SAFE)
kfree(msg[1].buf);

return status;
}

I2C SMBus协议传输执行:I2C核心的通用传输接口

本代码片段是Linux I2C核心层中执行SMBus(System Management Bus)协议传输的核心实现。其主要功能是为内核中的所有I2C设备驱动提供一个统一的、高级的函数接口(i2c_smbus_xfer),用于执行所有标准的SMBus事务,如读/写字节、字、块数据等。它封装了总线锁定、重试、超时以及从原生硬件操作到软件模拟的回退等复杂逻辑,极大地简化了设备驱动的编写。

实现原理分析

此功能的实现分为两个层次:一个公共的封装函数i2c_smbus_xfer和一个内部的核心逻辑函数__i2c_smbus_xfer

  1. 总线锁定与封装 (i2c_smbus_xfer):

    • 这是一个提供给外部驱动调用的API。它的首要职责是通过调用__i2c_lock_bus_helper来锁定I2C适配器总线。这是一个关键步骤,确保了整个SMBus事务(可能包含多个I2C消息)的原子性,防止被系统中其他任务对同一I2C总线的访问所干扰。
    • 锁定总线后,它调用__i2c_smbus_xfer来执行实际的数据传输。
    • 无论传输成功与否,它最后都会调用i2c_unlock_bus来释放总线锁,保证总线资源可被其他使用者访问。
  2. 核心传输逻辑 (__i2c_smbus_xfer):

    • 选择传输函数: 这是此函数最核心的逻辑。它首先尝试从I2C适配器的算法结构(adapter->algo)中获取一个名为smbus_xfer的函数指针。这个函数由底层的I2C控制器驱动(如i2c-stm32.c)提供,代表了使用硬件加速来原生执行SMBus协议的能力。
    • 原子上下文处理: 如果当前处于原子上下文(如中断处理中,不允许睡眠),它会尝试选择一个特殊的、不会睡眠的smbus_xfer_atomic函数。
    • 执行与重试: 如果找到了原生的smbus_xfer函数,它会进入一个循环来调用此函数。这个循环实现了自动重试机制。如果底层驱动返回-EAGAIN(通常表示总线仲裁丢失),核心层会在超时期限内自动重试传输,重试次数由adapter->retries决定。
    • 软件模拟回退 (Fallback): 存在两种情况会触发回退机制:
      a. 底层I2C控制器驱动没有提供smbus_xfer函数。
      b. 底层驱动smbus_xfer函数,但它不支持当前请求的特定SMBus协议(protocol),并返回了-EOPNOTSUPP(操作不支持)。
      在这些情况下,核心层会调用i2c_smbus_xfer_emulated函数。这个函数会在软件层面将一个复杂的SMBus事务分解成一个或多个基本的I2C消息(struct i2c_msg),然后通过适配器的通用I2C传输函数master_xfer来发送这些消息,从而用软件模拟出硬件不支持的SMBus协议。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// i2c_smbus_xfer: 执行SMBus协议操作的公共API。
// @adapter: 指向I2C总线适配器的句柄。
// @addr: SMBus从设备在总线上的地址。
// @flags: I2C_CLIENT_* 标志 (通常为0或I2C_CLIENT_PEC)。
// @read_write: I2C_SMBUS_READ 或 I2C_SMBUS_WRITE。
// @command: 由从设备解释的命令字节。
// @protocol: 要执行的SMBus协议操作,例如 I2C_SMBUS_PROC_CALL。
// @data: 用于读取或写入数据的数据联合体。
// 描述:
// 此函数执行一个SMBus协议操作,成功返回0,失败则返回负的errno代码。
// 它是一个封装函数,主要负责总线锁定和解锁。
s32 i2c_smbus_xfer(struct i2c_adapter *adapter, u16 addr,
unsigned short flags, char read_write,
u8 command, int protocol, union i2c_smbus_data *data)
{
s32 res;

// 锁定I2C总线,确保操作的原子性。
res = __i2c_lock_bus_helper(adapter);
if (res)
return res;

// 调用核心函数执行实际的数据传输。
res = __i2c_smbus_xfer(adapter, addr, flags, read_write,
command, protocol, data);
// 解锁I2C总线。
i2c_unlock_bus(adapter, I2C_LOCK_SEGMENT);

return res;
}
// 导出符号,供内核其他模块调用。
EXPORT_SYMBOL(i2c_smbus_xfer);

// __i2c_smbus_xfer: 执行SMBus协议操作的核心逻辑。
s32 __i2c_smbus_xfer(struct i2c_adapter *adapter, u16 addr,
unsigned short flags, char read_write,
u8 command, int protocol, union i2c_smbus_data *data)
{
// 定义一个函数指针,用于指向实际的传输函数。
int (*xfer_func)(struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);
unsigned long orig_jiffies; // 用于超时计算。
int try; // 用于重试计数。
s32 res;

// 检查适配器是否处于挂起状态。
res = __i2c_check_suspended(adapter);
if (res)
return res;

/* 以下是内核跟踪点,用于调试和性能分析 */
trace_smbus_write(adapter, addr, flags, read_write,
command, protocol, data);
trace_smbus_read(adapter, addr, flags, read_write,
command, protocol);

// 清理标志位,只保留与I2C协议相关的标志。
flags &= I2C_M_TEN | I2C_CLIENT_PEC | I2C_CLIENT_SCCB;

// 默认使用适配器算法提供的标准smbus_xfer函数。
xfer_func = adapter->algo->smbus_xfer;
// 如果当前在不允许睡眠的原子上下文中。
if (i2c_in_atomic_xfer_mode()) {
// 优先使用适配器提供的原子传输函数。
if (adapter->algo->smbus_xfer_atomic)
xfer_func = adapter->algo->smbus_xfer_atomic;
// 如果没有,但有原子的master_xfer,则准备回退到I2C模拟。
else if (adapter->algo->master_xfer_atomic)
xfer_func = NULL;
}

// 如果找到了一个原生的SMBus传输函数 (硬件加速)。
if (xfer_func) {
// 在仲裁丢失时自动重试。
orig_jiffies = jiffies;
for (res = 0, try = 0; try <= adapter->retries; try++) {
// 调用底层驱动提供的传输函数。
res = xfer_func(adapter, addr, flags, read_write,
command, protocol, data);
// 如果返回的不是-EAGAIN (重试),则退出循环。
if (res != -EAGAIN)
break;
// 检查是否超时。
if (time_after(jiffies,
orig_jiffies + adapter->timeout))
break;
}

// 如果返回的不是-EOPNOTSUPP(操作不支持),或者没有master_xfer可以回退,则跳转到trace并返回。
if (res != -EOPNOTSUPP || !adapter->algo->master_xfer)
goto trace;
/*
* 如果适配器不支持此SMBus操作,则回退到软件模拟的i2c_smbus_xfer_emulated。
*/
}

// 执行软件模拟的SMBus传输。
res = i2c_smbus_xfer_emulated(adapter, addr, flags, read_write,
command, protocol, data);

trace:
/* 内核跟踪点,记录传输结果 */
trace_smbus_reply(adapter, addr, flags, read_write,
command, protocol, data, res);
trace_smbus_result(adapter, addr, flags, read_write,
command, protocol, res);

return res;
}
// 导出内部函数符号,可能被其他I2C核心代码使用。
EXPORT_SYMBOL(__i2c_smbus_xfer);

drivers/i2c/i2c-smbus.c 解析:Linux SMBus 扩展支持(SMBus Alert / Host Notify / SPD 自动实例化)

  • drivers/i2c/i2c-smbus.c | Linux 内核 SMBus 扩展模块 | 为 I2C/SMBus 子系统提供 SMBus Alert、SMBus Host Notify,以及基于 DMI 的 SPD 设备自动实例化能力

介绍

这份代码不是“SMBus 基本读写接口”的实现(那些通常在 i2c-core 侧负责并可通过 I2C 传输仿真),而是把 SMBus 里更偏“系统级语义”的扩展能力做成一个独立模块,主要包含三块:

  1. SMBus Alert(SMBALERT#)基础设施:共享告警线 + ARA(Alert Response Address)定位告警源设备,并回调到对应设备驱动的 alert()
  2. SMBus Host Notify 支持:通过 I2C slave 机制,在固定地址 0x08 接收 Host Notify 写入并触发内核处理。
  3. SPD 自动实例化:在部分平台上利用 DMI 的内存插槽信息,扫描 0x50..0x57 并自动创建 SPD/Hub 对应的 i2c_client(如 spd / ee1004 / spd5118)。

历史与背景

这项技术解决什么问题而诞生?

  • SMBus Alert:多个从设备共用一根告警线时,主机需要在中断到来后知道“是谁在报警”,并把处理交给正确的设备驱动。
  • Host Notify:部分 SMBus 设备希望主动“通知主机发生事件”,用标准化的总线消息替代单纯的 GPIO 中断或轮询。
  • SPD 自动实例化:PC/服务器场景里,SPD EEPROM/Hub 常常存在于 SMBus 上,但不一定在固件表里显式描述;内核需要一种机制让 SPD 设备“自动出现”,从而加载相应驱动(温度、内存条信息、RAS 等关联功能也会依赖它)。

里程碑/迭代脉络(按功能演进角度)

  • 早期核心是 SMBus Alert 基础设施(工作队列 + ARA 读取 + 回调 driver->alert)。
  • 随后加入 Host Notify:依赖 I2C slave 框架的成熟,把 Host Notify 的固定地址 0x08 作为“从设备端点”挂到适配器上。
  • SPD 自动实例化持续扩展内存类型覆盖范围:从常见 DDR/DDR2/DDR3/DDR4 到 LPDDR 系列,再到 DDR5/LPDDR5 对应的 hub(如 spd5118)选择逻辑。

社区活跃度与主流应用情况(你该怎么判断)

  • I2C/SMBus 属于内核长期维护的基础子系统,接口相对稳定,但会随新硬件能力(如 DDR5 SPD hub、I2C slave、平台固件信息质量)不断补齐边界。
  • 这份文件本身“变化频率通常不如网络/调度那类大子系统”,但在 新硬件/新平台接入时会出现针对性增强(比如 SPD 类型判断与实例化策略)。

核心原理与设计

1) SMBus Alert:从“共享中断”到“回调到对应设备驱动”

关键设计点:不能在硬中断里做会睡眠的 SMBus 访问。

  • 中断到来后,需要做 SMBus 读操作(对 ARA 地址发起 read_byte 获取告警源地址),这是可能睡眠的,因此代码采用:

    • threaded IRQdevm_request_threaded_irq 的线程处理函数)或
    • workqueueschedule_work
      把实际的 SMBus 访问移出硬中断上下文。

告警源识别流程(你读代码可以按这个顺序对照):

  1. 通过对 ARA client 执行 i2c_smbus_read_byte() 得到一个字节 status

  2. 解码得到:

    • addr = status >> 1
    • flag = status & 1
  3. 在该 adapter 上遍历子设备(device_for_each_child(adapter->dev, ...)),找到 client->addr == addr 的那个设备

  4. 如果该设备已绑定驱动,且驱动实现了 driver->alert(),则调用 alert(client, protocol, data)

    • 代码用 device_lock() 防止回调期间驱动对象变化

死循环防护/兜底策略:

  • SMBus Alert 可能出现“读到同一个 addr 反复出现,但没人处理/没人清除告警”的情况。
  • 代码用 prev_addr 记住上一次地址:如果连续读到相同地址且没有被正常处理,会进入一个“强制模式”,对总线上所有实现 alert() 的驱动都回调一次,然后退出循环,避免永远读不完。

2) Host Notify:把“固定地址 0x08”变成一个 I2C slave 端点

实现方式:

  • 创建一个 i2c_client,地址为 0x08,并设置 I2C_CLIENT_SLAVE

  • 调用 i2c_slave_register(client, cb) 注册回调

  • 回调里处理事件:

    • I2C_SLAVE_WRITE_RECEIVED:记录收到的第 1 个字节(当前实现重点抓“设备地址”字段)
    • I2C_SLAVE_STOP:如果收齐预期长度(代码里用常量长度判断),调用 i2c_handle_smbus_host_notify(adapter, addr)

已知局限:

  • 代码里明确写了当前限制:目前没有完整机制把 Host Notify 的 data 参数从 client 侧取出来,所以实现偏“触发通知”,细节数据可能拿不到或不完整(取决于现有 I2C slave 框架能力)。

3) SPD 自动实例化:利用 DMI 信息 + 扫描地址 + 创建 i2c_client

核心思路:

  1. 通过 DMI 枚举内存插槽,跳过空槽,统计 dimm_count

  2. 要求所有已填充槽的 memory type 一致,否则直接放弃实例化(避免类型混杂导致判断错误)

  3. 根据 memory type 选择要实例化的设备类型字符串(决定匹配哪个 SPD 驱动):

    • DDR/DDR2/DDR3/LPDDR(1/2/3) → spd
    • DDR4/LPDDR4 → ee1004
    • DDR5/LPDDR5 → spd5118(并带有额外策略)
  4. 由于不知道条子插在哪个槽,对 0x50..0x57 逐个探测并 i2c_new_scanned_device() 创建

策略点:启动速度与风险权衡

  • 扫描会带来一定启动期开销(最多 8 个地址探测),但换来“无需平台显式描述”的自动发现能力。
  • 对 DDR5/LPDDR5 还有额外 gating(是否 instantiate),体现了对“写保护/写使能风险”的保守处理倾向。

使用场景

首选场景(举例)

  1. 需要 SMBALERT# 的硬件监控/电源管理类芯片

    • 多个设备共用告警引脚,主机通过 ARA 定位源设备,进入对应驱动 alert() 做清除/读取状态寄存器等操作。
  2. 需要 Host Notify 的 SMBus 设备

    • 设备通过 SMBus 消息通知主机发生事件,而不是额外拉一根 GPIO。
  3. PC/服务器内存 SPD 设备自动发现

    • 固件未显式枚举 SPD 节点,但系统希望内核自动创建 SPD/Hub 设备,供 hwmon/EDAC/用户态工具链读取信息。

不推荐场景(以及原因)

  • 总线挂载设备复杂且对探测敏感的嵌入式 I2C:
    SPD 那种“扫描地址探测”的模式可能引发误触发或延迟,不如设备树/ACPI 静态描述可靠。
  • 没有 ARA/没有 SMBALERT 硬件线路的控制器:
    强行引入 SMBus Alert 机制没有意义,且可能造成中断/回调链路不完整(读不到源地址或无法清除)。
  • 需要 Host Notify 里完整 data payload且内核链路无法提供:
    当前实现更像“触发事件”,如果你业务逻辑必须依赖 Host Notify 的两个数据字节,可能需要扩展 I2C slave 框架或走其他事件通道。

对比分析

A) SMBus Alert vs “每设备独立 GPIO 中断”

  • 实现方式

    • SMBus Alert:共享线 + ARA 读回地址 + driver->alert() 回调
    • 独立 GPIO:每设备一个 IRQ,直接进入该设备 ISR/线程化处理
  • 性能开销

    • SMBus Alert:一次中断后可能需要多次 SMBus 读(循环读 ARA)+ 遍历子设备匹配
    • 独立 GPIO:路径更短,通常无需额外总线读来定位源
  • 资源占用

    • SMBus Alert:省 GPIO/IRQ 资源,适合引脚紧张
    • 独立 GPIO:占用更多引脚与中断资源
  • 隔离级别

    • SMBus Alert:共享线意味着“某个设备没清除告警”会影响整条链路,隔离较弱
    • 独立 GPIO:设备间互不干扰,隔离更强
  • 启动速度

    • 二者差异不大;Alert 主要影响运行期中断处理路径

B) Host Notify vs “轮询寄存器”

  • 实现方式

    • Host Notify:设备主动发消息到 0x08,内核触发处理
    • 轮询:定期 SMBus/I2C 读状态寄存器
  • 性能开销

    • Host Notify:事件驱动,空闲时几乎无开销
    • 轮询:持续占用总线带宽与 CPU 周期
  • 资源占用

    • Host Notify:无需额外 GPIO,但需要 I2C slave 支持与回调链路
    • 轮询:不需要 slave 支持,但长期耗资源
  • 隔离级别

    • Host Notify:共享固定地址端点,错误消息可能需要额外判别
    • 轮询:每设备独立访问,逻辑更直观但更耗时
  • 启动速度

    • Host Notify 初始化需要注册一个 slave client;轮询不需要额外初始化但会增加运行期负担

C) SPD 自动实例化 vs “设备树/ACPI 静态描述”

  • 实现方式

    • 自动实例化:DMI 判定 + 地址扫描 + i2c_new_scanned_device
    • 静态描述:固件/设备树直接给出节点
  • 性能开销

    • 自动实例化:启动期探测多个地址,存在额外 I2C 事务
    • 静态描述:启动期几乎无探测成本
  • 资源占用

    • 自动实例化:无需平台为每条 SPD 写描述,但需要扫描与 DMI 支持
    • 静态描述:平台维护成本更高,但系统行为确定
  • 隔离级别

    • 自动实例化:扫描可能触达非目标设备,隔离较弱(依赖总线容忍度)
    • 静态描述:隔离更强、确定性更高
  • 启动速度

    • 静态描述更快、更可预测;自动实例化在缺少描述的平台上更“省集成成本”

总结

关键特性

  • 把 SMBus 的“系统级扩展语义”从基础传输层拆分出来:Alert、Host Notify、SPD 自动实例化各自独立但共享 I2C 子系统框架。
  • Alert 的核心是:ARA 读回告警源地址 + 回调 driver->alert(),并且严格处理“不能在硬中断里睡眠”的上下文约束。
  • Host Notify 的核心是:在 0x08 建立 I2C slave 端点,把写入转成内核事件。
  • SPD 自动实例化的核心是:DMI 推断内存类型 + 扫描 0x50..0x57 自动建 client,强调“在缺少固件描述时也能工作”。

i2c_handle_smbus_alert / smbalert_work / smbus_alert:SMBALERT# 从不可睡眠上下文转移到可睡眠上下文并分发告警


i2c_smbus_alert / alert_data:告警处理的核心数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief SMBALERT# 基础设施的运行时对象。
*
* 该结构由 ARA 设备对应的 i2c_client 持有(clientdata),
* 用于把“不可睡眠上下文的触发”转换为“可睡眠上下文的实际处理”。
*/
struct i2c_smbus_alert {
struct work_struct alert; /**< workqueue 任务实体:在进程上下文执行实际 SMBus 查询与分发。 */
struct i2c_client *ara; /**< ARA(Alert Response Address)对应的 I2C client。 */
};

/**
* @brief 从 ARA 读取到的一次告警信息的抽象。
*
* addr/type/data 由 ARA 返回字节解析得到,用于在总线上定位告警源设备,
* 并向其驱动提供最小化的告警参数。
*/
struct alert_data {
unsigned short addr; /**< 7-bit 目标从设备地址(此处不处理 10-bit)。 */
enum i2c_alert_protocol type; /**< 告警协议类型(本段使用 SMBUS_ALERT)。 */
unsigned int data; /**< 告警附加标志位(此处来自 ARA 返回字节的最低位)。 */
};

i2c_handle_smbus_alert:不可睡眠上下文入口,调度 workqueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief 处理一次 SMBus 告警触发请求(不可睡眠上下文入口)。
*
* @param ara 相关适配器上的 ARA 设备 client,必须已通过 clientdata 绑定 i2c_smbus_alert 对象。
* @return schedule_work() 的返回值:若成功入队返回非 0,否则返回 0。
*
* @note 该函数设计为可在中断处理路径中调用:自身不发起 SMBus 事务,仅做任务调度。
*/
int i2c_handle_smbus_alert(struct i2c_client *ara)
{
struct i2c_smbus_alert *alert = i2c_get_clientdata(ara); /**< 取回 probe 阶段保存的运行时对象。 */

return schedule_work(&alert->alert); /**< 将实际处理延后到进程上下文执行。 */
}

smbalert_work:workqueue 任务体,转入 smbus_alert 执行实际事务

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief workqueue 回调:在可睡眠上下文执行 SMBALERT# 处理主逻辑。
*
* @param work work_struct 指针,由 i2c_smbus_alert.alert 成员嵌入。
*/
static void smbalert_work(struct work_struct *work)
{
struct i2c_smbus_alert *alert;

alert = container_of(work, struct i2c_smbus_alert, alert); /**< 由成员指针反推出宿主结构。 */

smbus_alert(0, alert); /**< 复用与中断线程处理相同的主逻辑;irq 参数在此路径不使用。 */
}

smbus_alert:主处理逻辑,读取 ARA 并分发到告警源设备驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* @brief SMBALERT# 主处理逻辑(在可睡眠上下文中执行)。
*
* @param irq 中断号;当通过 workqueue 调用时可忽略。
* @param d 指向 struct i2c_smbus_alert 的指针。
* @return IRQ_HANDLED。
*
* @details
* 通过 ARA 读取“告警源地址+标志位”,并在该适配器的子设备集合中定位对应 i2c_client,
* 然后触发其 i2c_driver.alert() 回调。
*
* 该函数包含一段“防止非处理告警导致死循环”的控制逻辑:当连续读到同一地址且未被处理,
* 会进入一次“广播式调用所有带 alert() 的驱动”的兜底流程并随后退出。
*/
static irqreturn_t smbus_alert(int irq, void *d)
{
struct i2c_smbus_alert *alert = d; /**< 运行时对象,包含 ARA client 与 work_struct。 */
struct i2c_client *ara; /**< ARA client:用于发起 SMBus read byte 获取告警源。 */
unsigned short prev_addr = I2C_CLIENT_END; /**< 上一次处理的地址,用于检测重复地址导致的潜在非终止循环。 */

ara = alert->ara;

for (;;) {
s32 status; /**< ARA 返回的原始字节或负错误码。 */
struct alert_data data; /**< 解析后的告警信息,用于向下分发。 */

status = i2c_smbus_read_byte(ara); /**< 向 ARA 发起 SMBus 读:可能睡眠,因此必须在可睡眠上下文调用。 */
if (status < 0)
break; /**< 读失败或无更多告警时退出循环。 */

data.data = status & 1; /**< ARA 返回字节最低位作为附加标志。 */
data.addr = status >> 1; /**< 高 7 位为告警源 7-bit 地址。 */
data.type = I2C_PROTOCOL_SMBUS_ALERT; /**< 标记为 SMBUS_ALERT 协议类型。 */

dev_dbg(&ara->dev, "SMBALERT# from dev 0x%02x, flag %d\n",
data.addr, data.data);

/**
* 在该 adapter 的子设备集合中查找地址匹配的 i2c_client,
* 并触发其驱动的 alert() 回调。
*
* 返回值语义依赖回调实现:典型地用 -EBUSY 表示“已处理并应停止迭代”。
*/
status = device_for_each_child(&ara->adapter->dev, &data,
smbus_do_alert);

/**
* 兜底逻辑:若连续两次读到同一地址,且上一次未得到“已处理”的信号,
* 继续循环将可能永不终止(告警源未清除、ARA 反复报告同一设备)。
*
* 因此改为对总线上的所有已绑定驱动且具备 alert() 的设备进行一次强制回调,
* 然后退出循环,避免在内核线程中形成不可控的长时间占用。
*/
if (data.addr == prev_addr && status != -EBUSY) {
device_for_each_child(&ara->adapter->dev, &data,
smbus_do_alert_force);
break;
}
prev_addr = data.addr; /**< 记录本次地址用于下一轮重复检测。 */
}

return IRQ_HANDLED;
}

module_i2c_driver / smbus_do_alert / smbus_do_alert_force / smbalert_probe / smbalert_remove:SMBALERT# 驱动注册、告警分发与资源管理


module_i2c_driver:I2C 驱动模块化注册宏

作用与原理(仅保留关键点)

  • 该宏把 I2C 驱动的注册/注销与模块的 init/exit 绑定,减少模板代码。
  • 其核心是将 i2c_add_driver() / i2c_del_driver() 作为模块加载/卸载时的动作。
1
2
3
4
5
6
7
8
/**
* @brief I2C 驱动模块化注册辅助宏。
*
* 语义:模块加载时调用 i2c_add_driver(&driver),模块卸载时调用 i2c_del_driver(&driver)。
* 适用于无需自定义 init/exit 逻辑的 I2C 驱动。
*/
#define module_i2c_driver(__i2c_driver) \
module_driver(__i2c_driver, i2c_add_driver, i2c_del_driver)

smbus_do_alert:仅通知“告警源地址匹配”的设备驱动,并用返回码控制迭代终止

作用与原理

  • device_for_each_child() 会遍历适配器下的子设备(挂在该 I2C 适配器上的所有 i2c_client 对应 device)。
  • 回调返回 非 0 时会停止遍历,并把该值作为 device_for_each_child() 的返回值上抛。
  • 本函数用 -EBUSY 表示“已找到告警源并已通知其驱动,应停止继续遍历”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* @brief 对指定地址的告警源设备进行精确分发(仅匹配地址的 client 会被通知)。
*
* @param dev 被遍历到的子设备(预期为 i2c_client 对应的 struct device)。
* @param addrp 指向 struct alert_data 的指针,包含告警源地址与参数。
*
* @return
* 0:当前 dev 非目标设备或无需处理,继续遍历;
* -EBUSY:已通知目标设备驱动,停止遍历;
* 其他负值:用于表达“不支持/无驱动”等状态,上层可据此决定是否兜底处理。
*/
static int smbus_do_alert(struct device *dev, void *addrp)
{
struct i2c_client *client = i2c_verify_client(dev); /**< 将 device 校验/转换为 i2c_client;失败返回 NULL。 */
struct alert_data *data = addrp; /**< 告警参数:告警源地址、协议类型、附加数据。 */
struct i2c_driver *driver;
int ret;

if (!client || client->addr != data->addr)
return 0; /**< 非目标地址:继续遍历其他子设备。 */
if (client->flags & I2C_CLIENT_TEN)
return 0; /**< 10-bit 地址不在本处理范围内:跳过。 */

/**
* @note device_lock() 关键点:
* 用于稳定 client->dev.driver 指针及其派生的 i2c_driver 对象。
* 单核下仍必要:可能与解绑/重绑定、或其他上下文对 driver 指针的变更交错。
*/
device_lock(dev);
if (client->dev.driver) {
driver = to_i2c_driver(client->dev.driver); /**< 将通用 driver 转为 i2c_driver,以访问 alert 回调。 */
if (driver->alert) {
driver->alert(client, data->type, data->data);/**< 调用设备驱动的告警处理回调。 */
ret = -EBUSY; /**< 用非 0 返回值终止 device_for_each_child() 迭代。 */
} else {
dev_warn(&client->dev, "no driver alert()!\n");/**< 驱动未实现告警回调:属于能力缺失。 */
ret = -EOPNOTSUPP;
}
} else {
dev_dbg(&client->dev, "alert with no driver\n"); /**< 设备未绑定驱动:无法分发到具体处理逻辑。 */
ret = -ENODEV;
}
device_unlock(dev);

return ret;
}

smbus_do_alert_force:不按地址过滤,调用所有实现了 alert() 的设备驱动(兜底广播)

作用与原理

  • 用于处理“ARA 反复报告同一地址且未被任何驱动有效处理”的场景。
  • smbus_do_alert 的关键差异:不再要求 client->addr == data->addr,而是对所有子设备中实现 driver->alert 的都调用一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @brief 告警兜底分发:对总线上所有实现 alert() 的驱动进行回调(不做地址过滤)。
*
* @param dev 被遍历到的子设备。
* @param addrp 指向 struct alert_data 的指针(作为回调参数传递)。
*
* @return 固定返回 0:不中止遍历,保证“广播式”覆盖所有设备。
*/
static int smbus_do_alert_force(struct device *dev, void *addrp)
{
struct i2c_client *client = i2c_verify_client(dev);
struct alert_data *data = addrp;
struct i2c_driver *driver;

if (!client || (client->flags & I2C_CLIENT_TEN))
return 0; /**< 非 i2c_client 或 10-bit:跳过。 */

device_lock(dev); /**< 稳定 dev.driver 与回调表,避免并发解绑导致野指针风险。 */
if (client->dev.driver) {
driver = to_i2c_driver(client->dev.driver);
if (driver->alert)
driver->alert(client, data->type, data->data);/**< 广播式通知:由各驱动自行判定是否与自身相关并清除告警源。 */
}
device_unlock(dev);

return 0;
}

smbalert_probe:建立 SMBALERT# 基础设施(workqueue + 可选 threaded IRQ)

作用与原理

  • 创建并初始化 struct i2c_smbus_alert,并挂到 ARA i2c_client 的 clientdata。

  • IRQ 获取顺序体现了“平台差异屏蔽”策略:

    1. 优先使用平台数据 setup->irq
    2. 否则尝试从固件节点按名获取 "smbus_alert" IRQ;
    3. 再否则回退为从 GPIO "smbalert" 获取并转换为 IRQ,并设置下降沿触发。
  • 若成功拿到 IRQ,则使用 devm_request_threaded_irq() 注册 线程化中断处理函数 smbus_alert(thread_fn),以保证可以执行可能睡眠的 SMBus 读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* @brief ARA 设备 probe:初始化 SMBALERT# 处理对象并按需注册 IRQ。
*
* @param ara ARA(Alert Response Address)对应的 i2c_client。
* @return 0 成功;负 errno 失败。
*/
static int smbalert_probe(struct i2c_client *ara)
{
struct i2c_smbus_alert_setup *setup = dev_get_platdata(&ara->dev); /**< 可选平台数据:可能直接提供 irq。 */
struct i2c_smbus_alert *alert;
struct i2c_adapter *adapter = ara->adapter;
unsigned long irqflags = IRQF_SHARED | IRQF_ONESHOT; /**< ONESHOT:线程处理中屏蔽该 IRQ,避免重入。 */
struct gpio_desc *gpiod;
int res, irq;

alert = devm_kzalloc(&ara->dev, sizeof(struct i2c_smbus_alert),
GFP_KERNEL); /**< devm 托管:设备移除时自动释放。 */
if (!alert)
return -ENOMEM;

if (setup) {
irq = setup->irq; /**< 平台显式提供 IRQ:直接使用。 */
} else {
irq = fwnode_irq_get_byname(dev_fwnode(adapter->dev.parent),
"smbus_alert"); /**< 从固件节点按名获取 IRQ。 */
if (irq <= 0) {
gpiod = devm_gpiod_get(adapter->dev.parent, "smbalert", GPIOD_IN); /**< 回退:用 GPIO 描述符获取 SMBALERT# 输入。 */
if (IS_ERR(gpiod))
return PTR_ERR(gpiod);

irq = gpiod_to_irq(gpiod); /**< GPIO -> IRQ 映射。 */
if (irq <= 0)
return irq;

irqflags |= IRQF_TRIGGER_FALLING; /**< SMBALERT# 常见为低有效:下降沿触发。 */
}
}

INIT_WORK(&alert->alert, smbalert_work); /**< 初始化 work:用于 i2c_handle_smbus_alert() 路径调度。 */
alert->ara = ara; /**< 保存 ARA client,供 smbus_alert() 发起 SMBus 读。 */

if (irq > 0) {
res = devm_request_threaded_irq(&ara->dev, irq, NULL, smbus_alert,
irqflags, "smbus_alert", alert); /**< 线程化中断:thread_fn 可睡眠,适配 SMBus 事务。 */
if (res)
return res;
}

i2c_set_clientdata(ara, alert); /**< 绑定运行时对象:供 remove 与 i2c_handle_smbus_alert 取回。 */
dev_info(&adapter->dev, "supports SMBALERT#\n");

return 0;
}

smbalert_remove:移除时同步取消 work,避免卸载后访问已释放对象

作用与原理

  • 使用 cancel_work_sync() 保证:

    • 若 work 尚未执行:从队列移除;
    • 若 work 正在执行:等待其结束;
  • 防止出现 work 回调继续访问 alertara 导致的释放后使用。

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief ARA 设备 remove:取消可能排队/执行中的 work,保证资源安全回收。
*
* @param ara ARA 对应的 i2c_client。
*/
static void smbalert_remove(struct i2c_client *ara)
{
struct i2c_smbus_alert *alert = i2c_get_clientdata(ara); /**< 取回 probe 时设置的运行时对象。 */

cancel_work_sync(&alert->alert); /**< 同步取消:避免卸载后 work 仍访问对象。 */
}