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,
};
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 这个自定义结构体串联成一个整体。下图展示了数据流与依赖关系。
3.5 关键流程总结
- 设备树解析阶段:内核启动时,SPI 控制器驱动扫描子节点,根据
compatible匹配到epd_spi_driver,并调用probe。 - probe 初始化阶段:驱动解析设备树获取 GPIO 和面板参数,配置 SPI 模式,分配字符设备号并注册
cdev。 - 用户空间交互阶段:应用程序通过
open("/dev/epd0")获得文件描述符,通过ioctl下发初始化命令、加载波形文件、触发刷新。 - SPI 传输阶段:所有 EPD 控制指令(如写寄存器、发送图像数据)最终通过
spi_sync_transfer()或spi_write_then_read()完成。
- 设备树中的
reg属性必须与 SPI 控制器片选编号一致,否则无法选中设备。 - 字符设备
ioctl命令号建议使用_IOW()/_IOR()宏生成,避免与内核其他命令冲突。 - SPI 传输的
speed_hz不应超过设备树中spi-max-frequency的值,否则可能导致通信失败。