Linux驱动-led、beep

上一次成功将Linux系统移植到IMX6ULL开发板上,今天我们来利用应用层和驱动层编写led和beep的代码。

一、Linux为什么要分成【应用层】和【驱动层】?

为什么不能直接写程序操作GPIO,非要拆成驱动+APP两套代码?

1. 用户态(应用层)与内核态(驱动层)隔离

Linux有内存保护机制:

应用层(用户态) :程序运行在用户空间,权限低,不能直接访问物理寄存器、硬件地址 。如果应用程序可以随便操作硬件,一旦程序越界、崩溃,会直接破坏整个系统。我们写的C语言APP、printf、open/write/read都跑在这里。

驱动层(内核态):内核拥有最高权限,可以操作GPIO、寄存器、中断等硬件资源。驱动运行在内核空间。

分层的目的:

  1. 安全隔离:应用程序崩溃只会杀死自己进程,不会让整个Linux内核panic死机;硬件操作全部收敛到内核驱动里统一管控。
  2. 统一接口 :Linux把所有硬件抽象成文件 ,一切皆文件。不管是LED、蜂鸣器、按键、串口,都能用open/read/write/close这套标准文件函数操作。应用层不需要关心底层是LED还是蜂鸣器。
  3. 解耦 :更换硬件(比如从IMX6ULL换到其他芯片),应用层代码几乎不用改动,只需要修改底层驱动。应用层只负责业务逻辑,驱动只负责硬件操作。

2. 应用层与驱动层如何交互?

交互通道:设备文件(/dev/xxx)

  1. 驱动加载(insmod xxx.ko):驱动在内核注册设备,生成/dev/beep_misc、/dev/led_misc设备节点。
  2. 应用层open("/dev/beep_misc", O_RDWR):打开设备文件,系统调用陷入内核,调用驱动里.open函数。
  3. 应用层write(fd, &buf, len):应用发起write系统调用,陷入内核 ,执行驱动注册的beep_write函数。
  4. copy_from_user():内核驱动函数把用户空间的数据拷贝到内核空间(用户空间和内核空间内存不能直接访问,必须使用这个拷贝函数)。
  5. 驱动拿到状态值,调用gpio_set_value()操作硬件GPIO,控制蜂鸣器/LED高低电平。
  6. 驱动返回,系统调用返回,应用层继续往下执行。

总结:APP调用标准文件API → 触发系统调用,陷入内核 → 执行驱动回调函数 → 驱动操作硬件。

二、两个关键架构:Misc混杂设备 + Platform平台总线

1. Misc杂项设备(miscdevice)有什么好处?

misc本质是简化版字符设备驱动,底层依然基于cdev字符设备封装。

原生cdev字符设备,我们需要手动做:

register_chrdev_region申请设备号 → cdev_init初始化cdev → cdev_add注册字符设备 → 手动创建设备节点class_create+device_create,代码量大,步骤繁琐。

misc设备优势

  1. 简化开发 :只需要填充struct miscdevice结构体,调用misc_register一行注册,自动完成cdev初始化、设备号分配、创建设备节点。不用手动管理class、device。
  2. 共用主设备号 10 :misc设备主设备号固定为10 ,只需要动态申请次设备号(MISC_DYNAMIC_MINOR),不用自己去申请、管理主设备号,省去设备号冲突的麻烦。
  3. 自动管理:misc_deregister卸载时自动删除/dev下的设备节点。

适合场景:简单的小型硬件外设,LED、蜂鸣器、按键这类简单字符设备。

2. Platform平台总线架构

Linux内核引入platform平台总线,核心目标:设备和驱动分离 。

在无platform框架的老式驱动中:硬件信息(GPIO引脚号、寄存器地址)直接硬编码写死在驱动代码里。一旦换硬件、改引脚,需要修改驱动源码,重新编译内核模块,耦合度极高。

platform架构把代码拆成两部分:

  1. 设备端 :设备树DTS。描述硬件信息:哪个GPIO、引脚功能、compatible匹配字符串,属于硬件描述。
  2. 驱动端 :platform_driver。写硬件操作逻辑,不写死硬件资源,属于驱动逻辑。

