找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1213

积分

0

好友

177

主题
发表于 前天 03:20 | 查看: 5| 回复: 0

图片

1. 什么是驱动

驱动本质上是对底层硬件设备的操作进行封装,并向上层应用提供统一的函数接口。

设备分类:Linux内核将设备主要分为三类:字符设备、块设备和网络设备

  • 字符设备:指以字节流形式进行顺序读写的设备,数据需要按照先后顺序访问,不支持随机读取。常见的字符设备包括鼠标、键盘、串口、控制台和LED等。其驱动程序通常需要实现 openclosereadwrite 等基本系统调用。
  • 块设备:指可以从设备任意位置读取一定长度数据的设备,支持随机访问。典型的块设备有硬盘、U盘和SD卡。
  • 网络设备:负责数据报文的发送和接收,它既可以是一个物理硬件设备(如网卡),也可以是一个纯粹的软件抽象(如回环接口 lo)。

下面通过一个具体例子说明整个调用过程:

  1. 应用程序调用C库的 open 函数,例如 open("/dev/pin4", O_RDWR),意图以可读写方式打开 /dev 目录下的 pin4 设备。
  2. 该调用会触发一次软中断(中断号为 0x80),使CPU从用户空间切换到内核空间,并进入 system_call 函数。
  3. system_call 根据传递的系统调用号(对应 open)和参数(设备名),通过虚拟文件系统层(VFS)的 sys_open 函数进行处理。
  4. sys_open 根据设备名在内核维护的驱动链表中,查找对应的设备驱动。驱动链表中的每个节点都包含设备号设备驱动函数
  5. 找到驱动后,最终会执行驱动中定义的 open 函数,该函数内部包含操作硬件寄存器以控制特定IO口的代码。

编写驱动的主要任务就是向内核添加这样一个驱动节点,需要明确三项内容:

  1. 设备名
  2. 设备号(主设备号+次设备号)
  3. 设备驱动函数(包含操作寄存器的代码)

综上所述,从用户态 open 调用到最终操控硬件的完整路径是:用户调用触发软中断进入内核态 -> system_call -> VFS的 sys_open -> 在驱动链表中根据设备名/设备号查找驱动 -> 执行驱动中对应的操作函数(如 pin4_open)-> 函数内操作寄存器控制硬件。

2. 用户态、内核态与驱动链表详解

用户态

  • 指应用程序运行的环境。开发者基于C标准库进行编程,库函数(如 open, read, write, fork, socket)封装了与内核交互的接口,应用程序通过调用这些API来“支配”内核完成具体工作。这属于系统编程的范畴。

内核态

  • 当用户需要操作硬件时,必须通过内核态的驱动程序。驱动程序是内核的一部分,直接与硬件寄存器打交道。例如,树莓派的 wiringPi 库在用户态提供的接口,其底层就是由对应的内核驱动实现的。理解内核态工作机制是深入Linux系统的关键。
  • Linux遵循“一切皆文件”的哲学,所有设备在 /dev 目录下都有一个对应的设备文件。应用程序可以像操作普通文件一样,使用 openreadwrite 等函数操作这些设备文件。
  • 内核通过设备号来唯一标识和管理设备。设备号由主设备号次设备号组成。主设备号标识设备类型(对应特定的驱动程序),次设备号标识同一驱动程序下的不同设备实例。例如,可以为两个LED灯编写同一个字符设备驱动,分配相同的主设备号(如5),但赋予不同的次设备号(如1和2)来区分它们。

驱动链表

  • 内核通过一个链表来管理所有已加载的设备驱动。添加驱动发生在我们将编译好的驱动模块(.ko 文件)加载到内核时;查找驱动则发生在用户空间调用 open 等函数时。
  • 驱动程序在链表中的位置由设备号来索引。因此,设备号不仅用于区分设备,也决定了驱动在内核数据结构中的组织方式。
  • system_call 收到系统调用请求时,它会读取寄存器(如 eax 存放系统调用号)中的参数,通过系统调用表(sys_call_table)找到对应的内核处理函数(如 sys_open),进而完成后续的驱动查找与调用。

3. 字符设备驱动工作原理

在Linux中,对硬件的访问最终被抽象为对文件的操作。字符设备驱动的工作原理围绕几个关键数据结构展开:

  1. struct inode:描述文件系统中的一个文件(包括设备文件)的元信息,如文件类型、权限、设备号等。
  2. struct file:每次打开文件时,VFS层都会创建一个该结构体,代表一个“打开的文件”,记录本次打开的操作状态、位置偏移等。
  3. struct cdev:内核中描述一个字符设备的核心结构体,其中最重要的成员是 ops 指针,指向设备的具体操作函数集合(file_operations)。

图片

调用流程

  1. 应用层调用 open(“/dev/pin4”, …)
  2. VFS层根据 /dev/pin4 对应的 inode,获知这是一个字符设备,并获取其设备号。
  3. 根据设备号,在内核中找到对应的 struct cdev 结构体。
  4. cdev 的地址记录在 inode->i_cdev,并将 cdev->ops(即 file_operations)的地址记录在新创建的 struct file->f_op 中。
  5. file 结构体与一个文件描述符(fd)关联,并返回给应用层。
  6. 此后,应用层通过 fd 进行的 readwrite 等操作,最终都会通过 file->f_op 找到驱动中具体的函数来执行。

驱动开发者的任务,就是在驱动模块初始化时(通常通过 module_init 指定的函数),完成 cdev 结构的初始化(cdev_init)并将其添加到内核(cdev_add),从而建立设备号与操作函数集的绑定。

基于框架编写驱动代码

