板块:嵌入式 Linux
上一篇把 platform driver 的骨架讲了一遍,这篇把内容填上:驱动和设备怎么匹配、用户空间怎么和驱动交互、read/write/ioctl 怎么实现、不同类型的外设该挂进哪个框架、GPIO 中断怎么接、数据在内核和用户空间之间怎么传——把这几件事合在一起讲清楚。
一、四种匹配方式
内核里 platform_match() 函数按固定优先级依次尝试四种匹配方式,找到一种成功就停下来。[¹]
来源:drivers/base/platform.c,platform_match() 函数:
static int platform_match(struct device *dev, struct device_driver *drv){struct platform_device *pdev = to_platform_device(dev);struct platform_driver *pdrv = to_platform_driver(drv); /* 1. driver_override:强制绑定,优先级最高 */ if (pdev->driver_override) return !strcmp(pdev->driver_override, drv->name); /* 2. OF style:设备树 compatible 字符串匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 3. ACPI style:x86/ACPI 平台用 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 4. id_table:按设备名查表 */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* 5. 最后兜底:直接比较 name 字符串 */ return (strcmp(pdev->name, drv->name) == 0);}
嵌入式开发常用的是方式 2 和方式 4,方式 5 在没有设备树的老平台或者调试时用得到。
方式 2:of_match_table(设备树)
现代嵌入式 Linux 的标准做法,上一篇已经讲过。设备树节点的 compatible 和驱动的 of_match_table 对上即匹配:
static conststruct of_device_id mydev_of_match[] = { { .compatible = "myvendor,sensor-v1" }, { .compatible = "myvendor,sensor-v2", .data = &v2_config }, /* .data 可以传版本特定配置 */ { }};MODULE_DEVICE_TABLE(of, mydev_of_match);
of_match_table 里每一项的 .data 字段可以存一个指针,probe() 里通过 of_device_get_match_data() 取回来,实现"同一个驱动支持多个硬件版本,行为略有不同":
static int mydev_probe(struct platform_device *pdev){ conststruct mydev_config *cfg; cfg = of_device_get_match_data(&pdev->dev); /* cfg 指向匹配到的那一行的 .data */ if (cfg && cfg->has_dma) /* ... 走 DMA 路径 ... */}
方式 4:id_table(按名称查表,无设备树时用)
没有设备树的老平台,或者在代码里手动注册 platform_device 时,用 id_table 按名字匹配:
/* 驱动侧 */static conststruct platform_device_id mydev_id_table[] = { { "mydevice-A", (kernel_ulong_t)&config_A }, { "mydevice-B", (kernel_ulong_t)&config_B }, { } /* sentinel */};MODULE_DEVICE_TABLE(platform, mydev_id_table);staticstruct platform_driver mydev_driver = { .probe = mydev_probe, .remove = mydev_remove, .id_table = mydev_id_table, .driver = { .name = "mydevice" },};
/* 设备侧(board file 或测试时手动注册)*/staticstruct platform_device mydev_A = { .name = "mydevice-A", .id = -1,};platform_device_register(&mydev_A);
probe() 里拿到的 pdev->id_entry->driver_data 就是 id_table 里对应行的 driver_data 字段。
方式 5:名称直接比较
最简单,驱动的 .driver.name 和 platform_device 的 .name 一致就匹配。调试用,或者极简场景下用。不推荐在正式驱动里依赖这个。
[¹] 参考:内核源码 drivers/base/platform.c,platform_match() 函数;O'Reilly "Linux Device Drivers Development",Chapter 7
二、用户空间与驱动交互的全貌
用户空间 (app) │ │ open("/dev/mydevice", O_RDWR) │ read(fd, buf, len) │ write(fd, buf, len) │ ioctl(fd, CMD, arg) │ close(fd) │ ▼ ─── 系统调用边界 ─── 内核空间 ─── │ ▼VFS 层(虚拟文件系统) │ ▼字符设备层 → file_operations 函数表 │ .open → mydev_open() │ .read → mydev_read() │ .write → mydev_write() │ .unlocked_ioctl → mydev_ioctl() │ .release → mydev_release() │ ▼platform driver 的私有数据(priv->base 寄存器) │ ▼硬件寄存器(通过 ioread32/iowrite32 操作)
用户空间的每一个系统调用,最终都落到驱动里注册的 file_operations 函数上。
三、内核空间和用户空间的数据传递
这是新手最容易踩坑的地方:内核空间和用户空间不能直接用 memcpy 互相访问,必须用专用函数。[²]
原因:用户空间指针传进内核后不能直接解引用——指针可能无效,可能指向换出的页,也可能是恶意构造的地址,直接访问会导致内核 oops 或安全漏洞。
/* 从用户空间读数据到内核 */unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);/* 返回值:未能复制的字节数,成功返回 0 *//* 从内核写数据到用户空间 */unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);/* 返回值:未能复制的字节数,成功返回 0 *//* 单个简单类型的快捷版本 */put_user(kernel_val, user_ptr); /* 写一个值到用户空间 */get_user(kernel_var, user_ptr); /* 从用户空间读一个值 */
__user 是给 sparse 静态分析工具看的注解,告诉它这个指针来自用户空间,运行时没有实际影响但有助于发现代码里的错误用法。
在 read()、write()、ioctl() 三个函数里,所有涉及用户空间缓冲区的地方都必须用这套函数:
static ssize_t mydev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos){struct mydev_priv *priv = file->private_data; u32 val; /* 从寄存器读一个值 */ val = ioread32(priv->base + DATA_REG); /* 把这个值写到用户空间的 buf */ if (copy_to_user(buf, &val, sizeof(val))) return -EFAULT; return sizeof(val);}static ssize_t mydev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos){struct mydev_priv *priv = file->private_data; u32 val; /* 从用户空间的 buf 读一个值 */ if (copy_from_user(&val, buf, sizeof(val))) return -EFAULT; /* 写到寄存器 */ iowrite32(val, priv->base + CTRL_REG); return sizeof(val);}
[²] 参考:内核文档 "copy_to_user / copy_from_user";Linux kernel labs,https://linux-kernel-labs.github.io/refs/pull/189/merge/labs/device_drivers.html
四、ioctl:发控制命令
read 和 write 只能传数据流,控制命令用 ioctl。比如配置 ADC 量程、让屏幕旋转 90 度、清空 DAC 输出——这些用 ioctl 比用 read/write 更清晰。
ioctl 命令号的定义
内核提供了宏来生成不冲突的 ioctl 命令号,定义在共享头文件里(驱动和 app 都 include 这个文件):[²]
/* my_ioctl.h —— 驱动和用户空间 app 共享这个头文件 */#include <linux/ioctl.h>#define MYDEV_MAGIC 'M' /* 幻数,选一个你的驱动专用的字符,参考内核 Documentation/userspace-api/ioctl/ioctl-number.rst *//* _IO:无数据传输的命令 */#define MYDEV_RESET _IO(MYDEV_MAGIC, 0)/* _IOR:从内核读数据到用户空间(Read from kernel) */#define MYDEV_GET_STATUS _IOR(MYDEV_MAGIC, 1, u32)/* _IOW:用户空间写数据到内核(Write to kernel) */#define MYDEV_SET_FREQ _IOW(MYDEV_MAGIC, 2, u32)/* _IOWR:双向传输 */#define MYDEV_XFER _IOWR(MYDEV_MAGIC, 3, struct mydev_xfer_data)
驱动侧的 ioctl 实现
static long mydev_ioctl(struct file *file, unsigned int cmd, unsigned long arg){struct mydev_priv *priv = file->private_data; u32 val; int ret = 0; /* 验证命令号来自本驱动(检查幻数和编号范围) */ if (_IOC_TYPE(cmd) != MYDEV_MAGIC) return -ENOTTY; switch (cmd) { case MYDEV_RESET: /* 无数据传输,直接操作寄存器 */ iowrite32(0x1, priv->base + RESET_REG); break; case MYDEV_GET_STATUS: /* 从寄存器读值,写回用户空间 */ val = ioread32(priv->base + STATUS_REG); if (copy_to_user((void __user *)arg, &val, sizeof(val))) return -EFAULT; break; case MYDEV_SET_FREQ: /* 从用户空间读值,写进寄存器 */ if (copy_from_user(&val, (void __user *)arg, sizeof(val))) return -EFAULT; iowrite32(val, priv->base + FREQ_REG); break; default: ret = -ENOTTY; /* 命令不支持 */ break; } return ret;}
用户空间 app 调用
/* app.c */#include <fcntl.h>#include <sys/ioctl.h>#include "my_ioctl.h" /* 和驱动共享的头文件 */int main(void){ int fd = open("/dev/mydevice", O_RDWR); if (fd < 0) { perror("open"); return 1; } /* 复位设备 */ ioctl(fd, MYDEV_RESET); /* 读状态 */ uint32_t status; ioctl(fd, MYDEV_GET_STATUS, &status); printf("status = 0x%08x\n", status); /* 设置频率 */ uint32_t freq = 1000000; ioctl(fd, MYDEV_SET_FREQ, &freq); close(fd); return 0;}
五、完整的字符设备驱动:从 probe 到 file_operations
把上面所有东西串成一个完整的驱动,带 misc 设备注册(misc 是最简单的字符设备注册方式,不需要手动申请主设备号):
/* mydevice.c */#include <linux/module.h>#include <linux/platform_device.h>#include <linux/miscdevice.h>#include <linux/fs.h>#include <linux/of.h>#include <linux/io.h>#include <linux/interrupt.h>#include <linux/uaccess.h>#include "my_ioctl.h"/* 寄存器偏移(对应你的硬件手册) */#define DATA_REG 0x00#define CTRL_REG 0x04#define STATUS_REG 0x08#define RESET_REG 0x0C#define FREQ_REG 0x10struct mydev_priv { void __iomem *base; int irq;struct miscdevice miscdev; /* misc 设备,内嵌在私有数据里 */struct device *dev;};/* ── file_operations ── */static int mydev_open(struct inode *inode, struct file *file){ /* 从 misc 设备找回私有数据,存到 file->private_data */struct mydev_priv *priv = container_of(file->private_data, struct mydev_priv, miscdev); file->private_data = priv; dev_dbg(priv->dev, "opened\n"); return 0;}static int mydev_release(struct inode *inode, struct file *file){ return 0;}static ssize_t mydev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos){struct mydev_priv *priv = file->private_data; u32 val; if (count < sizeof(val)) return -EINVAL; val = ioread32(priv->base + DATA_REG); if (copy_to_user(buf, &val, sizeof(val))) return -EFAULT; return sizeof(val);}static ssize_t mydev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos){struct mydev_priv *priv = file->private_data; u32 val; if (count < sizeof(val)) return -EINVAL; if (copy_from_user(&val, buf, sizeof(val))) return -EFAULT; iowrite32(val, priv->base + CTRL_REG); return sizeof(val);}static long mydev_ioctl(struct file *file, unsigned int cmd, unsigned long arg){struct mydev_priv *priv = file->private_data; u32 val; if (_IOC_TYPE(cmd) != MYDEV_MAGIC) return -ENOTTY; switch (cmd) { case MYDEV_RESET: iowrite32(0x1, priv->base + RESET_REG); break; case MYDEV_GET_STATUS: val = ioread32(priv->base + STATUS_REG); if (copy_to_user((void __user *)arg, &val, sizeof(val))) return -EFAULT; break; case MYDEV_SET_FREQ: if (copy_from_user(&val, (void __user *)arg, sizeof(val))) return -EFAULT; iowrite32(val, priv->base + FREQ_REG); break; default: return -ENOTTY; } return 0;}static conststruct file_operations mydev_fops = { .owner = THIS_MODULE, .open = mydev_open, .release = mydev_release, .read = mydev_read, .write = mydev_write, .unlocked_ioctl = mydev_ioctl,};/* ── 中断处理 ── */static irqreturn_t mydev_isr(int irq, void *data){struct mydev_priv *priv = data; u32 status; status = ioread32(priv->base + STATUS_REG); /* 清中断标志(具体寄存器操作看你的硬件手册) */ iowrite32(status, priv->base + STATUS_REG); dev_dbg(priv->dev, "IRQ: status=0x%08x\n", status); return IRQ_HANDLED;}/* ── probe / remove ── */static int mydev_probe(struct platform_device *pdev){struct mydev_priv *priv;struct resource *res; int ret; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->dev = &pdev->dev; /* 获取并映射寄存器 */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); priv->base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); /* 获取中断 */ priv->irq = platform_get_irq(pdev, 0); if (priv->irq < 0) return priv->irq; ret = devm_request_irq(&pdev->dev, priv->irq, mydev_isr, 0, dev_name(&pdev->dev), priv); if (ret) return ret; /* 注册 misc 设备,内核自动分配次设备号,/dev/mydevice 自动创建 */ priv->miscdev.minor = MISC_DYNAMIC_MINOR; priv->miscdev.name = "mydevice"; priv->miscdev.fops = &mydev_fops; ret = misc_register(&priv->miscdev); if (ret) { dev_err(&pdev->dev, "misc_register failed: %d\n", ret); return ret; } platform_set_drvdata(pdev, priv); dev_info(&pdev->dev, "probe OK, /dev/mydevice ready\n"); return 0;}static int mydev_remove(struct platform_device *pdev){struct mydev_priv *priv = platform_get_drvdata(pdev); misc_deregister(&priv->miscdev); dev_info(&pdev->dev, "removed\n"); return 0;}static conststruct of_device_id mydev_of_match[] = { { .compatible = "myvendor,mydevice-v1" }, { }};MODULE_DEVICE_TABLE(of, mydev_of_match);staticstruct platform_driver mydev_driver = { .probe = mydev_probe, .remove = mydev_remove, .driver = { .name = "mydevice", .of_match_table = mydev_of_match, },};module_platform_driver(mydev_driver);MODULE_LICENSE("GPL");MODULE_DESCRIPTION("Full platform driver with char device example");
六、GPIO 中断触发的驱动
上面用的是 platform_get_irq(),它从设备树的 interrupts 属性取 GIC 直连的中断。但很多外设的中断线接在 GPIO 上,用法稍有不同。
设备树节点:
sensor_irq_node: mysensor@44 { compatible = "myvendor,mysensor"; reg = <0x44>; interrupt-parent = <&gpio1>; /* GPIO1 控制器 */ interrupts = <5 IRQ_TYPE_EDGE_FALLING>; /* GPIO1_IO05,下降沿触发 */};
驱动里申请 GPIO 中断有两种方法。
方法一:用 gpiod 接口(现代推荐写法)
#include <linux/gpio/consumer.h>static int mysensor_probe(struct platform_device *pdev){struct mysensor_priv *priv;struct gpio_desc *irq_gpio; int irq, ret; /* 从设备树的 interrupts 属性解析 GPIO,然后申请为中断 */ irq_gpio = devm_gpiod_get(&pdev->dev, NULL, GPIOD_IN); if (IS_ERR(irq_gpio)) return PTR_ERR(irq_gpio); /* 可能是 -EPROBE_DEFER */ /* 把 GPIO 转换成 Linux IRQ 号 */ irq = gpiod_to_irq(irq_gpio); if (irq < 0) return irq; ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, /* 硬中断上半部,NULL 表示不用 */ mysensor_irq_thread, /* 线程化下半部,在内核线程里执行 */ IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "mysensor-irq", priv); if (ret) return ret; return 0;}/* 线程化中断处理,可以睡眠,适合需要 I2C/SPI 通信的场景 */static irqreturn_t mysensor_irq_thread(int irq, void *data){struct mysensor_priv *priv = data; /* 这里可以做 I2C 读取,因为是线程上下文,可以睡眠 */ /* i2c_smbus_read_byte_data(priv->client, REG_STATUS); */ return IRQ_HANDLED;}
方法二:直接用 platform_get_irq()
如果设备树写的是 interrupt-parent = <&gic> 直连 GIC,或者内核在解析设备树时已经帮你把 GPIO 中断转换好了(很多平台会这样),就直接用 platform_get_irq(pdev, 0) 拿到 IRQ 号,跟之前一样处理。
线程化中断(request_threaded_irq)和普通中断的区别:
| | 线程化中断(request_threaded_irq) |
|---|
| | |
| | |
| | |
| | |
GPIO 上挂着 I2C 传感器中断的场景,几乎都应该用 request_threaded_irq,因为处理中断必须读 I2C,而 I2C 操作可能睡眠。
七、不同外设对应不同上层框架
platform driver 只是"把驱动和硬件配对、拿到寄存器地址"的机制,拿到硬件资源之后怎么向上层暴露,取决于外设类型。不是所有驱动都要注册字符设备。
SPI ADC / I2C ADC → IIO 框架
ADC、DAC 这类数据采集设备,应该注册进 IIO(Industrial I/O)框架,而不是自己写字符设备。IIO 框架统一管理采样、触发、缓冲,用户空间通过 /sys/bus/iio/devices/iio:device0/in_voltage0_raw 读数据,或者通过 /dev/iio:device0 做连续采样。[³]
/* probe() 里注册 IIO 设备 */static int myadc_probe(struct platform_device *pdev){struct iio_dev *indio_dev;struct myadc_priv *priv; /* 分配 iio_dev,同时分配驱动私有数据 */ indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*priv)); if (!indio_dev) return -ENOMEM; priv = iio_priv(indio_dev); /* ... 初始化硬件 ... */ indio_dev->name = "myadc"; indio_dev->modes = INDIO_DIRECT_MODE; indio_dev->info = &myadc_iio_info; /* 包含 read_raw 回调 */ indio_dev->channels = myadc_channels; indio_dev->num_channels = ARRAY_SIZE(myadc_channels); return devm_iio_device_register(&pdev->dev, indio_dev);}
用户空间读 ADC:
# 单次读取通道 0 的原始值cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw# 换算成电压(单位 mV)cat /sys/bus/iio/devices/iio:device0/in_voltage0_scale
不需要也不应该给 ADC 写 read()/ioctl(),IIO 框架已经帮你处理了用户空间接口。
SPI/I2C 屏幕 → framebuffer 或 DRM
屏幕驱动通常注册进 framebuffer 框架(/dev/fb0)或 DRM/KMS 框架。用户空间通过 mmap 把帧缓冲映射到内存直接写像素,或者用 ioctl(fd, FBIOGET_VSCREENINFO, ...) 查询屏幕参数。
屏幕驱动确实需要 ioctl——但这些 ioctl 是 framebuffer 框架或 DRM 框架统一定义的,不是每个屏幕驱动自己定义的。驱动只需要实现框架要求的回调(比如 fb_ops 里的 .fb_fillrect、.fb_copyarea),框架处理 ioctl 分发。
简单小尺寸 SPI 屏(如 ST7735、ILI9341)可以用内核的 mipi-dbi 框架(tinydrm 已在 5.4 合并进 drivers/gpu/drm/tiny/),让驱动变得很薄:
/* 基于 mipi-dbi 框架的 ILI9341 驱动骨架,极简 */static int ili9341_probe(struct spi_device *spi){struct mipi_dbi_dev *dbidev;struct mipi_dbi *dbi; /* ... */ mipi_dbi_init(spi, dc, dbi, &ili9341_pipe_funcs, &ili9341_mode); return mipi_dbi_dev_init(dbidev, &ili9341_funcs, &ili9341_mode);}
注意 SPI 屏的驱动入口是 spi_device,不是 platform_device,是 SPI 子系统管理的设备,和前面的 platform driver 走的是不同的总线。概念完全一样,换了个壳。
SPI DAC → IIO 框架(写通道)
DAC 也进 IIO,用户空间写 /sys/bus/iio/devices/iio:device1/out_voltage0_raw。实现上和 ADC 一样,iio_info 里提供 .write_raw 回调。
I2C 传感器(温湿度、IMU)→ IIO 或 hwmon
温湿度、气压等环境传感器:
- • 进 hwmon:
/sys/class/hwmon/hwmon0/temp1_input,适合只需要单次读取的简单场景
八、框架不止字符设备
总线-驱动-设备模型的 platform driver,在 probe() 里可以注册进任何内核子框架,不局限于字符设备:
| | |
|---|
| | /dev/xxx |
| | /sys/bus/iio/ |
| | /sys/class/hwmon/ |
| | /dev/input/eventX |
| | 内核内部 API,或 regulator sysfs |
| | |
| | /dev/rtc0,ioctl(RTC_WKALM_SET, ...) |
| | /dev/watchdog |
| | /sys/class/pwm/ |
| | |
同一个 SoC 上的 SPI 控制器本身是一个 platform driver,probe() 里注册 spi_master,之后挂在这条 SPI 总线上的设备(比如 ADC、屏幕)就变成了 spi_device,由 SPI 子系统管理。platform driver 可以是"总线控制器驱动",也可以是"设备驱动",两者都用同一套框架,只是 probe() 里注册的东西不同。[⁴]
[³] 参考:内核文档 "Industrial I/O Core",https://docs.kernel.org/driver-api/iio/core.html[⁴] 参考:内核文档 "Platform Devices and Drivers",https://www.kernel.org/doc/html/latest/driver-api/driver-model/platform.html
九、驱动编译和加载
Makefile
# 驱动模块obj-m += mydevice.o# 如果驱动由多个 .c 文件组成:# mydevice-objs := mydevice_core.o mydevice_spi.o# obj-m += mydevice.oKDIR ?= /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)all: $(MAKE) -C $(KDIR) M=$(PWD) modulesclean: $(MAKE) -C $(KDIR) M=$(PWD) clean
本机编译(x86,测试用):
make
交叉编译(目标板是 ARM):
make ARCH=arm \ CROSS_COMPILE=arm-linux-gnueabihf- \ KDIR=/path/to/your/kernel/source
内核源码路径必须是已经配置过(make defconfig 或 menuconfig)并且执行过 make modules_prepare 的,否则头文件不完整。
编译结果是 mydevice.ko,把它 scp 到目标板。
加载和卸载
# 加载模块(需要 root)insmod mydevice.ko# 加载时传参(如果驱动里有 module_param)insmod mydevice.ko debug_level=2# 查看加载是否成功lsmod | grep mydevicedmesg | tail -20 # 看 probe 的打印# 查看设备节点(misc_register 后自动创建)ls -l /dev/mydevice# 查看驱动绑定状态ls -l /sys/bus/platform/drivers/mydevice/# 里面有符号链接指向已绑定的 platform_device# 卸载rmmod mydevice# 持久化加载(放到这个目录,depmod 后开机自动加载)cp mydevice.ko /lib/modules/$(uname -r)/extra/depmod -aecho "mydevice" >> /etc/modules # 或 /etc/modules-load.d/mydevice.conf
调试信息级别
dev_info、dev_dbg、dev_err 对应不同级别,dev_dbg 默认不打印,需要开启:
# 方法一:动态开启特定驱动的 debug 打印echo "file mydevice.c +p" > /sys/kernel/debug/dynamic_debug/control# 方法二:编译时加 -DDEBUG# ccflags-y += -DDEBUG
十、完整的用户空间 App 示例
/* app.c —— 和驱动交互的完整示例 */#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <fcntl.h>#include <unistd.h>#include <sys/ioctl.h>#include "my_ioctl.h" /* 和驱动共享的头文件,包含 MYDEV_* 命令定义 */int main(void){ int fd; uint32_t val; ssize_t n; fd = open("/dev/mydevice", O_RDWR); if (fd < 0) { perror("open /dev/mydevice"); return EXIT_FAILURE; } /* 1. 发 ioctl 复位设备 */ if (ioctl(fd, MYDEV_RESET) < 0) { perror("ioctl MYDEV_RESET"); goto out; } /* 2. 用 ioctl 读状态寄存器 */ if (ioctl(fd, MYDEV_GET_STATUS, &val) < 0) { perror("ioctl MYDEV_GET_STATUS"); goto out; } printf("status = 0x%08x\n", val); /* 3. 用 write 写控制寄存器(对应驱动的 mydev_write) */ val = 0x00000001; /* 启动 */ n = write(fd, &val, sizeof(val)); if (n < 0) { perror("write"); goto out; } /* 4. 用 read 读数据寄存器(对应驱动的 mydev_read) */ n = read(fd, &val, sizeof(val)); if (n < 0) { perror("read"); goto out; } printf("data = 0x%08x\n", val);out: close(fd); return 0;}
编译:
# 本机测试gcc -o app app.c -I./include# 交叉编译arm-linux-gnueabihf-gcc -o app app.c -I./include
把 app scp 到目标板,直接运行即可。
小结
这套框架的完整链路:
设备树 compatible │ 匹配 ▼platform driver probe() │ 获取资源(寄存器地址、中断号) ▼初始化硬件 │ 注册进上层框架 ├──→ misc/cdev → file_operations(read/write/ioctl) ├──→ IIO → iio_info(read_raw/write_raw) ├──→ hwmon → hwmon_ops ├──→ input → input_dev └──→ framebuffer / DRM → fb_ops │ ▼ 用户空间 open / read / write / ioctl │ copy_to_user / copy_from_user │ 跨越内核/用户空间边界 用户 app
选哪个框架不是随意的——它决定了用户空间用什么工具读数据、内核里哪些子系统能复用这个驱动。ADC 进 IIO,屏幕进 DRM,按键进 input,不要每个外设都自己造字符设备。
参考资料
- 1. 内核源码:
drivers/base/platform.c,platform_match() — https://github.com/torvalds/linux/blob/master/drivers/base/platform.c - 2. Linux kernel labs:Character device drivers — https://linux-kernel-labs.github.io/refs/pull/189/merge/labs/device_drivers.html
- 3. 内核文档:Industrial I/O Core — https://docs.kernel.org/driver-api/iio/core.html
- 4. 内核文档:Platform Devices and Drivers — https://www.kernel.org/doc/html/latest/driver-api/driver-model/platform.html
- 5. 内核文档:Devres(devm_* 托管资源)— https://docs.kernel.org/driver-api/driver-model/devres.html
- 6. LWN.net:IOCTL and unlocked_ioctl — https://lwn.net/Articles/119652/
- 7. O'Reilly "Linux Device Drivers Development"(John Madieu,2nd ed.,2022)