Linux内核DesignWare I2C驱动解析:寄存器、传输与时序调优
2026/9/13 2:58:24 网站建设 项目流程

简介:这份资源是Synopsys DesignWare I2C Core的驱动源代码压缩包,面向嵌入式系统开发者与Linux内核驱动工程师,专门解决SoC上DesignWare I2C硬核作为总线主控制器时在操作系统中的驱动对接与运行问题。包内共2个文件,包括1个C源码文件和1个头文件,整体仅8KB,结构精简,可直接阅读和移植;C源文件实现了I2C主设备模式下的初始化、收发命令、数据处理等核心逻辑,头文件则包含函数声明、寄存器定义与结构体封装,完整呈现了驱动与硬件层的接口关系。由于驱动仅支持主设备模式,尤其适合需要控制传感器、存储芯片等外部I2C从设备的场景。当前已有441人学习,适合正在研究Synopsys DesignWare系列IP或需要在Linux下编写I2C驱动的工程师参考。通过分析该驱动,可以快速掌握I2C控制器驱动的框架、寄存器操作要点以及Linux内核驱动模型的对接方式,为后续平台移植或硬件调试提供直接依据。

1. 从 i2c-designware-core.c 开始:DesignWare I2C 驱动到底在管什么

解包拿到i2c-designware-core.c那一刻,基本就摸到了 Synopsys DesignWare I2C 控制器在 Linux 内核里的主干。这个文件不针对某个具体 SoC,而是把所有 DesignWare I2C IP 的通用逻辑收敛在一起:寄存器读写、中断处理、传输状态机、时序计算。真正和板卡绑定的部分,由i2c-designware-platdrv.ci2c-designware-pcidrv.c去填充时钟和设备资源。做板卡 bringup 时会遇到三种典型需求:把驱动移植到新平台、调 400kHz 速率下的随机 NACK、排查总线卡死。下面按读源码和调板子的顺序展开,先讲寄存器模型,再走一遍主机传输路径,然后落到时序参数和验证手段上。

2. DesignWare I2C 控制器寄存器模型:core 层为什么能跨平台复用

2.1 用一张寄存器表把控制器的脾气说清楚

DesignWare I2C 的寄存器以 32 位宽挂在 APB 总线上,内核里统一用dw_readl / dw_writel访问。虽然 IP 版本很多,但寄存器布局相对稳定,这也是同一个 core 文件能服务大量 SoC 的前提。

寄存器名关键位域作用什么时候操作它
DW_IC_CON主机/从机使能、是否禁用从机、是否启用重复起始、速度档位编码probe 阶段确定master_cfg,每次 transfer 前重新写回
DW_IC_TAR本次事务的目标地址,7 位或 10 位,由 bit9 区分每次 xfer_init 时写入
DW_IC_DATA_CMDbit0-7 数据;bit8 为 1 表示读命令;bit9 为 1 表示 STOP;bit10 为 1 表示 RESTART读写数据的唯一入口
DW_IC_RX_TL / DW_IC_TX_TL接收/发送 FIFO 触发中断的水位阈值初始化时按 FIFO 深度计算
DW_IC_RAW_INTR_STAT原始中断状态,包含 RX_FULL、TX_EMPTY、TX_ABRT、STOP_DET 等中断 handler 里读取并判决
DW_IC_TX_ABRT_SOURCE上一次事务中止的具体原因位图收到 TX_ABRT 中断后必须读
DW_IC_FS_SPKLEN快速模式尖峰抑制长度,用于滤掉 SCL/SDA 上的毛刺400kHz 通信不稳时调节

从这张表可以看出,驱动每次传输都要重新规划目标地址、速度档位和起始/停止条件,而不是在 probe 时配一次就结束。DW_IC_CON里尤其要注意 bit2 的 RESTART_EN,它决定了连续多个i2c_msg之间是走重复起始还是先 STOP 再 START。复合事务是否会被拆开,就是由这个位控制的。

2.1.1 速度档位的编码不要写死

DW_IC_CON的 bit6-bit7 编码速度模式,常见定义如下:

#define DW_IC_CON_SPEED_STD 0x1 #define DW_IC_CON_SPEED_FAST 0x2 #define DW_IC_CON_SPEED_HIGH 0x3 #define DW_IC_CON_SPEED_SHIFT 6

