Linux驱动

【一】驱动的概念:通过软件驱动硬件,使硬件处于某种工作模式,提供控制硬件的方法。

【二】Linux架构体系(精简):


【三】Linux驱动的开发步骤:1、驱动编写;2、驱动编译;3、驱动使用、测试

[1] Linux驱动模块的编写:

遵循内核提供的方法,Linux驱动的开发是调用的内核提供的函数宏(宏函数);

1、入口,加载。将驱动加载到Linux内核中时,从入口函数执行。

入口函数调用 内核提供的 module_init(入口函数名); 宏函数进行注册。

入口函数的返回值为 int ,形参为 void :

int __init xxx_func(void){

}

ps: __init 用来将驱动的入口函数的地址统一放在一个 __init段中进行统一加载。

2、出口,卸载。将驱动从Linux内核中卸载、删除时,从出口函数执行

出口函数通过 module_exit(出口函数名); 宏函数 进行注册

出口函数的返回值为 void ,形参为 void:

void __exit xxx_func(void){

}

ps: __exit 用来将驱动的出口函数的地址统一放在一个 __exit段中进行统一加载。

3、GPL协议申明,表明该驱动的代码遵循GPL开源协议

申明通过 MODULE_LICENSE("GPL"); 宏函数进行申明,表明遵循GPL协议。


内核文件中的 /include/linux/init.h 中可以看到声明的 module_init();module_exit(); 宏函数,/include/linux/module.h 中可以看到声明的 MODULE_LICENSE宏函数。

所以在模块编写是要包含相应的头文件

#include

#include


示例的驱动代码:

demo.c:

#include

#include

int __init demo_init(void){

//在这里完成驱动的工作内容

printk(KERN_INFO "-----%s------%s------%d-----%s",__FILE__,__func__,__LINE__," driver is running.."); //驱动用的格式化输出的函数,和printf很像,可以设置打印输出的级别。

return 0;

}

void __exit demo_exit(void){

//在这里编写驱动卸载、退出的逻辑

printk(KERN_INFO "-----%s------%s------%d-----%s",__FILE__,__func__,__LINE__," driver is stop..");

}

module_init(demo_init);

module_exit(demo_exit);

MOUDLE_LICENSE("GPL");

[2] 内核编译模块:

编译器:gcc  如果是用于其他平台,则需要交叉编译工具

如何去编译?

在编写应用层代码时:gcc make 预处理

在编译驱动时,需要用内核提供的相关的函数,需要内核构建的方法。

1、编译内核模块的Makefile :

编译方法:

外部编译:将内核模块的源文件放在内核源码外部进行编译

内部编译:将内核模块的源文件放在内核源码中进行编译,需要Kconfig,Makefile,make memuconfig

静态编译、将内核模块编译进uImage中

动态编译:编译生成一个动态模块 xxxx.ko

开发时使用外部动态编译,方便测试模块时的加载和卸载。

内核源码里的/kernelxxx/Documentation/kbuild 文件包含了编译内核模块的编译详细说明


内核模块编译的MakeFile:

KERNDIR:= /lib/moudle/3.2.0-29-generic-pae/build

PWD:= $(shell pwd) # 执行pwd命令,并把结果赋给PWD变量

obj-m:= demo.o  #指定要编译生成的.ko模块依赖于哪个.o文件

all:

    make -C $(KERNDIR) M=$(PWD) modules

clean:

    make -C $(KERNDIR) M=$(PWD) clean


[3] 使用内核模块

1、查看内核模块信息的命令

              modinfo xxx.ko

可以查看内核模块的信息

查看当前内核中已经插入的动态模块:

          lsmod


查看内核的日志信息

          dmesg

              选项:

                -c      清除当前内核日志信息

2、将内核模块加载到内核中,和内核形成一个整体来运行。需要管理员权限。

          insmod xxx.ko

在加载模块时,加载函数会被调用,加载只会执行一次

3、将内核中的内核模块从内核中卸载出来

            remod xxxx.ko

在卸载模块时,卸载函数会被调用,卸载函数只会执行一次

【四】尝试编写字符设备驱动程序框架

[1]  内核模块是Linux内核进行组件管理的一种方式,设备驱动都是基于内核模块进行注册和注销的。不单单是字符设备,块设备和网络设备都是基于模块进行加载和删除。

字符设备:I/O传输过程中以字符为单位进行传输。用户在对字符设备进行读写请求时,实际硬件的读写操作一般紧接着发生。(除了块设备和网络设备,其他的设备都是字符设备,LCD、键盘、触摸屏、GPIO口)

