day48-设备驱动

day48-设备驱动

一、设备驱动简介

  Linux的设备驱动是一种软件层,它可以让应用程序和硬件设备之间进行通信,同时隐藏设备的工作细节。Linux的设备驱动可以分为三种基本类型:字符设备驱动块设备驱动网络设备驱动

  • 字符设备驱动是一种可以按字节以串行顺序依次访问的设备,如键盘,鼠标等。
  • 块设备驱动是一种可以用任意顺序进行访问,以块为单位进行操作的设备,如硬盘,软驱等。
  • 网络设备驱动是一种负责发送和接收数据报文的设备,如网卡等。

除了这三种基本类型,Linux还定义了一些独特的驱动体系结构,如TTY驱动,IIC驱动,USB驱动,PCI驱动,LCD驱动等。

  Linux的设备驱动可以以两种方式存在:编译进内核或者作为模块加载进内核。编译进内核的设备驱动会在启动内核时就被激活,而模块方式的设备驱动会在需要时才被加载进内核空间。Linux的设备驱动都按照操作系统给出的独立于设备的接口而设计,应用程序可以使用统一的系统调用接口来访问各个设备。

  Linux的设备驱动模型包含设备 (device)、总线 (bus)、类 (class)和驱动 (driver),它们之间相互关联。其中设备 (device)和驱动 (driver)通过总线 (bus)绑定在一起。Linux内核中,分别用 bus_type 、 device_driver 和 device 结构来描述总线、驱动和设备。每个设备都有一个对应的设备文件(又名:设备节点),用户程序通过这个文件来使用驱动程序操作设备。每个设备文件都有一个主从设备号来标识其类型和编号,并且有一个 inode 结构来管理其属性和操作函数指针。除了读写设备文件外,用户程序还可以通过 ioctl 接口来配置和修改特定的设备属性。

驱动




二、字符设备编程

2.1 设备号表示方式

设备号的类型为dev_t,该类型在<linux/types.h>中定义。

在内核版本2.6.0中,dev_t是32位,其中高12位用于表示主设备号,其余20位用于表示次设备号。

驱动

对于主次设备号,可以使用<linux/kdev_t.h>的宏来获取dev_t类型的主次设备号,也可以用其中的宏来生产一个dev_t

1
2
3
MAJOR(dev_t dev); //用于获取设备的主设备号
MINOR(dev_t dev); //用于获取设备的次设备号
MKDEV(int major, int minor); //根据所给的主次设备号生成dev_t

2.2 字符设备主次设备号分配和释放

内核中要创建一个设备,首先需要分配设备号,分配的设备号可以手动分配,也可以自动分配。
用于分配设备号的函数在<linux/fs.h>中。

  • 静态分配
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int register_chrdev_region(dev_t first, unsigned int count, char* name);

    //first:
    //要分配的设备编号的范围起始值。
    //count:
    //是要分配的设备号数量。
    //name:
    //和该设备关联的名字。
    //返回值:
    //设备分配成功时,该函数返回0,失败时返回负的错误码。
  • 动态分配
1
2
3
4
5
6
7
8
9
10
11
12
int alloc_chrdev_region(dev_t* dev, unsigned int firstminor, unsigned int count, char* name);

// dev:
// dev用于保存输出的设备号。
// firstminor:
// 该参数是次设备号的分配范围起始值。
// count:
// 该参数是要分配次设备号数量。
// name:
// 和该设备关联的名字。
// 返回值:
// 设备分配成功时,该函数返回0,失败时返回负的错误码。

分配成功后,可以通过指令cat /proc/devices | grep <设备名字>来查看。

  • 释放设备号
    1
    2
    3
    4
    5
    6
        void unregister_chrdev_region(dev_t dev, unsigned int count);

    // dev
    // 该参数为需要释放的设备号
    // count
    // 该参数为需要释放的设备数量

2.3 和字符设备相关的重要数据结构以及函数

  • cdev
    cdev表示了一个字符设备,其中最重要的是成员是ops,我们字符设备所能实现的功能都需要依靠这个这个。用户在调用相关函数时最终会通过这个ops指向我们所实现的各种函数中。其中的owner表示了模块的所属,一般都初始化为宏THIS_MODULE
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //内核版本:5.4.0-1071-raspi
    struct cdev {
    struct kobject kobj;
    struct module *owner;//所属模块
    const struct file_operations *ops;//文件操作
    struct list_head list;
    dev_t dev;//设备号
    unsigned int count;
    } __randomize_layout;

初始化一个字符设备和注销字符设备时会用到如下几种函数:

1
2
3
4
5
6
7
8
9
//初始化一个cdev结构体,并和file_operations绑定
void cdev_init(struct cdev *, const struct file_operations *);
//分配一个cdev结构体
struct cdev *cdev_alloc(void);
//添加一个字符设备到内核,在添加之前,第二个参数设备号需要已经被注册过,第三个参数是分配的范围,以所给的设备号,初始几个设备。
int cdev_add(struct cdev *, dev_t, unsigned);
//cdev_del必须和cdev_add配合使用,cdev_del用于删除一个字符设备。在卸载驱动时要删除已经添加了的字符设备。
void cdev_del(struct cdev *);

  • struct file_operations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//内核版本:5.4.0-1071-raspi
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
//。。。省略
};

该结构用来表明设备的文件操作,用户应用的文件操作(open,write,read,close等操作)最终会通过调用该结构中的函数指针来调用我们自己定义相关操作函数。设备不支持的调用可以设置为NULL;
该结构中的struct module *owner;是指向拥有该结构体模块的指针,一般该成员会被初始化为<linux/module.h>中的宏THIS_MODULE
其中常用的操作有open,write,read,llseek,unlocked_ioctl等。

2.4 创建字符设备步骤

字符设备创建中的必要步骤有以下几点 :

  • 申请或分配设备号,可以使用 register_chrdev_region 或 alloc_chrdev_region 函数,设备号由主设备号和次设备号组成,主设备号用来区分不同种类的设备,次设备号用来区分同一类型的多个设备。
  • 定义并实现文件操作结构体,如 file_operations ,它包含了对设备文件的读写、打开、关闭等操作函数的指针。
  • 初始化字符设备结构体,可以使用 cdev_init 或 cdev_alloc 函数,字符设备结构体是 cdev 类型,它包含了设备号、文件操作结构体指针等信息。
  • 添加字符设备到内核,可以使用 cdev_add 函数,这样内核就能识别该设备并调用相应的操作函数。
  • 创建设备节点,可以使用 mknod 命令或 device_create 函数,在 /dev 目录下生成一个与设备关联的文件,用户程序可以通过这个文件来访问设备。

在卸载字符设备时,需要执行以下步骤 :

  • 删除字符设备,可以使用 cdev_del 函数,这样内核就不再识别该设备。
  • 释放设备号,可以使用 unregister_chrdev_region 函数,这样该设备号就可以被其他设备使用。
  • 释放分配的内存空间,可以使用 kfree 函数,避免内存泄漏。

2.5 编译和加载字符设备步骤

  1. 编写驱动源代码文件,如 chardev.c ,实现字符设备的操作函数,如 open, read, write, release 等,并定义 file_operations 结构体,将操作函数指针赋值给相应的成员。
  2. 编写 Makefile 文件,指定内核源码目录,模块名称,编译选项等,如 obj-m := chardev.o 。
  3. 在终端中进入驱动源码目录,执行 make 命令,生成模块文件,如 chardev.ko 。
  4. 使用 insmod 或 modprobe 命令加载模块文件到内核空间,如 insmod chardev.ko ,可以使用 lsmod 或 cat /proc/devices 命令查看模块是否加载成功。
  5. 使用 mknod 或 device_create 函数创建设备节点,如 mknod /dev/chardev c 232 0 ,其中 c 表示字符设备,232 是主设备号,0 是次设备号。
  6. 使用 chmod 命令修改设备节点的权限,如 chmod 666 /dev/chardev ,使得用户程序可以访问该设备。
    编写用户程序,如 test.c ,使用 open, read, write, close 等系统调用来操作设备文件,如 open(“/dev/chardev”, O_RDWR) 。
  7. 编译用户程序,生成可执行文件,如 gcc test.c -o test 。(可在makefile一并编译)
  8. 在终端中运行用户程序,测试驱动功能,如 ./test 。
  9. 使用 rmmod 命令卸载模块文件,如 rmmod chardev ,可以使用 lsmod 或 cat /proc/devices 命令查看模块是否卸载成功。