master_cfg通常是这样拼出来的:

dev->master_cfg = DW_IC_CON_MASTER | DW_IC_CON_SLAVE_DISABLE | DW_IC_CON_RESTART_EN | DW_IC_CON_SPEED_FAST << DW_IC_CON_SPEED_SHIFT;

速度档位必须左移到位域上,很多第一次移植的人直接把0x2写进 CON,结果控制器按标准模式 100kHz 跑,外设偶发超时。关注dev->master_cfg的最终值,是排查速率不对的第一入口。

2.2 core、platdrv、pcidrv 三层为什么这么切

i2c-designware-core.c本身不注册平台设备,也不直接管理时钟,它只提供一组内部接口给两个前端驱动调用。核心结构体dw_i2c_dev把硬件资源和回调都抽象了出来,下面这个简化定义基本对应实际代码:

struct dw_i2c_dev { void __iomem *base; struct clk *clk; u32 (*get_clk_rate_khz)(struct dw_i2c_dev *dev); u32 master_cfg; u32 rx_fifo_depth; u32 tx_fifo_depth; struct i2c_adapter adapter; struct completion cmd_complete; };

平台驱动侧要做的事情很明确:从设备树或 ACPI 拿寄存器基址和中断,创建dw_i2c_dev,把get_clk_rate_khz指向自己的实现,然后调用 core 的i2c_dw_probe()

dev->base = devm_platform_ioremap_resource(pdev, 0); dev->get_clk_rate_khz = i2c_dw_get_clk_rate_khz; ret = i2c_dw_probe(dev);

这种分层最大的好处是,PCI 平台只需要把资源获取方式换成 PCI BAR,传输逻辑一字不改。get_clk_rate_khz为什么必须做成回调?因为 DesignWare I2C 的输入时钟在不同 SoC 上可能是独立时钟、父子时钟或固定 PLL,core 层没法假设一个统一频率来源。

2.3 用 COMP_PARAM 寄存器自动探测 FIFO 深度

另一个让 core 层通用化的关键是运行时探测。DesignWare I2C 提供一组DW_IC_COMP_PARAM_*寄存器,用来报告 IP 实例的配置参数,包括 FIFO 深度、地址模式、是否支持 HS 模式等。内核在i2c_dw_probe()里会做这样一件事:

u32 param1 = dw_readl(dev, DW_IC_COMP_PARAM_1); /* 寄存器按 depth-1 编码,读出后加 1 才是真实深度 */ dev->rx_fifo_depth = ((param1 >> 8) & 0xff) + 1; dev->tx_fifo_depth = ((param1 >> 16) & 0xff) + 1;

FIFO 深度决定了两件事:中断水位线怎么设,以及 DMA 传输在剩余多少字节时切回 PIO。如果移植时手动写死 64 或 256,DMA 余数处理就可能错位,出现“最后一个字节读不到”的怪问题。用探测值而不是平台传值,是这套驱动稳健的原因之一。

core 层还在这里登记适配器能力:

static u32 i2c_dw_func(struct i2c_adapter *adap) { return I2C_FUNC_I2C | I2C_FUNC_10BIT_ADDR | I2C_FUNC_SMBUS_BYTE | I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA | I2C_FUNC_SMBUS_I2C_BLOCK; }

I2C_FUNC_I2C表示支持裸 I2C 读写,I2C_FUNC_SMBUS_*表示 SMBus 相关命令可以在用户空间直接发。i2cdetect能扫出哪些地址、i2cget能用哪些传输类型,都由这个返回值决定。

3. 主机传输路径:DesignWare I2C 从 i2c_transfer 到 IC_DATA_CMD 的信令细节

3.1 adapter 和 master_xfer 是怎么挂上的

用户空间通过/dev/i2c-N发起读写时,最终都会走到i2c_transfer(),再由适配器的算法层分发到具体控制器。DesignWare 的适配器算法定义很精简:

static const struct i2c_algorithm i2c_dw_algo = { .master_xfer = i2c_dw_xfer, .functionality = i2c_dw_func, };