上层应用测试代码 (pin4test.c)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void main()
{
    int fd;
    fd = open("/dev/pin4", O_RDWR);
    if(fd < 0){
        printf("open fail\n");
        perror("reason:");
    } else {
        printf("open successful\n");
    }
    write(fd, ‘1‘, 1); // 尝试向设备写入一个字符‘1‘
}

内核驱动框架代码 (以字符设备为例)

这是一个最简化的字符设备驱动框架:

#include <linux/fs.h>          // file_operations声明
#include <linux/module.h>      // module_init, module_exit声明
#include <linux/init.h>        // __init, __exit宏定义声明
#include <linux/device.h>      // class, device声明
#include <linux/uaccess.h>     // copy_from_user头文件
#include <linux/types.h>       // 设备号 dev_t 类型声明
#include <asm/io.h>            // ioremap, iounmap头文件

static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno;                 // 设备号
static int major = 231;             // 主设备号
static int minor = 0;               // 次设备号
static char *module_name = “pin4”;  // 模块名/设备名

// 驱动操作函数
static int pin4_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO “pin4_open\n”);
    return 0;
}

static ssize_t pin4_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    printk(KERN_INFO “pin4_write\n”);
    // 此处应添加实际的硬件操作代码,例如:写寄存器控制GPIO
    return 0;
}

// file_operations 结构体,向上层提供操作接口
static struct file_operations pin4_fops = {
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};

// 驱动模块初始化函数
int __init pin4_drv_init(void)
{
    int ret;
    devno = MKDEV(major, minor);                          // 1. 创建设备号
    ret = register_chrdev(major, module_name, &pin4_fops); // 2. 注册字符设备驱动
    if (ret < 0) {
        printk(KERN_ERR “register chrdev failed!\n”);
        return ret;
    }
    pin4_class = class_create(THIS_MODULE, “myfirstdemo”); // 3. 创建一个设备类
    if (IS_ERR(pin4_class)) {
        unregister_chrdev(major, module_name);
        return PTR_ERR(pin4_class);
    }
    // 4. 在 /dev 下自动创建设备节点
    pin4_class_dev = device_create(pin4_class, NULL, devno, NULL, module_name);
    if (IS_ERR(pin4_class_dev)) {
        class_destroy(pin4_class);
        unregister_chrdev(major, module_name);
        return PTR_ERR(pin4_class_dev);
    }
    printk(KERN_INFO “pin4 driver init OK\n”);
    return 0;
}

// 驱动模块退出函数
void __exit pin4_drv_exit(void)
{
    device_destroy(pin4_class, devno); // 销毁设备
    class_destroy(pin4_class);         // 销毁类
    unregister_chrdev(major, module_name); // 注销驱动
    printk(KERN_INFO “pin4 driver exit\n”);
}

module_init(pin4_drv_init); // 指定模块入口
module_exit(pin4_drv_exit); // 指定模块出口
MODULE_LICENSE(“GPL v2”);   // 声明开源协议

驱动框架执行流程

  1. module_init(pin4_drv_init) 宏指定驱动加载时的初始化入口。
  2. pin4_drv_init 函数中,依次完成:创建设备号 -> 向内核注册驱动(关联 file_operations)-> 创建设备类 -> 在 /dev 下自动创建设备节点。
  3. 应用层 open 调用时,内核根据设备名/号找到此驱动,并调用驱动中定义的 pin4_open 函数。
  4. 卸载驱动时,module_exit(pin4_drv_exit) 指定的函数被调用,执行销毁设备节点、注销驱动等清理工作。

手动创建设备节点
除了代码自动创建,也可以手动创建:sudo mknod /dev/pin4 c 231 0。其中 c 表示字符设备,231 是主设备号,0 是次设备号。删除使用 rm 命令即可。

驱动模块代码编译与加载

驱动模块编译

驱动模块的编译需要配置好的内核源码。以交叉编译为例,基本步骤如下:

  1. 将上述驱动代码(如命名为 pin4driver.c)放入内核源码树的驱动目录下,例如 /drivers/char/
  2. 修改该目录下的 Makefile,添加一行:obj-m += pin4driver.o,告诉编译系统将此文件编译为模块。
  3. 在内核源码根目录下执行编译命令,指定架构和交叉编译器:
    ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules
  4. 编译成功后,会在对应目录生成 pin4driver.ko 文件,这就是可加载的内核模块。

在目标板(如树莓派)上加载驱动

  1. 将编译好的 pin4driver.ko 和交叉编译后的上层测试程序 pin4test 拷贝到目标板。
  2. 加载驱动模块:sudo insmod pin4driver.ko
    • 使用 lsmod 命令可查看已加载的模块。
    • 使用 ls /dev/pin4 检查设备节点是否成功创建。
  3. 为设备节点设置合适权限,以便普通用户访问:sudo chmod 666 /dev/pin4
  4. 运行测试程序:./pin4test
  5. 查看内核日志,确认驱动函数被调用:dmesg | grep pin4,应该能看到 “pin4_open” 和 “pin4_write” 的输出。
  6. 卸载驱动模块:sudo rmmod pin4driver

关于编译环境的说明:驱动模块需要与运行的内核版本严格匹配,因此通常需要在拥有完整内核源码和交叉编译工具链的主机(如虚拟机)上进行编译,而不是在资源有限的嵌入式目标板上直接编译,这属于嵌入式开发运维部署中的常见实践。




上一篇:Milvus RootCoord源码解析:深度剖析分布式元数据管理与DDL调度引擎
下一篇:智谱AI开源视频生成四大核心技术:角色动画、实时流、多主体与高效训练
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 03:20 , Processed in 0.127797 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表