学习笔记——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
相关推荐
世人万千丶6 分钟前
Flutter 框架跨平台鸿蒙开发 - 家庭健康档案云应用
学习·flutter·华为·开源·harmonyos·鸿蒙
进击的小头9 分钟前
第6篇:嵌入式芯片算力核心来源:多级流水线架构与指令并行机制详解
单片机·嵌入式硬件·架构
jacklood10 分钟前
煤矿用甲烷报警仪的性能试验具体方法
单片机·嵌入式硬件·煤矿电子
東雪木14 分钟前
Java学习——泛型基础:泛型的核心作用、泛型类 / 方法 / 接口的定义
java·学习·java面试
加勒比之杰克19 分钟前
从阻塞 IO 到 epoll:把 Linux 网络 IO 一次讲透
linux·网络·windows·select·多路转接·epoll·poll
电磁脑机24 分钟前
无总线场同步:意识本质、AGI困境与脑机革命的核心理论重构
分布式·神经网络·架构·信号处理·agi
東雪木25 分钟前
Java学习——内部类(成员内部类、静态内部类、局部内部类、匿名内部类)的用法与底层实现
java·开发语言·学习·java面试
AI_零食27 分钟前
二十四节气物候现象速览卡片:鸿蒙Flutter框架 实现的传统文化应用
学习·flutter·华为·开源·harmonyos·鸿蒙
开开心心_Every27 分钟前
文字转语音无字数限,对接微软接口比付费爽
运维·服务器·人工智能·edge·pdf·paddle·segmentfault
LittroInno28 分钟前
AI云台相机系统——从模块到整机的集成架构解析
人工智能·数码相机·架构