Linux字符设备驱动开发
一、字符设备驱动模板
1.1 基本结构
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "demo" // 设备名称
#define CLASS_NAME "demo_class" // 类名称
// 1. 文件操作结构体
static struct file_operations fops;
// 2. 设备号变量
static dev_t dev;
// 3. 字符设备结构体
static struct cdev cdev;
// 4. 类和设备指针
static struct class *demo_class = NULL;
static struct device *demo_device = NULL;
1.2 初始化函数
static int __init demo_init(void)
{
int ret;
// 1. 创建设备号
// 方式1:静态分配(已知主设备号)
dev = MKDEV(248, 0); // 主设备号248,次设备号0
// 2. 注册设备号区域
ret = register_chrdev_region(dev, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to register chrdev region\n");
return ret;
}
// 方式2:动态分配(推荐)
// ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
// 3. 初始化cdev结构
cdev_init(&cdev, &fops);
// 4. 添加cdev到系统
ret = cdev_add(&cdev, dev, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
unregister_chrdev_region(dev, 1);
return ret;
}
// 5. 创建设备类
demo_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(demo_class)) {
printk(KERN_ERR "Failed to create class\n");
cdev_del(&cdev);
unregister_chrdev_region(dev, 1);
return PTR_ERR(demo_class);
}
// 6. 创建设备节点
demo_device = device_create(demo_class, NULL, dev, NULL, DEVICE_NAME);
if (IS_ERR(demo_device)) {
printk(KERN_ERR "Failed to create device\n");
class_destroy(demo_class);
cdev_del(&cdev);
unregister_chrdev_region(dev, 1);
return PTR_ERR(demo_device);
}
printk(KERN_INFO "Device driver initialized\n");
return 0;
}
1.3 退出函数
static void __exit demo_exit(void)
{
// 1. 销毁设备节点
device_destroy(demo_class, dev);
// 2. 销毁设备类
class_destroy(demo_class);
// 3. 删除cdev
cdev_del(&cdev);
// 4. 注销设备号
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "Device driver removed\n");
}
1.4 模块声明
// 模块初始化函数声明
module_init(demo_init);
// 模块退出函数声明
module_exit(demo_exit);
// 模块许可证声明
MODULE_LICENSE("GPL"); // 代码是开源的
二、驱动开发操作步骤
2.1 创建驱动文件
# 1. 进入内核驱动目录
cd /path/to/linux/drivers/char
# 2. 创建新的驱动文件
vim led.c
# 3. 在vim中批量替换
:%s/旧名字/新名字/g
# 例如::%s/demo/led/g
# 将所有"demo"替换为"led"
2.2 配置设备树
2.2.1 禁用Linux自带LED驱动
# 1. 编辑设备树文件
vim arch/arm/boot/dts/imx6ull-alientek-emmc.dts
# 2. 注释掉Linux自带的LED节点
# 找到类似这样的内容:
# leds {
# compatible = "gpio-leds";
# led0 {
# label = "sys-led";
# gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
# linux,default-trigger = "heartbeat";
# };
# };
# 在前面添加"/*",后面添加"*/"注释掉
2.2.2 编译设备树
# 1. 编译设备树
make ARCH=arm dtbs
# 2. 复制到TFTP目录
cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /tftpboot/
# 3. 重命名为imx6.dtb
cd /tftpboot
mv imx6ull-alientek-emmc.dtb imx6.dtb
2.3 创建设备节点
2.3.1 查看设备号
# 查看已注册的设备号
cat /proc/devices
# 输出示例:
# Character devices:
# 1 mem
# 4 tty
# 248 led
# 250 mydevice
2.3.2 手动创建设备节点
# 创建字符设备节点
mknod /dev/led c 248 0
# 参数说明:
# /dev/led - 设备节点路径
# c - 字符设备类型
# 248 - 主设备号(从/proc/devices获取)
# 0 - 次设备号
三、自动分配设备号
3.1 自动分配函数
// 自动分配设备号(推荐使用)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
// 参数说明:
// dev - 返回分配的设备号
// baseminor - 起始次设备号(通常为0)
// count - 要分配的设备数量
// name - 设备名称
// 使用示例:
alloc_chrdev_region(&dev, 0, 1, DEV_NAME);
3.2 自动分配的优点
-
无需手动创建设备节点(mknod)
-
自动在/dev下创建设备文件
-
避免设备号冲突
-
更方便的设备管理
四、杂项设备驱动
4.1 杂项设备特点
-
主设备号固定为10
-
不需要手动mknod
-
简化驱动开发
-
自动创建设备节点
4.2 杂项设备驱动模板
// led_misc.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h> // 包含杂项设备头文件
#define DEV_NAME "led"
// 1. 文件操作结构体
static struct file_operations fops;
// 2. 杂项设备结构体
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
.name = DEV_NAME, // 设备名称
.fops = &fops // 文件操作结构体
};
// 3. 初始化函数
static int __init led_init(void)
{
int ret;
// 注册杂项设备
ret = misc_register(&misc_dev);
if (ret < 0) {
printk(KERN_ERR "Failed to register misc device\n");
return ret;
}
printk(KERN_INFO "Misc device registered\n");
return 0;
}
// 4. 退出函数
static void __exit led_exit(void)
{
// 注销杂项设备
misc_deregister(&misc_dev);
printk(KERN_INFO "Misc device unregistered\n");
}
五、编译内核模块
5.1 配置步骤
5.1.1 修改Kconfig
# 编辑Kconfig文件
vim Kconfig
# 将模块类型从bool改为tristate
# 修改前:
# config DEMO
# bool "Demo driver"
# 修改后:
# config DEMO
# tristate "Demo driver"
tristate含义:
-
< >或[ ]:不编译 -
<M>或[M]:编译为模块(.ko文件) -
<*>或[*]:编译进内核(built-in)
5.1.2 配置内核
# 进入配置界面
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 导航到对应模块
# Device Drivers → Character devices → Your Driver
# 按M键选择编译为模块
# 显示为:<M> Your Driver
5.2 编译模块
# 编译所有模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
# 编译完成后,在驱动目录生成.ko文件
# 例如:drivers/char/xxx.ko
# 将模块复制到NFS共享目录
cp drivers/char/xxx.ko ~/nfs/imx6/rootfs/
5.3 开发板上操作
5.3.1 加载模块
# 在开发板上执行
insmod xxx.ko
# 查看加载的模块
lsmod
# 输出示例:
# Module Size Used by
# xxx 16384 0
5.3.2 卸载模块
# 卸载模块
rmmod xxx
# 注意:使用模块名,不是文件名
# rmmod xxx(正确)
# rmmod xxx.ko(错误)
5.4 查看模块信息
# 查看模块信息
modinfo xxx.ko
# 输出示例:
# filename: /xxx.ko
# license: GPL
# author: Your Name
# description: LED Driver
# depends:
# vermagic: 4.1.15 SMP preempt mod_unload modversions ARMv7 p2v8
六、关键点总结
6.1 驱动开发流程
1. 编写驱动代码(led.c)
2. 修改Makefile添加驱动
3. 修改Kconfig配置选项
4. 配置内核(make menuconfig)
5. 编译模块(make modules)
6. 复制到开发板(NFS/TFTP)
7. 加载测试(insmod)
8. 卸载(rmmod)
6.2 三种设备号分配方式
| 方式 | 函数 | 特点 | 是否需要mknod |
|---|---|---|---|
| 静态分配 | register_chrdev_region | 需要指定设备号 | 需要 |
| 动态分配 | alloc_chrdev_region | 自动分配设备号 | 不需要 |
| 杂项设备 | misc_register | 主设备号固定为10 | 不需要 |
6.3 文件操作结构体
static struct file_operations fops = {
.owner = THIS_MODULE, // 模块所有者
.open = led_open, // 打开设备
.read = led_read, // 读取设备
.write = led_write, // 写入设备
.release = led_release, // 关闭设备
// 可选:.ioctl 或 .unlocked_ioctl
};
6.4 重要头文件
#include <linux/init.h> // 初始化和退出宏
#include <linux/module.h> // 模块相关
#include <linux/fs.h> // 文件系统
#include <linux/cdev.h> // 字符设备
#include <linux/device.h> // 设备类
#include <linux/miscdevice.h> // 杂项设备
6.5 注意事项
-
模块许可证 :必须包含
MODULE_LICENSE("GPL") -
错误处理:每个步骤都要检查返回值
-
资源释放:退出函数要按创建顺序的逆序释放资源
-
设备节点:自动分配设备号后不需要手动mknod
-
模块名:加载/卸载时使用模块名,不是文件名
6.6 开发板验证
# 查看设备是否创建成功
ls -l /dev/led
# 查看内核消息
dmesg | tail
# 测试设备读写
echo "1" > /dev/led
cat /dev/led