i2c_dw_xfer()不做逐字节搬运,而是负责三件事:等待上一次传输结束、初始化本次事务的状态机、然后阻塞等待完成信号量。真正的数据流在中断里,或者在 DMA 完成回调里推进。

平台驱动注册适配器时,一般会设置adap->nr,也可以设为-1让内核动态分配总线号。Probe 成功后/proc/device-tree/sys/bus/i2c/devices下就能看到对应的i2c-N

3.2 状态机字段:msg_write_idx 和 msg_read_idx

一次i2c_transfer可能携带多个i2c_msg,写读交替也常见。DesignWare 驱动用两个游标记录进度:

dev->msgs = msgs; dev->msgs_num = num; dev->msg_write_idx = 0; dev->msg_read_idx = 0; dev->cmd_err = 0; i2c_dw_disable(dev); dw_writel(dev, msgs[0].addr, DW_IC_TAR); dw_writel(dev, dev->master_cfg, DW_IC_CON); i2c_dw_clear_int(dev); dw_writel(dev, 1, DW_IC_ENABLE); dw_writel(dev, DW_IC_INTR_MASTER_MASK, DW_IC_INTR_MASK); wait_for_completion_timeout(&dev->cmd_complete, adap->timeout);

这段初始化里有个容易被忽略的细节:写DW_IC_TARDW_IC_CON之前要先 disable 控制器,改完再 enable,否则新配置不生效。不同版本的 IP 对寄存器写入时序要求不太一样,统一先关后开是兼容性最好的做法。

cmd_complete由中断里的最后一步触发。等待超时后会查dev->cmd_err,它保存的是中断中记录的 errno。这个 errno 会被翻译成i2c_transfer()的返回值,用户空间的read()/write()再收到-EIO-ENXIO-ETIMEDOUT

3.2.1 重复起始在哪里生效

多个i2c_msg之间是否需要 STOP 再 START,取决于硬件是否支持 RESTART。驱动在master_cfg里打开了DW_IC_CON_RESTART_EN,所以经典的“先写寄存器地址再读数据”组合事务会在一个总线周期内完成,不会在中间释放总线。这也是i2ctransfer可以一次发w2@0x50 ... r4而不会丢地址的原因。

3.3 IC_DATA_CMD:写一个字节、读一个字节的信令

这是整个控制器最核心的寄存器,数据、方向、STOP、RESTART 全部通过它表达。

位域含义
bit7-0要发送的数据,或读到的数据
bit8写 1 表示发起一次读操作
bit9写 1 表示当前字节结束后发送 STOP 条件
bit10写 1 表示当前字节结束后发送 RESTART 条件

写单个字节时,把数据填到低 8 位,最后一个字节把 bit9 置 1:

u32 cmd = data & 0xff; if (last) cmd |= DW_IC_CMD_STOP; dw_writel(dev, cmd, DW_IC_DATA_CMD);

读操作则相反,想读取一字节,必须先向 FIFO 写入一个“空的读请求”,也就是只置 bit8:

/* 发起一次读,数据位无效 */ dw_writel(dev, DW_IC_DATA_CMD | DW_IC_CMD_READ, DW_IC_DATA_CMD);

随后在RX_FULL中断里把数据取走:

u32 val = dw_readl(dev, DW_IC_DATA_CMD); *rx_buf++ = (u8)(val & 0xff);

这里有个常见误区:直接dw_readl(DW_IC_DATA_CMD)不会主动触发总线上的读动作,必须先把读命令写进 FIFO,控制器才会在总线上产生读时钟。反过来,中断里漏读了DW_IC_DATA_CMD,FIFO 会被读数据占满,后续中断全部停摆。PIO 模式下,DW_IC_RX_TL水位决定了什么时候触发 RX_FULL,驱动默认会把水位设在 FIFO 深度的一半,避免太频繁进中断。

3.4 TX_ABRT 是怎么被翻译成 errno 的

总线出错时,控制器会同时拉起DW_IC_INTR_TX_ABRT中断,并锁存原因到DW_IC_TX_ABRT_SOURCE。驱动必须在这个中断里把原因读出来,否则后续判断都会失真:

