日期:2014-05-16  浏览次数:20785 次

linux设备驱动(十五)--与硬件通信

linux设备驱动(十五)--与硬件通信
2010年09月25日
  在学习之前,首先了解下ARM和PC总线的结构。 I/O端口和I/O内存
  每种外设都是通过读写寄存器来进行控制。 
  在硬件层,内存区和 I/O 区域没有概念上的区别: 它们都是通过向在地址总线和控制总线发出电平信号来进行访问,再通过数据总线读写数据。
  因为外设要与I\O总线匹配,而大部分流行的 I/O 总线是基于个人计算机模型(主要是 x86 家族:它为读和写 I/O 端口提供了独立的线路和特殊的 CPU 指令),所以即便那些没有单独I/O 端口地址空间的处理器,在访问外设时也要模拟成读写I\O端口。这一功能通常由外围芯片组(PC 中的南北桥)或 CPU 中的附加电路实现(嵌入式中的方法) 。
  Linux 在所有的计算机平台上实现了 I/O 端口。但不是所有的设备都将寄存器映射到 I/O 端口。虽然ISA设备普遍使用 I/O 端口,但大部分 PCI 设备则把寄存器映射到某个内存地址区,这种 I/O 内存方法通常是首选的。因为它无需使用特殊的处理器指令,CPU 核访问内存更有效率,且编译器在访问内存时在寄存器分配和寻址模式的选择上有更多自由。
  I/O寄存器和常规内存
  尽管硬件寄存器和内存非常相似,但程序员在访问I/O寄存器的时候必须注意避免由于CPU或编译器不恰当的优化而改变预期的I/O操作(也即对寄存器的地址都声明为volatile)。
  I/O寄存器和RAM的最主要区别就是I/O操作具有边际效应(其实边际效应就是对I/O寄存器操作,导致高低电平的变化,从而促使硬件进行相对应的行为)。
  因为存储单元的访问速度对 CPU 性能至关重要,编译器会对源代码进行优化,主要是: 使用高速缓存保存数值和 重新编排读/写指令顺序。但对I/O 寄存器操作来说,这些优化可能造成致命错误。因此,驱动程序必须确保在操作I/O 寄存器时,不使用高速缓存,且不能重新编排读/写指令顺序。
  解决的方法:
  对于硬件自身缓存引起的问题:只要把底层硬件配置成在访问I/O区域时禁止硬件缓存即可。
  对于编译器优化和硬件重新排序引起的问题:对硬件必须以特定顺序执行的操作之间设置内存屏障。linux提供了以下宏来解决可能的排序问题。
  #include 
  void barrier(void) 这个函数通知编译器插入一个内存屏障,但对硬件没影响。编译后的代码会把当前CPU寄存器的所有修改过的数值保存到内存中,需要这些数据时再读出来。对barrier的调用,可阻止在屏障前后的编译器优化,但硬件能完成自己的重新排序。其实 中并没有这个函数,因为它是在kernel.h包含的头文件compiler.h中定义的*/
  #include 
  #define barrier()  _memory_barrier()
  但在内核中也有如下定义方式:
  #define barrier()  _asm_volatile("":::"memory") 
  CPU越过内存屏障后,将刷新自已对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在barrier()之后刷新寄存器对变量的分配。 
  #include 
  void rmb(void);/*保证任何出现于屏障前的读在执行任何后续的读之前完成*/
  void wmb(void);/*保证任何出现于屏障前的写在执行任何后续的写之前完成*/
  void mb(void);/*保证任何出现于屏障前的读写操作在执行任何后续的读写操作之前完成*/
  void read_barrier_depends(void);/*一种特殊的、弱些的读屏障形式。rmb 阻止屏障前后的所有读指令的重新排序,read_barrier_depends 只阻止依赖于其他读指令返回的数据的读指令的重新排序。区别微小, 且不在所有体系中存在。除非你确切地理解它们的差别, 并确信完整的读屏障会增加系统开销,否则应当始终使用 rmb。*/
  /*以上指令是barrier的超集*/
  void smp_rmb(void);
  void smp_read_barrier_depends(void);
  void smp_wmb(void);
  void smp_mb(void);
  /*仅当内核为 SMP 系统编译时插入硬件屏障; 否则, 它们都扩展为一个简单的屏障调用。*/
  这里介绍个小资料
  1.内核中往往有如下语句:
  #define _set_task_state(tsk,state_value) \
  do {(tsk)->state = state_value;} while(0)
  #define set_task_state(tsk,state_value) \
  set_mb((tsk)->state,state_value)
  两者区别在于:set_task_state(tsk,state_value)带有一个memory barrier,而_set_task_state却没有。当task的state为RUNNING时,由于scheduler可能会访问这个state,因此此时要改变为其他状态(如INTERRUPTIBLE),则应该用set_task_state来保证其原子性。而当state不为RUNNING时,因为没有人会访问task,所以可以用_set_task_state。但用set_task_state总是安全的,但_set_task_state会比较快。
  2.在include/asm-i386/system.h中,定义了如下一条语句:
  #define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory") 分析如下几点:
  1)set_mb(),mb(),barrier()函数追踪到底,就是__asm__ __volatile__("":::"memory"),而这行代码就是内存屏障。 
  2)__asm__用于指示编译器在此插入汇编语句 
  3)__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。 
  4)memory强制gcc编译器假设RAM所有内存单元均被汇编指令修改,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去访问内存。 
  5)"":::表示这是个空指令。barrier()不用在此插入一条串行化汇编指令。在后文将讨论什么叫串行化指令。 
  6)__asm__,__volatile__,memory在前面已经解释 
  7)lock前缀表示将后面这句汇编语句:"addl $0,0(%%esp)"作为cpu的一个内存屏障。 
  8)addl $0,0(%%esp)表示将数值0加到esp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,esp寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock指令,在__asm__,__volatile__,memory的作用下,用作cpu的内存屏障。 
  9)set_current_state()和__set_current_state()区别就不难看出。 
  10)至于barrier()就很易懂了。 
  3.#include  
  "void rmb(void);" 
  "void wmb(void);" 
  "void mb(void);" 
  这些函数在已编译的指令流中插入硬件内存屏障;具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb 保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier函数的超集。解释一下:编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对I/O操作进行类似优化很可能造成致命错误。所以要使用内存屏障,以强制该语句