Linux驱动理论(二)——Linux字符设备驱动

一、设备驱动介绍

1、设备驱动分类

设备驱动可以分为下面三类:字符设备驱动、块设备驱动、网络设备驱动

2、设备驱动在内核中的结构

在内核态中,系统调用接口接收用户态应用的请求,经由虚拟文件系统 VFS 向下调度,VFS 对接 Buffer Cache、网络子系统,同时将不同的硬件操作分发到三类设备驱动:字符设备驱动、块设备驱动、网络设备驱动;其中网络子系统内部又分层实现 BSD socket、inet 地址族、TCP/UDP 传输层、IP 网络层,网络设备驱动承接网络子系统的指令;各类设备驱动统一通过设备接口与底层物理硬件交互,完成用户程序到硬件的读写、网络通信等操作,实现了对不同硬件设备的抽象封装,让上层无需关心具体硬件细节。

应用层程序通过系统调用接口进入内核,内核内的虚拟文件系统向上提供统一接口,向下抽象出字符设备文件、块设备文件,同时存在输入子系统、V4L2 等各类内核子系统;TTY 驱动、USB 驱动、磁盘驱动等具体设备驱动,依附于内核子系统与设备文件层,承接上层的操作请求,驱动再与硬件层面的控制器、总线交互,最终控制对应物理硬件,完成应用程序对硬件设备的访问,屏蔽硬件细节。

二、字符设备驱动原理

1、设备号理解

  • 包括主设备号和次设备号。
  • 主设备号: 表示是哪一类的驱动 usb、adc、led、uart、i2c、spi、dma 。
  • 通过 cat /proc/devices 显示系统主设备号。
  • 字符设备和块设备的设备号是独立的。
  • 设备号内核用一个变量来表示 dev_t(u32)来表示一个设备号
  • 高20为来表示主设备号,低12为表示次设备号,主设备号:表示范围 0 ~ ( 1M - 1 ),次设备号:表示范围 0 ~ 4095
  • 可以使用一个宏函数去生成一个设备号
cpp 复制代码
#define MINORBITS 20
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
dev_t devno = MKDEV(500,0)

2、register_chrdev_region 静态申请设备号

cpp 复制代码
#include<linux/fs.h>
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)

函数功能:静态申请一个范围内的设备号

函数参数:【form】,从哪一个设备号开始向内核去申请设备号,向内核去申请一个或多个设备号,如果这个设备号内核没有占用,可以申请到,否则申请失败。【count】,表示要申请设备的个数。【name】,要申请设备号的名称。

函数返回值:成功返回0,失败负数

cpp 复制代码
int ret;
dev_t devno = MKDEV(major,minor);
ret = register_chrdev_region(devno,1,"hello");
if(ret < 0 )
{
printk("fail to get devno\n");
return ret;
}

使用命令去查看申请到的设备号 cat /proc/devices 。

3、alloc_chrdev_region 动态申请设备号

cpp 复制代码
#include<linux/fs.h>
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)

函数功能:动态申请设备号,随机分配一个设备号,一个可用的设备号

函数参数:【dev】,设备号变量的指针;【baseminor】,次设备号开始于那一个数字;【count】,需要的多少个设备;【name】,要申请设备的名称

函数返回值:成功返回0,失败负数

4、unregister_chrdev_region 注销设备号

cpp 复制代码
#include<linux/fs.h>
/**
* unregister_chrdev_region() - return a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count)

函数功能:注销设备号

函数参数:【form】,要释放的设备号;【count】,要释放几个设备号

函数返回值:无返回值

5、tags 索引搜索系统

  • 在内核源码路径下使用命令生成索引
cpp 复制代码
make tags
  • 在内核源码的顶层目录下查找索引
bash 复制代码
vi -t 函数名/结构体名/变量名
vi -t MKDEV
vi -t unregister_chrdev_region

可以找到函数的定义和结构体定义,可以使用ctrl + ] 进行函数跟踪或查找系统所有定义,使用ctrl + t 返回上一级目录

6、cdev_init 字符设备初始化

cpp 复制代码
#include<linux/cdev.h>
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};
/**
cdev_init() - initialize a cdev structure
@cdev: the structure to initialize
@fops: the file_operations for this device
Initializes @cdev, remembering @fops, making it ready to add to the
system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

函数功能:cdev_init() 初始化一个cdev结构体,把cdev这个结构体和file_operations结构体进行关联

函数参数:【cdev】,要初始化的cdev结构体指针;【fops】,要初始化的file_operations结构体指针

函数返回值:无返回值

cpp 复制代码
cdev.owner = THIS_MODULE; // 表示的是指向本模块的指针
#define THIS_MODULE (&__this_module)

7、file_operations 结构体

file_operations结构体是字符设备驱动所支持的文件操作

cpp 复制代码
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
};
cpp 复制代码
struct cdev cdev;
struct file_operations hello_fops = {
    .owner = THIS_MODULE
};
// 表示的是指向本模块的指针
// cdev.owner = THIS_MODULE
// #define THIS_MODULE (&__this_module)
static void char_reg_setup_cdev (void)
{
int error, devno = MKDEV (hello_major, hello_minor);
cdev_init (&cdev, &hello_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &hello_fops;
error = cdev_add (&cdev, devno , 1);
if (error < 0 )
{
printk (KERN_NOTICE "Error %d adding char_reg_setup_cdev", error);
}
}

8、cdev_add 字符设备添加到内核链表内

cpp 复制代码
#include<linux/cdev.h>
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

函数功能:字符设备添加到内核链表内

函数参数:【cdev】,cdev结构体指针;【dev】,设备号;【cout】,设备的个数

函数返回值:成功返回0,失败返回负数

9、cdev_del 从系统链表中删除一个设备

cpp 复制代码
#include<linux/cdev.h>
/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*/
void cdev_del(struct cdev *p)

