简介:这份资源是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.c或i2c-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_CMD | bit0-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 6master_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_TAR和DW_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 Mode | 100 kHz | 4000 ns | 4700 ns |
| Fast Mode | 400 kHz | 600 ns | 1300 ns |
| Fast Mode Plus | 1 MHz | 260 ns | 500 ns |
| High-Speed Mode | 3.4 MHz | 120 ns | 160 ns |
DesignWare 控制器通过DW_IC_SS_SCL_HCNT / LCNT和DW_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是目标高电平或低电平持续时间;tr和tf是 SCL 的上升沿和下降沿时间。算出来的值写入对应模式的 HCNT/LCNT 寄存器。
高电平时间要把上升沿和下降沿都算进去,因为 SCL 从低到高再到下一个低电平的完整高电平周期,包含了信号爬升和降落的过程。低电平时间只加下降沿,是因为低电平从高到低后再开始计时。offset参数留给各平台微调,通常和 SDA hold time 相关。
如果没有在设备树里显式提供边沿时间,驱动会按规范允许的最大边沿时间去推算。这会导致实际算出来的 HCNT 偏大,SCL 频率略低于目标值。对 100kHz 的慢速传感器无所谓,对 400kHz 的电容触摸控制器来说,时序余量就会变小。
4.3 设备树参数怎么给才合理
DesignWare I2C 平台驱动会读取clock-frequency、i2c-sda-hold-time-ns、i2c-scl-rising-time-ns和i2c-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返回值 |
| 部分器件随机 NACK | tHIGH 不足 | 实测上升沿并填入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 r4i2cdetect -r使用 SMBus read byte 探测,比默认的 quick write 更不容易打扰只读器件。i2cget验证单字节读,i2ctransfer验证多字节组合事务:先发送两个字节的寄存器地址,再连续读四个字节。如果这几条都通过,说明 TAR、DATA_CMD、RESTART 和中断路径都在工作。接下来再挂业务驱动,定位问题就方便得多。
5.2 总线恢复:不是每个 SoC 都能自动吐出 SDA
DesignWare I2C 控制器卡死在 SDA 被拉低的状态时,单纯复位控制器寄存器没有用,必须通过 GPIO 模拟 9 个 SCL 脉冲让从机释放总线。内核通用恢复机制会检测适配器节点上的i2c-scl-gpios和i2c-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 才代表控制器真实输出。
本文还有配套的精品资源,点击获取