ARM-04-驱动-Misc ,Platform ,DTS

一、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.cled_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.cres[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 函数
现代内核推荐 不推荐 部分场景 推荐

五、总结一句话

  • Miscmisc_register() 一行注册,省掉 4 步繁琐流程,适合快速原型。
  • Platform:设备资源和驱动逻辑分离,换板子只改 device.c,同一 driver 服务多个设备。
  • DTS:把硬件信息彻底搬出内核代码,放进独立 .dts 文件,同一内核镜像配不同 DTB 跑不同板子。
相关推荐
never forget shyang2 小时前
CCS20.2.0使用教程
c语言·git·单片机
UTP协同自动化测试11 小时前
物联网模组测试难点 |APP指令下发+UART 响应+GPIO 电平变化,如何一次性验证?
功能测试·嵌入式硬件·物联网·模块测试
yoyobravery12 小时前
蓝桥杯第15届单片机满分
单片机·职场和发展·蓝桥杯
4caf114 小时前
作业2:6位数码管静态显示
嵌入式硬件·51单片机
不做无法实现的梦~14 小时前
STM32解析PPM协议
stm32·单片机·嵌入式硬件
czhaii15 小时前
基于Arm Cortex-M7内核GD32H7
单片机·嵌入式硬件
番茄灭世神15 小时前
MCU开发常见软件BUG总结(持续更新)
c语言·stm32·单片机·嵌入式·gd32
fenglllle15 小时前
使用AI能力编译ARM版本的截图软件
arm开发·人工智能
wanghanjiett15 小时前
双轮平衡车建模及控制 2 PID控制原理与调参
嵌入式硬件·控制算法