day05

来源:互联网 发布:电动牙刷推荐 知乎 编辑:程序博客网 时间:2024/05/17 06:19
回顾:
1.linux内核字符设备涉及其他两个数据结构
  struct inode
          .i_rdev  设备号
                           驱动根据i_rdev获取次设备号,通过次设备号区分硬件个体
  struct file
  file和inode也存在一定的关系:fbmem.c
 
2.linux内核字符设备文件的自动创建
    三个保证+四个函数
 
3.linux内核混杂设备驱动编程
  本质还是字符设备
  主设备号由内核定义好为10
  通过次设备号区分
  struct miscdevice
          .minor = MISC_DYNAMIC_MINOR,让内核帮你分配次设备号
          .name = 内核自动帮你创建的设备文件名
          .fops = 混杂设备的硬件操作接口
  配套函数
  misc_regsiter
  misc_deregister
                  
4.linux内核中断编程
    面试题:谈谈对中断的理解
    4.1.首先谈谈计算机为什么有中断机制
        中断产生的根本原因就是因为外设的数据处理速度
        远远慢于CPU
        务必举例子说明,以CPU读取UART接收缓冲区的数据为例谈谈
        当CPU读取UART接收缓冲区的数据,发现UART接收缓冲区的数据
        没有准备就绪,首先想到轮训方式,也就是CPU不做其他任何事情
        原地死等直到UART接收缓冲区的数据准备就绪,这样会造成CPU的
        利用率降低,耗费大量的CPU资源,立马想到轮训的死对头
        中断方式,CPU采用中断方式获取UART接收缓冲区的数据流程如下:
        当CPU读取UART接收缓冲区的数据,发现UART接收缓冲区没有准备好
        数据,那么CPU可以干别的事情(处理其他进程,其他进程获取CPU资源)
        一旦将来UART接收缓冲区数据准备就绪,UART控制器最终
        会给CPU发送一个中断电信号(类似,嗨,我这里准备好数据了)
        CPU一旦接收到这个中断电信号,CPU立马停止手头的工作(处理进程)
        转去处理UART接收缓冲区的数据,一旦处理完毕,CPU还会
        回到原先被打断的位置继续执行(继续执行原先处理的那个进程)
        此时此刻,CPU至少两件事,大大提高了CPU的利用率
      
     4.2.中断的硬件触发的流程
         重点要突出中断控制器
         务必举例子说明,以按键为例,谈谈
         首先画出一个按键的简要硬件连接示意图,key.bmp
         然后看图阐述按键中断的触发流程
         1.当按键按下,产生一个下降沿电信号
           此电信号首先跑到中断控制器
         2.中断控制器拿到此下降沿电信号以后,开始进行
           一番的判断
         3.中断控制器首先判断GPIOA28此引脚中断功能
           是否使能,如果禁止,直接丢弃,如果使能
           中断控制器继续判断
         4.接下来中断控制器判断此下降沿电信号是否是有效的
           中断触发电信号(五种),如果无效,直接丢弃
           如果为有效的中断触发信号,继续判断
         5.接下来中断控制器判断当前CPU是否有正在处理的
           高优先级中断,如果有,直接丢弃,如果没有,
           中断控制器继续判断
         6.接下来中断控制器判断此中断信号到底给哪个
           CPU核发送中断信号,是给CPU0啊,还是给全部CPU核发
         7.最后中断控制器还要判断此中断信号到底以哪种
           方式发送给CPU核,以IRQ形式?还是以FIQ形式
           比如这里我们选择IRQ形式发送
           最后CPU核就会接收到按键发送过来的中断电信号
         8.CPU核一旦接收到了按键发送的IRQ中断信号
           CPU核立马触发一个IRQ中断异常,CPU核立马
           要处理IRQ中断异常:
           CPU核硬件上自动完成:
               备份CPSR到SPSR_IRQ
               设置CPSR
                       MODE
                       T
                       I
                     F
               保存返回地址LR_IRQ=PC-4
               设置PC=0x18,让CPU核跑到0x18地址去继续运行
               至此开启了软件进一步处理IRQ中断异常的流程
               
          软件处理IRQ中断异常的流程:
          1.首先要建立一个异常向量表出来
            异常向量表就是在ARM核7种异常的处理入口地址(0x00/0x04/0x08/0x0c/0x10/0x18/0x1c)
            放置自己的代码,每当异常发生,CPU核都会去处理对应的代码
            如果可以,直接写一个异常向量表的代码:
            b   reset   //0x00
            ldr pc _undef_func //0x04
            ldr pc _swi_func //0x08
            ldr pc _pre_instr_func //0x0c
            ldr pc _data_func //0x10
            b .  //0x14 保留
            ldr pc _irq //0x18
            ldr pc _fiq //0x1c
           
          2.CPU核一旦跑到0x18地址运行,软件上做:
            1.保护现场,保护CPU原先处理的任务的现场
              就是做一个压栈处理
            2.根据用户需求完成IRQ中断的处理
              例如打印一句话
            3.恢复现场,恢复到原先被打断的任务的现场
              就是做一个出栈处理
              至此真个IRQ中断处理完毕
             
       4.3.接下来继续谈谈中断的软件编程
               中断的软件编程只需写四部分代码即可:
               1.编写异常向量表的代码
               2.编写保护现场的代码
               3.根据用户需求完成对中断的处理
                 本质就是编写一个中断处理函数
                 中断处理函数中做什么事由用户需求来定
               4.编写恢复现场的代码
               总结:不管是ARM裸板编程还是基于linux系统的中断编程
               程序员只需完成第3步即可,其余124都是由ARM公司
               或者芯片厂家完成!
               此时此刻务必画出一个中断的处理流程图,参见irq.bmp    
               
               中断服务程序=中断服务历程=中断处理函数          
           
          问:在linux系统中,如何软件编程实现第三步呢?
          答:利用大名鼎鼎的以下两个函数即可完成第三步
 
