1 引言
嵌入式系统是在有限的资源及有限的体积中运行的、高效地实现某种特殊功能的功能集合,常常要面对较弱的CPU处理能力、少量的电力消耗、有限的内存空间、非常小的体积、简洁特定的用户操作界面。它的目的是在设计时限定好的资源中来实现用户需要的功能。
PowerPC823是Motorola公司的PowerQUICC系列嵌入式通信处理器中的一种,以精简指令集RISC的体系结构为基础,集成了32位微处理器和多种外设接口,具有强大的通信和网络协议处理能力,广泛应用于通信和网络产品。功能结构主要包括:嵌入式PowerPC内核,系统接口单元(System Interface Unit,SIU)和通信处理模块(Communications Processor Module,CPM)。
本文主要分析了PowerPC823的PCMCIA的接口、行列式扫描键盘的原理、外部I/O存储器的访问、Linux下字符设备驱动程序和内核模块的实现、Input机制编程原理等,最后笔者结合Pcmcia接口如何实现键盘驱动。
2.Pcmcia接口简介
对于控制Pcmcia接口,Pcmcia接口模块提供有6种类型接口寄存器:Pcmcia接口输入引脚寄存器(PIPR)、Pcmcia接口状态变化寄存器(PSCR)、Pcmcia接口使能寄存器(PER)、Pcmcia接口通用控制寄存器B(PGCRB)、Pcmcia基地址寄存器(PBR0-7)和Pcmcia选项寄存器(POR0-7)。这些寄存器都被存储器映射到内部控制寄存器区内。其中,前4种类型的寄存器既可以用于控制Pcmcia接口模块的通用I/O引脚(包括8个输入IP_B[0:7]引脚和2个输出OP_B[2:3]引脚);而后2种类型寄存器共有8对16个寄存器,这些寄存器专用于Pcmcia控制器。通过对Pcmcia接口模块的寄存器编程,用户可以灵活地控制Pcmcia接口模块。
在实现的开发板上,根据行列式扫描键盘的原理,屏蔽掉Pcmcia控制器,把引脚IP_B[0:7]定义为通用I/O口线,Pcmcia接口有几个比较关键的寄存器,它们分别是PIPR、PSCR、PER、PGCRB(A)。
PIPR:Pcmcia接口输入管脚寄存器,用来采样Pcmcia输入信号,当Pcmcia控制器停止工作时,PIPR的16-23位可以读写作为通用I/O口线的IP_B[0:7]。
PSCR:Pcmcia接口状态变化寄存器,跟踪Pcmcia接口输入信号状态的变化。
PER:Pcmcia接口中断使能寄存器,不过位116-22和位24-27分别报告给了不同的中断等级。
PGCRB(A):Pcmcia接口通用控制寄存器,对产生的中断指定不同的中断控制。在我们的开发板上使用了PGCRB。
3.行列式扫描键盘的原理
随着信息家电、手持设备、无线设备等的迅速发展,相应的硬件和软件也得到迅速发展,在以MCU为核心的许多应用程序中,都需要有键盘的输入功能以实现人机接口。一般而言,硬件上的实现方法有两种:第一种直接使用一路I/O口,每个引脚引一个按键,在程序中使用轮询的方式判断是否有键按下,然后就具体的按健执行对应的功能。另一种就是采用阵列式按健键盘和MCU的带有键盘中断的并行I/O口组成。
针对开发板的设计,利用外接的一块串口芯片ST16C552,把其8根打印机口的并行输出口线和4根状态信号线定义为列,而与Pcmcia接口的IP[0:7]定义为行,在初始化与之相关的寄存器的初始值后(驱动实现中介绍初始化),当有键被按下时,与Pcmcia的I/O口相连的那行输入口线产生中断,可以读取用来采样Pcmcia输入信号的PIPR寄存器中位的值确定行,此时它所对应的位是低电平。在列的扫描轮询的程序中通过软件依次对列的12根输出口线置“0”,若此时读取PIPR的值与开始时一致,就可以确定出列,Input机制调用函数把行列所对应的键的键值作为参数传递给内核,在LCD屏上显示对应的按键。
4.外部I/O存储器的访问
CPU常常连接外部设备,在开发板的设计中,外接了拥有一些寄存器的一块串口芯片ST16C552,它们被称为外部I/O存储器。外部I/O存储器通过总线被CPU读或者写,在Linux操作系统中,不管是用户空间还是内核空间,都不能使用物理地址直接访问。因为Linux使用了MPC的MMU单元,MMU控制物理地址到虚拟地址的转换,所有对外部I/O存储器物理地址的操作都必须预先在MMU中注册,并且返回虚拟地址,通过访问虚拟地址来实现访问物理地址,Linux中使用下面的函数来实现物理地址到虚拟地址的转换及注册:
void *ioremap(unsigned long address,unsigned long size);
void iounmap(void *addr);
对于不同大小端口的访问,有不同的访问函数,在键盘驱动中使用了函数void writeb(unsigned value,address)。
writeb(0x00,io_base);
writeb(0xeb,io_base+2);
wmb();
最后的wmb()是一个write memory barrier宏定义,它保证在它前面的写操作按顺序执行。
若读者有兴趣可以去读文件include<asm/io.h>,总结了对i/O端口访问、申请和释放的函数。
5.Linux下字符设备驱动程序和内核模块机制的简述
设备驱动程序是操作系统内核和硬件之间的接口,属内核的一部分,主要功能如下:
对设备初始化或释放;
把数据从内核传送到硬件和从硬件读取数据;
读取应用程序传送给设备的数据和回送应用程序请求的数据;
监测和处理设备出现的异常。
设备驱动程序为应用程序屏蔽了硬件的细节,这在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。
编写键盘设备驱动,需要了解Linux Module(Linux内核)的概念。Linux内核是一个整体的结构,因此向内核添加任何东西或者删除某些功能,都十分困难。为了解决这个问题,Linux引入了内核模块机制,从而可以动态地在内核中添加或者删除模块。在调入内核之后,Linux Module和内核处于同一地址空间的,它们可以相互调用函数,直接访问对方的地址,每个Linux模块必定包括以下两个函数,从KerneL2.2开始定义有点变化,在键盘驱动中定义为:
static int __int pkbd_init(void)这个函数在模块调入内核(insmod)时被调用,它在内核中注册一定的功能函数,告诉内核可以做这些事情。
static void __exit pkbd_exit(void)这个函数把pkbd_init()在内核中注册的功能函数完全卸载掉,如果没有完全卸载,在此模块下次调入时,将因为有重名的函数而导致调入失败。
然后在在源代码文件末尾使用下面的语句:
module_init(pkbd_init);
module_exit(pkbd_exit);
这样做的好处就是每个模块都可以有自己的初始化和卸载函数的函数名,多个模块在调试时不会有函数名重复的问题。
Pcmcia接口的键盘驱动程序中,由于采用了Input机制,中间的很多函数模块由其提供的接口功能完成了。因为键盘有个按键去抖动的中断延迟执行过程,需要比较长的时间,实时性要求不高的程序,正好可以使用内核中提供的task queue、tasklet、kernel timer等机制来编写。现以kernel timer来说明,它是一个双向链表,成员结构定义如下(定义在/linux/timer.h);
struct timer_list{
struct list_head list;
unsigned long expires;
unsigned long data;
void(*function)(unsigned long);
};
其中:expires的单位是jiffies,表示此timer的expires的时间(是一个绝对时间,但系统的jiffies大于或等于expires时,function被调用);
function是在此timer expire后被调用的函数;
data是传递给function的参数。
主要的kernel timer函数:
static inline void init_timer(struct timer_list *timer);
在使用一个kernel timer前,先调用此函数初始化,一般在pkbd_init()中进行。
extern void add_timer(struct timer_list *timer);
把一个kernel timer加入到kernel中,同时激活。
extern int del_timer(struct timer_list *timer);
删除一个kernel timer。必须在pkbd_exit()中释放前面定义的kernel timer。
6.Input机制编程原理
Input驱动的核心是input.o模块,它必需在其他input模块之前加载,在这两组模块之间起通信桥梁的作用。加载的驱动模块服务于硬件,对input.o模块产生触发事件(如按键,鼠标移动),然后从input.o获得事件并通过各种接口传递出去,象按键就传递给了内核,鼠标移动就通过模拟的PS/2接口传递给GPM等等。
如带USB鼠标和USB键盘的通用配置,必须在内核编译选项中加入下面这些模块:
input.o
mousedev.o
keybdev.o
usbcore.o
usb-[uo]hci.o
hid.o
Input机制键盘设备驱动编程的主要函数有:
input register_device(&button_dev);input设备结构注册的调用函数
input_report_key(struct input_dev *dev,int code,int value);EV_KEY是最简单的事件,主要针对于键和按钮,定义了从0到KEY_MAX的键值(可以参见文件linux/input.h),value是个真实值,任何非零值表示键被按下,零值表示键释放掉,但只有在value的值与上次不同时输入的键值才激发键盘事件。
除EV_KEY时间之外,还有其他几类基本事件类型:如EV_REL和EV_ABS它们主要应用在由设备产生的相对和绝对值,如鼠标的移动就是一个相对值。
为了更清楚的说明Input机制编程,下面是个最简单的示例程序,设备只有一个按钮,对应的I/O口为BUTTON_PORT,当按钮被按下或者释放掉,就产生BUTTON_IRQ中断事件。
static void button_interrupt(int irq,void *dummy,struct pt_regs *fp)
{input_report_key(&button_dev,BTN_1,inb(BUTTON_PORT)&1);
}
static int __init button_init(void)
{
if(request_irq(BUTTON_IRQ,button_interrupt,0,"button",NULL)){
printk(KERN_ERR"button.c:Can't allocate irq%d ",button_irq);
return -EBUSY;
}
button_dev.evbit[0]=BIT(EV_KEY);
button_dev.keybit[LONG(BTN_0)] =BIT(BTN_0);
input_register_device(&button_dev);
}
static void __exit button_exit(void){
input_unregister_device(&button_dev);
free_irq(BUTTON_IRQ,button_interrupt);
}
module_init(button_init);
module_exit(button_exit);
7.Pcmcia接口的键盘驱动实现
在Pcmcia接口的键盘驱动程序中,主要的过程就是在初始化模块中对硬件相关寄存器的初始化,申请中断,初始化首先要得到IMMAP寄存器的地址,然后初始化Pcmcia接口寄存器:
immap=(immap_t *)(mfspr(IMMR)&0xFFFF0000);
immap->im_pcmcia.pcmc_pscr = 0;
immap->im_pcmcia.pcmc_per |=0x0000fe00;
immap->im_pcmcia.pcmc_pgcrb = 1 <<19;
设定键盘事件,定义键值:
set_bit(EV_KEY,pcmkbd_dev.evbit)
set_bit(KEY_1,pcmkbd_dev.keybit);
这是定义数字键“1”,这样就可以定义你所需要的键了。接下来申请中断、初始化tasklet和内核定时器、注册Input键盘事件等。
在程序中,底半部任务队列处理程序pcmcia_do_tasklet启动名为pcmcia_timer的内核定时器,这里利用了Linux提供的计时机制jiffies,jiffies的分辨率是10毫秒,在内核中定义为HZ(1秒),这里设为100毫秒后,pcmcia_timer到期,内核执行pcmcia_timer的处理函数pcmcia_timer_timed_out。在处理函数中主要进行行列扫描。将确定的按键键值作为input机制提供的函数input_report_key(&pcmkbd_dev,key_value[h][1],!u)的key_value的参数传递给内核就可以了。
同时要在static void __exit pcmkbd_exit(void)模块中删除掉内核定时器、I/O端口和申请的中断。
8 结束语
嵌入式Linux应用将会越来越广泛,拥有广阔的市场前景。作为人机交互的键盘功能的实现也是非常重要的,本文利用Input机制原理和内核设备驱动的实现,通过Pcmcia接口实现了键盘功能,相信这个方案在嵌入式产品的开发中也很新颖,这种方案必将得到越来越多的应用。