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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
|
@page page_device_i2c_dm I2C device model (DM)
# I2C bus framework under `RT_USING_DM`
When **`RT_USING_DM`** and **`RT_USING_I2C`** are enabled, RT-Thread adds an **I2C-specific `rt_bus`** (`name = "i2c"`) on top of the core DM bus layer (@ref page_device_bus). **Bus controllers** still register as **`RT_Device_Class_I2CBUS`** devices (`i2c0`, `i2c1`, …); **slave drivers** bind as **`struct rt_i2c_client`** devices discovered from device tree children.
Legacy **manual** usage (`rt_device_find("i2c1")` + `rt_i2c_transfer`) remains valid — see @ref page_device_i2c. This page covers **automatic DT binding** and **`RT_I2C_DRIVER_EXPORT`**.
Sources:
| File | Role |
| --- | --- |
| **`dev_i2c_bus.c`** | **`struct rt_bus i2c_bus`**, match/probe, **`i2c_bus_scan_clients`** |
| **`dev_i2c_dm.c`** | **`i2c_timings_ofw_parse`** |
| **`dev_i2c_core.c`** | Calls **`i2c_bus_scan_clients`** after **`rt_i2c_bus_device_register`** |
| **`dev_i2c.h`** | **`rt_i2c_client`**, **`rt_i2c_driver`**, **`RT_I2C_DRIVER_EXPORT`** |
| **`dev_i2c_dm.h`** | **`struct i2c_timings`**, scan/timings declarations |
---
## Kconfig
| Option | Role |
| --- | --- |
| **`RT_USING_I2C`** | Core I2C stack (`dev_i2c_core.c`, `dev_i2c_dev.c`, …) |
| **`RT_USING_DM`** | Adds **`dev_i2c_bus.c`** + **`dev_i2c_dm.c`** (I2C bus + OFW scan) |
| **`RT_USING_OFW`** | Required for **`i2c_bus_scan_clients`** and **`i2c_timings_ofw_parse`** |
| **`SOC_DM_I2C_DIR`** | BSP SoC adapters (e.g. `i2c-rk3x.c`) |
Without **`RT_USING_DM`**, **`struct rt_i2c_client`** has no embedded **`rt_device`** — only **`bus`** + **`client_addr`**.
---
## Architecture
```
Platform I2C controller (e.g. rk3x, bit-bang)
|
| probe: i2c_timings_ofw_parse, rt_dm_dev_iomap, clocks
| bus->parent.ofw_node = controller np
| rt_i2c_bus_device_register(&bus->parent, "i2cN")
v
rt_device "i2cN" (RT_Device_Class_I2CBUS, user_data -> rt_i2c_bus_device)
|
| i2c_bus_scan_clients(bus) /* dev_i2c_core.c */
v
For each available child in DT:
alloc rt_i2c_client, reg -> client_addr, ofw_node
rt_i2c_device_register(client) -> rt_bus_add_device("i2c", ...)
v
i2c_match: driver ids[] name OR ofw_ids compatible
|
v
i2c_probe -> driver->probe(client)
|
v
Client driver uses client->bus + rt_i2c_transfer / master_send
```
**Two bus layers**:
| Layer | Object | Example |
| --- | --- | --- |
| **I2C bus device** | **`struct rt_i2c_bus_device`** | Hardware adapter; **`master_xfer`**, mutex **`lock`** |
| **DM I2C bus** | **`struct rt_bus` `i2c_bus`** | Binds **`rt_i2c_driver`** ↔ **`rt_i2c_client`** |
Core DM **`platform`** bus probes the **controller**; subsystem **`i2c`** bus probes **clients** under that controller’s OFW node.
---
## `struct rt_bus i2c` (dev_i2c_bus.c)
Registered at boot: **`INIT_CORE_EXPORT(i2c_bus_init)`** → **`rt_bus_register(&i2c_bus)`**.
| Callback | Behavior |
| --- | --- |
| **`i2c_match`** | 1) Match **`driver->ids[].name`** to **`client->name`** (from DT node name). 2) Else **`rt_ofw_node_match(client->ofw_node, driver->ofw_ids)`**. Sets **`client->id`** or **`client->ofw_id`**. |
| **`i2c_probe`** | Requires **`client->bus`**; calls **`driver->probe(client)`**. |
| **`i2c_remove` / `i2c_shutdown`** | Forward to driver hooks. |
**Driver registration**:
```c
rt_err_t rt_i2c_driver_register(struct rt_i2c_driver *driver);
/* RT_I2C_DRIVER_EXPORT(drv) -> RT_DRIVER_EXPORT(drv, i2c, BUILIN) */
```
Sets **`driver->parent.bus = &i2c_bus`** then **`rt_driver_register`**.
**Client registration**:
```c
rt_err_t rt_i2c_device_register(struct rt_i2c_client *client);
/* internally: rt_bus_add_device(&i2c_bus, &client->parent) */
```
---
## Device tree: controller + clients
### Controller node
```dts
i2c1: i2c@ff160000 {
compatible = "rockchip,rk3588-i2c", "rockchip,rk3066-i2c";
reg = <0x0 0xff160000 0x0 0x1000>;
interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru CLK_I2C1>, <&cru PCLK_I2C1>;
clock-names = "i2c", "pclk";
#address-cells = <1>;
#size-cells = <0>;
rtc@68 {
compatible = "dallas,ds1307";
reg = <0x68>;
};
};
```
| Property | Role |
| --- | --- |
| **`clock-frequency`** | Parsed into **`struct i2c_timings.bus_freq_hz`** |
| **`i2c-scl-rising-time-ns`**, **`i2c-scl-falling-time-ns`**, … | Optional timing cells — **`i2c_timings_ofw_parse`** |
| **`#address-cells = <1>`**, **`#size-cells = <0>`** | Standard I2C child addressing |
| **Child `reg`** | 7-bit (or 10-bit) slave address → **`client->client_addr`** |
### Client discovery (`i2c_bus_scan_clients`)
Called automatically when **`rt_i2c_bus_device_register`** succeeds (**`RT_USING_DM`** only).
For each **available** child of **`bus->parent.ofw_node`**:
1. If child has **`compatible`**, use it as client node; else look one level deeper (**i2c-mux** style).
2. **`rt_calloc`** **`struct rt_i2c_client`**.
3. Read **`reg`** → **`client_addr`**.
4. **`client->parent.ofw_node`**, **`client->name = rt_ofw_node_name`**, **`client->bus = bus`**.
5. **`rt_dm_dev_set_name(&client->parent, "%s", name)`**.
6. **`rt_i2c_device_register(client)`** → triggers **`i2c_match`** + **`probe`** if a driver is already registered.
**Order note**: If the **client driver** registers before the **bus**, clients are not scanned yet. Typically **controller `probe` runs first** (platform init), then scan instantiates clients while **I2C drivers** are already linked via **`RT_I2C_DRIVER_EXPORT`**. If a driver loads late, you may need manual **`rt_i2c_device_register`** or re-scan (not provided in-tree — ensure init order).
---
## Bus controller driver (adapter)
Pattern from SoC drivers (e.g. **`bsp/rockchip/dm/i2c/i2c-rk3x.c`**):
```c
static rt_err_t my_i2c_probe(struct rt_platform_device *pdev)
{
struct rt_device *dev = &pdev->parent;
struct my_i2c *i2c = rt_calloc(1, sizeof(*i2c));
i2c_timings_ofw_parse(dev->ofw_node, &i2c->timings, RT_TRUE);
i2c->regs = rt_dm_dev_iomap(dev, 0);
i2c->irq = rt_dm_dev_get_irq(dev, 0);
/* clocks: rt_clk_get_by_name, prepare_enable */
rt_dm_dev_set_name_auto(&i2c->parent.parent, "i2c");
i2c->parent.ops = &my_i2c_ops;
i2c->parent.parent.ofw_node = dev->ofw_node;
dev->user_data = i2c;
return rt_i2c_bus_device_register(&i2c->parent, rt_dm_dev_get_name(dev));
}
```
| Step | API |
| --- | --- |
| Timings | **`i2c_timings_ofw_parse(np, &timings, use_defaults)`** |
| Resources | **`rt_dm_dev_iomap`**, **`rt_dm_dev_get_irq`**, **`rt_clk_*`** (@ref page_device_clk) |
| Naming | **`rt_dm_dev_set_name`** / **`rt_dm_dev_set_name_auto`** → **`i2c0`**, **`i2c1`**, … |
| Publish bus | **`rt_i2c_bus_device_register(&i2c->parent, name)`** — triggers **client scan** |
**`rt_i2c_bus_device`** embeds **`struct rt_device parent`**; **`user_data`** on that device points at the full adapter struct (controller private data).
Bit-bang buses (**`RT_USING_I2C_BITOPS`**) call **`rt_i2c_bit_add_bus`** → same **`rt_i2c_bus_device_register`** path.
---
## Client driver (I2C slave)
```c
static const struct rt_ofw_node_id my_ids[] = {
{ .compatible = "vendor,my-sensor", .data = (void *)&chip_info },
{ /* sentinel */ },
};
static const struct rt_i2c_device_id my_tbl[] = {
{ .name = "my-sensor", .data = (void *)&chip_info },
{ /* sentinel */ },
};
static struct rt_i2c_driver my_driver = {
.ids = my_tbl, /* optional: match by DT node name */
.ofw_ids = my_ids, /* match by compatible */
.probe = my_probe,
.remove = my_remove,
};
RT_I2C_DRIVER_EXPORT(my_driver);
static rt_err_t my_probe(struct rt_i2c_client *client)
{
struct rt_i2c_bus_device *bus = client->bus;
rt_uint16_t addr = client->client_addr;
const void *cfg = rt_i2c_client_id_data(client);
/* Use rt_i2c_master_send/recv or rt_i2c_transfer on client->bus */
return RT_EOK;
}
```
| Field / API | Role |
| --- | --- |
| **`client->bus`** | Parent adapter — **must** be set (scan sets it; manual clients must assign). |
| **`client->client_addr`** | From DT **`reg`** (scan) or driver setup. |
| **`client->parent.ofw_node`** | Child DT node for **`rt_dm_dev_get_irq`** etc. |
| **`rt_i2c_client_id_data(client)`** | **`id->data`** or **`ofw_id->data`** from match table |
After **`probe`**, many drivers register a functional device (RTC, sensor, regulator) on top of the client, using **`client->parent`** only as the DM binding handle.
---
## `struct i2c_timings` (OFW)
Defined in **`dev_i2c_dm.h`**. **`i2c_timings_ofw_parse(dev_np, timings, use_defaults)`** reads:
| DT property | Field |
| --- | --- |
| **`clock-frequency`** | **`bus_freq_hz`** (default 100 kHz if missing and **`use_defaults`**) |
| **`i2c-scl-rising-time-ns`** | **`scl_rise_ns`** |
| **`i2c-scl-falling-time-ns`** | **`scl_fall_ns`** |
| **`i2c-scl-internal-delay-ns`** | **`scl_int_delay_ns`** |
| **`i2c-sda-falling-time-ns`** | **`sda_fall_ns`** |
| **`i2c-sda-hold-time-ns`** | **`sda_hold_ns`** |
| **`i2c-digital-filter-width-ns`** | **`digital_filter_width_ns`** |
| **`i2c-analog-filter-cutoff-frequency`** | **`analog_filter_cutoff_freq_hz`** |
Controller **`probe`** passes parsed timings into IP-specific **`calc_timings`** (SoC-specific).
---
## Transfer path (unchanged by DM)
DM only changes **how clients appear**. Data path is still:
```c
rt_i2c_bus_lock(bus, timeout);
rt_i2c_transfer(bus, msgs, num); /* -> ops->master_xfer */
rt_i2c_bus_unlock(bus);
```
Or **`rt_i2c_master_send` / `rt_i2c_master_recv`**. Bus mutex serializes masters on one adapter.
---
## DM vs non-DM summary
| Topic | With **`RT_USING_DM`** | Without DM |
| --- | --- | --- |
| Client struct | **`rt_i2c_client`** embeds **`rt_device parent`** | **`bus` + `client_addr` only |
| Binding | **`RT_I2C_DRIVER_EXPORT`** + DT scan | App finds bus, bit-bangs address manually |
| Bus register side effect | **`i2c_bus_scan_clients`** | No scan |
| Timing helpers | **`i2c_timings_ofw_parse`** | Driver parses DT itself or fixed Hz |
---
## Engineer checklist
1. **Controller**: platform **`probe`** → timings + resources → **`rt_i2c_bus_device_register`** with **`ofw_node`** on **`bus->parent`**.
2. **Client driver**: **`ofw_ids`** + optional **`ids`**, **`RT_I2C_DRIVER_EXPORT`**, **`probe`** uses **`client->bus`** and **`client_addr`**.
3. **DT**: controller **`#address-cells` / `#size-cells`**, child **`reg`** + **`compatible`**.
4. **Transfers**: always through **`client->bus`**, respect **`rt_i2c_bus_lock`** if sharing with other code.
5. **Remove**: controller **`remove`** should **`rt_device_unregister`** bus; clients removed via **`i2c_remove`** when bus tears down (verify BSP order).
---
## Pitfalls
- **`client->bus == NULL`** in **`i2c_probe`** → **`-RT_EINVAL`** — scan did not run or client registered manually without **`bus`**.
- **Wrong `reg` address** — probe succeeds but **`rt_i2c_transfer`** NACKs on wire.
- **Driver before bus** — no automatic re-scan; child never **`probe`** until bus registers.
- **i2c-mux depth** — scan only handles **one** level of non-compatible wrapper; deeper mux trees may need explicit client creation.
- **Name vs compatible match** — **`ids[].name`** must match DT **node name**, not **`compatible`** string; prefer **`ofw_ids`** for compatible-based binding.
- **Mixing DM client with legacy app** — app can still **`rt_i2c_bus_device_find("i2c1")`**; ensure mutex discipline if both access the same bus.
---
## See also
- @ref page_device_i2c — protocol, **`rt_i2c_transfer`**, application usage
- @ref page_device_bus — core **`rt_bus`** / **`rt_driver`**
- @ref page_device_dm — **`rt_dm_dev_iomap`**, naming
- @ref page_device_platform — controller **`RT_PLATFORM_DRIVER_EXPORT`**
- @ref page_device_ofw — DT properties
- `components/drivers/include/drivers/dev_i2c.h`
- `components/drivers/i2c/dev_i2c_bus.c`, `dev_i2c_core.c`, `dev_i2c_dm.c`
|