程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

linux驱动基础概念以及驱动程序框架搭建

在进行linux驱动开发之前,我们先来思考一下什么是linux驱动?我们在前面的文章中介绍过Mini2440裸机程序的开发,比如如何点亮LED、如何通过LCD显示图片。

但是如果我们开发板移植了linux内核之后,我还想点亮LED,那该怎么办呢?

由于我们编写的应用程序是无法和硬件直接打交道的,为此衍生了驱动程序,驱动程序充当了硬件和应用程序之间的枢纽。

因此驱动程序的表现形式可能就是一些标准的、事先协定好的API函数,编写一个驱动只需要去完成相应函数的填充就可以了。

一、linux驱动介绍

1.1 linux内核架构

linux内核作为操作系统内核,向下承接最底层的硬件驱动,向上提供应用层的接口实现,适用于各类软硬件结合系统。

其拥有五大核心部分:进程管理、内存管理、文件系统、设备驱动与网络模块。

下图是从网上找来的linux内核的架构图,下图反映了应用程序、linux内核、驱动程序、硬件的关系:

这里我们只介绍设备驱动,其它部分内容不是我们这一节的重点。

1.2 linux设备驱动

linux 将所有的外设分为 3 类:

  • 字符设备:字符设备是能够像字节流(比如文件)一样被访问的设备,就是说对它的读写是以字节为单位的。 比如串口在进行收发数据时就是一个字节一个字节的进行的,我们可以在驱动程序内部使用缓冲区来存放数据以提高效率,但是串口本身对这并没有要求。字符设备的驱动程序中实现了 open、close、read、write 等系统调用,应用程序可以通过设备文件(比如/dev/ttySAC0 等)来访问字符设备。
  • 块设备:块设备上的数据以块的形式存放,比如 NAND FLASH上的数据就是以页为单位存放的。块设备驱动程序向用户层提供的接口与字符设备一样, 应用程序也可以通过相应的设备文件(比如/dev/mtdblock0、/dev/hda1 等)来调用 open、close、read、write 等系统调用,与块设备传送任意字节的数据。对用户而言,字符设备和块设备的访问方式没有差别。块设备驱动程序的特别之处如下。

1). 操作硬件的接口实现方式不一样。
块设备驱动程序先将用户发来的数据组织成块,再写入设备;或从设备中读出若干块数据,再从中挑出用户需要的。
2). 数据块上的数据可以有一定的格式。
通常在块设备中按照一定的格式存放数据,不同的文件系统类型就是用来定义这些格式的。内核中,文件系统的层次位于块设备驱动程序上面,这意味着块设备驱动程序除了向用户层提供与字符设备一样的接口外,还要向内核其他部件提供一些接口,这些接口用户是看不到的。这些接口使得可以在块设备上存放文件系统,挂载块设备。

  • 网络设备:网络设备同时具有字符设备、块设备的部分特点,无法将它归入这两类中:如果说它是字符设备,他的输入/输出却是有结构的、成块的(报文、包、帧);如果说它是块设备,它的“块”又不是固定大小的,大到数百甚至数千字节,小到几字节。UNIX 式的操作系统访问网络接口的方法是给它们分配一个唯一的名字(比如 eth0),但这个名字在文件系统中(比如/dev 目录下)不存在对应的节点项。应用程序、内核和网络驱动程序间的通信完全不同于字符设备、块设备,库、内核提供了一套和数据包传输相关的函数,而不是 open、read、write 等。

在linux系统中,有一个约定俗成的说法”一切皆是文件“。应用程序使用设备文件节点访问对应设备。

linux下的各种硬件设备以文件的形式存放在/dev目录下,通过ls /dev查看:

