一、 驱动工程搭建:模块的加载与卸载
在Linux驱动开发中,我们可以将驱动代码编译进内核(静态加载),也可以编译成模块(.ko文件),在需要时动态加载(动态加载)。动态加载更加灵活,是我们开发调试时的首选。
1. 模块加载与卸载函数
Linux内核为我们提供了两个宏来声明模块的入口和出口:
| 函数名 | 功能 |
|---|---|
module_init |
驱动模块加载入口(insmod时调用) |
module_exit |
驱动模块卸载入口(rmmod时调用) |
2. 驱动初始化流程(了解内核启动顺序)
内核启动时,各个驱动的初始化是有严格顺序的。Linux通过不同的initcall级别来管理:
| 函数名 | 功能 | 级别 |
|---|---|---|
pure_initcall |
内存池、基础硬件自检加载,最早执行 | 0 |
core_initcall |
调度器、中断器、时钟源内核核心初始化 | 1 |
postcore_initcall |
CPU缓存、总线控制器,核心功能完成后初始化 | 2 |
arch_initcall |
Soc外设、平台设备注册,架构相关初始化 | 3 |
subsys_initcall |
网络协议栈、文件系统框架,子系统初始化 | 4 |
fs_initcall |
虚拟文件系统VFS、根文件系统挂载 | 5 |
device_initcall |
字符设备、块设备、I2C控制器、设备驱动初始化 | 6 |
late_initcall |
调试接口、延迟加载模块,最后阶段初始化 | 7 |
注意 :我们编写的一般驱动通常属于
device_initcall级别,也就是第6级。
二、 字符设备驱动框架
字符设备是Linux驱动中最基础的一类设备(如LED、按键、串口等)。它的核心思想是:应用层通过open/read/write等标准文件接口,经过VFS(虚拟文件系统),最终调用到驱动层对应的操作函数。
1. 字符设备流程
字符设备驱动可以通过静态加载 (编译进内核)和动态加载(insmod)两种方式加载。我们主要采用动态加载。
核心流程如下:
- 创建一个
cdev结构体。 - 关联
dev_t(设备号)和file_operations(文件操作集)。 - 向内核注册
cdev。 - 应用层通过设备号找到
cdev,进而找到file_operations,执行具体的硬件操作。
图解逻辑 :
用户空间(
open) -> VFS -> 设备号(dev_t) ->cdev结构体 ->file_operations结构体 -> 具体驱动函数(open/read/write)。
2. file_operations 结构体
这是驱动开发中最核心的结构体,它定义了驱动能对设备做什么操作。我们需要根据需求实现其中的函数指针。
c
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
// ... (省略部分)
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *); // 对应close
// ...
};
3. 字符设备函数接口详解
(1) cdev操作函数
| 函数名 | 功能 |
|---|---|
cdev_init |
初始化字符设备结构体 |
cdev_alloc |
申请字符设备结构体 |
cdev_add |
添加字符设备(注册到内核) |
cdev_del |
删除字符设备 |
函数原型:
c
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
void cdev_del(struct cdev *p);
(2) 设备号操作接口
设备号dev_t由主设备号(Major)和次设备号(Minor)组成。
| 函数名 | 功能 |
|---|---|
register_chrdev_region |
静态注册已知设备号范围 |
alloc_chrdev_region |
动态分配设备号(推荐,由内核自动分配主设备号) |
unregister_chrdev_region |
释放注册的设备号 |
register_chrdev |
注册设备号并关联fops(老版本内核使用,不推荐) |
动态分配示例:
c
dev_t dev_num;
int major;
alloc_chrdev_region(&dev_num, 0, 1, "my_led"); // 分配1个设备号
major = MAJOR(dev_num); // 获取主设备号
(3) 设备类与设备创建(自动创建设备节点)
在旧版本中我们需要手动mknod创建设备节点,现在我们可以利用内核自动创建。
| 函数名 | 功能 |
|---|---|
class_create |
创建设备类目录(在/sys/class下) |
class_destroy |
销毁设备类 |
device_create |
创建设备(会在/dev下生成设备节点) |
device_destroy |
销毁设备 |
三、 sys文件系统
1. 什么是sysfs?
sysfs是一个虚拟文件系统,挂载在/sys目录下。它将内核中的设备、驱动、总线等信息以文件的形式展示给用户空间。它的文件不对应硬盘上的文件,而是在内存中。
2. sys目录结构
/sys/devices:按物理设备类型分类。/sys/bus:按总线类型分类。/sys/class:按设备功能分类(如leds,input)。/sys/kernel:内核相关配置。
3. sys文件系统的使用
在驱动中,我们常用它来导出信息或进行调试:
- 设备信息显示 :用户可以通过
cat或echo直接读写属性。 - 动态设备管理:实时反映热插拔状态。
- 驱动调试:直接读取/写入寄存器。
常用API:
| 函数名 | 功能 |
|---|---|
kobject_create_and_add |
创建并添加kobject |
sysfs_create_file |
创建属性文件 |
device_create_file |
在设备目录下创建文件 |
四、 LED灯驱动实战
结合前面的字符设备框架,我们来编写一个简单的LED驱动。在IMX6ULL中,LED通常通过GPIO控制。
1. 内存映射接口(ioremap)
在Linux中,不能直接访问物理地址(如寄存器地址)。我们需要将物理地址映射为虚拟地址。
| 函数名 | 功能 |
|---|---|
ioremap |
将物理地址映射为内核虚拟地址 |
iounmap |
解除映射 |
devm_ioremap |
带资源管理的映射(驱动卸载自动解除,推荐) |
copy_to_user |
内核层向用户层拷贝数据 |
copy_from_user |
用户层向内核层拷贝数据 |
2. 驱动编写思路
- 定义设备号、cdev、class、device结构体。
- 编写
open、write、release函数。open:通常用于初始化硬件或增加引用计数。write:接收应用层传来的数据(如1或0),通过copy_from_user获取,然后操作GPIO寄存器。release:关闭设备时的清理工作。
- 编写入口函数
__init:- 注册设备号
alloc_chrdev_region。 - 初始化cdev
cdev_init并添加cdev_add。 - 创建类
class_create。 - 创建设备
device_create。 - 映射寄存器
ioremap。
- 注册设备号
- 编写出口函数
__exit:- 反向操作:
iounmap、device_destroy、class_destroy、cdev_del、unregister_chrdev_region。
- 反向操作:
3. 代码框架示例
c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#define LED_CNT 1
#define LED_NAME "led"
// 假设寄存器地址(实际需查数据手册)
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
// 定义设备结构体
struct led_dev {
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
void __iomem *ccm_ccgr1;
void __iomem *sw_mux_gpio1_io03;
void __iomem *gpio1_dr;
void __iomem *gpio1_gdir;
};
struct led_dev leddev;
// 写函数
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int ret;
unsigned char databuf[1];
unsigned char ledstat;
ret = copy_from_user(databuf, buf, cnt);
if(ret < 0) return -EFAULT;
ledstat = databuf[0];
if(ledstat == 1) { // 开灯
// 操作寄存器
u32 val = readl(leddev.gpio1_dr);
val &= ~(1 << 3);
writel(val, leddev.gpio1_dr);
} else if(ledstat == 0) { // 关灯
u32 val = readl(leddev.gpio1_dr);
val |= (1 << 3);
writel(val, leddev.gpio1_dr);
}
return cnt;
}
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &leddev;
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.release = led_release,
};
// 入口函数
static int __init led_init(void)
{
// 1. 注册字符设备号
if(leddev.major) {
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(leddev.devid, LED_CNT, LED_NAME);
} else {
alloc_chrdev_region(&leddev.devid, 0, LED_CNT, LED_NAME);
leddev.major = MAJOR(leddev.devid);
leddev.minor = MINOR(leddev.devid);
}
// 2. 初始化cdev
leddev.cdev.owner = THIS_MODULE;
cdev_init(&leddev.cdev, &led_fops);
cdev_add(&leddev.cdev, leddev.devid, LED_CNT);
// 3. 创建类和设备
leddev.class = class_create(THIS_MODULE, LED_NAME);
leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LED_NAME);
// 4. 初始化硬件(ioremap)
leddev.ccm_ccgr1 = ioremap(CCM_CCGR1_BASE, 4);
leddev.sw_mux_gpio1_io03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
leddev.gpio1_dr = ioremap(GPIO1_DR_BASE, 4);
leddev.gpio1_gdir = ioremap(GPIO1_GDIR_BASE, 4);
// 具体寄存器配置代码略...
printk("led init success!\n");
return 0;
}
// 出口函数
static void __exit led_exit(void)
{
iounmap(leddev.ccm_ccgr1);
iounmap(leddev.sw_mux_gpio1_io03);
iounmap(leddev.gpio1_dr);
iounmap(leddev.gpio1_gdir);
cdev_del(&leddev.cdev);
unregister_chrdev_region(leddev.devid, LED_CNT);
device_destroy(leddev.class, leddev.devid);
class_destroy(leddev.class);
printk("led exit!\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
五、 补充:misc设备驱动(杂项设备)
除了标准的字符设备,Linux还有一类特殊的字符设备叫Misc设备(杂项设备)。
- 特点 :主设备号固定为10,次设备号由内核动态分配。
- 优势:编写更简单,不需要手动申请设备号、创建cdev等繁琐步骤。
- 适用场景:看门狗、加密硬件、非标准设备等。
如果你的LED驱动不想写那么长,可以尝试用Misc框架重写。
常用接口:
| 函数名 | 功能 |
|---|---|
misc_register |
注册混杂设备 |
misc_deregister |
注销混杂设备 |
结构体:
c
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
// ...
};
总结
- 驱动工程搭建 :理解
module_init和module_exit,知道驱动是动态加载的。 - 字符设备框架 :核心是
file_operations,流程是"申请设备号 -> 初始化cdev -> 创建设备节点"。 - sys文件系统:提供了内核与用户空间的交互接口。
- LED实战 :通过
ioremap映射寄存器,在write函数中根据应用层数据控制硬件。
希望这篇博客能帮你打通Linux驱动开发的"任督二脉"!如有疑问,欢迎在评论区留言交流。