Linux下的驱动开发一

news2024/11/17 21:52:29

设备驱动

  1. 设备驱动程序(Device Driver)是操作系统中的一种软件组件,负责管理和控制计算机硬件设备的工作。驱动程序通过提供操作系统和硬件设备之间的接口,使得操作系统和应用程序能够与硬件设备进行交互,而无需了解硬件的具体细节。
  2. 主要功能
    • 硬件抽象:设备驱动程序屏蔽了硬件的复杂性,提供了统一的接口。
    • 设备控制:设备驱动程序负责管理硬件设备的初始化、配置、运行和关闭。
    • 中断处理:很多硬件设备通过中断与CPU通信。设备驱动程序要能够处理这些中断信号,并在需要的时候通知操作系统进行相应的处理。
    • 数据传输:设备驱动程序通常负责在设备和主存储器之间传输数据。
    • 设备管理:设备驱动程序还负责管理设备资源的分配和访问,确保多个进程或线程能够安全地访问同一个硬件设备。
  3. 分类
    • 字符设备驱动:处理按字符流进行输入输出的设备,例如串口、键盘等。这类设备允许按字符读取或写入数据。
    • 块设备驱动:处理按数据块读写的设备,例如硬盘、固态硬盘等。这类设备允许随机访问数据。
    • 网络设备驱动:处理网络接口设备的驱动程序,负责在操作系统与网络硬件之间传输数据。
  4. Linux中的设备驱动
  • 在Linux系统中,设备驱动程序一般以内核模块(Kernel Module)的形式存在,可以动态地加载和卸载。
  • 设备驱动开发者需要了解Linux内核的编程接口(API)以及设备驱动的体系结构。
  1. 驱动开发流程步骤
    1. 注册设备:通过适当的API将设备驱动程序注册到内核中,以便内核能够识别并管理设备。
    2. 实现文件操作接口:Linux中的设备通常以文件的形式呈现,设备驱动需要实现诸如open()、read()、write()等文件操作接口。
    3. 处理中断:如果设备支持中断,驱动程序需要注册中断处理程序,处理硬件中断请求。
    4. 内存映射:某些设备可能需要将硬件寄存器或内存区域映射到用户空间,以提高数据传输效率。

一、内核模块

  1. 可以动态加载和卸载的代码段,用来扩展或修改Linux内核的功能,而无需重启系统或重新编译内核。
  2. 内核模块可以在系统运行时通过insmod命令加载到内核中,或者通过rmmod命令卸载,而无需重启系统。
  3. 通过内核模块机制,内核的功能可以按需扩展。
  4. 内核模块直接运行在内核空间,与内核共享相同的地址空间,因此模块能够直接访问内核的功能和数据结构。
  5. 一个简单的内核模块例子
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");               // 指定GPL许可证
MODULE_AUTHOR("Author Name");        // 作者信息
MODULE_DESCRIPTION("A Simple Kernel Module");  // 模块描述

// 初始化函数
static int __init my_module_init(void)
{
    printk(KERN_INFO "Hello, Kernel! My module has been loaded.\n");
    return 0;  // 返回0表示加载成功
}

// 退出函数
static void __exit my_module_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel! My module has been unloaded.\n");
}

// 指定初始化和退出函数
module_init(my_module_init);
module_exit(my_module_exit);

二、内核模块必须包含的部分

  1. 模块初始化函数(module_init):这是内核加载模块时调用的函数,用来初始化模块,注册设备驱动或其他功能。通常通过module_init()宏指定初始化函数。

  2. 模块退出函数(module_exit):这是在卸载模块时调用的函数,用于清理资源、注销设备驱动等。通过module_exit()宏指定退出函数。

  3. 模块许可证声明(MODULE_LICENSE):指定模块的许可证类型,通常为"GPL"(GNU General Public License),表明模块可以与内核兼容,否则内核可能拒绝加载非GPL的模块。

三、常用的模块指令

  1. lsmod :列出已加载的模块
    lsmod 用于显示当前内核中加载的模块列表。它读取 /proc/modules 文件,并以更友好的格式显示已加载的模块。
  2. modinfo - 显示模块的详细信息
    modinfo 用于显示已加载模块或模块文件的详细信息,例如作者、许可证、依赖项等。你可以使用它来查看内核模块的元数据。
    m o d i n f o 模块名 modinfo 模块名 modinfo模块名
  3. insmod - 手动加载模块
    insmod 是一个用于手动加载模块到内核的命令。它需要指定模块的完整路径(通常是.ko文件)。不指定的话,默认为当前目录
  4. rmmod - 卸载模块
    rmmod 用于卸载已经加载的模块。它会移除指定的模块,并解除与之相关的依赖关系。如果模块正在被其他模块使用或正在使用的资源没有被释放,它会阻止卸载。不用加ko
  5. dmesg - 查看内核日志
    dmesg 用于查看内核日志输出。加载或卸载内核模块时,内核日志会记录相关信息,比如模块加载成功与否、是否存在错误等。
  • 如果模块加载时有任何 printk() 输出,它应该出现在 dmesg 中。你可以使用 grep 来过滤特定的输出,
