正式开启驱动开发之路

驱动,字面意思,也就是驱使一个硬件运转动作起来。譬如:没有网卡驱动,你怎么可能会用得上网卡,此时的网卡芯片就是废铜烂铁。没有摄像头驱动,怎么可能会使用摄像头,摄像头只会是一个摆设。驱动是Linux内核中比重应该算是最大的一部分了,所以说开发驱动,也算是和正宗的Linux内核打交道了。这也算是比较底层的工作了。

我们知道计算机操作系统是分层的,最底层就是硬件,然后硬件之上就是操作系统,进行内存管理,进程调度,进程间通信,文件系统,和各种驱动。操作系统提供API,然后上层的程序员使用操作系统提供的API就来编写程序。这里面的API都是内核实现的,其中一部分API就是驱动实现的,比如open,read,write等等。驱动提供的是API的原型,也就是源代码,并不是直接的open等API这个函数名,操作系统进一步封装才整体的对上层提供固定的API。

1、准备工作

需要事先配置好的和需要的东西如下:

(1)、开发板中要正常运行一个Linux,这个Linux就是我们要对这个Linux开发驱动,必须是自己编译的zImage.

(2)、内核源码树:就是上面我们下载到开发板中的Linux内核的源代码。编写的驱动要指定源代码

(3)、nfs挂载rootfs,通过nfs下载根文件系统到开发板,也可以fastboot下载根文件系统镜像,再mount挂载到服务器Ubuntu中。

2、从最简单的驱动分析

#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build  

            
# 开发板的linux内核的源码树目录
KERN_DIR = /home/mengchao/flushbonding/kernel

obj-m   += module_test.o

all:
    make -C $(KERN_DIR) M=`pwd` modules 

cp:
    cp *.ko /root/porting_x210/rootfs/rootfs/driver_test

.PHONY: clean                                                                             
clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
#include <linux/module.h>        // module_init  module_exit
#include <linux/init.h>            // __init   __exit

// 模块安装函数
static int __init chrdev_init(void)
{    
    printk(KERN_INFO "chrdev_init helloworld init\n");
    //printk("<7>" "chrdev_init helloworld init\n");
    //printk("<7> chrdev_init helloworld init\n");

    return 0;
}

// 模块下载函数
static void __exit chrdev_exit(void)
{
    printk(KERN_INFO "chrdev_exit helloworld exit\n");
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");                // 描述模块的许可证,一定要加,不然会有莫名其妙的错误
MODULE_AUTHOR("aston");                // 描述模块的作者
MODULE_DESCRIPTION("module test");    // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");            // 描述模块的别名信息

上面的代码make编译之后,会生成一个后缀为.ko的文件,这个就是我们想要的驱动文件。下面我们慢慢解释驱动代码。Makefile大概就是固定的,会移植就行了。

模块编译之后,我们要学会去编译和安装、卸载,分别是如下命令

(1)lsmod(list module,将模块列表显示),功能是打印出当前内核中已经安装的模块列表
(2)insmod(install module,安装模块),功能是向当前内核中去安装一个模块,用法是insmod xxx.ko
(3)modinfo(module information,模块信息),功能是打印出一个内核模块的自带信息。,用法是modinfo xxx.ko
(4)rmmod(remove module,卸载模块),功能是从当前内核中卸载一个已经安装了的模块,用法是rmmod xxx(注意卸载模块时只需要输入模块名即可,不能加.ko后缀)

先lsmod再insmod看安装前后系统内模块记录。实践测试标明内核会将最新安装的模块放在lsmod显示的最前面。insmod与module_init宏。模块源代码中用module_init宏声明了一个函数(在我们这个例子里是chrdev_init函数),作用就是指定chrdev_init这个函数和insmod命令绑定起来,也就是说当我们insmod module_test.ko时,insmod命令内部实际执行的操作就是帮我们调用chrdev_init函数。
照此分析,那insmod时就应该能看到chrdev_init中使用printk打印出来的一个chrdev_init字符串,但是实际没看到。原因是ubuntu中拦截了,要怎么才能看到呢?在ubuntu中使用dmesg命令就可以看到了。模块安装时insmod内部除了帮我们调用module_init宏所声明的函数外,实际还做了一些别的事(譬如lsmod能看到多了一个模块也是insmod帮我们在内部做了记录),但是我们就不用管了。

模块的版本信息

使用modinfo xxx查看模块的版本信息.insmod时模块的vermagic必须和内核的相同,否则不能安装,报错信息为:insmod: ERROR: could not insert module module_test.ko: Invalid module format
模块和内核要同出一门。

函数修饰符

init,本质上是个宏定义,在内核源代码中就有#define __init xxxx。这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。
整个内核中的所有的这类函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。

3、file_operations结构体

字符设备驱动,他的工作原理就是应用层->API->设备驱动->硬件.而驱动源码中提供真正的open、read、write、等API的实体。实体就是通过这个file_operations这个结构体来实现。这个结构体的元素主要是指针,用来挂接具体的函数实体。设备驱动向内核注册的时候要提供该结构体类型的变量。注册字符设备驱动中最重要的就是提供这个file_operations结构体。当应用层调用了open的时候,就会寻找到相应的file_operations中定义的open的实体函数。

// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
    .owner        = THIS_MODULE,                 // 惯例,直接写即可
    
    .open        = test_chrdev_open,             // 将来应用open打开这个设备时实际调用的的就是这个                                                                                                                 //test_chrdev_open对应的实体
    .release    = test_chrdev_release,        //close对应的函数
    .write         = test_chrdev_write,             //write对应的函数
    .read        = test_chrdev_read,             //read对应的函数
};