字符设备是最基本的最常用的设备,。它将千差万别的各种硬件设备采用一个统一的接口封装起来,屏蔽硬件差异,简化了应用层操作。

块设备:块设备与字符相反,它的数据传输模块以块(内存缓冲)为单位传输,用户对块读写时,硬件上的读写操作不会紧接着发生,即用户请求和硬件操作是异步的。(磁盘、闪存类等存储设备)

网络设备:网络设备是一类特殊的设备它不能像字符设备和块设备那样通过对应的设备文件访问,也不能直接通过read和weite进行数据请求,而是通过socket接口函数进行访问。(网卡)

-

设备文件:字符设备和块设备有设备文件,网络设备没有设备文件。

[2]  字符设备驱动必须提供一个通用字符设备的结构体

描述所字符设备驱动的结构体: cdev结构体


cdev结构体

struct module *owner; //赋值THIS_MODULE 表明该结构体指的是当前的驱动。

dev_t  dev; //设备号

设备号是用来唯一标识设备的,就像身份证号码一样,内核就是用设备号来管理设备。

数据类型是32位的无符号整型(32-bit unsigned integer number),由两部分组成,主设备号+次设备号,来表示同种设备中的若干个不同设备。主设备号是高12bit,次设备号是低20bit 。

调用内核中的宏函数来操作设备号:

MAJOR(dev_t dev)  //从设备号中提取主设备号

MINOR(dev_t dev) //从设备号中提取次设备号

MKDEV(int ma,int mi) //根据主次设备号生成设备号

关于上述三个函数,内核的源码中是这样写的:

unsigned int count; //设备个数,表明该驱动同时驱动了多少个设备

struct list_head list; //cdev列表

const struct file_operations *ops; //文件结构体指针变量

file_operayions 是操作方法集合,包含了很多函数指针。应用程序通过系统调用这些函数指针调用驱动的方法来操作设备。

应用程序 》系统调用 》内核函数指针 》操作设备

file_operations 结构体

这是常用的file_operatrions常用的函数接口

file_operations 是提供给应用层的操作方法集


[3]  编写字符设备驱动:

0、分配设备号,注册设备号:

方法1:自动分配设备号,下面是内核提供的分配设备号的函数

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

参数:

    dev   :定义一个dev_t类型定义的变量,取地址传入,通过参数的方式给写入设备号到变量中

    baseminor    :次设备号起始,规定次设备号从哪个数值开始,一般设定从0开始

    count   :指设备的个数

    name     :给设备起的名字

返回值 :成功则返回0,失败则返回一个负数的错误代码

方法2:指定设备号注册

int register_chrdev_region(dev_t from, unsigned count, const char *name)

参数:

    from   :填写需要注册的设备号(MKDEV(major, minor))

    count   :指设备的个数

    name     :给设备起的名字

返回值 :成功则返回0,失败则返回一个负数的错误代码


既然有注册设备号,那就会有注销设备号。内核提供了注销设备号的方法,无论是自动分配的还是指定注册的,都使用这个函数来注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count)

参数:

    from   :填写需要注销的设备号

    count   :指需要注销的设备的个数

1、 为cdev结构体分配内存空间:内核提供了分配内存空间的函数:

                struct cdev *cdev_alloc(void)

cdev_alloc 为cdev结构体分配空间,无需传参,成功返回一个cdev分配到的结构体地址,失败则返回NULL

2、初始化cdev结构体:内核提供cdev初始化函数:

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

参数:

    cdev  :cdev结构体指针

    fops   :操作方法集结构体

3、添加(注册)字符设备到内核中,由内核统一管理,内核提供添加(注册)字符设备到内核中的函数:

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

参数 :

     p  :cdev结构体指针

     dev   :设备号

      count  :设备个数

返回值 :成功则返回0,失败则返回一个负数的错误代码

4、删除(注销)字符设备:

void cdev_del(struct cdev *p)

删除字符设备的函数应该卸载内核模块的卸载函数中。

【五】开始编写字符设备驱动代码:

复习:内核模块三要素:入口函数(module_init)、出口函数(module_exit)、协议申明(MODULE_LICENSE)

注册字符设备,申请cdev结构体,初始化,添加到内核,从内核中删除字符设备中要释放cdev结构体内存


【六】开始编译写好的驱动代码










文章未完成,还在整理中,2020/02/28

你可能感兴趣的:(Linux驱动)