root@zhengyang:/work/sambashare/linux-5.2.8-drivers# ls /dev
agpgart          full          mapper              sda1      tty15  tty33  tty51      ttyS10  ttyS29   vcs6
autofs           fuse          mcelog              sda2      tty16  tty34  tty52      ttyS11  ttyS3    vcs7
block            hidraw0       mem                 sda5      tty17  tty35  tty53      ttyS12  ttyS30   vcsa
bsg              hpet          memory_bandwidth    sg0       tty18  tty36  tty54      ttyS13  ttyS31   vcsa1
btrfs-control    hugepages     midi                sg1       tty19  tty37  tty55      ttyS14  ttyS4    vcsa2
bus              hwrng         mqueue              shm       tty2   tty38  tty56      ttyS15  ttyS5    vcsa3
cdrom            initctl       net                 snapshot  tty20  tty39  tty57      ttyS16  ttyS6    vcsa4
cdrw             input         network_latency     snd       tty21  tty4   tty58      ttyS17  ttyS7    vcsa5
char             kmsg          network_throughput  sr0       tty22  tty40  tty59      ttyS18  ttyS8    vcsa6
console          lightnvm      null                stderr    tty23  tty41  tty6       ttyS19  ttyS9    vcsa7
core             log           port                stdin     tty24  tty42  tty60      ttyS2   uhid     vfio
cpu_dma_latency  loop0         ppp                 stdout    tty25  tty43  tty61      ttyS20  uinput   vga_arbiter
cuse             loop1         psaux               tty       tty26  tty44  tty62      ttyS21  urandom  vhci
disk             loop2         ptmx                tty0      tty27  tty45  tty63      ttyS22  userio   vhost-net
dmmidi           loop3         pts                 tty1      tty28  tty46  tty7       ttyS23  vcs      vhost-vsock
dri              loop4         random              tty10     tty29  tty47  tty8       ttyS24  vcs1     vmci
dvd              loop5         rfkill              tty11     tty3   tty48  tty9       ttyS25  vcs2     vsock
ecryptfs         loop6         rtc                 tty12     tty30  tty49  ttyprintk  ttyS26  vcs3     zero
fb0              loop7         rtc0                tty13     tty31  tty5   ttyS0      ttyS27  vcs4
fd               loop-control  sda                 tty14     tty32  tty50  ttyS1      ttyS28  vcs5

linux把对硬件的操作全部抽象成对文件的操作,比如open、read、write、close等。

每个设备文件都有其文件属性(c或者b),使用ll /dev 的命令查看, 表明其是字符设备或者块设备,网络设备没有在这个文件夹下。

二、linux驱动开发步骤

Linux 内核就是由各种驱动组成的,内核源码中有大约 85%是各种驱动程序的代码。内核中驱动程序种类齐全,可以在同类驱动的基础上进行修改以符合具体单板。

编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有驱动程序的框架,在这个框架中加入这个硬件。

比如,x86 架构的内核对 IDE 硬盘的支持非常完善:首先通过 BIOS 得到硬盘的信息,或者使用默认 I/O 地址去枚举硬盘,然后识别分区、挂载文件系统。对于其他架构的内核,只是要指定了硬盘的访问地址和中断号,后面的枚举、识别和挂接的过程完全是一样的。也许修改的代码不超过 10 行,花费精力的地方在于:了解硬盘驱动的框架, 找到修改的位置。

编写驱动程序还有很多需要注意的地方,比如:驱动程序可能同时被多个进程使用,这需要考虑并发的问题;尽可能发挥硬件的作用以提高性能。比如在硬盘驱动程序中既可以使用 DMA 也可以不用,使用 DMA 时程序比较复杂,但是可以提高效率;处理硬件的各种异常情况,否则出错时可能导致整个系统崩溃。

2.1 驱动程序开发步骤

一般来说,编写一个 linux 设备驱动程序的大致流程如下:

  • 查看原理图、数据手册,了解设备的操作方法;
  • 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;
  • 实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序;
  • 设计所要实现的操作,比如 open、close、read、write 等函数;
  • 实现中断服务(中断并不是每个设备驱动所必须的);
  • 编译该驱动程序到内核中,或者用 insmod 命令加载;
  • 测试驱动程序;

2.2 驱动程序的加载和卸载

