一、Misc 杂项设备驱动
1.1 为什么用 Misc?
传统字符设备注册需要 4 步:
alloc_chrdev_region() → 申请设备号
cdev_init() → 初始化 cdev
cdev_add() → 注册 cdev
class_create() + device_create() → 创建 /dev 节点
Misc 把以上 4 步封装成 1 行 :misc_register(),专为 LED、按键等轻量设备设计,主设备号固定为 10,次设备号动态分配。
1.2 核心代码结构(misc_led.c)
① 头文件
c
#include <linux/miscdevice.h> // Misc 驱动必须包含
#include <linux/fs.h> // file_operations
#include <asm/io.h> // ioremap / iounmap
#include <asm/uaccess.h> // copy_from_user
② 硬件寄存器地址(写死在驱动里,第一代写法)
c
#define IOMUXC_SW_MUX 0x20e0068U // 引脚复用寄存器
#define IOMUXC_SW_PAD 0x20E02F4U // 引脚电气属性寄存器
#define GPIO1_DR 0x209C000U // GPIO 数据寄存器
#define GPIO1_GDIR 0x209C004U // GPIO 方向寄存器
static volatile unsigned int *sw_mux;
static volatile unsigned int *sw_pad;
static volatile unsigned int *gpio1_dr;
static volatile unsigned int *gpio1_gdir;
为什么加 volatile? 告诉编译器"不要优化这个变量的读写",因为它映射的是真实硬件寄存器,值随时可能被硬件改变。
③ LED 操作函数
c
static void led_init(void)
{
*sw_mux = 0x05; // 设置引脚为 GPIO 模式
*sw_pad = 0x10b0; // 设置引脚电气属性
*gpio1_gdir |= (1 << 3); // GPIO1_IO03 设为输出
*gpio1_dr |= (1 << 3); // 默认输出高(LED 灭)
}
static void led_on(void) { *gpio1_dr &= ~(1 << 3); } // 输出低 → LED 亮
static void led_off(void) { *gpio1_dr |= (1 << 3); } // 输出高 → LED 灭
④ file_operations 和 miscdevice
c
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = open,
.read = read,
.write = write,
.release = close
};
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR, // 自动分配次设备号
.name = "led", // 对应 /dev/led
.fops = &fops
};
⑤ write 函数(应用层通信核心)
c
static ssize_t write(struct file *file, const char __user *buf,
size_t len, loff_t *offset)
{
unsigned char data[10] = {0};
// 取 len 和 sizeof(data) 中较小的值,防止溢出
size_t len_cp = len < sizeof(data) ? len : sizeof(data);
int size_cp = copy_from_user(data, buf, len_cp);
if (size_cp < 0)
return size_cp;
if (!strcmp(buf, "ledon"))
led_on();
else if (!strcmp(buf, "ledoff"))
led_off();
else
return -EINVAL;
return size_cp;
}
注意 :
strcmp比较的是buf(内核拷贝后的指针),不是data,这里代码实际上比较的是用户空间指针buf,正确做法应该是比较data。课堂代码存在此小问题,实际项目应改为strcmp(data, "ledon")。
⑥ init / exit 函数
c
static int __init led1_init(void)
{
int ret = misc_register(&misc); // ① 注册 /dev/led
if (ret < 0) goto err_misc;
// ② ioremap 映射物理地址到内核虚拟地址
sw_mux = ioremap(IOMUXC_SW_MUX, 4);
sw_pad = ioremap(IOMUXC_SW_PAD, 4);
gpio1_dr = ioremap(GPIO1_DR, 4);
gpio1_gdir = ioremap(GPIO1_GDIR, 4);
printk("led_init success\n");
return 0;
err_misc:
misc_deregister(&misc);
return ret;
}
static void __exit led1_exit(void)
{
iounmap(gpio1_gdir); // 逆序释放
iounmap(gpio1_dr);
iounmap(sw_pad);
iounmap(sw_mux);
misc_deregister(&misc); // 注销设备,/dev/led 消失
}
module_init(led1_init);
module_exit(led1_exit);
MODULE_LICENSE("GPL"); // 必须声明,否则内核报警告
1.3 编译步骤
方式一:编译进内核(不推荐调试阶段)
bash
# 在内核 make menuconfig 里把驱动选为 Y
make menuconfig
make zImage # 重新编译内核
方式二:编译为独立模块 .ko(推荐)
第一步:写 Makefile(和驱动放在同一目录)
makefile
# 内核源码路径(根据你的环境修改)
KDIR := /path/to/linux-kernel
obj-m := misc_led.o # 要编译的模块名
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
第二步:编译
bash
make
# 生成 misc_led.ko
第三步:拷贝到开发板并加载
bash
# 通过 TFTP / SCP / U盘 把 .ko 传到开发板
insmod misc_led.ko # 加载模块
lsmod # 查看已加载的模块
cat /proc/devices # 查看设备,找主设备号 10
ls /dev/led # 确认 /dev/led 节点已创建
rmmod misc_led # 卸载模块(注意:用模块名不加.ko)
第四步:应用层测试程序
c
// test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("/dev/led", O_RDWR);
write(fd, "ledon", 5);
sleep(1);
write(fd, "ledoff", 6);
close(fd);
return 0;
}
bash
# 交叉编译测试程序(ARM 平台)
arm-linux-gnueabihf-gcc test.c -o test
./test
二、Platform 驱动模型
2.1 为什么要用 Platform?
| 问题 | Misc 写法的缺陷 |
|---|---|
| 换板子 | 必须修改驱动代码,重新编译内核 |
| 多个 LED | 每个 LED 写一套驱动,代码冗余 |
Platform 模型把一个驱动文件拆成两个:
led_device.c → 描述"硬件在哪"(寄存器地址、资源)
led_driver.c → 描述"怎么操作硬件"(控制逻辑)
换板子 → 只改 led_device.c,led_driver.c 一行不动。
2.2 Platform 总线的匹配机制
Platform 总线
├── device 链表
│ └── { name="led", resource=[4个寄存器地址] } ← led_device.c 注册
└── driver 链表
└── { name="led", probe=..., remove=... } ← led_driver.c 注册
每次新注册时,总线对比 device.name == driver.name
→ 匹配成功 → 调用 probe(pdev)
→ rmmod device 时 → 调用 remove(pdev)
2.3 led_device.c 代码解析
① resource 数组(硬件资源清单)
c
static struct resource res[4] = {
[0] = {
.start = 0x20e0068, // 寄存器起始地址
.end = 0x20e0068 + 4 - 1, // 结束地址(闭区间:start 到 start+3)
.name = "iomuxc_sw_mux" // 名字(调试用)
},
[1] = { .start = 0x20E02F4, .end = 0x20E02F4 + 4 - 1, .name = "iomuxc_sw_pad" },
[2] = { .start = 0x209C004, .end = 0x209C004 + 4 - 1, .name = "gpio1_gdir" },
[3] = { .start = 0x209C000, .end = 0x209C000 + 4 - 1, .name = "gpio1_dr" }
};
为什么 end = start + 4 - 1? 寄存器占 4 字节,地址范围是 [start, start+3],闭区间长度公式:
end = start + size - 1。
② platform_device 结构体
c
static void release(struct device *dev) {} // 必须提供,否则内核报警告
static struct platform_device dev = {
.name = "led", // ← 关键!必须和 driver 的 name 完全一致
.id = -1, // 只有一个设备时写 -1
.num_resources = 4, // resource 数组元素个数
.resource = res,
.dev = { .release = release }
};
③ 注册与注销
c
static int __init led_init(void)
{
int ret = platform_device_register(&dev);
if (ret < 0) goto err_reg;
return 0;
err_reg:
platform_device_unregister(&dev);
return ret;
}
static void __exit led_exit(void)
{
platform_device_unregister(&dev);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
2.4 led_driver.c 代码解析
① probe 函数(匹配成功后的初始化入口)
c
static int probe(struct platform_device *pdev)
// ↑
// pdev 就是 led_device.c 里的 dev,内核 match 成功后传进来
{
int ret = misc_register(&misc); // ① 创建 /dev/led
if (ret < 0) goto err_misc;
// ② 从 pdev 取地址,做 ioremap
// ioremap(起始地址, 大小)
// 大小 = end - start + 1(闭区间求长度公式)
sw_mux = ioremap(
pdev->resource[0].start,
pdev->resource[0].end - pdev->resource[0].start + 1
);
sw_pad = ioremap(pdev->resource[1].start,
pdev->resource[1].end - pdev->resource[1].start + 1);
gpio1_gdir = ioremap(pdev->resource[2].start,
pdev->resource[2].end - pdev->resource[2].start + 1);
gpio1_dr = ioremap(pdev->resource[3].start,
pdev->resource[3].end - pdev->resource[3].start + 1);
return 0;
err_misc:
misc_deregister(&misc);
return ret;
}
pdev->resource[0].start 是什么? 就是
led_device.c里res[0].start = 0x20e0068。内核通过 pdev 把整个 device 结构体传给驱动,驱动这样拿到地址,不用自己写死。
② remove 函数(设备卸载时清理)
c
static int remove(struct platform_device *pdev)
{
iounmap(gpio1_gdir); // 逆序释放(和 probe 申请顺序相反)
iounmap(gpio1_dr);
iounmap(sw_pad);
iounmap(sw_mux);
misc_deregister(&misc);
return 0;
}
③ platform_driver 结构体和注册
c
static struct platform_driver drv = {
.probe = probe, // 匹配成功时内核自动调用
.remove = remove, // 设备卸载时内核自动调用
.driver = {
.name = "led" // ← 必须和 device 的 name 一模一样!
}
};
static int __init led1_init(void)
{
platform_driver_register(&drv);
return 0;
}
static void __exit led1_exit(void)
{
platform_driver_unregister(&drv);
}
module_init(led1_init);
module_exit(led1_exit);
MODULE_LICENSE("GPL");
2.5 完整运行流程
insmod led_device.ko
└─ platform_device_register("led", resource=[4个地址])
内核:有没有叫 "led" 的 driver?→ 没有,先等
insmod led_driver.ko
└─ platform_driver_register(name="led")
内核:有没有叫 "led" 的 device?→ 有!
match 成功 → 调用 probe(pdev)
→ misc_register() → /dev/led 出现
→ ioremap × 4
应用层:
open("/dev/led") → write("ledon") → LED 亮
rmmod led_driver.ko
└─ remove() 自动调用
→ iounmap × 4
→ misc_deregister() → /dev/led 消失
2.6 编译步骤
Makefile(两个模块写在一个 Makefile)
makefile
KDIR := /path/to/linux-kernel
obj-m := led_device.o led_driver.o
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
编译与加载
bash
make # 生成 led_device.ko 和 led_driver.ko
# 加载顺序无所谓,先后都可以
insmod led_device.ko # 注册硬件资源
insmod led_driver.ko # 注册驱动,触发 probe → /dev/led 出现
lsmod # 查看两个模块都在
rmmod led_driver # 先卸载 driver(触发 remove)
rmmod led_device # 再卸载 device
加载顺序无所谓:后加载的那个会遍历对方链表,触发 match。先 device 后 driver,或先 driver 后 device 均可。
2.7 关键易错点
| 易错点 | 说明 |
|---|---|
| name 大小写不一致 | "led" 和 "Led" 不匹配,probe 不会被调用 |
| 忘记写 release | platform_device.dev.release 必须有,否则内核报警告 |
| resource flag 设错 | 内存资源用 IORESOURCE_MEM,IO端口用 IORESOURCE_IO,设错会崩溃 |
| end 计算错误 | end = start + size - 1,不是 start + size |
三、DTS 设备树
3.1 三代写法演进
| 代次 | 地址在哪 | 换板子代价 |
|---|---|---|
| 第一代:地址写死在驱动 | 驱动 .c 文件 | 改驱动 + 重编内核 |
| 第二代:platform_device | led_device.c | 改 led_device.c + 重编内核 |
| 第三代:DTS | .dts 文本文件 | 只改 .dts + 重编 DTB,内核不动 |
3.2 DTS / DTB 关系
你写的文本文件 编译 内核启动时读取
pt.dts ──dtc──► pt.dtb ──U-Boot──► 内核解析 → 建节点树
(人类可读) (二进制)
- DTS:Device Tree Source,文本格式,你来写
- DTB:Device Tree Blob,二进制,内核识别
- dtc:编译器,把 DTS 编译成 DTB
3.3 DTS 语法结构
dts
/ { /* 根节点,固定写法 */
pt_led { /* 自定义 LED 节点,名字随意 */
compatible = "pt-led"; /* 驱动匹配字符串,驱动里要一致 */
name1 = "myled"; /* 自定义字符串属性 */
reg = < /* 寄存器地址和大小,成对出现 */
0x20e0068 4 /* sw_mux: 地址 大小 */
0x20E02F4 4 /* sw_pad: 地址 大小 */
0x209C004 4 /* gpio1_gdir:地址 大小 */
0x209C000 4 /* gpio1_dr: 地址 大小 */
>;
status = "okay"; /* 必须是 okay,否则内核忽略此节点 */
};
};
关键属性说明
| 属性 | 含义 |
|---|---|
compatible |
驱动匹配依据,驱动里 of_find_node_by_path 找到后可读取 |
reg |
寄存器地址和长度,成对 出现:<地址 长度 地址 长度 ...> |
status |
"okay" 启用,"disabled" 禁用(禁用则节点不可见) |
| 自定义属性 | 随意命名,驱动用 of_property_read_string 读取 |
3.4 DTS 编译步骤
第一步:把 .dts 放到内核目录
bash
cp pt.dts arch/arm/boot/dts/
第二步:修改 Makefile,让编译系统知道要编译它
makefile
# 文件:arch/arm/boot/dts/Makefile
# 在对应 CONFIG 下面加一行
dtb-$(CONFIG_ARCH_MX6) += pt.dtb
第三步:编译
bash
# 只编译这一个设备树
make pt.dtb
# 或编译所有设备树
make dtbs
第四步:部署到开发板
bash
# 方式1:TFTP 传输
# 在 U-Boot 里:tftp 0x83000000 pt.dtb
# 方式2:拷贝到 SD 卡 boot 分区
cp pt.dtb /media/sdcard/boot/
# U-Boot 会在启动时把 DTB 加载并传给内核
# bootm ${kernel_addr} - ${fdt_addr}
3.5 led_dts.c 代码解析
① 新增头文件
c
#include <linux/of.h> // of = Open Firmware,设备树所有 API 在这里
② init 函数里读设备树的完整流程
c
static int __init led1_init(void)
{
struct device_node *pnode; // 设备节点指针
const char *pcom; // compatible 字符串
const char *pname1; // name1 字符串
u32 led_array[8] = {0}; // reg 属性:8个u32 = 4组(地址+长度)
// ① 注册字符设备(先建 /dev/led)
int ret = misc_register(&misc);
if (ret < 0) goto err_misc;
// ② 按路径查找设备树节点(路径必须和 .dts 里节点名一致)
pnode = of_find_node_by_path("/pt_led");
if (pnode == NULL) {
printk("of_find_node_by_path err\n");
return -1;
}
// ③ 读字符串属性
of_property_read_string(pnode, "compatible", &pcom);
of_property_read_string(pnode, "name1", &pname1);
printk("compatible=%s name1=%s\n", pcom, pname1);
// ④ 读 reg 数组(8个u32)
of_property_read_u32_array(pnode, "reg", led_array,
sizeof(led_array) / sizeof(led_array[0]));
// ⑤ 用读出来的地址做 ioremap
sw_mux = ioremap(led_array[0], led_array[1]);
sw_pad = ioremap(led_array[2], led_array[3]);
gpio1_gdir = ioremap(led_array[4], led_array[5]);
gpio1_dr = ioremap(led_array[6], led_array[7]);
return 0;
err_misc:
misc_deregister(&misc);
return ret;
}
③ led_array 的内容对应关系
DTS 里:
reg = <0x20e0068 4 0x20E02F4 4 0x209C004 4 0x209C000 4>;
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
led_array[0] [1] [2] [3] [4] [5] [6] [7]
led_array[0] = 0x20e0068 (sw_mux 地址)
led_array[1] = 4 (sw_mux 大小)
led_array[2] = 0x20E02F4 (sw_pad 地址)
led_array[3] = 4 (sw_pad 大小)
led_array[4] = 0x209C004 (gpio1_gdir 地址)
led_array[5] = 4 (gpio1_gdir 大小)
led_array[6] = 0x209C000 (gpio1_dr 地址)
led_array[7] = 4 (gpio1_dr 大小)
所以:ioremap(led_array[偶数下标], led_array[奇数下标])
3.6 常用 of_xxx API 速查
c
// ① 按路径找节点
struct device_node *of_find_node_by_path(const char *path);
// 示例:of_find_node_by_path("/pt_led")
// ② 读字符串属性
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string);
// 示例:of_property_read_string(pnode, "compatible", &pcom)
// ③ 读单个 u32
int of_property_read_u32(struct device_node *np,
const char *propname,
u32 *out_value);
// ④ 读 u32 数组
int of_property_read_u32_array(struct device_node *np,
const char *propname,
u32 *out_values,
size_t sz); // sz = 要读几个
// 示例:of_property_read_u32_array(pnode, "reg", led_array, 8)
3.7 led_dts.c 编译步骤
Makefile
makefile
KDIR := /path/to/linux-kernel
obj-m := led_dts.o
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
编译与加载
bash
# 1. 先编译并部署 DTB(只需做一次,重启后生效)
make pt.dtb
cp arch/arm/boot/dts/pt.dtb /media/sdcard/boot/
# 重启开发板,使 DTB 生效
# 2. 编译驱动模块
make
# 生成 led_dts.ko
# 3. 加载模块(DTB 已在内核里,直接用 of_find_node 找节点)
insmod led_dts.ko
# 应该看到 printk 输出:compatible=pt-led name1=myled
# 4. 测试
./test_app # write("ledon") / write("ledoff")
# 5. 卸载
rmmod led_dts
注意 :DTS 方案不再需要
led_device.c,设备树的 pt.dts 已经替代了它的功能。只需要加载一个led_dts.ko。
3.8 关键易错点
| 易错点 | 说明 |
|---|---|
| 节点路径写错 | of_find_node_by_path("/pt_led") 必须和 DTS 节点名完全一致 |
| status 不是 "okay" | 写 "disabled" 或不写,节点对驱动不可见,返回 NULL |
| reg 成对规则 | <地址 大小 地址 大小>,读进数组后偶数下标=地址,奇数下标=大小 |
| DTB 未更新就重启 | 改了 .dts 但没有重新编译 DTB 并部署,节点还是旧的 |
四、三种写法完整对比
| 对比项 | misc_led.c(第一代) | Platform(第二代) | DTS(第三代) |
|---|---|---|---|
| 硬件地址在哪 | 驱动 .c 里的宏定义 | led_device.c 的 resource | pt.dts 文件 |
| 换板子改什么 | 驱动代码 + 重编内核 | led_device.c + 重编内核 | 只改 .dts + 重编 DTB |
| 内核需要重编 | 是 | 是 | 否 |
| 驱动文件数量 | 1个 | 2个(device + driver) | 1个(无需 device.c) |
| 初始化入口 | __init 函数 |
probe 函数(内核自动调用) |
__init 函数 |
| 现代内核推荐 | 不推荐 | 部分场景 | 推荐 |
五、总结一句话
- Misc :
misc_register()一行注册,省掉 4 步繁琐流程,适合快速原型。 - Platform:设备资源和驱动逻辑分离,换板子只改 device.c,同一 driver 服务多个设备。
- DTS:把硬件信息彻底搬出内核代码,放进独立 .dts 文件,同一内核镜像配不同 DTB 跑不同板子。