dmesg | grep "Device registered"
找到所有包含 Device registered 的日志条目,这些通常是由 printk 函数输出的设备注册信息(假设驱动程序中有 printk("Device registered...") 这样的语句)。
dmesg | grep my_module//查询特定模块的日志信息

一、字符设备驱动

  1. 注册字符设备
  • 在字符设备驱动中,首先要将设备注册到系统中。Linux内核通过register_chrdev()或**alloc_chrdev_region()**函数分配设备号,并注册字符设备。
    • register_chrdev():用于直接注册字符设备。
    • alloc_chrdev_region():用于动态分配主设备号。
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
参数:
	dev_t *dev:指向保存设备号的变量的指针。该函数成功执行后,会通过这个指针将分配到的主设备号和次设备号返回给调用者。包含了主设备号和次设备号。在设备注册时,系统通过 dev_t 识别不同的设备。
	unsigned int firstminor:表示要分配的第一个次设备号(minor number)。通常设置为 0,表示从第一个次设备号开始。
	unsigned int count:表示需要分配的连续次设备号的数量。对于一个简单的字符设备,通常设置为 1。
如果你希望一个驱动程序支持多个次设备,可以设置 count 为更大的值。
	const char *name:设备的名称,它通常是一个用于标识设备的字符串。例如,你可以将它设置为你的驱动程序名称或设备名称。
	
返回值:
	成功时返回 0,表示设备号分配成功。
	失败时返回负数的错误代码(如 -ENOMEM,表示内存不足)。	
  • 使用范例
static int major;
static int minor;
 dev_t dev;
    if (alloc_chrdev_region(&dev, 0, 1, "my_device") < 0) {
        printk(KERN_ERR "Failed to allocate device number.\n");
        return -1;
    }
major = MAJOR(dev);//获取主设备号 
minor = MINOR(dev);//获取从设备号

  1. 定义file_operation结构
  • file_operations结构体是字符设备驱动的核心,定义了设备的各种操作接口。需要实现其中几个函数来支持常见的文件操作。
  • 常见的几个
static struct file_operations fops = {
    .open = device_open,     // 打开设备
    .release = device_release,  // 关闭设备
    .read = device_read,     // 读取设备数据
    .write = device_write,   // 写入设备数据
};
  1. 实现设备操作函数
static int device_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device opened.\n");
    return 0;
}
static int device_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device closed.\n");
    return 0;
等等
  1. 注册和注销字符设备
  • 注册字符设备并将设备号绑定到file_operations结构体。通常在模块加载时完成这一过程
  • 声明字符设备结构体,用于表示并管理驱动程序中的一个字符设备。之后可以通过 cdev_init() 初始化它,通过 cdev_add() 将它注册到内核,使其作为字符设备提供给用户使用。
  • class_create
    • 用于在 Linux 内核中创建一个设备类的函数。
    • 设备类是用于分组和管理设备的抽象概念,它使得多个相似类型的设备可以被组织和识别,比如分为字符设备、块设备等。
    • 通常,在字符设备驱动程序开发中,创建设备类是用于创建 /sys/class 下的设备节点目录,以便用户空间可以通过 /dev 目录访问设备文件。
struct class *class_create(struct module *owner, const char *name);
参数:
	owner:指向拥有该类的模块。通常设置为 THIS_MODULE
	name:类的名称,用于识别和创建类目录。它会出现在 /sys/class/ 目录下。
返回值:
	class_create 成功时会返回一个指向 struct class 的指针,表示创建的类。如果失败,返回 ERR_PTR(-ENOMEM) 或者其他负值错误指针。
  • device_create 是用于在 Linux 内核中创建设备节点的函数,常与 class_create 搭配使用。它会在 /sys/class/ 下创建设备相关的目录,并且通常会在 /dev 下创建设备文件,以供用户空间使用。用户可以通过该设备文件与内核中的字符设备进行交互,比如使用 open、read、write 系统调用。
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
参数:
	cls: 指向通过 class_create 创建的设备类(struct class)。它指定了该设备属于哪个设备类,即设备会被归属到 /sys/class/cls_name/ 目录下。
	parent: 设备的父设备。如果当前设备没有父设备(通常在多数情况下是 NULL),传 NULL 即可。
	devt: 设备号,类型为 dev_t。这是通过 MKDEV(major, minor) 创建的一个 32 位的设备号,包含主设备号和次设备号,标识设备在系统中的唯一性。
	drvdata: 驱动私有数据的指针。这个指针通常会指向设备的私有数据,设备驱动可以通过该指针存储和管理与设备相关的数据。
	fmt: 设备名称格式字符串。设备文件的名称可以使用格式化字符串,类似于 printf。例如,可以为设备命名为 "my_device%d",其中 %d 表示动态分配的设备号或某些其他标识符。	

在这里插入图片描述

static struct cdev my_cdev;
  • 驱动初始化函数
    • 主要任务
      • 分配和注册设备号 register_chrdev_region()
      • 初始化 cdev 结构体 cdev_init(&my_cdev, &fops);
      • 将 cdev 添加到系统中 cdev_add(&my_cdev, dev, 1);
      • 创建设备类,并在设备类下创建设备文件节点
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>  // 设备类和设备节点所需的头文件

