uboot
kernel
rootfs
2.linux系统的划分
用户空间
内核空间
3.linux内核子系统
4.linux模块开发的特点
七大注意事项
5.加载函数,卸载函数
insmod/modprobe rmmod
lsmod modinfo
6.编译模块
Makefile
7.模块信息
MODULE_LICENSE("GPL");
8.设置内核默认的输出级别的方法:
方法1:
修改/proc/sys/kernel/printk文件
echo 8 > /proc/sys/kernel/printk 打印所有的信息
echo 4 > /proc/sys/kernel/printk 打印输出小于4的级别的信息
方法2:方法1无法解决设置内核启动时候的输出信息
在uboot的bootargs中设置默认的输出级别
debug/quiet/loglevel=数字(级别)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 debug //级别为10
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 quiet //级别为4
setenv bootargs root=/dev/nfs nfsroot=192.168.1.8:/opt/rootfs
ip=192.168.1.6:192.168.1.8:192.168.1.1:255.255.255.0::eth0:on
init=/linuxrc console=ttySAC0,115200 loglevel = 4
案例:要求在加载驱动模块时,点亮所有的灯
要求在卸载驱动模块时,关闭所有的灯
mount -t vfat /dev/sda1 /mnt
cd /mnt
ls 查看u盘的信息
总结:
1.内核模块参数
作用:在加载模块和加载模块以后,能够
给模块传递相应的参数信息
module_param
module_param_array
权限问题:
非0:在/sys/module/模块名/paramters/文件
通过修改这个文件完成对变量的内容修改
问题:会占用内存的资源
权限为0:就不会有一个文件存在,只能在模块
加载的时候才能修改
2.模块的符号导出
作用:将函数和变量导出,供其他模块使用
EXPORT_SYMBOL
EXPORT_SYMBOL_GPL
前者任何模块都能使用,后者只能给遵循GPL协议的
模块使用,所有要求模块编程时,一律添加:
MODULE_LICENSE("GPL");
3.printk
能够指定打印输出的级别:8级
数字越小,级别越高
一般linux系统都有一个默认的输出级别:
如何配置默认的输出级别:
修改/proc/sys/kernel/printk
在uboot中指定:debug quiet loglevel=数字
4.linux系统调用的原理和实现SCI
作用:
1.为用户提供统一的硬件抽象层
操作一个文件,无需关注这个文件存在
硬盘上,SD卡,U盘,只需调用open,read,
write等函数操作即可
2.安全保护
原理:
1.应用程序调用open
2.进程会调用C库的open函数的实现
3.C库的open实现会将open对应的系统调用号
保存在寄存器中
4.C库的open实现会调用swi(svc)触发一个软中断
异常
5.这时进程就会跳转到内核预先指定的一个位置
6.对应的位置就在内核定义好的异常向量的入口
vector_swi
7.这个函数会根据系统调用号,在内核预先
定义好的一个系统调用表中找到对应的open的
内核实现(sys_open)
8.找到这个函数以后,执行这个函数
9.执行完毕以后,原路返回给用户空间
如何添加一个系统调用:
1.在内核代码中arch/arm/kernel/sys_arm.c
添加一个系统调用的内核实现sys_add
2.在内核代码中arch/arm/include/asm/unistd.h
中添加一个新的系统调用号__NR_add
3.在内核代码中arch/arm/kernel/calls.S
中的系统调用表sys_call_table中添加
一个项:CALL(sys_add)
4.在用户空间调用syscall完成调用新添加的
系统调用实现sys_add
注意:syscall这个函数本身会帮你实现调用
swi(svc),你只需传递一个系统调用号
总结:一个进程从用户空间到内核空间的转换靠软中断!
5.linux内核提供的GPIO操作的库函数
CPU的GPIO对于内核来说是一种资源,
这种资源新式是以软件编号的新式存在
S5PV210_GPC0(3).....
gpio_request
gpio_direction_output
gpio_direction_input
gpio_set_value
gpio_get_value
gpio_free
1.linux设备驱动分类
按管理的设备硬件来分:
字符设备
按字节流来访问,能够顺序访问,也能够
指定位置的访问
按键,串口,终端,触摸屏,LCD等
块设备
在unix系统下,块设备按一定的数据块进行
访问,数据块为512字节,或者1K等
在linux系统下,块设备即可按数据块进行
访问,也可以按字节流访问,那么他和字符
设备本质的区别在于linux系统描述块设备和
字符设备的数据结构和操作方法是不一样的。
硬盘,U盘,SD卡,TF卡,nandflash,norflash
网络设备
网卡,网络设备一般都要结合TCP/IP协议栈
来实现。
2.字符设备驱动
驱动程序的作用:
1.管理操作硬件
2.给用户提供访问硬件操作的方法(接口)
led_on
led_off
read_uart
write_uart
....
unix、linux:一切皆文件!
问:应用程序如何访问硬件呢?
答:硬件设备在linux系统下,会以设备文件的形式
存在,设备文件(字符和块)在/dev/,那么应用程序
要访问硬件其实就是对设备文件的访问。
问:应用程序如何访问设备文件呢?
答:通过调用系统调用函数来实现对其访问,访问
设备文件和访问普通的文件的方式是一样的
open,read,write,ioctl,mmap,close...
问:应用程序通过设备文件如何在茫茫的内核驱动
代码中找到自己对应的驱动程序呢?
答:设备文件本身包含了一些属性:设备文件是字符
设备文件(c)还是块设备文件(b),
还包括了主设备号和次设备号这两个重要的属性。
应用程序就是根据主设备号找到对应的驱动程序。
一个驱动程序只有一个主设备号(进行绑定)。
问:次设备号的作用是什么?
答:主设备号用于应用程序找到驱动程序;
次设备号用于应用程序找到具体要操作访问的
设备个体,比如:
S5PV210处理器有4个串口,只需一个串口驱动
来管理即可,四个串口共享一个主设备号,
应用程序通过次设备号来分区要访问的串口
设备号:主设备号和次设备号
数据类型:
dev_t (unsigned int)
高12位:主设备号
低20位:次设备号
设备操作宏
MAJOR
MINOR
MKDEV
如果要实现一个驱动和设备号的绑定,首先需要
向内核申请设备号资源,只有完成申请以后,才能
进行后续的绑定工作!
问:如何向内核申请设备号呢?
答:静态分配和动态分配
静态分配
1.首先通过cat /proc/devices查看linux系统中哪个主设备号没有
被占用
Character devices: 字符设备
主设备号 设备名称
1 mem
2 pty
3 ttyp
4 /dev/vc/0
...
Block devices: 块设备
259 blkext
7 loop
8 sd
31 mtdblock
2.然后根据你的设备个数分配次设备号,
如果设备个数只有一个,一般次设备号从0开始
dev_t dev_id = MKDEV(主设备号,次设备号);
3.调用register_chrdev_region;向内核
申请即可
4.如果主设备号为0,静态分配失败。
动态分配
1.调用alloc_chrdev_region直接向内核去
申请设备号,也就是让操作系统内核帮你分配
设备号。
释放设备号:
unregister_chrdev_region将设备号资源
归还操作系统内核
案例:要求驱动程序能够动态选择静态分配和动态分配
两种方法实现设备号的申请
提示:采用模块参数的方法
<<linux设备驱动程序>>第三版
<<linux内核设计与实现>>第三版
四个重要的数据结构:
1.struct file
作用:描述文件打开以后的状态属性
生命周期:从open打开成功由内核创建
到close关闭文件时进行销毁
重要的成员:
struct file_operations *f_op;//指向驱动
程序中实现的各个硬件操作方法
f_op = &led_fops;
unsigned int f_flags;//文件的操作属性
loff_t f_ops; //文件操作位置
void *private_data; //??
总结:设备文件的操作方法最终来源于f_op
他指向驱动中的操作集合。
struct inode
作用:描述一个文件的物理属性
生命周期:文件存在,内核创建
文件销毁,内核销毁对应的inode
重要成员:
dev_t i_rdev; //存放设备号
struct cdev *i_cdev; //指向一个字符设备
一个文件只有一个inode,可以有多个file
问:struct file和struct file_operations如何关联:
答:1.当应用程序调用open时,最终调用sys_open
2.sys_open创建struct file结构体内存,描述
打开的文件信息
3.通过某种机制<?>获取到驱动的struct file_operations
然后将驱动的file_operations的地址赋值给
struct file的f_op
4.sys_open最后再调用驱动的file_operations里的
open函数,这个open指针指向驱动的led_open
问:应用程序如何read,write设备呢?
答:由于对设备的访问总是先open,一旦先open,就需要
做上面的过程,一旦过程执行,file和底层驱动的
file_operations就进行了关联,
以后read,write最终:
read->sys_read->file->f_op->read = led_read
write->sys_write->file->f_op->write = led_write
问:如何将驱动的file_operations注册到内核中?
问:应用程序如何通过设备号找到驱动程序的?
一旦找到驱动程序,就等于找到file_operations
答:struct cdev
问:应用程序如何通过系统调用函数完成对硬件设备的访问?
答:
1.编写安装字符设备驱动程序
1.1 分配初始化struct file_operations
struct file_operations led_fops = {
.open = led_open,
.release = led_close,
.read = led_read,
...
};
1.2 分配初始化struct cdev
struct cdev led_cdev;
cdev_init(&led_cdev, &led_fops);
结果就是led_cdev.ops = &led_fops
cdev_add(&led_cdev, 设备号,设备个数);
结果就是将led_cdev添加到内核的cdev的数组之中
下标是以设备号为索引!
一旦完成对cdev的注册,就等于有了一个真实
的字符设备,关键这个驱动有了对应的操作
集合=>led_fops
2.应用程序首先要open打开设备
2.1 应用程序调用open打开设备文件(打开设备)
2.2 调用C库的open实现
2.3 C库的open实现会保存open的系统调用号
2.4 C库的open实现调用swi触发一个软中断,跳转到
内核态
2.5 根据系统调用号,在系统调用表中找到open对应的
内核系统调用实现sys_open
2.6 调用sys_open,sys_open会做如下工作:
1.通过inode.i_rdev获取设备号
2.根据设备号在内核cdev数组中找到对应的
字符设备驱动cdev(led_cdev)
3.然后将找到的cdev的地址赋值给inode.i_cdev指针
用于缓存和别的用途(?)
4.创建struct file结构体内存用于描述打开的
设备文件信息
5.根据已经获得的cdev,从而获取其中的驱动
操作集合ops(led_fops)
6.将字符设备驱动的操作接口ops在赋值
给file->f_op = &led_fops
7.最后在调用一次file->f_op->open = led_open
3.后续就可以对设备进行read,write等操作
app:read(fd, buf, size);
kernel:
sys_read:
获取struct file
获取上一次操作的偏移file->f_pos
直接调用:
file->f_ops->read(file,buf,size,file->f_pos)
结果:最终完成对底层read函数的调用
总结:完成一个字符设备驱动主要对cdev和struct file_operations进行操作
案例:要求应用程序open设备时,打开所有的灯
要求应用程序close设备时,关闭所有的灯
1.添加头文件
2.添加加载函数
申请设备号
分配初始化struct file_operations
.open = led_open
.release = led_close
分配初始化注册cdev
申请GPIO资源
3.添加卸载函数
释放GPIO资源
卸载cdev
释放设备号
实验步骤:
1.编译驱动模块
2.交叉编译应用程序
arm-linux-gcc -o led_test led_test.c
3.insmod led_drv.ko
4.cat /proc/devices //查看动态申请的主设备号
5.创建设备节点
mknod /dev/myled c 250 0
6../led_test //查看打印信息和灯的开关状态
7.优化代码
app:read
kernel:
sys_read->led_read:
函数原型:
ssize_t (*read)(struct file *filp,
char __user *buf,
size_t count,
loff_t *f_pos);
切记:buf在内核空间不能直接使用,buf指向用户空间
*buf = 1(不允许)
必须利用copy_to_user完成数据由内核空间
到用户空间的转移
案例:要求用户写1,开灯
要求用户写0,关灯
int cmd;
int stat;
cmd = 1;
write(fd, &cmd, 4);
read(fd, &stat, 4);
cmd = 0;
write(fd, &cmd, 4);
读取灯的状态:1表示灯亮,0表示灯灭
案例:指定其中一盏的操作
提示:在用户和内核定义一个结构体
struct led_cmd {
int cmd; //指定开关命令
int index;//指定灯的编号
};
int (*ioctl)(struct inode *inode,
struct file *filp,
unsigned int cmd,
unsigned long arg);
app:
ioctl(fd, LED_ON);//仅仅发送命令
或者
int index = 2;
ioctl(fd, LED_ON, &index);//发送命令和写入输入到设备
driver:
cmd:对应的就是应用程序的LED_ON
arg:存放的就是用户空间的缓冲区地址&index
所以在内核空间不能直接对arg访问,
需要使用copy_from_user....
回顾:
1.设备节点、设备文件
问:设备文件干什么用?
答:用于表示设备,用户访问设备文件,就是在访问设备
问:设备文件在哪里?
答:在/dev
问:设备文件如何创建?
答:手工创建mknod /dev/设备文件名 <c|b> 主设备号 次设备号
自动创建
2.设备号
问:设备号包含什么内容?
答:包含主,次设备号
问:主,次设备号的作用?
答:应用程序通过主设备号找到驱动程序
应用程序通过次设备号找到同类型设备的不同个体,比如串口
问:由于设备号对于内核来说是一种资源,如何向内核申请?
答:静态申请和动态申请
静态申请:事先查看当前系统里哪个主设备号是空闲的
优点是可以提前创建好设备文件,缺点不便于驱动的推广
动态申请:让内核帮你申请一个设备号
优点是便于驱动的推广,缺点不能提前创建好设备节点
问:设备号数据类型是什么?
答:dev_t 本质上是一个unsigned int
12-》主
20-》次
MAJOR
MINOR
MKDEV
3.字符设备四个重要数据结构
struct inode:
问:这个结构描述什么内容?
答:用于描述一个文件的物理特性,一个文件有一个对应的inode
问:生命周期?谁来创建?
答:文件存在,它存在,文件销毁,它销毁;
内核创建
问:有哪些重要的成员?
答:i_rdev(存放设备文件对应的设备号信息)
i_cdev(如果这个设备文件是字符设备文件,
它存放的就是字符设备驱动对应的数据结构cdev)
用于缓存
struct file
问:描述什么属性?
答:描述设备文件被成功打开以后的状态属性
问:生命周期?谁来创建?
答:文件打开以后,内核创建
文件关闭以后,内核进行销毁
问:有哪些重要的成员?
答:f_op(存放底层驱动的硬件操作集合)
f_flags(存放应用程序调用open时指定的操作属性,O_RDWR|O_NONBLOCK)
f_pos(存放文件操作的位置信息)
private_data文件私有数据?
struct cdev
问:描述什么内容?
答:描述一个字符设备
问:生命周期?谁来创建?
答:由驱动程序在加载函数中完成cdev的分配,初始化和注册工作
由驱动程序在卸载函数中完成cdev的删除操作,一旦删除
,内核就不再存在一个字符设备设备驱动
问:有哪些重要成员?
答:ops(存放底层驱动的硬件操作集合)
dev(存放设备号)
count(存放设备的个数)
问:cdev涉及的相关操作函数
答:cdev_init
cdev_add
cdev_del
问:如何完成cdev的注册过程?
答:当调用cdev_add时,根据设备号,将cdev
添加到内核事先定义好一个cdev的数组中,以后
应用程序就可以通过设备号来访问这个数组,获取
对应的cdev,也就是获取到对应的字符设备驱动
struct file_operations
问:描述什么内容?
答:它仅仅就是包含了一堆的硬件操作的接口
问:应用程序如何和这些接口对应的呢?
答:一一对应
open->sys_open->.open = x_open
close->sys_close->.release = x_close
read->sys_read->.read = x_read
write->sys_write->.write = x_write
ioctl->sys_ioctl->.ioctl = x_ioctl
问:应用程序到sys_系统调用函数这个过程
是内核已经帮你实现,sys_open->.open = x_open
是如何关联的呢?
答:答案参看day03.txt笔记
4.如何实现一个字符设备驱动
案例:按键驱动的一个版本
要求:能够获取按键值
按键按下,获取的键值为0x50
按键松开,获取的键值为0x51
1.了解硬件原理
2.驱动编写过程
1.分配初始驱动操作集合
struct file_operations btn_fops = {
.owner = THIS_MODULE,
.open = btn_open,
.release = btn_close,
.read = btn_read
};
2.分配cdev
3.在加载函数中
分配设备号
初始化cdev
注册cdev
4.在卸载函数中
删除cdev
释放设备号
5.填充btn_open
申请GPIO资源GPH0_0
配置为输入口
6.填充btn_close
释放GPIO资源
7.填充btn_read
读取GPIO的管脚状态
判断管脚状态
如果是1:把0x51 copy_to_user到用户空间
如果是0:把0x50 copy_to_user到用户空间
实验步骤:
1.去除官方按键驱动
make menuconfig //在内核源代码根目录
Device Drivers->
Input device support --->
[*] Keyboards --->
[*] S3C gpio keypad support //去掉
make zImage
cp arch/arm/boot/zImage /tftpboot
重启开发板,用新内核引导
2.insmod btn_drv.ko
3.cat /proc/devices //查看申请主设备号
4.mknod /dev/buttons c 主设备号 0
5../btn_test 操作按键查看信息
6.去掉应用程序的打印信息
7../btn_test & //让应用程序后台运行
8.top //查看CPU的使用情况
案例:用ioctl实现读取按键信息
中断相关内容:
问:为什么有中断
答:由于外设的处理速度相对处理器比较慢,
如果CPU采用轮训或者定期检查设备,浪费很多
CPU的资源,做很多无用功。采用中断,可以最大
的利用CPU
问:中断在硬件的连接方式是怎么样的?
答:外设->中断控制器->CPU
首先外设操作,产生电信号,发送给中断控制器
中断控制器能够检测和处理电信号,决定是否
将信号发送CPU,如果发送给CPU,CPU相应这个
电信号,做后续的中断处理
问:CPU进行中断的处理流程
中断有优先级
中断可以打断中断,当然也可以打断执行的进程
中断异常向量表(内核启动时创建)
保存现场
处理中断(执行中断服务程序)
恢复现场
问:内核如何实现中断编程的?
答:request_irq
free_irq
问:内核要求中断处理程序应该注意哪些事项?
答:1.要求中断处理过程越快越好
2.中断处理程序在内核空间,是随机执行,
不隶属于任何进程,不参与进程的调度
3.中断不能和用户空间进行数据的交互
如果要交互,需要通过系统调用
4.中断处理程序不能调用引起阻塞的函数
copy_*
问:虽然理想状态是要求中断处理函数执行的越快越好,
但某些场合是无法满足这个要去的,比如网卡接收数据的过程,
如果网卡对应的中断处理程序长时间的占有CPU的资源,
就会影响系统的并发和响应能力,怎么办呢?
答:如果对于这种情况,可以采用顶半部+底半部的实现方法
其实就是将以前的中断处理程序分为两部分:
顶半部:就是中断处理程序,做一些比较紧急的事情,
比如将网卡数据包从网卡硬件缓冲区拷贝到主存中,
这个过程不可中断,然后一定要在顶半部中登记底半部
要做的事,CPU会在空闲,适当的时候再去执行底半部剩余的工作。
底半部:做一些不紧急,相对耗时的工作,比如将数据包
提交给协议层的过程,这个过程可以被别的新的中断所打断
问:底半部是如何的实现呢?
问:顶半部和底半部如何关联的呢?
答:底半部的实现机制如下:
tasklet
工作队列
软中断
问:tasklet怎么玩?
答:
按键中断测试步骤:
1.insmod btn_drv.ko
2.cat /proc/interrupts //查看中断的注册信息
中断号 中断触发的次数 中断类型 中断名称
CPU0
16: 137 s3c-uart s5pv210-uart
18: 273 s3c-uart s5pv210-uart
32: 0 s5p_vic_eint KEY_UP
1.设备节点、设备文件
问:设备文件干什么用?
答:用于表示设备,用户访问设备文件,就是在访问设备
问:设备文件在哪里?
答:在/dev
问:设备文件如何创建?
答:手工创建mknod /dev/设备文件名 <c|b> 主设备号 次设备号
自动创建
2.设备号
问:设备号包含什么内容?
答:包含主,次设备号
问:主,次设备号的作用?
答:应用程序通过主设备号找到驱动程序
应用程序通过次设备号找到同类型设备的不同个体,比如串口
问:由于设备号对于内核来说是一种资源,如何向内核申请?
答:静态申请和动态申请
静态申请:事先查看当前系统里哪个主设备号是空闲的
优点是可以提前创建好设备文件,缺点不便于驱动的推广
动态申请:让内核帮你申请一个设备号
优点是便于驱动的推广,缺点不能提前创建好设备节点
问:设备号数据类型是什么?
答:dev_t 本质上是一个unsigned int
12-》主
20-》次
MAJOR
MINOR
MKDEV
3.字符设备四个重要数据结构
struct inode:
问:这个结构描述什么内容?
答:用于描述一个文件的物理特性,一个文件有一个对应的inode
问:生命周期?谁来创建?
答:文件存在,它存在,文件销毁,它销毁;
内核创建
问:有哪些重要的成员?
答:i_rdev(存放设备文件对应的设备号信息)
i_cdev(如果这个设备文件是字符设备文件,
它存放的就是字符设备驱动对应的数据结构cdev)
用于缓存
struct file
问:描述什么属性?
答:描述设备文件被成功打开以后的状态属性
问:生命周期?谁来创建?
答:文件打开以后,内核创建
文件关闭以后,内核进行销毁
问:有哪些重要的成员?
答:f_op(存放底层驱动的硬件操作集合)
f_flags(存放应用程序调用open时指定的操作属性,O_RDWR|O_NONBLOCK)
f_pos(存放文件操作的位置信息)
private_data文件私有数据?
struct cdev
问:描述什么内容?
答:描述一个字符设备
问:生命周期?谁来创建?
答:由驱动程序在加载函数中完成cdev的分配,初始化和注册工作
由驱动程序在卸载函数中完成cdev的删除操作,一旦删除
,内核就不再存在一个字符设备设备驱动
问:有哪些重要成员?
答:ops(存放底层驱动的硬件操作集合)
dev(存放设备号)
count(存放设备的个数)
问:cdev涉及的相关操作函数
答:cdev_init
cdev_add
cdev_del
问:如何完成cdev的注册过程?
答:当调用cdev_add时,根据设备号,将cdev
添加到内核事先定义好一个cdev的数组中,以后
应用程序就可以通过设备号来访问这个数组,获取
对应的cdev,也就是获取到对应的字符设备驱动
struct file_operations
问:描述什么内容?
答:它仅仅就是包含了一堆的硬件操作的接口
问:应用程序如何和这些接口对应的呢?
答:一一对应
open->sys_open->.open = x_open
close->sys_close->.release = x_close
read->sys_read->.read = x_read
write->sys_write->.write = x_write
ioctl->sys_ioctl->.ioctl = x_ioctl
问:应用程序到sys_系统调用函数这个过程
是内核已经帮你实现,sys_open->.open = x_open
是如何关联的呢?
答:答案参看day03.txt笔记
4.如何实现一个字符设备驱动
案例:按键驱动的一个版本
要求:能够获取按键值
按键按下,获取的键值为0x50
按键松开,获取的键值为0x51
1.了解硬件原理
2.驱动编写过程
1.分配初始驱动操作集合
struct file_operations btn_fops = {
.owner = THIS_MODULE,
.open = btn_open,
.release = btn_close,
.read = btn_read
};
2.分配cdev
3.在加载函数中
分配设备号
初始化cdev
注册cdev
4.在卸载函数中
删除cdev
释放设备号
5.填充btn_open
申请GPIO资源GPH0_0
配置为输入口
6.填充btn_close
释放GPIO资源
7.填充btn_read
读取GPIO的管脚状态
判断管脚状态
如果是1:把0x51 copy_to_user到用户空间
如果是0:把0x50 copy_to_user到用户空间
实验步骤:
1.去除官方按键驱动
make menuconfig //在内核源代码根目录
Device Drivers->
Input device support --->
[*] Keyboards --->
[*] S3C gpio keypad support //去掉
make zImage
cp arch/arm/boot/zImage /tftpboot
重启开发板,用新内核引导
2.insmod btn_drv.ko
3.cat /proc/devices //查看申请主设备号
4.mknod /dev/buttons c 主设备号 0
5../btn_test 操作按键查看信息
6.去掉应用程序的打印信息
7../btn_test & //让应用程序后台运行
8.top //查看CPU的使用情况
案例:用ioctl实现读取按键信息
中断相关内容:
问:为什么有中断
答:由于外设的处理速度相对处理器比较慢,
如果CPU采用轮训或者定期检查设备,浪费很多
CPU的资源,做很多无用功。采用中断,可以最大
的利用CPU
问:中断在硬件的连接方式是怎么样的?
答:外设->中断控制器->CPU
首先外设操作,产生电信号,发送给中断控制器
中断控制器能够检测和处理电信号,决定是否
将信号发送CPU,如果发送给CPU,CPU相应这个
电信号,做后续的中断处理
问:CPU进行中断的处理流程
中断有优先级
中断可以打断中断,当然也可以打断执行的进程
中断异常向量表(内核启动时创建)
保存现场
处理中断(执行中断服务程序)
恢复现场
问:内核如何实现中断编程的?
答:request_irq
free_irq
问:内核要求中断处理程序应该注意哪些事项?
答:1.要求中断处理过程越快越好
2.中断处理程序在内核空间,是随机执行,
不隶属于任何进程,不参与进程的调度
3.中断不能和用户空间进行数据的交互
如果要交互,需要通过系统调用
4.中断处理程序不能调用引起阻塞的函数
copy_*
问:虽然理想状态是要求中断处理函数执行的越快越好,
但某些场合是无法满足这个要去的,比如网卡接收数据的过程,
如果网卡对应的中断处理程序长时间的占有CPU的资源,
就会影响系统的并发和响应能力,怎么办呢?
答:如果对于这种情况,可以采用顶半部+底半部的实现方法
其实就是将以前的中断处理程序分为两部分:
顶半部:就是中断处理程序,做一些比较紧急的事情,
比如将网卡数据包从网卡硬件缓冲区拷贝到主存中,
这个过程不可中断,然后一定要在顶半部中登记底半部
要做的事,CPU会在空闲,适当的时候再去执行底半部剩余的工作。
底半部:做一些不紧急,相对耗时的工作,比如将数据包
提交给协议层的过程,这个过程可以被别的新的中断所打断
问:底半部是如何的实现呢?
问:顶半部和底半部如何关联的呢?
答:底半部的实现机制如下:
tasklet
工作队列
软中断
问:tasklet怎么玩?
答:
数据结构
struct tasklet_struct
{
//tasklet的处理函数,底半部要完成的任务
就在这个函数中来实现
void (*func)(unsigned long);
//给处理函数传递的参数,一般传递指针
unsigned long data;
};
问:如何使用?
答:1.分配
2.初始化
方法1:
DECLARE_TASKLET( taskletname,
tasklet_func,
data);
方法2:
struct tasklet_struct mytasklet;
tasklet_init(&mytasklet, tasklet_func, data);
3.在中断处理函数中(顶半部)进行登记,切记登记
而不是调用,CPU会在适当的时候会调用tasklet的处理函数
tasklet_schedule(&mytasklet); //完成登记
4.tasklet的处理函数还是工作在中断上下文中
问:如果在底半部的处理过程中,有睡眠的情况怎么办?
答:这个时候考虑使用工作队列。
问:工作队列使用?
答:工作数据结构:
struct work_struct:关心其中的处理函数这个字段
延时的工作数据结构
struct delayed_work:能够指定在哪个时刻去执行对应的处理函数
问:如何使用呢?
答:1.分配工作或者延时工作
struct work_struct mywork;
struct delayed_work mydwork;
2.初始化工作和延时工作
INIT_WORK(&mywork, my_work_func);
INIT_DELAYED_WORK(&mydwork, my_dwork_func);
3.在中断处理函数(顶半部)登记工作或延时工作
schedule_work(&mywork);//CPU会在适当的时候
会调用执行对应的处理函数
或者
schedule_delayed_work(&mydwork, 5*HZ);
//CPU会在5s以后执行对应的处理函数
问:调用schedule_work或者schedule_delayed_work
都会将工作和延时工作交给内核默认的工作队列和内核
线程去执行,默认的内核线程(events/0) ,如果
都将工作和延时工作交给内核默认的线程去处理,无形
会增大它的负载。对于这种情况怎么办呢?
答:自己创建自己的内核线程和工作队列,然后将
自己的工作交给自己的工作队列和内核线程来处理即可,
减轻了内核默认的线程和默认的工作队列的负担。
问:如何实现?
答:1.分配工作和延时工作
2.初始化工作和延时工作
3.创建自己的工作队列和内核线程
工作队列指针 = create_workqueue(线程名);
4.在中断处理函数中(顶半部)进行关联和登记工作
queue_work(自己的工作队列,自己的工作);
或者
queue_delayed_work(自己的工作队列,自己的延时工作,时间间隔);
按键中断测试步骤:
1.insmod btn_drv.ko
2.cat /proc/interrupts //查看中断的注册信息
中断号 中断触发的次数 中断类型 中断名称
CPU0
16: 137 s3c-uart s5pv210-uart
18: 273 s3c-uart s5pv210-uart
32: 0 s5p_vic_eint KEY_UP
内核定时器相关内容:
1.系统定时器硬件
通过软件可以设置定时器硬件的工作频率
周期性的发生时钟中断
2.既然是时钟中断,在内核里必然有对应的
中断处理程序,这个程序做什么事呢?
1.更新系统的运行时间
2.更新实际时间
3.检查进程的时间片信息
4.执行超时的定时器
5.做相关的统计信息
...
3.时间处理相关的概念:
HZ:用于设置硬件定时器的工作频率
ARM:HZ=100,表明一秒钟能发生100时钟中断
tick:1/HZ,发生一次时钟中断所需的时间间隔
ARM:HZ=100,1tick= 10ms
jiffies:内核的32全局变量,用来记录自开机以来
发生了多少次时钟中断,一般内核用它来表示时间
unsigned long timeout = jiffies + HZ/2;//500ms以后的时间
jiffies注意事项:回绕问题
time_after/time_before
内核定时器:
1.数据结构:
struct timer_list {
unsigned long expires; //超时时候jiffies的值
function; //超时处理函数
data; //给超时处理函数传递的参数,一般传递指针
} ;
2.如何使用定时器呢?
1.分配定时器
struct timer_list mytimer;
2.初始化定时器
init_timer(&mytimer);//驱动关心的三个字段需要另外指定
//指定超时时候的时间
mytimer.expires = jiffies + 5*HZ;
//指定超时处理函数
mytimer.function = mytimer_func;
//指定给超时处理函数传递的参数
mytimer.data = (unsigned long)&mydata;
3.向内核添加启动定时器
add_timer(&mytimer);//后续关于定时器的处理
都是内核来做(时钟中断处理程序)
一旦定时器到期,内核执行超时处理函数
4.如果要修改定时器
mod_timer(&mytimer, jiffies + 2*HZ);
设置新的超时时间为2s以后
mod_timer = del_timer +
expires + jiffies + 2*HZ +
add_timer
5.删除定时器 del_timer(&mytimer);
案例:要求加载驱动程序以后,每隔2s
打印一句话
案例:要求加载驱动程序以后,每隔2s开关灯
案例:要求加载驱动程序以后,能够动态修改
灯的闪烁频率 1000ms,2000ms,5000ms,500ms,200ms
提示:不能使用字符设备驱动框架,
利用模块传参的方式实现频率的修改
提示ms的处理用如下宏:
msecs_to_jiffies(6000);6000表示的6000ms
linux内核并发和竞态:
1.概念
并发
竞态
共享资源:硬件和软件上的全局变量
互斥访问
临界区
2.哪些情况会产生竞态
1.SMP
2.进程和进程
3.进程和中断
4.中断和中断
3.linux内核提供互斥访问的机制
中断屏蔽
问:中断屏蔽能够解决哪些情况的竞态问题?
答:进程和进程,中断和进程,中断和中断
内核提供了一组操作方法:
unsigned long flags;//用于保存中断状态信息
local_irq_save(flags);
临界区
local_irq_restore(flags);
使用注意事项:
由于中断对于操作系统的运行至关重要,所以
长时间的屏蔽中断是危险的,所以要求屏蔽
中断以后,内核执行的路径的速度要快,尽快的
恢复中断。
原子操作
问:能够解决哪些场合的竞态
答:所有场合的竞态情况
位原子操作
以后在驱动开发中,如果涉及到对共享资源
进行位运算,要考虑使用位原子操作的方法
set_bit
clear_bit
change_bit
test_bit
案例:要求将数值0x5555->0xaaaa
不允许使用change_bit
请自己实现set_bit,clear_bit,change_bit,test_bit
整型原子操作
自旋锁
信号量
并发和竞态
1.概念
并发
竞态
共享资源
互斥访问
临界区
2.linux系统,哪些情况会产生竞态
SMP
进程和进程之间的抢占
中断和进程
中断和中断
3.linux系统,提供了哪些互斥机制
中断屏蔽
原子操作
位原子操作
set_bit(nr, void *addr);
clear_bit
change_bit
test_bit
整型原子操作
数据类型:atomic_t
如何使用:
1.分配初始化
atomic_t v = ATOMIC_INIT(1);//int open_cnt = 1
或者:
atomic_t v;
atomic_set(&v, 1);
2.内核提供的整型原子操作的方法:
atomic_set
atomic_read
atomic_add
atomic_sub
atomic_inc
atomic_dec
...
--open_cnt:不是原子的
案例:要求LED灯设备只能被一个应用软件所打开
驱动设计:
1.分配整型原子变量v
2.分配初始化struct file_operations;
3.分配struct cdev
4.在加载函数中完成:
申请设备号
初始化注册cdev
初始化整型原子变量v=1
创建设备类(树枝)
创建设备节点
5.在卸载函数中完成:
删除设备节点
设备设备类
卸载cdev
释放设备号
6.led_open:
操作v并判断其值,
根据值来判断设备的打开状态
7.led_close:
操作v,能够让别的进程打开设备
测试步骤:
1.insmod led_drv.ko
2.cat /proc/devices //查看设备信息
3.ls /dev/myled -lh 查看设备节点
4../led_test & //后台运行
5../led_test //启动第二个进程
自旋锁 = 自旋 + 锁
问:自旋锁能够解决哪些场合的竞态?
答:标准自旋锁除了中断都能解决,但是衍生自旋锁
能够解决所有场合的竞态
数据类型:spinlock_t
如何使用自旋锁呢?
1.分配自旋锁
spinlock_t lock;
2.初始化自旋锁
spin_lock_init(&lock);
3.获取锁
spin_lock(&lock);//获取进程获取不了锁,将会
原地忙等待
或者
spin_trylock(&lock); //如果获取不了锁,函数
将会返回false,获取获取返回true,一定要
对这个函数的返回值进行判断。
4.执行临界区:
临界区要求执行的速度要快,不能调用
引起阻塞的函数copy_to_user,copy_from_user,
kmalloc等
5.释放锁
spin_unlock(&lock);
注意事项:由于以上的锁是标准锁,除了中断都能解决,
如果要涉及中断,要使用衍生的自旋锁:
获取锁:
unsigned long flags
spin_lock_irqsave(flags);
释放锁:
spin_lock_irqrestore(flags);
案例:要求使用自旋锁能够实现设备驱动只能被一个
应用软件所访问。
信号量
本质上是睡眠锁
数据类型:struct semaphore
如何使用呢?
1.分配
struct semaphore sema;
2.初始化
sema_init(&sema, 1); //互斥信号量
3.获取信号量
down(&sema);//如果进程无法获取信号量,进程
将被设置为不可中断的休眠状态
或者
down_interruptible(&sema);//如果进程无法
获取信号量,进程将被设置可中断的休眠状态,
也就表明它能够接收外来的信号。
所以对这个函数一定做返回值的判断,如果
返回0,表明正常获取信号量,否则表明接收到信号
注意:以上两个函数都会引起进程的休眠,所以
不能在中断上下文中使用
或者down_trylock(&sema);//如果获取信号量,
立即返回0,否则返回非0,通过判断返回值
来指示是否正常获取信号量,可以用于中断上下文中
4.临界区:可以时间很长
5.释放信号量:
up(&sema);
1.一方面释放信号量
2.唤醒休眠的进程
案例:要求使用信号量来实现一个设备只能被一个任务进行操作
实验步骤:
down(&sema);
insmod led_drv.ko
./led_test & //打开设备成功,进入休眠(sleep)
./led_test & //进入僵死状态,发kill信号无法杀死
kill 第二个进程的pid //无法杀死
kill 第一个进程的pid //两个进程都退出
第二个进程退出的原因:
1.第一个进程释放了信号量
2.唤醒第二个进程,D->S
3.第二个进程会检查是否接收到信号,如果接收到
就处理信号
down会将进程设置为不可中断的休眠状态
down_interruptible(&sema);
两个进程都进入S状态
并且都能处理信号
问:如果我仅仅只想让进程休眠,不会涉及竞态问题,
如何实现让一个进程休眠?
问:如果串口或者按键没有数据或者没有操作,那么
最好的做法就是让应用程序进入休眠状态,怎么
休眠?(休眠就是让CPU资源从当前进程撤出来,给
别的任务使用)
答:等待队列,那么信号量让进程休眠这个机制就是
利用等待队列来实现的。
等待队列实现机制:
1.等待队列头
数据类型:wait_queue_head_t
2.等待队列:里面存放的就是要休眠的进程
数据类型:wait_queue_t
3.调度器:内核已经实现
编程方法:
方法1:
1.分配等待队列头
wait_queue_head_t wq;
2.初始等待队列头
init_waitqueue_head(&wq);
后者:
DECLARE_WAIT_QUEUE_HEAD(wq);//分配初始化
3.分配等待队列
//分配初始化名为name的等待队列,
task=current,就是将当前进程添加到
name所对应的等待队列中。
注意:current是内核全局变量,用来记录
当前进程(指向当前进程对应的struct task_struct)
DECLARE_WAITQUEUE(name, task);
或者
wait_queue_t wait; //分配等待队列
init_waitqueue_entry(&wait, current);
初始化等待队列,就是让当前进程current
填充到等待队列这个容器中。
注意:对设备的操作,比如read,write,一般
都有对应的独立的等待队列头,
但是等待队列这个容器可以有多个,因为每一个
要存放的不同的进程
4.调用add_wait_queue将要休眠的进程添加到
自己分配初始化好的等待队列头所在的数据链中
当前进程并没有进入真正的休眠;
current->state = TASK_INTERRUPTIBLE;
5.进入一个死循环for(;;)
6.判断设备是否可以用,如果可用,跳出死循环
如果不可用,继续执行
7.继续执行,调用signal_pending(current)
判断当前进程是否接收到信号,如果接收到信号
函数返回非0,否则返回0,如果没有接收到信号
代码继续执行,如果接收到信号,跳出死循环
8.如果设备不可用,还有进程没有接收到信号
最后调用schedule()完成真正的休眠
9.进程被唤醒由两个原因:
进程接收到了信号
设备可用
10.一旦schedule函数返回,应该再次判断
是哪种原因起因的唤醒,最后跳出死循环
11.设置当前进程的状态为
current->state = TASK_RUNNING;
12.将休眠的进程从等待队列头所在的数据链中移除
注意:死循环不一定要有!
参考代码实现:
1.分配初始化等待队列头
wait_queue_head_t wq;
init_waitqueue_head(&wq);
2.分配初始化等待队列(存放进程的容器)
wait_queue_t wait;
init_waitqueue_entry(&wait, current);
将当前进程添加到容器里去
3.add_wait_queue(&wq, &wait);
将当前进程添加到队列头
4.while (1) {
5.current->state = TASK_INTERRUPTIBLE;
设置当前进程的状态为TASK_INTERRUPTIBLE
6.if(设备数据可用)
break; //可用
7.if (signal_pending(current))
break;;/接收到信号
8.schedule(); //真正的休眠
一旦调用这个函数,进程就到这里
停止,等待着被唤醒,唤醒的方式
有两种:一个设备可用,接收到信号
}
9.current->state = TASK_RUNNING;
设置当前进程的状态为TASK_RUNNING
10.remove_wait_queue(&wq, &wait);将当前进程
从队列中移除
方法2:
1·分配初始化等待队列头
wait_queue_head_t wq;
init_waituqueue_head(&wq);
2.wait_event_interuptible(&wq, condition);
wait_event
wait_event_timeout
wait_event_interruptible_timeout
对于以上函数只需传递一个等待队列头和
设备是否可用的条件即可
方法1和方法2唤醒休眠的方法如下:
在适当的地方调用
wake_up/wake_up_interruptible
进行唤醒
一般来说适当的地方都是在中断处理函数中
因为中断的发生代表设备有所操作,
比如按键有按键操作,就会产生中断
比如串口接收到数据,产生接收中断
案例:编写实现按键驱动的第二个版本
回顾按键驱动第一版本:采用轮训方式
结果:虽然能够满足用户的需求,但是
太浪费CPU的资源
如何改进:
如果用户没有按键操作,就应该让
读取按键的进程进行休眠,如果一旦有按键
操作了,在唤醒休眠的进程就可以了!
如何实现休眠:等待队列。
驱动设计:
0.定义按键的硬件结构体
struct btn_resource {
int irq; //中断号
int gpio; //GPIO编号
char *name; //名称
};
初始化板卡的按键信息
static struct btn_resource btn_info[] = {
[0] = {
.irq = IRQ_EINT(0),
.gpio = S5PV210_GPH0(0),
.name = "KEY_UP"
}
};
1.分配cdev
2.分配初始file_operations
.read = btn_read //读取按键值:0x50,0x51
3.分配设备类指针
分配等待队列头
4.在加载函数中
申请设备号
初始化注册cdev
申请中断和注册中断处理函数
自动创建设备节点
申请按键对应的GPIO资源
初始化等待队列头
5.在卸载函数中
释放GPIO资源
删除设备节点
释放中断资源和中断处理函数
卸载cdev
释放设备号
6.btn_read
wait_event_interruptible来判断按键是否有操作
如果没有操作,进程进入休眠,否则
copy_to_user();//按键信息上报给用户
7.按键中断处理函数中
获取按键的状态
设置上报的键值0x50/0x51
wake_up_interruptible唤醒休眠的进程
回顾:
1.linux内核互斥访问机制
中断屏蔽
原子操作
自旋锁
信号量
2.linux内核等待队列机制
目的:如何在内核空间让一个进程进入休眠状态
问:在什么场合让进程休眠?
答:如果外围设备不可用或者设备驱动的数据不可用,
这时应该让进程进入休眠,而不是轮训的查询设备状态
或者数据的状态
问:内核如何实现等待队列机制?
答:等待队列头:wait_queue_head_t
等待队列:wait_queue_t
当前进程:current(指向每一个进程的task_struct结构体)
printk("current process pid = %d, name = %s\n",
current->pid, current->comm);
软件的实现方法:
方法1:
步骤比较多,但非常灵活
方法2:
实现简单,但不够灵活】
非阻塞:
1.应用程序调用open时,指定O_NONBLOCK,表示
应用程序对设备访问是采用非阻塞
open("a.txt", O_RDWR|O_NONBLOCK);
open("/dev/buttons", O_RDWR|O_NONBLOCK);
问:设备驱动如何直到应用程序是采用非阻塞方式访问?
答:驱动程序通过file指针获取其成员f_flags
因为f_flags存放的就是O_RDWR|O_NONBLOCK
2.在驱动程序中通过如下方式来判断即可
if (file->f_flags & O_NONBLOCK) {
if (设备数据不可用)
return -EAGAIN
}
linux内核内存相关内容:
1.linux内核在启动时,会为每一个物理页分配
struct page结构体,也就是说一看到page结构体
就是代表的是一个物理地址,而不是虚拟页
2.虚拟地址的好处
3.虚拟地址的划分
用户空间 0x00000000~0xBFFFFFFFF
内核空间 0xC0000000~0xFFFFFFFFF
用户空间和内核空间只允许操作虚拟地址,不能
使用访问物理地址
4.MMU
1.虚拟地址转换为物理地址
2.会检查对地址的访问权限
3.控制cache等
uclinux-不带MMU
5.内核虚拟地址(1G)在内核初始化,就已经事先
和对应的物理内存建立好了线性的地址映射。
但是用户的虚拟地址并没有这么做,需要动态得
建立虚拟到物理的映射。
6.如果物理内存大于1G,那怎么去建立映射呢?
这时就需要对内核1G的虚拟地址进行划分:
X86平台:
低端内存(直接内存映射区)
存在一个物理到虚拟之间线性关系
虚拟地址=0xc0000000 + (物理地址 - 物理地址的起始地址)
一般zImage就在这个区域
高端内存:起始由物理内存的大小来决定
动态内存映射区
永久内存映射区
固定内存映射区
ARM平台:
Virtual kernel memory layout:
vector : 0xffff0000 - 0xffff1000 ( 4 kB)
fixmap : 0xfff00000 - 0xfffe0000 ( 896 kB)
DMA : 0xff000000 - 0xffe00000 ( 14 MB)
vmalloc : 0xf4800000 - 0xfc000000 ( 120 MB)
低端内存:lowmem : 0xc0000000 - 0xf4000000 ( 832 MB)
modules : 0xbf000000 - 0xc0000000 ( 16 MB)
.init : 0xc0008000 - 0xc0037000 ( 188 kB)
.text : 0xc0037000 - 0xc0837000 (8192 kB)
.data : 0xc0838000 - 0xc0887760 ( 318 kB)
7.内核内存分配的方法:
kmalloc/kfree (128K,32M)
__get_zeroed_page/free_page
__get_free_pages/free_pages
都是在低端内存,物理上连续的,虚拟也连续
vmalloc/vfree(默认是在动态内存映射区)
虚拟上连续,但物理上不一定连续
8.由于在用户空间还是在内核空间,都不允许直接
访问物理地址,如果要访问物理地址,必须将
物理地址映射到对应的虚拟地址上,这种映射
是动态映射
虚拟地址 = ioremap(物理地址,长度);
如果不使用,要解除地址映射
iounmap(虚拟地址);
以后访问这个虚拟地址,就是在访问对应的物理地址!
案例:不再使用GPIO的库函数,而是通过直接访问
寄存器来实现开关灯,ioctl
案例:寄存器编辑器
用户测试:
./registereditor w 0xe0200084 0xf
./registereditor r 0xe0200084
驱动程序:
重点完成:将用户指定的物理地址映射到对应的虚拟地址上
应用程序:
struct registereditor_info{
unsigned long addr;
unsigned long data;
};
struct registereditor_info req;
req.addr = 0xe0200084;
req.data = 0xf;
ioctl(fd, REGISTEREDITOR_WRITE_CMD, &req);
或
req.addr = 0xe0200084;
ioctl(fd, REGISTEREDITOR_READ_CMD, &req);
驱动ioctl:
1.获取用户要访问的物理地址信息
struct registeredtor_info req;
void *register_base;
copy_from_user(&req, (struct registereditor_info*)arg, sizeof(req));
2.解析req,然后做地址映射
register_base = ioremap(req.addr, 4);
3.解析命令
case WRITE_CMD:
*regiter_base = req.data;
break;
case REAE_CMD:
req.data = *register_base;
copy_to_user(buf, &req, sizeof(req));
break;
4.解除地址映射
iounmap(register_base);
测试蜂鸣器:
./registereditor w 0xe02000a0 0x1111 //配置为输出口
./registereditor w 0xe02000a4 0xf //开
./registereditor w 0xe02000a4 0x0 //关
上位机:PC
用QT设计界面:
能够输入地址信息
能够输入数据信息
能够指定读和写
并且设计为客户端
能够将地址和数据和读写命令发送开发板
开发板:
应用程序:设计为服务器
能够接收上位机PC的数据信息,然后
解析到底是读还是写,然后对地址和数据进行操作即可
寄存器编辑器->EEPROM->ADC->温度传感器->软件升级
回顾:
1.非阻塞
app:open(...,...|O_NONBLOCK);
driver:if (file->f_flags & O_NONBLOCK) {
//非阻塞
if (设备不可用)
return -EAGAIN;
}
//阻塞
wait_event_interruptible(wq,设备数据是否可用的标志);
2.linux内核内存相关内容
kmalloc/kfree
...
GFP_KERNEL
GFP_ATOMIC
linux内核虚拟内存空间(1G)的划分
3.如何在驱动中访问物理地址?
用户在应用程序还是在内核都不能去对物理地址
直接去访问,如果要访问,必须把物理地址映射到
对应的虚拟地址空间,通过ioremap能够将
物理地址映射到内核的虚拟地址空间。
虚拟地址 = ioremap(物理地址,长度);
linux内核链表:
1.链表结构体
struct list_head {
struct list_head *next, *preve;
}; //只有指针域,没有数据域,并且跟具体的
数据结构无关
2.问如何使用内核链表呢?
1.定义自己的数据结构,然后将list_head嵌入其中
struct fox {
int heigh;
int weigh;
struct list_head list; //指针域
};
2.分配初始化一个链表头,链表头一般都是list_head
struct list_head mylist;
INIT_LIST_HEAD(&mylist); //mylist.next和mylist.prev都指向自己
3.分配节点
struct fox *fox1 = kzalloc(sizeof(struct fox), GFP_KERNEL);
struct fox *fox2 = kzalloc(sizeof(struct fox), GFP_KERNEL);
struct fox *fox3 = kzalloc(sizeof(struct fox), GFP_KERNEL);
4.list_add/list_add_tail将节点插入到链表头所在的
数据链中
list_add(&fox1->list, &mylist);
list_add(&fox2->list, &mylist);
list_add(&fox3->list, &mylist);
结果:3->2->1
list_add_tail(&fox1->list, &mylist);
list_add_tail(&fox2->list, &mylist);
list_add_tail(&fox3->list, &mylist);
结果:1->2->3
5.用list_del删除节点
list_del(&fox1->list);
6.遍历链表
struct list_head *pos;//临时存放每一个节点的
指针域
struct list_head *n;//临时存放每一个节点的下一个
节点的指针域
list_for_each(pos, &mylist){
遍历链表mylist,取出每一个节点的指针域
list的指针赋值给pos
}
list_for_each_safe(pos, n, &mylist){
遍历链表mylist,取出每一个节点的指针域
list的指针赋值给pos,并且取出这个节点的
下一个节点的指针域的指针赋值给n
}
注意:如果仅仅是遍历链表,两者都可以使用,
如果在遍历链表时要进行删除节点操作,必须
使用后者,如果使用前者,会造成断链和内核
奔溃!
7.通过以上两个遍历链表的方法能够得到每一个节点
的指针域,如何通过这个指针域获取数据域的信息呢?
答:利用container_of
struct people {
int 张三;
int 李四;
int 王二;
};
已知结构体成员李四的首地址,问如何访问
王二
结构体的首地址 = container_of(李四的首地址,
struct people,
李四)
结构体首地址 = container_of(已知成员的首地址,
结构体名,
已知成员在结构体中的变量名);
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
如果通过遍历方法能够获取每一个节点的指针域的
地址,就可以通过list_entry获取结构体的首地址
struct fox *fox1 = list_entry(pos,
struct fox, list);
8.如果要遍历和访问数据信息
list_for_each/list_for_each_safe + list_entry
内核提供额外的链表操作方法:
struct fox *fox1;//要操作的节点
struct fox *fox_next;//操作节点的下一个节点
list_for_each_entry(节点的首地址,链表头,指针域的变量名)
list_for_each_entry(fox1, &mylist, list);
或
list_for_each_entry_safe(fox1, fox_next, &mylist, list)
案例:要求使用list_for_each_entry/list_for_each_entry_safe
替代list_for_each+list_entry...
回顾:
1.linux内核链表区别于传统链表
目的:
面试题:谈谈链表和数组
1.传统链表和具体的数据结构要关联
2.传统链表都有自己的操作方法
3.内核链表不关心具体的数据结构,只跟
struct list_head相关
4.内核链表操作都是基于list_head
5.传统链表的指针域都是指向下一个或者前一个节点的首地址
6.内核链表的指针域只跟list_head相关,和节点首地址无关
list_entry(container_of)
7.记住内核链表操作的一些方法
2.设备-总线-驱动模型
目的:硬件和软件的分离
参看4页PPT
一般内核编程实现:
platform_device描述的硬件信息一般都是在
板级的平台代码中来注册好mach-cw210.c
platform_driver描述的软件信息一般都是在
各自的驱动程序中完成注册
linux的mmap相关内容:
案例:分析LED驱动程序的数据流的走向
案例:分析按键驱动程序的数据流的走向
通过分析这两个驱动程序,都有一个共同的问题:
用户->内核->硬件
硬件->内核->用户
都要经过两次的数据拷贝,如果这种实现方式对于
摄像头,视频采集卡这里设备,都要经过两次
数据拷贝,对性能来说有很大的影响,对于LED,
按键的影响几乎可以忽略不计。
问:如何解决这种问题呢?
答:目标就是将两次的数据拷贝变成1次的数据拷贝即可。
问:如何实现这种1次拷贝呢?
答:将设备的物理信息只需要映射用户空间的虚拟地址即可,而不是
再映射到内核(当然你也可以映射)。那么以后用户
访问用户进程的虚拟地址就是在访问设备。
最终:将设备的物理信息映射用户进程的某一个虚拟地址空间里即可。
问:如何实现映射呢?
答:mmap
app:
void *addr;
int fd;
fd = open("a.txt",...);
addr = mmap(0, 0x1000, PROT_READ|PROT_WRITE,
MAP_SHARED,fd, 0);
将文件(磁盘信息)映射用户进程的虚拟地址空间addr
以后用户访问addr就是在访问文件,不再进行
两次的数据拷贝过程。
内核:
1.当应用程序调用mmap
2.调用C库的mmap,保存mmap的系统调用号,
然后调用swi、svc触发一个软中断异常
3.跳转到内核事先定义好的异常向量表的入口
vector_swi处
4.根据系统调用号在内核事先定义好的一个系统
调用表里找到对应的内核系统调用实现sys_mmap
5.sys_mmap就会在用户3G虚拟地址空间中的MMAP区域
找到一个空闲的虚拟内存区域,找到以后,
创建vm_area_struct对象来表述这块虚拟内存区域
这个对象里包括:vm_start(虚拟起始地址)
vm_end(虚拟结束地址)...
6.最后sys_mmap调用驱动程序的x_mmap(.mmap = x_mmap)
同时将内核创建的vm_ares_struct对象的指针
传递给x_mmap
7.底层驱动的x_mmap就能够通过上面创建的对象
指针获取要映射的用户进程的虚拟地址的起始地址
,并且物理地址事先也是知道的。
8.在x_mmap有了用户进程的虚拟地址,也有了
设备的物理地址,只需调用remap_pfn_range来完成
物理地址和用户进程的虚拟地址的映射。一旦完成
映射,用户的mmap函数就返回,并且将映射好的
用户进程的虚拟起始地址返回给应用程序,
9.以后应用程序访问这个虚拟地址就是再访问物理地址
总结:底层驱动的mmap就完成物理地址到用户进程
虚拟地址的映射!
注意:映射的时候,大小必须是页的整数倍!
案例:采用mmap实现对LED灯的控制
linux的IO多路监听机制(select/poll)
问:如果一个应用程序对多个设备进行同时的
读操作,如何实现?
方案1:采用串行读
while(1) {
read(串口);
read(按键);
recvfrom(网络);
}
这个代码会造成数据的丢失问题!
方法2:为每一个读设备操作创建一个线程来实现
这种实现会随着设备的增多,增加系统的负载
方案3:使用linux系统的IO的多路监听机制来实现
起始就让主进程能够对多个设备进行监听,
一旦有一个设备可用,主进程就去读取对应的
设备即可。
UC的select系统调用函数的使用:
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
nfds:当前进程中最大的文件描述符+1
readfds:监听设备的读
writefds:监听设备的写
exceptfds:监听设备的异常
fd_set:文件描述符集合,这里面存放的
是要监听的设备,如果要想对一个设备
进行监听,那么首先就应该让设备对应的fd
添加到其中。
timeout:超时,如果主进程调用select完成对设备的
监听,如果监听的设备不可用(不可读,不可写,无异常),
主进程就会进入休眠状态,如果指定了超时时间,
超时时间到期并且没有设备可用,主进程就会被唤醒,
返回0,如果这个参数指定为NULL,表示永久休眠!
如果主进程在超时时间未到期,有设备可用了,
select函数也返回(主进程被唤醒),返回值>0
函数返回值:如果返回0表示超时;如果返回>0,表示
设备可用,如果<0,select系统调用失败!
函数的功能:能够监听多个设备的读,写,异常情况,
如果设备不可用,主进程调用此函数进入休眠状态。
返回的原因有三种:
1.超时返回
2.设备可用返回
3.接收到信号
fd_set文件描述符集合相关操作的方法:
//去掉要监听的某个设备
void FD_CLR(int fd, fd_set *set);
//判断哪个设备引起的select函数的返回,如果
是这个设备引起的返回,函数返回真,否则返回假
int FD_ISSET(int fd, fd_set *set);
//添加要监听的设备
void FD_SET(int fd, fd_set *set);
//清空文件描述符集合
void FD_ZERO(fd_set *set);
回顾:
mmap:
目的:将设备的物理信息映射到用户空间的虚拟地址空间中
结果:数据完成一次的拷贝
驱动:就只是完成地址的映射
3G用户虚拟内存中的MMAP区
地址一定要是页的整数倍
具体的实现过程参看day09.txt笔记!
read,write,ioctl:
实现都是基于将物理信息映射到内核虚拟地址空间中
结果:用户,内核,硬件,两次数据拷贝
如果使用ioremap,映射的内核虚拟地址在VMALLOC区
总结:一个物理地址即可映射到用户的虚拟地址空间中,
也可以映射到内核的虚拟地址空间中。
select/poll:
应用程序调用select和poll最终调用到内核驱动的poll接口!
目的:能够实现同时对多个设备进行监听。
select的系统调用实现过程:
1.当应用程序调用select
2.调用C库的select
3.保存select系统调用号,然后调用swi或者svc
触发一个软中断
4.进程就由用户态进入内核态,跳转到内核对应的
异常向量表的入口vector_swi
5.根据系统调用号在内核事先建立好的系统调用表里
找到对应的内核系统调用实现函数sys_select,然后
执行这个函数
6.进入一个死循环中
7.把监听设备对应的驱动程序中的poll函数都挨个
执行一遍,每个驱动的poll会将主进程添加到
各自的等待队列头中,然后判断设备或者数据是否
可用,如果可用,返回非0,否则返回0
8.判断设备是否可用,判断是否超时,判断主进程是否
接收到信号,如果都没有
9.调用poll_schedule_timeout进入真正的休眠状态
10.一旦其中有一个设备可用,对应设备的驱动程序
比如中断处理函数就会唤醒休眠的主进程,那么
poll_schedule_timeout函数返回,在此执行一遍死循环
11.在此调用每一个设备驱动的poll,如果是设备可用
必然有一个设备驱动的poll返回非0
12.在此判断是否设备可用,不用判断超时了,也不用
判断是否接收到信号,直接跳出死循环,返回
给用户空间,这样主进程就从select函数返回,继续
往下执行
13.主进程要判断是哪个设备引起的唤醒
14.如果是这个设备引起的唤醒,接下来就
进行read,write,ioctl的操作过程
linux输入子系统
1.谈谈按键驱动的第一版本和第二版本的缺点
2.引入内核分层思想
3.分析linux输入子系统
4.input子系统的框架:
app:open,read,write...
------------------------------
input核心层(drivers/input/input.c)
1.对上(用户)提供了统一的访问设备的接口方法
2.对下(驱动)提供了统一的注册硬件到核心层的方法
-------------------------------
设备驱动层
1.只关心硬件相关的信息
-------------------------------
按键 触摸屏 鼠标 游戏摇杆
问:核心层给用户提供的统一的访问硬件的方法接口长什么样?
问:核心层给驱动提供的统一注册硬件到核心层的方法和数据结构长什么样?
问:核心层提供的统一访问硬件接口方法如何去操作使用驱动的硬件信息?
问:如何写一个符合input子系统的按键驱动呢?
答: 答案都在input.c核心层代码中
input核心层:
subsys_initcall(input_init);
//注册一个字符设备,并且给用户提供的访问接口input_fops
register_chrdev(INPUT_MAJOR, "input", &input_fops);
struct file_operations input_fops = {
.owner = THIS_MODULE,
.open = input_open_file,
};
问:其他的函数接口跑哪里了?
答:由于对设备的访问永远先open,应用程序调用
open,最终调用input_open_file
问:这个函数做了哪些事情?
input_open_file:
struct input_handler *handler;
struct file_operations *old_fops, *new_fops
以次设备号/32为索引在input_table全局数组
中找到对应的一项input_handler
handler = input_table[iminor(inode) >> 5];
问:既然是个数组,肯定在访问元素之前,要对
数组中的每一个元素进行初始化,在何时何地被填充的?
//找到handler以后,在从中取出fops给new_fops
new_fops = fops_get(handler->fops);
//备份之前的操作集合
old_fops = file->f_op = &input_fops;
//更新设备的操作集合
file->f_op = new_fops=handler->fops;
以后应用程序调用read,write等操作接口
最后访问的是handler->fops
//最终调用handler->fops->open函数
new_fops->open(inode, file);
总结:后续代码的研究只关注input_handler
问:既然是个数组,肯定在访问元素之前,要对
数组中的每一个元素进行初始化,在何时何地被填充的?
答:通过搜索代码,input_table数组元素的填充
在函数input_register_handler中完成
int input_register_handler(struct input_handler *handler);
说明在内核别的地方,别人会
1.分配struct input_handler
2.初始化struct input_handler
3.调用此函数完成对input_handler的注册过程
input_handler_register:
struct input_dev *dev;
//把分配初始化的input_handler填充到数组中,
以后input_open_file函数使用
input_table[handler->minor >> 5] = handler;
//将分配初始化好的input_handler节点添加到input_handler_list中
input_handler_list
list_add_tail(&handler->node, &input_handler_list);
//遍历input_dev_list,取出每一个节点跟分配初始化好的
input_handler做匹配(成功的)
list_for_each_entry(dev, &input_dev_list, node)
input_attach_handler(dev, handler);
问:input_handler_list什么时候被遍历的?
问:input_dev_list什么时候被添加节点的?
问:input_attach_handler函数做了什么事情?
通过搜索代码可知,问题1和2的答案在函数
input_register_device中完成
int input_register_device(struct input_dev *dev)
在内核别人的底层驱动中:
1.分配input_dev
2.初始化input_dev
3.调用input_register_device向核心层的input_dev_list
链表中注册硬件信息
input_register_device:
//将分配初始化好的input_dev添加到input_dev_list中
list_add_tail(&dev->node, &input_dev_list);
//遍历链表input_handler_list,取出每一个节点handler
调用input_attach_handler
list_for_each_entry(handler, &input_handler_list, node)
input_attach_handler(dev, handler);
问:input_attach_handler函数做了什么事情?
答://匹配input_handler和input_dev
input_match_device(handler, dev);
//如果匹配成功,说明硬件和软件进行了联系
(注册字符设备+硬件的处理)
handler->connect(handler, dev, id);
问:如何建立硬件和软件的连接呢?
如何建立input_handler和input_dev的链接呢?
答:evdev_connect:
handle.dev = 左边;硬件信息
handle.handler = 右边;纯软件
//注册input_handle
input_register_handle(&evdev->handle);
//将input_handle添加到input_dev.h_list中
list_add_tail_rcu(&handle->d_node, &dev->h_list);
//将input_handle添加到input_handler.h_list
list_add_tail_rcu(&handle->h_node, &handler->h_list);
总结:以后通过input_dev或者input_handler的h_list
就能够找到对方!
一旦建立好链接,后续就可以进行对设备的访问:
问:如何访问?以读取按键为例
app:read->sys_read->evdev_read:
//如果缓冲区没有按键数据,将当前
进程添加到evdev->wait等待队列头中,进入
休眠
wait_event_interruptible(evdev->wait,缓冲区是否有按键数据);
问:谁来唤醒?
答:当然在中断处理函数中进行唤醒
通过搜索代码可知,真正的唤醒由evdev_event
函数来实现唤醒,但这个函数属于
input_handler.event = evdev_event;
问:如何在底层驱动的中断处理函数中调用到
evdev_event来实现唤醒呢?
答:这个时候涉及到硬件input_dev找input_handler
的问题了
问:左边如何找右边?
答:在中断处理函数中调用input_event来实现
唤醒休眠的进程和数据传递的功能
input_event:
input_handle_event
input_pass_event
//通过input_dev.h_list取出input_handle
list_for_each_entry_rcu(handle, &dev->h_list, d_node) {
//通过input_handle取出input_handler
handler = handle->handler;
//调用handler->event:
1.将硬件上报的信息type,code,value上报input_handler
2.唤醒休眠的进程
handler->event(handle, type, code, value);
}
问:如何写一个符合input子系统的按键驱动呢?
答:
1.分配input_dev
struct input_dev *btn_dev = input_allocate_device();
2.初始化input_dev
struct input_dev {
.name = "tarena_buttons",
.evbit = 指定哪类事件
//同步类事件
#define EV_SYN0x00
//按键类事件
#define EV_KEY0x01
//相对位移坐标事件
#define EV_REL0x02
//绝对位移坐标事件
#define EV_ABS0x03
//重复类事件
#define EV_REP0x14
注意:初始化必须指定该设备能够上报哪些事件
.keybit = 上报按键类事件中的哪些事件
一般要指定为要上报的按键值。
上报的按键值在input.h中定义
KEY_1,KEY_2,KEY_L,KEY_UP
.absbit = 上报绝对位移的坐标
...
}
3.调用input_register_device注册input_dev
1.添加节点到input_dev_list
2.遍历input_handler_list进行匹配,
匹配成功,内核帮你实现input_dev和input_handler
(evdev_handler)之间的链接关系,这样
硬件就有软件的支持和对应的操作方法了(evdev_fops)
4.注册中断处理函数
5.申请GPIO
6.地址映射
问:应用程序如何获取底层驱动上报的数据信息?
答:用户空间和内核空间通过struct input_event
来实现数据的交互
struct input_event {
struct timeval time; //时间信息
__u16 type; //事件类型 EV_KEY EV_ABS EV_ABS
__u16 code; 键值 ABS_X ABS_Y
__s32 value; 按键状态(1:按下,0:松开) x坐标 y坐标
};
案例:按键驱动第三版本
测试步骤:
insmod btn_drv.ko
cat /proc/interrupts //查看中断注册信息
cat /proc/bus/input/devices //查看输入设备的信息和设备节点
新注册的按键信息如下:
I: Bus=0000 Vendor=0000 Product=0000 Version=0000
N: Name="tarena_buttons"
P: Phys=
S: Sysfs=/devices/virtual/input/input3
U: Uniq=
H: Handlers=kbd event3:对应的设备节点:/dev/event3
B: EV=100003
B: KEY=40 90000000
测试方法1:cat /dev/event3
测试方法2:hexdump /dev/event3
命令头 时间 信息 事件类型 键值 按键状态低位 高位
按下
上报按键事件
0000000 6ac0 0000 47fc 0009 0001 0026 0001 0000
上报同步类事件
0000010 6ac0 0000 4805 0009 0000 0000 0000 0000
松开
0000020 6ac0 0000 54f8 000c 0001 0026 0000 0000
0000030 6ac0 0000 54fc 000c 0000 0000 0000 0000
测试方法3:./btn_test /dev/event3
测试方法4:exec 0</dev/tty1 重定向标准输入
操作按键执行ls命令
按键驱动:
0.按键硬件的接法
独立式
矩阵式
1.轮训方式
驱动第一版本
太占用CPU的资源
2.中断
中断的硬件接法
中断控制器
中断的处理流程
linux内核对中断处理函数的要求
顶半部和底半部机制(网卡)
驱动第二版本
驱动和应用程序的关系及其密切
驱动第三版版本
能够使应用和驱动进行无缝的衔接
input子系统驱动框架
按键去抖:
硬件去抖
软件去抖
linux内核中采用定时器去抖
------------------------------------------------
I2C总线:
主端:主设备,master,CPU
从端:从设备,slave,挂接的多个从设备
主端,从端通过两根信号线链接:
SCL:时钟信号线,时钟由主端来提供
SDA:数据信号线,数据线由双方来管理,当主端向从端
写入数据时,数据线由主端控制(配置输出口);
当主端从从设备获取数据时,数据线从端控制,
CPU主端应该配置为输入端口。如果主端和从端都配置
输入口,上拉电阻拉高,同样时钟线也接上拉电阻。
SCL和SDA默认的电平状态是高(由上拉电阻决定!)
问:主端如何通过这两根线访问到具体的某一个从设备?
问:主端和从端如何通过这两根进行数据的交互?
答:答案在I2C总线协议中
I2C总线协议:
START信号:起始信号,由主设备发起,SCL为高电平,
SDA由高向低跳变
STOP信号:结束信号,由主设备发起,SCL为高电平,
SDA由低向高跳变
从设备地址:用来标识从设备的唯一性,从设备地址
由芯片厂家和原理图的硬件连接共同来决定:
AT24C02(电可擦除存储器):
设备地址:1010A2A1A0R/W,设备地址不算读写位-》
1010A2A1A0=01010000=>0x50(真正的设备地址)
R=1,读,读设备地址:10100001 = 0xa1
W=0,写,写设备地址:10100000 = 0xa0
LM77(温度传感器):
设备地址:10010A1A0(A1,A0都接地)-》1001000
最终设备地址:01001000=>0x48
ADP8860(背光芯片):
设备地址:0101010x->最终的设备地址00101010=>0x2a
注意:如果对从设备访问,一定要获取从设备的地址,
因为主设备通过从设备地址来确定要访问的从设备
ACK信号:应答信号,低电平有效,用来表示
设备是否在为或者数据交互的一个状态!
如果主设备给从设备发送完设备地址以后,从设备
应该给主设备发一个ACK信号。
注意:I2C总线数据交互每次传输1字节数据!
问:主设备和从设备如何通过SCL和SDA完成数据的交互呢?
答:
1.主设备发送START信号
2.主设备发送从设备地址(确定要访问的具体的从设备)
3.主设备发送读写信号(WRITE=0,READ=1)
4.从设备如果在位,那么必须给主设备发送一个应答信号(低电平)
5.根据芯片手册的要求完成读写
地址,数据,ACK
6.如果数据交互完毕,主设备发送STOP信号
问:SCL和SDA如何搭配使用?
答:当SCL为高电平,SDA上的数据保持稳定,可以进行
读取操作;
当SCL为低电平,SDA上的数据可以进行修改!
I2C总线的应用领域:
手机:PMIC(电源管理芯片),各种传感器,摄像头,电容触摸屏芯片...
发现规律:
1.S5PV210内部集成了I2C控制器,I2C总线上数据交互时,
所需的时序要求,都可以通过操作控制器,由控制器
来帮咱们发起。
2.但是控制器发起的时序,有的是固定的,有的是可变的。
固定:START,STOP,ACK,R/W
可变:设备地址,读写访问的地址(寄存器),数据
这些数据可变的数据信息都来自于操作访问的芯片(从设备)
3.只需将可变的额外的信息告诉给I2C控制器,开启控制
完成最终的数据的交互即可。
由于以上3点,内核实现I2C驱动,如下:
I2C总线驱动:
管理的硬件对象是I2C控制器,关心如何
进行数据的传输,而关心数据的特定含义!
总线驱动一般由芯片厂家提供,都是附加在
提供给的linux内核源码中,只需配置添加
即可:
make menuconfig
Device Drivers --->
I2C Supoort->
I2C Hardware Bus support --->
<*> S3C2410 I2C Driver //三星ARM i2c总线驱动
drivers/i2c/busses/i2c-s3c2410.c
I2C设备驱动:
操作管理的对象就是I2C从设备,关心的从设备地址,
要访问从设备的内部某个寄存器的地址和寄存器中的某个数据信息
关心的数据的特定含义,而不关心数据如何传输的,
数据的传输靠I2C总线驱动,I2C设备驱动只需将
要操作的数据丢给I2C总线驱动来完成数据的最终交互即可。
linux内核I2C驱动框架:
app:open,read,write...实现读取温度,将某个数据写入到eeprom中的某个地址
//温度,地址,数据就是一个特定含义的数据
buf[0] = 0x10;
buf[1] = 0x55;//将0x55写入到0x10地址
write(fd, buf, 2);
---------------------------------------------
I2C设备驱动:关心的数据的含义,不关心数据如何传输的
eeprom_open,eeprom_write,eeprom_read...
---------------------------------------------
内核提供统一的方法实现设备驱动和总线驱动的
数据的交互:
i2c_transfer();//老式接口
SMBUS接口(提供了一组函数);//新式接口
新式接口能够对老式接口进行兼容!
作用:I2C设备驱动和I2C总线驱动交互的函数方法!
----------------------------------------------
I2C总线驱动:关心的数据是如何传输的,而
不关心具体的操作数据的含义!
===============================================
I2C控制器 <=>从设备
问:如何实现一个从设备的I2C设备驱动呢?
答:I2C设备驱动也采用设备-总线-驱动模型
内核已经帮你定义好了一个I2C的虚拟总线i2c_bus_type,
在这个总线上维护这两个链表:dev链表,drv链表;
dev链表中的每一个节点对应的数据结构i2c_client,存放硬件信息
drv链表中的每一个节点对应的数据结构i2c_driver,纯软件信息
每当向dev链表或者drv链表添加新节点时,它们都会
1.添加节点到各自的链表中;
2.遍历对方的链表,取出每一个节点,然后进行匹配:
拿i2c_client.name和i2c_driver.id_table.name
进行比较,如果比较成功,调用i2c_driver.probe
函数,并且将匹配成功的i2c_client的首地址传递
给probe函数,probe函数要完成的工作完全由你来决定!
比如可以注册一个字符设备,给用户提供访问接口!
问:i2c_client和i2c_driver如何使用?
答:i2c_client描述的硬件相关的信息
i2c_driver描述对硬件(i2c从设备)操作的纯软件
i2c_driver如何使用?
1.分配i2c_driver
2.初始化i2c_driver
struct i2c_driver eeprom_drv = {
.id_table = 名字一定要初始化
.probe = 一旦匹配成功,执行次接口函数
.remove = 删除调用
};
3.调用i2c_add_driver注册
1.将i2c_driver节点添加到drv链表中
2.遍历dev链表,取出每一个节点i2c_client
跟自己匹配,如果匹配成功,调用probe函数
i2c_client如何使用?
i2c_client的分配初始化注册过程都是内核来帮你实现,
你只需对i2c_board_info进行分配初始化注册即可,
内核根据i2c_board_info再去填充i2c_client信息。
i2c_client的使用就放在i2c_board_info结构体的使用:
1.必须在平台代码中mach-cw210.c中完成对
i2c_board_info从设备信息的初始化
1.打开arch/arm/mach-s5pv210/mach-cw210.c
2.在文件中添加i2c_board_info的分配初始化
static struct i2c_board_info eeprom[] = {
I2C_BOARD_INFO("at24c02", 0x50)
};//at240c2最终会赋值给i2c_client.name
//0x50最终赋值给i2c_client.addr
3.必须在平台代码mach-cw210.c的初始化函数
mach_init = smdkc110_machine_init中注册
分配初始化好的i2c_board_info
i2c_register_board_info(0,eeprom, ARRAY_SIZE(eeprom));
一个参数:总线编号,看从设备挂接在哪个总线上,
s5pv210有四个I2C总线,对于at24c02这个eeprom挂接在
I2C0总线
第二个参数:从设备硬件信息
第三个参数:从设备硬件的个数
//当向内核完成从设备硬件信息注册以后,
每当内核初始化时,首先注册从设备信息,
然后在初始I2C总线驱动,I2C总线驱动根据之前
初始化的从设备信息,依次探测总线的从设备,
如果发现从设备存在,就为每一个从设备
实例化一个i2c_client,
将i2c_board_info的type给i2c_client.name,
将i2c_board_info的addr给i2c_client.addr,
将i2c_board_info.platform_data给i2c_client.platform_data
然后向内核注册:
1.将i2c_client添加dev链表中
2.遍历drv链表,取出每一个i2c_driver进行
匹配,一旦匹配成功,调用i2c_driver.probe函数
并且将i2c_client从设备硬件信息的首地址传递
probe,这样probe函数就可以从i2c_client中
获取到i2c从设备地址和platform_data中自己定义的
数据信息
案例:编写AT24C02,EEPROM的I2C设备驱动
1.打开arch/arm/mach-s5pv210/mach-cw210.c
在文件开头添加如下代码:
//分配初始化i2c_board_info
static struct i2c_board_info at24c02[] = {
{
I2C_BOARD_INFO("at24c02", 0x50)
}
}; //“at24c02”-》i2c_client.name,0x50->i2c_client.addr
2. 打开arch/arm/mach-s5pv210/mach-cw210.c
在smdkc110_machine_init函数中添加如下代码:
i2c_register_board_info(0,
at24c02,
ARRAY_SIZE(at24c02));
3.make zImage
4.说明:每当在平台代码添加对从设备硬件信息的
初始化注册以后,内核再重新初始化时,首先将
这些从设备的硬件信息(i2c_board_info添加到
内核的一个链表中,然后再初始化I2C总线驱动,
总线驱动会遍历这个链表,取出每一个从设备的
设备地址(i2c_board_info.addr),然后
I2C总线驱动发START信号,发设备地址,然后探测
是否有ACK的应答信号,如果有应答信号,说明
从设备在总线上存在,然后内核会实例化一个
i2c_client,然后将i2c_board_info的信息填充给
i2c_client,然后注册i2c_client.
5.编写i2c_driver的驱动代码at24c02_drv.c
./at24c02_test w addr data
./at24c02_test w 0x10 100
./at24c02_test r addr
./at24c02_test r 0x10
6.将软件版本写入eeprom中,供用户以后来查看
软件版本号的格式:SYYMMDDXY,例如:S14042101
硬件版本号的格式:HYYMMDDXY,例如:H14042100
地址范围:0~255,要注意地址要进行规划和划分!
回顾:
1.采用平台总线实现按键驱动
gpio_keys.c(内核GPIO类型的按键参考驱动)
static struct platform_device btn_dev = {
.name = "mybtn",
.id = -1,
.dev = {
.platform_data = 指向自己定义硬件相关的数据信息
}
};
int btn_probe(struct platform_device *pdev)
{
//获取自己私有的硬件数据信息
struct my_prive *p = pdev->dev.platform_data;
return 0;
}
2.I2C相关信息
1.谈谈I2C硬件相关内容
两线式串行总线
SCL
SDA
2.SCL和SDA默认状态由上拉电阻拉高
3.CPU访问从设备
START
STOP
从设备地址:芯片手册
ACK信号
4.SCL和SDA的搭配使用问题
5.读写问题:
5步骤
6.gpio模拟I2C?
3.I2C在linux内核驱动框架
I2C总线驱动
I2C设备驱动
设备-总线-驱动
i2c_client->i2c_board_info
i2c_driver
SMBUS相关的函数如何使用
4.用EEPROM(I2C总线接口)存储软,硬件版本号
-----------------------------------------------
混杂设备驱动:
定义:就是主设备号(10)由内核定义好的一类字符设备
,通过次设备号来分区每一个混杂设备。
内核通过miscdevice数据结构来描述混杂设备:
struct miscdevice {
int minor; //次设备号
char *name; //设备节点名
struct file_operations *fops;//给用户提供的访问接口
};
1.分配struct miscdevice
2.初始化struct miscdevice
3.调用misc_register()向内核注册
1.将此节点添加到链表中
2.自动帮你创建设备节点
3.以后应用程序调open,read...最终访问的是
struct miscdevice里的fops相关接口
-----------------------------------------------
linux内核misc混杂设备驱动框架:
app:open,read,write....
---------------------------------
混杂设备的核心层(drivers/char/misc.c)
1.对上(用户)提供统一的访问操作接口
2.对下(驱动)提供统一的操作硬件的方法
----------------------------------
设备驱动
misc_register
struct miscdevice
混杂设备核心层:
subsys_initcall(misc_init);
//创建设备类(树枝)
misc_class = class_create(THIS_MODULE, "misc");
//注册字符设备,给用户提供的访问操作接口misc_fops
register_chrdev(MISC_MAJOR,"misc",&misc_fops)
问:应用程序调用read,write怎么样最终访问到
miscdevice的fops呢?
答:misc_fops.open = misc_open
misc_open:
//遍历链表misc_list,取出每一个节点
miscdevice,根据次设备找到对应的驱动
miscdevice,再从miscdevice中取出操作集合
list_for_each_entry(c, &misc_list, list) {
if (c->minor == minor) {
new_fops = fops_get(c->fops);
break;
}
}
//备份
old_fops = file->f_op = &misc_fops;
//更新
file->f_op = new_fops = miscdevice->fops;
结果:以后应用程序调用read,write等
操作接口,最终访问的是miscdevice->fops
问:misc_list链表什么时候添加节点的?
答:在函数misc_register中完成新节点miscdevice
的添加工作,并且帮你自动创建设备节点
案例:利用混杂设备实现led灯驱动
1.分配miscdevice
2.初始化miscdevice
3.注册miscdevice
4.申请GPIO
5.配置GPIO为输出
6.led_ioctl
控制灯
-----------------------------------------------
AD,DA,ADC,DAC
AD:模拟转数字过程,录音
DA:数字转模拟过程,放音
ADC:将模拟信号转数字信号的硬件逻辑单元
DAC:将数字信号转模拟信号的硬件逻辑单元
手机:录音,放音,需要ADC,DAC,这两个部件集成
一个音频芯片中(audio codec)WM8960
衡量ADC工作参数指标:
1.分辨率
用来描述对模拟信号的分辨能力,
也能描述最小的量度信息
一般来说分辨率为8,10,12,16,24
表示描述一个事物时,用多少位的数字量来描述
比如8位:一个数字量:01001111
比如10为:一个数字量:0100111101
分辨率越大,精度就越高,分辨能力越强
转换时间越长!
对于电压模拟信号0~3.3V,并且使用ADC进行转换
对应的分辨率为10位:
最小量度:3.3/(1<<10) = 0.0032V
模拟量 数字量
3.3V 1111111111
A:B*0.0032 B:0110000000
0V 0000000000
2.根据开发板上的硬件电路,目标是对一个电压模拟
信号进行采样,需要一个ADC模块,如何选型?
1.采用外置的ADC芯片,这时要考虑使用什么样的
通信接口(I2C,SPI,UART,GPIO),因为不同的
接口导致驱动程序的设计是不一样的。
2.采用CPU自带的ADC转换芯片,这个驱动程序
只需操作硬件寄存器即可。
3.开发板使用自带的ADC,S5PV210自带的ADC的硬件特性:
1.分辨率:10bit和12bit
2.模拟输入通道:10路,但ADC转换器只有1个
3.最大的工作频率5MHz
4.采样的模拟电压输入范围:0~3.3V
5.ADC开启转换,转换结束以后,产生内部中断,
告诉CPU,ADC转换结束,请读取数据
6.10路IO都是input
7.涉及的相关寄存器:
配置寄存器
选择分辨率
判断ADC转换的状态
使能分频
设置分频系数
启动ADC的转换
延时寄存器
数据寄存器
12bit:
data = 数据寄存器 & 0xfff;
10bit:
data = 数据寄存器 & 0x3ff;
模拟输入通道选择寄存器
ADC中断清除寄存器
1.采用混杂设备驱动机制来实现
2.注册中断
3.使用等待队列
4.adc_read
获取ADC转换的数据信息,上报给用户
5.adc_ioctl
配置ADC的分辨率,模拟输入通道
------------------------------------------------
一线总线(1-WIRE)
硬件特性:
1.总线就一根数据线
2.数据线接上拉电阻,数据线初始电平为高
3.硬件接法:
1.三线式
2.二线式
前者有独立的电源,后者没有,通过数据线给(内部有电容)
问:CPU如何访问某一个从设备
问:CPU和从设备如何通过这根数据线完成数据的交互
答:DS18B20芯片手册
特性:
分辨率可编程的一线式数字温度传感器
1.分辨率:9,10,11,12
2.片内集成了64bit ROM,存放设备唯一的序列码
3.DS18B20上电处于低功耗模式,默认的分辨率12
4.开启AD转换,master必须发送0x44命令
5.内部集成了9byte的RAM,并且将这个RAM进行划分:
byte0,byte1分别存放温度的低,高字节
byte2,byte3分别存放高低报警阀值
byte4用于配置分辨率
6.内部还集成了3字节的EEPROM,用来存放高低温报警阀值
和分辨率的配置信息。
7.总结:对DS18B20的访问起始就是内部RAM的访问
8.byte0,byte1:存放转换以后的温度值
读取byte0和byte1后,然后将数值*0.0625
温度值 = ((byte1 << 8) | byte0) * 0.0625
byte4:存放的是DS18B20转换时指定的分辨率信息
0x1f->9
0x3f->10
0x5f->11
0x7f->12
如果让DS18B20以其中一个分辨率来转换,只需将
对应的值写入到byte4对应的内存即可。
CPU访问DS18B20从设备的步骤如下:
1.CPU发送初始化信号
1.配置GPIO为输出口
2.CPU输出0
3.延时500us
4.CPU输出1
5.延时30us
6.配置GPIO为输入
2.CPU发送ROM命令
SERACH ROM
MATCH ROM
READ ROM
SKIP ROM->0xCC (11001100)
问:CPU如何通过一根数据线将0xcc发送出去?
问:CPU发送0xcc,从高位还是从低位开始发?
答:从低位开始发。
由于没有时钟线,所以一位一位发送需要
时序的要求。
这里就涉及读0,1,写0,1的时序。
如果搞定了读写0,1的时序操作,
发送ROM命令字就没有问题
3.CPU发送功能命令
CONVETT 0x44:开启AD的转换
WRITE RAM 0x4E:写内存,配置寄存器
READ RAM 0xBE:读内存,读取温度值
以配置分辨率和读取温度为例:
配置分辨率:
1.CPU发送初始化信号
2.CPU发送SKIP ROM命令
3.CPU发送WRITE RAM命令
4.CPU发送TH到byte2
5.CPU发送TL到byte3
6.CPU发送分辨率(0x1f,0x3f...)到byte4
读取温度:
1.CPU发送初始化信号
2.CPU发送SKIP ROM命令
3.CPU发送CONVETT开启AD的转换,转换以后,将
温度值保存在RAM的byte0和byte1
4.CPU发送初始化信号
5.CPU发送SKIP ROM命令
6.CPU发送READ RAM
7.CPU读取RAM byte0
8.CPU读取RAM byte1
9.CPU发送初始化信号,结束后续RAM的读取工作
1.混杂设备
字符设备(主设备号定义号)
struct miscdevice{
.minor
.name
.fops
}
2.AD
选型问题
1.外置的ADC芯片
2.CPU内置ADC
驱动设计
外置的ADC->ADC和CPU之间通信接口(I2C)->
I2C设备驱动
内置的ADC->标准的字符设备,提供操作访问
寄存器的接口即可
3.DS18B20
1.一线总线
1根数据线
三线式
两线式
2.一线总线数据通信问题
参看具体的一线器件
1.分辨率可编程的一线式数字温度传感器
内部集成ADC(将温度模拟信号转换数字信号,通过数据线进行传输)
分辨率:9,10,11,12
2.CPU跟DS18B20之间的通信和访问,其实就是
CPU访问内部的RAM(寄存器)
3.如何访问内部的寄存器呢?
三部曲:
1.CPU发送初始化信号
2.CPU发送ROM命令
3.CPU发送功能命令
4.如何实现读写问题?
根据芯片手册来了解掌握读写0,1的时序
5.芯片提供了相应的操作的流程图
以配置分辨率和读取温度为例:
配置分辨率:
1.CPU发送初始化信号
2.CPU发送SKIP ROM命令
3.CPU发送WRITE RAM命令
4.CPU发送TH到byte2
5.CPU发送TL到byte3
6.CPU发送分辨率(0x1f,0x3f...)到byte4
读取温度:
1.CPU发送初始化信号
2.CPU发送SKIP ROM命令
3.CPU发送CONVETT开启AD的转换,转换以后,将
温度值保存在RAM的byte0和byte1
4.CPU发送初始化信号
5.CPU发送SKIP ROM命令
6.CPU发送READ RAM
7.CPU读取RAM byte0
8.CPU读取RAM byte1
9.CPU发送初始化信号,结束后续RAM的读取工作
-----------------------------------------------
linux内核如何管理显存:
1.操作LCD不重要,因为操作LCD起始实在操作LCD对应的
显存!
2.linux内核framebuffer驱动框架:
app:open,read,write,ioctl...
-----------------------------
framebuffer核心层:drivers/video/fbmem.c
1.对上(用户)提供统一的访问操作接口
2.对下(驱动)提供统一的注册硬件的方法和对应的数据结构
------------------------------
设备驱动(显存和LCD控制器)
问:核心层fbmem.c给用户提供的统一操作接口长什么样?
问:核心层fbmem.c给底层驱动提供统一注册硬件的
方法和数据结构长什么样?
问:核心层的操作硬件的接口如何实现操作显存?
答:fbmem.c
subsys_initcall(fbmem_init):
//注册一个字符设备驱动,并且给用户提供的访问
操作显存的接口是fb_fops
register_chrdev(FB_MAJOR,"fb",&fb_fops)
//创建设备类(树枝)
fb_class = class_create(THIS_MODULE, "graphics");
打开LCD:
app:open->sys_open->fb_open:
int fbidx = iminor(inode);
struct fb_info *info;
//以次设备号为索引,在全局数组中取出一项
fb_info(问:registered_fb数组何时何地被初始化的?)
info = registered_fb[fbidx];
info->fbops->fb_open //调用次函数
读显存信息:
app:read->sys_read->fb_read:
//以次设备号为索引,在数组取出一项fb_info
int fbidx = iminor(inode);
struct fb_info *info = registered_fb[fbidx];
u32 *buffer, *dst;
u32 __iomem *src;
//如果fb_info中有fb_read读显存的方法,那么就调用
此方法实现读显存,
if (info->fbops->fb_read)
return info->fbops->fb_read(info,
buf, count, ppos);
如果没有fb_read,那么内核实现一个默认的
读显存实现
//从fb_info中获取屏幕的大小信息
total_size = info->screen_size;
//或者从fb_info.fix中获取显存的大小信息
total_size = info->fix.smem_len;
//分配内核缓冲区
count = total_size;
buffer = kmalloc(count);
//从fb_info中获取显存的内核虚拟起始地址
src = (u32 __iomem *) (info->screen_base);
while (count) {
dst = buffer;
//从显存读取数据到内核缓冲区
*dst++ = fb_readl(src++);
//从内核缓冲区拷贝数据到用户缓冲区
copy_to_user(buf, buffer, c)
}
总结:fb_read的实现严重依赖fb_info!
app:mmap->sys_mmap->fb_mmap:
//以次设备号为索引在数组中取出fb_info
struct fb_info *info = registered_fb[fbidx];
//从fb_info中获取显存的起始物理地址
start = info->fix.smem_start;
off += start;
//内核帮你实现将显存的物理地址映射到用户的虚拟地址
空间中,以后用户访问mmap的返回值就是再访问显存
if (io_remap_pfn_range(vma, vma->vm_start,
off >> PAGE_SHIFT,
vma->vm_end - vma->vm_start,
vma->vm_page_prot))
总结:底层驱动的fb_mmap实现地址的映射要严重
依赖fb_info.fix.smem_start(显存的起始物理地址);
那么一旦完成将显存的起始物理地址映射到用户空间的
某一个虚拟地址,以后用户访问mmap的返回值,就是在
访问显存!
获取显存,屏幕的物理信息:
app:ioctl->sys_ioctl->fb_ioctl:
struct fb_info *info = registered_fb[fbidx];
struct fb_var_screeninfo var; //屏幕的可变参数信息
struct fb_fix_screeninfo fix; //屏幕的固定参数信息
switch (cmd) {
case FBIOGET_VSCREENINFO:
var = info->var;
copy_to_user(argp, &var, sizeof(var))
break; //将屏幕的可变参数信息上报给用户
case FBIOGET_FSCREENINFO:
fix = info->fix;
copy_to_user(argp, &fix, sizeof(fix))
break;//将屏幕的固定参数信息上报给用户
...
总结:设置,获取屏幕的参数信息要严重依赖
fb_info(fix,var)
总结:由于核心层这些函数接口的实现都依赖
fb_info结构体,但fb_info由来自registered_fb
数组。
问:这个数组何时何地被初始化?这个数组的
fb_info怎么添加的?
答:
int
register_framebuffer(struct fb_info *fb_info)
{
//找空项
for (i = 0 ; i < FB_MAX; i++)
if (!registered_fb[i])
break;
//创建设备节点:主设备号29,次设备号i
设备节点名:fb0,fb1,fb2,.....fb31
device_create(fb_class, fb_info->device,
MKDEV(FB_MAJOR, i), NULL, "fb%d", i);
//填充fb_info到数组中,供以后应用程序和
核心层来使用!
registered_fb[i] = fb_info;
}
如何实现一个LCD驱动呢?
1.分配fb_info
2.初始化fb_info
指定显存的物理起始地址
指定显存的内核虚拟起始地址
指定显存的大小
指定显存的可变参数信息
指定显存的固定参数信息
...
3.调用register_framebuffer注册fb_info到
核心层,供用户和核心层来使用来访问显存
4.LCD控制器硬件相关的初始化
寄存器的地址映射
寄存器的默认参数的配置
案例:每隔10行画一行,时间间隔100ms
案例:在屏幕的正中央画一个正方形
测试:
./lcd_test /dev/fb0
-----------------------------------------------
LCD控制器和LCD面板之间的硬件特性:
VD0~VD23:24根数据线,用于传输RGB
VSYNC:垂直同步信号,实现控制写一帧的信号
HSYNC:水平同步信号,实现控制写一行的信号
VCLK:像素时钟信号,控制写一个像素的信号,每一个
时钟周期,写一个像素点。
VDEN:数据使能信号。
LCD控制器和LCD面板之间的时间参数的配置:
VSPW+1:表示VSYNC的脉冲宽度
VSPW+1=tvpw=10=>VSPW=9
VBPD+1:表示当发送完VSYNC信号以后,在经过多久
使能数据
VBPD+1=tvb-tvpw=23-10=>VBPD=12
VFPD+1:表示写下一帧的时间间隔
VFPD+1=tvfp=22=>VFPD=21
HSPW+1:表示水平同步信号的脉冲宽度
HSPW+1=thpw=20=>HSPW=19
HBPD+1:表示发送完HSYNC信号以后,经过多久使能
数据
HBPD+1=thb-thpw=46-20=>HBPD=25
HFPD+1:表示写下一行的时间间隔
HFPD+1=thfp=210=>HFPD=209
一旦以上时间参数信息设置好以后,只需将
计算出来的值写入到LCD控制器对应的寄存器即可,
LCD控制器硬件就以这样的时序来操作LCD面板。
切记:如果以后CPU没换,换LCD屏,记住要注意
这些时间参数!时间参数的换算要结合LCD控制器的
时序图和LCD面板的时序图!
像素时钟=33.3MHz
实现图像的显示方法:
1.不使用任何图形框架,直接操作显存;
2.使用现成的图形框架,比如QT,GTK等
QT本身最底层的操作还是基于第一种方法,
只是QT给用户封装了一些类和操作接口!
所以QT玩的是它的库!移植QT,移植的QT的库!
回顾:
1.LCD相关内容
硬件特性
24位:VD0~VD23:数据线,RGB:888
16位:VD0~VD15:数据线,RGB:565
VSYNC
HSYNC
VCLK
VDEN
会算:上,下,左,右边界的时间参数
根据LCD控制器的时序和LCD面板的时序
还要会看时序操作图!
LCD显示原理:LCD面板显示的内容都是来自显存!
显存是共享主存!
显存和LCD面板之间的对应关系!
linux内核对framebuffer的驱动设计!
app:open,read,write,mmap对显存操作
----------------------------------
核心层:
fb_open
fb_read
fb_write
fb_mmap
fb_ioctl
这些函数接口的实现都依赖struct fb_info
fb_info:
显存的大小
显存的物理的起始地址
显存的内核起始虚拟地址
屏幕的物理信息:分辨率等
-----------------------------------------------
LCD底层驱动:
1.分配fb_info
struct fb_info *mfb_info =
framebuffer_alloc(0, NULL);
2.根据屏幕信息填充fb_info
3.调用register_framebuffer注册fb_info
到核心层去
4.LCD控制器相关的初始化
应用程序对显存的操作【mmap】
驱动程序:重点把握在移植方面【换个新屏,注意参数的修改】!
一般关于屏幕硬件相关的参数信息都是在平台代码中!
-------------------------------------------------
nandflash驱动开发:
面试题:请描述norflash和nandflash的区别!
nandflash硬件操作特性:
DATA0~DATA7:8根数据线,传输命令,地址,数据
CLE:当CLE为高电平时,数据线上传输的命令
ALE:当ALE为高电平时,数据线上传输的地址
当CLE和ALE都为低电平,数据线上传输的数据
WE:写使能,低电平有效
RE:读使能,低电平有效
CE:片选使能信号,低电平有效,低电平时,芯片选中!
由于8根数据线由DM9000网卡,内存和flash一起使用,
必须通过片选来指定谁来使用!
R/B:状态信号,当为低电平时,处于忙,当为高电平时,处于空闲
CPU如何访问nandflash:
1.发送命令
2.发送地址
3.读写数据
以读取ID为例:
nand s5pv210mx25
发送命令 使能片选 NFCMMD = 0x90 gpio模拟
拉高CLE
使能WE
将命令0x90写入到数据线上
发送地址 使能片选 NFADDR=0x00
拉高ALE
使能WE
将地址0x00写入到数据线上
读写数据 使能片选 data = NFDATA
拉低ALE,CLE
使能RE
获取数据线上的数据信息
案例:通过uboot命令实现读取nand的ID信息
tarena # help mw
mw [.b, .w, .l] address value [count]
- write memory
tarena # help md
md [.b, .w, .l] address [# of objects]
- memory display
tarena # mw b0e00008 90 //发命令
tarena # mw b0e0000c 0 //发地址
tarena # md.b b0e00010 1 //读
b0e00010: ec .
tarena # md.b b0e00010 1 //读
b0e00010: d3 .
tarena # md.b b0e00010 1 //读
b0e00010: 51 Q
tarena # md.b b0e00010 1 //读
b0e00010: 95 .
tarena # md.b b0e00010 1 //读
b0e00010: 58 X
案例:读8000地址里的数据
计算页内偏移地址和页地址
页地址=8000/2048 = 3 = 0x3
页内偏移=8000-2048*3=0x740
第一次发:0x40
第二次发:0x07
第三次发:0x3
第四次,五次发:0x00
可以使用nand dump 0 读取第一页的数据,然后
通过uboot命令读取数据跟nand dump读得数据做比较!
-----------------------------------------------
嵌入式linux系统中如何使用nandflash?
问:如何实现在整个linux系统运行的情况下,实现
对u-boot.bin,zImage,rootfs.cramfs等镜像文件的
更新和固化呢?
答:之前可以通过uboot来实现对这些镜像文件的
更新,更新之前需要人为的规划nand的分区
假设nand分区如下:
0--------2M--------7M--------12M-------剩余
uboot zImage rootfs userdata
固化方法1:通过uboot
tftp 50008000 u-boot.bin
nand erase 0 200000
nand write 50008000 0 200000
...
问题:方法1必须在uboot模式下完成更新!
linux系统中如何使用nandflash:
1.查看nand的分区表
cat /proc/mtd
dev: size erasesize name
mtd0: 00100000 00020000 "bootloader"
mtd1: 00300000 00020000 "Logo"
mtd2: 00500000 00020000 "kernel"
mtd3: 3f600000 00020000 "rootfs"
2.查看每一个分区对应的设备节点信息
ls /dev/mtd* -lh
crw-rw---- 1 root 0 90, 0 Jan 1 00:00 /dev/mtd0
crw-rw---- 1 root 0 90, 1 Jan 1 00:00 /dev/mtd0ro
crw-rw---- 1 root 0 90, 2 Jan 1 00:00 /dev/mtd1
crw-rw---- 1 root 0 90, 3 Jan 1 00:00 /dev/mtd1ro
crw-rw---- 1 root 0 90, 4 Jan 1 00:00 /dev/mtd2
crw-rw---- 1 root 0 90, 5 Jan 1 00:00 /dev/mtd2ro
crw-rw---- 1 root 0 90, 6 Jan 1 00:00 /dev/mtd3
crw-rw---- 1 root 0 90, 7 Jan 1 00:00 /dev/mtd3ro
brw-rw---- 1 root 0 31, 0 Jan 1 00:00 /dev/mtdblock0
brw-rw---- 1 root 0 31, 1 Jan 1 00:00 /dev/mtdblock1
brw-rw---- 1 root 0 31, 2 Jan 1 00:00 /dev/mtd
nandflash即可作为块设备使用,也可以作为字符设备使用!
块设备对应的设备节点:/dev/mtdblock0,/dev/mtdblock1,/dev/mtdblock2...
字符设备对应的节点:/dev/mtd0,/dev/mtd1,/dev/mtd2...
问:块设备节点和字符设备节点如何使用?
答:
块设备节点的使用:
必须先mount一种文件系统类型:
mount -t yaffs2 /dev/mtdblock3 /mnt
将第四块分区挂接到mnt目录,指定的文件系统是yaffs2
以后访问mnt目录就是在访问第四块分区!就可以
在mnt目录实现读写文件!
cd /mnt 进入了第四块分区,可以实现读写文件
如果不用了,要进行卸载:
umount /mnt
字符设备节点使用:
必须使用nand操作工具:
flash_eraseall(擦除工具)
nandwrite(写工具)
nanddump(读工具)
这三个工具需要配置busybox来支持!
配置busybox支持nand操作命令的步骤:
1.make menuconfig
Miscellaneous Utilities --->
[*] nandwrite
[*] nanddump
[*] flash_eraseall
2.make //重新编译busybox
3.make install //安装busybox
4.重启系统,用新的支持nand操作工具的rootfs
问:这三个工具如何使用?
5.首先修改内核nand驱动的分区表
5.1打开nand官方驱动
vim 内核源码/drivers/mtd/nand/s3c_nand.c
5.2找到分区表:
struct mtd_partition s3c_partition_info[] = {
41 #if 1
42 {
43 .name = "bootloader",
44 .offset = (0), /* for bootload er */
45 .size = (1024*SZ_1K),
46 .mask_flags = MTD_CAP_NANDFLASH,
47 },
48 {
49 .name = "Logo",
50 .offset = (2*SZ_1M),
51 .size = (3*SZ_1M),
52 },
53 {
54 .name = "kernel",
55 .offset = MTDPART_OFS_APPEND,
56 .size = (5*SZ_1M),
57 },
58 {
59 .name = "rootfs",
60 .offset = MTDPART_OFS_APPEND,
.size = MTDPART_SIZ_FULL,
62 }
};
将以上的分区表进行修改,修改为:
struct mtd_partition s3c_partition_info[] = {
41 #if 1
42 {
43 .name = "uboot",
44 .offset = (0), //起始地址
45 .size = (1024*SZ_1K*2), //分区大小
46 .mask_flags = MTD_CAP_NANDFLASH,
47 },
48 {
49 .name = "kernel",
50 .offset = (2*SZ_1M),
51 .size = (5*SZ_1M),
52 },
53 {
54 .name = "rootfs",
55 .offset = MTDPART_OFS_APPEND, //紧接着上一个分区开始
56 .size = (5*SZ_1M),
57 },
58 {
59 .name = "userdata",
60 .offset = MTDPART_OFS_APPEND,
.size = MTDPART_SIZ_FULL, //剩余空间
62 }
};
5.3重新编译内核make zImage
5.4重新用新内核引导系统
5.5查看新的分区表信息 cat /proc/mtd
6.使用三个工具来对新的分区表进行操作
案例:将zImage写入到第二块分区"kernen"
6.1 flash_eraseall /dev/mtd1 //擦除第二块分区,注意使用对应的字符设备节点
6.2 nandwrite -p /dev/mtd1 zImage //将zImage写入到第二块分区
7.验证zImage是否写入成功,通过uboot来读取然后引导
7.1 进入uboot模式
7.2 执行一下命令
nand read 50008000 200000 500000
bootm 50008000 //查看写入的内核是否能正常启动
如果能正常启动,说明nandwrite等命令是正常!
8.制作根文件系统镜像rootfs.cramfs
//将根文件系统rootfs制作为rootfs.cramfs镜像文件
8.1 sudo mkfs.cramfs rootfs rootfs.cramfs
8.2 flash_eraseall /dev/mtd2 //先擦
8.3 nandwrite -p /dev/mtd2 rootfs.cramfs //将rootfs.cramfs写入到第三块分区
8.4 验证rootfs.cramfs是否能被挂接
配置linux内核源码
make menuconfig //支持cramfs文件系统
File systems --->
[*] Miscellaneous filesystems --->
<*> Compressed ROM file system support (cramfs)
make zImage
用新内核引导
8.5 进入uboot模式,然后设置bootargs
setenv bootargs root=/dev/mtdblock2
init=/linuxrc console=ttySAC0,115200
rootfstype=cramfs
或者:
setenv bootargs root=1f02
init=/linuxrc console=ttySAC0,115200
rootfstype=cramfs
boot //启动系统,查看系统是否能正常启动
8.6 系统起来以后
cat /proc/cmdline 查看是否是采用本地方式启动
mkdir /hello //是不能创建文件目录,cramfs
是只读文件系统
------------------------------------------------
linux内核nandflash驱动框架:
app:open("a.txt"),read,write文件
--------------------------------------
文件系统(yaffs2)
--------------------------------------
块设备驱动层:
作用:帮你实现读写优化
--------------------------------------
nandflash协议层:
它知道发送什么命令和地址和数据信息,
这一层关心的是数据的特定含义!
比如命令是0x90,地址0x00。
问:0x90和0x00这些数据最终面临如何传输问题?
----------------------------------------
nand底层驱动
就是完成将nand协议层涉及的数据信息传输出去
不关心数据的特定含义,只关心如何实现
数据的交互!
比如:到底是采用通过寄存器实现数据传输,
还是GPIO模拟方式来实现数据传输
问:如何实现nandflash驱动?
答:
1.分配nand_chip
2.分配mtd_info
3.初始化nand_chip
初始化nand_chip中最底层的:
发送命令的方法;
发送地址的方法
读写数据的方法
因为接口不一样,最终导致发送命令,地址和数据的方法也不一样!
4.调用nand_scan_ident扫描识别板卡nand信息
5.调用nand_scan_tail初始化注册mtd_info到内核中
6.添加分区表信息
7.硬件相关的初始化
地址映射
GPIO的复用处理
LCD控制器相关寄存器的初始化
nand_scan_ident:协议层提供的扫描识别nand信息的方法
struct nand_chip *chip = mtd->priv = 驱动分配的nand_chip
nand_set_defaults(chip, busw);
if (!chip->select_chip)
chip->select_chip = nand_select_chip;
内核给提供一个默认的操作片选管脚的方法,
通过代码分析可知,默认的函数不能用,
只能自己写!
if (chip->cmdfunc == NULL)
chip->cmdfunc = nand_command;
内核提供一个默认的发送命令和地址的方法,
通过代码可知,默认的函数的实现都依赖
nand_chip->cmd_ctrl,这个接口内核没有
提供默认的实现,需要自己去实现!
if (!chip->read_byte)
chip->read_byte = busw ? nand_read_byte16 : nand_read_byte;
内核提供一个默认的读nand的方法,
通过分析代码可知,默认的函数需要自己
将chip中的IO_ADDR_R字段进行填充,填充为
数据寄存器的内核虚拟地址
if (!chip->write_buf)
chip->write_buf = busw ? nand_write_buf16 : nand_write_buf;
内核提供默认的写nand的方法,
通过分析代码可知,默认的函数需要自己将chip
中的IO_ADDR_W填充为数据寄存器的内核虚拟地址
nand_get_flash_type//使用chip相关的接口
实现对flash的操作,这些接口什么时候初始化的!
并且这些接口的实现要依赖CPU和nand之间的接口!
总结:协议层知道如何发送具体的数据,但是需要
底层驱动通过nand_chip提供最底层的硬件发送数据的
方法!这些方法都需要nand底层驱动来实现!实现过程
起始就是对nand_chip的初始化!
回顾:
1.关于nand硬件相关的接口
8根数据线:命令,地址,数据
CLE
ALE
WE
RE
CE
R/B
WP
2.学会看nand操作的时序图
如何发命令
如何发地址
如何读写数据
READ ID,READ数据
3.通过uboot的mw,md来操作nand
4.一定要掌握uboot如何烧写nand
5.在linux系统中如何使用nand
cat /proc/mtd:查看nand的分区信息
ls /dev/mtd* -lh:查看nand每一个分区的对应设备节点
块设备节点:/dev/mtdblock0,/dev/mtdblock1...
字符设备节点:/dev/mtd0,/dev/mtd1...
如何使用两个节点?
块设备节点:
mount -t yaffs2 /dev/mtdblock1 /mnt
以后mnt就是第二块分区的入口,即可
在mnt中实现文件的操作
umount /mnt
字符设备节点:
flash_eraseall /dev/mtd1
nandwrite -p /dev/mtd1 zImage
nanddump
案例:制作系统安装软件
目标:能够自动化安装uboot,zImage,rootfs.cramfs,
userdata.img
提示:initramfs+三个工具+启动脚本
6.nand底层驱动的实现
围绕着nand_chip结构:
select_chip:操作片选
cmd_ctrl:实现发送命令和地址
read_byte:读
write_buf:写
dev_ready:判断nand的状态
以上函数都需要底层驱动开发员来实现!
问:怎么实现?
答:由于nand和CPU之间的通信接口存在差异性,
比如有的CPU访问nand通过寄存器,有的CPU访问
nand通过GPIO,最终导致发送命令,地址,数据
的方法不一样,所以需要根据硬件的操作特性
来实现以上的方法!
7.如何实现一个nandflash的驱动呢?
1.分配nand_chip
2.分配mtd_info
3.关联nand_chip和mtd_info
4.初始化nand_chip
5.nand_scan_ident
6.nand_scan_tail
7.添加分区表:会分区!
8.硬件相关的初始化
-----------------------------------------------
实验:
1.去除官方的nand驱动
进入内核源码执行:
make menuconfig
Device Drivers --->
<*> Memory Technology Device (MTD) support --->
<*> NAND Device Support --->
<*> NAND Flash support for S3C SoC //去掉
make zImage
用新内核重启系统
2.编译驱动
-----------------------------------------------
DM9000网卡硬件特性:
DATA0~DATA15:16根数据线
CMD:当CMD=0,数据线上传输的是DM9000片内寄存器地址
当CMD=1,数据线上传输的是DM9000片内寄存器地址中的数据
CMD接到CPU的地址线ADDR2上!
CS:片选信号,低电平有效,通过S5PV210的MEMORY MAP
得到DM9000网卡的基地址0x88000000,并且对应的
地址线ADDR2=CMD,所以得到两个地址:
CMD = 0 => 0x88000000
CMD = 1 => 0x88000000 + 4
访问这两个地址都会将CS信号拉低,使能DM9000
INT:外部中断,连接到CPU的XEINT10
产生中断的原因:
1.当DM9000网卡接收到外部的数据时,产生
中断通知CPU
2.当DM9000发送完毕一包数据以后,通知
CPU可以继续发包
3.数据链路发生改变时,产生中断
EECS:当EECS为低电平,采用的是16位模式
当EECS为高电平,采用的是8位模式
EECK:当EECK为低电平,采用的是高电平触发
当EECK为高电平,采用的是低电平触发
DM9000如何实现发送和接收数据包?
DM9000内部集成了16K的RAM,
前3K用于发送缓冲区
后13K用于接收数据缓冲区
DM9000发包过程:
1.将第一个数据包放在TX SRAM中
2.将包的长度告诉DM9000
3.设置DM9000的发送控制器寄存器,启动DM9000硬件
发包
4.将第二包数据放入TX SRAM中
5.等第一包发送完毕以后,将第二包的长度
告诉DM9000
6.启动第二包的硬件发送
...
DM9000接包过程:
1.如果外部来数据,DM9000硬件将外部的数据
接收到RX SRAM
2.接收数据包以后,产生中断,告诉CPU有数据包到来
3.CPU开始读取数据包,首先判断数据包的有效性
如果数据包有问题,直接丢弃
4.CPU首先读取数据包的前4字节数据头:
数据包有效表示:如果为0表示数据包无效,如果为1表示数据包正常
数据包的状态,CRC错误状态等
数据包长度低字节,
数据包长度高字节
5.如果数据包正常,然后CPU从DM9000的RX SRAM中
将数据包读到主存中!
-----------------------------------------------
linux内核网卡驱动框架:
app:socket,sendto,recvfrom
-------------------------------------
-----
----- 网络协议层
-----
-----
--------------------------------------
网卡驱动
1.将协议层的数据包发送出去
协议层调用驱动提供的ndo_start_xmit
函数完成最终的发包过程,ndo_start_xmit
需要底层驱动根据硬件操作来实现!
2.将DM9000网卡接收的数据包递交给协议层
协议层给驱动提供了一个netif_rx函数
实现将数据包提交给协议层的方法,这个
函数由协议层实现。
--------------------------------------
DM9000网卡硬件
linux内核如何实现一个网卡驱动?
内核用struct net_device来描述一个网卡设备!
1.分配net_device
struct net_device *myndev = alloc_etherdev( 0 );
2.初始化net_device
3.调用register_netdev向协议层注册网卡设备,
一旦完成net_device的注册,协议层就可以使用
网卡完成数据的交互
4.硬件相关的初始化过程!
案例:内核虚拟网卡设备驱动。
测试:
insmod vndcard.ko
ifconfig vndcard up //打开网卡
ifconfig vndcard 3.3.3.3 //配置IP
ping 3.3.3.3 //ping 自己
ping 3.3.3.4 //ping 别人
ifconfig vndcard down //关闭网卡
DM9000网卡驱动分析:drivers/net/dm9000.c
dm9000_probe:
struct net_device *ndev;
//分配net_device,表明要指定了一个网卡设备
ndev = alloc_etherdev(0);
//地址映射
io_addr = ioremap(0x88000000, iosize);
io_data = ioremap(0x88000004, iosize);
//初始化net_device的base_addr,网卡的基地址
ndev->base_addr = db->io_addr;
//初始化net_device的中断号
ndev->irq = IRQ_EINT10;
//复位dm9000
dm9000_reset(db);
//初始化net_device通用的字段信息
ether_setup(ndev);
//初始化网卡底层操作集合,重点是发包函数:ndo_start_xmit
ndev->netdev_ops = &dm9000_netdev_ops;
dm9000_netdev_ops:
//ifconfig eth0 up
.ndo_open = dm9000_open,
//ifconfig eth0 down
.ndo_stop = dm9000_stop,
//ping,sendto底层发包函数
.ndo_start_xmit= dm9000_start_xmit,
//向协议层注册net_device,最终添加
一个真实的网卡设备
register_netdev(ndev);
问:中断处理函数在哪里注册的?
//当执行ifconfig eth0 up打开网卡设备时,调用
dm9000_open:
//注册中断处理函数
request_irq(dev->irq, dm9000_interrupt, irqflags, dev->name, dev)
//复位初始化DM9000
dm9000_reset(db);
dm9000_init_dm9000(dev);
//通知协议层,网卡设备准备就绪!
netif_start_queue(dev);
//当执行ifconfig eth0 down关闭网卡
dm9000_stop:
//通知协议层网卡设备即将关闭,停止
发送数据
netif_stop_queue(ndev);
//释放中断
free_irq(ndev->irq, ndev);
//关闭网卡硬件
dm9000_shutdown(ndev);
//当协议层要将一个网络数据包通过DM9000发送出去,
协议层必须调用驱动注册的ndo_start_xmit函数:
dm9000_start_xmit:
1.保证TX SRAM中只有两个数据包
发包计数++;
如果已经有两个包,通知协议层,停止发送队列
2.将第一包数据写入到TX SRAM中
3.将第一包的数据长度告诉DM9000
4.启动DM9000硬件发送
5.dev_kfree_skb(skb);//通知协议层,释放缓冲区
6.第二包写入TX SRAM中
7.发包计数++
8.调用netif_stop_queue(dev);通知协议层停止发送队列
问:什么时候发送第二包?什么时候发送第三包?
答:在中断处理函数,因为DM9000发送完数据包以后,
产生中断,告诉CPU发送数据包结束
中断产生的原因:
1.接收数据包产生中断
2.发送数据包产生中断
dm9000_interrupt:
1.判断是接收中断还是发送中断
dm9000_tx_done(dev, db)//发送中断的
执行函数
dm9000_rx接收中断执行函数
dm9000_tx_done:
发包计数--;
如果还有一包数据,启动第二包的数据传输
调用netif_wake_up通知协议层可以发送第三包
总结:对于发包过程,一定要将发包函数dm9000_start_xmit
和中断处理函数结合来看!
DM9000接收数据包:
1.当DM9000硬件接收到数据包以后,产生中断,
内核调用对应的中断处理函数:
dm9000_interrupt:
//通过读取DM9000内部的中断状态寄存器,
判断是发送中断还是接收中断
if (int_status & ISR_PRS)
dm9000_rx(dev);//接收中断
dm9000_rx:
struct sk_buff *skb;//接收缓冲区
1.读取RX SRAM中的第一个字节
判断包的有效性,如果第一字节非1,
直接丢弃(从中断处理函数返回)
如果数据包有效,继续读取前4字节;
2.要判断包的长度是否有效
3.判断状态
4.从RX SRAM读取数据包到主存中
skb = dev_alloc_skb(RxLen);//分配接收数据包的缓冲区
skb_reserve(skb, 2); //ip头对齐
rdptr = (u8 *) skb_put(skb, RxLen - 4);
//让rdptr执行缓冲区
5.将RX SRAM中的数据读到分配的缓冲区中
6.调用eth_type_trans(skb, dev)完成对skb缓冲的信息
进行填充修饰
7.最终调用协议层提供的netif_rx(skb);将读到的
数据提交给协议层!
-----------------------------------------------
linux内核OOPS问题的解决方法:
1.动态加载模块引起的oops问题
~ # ./led_test
led_write
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = f34e4000
[00000000] *pgd=53545031, *pte=00000000, *ppte=00000000
Internal error: Oops: 817 [#1] PREEMPT
last sysfs file: /sys/devices/virtual/mtd/mtd3/mtdblock3/dev
//引起问题的模块可能是
Modules linked in: led_drv virtualnetdevice nand_drv [last unloaded: nand_drv]
CPU: 0 Not tainted (2.6.35.7-Concenwit #7)
//引起内核崩溃的代码在哪里
在函数led_read中引起的,在模块led_drv中
PC is at led_read+0x50/0x6c [led_drv]
LR is at 0x1
内核崩溃各个寄存器的信息
pc : [<bf012200>] lr : [<00000001>] psr: 40000013
sp : f34ddf28 ip : 0000001c fp : f34ddf3c
r10: 00000000 r9 : f34dc000 r8 : 00000004
r7 : f34ddf70 r6 : be934d68 r5 : f353dc80 r4 : 00000004
r3 : 00000000 r2 : 00000000 r1 : bf012228 r0 : bf012243
Flags: nZcv IRQs on FIQs on Mode SVC_32 ISA ARM Segment user
Control: 10c5387d Table: 534e4019 DAC: 00000015
SP: 0xf34ddea8:
dea8 ffffff9c f34ddedc 00000000 00000000 c008761c f34a4000 ffffffff f34ddf14
dec8 be934d68 f34ddf70 f34ddf3c f34ddee0 c0037a6c c00372ac bf012243 bf012228
dee8 00000000 00000000 00000004 f353dc80 be934d68 f34ddf70 00000004 f34dc000
df08 00000000 f34ddf3c 0000001c f34ddf28 00000001 bf012200 40000013 ffffffff
df28 00000004 f353dc80 f34ddf6c f34ddf40 c00ee6d0 bf0121bc 00000000 00000000
df48 00000020 f353dc80 be934d68 00000000 00000000 00000004 f34ddfa4 f34ddf70
df68 c00ee840 c00ee628 00000000 00000000 00000005 00000000 f34dc000 00000000
df88 00000000 00000000 00000003 c0038168 00000000 f34ddfa8 c0037fc0 c00ee808
FP: 0xf34ddebc:
debc f34a4000 ffffffff f34ddf14 be934d68 f34ddf70 f34ddf3c f34ddee0 c0037a6c
dedc c00372ac bf012243 bf012228 00000000 00000000 00000004 f353dc80 be934d68
defc f34ddf70 00000004 f34dc000 00000000 f34ddf3c 0000001c f34ddf28 00000001
df1c bf012200 40000013 ffffffff 00000004 f353dc80 f34ddf6c f34ddf40 c00ee6d0
df3c bf0121bc 00000000 00000000 00000020 f353dc80 be934d68 00000000 00000000
df5c 00000004 f34ddfa4 f34ddf70 c00ee840 c00ee628 00000000 00000000 00000005
df7c 00000000 f34dc000 00000000 00000000 00000000 00000003 c0038168 00000000
df9c f34ddfa8 c0037fc0 c00ee808 00000000 00000000 00000003 be934d68 00000004
R5: 0xf353dc00:
dc00 f353d500 c00ef07c 00000000 00000000 00000000 00000000 00000000 00000000
dc20 00000000 00000000 00000000 00000000 00000000 00000000 00000000 f353d800
dc40 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
dc60 00000000 00000000 00000000 00000000 f353dc70 f353dc70 00000000 00000000
dc80 f349ec74 f349ec74 f34b1a80 f3aebc00 bf01226c 00000001 00000002 0000001f
dca0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 f353d800
dcc0 00000000 00000000 00000000 00000000 00000000 00000000 ffffffff ffffffff
dce0 00000000 00000000 00000000 00000000 f353dcf0 f353dcf0 f3531620 00000000
R7: 0xf34ddef0:
def0 00000004 f353dc80 be934d68 f34ddf70 00000004 f34dc000 00000000 f34ddf3c
df10 0000001c f34ddf28 00000001 bf012200 40000013 ffffffff 00000004 f353dc80
df30 f34ddf6c f34ddf40 c00ee6d0 bf0121bc 00000000 00000000 00000020 f353dc80
df50 be934d68 00000000 00000000 00000004 f34ddfa4 f34ddf70 c00ee840 c00ee628
df70 00000000 00000000 00000005 00000000 f34dc000 00000000 00000000 00000000
df90 00000003 c0038168 00000000 f34ddfa8 c0037fc0 c00ee808 00000000 00000000
dfb0 00000003 be934d68 00000004 be934d68 00000000 00000000 00000000 00000003
dfd0 00000000 00000000 40024000 be934d74 00000000 be934d58 0000854c 400da6cc
R9: 0xf34dbf80:
bf80 f348b684 00000000 f3dbee20 f34b1b90 c05de350 c005b9bc 00000000 f34dddb0
bfa0 f34dbfcc c030f258 f34b1b80 00000000 00000000 00000000 f34dbff4 f34dbfc8
bfc0 c0081ff8 c030f264 00000000 00000000 f34dbfd0 f34dbfd0 f34dddb0 c0081f74
bfe0 c0069ba0 00000013 00000000 f34dbff8 c0069ba0 c0081f80 21106021 21106421
c000 00000000 00000002 00000000 f34ccf00 c0845c58 00000000 00000017 f34dc000
c020 f34ccf00 c0845790 c08394a0 00000000 f349f480 c0852b7c f34ddc4c f34ddc20
c040 c05de4e0 00000000 00000000 00000000 00000000 00000000 01000000 00000000
c060 4001d100 00000000 00000000 00000000 00000000 00000000 00000000 00000000
引起内核崩溃的进程
Process led_test (pid: 123, stack limit = 0xf34dc2f0)
Stack: (0xf34ddf28 to 0xf34de000)
df20: 00000004 f353dc80 f34ddf6c f34ddf40 c00ee6d0 bf0121bc
df40: 00000000 00000000 00000020 f353dc80 be934d68 00000000 00000000 00000004
df60: f34ddfa4 f34ddf70 c00ee840 c00ee628 00000000 00000000 00000005 00000000
df80: f34dc000 00000000 00000000 00000000 00000003 c0038168 00000000 f34ddfa8
dfa0: c0037fc0 c00ee808 00000000 00000000 00000003 be934d68 00000004 be934d68
dfc0: 00000000 00000000 00000000 00000003 00000000 00000000 40024000 be934d74
dfe0: 00000000 be934d58 0000854c 400da6cc 60000010 00000003 00000000 00000000
回溯信息,从下往上看
Backtrace:
[<bf0121b0>] (led_read+0x0/0x6c [led_drv]) from [<c00ee6d0>] (vfs_read+0xb4/0x160)
r5:f353dc80 r4:00000004
[<c00ee61c>] (vfs_read+0x0/0x160) from [<c00ee840>] (sys_read+0x44/0x70)
r8:00000004 r7:00000000 r6:00000000 r5:be934d68 r4:f353dc80
[<c00ee7fc>] (sys_read+0x0/0x70) from [<c0037fc0>] (ret_fast_syscall+0x0/0x30)
r8:c0038168 r7:00000003 r6:00000000 r5:00000000 r4:00000000
Code: eb497ec2 e3a03000 e59f1014 e59f0014 (e5833000)
Segmentation fault
但是很多情况,内核崩溃时,仅仅告诉你pc指针的内容,如:
pc : [<bf012200>] ,这种问题怎么解决呢?
解决方法如下:
1.在开发板执行
cat /proc/kallsyms > /symbol.txt
获取内核崩溃时所有的符号表信息,保存文件中
2.打开symbol.txt文件,在文件中找到跟pc指针最接近
的一个地址信息:
bf0121b0 t led_read [led_drv]
这样就确定了出现问题的模块是led_drv.ko
出现问题的函数有可能是led_read函数
3.反汇编led_drv.ko问题模块
arm-linux-objdump -D led_drv.ko > led_drv.dis
4.打开反汇编文件led_drv.dis,找到led_read函数的
地址信息:1b0 <led_read>,而led_read函数在
symbol.txt中的地址为bf0121b0,现在由已知pc在
symbol.txt中的地址为bf012200,那么pc在汇编文件
中的地址应该200(bf012200 - (bf0121b0 - 1b0))
5.查看汇编文件中的200地址出的代码:
200: e5833000 str r3, [r3]
6.查看内核崩溃时各个寄存器的信息,获取r3当时的
内容:r3 : 00000000
7.查阅led_read函数,看哪个地方对0地址进行访问!
2.静态编译驱动到内核引起的oops问题
1.将led_drv.c 拷贝到内核源码的drivers/char/
2.打开内核源码的drivers/char/Makefile
3.添加:
obj-y += led_drv.o
4.make zImage
~ # ./led_test
led_write
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = f34d8000
[00000000] *pgd=534b6031, *pte=00000000, *ppte=00000000
Internal error: Oops: 817 [#1] PREEMPT
last sysfs file: /sys/devices/virtual/video4linux/video21/dev
Modules linked in:
CPU: 0 Not tainted (2.6.35.7-Concenwit #8)
PC is at led_read+0x50/0x6c
LR is at 0x1
pc : [<c02ac224>] lr : [<00000001>] psr: 40000013
sp : f34e1f28 ip : 0000001c fp : f34e1f3c
r10: 00000000 r9 : f34e0000 r8 : 00000004
r7 : f34e1f70 r6 : be844d68 r5 : f34aa700 r4 : 00000004
r3 : 00000000 r2 : 00000000 r1 : c0641528 r0 : c079caaa
Flags: nZcv IRQs on FIQs on Mode SVC_32 ISA ARM Segment user
Control: 10c5387d Table: 534d8019 DAC: 00000015
PC: 0xc02ac1a4:
c1a4 e51b2018 e59f3018 e59f1018 e59f0018 e5832040 eb0cc843 e1a00004 e24bd014
c1c4 e89da830 c08eb1fc c064151c c079caaa e1a0c00d e92dd830 e24cb004 e1a0000d
c1e4 e1a04002 e3c03d7f e3c3303f e5933008 e2912004 30d22003 33a03000 e3530000
c204 1a000003 e1a00001 e3a02004 e59f101c ebff16b9 e3a03000 e59f1014 e59f0014
c224 e5833000 eb0cc827 e1a00004 e89da830 c08eb23c c0641528 c079caaa e1a0c00d
c244 e92dd830 e24cb004 e59f4044 e3a00020 e3a01000 e4945004 ebff7574 e3a01000
c264 e3a00021 ebff7571 e3a00020 ebff7b3d e3a00021 ebff7b3b e1a05a05 e1a00004
c284 ebf914f6 e1a00005 e3a01001 ebf91546 e89da830 c08eb1fc e1a0c00d e92dd800
SP: 0xf34e1ea8:
1ea8 ffffff9c f34e1edc 00000000 00000000 c008761c f34c1000 ffffffff f34e1f14
1ec8 be844d68 f34e1f70 f34e1f3c f34e1ee0 c0037a6c c00372ac c079caaa c0641528
1ee8 00000000 00000000 00000004 f34aa700 be844d68 f34e1f70 00000004 f34e0000
1f08 00000000 f34e1f3c 0000001c f34e1f28 00000001 c02ac224 40000013 ffffffff
1f28 00000004 f34aa700 f34e1f6c f34e1f40 c00ee6d0 c02ac1e0 00000000 00000000
1f48 00000020 f34aa700 be844d68 00000000 00000000 00000004 f34e1fa4 f34e1f70
1f68 c00ee840 c00ee628 00000000 00000000 00000005 00000000 f34e0000 00000000
1f88 00000000 00000000 00000003 c0038168 00000000 f34e1fa8 c0037fc0 c00ee808
FP: 0xf34e1ebc:
1ebc f34c1000 ffffffff f34e1f14 be844d68 f34e1f70 f34e1f3c f34e1ee0 c0037a6c
1edc c00372ac c079caaa c0641528 00000000 00000000 00000004 f34aa700 be844d68
1efc f34e1f70 00000004 f34e0000 00000000 f34e1f3c 0000001c f34e1f28 00000001
1f1c c02ac224 40000013 ffffffff 00000004 f34aa700 f34e1f6c f34e1f40 c00ee6d0
1f3c c02ac1e0 00000000 00000000 00000020 f34aa700 be844d68 00000000 00000000
1f5c 00000004 f34e1fa4 f34e1f70 c00ee840 c00ee628 00000000 00000000 00000005
1f7c 00000000 f34e0000 00000000 00000000 00000000 00000003 c0038168 00000000
1f9c f34e1fa8 c0037fc0 c00ee808 00000000 00000000 00000003 be844d68 00000004
R0: 0xc079ca2a:
ca28 6f6e6920 25206564 6c253a73 49000a75 65646f6e 6361632d 26006568 646f6e69
ca48 693e2d65 69746f6e 6d5f7966 78657475 6e692600 2d65646f 6d5f693e 78657475
ca68 6e692600 2d65646f 615f693e 636f6c6c 6d65735f 2f736600 72747461 3c00632e
ca88 6c613e34 5f636f6c 203a6466 746f6c73 20642520 20746f6e 4c4c554e 25000a21
caa8 73250973 7366000a 6c69662f 73797365 736d6574 2500632e 00732a2e 69766564
cac8 00206563 64206f6e 63697665 6d200065 746e756f 6f206465 7700206e 20687469
cae8 79747366 00206570 25206925 75252069 2075253a 77722000 6f722000 68732000
cb08 64657261 0069253a 73616d20 3a726574 20006925 706f7270 74616761 72665f65
cb28 253a6d6f 75200069 6e69626e 6c626164 30200065 000a3020 6d616e26 61707365
R1: 0xc06414a8:
14a8 5f746573 7369646c 00000063 00000000 c010b000 c010b11c 00000000 00000000
14c8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 c02a9b70
14e8 00000000 c010a798 00000000 00000000 00000000 00000000 00000000 00000000
1508 00000000 00000000 00000000 00000000 00000000 5f64656c 74697277 00000065
1528 5f64656c 64616572 00000000 00000000 c02ac740 00000000 c02ac2dc c02ac5fc
1548 00000000 00000000 c02ac5a0 00000000 00000000 c02ac29c c02ac2c8 c02ac394
1568 00000000 c02ac364 00000000 c02ac56c 00000000 00000000 00000000 00000000
1588 c02ac4d4 00000000 00000000 00000000 00000000 00000000 c02ace2c 00000000
R5: 0xf34aa680:
a680 00000000 c00ef07c 00000000 00000000 00000000 00000000 00000000 00000000
a6a0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 f34aa000
a6c0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
a6e0 00000000 00000000 00000000 00000000 f34aa6f0 f34aa6f0 00000000 00000000
a700 f349ec74 f349ec74 f34a8c00 f3ae6180 c0861a78 00000001 00000002 0000001f
a720 00000000 00000000 00000000 00000000 00000000 00000000 00000000 f34aa000
a740 00000000 00000000 00000000 00000000 00000000 00000000 ffffffff ffffffff
a760 00000000 00000000 00000000 00000000 f34aa770 f34aa770 f352c2a0 00000000
R7: 0xf34e1ef0:
1ef0 00000004 f34aa700 be844d68 f34e1f70 00000004 f34e0000 00000000 f34e1f3c
1f10 0000001c f34e1f28 00000001 c02ac224 40000013 ffffffff 00000004 f34aa700
1f30 f34e1f6c f34e1f40 c00ee6d0 c02ac1e0 00000000 00000000 00000020 f34aa700
1f50 be844d68 00000000 00000000 00000004 f34e1fa4 f34e1f70 c00ee840 c00ee628
1f70 00000000 00000000 00000005 00000000 f34e0000 00000000 00000000 00000000
1f90 00000003 c0038168 00000000 f34e1fa8 c0037fc0 c00ee808 00000000 00000000
1fb0 00000003 be844d68 00000004 be844d68 00000000 00000000 00000000 00000003
1fd0 00000000 00000000 40024000 be844d74 00000000 be844d58 0000854c 400da6cc
R9: 0xf34dff80:
ff80 00000000 401e5774 f34dffa4 f34dff98 c006a394 c006a2a0 00000000 f34dffa8
ffa0 c0037fc0 c006a388 00000000 401e5774 00000000 00000000 00000008 401278e4
ffc0 00000000 401e5774 401e7178 000000f8 401e7178 00000000 beb73ea4 00000000
ffe0 00000000 beb73d10 400edc6c 401530d8 60000010 00000000 00000000 00000000
0000 00000000 00000002 00000000 f348ac00 c0845c58 00000000 00000017 f34e0000
0020 f348ac00 c0845790 c08394a0 00000000 f349f480 c0852b7c f34e1c4c f34e1c20
0040 c05de700 00000000 00000000 00000000 00000000 00000000 01000000 00000000
0060 4001d100 00000000 00000000 00000000 00000000 00000000 00000000 00000000
Process led_test (pid: 65, stack limit = 0xf34e02f0)
Stack: (0xf34e1f28 to 0xf34e2000)
1f20: 00000004 f34aa700 f34e1f6c f34e1f40 c00ee6d0 c02ac1e0
1f40: 00000000 00000000 00000020 f34aa700 be844d68 00000000 00000000 00000004
1f60: f34e1fa4 f34e1f70 c00ee840 c00ee628 00000000 00000000 00000005 00000000
1f80: f34e0000 00000000 00000000 00000000 00000003 c0038168 00000000 f34e1fa8
1fa0: c0037fc0 c00ee808 00000000 00000000 00000003 be844d68 00000004 be844d68
1fc0: 00000000 00000000 00000000 00000003 00000000 00000000 40024000 be844d74
1fe0: 00000000 be844d58 0000854c 400da6cc 60000010 00000003 00000000 00000000
Backtrace:
[<c02ac1d4>] (led_read+0x0/0x6c) from [<c00ee6d0>] (vfs_read+0xb4/0x160)
r5:f34aa700 r4:00000004
[<c00ee61c>] (vfs_read+0x0/0x160) from [<c00ee840>] (sys_read+0x44/0x70)
r8:00000004 r7:00000000 r6:00000000 r5:be844d68 r4:f34aa700
[<c00ee7fc>] (sys_read+0x0/0x70) from [<c0037fc0>] (ret_fast_syscall+0x0/0x30)
r8:c0038168 r7:00000003 r6:00000000 r5:00000000 r4:00000000
Code: ebff16b9 e3a03000 e59f1014 e59f0014 (e5833000)
---[ end trace 8df67af93553409e ]---
Segmentation fault
如果仅仅告诉PC指针的内容,如何解决?
pc : [<c02ac224>]
步骤如下:
1.反汇编内核原始镜像vmlinux
arm-linux-objdump -D vmlinux > vmlinux.dis
2.打开反汇编文件,直接搜索pc指针对应的地址
c02ac224: e5833000 str r3, [r3]
看看这句所在函数:c02ac1d4 <led_read>:
3.查看内核崩溃r3寄存器的内容
4.查看led_read函数找到问题所在