2.6 示例代码

  • myled.c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    //1. 头文件
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kdev_t.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/device.h>
    #include <linux/uaccess.h>
    #include <asm/io.h>
    #include "cmd.h"

    #define CHAR_MAJOR 500 //主设备号
    #define CHAR_MINOR 0 //次设备号
    static dev_t devno; //设备号
    static int count = 1; //设备数量
    static struct class *mycls;
    static struct device *mydev;
    static struct cdev mycdev; //字符设备
    static char kbuf[32];

    //led2的物理地址
    #define GPX2CON 0x11000C40
    #define GPX2DAT 0x11000C44
    //内核虚拟地址
    static volatile unsigned int *conaddr;
    static volatile unsigned int *dataddr;

    static int myled_open(struct inode *id, struct file *fp)
    {
    printk("open ok!\n");
    return 0;
    }

    static int myled_close(struct inode *id, struct file *fp)
    {
    printk("close ok!\n");
    return 0;
    }

    static ssize_t myled_read(struct file *fp, char __user *ubuf, size_t len, loff_t *off)
    {
    int ret;
    //从设备中读取数据到用户空间

    ret = copy_to_user(ubuf, kbuf, len);
    if (0 != ret)
    return -1;

    return 0;
    }

    static ssize_t myled_write(struct file *fp, const char __user *ubuf, size_t len, loff_t *off)
    {
    int ret;
    ret = copy_from_user(kbuf, ubuf, len);
    if (0 != ret)
    return -1;
    return 0;
    }

    static long myled_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
    {
    switch (cmd)
    {
    case LED_ON:
    printk("LED on!\n");
    writel(readl(dataddr)|(0x1<<7), dataddr);
    break;
    case LED_OFF:
    printk("LED off!\n");
    writel(readl(dataddr)&(~(0x1<<7)), dataddr);
    break;
    default:
    printk("No such command!\n");
    }
    return 0;
    }


    static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = myled_open,
    .release = myled_close,
    .read = myled_read,
    .write = myled_write,
    .unlocked_ioctl = myled_ioctl,

    };

    //2. 定义加载与卸载函数
    static int __init myled_init(void)
    {
    /*一. kernel*/
    int ret;

    //1. 申请设备号
    #if 1
    //a) 静态申请设备号
    devno = MKDEV(CHAR_MAJOR, CHAR_MINOR);
    ret = register_chrdev_region(devno, count, "myleddev");
    #else
    //b) 动态申请设备号
    ret = alloc_chrdev_region(&devno, 0, count, "myleddev");
    #endif
    if (0 != ret)
    return -1;

    //2. 初始化字符设备
    cdev_init(&mycdev, &fops);

    //3. 添加设备
    ret = cdev_add(&mycdev, devno, count);
    if (0 != ret)
    goto err;

    //4. 创建设备节点
    //1) 手动创建: mknod /dev/设备 c 主设备号 次设备号
    //2) 代码创建
    mycls = class_create(THIS_MODULE, "myledcls");
    if (IS_ERR(mycls))
    goto err1;

    mydev = device_create(mycls, NULL, devno, NULL, "myled");
    if (IS_ERR(mydev))
    goto err2;

    /*二. hardware*/
    //1. 把物理地址映射到内核空间
    conaddr = ioremap(GPX2CON, 0x4);
    dataddr = ioremap(GPX2DAT, 0x4);

    //2. 控制硬件
    writel((readl(conaddr)&(~(0xf<<28)))|(0x1<<28), conaddr); //设置gpx2_7 output
    writel(readl(dataddr)&(~(0x1<<7)), dataddr); //关灯

    printk("myled init ok!\n");
    return 0;

    err2:
    //销毁类
    class_destroy(mycls);
    err1:
    //删除设备
    cdev_del(&mycdev);
    err:
    //注销设备号
    unregister_chrdev_region(devno, count);
    return -1;
    }

    static void __exit myled_exit(void)
    {
    //解除映射
    iounmap(conaddr);
    iounmap(dataddr);
    //销毁设备节点
    device_destroy(mycls, devno);
    //销毁类
    class_destroy(mycls);
    //删除设备
    cdev_del(&mycdev);
    //注销设备号
    unregister_chrdev_region(devno, count);
    printk("myled exit ok!\n");
    }

    //3. 声明加载与卸载函数
    module_init(myled_init);
    module_exit(myled_exit);

    //4. 模块信息
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Liu Jing");
    MODULE_DESCRIPTION("This is a sample chrdev driver!");




  • cmd.h:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #ifndef __MYCMD_H__
    #define __MYCMD_H__

    //#define LED_ON 0
    //#define LED_OFF 1

    #define LED_ON _IOW('L', 0, int)
    #define LED_OFF _IOW('L', 1, int)

    #define PWM_ON _IOW('P', 0, int)
    #define PWM_OFF _IOW('P', 1, int)

    #endif

  • app.c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    /*===============================================
    * 文件名称:app.c
    * 创 建 者:
    * 创建日期:2023年06月02日
    * 描 述:
    ================================================*/
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/ioctl.h>
    #include "cmd.h"

    int main(int argc, char *argv[])
    {
    int fd = open("/dev/myled", O_RDWR);
    if (fd < 0)
    {
    perror("open");
    return -1;
    }
    printf("open success!\n");

    while (1)
    {
    ioctl(fd, LED_ON);
    sleep(1);
    ioctl(fd, LED_OFF);
    sleep(1);
    }

    close(fd);

    return 0;
    }

    对应makefile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
KERNEL_DIR = /home/hqyj/homework/linux-3.14.0
CUR_DIR = $(shell pwd)

ifeq ($(KERNELRELEASE), )

all:
make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
arm-none-linux-gnueabi-gcc app.c -o app

clean:
make -C $(KERNEL_DIR) M=$(CUR_DIR) clean

install:
cp *.ko /source/nfs/rootfs
cp app /source/nfs/rootfs

else

obj-m = myled.o

endif

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2020-2024 nakano-mahiro
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信