ARM嵌入式笔记2

ARM 136浏览
<--------------------------从输入子系统调用开始记--------------------->
输入子系统由驱动层,输入子系统核心层(Input Core),和事件处理层(Event Handler)三个部分组成。一个输入事件,如鼠标移动,键盘按键按下,通过Driver-->Input Core-->Event Handler-->userspace的顺序到达用户空间的应用程序。
驱动层:将底层的硬件输入转化为统一的事件形式,向输入核心(Input Core)汇报。
输入核心层:为驱动层提供输入设备注册和操作接口。 如input_register_device;通知事件处理层对事件进行处理,在/PROC下产生相应的 设备信息。
事件处理层:主要作用是和用户空间交互。Linux在用户空间将所有设备当成文件来处理,在一般的驱动程序中都提供有fops接口,以及在/dev下生成相应的设备文件nod,而在输入子系统中,这些工作都是由事件处理层完成的。
     在Linux内核中,input设备用input_dev结构体描述,使用输入子系统实现输入设备驱动的时候,驱动的核心工作是向系统报告按键,触摸屏,鼠标等输入事件(event,通过Input_event结构体描述),不再需要关心文件操作接口。因为输入子系统已经完成了文件操作接口,驱动报告的事件经过InputCore和EventHandler最终到达用户空间。
注册输入设备
    int input_register_device(struct input_dev *dev);
 注销输入设备:
    void input_unregister_device(struct input_dev *dev); 
告诉输入子系统我的设备驱动支持哪些事件:
    set_bit(EV_KEY, buttion_dev.evbit);
    struct iput_dev有二个成员 : evbit:事件类型   keybit:按键类型。
 
该设备驱动所能支持的事件。

 //EV_SYN      同步事件
 
       //EV_KEY       键盘事件
 
       //EV_REL       相对坐标事件,用于鼠标
 
       //EV_ABS       绝对坐标事件,用于摇杆
 
       //EV_MSC      其他事件
 
       //EV_LED       LED灯事件
 
       //EV_SND      声音事件
 
       //EV_REP       重复按键事件
 
       //EV_FF         受力事件
 
       //EV_PWR      电源事件
 
       //EV_FF_STATUS  受力状态事件
当事件类型为EV_KEY时,还需要指明按键类型:BTN_LEFT(鼠标左键) BTN_RIGHT(鼠标右键)BTN_MIDDLE(鼠标中键)BTN_0(0键)BTN_!(1键)
 
用于报告EV_KEY事件的函数:
     void input_report_key(struct input_dev *dev, unsigned int code, int value);
用于报告EV_REL事件的函数
     void input_report_rel(struct input_dev *dev, unsigned int code, int value);
用于报告EV_ABS事件的函数
     void input_report_abs(struct input_dev *dev, unsigned int code, int value);  
     参数: code:事件的代码(如0x110为鼠标中键,具体参考在/include/linux/input.h),value:事件的值(如果事件的类型是EV_KEY,当按键按下时为1,松开时值为0)
用于告诉InputCore,此次报告已经结束: 
    input_sync(input_dev);

实例:
    在触摸屏驱动中,一次点击的整个报告过程如下:
    input_report_abs(intput_dev, ABS_X, x);    //x坐标
    input_report_abs(intput_dev,ABD_Y,y);     //y坐标
    input_report_abs(input_dev,ABS_PRESSURE,1);    //是按下还是弹起
    input_sync(input_dev);    //同步
 
