3. Linux驱动框架:SPI设备驱动模型、字符设备驱动框架、设备树绑定

EPD(电子纸显示器)控制器通常通过 SPI 总线与主控 SoC 通信。在 Linux 内核中,驱动开发者需要将 SPI 设备驱动模型、字符设备驱动框架以及设备树(Device Tree)绑定三者有机结合,才能构建一个可运行、可维护的 EPD 驱动。本章将逐一拆解这三个核心组件,并展示它们如何协同工作。

3.1 SPI 设备驱动模型

Linux SPI 子系统采用主机控制器驱动(Controller Driver)与设备驱动(Device Driver)分离的架构。对于 EPD 驱动开发,我们主要关注设备驱动侧,即如何编写一个标准的 SPI 客户端驱动。

3.1.1 核心数据结构

数据结构 作用 关键成员
struct spi_device 代表一个 SPI 从设备,由 SPI 核心在设备树匹配后创建 controller、max_speed_hz、mode、chip_select
struct spi_driver SPI 设备驱动结构体,用于注册和匹配设备 id_table、probe、remove、driver
struct spi_transfer 描述一次 SPI 传输的细节(tx/rx buffer、长度、速度等) tx_buf、rx_buf、len、speed_hz
struct spi_message 包含一个或多个 spi_transfer,保证原子性传输 transfers、complete、context

3.1.2 驱动注册与注销

EPD 驱动通过 module_spi_driver() 宏完成注册,该宏展开后包含 module_init 和 module_exit。在 probe 回调中,驱动获得 spi_device 指针,并完成硬件初始化、内存分配、字符设备注册等动作。

static int epd_probe(struct spi_device *spi)
{
    struct epd_dev *epd;
    int ret;

    epd = devm_kzalloc(&spi->dev, sizeof(*epd), GFP_KERNEL);
    if (!epd)
        return -ENOMEM;

    epd->spi = spi;
    spi_set_drvdata(spi, epd);

    /* 配置 SPI 模式:CPOL=0, CPHA=0, 8位数据 */
    spi->mode = SPI_MODE_0;
    spi->bits_per_word = 8;
    ret = spi_setup(spi);
    if (ret)
        return ret;

    /* 后续:字符设备注册、EPD 控制器初始化 */
    return 0;
}

static void epd_remove(struct spi_device *spi)
{
    struct epd_dev *epd = spi_get_drvdata(spi);
    /* 清理资源 */
}

static const struct of_device_id epd_of_match[] = {
    { .compatible = "vendor,epd-controller", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, epd_of_match);

static struct spi_driver epd_spi_driver = {
    .driver = {
        .name   = "epd",
        .of_match_table = epd_of_match,
    },
    .probe  = epd_probe,
    .remove = epd_remove,
};
module_spi_driver(epd_spi_driver);
关键点: spi_setup() 必须在 probe 中显式调用,否则 SPI 控制器不会应用新的模式参数。EPD 控制器通常要求 SPI 时钟极性为 0、相位为 0,且最高时钟频率不超过 20MHz。

3.2 字符设备驱动框架

EPD 控制器在用户空间通常通过帧缓冲(Frame Buffer)或字符设备接口访问。对于波形调优和局部刷新场景,字符设备提供了更灵活的 ioctl 控制通道。

3.2.1 设备号分配与 cdev 初始化

字符设备驱动需要向内核申请设备号,并关联 struct cdev 与 struct file_operations。

static int epd_chrdev_init(struct epd_dev *epd)
{
    dev_t devno;
    int ret;

    /* 动态分配设备号 */
    ret = alloc_chrdev_region(&devno, 0, 1, "epd");
    if (ret)
        return ret;

    epd->devno = devno;

    /* 初始化 cdev */
    cdev_init(&epd->cdev, &epd_fops);
    epd->cdev.owner = THIS_MODULE;

    ret = cdev_add(&epd->cdev, devno, 1);
    if (ret) {
        unregister_chrdev_region(devno, 1);
        return ret;
    }

    /* 自动创建设备节点 */
    epd->class = class_create(THIS_MODULE, "epd_class");
    device_create(epd->class, NULL, devno, NULL, "epd%d", 0);

    return 0;
}

3.2.2 file_operations 实现

EPD 字符设备至少需要实现 open、release、ioctl 三个接口。ioctl 是波形调优的核心入口,用于下发初始化序列、加载波形文件、触发刷新等。

static long epd_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct epd_dev *epd = filp->private_data;

    switch (cmd) {
    case EPD_IOC_INIT:
        return epd_hw_init(epd);
    case EPD_IOC_LOAD_WAVEFORM:
        return epd_load_waveform(epd, (const char __user *)arg);
    case EPD_IOC_DISPLAY_UPDATE:
        return epd_display_update(epd, arg);
    default:
        return -ENOTTY;
    }
}

