学习笔记——Linux字符设备驱动

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 注意事项

  1. 模块许可证 :必须包含MODULE_LICENSE("GPL")

  2. 错误处理:每个步骤都要检查返回值

  3. 资源释放:退出函数要按创建顺序的逆序释放资源

  4. 设备节点:自动分配设备号后不需要手动mknod

  5. 模块名:加载/卸载时使用模块名,不是文件名

6.6 开发板验证

复制代码
# 查看设备是否创建成功
ls -l /dev/led

# 查看内核消息
dmesg | tail

# 测试设备读写
echo "1" > /dev/led
cat /dev/led
相关推荐
云小逸2 小时前
【Nmap源码学习】Nmap 网络扫描核心技术深度解析:从协议识别到性能优化
网络·学习·性能优化
梦梦代码精2 小时前
Gitee 年度人工智能竞赛开源项目评选揭晓!!!
开发语言·数据库·人工智能·架构·gitee·前端框架·开源
工程师0072 小时前
计算机网络知识(一)
运维·服务器·计算机网络
来自晴朗的明天2 小时前
10、LM2904 单电源反向比例运算放大器电路
单片机·嵌入式硬件·硬件工程
Trouvaille ~2 小时前
【Linux】UDP Socket编程实战(三):多线程聊天室与线程安全
linux·服务器·网络·c++·安全·udp·socket
江湖有缘2 小时前
Docker环境下使用RustScan端口扫描工具教程
运维·docker·容器
海棠AI实验室2 小时前
VS Code Remote-SSH :原理、前置条件、配置套路与踩坑清单
运维·ssh
梦想的旅途22 小时前
Java/Python/Go 实现企微外部群自动化消息推送
运维·自动化·企业微信
赋创小助手2 小时前
NVIDIA B200 GPU 技术解读:Blackwell 架构带来了哪些真实变化?
运维·服务器·人工智能·深度学习·计算机视觉·自然语言处理·架构