一个相对完整的实例:
//在按键中断中报告事件
static void button_interrupt(int irq, void *dummy, struct pt_regs *fp){
    ...... ...... ...... ......
    //这里需要把支持的按键都往内核中报告一次,因为不知道具体哪个按键被按下了
    input_report_key(&button_dev, BTN_0, inb(BUTTON_PORT0));
    input_report_key(&button_dev, BTN_1, inb(BUTTON_PORT1));
    input_sync(&button_dev); 
  //同步
 
    ...... 


static int __init button_init(void){
    ...... ...... ......
    //申请中断
    if(request_irq(BUTTON_IRQ, button_interrupt, 0, "button", NULL))
        return -EBUSY;
    set_bit(EV_KEY, button_dev.evbit);    //支持EV_KEY事件
    set_bit(BTN_0, button_dev.keybit);    //设备支持二个键BTN_0,BTN_1
    
set_bit(BTN_1, button_dev.keybit);
    input_register_device(&button_dev);
    ...... 

}
 
上面的按键驱动的应用程序:
 
#include <stdio.h>
 
 #include <stdlib.h>
 
 #include <unistd.h>
 
#include <sys/ioctl.h>
 
#include <sys/types.h>
 
#include <sys/stat.h>
 
#include <fcntl.h>
 
#include <sys/select.h>
 
#include <sys/time.h>
 
#include <errno.h>
 
#include <linux/input.h>
 
int main(void)
 
 {
 
int
buttons_fd;
 
 int
key_value,i=0,count;
 
struct
input_event ev_key;
 
 buttons_fd
= open("/dev/event0", O_RDWR);
 
 if
(buttons_fd < 0) {
 
 perror("open
device buttons");
 
 exit(1);
 
 }
 
 for
(;;) {
 
 count
= read(buttons_fd,&ev_key,sizeof(struct input_event));
 
 for(i=0;
i<(int)count/sizeof(struct input_event); i++)
 
 if(EV_KEY==ev_key.type)
 
 printf("type:%d,code:%d,value:%dn",
ev_key.type,ev_key.code,ev_key.value);
 
 if(EV_SYN==ev_key.type)
 
 printf("syn
eventnn");
 
 }
 
 close(buttons_fd);
 
 return
0;
 
 } 

触摸屏的工作流程:
1.指定是使用X/Y坐标分别转换模式,还是X/Y坐标自动转换模式。
2. 设置触摸屏等待中断模式。
3. 如果中断到来,就采用相应的转换模式,转换坐标。
4. 获取到正确的X/Y坐标后,再返回等待中断模式。

总线是一种传输信号的信道,总线是连接一个或多个导体的电气连线,总线由电气接口和编程接口组成。
PCI总线的优点:
1.在计算机和外设之间传输数据时具有更好的性能。
2.能够尽量独立于具体的平台。
3.可以方便的实现即插即用。

图片
图片
只有PCI桥才能生成PCI总线
每个PCI设备由一个总线号,一个设备号,和一个功能号确定。PCI规范允许一个系统最多拥有256条总线,每条总线最多带32个设备,每个设备可以是最多8个功能的多功能板(如一个音频设备带一个CD-ROM驱动器)。

/proc/iomem 描述了系统中所有设备I/O在内存地址空间上的映射。地址从1G开始的第一个设备在/proc/iomem中的描述:
        40000000 - 400003ff : 0000 : 00 :1f.1
     这是一个PCI设备,40000000 - 400003ff是它所映射的内存空间地址,占据了内存地址空间1024Bytes的位置,而0000 : 00 :1f.1则是这个PCI外设的地址,它以冒号和逗号分隔为四部分:第一个16位表示域,第二个8位表示一个总线号,第三个5位表示一个设备号,最后是3位表示功能号。

Linux系统中遍历PCI总线是采用深度优先的方式的。

每个PCI设备都有一组固定格式的寄存器,即配置寄存器,配置寄存器由Linux内核中的初始化代码和驱动程序共同使用,内核在启动时负责对配置寄存器进行初始化。包括设置中断号和i/o基址等:
 图片
00H-01H:制造商标识
02H-03H:设备标识 
04H-05H:命令寄存器
06H-07H:状态寄存器
08H:版本识别号寄存器
09H:分类代码寄存器
0cH:行长度寄存器 
0dH:主设备延迟时间寄存器
0eH:头标类型寄存器
0fH:自测试寄存器
10H-13H:基地址寄存器0 
14H-17H:基地址寄存器0 
18H-1bH:基地址寄存器0 
1cH-29H:基地址寄存器0 
30H-33H:扩展ROM基地址
34H-3bH:保留
3cH:中断线寄存器
3dH:中断引脚寄存器,如果值为0表示不支持中断,非0表示支持中断
3eH:最小授权寄存器
3fH:最大延迟寄存器 

在Linux内核中,PCI驱动使用struct pci_driver结构来描述:
struct pci_driver{
    ...... ...... ...... ......
    const struct pci_device_id *id_table;    //这条PCI上所支持的所有设备
    int (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
    void (*remove)(struct pci_dev *dev);
    /*Device removed (Null if not a hot-plug capable driver)*/
    ...... ...... ......

注册PCI驱动使用:
    pci_register_driver(struct pci_driver *drv); 
在PCI驱动使用PCI设备的任何资源前,驱动必须调用如下函数来使能设备:
    int pci_enable_device(strutc pci_dev *dev); 
 
一个PCI设备最多可以实现6个地址区域,大多数PCI设备在这些区域实现I/O寄存器,Linux提供了一组函数来获取这些区间的地址:
         pci_resource_start(struct pci_dev *dev, int bar);
     返回指定区域的起始地址,这个区域通过参数bar指定, 范围从0-5,表示6个PCI区域中的一个。
         pci_resource_end(struct pci_dev *dev, int bar);
     返回指定区域的未地址。
 
Linux中网卡的PCI驱动:/driver/net/hamachi.c

在Linux中,终端是一类字符设备的统称,包括了三种类型:控制台,串口和伪终端。
控制台:供内核使用的终端为控制台,控制台在Linux启动时,通过命令console=...指定,如果没有指定控制台,系统把第一个注册的终端(TTY)作为控制台。1.控制台是一个虚拟的终端,它必须映射到真正的终端上。2.控制台可以简单理解为printk输出的地方。3.控制台是个只输出的设备,功能很简单,只能在内核中访问。
伪终端:伪终端是一种特殊的终端设备,由主-从二个成对的设备组成,当打开主设备时,对应的从设备也随之打开, 形成连接状态。输入到    主设备的数据成为从设备的输出,输出到从设备的数据成为主设备的输出,形成双向管道。伪终端设备常用于远程登录服务器来建立网络和终端的关连。当使用telent远程登录到另一台主机时,telent进程和远程主机的telent服务器相连接,telent服务器使用某个主设备并通过对应的从设备与telent进程相互通讯。
图片 图片
TTY驱动从硬件收到数据后,把数据传递到TTY核心,下图中ldisc.receive_buf()中存储接收到的数据。TTY核心将从TTY驱动收到的数据缓存到一个tty_flip_buffer类型的结构中,该结构包含二个数组。从TTY设备收到的数据存放于第一个数组,当这个数组满时,等待数据的用户将被通知,当这个用户从这个数组读数据时,任何从TTY驱动新来的数据将被存放在第二个数组,当第二个数组存放满后,数据再次提交给用户,并且驱动又开始填充第一个数组,以此交替。
图片

串口驱动程序设计:
Linux内核用uart_driver描述串口驱动:
struct uart_driver{
    struct module *owner;
    const char *driver_name;    //驱动名
    const char *dev_name;    //设备名
    int     major;    //主设备号
    int     minor;    //起始次设备号
    int     nr;    //设备数
    struct *cons;    
    struct uart_state *state;
    struct tty_driver *tty_driver;

注册串口驱动程序:
    int uart_register_driver(struct uart_driver *drv);

Uart端口的描述:
struct uart_port{
    spinlock_t lock;    //端口锁
    unsigned int iobase;    //IO端口基地址
    unsigned char __iomem *membase;    //IO内存基地址
    unsigned int irq;    //中断号
    unsigned char fifosize;    //传输fifo大小
    const struct uart_ops *ops;
    ...... ...... ...... ...... ...... ...... .....

struct uart_ops {
    unsigned int(*tx_empty)(struct uart_port *);
    void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
    unsigned int (*get_mctrl)(struct uart_port *);
    void (*stop_tx)(struct uart_port *);    //停止发送
    void (*start_tx)(struct uart_port *);    //开始发送
    void (*send_xchar)(struct uart_port *, char ch);//发送XCHAR
    void (*stop_rx)(struct uart_port *);    //停止接收
    ...... ...... ...... ...... ...... ..... 

添加一个UART串口:
    int uart_add_one_port(struct uart_driver *drv, struct uart_port *port); 

驱动书写操作流程:
1.定义一个uart_driver的变量,并初始化。
2.使用uart_register_driver来注册这个驱动。
3.初始化uart_port和ops函数表。
4.调用uart_add_one_port添加初始化好的uart_port

块设备驱动设计:
块设备的体系结构:
图片
VFS:虚拟文件系统接口
Disk Cache:当用户发起文件访问请求的时候,首先会到DiskCache中寻找文件是否被缓存了,如果在Cache中,则直接从Cache中读取。否则就到具体的文件系统中读取数据。
Mapping Layer:1.首先确定文件系统的Block Size,然后计算所请求的数据包含多少个Block。2.调用具体文件系统的函数来访问文件的inode,确定所请求的数据在磁盘上的逻辑块地址。
Generic Block Layer:Linux内核为块设备抽象了统一的模型,把块设备看成是由若干个扇区组成的数据空间。上层的读写请求在通用块层(Generic Block Layer)被构造成一个或多个bio结构。

I/O scheduler layer:I/O调度层负责将I/O操作排序,采用某种算法(如:电梯调度算法)来高效地处理操作。
Block Device Driver:块设备驱动层


Linux内核使用struct gendisk来描述块设备:
struct gendisk{
    int major;    //主设备号
    int first_minor;    //次设备号
    int minors;
    char disk_name[DISK_NAME_LEN];    //驱动名
    struct block_device_operations *fops;    
    struct request_queue *queue;    //请求队列
    ...... ...... ...... ...... ...... ......
    int node_id;
}
向内核注册块设备:
    void add_disk(struct gendisk *gd);
块设备所支持的操作:
struct block_device_operations{
    int (*open)(struct block_device *, fmode_t);
    int (*release)(struct gendisk *, fmode_t);
    int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
    ...... ...... ...... ...... ...... ......
}
在Linux中,用struct request来表示等待处理的块设备I/O请求:
struct request{
    struct list_head queuelist;    //链表结构
    sector_t sector;    //要操作的首个扇区
    unsigned long nr_sectors;    //要操作的扇区数目
    struct bio *bio;    //请求的bio结构体链表
    struct bio *biotail;    //请求的bio结构体链表尾
    ...... ...... ...... ...... ..... ......
}
内核中操作请求队列的函数 :
    struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); 
    初始化请求队列,一般在块设备驱动的模块加载函数中调用 。
    void blk_cleanup_queue(request_queue_t *q);
    清除请求队列,这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用 。
    struct request *elv_next_request(request_queue_t *queue);
    返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL, 该函数不会清除请求,它仍然将这个请求保留在队列上。
    void blk_dequeue_request(struct request *req);
    从队列中删除一个请求。
 
 块设备驱动测试:
1.insmod simple-blk.ko    //加载块设备
2.ls /dev/simp_blkdev    //查看加载的设备
3.mkfs.ext3 /dev/simp_blkdev   //把simp_blkdev这个块设备格式化为ext3格式
4.mkdir -p /mnt/blk    //创建目录
5.mount 
/dev/simp_blkdev /mnt/blk    //挂载到这个新创建的目录
6.cp /etc/init.d/* /mnt/blk    //测试拷入
7.ls /mnt/blk    //查看拷入
8.umount /mnt/blk
9.ls /mnt/blk 


数据访问流程:
图片 图片

 __make_request函数用于制造一个请求,制造一个请求的元素是BIO:1个struct bio代表一次块设备I/O请求,IO调度器可将连续的bio合并成一个请求struct request:
struct bio{
    sector_t bi_sector;    //要访问的第一个扇区
    unsigned int bi_size;    //以字节为单位所需传输的数据大小
    struct bio_vec *bi_io_vec;    //实际的vec列表
    ...... ...... ...... ...... ......
}
struct bio_vec{
    struct page *bv_page;    //页指针
    unsigned int bv_len;    //传输的数据长度
    unsigned int bv_offset;    //偏移量
}
在__make_request函数中,使用了IO调度器,将多个bio的访问顺序进行了优化,调整,合并为一个request,然后提交给用户指定的函数处理。但是对于ramdisk,U盘,记忆棒之类的设备,并不存在磁盘所面临的寻道时间。因而对这样的块设备而言,IO调度器不但发挥不了作用,反而其本身将白白耗掉不少内存和CPU。这个问题的解决办法是:驱动自己实现request_queue所需要的make_request_fn函数,不使用__make_request;

实现make_request_fn:
    request_queue_t *blk_alloc_queue(int gfp_mask);
    分配“请求队列”,对于FLASH,RAM盘等完全随机访问的非机械设备,并不需要进行复杂的IO调度,这个时候应该使用上述函数分配一个请求队列。
    void blk_queue_make_request(request_queue_t *q, make_request_fn *mfn);
    绑定“请求队列”和“制造请求”函数。
 

LCD驱动程序设计: 
    FrameBuffer从本质上讲是图形设备的硬件抽象,对开发者而言,FrameBuffer是一块显示缓存,往显示缓存中写入特定格式的数据就意味着向屏幕输出内容。通过不断的向FrameBuffer中写入数据,显示控制器就自动的从FrameBuffer中取数据并显示出来。 
    帧缓冲设备对应的设备文件为/dev/fd*,如果系统有多个显示卡,Linux还可支持多个帧缓冲设备,最多可达32个,分别为/dev/fd0-/dev/fd31,而/dev/fd则为当前缺省的帧缓冲设备,通常指向/dev/fd0,帧缓冲设备为标准字符设备,主设备号为29,次设备号从0到31。 

    在程序中打开这个/dev/fd0文件,并改变其中的内容,相应的内容会自动反映到屏幕上。

FrameBuffer系统架构图:
图片 

 Linux内核使用struct fb_info来描述帧缓冲设备:
struct fb_info{
    ...... ...... ...... ...... ......
    struct fb_var_screeninfo var;    //可变参数
    struct fb_fix_screeninfo fix;    //固定参数
    ...... ...... ...... ...... ......
    struct fb_ops *fbops;    //帧缓冲操作
    ...... ...... ...... ...... ......

struct fb_var_screeninfo记录了用户可以修改的显示参数,包括屏幕分辨率等:

struct fb_var_screeninfo {
      __u32 xres;            /* visible resolution    可见分辨率 */
      __u32 yres;
      __u32 xres_virtual;        /* virtual resolution    虚拟分辨率 */
      __u32 yres_virtual;
      __u32 xoffset;            /* 从虚拟分辨率到可见分辨率的偏移量*/
      __u32 yoffset;            
      __u32 bits_per_pixel;        /* 像素深度            */
      __u32 grayscale;        /* 灰度级 */
      struct fb_bitfield red;        
      struct fb_bitfield green;    
      struct fb_bitfield blue;
      struct fb_bitfield transp;    /* 透明度            */    
      __u32 nonstd;            /* 非标准像素格式 */
      ……
      __u32 pixclock;            /* 像素时钟,单位是皮秒*/
      __u32 left_margin;            /* 左侧边缘区*/
      __u32 right_margin;            /*右侧边缘区    */
      __u32 upper_margin;        /*顶部边缘区    */
      __u32 lower_margin;
      __u32 hsync_len;        /*水平扫描边缘区    */
      __u32 vsync_len;        /*垂直扫描边缘区    */
      …….
};
图片
struct fb_fix_screeninfo记录了用户不可以修改的显示参数,如显示缓存的物理地址等:

struct fb_fix_screeninfo {
    char id[16];     /* 设备名*/

    unsigned long smem_start;     /*
frame buffer 缓冲区起始地址(物理地址)
*/
    __u32 smem_len;     /*
缓冲区长度
*/
    __u32 type;        /*
设备类型,示例TFT或STN
*/

    __u32 type_aux;     /* Interleave for interleaved Planes */
    __u32 visual;        /* 色彩类型,真彩色、假彩色或单色*/

    __u16 xpanstep;     /* zero if no hardware panning */
    __u16 ypanstep;     /* zero if no hardware panning */
    __u16 ywrapstep;     /* zero if no hardware ywrap */
    __u32 line_length;    /* 屏幕上每行的字节数*/

    unsigned long mmio_start; /*
IO映射区起始地址(物理地址)
*/
    __u32 mmio_len;     /*
IO 映射区长度
*/
    __u32 accel;     /*
指出使用的加速卡是哪些特定的芯片
*/
    __u16 reserved[3];     /*
系统保留
*/
}; 

struct fb_ops包含了对控制器进行操作的函数指针:
struct fb_ops {
/* open/release and usage marking */
struct module *owner;
int (*fb_open)(struct fb_info *info, int user);
int (*fb_release)(struct fb_info *info, int user);
int (*fb_get_fix)(struct fb_fix_screeninfo *fix, int con,struct fb_info *info);/* get non settable parameters */
int (*fb_get_var)(struct fb_var_screeninfo *var, int con,struct fb_info *info);/* get settable parameters */
int (*fb_set_var)(struct fb_var_screeninfo *var, int con,struct fb_info *info);/* set settable parameters */
int (*fb_get_cmap)(struct fb_cmap *cmap, int kspc, int con,struct fb_info *info);/* get colormap */
int (*fb_set_cmap)(struct fb_cmap *cmap, int kspc, int con,struct fb_info *info);/* set colormap */
int (*fb_pan_display)(struct fb_var_screeninfo *var, int con,struct fb_info *info);/* pan display (optional) */
int (*fb_ioctl)(struct inode *inode, struct file *file, unsigned int cmd,unsigned long arg, int con, struct fb_info *info);/* perform fb specific ioctl (optional) */
int (*fb_mmap)(struct fb_info *info, struct file *file, struct vm_area_struct *vma);/* perform fb specific mmap */
int (*fb_rasterimg)(struct fb_info *info, int start);/* switch to/from raster image mode */
        ...... ...... 

}; 
 注册一个帧缓冲设备:
    int register_framebuffer(struct fb_info *fb_info);
注销一个帧缓冲设备:
    int unregister_framebuffer(struct fb_info *fb_info);