驱动第一天

来源:互联网 发布:软件开发风险 编辑:程序博客网 时间:2024/05/21 07:23

驱动第一天

一、驱动编程

[1] 虚拟地址空间分配

内核空间: 3G - 4G

应用程序空间: 0-3G

 

[2] 应用程序栈 VS 内核线程栈

应用程序栈大,内核线程栈小

 

[3] 内核中3G开始,物理地址映射区

物理地址映射区就是直接把一块物理内存(低地址位置),固定映射到3G开始的地方(768M)

 

[4] 驱动工作方式

应用程序不能完成的功能会请求内核完成,内核不能完成的功能请求驱动完成

 

【驱动模块管理】

[1] 加载函数(初始化函数)

       模块加载的时候调用的

      

[2] 卸载函数(退出函数)

       模块卸载的时候调用的

      

[3]模块的动态加载和卸载命令

   sudo insmod hello.ko                       加载命令

   sudo rmmod hello                           卸载命令

   sudo lsmod | grep hello                    查询模块加载情况

  

[4] 源代码跟踪(利用tags工具)VI必须配置好)

   1. 到达内核的源代码目录

   2. vim打开任意的源代码文件

   3. F9来生成tags文件

   4. vim打开模块源代码,然后用下面命令设置tags文件的位置

      :set tags=内核源代码目录/tags

   5. 在模块源代码里面按ctrl + ] 组合键来查找对用的源代码定义

  

[5] 理解module_initmodule_exit这个两个宏的用途,无需理解原理

[6] 模块信息查询

       modinfo       模块名.ko

[7] 模块参数

       1. 模块参数定义

       module_param(参数名, 类型, 权限)

       module_param_array(数据名, 成员类型, 接收数据参数个数, 权限)

       2. 模块参数传参

       sudo insmod hello.ko myint=100

       sudo insmod hello.ko array=5,6,7,8

       3. 模块参数描述

       MODULE_PARM_DESC(参数名, 参数描述)

       4. 系统中参数查看

          权限:ls -l /sys/module/模块名/parameters/参数名 (参数是用文件来表示的)

          值:cat /sys/module/模块名/parameters/参数名

         

【字符设备驱动功能】

[1] 分类

    1.字符设备

    2.块设备

    3.网络设备

   

[2] 驱动功能

       [字符设备驱动]

       实现字符设备初始化、接收发送数据、释放设备需要的资源

       [块设备驱动]

       从块设备上读取一块或者多块数据到buffer cache

       buffer cache获得数据,写到块设备

       [网络设备驱动]

       发送网络协议栈产生的协议包到网络

       接收网络协议包,交给网络协议栈解析

      

[3] 设备号

       [作用]

       帮助我们找到要操作设备

       [申请]

       操作是否成功 = register_chrdev_region(起始设备号, 设备的数量, “设备号名字”)

       if (操作是否成功 < 0) {

              打印错误

       }

       [查看设备号是否申请成功]

       cat /proc/devices

       主设备号  名字

       [释放]

       unregister_chrdev_region(起始设备号, 设备的数量);

 

二、驱动源代码结构

// 所有模块都必须加的头文件

#include <linux/module.h>

#include <linux/kernel.h>

 

// 模块有定义参数需要加这个头文件

#include <linux/moduleparam.h>

 

// 模块参数定义(可选)

// myshort为参数名称

short int myshort = 1;

int array[10];

 

// module_param(参数名, 类型, 权限)

module_param(myshort, short, 0400);

 

// MODULE_PARM_DESC(参数名, 参数描述)

MODULE_PARM_DESC(myshort, "A short integer");

 

// module_param_array(数组名, 成员类型, 数组传参个数, 权限)

module_param_array(array, int, &arr_argc, 0400);

MODULE_PARM_DESC(array, "An array of integers");

 

// 主次设备号变量定义

int kernel_major = 250;

int kernel_minor = 0;

 

// __init 告诉内核这个函数在模块加载之后,没有用了

int __init kernel_init(void)

{

       设备号的合成

       申请设备号

      

       return 0;

}

 

// __exit 告诉内核这个函数式模块的卸载函数,在模块被静态编译到内核时,无需这个函数

void __exit kernel_exit(void)

{

       释放设备号

}

 

// 指定模块初始化函数

module_init(kernel_init);

 

// 指定模块的卸载函数

module_exit(kernel_exit);

 

MODULE_LISENCE("GPL");  // 必须的

 

// 可选

MODULE_AUTHOR(作者);

MODULE_DESCRIPTION(模块描述);

MODULE_SUPPORTED_DEVICE(模块设备描述);

 

三、字符设备

inode结构】

[头文件]

#include <linux/fs.h>

struct inode {

       umode_t              i_mode;          // 确认文件的类型

       dev_t                i_rdev;          // 设备文件节点的设备编号

       union {

