Linux Platform 总线设备驱动模型之LCD UART GPIO I2C SPI DMA I2S Camera
第一部分:总线的概念与 Linux 的“虚实”之道
1.1 什么是总线(Bus)?
总线的本质:同类设备约定共同遵循的通讯协议与时序约束。
-
物理层:电压逻辑电平、高低电平维持时间。
-
链路层:命令/数据的格式封装。
-
本质:总线是一种抽象,它定义了 CPU 与外部设备如何进行“对话”。
1.2 Linux 系统总线的分类(虚实结合)
Linux 驱动模型将总线分为两类:
-
实际存在的物理总线:I2C、SPI、USB、PCIe 等。这些有明确的电气特性和协议,连接的是外挂的外设(如图中的 Camera、加速度传感器)。
-
虚拟总线(Platform Bus):这是 Linux 内核为了统一管理直接映射到 CPU 内存地址空间的设备而抽象出来的总线。它没有物理的导线,只是一个软件逻辑链表。
1.3 核心精华
Platform Bus 存在的理由:在 SoC 体系下,许多设备(如 UART、LCD 控制器、GPIO 控制器)直接集成在芯片内部,它们不像 USB 那样可以热插拔,而是需要内核启动时由 Bootloader 或设备树发现。Platform Bus 为这些“不会跑”的静态设备提供了一套标准的 “匹配-探测-注册” 机制。
第二部分:从 CPU 到外设
定位 CPU Core 到最终外设的链路。需要以一个典型的智能手机开发板为例进行拆解:
2.1 顶层:ARM Core + Platform Bus
ARM Core 通过内部总线架构连接到 Platform Bus。这个 Platform Bus 本质上是一个软件容器,里面挂载了所有挂载在 SoC 内部内存映射地址空间的设备控制器。
2.2 中间层:设备控制器(Controller Hub)
智能手机开发板的 LCD 控制器、UART 控制器、Camera 控制器、GPIO 控制器、I2C 控制器 全部直接挂在 Platform Bus 上。
-
本质:这些都是属于 SoC 内部的 IP 核。它们的寄存器地址、中断号是固定写死的(或由设备树指定)。
-
关键点:这些控制器本身也是 Platform 设备。你需要为它们编写对应的
platform_driver。
2.3 底层:物理外设与次级总线桥接
-
I2C Controller 既是 Platform 设备(挂载 Platform 总线),又是 I2C 主机控制器(生成 I2C 物理总线)。
-
挂载在 I2C Bus 上的设备(Camera、加速度传感器、Touch Screen)是 I2C 客户设备,它们的驱动由 I2C 子系统 管理,而不是直接由 Platform Bus 管理。
-
GPIO Controller 上挂载的 Keys(按键),它们可能通过 GPIO 机制检测电平变化,本质上也是挂靠在 Platform 总线的 GPIO 子系统中。
第三部分:Platform 总线的精髓与常见误区
3.1 分辨“Platform Device”与“Platform Driver”的本质
-
误区:觉得
platform_device就是硬件设备。 -
真相:
platform_device是内核中的数据结构,代表了硬件在内存中的入口(比如内存映射基址、IRQ号)。platform_driver是操作这些硬件的代码逻辑。 -
匹配机制:Linux 会遍历所有
platform_driver,去匹配platform_device的name字段。只有匹配成功,驱动才会被加载。
3.2 为什么要有 Platform Bus?直接操作内存不香吗?
在上世纪的内核中,很多驱动直接像 *(volatile uint32_t*)0x12345678 = 0xFF 这样写寄存器。这种做法导致:
-
解耦性极差:换了芯片厂商,基址变了,代码就得改。
-
无生命期管理:不支持设备热插拔(尽管 Platform 大多不热插拔,但需要统一移除逻辑)。 Platform Bus 将硬件描述(在设备树或
arch/arm/mach-xxx中)与驱动行为分离开来,实现了代码复用。同一个 UART 驱动,可以轻松适配不同厂商的 SoC。
3.3 设备树(Device Tree)与 Platform 的紧密结合
现代内核中,Platform 设备几乎全部通过 设备树(DTS/DTB) 描述。
-
在 DTS 中:
compatible = "vendor,device"字段会与platform_driver的of_match_table进行匹配。 -
调试技巧:查看
/sys/firmware/devicetree/base/来确认内核是否正确解析了硬件描述。
第四部分:场景调试——当Platform 驱动不起作用时
当编写了一个 platform_driver,编译进内核,却发现 probe 函数没有被调用,或者设备没反应,如何定位?
场景 1:匹配失败(最常见)
现象:dmesg 没有任何打印,modprobe 也成功,但 /sys/devices/platform/ 下没有你的设备节点。 调试方法:
-
运行:
cat /sys/bus/platform/devices/*/modalias | grep <设备名> -
检查
platform_driver中的driver.name和of_match_table是否与 DTS 完全一致。 -
查看 DTS 是否被内核加载:
dtc -I fs /sys/firmware/devicetree/base/。
场景 2:中断申请失败
现象:probe 执行到 request_irq 时失败,设备报错。 调试方法:
-
确认中断号:在 DTS 中检查
interrupts属性是否正确。 -
检查中断引脚是否被其他设备(如 GPIO 子系统)占用:
cat /proc/interrupts。 -
使用
devm_request_irq(申请资源自动管理)避免资源泄漏。
场景 3:内存映射导致 Kernel Panic
现象:ioremap 之后访问寄存器导致 Oops。 调试方法:
-
检查
platform_get_resource返回的start和end地址是否有效。 -
确认是不是在
__init函数之外访问了remap后的地址(非 ioremap 导致的无效访问)。 -
使用
perf或kprobe动态跟踪ioremap调用的入口参数。
第五部分:Platform Driver 范本
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
/**
* @brief 私有数据结构:用于保存 Platform 设备的具体状态与资源。
*
* 在 probe 函数中分配并保存,在 remove 函数中释放。
*/
struct demo_platform_dev_priv {
void __iomem *base_addr; /**< 映射后的寄存器基址 */
int irq_num; /**< 中断号 */
struct device *dev; /**< 关联的设备指针 */
};
/**
* @brief 硬件初始化函数(典型的寄存器配置)。
*
* @param priv 指向驱动私有数据的指针。
* @return 0 成功,负数 失败。
*/
static int demo_plat_hw_init(struct demo_platform_dev_priv *priv)
{
// 示例:写指定寄存器
writel(0x12345678, priv->base_addr + 0x04);
return 0;
}
/**
* @brief 中断服务程序(ISR)。
*
* @param irq 触发的中断号。
* @param dev_id 传递给 request_irq 的私有数据指针。
* @return 返回 IRQ_HANDLED 或 IRQ_NONE。
*/
static irqreturn_t demo_plat_isr(int irq, void *dev_id)
{
struct demo_platform_dev_priv *priv = dev_id;
// 读取中断状态寄存器
u32 status = readl(priv->base_addr + 0x00);
if (status & 0x01) {
// 处理对应中断
writel(status, priv->base_addr + 0x00); // 清除中断位
// ... 完成业务逻辑 ...
return IRQ_HANDLED;
}
return IRQ_NONE;
}
/**
* @brief Platform 驱动的探测函数(核心入口)。
*
* 当内核匹配到对应的 device 时调用。负责:
* 1. 获取资源(内存、中断)。
* 2. 映射内存地址(ioremap)。
* 3. 申请中断。
* 4. 注册框架(如 miscdevice, input device 等)。
*
* @param pdev 指向匹配到的 platform_device 结构指针。
* @return 0 成功,负数 失败。
*/
static int demo_plat_probe(struct platform_device *pdev)
{
struct demo_platform_dev_priv *priv;
struct resource *res;
int ret;
// 1. 分配私有数据结构
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->dev = &pdev->dev;
platform_set_drvdata(pdev, priv); // 将 priv 关联到 pdev,方便 remove 取回
// 2. 获取内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource found\n");
return -ENXIO;
}
// 3. 映射寄存器物理地址到虚拟地址
priv->base_addr = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base_addr)) {
dev_err(&pdev->dev, "ioremap failed\n");
return PTR_ERR(priv->base_addr);
}
// 4. 获取中断号
priv->irq_num = platform_get_irq(pdev, 0);
if (priv->irq_num < 0) {
dev_err(&pdev->dev, "No IRQ found\n");
return priv->irq_num;
}
// 5. 申请中断
ret = devm_request_irq(&pdev->dev, priv->irq_num, my_plat_isr,
IRQF_TRIGGER_RISING, dev_name(&pdev->dev), priv);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to request IRQ %d\n", priv->irq_num);
return ret;
}
// 6. 硬件初始化
demo_plat_hw_init(priv);
dev_info(&pdev->dev, "demo Platform Device probed successfully\n");
return 0;
}
/**
* @brief Platform 驱动的移除函数。
*
* 当设备被拔除或驱动被卸载时调用。负责清理硬件状态并释放资源。
*
* @param pdev 指向 platform_device 结构指针。
*/
static int demo_plat_remove(struct platform_device *pdev)
{
// devm_* 系列函数会自动释放资源(ioremap, kzalloc, request_irq 等)
// 因此这里只需要做标准的清理动作,如禁用硬件时钟、重置寄存器等。
struct demo_platform_dev_priv *priv = platform_get_drvdata(pdev);
if (priv) {
// 可选:做硬件安全的关闭操作
writel(0x00, priv->base_addr + 0x00); // 关闭所有中断
}
dev_info(&pdev->dev, "My Platform Device removed\n");
return 0;
}
/**
* @brief 设备树匹配表(核心关键)。
*
* 该表列出了此驱动支持的所有 compatible 字符串。
* 当 DTS 中的设备节点 compatible 属性与表中字符串匹配时,
* 内核就会调用 my_plat_probe 函数。
*/
static const struct of_device_id demo_plat_of_match[] = {
{ .compatible = "vendor,demo-device-v1", },
{ .compatible = "vendor,demo-device-v2", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, demo_plat_of_match);
/**
* @brief Platform 驱动结构体。
*
* 注册到内核的总线匹配队列中。
*/
static struct platform_driver demo_plat_driver = {
.probe = demo_plat_probe,
.remove = demo_plat_remove,
.driver = {
.name = "demo_platform_driver", /**< 驱动名称,用于旧式 legacy ID 匹配 */
.of_match_table = demo_plat_of_match, /**< 设备树匹配表(推荐方式) */
},
};
/**
* @brief 模块初始化入口。
*
* 调用 platform_driver_register 将此驱动注册到内核。
*
* @note 此函数仅在模块加载时执行一次。
*/
static int __init demo_plat_init(void)
{
return platform_driver_register(&demo_plat_driver);
}
/**
* @brief 模块退出入口。
*
* 调用 platform_driver_unregister 注销驱动。
*/
static void __exit demo_plat_exit(void)
{
platform_driver_unregister(&demo_plat_driver);
}
module_init(demo_plat_init);
module_exit(demo_plat_exit);
MODULE_AUTHOR("demo");
MODULE_DESCRIPTION("Example Linux Platform Device Driver");
MODULE_LICENSE("GPL v2");
如何与 Platform Bus 关联?
这段代码可以用于 “LCD控制器”、“UART控制器” 等 Controller 的实际驱动实现基本框架。它们通过 demo_plat_driver 注册到 Platform Bus 上,当 DTS 中定义了 compatible = "vendor,demo-device-v1" 的设备节点时,demo_plat_probe 就会执行。 Platform Bus 就是驱动与设备匹配的后台核心机制。
第六部分 LCD 控制器 和 UART 控制器
窗口 1:深入 UART 控制器驱动(Linux 5.10 中的 8250_dw.c)
在 Linux 5.10 中,属于 ARM SoC 的 UART 控制器驱动大多数位于 drivers/tty/serial/,其中 DesignWare 8250 是最典型的例子。它完美呼应了您给的图中 Platform Bus 挂载 UART Controller 的模型。
1.1 实际硬件行为与驱动映射
-
硬件:DW_apb_uart (DesignWare UART)。它有 TX/RX FIFO,支持 DMA 或 PIO 模式。
-
平台总线:对应图中的 Platform Bus 上的 UART Controller 模块。
-
核心动作:在 probe 阶段初始化硬件、申请 I/O 内存、中断、时钟(clk),然后调用
serial8250_register_8250_port将其注册到 Linux TTY 子系统。
1.2 代码片段基于 Linux 5.10
// 部分代码源自 drivers/tty/serial/8250/8250_dw.c (Linux 5.10)
#include <linux/io.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/serial_8250.h>
#include <linux/dma-mapping.h>
/**
* @brief 8250_dw 驱动的私有结构体,管理 UART 控制器的硬件资源。
*
* 对应图中 Platform Bus 上挂载的 UART Controller 实例。
*/
struct dw8250_data {
void __iomem *regs; /**< 映射后的寄存器基址(IO内存) */
int irq; /**< 中断号 */
struct clk *clk; /**< 总线时钟,防止 UART 因为时钟关闭而冻结 */
struct uart_8250_port *port; /**< 内核串口核心结构 */
int dma_enabled; /**< 是否启用了 DMA 模式 */
};
/**
* @brief 将 UART 硬件初始化为 8250 标准端口并注册到 Linux TTY 层。
*
* 这个函数是硬核调试入口。如果终端没有输出,问题往往出在这里。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数 错误码。
*/
static int dw8250_probe(struct platform_device *pdev)
{
struct dw8250_data *data;
struct resource *res;
struct uart_8250_port port = {0}; // 8250 核心结构
int ret;
// 1. 分配私有数据,用于保存硬件状态
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data) return -ENOMEM;
platform_set_drvdata(pdev, data);
// 2. 获取 Device Tree 或平台资源描述的内存地址空间
// 对应图中 Platform Bus 给 UART Controller 分配的首地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) return -ENXIO;
// 3. 将物理地址映射到虚拟地址
data->regs = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(data->regs)) return PTR_ERR(data->regs);
// 4. 获取并使能设备时钟 (clock)
data->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(data->clk)) { /* 如果时钟不存在,某些平台可能可以容忍,但通常必须启用 */ }
if (!IS_ERR(data->clk)) {
clk_prepare_enable(data->clk);
}
// 5. 申请中断号(图中对应 UART Controller 与 CPU 的中断连接)
data->irq = platform_get_irq(pdev, 0);
if (data->irq < 0) return data->irq;
// 6. 填充 8250 核心结构体
port.port.iotype = UPIO_MEM32; // 或 UPIO_MEM,取决于寄存器宽度
port.port.mapbase = res->start;
port.port.membase = data->regs;
port.port.irq = data->irq;
port.port.dev = &pdev->dev;
port.port.flags = UPF_SHARE_IRQ | UPF_FIXED_PORT;
port.port.uartclk = clk_get_rate(data->clk); // 时钟速率决定了波特率的计算
// 7. 注册到内核的 8250 serial 核心层,这步完成后 `/dev/ttySx` 才会出现
// 这里也是最容易出问题的点之一:UART 初始化不成功却返回 0 将导致死机
ret = serial8250_register_8250_port(&port);
if (ret < 0) { /* 错误处理 */ }
// 保存 port 指针以便后续移除
data->port = (struct uart_8250_port *)port;
dev_info(&pdev->dev, "DW UART registered at MMIO 0x%llx, IRQ %d\n",
(unsigned long long)res->start, data->irq);
return 0;
}
/**
* @brief 驱动移除逻辑,清理硬件资源。
*
* @param pdev Platform 设备指针。
*/
static int dw8250_remove(struct platform_device *pdev)
{
struct dw8250_data *data = platform_get_drvdata(pdev);
if (data) {
if (data->port)
serial8250_unregister_port(data->port->port.line); // 注销端口
if (!IS_ERR(data->clk))
clk_disable_unprepare(data->clk);
}
return 0;
}
// 匹配表(DTS 中的 compatible 字段)
static const struct of_device_id dw8250_of_match[] = {
{ .compatible = "snps,dw-apb-uart" }, // 最常见匹配项
{ .compatible = "rockchip,rk3399-uart" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, dw8250_of_match);
// 注册到 Platform 总线
static struct platform_driver dw8250_driver = {
.probe = dw8250_probe,
.remove = dw8250_remove,
.driver = {
.name = "dw-apb-uart",
.of_match_table = dw8250_of_match,
},
};
module_platform_driver(dw8250_driver);
1.3 资深视角:UART 驱动的潜在“坑”
-
Clock 问题:UART 靠时钟计算波特率。如果你的 DTS 里
clocks没配好,或者时钟频率不对,/dev/ttyS0虽然存在但发出来的是乱码。 -
DMA vs. PIO:如果数据量大,是否启用 DMA?现代 SoC 会默认开启。但在
probe中开启 DMA 之前,必须确认dma_set_mask_and_coherent返回成功。 -
硬件流控(CTS/RTS):如果在
probe里没有根据硬件状态正确配置寄存器,远程连接会莫名其妙断开。此时需用regmap或直接writel读写寄存器。
窗口 2:深入 LCD 控制器驱动(Linux 5.10 中的 DRM/KMS)
在 Linux 5.10 中,LCD 控制器通常属于 DRM (Direct Rendering Manager) 子系统。以图为例,LCD Controller 挂载在 Platform Bus 上,它负责将 Framebuffer 扫描到显示屏上。
2.1 核心逻辑分析
-
DRM 架构:Probe 时,向 DRM 核心注册一个
drm_device。 -
硬件动作:配置
Pixel Clock、Resolution、Bpp(色彩深度)。使用drm_atomic_helper机制进行无闪烁的帧切换。
2.2 代码片段与 Doxygen 注解(基于 Linux 5.10 的 SimpleDRM/PL111)
// 部分代码基于 drivers/gpu/drm/pl111/pl111_drv.c (Linux 5.10)
#include <drm/drm_device.h>
#include <drm/drm_crtc.h>
#include <drm/drm_encoder.h>
#include <drm/drm_framebuffer.h>
#include <drm/drm_modeset_helper.h>
/**
* @brief LCD 控制器的私有结构体(对应图中 LCD Controller 块)。
*
* 包含硬件配置(内存基址、时钟、DMA设置)以及 DRM 核心对象。
*/
struct lcdc_priv {
void __iomem *base; /**< 映射后的寄存器基址 */
struct clk *clk; /**< LCD 专用时钟 */
struct drm_device *drm_dev; /**< 指向 DRM 核心设备结构 */
struct drm_crtc *crtc; /**< CRTC (CRT Controller) 负责时序 */
};
/**
* @brief 初始化 LCD 控制器并注册到 DRM 子系统。
*
* 这是 LCD 驱动最重要的入口。如果系统启动后屏幕不亮,问题极大可能出现在这里。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数 错误码。
*/
static int lcdc_probe(struct platform_device *pdev)
{
struct drm_device *drm;
struct lcdc_priv *priv;
struct resource *res;
int ret;
// 1. 分配私有数据
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv) return -ENOMEM;
platform_set_drvdata(pdev, priv);
// 2. 获取并映射寄存器
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) return -ENXIO;
priv->base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
// 3. 获取时钟 (LCD 专用的 Pixel Clock)
priv->clk = devm_clk_get(&pdev->dev, "lcdc_clk");
if (IS_ERR(priv->clk)) return PTR_ERR(priv->clk);
clk_prepare_enable(priv->clk);
// 4. 创建 DRM 核心设备并初始化模式设置(KMS)
// 这一步是接口的关键,将硬件呈现给 Linux 用户层(如 Wayland, X11)
drm = drm_dev_alloc(&lcdc_driver, &pdev->dev);
if (IS_ERR(drm)) {
ret = PTR_ERR(drm);
goto err_clk;
}
priv->drm_dev = drm;
// 5. 初始化 CRTC, Encoder, Connector (KMS 三件套)
// DRM 框架需要知道当前的显示时序 (Mode)、连接器类型 (HDMI, LVDS, DSI)
ret = drm_mode_config_init(drm);
if (ret) goto err_drm_free;
// 6. 设置 DRM 的显示引擎:将物理 Framebuffer 扫描到显示屏的机制
ret = lcdc_setup_scanout_engine(drm);
if (ret) goto err_mode_config_cleanup;
// 7. 注册 DRM 设备到用户空间(这步完成后 /dev/dri/card0 才会出现)
ret = drm_dev_register(drm, 0);
if (ret) goto err_mode_config_cleanup;
dev_info(&pdev->dev, "LCDC (LCD Controller) initialized successfully\n");
return 0;
err_mode_config_cleanup:
drm_mode_config_cleanup(drm);
err_drm_free:
drm_dev_put(drm);
err_clk:
clk_disable_unprepare(priv->clk);
return ret;
}
/**
* @brief 移除 LCD 控制器,清理 DRM 资源。
*
* @param pdev Platform 设备指针。
*/
static int lcdc_remove(struct platform_device *pdev)
{
struct lcdc_priv *priv = platform_get_drvdata(pdev);
if (priv) {
if (priv->drm_dev) {
drm_dev_unregister(priv->drm_dev);
drm_mode_config_cleanup(priv->drm_dev);
drm_dev_put(priv->drm_dev);
}
if (!IS_ERR(priv->clk))
clk_disable_unprepare(priv->clk);
}
return 0;
}
窗口 3:结合性能图谱的资深调试与场景解决
场景 1:UART probe 成功,但无法发送数据(发送字符失败)
症状:echo "hello" > /dev/ttyS0 卡住。 调试流:
-
第一层(查看日志):
dmesg | grep dw8250,看probe是否返回 0。如果 probe 成功了,说明 Platform 匹配没问题。 -
第二层(观测硬件状态 - 结合图谱的 Block Device/Drive层):使用
perf或ftrace跟踪dw8250_probe的执行时间。trace-cmd record -e serial:uart_write -e serial:serial_irq -p function_graph -n 100 trace-cmd report
-
第三层(排查中断):
-
运行
cat /proc/interrupts,检查dw8250注册的中断号是否有计数增加。 -
如果 TX IRQ 不触发,说明可能是
IRQ资源没映射正确(查 DTS),或者clk没使能导致硬件没电。
-
-
极限手段(调试寄存器):
-
使用
devmem2(用户态工具)直接读取物理地址的寄存器值,看LSR(Line Status Register)的 THRE(Transmit Holding Register Empty)位是否为 1。如果为 0,硬件永远发不出数据。
-
场景 2:LCD 屏幕不亮(既无背光也无任何像素,但 DRM 注册成功)
症状:系统启动,/dev/dri/card0 存在,但屏幕全黑。 调试流:
-
第一层(逻辑流追踪):使用
ftrace跟踪整个drm_atomic_commit流程。echo 0 > /sys/kernel/debug/tracing/tracing_on echo 1 > /sys/kernel/debug/tracing/events/drm/enable echo function > /sys/kernel/debug/tracing/current_tracer echo 1 > /sys/kernel/debug/tracing/tracing_on # 此时在用户态触发一次绘图 (例如运行 weston 或 modetest) cat /sys/kernel/debug/tracing/trace
如果在 Atomic Commit 过程中有某个
drm_bridge或drm_encoder处于disabled状态,FTrace 一目了然。 -
第二层(电源树检查):检查
clk和regulator。LCD 需要大量的 Power。使用cat /sys/kernel/debug/clk/clk_summary检查lcdc_clk的状态是否为enabled。 -
第三层(PinMux 检查):查看
dmesg中有无pinctrl相关的 Warning。有可能是 GPIO 引脚被复用到了 UART 或 JTAG 上,导致 LCD 数据线 (RGB/LVDS) 没信号。 -
第四层(使用 Tracepoint/Bpftrace 动态插桩):
-
bpftrace -e 'kprobe:lcdc_setup_scanout_engine /arg1 == 0x10000000/ { printf("Scanout engine setup called with arg 0x%llx\n", arg1); }' -
这可以在不改动代码的情况下,动态追踪内存地址 0x10000000 被传入该函数时的情况。
-
性能问题是堆栈中某些层的阻塞:
-
对于 UART 驱动:问题通常出在
IRQ storm(中断风暴)或DMA瓶颈。-
工具:
perf top -C 0(看 CPU0 中断占比),perf record -e irq:*(找出哪个中断处理程序占用 CPU)。
-
-
对于 LCD 驱动:问题通常出在
drm_atomic_helper_commit等耗时很长的路径上。-
工具:
perf record -e sched:sched_wakeup -e drm:drm_atomic_commit -a -- sleep 10-> 结合perf script,看到底是哪个 进程 在延迟 atomic commit。通常这是因为 VBlank IRQ 丢失导致的帧冻住。
-
第七部分 回到设备驱动基础:GPIO 控制器
第一章:GPIO 控制器在 Platform Bus 中的位置
GPIO 控制器( GPIO Controller)直接挂载在 Platform Bus 上。这是 SoC 内部提供通用输入输出引脚的硬件模块。它的核心职责是:
-
管理引脚方向:输入或输出。
-
读写电平:高或低。
-
中断能力:检测引脚电平变化(上升沿、下降沿、高电平、低电平),并产生中断通知 CPU。
在 Linux 中,GPIO 子系统提供了标准化的 gpio_chip 接口,所有具体的 GPIO 控制器驱动都需要实现该接口,然后注册到内核。
第二章:Linux 5.10 典型 GPIO 控制器 —— gpio-pl061.c
ARM 架构中最常见的 GPIO 控制器之一是 PL061(PrimeCell GPIO),广泛用于各种 Cortex-A 系列 SoC(如 Versatile, VExpress, 有些高通的早期芯片等)。
以下代码基于 Linux 5.10 drivers/gpio/gpio-pl061.c。
2.1 硬件寄存器映射
PL061 有四个关键寄存器:
-
GPIO_DATA:读写引脚电平(偏移 0x000 ~ 0x3FC,由位掩码决定)。 -
GPIO_DIR:方向寄存器(0x400),1=输出,0=输入。 -
GPIO_IS:中断触发类型(0x404),1=电平触发,0=边沿触发。 -
GPIO_IBE:中断边沿触发类型(0x408),1=双沿触发,0=单沿触发(由 IEV 决定)。
2.2 核心代码
// 基于 Linux 5.10 drivers/gpio/gpio-pl061.c
#include <linux/io.h>
#include <linux/irq.h>
#include <linux/gpio/driver.h>
#include <linux/platform_device.h>
/**
* @brief PL061 GPIO 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 GPIO Controller 硬件实例。
*/
struct pl061 {
void __iomem *base; /**< 映射后的寄存器基址 */
struct gpio_chip gc; /**< GPIO 核心抽象,供通用 GPIO 层调用 */
struct irq_chip irq_chip; /**< 中断控制器抽象 */
int irq; /**< GPIO 控制器本身的中断号(由 SoC 中断控制器提供) */
spinlock_t lock; /**< 自旋锁,保护寄存器并发访问 */
};
/* 寄存器偏移量定义 */
#define GPIO_DATA 0x000
#define GPIO_DIR 0x400
#define GPIO_IS 0x404
#define GPIO_IBE 0x408
#define GPIO_IEV 0x40C
#define GPIO_IE 0x410
#define GPIO_RIS 0x414
#define GPIO_MIS 0x418
#define GPIO_IC 0x41C
/**
* @brief 读取某个引脚的输入电平。
*
* 此函数被 GPIO 通用层调用(通过 gc->get 函数指针)。
*
* @param gc 指向 gpio_chip 结构。
* @param offset 引脚编号(0 ~ ngpio-1)。
* @return 1 为高电平,0 为低电平。
*/
static int pl061_get(struct gpio_chip *gc, unsigned offset)
{
struct pl061 *pl061 = gpiochip_get_data(gc);
u32 val;
/* 读取 DATA 寄存器,但只有对应 bit 有效,其他位被硬件忽略 */
val = readl(pl061->base + GPIO_DATA + (1 << (offset + 2)));
return !!(val & (1 << offset));
}
/**
* @brief 设置某个引脚的输出电平。
*
* @param gc 指向 gpio_chip 结构。
* @param offset 引脚编号。
* @param val 1 为高电平,0 为低电平。
*/
static void pl061_set(struct gpio_chip *gc, unsigned offset, int val)
{
struct pl061 *pl061 = gpiochip_get_data(gc);
unsigned long flags;
u32 data;
spin_lock_irqsave(&pl061->lock, flags);
data = readl(pl061->base + GPIO_DATA + (1 << (offset + 2)));
if (val)
data |= (1 << offset);
else
data &= ~(1 << offset);
writel(data, pl061->base + GPIO_DATA + (1 << (offset + 2)));
spin_unlock_irqrestore(&pl061->lock, flags);
}
/**
* @brief 设置引脚方向。
*
* @param gc 指向 gpio_chip。
* @param offset 引脚编号。
* @param direction 方向(0:输入,1:输出)。
* @return 0 成功。
*/
static int pl061_dir(struct gpio_chip *gc, unsigned offset, int direction)
{
struct pl061 *pl061 = gpiochip_get_data(gc);
unsigned long flags;
u32 dir;
spin_lock_irqsave(&pl061->lock, flags);
dir = readl(pl061->base + GPIO_DIR);
if (direction)
dir |= (1 << offset);
else
dir &= ~(1 << offset);
writel(dir, pl061->base + GPIO_DIR);
spin_unlock_irqrestore(&pl061->lock, flags);
return 0;
}
/* 中断相关函数:配置触发类型、使能、屏蔽、ACK 等 */
static void pl061_irq_ack(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
struct pl061 *pl061 = gpiochip_get_data(gc);
u32 offset = d->hwirq;
writel(1 << offset, pl061->base + GPIO_IC); // 清除中断状态
}
static void pl061_irq_enable(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
struct pl061 *pl061 = gpiochip_get_data(gc);
u32 offset = d->hwirq;
u32 ie;
ie = readl(pl061->base + GPIO_IE);
ie |= (1 << offset);
writel(ie, pl061->base + GPIO_IE);
}
static void pl061_irq_disable(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
struct pl061 *pl061 = gpiochip_get_data(gc);
u32 offset = d->hwirq;
u32 ie;
ie = readl(pl061->base + GPIO_IE);
ie &= ~(1 << offset);
writel(ie, pl061->base + GPIO_IE);
}
static int pl061_irq_set_type(struct irq_data *d, unsigned int type)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
struct pl061 *pl061 = gpiochip_get_data(gc);
u32 offset = d->hwirq;
u32 is, ibe, iev;
unsigned long flags;
spin_lock_irqsave(&pl061->lock, flags);
is = readl(pl061->base + GPIO_IS);
ibe = readl(pl061->base + GPIO_IBE);
iev = readl(pl061->base + GPIO_IEV);
is &= ~(1 << offset);
ibe &= ~(1 << offset);
iev &= ~(1 << offset);
if (type & IRQ_TYPE_LEVEL_MASK) {
/* 电平触发 */
is |= (1 << offset);
if (type & IRQ_TYPE_LEVEL_HIGH)
iev |= (1 << offset);
/* 低电平触发则 iev 位保持 0 */
} else if (type & IRQ_TYPE_EDGE_MASK) {
/* 边沿触发 */
if ((type & IRQ_TYPE_EDGE_BOTH) == IRQ_TYPE_EDGE_BOTH) {
ibe |= (1 << offset); /* 双沿触发 */
} else {
ibe &= ~(1 << offset);
if (type & IRQ_TYPE_EDGE_RISING)
iev |= (1 << offset);
}
}
writel(is, pl061->base + GPIO_IS);
writel(ibe, pl061->base + GPIO_IBE);
writel(iev, pl061->base + GPIO_IEV);
spin_unlock_irqrestore(&pl061->lock, flags);
return 0;
}
/* 中断处理函数:被 GPIO 控制器本身的中断线触发 */
static irqreturn_t pl061_irq_handler(int irq, void *dev_id)
{
struct pl061 *pl061 = dev_id;
u32 mis, offset;
mis = readl(pl061->base + GPIO_MIS); /* 读取挂起的中断状态 */
if (!mis)
return IRQ_NONE;
/* 遍历所有挂起的引脚,逐个处理 */
while (mis) {
offset = __ffs(mis); /* 找到最低位对应的引脚 */
generic_handle_domain_irq(pl061->gc.irq.domain, offset);
mis &= ~(1 << offset);
}
return IRQ_HANDLED;
}
/**
* @brief PL061 驱动的探测函数。
*
* 这是 Platform Bus 匹配成功后的入口,负责初始化 GPIO 控制器硬件并注册到内核。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误码。
*/
static int pl061_probe(struct platform_device *pdev)
{
struct pl061 *pl061;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
pl061 = devm_kzalloc(&pdev->dev, sizeof(*pl061), GFP_KERNEL);
if (!pl061)
return -ENOMEM;
platform_set_drvdata(pdev, pl061);
spin_lock_init(&pl061->lock);
// 2. 获取并映射 I/O 内存
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENXIO;
pl061->base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
if (!pl061->base)
return -ENOMEM;
// 3. 获取中断号(本 GPIO 控制器作为中断控制器时,连接 GIC 的中断线)
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
pl061->irq = irq;
// 4. 填充 gpio_chip 结构
pl061->gc = (struct gpio_chip) {
.label = dev_name(&pdev->dev),
.parent = &pdev->dev,
.owner = THIS_MODULE,
.base = -1, /* 自动分配基号 */
.ngpio = 8, /* PL061 有 8 个引脚 */
.get = pl061_get,
.set = pl061_set,
.direction_input = pl061_dir,
.direction_output = pl061_dir,
.set_config = gpio_generic_config, /* 支持驱动强度等 */
};
// 5. 注册 GPIO 控制器
ret = devm_gpiochip_add_data(&pdev->dev, &pl061->gc, pl061);
if (ret) {
dev_err(&pdev->dev, "Failed to add gpiochip\n");
return ret;
}
// 6. 初始化中断控制器(将 GPIO 引脚作为 IRQ 源)
pl061->irq_chip = (struct irq_chip) {
.name = "pl061",
.irq_ack = pl061_irq_ack,
.irq_enable = pl061_irq_enable,
.irq_disable = pl061_irq_disable,
.irq_set_type = pl061_irq_set_type,
};
// 创建 gpio_irq_chip 并设置给 gpio_chip
ret = gpiochip_irqchip_add(&pl061->gc, &pl061->irq_chip,
0, handle_bad_irq, IRQ_TYPE_NONE);
if (ret) {
dev_err(&pdev->dev, "Failed to add irqchip\n");
return ret;
}
// 7. 设置中断处理函数(硬件级别的中断)
ret = devm_request_irq(&pdev->dev, pl061->irq, pl061_irq_handler,
IRQF_SHARED, dev_name(&pdev->dev), pl061);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 8. 启用 GPIO 中断(主使能)
writel(0xFFFF, pl061->base + GPIO_IE); /* 所有引脚都允许触发中断 */
dev_info(&pdev->dev, "PL061 GPIO controller initialized\n");
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int pl061_remove(struct platform_device *pdev)
{
/* devm_* 自动释放资源,这里只需做硬件安全关闭 */
struct pl061 *pl061 = platform_get_drvdata(pdev);
if (pl061) {
writel(0x0, pl061->base + GPIO_IE); // 屏蔽所有中断
writel(0xFFFFFFFF, pl061->base + GPIO_IC); // 清除所有挂起中断
}
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id pl061_of_match[] = {
{ .compatible = "arm,pl061", },
{ .compatible = "arm,primecell", }, // 有些 SoC 用 primecell 作为父类
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, pl061_of_match);
/* Platform 驱动结构 */
static struct platform_driver pl061_driver = {
.probe = pl061_probe,
.remove = pl061_remove,
.driver = {
.name = "pl061",
.of_match_table = pl061_of_match,
},
};
module_platform_driver(pl061_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("ARM PL061 GPIO Controller Driver");
MODULE_LICENSE("GPL v2");
第三章:GPIO 驱动调试的核心难点
3.1 中断风暴(Interrupt Storm)
问题描述:一个 GPIO 引脚被配置为边沿触发中断,但硬件上始终有杂波或抖动,导致中断处理函数被不断调用,CPU 陷入软中断死循环。
调试方法:
-
观察
perf:perf top -e irq:irq_handler_entry
如果看到
pl061_irq_handler在列表中占据前几位,说明中断触发频繁。 -
检查中断计数:
cat /proc/interrupts | grep pl061
观察该中断号的后增量是否每秒数千次。
-
临时解决方案:
-
在 DTS 中添加
debounce属性(如果内核支持)。 -
手动在驱动中使能
GPIO_IBE(双沿触发)配合软件去抖。
-
3.2 GPIO 引脚被多重复用(PinMux 冲突)
问题现象:设置 GPIO 输出时,电平不变化,或者读到的输入值始终不变(可能被外设拉死)。
调试方法:
-
检查 pinctrl 状态:
cat /sys/kernel/debug/pinctrl/pinctrl-handles
查看该引脚是否被某个外设(如 UART、I2C)永久占用为功能引脚。
-
手动切换功能:
echo "gpio" > /sys/kernel/debug/pinctrl/xxx/pinmux
3.3 中断号未映射或无法触发(IRQ Domain 问题)
现象:用户层 poll() 或 select() 在 GPIO 中断上永远等待,但实际电平翻转。
调试方法:
-
验证 IRQ 号是否正确:在
probe中使用irq_find_mapping后打印结果。 -
使用 tracepoint:
trace-cmd record -e gpio:* -e irq:irq_handler_exit trace-cmd report | grep pl061
查看是否有
irq_handler_exit不匹配。
第四章:结合性能图谱的调试场景示例
假设系统发生卡顿,怀疑是某个 GPIO 引发了高频中断。可以按照第一张性能思路层层定位:
-
检查系统宏观状态(CPU 与 Memory 层):
top -H -p <pid> # 查看哪个 CPU 使用率最高,如果是软中断高,说明中断多。
-
详细追踪中断( Interrupt Controller 层):
perf record -e irq:irq_handler_entry -e irq:irq_handler_exit -a -- sleep 5 perf script
找出哪个中断号被频繁调用。
-
深入该 GPIO 控制器驱动的内核函数(Device Drivers 层):
bpftrace -e 'kprobe:pl061_irq_handler /arg1 == <your_irq_number>/ { @count[tid] = count(); }' -
确认硬件层面抖动:
-
用示波器或逻辑分析仪观察 GPIO 引脚,确认是否确实存在物理脉冲。
-
第五章:与其他设备控制器的协同
GPIO 通常不独立存在,它常为其他控制器提供中断、复位、使能信号。例如:
-
LCD 控制器使用 GPIO 作为复位引脚。
-
Camera 控制器使用 GPIO 作为电源使能。
-
I2C/SPI 控制器使用 GPIO 模拟总线(如果硬件 i2c 挂掉,可作为降级方案)。
在调试这类复合依赖时,务必先用 gpioinfo 或 /sys/kernel/debug/gpio 检查被引用的 GPIO 是否正确导出并可访问。
第八部分 I2C 控制器
第一章:I2C 控制器在 Platform Bus 中的位置
I2C Controller 是一个独立挂载在 Platform Bus 上的硬件模块。它作为 I2C 总线的主机(Master),负责与各种传感器、EEPROM、PMIC 等从设备(Slave)通信。
Linux 中 I2C 子系统分为三层:
-
I2C 核心层(
drivers/i2c/i2c-core.c):提供通用 API,管理总线、设备、算法。 -
I2C 控制器驱动(适配器):由具体的 Platform 驱动实现,负责操作硬件寄存器,执行 I2C 时序。
-
I2C 客户端驱动:如
rtc-pcf8563.c,调用 I2C 核心层函数读写特定从设备。
第二章:Linux 5.10 典型 I2C 控制器 —— i2c-designware-platform.c
DesignWare I2C 是 ARM SoC 中最广泛使用的 I2C 控制器之一(如 Rockchip、Allwinner、ST、Intel 等)。其驱动在 Linux 5.10 中位于 drivers/i2c/busses/i2c-designware-platform.c(配合 i2c-designware-core.c)。
2.1 硬件关键寄存器
-
IC_CON:控制寄存器(I2C 使能、传输模式、速度、10-bit 地址)。 -
IC_TAR:目标地址寄存器。 -
IC_DATA_CMD:数据命令寄存器(写入或读取)。 -
IC_SS_SCL_HCNT/IC_SS_SCL_LCNT:标准模式的高低电平计数(用于时序)。 -
IC_RAW_INTR_STAT:原始中断状态寄存器。 -
IC_CLR_INTR:清除中断。
2.2 核心代码
// 基于 Linux 5.10 drivers/i2c/busses/i2c-designware-*.c
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/platform_device.h>
#include <linux/delay.h>
/**
* @brief DesignWare I2C 控制器私有数据结构。
*
* 对应图中 Platform Bus 上的 I2C Controller 硬件实例。
*/
struct dw_i2c_dev {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< I2C 控制器中断号 */
struct clk *clk; /**< 用于计算波特率 */
struct i2c_adapter adapter; /**< I2C 适配器抽象接口 */
struct completion cmd_complete; /**< 用于同步等待传输完成 */
struct i2c_msg *msg; /**< 当前正在处理的 I2C 消息 */
int msg_idx; /**< 消息内的字节索引 */
int msg_num; /**< 消息总数 */
int status; /**< 传输状态 */
spinlock_t lock; /**< 保护硬件状态的自旋锁 */
};
/* 寄存器偏移量定义(节选) */
#define DW_IC_CON 0x00
#define DW_IC_TAR 0x04
#define DW_IC_DATA_CMD 0x10
#define DW_IC_SS_SCL_HCNT 0x14
#define DW_IC_SS_SCL_LCNT 0x18
#define DW_IC_INTR_MASK 0x20
#define DW_IC_RAW_INTR_STAT 0x24
#define DW_IC_RX_TL 0x28
#define DW_IC_TX_TL 0x2C
#define DW_IC_CLR_INTR 0x30
/**
* @brief 从 I2C 设备读取一个字节(核心硬件操作)。
*
* 此函数由中断处理程序调用,或由轮询代码调用。
*
* @param dev 指向 dw_i2c_dev 结构。
* @param cmd 控制码(读/写标志)。
* @return 读取到的字节(如果 cmd 包含读标志)。
*/
static inline u32 dw_i2c_read_data_cmd(struct dw_i2c_dev *dev, u32 cmd)
{
/* 写 cmd 寄存器启动传输,然后读数据 */
writel(cmd, dev->base + DW_IC_DATA_CMD);
return readl(dev->base + DW_IC_DATA_CMD);
}
/**
* @brief 发送 I2C 消息(阻塞等待完成)。
*
* @param msg I2C 消息结构体。
* @param msgs 消息数组。
* @param num 消息数量。
* @return 实际传输的消息数量,负数表示错误。
*/
static int dw_i2c_xfer_msg(struct dw_i2c_dev *dev, struct i2c_msg *msg, int num)
{
u32 addr = msg->addr;
u32 tar = 0;
unsigned long flags;
int i;
// 1. 设置目标地址(7-bit 或 10-bit)
if (msg->flags & I2C_M_TEN)
tar = 0x0400 | (addr & 0x3FF); /* 10-bit 地址 */
else
tar = addr & 0x7F;
spin_lock_irqsave(&dev->lock, flags);
writel(tar, dev->base + DW_IC_TAR);
spin_unlock_irqrestore(&dev->lock, flags);
// 2. 准备传输数据
dev->msg = msg;
dev->msg_idx = 0;
dev->msg_num = num;
dev->status = 0;
reinit_completion(&dev->cmd_complete);
// 3. 启用中断(接收、发送、传输完成)
spin_lock_irqsave(&dev->lock, flags);
writel(0xFFFF, dev->base + DW_IC_INTR_MASK);
spin_unlock_irqrestore(&dev->lock, flags);
// 4. 填充数据到 TX FIFO(如果消息是写操作)
if (!(msg->flags & I2C_M_RD)) {
for (i = 0; i < msg->len; i++) {
u32 cmd = msg->buf[i];
// 最后字节设置 STOP 位(取决于是否需要续传)
if (i == msg->len - 1)
cmd |= 0x200; // STOP 标志
writel(cmd, dev->base + DW_IC_DATA_CMD);
}
}
// 5. 等待中断完成或超时
if (!wait_for_completion_timeout(&dev->cmd_complete, 5 * HZ)) {
dev_err(&dev->adapter.dev, "I2C transfer timeout\n");
return -ETIMEDOUT;
}
// 6. 检查传输状态
if (dev->status & (1 << 8)) { // 错误标志
return -EIO;
}
// 7. 如果是读消息,从 RX FIFO 读取数据(在中断处理中已完成)
return num;
}
/**
* @brief I2C 传输入口函数(由 I2C 核心层调用)。
*
* @param adap 指向 i2c_adapter。
* @param msgs 消息数组。
* @param num 消息数量。
* @return 实际传输的消息数量。
*/
static int dw_i2c_master_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
struct dw_i2c_dev *dev = i2c_get_adapdata(adap);
int i, ret;
// 串行处理每个消息(由于 I2C 通常需要连续传输)
for (i = 0; i < num; i++) {
ret = dw_i2c_xfer_msg(dev, &msgs[i], num - i);
if (ret < 0)
return ret;
// 如果消息支持重复启动(REPEATED START),继续
}
return num;
}
/**
* @brief I2C 中断处理函数(最核心的部分)。
*
* 处理各种 I2C 硬件事件:传输完成、接收 FIFO 满、错误等。
*
* @param irq 中断号。
* @param dev_id 指向 dw_i2c_dev 的指针。
*/
static irqreturn_t dw_i2c_irq_handler(int irq, void *dev_id)
{
struct dw_i2c_dev *dev = dev_id;
u32 stat, mask;
int i;
// 1. 读取原始中断状态
stat = readl(dev->base + DW_IC_RAW_INTR_STAT);
mask = readl(dev->base + DW_IC_INTR_MASK);
stat &= mask;
// 2. 处理传输完成中断(STOP 检测)
if (stat & (1 << 9)) { // STOP_DET
writel(1 << 9, dev->base + DW_IC_CLR_INTR); // 清除 STOP 中断
dev->status |= 1;
complete(&dev->cmd_complete); // 唤醒等待的 xfer 线程
}
// 3. 处理 RX 数据可用中断
if (stat & (1 << 2)) { // RX_AVAIL
// 循环读取 RX FIFO 中的数据
while (readl(dev->base + DW_IC_STATUS) & (1 << 3)) {
u32 data = readl(dev->base + DW_IC_DATA_CMD);
if (dev->msg && dev->msg_idx < dev->msg->len) {
dev->msg->buf[dev->msg_idx++] = (data & 0xFF);
}
}
}
// 4. 处理错误中断(NACK、总线错误等)
if (stat & (1 << 7)) { // NACK
dev->status |= (1 << 8);
complete(&dev->cmd_complete);
}
// 5. 处理 TX 空中断(写数据)
if (stat & (1 << 1)) { // TX_EMPTY
// 如果还有数据要发送,继续填充
if (dev->msg && dev->msg_idx < dev->msg->len) {
// 写数据到 TX FIFO(略)
}
}
return IRQ_HANDLED;
}
/**
* @brief Platform 探测函数。
*
* 初始化 I2C 控制器硬件,注册到 I2C 核心。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int dw_i2c_probe(struct platform_device *pdev)
{
struct dw_i2c_dev *dev;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
platform_set_drvdata(pdev, dev);
// 2. 获取并映射 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENXIO;
dev->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(dev->base))
return PTR_ERR(dev->base);
// 3. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
dev->irq = irq;
// 4. 获取时钟
dev->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(dev->clk))
dev->clk = NULL;
if (dev->clk)
clk_prepare_enable(dev->clk);
// 5. 初始化同步完成量
init_completion(&dev->cmd_complete);
spin_lock_init(&dev->lock);
// 6. 设置 I2C 适配器结构
dev->adapter = (struct i2c_adapter) {
.owner = THIS_MODULE,
.class = I2C_CLASS_HWMON | I2C_CLASS_SPD,
.algo = &dw_i2c_algo,
.retries = 3,
.dev = {
.parent = &pdev->dev,
},
};
i2c_set_adapdata(&dev->adapter, dev);
// 7. 配置 I2C 时序(计算 HCNT/LCNT)
// 这部分取决于输入时钟和所需波特率(通常 100kHz 或 400kHz)
// 此处省略公式计算,实际驱动中需要根据时钟和 ICCL/ICCH 调整
writel(0x1F, dev->base + DW_IC_SS_SCL_HCNT);
writel(0x2F, dev->base + DW_IC_SS_SCL_LCNT);
// 8. 使能 I2C 控制器
writel(0x00000065, dev->base + DW_IC_CON); // 标准模式,7-bit,MST,ENABLE
writel(0x00000001, dev->base + DW_IC_CON); // 启用
// 9. 注册中断
ret = devm_request_irq(&pdev->dev, dev->irq, dw_i2c_irq_handler,
IRQF_SHARED, "dw_i2c", dev);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ %d\n", dev->irq);
return ret;
}
// 10. 注册到 I2C 核心
ret = i2c_add_adapter(&dev->adapter);
if (ret) {
dev_err(&pdev->dev, "Failed to add i2c adapter\n");
return ret;
}
dev_info(&pdev->dev, "DesignWare I2C adapter registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, dev->irq);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int dw_i2c_remove(struct platform_device *pdev)
{
struct dw_i2c_dev *dev = platform_get_drvdata(pdev);
// 1. 删除 I2C 适配器(也会断开所有从设备)
i2c_del_adapter(&dev->adapter);
// 2. 禁用控制器
writel(0x0, dev->base + DW_IC_CON);
// 3. 禁用时钟
if (dev->clk)
clk_disable_unprepare(dev->clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id dw_i2c_of_match[] = {
{ .compatible = "snps,designware-i2c" },
{ .compatible = "rockchip,rk3399-i2c" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, dw_i2c_of_match);
/* Platform 驱动结构 */
static struct platform_driver dw_i2c_driver = {
.probe = dw_i2c_probe,
.remove = dw_i2c_remove,
.driver = {
.name = "dw_i2c",
.of_match_table = dw_i2c_of_match,
},
};
module_platform_driver(dw_i2c_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("DesignWare I2C Platform Driver");
MODULE_LICENSE("GPL v2");
第三章:I2C 调试核心难点
3.1 I2C 总线卡死(Bus Busy)
现象:i2cdetect -y <bus> 卡住,或者 dmesg 显示 bus is busy。
原因:某个从设备在通信过程中异常掉电,导致 SDA 被该设备拉低,而主机无法释放。
调试方法:
-
使用
i2ctools尝试恢复:echo 0 > /sys/bus/i2c/devices/i2c-<bus>/bus_reset # 如果支持
-
手动复位 I2C 控制器:在
probe中添加代码,向IC_CON写入 0 再写 1 以软复位。 -
查看硬件状态寄存器:
devmem2 <base_addr>+0x100 # 某个 SoC 的 I2C 状态寄存器
-
暴力解法:如果无法复位,只有重新上电或复位整个 SoC。
3.2 传输超时(Timeout)
现象:i2c_read 或 i2c_write 返回 -ETIMEDOUT。
可能原因:
-
时钟频率配置错误(
HCNT/LCNT计算错误)。 -
从设备 NACK(地址错误或设备不存在)。
-
中断处理程序没有正确唤醒
completion。
调试方法:
-
使用
perf跟踪中断:perf record -e irq:irq_handler_entry -e irq:irq_handler_exit -p <irq_number>
查看中断是否被触发。
-
增加调试打印:在
dw_i2c_irq_handler中打印stat值。 -
检查波特率:用示波器测量 SCL 频率,是否与期望(100kHz/400kHz)匹配。
3.3 数据错乱(Crc mismatch 或字节丢失)
现象:读取的数据与预期不符,或者读回来的字节数不全。
原因:RX FIFO 触发水平(RX_TL)设置不当,导致中断处理程序来不及读取数据就发生溢出。
调试方法:
-
检查
RX_TL寄存器值(默认 0:每收到 1 字节中断一次)。对于高速传输,可调高阈值如 8。 -
使用
perf测量 ISR 执行时间,如果太长,考虑使用 DMA 模式。
第四章:结合性能调试场景示例
场景:系统在大量读写 EEPROM 时偶尔卡死 10 秒,随后恢复。
分析:
-
宏观观察(
top看到iowait升高)。 -
查看 I2C 总线:
i2cdetect -y <bus>显示 No response(总线死锁)。 -
追踪 I2C 核心事件:
trace-cmd record -e i2c:i2c_transfer -e i2c:i2c_result -e i2c:i2c_write -e i2c:i2c_read trace-cmd report | grep timeout
发现传输在某个消息后没有触发
STOP_DET中断。 -
深入中断层( Device Drivers -> I2C Controller):
bpftrace -e 'kprobe:dw_i2c_irq_handler { @start[tid]=nsecs; } kretprobe:dw_i2c_irq_handler { if (nsecs - @start[tid] > 1000000) { printf("Slow ISR: %d ns\n", nsecs - @start[tid]); } }'发现中断服务程序偶尔花费超过 1ms,可能是因为 RX FIFO 满了,ISR 在循环读取大量数据时占用了过多时间,导致其他中断(如 TIMER)被延迟,最终 I2C 超时。
-
最终解决方案:降低
RX_TL值(从 0 改为 8),增加 DMA 支持,避免 CPU 在 ISR 中忙于读取大量字节。
第五章:与其他控制器的协同
I2C 总线通常连接:
-
RTC:读取实时时间。
-
PMIC:调节电压。
-
音频编解码器:配置寄存器。
-
触摸屏控制器:通过 I2C 上报触摸坐标。
调试这类复合场景时,建议:
-
先独立验证 I2C 总线:使用
i2cdetect确保所有设备地址可见。 -
再验证具体设备:使用
i2ctransfer发送自定义命令,对比数据手册中的寄存器回复。 -
使用
perf跟踪中断:确认 I2C 中断与其他总线(如 SPI、UART)共享中断线时是否存在优先级倒置问题。
第九部分 SPI 控制器
第一章:SPI 控制器在 Platform Bus 中的位置
SPI 控制器 与 I2C 控制器并列,都挂载在 Platform Bus 上。SPI (Serial Peripheral Interface) 是一种全双工、同步通信接口,通常比 I2C 速度更快,用于连接 NOR Flash、传感器、触摸屏控制器、音频 Codec 等高速或需要高吞吐量的设备。
Linux 中 SPI 子系统同样分为三层:
-
SPI 核心层 (
drivers/spi/spi.c):管理spi_controller(主机)、spi_device(从设备) 和spi_message(传输事务)。 -
SPI 控制器驱动:具体 SoC 的 SPI 主机驱动(例如
spi-dw.c),负责操作硬件寄存器,执行transfer_one_message。 -
SPI 客户端驱动:如
drivers/spi/spi-nor.c(Flash)、drivers/iio/imu/st_lsm6dsx/(传感器),调用 SPI 核心 API 进行读写。
第二章:Linux 5.10 典型 SPI 控制器驱动 —— spi-dw.c
DesignWare SPI 是 ARM SoC 生态中非常普及的 SPI 主机控制器 IP (如 Rockchip, Allwinner, ST 等)。以下代码基于 Linux 5.10 drivers/spi/spi-dw.c 和 spi-dw-core.c。
2.1 硬件关键寄存器
-
DW_SPI_CTRLR0:控制寄存器,配置传输模式(SPI 模式 0~3)、帧格式(Motorola, TI, Microwire)。 -
DW_SPI_SSIENR:使能 SPI 控制器。 -
DW_SPI_BAUDR:波特率分频寄存器。 -
DW_SPI_TXDR:发送数据寄存器。 -
DW_SPI_RXDR:接收数据寄存器。 -
DW_SPI_SR:状态寄存器(TX 满?RX 空?忙碌?)。 -
DW_SPI_IMR/DW_SPI_ISR:中断掩码与状态。
2.2 核心代码
// 基于 Linux 5.10 drivers/spi/spi-dw-core.c (简化版)
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/spi/spi.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/dma-mapping.h>
/**
* @brief DesignWare SPI 控制器私有数据结构。
*
* 对应于图中 Platform Bus 上的 SPI Controller 硬件实例。
*/
struct dw_spi {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< SPI 中断号 */
struct clk *clk; /**< 时钟 (用于波特率计算) */
struct spi_controller *master; /**< SPI 主机抽象,供核心层调用 */
struct spi_message *cur_msg; /**< 当前正在处理的消息 */
struct spi_transfer *cur_transfer; /**< 当前传输段 */
int tx_len; /**< TX 剩余字节数 */
int rx_len; /**< RX 剩余字节数 */
u8 *tx; /**< TX 缓冲指针 */
u8 *rx; /**< RX 缓冲指针 */
bool use_dma; /**< 是否使用 DMA */
spinlock_t lock; /**< 保护硬件状态的自旋锁 */
};
/* 寄存器偏移量(节选) */
#define DW_SPI_CTRLR0 0x00
#define DW_SPI_SSIENR 0x08
#define DW_SPI_BAUDR 0x0C
#define DW_SPI_TXDR 0x20
#define DW_SPI_RXDR 0x24
#define DW_SPI_SR 0x28
#define DW_SPI_IMR 0x2C
#define DW_SPI_ISR 0x30
#define DW_SPI_RXFLR 0x3C
#define DW_SPI_TXFLR 0x40
/**
* @brief 读取状态寄存器。
*
* @param dws 指向 dw_spi 结构。
* @return 状态值。
*/
static inline u32 dw_spi_read_sr(struct dw_spi *dws)
{
return readl(dws->base + DW_SPI_SR);
}
/**
* @brief 检查 SPI 控制器是否空闲。
*
* @param dws 指向 dw_spi 结构。
* @return true 空闲,false 忙碌。
*/
static inline bool dw_spi_is_busy(struct dw_spi *dws)
{
return dw_spi_read_sr(dws) & 0x01; // SR_BUSY
}
/**
* @brief 从 RX FIFO 读取数据并保存到 rx 缓冲区。
*
* 在中断或轮询模式下调用。
*
* @param dws 指向 dw_spi 结构。
*/
static void dw_spi_read_rx(struct dw_spi *dws)
{
while (dws->rx_len) {
if (!(dw_spi_read_sr(dws) & 0x20)) // RX FIFO 为空
break;
if (dws->rx)
*dws->rx++ = readl(dws->base + DW_SPI_RXDR);
else
readl(dws->base + DW_SPI_RXDR); // 丢弃数据
dws->rx_len--;
}
}
/**
* @brief 向 TX FIFO 写入数据。
*
* 在中断或轮询模式下调用。
*
* @param dws 指向 dw_spi 结构。
*/
static void dw_spi_write_tx(struct dw_spi *dws)
{
while (dws->tx_len && !(dw_spi_read_sr(dws) & 0x02)) { // TX FIFO 未满
if (dws->tx)
writel(*dws->tx++, dws->base + DW_SPI_TXDR);
else
writel(0, dws->base + DW_SPI_TXDR); // 发送哑字节
dws->tx_len--;
}
}
/**
* @brief SPI 中断处理函数。
*
* 处理 TX 空、RX 满、传输错误等事件。
*
* @param irq 中断号。
* @param dev_id 指向 dw_spi 结构。
*/
static irqreturn_t dw_spi_irq_handler(int irq, void *dev_id)
{
struct dw_spi *dws = dev_id;
u32 isr, imr;
spin_lock(&dws->lock);
// 1. 读取中断状态
isr = readl(dws->base + DW_SPI_ISR);
imr = readl(dws->base + DW_SPI_IMR);
isr &= imr; // 仅处理已使能的中断
// 2. 处理 RX 数据
if (isr & 0x04) { // RX 数据有效
dw_spi_read_rx(dws);
}
// 3. 处理 TX 空间
if (isr & 0x02) { // TX 空间可用
dw_spi_write_tx(dws);
}
// 4. 检查传输是否完成
if (dws->tx_len == 0 && dws->rx_len == 0) {
// 本次消息全部完成
spi_finalize_current_transfer(dws->master);
// 清除中断使能(暂时停止中断,等待下一条消息)
writel(0, dws->base + DW_SPI_IMR);
}
// 5. 清除中断标志(通常写 0 清除,或读取 ISR 自动清除)
// 具体取决于硬件设计,某些 IP 需要写 1 清除。
writel(isr, dws->base + DW_SPI_ISR); // 写 ISR 值清除(DesignWare 特性)
spin_unlock(&dws->lock);
return IRQ_HANDLED;
}
/**
* @brief 执行一个 SPI 传输段。
*
* 由 spi_controller 的 transfer_one 回调调用。
*
* @param master 指向 spi_controller 结构。
* @param transfer 指向当前传输段。
* @return 0 成功,非零错误。
*/
static int dw_spi_transfer_one(struct spi_controller *master,
struct spi_device *spi,
struct spi_transfer *transfer)
{
struct dw_spi *dws = spi_controller_get_devdata(master);
u32 txlen, rxlen, baud_div;
u32 ctrlr0 = readl(dws->base + DW_SPI_CTRLR0);
// 1. 设置 SPI 模式 (CPOL, CPHA)
ctrlr0 &= ~(0x03 << 6);
if (spi->mode & SPI_CPOL)
ctrlr0 |= (1 << 6);
if (spi->mode & SPI_CPHA)
ctrlr0 |= (1 << 7);
writel(ctrlr0, dws->base + DW_SPI_CTRLR0);
// 2. 设置波特率分频
if (transfer->speed_hz) {
baud_div = clk_get_rate(dws->clk) / transfer->speed_hz;
baud_div = clamp(baud_div, 2U, 0xFFFFU); // 最小分频 2
writel(baud_div / 2, dws->base + DW_SPI_BAUDR);
}
// 3. 设置数据长度 (不是所有版本都支持可变长度,这里简化)
ctrlr0 &= ~0x0F;
ctrlr0 |= ((transfer->bits_per_word - 1) & 0x0F);
writel(ctrlr0, dws->base + DW_SPI_CTRLR0);
// 4. 设置 DMA 或 PIO
if (transfer->len > 32 && dws->use_dma) {
// ... 启用 DMA 模式 (省略)
}
// 5. 填充数据到 TX FIFO (PIO 模式)
dws->tx_len = transfer->len;
dws->rx_len = transfer->len;
dws->tx = (u8 *)transfer->tx_buf;
dws->rx = (u8 *)transfer->rx_buf;
dws->cur_transfer = transfer;
// 6. 启用中断
writel(0x0F, dws->base + DW_SPI_IMR); // 启用 RX, TX, 错误中断
// 7. 开始发送
dw_spi_write_tx(dws);
// 8. 等待中断完成 (由中断处理程序调用 spi_finalize_current_transfer)
return 0; // 异步方式,中断完成时通知
}
/**
* @brief Platform 探测函数。
*
* 初始化 SPI 控制器硬件,注册到 SPI 核心层。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int dw_spi_probe(struct platform_device *pdev)
{
struct dw_spi *dws;
struct resource *res;
struct spi_controller *master;
int ret, irq;
// 1. 分配 SPI 控制器 (用 devm_spi_alloc_master)
master = spi_alloc_master(&pdev->dev, sizeof(struct dw_spi));
if (!master)
return -ENOMEM;
platform_set_drvdata(pdev, master);
dws = spi_master_get_devdata(master);
dws->master = master;
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
dws->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(dws->base)) {
dev_err(&pdev->dev, "Failed to map registers\n");
return PTR_ERR(dws->base);
}
// 3. 获取 IRQ
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
dws->irq = irq;
// 4. 获取时钟
dws->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(dws->clk))
dws->clk = NULL;
if (dws->clk)
clk_prepare_enable(dws->clk);
// 5. 设置主机能力
master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
master->bits_per_word_mask = SPI_BPW_RANGE_MASK(4, 32);
master->transfer_one = dw_spi_transfer_one;
master->num_chipselect = 4; // 根据硬件
master->max_speed_hz = clk_get_rate(dws->clk) / 2; // 最大波特率
master->dev.of_node = pdev->dev.of_node;
master->auto_runtime_pm = true;
spin_lock_init(&dws->lock);
// 6. 硬件初始化:禁用控制器,配置默认模式
writel(0, dws->base + DW_SPI_SSIENR);
writel(0x00010000, dws->base + DW_SPI_CTRLR0); // 默认 8-bit, Motorola 格式
// 设置片选极性 (CS) 等 ...
// 7. 注册中断
ret = devm_request_irq(&pdev->dev, dws->irq, dw_spi_irq_handler,
IRQF_SHARED, "dw_spi", dws);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 8. 使能 SPI 控制器
writel(0x01, dws->base + DW_SPI_SSIENR);
// 9. 注册到 SPI 核心
ret = devm_spi_register_controller(&pdev->dev, master);
if (ret) {
dev_err(&pdev->dev, "Failed to register SPI controller\n");
return ret;
}
dev_info(&pdev->dev, "DesignWare SPI controller registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, dws->irq);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int dw_spi_remove(struct platform_device *pdev)
{
struct spi_controller *master = platform_get_drvdata(pdev);
struct dw_spi *dws = spi_master_get_devdata(master);
// 1. 禁用 SPI 控制器
writel(0, dws->base + DW_SPI_SSIENR);
// 2. 禁用时钟
if (dws->clk)
clk_disable_unprepare(dws->clk);
// 3. 禁用 DMA (如果有)
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id dw_spi_of_match[] = {
{ .compatible = "snps,dw-spi" },
{ .compatible = "rockchip,rk3399-spi" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, dw_spi_of_match);
/* Platform 驱动结构 */
static struct platform_driver dw_spi_driver = {
.probe = dw_spi_probe,
.remove = dw_spi_remove,
.driver = {
.name = "dw_spi",
.of_match_table = dw_spi_of_match,
},
};
module_platform_driver(dw_spi_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("DesignWare SPI Platform Driver");
MODULE_LICENSE("GPL v2");
第三章:SPI 调试核心难点
3.1 片选 (CS) 管理
SPI 的 CS (Chip Select) 是硬件片选,但很多芯片是低电平有效。
常见问题:
-
CS 信号无法拉低/拉高,导致从设备不响应。
-
CS 信号与总线数据的时间窗口冲突(setup/hold 时间)。
调试方法:
-
查看 pinctrl 状态:
cat /sys/kernel/debug/pinctrl/pinctrl-handles | grep <spi_bus>
确认 CS 引脚被正确配置为
spi0_cs0功能。 -
手动切换 CS:当 SPI 驱动不支持时,可以在驱动中明确调用
gpio_set_value(gpio_cs, 0);。 -
使用逻辑分析仪:抓取 CS 与 MISO/MOSI 的波形,观察拉低时间是否正确。
3.2 波特率与时钟极性 (CPOL/CPHA)
这是 SPI 最头疼的问题。模式不匹配会导致接收的数据错位。
现象:读取的数据全是 0x00 或 0xFF,或读取的值反相。
调试方法:
-
匹配模式:仔细阅读从设备数据手册,确定需要的模式 (Mode 0~3)。
-
检查驱动设置:确保
dw_spi_transfer_one中正确设置了ctrlr0的 6/7 位。 -
示波器检查:测量
SCLK在空闲时的电平(高或低)以及数据采样时刻。
3.3 DMA 传输异常
使用 DMA 时,如果 DMA 通道配置错误,可能导致数据错乱或 FIFO 溢出。
现象:大量数据传输时丢数据,或中断风暴(DMA 没完成,CPU 反复轮询状态)。
调试方法:
-
检查 DMA 错误日志:
dmesg | grep dma。 -
不使用 DMA:设置
dws->use_dma = false;先让 PIO 模式工作正常,再调试 DMA。 -
使用
perf追踪 DMA 事件:perf record -e dma:* -a -- sleep 5 perf report
看 dma 请求是否被及时处理。
3.4 传输死锁
现象:SPI 传输卡住,CPU 占用 100%,但数据没发出去。
原因:中断被禁用了,或者中断处理程序没有正确唤醒 SPI 核心层。
调试方法:
-
检查中断状态:
cat /proc/interrupts | grep <irq>,看触发次数。 -
动态跟踪 ISR:
bpftrace -e 'kprobe:dw_spi_irq_handler { @count[tid]=count(); } kretprobe:dw_spi_irq_handler { if (retval != 0) printf("ISR error\n"); }' -
检查锁机制:确保
dw_spi_irq_handler里正确使用了spin_lock,但不要长时间持有。
第四部分:结合性能调试场景示例
场景:系统启动时挂载 NOR Flash (spi-nor) 失败,导致内核无法挂载文件系统,从设备响应超时。
分析流程:
-
宏观观察(启动日志
dmesg):dmesg | grep spi
看到 "spi_nor: probe failed -ETIMEDOUT"。
-
追踪 SPI 传输(图谱中 Device Drivers -> SPI Controller):
trace-cmd record -e spi:spi_transfer -e spi:spi_sync trace-cmd report
显示最后一个传输
tx数据被写入了,但是rx没有收到数据。 -
硬件检查:
-
使用
devmem2读取DW_SPI_SR状态寄存器:
devmem2 <base_addr>+0x28
发现
SR_BUSY一直为 1,表示控制器处于忙碌状态,且SR_TX_EMPTY已经为 0,但RX_FIFO不为空。 -
-
分析原因:可能是因为 DMA 配置错误,导致 RX FIFO 数据未及时读取,最终控制器卡死在忙碌状态。
-
解决方案:
-
禁用 DMA:在 DTS 中设置
dma-names = "";或强制dws->use_dma = false;。 -
测试 PIO 模式,观察是否成功。如果成功,说明 DMA 配置有问题。
-
第五章:与其他控制器的协同
SPI 总线通常连接:
-
NOR Flash:存储 Bootloader、内核、文件系统。
-
传感器:I2C 和 SPI 都可用,但 SPI 吞吐量更高。
-
触摸屏控制器:主要用于大屏幕。
-
LCD 接口(SPI 屏):驱动 RGB 数据显示。
调试复合场景时,建议:
-
优先验证 SPI 总线:使用
spidev工具测试。 -
确认片选分配:在 DTS 中,确保
reg属性与物理 CS 线对应。 -
调试 cs_gpio:如果使用 GPIO 模拟 CS,必须确保 GPIO 方向正确且在 SPI 传输前后正确拉低/拉高。
第十部分 DMA 控制器
第一章:DMA 控制器在 Platform Bus 中的位置
DMA 控制器通常作为一个独立的 Platform 设备存在。它并不直接连接最终外设(如LCD、Camera),而是作为 数据搬运工,为其他控制器(如 UART、I2C、SPI、Audio)提供内存与外设之间、或者内存与内存之间的数据搬移服务。
Linux 中 DMA 子系统同样分为三层:
-
DMA 核心层 (
drivers/dma/dmaengine.c):提供通用 DMA 引擎 API,管理通道、描述符和传输。 -
DMA 控制器驱动:具体硬件(如 PL330、DW_DMAC、DMA-APB),负责管理硬件通道、中断和传输队列。
-
DMA 客户端驱动:例如
dw_spi.c中的 DMA 支持代码,通过dmaengine_prep_slave_sg申请 DMA 通道并使用。
第二章:Linux 5.10 典型 DMA 控制器驱动 —— dma-pl330.c
PL330 (PrimeCell DMA Controller) 是 ARM 架构 SoC 中最常见的 DMA 控制器之一(广泛用于各种 Cortex-A 系列)。以下代码基于 Linux 5.10 drivers/dma/pl330.c。
2.1 硬件关键概念
-
通道 (Channel):每个 PL330 有多个 DMA 通道(通常 8 或 16 个),可独立配置。
-
描述符 (Descriptor):DMA 传输的指令(源地址、目的地址、长度、中断设置)。
-
Microcode:PL330 有一个微处理器,通过微程序控制 DMA 动作(如
DMAMOV,DMALD,DMSTP等)。 -
中断:每个通道传输完成或出错时触发中断。
2.2 核心代码与 Doxygen 注解
// 基于 Linux 5.10 drivers/dma/pl330.c (精简版)
#include <linux/io.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/amba/pl330.h>
/**
* @brief PL330 硬件寄存器偏移量(节选)。
*/
#define PL330_CHANNEL_BASE 0x100
#define PL330_CHANNEL_CMD 0x00
#define PL330_CHANNEL_CTL 0x04
#define PL330_CHANNEL_STATUS 0x08
#define PL330_CHANNEL_SRC_ADDR 0x0C
#define PL330_CHANNEL_DST_ADDR 0x10
#define PL330_CHANNEL_LEN 0x14
#define PL330_CHANNEL_NEXT 0x18
/**
* @brief PL330 DMA 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 DMA Controller 硬件实例。
*/
struct pl330_dmac {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< 中断号 */
struct dma_device dma_dev; /**< DMA 引擎抽象,供核心调用 */
struct pl330_channel *channels; /**< 所有物理通道数组 */
int num_channels; /**< 通道数量 (从硬件读取) */
spinlock_t lock; /**< 全局自旋锁 */
bool pdata_present; /**< 平台数据是否存在 */
};
/**
* @brief PL330 单个通道的私有数据结构。
*/
struct pl330_channel {
struct pl330_dmac *dmac; /**< 所属的 DMAC */
struct dma_chan chan; /**< DMA 通道抽象 */
int hw_channel_id; /**< 硬件通道 ID */
void __iomem *reg_base; /**< 通道寄存器基址 */
struct pl330_desc *next_desc; /**< 下一个待处理的描述符 */
bool busy; /**< 通道是否忙碌 */
spinlock_t lock; /**< 通道自旋锁 */
};
/**
* @brief DMA 传输描述符。
*/
struct pl330_desc {
dma_addr_t src_addr; /**< 源地址(物理地址) */
dma_addr_t dst_addr; /**< 目的地址(物理地址) */
size_t len; /**< 传输长度 */
enum dma_transfer_direction dir; /**< 传输方向 */
struct pl330_desc *next; /**< 链式描述符 */
struct dma_async_tx_descriptor txd; /**< 对应 DMA 核心的 TX 描述符 */
};
/**
* @brief 将 DMA 描述符写入硬件通道。
*
* 此函数将描述符的源地址、目的地址、长度、控制字写入通道寄存器,
* 然后启动传输。
*
* @param channel 指向 PL330 通道。
* @param desc 指向描述符。
* @return 0 成功,负数错误。
*/
static int pl330_hw_start_transfer(struct pl330_channel *channel,
struct pl330_desc *desc)
{
void __iomem *regs = channel->reg_base;
u32 ctl = 0;
// 1. 写入源地址
writel(desc->src_addr, regs + PL330_CHANNEL_SRC_ADDR);
// 2. 写入目的地址
writel(desc->dst_addr, regs + PL330_CHANNEL_DST_ADDR);
// 3. 写入长度(以字节为单位)
writel(desc->len, regs + PL330_CHANNEL_LEN);
// 4. 配置控制字(中断使能、方向、宽度等)
ctl |= (1 << 8); // 传输完成中断启用
if (desc->dir == DMA_MEM_TO_DEV)
ctl |= (1 << 7); // 方向:内存 -> 外设
else if (desc->dir == DMA_DEV_TO_MEM)
ctl &= ~(1 << 7); // 方向:外设 -> 内存
else if (desc->dir == DMA_MEM_TO_MEM)
ctl |= (1 << 6); // 内存 -> 内存
writel(ctl, regs + PL330_CHANNEL_CTL);
// 5. 启动传输 (写入命令 0x0000 表示启动)
writel(0x0000, regs + PL330_CHANNEL_CMD);
channel->busy = true;
return 0;
}
/**
* @brief DMA 中断处理函数。
*
* 当某个通道完成传输或发生错误时触发。
*
* @param irq 中断号。
* @param dev_id 指向 pl330_dmac 结构。
*/
static irqreturn_t pl330_irq_handler(int irq, void *dev_id)
{
struct pl330_dmac *dmac = dev_id;
u32 status;
int i;
spin_lock(&dmac->lock);
// 1. 读取全局中断状态寄存器(具体实现依赖硬件)
// 这里简化:轮询所有通道的状态寄存器
for (i = 0; i < dmac->num_channels; i++) {
struct pl330_channel *ch = &dmac->channels[i];
void __iomem *regs = ch->reg_base;
u32 st = readl(regs + PL330_CHANNEL_STATUS);
// 2. 检查是否完成(完成状态位通常为 0x02)
if (st & 0x02) {
// 清除完成状态(写入该位清零)
writel(0x02, regs + PL330_CHANNEL_STATUS);
ch->busy = false;
// 通知 DMA 核心当前传输已完成
dma_cookie_complete(&ch->next_desc->txd);
dma_async_tx_descriptor_callback(&ch->next_desc->txd);
// 如果有下一个描述符,启动它
if (ch->next_desc->next) {
struct pl330_desc *next = ch->next_desc->next;
ch->next_desc = next;
pl330_hw_start_transfer(ch, next);
} else {
ch->next_desc = NULL;
}
}
}
spin_unlock(&dmac->lock);
return IRQ_HANDLED;
}
/**
* @brief 分配 DMA 描述符。
*
* 由 DMA 核心调用,用于创建一个新的传输描述符。
*
* @param chan 指向 dma_chan。
* @param len 传输长度。
* @param flags 描述符标志。
* @return 指向描述符的指针。
*/
static struct dma_async_tx_descriptor *
pl330_prep_slave_sg(struct dma_chan *chan, struct scatterlist *sgl,
unsigned int sg_len, enum dma_transfer_direction dir,
unsigned long flags, void *context)
{
struct pl330_channel *ch = container_of(chan, struct pl330_channel, chan);
struct pl330_desc *desc;
struct scatterlist *sg;
int i;
// 1. 分配描述符(仅处理第一个 scatterlist 简化)
if (sg_len != 1)
return NULL; // 简化起见,仅处理单个 sg
sg = sgl;
desc = kzalloc(sizeof(*desc), GFP_ATOMIC);
if (!desc)
return NULL;
// 2. 从 sg 中提取物理地址
desc->src_addr = sg_dma_address(sg);
desc->dst_addr = sg_dma_address(sg);
desc->len = sg_dma_len(sg);
desc->dir = dir;
// 3. 初始化 TX 描述符核心结构
dma_async_tx_descriptor_init(&desc->txd, chan);
desc->txd.tx_submit = pl330_tx_submit; // 提交时调用
desc->txd.flags = flags;
return &desc->txd;
}
/**
* @brief 提交描述符到 DMA 通道队列。
*
* 由 DMA 核心调用,将描述符加入到通道的待处理队列。
*
* @param tx 指向 dma_async_tx_descriptor。
* @return 已提交的 cookie。
*/
static dma_cookie_t pl330_tx_submit(struct dma_async_tx_descriptor *tx)
{
struct pl330_desc *desc = container_of(tx, struct pl330_desc, txd);
struct dma_chan *chan = tx->chan;
struct pl330_channel *ch = container_of(chan, struct pl330_channel, chan);
dma_cookie_t cookie;
unsigned long flags;
spin_lock_irqsave(&ch->lock, flags);
// 1. 分配 cookie
cookie = dma_cookie_assign(desc->txd.chan);
desc->txd.cookie = cookie;
// 2. 如果没有正在进行的传输,立即启动
if (!ch->busy) {
ch->next_desc = desc;
pl330_hw_start_transfer(ch, desc);
} else {
// 否则添加到队列末尾(这里简化处理)
// 实际驱动中需要维护链表
ch->next_desc = desc;
}
spin_unlock_irqrestore(&ch->lock, flags);
return cookie;
}
/**
* @brief Platform 探测函数。
*
* 初始化 DMA 控制器硬件,注册到 DMA 引擎。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int pl330_probe(struct platform_device *pdev)
{
struct pl330_dmac *dmac;
struct resource *res;
int ret, irq, i;
// 1. 分配私有数据结构
dmac = devm_kzalloc(&pdev->dev, sizeof(*dmac), GFP_KERNEL);
if (!dmac)
return -ENOMEM;
platform_set_drvdata(pdev, dmac);
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
dmac->base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
if (!dmac->base) {
dev_err(&pdev->dev, "Failed to map registers\n");
return -ENOMEM;
}
// 3. 获取 IRQ
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
dmac->irq = irq;
// 4. 读取通道数量(从硬件寄存器读,或从 platform_data 读)
// 这里假设固定 8 通道
dmac->num_channels = 8;
// 5. 分配通道数组
dmac->channels = devm_kzalloc(&pdev->dev,
dmac->num_channels * sizeof(struct pl330_channel),
GFP_KERNEL);
if (!dmac->channels)
return -ENOMEM;
spin_lock_init(&dmac->lock);
// 6. 初始化每个通道
for (i = 0; i < dmac->num_channels; i++) {
struct pl330_channel *ch = &dmac->channels[i];
ch->dmac = dmac;
ch->hw_channel_id = i;
ch->reg_base = dmac->base + PL330_CHANNEL_BASE + i * 0x20;
spin_lock_init(&ch->lock);
ch->chan.device = &dmac->dma_dev;
ch->chan.private = NULL;
// 初始化 DMA 核心通道
list_add_tail(&ch->chan.device_node, &dmac->dma_dev.channels);
}
// 7. 设置 DMA 设备结构
dmac->dma_dev.dev = &pdev->dev;
dmac->dma_dev.channels = LIST_HEAD_INIT(dmac->dma_dev.channels);
dmac->dma_dev.direction = (DMA_MEM_TO_DEV | DMA_DEV_TO_MEM | DMA_MEM_TO_MEM);
dmac->dma_dev.src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
dmac->dma_dev.dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
dmac->dma_dev.device_prep_slave_sg = pl330_prep_slave_sg;
dmac->dma_dev.device_tx_submit = pl330_tx_submit;
dmac->dma_dev.device_alloc_chan_resources = pl330_alloc_chan_resources;
dmac->dma_dev.device_free_chan_resources = pl330_free_chan_resources;
// 8. 注册中断
ret = devm_request_irq(&pdev->dev, dmac->irq, pl330_irq_handler,
IRQF_SHARED, "pl330", dmac);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 9. 注册到 DMA 核心
ret = dma_async_device_register(&dmac->dma_dev);
if (ret) {
dev_err(&pdev->dev, "Failed to register DMA device\n");
return ret;
}
dev_info(&pdev->dev, "PL330 DMA controller registered at 0x%llx, IRQ %d, %d channels\n",
(unsigned long long)res->start, dmac->irq, dmac->num_channels);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int pl330_remove(struct platform_device *pdev)
{
struct pl330_dmac *dmac = platform_get_drvdata(pdev);
// 1. 注销 DMA 设备
dma_async_device_unregister(&dmac->dma_dev);
// 2. 禁用中断(通过 devm 自动释放)
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id pl330_of_match[] = {
{ .compatible = "arm,pl330" },
{ .compatible = "arm,primecell", }, // 某些设备可能用 primecell
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, pl330_of_match);
/* Platform 驱动结构 */
static struct platform_driver pl330_driver = {
.probe = pl330_probe,
.remove = pl330_remove,
.driver = {
.name = "pl330",
.of_match_table = pl330_of_match,
},
};
module_platform_driver(pl330_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("ARM PL330 DMA Controller Driver");
MODULE_LICENSE("GPL v2");
第三章: DMA 调试核心难点
3.1 内存一致性(Cache coherency)
现象:DMA 写入了数据,但 CPU 读取时看到的是旧值;或者 CPU 写入了数据,DMA 读到的也是旧值。
原因:CPU 缓存与 DDR 之间的不一致性。DMA 直接访问物理内存,绕过 CPU Cache。
解决方案:
-
使用
dma_alloc_coherent分配内存(可保证 CPU 和 DMA 视图一致)。 -
使用
dma_map_single和dma_unmap_single,并确保dma_sync_single_for_device/dma_sync_single_for_cpu被正确调用。 -
在 Client 驱动中:如果接收数据后 CPU 读取错误,添加
dma_sync_single_for_cpu。
3.2 虚拟地址 vs. 物理地址
现象:DMA 访问内存地址错误,导致数据损坏或系统崩溃。
原因:DMA 使用物理地址,而 CPU 使用虚拟地址。驱动错误地将虚拟地址传给了 DMA 控制器。
调试方法:
-
检查 DMA 映射:在
pl330_hw_start_transfer中打印src_addr和dst_addr,确认它们属于dma_addr_t类型(物理地址)。 -
使用
perf跟踪dma_map_single和dma_unmap_single:perf record -e dma:dma_map_page -e dma:dma_unmap_page -a
-
检查 DMA 限制:某些 DMA 只能访问 32 位地址,如果物理地址 > 4GB,需要设置
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))。
3.3 中断丢失或过度中断
现象:DMA 传输完成但没有中断触发;或者中断触发太频繁导致 CPU 占用率飙升。
原因:
-
中断配置错误(通道中断未使能)。
-
DMA 驱动未清除中断标志位。
-
描述符中的
DMA_PREP_INTERRUPT未设置。
调试方法:
-
检查中断统计:
cat /proc/interrupts | grep pl330,看触发次数是否正确。 -
使用
bpftrace动态追踪中断处理程序:bpftrace -e 'kprobe:pl330_irq_handler { @start[tid]=nsecs; } kretprobe:pl330_irq_handler { if (retval != IRQ_HANDLED) printf("IRQ not handled\n"); }' -
检查 DMA 核心的事件:
trace-cmd record -e dma:* -e irq:irq_handler_entry trace-cmd report | grep "pl330"
3.4 描述符链式传输(Cyclic/Scatter-Gather)问题
现象:DMA 只传输了第一个 sg 块,剩余数据丢失,或传输中断在中间停止。
原因:没有正确设置 next 描述符的硬件地址,或者 config 中没有正确配置链式操作模式。
调试方法:
-
检查配置:在
pl330_hw_start_transfer中,确认NEXT寄存器被写入下一个描述符的物理地址。 -
使用
perf跟踪传输回调:perf record -e dma:* -e dma:dma_tx_status -a -- sleep 10 perf script | grep -E "pl330|tx_status"
看何时返回完成状态。
第四章:结合性能调试场景示例
场景:高速 SPI Flash 写入时,写入性能低于理论值(例如实际速度只有理论峰值的一半),同时系统 iowait 高。
分析流程:
-
宏观观察(图谱的 CPU 和 Memory 层):
top显示iowait较高;iostat -x 1显示磁盘设备(实际是 Flash)利用率高但吞吐量低。 -
查看 SPI 传输(设备驱动层):使用
trace-cmd记录 SPI 传输:trace-cmd record -e spi:spi_transfer -e spi:spi_sync trace-cmd report
发现每次 SPI 传输只发送 256 字节,大量时间浪费在 SPI 核心层对
spi_message的拆分和处理上。 -
查看 DMA 使用情况(DMA 控制器层):
perf stat -e dma:* -a -- timeout 10
发现 DMA 中断触发次数极高,与 SPI 传输次数匹配,意味着每个 SPI 传输都引发了中断,CPU 频繁处理中断上下文切换。
-
根本原因:SPI 驱动没有利用 DMA 的
scatter-gather功能,而是逐个传输小数据块。每次传输后 DMA 完成中断会唤醒 CPU,导致上下文切换开销。 -
解决方案:
-
配置 SPI 驱动使用
dmaengine_prep_slave_sg将多个小 buffer 合并为一次 DMA 传输。 -
将 DMA 中断的聚合超时时间调大(如果支持
DMA_INTERRUPT_AGGREGATION)。 -
调整 DMA 的
burst length提高总线利用率。
-
第五章:与其他控制器的协同
DMA 最核心的作用就是为其他控制器提供数据搬运服务。以下是一些典型的依赖关系:
| 控制器 | DMA 用途 | 调试关键点 |
|---|---|---|
| SPI | 批量读写 Flash、传感器 | DMA 通道与 SPI 控制器匹配;DMA 缓冲区与 SPI 缓冲区的一致性 |
| I2C | 高速传输 EEPROM、Camera 数据 | I2C 中断与 DMA 完成中断的协同;DMA 传输长度边界 |
| UART | 高波特率数据收发 | DMA 接收时处理 DMA_RX_ERR;UART 硬件流控与 DMA 的配合 |
| Audio | I2S/PCM 音频流 | DMA 循环模式(Cyclic)设置;双缓冲避免 XRUN |
| Camera | 接收 MIPI/CSI 数据 | DMA 重映射(remap)大量物理不连续的缓冲区到连续虚拟地址 |
调试复合场景时,建议:
-
先验证 DMA 本身:使用
dmatest模块(CONFIG_DMATEST)测试 DMA 通道基本功能。 -
查看 DMA 调试信息:
ls /sys/kernel/debug/dma/dma-*获取每个通道的当前状态。 -
确认 DMA 配置:DMA 的
config中的slave_id必须与外设的 DMA 请求号匹配(查看 SoC 手册)。
第十一部分 音频控制器(I2S)
第一章:音频控制器在 Platform Bus 中的位置
音频控制器(通常以 I2S/PCM 接口形式存在)作为 Platform 设备挂载在总线中。它负责将内存中的音频数据(PCM 格式)通过 I2S 总线发送到外部 Codec 或扬声器,或者从麦克风接收音频数据。
Linux 音频子系统(ALSA/ASoC)的层级结构:
-
ASoC 核心层 (
sound/soc/soc-core.c):管理音频设备和 DAI 链路。 -
CPU DAI 驱动(即本篇文章重点):SoC 内部的 I2S 控制器驱动。
-
Codec DAI 驱动:外部音频 Codec 芯片(如 WM8960, CS42L51)。
-
Machine 驱动:将 CPU DAI 和 Codec DAI 链接在一起。
第二章:Linux 5.10 典型音频控制器驱动 —— fsl_ssi.c
Freescale SSI (Synchronous Serial Interface) 是 i.MX 系列 SoC 中的经典 I2S 控制器。以下代码基于 Linux 5.10 sound/soc/fsl/fsl_ssi.c,展示核心 DMA 和 I2S 配置逻辑。
2.1 硬件关键寄存器
-
SSIx_CR:控制寄存器(发送/接收使能、同步/异步、网络模式)。 -
SSIx_SR:状态寄存器(TX/RX FIFO 状态、中断标志)。 -
SSIx_DR:数据寄存器。 -
SSIx_CCR:时钟控制寄存器(主从模式、时钟源、分频)。 -
SSIx_ISR:中断状态寄存器。
2.2 核心代码
// 基于 Linux 5.10 sound/soc/fsl/fsl_ssi.c (简化版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <sound/soc.h>
#include <sound/dmaengine_pcm.h>
#include <linux/platform_device.h>
/**
* @brief FSL SSI 音频控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 Audio Controller 硬件实例。
*/
struct fsl_ssi {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< I2S 中断号 */
struct clk *clk; /**< 音频主时钟 */
struct snd_soc_dai_driver dai_drv; /**< DAI 驱动抽象,供 ASoC 调用 */
struct snd_dmaengine_dai_dma_data dma_params_tx; /**< 发送 DMA 参数 */
struct snd_dmaengine_dai_dma_data dma_params_rx; /**< 接收 DMA 参数 */
bool playback_active; /**< 播放是否激活 */
bool capture_active; /**< 录音是否激活 */
spinlock_t lock; /**< 保护寄存器的自旋锁 */
};
/* 寄存器偏移量(节选) */
#define SSI_CR 0x00
#define SSI_IMR 0x04
#define SSI_DR 0x08
#define SSI_SR 0x0C
#define SSI_CCR 0x10
#define SSI_ISR 0x14
/**
* @brief 配置 I2S 时钟。
*
* @param ssi 指向 fsl_ssi 结构。
* @param rate 采样率(如 44100, 48000)。
* @param fmt 音频格式(I2S, left-justified 等)。
* @return 0 成功,负数错误。
*/
static int fsl_ssi_set_dai_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
struct fsl_ssi *ssi = snd_soc_dai_get_drvdata(dai);
u32 cr = readl(ssi->base + SSI_CR);
// 1. 清除相关配置位
cr &= ~(0x03 << 4); // 清除 I2S 模式配置
// 2. 设置 DAI 格式 (I2S, LSB, MSB 等)
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
case SND_SOC_DAIFMT_I2S:
cr |= (0x01 << 4); // 设置为 I2S 模式
break;
case SND_SOC_DAIFMT_LEFT_J:
cr |= (0x02 << 4); // 左对齐
break;
// ... 其他格式
}
// 3. 设置主从模式
switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
case SND_SOC_DAIFMT_CBM_CFM: // Codec 作为主时钟
cr |= (1 << 7); // 外部时钟模式
break;
case SND_SOC_DAIFMT_CBS_CFS: // SSI 作为主时钟
cr &= ~(1 << 7);
break;
}
writel(cr, ssi->base + SSI_CR);
return 0;
}
/**
* @brief 配置采样率和位宽。
*
* @param dai 指向 DAI。
* @param rate 采样率。
* @param format 位宽(16/24/32 位)。
* @return 0 成功。
*/
static int fsl_ssi_set_dai_fmt_rate(struct snd_soc_dai *dai, unsigned int rate,
unsigned int format)
{
struct fsl_ssi *ssi = snd_soc_dai_get_drvdata(dai);
u32 cr = readl(ssi->base + SSI_CR);
u32 ccr = readl(ssi->base + SSI_CCR);
unsigned long clk_rate;
u32 div;
// 1. 计算时钟分频(根据主时钟频率和采样率)
clk_rate = clk_get_rate(ssi->clk);
div = clk_rate / (rate * 32 * 2); // 假设 32 帧/周期,双声道
if (div < 1) div = 1;
if (div > 256) div = 256;
// 2. 设置分频器
ccr &= ~0xFFFF;
ccr |= div;
writel(ccr, ssi->base + SSI_CCR);
// 3. 设置位宽
cr &= ~(0x03 << 8);
switch (format) {
case SNDRV_PCM_FORMAT_S16_LE:
cr |= (0x00 << 8); // 16 位
break;
case SNDRV_PCM_FORMAT_S24_LE:
cr |= (0x01 << 8); // 24 位
break;
case SNDRV_PCM_FORMAT_S32_LE:
cr |= (0x02 << 8); // 32 位
break;
}
writel(cr, ssi->base + SSI_CR);
return 0;
}
/**
* @brief 触发音频传输(启动/停止)。
*
* @param dai 指向 DAI。
* @param cmd 触发命令(SNDRV_PCM_TRIGGER_START/STOP)。
* @param stream 播放或录音。
* @return 0 成功。
*/
static int fsl_ssi_trigger(struct snd_soc_dai *dai, int cmd, int stream)
{
struct fsl_ssi *ssi = snd_soc_dai_get_drvdata(dai);
unsigned long flags;
u32 cr;
spin_lock_irqsave(&ssi->lock, flags);
cr = readl(ssi->base + SSI_CR);
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
if (stream == SNDRV_PCM_STREAM_PLAYBACK) {
cr |= (1 << 2); // 使能发送 TE
cr |= (1 << 6); // 使能发送中断
ssi->playback_active = true;
} else {
cr |= (1 << 3); // 使能接收 RE
cr |= (1 << 7); // 使能接收中断
ssi->capture_active = true;
}
break;
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
if (stream == SNDRV_PCM_STREAM_PLAYBACK) {
cr &= ~(1 << 2);
cr &= ~(1 << 6);
ssi->playback_active = false;
} else {
cr &= ~(1 << 3);
cr &= ~(1 << 7);
ssi->capture_active = false;
}
break;
default:
spin_unlock_irqrestore(&ssi->lock, flags);
return -EINVAL;
}
writel(cr, ssi->base + SSI_CR);
spin_unlock_irqrestore(&ssi->lock, flags);
return 0;
}
/**
* @brief 中断处理函数。
*
* 处理传输完成、FIFO 空/满、错误等事件。
*
* @param irq 中断号。
* @param dev_id 指向 fsl_ssi 结构。
*/
static irqreturn_t fsl_ssi_irq_handler(int irq, void *dev_id)
{
struct fsl_ssi *ssi = dev_id;
u32 isr, imr;
// 1. 读取中断状态和掩码
isr = readl(ssi->base + SSI_ISR);
imr = readl(ssi->base + SSI_IMR);
isr &= imr;
// 2. 处理发送完成中断(当 TX FIFO 为空时触发)
if (isr & (1 << 6)) {
// 写 1 清除中断标志
writel(1 << 6, ssi->base + SSI_ISR);
// 如果还有数据要发送(DMA 会持续填充),否则通知上层
if (!ssi->playback_active) {
// 通知音频核心层
}
}
// 3. 处理接收中断(当 RX FIFO 有数据时触发)
if (isr & (1 << 7)) {
writel(1 << 7, ssi->base + SSI_ISR);
// 读取数据,DMA 通常会处理
}
// 4. 处理错误中断(FIFO 溢出/下溢)
if (isr & (1 << 2)) { // TX 下溢
dev_err(ssi->dai_drv.dev, "Tx Underflow\n");
writel(1 << 2, ssi->base + SSI_ISR);
}
if (isr & (1 << 3)) { // RX 溢出
dev_err(ssi->dai_drv.dev, "Rx Overflow\n");
writel(1 << 3, ssi->base + SSI_ISR);
}
return IRQ_HANDLED;
}
/**
* @brief Platform 探测函数。
*
* 初始化 I2S 控制器硬件,注册到 ASoC 框架。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int fsl_ssi_probe(struct platform_device *pdev)
{
struct fsl_ssi *ssi;
struct resource *res;
int ret, irq;
// 1. 分配私有数据
ssi = devm_kzalloc(&pdev->dev, sizeof(*ssi), GFP_KERNEL);
if (!ssi)
return -ENOMEM;
platform_set_drvdata(pdev, ssi);
// 2. 获取并映射 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
ssi->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(ssi->base))
return PTR_ERR(ssi->base);
// 3. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
ssi->irq = irq;
// 4. 获取时钟
ssi->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(ssi->clk)) {
dev_err(&pdev->dev, "Failed to get clock\n");
return PTR_ERR(ssi->clk);
}
clk_prepare_enable(ssi->clk);
spin_lock_init(&ssi->lock);
// 5. 设置 DMA 参数(供 dmaengine_pcm 使用)
ssi->dma_params_tx.maxburst = 16;
ssi->dma_params_tx.addr = res->start + SSI_DR;
ssi->dma_params_tx.addr_width = 4; // 32 位
ssi->dma_params_rx.maxburst = 16;
ssi->dma_params_rx.addr = res->start + SSI_DR;
ssi->dma_params_rx.addr_width = 4; // 32 位
// 6. 初始化 DAI 驱动
ssi->dai_drv = (struct snd_soc_dai_driver) {
.name = "fsl-ssi",
.playback = {
.stream_name = "Playback",
.channels_min = 2,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_48000,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
.capture = {
.stream_name = "Capture",
.channels_min = 2,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_48000,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
},
.ops = &fsl_ssi_dai_ops,
};
ssi->dai_drv.dev = &pdev->dev;
// 7. 注册 DMA(音频 PCM 设备使用 DMA 进行数据传输)
ret = devm_snd_soc_register_component(&pdev->dev,
&fsl_ssi_component, &ssi->dai_drv, 1);
if (ret) {
dev_err(&pdev->dev, "Failed to register DAI\n");
return ret;
}
// 8. 注册中断
ret = devm_request_irq(&pdev->dev, ssi->irq, fsl_ssi_irq_handler,
IRQF_SHARED, "fsl_ssi", ssi);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
dev_info(&pdev->dev, "FSL SSI audio controller registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, ssi->irq);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int fsl_ssi_remove(struct platform_device *pdev)
{
struct fsl_ssi *ssi = platform_get_drvdata(pdev);
// 1. 禁用 I2S 控制器
writel(0, ssi->base + SSI_CR);
// 2. 禁用时钟
clk_disable_unprepare(ssi->clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id fsl_ssi_of_match[] = {
{ .compatible = "fsl,imx6-ssi" },
{ .compatible = "fsl,imx7d-ssi" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, fsl_ssi_of_match);
/* Platform 驱动结构 */
static struct platform_driver fsl_ssi_driver = {
.probe = fsl_ssi_probe,
.remove = fsl_ssi_remove,
.driver = {
.name = "fsl-ssi",
.of_match_table = fsl_ssi_of_match,
},
};
module_platform_driver(fsl_ssi_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Freescale SSI I2S Audio Controller Driver");
MODULE_LICENSE("GPL v2");
第三章:音频控制器调试核心难点
3.1 音频流同步(Clock Synchronization)
现象:播放音频时出现噪声、爆音(pop-click)或节奏偏差。
原因:I2S 总线时钟(BCLK)和帧时钟(LRCLK)与 Codec 不匹配,导致采样率不一致。
调试方法:
-
使用
alsa-utils检查硬件参数:aplay -l # 列出音频设备 cat /proc/asound/card0/stream0 # 查看当前流状态
-
查看时钟配置:
cat /sys/kernel/debug/clk/clk_summary | grep ssi
确认
ssi_clk实际频率是否符合预期。 -
使用
perf跟踪音频 DMA 中断:perf record -e dma:dma_* -a -- timeout 10 perf script | grep ssi
3.2 音频卡顿(XRUN — Under/Overflow)
现象:播放音频时出现周期性卡顿或杂音,CPU 占用率瞬间飙升。
原因:DMA 传输来不及填充或读取数据,导致 FIFO 下溢(播放)或上溢(录音)。
调试方法:
-
使用
dmesg查看 XRUN 信息:dmesg | grep "XRUN"
-
增加音频缓冲大小:
cat /proc/asound/card0/pcm0p/period_size echo 8192 > /proc/asound/card0/pcm0p/period_size # 调整
-
检查 DMA 优先级:
-
查看 DMA 通道是否被高优先级中断抢占。
-
使用
perf top -C 0看 DMA 中断处理是否耗时过高。
-
3.3 录音无声音或声音异常
现象:录音设备能打开,但录制的数据全是 0 或杂音。
原因:
-
I2S 方向配置错误(接收未使能)。
-
GPIO/引脚复用(PinMux)错误,导致音频信号无法到达 CPU。
-
Codec 供电或配置错误。
调试方法:
-
查看 GPIO 状态:
cat /sys/kernel/debug/pinctrl/pinctrl-handles | grep i2s
-
检查 AlsaMixer 音量:
amixer scontrols amixer sset 'Capture Volume' 80%
-
使用
arecord抓取原始数据测试:arecord -d 5 -f cd -t raw /tmp/audio.raw od -x /tmp/audio.raw | head -20 # 查看是否有实际数据
第四章:结合性能调试场景示例
场景:在嵌入式板上使用 ALSA 播放 MP3 时,播放 3 分钟后出现爆音,然后完全无声,top 显示 kworker CPU 占用接近 100%。
分析流程:
-
宏观层面(CPU 和 Memory 层):
-
top显示kworker/u:4占用 CPU 较高。 -
perf top显示fsl_ssi_irq_handler和dma_async_memcpy_buf_to_buf占用高。
-
-
追踪 DMA 中断(DMA 控制器层):
perf record -e dma:dma_* -a -- timeout 5 perf script | grep fsl_ssi
发现 DMA 中断频率很高,同时
dma_sync_single_for_device调用频繁。 -
查看音频缓冲状态(音频核心层):
cat /proc/asound/card0/pcm0p/period_size cat /proc/asound/card0/pcm0p/period_bytes
发现
period_size设置太小(128 帧),导致中断频率过高,kworker抢占 CPU。 -
根本原因:音频应用配置的
period_size过小,DMA 每次传输 128 帧(约 2.9ms 数据)产生一次中断,kworker 忙于处理中断上下文切换。 -
解决方案:
-
增加
period_size到 1024 或 2048 帧。 -
在 ALSA 应用层配置:
snd_pcm_hw_params_set_period_size(handle, params, 1024); -
验证后,爆音消失,CPU 占用降至正常。
-
第五章:与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| DMA | 音频数据传输的核心,搬运 PCM 数据 | DMA 通道分配、缓冲区循环模式 |
| I2C | 用于配置外部 Codec 寄存器 | 确保 Codec 上电,I2C 可访问 |
| GPIO | 控制 Codec 的复位引脚、电源使能 | GPIO 方向与电平,上电序列 |
| PWM | 用于耳机插孔检测或 LED 指示 | PWM 频率匹配 |
| PMIC | 提供音频电源和时钟 | 电源稳定,无纹波 |
第十二部分 Camera 控制器(CSI)
第一章:Camera 控制器在 Platform Bus 中的位置
Camera 控制器 是一个挂载在 Platform Bus 上的 SoC 内部硬件模块。它主要负责接收来自外部图像传感器(如 CMOS 传感器)的数据流,通常通过 MIPI CSI-2(最常用)或 并行接口(Parallel Camera Interface) 连接。
Linux 中 Camera 子系统基于 V4L2 (Video for Linux 2),它的架构分为三层:
-
V4L2 核心层(
drivers/media/v4l2-core/):提供统一的 API,管理视频设备、buffers、controls 和 events。 -
CSI 主机控制器驱动(即本篇文章重点):负责接收原始图像数据,解析 MIPI D-PHY 信号,将数据通过 DMA 搬运到内存。
-
传感器驱动(Subdev):负责配置传感器(I2C/SPI 访问),设置曝光、增益、分辨率、帧率等。
第二章:Linux 5.10 典型 CSI 控制器驱动 —— rockchip-cif.c
Rockchip CIF (Camera Interface) 是 RK3399 / RK3568 等 SoC 上广泛使用的 CSI 控制器,支持 MIPI CSI-2 和并行接口。以下代码基于 Linux 5.10 drivers/media/platform/rockchip/cif/,展示核心逻辑。
2.1 硬件关键概念
-
MIPI D-PHY:物理层,负责高速串行数据接收(每通道 1Gbps+)。
-
CSI-2 协议:包含短包(帧开始/结束、行开始/结束)和长包(图像数据)。
-
DMA 引擎:将接收的 MIPI 数据流打包成连续的帧,写入预分配的内存缓冲区。
-
中断:帧开始(SoF)、帧结束(EoF)、DMA 错误(总线错误、缓冲区溢出)。
2.2 核心代码
// 基于 Linux 5.10 drivers/media/platform/rockchip/cif/cif.c (简化)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/videobuf2-dma-contig.h>
/**
* @brief Rockchip CIF 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 Camera Controller 硬件实例。
*/
struct rk_cif_dev {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< CSI 中断号 */
struct clk *pclk; /**< 控制器时钟 */
struct clk *aclk; /**< 总线时钟 */
struct v4l2_device v4l2_dev; /**< V4L2 设备抽象 */
struct video_device *vdev; /**< 视频设备节点 /dev/videoX */
struct vb2_queue queue; /**< 视频缓冲区队列 */
struct list_head buffers; /**< 已入队的缓冲区链表 */
spinlock_t lock; /**< 保护硬件和缓冲区的锁 */
struct rk_cif_buffer *curr_buf; /**< 当前正在填充的缓冲区 */
struct rk_cif_sensor_info sensor; /**< 当前连接的传感器信息 */
};
/* 寄存器偏移量(节选) */
#define CIF_CTRL 0x00 // 控制寄存器
#define CIF_IMR 0x04 // 中断掩码
#define CIF_ISR 0x08 // 中断状态
#define CIF_DATA_BASE 0x10 // 帧缓冲基址寄存器
#define CIF_DATA_LEN 0x14 // 缓冲区长度
#define CIF_MIPI_CTRL 0x30 // MIPI 控制
#define CIF_MIPI_STATUS 0x34 // MIPI 状态
/**
* @brief 配置 CSI 接口(MIPI 或并行)。
*
* @param csi 指向 rk_cif_dev 结构。
* @param mode 接口模式(MIPI / Parallel)。
* @param lanes MIPI 通道数(1/2/4)。
*/
static void rk_cif_set_interface(struct rk_cif_dev *csi, int mode, int lanes)
{
u32 val = readl(csi->base + CIF_CTRL);
if (mode == 0) {
// 并行接口
val &= ~(1 << 8);
writel(val, csi->base + CIF_CTRL);
} else {
// MIPI CSI-2 接口
val |= (1 << 8);
writel(val, csi->base + CIF_CTRL);
// 设置 MIPI 通道数
val = readl(csi->base + CIF_MIPI_CTRL);
val &= ~(0x03 << 4);
val |= (lanes & 0x03) << 4;
writel(val, csi->base + CIF_MIPI_CTRL);
}
}
/**
* @brief 初始化 DMA 缓冲区并将物理地址写入硬件。
*
* @param csi 指向 rk_cif_dev。
* @param buf 指向 RK CIF 缓冲区结构。
*/
static void rk_cif_set_buffer(struct rk_cif_dev *csi,
struct rk_cif_buffer *buf)
{
dma_addr_t dma_addr = buf->dma_addr;
size_t len = buf->length;
// 1. 写入缓冲区基址(硬件要求 64 位地址,这里简化)
writel(lower_32_bits(dma_addr), csi->base + CIF_DATA_BASE);
writel(upper_32_bits(dma_addr), csi->base + CIF_DATA_BASE + 4);
// 2. 写入缓冲区长度
writel(len, csi->base + CIF_DATA_LEN);
}
/**
* @brief 启动图像采集(硬件 streaming)。
*
* @param csi 指向 rk_cif_dev。
*/
static void rk_cif_start_streaming(struct rk_cif_dev *csi)
{
u32 ctrl, imr;
// 1. 检查是否有可用缓冲区
if (list_empty(&csi->buffers)) {
dev_err(csi->v4l2_dev.dev, "No buffers available for streaming\n");
return;
}
// 2. 取出第一个缓冲区用于 DMA
csi->curr_buf = list_first_entry(&csi->buffers,
struct rk_cif_buffer, list);
list_del_init(&csi->curr_buf->list);
rk_cif_set_buffer(csi, csi->curr_buf);
// 3. 启用中断
imr = readl(csi->base + CIF_IMR);
imr |= (1 << 4); // 帧结束中断 Enable
imr |= (1 << 5); // 帧开始中断 Enable
imr |= (1 << 7); // DMA 错误中断 Enable
writel(imr, csi->base + CIF_IMR);
// 4. 启动 MIPI 接收
ctrl = readl(csi->base + CIF_CTRL);
ctrl |= (1 << 0); // 主使能
ctrl |= (1 << 6); // 数据采集使能
writel(ctrl, csi->base + CIF_CTRL);
}
/**
* @brief 停止图像采集。
*
* @param csi 指向 rk_cif_dev。
*/
static void rk_cif_stop_streaming(struct rk_cif_dev *csi)
{
u32 ctrl, imr;
// 1. 禁用中断
imr = readl(csi->base + CIF_IMR);
imr &= ~(1 << 4);
imr &= ~(1 << 5);
imr &= ~(1 << 7);
writel(imr, csi->base + CIF_IMR);
// 2. 关闭硬件
ctrl = readl(csi->base + CIF_CTRL);
ctrl &= ~(1 << 0);
ctrl &= ~(1 << 6);
writel(ctrl, csi->base + CIF_CTRL);
}
/**
* @brief CSI 中断处理函数。
*
* 处理 DMA 完成、帧结束、错误等事件。
*
* @param irq 中断号。
* @param dev_id 指向 rk_cif_dev 结构。
*/
static irqreturn_t rk_cif_irq_handler(int irq, void *dev_id)
{
struct rk_cif_dev *csi = dev_id;
u32 isr, imr, status;
int handled = 0;
// 1. 读取中断状态
isr = readl(csi->base + CIF_ISR);
imr = readl(csi->base + CIF_IMR);
isr &= imr; // 仅处理已使能的中断
// 2. 帧结束中断(最重要)
if (isr & (1 << 4)) {
// 写 1 清除中断标志
writel(1 << 4, csi->base + CIF_ISR);
// 完成当前缓冲区
if (csi->curr_buf) {
struct rk_cif_buffer *buf = csi->curr_buf;
// 标记缓冲区完成
buf->vbuf.vb2_buf.timestamp = ktime_get_ns();
buf->vbuf.sequence = csi->sequence++;
vb2_buffer_done(&buf->vbuf.vb2_buf, VB2_BUF_STATE_DONE);
}
// 取出下一个缓冲区
if (!list_empty(&csi->buffers)) {
csi->curr_buf = list_first_entry(&csi->buffers,
struct rk_cif_buffer, list);
list_del_init(&csi->curr_buf->list);
rk_cif_set_buffer(csi, csi->curr_buf);
} else {
// 没有缓冲区可用,暂时停止采集
csi->curr_buf = NULL;
// 可以选择将硬件停止,或者继续丢弃数据(取决于应用)
// rk_cif_stop_streaming(csi);
}
handled = 1;
}
// 3. DMA 错误中断
if (isr & (1 << 7)) {
dev_err(csi->v4l2_dev.dev, "DMA error (bus error or overflow)\n");
writel(1 << 7, csi->base + CIF_ISR);
// 重置 DMA 引擎
// rk_cif_reset_dma(csi);
handled = 1;
}
// 4. MIPI 状态错误
if (isr & (1 << 1)) {
status = readl(csi->base + CIF_MIPI_STATUS);
dev_err(csi->v4l2_dev.dev, "MIPI error: status=0x%x\n", status);
writel(1 << 1, csi->base + CIF_ISR);
// 读取 MIPI 状态寄存器以清除错误
handled = 1;
}
return handled ? IRQ_HANDLED : IRQ_NONE;
}
/**
* @brief 缓冲区队列操作:将应用程序的缓冲区加入硬件处理队列。
*
* @param vb 指向 vb2_buffer 结构。
*/
static int rk_cif_queue_setup(struct vb2_queue *q, unsigned int *num_buffers,
unsigned int *num_planes, unsigned int sizes[],
struct device *alloc_devs[])
{
// 简单的单平面配置
if (*num_planes)
return 0;
*num_planes = 1;
sizes[0] = 1920 * 1080 * 4; // 假设 1920x1080 NV12
return 0;
}
/**
* @brief 将缓冲区的物理地址关联到 DMA 驱动。
*/
static int rk_cif_buf_prepare(struct vb2_buffer *vb)
{
struct rk_cif_dev *csi = vb2_get_drv_priv(vb->vb2_queue);
struct rk_cif_buffer *buf = container_of(vb, struct rk_cif_buffer, vbuf);
buf->dma_addr = vb2_dma_contig_plane_dma_addr(vb, 0);
buf->length = vb2_plane_size(vb, 0);
return 0;
}
/**
* @brief 应用程序将缓冲区入队时调用。
*/
static void rk_cif_buf_queue(struct vb2_buffer *vb)
{
struct rk_cif_dev *csi = vb2_get_drv_priv(vb->vb2_queue);
struct rk_cif_buffer *buf = container_of(vb, struct rk_cif_buffer, vbuf);
unsigned long flags;
spin_lock_irqsave(&csi->lock, flags);
list_add_tail(&buf->list, &csi->buffers);
spin_unlock_irqrestore(&csi->lock, flags);
// 如果硬件空闲且没有当前 buffer,立即开始
if (!csi->curr_buf && vb2_start_streaming_called(vb->vb2_queue)) {
rk_cif_start_streaming(csi);
}
}
/**
* @brief Platform 探测函数。
*
* 初始化 CSI 控制器硬件,注册到 V4L2 框架。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rk_cif_probe(struct platform_device *pdev)
{
struct rk_cif_dev *csi;
struct resource *res;
struct video_device *vdev;
int ret, irq;
// 1. 分配私有数据结构
csi = devm_kzalloc(&pdev->dev, sizeof(*csi), GFP_KERNEL);
if (!csi)
return -ENOMEM;
platform_set_drvdata(pdev, csi);
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENXIO;
csi->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(csi->base))
return PTR_ERR(csi->base);
// 3. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
csi->irq = irq;
// 4. 获取时钟
csi->pclk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(csi->pclk))
return PTR_ERR(csi->pclk);
csi->aclk = devm_clk_get(&pdev->dev, "aclk");
if (IS_ERR(csi->aclk))
return PTR_ERR(csi->aclk);
clk_prepare_enable(csi->pclk);
clk_prepare_enable(csi->aclk);
spin_lock_init(&csi->lock);
INIT_LIST_HEAD(&csi->buffers);
// 5. 初始化 V4L2 设备
ret = v4l2_device_register(&pdev->dev, &csi->v4l2_dev);
if (ret) {
dev_err(&pdev->dev, "Failed to register V4L2 device\n");
return ret;
}
// 6. 初始化 VB2 队列
csi->queue = (struct vb2_queue) {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF,
.ops = &rk_cif_qops,
.mem_ops = &vb2_dma_contig_memops,
.drv_priv = csi,
.buf_struct_size = sizeof(struct rk_cif_buffer),
.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC,
};
ret = vb2_queue_init(&csi->queue);
if (ret) {
dev_err(&pdev->dev, "Failed to init vb2 queue\n");
return ret;
}
// 7. 注册视频设备
vdev = video_device_alloc();
if (!vdev) {
dev_err(&pdev->dev, "Failed to alloc video device\n");
return -ENOMEM;
}
csi->vdev = vdev;
vdev->v4l2_dev = &csi->v4l2_dev;
vdev->queue = &csi->queue;
vdev->fops = &rk_cif_fops;
vdev->ioctl_ops = &rk_cif_ioctl_ops;
vdev->release = video_device_release;
snprintf(vdev->name, sizeof(vdev->name), "rk-cif");
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
dev_err(&pdev->dev, "Failed to register video device\n");
video_device_release(vdev);
return ret;
}
// 8. 注册中断
ret = devm_request_irq(&pdev->dev, csi->irq, rk_cif_irq_handler,
IRQF_SHARED, "rk-cif", csi);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
dev_info(&pdev->dev, "Rockchip CIF camera controller registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, csi->irq);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rk_cif_remove(struct platform_device *pdev)
{
struct rk_cif_dev *csi = platform_get_drvdata(pdev);
// 1. 停止采集
rk_cif_stop_streaming(csi);
// 2. 注销视频设备
video_unregister_device(csi->vdev);
// 3. 禁用时钟
clk_disable_unprepare(csi->pclk);
clk_disable_unprepare(csi->aclk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rk_cif_of_match[] = {
{ .compatible = "rockchip,rk3399-cif" },
{ .compatible = "rockchip,rk3568-cif" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rk_cif_of_match);
/* Platform 驱动结构 */
static struct platform_driver rk_cif_driver = {
.probe = rk_cif_probe,
.remove = rk_cif_remove,
.driver = {
.name = "rk-cif",
.of_match_table = rk_cif_of_match,
},
};
module_platform_driver(rk_cif_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip Camera Interface Driver");
MODULE_LICENSE("GPL v2");
第三部分:Camera 调试核心难点
3.1 MIPI D-PHY 信号完整性
现象:摄像头无法采集图像,或者画面出现噪声、雪花、垂直条纹。
原因:MIPI 物理层信号质量差(走线过长、阻抗不匹配、干扰)。
调试方法:
-
检查 MIPI 时钟和通道(lane)映射:
-
确认 DTS 中的
lane配置与硬件 PCB 一致。 -
使用示波器或逻辑分析仪查看 MIPI 时钟是否稳定。
-
-
调整 D-PHY 驱动强度:某些驱动支持通过寄存器微调驱动电流。
-
降低 MIPI 速率:在传感器驱动中降低 MIPI 频率(如 1Gbps -> 800Mbps)。
3.2 DMA 缓冲区不足或映射失败
现象:VIDIOC_QBUF 入队成功,但 VIDIOC_DQBUF 无数据返回,dmesg 出现 "DMA error"。
原因:DMA 无法将数据写入缓冲区的物理地址,或者缓冲区太小。
调试方法:
-
检查缓冲区大小:
cat /sys/kernel/debug/dma/dma-*查看 DMA 分配情况。 -
强制分配连续内存:确保
CONFIG_DMA_CMA启用且分配足够大(例如 64MB)。 -
使用
strace跟踪用户空间:strace -e trace=ioctl v4l2-ctl -d /dev/video0 --stream-mmap --stream-to=/tmp/out.raw
查看
VB2_BUF_FLAG_DONE何时被设置。
3.3 传感器配置与初始化失败
现象:v4l2-ctl --all 显示支持的格式正确,但启动流后立即停止。
原因:传感器驱动(subdev)未能正确设置分辨率、格式或曝光。
调试方法:
-
使用
media-ctl检查管道:media-ctl -p media-ctl -v -V "\"Sensor\":0[fmt:UYVY8_1X16/1920x1080]"
-
捕获 I2C 通信:如果传感器通过 I2C 配置,使用
i2cdetect确认地址正确。 -
查看传感器驱动日志:
cat /sys/kernel/debug/dynamic_debug/control | grep sensor_name echo 'file sensor_name.c +p' > /sys/kernel/debug/dynamic_debug/control
3.4 帧率不稳定(丢帧)
现象:采集到的视频有卡顿或跳帧,v4l2-ctl --stream-mmap --count=100 实际收到的帧少于预期。
原因:DMA 中断处理慢,或者应用程序来不及入队新缓冲区。
调试方法:
-
使用
perf监控中断处理延迟:perf record -e irq:* -e dma:dma_complete -a -- sleep 5 perf script | grep cif
-
增加缓冲池深度:在用户空间
v4l2-ctl或应用中使用set buffer count(例如 4 或 6)。 -
优先处理中断:使用
sched_setscheduler将摄像头进程设为实时优先级。
第四章:结合性能调试场景示例
场景:嵌入式系统上运行 GStreamer 管道采集高清(1080p)视频,3 分钟后系统响应变慢,top 显示 irq/XX-cif 占用 CPU 很高,视频出现间歇性跳帧。
分析流程:
-
宏观层面(图谱的 CPU 层):
-
top显示irq/XX-cif占用 CPU 约 30%-40%。 -
perf top -G显示rk_cif_irq_handler在dma_sync_single_for_cpu上花费大量时间。
-
-
查看中断统计(设备驱动层):
cat /proc/interrupts | grep cif
发现中断触发频率非常高(每秒约 120 次,符合 60fps * 2 帧开始/结束),但每次 ISR 执行时间长。
-
分析中断处理(图谱的 Device Drivers -> CSI Controller):
-
使用
bpftrace动态插桩:
bpftrace -e 'kprobe:rk_cif_irq_handler { @start[tid]=nsecs; } kretprobe:rk_cif_irq_handler { if (nsecs - @start[tid] > 100000) { printf("Slow IRQ: %d us\n", (nsecs - @start[tid]) / 1000); } }'发现 ISR 耗时经常超过 150 微秒(对于 16.6ms 的帧周期来说较长)。
-
-
根本原因:中断处理程序中进行了不必要的 DMA 同步操作。每次
vb2_buffer_done触发前,驱动程序调用了dma_sync_single_for_cpu,这涉及大量的缓存刷新操作。 -
解决方案:
-
使用
dma_alloc_coherent替代dma_alloc_writecombine(减少同步开销)。 -
在 ISR 中仅记录完成事件,将实际数据同步推迟到
DQBUF时。 -
最终优化:将中断合并模式(Interrupt coalescing)调大,改为每 4 帧触发一次中断,而不是每帧。
-
第五部分:与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| MIPI D-PHY | 物理层接收器,集成在 CSI 控制器内 | 信号质量、通道映射、驱动强度 |
| DMA | 将 MIPI 数据写入内存 | 连续物理内存、缓冲区对齐、中断聚合 |
| I2C | 配置传感器寄存器 | 确保地址正确,I2C 时序匹配 |
| GPIO | 传感器复位、电源使能、中断触发 | 正确时序(上电 -> 复位 -> 初始化) |
| PMIC | 提供传感器电源 | 电压稳定,无噪声 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)