linux设备驱动属于内核的一部分,设备驱动可以以一下两种方式加载到内核中:

  • 直接编译进linux内核,随同linux启动时加载;
  • 设备驱动可以将它作为模块在使用时再加载,模块的扩展名为.ko,使用 insmod 命令加载,使用 rmmod 命令卸载;

2.3 驱动程序执行流程

假设,我们编写好了LED设备对应的字符驱动程序,并且加载到了linux内核,那么我们的应用程序点亮LED的流程是怎样的呢?

  • 应用程序使用库提供的open函数打开代表 LED的设备文件;
  • 库根据open函数传入的参数执行“swi”指令,这条指令会引起 CPU 异常,进入内核;
  • 内核的异常处理函数根据这些参数找到相应的驱动程序,返回一个文件句柄给库,进而返回给应用程序;
  • 应用程序得到文件句柄后,使用库提供的 write 或 ioclt 函数发出控制命令;
  • 库根据 write 和 ioclt 函数传人的参数执行 “swi” 指令, 这条指令会引起 CPU 异常,进入内核;
  • 内核的异常处理函数根据这些参数调用驱动程序的相关函数。

库(比如 glibc)给应用程序提供的 open、read、write、ioctl、mmap 等接口函数被称为系统调用,它们都是设置好相关寄存器后,执行某条指令引发异常进入内核。

除系统调用接口外, 库还提供其他函数, 比如字符串处理函数(strcpy、 strcmp 等)、 输入/输出函数(scanf、printf 等)、数学库,还有应用程序的启动代码等。

在异常处理函数中,内核会根据传入的参数执行各种操作,比如根据设备文件名找到对应的驱动程序,调用驱动程序的相关函数等。

与应用程序不同,驱动程序从不主动运行,它是被动的:根据应用程序的要求进行初始化,根据应用程序的要求进行读写。

驱动程序加载进内核时,只是告诉内核我在这里,我能做这些工作,至于这些工作何时开始,取决于应用程序。当然,这不是绝对的,比如用户完全可以写一个系统时钟触发的驱动程序,让它自动点亮 LED。
在 linux 系统中,应用程序运行于用户空间,拥有 MMU 的系统能够限制应用程序的权限(比如将它限制于某个内存块中),这可以避免应用程序的错误使整个系统崩溃。而驱动程序运行于内核空间,它是系统信任的一部分,驱动程序的错误有可能导致整个系统崩溃。

三、设备驱动程序框架(hello_dev案例)

按照前面的介绍,我们大致对设备驱动有了一个粗略的了解,本小节我们将会搭建一个简单的字符设备驱动程序的框架。这里以hello_dev驱动为例。

在/work/sambashare下创建一个drivers文件夹,然后在drivers目录下创建一个文件夹命名为1.hello_dev,用来保存我们第一个驱动的源代码。

3.1 hello_open、hello_read等方法实现

int hello_open(struct inode *p, struct file *f)
{
    printk("hello_open\n");
    return 0;
}

ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk("hello_write\n");
    return 0;
}

ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk("hello_read\n");
    return 0;
}

3.2 注册驱动程序

定义一个cdev结构成员:

struct cdev *gDev;

通过cdev名字不难猜出,这代表一个字符设备。

定义一个file_operations结构成员:

struct file_operations *gFile;

通过file_operations名字不难猜测出,这个结构成员定义了对设备文件操作的各个回调函数。

int hello_init(void)
{
    int ret = 0;

    printk("%s enter.\n", __func__);

    devNum = MKDEV(reg_major, reg_minor);

     /*  静态注册一组字符设备编号 */
    ret = register_chrdev_region(devNum, subDevNum, "hello_dev");

    /* 返回值为负数,表示操作失败 */
    if(ret < 0){
        printk("register char dev region error\n");
        goto fail_devid;
    }

     gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
     if (!gDev) {
        printk("kzalloc for cdev error\n");
        goto fail_gdev;
    }

     gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
     if (!gFile) {
         printk("kzalloc for file_operations error\n");
         goto fail_gfile;
     }

     gFile->open = hello_open;
     gFile->read = hello_read;
     gFile->write = hello_write;
     gFile->owner = THIS_MODULE;

    /* 初始化字符设备,添加字符设备 */
     cdev_init(gDev, gFile);
     ret = cdev_add(gDev, devNum, 1);
     /* 返回值为负数,表示操作失败 */
     if (ret < 0) {
        printk("char device add failed\n");
        goto fail_cdev;
     }else{
        printk("char device add success\n");
     }
     return 0;

fail_cdev:
    kfree(gFile);
fail_gfile:
    kfree(gDev);
fail_gdev:
    unregister_chrdev_region(devNum, subDevNum);
fail_devid:
    return ret;
}

