LCD和USB驱动程序移植
嵌入式Linux设备驱动开发之LCD和USB驱动程序移植
《嵌入式Linux应用完全开发手册》第4篇第24章总结归纳
本章目标
- 了解TTY层下LCD和USB键盘驱动程序的框架
- 掌握移植LCD驱动程序的方法
- 使用LCD和USB设备
LCD驱动程序移植
LCD和USB键盘驱动程序框架
框架概述
具备人机交互功能的串口可以作为控制台和终端,同样,LCD和键盘组合起来也可以。
对LCD的操作可以像串口一样,通过终端设备层的封装(/dev/ttyx设备)来输出内容,也可以通过frame buffer(/dev/fbx)直接再显存上绘制图像。
frame buffer即帧缓冲,是一种独立于硬件的抽象图形设备,它使得应用程序可以通过一组定义良好的接口访问各类图形设备,不需要了解底层硬件细节。从用户的观点来看,frame buffer设备与/dev目录下其他设备没有区别,通过/dev/fbx设备文件来访问它(fb0表示第一个frame buffer设备、fb1表示第二个、…)。
frame buffer设备提供了一些ioctl接口来查询、设置图形设备的属性,比如分辨率、像素位宽等,另外,它属于“普通的”内存设备,类似/dev/mem:可以读(read)、写(write)、移动访问位置(seek)以及将这块内存映射给用户(mmap)。不同的是frame buffer的内存不是所有的内存,而是显卡专用的内存。应用程序可以直接更改frame buffer内存中的数据,效果立刻就能在显示器中看到。
/de//tty1等终端设备文件通过显示驱动程序和键盘驱动程序(还有其他输入设备,比如触摸屏)为它们提供输出、输入功能。
TTY和frame buffer驱动程序的框架如下图所示,输入设备以USB键盘为例:drivers/char/vt.c
用来支持显示器/键盘组成的终端设备,之所以被称为“虚拟终端”,是因为可以在一个物理终端设备上运行多个“虚拟终端”(也叫虚拟控制台),比如可以使用第一个虚拟终端来显示系统信息,使用第2个虚拟终端来运行文本模式程序,第3个虚拟终端来运行图形程序。它们可以同时运行,使用一些组合键可以切换到某个虚拟终端上。
虚拟终端层管理着这些虚拟终端,比如为它们分配缓冲区、切换虚拟终端时把它的内容输出到显示器、键盘有输入时把数据填入到当前终端的缓冲区中。它向上提供了封装好的接口,向下通过调用显示器/键盘的接口完成输入输出功能。
- 显示驱动程序
drivers/console/fbcon.c
文件向上提供了一个很重要的数据结构fb_con,所有的输出都是通过fb_con中的成员函数来实现的,bitblit.c、font.c也都处于drivers/console
目录下,它们和drivers/video/fbmem.c
一起,实现fb_con结构中的函数。另外,fbmem.c是frame buffer驱动程序,它向应用层提供/dev/fbx设备的访问接口,应用程序可以通过它绘制图形。drivers/video/s3c2410fb.c
文件是架构相关的代码,它实现LCD控制器的初始化、向fbmem.c注册frame buffer设备,并提供一些与架构相关的函数,比如设置分辨率、像素位宽等需要设置操作寄存器的函数。 - 键盘驱动程序
drivers/input/input.c
表示“输入设备”,有键盘、鼠标等。drivers/keyboard.c
是键盘驱动程序的封装,在它的下边,可以是一般的键盘,也可以是符合HID规范的键盘。HID是英文“Human Interface Device”得缩写,它通常指USB-HID规范,但是也有其他类型的遵循HID规范的设备(比如蓝牙键盘、蓝牙鼠标)。所以drivers/hid-core.c
、hid-input.c
两个文件将HID规范的共性提炼出来,它们的下面是各类具体实现,比如USB的drivers/hid/hidusb/hid-core.c
、hid-quirks.c
等。
操作实例
下面以几个操作的函数调用过程来理解TTY和frame buffer驱动程序的层次结构。注意:这只是为了在阅读内核源码时,给读者提供一些函数调用间的脉络关系。刚接触某类驱动时,了解各函数、结构间的调用关系是一件困难的事情。
- 注册frame buffer设备时,显示LOGO的过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27s3c2410fb_probe(video/s3c2410fb.c) ->
register_framebuffer(fbmem.c) ->
fb_info->node = i;//registered_fb[i]为空项,本例中i=0
registered_fb[i] = fb_info;
fb_notifier_call_chain(fb_notify.c) ->//它会调用fbcon_event_notify(fbcon.c)
fbcon_fb_registered(video/console/fbcon.c)
info_idx = idx//即info->node,值为上面的i
fbcon_takeover(1)(video/console/fbcon.c) ->
con2fb_map[i] = info_idx;//i=0,info_idx=0
take_over_console(char/vt.c) ->
register_con_driver(char/vt.c) ->
csw->con_startup(...) ->//即fbcon_startup(video/console/fbcon.c)
info = registered_fb[info_idx];
info->fbops->fb_open(...)(video/s3c2410fb.c)
bind_con_driver(char/vt.c) ->
visual_init(char/vt.c) ->
vc->vc_sw->con_init //即fbcon_init
fbcon_init(video/console/fbcon.c) ->
//以下准备LOGO
fbcon_prepare_logo(video/console/fbcon.c) ->
fb_prepare_logo(video/fbmem.c) ->
fb_logo.logo = fb_find_logo(depth);//logo.c
//打印Console:swicthing to colour frame buffer device 30x40
update_screen(vc);(include/linux/vt_kern.h) ->
redraw_screen(char/vt.c) ->
vc->vc_sw->con_switch(vc);->//即fbcon_switch(fbcon.c)
fb_show_logo(video/fbmem.c)//显示LOGO - 对/dev/ttyx调用write函数时的过程在使用/dev/ttyx作为控制台的shell中,运行某个程序时,如果里面有“printf(“hello world!”)”字样的语句,它会调用到内核的tty_write函数。
1
2
3
4
5
6
7
8
9
10
11
12tty_write(char/tty_io.c)->
ld = tty_ldisc_ref_wait(tty)//它就是char/n_tty.c中的tty_ldisc_N_TTY
do_tty_write(ld->write,tty,file,buf,count)(char/tty_io.c)->
write_chan(就是上面的ld->write,char/n_tty.c中tty_ldisc_N_TTY的成员函数)->
tty->driver->write(即con_write,char/vt.c)->
do_con_write(char/vt.c)->
vc->vc_sw->con_putcs(即fbcon_putcs,video/console/fbcon.c)->
ops->putcs(即bit_putcs,video/console/bitblit.c)->
dst = fb_get_buffer_offset(video/fbmem.c)//获取要写入的显存位置
bit_putcs_aligned/bit_putcs_unaligned(video/console/bitblit.c)
src = vc->vc_font.data + (src_readw(s++)&charmask)*cellsize;//获得字符的点阵
__fb_pad_aligned_buffer(fb.h)//将点阵写入显存
然后会调用行规程的write_chan函数,它又会调用“tty->driver->write”,对于串口,它是drivers/serial/serial_core.c
中的uart_write函数,它直接输出ASCII字符;对于显示器,它是drivers/char/vt.c
中的con_write函数,它更复杂。在LCD显示器上显示字符时,先要根据这些字符得到它们的点阵,然后再将它们画出来。drivers/video/console/fbcon.c
中的fbcon_putcs函数通过drivers/video/console/bitblit.c
、drivers/video/fbmem.c
提供的一些函数来获得点阵、写到显存中去。其中的“vc->vc_font.data”指向某个字库,以字符为索引即可找到它的点阵。在drivers/video/console/fonts.c
文件中定义了一个fonts数组,每个表项是一个字库,比如font_vga_8x8、font_vga_8x16等。在devices/video/fbcon.c
中初始化frame buffer控制台时,会把vc->vc_font.data指向某个字库。 - USB键盘按下时的函数调用过程
与串口相似,键盘的读取以中断来驱动。以USB键盘为例,调用过程如下:hid_irq_in是USB中断传输方式的中断处理函数,当键盘被按下时,它导致后续的一系列函数被调用,与图24.1对应,它从底层的1
2
3
4
5
6
7
8
9
10
11
12hid_irq_in(hid/usbhid/hid-core.c) ->
hid_input_report(hid/hid-core.c) ->
hid_input_field(hid/hid-core.c) ->
hid_process_event(hid/hid-core.c) ->
hidinput_hid_evenet(hid/hid-input) ->
input_event(input/input.c) ->
dev->event(...)
handle->handler->event,即kbd_event(char/keyboard.c) ->
kbd_rawcode/kbd_keycode(char/keyboard.c) ->
put_queue(vc,data)(char/keyboard.c) ->//数据放入终端缓冲区
tty_insert_flip_char(include/linux/tty_flip.h)//放数据
con_schedule_flip(kbd_kern.h)//唤醒等待数据的进程drivers/hid/usbhid/hid-core.c
一直向上调用到drivers/input/input.c
中的input_event函数,接着input_event函数根据调用drivers/char/keyboard.c
注册的处理函数将数据放入虚拟终端设备的缓冲区中,然后等待数据的进程。
S3C2410/S3C2440 LCD控制器驱动程序移植
从图24.1可知,架构相关的代码为drivers/video/s3c2410fb.c
,移植的思想是一样的:先确定LCD控制器所用的资源,然后把它们加入平台设备结构,最后修改代码是这些资源可用。
硬件连线图如下图所示:
平台设备结构
LCD控制器的平台设备在arch/arm/plat-s3c24xx/devs.c
中定义,它所用的资源都是固定的,不需要任何改动。它的平台设备结构定义如下:
1 | /* LCD Controller */ |
s3c_device_lcd
结构,已经加入了S3C2410、S3C2440开发板的设备列表中了。
arch/arm/mach-s3c2410/mach-smdk2410.c
1
2
3
4
5static struct platform_device *smdk2410_devices[] __initdata = {
...
&s3c_device_lcd,
...
};arch/arm/mach-s3c2440/mach-smdk2440.c
而LCD控制器驱动程序1
2
3
4
5static struct platform_device *smdk2440_devices[] __initdata = {
...
&s3c_device_lcd,
...
};drivers/video/s3c2410fb.c
的入口函数为:平台设备s3c_device_lcd和平台驱动s3c2410fb_driver的名字都是“s3c2410-lcd”,所以注册了s3c2410fb_driver之后,它的s3c2410fb_probe函数将被调用来设置LCD控制器。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15static struct platform_driver s3c2410fb_driver = {
.probe = s3c2410fb_probe,
.remove = s3c2410fb_remove,
.suspend = s3c2410fb_suspend,
.resume = s3c2410fb_resume,
.driver = {
.name = "s3c2410-lcd",
.owner = THIS_MODULE,
},
};
int __devinit s3c2410fb_init(void)
{
return platform_driver_register(&s3c2410fb_driver);
}
底层驱动代码分析及修改
s3c2410fb_probe函数完成初始化LCD控制器、注册中断处理函数、注册frame buffer设备等工作,它的流程图如下图所示:
这个函数中,与单板相关的就是其中的mach-info结构。它是平台设备s3c_device_lcd结构中的dev.platform_data成员,读者可以查看s3c2410fb_init_registers函数来了解它的功能。但是在前面看到的s3c_device_lcd结构中,并没有指定这个成员。它在其他函数中设置;对于S3C2440,单板初始化函数smdk2440_machine_init调用s3c24xx_fb_set_platdata函数来设置;对于S3C2410,没有设置。
smdk2440_machine_init函数在arch/arm/mach-s3c2440/mach-smdk2440.c
中,如下所示:
1 | static void __init smdk2440_machine_init(void) |
smdk2440_lcd_cfg结构表示LCD控制器的一些配置,比如分辨率、时间特性等。
s3c24xx_fb_set_platdata函数在arch/arm/plat-s3c24xx/devs.c
中,它直接将参数smdk2440_lcd_cfg赋给设置平台设备s3c_device_lcd结构中的dev.platform_data成员。代码如下:
1 | void __init s3c24xx_fb_set_platdata(struct s3c2410fb_mach_info *pd) |
所以,对于S3C2440,需要修改smdk2440_lcd_cfg结构;对于S3C2410,仿照S3C2410增加一个smdk2410_lcd_cfg结构,并调用s3c24xx_fb_set_platdata函数来设置它。
smdk2440_lcd_cfg是s3c2410fb_mach_info结构类型,这个类型在include/asm-arm/arch-s3c2410/fb.h
文件中定义,下面分析它的各个成员的意义。
1 | struct s3c2410fb_mach_info { |
fixed_syncs
被设为1时表示“固定的”时间参数和边框大小,这意味着用户程序无法调整分辨率等参数,因为底层驱动不修改时间参数和边框大小。从s3c2410fb.c中的相关代码来看,它就是不在重新设置LCDCON2/3/4寄存器中的相关位。type
表示LCD的类型,从LCDCON1寄存器位[6:5]可以知道它有4种取值,如下所示:
1 | 00 = 4-bit dual scan display mode (STN) |
width
、height
用来设置图像的宽度和高度,它们取xres、yres的默认值。s3c2410fb_val
结构的定义如下,xres、yres和bpp分别表示图像宽度、高度和像素位宽的最小、最大、默认值。
1 | struct s3c2410fb_val { |
struct s3c2410fb_hw regs
表示LCDCON1-LCDCON5共5个LCD控制器的控制寄存器。它们用来设置LCD类型、像素数据的格式。gpcup
、gpcup_mask
、gpccon
、gpccon_mask
、gpdup
、gpdup_mask
、gpdcon
、gpdcon_mask
用来设置GPC、GPD两组GPIO引脚,gpcup
和gpccon_mask
两个成员被用来设置GPCUP寄存器:gpcup
表示新值,gpccon_mask
表示要设置的位。lpcsel
表示LPCSEL寄存器,它用来支持SEC公司生产的TFT LCD,对于一般的LCD,不用设置这个寄存器。
本开发板使用240x320,16bpp的TFT LCD,内核自带的smdk2440_lcd_cfg结构并不适用于这个开发板,并且它的设置有一些错误:没有指定GPIO寄存器的值,“type”设置错了。原来的值如下:
1 | static struct s3c2410fb_mach_info smdk2440_lcd_cfg __initdata = { |
它把GPIO的值屏蔽掉了,原因是“currently setup by downloader”,这也许是这个驱动的开发者在调试时,另外使用某种下载器来设置GPIO。
上面的type
被设置为S3C2410_LCDCON1_TFT16BPP
,这是错误的,“type”表示“类型”,而“S3C2410_LCDCON1_TFT16BPP”表示“TFT”类型下数据的格式。应该设为以下4个值之一:
1 |
下面修改代码
- 对于S3C2440单板
修改smdk2440_lcd_cfg结构,它在arch/arm/mach-s3c2440/mach-smdk2440.c
文件中,修改后的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61/* LCD driver info */
static struct s3c2410fb_mach_info smdk2440_lcd_cfg __initdata = {
.regs = {
.lcdcon1 = S3C2410_LCDCON1_TFT16BPP |
S3C2410_LCDCON1_TFT |
S3C2410_LCDCON1_CLKVAL(0x04),
.lcdcon2 = S3C2410_LCDCON2_VBPD(1) |
S3C2410_LCDCON2_LINEVAL(319) |
S3C2410_LCDCON2_VFPD(5) |
S3C2410_LCDCON2_VSPW(1),
.lcdcon3 = S3C2410_LCDCON3_HBPD(36) |
S3C2410_LCDCON3_HOZVAL(239) |
S3C2410_LCDCON3_HFPD(19),
.lcdcon4 = S3C2410_LCDCON4_MVAL(13) |
S3C2410_LCDCON4_HSPW(5),
.lcdcon5 = S3C2410_LCDCON5_FRM565 |
S3C2410_LCDCON5_INVVLINE |
S3C2410_LCDCON5_INVVFRAME |
S3C2410_LCDCON5_PWREN |
S3C2410_LCDCON5_HWSWP,
},
/* currently setup by downloader */
.gpccon = 0xaaaaaaaa,
.gpccon_mask = 0xffffffff,
.gpcup = 0xffffffff,
.gpcup_mask = 0xffffffff,
.gpdcon = 0xaaaaaaaa,
.gpdcon_mask = 0xffffffff,
.gpdup = 0xffffffff,
.gpdup_mask = 0xffffffff,
.fixed_syncs = 1,
.type = S3C2410_LCDCON1_TFT,
.width = 240,
.height = 320,
.xres = {
.min = 240,
.max = 240,
.defval = 240,
},
.yres = {
.min = 320,
.max = 320,
.defval = 320,
},
.bpp = {
.min = 16,
.max = 16,
.defval = 16,
},
}; - 对于S3C2410单板
仿照arch/arm/mach-s3c2440/mach-smdk2440.c
来修改arch/arm/mach-s3c2410/mach-smdk2410.c
①增加smdk2410_lcd_cfg结构
直接把smdk2440_lcd_cfg的内容搬到mach-smdk2410.c中,改名为smdk2410_lcd_cfg即可。
②使用smdk2410_lcd_cfg结构
在S3C2410单板初始化函数smdk2410_init中,调用s3c24xx_fb_set_platdata函数。除增加的smdk2410_lcd_cfg结构外,还要增加如下所示的代码:1
2
3
4
5
6
7
8
9
10
11
12
...
/* LCD driver info */
static struct s3c2410fb_mach_info smdk2440_lcd_cfg __initdata = {
...
};
...
static void __init smdk2410_init(void)
{
s3c24xx_fb_set_platdata(&smdk2410_lcd_cfg);
...
}
配置内核以使用LCD
对LCD的配置有两方面,一是frame buffer方面的配置,二是控制台方面的配置。
配置内容如下:
1 | Device Drivers ---> |
- 通过LCD显示内核信息
以前使用串口作为控制台(打印内核信息)时,命令行参数为“console=ttySAC0”,现在可以多加一项,比如“console=ttySAC0 console=tty1”。tty1表示第一个虚拟终端,tty2表示第二个虚拟终端,而tty0表示当前的虚拟终端。 - 操作/dev/tty1输出字符:如果使用mdev机制,这个步骤可以省略。在串口控制台,使用“echo hello > /dev/tty0”命令可以在LCD上显示“hello”字符串。
1
2
3
4
5
6
7mknod /dev/tty0 c 4 0
mknod /dev/tty1 c 4 1
mknod /dev/tty2 c 4 2
mknod /dev/tty3 c 4 3
mknod /dev/tty4 c 4 4
mknod /dev/tty5 c 4 5
mknod /dev/tty6 c 4 6 - 操作/dev/fb0绘制图像
首先如下创建设备文件,如果使用mdev机制,这个步骤可以省略:然后使用frame buffer测试程序执行“fb_test /dev/fb0”即可在LCD上看到很多同心圆,并且在控制台打印出frame buffer的属性。1
mknod /dev/fb0 c 29 0
USB驱动程序移植
USB驱动程序概述
USB(Universal Serial Bus)即“通用串行外部总线”,在各种场所已经大量使用。它接口简单(只有5V和GND、两根数据线D+和D-),可以外接硬盘、键盘、鼠标、打印机等多种设备。要使用尽可能少的接口支持尽可能多的外设,USB是一个好的选择,在嵌入式设备中尤其如此。
USB总线规范有1.1版和2.0版。USB1.1支持两种传输速率:低速(Low Speed)1.5Mbit/s、全速12Mbit/s,对于鼠标、键盘、CD-ROM等设备,这样的速率足够。但是在访问硬盘、摄像机时,就显得很慢。为此,USB2.0提供了一种更高的传输速率:高速,它可以达到480Mbit/s。USB2.0向下兼容USB1.1,可以遵循USB1.1规范的设备连接到USB2.0控制器上,也可以把USB2.0的设备USB1.1控制器上。
USB总线的硬件拓扑结构如下图所示。
USB主机控制器(USB Host Controller)通过根集线器(Root Hub)与其他设备相连接。集线器也属于USB设备,通过它可以在一个USB接口上扩展出多个接口。除根集线器外,最多可以层叠5个集线器,每条USB电缆的最大长度是5m,所以USB总线的最大距离为30m。一条USB总线上可以外接127个设备,包括根集线器和其他集线器。整个结构图就是一个星状结构,一条USB总线上所有设备共享一条通往主机的数据通道,同一时刻只能有一个设备与主机通信。
通过USB主机控制器来管理外接的USB设备,USB主机控制器共分3种,UHCI、OHCI和EHCI,其中的“HCI”表示“Host Controller Interface”。UHCI、OHCI属于USB1.1的主机控制器规范,而EHCI是USB2.0的主机控制规范。UHCI(Universal HCI),它是由Intel公司制定得标准,它的硬件做的事情少,这使得软件比较复杂。与之相对的是OHC(Open HCI),它由Compaq、Microsoft和National Semiconductor联合制定,在硬件方面它具备更多的智能,使得软件相对简单。
这些差别只存在于底层的USB主机控制器的驱动程序,对它之上的软件没有影响。USB2.0的主机控制程序只有EHCI(Enhanced HCI)一种
在配置内核的时候,经常可以看到“HCD”字样,它表示“Host Controller Drivers”,即主机控制器驱动程序。比如有uhci-hcd、ohci-hcd、ehci-hcd等驱动模块。
USB驱动程序分两类:USB主机控制器驱动程序(Host Controller Drivers)、USB设备驱动程序(USB device drivers)。它们在内核中的层次如图所示。
USB主机控制器驱动程序提供访问USB设备的接口,它只是一个“数据通道”,至于这些数据有什么用,这要靠上层的USB设备驱动程序来解释。USB设备驱动程序使用下层驱动提供的接口来访问USB设备,不需要关心传输的具体细节。
配置内核支持USB键盘、USB鼠标和USB硬盘
S3C2410/S3C2440的USB控制器有如下特性
符合OHCI1.0规范
支持USB1.1版本
有两个插口
支持低速设备和全速设备
Linux内核中对OHCI主机控制器支持完善,并有多种USB设备驱动程序。Linux2.6.22.6也已经支持S3C2410/S3C2440的USB控制器,只不过第二个插口上电后默认为USB Device插口,如果要将它改为USB Host插口(比如没有USB集线器,却需要同时接入USB键盘、USB鼠标时),只要设置MISCCR寄存器的位3即可,所有的修改都在文件drivers/usb/host/ohci-s3c2410.c
中完成,代码如下:
1 |
|
现在只需要配置内核启用它们:
1 | Device Drivers ---> |
USB控制器的时钟是在U-Boot中设置的,UCLK必须设为48MHZ。
USB设备的使用
连接USB设备时需要注意:S3C2410/S3C2440既可以作为USB主机,也可以作为USB设备。作为USB主机时对外提供两个接口,对应板上叠起来的两个USB接口,下面的称为HOST1,上面的称为HOST2;作为USB设备时,对外也提供一个接口,对应板上的USB_DEVICE接口。
HOST2和USB_DEVICE在S3C2410/S3C2440上的引脚是复用的。要在开发板上使用两个USB设备时,除HOST1外,可以设置跳线使用HOST2;要使用更多的USB设备,必须通过USB集线器来连接。
使用LCD和USB键盘作为终端
现有的内核已经支持LCD和USB键盘,可以使用它们来作为控制台、终端了。前面说过,在命令行参数中增加“console=tty1”就可以在LCD上显示内核信息,不过要想使用它们来登录系统,需要修改/etc/inittab文件,增加以下6行:
1 | tty1::askfirst:-/bin/sh |
它们在6个虚拟终端上启动shell程序,接上USB键盘和LCD后,可以看到如下字样的提示信息:
1 | Please press Enter to activate this consorl |
在键盘上按回车键,就可以像在串口终端上一样使用USB键盘、LCD来控制系统了。
使用U盘
首先在开发板上创建如下设备文件
1 | mknod /dev/sda b 8 0 |
接U盘后,即可像前面使用硬盘、SD卡一样来使用U盘了。
1 | fdsik /dev/sda //进入菜单,对U盘进行分区,修改分了一个主分区/dev/sda1 |