              struct cdev   *i_cdev;         // 字符设备内部结构

       };

};

【文件结构】

[头文件]

#include <linux/fs.h>

注意:这个结构跟用户空间的FILE没有任何关系,FILE是在C库中定义的

struct file {

// 文件模式:文件的权限内核在调用file_operations接口之前就已经验证,如果不允许,内核直接拒绝

//                   FMODE_READ           可读

//                   FMODE_WRITE          可写      

 

mode_t f_mode;                                                      

// 当前读写位置,64位的(long long),驱动可以读取它来获得文件的当前位置,但是不要改变它,只有在llseek函数中才修改,他是用来改变文件位置指针

loff_t f_pos;

// 文件标志: 所有标志在linux/fcntl.h头文件中定义

       //                   O_NONBLOCK              非阻塞(最常用)

       //                   O_ACCMODE(3)            取出访问模式位

       //                   O_RDONLY(00)            只读方式打开

       //                   O_WRONLY(01)            只写方式打开

       //                   O_RDWR(02)              读写方式打开

       unsigned int f_flags;

       // 驱动操作接口,为文件操作提供响应

       struct file_operations *f_op;

       // open 系统调用设置这个指针为 NULL, 私有数据,通常用来保存驱动自己的数据

       void *private_data;

// 关联到文件的目录入口( dentry )结构, 驱动一般只用到filp->f_dentry->d_inode存取inode结构

       struct dentry *f_dentry;

};

概述:这个结构用来在内核中描述一个打开的文件,内核在open文件时会创建它,并且会将其在文件上操作的任何函数,直到最后的关闭,一定要所有打开该文件的程序都关闭该文件后才释放这个结构;filp通常表示这个结构的指针;

struct cdev {

       struct kobject kobj;                        // 内嵌的kobject对象

       struct module *owner;                       // 所属模块

       struct file_operations *ops;                // 文件操作结构体

       struct list_head list;

       dev_t  dev;                                 // 设备号

       unsigned int count;

};

【字符设备创建】

[1] 【设备结构分配和初始化】

[2] 【文件操作接口】实现

[3] 【设备编号注册】

[4] 【字符设备添加】

[5] 【设备文件节点创建】

【字符设备销毁】

[1] 【设备文件节点销毁】

[2] 【字符设备删除】

[3] 【设备编号回收】

【设备编号获取】

 

[头文件]

#include <linux/types.h>                           dev_t定义

#include <linux/kdev_t.h>                           操作宏定义

[]       

/*

 * @brief                         获取主编号

 * @param[in]           dev       设备编号

 * @return                        主编号

 */

MAJOR(dev_t dev);

/*

 * @brief                          获取次编号

 * @param[in]           dev        设备编号

 * @return              次编号

 */

MINOR(dev_t dev);              

/*

 * @brief               获取设备编号

 * @param[in]           major            主编号

 * @param[in]           minor            次编号

 * @return              设备编号

 */

MKDEV(major, minor);   获取编号

[头文件]

#include<linux/fs.h>

 

[函数]

/*

 * @brief              获取次编号

 * @param[in]          inode            设备文件节点

 * @return              次编号

 */

static inline unsigned iminor(const struct inode *inode);

/*

 * @brief               获取主编号

 * @param[in]           inode          设备文件节点

 * @return              主编号

 */

static inline unsigned imajor(const struct inode *inode)

【设备编号注册/回收】

[头文件]

#include <linux/fs.h>

[函数]

/*

 * @brief             分配设备编号

 * @param[in]           first    起始设备编号

 * @param[in]           count    分配编号数量

 * @param[in]           name     设备名称(/proc/devices文件中可见)

 * @return                 =0     分配成功

 *                         <0     错误码

 */

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

/*

 * @brief             动态分配设备编号

 * @param[out]           dev                设备编号

 * @param[in]           firstminor          分配第一个次编号

 * @param[in]           count               分配编号数量

 * @param[in]           name                设备名称(/proc/devices文件中可见)

 * @return                =0                分配成功

 *                        <0                 错误码

 */

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

/*

 * @brief             回收设备编号

 * @param[in]           first                第一个次编号

 * @param[in]           count                分配编号数量

 */

void unregister_chrdev_region(dev_t first, unsigned int count);

【设备结构构造】

[头文件]

#include <linux/cdev.h>

[函数]

/*

 * @brief                  动态分配struct cdev结构(描述一个字符设备)

 * @return                 struct cdev结构指针

 */

struct cdev *cdev_alloc(void);

/*

 * @brief               初始化struct cdev结构

 * @param[in]           cdev                           struct cdev结构已经定义存在

 * @param[in]           fops                            驱动操作接口,为文件操作提供响应

 * @notes               函数会自动做cdev->ops = fops;但是没有dev->cdev.owner = THIS_MODULE;

 */

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

【字符设备添加/删除】

[头文件]

#include <linux/cdev.h>