具体的open、read等函数及参数,可以到内核中去找到相关的,这里就不放了。这几个函数内部应该是要操作具体的硬件的,根据具体的硬件自己实现。

4、设备号

内核中有一个类似数组的东西来存储注册字符设备驱动,最多存储256个设备。每一个字符设备都要对应一个设备号。设备号就是一个整数,从1到256,可以使用命令cat /proc/devices查看内核中已经注册过的字符设备驱动的设备号。

设备号还分为主设备号和次设备号,老的字数设备驱动的注册函数只能注册主设备号,不能使用次设备号,新的可以同时指定。

5、注册字符设备驱动

向内核注册字符设备驱动,当我们insmod的时候肯定应该就是自动注册设备字符设备驱动喽,所以应该在module_init()中要调用注册函数,在我们rmmod的时候肯定应该就是自动注销设备字符设备驱动,所以应该在module_exit()中要调用注销函数。

老的注册函数register_chrdev

注册函数分为新的和老的两个函数,但是还都可以使用。注册函数主要作用就是向内核注册我们写的file_operations结构体,还有就是提供设备号。

老的注册函数接口是register_chrdev,要包含头文件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,参数分别是(主设备号,次设备号开始,次设备号个数,设备名称,文件描述符),这个注册函数一次性256个子设备,这种固定模式不太好,不能比256更多子设备,所以后来开始废除这种模式)。注册成功返回主设备号,不成功返回一个负整数。在lsmod的时候,显示的名字就是我们注册函数参数中的name.

使用register_chrdev的时候,如果参数major指定为0的时候,可以自动分配设备号(分配的策略就是从255往回找,哪个设备号没被用,就分配给他),不指定为0,则应该指定为确定的设备号,指定确定的设备号要先看cat /proc/devices哪个设备号没有被使用,否则不成功。

老的注销函数unregister_chrdev

static inline void unregister_chrdev(unsigned int major, const char *name)
{
         __unregister_chrdev(major, 0, 256, name);
}

如果上面register_chrdev自动分配的主设备号,那么我们要定义一个变量接受register_chrdev返回的主设备号值,传给unregister_chrdev中的major参数。

新的注册函数

新的注册函数要分为两步,第一步先分配设备号,第二步调用cdev相关函数注册file_operations.

第一步:注册设备号

注册设备号有两个函数,register_chrdev_region是自己指定主次设备号,alloc_chrdev_region是自动分配主设备号。

register_chrdev_region(dev_t first,unsigned int count,char *name)

First :要分配的设备编号范围的初始值, 这组连续设备号的起始设备号, 相当于register_chrdev()中主设备号

Count:连续编号范围. 是这组设备号的大小(也是次设备号的个数)

Name:编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称

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

dev: 输出型参数,获得一个分配到的设备号。可以用MAJOR宏和MINOR宏,将主设备号和次设备号,提取打印出来,看是自动分配的是多少,方便我们在mknod创建设备文件时用到主设备号和次设备号

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

count: 次设备号的个数

name: 驱动的名字

返回值: 小于0,则错误,自动分配设备号错误。否则分配得到的设备号就被第一个参数带出来

关于设备号有三个宏MKDEV、MAJOR、MINOR

MKDEV:是用来将主设备号和次设备号,转换成一个主次设备号的。(设备号)
MAJOR:从设备号里面提取出来主设备号的。
MINOR:从设备号中提取出来次设备号的。

第二步:cdev相关注册file_operations

cdev结构体位于linux/Cdev.h头文件下,可以看到里边有一个file_operations结构体