函数功能:从系统链表中删除一个字符设备

函数参数:【cdev】,要删除的结构体指针

函数返回值:无返回值

10、mknod 创建设备文件

创建设备文件,把设备号和文件进行关联。

11、实例字符设备实现

字符设备实现,对设备进行打开与关闭操作

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
    int fd = open("/dev/hello", O_RDWR);
    if (fd < 0)
    {
        perror("open");
        exit(-1);
    }
    printf("fd=%d\n", fd);
    close(fd);
    return 0;
}
cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/cdev.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("YShengli");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");
#define HELLO_MAJOR 500
// 创建1个字符设备 cdev
struct cdev hello_cdev;
// 应用程序open打开设备文件时调用
static int hello_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Hello open!\n");
return 0;
}
// 应用程序close关闭设备文件时调用
static int hello_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Hello release!\n");
return 0;
}
// file_operations 结构体
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
};
// 模块初始化函数
// insmod hello.ko  -> module_init() -> hello_init()
static int __init hello_init(void)
{
printk(KERN_INFO "Hello world!\n");
// 申请1个设备号
int ret = register_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1, "hello");
if (ret < 0) {
printk(KERN_ERR "Failed to register char device region\n");
return ret;
}
// 初始化 cdev
cdev_init(&hello_cdev, &hello_fops);
// 添加 cdev 到系统
ret = cdev_add(&hello_cdev, MKDEV(HELLO_MAJOR, 0), 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add char device\n");
return ret;
}
return 0;
}
// 模块退出函数
// rmmod hello.ko  -> module_exit() -> hello_exit()
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye world!\n");
// 释放设备号
unregister_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1);
// 删除 cdev 从系统
cdev_del(&hello_cdev);
}
module_init(hello_init);
module_exit(hello_exit);

12、字符设备驱动调用原理

驱动模块先通过cdev_init将hello_cdev字符设备结构体与文件操作函数集fops建立关联,再使用cdev_add把该字符设备注册到内核 cdev 链表;执行mknod创建设备节点时,内核会遍历 cdev 链表,依据设备号把 inode 节点的i_cdev指针指向对应的hello_cdev;当用户态程序调用open("/dev/hello")打开设备文件,内核通过设备号找到对应的 inode 结构体,将 file 结构体的f_op指针赋值为驱动的fops,进而执行驱动里的fops->open函数;调用close关闭设备时,则会触发驱动的fops->release函数,完成用户程序到字符设备驱动的完整调用链路。

  1. 在驱动程序中,首先要创建一个cdev结构,
cpp 复制代码
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};
// 可以手动创建一个cdev结构体加上cdev_init,也可以使用cdev_alloc函数去创建
struct cdev cdev;
cdev_init(&cdev, &fops);
cdev.owner = THIS_MODULE;
  1. 静态创建结构体后有要对结构体进行初始化,使用cdev_init函数进行,cdev_init有两个参数
cpp 复制代码
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;
}
// 把结构体清除为0,然后赋了一个 cdev->ops 的值,因此cdev->ops 指向ops结构体
  1. 使用cdev_add把设备好加入到cdev组成的链表当中。
cpp 复制代码
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    p->dev = dev;
    p->count = count;
    return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
// 把设备号加入到cdev结构体中.因此cdev结构体中的成员ops指向驱动当中的ops结构体,dev成员是保存的设备号
  1. mknod 时会在内核中创建一个inode的结构体,这里只写inode重要的成员信息