if (stat & DW_IC_INTR_TX_ABRT) { u32 src = dw_readl(dev, DW_IC_TX_ABRT_SOURCE); if (src & DW_IC_TX_ABRT_7B_ADDR_NOACK) dev->cmd_err = -ENXIO; else if (src & DW_IC_TX_ABRT_ARB_LOST) dev->cmd_err = -EAGAIN; else dev->cmd_err = -EIO; }

ABRT_7B_ADDR_NOACK对应目标地址无应答,这通常出现在外设地址写错、器件没上电或上拉电阻缺失的场景。ABRT_ARB_LOST则说明有其他主机同时在占用总线。用户空间收到-ENXIO时,第一反应应该是核对地址而不是怀疑驱动,这个信号已经足够明确。

提示:读取DW_IC_TX_ABRT_SOURCE必须在清除中断之前完成。驱动随后会写入DW_IC_INTR_MASK关闭相关中断位,丢失原因位图会让调试时看到的错误永远是一个笼统的-EIO

4. 时序参数:把 DesignWare I2C 的 tHIGH / tLOW 调进 spec

4.1 速率档位对应的总线时间要求

I2C 总线的时序由 SCL 高电平时间 tHIGH 和低电平时间 tLOW 决定。不同速率模式下,控制器必须满足 NXP I2C 规范中的最低时间要求。

模式SCL 频率tHIGH 最小值tLOW 最小值
Standard Mode100 kHz4000 ns4700 ns
Fast Mode400 kHz600 ns1300 ns
Fast Mode Plus1 MHz260 ns500 ns
High-Speed Mode3.4 MHz120 ns160 ns

DesignWare 控制器通过DW_IC_SS_SCL_HCNT / LCNTDW_IC_FS_SCL_HCNT / LCNT两组寄存器分别控制高低电平的时钟周期数。驱动在调时序时,做的就是把“纳秒要求”换算成“输入时钟周期数”。

4.2 用输入时钟频率反推 HCNT / LCNT

现代内核版本里,时序计算已经泛化成一个通用函数,传入目标时间、上升沿和下降沿即可。核心逻辑如下:

static u32 i2c_dw_scl_hcnt(u32 ic_clk, u32 tSYMBOL, u32 tr, u32 tf, int offset) { return DIV_ROUND_CLOSEST((u64)ic_clk * (tSYMBOL + tr + tf), 1000000000) + offset; } static u32 i2c_dw_scl_lcnt(u32 ic_clk, u32 tSYMBOL, u32 tf, int offset) { return DIV_ROUND_CLOSEST((u64)ic_clk * (tSYMBOL + tf), 1000000000) + offset; }

ic_clk是 DesignWare I2C 控制器的输入时钟频率,单位 Hz;tSYMBOL是目标高电平或低电平持续时间;trtf是 SCL 的上升沿和下降沿时间。算出来的值写入对应模式的 HCNT/LCNT 寄存器。

高电平时间要把上升沿和下降沿都算进去,因为 SCL 从低到高再到下一个低电平的完整高电平周期,包含了信号爬升和降落的过程。低电平时间只加下降沿,是因为低电平从高到低后再开始计时。offset参数留给各平台微调,通常和 SDA hold time 相关。

如果没有在设备树里显式提供边沿时间,驱动会按规范允许的最大边沿时间去推算。这会导致实际算出来的 HCNT 偏大,SCL 频率略低于目标值。对 100kHz 的慢速传感器无所谓,对 400kHz 的电容触摸控制器来说,时序余量就会变小。

4.3 设备树参数怎么给才合理

DesignWare I2C 平台驱动会读取clock-frequencyi2c-sda-hold-time-nsi2c-scl-rising-time-nsi2c-scl-falling-time-ns这几个属性。一个典型的 400kHz 配置如下:

&i2c0 { status = "okay"; clock-frequency = <400000>; i2c-sda-hold-time-ns = <250>; i2c-scl-rising-time-ns = <100>; i2c-scl-falling-time-ns = <100>; };

边沿时间不是随便填的。正确做法是用逻辑分析仪或示波器实测 SCL 的 10% 到 90% 上升时间,把实测值填进设备树。如果填得太小,计算出的 HCNT 偏小,tHIGH 会离 600ns 的 spec 下限越来越近;填得太大,则白白压缩有效带宽。i2c-sda-hold-time-ns控制的是 SDA 在 SCL 下降沿之后保持的时间,对挂在同一条总线上的旧式 EEPROM 尤其重要,设太短会偶发读错数据。