5.linux内核中断编程的操作步骤
  5.1.再次明确,中断编程涉及的代码有四部分
      异常向量表
      保护现场
      根据用户需求完成中断处理函数
      恢复现场
      但是在linux系统,程序员只需关注第三步中断处理函数的完成即可
  5.2.明确由于异常向量表定义在内核中,所以中断的软件处理都是在内核中完成
      也就是中断处理函数位于内核空间    
  5.3.驱动开发者只需利用以下两个函数向内核注册或者删除
      硬件中断的中断处理函数即可,一旦注册成功,将来硬件
      中断触发,内核就会自动调用注册的中断处理函数
      int request_irq(unsigned int irq,
                              irq_handler_t handler,
                              unsigned long flags,
                              const char *name,
                              void *dev)              
        函数功能:由于CPU的硬件中断(GPIO)对于内核来说
                            都是一种宝贵的资源,所以驱动要想使用
                            某个硬件中断,首先向内核申请硬件中断资源,
                            然后向内核注册这个硬件中断对应的中断处理函数  
                            一旦完成注册,将来这个硬件中断触发,内核就会
                            调用注册的中断处理函数
        参数:
        irq:linux内核给每一个CPU的硬件中断(GPIO)都分配指定一个
            软件编号,此软件编号又称中断号(类似硬件中断的身份证号)
            硬件GPIO     GPIO编号       中断号
            GPIOA28    PAD_GPIO_A+28    gpio_to_irq(PAD_GPIO_A+28)
        handler: 传递要注册的中断处理函数
                         中断处理函数的原型
                         irqreturn_t (*irq_handler_t)(int irq, void *dev)
                         irq:就是当前触发的硬件中断对应的中断号
                         dev:就是保存给中断处理函数传递的参数
                             也就是request_irq的第五个参数
                              
                         返回值:
                         IRQ_NONE:中断处理函数执行失败
                         IRQ_HANDLED:中断处理函数执行成功
                          
                         例如:
                         irqreturn_t button_isr(int irq, void *dev)
                         {
                               //根据用户需求完成相关内容
                               //比如开个灯
                                 return IRQ_NONE;//失败
                                 或者
                                 return IRQ_HANDLED; //成功
                         }
                          
          flags:中断标志    
                      外部中断:就是肉眼在硬件原理图看能够看到的硬件中断连接线
                                         这个硬件中断连接线对应的中断称之为外部中断
                      内部中断:就是处理器内部各种硬件控制器对应的硬件中断线(肉眼看不到)
                               比如UART控制器和中断控制器之间有一根硬件中断线
                                                   
                      如果是外部中断,flags需要指定有效的硬件中断触发方式
                      IRQF_TRIGGER_FALLING:下降沿触发
                      IRQF_TRIGGER_RISING:上升沿触发
                      IRQF_TRIGGER_HIGH:高电平触发
                      IRQF_TRIGGER_LOW:低电平触发
                      IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING:双边沿
                              
                      如果是内部中断,flags直接给0                       
         
        name:中断名称,将来中断申请注册完毕,通过执行
               cat /proc/interrupts查看这个name
               查看中断是否注册成功
                         
        dev:给中断处理函数传递的参数
             如果不想传递参数直接给NULL
                 回忆线程的玩法
                  
                 void *thread_func(void *    arg)
                 {
                      //arg保存创建现场时传递的参数
                      //arg = &data
                      printf("%#x\n", *(int *)arg);
                 }
                  
                 int data = 0x55;
                 pthread_create(&tid, NULL,  
                                             thread_func, &data);         
       
        结论:一旦中断申请成功并且中断处理函数注册成功
              将来静静的等待硬件中断的触发,一旦触发
              内核就会调用等待中的中断处理函数
               
        void free_irq(int irq, void *dev)
        函数功能:释放中断资源并且删除中断处理函数
        irq:指定要释放的硬件中断资源
        dev:给中断处理函数传递的参数,切记:注册中断处理函数时
            传递什么参数,在释放的时候务必传递同样的参数
            否则导致内核崩溃!
         
        案例:编写按键中断驱动,实现按键按下或者松开
              能够打印按键的操作状态和按键值
        实施步骤:
        上位机执行:
          //去除官方的按键驱动
          cd /opt/kernel
          make menuconfig  
                  Device Drivers->
                          input device supports->
                                  <*>keyboards->
                                          //按N键去除
                                          <*>...key...
          保存退出
          make uImage
          cp arch/arm/boot/uImage /tftpboot
          重启下位机,用新的uImage启动系统
                                          
            mkdir /opt/drivers/day05/1.0 -p
            cd /opt/drivers/day05/1.0
            vim btn_drv.c
            vim Makefile
            make
            cp btn_drv.ko /opt/rootfs/home/drivers
            
        下位机执行:
         cd /home/drivers
         insmod btn_drv.ko
         cat /proc/interrupts //查看中断是否注册成功
                       CPU0        
                         33:           0       GIC  pl08xdmac
                         34:           0       GIC  pl08xdmac
                         ...
                         134:          0      GPIO  KEY_UP
                        147:          0      GPIO  KEY_DOWN
                        说明:
                        第一列:硬件中断对应的中断号
                        第二列:硬件中断触发的次数,触发一次自动加1
                        第三列:硬件中断的类型
                        第四列:硬件中断的名称,就是request_irq的第四个参数
                        
         操作按键
         观察打印信息
         cat /proc/interrupts //查看中断的触发次数
              
           案例:添加其余按键支持!
 