static int major;            // 存储主设备号
static dev_t dev;            // 存储设备号(包含主设备号和次设备号)
static struct cdev my_cdev;  // 定义字符设备结构体
static struct class *my_class = NULL;  // 设备类,用于设备节点的创建
static struct device *my_device = NULL; // 设备节点
extern struct file_operations fops;  // 假设文件操作函数集合在其他地方定义

// 模块初始化函数
static int __init my_driver_init(void) {
    int ret;

    // 动态分配设备号
    // dev:指向设备号变量的指针,用于存储分配到的设备号
    // 0:从次设备号0开始分配
    // 1:分配一个次设备号
    // "my_device":设备名称,用于区分设备
    ret = alloc_chrdev_region(&dev, 0, 1, "my_device");
    if (ret < 0) {
        // 如果分配失败,打印错误信息并返回错误代码
        printk(KERN_ERR "Failed to allocate device number.\n");
        return ret;  // 返回错误代码,模块加载失败
    }

    // 获取分配到的主设备号
    major = MAJOR(dev);
    // 打印信息,通知设备已注册并显示主设备号
    printk(KERN_INFO "Device registered with major number %d\n", major);

    // 初始化 cdev 结构体,并将其与文件操作函数 fops 关联
    // cdev_init 用于设置 cdev 结构体的操作函数,cdev 是字符设备的核心结构体
    cdev_init(&my_cdev, &fops);

    // 将字符设备添加到内核
    // dev:设备号(包含主设备号和次设备号)
    // 1:表示注册的次设备数量
    ret = cdev_add(&my_cdev, dev, 1);
    if (ret < 0) {
        // 如果添加字符设备失败,注销设备号,并打印错误信息
        printk(KERN_ERR "Failed to add cdev.\n");
        unregister_chrdev_region(dev, 1);  // 释放分配的设备号
        return ret;  // 返回错误代码,模块加载失败
    }

    // 创建设备类,"my_device_class" 是类的名称
    // class_create 用于创建设备类,它会在 /sys/class/ 下创建相应的目录
    my_class = class_create(THIS_MODULE, "my_device_class");
    if (IS_ERR(my_class)) {
        // 如果设备类创建失败,清理已添加的字符设备,并注销设备号
        printk(KERN_ERR "Failed to create class.\n");
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(my_class);
    }

    // 在设备类下创建设备节点
    // device_create 会在 /dev 下创建一个设备文件
    // dev 是设备号,"my_device" 是设备文件名
    my_device = device_create(my_class, NULL, dev, NULL, "my_device");
    if (IS_ERR(my_device)) {
        // 如果设备节点创建失败,清理设备类、字符设备,并注销设备号
        printk(KERN_ERR "Failed to create device.\n");
        class_destroy(my_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(my_device);
    }

    // 模块加载成功,返回0
    printk(KERN_INFO "Device node created successfully\n");
    return 0;
}

// 模块卸载函数
static void __exit my_driver_exit(void) {
    // 销毁设备节点
    device_destroy(my_class, dev);

    // 销毁设备类
    class_destroy(my_class);

    // 从内核中删除字符设备
    cdev_del(&my_cdev);

    // 注销设备号,释放分配的主设备号和次设备号
    unregister_chrdev_region(dev, 1);

    // 打印信息,通知设备已被注销
    printk(KERN_INFO "Device unregistered.\n");
}

// 将初始化和退出函数分别设置为模块加载和卸载时的回调函数
module_init(my_driver_init);  // 注册模块初始化函数
module_exit(my_driver_exit);  // 注册模块卸载函数

// 申明许可证,防止内核拒绝加载模块(GPL是开源驱动的常用许可证)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author");
MODULE_DESCRIPTION("A simple Linux char driver with device node creation");

五、字符设备文件与普通文件的区别

属性字符设备文件普通文件
定义用于与硬件设备交互的特殊文件用于存储用户数据的常规文件
位置通常位于 /dev/ 目录下位于文件系统的任意位置(如 /home/user/
交互方式通过字符流与设备驱动交互通过文件系统读写数据,支持随机访问
文件类型标记c (字符设备)- (普通文件)
文件内容不存储数据,作为与设备交互的接口存储持久化数据,如文本、二进制等
主/次设备号通过主设备号和次设备号标识无主次设备号概念,使用文件路径访问
读写操作顺序访问,通过驱动传递数据到硬件设备支持顺序或随机访问,读写磁盘上的数据

六、字符设备驱动的结构

在这里插入图片描述

七、内核空间与用户空间的相互访问

  1. read

    1. file_operations 结构体中的 read 函数用于处理从设备读取数据到用户空间的请求。该函数的实现需要将设备数据传递给用户空间的进程,这通常通过内核空间和用户空间之间的数据拷贝来实现。
    2. 函数
    ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
    
    filp:指向打开文件的文件结构体指针。通过它可以访问与该文件(或设备)相关的特定信息。
    buf:这是用户空间中的缓冲区指针,数据需要通过它传递给用户空间。
    count:需要传输的数据字节数,即用户想读取的最大数据量。
    f_pos:文件位置指针,指示当前文件读写的偏移量。
    
    1. copy_to_user数据从内核空间传输到用户空间
    long copy_to_user(void __user *to, const void *from, unsigned long n);
    参数:
    to 是用户空间的目标缓冲区地址
    from是内核空间的源数据地址
    n是要拷贝的字节数	
    
  2. write

    1. file_operations 中的 write 函数,从用户空间向内核空间传递数据。在 Linux 驱动开发中,write 函数用于将用户空间的数据写入到设备,或更新内核空间中的数据。
    2. 函数
ssize_t (*write) (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
参数:
	filp:指向文件结构体的指针,代表打开的文件(或设备)。
	buf:这是来自用户空间的缓冲区指针,内核需要从该缓冲区读取数据。
	count:要写入的字节数,即用户空间进程传递给内核的最大数据量。
	f_pos:文件位置指针,指示文件当前写操作的位置偏移量。
  1. copy_from_user从用户空间拷贝数据到内核空间的关键函数
    • 直接访问用户空间的地址是危险的,因此必须通过此函数进行拷贝。
    • 可以将数据写入硬件设备或进行其他操作。
    • write 函数返回实际写入的字节数,表示驱动程序从用户空间成功接收的数据量。如果传输失败(例如 copy_from_user 失败),则返回负数以指示错误。
  2. 举例如下
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>  // for copy_to_user, copy_from_user

#define DEVICE_NAME "my_char_device"
#define BUFFER_SIZE 1024

static int major;
static char kernel_buffer[BUFFER_SIZE];  // 用于存储用户写入的数据
static size_t buffer_len = 0;  // 追踪缓冲区中的数据长度

// 函数声明
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char __user *, size_t, loff_t *);

// file_operations 结构体,定义了设备操作
static struct file_operations fops = {
    .read = device_read,
    .write = device_write,
    .open = device_open,
    .release = device_release,
};

// 打开设备时调用的函数
static int device_open(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "Device opened\n");
    return 0;
}

// 关闭设备时调用的函数
static int device_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "Device closed\n");
    return 0;
}

