DAY4-模块的使用和字符设备驱动

178 阅读9分钟
title
categories: Linux
abbrlink: 1b386203
date: 2021-09-16 00:34:24

5.printk的打印级别

内核中的打印函数printk打印的内容是否显示取决于打印级别,只有当printk的打印级别高于内核默认打印级别时才打印。

(1)printk打印级别 ---------- 0~7(数字越小,优先级越高)

#define KERN_EMERG  "<0>"   /*系统不可用*/
#define KERN_ALERT  "<1>"   /*必须立即处理的错误信息*/
#define KERN_CRIT   "<2>"   /*严重错误信息*/
#define KERN_ERR    "<3>"   /*错误信息*/
#define KERN_WARNING    "<4>"   /*警告信息*/
#define KERN_NOTICE "<5>"   /*需要注意的信息*/
#define KERN_INFO   "<6>"   /*一般信息*/
#define KERN_DEBUG  "<7>"   /*调试信息*/

printk不提供打印级别使用默认打印级别,可以通过查看/proc/sys/kernel/printk来查看,第二个数字就是printk的默认打印级别。

image-20220801234343741

(2)内核默认打印级别

/proc/sys/kernel/printk中的第一个数字就是内核默认打印级别,可以通过uboot的环境变量bootargs传递内核默认打印级别。

uboot中修改bootargs环境变量

image-20220801234349479

image-20220801234402589

练习:

将内核默认打印级别改为8

6.模块符号的导出

模块导出符号可以将模块中的 变量/函数 导出,供内核其他 代码/模块使用。

(1)如何导出

内核中提供了相应的宏来实现模块的导出

EXPORT_SYMBOL -------------- 使用无限制
EXPORT_SYMBOL_GPL ---------- 只有遵循GPL协议的代码才可以使用

(2)模块依赖

如果一个模块使用了另一个模块的 变量/函数,该模块依赖于另一个模块,加载模块时必须先加载依赖的模块,如果一个模块被内核使用,该模块不得卸载。

!image-20220801234410623)

7.内核模块参数

内核的模块参数不但可以在编写代码时设置其的值。

还可以在加载模块时设置其的值。

甚至可以再加载模块后修改其的值。

(1)模块参数的使用

在模块中声明一些变量,使用以下语法将这些变量设置为模块参数

module_param(模块参数名,模块参数类型,访问权限);
module_param_array(数组模块参数名,数组元素类型,NULL,访问权限);

在代码其他地方使用模块参数和使用普通变量没有区别。

1)加载模块时可以通过 "模块参数名=值" 的方式来修改模块参数的值

image-20220801234416727

2)当模块加载成功后,那些访问权限非0的模块参数就会出现在以下路径下

/sys/module/模块名/parameters

存在和模块参数名相同的文件,这些文件的权限来自于模块参数的权限,内容来自于模块参数的值。 image-20220801234421742

而且可以通过修改文件中保存的数据来修改对应模块参数

image-20220801234429014

练习:

测试通过命令行和文件修改模块参数的值

二.字符设备驱动

1.Linux设备驱动分类

(1)字符设备

按字节流访问,一般只能顺序访问

绝大多数的设备是字符设备,比如LED,按键,键盘,鼠标,串口,LCD.....

字符设备通过字符设备文件来访问

(2)块设备

按数据块访问,块的大小固定,具有随机访问能力

存储设备通常都是块设备,比如内存,磁盘,SD卡,U盘.....

块设备通过块设备文件来访问

(3)网络设备

一般只代表网卡设备,驱动实现结合协议栈(TCP/IP)

网络设备的访问不通过设备文件,而是通过套接字(通信地址)访问

2.字符设备驱动的实现

(1)概述

驱动时沟通底层硬件和上层应用的桥梁,访问设备文件通过文件系统IO,在用户层访问设备文件和访问普通文件没有驱动。

open read write ioctl mmap close....              

(2)如果通过设备文件找到对应驱动

Linux中所有的设备文件在/dev目录下

内核中有很多的字符设备驱动,这些字符设备钱多那个和字符设备文件匹配的方式是通过设备号

image-20220801234437001

设备号分为主设备号和次设备号

            ```c
            主设备号用来区分不同类的设备
            次设备号用于区分同类设备的不同个体
            ```

image-20220801234453010

系统中已经被驱动所使用的的主设备号可以在/proc/devices 文件中查询

image-20220801234459138)

(3)内核中设备号的表示

数据类型:dev_t(32位)

12------------- 主设备号
​
 低20------------- 次设备号          

内核中提供的操作设备号的宏

MAJOR(设备号);//通过设备号获取主设备号
MINOR(设备号);//通过设备号获取次设备号
MKDEV(主设备号,次设备号);//通过主设备号和次设备号构造设备号

(4)如何向内核申请设备号

需要包含头文件

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

1)静态申请

首先选择一个未被内核使用的主设备号(/proc/devices) ===> 210

根据设备个数分配次设备号,一般从0开始

主设备号+次设备号 ===> 设备号

调用 register_chrdev_region 函数向内核申请

int register_chrdev_region(dev_t from, unsigned count, const char *name);
参数:
    from - 要申请的起始设备号
    count - 设备号个数
    name - 设备号在内核中的名称
返回0申请成功,否则失败    

不再使用申请的设备号,通过 unregister_chrdev_region 函数注销设备号

 void unregister_chrdev_region(dev_t from, unsigned count);
 参数:
    from - 要申请的起始设备号
    count - 设备号个数

2)动态申请