5.linux内核中断编程之顶半部和底半部机制
  5.1.明确:
  "任务":linux内核中,任务细分三类:
         进程,软中断,硬件中断
         中断:包括软中断和硬件中断
               任务运行的前提是任务必须获取到CPU资源        
  “优先级”:衡量一个任务获取CPU资源的一种能力,优先级越高
           此任务获取CPU资源的能力就越强,它投入运行的时间
           就能够提前
           linux内核任务优先级的划分:
           硬件中断的优先级高于软中断
           软中断的优先级高于进程
           进程之间存在优先级,有高优先级的进程和低优先级的进程
           软中断同样也有优先级之分
           硬件中断无优先级之分
   “休眠”:只是当前进程会释放所占用的CPU资源给其他进程使用
                    中断的世界里没有休眠!
                    中断不参与进程的调度!
                     
  5.2.linux内核对于中断处理函数(button_isr)的要求
  切记:linux内核要求中断处理函数执行的速度越快越好
        更不能进行休眠操作  
        如果中断处理函数长时间占用CPU资源,而本身它的
        优先级最高,那就会导致其他的任务无法及时获取到
        CPU资源没法投入运行,影响了并发和响应能力
         
  问:linux内核中有些硬件的中断处理函数是无法满足
      linux内核的这个要求,如果不能够满足,势必会影响
      linux内核的并发和响应能力,对于这种情况,linux内核
      如何解决呢?
  答:如果有中断处理函数长时间占用CPU资源的情况,使用
      linux内核的顶半部和底半部机制进行优化!
      明确顶半部和底半部的优化过程,参见bh.bmp
          中断编程的顶半部和底半部本质目的就是解决中断
          处理函数长时间占用CPU资源引起影响系统的并发和
          响应能力!
          
  5.3.linux内核中断编程之顶半部特点
  顶半部做原先中断处理函数中比较紧急,耗时较短的内容
  一旦硬件中断触发,CPU首先执行顶半部,执行时间非常快,
  会立即释放CPU资源给其他任务使用,CPU在执行期间不允许
  发生CPU资源的切换,也就是踏踏实实的执行顶半部
  顶半部本质就是中断处理函数,只是现在里面做的内容比较快
   
  5.4.linux内核中断编程之底半部特点
  底半部做原先中断处理函数中比较耗时,不紧急的内容
  CPU会在“适当”的时候去执行底半部(明确:当CPU执行底半部的时候
  底半部的优先级势必是最高滴),但是CPU在执行期间,如果来了
  一个高优先级的任务,势必会抢走底半部的CPU资源,当然高优先级
  的任务执行完毕,还会把CPU资源给底半部,底半部继续被处理
  底半部本质就是延后执行的一种手段而已!
  如果将来单纯的仅仅的让某个事件延后执行,可以利用底半部机制来实现
  底半部实现方法有三种:
  tasklet
  工作队列
  软中断
   
  5.4.底半部实现机制之tasklet
  tasklet特点:tasklet本身基于软中断实现,优先级是高于进程
               而低于硬件中断
               所以tasklet对应的延后处理函数不能进行休眠操作
               tasklet对应的延后处理函数执行原先中断处理函数
               中比较耗时,不紧急的内容
   
  linux内核描述tasklet的数据结构:
  struct tasklet_struct {
        void (*func)(unsigned long data);
        unsigned long data;
      ...
  };
  成员说明:
  func:tasklet的延后处理函数,做原先中断处理函数中不紧急耗时较长的内容
       由于基于软中断实现,所以里面不能进行休眠操作
           形参data就是保存传递过来的参数
  data:给tasklet延后处理函数传递的参数
   
  利用tasklet实现底半部延后执行的编程步骤:
  1.定义初始化一个tasklet对象
    int g_data = 0x55;
    DECLARE_TASKLET(btn_tasklet, //对象名
                                 btn_tasklet_func,//tasklet的延后处理函数
                                 (unsigned long)&g_data);//给延后处理函数传递的参数
    结果:
    btn_tasklet.func = btn_tasklet_func;
    btn_tasklet.data = (unsigned long)&g_data;
   
  2.编写tasklet延后处理函数:btn_tasklet_func
    //data = (unsigned long)&g_data;
    void btn_tasklet_func(unsigned long data)
    {
         //做原先中断处理函数中耗时不紧急的内容
    }                                     
     
  3.在顶半部中断处理函数中向内核"登记"(而不是调用),一旦
    登记完成,内核会在"适当"的时候去调用tasklet的延后处理函数
    登记的函数为:
    tasklet_schedule(&btn_tasklet);
     
  案例:利用tasklet优化按键驱动
  分析:
  原先的按键驱动中断处理函数中,其实它的执行速度已经很快了
  但是还是有进一步优化的空间,原因是由于按键的中断处理函数
  中调用printk函数,而printk函数操作的硬件设备是UART,而UART
  本身就是一个低速设备,当CPU执行printk函数时,执行时间势必
  拉长,多少会影响系统的并发和响应;
  切记切记:printk虽然能够打印调试信息,但是用多了没好处!
            它的执行占用大量的CPU资源
  实施步骤:
  同上           
   
   
  5.5.底半部实现机制之工作队列
  工作队列特点:工作队列诞生的本质就是解决tasklet的延后
                处理函数不能进行休眠操作的问题
                因为有些场合需要在延后的过程中进行休眠操作
                对于这种情况必须使用工作队列
                所以工作队列基于进程实现!
                所以工作队列的延后处理函数可以进行休眠操作
                但是它的优先级低于软中断,低于硬件中断
                工作队列延后处理函数中做耗时较长不紧急的内容
   
  linux内核描述工作队列的数据结构
  struct work_struct {
          work_func_t func;
          ...
  };
  func:工作队列的延后处理函数,里面可以进行休眠操作
   
  配套函数:
  //给工作队列添加一个延后处理函数
  INIT_WORK(&工作队列对象, 工作队列的延后处理函数);
   
  //向内核登记工作队列的延后处理函数,一旦登记完成
  将来内核在适当的时候调用其延后处理函数
  schedule_work(&工作队列对象);
   
  利用工作队列实现延后执行的编程步骤:
  1.定义初始化工作队列对象
    struct work_struct btn_work; //定义对象
    INIT_WORK(&btn_work, btn_work_function);
     
  2.编写工作队列延后处理函数
    此函数可以进行休眠操作
    //work指针就是指向自己定义初始化的对象
    //work=&btn_work
    void btn_work_function(struct work_struct *work)
    {
        //不紧急耗时较长的内容
    }
  3.向内核登记工作队列的延后处理函数,一旦登记完成
    内核在适当的时候调用延后处理函数
    schedule_work(&btn_work);
   
  总结:
  1.如果延后执行的内容中,没有休眠操作,用tasklet或者工作队列都可以
  2.如果延后执行的内容中,有休眠操作,只能用工作队列
  3.如果延后执行的内容中,没有休眠操作,并且考虑效率问题,使用tasklet
     
  案例:利用工作队列优化按键驱动
  同上
   
  作业:认真研究内核大名鼎鼎的宏:container_of
   
   
   
   
               
原创粉丝点击