// 读取设备时调用的函数
static ssize_t device_read(struct file *filep, char __user *user_buffer, size_t len, loff_t *offset) {
    ssize_t bytes_read;

    // 如果文件位置偏移量大于数据长度,表示读取完毕
    if (*offset >= buffer_len)
        return 0;

    // 限制读取的字节数,避免超出缓冲区
    if (len > buffer_len - *offset)
        len = buffer_len - *offset;

    // 将数据从内核缓冲区拷贝到用户空间
    if (copy_to_user(user_buffer, kernel_buffer + *offset, len)) {
        return -EFAULT;
    }

    // 更新文件偏移量
    *offset += len;

    // 返回实际读取的字节数
    bytes_read = len;
    printk(KERN_INFO "Sent %zu bytes to the user\n", bytes_read);

    return bytes_read;
}

// 写入设备时调用的函数
static ssize_t device_write(struct file *filep, const char __user *user_buffer, size_t len, loff_t *offset) {
    // 如果写入的数据大于缓冲区容量,进行裁剪
    if (len > BUFFER_SIZE - 1)
        len = BUFFER_SIZE - 1;

    // 从用户空间拷贝数据到内核缓冲区
    if (copy_from_user(kernel_buffer, user_buffer, len)) {
        return -EFAULT;
    }

    // 确保缓冲区内容是以 NULL 结尾的字符串
    kernel_buffer[len] = '\0';
    buffer_len = len;  // 更新缓冲区中的数据长度

    printk(KERN_INFO "Received %zu bytes from the user\n", len);
    return len;
}

// 模块加载时调用的初始化函数
static int __init my_char_device_init(void) {
    major = register_chrdev(0, DEVICE_NAME, &fops);  // 动态分配主设备号

    if (major < 0) {
        printk(KERN_ALERT "Failed to register char device\n");
        return major;
    }

    printk(KERN_INFO "Registered char device with major number %d\n", major);
    return 0;
}

// 模块卸载时调用的清理函数
static void __exit my_char_device_exit(void) {
    unregister_chrdev(major, DEVICE_NAME);  // 注销设备
    printk(KERN_INFO "Unregistered char device\n");
}

module_init(my_char_device_init);
module_exit(my_char_device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example Author");
MODULE_DESCRIPTION("A simple character device driver");

八、模块的卸载mod_exit(void)

  1. 模块卸载时要逆序释放资源
    • 在模块加载过程中,资源是按照一定的顺序分配的。在卸载模块时,必须按照相反的顺序释放这些资源。
    • 这是为了避免在使用已被释放的资源时出现问题,例如,在设备节点被销毁后,类或设备号还未释放,可能会导致系统崩溃。
  2. 正确顺序
删除设备节点
删除类
删除字符设备对象
释放设备号
  1. 举例说明
// 模块卸载函数
static void __exit my_char_device_exit(void) {
    // 1. 删除设备节点
    device_destroy(cls, MKDEV(major, 0));

    // 2. 销毁设备类
    class_destroy(cls);

    // 3. 删除字符设备对象
    cdev_del(&cdev_obj);

    // 4. 释放设备号
    unregister_chrdev_region(MKDEV(major, 0), 1);

    printk(KERN_INFO "Device unregistered and resources released\n");
}
module_exit(my_char_device_exit);

九、ioctl

  1. ioctl(Input/Output Control)是一种强大的机制,允许用户空间程序与设备驱动程序进行复杂的交互,不仅限于标准的读写操作。
  2. 通过 ioctl,可以执行设备特定的操作,如配置设备参数、获取设备状态等。
  3. 举例说明
/* my_char_device.c */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>  // for copy_to_user, copy_from_user
#include "tv_ioctl.h"       // 包含命令码和结构体定义

#define DEVICE_NAME "my_char_device"
#define CLASS_NAME "my_char_class"

static int major;
static struct class *cls;
static struct cdev cdev_obj;

// 设备数据
static struct tv_stat current_stat = {
    .chnl = 1,
    .light = 50,
    .vol = 30
};

// 打开设备
static int my_device_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device opened\n");
    return 0;
}