cpp 复制代码
struct inode {
    dev_t i_rdev; //该成员表示设备文件的inode结构,它包含了真正的设备编号。
    // 该成员表示字符设备的内核的 内部结构。当inode指向一个字符设备文件时,该成员包含了指向struct cdev结构的指针,
    // 其中cdev结构是字符设备结构体。
    struct cdev *i_cdev;
};
// 执行mknod时会对这个结构体进行赋值,怎么赋值的呢?
// 首先会对i_rdev 这个成员赋值,会对成员赋值为相应的设备号。
// 然后对i_cdev这个指针赋值. 这个是通过查询系统的cdev链表实现的,根据主设备号和次设备号找到cdev这个结构体,
// 因此i_cdev= &cdev.
  1. 应用程序调用 open("/dev/hello")这个函数时
cpp 复制代码
/*
应用程序调用 open("/dev/hello")这个函数时,内核会调用sys_open,继续向下调用
chrdev_open(struct inode * inode, struct file * filp)
内核会在调用open函数时创建struct file 结构体,并且会对file结构体的成员f_op赋值为驱动中的file_operation结构体
(通过设备号可以找到inode结构体,inode结构体中有i_cdev指针,指向系统的cdev的结构体,cdev的成员ops指向驱动的
file_operation成员hello_ops),应用程序调用open,驱动中也会调用file_operation的open函数,故系统正确的调用
应用程序调用open后,对于驱动来说,filep->f_op = hello_ops,最终会调用驱动的open函数
*/
struct file {
    struct path f_path;
    const struct file_operations *f_op;
    spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */
    atomic_long_t f_count;
    struct inode *f_inode; /* cached value */
};

三、字符设备读写数据

1、file_operations的read接口

cpp 复制代码
#include<asm/uaccess.h>
ssize_t (*read) (struct file *filep, char __user *to , size_t count , loff_t * off);

函数功能:把驱动中的数据复制到应用程序内

函数参数:【filep】,指向内核创建的file结构体指针;【to】,要向用户空间发送数据的指针,应用程序的地址;__user,表示应用程序的地址;【count】,用户空间要读取的字节数;【off】,文件指针的偏移量

函数返回值:成功返回发送到应用程序的字节数,失败返回负数

2、copy_to_user

cpp 复制代码
#include<linux/uaccess.h>
extern inline long copy_to_user(void __user *to, const void *from, long n)
{
return __copy_tofrom_user((__force void *)to, from, n, to);
}

函数功能:把驱动中的数据复制到应用程序内

函数参数:【to】,数据的目的,应用程序的地址;【__user】,应用程序的地址;【from】,数据的源,驱动的地址;【n】,数据的大小

函数返回值:成功返回发送到应用程序的字节数,失败返回负数

3、file_operations的write接口

cpp 复制代码
#include<asm/uaccess.h>
ssize_t (*write) (struct file *, const char __user *from, size_t count, loff_t *off);

函数功能:把应用程序内的数据写入到驱动内

函数参数:【filep】,指向内核创建的file结构体指针;【from】,要接受用户空间发给内核空间数据的指针,应用程序的地址;【count】,用户空间要写的字节数;【off】,文件指针的偏移量

函数返回值:成功写入到驱动的字节数,失败返回负数

4、copy_from_user

cpp 复制代码
#include<asm/uaccess.h>
extern inline long copy_from_user(void *to, const void __user *from, long n)
{
    return __copy_tofrom_user(to, (__force void *)from, n, from);
}

函数功能:把应用程序中的数据复制到驱动内

函数参数:【to】,数据的目的,驱动中的地址;【from】,数据的源,应用程序的地址;【n】,数据的大小

函数返回值:成功写入到驱动中的字节数,失败返回负数

5、实例字符设备读写

  • test.c
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    char buf[128]={0};
    int ret; 
    // open -> 驱动调用 -> hello_open()
    int fd = open("/dev/hello", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    printf("fd = %d\n", fd);

    
    memset(buf, 'O', sizeof(buf)-1);   // 写入127个字符O
    // 写入设备文件
    // write -> 驱动调用 -> hello_write()
    ret = write(fd, buf, sizeof(buf));
    if (ret < 0) {
        perror("write");
        return -1;
    }
    memset(buf, 0, sizeof(buf)); // 清空buf
    // 读取设备文件
    // read -> 驱动调用 -> hello_read()
    ret = read(fd, buf, sizeof(buf));
    if (ret < 0) {
        perror("read");
        return -1;
    }
    printf("read %d bytes: %s\n", ret, buf);

    // 关闭设备文件 
    // close -> 驱动调用 -> hello_release()
    close(fd);
    return 0;
}
  • hello.c
cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/cdev.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("YShengli");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");