4.3.1 老内核和新内核的默认值差异

Linux 5.4 时代的内核没有这么精细的边沿计算,很多平台直接靠DW_IC_CON里的速度档位和固定分频系数跑。升级内核后同一块板子出现 SCL 频率偏差,多半是新内核改用通用时序计算导致的。遇到这种回归,不要急着改外部上拉电阻,先在设备树里补上边沿时间属性,再用示波器复测。

4.4 快速排查表:速率异常时看什么

现象优先检查调整方向
SCL 频率明显低于目标输入时钟频率配置确认 clk_rate 是否正确,检查get_clk_rate_khz返回值
部分器件随机 NACKtHIGH 不足实测上升沿并填入i2c-scl-rising-time-ns
读 EEPROM 最后一位出错SDA hold time 不够增大i2c-sda-hold-time-ns
400kHz 下波形振荡尖峰抑制太弱调整DW_IC_FS_SPKLEN,或在设备树外加上拉电容
多主机场景总线频繁占用总线冲突检查ABRT_ARB_LOST,确认另一个主机是否在持续访问

DW_IC_FS_SPKLEN在驱动里的默认值是按 50ns 尖峰抑制目标算的。如果板子上 I2C 走线较长,干扰毛刺宽于默认值,可以按输入时钟频率反推一个更大的值。

5. 板级验证与观察:让 DesignWare I2C 在系统里可测量

5.1 先用最小命令确认控制器活着

驱动加载成功后,第一组命令不需要写任何代码:

i2cdetect -l i2cdetect -y -r 0 i2cget -y 0 0x50 0x00 i2ctransfer -y 0 w2@0x50 0x00 0x08 r4

i2cdetect -r使用 SMBus read byte 探测,比默认的 quick write 更不容易打扰只读器件。i2cget验证单字节读,i2ctransfer验证多字节组合事务:先发送两个字节的寄存器地址,再连续读四个字节。如果这几条都通过,说明 TAR、DATA_CMD、RESTART 和中断路径都在工作。接下来再挂业务驱动,定位问题就方便得多。

5.2 总线恢复:不是每个 SoC 都能自动吐出 SDA

DesignWare I2C 控制器卡死在 SDA 被拉低的状态时,单纯复位控制器寄存器没有用,必须通过 GPIO 模拟 9 个 SCL 脉冲让从机释放总线。内核通用恢复机制会检测适配器节点上的i2c-scl-gpiosi2c-sda-gpios

&i2c0 { compatible = "snps,designware-i2c"; reg = <0xf0001000 0x1000>; interrupts = <0 42 4>; clock-frequency = <400000>; i2c-scl-gpios = <&gpio6 2 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; i2c-sda-gpios = <&gpio6 3 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; };

给 i2c0 节点配上 scl/sda-gpios 后,驱动 probe 时会自动注册总线恢复信息,下次传输超时后内核会在下一次 transfer 前执行复位序列。这个配置一定要在 gpio 控制器已经 ready 的层次上,否则会形成循环依赖,probe 失败。

5.3 用 tracepoint 和逻辑分析仪测时序

最后检查传输路径是否真的按预期走,可以用内核 tracepoint 观察每次 read/write 的结果:

mount -t tracefs tracefs /sys/kernel/tracing echo 1 > /sys/kernel/tracing/events/i2c/i2c_write/enable echo 1 > /sys/kernel/tracing/events/i2c/i2c_read/enable cat /sys/kernel/tracing/trace_pipe

每次 I2C 事务结束都会打印i2c_result事件,能看到返回值和消息长度。如果某个地址在i2cdetect里能看到、但业务读写总是 -EIO,对比 trace 里的 ret 值和实际访问的 addr 就能快速圈定是地址问题还是外部器件问题。逻辑分析仪采样率设 4MHz 以上,探头接靠近 SoC one side,测出来的 tHIGH 才代表控制器真实输出。

本文还有配套的精品资源,点击获取

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

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

立即咨询