大概介绍一下这里面几个主要函数:

(1) 使用register_chrdev_region()来静态注册一组字符设备编号,当返回值小于0,表示注册失败。

/*静态注册一组字符设备编号*/
int register_chrdev_region(dev_t from, unsigned count, const char *name); 

参数如下:

  • from:注册的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100,起始次设备号为0;
  • count:需要连续注册的次设备编号个数,比如: 起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上;
  • *name:字符设备名称,卸载驱动的时候指定的就是这个名字;

(2) 使用cdev_init初始化字符设备结构体cdev,file_operations结构体放入cdev-> ops 里。

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

其中cdev结构体的成员,如下所示:

struct cdev {
       struct kobject    kobj;                   //内嵌的kobject对象 
       struct module   *owner;                   //所属模块
       const struct file_operations  *ops;       //操作方法结构体
       struct list_head  list;               //与 cdev 对应的字符设备文件的 inode->i_devices 的链表头
       dev_t dev;                       //起始设备编号,可以通过MAJOR(),MINOR()来提取主次设备号
       unsigned int count;                      //连续注册的次设备号个数
};

(3) 使用cdev_add将字符设备gDev添加到系统,并将dev(起始设备编号)放入cdev-> dev里,  count(次设备编号个数)放入cdev->count里:

int cdev_add(struct cdev *p, dev_t dev, unsigned count);

3.3 卸载驱动程序

void __exit hello_exit(void)
{
    printk("hello driver exit\n");
    cdev_del(gDev);
    kfree(gFile);
    kfree(gDev);
    unregister_chrdev_region(devNum, subDevNum);
    return;
}

首先使用cdev_del从系统中移除字符设备gDev,然后使用unregister_chrdev_region注销字符设备:

void unregister_chrdev_region(dev_t from, unsigned count);

参数如下:

  • from::注销的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0;
  • count:需要连续注销的次设备编号个数,比如:起始次设备号为0,baseminor=100,表示注销掉0~99的次设备号;

3.4 驱动入口函数

module_init(hello_init);
module_exit(hello_exit);

3.5 Makefile文件

KERN_DIR :=/work/sambashare/linux-5.2.8
all:
    make -C $(KERN_DIR) M=`pwd` modules 
clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order

obj-m += hello_dev.o

这里实际执行的命令是:

make -C /work/sambashare/linux-5.2.8 M=`pwd` modules

其中`pwd`会返回shell命令pwd执行的结果。这里-C表示切换到内核工作路径下。然后执行:

make M=`pwd` modules

我们切换到内核Makefile路径下,可以发现目标modules定义如下:

# Build modules
#
# A module can be listed more than once in obj-m resulting in
# duplicate lines in modules.order files.  Those are removed
# using awk while concatenating to the final file.

PHONY += modules
modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux) modules.builtin
        $(Q)$(AWK) '!x[$$0]++' $(vmlinux-dirs:%=$(objtree)/%/modules.order) > $(objtree)/modules.order
        @$(kecho) '  Building modules, stage 2.';
        $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost
        $(Q)$(CONFIG_SHELL) $(srctree)/scripts/modules-check.sh

我们输出make运行日志信息:

大致可以看出来应该就是执行了这些命令。

“M=”选项的作用是,当用户需要以某个内核为基础编译一个外部模块的话,需要在make modules 命令中加入“M=dir”,程序会自动到你所指定的dir目录中查找模块源码,将其编译,生成ko文件。