#define HELLO_MAJOR 500

char buf[128]={0};

// 创建1个字符设备 cdev
struct cdev hello_cdev;

 // 应用程序open打开设备文件时调用
static int hello_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello open!\n");
    return 0;
}

 // 应用程序close关闭设备文件时调用
static int hello_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello release!\n");
    return 0;
}

 // 应用程序read读取设备文件时调用
static ssize_t hello_read(struct file *file, char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello read!\n");
    // 从内核空间复制数据到用户空间
    if (copy_to_user(user_buf,buf, count)) {
        printk(KERN_ERR "Failed to copy to user\n");
        return -EFAULT;
    }
    return count;
}

 // 应用程序write写入设备文件时调用
static ssize_t hello_write(struct file *file, const char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello write!\n");
    // 从用户空间复制数据到内核空间
    if (copy_from_user(buf, user_buf, count)) {
        printk(KERN_ERR "Failed to copy from user\n");
        return -EFAULT;
    }
    return count;
}

// file_operations 结构体
static struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .release = hello_release,
    .read = hello_read,
    .write = hello_write,
};

// 模块初始化函数 
// insmod hello.ko  -> module_init() -> hello_init()
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world!\n");
    // 申请1个设备号
    int ret = register_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1, "hello");
    if (ret < 0) {
        printk(KERN_ERR "Failed to register char device region\n");
        return ret;
    }
    // 初始化 cdev
    cdev_init(&hello_cdev, &hello_fops);
    // 添加 cdev 到系统
    ret = cdev_add(&hello_cdev, MKDEV(HELLO_MAJOR, 0), 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add char device\n");
        return ret;
    }
    return 0;
}

// 模块退出函数
// rmmod hello.ko  -> module_exit() -> hello_exit()
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye world!\n");
    // 释放设备号
    unregister_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1);
    // 删除 cdev 从系统
    cdev_del(&hello_cdev);
}

module_init(hello_init);
module_exit(hello_exit);
  • Makefile
cpp 复制代码
$(warning KERNELRELEASE=$(KERNELRELEASE))           # 打印提示,打印变量内的值
ifeq ($(KERNELRELEASE),)              

#KERNELDIR ?= /home/linux/emb2307/12-linuxsys/myir-imx-linux      #交叉开发内核路径

KERNELDIR ?= /lib/modules/$(shell uname -r)/build   # 本地开发, ubuntu22.04的路径,内核头文件目录

PWD := $(shell pwd)                     # 获取路径

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	rm -rf *.o *~ core .depend .*.cmd *.mod *.mod.c .tmp_versions Module* modules*
	gcc test.c -o test

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod *.mod.c .tmp_versions Module* modules*
	rm -rf test

.PHONY: modules modules_install clean

else
    obj-m := hello.o
endif

四、字符设备的控制

1、ioctl 控制设备

cpp 复制代码
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

函数功能:给设备发送控制命令, 是linux的系统调用函数。

函数参数:【fd】,要操作的设备文件描述符;【request】,要发送的命令;... ,可变参数。

函数返回值:成功返回0,失败返回-1。

2、file_operations的unlocked_ioctl接口

cpp 复制代码
#include<linux/fs.h>
#include<asm/ioctl.h>
long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg);

函数功能:应用程序调用ioctl , 则驱动调用unlocked_ioctl接口函数。

函数参数:【file】,要操作文件file结构体;【cmd】,应用程序发送的命令,就是应用程序的request参数;【arg】, ioctl的第三个参数。

函数返回值:成功返回0,失败返回负的错误码。

3、ioctl命令定义格式

函数功能:驱动中和应用程序中关于ioctl的命令定义格式。

函数参数:【type】,表示类型,是命令的类别(组),这个数的表示范围是0x0 ~ 0xff。【nr】,这一组命令的第几个,led控制的方式, ledon、ledoff;【size】,读写的数据。【IO】,读设备还是写设备,_IO 只控制设备,不读写设备;_IOR读设备,读取1个字节的数据;_IOW写设备,写1个字节的数据;_IOWR读写设备,读写1个字节的数据。

例如,定义1个命令:

cpp 复制代码
#define LEDON _IO('A',0)
#define LEDOFF _IO('A',1)
#define LEDSET _IOW('A',2,1)

4、实例控制设备

实现对字符设备的控制操作,给设备发送命令

  • test.c
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define  LEDON      _IO('A', 0x00)
#define  LEDOFF     _IO('A', 0x01)
#define  LEDSET     _IO('A', 0x02)