platform架构优势:

  1. 设备与驱动解耦 :硬件信息写在设备树,驱动只写操作逻辑。修改引脚,只修改设备树,不用重新编译驱动ko。
  2. 自动匹配 :内核启动时,扫描设备树,根据compatible字符串,匹配设备和驱动。匹配成功自动调用驱动的probe函数。
  3. 资源统一管理 :在probe函数中,通过of_get_named_gpio从设备树读取GPIO资源。
  4. 支持自动卸载资源 :搭配devm_系列资源申请函数(devm_gpio_request),驱动退出时自动释放GPIO资源,减少内存泄漏、资源忘记释放的bug。

匹配流程:内核扫描DTS → 找到compatible = "pute,myled"设备节点 → 和platform_driver里of_match_table对比字符串 → 匹配成功,执行probe函数。

3. Pinctl(引脚控制子系统)

Pinctl 子系统和 GPIO 子系统:

Pinctl :管理引脚复用功能、引脚电气属性 。一个芯片引脚可以复用为 GPIO、I2C、SPI、UART 等功能;Pinctl 负责配置引脚复用功能、上下拉、驱动强度、速度。

GPIO 子系统:引脚已经被 Pinctl 配置为 GPIO 模式之后,才用来控制高低电平、输入输出方向。

Pinctl 决定这个引脚是什么功能;GPIO 子系统控制引脚电平。

Pinctl 子系统优势:

  1. 引脚复用管理:SOC 引脚资源有限,一个引脚有多种复用功能,Pinctl 统一管理引脚功能选择,避免多个外设抢占同一个引脚。
  2. 引脚电气配置:配置上下拉电阻、驱动能力、信号速率,保证硬件电气稳定性。
  3. 全部在设备树配置,驱动代码零改动 :引脚复用配置写在 DTS 的pinctrl节点,驱动代码不需要操作寄存器配置引脚复用,内核 pinctl 驱动自动完成。
  4. 状态管理:支持默认状态、休眠状态,设备上电自动切换引脚功能,休眠自动切换引脚状态。

工作流程:内核解析 DTS 中的 pinctrl 节点 → pinctl 子系统配置引脚复用功能为 GPIO → 驱动代码才能正常使用 GPIO 子系统 APIgpio_direction_output、gpio_set_value。

三、LED驱动完整源码(led_platform_misc.c)

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <asm/uaccess.h>

static int ledgpiono = 0;  // 保存从设备树读到的LED gpio编号

// 对应应用层write系统调用的回调函数
static ssize_t led_write(struct file *fp, const char __user *puser, size_t n, loff_t *off)
{
    unsigned long nret = 0;
    int ledstat = 0;
    // copy_from_user:把用户空间的数据拷贝到内核空间
    // 参数:内核缓冲区,用户空间地址,拷贝字节数
    nret = copy_from_user(&ledstat, puser, n);
    if (nret) {
        pr_info("copy_from_user failed\n");
        return -1;
    }
    // 逻辑:应用传入1,GPIO输出低电平点亮LED;传入0,高电平熄灭
    if (1 == ledstat) {
        gpio_set_value(ledgpiono, 0);
    } else if (0 == ledstat) {
        gpio_set_value(ledgpiono, 1);
    }
    return n; // 返回成功写入的字节数,给应用层write返回值
}

// 文件操作集:把write函数注册进去,应用调用write就执行led_write
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .write = led_write,
};

// misc杂项设备结构体
static struct miscdevice led_misc = {
    .minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
    .name = "led_misc",          // 设备名字,生成 /dev/led_misc
    .fops = &fops,               // 绑定文件操作方法
};