struct cdev {
     struct kobject kobj;
    struct module *owner;//填充时,值要为 THIS_MODULE,表示模块
    const struct file_operations *ops;//这个file_operations结构体,注册驱动的关键,要填充成这个结构体变量
    struct list_head list;
    dev_t dev;//设备号,主设备号+次设备号
    unsigned int count;//次设备号个数
};

相关函数;

cdev_alloc:让内核为这个结构体分配内存。

cdev_init:将struct cdev类型的结构体变量和file_operations结构体进行绑定的。

cdev_add:向内核里面添加一个驱动,注册驱动。

cdev_del:从内核中注销掉一个驱动。注销驱动。

新的注销函数

在module_exit中调用的函数中先调用cdev_del,再调用下面中对应的一个函数unregister_chrdev_regionunalloc_chrdev_region

6、创建设备节点

设备没有设备节点的话,就没有办法给用户层使用,所以我们要创建设备节点。

使用手动创建节点的方法:

使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号

使用代码自动创建设备节点

udev(嵌入式中用的是mdev)应用层的一个应用程序内核驱动和应用层udev之间有一套信息传输机制(netlink协议)应用层启用udev,内核驱动中使用相应接口,驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除

内核驱动设备类相关函数

class_create(owner, name)  //创建一个设备类,owner:THIS_MODULE,name  : 名字
                                                          //创建后会产生/sys/class/name    设备类
struct device *device_create(struct class *class, struct device *parent,
            dev_t devt, void *drvdata, const char *fmt, ...)  //创建一个字符设备文件
    //在/dev目录下产生一个名为class的设备文件节点
device_destroy(test_class, mydev); 

函数功能:函数device_destroy()用于从linux内核系统设备驱动程序模型中移除一个设备,并删除/sys/devices/virtual目录下对应的设备目录及/dev/目录下对应的设备文件

class_destroy(test_class);//删除一个类

7、驱动中操作硬件

驱动中寄存器地址不同了。原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址。寄存器的物理地址是CPU设计时决定的,从datasheet中查找到的。而且编程方法不同。裸机中习惯直接用函数指针操作寄存器地址,而kernel中习惯用封装好的io读写函数来操作寄存器,以实现最大程度可移植性。
内核中有2套虚拟地址映射方法:动态和静态
静态映射方法,内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核,在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效,对于移植好的内核,你用不用他都在那里
动态映射方法,驱动程序根据需要随时动态的建立映射、使用、销毁映射映射是短期临时的

三星版本内核中的静态映射表
主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h
CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。所以内核在定义寄存器地址时都是先找到基地址,然后再用基地址+偏移量来寻找具体的一个寄存器。
map-s5p.h中定义的就是要用到的几个模块的寄存器基地址。
map-s5p.h中定义的是模块的寄存器基地址的虚拟地址。
虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h
#define S3C_ADDR_BASE (0xFD000000) // 三星移植时确定的静态映射表的基地址,表中的所有虚拟地址都是以这个地址+偏移量来指定的,GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h表中是GPIO的各个端口的基地址的定义,GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h

静态映射操作

就是在open,read,write,close函数中使用虚拟地址操作硬件而已,简单

动态映射操作

建立动态映射
request_mem_region,向内核申请(报告)需要映射的内存资源。
ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址
销毁动态映射
iounmap
release_mem_region

上面都是各个独立的寄存器分别进行独立映射,实际上更多情况应该是用结构体封装的方式进行多个单个寄存器一起进行映射。

内核提供的读写寄存器的接口

writel、readl、iowrite32、ioread32

应用和驱动之间的数据交换

比如在应用层定义了一个buffer的数组,要想传递到内核驱动中,要使用下面的函数,因为内核空间和用户空间是分开的
(1)copy_from_user,用来将数据从用户空间复制到内核空间
(2)copy_to_user

实例:

code

#include <linux/module.h>        // module_init  module_exit
#include <linux/init.h>            // __init   __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>        // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>
#include <linux/io.h>
#include <linux/ioport.h>


#define MYMAJOR        200
#define MYNAME        "testchar"

#define GPJ0CON        S5PV210_GPJ0CON
#define GPJ0DAT        S5PV210_GPJ0DAT

#define rGPJ0CON    *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT    *((volatile unsigned int *)GPJ0DAT)

#define GPJ0CON_PA    0xe0200240
#define GPJ0DAT_PA     0xe0200244

#define S5P_GPJ0REG(x)        (x)
#define S5P_GPJ0CON            S5P_GPJ0REG(0)
#define S5P_GPJ0DAT            S5P_GPJ0REG(4)

unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;

static void __iomem *baseaddr;            // 寄存器的虚拟地址的基地址