int main(int argc, char const *argv[])
{
    char buf[128]={0};
    int ret; 
    // open -> 驱动调用 -> hello_open()
    int fd = open("/dev/hello", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    printf("fd = %d\n", fd);

    // 控制LED
    // ioctl -> 驱动调用 -> hello_ioctl()
    ret = ioctl(fd, LEDON);
    if (ret < 0) {
        perror("ioctl LEDON");
        return -1;
    }
    printf("LED ON\n");

    // 控制LED
    // ioctl -> 驱动调用 -> hello_ioctl()
    ret = ioctl(fd, LEDOFF);
    if (ret < 0) {
        perror("ioctl LEDOFF");
        return -1;
    }
    printf("LED OFF\n");

    // 控制LED
    // ioctl -> 驱动调用 -> hello_ioctl()
    ret = ioctl(fd, LEDSET, 0x01);
    if (ret < 0) {
        perror("ioctl LEDSET");
        return -1;
    }
    printf("LED SET\n");


    // 关闭设备文件 
    // close -> 驱动调用 -> hello_release()
    close(fd);
    return 0;
}
  • hello.c
cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#define LEDON _IO('A', 0x00)
#define LEDOFF _IO('A', 0x01)
#define LEDSET _IO('A', 0x02)

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("YShengli");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");

#define HELLO_MAJOR 500

char buf[128] = {0};

// 创建1个字符设备 cdev
struct cdev hello_cdev;

// 应用程序open打开设备文件时调用
static int hello_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello open!\n");
    return 0;
}

// 应用程序close关闭设备文件时调用
static int hello_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello release!\n");
    return 0;
}

// 应用程序read读取设备文件时调用
static ssize_t hello_read(struct file *file, char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello read!\n");
    // 从内核空间复制数据到用户空间
    if (copy_to_user(user_buf, buf, count))
    {
        printk(KERN_ERR "Failed to copy to user\n");
        return -EFAULT;
    }
    return count;
}

// 应用程序write写入设备文件时调用
static ssize_t hello_write(struct file *file, const char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello write!\n");
    // 从用户空间复制数据到内核空间
    if (copy_from_user(buf, user_buf, count))
    {
        printk(KERN_ERR "Failed to copy from user\n");
        return -EFAULT;
    }
    return count;
}

static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    printk(KERN_INFO "Hello ioctl!\n");
    // 根据cmd判断控制LED的哪个操作
    switch (cmd)
    {
    case LEDON:
        printk(KERN_INFO "LED ON\n");
        break;
    case LEDOFF:
        printk(KERN_INFO "LED OFF\n");
        break;
    case LEDSET:
        printk(KERN_INFO "LED SET\n");
        break;
    default:
        printk(KERN_ERR "Invalid ioctl command\n");
        return -EINVAL;
    }
    return 0;
}

// file_operations 结构体
static struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .release = hello_release,
    .read = hello_read,
    .write = hello_write,
    .unlocked_ioctl = hello_ioctl,
};

// 模块初始化函数
// insmod hello.ko  -> module_init() -> hello_init()
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world!\n");
#if 0
    // 申请1个设备号
    int ret = register_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1, "hello");
    if (ret < 0) {
        printk(KERN_ERR "Failed to register char device region\n");
        return ret;
    }
    // 初始化 cdev
    cdev_init(&hello_cdev, &hello_fops);
    // 添加 cdev 到系统
    ret = cdev_add(&hello_cdev, MKDEV(HELLO_MAJOR, 0), 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add char device\n");
        return ret;
    }
#else
    // register_chrdev 使用这个函数
    int ret = register_chrdev(HELLO_MAJOR, "hello", &hello_fops);
    if (ret < 0)
    {
        printk(KERN_ERR "Failed to register char device\n");
        return ret;
    }
#endif
    return 0;
}

// 模块退出函数
// rmmod hello.ko  -> module_exit() -> hello_exit()
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye world!\n");
#if 0 
    // 释放设备号
    unregister_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1);
    // 删除 cdev 从系统
    cdev_del(&hello_cdev);
#else
    // 注销字符设备
    unregister_chrdev(HELLO_MAJOR, "hello");
#endif
}

module_init(hello_init);
module_exit(hello_exit);
  • Makefile
cpp 复制代码
$(warning KERNELRELEASE=$(KERNELRELEASE))           # 打印提示,打印变量内的值
ifeq ($(KERNELRELEASE),)              

#KERNELDIR ?= /home/linux/emb2307/12-linuxsys/myir-imx-linux      #交叉开发内核路径

KERNELDIR ?= /lib/modules/$(shell uname -r)/build   # 本地开发, ubuntu22.04的路径,内核头文件目录

PWD := $(shell pwd)                     # 获取路径

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	rm -rf *.o *~ core .depend .*.cmd *.mod *.mod.c .tmp_versions Module* modules*
	gcc test.c -o test

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod *.mod.c .tmp_versions Module* modules*
	rm -rf test

