第一部分:总线的概念与 Linux 的“虚实”之道

1.1 什么是总线(Bus)?

总线的本质:同类设备约定共同遵循的通讯协议与时序约束

  • 物理层:电压逻辑电平、高低电平维持时间。

  • 链路层:命令/数据的格式封装。

  • 本质:总线是一种抽象,它定义了 CPU 与外部设备如何进行“对话”。

1.2 Linux 系统总线的分类(虚实结合)

Linux 驱动模型将总线分为两类:

  1. 实际存在的物理总线:I2C、SPI、USB、PCIe 等。这些有明确的电气特性和协议,连接的是外挂的外设(如图中的 Camera、加速度传感器)。

  2. 虚拟总线(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_devicename 字段。只有匹配成功,驱动才会被加载。

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_driverof_match_table 进行匹配。

  • 调试技巧:查看 /sys/firmware/devicetree/base/ 来确认内核是否正确解析了硬件描述。


第四部分:场景调试——当Platform 驱动不起作用时

当编写了一个 platform_driver,编译进内核,却发现 probe 函数没有被调用,或者设备没反应,如何定位?

场景 1:匹配失败(最常见)

现象dmesg 没有任何打印,modprobe 也成功,但 /sys/devices/platform/ 下没有你的设备节点。 调试方法

  1. 运行:cat /sys/bus/platform/devices/*/modalias | grep <设备名>

  2. 检查 platform_driver 中的 driver.nameof_match_table 是否与 DTS 完全一致。

  3. 查看 DTS 是否被内核加载:dtc -I fs /sys/firmware/devicetree/base/

场景 2:中断申请失败

现象probe 执行到 request_irq 时失败,设备报错。 调试方法

  1. 确认中断号:在 DTS 中检查 interrupts 属性是否正确。

  2. 检查中断引脚是否被其他设备(如 GPIO 子系统)占用:cat /proc/interrupts

  3. 使用 devm_request_irq(申请资源自动管理)避免资源泄漏。

场景 3:内存映射导致 Kernel Panic

现象ioremap 之后访问寄存器导致 Oops。 调试方法

  1. 检查 platform_get_resource 返回的 startend 地址是否有效。

  2. 确认是不是在 __init 函数之外访问了 remap 后的地址(非 ioremap 导致的无效访问)。

  3. 使用 perfkprobe 动态跟踪 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 ClockResolutionBpp(色彩深度)。使用 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 卡住。 调试流

  1. 第一层(查看日志)dmesg | grep dw8250,看 probe 是否返回 0。如果 probe 成功了,说明 Platform 匹配没问题。

  2. 第二层(观测硬件状态 - 结合图谱的 Block Device/Drive层):使用 perfftrace 跟踪 dw8250_probe 的执行时间。

    trace-cmd record -e serial:uart_write -e serial:serial_irq -p function_graph -n 100
    trace-cmd report
  3. 第三层(排查中断)

    • 运行 cat /proc/interrupts,检查 dw8250 注册的中断号是否有计数增加。

    • 如果 TX IRQ 不触发,说明可能是 IRQ 资源没映射正确(查 DTS),或者 clk 没使能导致硬件没电。

  4. 极限手段(调试寄存器)

    • 使用 devmem2(用户态工具)直接读取物理地址的寄存器值,看 LSR(Line Status Register)的 THRE(Transmit Holding Register Empty)位是否为 1。如果为 0,硬件永远发不出数据。

场景 2:LCD 屏幕不亮(既无背光也无任何像素,但 DRM 注册成功)

症状:系统启动,/dev/dri/card0 存在,但屏幕全黑。 调试流

  1. 第一层(逻辑流追踪):使用 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_bridgedrm_encoder 处于 disabled 状态,FTrace 一目了然。

  2. 第二层(电源树检查):检查 clkregulator。LCD 需要大量的 Power。使用 cat /sys/kernel/debug/clk/clk_summary 检查 lcdc_clk 的状态是否为 enabled

  3. 第三层(PinMux 检查):查看 dmesg 中有无 pinctrl 相关的 Warning。有可能是 GPIO 引脚被复用到了 UART 或 JTAG 上,导致 LCD 数据线 (RGB/LVDS) 没信号。

  4. 第四层(使用 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 内部提供通用输入输出引脚的硬件模块。它的核心职责是:

  1. 管理引脚方向:输入或输出。

  2. 读写电平:高或低。

  3. 中断能力:检测引脚电平变化(上升沿、下降沿、高电平、低电平),并产生中断通知 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 陷入软中断死循环。

调试方法

  1. 观察 perf

    perf top -e irq:irq_handler_entry

    如果看到 pl061_irq_handler 在列表中占据前几位,说明中断触发频繁。

  2. 检查中断计数

    cat /proc/interrupts | grep pl061

    观察该中断号的后增量是否每秒数千次。

  3. 临时解决方案

    • 在 DTS 中添加 debounce 属性(如果内核支持)。

    • 手动在驱动中使能 GPIO_IBE(双沿触发)配合软件去抖。

3.2 GPIO 引脚被多重复用(PinMux 冲突)

问题现象:设置 GPIO 输出时,电平不变化,或者读到的输入值始终不变(可能被外设拉死)。

调试方法

  1. 检查 pinctrl 状态

    cat /sys/kernel/debug/pinctrl/pinctrl-handles

    查看该引脚是否被某个外设(如 UART、I2C)永久占用为功能引脚。

  2. 手动切换功能

    echo "gpio" > /sys/kernel/debug/pinctrl/xxx/pinmux

3.3 中断号未映射或无法触发(IRQ Domain 问题)

现象:用户层 poll()select() 在 GPIO 中断上永远等待,但实际电平翻转。

调试方法

  1. 验证 IRQ 号是否正确:在 probe 中使用 irq_find_mapping 后打印结果。

  2. 使用 tracepoint

    trace-cmd record -e gpio:* -e irq:irq_handler_exit
    trace-cmd report | grep pl061

    查看是否有 irq_handler_exit 不匹配。


第四章:结合性能图谱的调试场景示例

假设系统发生卡顿,怀疑是某个 GPIO 引发了高频中断。可以按照第一张性能思路层层定位:

  1. 检查系统宏观状态(CPU 与 Memory 层):

    top -H -p <pid>  # 查看哪个 CPU 使用率最高,如果是软中断高,说明中断多。
  2. 详细追踪中断( Interrupt Controller 层):

    perf record -e irq:irq_handler_entry -e irq:irq_handler_exit -a -- sleep 5
    perf script

    找出哪个中断号被频繁调用。

  3. 深入该 GPIO 控制器驱动的内核函数(Device Drivers 层):

    bpftrace -e 'kprobe:pl061_irq_handler /arg1 == <your_irq_number>/ { @count[tid] = count(); }'
  4. 确认硬件层面抖动

    • 用示波器或逻辑分析仪观察 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 子系统分为三层:

  1. I2C 核心层drivers/i2c/i2c-core.c):提供通用 API,管理总线、设备、算法。

  2. I2C 控制器驱动(适配器):由具体的 Platform 驱动实现,负责操作硬件寄存器,执行 I2C 时序。

  3. 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 被该设备拉低,而主机无法释放。

调试方法

  1. 使用 i2ctools 尝试恢复

    echo 0 > /sys/bus/i2c/devices/i2c-<bus>/bus_reset   # 如果支持
  2. 手动复位 I2C 控制器:在 probe 中添加代码,向 IC_CON 写入 0 再写 1 以软复位。

  3. 查看硬件状态寄存器

    devmem2 <base_addr>+0x100  # 某个 SoC 的 I2C 状态寄存器
  4. 暴力解法:如果无法复位,只有重新上电或复位整个 SoC。

3.2 传输超时(Timeout)

现象i2c_readi2c_write 返回 -ETIMEDOUT

可能原因

  • 时钟频率配置错误(HCNT/LCNT 计算错误)。

  • 从设备 NACK(地址错误或设备不存在)。

  • 中断处理程序没有正确唤醒 completion

调试方法

  1. 使用 perf 跟踪中断

    perf record -e irq:irq_handler_entry -e irq:irq_handler_exit -p <irq_number>

    查看中断是否被触发。

  2. 增加调试打印:在 dw_i2c_irq_handler 中打印 stat 值。

  3. 检查波特率:用示波器测量 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 上报触摸坐标。

调试这类复合场景时,建议:

  1. 先独立验证 I2C 总线:使用 i2cdetect 确保所有设备地址可见。

  2. 再验证具体设备:使用 i2ctransfer 发送自定义命令,对比数据手册中的寄存器回复。

  3. 使用 perf 跟踪中断:确认 I2C 中断与其他总线(如 SPI、UART)共享中断线时是否存在优先级倒置问题。

第九部分 SPI 控制器

第一章:SPI 控制器在 Platform Bus 中的位置

SPI 控制器 与 I2C 控制器并列,都挂载在 Platform Bus 上。SPI (Serial Peripheral Interface) 是一种全双工、同步通信接口,通常比 I2C 速度更快,用于连接 NOR Flash传感器触摸屏控制器音频 Codec 等高速或需要高吞吐量的设备。

Linux 中 SPI 子系统同样分为三层:

  1. SPI 核心层 (drivers/spi/spi.c):管理 spi_controller (主机)、spi_device (从设备) 和 spi_message (传输事务)。

  2. SPI 控制器驱动:具体 SoC 的 SPI 主机驱动(例如 spi-dw.c),负责操作硬件寄存器,执行 transfer_one_message

  3. 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.cspi-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 时间)。

调试方法

  1. 查看 pinctrl 状态

    cat /sys/kernel/debug/pinctrl/pinctrl-handles | grep <spi_bus>

    确认 CS 引脚被正确配置为 spi0_cs0 功能。

  2. 手动切换 CS:当 SPI 驱动不支持时,可以在驱动中明确调用 gpio_set_value(gpio_cs, 0);

  3. 使用逻辑分析仪:抓取 CS 与 MISO/MOSI 的波形,观察拉低时间是否正确。

3.2 波特率与时钟极性 (CPOL/CPHA)

这是 SPI 最头疼的问题。模式不匹配会导致接收的数据错位。

现象:读取的数据全是 0x00 或 0xFF,或读取的值反相。

调试方法

  1. 匹配模式:仔细阅读从设备数据手册,确定需要的模式 (Mode 0~3)。

  2. 检查驱动设置:确保 dw_spi_transfer_one 中正确设置了 ctrlr0 的 6/7 位。

  3. 示波器检查:测量 SCLK 在空闲时的电平(高或低)以及数据采样时刻。

3.3 DMA 传输异常

使用 DMA 时,如果 DMA 通道配置错误,可能导致数据错乱或 FIFO 溢出。

现象:大量数据传输时丢数据,或中断风暴(DMA 没完成,CPU 反复轮询状态)。

调试方法

  1. 检查 DMA 错误日志dmesg | grep dma

  2. 不使用 DMA:设置 dws->use_dma = false; 先让 PIO 模式工作正常,再调试 DMA。

  3. 使用 perf 追踪 DMA 事件

    perf record -e dma:* -a -- sleep 5
    perf report

    看 dma 请求是否被及时处理。

3.4 传输死锁

现象:SPI 传输卡住,CPU 占用 100%,但数据没发出去。

原因:中断被禁用了,或者中断处理程序没有正确唤醒 SPI 核心层。

调试方法

  1. 检查中断状态cat /proc/interrupts | grep <irq>,看触发次数。

  2. 动态跟踪 ISR

    bpftrace -e 'kprobe:dw_spi_irq_handler { @count[tid]=count(); } kretprobe:dw_spi_irq_handler { if (retval != 0) printf("ISR error\n"); }'
  3. 检查锁机制:确保 dw_spi_irq_handler 里正确使用了 spin_lock,但不要长时间持有。


第四部分:结合性能调试场景示例

场景:系统启动时挂载 NOR Flash (spi-nor) 失败,导致内核无法挂载文件系统,从设备响应超时。

分析流程

  1. 宏观观察(启动日志 dmesg):

    dmesg | grep spi

    看到 "spi_nor: probe failed -ETIMEDOUT"。

  2. 追踪 SPI 传输(图谱中 Device Drivers -> SPI Controller):

    trace-cmd record -e spi:spi_transfer -e spi:spi_sync
    trace-cmd report

    显示最后一个传输 tx 数据被写入了,但是 rx 没有收到数据。

  3. 硬件检查

    • 使用 devmem2 读取 DW_SPI_SR 状态寄存器:

    devmem2 <base_addr>+0x28

    发现 SR_BUSY 一直为 1,表示控制器处于忙碌状态,且 SR_TX_EMPTY 已经为 0,但 RX_FIFO 不为空。

  4. 分析原因:可能是因为 DMA 配置错误,导致 RX FIFO 数据未及时读取,最终控制器卡死在忙碌状态。

  5. 解决方案

    • 禁用 DMA:在 DTS 中设置 dma-names = ""; 或强制 dws->use_dma = false;

    • 测试 PIO 模式,观察是否成功。如果成功,说明 DMA 配置有问题。


第五章:与其他控制器的协同

SPI 总线通常连接:

  • NOR Flash:存储 Bootloader、内核、文件系统。

  • 传感器:I2C 和 SPI 都可用,但 SPI 吞吐量更高。

  • 触摸屏控制器:主要用于大屏幕。

  • LCD 接口(SPI 屏):驱动 RGB 数据显示。

调试复合场景时,建议:

  1. 优先验证 SPI 总线:使用 spidev 工具测试。

  2. 确认片选分配:在 DTS 中,确保 reg 属性与物理 CS 线对应。

  3. 调试 cs_gpio:如果使用 GPIO 模拟 CS,必须确保 GPIO 方向正确且在 SPI 传输前后正确拉低/拉高。

第十部分 DMA 控制器

第一章:DMA 控制器在 Platform Bus 中的位置

DMA 控制器通常作为一个独立的 Platform 设备存在。它并不直接连接最终外设(如LCD、Camera),而是作为 数据搬运工,为其他控制器(如 UART、I2C、SPI、Audio)提供内存与外设之间、或者内存与内存之间的数据搬移服务。

Linux 中 DMA 子系统同样分为三层:

  1. DMA 核心层 (drivers/dma/dmaengine.c):提供通用 DMA 引擎 API,管理通道、描述符和传输。

  2. DMA 控制器驱动:具体硬件(如 PL330、DW_DMAC、DMA-APB),负责管理硬件通道、中断和传输队列。

  3. 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。

解决方案

  1. 使用 dma_alloc_coherent 分配内存(可保证 CPU 和 DMA 视图一致)。

  2. 使用 dma_map_singledma_unmap_single,并确保 dma_sync_single_for_device / dma_sync_single_for_cpu 被正确调用。

  3. 在 Client 驱动中:如果接收数据后 CPU 读取错误,添加 dma_sync_single_for_cpu

3.2 虚拟地址 vs. 物理地址

现象:DMA 访问内存地址错误,导致数据损坏或系统崩溃。

原因:DMA 使用物理地址,而 CPU 使用虚拟地址。驱动错误地将虚拟地址传给了 DMA 控制器。

调试方法

  1. 检查 DMA 映射:在 pl330_hw_start_transfer 中打印 src_addrdst_addr,确认它们属于 dma_addr_t 类型(物理地址)。

  2. 使用 perf 跟踪 dma_map_singledma_unmap_single

    perf record -e dma:dma_map_page -e dma:dma_unmap_page -a
  3. 检查 DMA 限制:某些 DMA 只能访问 32 位地址,如果物理地址 > 4GB,需要设置 dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))

3.3 中断丢失或过度中断

现象:DMA 传输完成但没有中断触发;或者中断触发太频繁导致 CPU 占用率飙升。

原因

  • 中断配置错误(通道中断未使能)。

  • DMA 驱动未清除中断标志位。

  • 描述符中的 DMA_PREP_INTERRUPT 未设置。

调试方法

  1. 检查中断统计cat /proc/interrupts | grep pl330,看触发次数是否正确。

  2. 使用 bpftrace 动态追踪中断处理程序

    bpftrace -e 'kprobe:pl330_irq_handler { @start[tid]=nsecs; } kretprobe:pl330_irq_handler { if (retval != IRQ_HANDLED) printf("IRQ not handled\n"); }'
  3. 检查 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 中没有正确配置链式操作模式。

调试方法

  1. 检查配置:在 pl330_hw_start_transfer 中,确认 NEXT 寄存器被写入下一个描述符的物理地址。

  2. 使用 perf 跟踪传输回调

    perf record -e dma:* -e dma:dma_tx_status -a -- sleep 10
    perf script | grep -E "pl330|tx_status"

    看何时返回完成状态。


第四章:结合性能调试场景示例

场景:高速 SPI Flash 写入时,写入性能低于理论值(例如实际速度只有理论峰值的一半),同时系统 iowait 高。

分析流程

  1. 宏观观察(图谱的 CPU 和 Memory 层):top 显示 iowait 较高;iostat -x 1 显示磁盘设备(实际是 Flash)利用率高但吞吐量低。

  2. 查看 SPI 传输(设备驱动层):使用 trace-cmd 记录 SPI 传输:

    trace-cmd record -e spi:spi_transfer -e spi:spi_sync
    trace-cmd report

    发现每次 SPI 传输只发送 256 字节,大量时间浪费在 SPI 核心层对 spi_message 的拆分和处理上。

  3. 查看 DMA 使用情况(DMA 控制器层):

    perf stat -e dma:* -a -- timeout 10

    发现 DMA 中断触发次数极高,与 SPI 传输次数匹配,意味着每个 SPI 传输都引发了中断,CPU 频繁处理中断上下文切换。

  4. 根本原因:SPI 驱动没有利用 DMA 的 scatter-gather 功能,而是逐个传输小数据块。每次传输后 DMA 完成中断会唤醒 CPU,导致上下文切换开销。

  5. 解决方案

    • 配置 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)大量物理不连续的缓冲区到连续虚拟地址

调试复合场景时,建议:

  1. 先验证 DMA 本身:使用 dmatest 模块(CONFIG_DMATEST)测试 DMA 通道基本功能。

  2. 查看 DMA 调试信息ls /sys/kernel/debug/dma/dma-* 获取每个通道的当前状态。

  3. 确认 DMA 配置:DMA 的 config 中的 slave_id 必须与外设的 DMA 请求号匹配(查看 SoC 手册)。

第十一部分 音频控制器(I2S)

第一章:音频控制器在 Platform Bus 中的位置

音频控制器(通常以 I2S/PCM 接口形式存在)作为 Platform 设备挂载在总线中。它负责将内存中的音频数据(PCM 格式)通过 I2S 总线发送到外部 Codec 或扬声器,或者从麦克风接收音频数据。

Linux 音频子系统(ALSA/ASoC)的层级结构:

  1. ASoC 核心层 (sound/soc/soc-core.c):管理音频设备和 DAI 链路。

  2. CPU DAI 驱动(即本篇文章重点):SoC 内部的 I2S 控制器驱动。

  3. Codec DAI 驱动:外部音频 Codec 芯片(如 WM8960, CS42L51)。

  4. 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 不匹配,导致采样率不一致。

调试方法

  1. 使用 alsa-utils 检查硬件参数

    aplay -l   # 列出音频设备
    cat /proc/asound/card0/stream0  # 查看当前流状态
  2. 查看时钟配置

    cat /sys/kernel/debug/clk/clk_summary | grep ssi

    确认 ssi_clk 实际频率是否符合预期。

  3. 使用 perf 跟踪音频 DMA 中断

    perf record -e dma:dma_* -a -- timeout 10
    perf script | grep ssi

3.2 音频卡顿(XRUN — Under/Overflow)

现象:播放音频时出现周期性卡顿或杂音,CPU 占用率瞬间飙升。

原因:DMA 传输来不及填充或读取数据,导致 FIFO 下溢(播放)或上溢(录音)。

调试方法

  1. 使用 dmesg 查看 XRUN 信息

    dmesg | grep "XRUN"
  2. 增加音频缓冲大小

    cat /proc/asound/card0/pcm0p/period_size
    echo 8192 > /proc/asound/card0/pcm0p/period_size  # 调整
  3. 检查 DMA 优先级

    • 查看 DMA 通道是否被高优先级中断抢占。

    • 使用 perf top -C 0 看 DMA 中断处理是否耗时过高。

3.3 录音无声音或声音异常

现象:录音设备能打开,但录制的数据全是 0 或杂音。

原因

  • I2S 方向配置错误(接收未使能)。

  • GPIO/引脚复用(PinMux)错误,导致音频信号无法到达 CPU。

  • Codec 供电或配置错误。

调试方法

  1. 查看 GPIO 状态

    cat /sys/kernel/debug/pinctrl/pinctrl-handles | grep i2s
  2. 检查 AlsaMixer 音量

    amixer scontrols
    amixer sset 'Capture Volume' 80%
  3. 使用 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%。

分析流程

  1. 宏观层面(CPU 和 Memory 层):

    • top 显示 kworker/u:4 占用 CPU 较高。

    • perf top 显示 fsl_ssi_irq_handlerdma_async_memcpy_buf_to_buf 占用高。

  2. 追踪 DMA 中断(DMA 控制器层):

    perf record -e dma:dma_* -a -- timeout 5
    perf script | grep fsl_ssi

    发现 DMA 中断频率很高,同时 dma_sync_single_for_device 调用频繁。

  3. 查看音频缓冲状态(音频核心层):

    cat /proc/asound/card0/pcm0p/period_size
    cat /proc/asound/card0/pcm0p/period_bytes

    发现 period_size 设置太小(128 帧),导致中断频率过高,kworker 抢占 CPU。

  4. 根本原因:音频应用配置的 period_size 过小,DMA 每次传输 128 帧(约 2.9ms 数据)产生一次中断,kworker 忙于处理中断上下文切换。

  5. 解决方案

    • 增加 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),它的架构分为三层:

  1. V4L2 核心层drivers/media/v4l2-core/):提供统一的 API,管理视频设备、buffers、controls 和 events。

  2. CSI 主机控制器驱动(即本篇文章重点):负责接收原始图像数据,解析 MIPI D-PHY 信号,将数据通过 DMA 搬运到内存。

  3. 传感器驱动(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 物理层信号质量差(走线过长、阻抗不匹配、干扰)。

调试方法

  1. 检查 MIPI 时钟和通道(lane)映射

    • 确认 DTS 中的 lane 配置与硬件 PCB 一致。

    • 使用示波器或逻辑分析仪查看 MIPI 时钟是否稳定。

  2. 调整 D-PHY 驱动强度:某些驱动支持通过寄存器微调驱动电流。

  3. 降低 MIPI 速率:在传感器驱动中降低 MIPI 频率(如 1Gbps -> 800Mbps)。

3.2 DMA 缓冲区不足或映射失败

现象VIDIOC_QBUF 入队成功,但 VIDIOC_DQBUF 无数据返回,dmesg 出现 "DMA error"。

原因:DMA 无法将数据写入缓冲区的物理地址,或者缓冲区太小。

调试方法

  1. 检查缓冲区大小cat /sys/kernel/debug/dma/dma-* 查看 DMA 分配情况。

  2. 强制分配连续内存:确保 CONFIG_DMA_CMA 启用且分配足够大(例如 64MB)。

  3. 使用 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)未能正确设置分辨率、格式或曝光。

调试方法

  1. 使用 media-ctl 检查管道

    media-ctl -p
    media-ctl -v -V "\"Sensor\":0[fmt:UYVY8_1X16/1920x1080]"
  2. 捕获 I2C 通信:如果传感器通过 I2C 配置,使用 i2cdetect 确认地址正确。

  3. 查看传感器驱动日志

    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 中断处理慢,或者应用程序来不及入队新缓冲区。

调试方法

  1. 使用 perf 监控中断处理延迟

    perf record -e irq:* -e dma:dma_complete -a -- sleep 5
    perf script | grep cif
  2. 增加缓冲池深度:在用户空间 v4l2-ctl 或应用中使用 set buffer count(例如 4 或 6)。

  3. 优先处理中断:使用 sched_setscheduler 将摄像头进程设为实时优先级。


第四章:结合性能调试场景示例

场景:嵌入式系统上运行 GStreamer 管道采集高清(1080p)视频,3 分钟后系统响应变慢,top 显示 irq/XX-cif 占用 CPU 很高,视频出现间歇性跳帧。

分析流程

  1. 宏观层面(图谱的 CPU 层):

    • top 显示 irq/XX-cif 占用 CPU 约 30%-40%。

    • perf top -G 显示 rk_cif_irq_handlerdma_sync_single_for_cpu 上花费大量时间。

  2. 查看中断统计(设备驱动层):

    cat /proc/interrupts | grep cif

    发现中断触发频率非常高(每秒约 120 次,符合 60fps * 2 帧开始/结束),但每次 ISR 执行时间长。

  3. 分析中断处理(图谱的 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 的帧周期来说较长)。

  4. 根本原因:中断处理程序中进行了不必要的 DMA 同步操作。每次 vb2_buffer_done 触发前,驱动程序调用了 dma_sync_single_for_cpu,这涉及大量的缓存刷新操作。

  5. 解决方案

    • 使用 dma_alloc_coherent 替代 dma_alloc_writecombine(减少同步开销)。

    • 在 ISR 中仅记录完成事件,将实际数据同步推迟到 DQBUF 时。

    • 最终优化:将中断合并模式(Interrupt coalescing)调大,改为每 4 帧触发一次中断,而不是每帧。


第五部分:与其他控制器的协同

控制器 协同方式 调试关键点
MIPI D-PHY 物理层接收器,集成在 CSI 控制器内 信号质量、通道映射、驱动强度
DMA 将 MIPI 数据写入内存 连续物理内存、缓冲区对齐、中断聚合
I2C 配置传感器寄存器 确保地址正确,I2C 时序匹配
GPIO 传感器复位、电源使能、中断触发 正确时序(上电 -> 复位 -> 初始化)
PMIC 提供传感器电源 电压稳定,无噪声
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