上一次成功将Linux系统移植到IMX6ULL开发板上,今天我们来利用应用层和驱动层编写led和beep的代码。
一、Linux为什么要分成【应用层】和【驱动层】?
为什么不能直接写程序操作GPIO,非要拆成驱动+APP两套代码?
1. 用户态(应用层)与内核态(驱动层)隔离
Linux有内存保护机制:
应用层(用户态) :程序运行在用户空间,权限低,不能直接访问物理寄存器、硬件地址 。如果应用程序可以随便操作硬件,一旦程序越界、崩溃,会直接破坏整个系统。我们写的C语言APP、printf、open/write/read都跑在这里。
驱动层(内核态):内核拥有最高权限,可以操作GPIO、寄存器、中断等硬件资源。驱动运行在内核空间。
分层的目的:
- 安全隔离:应用程序崩溃只会杀死自己进程,不会让整个Linux内核panic死机;硬件操作全部收敛到内核驱动里统一管控。
- 统一接口 :Linux把所有硬件抽象成文件 ,一切皆文件。不管是LED、蜂鸣器、按键、串口,都能用
open/read/write/close这套标准文件函数操作。应用层不需要关心底层是LED还是蜂鸣器。 - 解耦 :更换硬件(比如从IMX6ULL换到其他芯片),应用层代码几乎不用改动,只需要修改底层驱动。应用层只负责业务逻辑,驱动只负责硬件操作。
2. 应用层与驱动层如何交互?
交互通道:设备文件(/dev/xxx)
- 驱动加载(
insmod xxx.ko):驱动在内核注册设备,生成/dev/beep_misc、/dev/led_misc设备节点。 - 应用层
open("/dev/beep_misc", O_RDWR):打开设备文件,系统调用陷入内核,调用驱动里.open函数。 - 应用层
write(fd, &buf, len):应用发起write系统调用,陷入内核 ,执行驱动注册的beep_write函数。 copy_from_user():内核驱动函数把用户空间的数据拷贝到内核空间(用户空间和内核空间内存不能直接访问,必须使用这个拷贝函数)。- 驱动拿到状态值,调用
gpio_set_value()操作硬件GPIO,控制蜂鸣器/LED高低电平。 - 驱动返回,系统调用返回,应用层继续往下执行。
总结:APP调用标准文件API → 触发系统调用,陷入内核 → 执行驱动回调函数 → 驱动操作硬件。
二、两个关键架构:Misc混杂设备 + Platform平台总线
1. Misc杂项设备(miscdevice)有什么好处?
misc本质是简化版字符设备驱动,底层依然基于cdev字符设备封装。
原生cdev字符设备,我们需要手动做:
register_chrdev_region申请设备号 → cdev_init初始化cdev → cdev_add注册字符设备 → 手动创建设备节点class_create+device_create,代码量大,步骤繁琐。
misc设备优势
- 简化开发 :只需要填充
struct miscdevice结构体,调用misc_register一行注册,自动完成cdev初始化、设备号分配、创建设备节点。不用手动管理class、device。 - 共用主设备号 10 :misc设备主设备号固定为10 ,只需要动态申请次设备号(
MISC_DYNAMIC_MINOR),不用自己去申请、管理主设备号,省去设备号冲突的麻烦。 - 自动管理:
misc_deregister卸载时自动删除/dev下的设备节点。
适合场景:简单的小型硬件外设,LED、蜂鸣器、按键这类简单字符设备。
2. Platform平台总线架构
Linux内核引入platform平台总线,核心目标:设备和驱动分离 。
在无platform框架的老式驱动中:硬件信息(GPIO引脚号、寄存器地址)直接硬编码写死在驱动代码里。一旦换硬件、改引脚,需要修改驱动源码,重新编译内核模块,耦合度极高。
platform架构把代码拆成两部分:
- 设备端 :设备树DTS。描述硬件信息:哪个GPIO、引脚功能、compatible匹配字符串,属于硬件描述。
- 驱动端 :platform_driver。写硬件操作逻辑,不写死硬件资源,属于驱动逻辑。
platform架构优势:
- 设备与驱动解耦 :硬件信息写在设备树,驱动只写操作逻辑。修改引脚,只修改设备树,不用重新编译驱动ko。
- 自动匹配 :内核启动时,扫描设备树,根据
compatible字符串,匹配设备和驱动。匹配成功自动调用驱动的probe函数。 - 资源统一管理 :在probe函数中,通过
of_get_named_gpio从设备树读取GPIO资源。 - 支持自动卸载资源 :搭配
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 子系统优势:
- 引脚复用管理:SOC 引脚资源有限,一个引脚有多种复用功能,Pinctl 统一管理引脚功能选择,避免多个外设抢占同一个引脚。
- 引脚电气配置:配置上下拉电阻、驱动能力、信号速率,保证硬件电气稳定性。
- 全部在设备树配置,驱动代码零改动 :引脚复用配置写在 DTS 的
pinctrl节点,驱动代码不需要操作寄存器配置引脚复用,内核 pinctl 驱动自动完成。 - 状态管理:支持默认状态、休眠状态,设备上电自动切换引脚功能,休眠自动切换引脚状态。
工作流程:内核解析 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代码简要说明
module_platform_driver:内核提供的宏,省去手动写module_init与module_exit;of_get_named_gpio:从设备树节点属性读取GPIO;devm_gpio_request:托管资源,驱动退出自动释放GPIO,无需手动gpio_free;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执行流程
open打开/dev/beep_misc设备文件,获取文件描述符fd;- while循环:每隔1秒交替向驱动写入1和0;
write触发系统调用,进入内核,执行驱动beep_write;- 驱动通过
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等外设驱动的思路都是相通的。