.PHONY: modules modules_install clean

else
    obj-m := hello.o
endif

五、字符设备新方法

1、字符设备注册方法 register_chrdev

cpp 复制代码
#include<linux/fs.h>
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{
    return __register_chrdev(major, 0, 256, name, fops);
}

函数功能:注册一个字符设备到系统内,register_chrdev = register_chrdev_region + cdev_init + cdev_add。

函数参数:【major】,主设备号;【name】,设备的名称;【fops】,file_operations 结构体指针。

函数返回值:成功返回0,失败返回负的错误码。

2、字符设备注册方法 unregister_chrdev

cpp 复制代码
#include<linux/fs.h>
static inline void unregister_chrdev(unsigned int major, const char *name)
{
    __unregister_chrdev(major, 0, 256, name);
}

函数功能:从系统中注销一个字符设备,unregister_chrdev = unregister_chrdev_region + cdev_del。

函数参数:【major】,主设备号;【name】,设备的名称。

函数返回值:成功返回0,失败返回负的错误码。

cpp 复制代码
​
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#define LEDON _IO('A', 0x00)
#define LEDOFF _IO('A', 0x01)
#define LEDSET _IO('A', 0x02)

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("YShengli");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");

#define HELLO_MAJOR 500

char buf[128] = {0};

// 创建1个字符设备 cdev
struct cdev hello_cdev;

extern void display(void); 

// 应用程序open打开设备文件时调用
static int hello_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello open!\n");
    return 0;
}

// 应用程序close关闭设备文件时调用
static int hello_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello release!\n");
    return 0;
}

// 应用程序read读取设备文件时调用
static ssize_t hello_read(struct file *file, char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello read!\n");
    // 从内核空间复制数据到用户空间
    if (copy_to_user(user_buf, buf, count))
    {
        printk(KERN_ERR "Failed to copy to user\n");
        return -EFAULT;
    }
    return count;
}

// 应用程序write写入设备文件时调用
static ssize_t hello_write(struct file *file, const char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello write!\n");
    // 从用户空间复制数据到内核空间
    if (copy_from_user(buf, user_buf, count))
    {
        printk(KERN_ERR "Failed to copy from user\n");
        return -EFAULT;
    }
    return count;
}

static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    printk(KERN_INFO "Hello ioctl!\n");
    // 根据cmd判断控制LED的哪个操作
    switch (cmd)
    {
    case LEDON:
        printk(KERN_INFO "LED ON\n");
        break;
    case LEDOFF:
        printk(KERN_INFO "LED OFF\n");
        break;
    case LEDSET:
        printk(KERN_INFO "LED SET\n");
        break;
    default:
        printk(KERN_ERR "Invalid ioctl command\n");
        return -EINVAL;
    }
    return 0;
}

// file_operations 结构体
static struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .release = hello_release,
    .read = hello_read,
    .write = hello_write,
    .unlocked_ioctl = hello_ioctl,
};

// 模块初始化函数
// insmod hello.ko  -> module_init() -> hello_init()
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world!\n");
#if 0
    // 申请1个设备号
    int ret = register_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1, "hello");
    if (ret < 0) {
        printk(KERN_ERR "Failed to register char device region\n");
        return ret;
    }
    // 初始化 cdev
    cdev_init(&hello_cdev, &hello_fops);
    // 添加 cdev 到系统
    ret = cdev_add(&hello_cdev, MKDEV(HELLO_MAJOR, 0), 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add char device\n");
        return ret;
    }
#else
    // register_chrdev 使用这个函数
    int ret = register_chrdev(HELLO_MAJOR, "hello", &hello_fops);
    if (ret < 0)
    {
        printk(KERN_ERR "Failed to register char device\n");
        return ret;
    }
    display();
#endif
    return 0;
}

// 模块退出函数
// rmmod hello.ko  -> module_exit() -> hello_exit()
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye world!\n");
#if 0 
    // 释放设备号
    unregister_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1);
    // 删除 cdev 从系统
    cdev_del(&hello_cdev);
#else
    // 注销字符设备
    unregister_chrdev(HELLO_MAJOR, "hello");
#endif
}

module_init(hello_init);
module_exit(hello_exit);

​

六、多文件与多模块编译

1、多文件编译

在Makefile中修改如下:

bash 复制代码
obj-m := hello.o
hello-objs := hello1.o hello2.o

2、实例多文件编译

  • 源文件
cpp 复制代码
07-char_register/Makefile
07-char_register/hello1.c
07-char_register/hello2.c
  • 源代码
