输入子系统由驱动层,输入子系统核心层(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:%d\n",
ev_key.type,ev_key.code,ev_key.value);
if(EV_SYN==ev_key.type)
printf("syn
event\n\n");
}
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驱动器)。
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结构。
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_fix_screeninfo记录了用户不可以修改的显示参数,如显示缓存的物理地址等:
unsigned long smem_start; /*
frame buffer 缓冲区起始地址(物理地址)*/ __u32 smem_len; /*
缓冲区长度*/ __u32 type; /*
设备类型,例如TFT或STN*/
unsigned long mmio_start; /*
IO映射区起始地址(物理地址) */ __u32 mmio_len; /*
IO 映射区长度 */ __u32 accel; /*
指出使用的加速卡是哪些特定的芯片 */ __u16 reserved[3]; /*
系统保留*/
};
...... ......
};
注册一个帧缓冲设备:
int register_framebuffer(struct fb_info *fb_info);
注销一个帧缓冲设备:
int unregister_framebuffer(struct fb_info *fb_info);