[函数]

/*

 * @brief                                 增加一个字符设备到系统

 * @param[in]           p                 描述字符设备的struct cdev结构

 * @param[in]           dev               关联到这个设备的第一个设备号

 * @param[in]           count             关联到这个设备的设备号个数

 * @return               0                添加成功

 *                       <0               错误码

 * @notes               函数执行成功后,立即可以使用该字符设备,也就是说调用这个之前设备驱动已经完全就绪

 */

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

[函数]

/*

 * @brief                                 删除一个字符设备

 * @param[in]           cdev              描述字符设备的struct cdev结构

 */

void cdev_del(struct cdev *dev);

【文件操作接口】

[系统调用]

头文件:<linux/ioctl.h>

/*

 * @brief                                  提供各种硬件控制能力

 * @param[in]           fd                 文件描述符

 * @param[in]           cmd                控制命令

 * @param[in | out]   ...                  不表示参数个数不定,只是不让编译器检查这个参数的类型

 * @return               0                 操作成功

 *                      <0                 错误码

 */

int ioctl(int fd, unsigned long cmd, ...);

[驱动接口]

/*

 * @brief                                    提供各种硬件控制能力

 * @param[in]           inode                文件节点

 * @param[in]           filp                 文件结构,用来在内核中表示一个打开的文件

 * @param[in]           cmd                  控制命令,就是系统调用的cmd参数

 * @param[arg]          arg                  命令参数,对应系统调用的...

 * @return                                   返回给系统调用

 *                       0                   操作成功

 *                       <0                  错误码

 *                      无效命令              -ENIVAL 或者 -ENOTTY

 */

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

[cmd]

+--------------+---------+-----------+--------+

|  direction   |   size  |    type   | number |

+--------------+---------+-----------+--------+

|<-----2------>|<---14-->|<----8---->|<--8--->|

<type>      魔数,见Documentation/ioctl/ioctl-number.txt的安排

<number>    顺序号,本驱动里的第几条命令

<direction> 方向,_IOC_NONE_IOC_WRITE_IOC_READ

<size>      涉及用户数据的大小

【设备文件节点创建】

 

[头文件]

#include <inux/device.h>

[函数]

/*

 * @brief               建立一个类结构

 * @param[in]           owner                   模块所有者

 * @param[in]           name                    类名

 * @return              类结构

 *                      IS_ERR(返回值)          判断是否是错误码

 *                      PTR_ERR(返回值)         取得错误码

 */

struct class *class_create(struct module *owner, const char *name);

/*

 * @brief            建立设备并且注册它到sysfs

 * @param[in]           class                      设备所要注册到的类

 * @param[in]           parent                     父设备

 * @param[in]           devt                       字符设备号

 * @param[in]           drvdata                    驱动私有数据

 * @param[in]           fmt                        格式化的设备名

 * @return             返回设备结构

 *                     IS_ERR(返回值)              判断是否是错误码

 *                     PTR_ERR(返回值)             取得错误码

 */

struct device *device_create(struct class *class, struct device *parent,

dev_t devt, void *drvdata, const char *fmt, ...);

【设备文件节点销毁】

 

[头文件]

#include <inux/device.h>

[函数]

/*

 * @brief               设备销毁

 * @param[in]           class             设备所属类

 * @param[in]           devt              设备号

 */

void device_destroy(struct class * class, dev_t devt);

/*

 * @brief               类销毁

 * @param[in]           class           要销毁的类

 */

void class_destroy (struct class * cls);

 

四、驱动基础知识图解

1.  Linux系统结构图


2.  Linux内核功能模块


3.  Linux系统内存结构图


4.  Linux内核层次结构图

5.  驱动程序VS应用程序


6.  驱动作用原理


 

五、典型Makefile详解

 

#打印$(KERNELRELEASE)变量中的值

$(warning KERNELRELEASE=$(KERNELRELEASE))

 

#判断是否被内核顶层目录的Makefile所调用

ifeq ($(KERNELRELEASE),)

 

#KERNELDIR ?= /home/lht/kernel2.6/linux-2.6.14

 

#内核源代码位置(内核顶层目录下的Makefile位置)

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

#KERNELDIR ?= /home/linux/work/kernel/linux-2.6.35

PWD := $(shell pwd)

 

modules:

#     调用内核顶层目录下的Makefile,然后通知内核顶层目录下的Makefile调用当前路径

#     下的Makefile完成内核模块的编译

#     -C 指定运行的Makefile所在的目录

#     M= 指定要编译的内核模块所在的位置

#     modules 告诉内核顶层目录的Makefile,当前编译的是内核模块

      $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

 

modules_install:

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

 

#清楚编译出来的所有文件

clean:

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

 

#声明伪目标

.PHONY: modules modules_install clean

 

else

#内核模块Makefile内容

    obj-m := hello.o

endif

 

2 0