cpp 复制代码
$(warning KERNELRELEASE=$(KERNELRELEASE))           # 打印提示,打印变量内的值
ifeq ($(KERNELRELEASE),)              

#KERNELDIR ?= /home/linux/emb2307/12-linuxsys/myir-imx-linux      #交叉开发内核路径

KERNELDIR ?= /lib/modules/$(shell uname -r)/build   # 本地开发, ubuntu22.04的路径,内核头文件目录

PWD := $(shell pwd)                     # 获取路径

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	rm -rf *.o *~ core .depend .*.cmd *.mod *.mod.c .tmp_versions Module* modules*
	gcc test.c -o test

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod *.mod.c .tmp_versions Module* modules*
	rm -rf test

.PHONY: modules modules_install clean

else
    obj-m := hello.o
	hello-objs := hello1.o hello2.o 
endif

hello1.c

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#define LEDON _IO('A', 0x00)
#define LEDOFF _IO('A', 0x01)
#define LEDSET _IO('A', 0x02)

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("YShengli");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");

#define HELLO_MAJOR 500

char buf[128] = {0};

// 创建1个字符设备 cdev
struct cdev hello_cdev;

extern void display(void); 

// 应用程序open打开设备文件时调用
static int hello_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello open!\n");
    return 0;
}

// 应用程序close关闭设备文件时调用
static int hello_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello release!\n");
    return 0;
}

// 应用程序read读取设备文件时调用
static ssize_t hello_read(struct file *file, char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello read!\n");
    // 从内核空间复制数据到用户空间
    if (copy_to_user(user_buf, buf, count))
    {
        printk(KERN_ERR "Failed to copy to user\n");
        return -EFAULT;
    }
    return count;
}

// 应用程序write写入设备文件时调用
static ssize_t hello_write(struct file *file, const char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello write!\n");
    // 从用户空间复制数据到内核空间
    if (copy_from_user(buf, user_buf, count))
    {
        printk(KERN_ERR "Failed to copy from user\n");
        return -EFAULT;
    }
    return count;
}

static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    printk(KERN_INFO "Hello ioctl!\n");
    // 根据cmd判断控制LED的哪个操作
    switch (cmd)
    {
    case LEDON:
        printk(KERN_INFO "LED ON\n");
        break;
    case LEDOFF:
        printk(KERN_INFO "LED OFF\n");
        break;
    case LEDSET:
        printk(KERN_INFO "LED SET\n");
        break;
    default:
        printk(KERN_ERR "Invalid ioctl command\n");
        return -EINVAL;
    }
    return 0;
}

// file_operations 结构体
static struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .release = hello_release,
    .read = hello_read,
    .write = hello_write,
    .unlocked_ioctl = hello_ioctl,
};

// 模块初始化函数
// insmod hello.ko  -> module_init() -> hello_init()
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world!\n");
#if 0
    // 申请1个设备号
    int ret = register_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1, "hello");
    if (ret < 0) {
        printk(KERN_ERR "Failed to register char device region\n");
        return ret;
    }
    // 初始化 cdev
    cdev_init(&hello_cdev, &hello_fops);
    // 添加 cdev 到系统
    ret = cdev_add(&hello_cdev, MKDEV(HELLO_MAJOR, 0), 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add char device\n");
        return ret;
    }
#else
    // register_chrdev 使用这个函数
    int ret = register_chrdev(HELLO_MAJOR, "hello", &hello_fops);
    if (ret < 0)
    {
        printk(KERN_ERR "Failed to register char device\n");
        return ret;
    }
    display();
#endif
    return 0;
}

// 模块退出函数
// rmmod hello.ko  -> module_exit() -> hello_exit()
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye world!\n");
#if 0 
    // 释放设备号
    unregister_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1);
    // 删除 cdev 从系统
    cdev_del(&hello_cdev);
#else
    // 注销字符设备
    unregister_chrdev(HELLO_MAJOR, "hello");
#endif
}

module_init(hello_init);
module_exit(hello_exit);

hello2.c

cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
void display(void)
{
    printk("driver:display\n");
}

3、多模块编译

多模块就是多驱动,可以编译出来好几个驱动。

cpp 复制代码
obj-m += hello.o
obj-m += char.o

4、实例多模块编译

  • hello.c
cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#define LEDON _IO('A', 0x00)
#define LEDOFF _IO('A', 0x01)
#define LEDSET _IO('A', 0x02)

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("YShengli");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");

#define HELLO_MAJOR 500

char buf[128] = {0};

// 创建1个字符设备 cdev
struct cdev hello_cdev;

// 应用程序open打开设备文件时调用
static int hello_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello open!\n");
    return 0;
}

