4. 驱动框架搭建:platform driver模型、设备树匹配、probe函数实现、资源申请

在嵌入式Linux系统中,EPD(电子纸)显示驱动通常采用 platform driver 模型进行框架搭建。该模型将硬件设备信息与驱动逻辑解耦,通过设备树(Device Tree)进行匹配,在 probe 函数中完成资源申请与初始化。本节将详细拆解这一过程,并给出可落地的代码结构与调试要点。

设计原则: platform driver 模型是 Linux 设备驱动模型的核心机制之一,适用于非热插拔、片上集成的外设(如 SPI/I2C 接口的 EPD 控制器)。通过设备树描述硬件拓扑,驱动只需关注匹配后的初始化逻辑,极大提高了代码的可移植性。

4.1 platform driver 模型与设备树匹配

EPD 驱动首先需要定义一个 platform_driver 结构体,并注册到内核。设备树中的节点通过 compatible 属性与驱动中的 of_match_table 进行匹配。匹配成功后,内核自动调用驱动的 probe 函数。

platform driver 匹配与 probe 调用流程 设备树节点 compatible = "vendor,epd-spi" platform_driver of_match_table = epd_of_match compatible 匹配 内核驱动核心 platform_driver_register() probe() 回调 资源申请 + 初始化
关键数据结构: of_device_id 数组用于声明驱动支持的设备树 compatible 字符串。内核通过该数组与设备树节点进行精确匹配。
// 设备树匹配表
static const struct of_device_id epd_of_match[] = {
    { .compatible = "vendor,epd-spi", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, epd_of_match);

// platform driver 定义
static struct platform_driver epd_driver = {
    .probe  = epd_probe,
    .remove = epd_remove,
    .driver = {
        .name = "epd_spi",
        .of_match_table = epd_of_match,
    },
};

module_platform_driver(epd_driver);

4.2 probe 函数实现:资源申请与初始化

probe 函数是驱动初始化的核心入口。在 EPD 驱动中,它需要完成以下关键任务:

  1. 获取设备树资源: 解析 GPIO、SPI 控制器、电源控制等硬件资源。
  2. 申请内存与数据结构: 分配私有数据结构体,存储设备状态。
  3. 初始化硬件: 设置 SPI 模式、时钟、GPIO 方向,执行 EPD 控制器复位序列。
  4. 注册 FrameBuffer: 调用 register_framebuffer() 将 EPD 显示设备注册到系统。
probe 函数内部执行流程 probe() 入口 解析设备树资源 of_get_property / gpiod_get 申请私有数据结构 devm_kzalloc 初始化 SPI / GPIO spi_setup / gpiod_direction EPD 硬件复位 时序序列 分配 FrameBuffer 内存 dma_alloc_coherent 填充 fb_info 结构 fix / var / ops register_framebuffer() 注册到系统 probe 成功返回 0

4.3 设备树资源解析与申请

设备树中描述了 EPD 模块的硬件连接信息。驱动通过 device_node 接口获取这些资源。以下是一个典型的 EPD 设备树节点示例:

// 设备树节点 (arch/arm/boot/dts/xxx.dts)
&spi1 {
    status = "okay";
    epd: epd@0 {
        compatible = "vendor,epd-spi";
        reg = <0>;
        spi-max-frequency = <2000000>;
        dc-gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>;
        rst-gpios = <&gpio1 13 GPIO_ACTIVE_HIGH>;
        busy-gpios = <&gpio1 14 GPIO_ACTIVE_HIGH>;
        vcc-supply = <&vcc_3v3>;
        panel-width = <800>;
        panel-height = <600>;
    };
};
避坑提示: 设备树中的 GPIO 标志 GPIO_ACTIVE_HIGH/LOW 必须与硬件原理图一致。错误的极性会导致 EPD 无法正常复位或响应。建议在 probe 初期通过 gpiod_get_value() 打印电平状态进行验证。

在 probe 函数中,使用以下 API 解析上述资源:

资源类型 解析 API 返回值 说明
SPI 设备 devm_spi_register_device() struct spi_device * 从设备树获取 SPI 从设备
GPIO 描述符 devm_gpiod_get() struct gpio_desc * 通过名称获取 GPIO,支持 _indexed 变体
电源 regulator devm_regulator_get() struct regulator * 获取供电控制句柄
整型属性 of_property_read_u32() int 读取 panel-width、panel-height 等

4.4 私有数据结构体设计

为每个 EPD 设备维护一个私有数据结构,用于存储硬件上下文、FrameBuffer 信息以及工作队列状态。这是驱动模块化的关键。

struct epd_priv {
    struct device *dev;
    struct spi_device *spi;
    struct gpio_desc *dc_gpio;
    struct gpio_desc *rst_gpio;
    struct gpio_desc *busy_gpio;
    struct regulator *vcc;

    u32 panel_width;
    u32 panel_height;
    u32 bytes_per_pixel;  // 通常为 1 (4-bit grayscale) 或 3 (RGB)

    struct fb_info *fb_info;
    void *fb_buffer;       // FrameBuffer 内存虚拟地址
    dma_addr_t fb_dma;     // DMA 物理地址

    struct completion refresh_done;
    struct mutex lock;
};
内存申请策略: 使用 devm_kzalloc() 申请私有数据结构,利用设备资源管理(devres)自动释放,避免内存泄漏。FrameBuffer 缓冲区建议使用 dma_alloc_coherent() 分配,保证 SPI DMA 传输时缓存一致性。

4.5 probe 函数完整骨架

以下代码展示了 EPD 驱动 probe 函数的典型实现骨架,包含资源申请、初始化和错误处理路径:

static int epd_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct epd_priv *priv;
    struct fb_info *info;
    int ret;

    // 1. 申请私有数据结构
    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    priv->dev = dev;
    mutex_init(&priv->lock);
    init_completion(&priv->refresh_done);
    platform_set_drvdata(pdev, priv);

    // 2. 获取 SPI 设备 (假设通过设备树子节点)
    priv->spi = devm_spi_register_device(dev, ...);
    if (IS_ERR(priv->spi)) {
        dev_err(dev, "failed to get SPI device\n");
        return PTR_ERR(priv->spi);
    }

    // 3. 获取 GPIO 资源
    priv->dc_gpio = devm_gpiod_get(dev, "dc", GPIOD_OUT_LOW);
    if (IS_ERR(priv->dc_gpio))
        return dev_err_probe(dev, PTR_ERR(priv->dc_gpio), "dc gpio failed\n");

    priv->rst_gpio = devm_gpiod_get(dev, "rst", GPIOD_OUT_HIGH);
    if (IS_ERR(priv->rst_gpio))
        return dev_err_probe(dev, PTR_ERR(priv->rst_gpio), "rst gpio failed\n");

    priv->busy_gpio = devm_gpiod_get(dev, "busy", GPIOD_IN);
    if (IS_ERR(priv->busy_gpio))
        return dev_err_probe(dev, PTR_ERR(priv->busy_gpio), "busy gpio failed\n");

    // 4. 获取电源 regulator
    priv->vcc = devm_regulator_get(dev, "vcc");
    if (IS_ERR(priv->vcc))
        return dev_err_probe(dev, PTR_ERR(priv->vcc), "vcc regulator failed\n");
    ret = regulator_enable(priv->vcc);
    if (ret)
        return ret;

    // 5. 读取面板参数
    ret = of_property_read_u32(dev->of_node, "panel-width", &priv->panel_width);
    ret |= of_property_read_u32(dev->of_node, "panel-height", &priv->panel_height);
    if (ret) {
        dev_err(dev, "missing panel dimensions in DT\n");
        return -EINVAL;
    }
    priv->bytes_per_pixel = 1; // 4-bit grayscale

    // 6. 执行 EPD 硬件复位序列
    epd_hardware_reset(priv);

    // 7. 分配 FrameBuffer 内存
    info = framebuffer_alloc(0, dev);
    if (!info)
        return -ENOMEM;
    priv->fb_info = info;

    // 8. 填充 fb_info (fix/var/ops) —— 后续章节详细展开
    epd_fb_fill_info(priv, info);

    // 9. 注册 FrameBuffer
    ret = register_framebuffer(info);
    if (ret) {
        dev_err(dev, "failed to register framebuffer\n");
        framebuffer_release(info);
        return ret;
    }

    dev_info(dev, "EPD probe success: %dx%d\n",
             priv->panel_width, priv->panel_height);
    return 0;
}
调试建议: 在 probe 函数的关键步骤(如 GPIO 获取、SPI 设置、复位完成)添加 dev_dbg() 或 dev_info() 打印。通过 dmesg 观察日志,可以快速定位资源申请失败的位置。使用 dev_err_probe() 可以优雅处理 EPROBE_DEFER 场景。

4.6 资源申请常见错误与排查

错误现象 可能原因 排查方法
probe 返回 -ENODEV 设备树 compatible 不匹配 检查 of_match_table 字符串与 DTS 是否一致
GPIO 获取失败 -EBUSY GPIO 被其他驱动占用或 pinmux 未配置 检查 pinctrl 设置,确认 GPIO 号正确
regulator 获取失败 -EPROBE_DEFER 电源 regulator 驱动未就绪 内核会自动重试 probe,无需手动处理
SPI 通信无响应 SPI 模式/频率不匹配或硬件连接错误 使用逻辑分析仪抓取 SPI 信号,核对时序
FrameBuffer 注册失败 fb_info 填充不完整或内存不足 检查 fix.smem_len、var.bits_per_pixel 等字段

通过以上步骤,EPD 驱动的 platform driver 框架搭建完成。probe 函数成功执行后,系统将拥有一个可用的 FrameBuffer 设备节点(如 /dev/fb0),为后续的显示链路调试奠定基础。