资讯详情

驱动笔记

1.系统开发相关内容 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:           标签: a2传感器二线交流传感器d10轴位移传感器lh系列高精度位移传感器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台