// probe函数:设备和驱动匹配成功之后,内核自动调用probe
static int led_probe(struct platform_device *pdevice)
{
    int ret = 0;
    // 注册misc设备,自动生成/dev节点
    ret = misc_register(&led_misc);
    if (ret) {
        pr_info("misc_register failed\n");
        return -1;
    }
    // 从设备树节点中读取gpio-myled0属性,获取GPIO编号
    ledgpiono = of_get_named_gpio(pdevice->dev.of_node, "gpio-myled0", 0);
    if (ledgpiono < 0) {
        pr_info("of_get_named_gpio failed\n");
        return -1;
    }
    // devm_gpio_request:申请GPIO资源,devm托管,自动释放
    ret = devm_gpio_request(&pdevice->dev, ledgpiono, "led_platform_drv");
    if (ret) {
        pr_info("devm_gpio_request failed\n");
        return -1;
    }
    // 设置GPIO为输出方向,初始电平1(LED熄灭)
    gpio_direction_output(ledgpiono, 1);
    pr_info("led_probe success\n");
    return 0;
}

// remove函数:rmmod卸载模块的时候执行
static int led_remove(struct platform_device *pdevice)
{
    misc_deregister(&led_misc); // 注销misc设备,删除/dev节点
    pr_info("led_remove success\n");
    return 0;
}

// 传统id匹配表(用于非设备树匹配方式)
static struct platform_device_id led_id_tables[] = {
    { .name = "myled"},
    {},
};
// 设备树of匹配表,compatible字符串,和DTS里保持一致
static struct of_device_id  led_of_match_table[] = {
    {.compatible = "pute,myled"},
    {},
};

// platform驱动结构体,绑定probe、remove、匹配表
struct platform_driver led_drv = {
    .probe = led_probe,
    .remove = led_remove,
    .driver = {
        .name = "myled",
        .owner = THIS_MODULE,
        .of_match_table = led_of_match_table,
    },
    .id_table = led_id_tables,
};

// 简化宏,等价于注册platform_driver,自动实现module_init/module_exit
module_platform_driver(led_drv);

MODULE_LICENSE("GPL"); // 模块许可证,不加会内核警告

LED代码简要说明

  1. module_platform_driver:内核提供的宏,省去手动写module_init与module_exit;
  2. of_get_named_gpio:从设备树节点属性读取GPIO;
  3. devm_gpio_request:托管资源,驱动退出自动释放GPIO,无需手动gpio_free;
  4. misc_register:注册杂项设备,自动在/dev下生成led_misc。

四、蜂鸣器驱动完整源码(beep_platform_misc.c)

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <asm/uaccess.h>

static int beepgpiono = 0;

static ssize_t beep_write(struct file *fp, const char __user *puser, size_t n, loff_t *off)
{
    unsigned long nret = 0;
    int beepstat = 0;
    nret = copy_from_user(&beepstat, puser, n);
    if (nret) {
        pr_info("copy_from_user failed\n");
        return -1;
    }
    // 应用写入1,GPIO输出低,蜂鸣器响;写入0,高电平,蜂鸣器关闭
    if (1 == beepstat) {
        gpio_set_value(beepgpiono, 0);
    } else if (0 == beepstat) {
        gpio_set_value(beepgpiono, 1);
    }
    return n;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .write = beep_write,
};

static struct miscdevice beep_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "beep_misc",
    .fops = &fops,
};

static int beep_probe(struct platform_device *pdevice)
{
    int ret = 0;
    ret = misc_register(&beep_misc);
    if (ret) {
        pr_info("misc_register failed\n");
        return -1;
    }
    beepgpiono = of_get_named_gpio(pdevice->dev.of_node, "gpio-beep", 0);
    if (beepgpiono < 0) {
        pr_info("of_get_named_gpio failed\n");
        return -1;
    }
    ret = devm_gpio_request(&pdevice->dev, beepgpiono, "beep_platform_drv");
    if (ret) {
        pr_info("devm_gpio_request failed\n");
        return -1;
    }
    gpio_direction_output(beepgpiono, 1);
    pr_info("beep_probe success\n");
    return 0;
}

static int beep_remove(struct platform_device *pdevice)
{
    misc_deregister(&beep_misc);
    pr_info("beep_remove success\n");
    return 0;
}

static struct platform_device_id beep_id_tables[] = {
    { .name = "putebeep"},
    {},
};

static struct of_device_id  beep_of_match_table[] = {
    {.compatible = "pute,putebeep"},
    {},
};