// 释放设备
static int my_device_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device closed\n");
    return 0;
}

// 读取设备(示例)
static ssize_t my_device_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
    const char *msg = "Hello from kernel space!\n";
    size_t len = strlen(msg);

    if (*ppos >= len)
        return 0; // EOF

    if (count > len - *ppos)
        count = len - *ppos;

    if (copy_to_user(buf, msg + *ppos, count))
        return -EFAULT;

    *ppos += count;
    printk(KERN_INFO "Sent %zu bytes to the user\n", count);
    return count;
}

// 写入设备(示例)
static ssize_t my_device_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    char kernel_buffer[100];

    if (count > sizeof(kernel_buffer) - 1)
        count = sizeof(kernel_buffer) - 1;

    if (copy_from_user(kernel_buffer, buf, count))
        return -EFAULT;

    kernel_buffer[count] = '\0';
    printk(KERN_INFO "Received %zu bytes from the user: %s\n", count, kernel_buffer);
    return count;
}

// ioctl 函数实现
static long chdev_ioctl(struct file *file, unsigned int cmd, unsigned long addr) {
    int ret = 0;
    struct tv_menu menu;
    struct tv_sw sw;
    struct tv_chnl chnl;
    struct tv_stat stat;

    switch(cmd){
        case TV_CMD_MENU:
            if (copy_from_user(&menu, (void __user *)addr, sizeof(menu))) {
                return -EFAULT;
            }
            printk(KERN_INFO "%s-%d ioctl MENU: color=%d, dbd=%d, light=%d\n",
                   __func__, __LINE__, menu.color, menu.dbd, menu.light);
            // 在此处处理 menu 数据,例如更新设备配置
            break;

        case TV_CMD_SW:
            if (copy_from_user(&sw, (void __user *)addr, sizeof(sw))) {
                return -EFAULT;
            }
            printk(KERN_INFO "%s-%d ioctl SW: sw=%d\n",
                   __func__, __LINE__, sw.sw);
            // 在此处处理 sw 数据,例如切换设备状态
            break;

        case TV_CMD_CHNL:
            if (copy_from_user(&chnl, (void __user *)addr, sizeof(chnl))) {
                return -EFAULT;
            }
            printk(KERN_INFO "%s-%d ioctl CHNL: channel=%d\n",
                   __func__, __LINE__, chnl.channel);
            // 在此处处理 chnl 数据,例如切换频道
            break;

        case TV_CMD_STAT:
            // 将当前状态复制到用户空间
            stat = current_stat;  // 假设 current_stat 已在设备中维护
            if (copy_to_user((void __user *)addr, &stat, sizeof(stat))) {
                return -EFAULT;
            }
            printk(KERN_INFO "%s-%d ioctl STAT: chnl=%d, light=%d, vol=%d\n",
                   __func__, __LINE__, stat.chnl, stat.light, stat.vol);
            break;

        default:
            printk(KERN_WARNING "Unknown ioctl command: %u\n", cmd);
            return -ENOTTY;  // 命令无效
    }

    return ret;
}

// file_operations 结构体
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_device_open,
    .release = my_device_release,
    .read = my_device_read,
    .write = my_device_write,
    .unlocked_ioctl = chdev_ioctl,  // 注册 ioctl 函数
};

// 模块加载函数
static int __init my_char_device_init(void) {
    dev_t dev;
    int ret;

    // 1. 分配设备号
    ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        printk(KERN_ALERT "Failed to allocate device number\n");
        return ret;
    }
    major = MAJOR(dev);
    printk(KERN_INFO "Allocated device number: %d\n", major);

    // 2. 初始化字符设备对象
    cdev_init(&cdev_obj, &fops);
    cdev_obj.owner = THIS_MODULE;

    // 3. 添加字符设备对象到系统中
    ret = cdev_add(&cdev_obj, dev, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to add cdev\n");
        return ret;
    }

    // 4. 创建设备类
    cls = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(cls)) {
        cdev_del(&cdev_obj);
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to create class\n");
        return PTR_ERR(cls);
    }

    // 5. 创建设备节点
    if (device_create(cls, NULL, dev, NULL, DEVICE_NAME) == NULL) {
        class_destroy(cls);
        cdev_del(&cdev_obj);
        unregister_chrdev_region(dev, 1);
        printk(KERN_ALERT "Failed to create device\n");
        return -1;
    }

    printk(KERN_INFO "Device created successfully\n");
    return 0;
}