调用alloc_chrdev_region 向内核申请

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name);
参数:
    dev - 设备号的地址
    baseminor - 起始次设备号
    count - 设备号个数    
    name - 设备号在内核中的名称
返回0申请成功,否则失败        

注销方法和静态申请设备号一样

练习:

使用动态申请实现设备号申请的模块

(5)字符设备驱动结构

1)cdev

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;//操作函数集合
	struct list_head list;
	dev_t dev;//设备号
	unsigned int count;
};

image-20220801234520797)

2)如何往内核中添加cdev

a.cdev_init -------------------- 初始化cdev(为cdev提供操作函数集合)

void cdev_init(struct cdev *cdev, const struct file_operations *fops);
//cdev和fops都要事先声明,fops只需要实现需要的接口

b.cdev_add ------------------- 将cdev添加到内核(同时绑定设备号)

int cdev_add(struct cdev *p, dev_t dev, unsigned count);
参数:
    p - 要添加的cdev结构
    dev - 绑定的起始设备号
    count - 设备号个数
返回0成功,否则失败    

c.cdev_del -------------------- 将cdev从内核中移除

void cdev_del(struct cdev *p);
//传入要移除的cdev

(6)创建设备文件

1)手动创建

mknod 设备文件路径 文件类型 主设备号 次设备号

比如:

mknod /dev/cdd c 244 0

练习:

编写一个应用程序实现对字符设备驱动的访问。

2)通过代码创建设备文件

需要添加的头文件:

#include <linux/device.h>         

a.创建设备类 ---------------------- class_create

struct class *class_create(struct module *owner, const char *name);
//设备类名对应 /sys/class 目录的子目录名
//返回设备类指针

销毁设备类 --------------------- class_destroy

void class_destroy(struct class *cls);

b.创建设备文件(设备节点) ------------------ device_create

struct device *device_create(设备类指针, 父设备指针,设备号, 额外数据, 设备文件名);
//成功会在 /dev 目录下生成设备文件
//返回设备指针

销毁设备文件 ------------------- device_destroy

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

注:内核中提供的和指针错误处理相关的宏

IS_ERR(指针);   --------------- 返回真出错
IS_ERR_OR_NULL(指针); --------- 返回真出错(判断空指针)
PTR_ERR(指针); ---------------- 将出错的指针转换成错误码
ERR_PTR(错误码); -------------- 将错误码转换成指针

(7)申请设备号和注册cdev合并

内核中提供了同时申请设备号和注册cdev的函数 --------------- register_chrdev

int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops);
参数:
    major - 要申请的主设备号,给0动态申请
    name - 设备号的名称
    fops - 操作函数集合
    
成功动态申请返回主设备号,静态申请返回0,失败返回错误码    

(8)ioctl接口

ioctl是一个专门用于硬件操作的接口,用于和实际数据传输相区分。

1)用户层接口

image-20220801234532718

参数:

fd - 文件描述符

request - 操作命令,代表某个动作(在内核中定义)

...... - 不定参数,可以有也可以没有,取决于内核中接口的定义

2)内核接口

需要包含的头文件:

#include <linux/ioctl.h>

对应file_operations中的成员:

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

Linux内核中构造ioctl命令的宏 --------------- _IO(字符,编号);

#define HELLO_ONE _IO('k',0)
#define HELLO_TWO _IO('k',1)

3.从字符设备文件到字符设备驱动的访问原理

字符设备文件有设备号,当打开字符设备文件(open),内核会通过设备号去遍历内核中的cdev数组,和其中每个cdev元素的设备号比较,找到一个和字符设备文件设备号相同的cdev元素,将匹配的cdev中的操作函数集合(file_operations)赋值给file结构中的file_operations,最后调用file_operation中的open函数。以后再通过系统IO去访问设备文件,相当于访问cdev中的file_operations接口。

image-20220801234540395

三.内核中的gpio操作函数

包含头文件:

#include <asm/gpio.h>
#include <mach/soc.h>
#include <mach/platform.h>

GPIO在内核中属于资源,使用前必须先申请

1.申请 ----------------- gpio_request

int gpio_request(unsigned gpio, const char *label);
参数:
    gpio - 申请的GPIO编号,比如GPIOE13 ------ PAD_GPIO_E+13
    label - gpio对应的名称
返回0成功,否则失败    

image-20220801234547403)

2.释放 ------------------- gpio_free

void gpio_free(unsigned gpio);
//传入GPIO编号

根据s5p6818的GPIO接口的说明,对应GPIO的操作步骤为

选择复用功能
选择输入/输出模式
获取输入值/设置输出值

3.选择复用功能 ----------------- nxp_soc_gpio_set_io_func

void nxp_soc_gpio_set_io_func(unsigned int io, unsigned int func);
参数:
    io - GPIO编号
    func - 复用功能,推荐使用宏,也可以直接给值(0,1,2,3)

image-20220801234555482

4.选择输入输出模式 ---------------- gpio_direction_input/gpio_direction_output

int gpio_direction_input(unsigned gpio);
//传入gpio编号

int gpio_direction_output(unsigned gpio, int value);
参数:
    gpio - gpio编号
    value - 默认输出电平

5.获取输入值/设置输出值 ----------------- gpio_set_value/gpio_get_value

int gpio_get_value(unsigned gpio);
//传入gpio编号,返回输入电平

void gpio_set_value(unsigned gpio, int value);
参数:
    gpio - gpio编号
    value - 输出电平

作业:

实现led驱动的应用程序,控制LED

尝试添加另外两盏灯