ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux内核DesignWare I2C驱动解析:寄存器、传输与时序调优

Linux内核DesignWare I2C驱动解析:寄存器、传输与时序调优 简介这份资源是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_CMDbit0-7 数据bit8 为 1 表示读命令bit9 为 1 表示 STOPbit10 为 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 上可能是独立时钟、父子时钟或固定 PLLcore 层没法假设一个统一频率来源。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 或 256DMA 余数处理就可能错位出现“最后一个字节读不到”的怪问题。用探测值而不是平台传值是这套驱动稳健的原因之一。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可以一次发w20x50 ... r4而不会丢地址的原因。3.3 IC_DATA_CMD写一个字节、读一个字节的信令这是整个控制器最核心的寄存器数据、方向、STOP、RESTART 全部通过它表达。位域含义bit7-0要发送的数据或读到的数据bit8写 1 表示发起一次读操作bit9写 1 表示当前字节结束后发送 STOP 条件bit10写 1 表示当前字节结束后发送 RESTART 条件写单个字节时把数据填到低 8 位最后一个字节把 bit9 置 1u32 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_CMDFIFO 会被读数据占满后续中断全部停摆。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 调进 spec4.1 速率档位对应的总线时间要求I2C 总线的时序由 SCL 高电平时间 tHIGH 和低电平时间 tLOW 决定。不同速率模式下控制器必须满足 NXP I2C 规范中的最低时间要求。模式SCL 频率tHIGH 最小值tLOW 最小值Standard Mode100 kHz4000 ns4700 nsFast Mode400 kHz600 ns1300 nsFast Mode Plus1 MHz260 ns500 nsHigh-Speed Mode3.4 MHz120 ns160 nsDesignWare 控制器通过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 控制器的输入时钟频率单位 HztSYMBOL是目标高电平或低电平持续时间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返回值部分器件随机 NACKtHIGH 不足实测上升沿并填入i2c-scl-rising-time-ns读 EEPROM 最后一位出错SDA hold time 不够增大i2c-sda-hold-time-ns400kHz 下波形振荡尖峰抑制太弱调整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 w20x50 0x00 0x08 r4i2cdetect -r使用 SMBus read byte 探测比默认的 quick write 更不容易打扰只读器件。i2cget验证单字节读i2ctransfer验证多字节组合事务先发送两个字节的寄存器地址再连续读四个字节。如果这几条都通过说明 TAR、DATA_CMD、RESTART 和中断路径都在工作。接下来再挂业务驱动定位问题就方便得多。5.2 总线恢复不是每个 SoC 都能自动吐出 SDADesignWare I2C 控制器卡死在 SDA 被拉低的状态时单纯复位控制器寄存器没有用必须通过 GPIO 模拟 9 个 SCL 脉冲让从机释放总线。内核通用恢复机制会检测适配器节点上的i2c-scl-gpios和i2c-sda-gpiosi2c0 { 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 才代表控制器真实输出。本文还有配套的精品资源点击获取
返回列表