3.6 完整代码hello_dev.c

#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>

#define OK   (0)
#define ERROR  (-1)

// 字符设备
struct cdev *gDev;
struct file_operations *gFile;

dev_t devNum;                // 起始设备编号
unsigned int subDevNum = 1;  // 次设备个数 
int reg_major = 232;         // 主设备编号
int reg_minor = 0;           // 起始次设备编号 

int hello_open(struct inode *p, struct file *f)
{
    printk("hello_open\n");
    return 0;
}

ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk("hello_write\n");
    return 0;
}

ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk("hello_read\n");
    return 0;
}

int hello_init(void)
{
    devNum = MKDEV(reg_major, reg_minor);

    if(OK == register_chrdev_region(devNum, subDevNum, "hello_dev")){
        printk("register_chrdev_region ok\n");
    }else {
        printk("register_chrdev_region error\n");
        return ERROR;
    }
    
     printk("hello driver init\n");
     gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
     gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
     gFile->open = hello_open;
     gFile->read = hello_read;
     gFile->write = hello_write;
     gFile->owner = THIS_MODULE;
     cdev_init(gDev, gFile);
     cdev_add(gDev, devNum, 1);
     return 0;
}

void __exit hello_exit(void)
{
    printk("hello driver exit\n");
    cdev_del(gDev);
    kfree(gFile);
    kfree(gDev);
    unregister_chrdev_region(devNum, subDevNum);
    return;
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
View Code

代码中引入的linux/xxx.h头文件位于linux-5.2.8源代码include/linux路径下。这里有很多头文件,初学linux编程,可能对各个头文件不熟悉,后面单独一小节介绍一下这些头文件。

3.7 编译驱动

在驱动代码所在路径运行如下命令,注意:需要配置编译器为arm-linux-gcc 4.8.3,和编译内核使用的编译器版本一致:

root@zhengyang:/work/sambashare/drivers/1.hello_dev# cd /work/sambashare/drivers/1.hello_dev
root@zhengyang:/work/sambashare/drivers/1.hello_dev# make

可以看到:

root@zhengyang:/work/sambashare/drivers/1.hello_dev# ll
总用量 292
drwxr-xr-x 4 root root  4096 2月  11 21:47 ./
drwxr-xr-x 3 root root  4096 2月  11 18:06 ../
-rwxrw-rw- 1 root root  1750 2月  11 21:30 hello_dev.c*
-rw-r--r-- 1 root root 99798 2月  11 21:30 hello_dev.ko
-rw-r--r-- 1 root root   317 2月  11 21:30 .hello_dev.ko.cmd
-rw-r--r-- 1 root root   593 2月  11 21:30 hello_dev.mod.c
-rw-r--r-- 1 root root 28516 2月  11 21:30 hello_dev.mod.o
-rw-r--r-- 1 root root 22295 2月  11 21:30 .hello_dev.mod.o.cmd
-rw-r--r-- 1 root root 72172 2月  11 21:30 hello_dev.o
-rw-r--r-- 1 root root 30565 2月  11 21:30 .hello_dev.o.cmd
-rwxrw-rw- 1 root root   191 2月  11 18:12 Makefile*
-rw-r--r-- 1 root root    57 2月  11 21:30 modules.order
-rw-r--r-- 1 root root     0 2月  11 21:30 Module.symvers

四、测试hello_dev驱动

如果需要测试驱动,我们需要将该驱动文件复制到根文件系统中, 并加载驱动到内核。然后在运行对应的应用程序。

4.1 加载驱动到内核

我们首先将驱动拷贝到根文件系统rootfs中:

cp /work/sambashare/drivers/1.hello_dev/hello_dev.ko /work/nfs_root/rootfs/

烧录根文件系统,启动内核,加载hello_dev驱动:

[root@zy:/]# insmod hello_dev.ko
hello_dev: loading out-of-tree module taints kernel.
hello_init enter.
char device add success 

可见,执行insmod的时候,驱动文件里的hello_init被调用了。 

4.2 编写测试应用程序

在1.hello_dev路径下,创建app文件夹:

mkdir test

新建main.c文件:

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

int main(int argc,char **argv)
{
    int fd;
    int val = 1;
    fd = open("/dev/hello_dev",O_RDWR);
    if(fd == -1){
        printf("can't open!\n");
        return -1;
    }else{
        printf("open success!\n");
    }
    write(fd,&val,4);
    return 0;
}

在test路径下,编译:

arm-linux-gcc -march=armv4t -o main main.c

将测试程序拷贝到根文件系统rootfs中:

cp /work/sambashare/drivers/1.hello_dev/test/main /work/nfs_root/rootfs/

然后在开发板中运行该程序:

[root@zy:/]# ./main
can't open!

这是因为还没有创建hello_dev驱动的设备文件,我们为hello_dev驱动手动创建设备文件:

[root@zy:/]# mknod /dev/hello_dev c 232 0

备注:这里的232和0要跟驱动文件里定义的主次设备号对应起来!

然后再次执行测试程序,发现成功了:

[root@zy:/]# ./main
hello_open
open success!
hello_write
[root@zy:/]# 

此外我们可以通过dmesg命令查看驱动输出信息。

4.3 卸载驱动

卸载hello_dev驱动:

rmmod hello_dev

出现如下错误:

rmmod: can't change directory to '/lib/modules': No such file or directory

报错,执行 “mkdir /lib/modules/xxx” 指令,xxx 是执行 uname -r 指令后查询的内核版本号。

mkdir -p /lib/modules/5.2.8

再次卸载驱动:

[root@zy:/]# rmmod hello_dev
hello driver exit

执行rmmod的时候,hello_exit被调用了。

如果想查看当前系统有哪些驱动,运行:

cat /proc/devices  

五、编译驱动到内核

之前介绍的驱动我们是单独编译,然后使用insmod命令安装到内核。这里介绍一下另一种方式,将驱动直接编译到内核。

5.1 复制驱动到内核

将驱动程序hello_dev.c复制到linux-5.2.8/drivers/char路径下:

cp /work/sambashare/drivers/1.hello_dev/hello_dev.c /work/sambashare/linux-5.2.8/drivers/char

5.2 修改Kconfig

cd /work/sambashare/linux-5.2.8/drivers/char
vim Kconfig

新增如下内容:

config HELLO
        bool "hello_dev"
        default y
        help
          hello driver

在源码顶层运行make menuconfig,在Device Drivers -> Character devices可以看到:

5.3 修改Makefile

cd /work/sambashare/linux-5.2.8/drivers/char
vim Makefile

新增如下内容:

obj-$(CONFIG_HELLO)             += hello_dev.o

5.4 编译内核

在源码顶层运行:

make s3c2440_defconfig
make uImage

可以看到编译信息:

这样hello_dev驱动就被编译进内核,就可以直接运行测试应用程序了。

六、设备文件

6.1 设备文件的作用

之前我们说过在/dev目录下有很多设备文件,比如:

那设备文件有什么作用呢?实际上,当我们应用程序通过open去打开一个设备的时候,首先就是从文件属性中获取到这个设备文件的类型、主设备号、以及次设备号,比如上图中的sda设备主设备号都是8,次设备号有0、1、2、5。

通过 设备类型 + 设备号(主、次设备号)我们就可以获取到这个设备的file_operations结构。通过file_operations结构我们就可以找到驱动程序中的读写、等方法。

6.2 主设备号设置

主设备号的设置有两种方法:

其一:通过cat /proc/devices可以找到有哪些已经使用的主设备号,我们手动指定一个没有使用的主设备号即可。

[root@zy:/]# cat /proc/devices
Character devices:
  1 mem
  2 pty
  3 ttyp
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  6 lp
  7 vcs
 10 misc
 13 input
 21 sg
 29 fb
 90 mtd
 99 ppdev
116 alsa
128 ptm
136 pts
180 usb
188 ttyUSB
189 usb_device
204 ttySAC
232 hello
250 rpmb
251 usbmon
252 watchdog
253 rtc
254 gpiochip
...

 其二:使用alloc_chrdev_region动态分配一组字符设备编号,由系统自动分配主设备号,注册成功将分配到的起始设备编号存放在dev指针中;

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

其中:

  • dev:存放起始设备编号的指针,可以用MAJOR宏和MINOR宏,将主设备号和次设备号提取出来;

  • baseminor:次设备号的基准,从第几个次设备号开始分配;

  • count:需要连续注册的次设备编号个数,比如: 起始次设备号(baseminor)为0,baseminor=2,表示0~1的此设备号都要绑定在同一个file_operations操作方法结构体上;

  • name:字符设备的名字;

6.3 /dev/xxx的创建

在之前的测试发现,应用程序调用open要打开一个设备文件,必须要有设备节点才行,不然会打开失败。

我们每次可以通过手动创建设备节点:

mknod /dev/xxx  c  主 次

不过这个方式相对来说比较麻烦。

我们可以利用mdev机制自动创建设备节点:

(1) 驱动需要调用class_create动态创建设备类,此函数的执行效果就是在/sys/class/目录下创建一个新的文件夹,比如/sys/class/led;

(2) 调用device_create在/sys/class/xxx类文件下创建dev文件,供mdev程序扫描生成/dev下的节点;

(3) 执行/sbin/mdev -s 去扫描上一步中创建的节点,其中/sbin/mdev由busybox生成;

(4) 编译内核需要配置make menuconfig;

Device Drivers ---> 
      Generic Driver Options ---> 
          [*] Support for uevent helper 

该选项的作用是启用uevent helper程序的支持。uevent是内核与用户空间之间通信的一种方式,当内核检测到新的设备时,会生成一个uevent来通知用户空间,使得用户空间能够及时响应设备插拔事件,并做出相应的处理。其中, uevent helper程序就是在接收到uevent后执行的用户空间程序,用来完成设备的热插拔处理。
在内核中,CONFIG_UEVENT_HELPER=y 的设定可以确保uevent helper程序能够被编译到内核中,从而能够正常地接收并响应uevent事件。

(4) 而/sbin/mdev何时执行,有两种方式,

方式一:可以通过配置文件系统/etc/init.d/rcS;

# 使用mdev动态管理u盘和鼠标等热插拔设备
/bin/echo /sbin/mdev > /proc/sys/kernel/hotplug

方式二:编译内核配置make menuconfig,path to uevent helper 配置为/sbin.mdev;

Device Drivers ---> 
      Generic Driver Options ---> 
          [*] Support for uevent helper 
          ()      path to uevent helper (NEW)

配置后,会在.config生成配置项CONFIG_UEVENT_HELPER_PATH=/sbin/mdev,即指定uevent helper程序为/sbin.mdev。

这样加载内核module之后才会自动在/dev下创建设备文件。具体可以参考这篇博客:dev下无法生成节点的分析思路和解决方法及原理

注:cdev_add和device_create区别:

  • cdev_init和cdev_add函数执行字符设备注册。 cdev_add将字符设备添加到系统中。当cdev_add函数成功完成时,设备处于活动状态,内核可以调用其操作;
  • 要从用户空间访问此设备,必须在/dev中创建设备节点;通过使用class_create创建设备类,然后使用device_create来完成/dev下设备文件的创建;

6.4 注意

编译linux内核的编译器、编译根文件系统的编译器、以及编译驱动和应用程序的工具链版本要保持一致,不然应用程序可能会缺少某些库而无法运行。

七、编写linux驱动所用到的头文件

由于编写linux驱动可能用到许多同文件,而这些头文件很难记住,因此,这里从网上找到一份,这些头文件可以在/usr/include路径下找到:

  • <linux/module.h> 最基本的文件,支持动态添加和卸载模块
  • <linux/fs.h>   包含了文件操作相关struct的定义,例如大名鼎鼎的struct file_operations、包含了struct inode 的定义,MINOR、MAJOR的头文件。
  • <linux/errno.h> 包含了对返回值的宏定义,这样用户程序可以用perror输出错误信息。
  • <linux/types.h> 对一些特殊类型的定义,例如dev_t, off_t, pid_t.其实这些类型大部分都是unsigned int型通过一连串的typedef变过来的,只是为了方便阅读。
  • <linux/cdev.h> 对字符设备结构cdev以及一系列的操作函数的定义。
  • <linux/wait.h>   等待队列相关头文件//内核等待队列,它包含了自旋锁的头文件
  • <linux/slab.h> 包含了kcalloc、kzalloc内存分配函数的定义。
  • <linux/uaccess.h> 包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义。
  • <linux/device.h> 包含了device、class 等结构的定义
  • <linux/io.h> 包含了ioremap、iowrite等内核访问IO内存等函数的定义。
  • <linux/miscdevice.h> 包含了miscdevice结构的定义及相关的操作函数。
  • <linux/interrupt.h> 使用中断必须的头文件
  • <linux/semaphore.h> 使用信号量必须的头文件
  • <linux/spinlock.h> 自旋锁
  • <linux/kfifo.h> fifo环形队列
  • <linux/timer.h> 内核定时器
  • <linux/fdreg.h> 软驱头文件,含有软盘控制器参数的一些定义。
  • <linux/hdreg.h> 硬盘参数头文件,定义访问硬盘寄存器端口、状态码和分区表等信息。
  • <linux/kernel.h> 内核头文件,含有一些内核常用函数的原形定义。
  • <linux/sched.h> 调度程序头文件,定义了任务结构task_struct、初始任务0的数据,以及一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。
  • <linux/tty.h> tty头文件,定义了有关tty_io,串行通信方面的参数、常数。
  • <const.h> 常数符号头文件,目前仅定义了i节点中i_mode字段的各标志位。
  • <ctype.h> 字符类型头文件,定义了一些有关字符类型判断和转换的宏。
  • <errno.h> 错误号头文件,包含系统中各种出错号。(Linus从minix中引进的)。
  • <fcntl.h> 文件控制头文件,用于文件及其描述符的操作控制常数符号的定义。
  • <signal.h> 信号头文件,定义信号符号常量,信号结构以及信号操作函数原型。
  • <string.h> 字符串头文件,主要定义了一些有关字符串操作的嵌入函数。
  • <termios.h> 终端输入输出函数头文件,主要定义控制异步通信口的终端接口。
  • <time.h> 时间类型头文件,主要定义了tm结构和一些有关时间的函数原形。
  • <unistd.h> Linux标准头文件,定义了各种符号常数和类型,并声明了各种函数。如,定义了__LIBRARY__,则还包括系统调用号和内嵌汇编_syscall0()等
  • <utime.h> 用户时间头文件,定义了访问和修改时间结构以及utime()原型。
  • <sys/stat.h> 文件状态头文件,含有文件或文件系统状态结构stat{}和常量。
  • <sys/times.h> 定义了进程中运行时间结构tms以及times()函数原型。
  • <sys/types.h> 类型头文件,定义了基本的系统数据类型。
  • <sys/utsname.h> 系统名称结构头文件。
  • <sys/wait.h> 等待调用头文件,定义系统调用wait()和waitpid()及相关常数符号。

八、代码下载

Young / s3c2440_project[drivers]

参考文章

[1]linux驱动

[2]Linux驱动开发

[3]Linux驱动学习(一):什么是Linux驱动

[4]一、Linux驱动之基础概念介绍(部分转载)

[5]Linux驱动基础开发(推荐)

[6]hello world!带你编写一个最简单的linux下的字符设备驱动

[7]29.使用register_chrdev_region()系列来注册字符设备

[8]编写linux驱动所用到的头文件

[9]The Linux Kernel documentation(官方文档)

[10]Linux 内部

[11]内核研究参考站点

posted @ 2022-02-09 20:22  大奥特曼打小怪兽  阅读(2354)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步