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的默认打印级别。
(2)内核默认打印级别
/proc/sys/kernel/printk中的第一个数字就是内核默认打印级别,可以通过uboot的环境变量bootargs传递内核默认打印级别。
uboot中修改bootargs环境变量
练习:
将内核默认打印级别改为8
6.模块符号的导出
模块导出符号可以将模块中的 变量/函数 导出,供内核其他 代码/模块使用。
(1)如何导出
内核中提供了相应的宏来实现模块的导出
EXPORT_SYMBOL -------------- 使用无限制
EXPORT_SYMBOL_GPL ---------- 只有遵循GPL协议的代码才可以使用
(2)模块依赖
如果一个模块使用了另一个模块的 变量/函数,该模块依赖于另一个模块,加载模块时必须先加载依赖的模块,如果一个模块被内核使用,该模块不得卸载。
!)
7.内核模块参数
内核的模块参数不但可以在编写代码时设置其的值。
还可以在加载模块时设置其的值。
甚至可以再加载模块后修改其的值。
(1)模块参数的使用
在模块中声明一些变量,使用以下语法将这些变量设置为模块参数
module_param(模块参数名,模块参数类型,访问权限);
module_param_array(数组模块参数名,数组元素类型,NULL,访问权限);
在代码其他地方使用模块参数和使用普通变量没有区别。
1)加载模块时可以通过 "模块参数名=值" 的方式来修改模块参数的值
2)当模块加载成功后,那些访问权限非0的模块参数就会出现在以下路径下
/sys/module/模块名/parameters
存在和模块参数名相同的文件,这些文件的权限来自于模块参数的权限,内容来自于模块参数的值。
而且可以通过修改文件中保存的数据来修改对应模块参数
练习:
测试通过命令行和文件修改模块参数的值
二.字符设备驱动
1.Linux设备驱动分类
(1)字符设备
按字节流访问,一般只能顺序访问
绝大多数的设备是字符设备,比如LED,按键,键盘,鼠标,串口,LCD.....
字符设备通过字符设备文件来访问
(2)块设备
按数据块访问,块的大小固定,具有随机访问能力
存储设备通常都是块设备,比如内存,磁盘,SD卡,U盘.....
块设备通过块设备文件来访问
(3)网络设备
一般只代表网卡设备,驱动实现结合协议栈(TCP/IP)
网络设备的访问不通过设备文件,而是通过套接字(通信地址)访问
2.字符设备驱动的实现
(1)概述
驱动时沟通底层硬件和上层应用的桥梁,访问设备文件通过文件系统IO,在用户层访问设备文件和访问普通文件没有驱动。
open read write ioctl mmap close....
(2)如果通过设备文件找到对应驱动
Linux中所有的设备文件在/dev目录下
内核中有很多的字符设备驱动,这些字符设备钱多那个和字符设备文件匹配的方式是通过设备号
设备号分为主设备号和次设备号
```c
主设备号用来区分不同类的设备
次设备号用于区分同类设备的不同个体
```
系统中已经被驱动所使用的的主设备号可以在/proc/devices 文件中查询
)
(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;
};
)
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)用户层接口
参数:
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接口。
三.内核中的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成功,否则失败
)
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)
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
尝试添加另外两盏灯