// 应用程序close关闭设备文件时调用
static int hello_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello release!\n");
    return 0;
}

// 应用程序read读取设备文件时调用
static ssize_t hello_read(struct file *file, char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello read!\n");
    // 从内核空间复制数据到用户空间
    if (copy_to_user(user_buf, buf, count))
    {
        printk(KERN_ERR "Failed to copy to user\n");
        return -EFAULT;
    }
    return count;
}

// 应用程序write写入设备文件时调用
static ssize_t hello_write(struct file *file, const char __user *user_buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "Hello write!\n");
    // 从用户空间复制数据到内核空间
    if (copy_from_user(buf, user_buf, count))
    {
        printk(KERN_ERR "Failed to copy from user\n");
        return -EFAULT;
    }
    return count;
}

static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    printk(KERN_INFO "Hello ioctl!\n");
    // 根据cmd判断控制LED的哪个操作
    switch (cmd)
    {
    case LEDON:
        printk(KERN_INFO "LED ON\n");
        break;
    case LEDOFF:
        printk(KERN_INFO "LED OFF\n");
        break;
    case LEDSET:
        printk(KERN_INFO "LED SET\n");
        break;
    default:
        printk(KERN_ERR "Invalid ioctl command\n");
        return -EINVAL;
    }
    return 0;
}

// file_operations 结构体
static struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .release = hello_release,
    .read = hello_read,
    .write = hello_write,
    .unlocked_ioctl = hello_ioctl,
};

// 模块初始化函数
// insmod hello.ko  -> module_init() -> hello_init()
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello world!\n");
#if 0
    // 申请1个设备号
    int ret = register_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1, "hello");
    if (ret < 0) {
        printk(KERN_ERR "Failed to register char device region\n");
        return ret;
    }
    // 初始化 cdev
    cdev_init(&hello_cdev, &hello_fops);
    // 添加 cdev 到系统
    ret = cdev_add(&hello_cdev, MKDEV(HELLO_MAJOR, 0), 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add char device\n");
        return ret;
    }
#else
    // register_chrdev 使用这个函数
    int ret = register_chrdev(HELLO_MAJOR, "hello", &hello_fops);
    if (ret < 0)
    {
        printk(KERN_ERR "Failed to register char device\n");
        return ret;
    }
#endif
    return 0;
}

// 模块退出函数
// rmmod hello.ko  -> module_exit() -> hello_exit()
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye world!\n");
#if 0 
    // 释放设备号
    unregister_chrdev_region(MKDEV(HELLO_MAJOR, 0), 1);
    // 删除 cdev 从系统
    cdev_del(&hello_cdev);
#else
    // 注销字符设备
    unregister_chrdev(HELLO_MAJOR, "hello");
#endif
}

module_init(hello_init);
module_exit(hello_exit);

再复制一份命名为char.c

cpp 复制代码
$(warning KERNELRELEASE=$(KERNELRELEASE))           # 打印提示,打印变量内的值
ifeq ($(KERNELRELEASE),)              

#KERNELDIR ?= /home/linux/emb2307/12-linuxsys/myir-imx-linux      #交叉开发内核路径

KERNELDIR ?= /lib/modules/$(shell uname -r)/build   # 本地开发, ubuntu22.04的路径,内核头文件目录

PWD := $(shell pwd)                     # 获取路径

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	rm -rf *.o *~ core .depend .*.cmd *.mod *.mod.c .tmp_versions Module* modules*
	gcc test.c -o test

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod *.mod.c .tmp_versions Module* modules*
	rm -rf test

.PHONY: modules modules_install clean

else
    obj-m += hello.o
    obj-m += char.o
endif
相关推荐
well06121 小时前
Linux粘滞位与Makefile机制深度解析
linux·运维·服务器
再写一行代码就下班1 小时前
linux sh脚本在windows修改导致无法使用解决方式
java·linux·centos
额额额对了1 小时前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm
子木HAPPY阳VIP2 小时前
Ubuntu 关闭防火墙操作步骤
linux·运维·ubuntu
王振超wzc2 小时前
嵌入式开发环境搭建--WM软件安装,Ubuntu操作系统安装
linux·运维·ubuntu
GeW4 小时前
制造强国底座:Linux与数据库驱动的新型工业基础设施
linux
xiaoye-duck4 小时前
《Linux 网络编程》深入理解 IO 多路复用:select 函数详解与 Echo 服务实战
linux·网络
Tairitsu_H4 小时前
[Linux系统] 命令行参数 | 环境变量 | 程序地址空间
linux·环境变量·命令行参数·程序地址空间
雯宝4 小时前
devices_init / buses_init / classes_init / firmware_init / hypervisor_init 函数
linux