struct platform_driver beep_drv = {
    .probe = beep_probe,
    .remove = beep_remove,
    .driver = {
        .name = "putebeep",
        .owner = THIS_MODULE,
        .of_match_table = beep_of_match_table,
    },
    .id_table = beep_id_tables,
};

module_platform_driver(beep_drv);
MODULE_LICENSE("GPL");

蜂鸣器驱动逻辑和LED几乎完全一致,只修改设备树匹配字符串、GPIO属性名称、misc设备名。两个驱动是独立模块,分开编译,分别加载。

五、蜂鸣器应用层APP完整源码(beep_app.c)

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(void) {
    int fd = 0;
    int beepstat = 0;
    int curstat = 0;
    // 打开misc驱动生成的设备文件 /dev/beep_misc
    fd = open("/dev/beep_misc", O_RDWR);
    if (-1 == fd)
    {
        perror("fail to open");
        return -1;
    }
    while (1)
    {
        beepstat = 1;
        write(fd, &beepstat, sizeof(beepstat)); // 写1到驱动,蜂鸣器响
        sleep(1);
        beepstat = 0;
        write(fd, &beepstat, sizeof(beepstat)); // 写0到驱动,蜂鸣器关闭
        sleep(1);
    }
    close(fd);
    return 0;
}

APP执行流程

  1. open打开/dev/beep_misc设备文件,获取文件描述符fd;
  2. while循环:每隔1秒交替向驱动写入1和0;
  3. write触发系统调用,进入内核,执行驱动beep_write;
  4. 驱动通过copy_from_user拿到beepstat的值,控制GPIO输出高低电平,实现蜂鸣器1秒响、1秒停。

LED的应用程序写法完全一样,打开/dev/led_misc,循环写入0/1控制亮灭。

六、设备树DTS节点(添加到板级dts文件)

驱动依赖设备树描述硬件资源,这里附上对应的DTS节点。

复制代码
myled {
    compatible = "pute,myled";
    gpio-myled0 = <&gpio1 3 GPIO_ACTIVE_LOW>;
    status = "okay";
};

putebeep {
    compatible = "pute,putebeep";
    gpio-beep = <&gpio1 4 GPIO_ACTIVE_LOW>;
    status = "okay";
};
  • compatible必须和驱动of_match_table完全一致,大小写敏感;
  • gpio编号根据你的开发板原理图修改。修改DTS后需要重新编译设备树,烧录到开发板。

这次实验,我们通过platform平台总线实现软硬件解耦 ,用misc杂项设备简化字符设备注册流程。

这套分层架构是Linux驱动的经典思想:驱动负责硬件底层操作,应用层负责业务逻辑,通过设备文件、系统调用完成交互 。

LED、蜂鸣器属于最简单的外设,掌握这套模型,后续按键、SPI等外设驱动的思路都是相通的。

相关推荐
mklpo1471 小时前
为不支持sse4.2命令集的老旧设备制作Win11 26H2系统镜像,2026-10-11。
运维·windows·python
2601_950790682 小时前
国产单片机厂家推荐:英锐恩8位MCU系列、外设资源与应用选型对照
单片机
碧海银沙音频科技研究院2 小时前
炬芯ATS308X的扩音器:麦克风到扬声器直通功能实现
java·linux·运维
睡一觉就好了。2 小时前
序列化与非序列化
linux·网络
想做小南娘,发现自己是女生喵2 小时前
第三周学习文档 Linux C 多进程与多线程
linux·c语言·学习
北京盛世宏博2 小时前
异地机房采集踩坑笔记:以太网温湿度记录仪跨网段通信调试实战
开发语言·笔记·单片机·php·传感器·温湿度
吴声子夜歌2 小时前
Docker入门与实战——实战案例(编程开发)(一)
运维·docker·容器
空空dream2 小时前
04 | 用 Jenkins 打通 CI自动构建镜像并推送到本地 Harbor
运维·ci/cd·jenkins
海宇AI2 小时前
零信任架构实战:基于海宇运营商近3个月平均账单构建自动化免押租赁授信网关
运维·人工智能·架构·自动化