// 模块卸载函数
static void __exit my_char_device_exit(void) {
    dev_t dev = MKDEV(major, 0);

    // 1. 删除设备节点
    device_destroy(cls, dev);

    // 2. 销毁设备类
    class_destroy(cls);

    // 3. 删除字符设备对象
    cdev_del(&cdev_obj);

    // 4. 释放设备号
    unregister_chrdev_region(dev, 1);

    printk(KERN_INFO "Device unregistered and resources released\n");
}

module_init(my_char_device_init);
module_exit(my_char_device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example Author");
MODULE_DESCRIPTION("A simple character device driver with ioctl");

十、硬件访问

  1. 驱动使用的是虚拟地址,ARM中访问的是物理地址
  2. 寄存器-物理地址转换为 虚拟地址
    • 虚拟地址 = ioremap(物理地址, 空间大小);
    • iounmap: 取消映射
  3. 小例子
#include <linux/io.h>

void __iomem *base_addr;
unsigned long phys_addr = 0xFE000000;  // 假设设备寄存器物理地址

// 在设备初始化时,映射物理地址到内核地址空间
base_addr = ioremap(phys_addr, 0x100);  // 映射 0x100 字节

// 读取设备寄存器(假设偏移 0x04 处的寄存器)
u32 value = readl(base_addr + 0x04);

// 写入设备寄存器
writel(value | 0x1, base_addr + 0x04);

// 在设备释放时,取消映射
iounmap(base_addr);

  1. 综合LED灯驱动开发
//chdev_ioctl
long chdev_ioctl(struct file *file, unsigned int cmd, unsigned long addr)
{
    int ret;
    struct led_ctl ctl;

    switch (cmd) {
    case LED_CMD_CTL:
        // 从用户空间拷贝数据到内核空间
        ret = copy_from_user(&ctl, (void __user *)addr, sizeof(ctl));
        if (ret) {
            pr_err("Failed to copy data from user space\n");
            return -EFAULT;  // 返回错误码
        }

        // 调用硬件控制函数,控制 LED
        led2_ctl(ctl.sw);
        break;

    default:
        pr_err("Unsupported ioctl command: %u\n", cmd);
        return -EINVAL;  // 返回无效命令错误
    }

    return 0;  // 成功返回
}

//led_ctl
void led2_ctl(int sw)
{
#define GPX2CON 0x11000c40  
#define GPX2DAT 0x11000c44

    void __iomem *conf = ioremap(GPX2CON, 4);
    void __iomem *data = ioremap(GPX2DAT, 4);

    // 检查是否映射成功
    if (!conf || !data) {
        pr_err("Failed to ioremap registers\n");
        if (conf) iounmap(conf);
        if (data) iounmap(data);
        return;
    }

    long val = readl(conf);
    val &= ~(0xF << 28);  // 清除 GPX2_7 的配置位
    val |= (0x1 << 28);   // 设置 GPX2_7 为输出模式
    writel(val, conf);

    // 控制 LED 开关状态
    val = readl(data);
    if (sw) {
        val |= (1 << 7);  // 打开 LED(设置 GPX2_7 为高电平)
    } else {
        val &= ~(1 << 7); // 关闭 LED(设置 GPX2_7 为低电平)
    }
    writel(val, data);

    // 解除映射
    iounmap(conf);
    iounmap(data);
}

  1. 用户空间调用代码
int main() {
    int fd;
    struct led_ctl ctl;

    // 1. 打开设备文件(假设设备节点为 /dev/my_led_device)
    fd = open("/dev/my_led_device", O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return -1;
    }

    // 2. 控制 LED 打开
    ctl.sw = 1;  // 设置为 1 表示打开 LED
    if (ioctl(fd, LED_CMD_CTL, &ctl) < 0) {
        perror("Failed to control LED (turn on)");
        close(fd);
        return -1;
    }
    printf("LED turned on\n");

    // 3. 控制 LED 关闭
    ctl.sw = 0;  // 设置为 0 表示关闭 LED
    if (ioctl(fd, LED_CMD_CTL, &ctl) < 0) {
        perror("Failed to control LED (turn off)");
        close(fd);
        return -1;
    }
    printf("LED turned off\n");

    // 4. 关闭设备文件
    close(fd);

    return 0;
}

十一、内核中的锁

  1. 自旋锁
  • 自旋锁是最常用的锁之一,适用于不允许睡眠的上下文(如中断上下文)。当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,线程会在一个循环(自旋)中不断检查锁是否释放,而不会主动让出 CPU 时间片。
  • 自旋锁不会引发上下文切换。
  • 自旋锁不允许在睡眠上下文中使用,因为自旋锁期望锁能够很快释放。
#include <linux/spinlock.h>

spinlock_t my_lock;

void my_function(void) {
    unsigned long flags;

    // 初始化自旋锁
    spin_lock_init(&my_lock);

    // 加锁(可被中断打断的代码)
    spin_lock_irqsave(&my_lock, flags);  // 保存中断状态并禁用中断

    // 临界区代码(操作共享资源)
    // ...

    // 解锁
    spin_unlock_irqrestore(&my_lock, flags);  // 恢复中断状态
}
  1. 互斥锁(Mutex)
  • 互斥锁是一种允许睡眠的锁。如果一个线程试图获取一个已经被其他线程持有的互斥锁,该线程会进入睡眠状态,直到锁可用。
  • 互斥锁适用于在进程上下文中保护共享资源,而不适用于中断上下文,因为它允许持有锁的线程阻塞。
#include <linux/mutex.h>

struct mutex my_mutex;

void my_function(void) {
    // 初始化互斥锁
    mutex_init(&my_mutex);

    // 加锁
    mutex_lock(&my_mutex);

    // 临界区代码(操作共享资源)
    // ...

    // 解锁
    mutex_unlock(&my_mutex);
}
  1. 读写锁
  • 读写锁允许多个读者同时获取锁,但写者只能独占锁。
  • 适用于读多写少的场景,因为多个线程可以同时进行读取,而不必相互等待。
#include <linux/rwlock.h>

rwlock_t my_rwlock;

void read_function(void) {
    unsigned long flags;

    // 初始化读写锁
    rwlock_init(&my_rwlock);

    // 读锁
    read_lock_irqsave(&my_rwlock, flags);

    // 临界区代码(读取共享资源)
    // ...

    // 解锁
    read_unlock_irqrestore(&my_rwlock, flags);
}

void write_function(void) {
    unsigned long flags;

    // 写锁
    write_lock_irqsave(&my_rwlock, flags);

    // 临界区代码(写入共享资源)
    // ...

    // 解锁
    write_unlock_irqrestore(&my_rwlock, flags);
}
  1. 原子变量
  • 操作是在 CPU 层面一次性完成的,保证了其操作是不可分割的,因此不会被中断。
  • 原子操作的性能通常比使用锁的同步机制更高,但它只适用于简单的整数操作,不适合复杂的临界区保护。
  • 使用示例
#include <linux/atomic.h>

atomic_t counter;

void init_counter(void) {
    // 初始化原子变量
    atomic_set(&counter, 0);
}

void increment_counter(void) {
    // 增加原子变量值
    atomic_inc(&counter);
}

void decrement_counter(void) {
    // 减少原子变量值
    atomic_dec(&counter);
}

int get_counter_value(void) {
    // 获取原子变量的当前值
    return atomic_read(&counter);
}

  1. 它们之间的区别
锁类型允许睡眠适用场景操作粒度性能特点
自旋锁短时间的临界区粗粒度自旋等待,适合中断上下文,不适合长时间持有
互斥锁需要睡眠的进程上下文粗粒度允许睡眠,不适合中断上下文,适合长时间持有
读写锁读多写少的场景粗粒度允许多个读者同时获取锁,但写者独占
信号量进程同步或资源访问粗粒度计数机制,支持多个进程并发访问,可阻塞进程
原子变量简单的计数或位操作细粒度无需加锁,适用于简单的数值操作和比较交换操作
RCU (Read-Copy Update)极端读多写少的场景细粒度非常高高效的读操作,无需加锁,写操作需更新后同步

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2174118.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【智能大数据分析 | 实验二】Spark实验:部署Spark集群

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈智能大数据分析 ⌋ ⌋ ⌋ 智能大数据分析是指利用先进的技术和算法对大规模数据进行深入分析和挖掘&#xff0c;以提取有价值的信息和洞察。它结合了大数据技术、人工智能&#xff08;AI&#xff09;、机器学习&#xff08;ML&a…

网易云多久更新一次ip属地

‌在数字化时代&#xff0c;网络成为了我们日常生活中不可或缺的一部分。无论是社交娱乐还是工作学习&#xff0c;IP地址作为网络身份的象征&#xff0c;都扮演着重要的角色。对于网易云音乐这样的热门应用来说&#xff0c;IP属地的显示不仅关乎用户体验&#xff0c;也涉及用户…

Supervisor进程管理工具安装

Supervisor进程管理工具安装 一、Supervisor概况及作用1.概况2.主要作用1&#xff09;、进程管理(1). 启动与停止进程(2). 自动重启进程(3). 进程状态监控 2&#xff09;、日志管理(1).集中化日志记录 3&#xff09;、资源管理与优化(1). 控制进程资源使用 二.安装 Supervisor1…

小白学大模型RAG:十大步骤分析GraphRAG的工作原理

对于普通的RAG&#xff0c;也就是前面课程讲的RAG&#xff0c;它的原理是通过将文本进行切分和向量化&#xff0c;通过计算文本之间的向量相似度&#xff0c;从而得到两个文本之间的语义相似度&#xff0c;从知识库中找出跟问题语义相似的知识点&#xff0c;再送给LLM得出最终答…

学习Java (五)

1.学习封装 package com.msb.test07; //封装 public class Girl {private int age; // 赋值public void setAge(int age){if(age > 30){this.age 18;}else{this.age age;}} // 读取public int getAge(){return age;} }package com.msb.test07;public class Test {p…

【Linux 报错】“make: ‘xxxx‘ is up to date.” 解决办法

一、报错原因 我们使用 make 命令&#xff0c;想要将 text.c 文件编译形成 可执行文件 text 时&#xff0c;报错如下 make: test is up to date. 中文含义&#xff1a;test 文件已经达到最新状态 意思是&#xff1a; test.c 文件里面的 所有源代码都没有修改过&#xff0c;你…

c++11~c++20 numeric_limits

在c中应该尽量少使用宏&#xff0c;用模板取而代之是明知的选择。我们可以使用numeric_limits来获取最大值和最小值&#xff0c;例如 #include <iostream> #include <limits> #include <iostream> using namespace std;int main() {cout << "int…

Go基础学习04-变量重声明;类型转换;类型断言;Unicode代码点;类型别名;潜在类型

目录 变量重声明 类型断言 类型转换 类型转换注意事项 Unicode代码点 类型别名、潜在类型 类型别名的意义 变量重声明 编写代码&#xff1a; package mainimport "fmt"var container []string{"Beijing", "Shanghai"}func main() {fmt.Pr…

Qt开发技巧(八)自带对话框的美化,内置快捷代码段,巧用匿名槽函数,另类动态换肤,资源动态加载

继续讲一些Qt开发中的技巧操作&#xff1a; 1.自带对话框的美化 Qt中有一些自带的对话框&#xff0c;比如文件选择对话框&#xff0c;颜色选择对话框等等&#xff0c;这些对话框看着跟系统的对话框没太大差别&#xff0c;实际这是Qt有意为之&#xff0c;为的是跟系统保持一致。…

欺诈文本分类检测(十七):支持分类原因训练

1. 引言 前文数据校正与增强进行了数据增强&#xff0c;本文将使用增强后的数据对模型进行进一步训练&#xff0c;以便得到能同时预测出分类标签、欺诈者、分类原因多个信息的模型。 为此&#xff0c;我们需要对整个训练过程进行调整&#xff0c;包括&#xff1a; 交叉训练逻…

苹果端侧AI布局深度分析

苹果 - 国际巨头的端侧 AI布局 深度分析 1.1.1 苹果AI&#xff1a;模型侧&#xff1a;MM1 3月&#xff0c;苹果发布多模态大模型MM1&#xff0c;拥有高达300亿参数。MM1融合密集模型与MoE变体&#xff0c;涵盖300亿、70亿、30亿参数版。MM1预训练指标领先&#xff0c;在多个多…

ubuntu 安装k8s

#关闭 Swap 内存&#xff0c;配置完成建议重启一下 nano /etc/fstab #注释下面相似的一行 #/swapfile none swap sw 0 0 #重启 reboot#部属k8s apt update && apt install -y apt-transport-https 下载 gpg 密钥 curl https://mi…

基于SpringBoot+Vue的高校实习管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

一次眼睛受损然后恢复的过程

由于多年没有社交比较宅,多年长期盯着电脑和手机,没有保护好眼睛 之前早上醒来有一段时间我眼睛老是分泌各种乱起八遭的东西,导致我眼睛看不清, 2023年3月有天的早上,我又不小心眼睛揉出血了,出门上班路上的时候才知道有这个问题,第二天早上就挂了去了眼科,医生给我开了如下的药…

单细胞转录组|scATAC-seq 数据整合

引言 本文在此展示了如何将多个源自人类外周血单核细胞的单细胞染色质数据集进行整合。其中一个数据集是通过10x Genomics的多组学技术获得的&#xff0c;它涵盖了每个细胞的DNA可及性和基因表达数据。另一个数据集则是通过10x Genomics的单细胞ATAC测序(scATAC-seq)技术得到的…

Mybatis-Plus自动填充时间的配置类

引言&#xff1a;在现代软件开发中&#xff0c;数据库操作是不可或缺的一部分。为了确保数据的准确性和完整性&#xff0c;我们常常需要在数据库记录中添加时间戳&#xff0c;例如创建时间和更新时间。MyBatis-Plus作为一个流行的持久层框架&#xff0c;提供了灵活的机制来实现…

官方权威解读|CNAS-CL01计量溯源性部分解读,供CNAS软件测试实验室参考

CNAS-CL01《检测和校准实验室能力认可准则》是软件测试实验室申请CNAS资质&#xff0c;建设符合CNAS要求的实验室质量管理体系时必须要参考的一部强制性准则。CNAS-CL01一共分为五大核心部分&#xff1a;通用要求、结构要求、资源要求、过程要求和管理体系要求。前面的文章中我…

【零散技术】微信支付保姆教程(一)

序言:时间是我们最宝贵的财富,珍惜手上的每个时分 微信支付十余年&#xff0c;早已成为国内必不可少的支付工具。但是开发对接中 繁杂的各类参数与文档&#xff0c;以及各种证书的申请&#xff0c;着实也成了不少开发者的噩梦&#xff0c;那么今天我们来看看&#xff0c;如何申…

3-3 AUTOSAR RTE 对SR Port的作用

返回总目录->返回总目录<- 一、前言 RTE作为SWC和BSW之间的通信机构,支持Sender-Receiver方式实现ECU内及ECU间的通信。 对于Sender-Receiver Port支持三种模式: 显式访问:若运行实体采用显示模式的S/R通信方式,数据读写是即时的;隐式访问:当多个运行实体需要读取…

小阿轩yx-案例:代码管理系统简介与部署

小阿轩yx-案例&#xff1a;代码管理系统简介与部署 前言 开发一个项目时&#xff0c;如果只有几十行代码或几百行代码时维护还算简单&#xff0c;但是代码数量达到一定程度或两三个人共同开发一个项目时&#xff0c;就很容易会出现代码混乱、冲突、排错难等问题。代码编写完成…