static int epd_open(struct inode *inode, struct file *filp)
{
    struct epd_dev *epd = container_of(inode->i_cdev, struct epd_dev, cdev);
    filp->private_data = epd;
    return 0;
}

static const struct file_operations epd_fops = {
    .owner          = THIS_MODULE,
    .open           = epd_open,
    .release        = epd_release,
    .unlocked_ioctl = epd_ioctl,
};
注意: 内核 5.0 以后推荐使用 unlocked_ioctl 替代 ioctl。驱动开发者需要自行确保并发安全,通常使用 mutex_lock 保护硬件访问。

3.3 设备树绑定

设备树(Device Tree)是描述硬件拓扑的扁平化数据结构。EPD 控制器驱动通过 compatible 属性与设备树节点匹配,并从中获取 SPI 参数、GPIO 引脚、复位时序等平台信息。

3.3.1 设备树节点示例

&spi1 {
    status = "okay";
    cs-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>;

    epd: epd@0 {
        compatible = "vendor,epd-controller";
        reg = <0>;
        spi-max-frequency = <12000000>;
        spi-cpol;
        spi-cpha;

        reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
        busy-gpios  = <&gpio1 16 GPIO_ACTIVE_HIGH>;
        dc-gpios    = <&gpio1 17 GPIO_ACTIVE_HIGH>;

        panel-width  = <800>;
        panel-height = <600>;
        waveform-path = "epd_waveform.bin";
    };
};

3.3.2 驱动中解析设备树

在 probe 函数中,驱动通过 of_* 系列 API 读取设备树属性,完成 GPIO 请求和参数配置。

static int epd_parse_dt(struct epd_dev *epd)
{
    struct device *dev = &epd->spi->dev;
    struct device_node *np = dev->of_node;

    /* 解析 GPIO */
    epd->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
    if (IS_ERR(epd->reset_gpio))
        return PTR_ERR(epd->reset_gpio);

    epd->busy_gpio = devm_gpiod_get(dev, "busy", GPIOD_IN);
    if (IS_ERR(epd->busy_gpio))
        return PTR_ERR(epd->busy_gpio);

    epd->dc_gpio = devm_gpiod_get(dev, "dc", GPIOD_OUT_LOW);
    if (IS_ERR(epd->dc_gpio))
        return PTR_ERR(epd->dc_gpio);

    /* 解析面板尺寸 */
    if (of_property_read_u32(np, "panel-width", &epd->width))
        return -EINVAL;
    if (of_property_read_u32(np, "panel-height", &epd->height))
        return -EINVAL;

    /* 解析波形文件路径(可选) */
    of_property_read_string(np, "waveform-path", &epd->waveform_path);

    return 0;
}

3.4 三层架构协同关系

SPI 设备驱动模型、字符设备驱动框架、设备树绑定三者并非孤立,而是通过 struct epd_dev 这个自定义结构体串联成一个整体。下图展示了数据流与依赖关系。

EPD驱动三层架构协同关系图 EPD 驱动三层架构协同关系 设备树 (DT) compatible / reg / GPIO SPI 设备驱动 spi_driver / probe / transfer 字符设备框架 cdev / fops / ioctl 匹配 注册 struct epd_dev(核心枢纽) spi_device *spi | dev_t devno | struct cdev cdev GPIO descs | panel params | waveform buffer 用户空间应用程序 open / ioctl / close 系统调用 说明:设备树提供硬件描述 → SPI 驱动完成总线通信 → 字符设备暴露操作接口给用户空间

3.5 关键流程总结

  1. 设备树解析阶段:内核启动时,SPI 控制器驱动扫描子节点,根据 compatible 匹配到 epd_spi_driver,并调用 probe。
  2. probe 初始化阶段:驱动解析设备树获取 GPIO 和面板参数,配置 SPI 模式,分配字符设备号并注册 cdev。
  3. 用户空间交互阶段:应用程序通过 open("/dev/epd0") 获得文件描述符,通过 ioctl 下发初始化命令、加载波形文件、触发刷新。
  4. SPI 传输阶段:所有 EPD 控制指令(如写寄存器、发送图像数据)最终通过 spi_sync_transfer() 或 spi_write_then_read() 完成。
实践要点:
  • 设备树中的 reg 属性必须与 SPI 控制器片选编号一致,否则无法选中设备。
  • 字符设备 ioctl 命令号建议使用 _IOW()/_IOR() 宏生成,避免与内核其他命令冲突。
  • SPI 传输的 speed_hz 不应超过设备树中 spi-max-frequency 的值,否则可能导致通信失败。