int mymajor;

char kbuf[100];            // 内核空间的buf


static int test_chrdev_open(struct inode *inode, struct file *file)
{
    // 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
    // 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
    printk(KERN_INFO "test_chrdev_open\n");
    
    rGPJ0CON = 0x11111111;
    rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));        // 亮
    
    return 0;
}

static int test_chrdev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "test_chrdev_release\n");
    
    rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
    
    return 0;
}

ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
    int ret = -1;
    
    printk(KERN_INFO "test_chrdev_read\n");
    
    ret = copy_to_user(ubuf, kbuf, count);
    if (ret)
    {
        printk(KERN_ERR "copy_to_user fail\n");
        return -EINVAL;
    }
    printk(KERN_INFO "copy_to_user success..\n");
    
    
    return 0;
}

// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
    size_t count, loff_t *ppos)
{
    int ret = -1;
    
    printk(KERN_INFO "test_chrdev_write\n");

    // 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
    //memcpy(kbuf, ubuf);        // 不行,因为2个不在一个地址空间中
    memset(kbuf, 0, sizeof(kbuf));
    ret = copy_from_user(kbuf, ubuf, count);
    if (ret)
    {
        printk(KERN_ERR "copy_from_user fail\n");
        return -EINVAL;
    }
    printk(KERN_INFO "copy_from_user success..\n");
    
    if (kbuf[0] == '1')
    {
        rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
    }
    else if (kbuf[0] == '0')
    {
        rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
    }
    
    
/*
    // 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
    // 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
    if (!strcmp(kbuf, "on"))
    {
        rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
    }
    else if (!strcmp(kbuf, "off"))
    {
        rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
    }
*/

    
    
    return 0;
}


// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
    .owner        = THIS_MODULE,                // 惯例,直接写即可
    
    .open        = test_chrdev_open,            // 将来应用open打开这个设备时实际调用的
    .release    = test_chrdev_release,        // 就是这个.open对应的函数
    .write         = test_chrdev_write,
    .read        = test_chrdev_read,
};


// 模块安装函数
static int __init chrdev_init(void)
{    
    printk(KERN_INFO "chrdev_init helloworld init\n");

    // 在module_init宏调用的函数中去注册字符设备驱动
    // major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
    // 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
    mymajor = register_chrdev(0, MYNAME, &test_fops);
    if (mymajor < 0)
    {
        printk(KERN_ERR "register_chrdev fail\n");
        return -EINVAL;
    }
    printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
    
/*    
    // 使用动态映射的方式来操作寄存器
    if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
        return -EINVAL;
    if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
        return -EINVAL;
    
    pGPJ0CON = ioremap(GPJ0CON_PA, 4);
    pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
*/    
//    *pGPJ0CON = 0x11111111;
//    *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));        // 亮
    // 测试1:用2次ioremap得到的动态映射虚拟地址来操作,测试成功
//    writel(0x11111111, pGPJ0CON);
//    writel(((0<<3) | (0<<4) | (0<<5)), pGPJ0DAT);
    
    // 测试2:用静态映射的虚拟地址来操作,测试成功
//    writel(0x11111111, GPJ0CON);
//    writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT);
    
    // 测试3:用1次ioremap映射多个寄存器得到虚拟地址,测试成功
    if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))
        return -EINVAL;
    baseaddr = ioremap(GPJ0CON_PA, 8);
    
    writel(0x11111111, baseaddr + S5P_GPJ0CON);
    writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);
    

    return 0;
}

// 模块下载函数
static void __exit chrdev_exit(void)
{
    printk(KERN_INFO "chrdev_exit helloworld exit\n");

    //*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
    //writel(((1<<3) | (1<<4) | (1<<5)), pGPJ0DAT);    
    //writel(((1<<3) | (1<<4) | (1<<5)), GPJ0DAT);    
    writel(((1<<3) | (1<<4) | (1<<5)), baseaddr + S5P_GPJ0DAT);    
    
/*    
    // 解除映射
    iounmap(pGPJ0CON);
    iounmap(pGPJ0DAT);
    release_mem_region(GPJ0CON_PA, 4);
    release_mem_region(GPJ0DAT_PA, 4);
*/
    iounmap(baseaddr);
    release_mem_region(baseaddr, 8);
    
    // 在module_exit宏调用的函数中去注销字符设备驱动
    unregister_chrdev(mymajor, MYNAME);
    
//    rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");                // 描述模块的许可证
MODULE_AUTHOR("aston");                // 描述模块的作者
MODULE_DESCRIPTION("module test");    // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");            // 描述模块的别名信息

Last modification:November 14th, 2019 at 06:39 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment