以下内容源于韦东山嵌入式课程的学习与整理,如有侵权请告知删除。
之前在博文中学习过LCD(SoC是S5PV210),作为对比,本文学习S3C2440这款SoC的LCD方面的内容。主要涉及以下三个内容:
一、LCD的硬件原理
1.1 LCD与SoC的连接图
随意打开任何一个开发板的原理图,如有它接有LCD的话,其引脚都是类似的。比如JZ2440开发板中,LCD接口的插座如下所示:
1.2 简析LCD的操作原理
由上图可知LCD有很多引脚,这些引脚如何理解呢?我们先看看LCD的操作原理。
如下图所示,早期的屏幕背后有一把电子枪,它一边移动,一边发出各种颜色的光线并打在各个像素上。
这里面有很多细节,我们一个个进行梳理:
1、电子枪怎么知道它应该移动到下一个像素了(向某个像素打出颜色之后,应该移动到下一个像素)?
有一条CLK时钟线与LCD相连。每收到一次CLK信号,电子枪就移动一个像素。
2、电子枪向某个像素发出颜色,那如何确定这个像素对应什么颜色呢?
这个像素的颜色,由连接LCD的三组信号线R、G、B来确定的(注意是3组线而非3根线)。
电子枪会根据RGB三组信号线的数据,发出相应的红绿蓝三种颜色的光线,由这三种颜色的光线合成这个像素的颜色。
3、电子枪从某一行最左边开始移动,一直移动到最右边,接下来应该跳到下一行的最左边。电子枪怎么知道它应该跳到下一行最左边?
有一条HSYNC(水平方向同步信号)信号线与LCD相连。每收到一次HSYNC信号脉冲,电子枪就跳到下一行最左边。
4、电子枪向最后一个像素发出颜色后,应该跳回原点(左上角第一个像素点)。电子枪怎么知道它应该跳回原点了?
有一条VSYNC(垂直方向同步信号)信号线与LCD相连。每收到一次VSYNC信号脉冲,电子枪就跳回原点。
5、上面提到的信号,是由谁发出的?
上面提到的信号,是由LCD控制器发出。
6、RGB三组线的数据从何而来?
这些数据肯定是由我们写程序的人提供的(比如我们想显示一幅图,需要我们提供图片数据)。
在内存中划分出一片区域,我们管它叫做显存(FrameBuffer),里面存放着我们要显示的RGB数据,LCD控制器从里面将数据读取出来后,通过RGB三组线传给电子枪,电子枪再依次将颜色打到显示屏上。
7、每个像素在FrameBuffer中占据多少位(即BPP,Bits Per Pixels)?
在下面的LCD引脚功能图中,可以看到有R0~R7、G0~G7、B0~B7共24个引脚,所以每个像素共占据8*3=24位,即硬件上LCD的BPP是确定的。但我们可以根据实际情况进行取舍,不一定要用完这些线(这24条引脚线只是表明这款LCD的最优能力),比如JZ2440开发板使用16BPP,因此LCD使用R0~R4、G0~G5、B0~B4共16个引脚与SoC相连,如本文开头第一张图所示。
1.3 LCD芯片的时序分析
查阅JZ2440开发板所采用的LCD芯片(其型号是AT043TN24)的数据手册,在P5有以下LCD引脚功能图:
可见LCD有很多信号,这些信号的传输要遵循时序关系。在LCD数据手册P13页有时序图如下:
从最小的像素开始分析(即首先看CLK),从中我们可以看出:
(1)电子枪每次在CLK下降沿(对于这款LCD是在CLK下降沿,对于其他型号可能是上升沿)从数据线Dn7-Dn0上得到数据(Dn7-Dn0上的数据,来源于前面介绍的FrameBuffer),转换成对应的颜色光线并打到像素上,然后移动到下一个像素位置。如此循环,从一行的最左边一直移动到一行的最右边,完成了一行的显示。这里假设一行有x个像素。
(2)当电子枪打完一行最右边的像素之后,就会收到一个水平方向同步信号Hsync。1个Hsync周期由4部分组成:thp、thb、thd、thf。thp信号的脉冲宽度不能太小,否则电子枪可能识别不到这个信号;电子枪正确识别thp信号后,会从最右端移动最左端,这个移动过程需要耗费时间thb,我们称之为移动时间(移动的过程中,LCD控制器不能发出数据);thf表示显示完最右边的像素之后,要经过多长时间电子枪才收到Hsync信号(并不是一显示完最后一个像素,就立马收到Hsync信号)。
(3)当电子枪一行一行地从最左上角(原点)移动到最右下角后,就会收到一个垂直方向同步信号Vsync,表示让电子枪跳回原点。1个Vsync周期也是由4部分组成:tvp、tvb、tvd、tvf,其中tvp是脉冲宽度,tvb是移动时间(移动的过程中,LCD控制器也不能发出数据),tvf表示显示完最下面的一行像素之后,要经过多长时间电子枪才收到Vsync信号。这里假设一共有y行,则LCD的分辨率就是x*y。
(4)时序图中的参数含义以及它们的典型取值,在LCD芯片的数据手册P11有介绍,后续我们需要根据典型取值来设置LCD控制器。
这些时序参数会怎样影响显示呢?详细内容可以参考该博客,其中有一个LCD显示配置示意图:
(1)当电子枪打完一行最右边的像素之后,等待HFP时长,才收到HSYNC信号;当电子枪收到HSYNC信号后,电子枪就会从最右边移到最左边,这移动过程耗费HBP时长。因此,HFP决定了右边黑框,HBP决定了左边黑框,且值越大则黑框越大。
(2)当电子枪打完最左下角的像素之后,需要等待VFP时长,才收到VSYNC信号;当电子枪收到VSYNC信号后,电子枪就会从最左下角的像素处,跳回原点,这移动过程耗费VBP时。因此,VFP决定了下边黑框,VBP决定了上边黑框,且值越大则黑框越大。
(3)我们可以修改HFP、HBP、VFP和VBP的值,以调整边框,中间灰色区域才是有效数据区域。
1.4 LCD编程的思路
根据上面的分析,可以得到LCD编程的思路:
(1)查阅LCD芯片的数据手册,得到相关的时序参数、分辨率、引脚极性等信息;
(2)根据从LCD芯片的数据手册得到的信息,设置LCD控制器寄存器,让其发出正确信号;
(3)在内存中分配一个FrameBuffer,明确BPP(即明确用多少位来表示一个像素),然后把FrameBuffer的首地址告诉LCD控制器;
完成上面步骤后,LCD控制器就会不断地从FrameBuffer中取出像素数据,然后配合其它控制信号,将像素数据发送给电子枪,电子枪再将颜色显示在LCD上面。以后我们想显示图像时,只需要编写程序向FrameBuffer中填入相应数据即可,硬件会自动地完成显示操作。
二、S3C2440的LCD控制器
2.1 LCD控制器的功能
几乎所有型号的ARM芯片,其LCD控制器的主要功能都是类似的,只是寄存器的操作稍有差别。LCD控制器的主要功能包括:
(1)取数据:从FrameBuffer中取出像素的数据。
我们需要将FrameBuffer的首地址、BPP、分辨率等信息告知LCD控制器。
(2)发数据:配合其他信号把像素数据发给LCD。
我们需要查阅LCD芯片的数据手册,得知LCD芯片的时序要求,并根据这些时序要求来设置LCD控制器、设置引脚的极性(比如是上升沿还是下降沿有效,是低脉冲还是高脉冲有效;对于JZ2440所采用的LCD芯片,是在CLK的下降沿获取数据,Hsync是低脉冲有效)。
2.2 LCD控制器的框图
S3C2440数据手册的第15章,介绍了LCD控制器如何支持STN和TFT这两种材质的LCD。我们常用LCD都是TFT材质的,比如JZ2440开发板采用的LCD就是TFT材质的。在P398有LCD控制器的框图:
一旦我们设置好REGBANK(一些寄存器的集合),LCDCDMA会自动(注意它是DMA,说明不需要CPU的参与)将内存中FrameBuffer里的像素数据,通过VIDPRCS发送到引脚VD[23:0]上,再配合TIMEGEN生成的各种的控制信号(通过REGBANK来设置它们的时序),这些像素数据对应的颜色就可以在屏幕上正确地显示出来。
2.3 像素数据的组织格式
S3C2440数据手册的P412,列出了像素数据的几种组织格式,即像素数据在FrameBuffer中是怎么摆放的。下面是几种常见的组织格式。
(1)当你设置LCD控制器输出的是24BPP的数据时,像素数据的组织格式如下:
(2)当你设置LCD控制器输出的是16BPP的数据时(JZ2440开发板正是使用16BPP),像素数据的组织格式如下:
(3)当你设置LCD控制器输出的是8BPP的数据时,像素数据的组织格式如下:
2.4 使用“调色板”
当使用8BPP时,会涉及到一个概念:调色板。
上面1.2小节中提到“LCD使用R0~R4、G0~G5、B0~B4共16个引脚与SoC相连”,于是LCD控制器每次将16位数据传给LCD(它认为1个像素的数据有16位),那么在FrameBuffer中,1个像素的数据就应该占据16位的存储空间,这样的话,LCD控制器从FrameBuffer中取得1个像素的16位数据后,就可以直接由RGB三组线传给LCD,不需要调色板的参与。16BPP时,刚好符合这种情形,如下图所示:
假设现在想节省FrameBuffer的空间,在FrameBuffer中原本用16位来表示1个像素的数据(即16BPP),现在改用8位来表示1个像素的数据(即8BPP),这是否可以呢?也是可以的,但需要解决一个问题:如何由FrameBuffer中8位的数据,得到16位的数据?这需要引入“调色板”。
作画时,我们通常在调色板里先配置好想要的颜色,然后用画笔沾到画布上作画。LCD控制器也借用了这个概念:从FrameBuffer中获得数据1,把这个数据1作为索引(相当于数组的下标),从调色板(相当于数组)中获得对应的数据2,再把数据2发给电子枪。
由于采用8BPP时,某个像素的数据(此时称为索引)在FrameBuffer中占据8位,则调色板中有2^8=256项容器,每一项容器中存放着16位数据。LCD控制器把FrameBuffer中的8位数据当做索引,在调色板中找到它对应的16位数据,然后将这16位数据发给电子枪。
调色板是一块特殊的内存,在使用8BPP时需要先设置调色板。在S3C2440数据手册中搜索“PALETTE”,在P416有以下内容,由此可知:调色板的起始地址0x4D00_0400(这个地址隶属于LCD控制器);一共有256项,每项占据4个字节,但只用到最低2个字节。
采用8BPP时,FrameBuffer中存储的不是真正的像素数据,因此8BPP也叫伪彩色;相对地,16BPP或者24BPP叫真彩色。
2.5 让LCD只显示某一种颜色
(1)对于16BPP和24BPP这些真彩色,需要往FrameBuffer中填充同一数值(某一种颜色所对应的像素数据)。
(2)对于8BPP,可以将调色板设置为同一种颜色(导致不同索引对应的颜色都一样),也可以往FrameBuffer中填充同一数值。
(3)S3C22440有一个“临时调色板”的特性,一旦使能临时调色板,不管FrameBuffer里面是什么数据,都只调用临时调色板中的数据。在S3C22440的数据手册P432有以下内容,它表明:一旦TPAL[24]=0b1,则只会调用TPAL[23:0]中设定的像素的数据。
2.6 LCD控制器的寄存器
LCD控制器的寄存器组,其位含义在S3C2440数据手册P422,包括LCDCON1~LCDCON5、LCDSADDR1~LCDSADDR3等寄存器。
通过这些寄存器,可以设置像素时钟的频率、LCD材质、BPP、使能LCD控制器,可以设置垂直方向、水平方向的相关参数(比如时序参数、一共有多少行像素、每行有多少个像素等等),可以设置FrameBuffer的基地址等内容。
这里先忽略这部份内容,在编程时(第5.1节)再深入讲解。
三、LCD编程_程序框架
本节主要有两个内容:讲解后续程序的框架;准备一个支持Nand、Nor启动的程序(后续我们将在这程序基础上修改)。
3.1 程序框架
LCD编程的框架(与对应的.c文件)如下图所示(尽可能地“高内聚低耦合”):
(1)首先我们需要一个提供操作菜单的测试程序(Icd_test.c),调用画线、画圆、写字函数。
(2)我们的目的,是在LCD上画线与画图(geomentry.c)、写字(font.c),其核心都是画点(farmebuffer.c),属于纯软件。
(3)接下来是LCD芯片自身的特性。不同的LCD,其配置参数也会不一样(时间参数、引脚极性等),体现在Icd_3.5.c 或 lcd_4.3.c。
(4)接下来根据LCD芯片特性来设置S3C2440的LCD控制器,体现在s3c2440_Icd_controller.c文件,它对S3C2440这款SoC的LCD控制器进行了设置。如果采用其他SoC(比如S5PV210),则添加这款SoC的LCD控制器设置代码即可(比如s5pv210_Icd_controller.c)。
(5)最后是LCD硬件操作。只要设置好上层的内容,这部份是硬件自动完成的,无需关注。
后续我们将根据这个框架来编写代码,例如课程安排如下:
3.2 面向对象编程
为了让程序更加好扩展,需要“面向对象编程”(简单地理解,就是定义一个结构体类型,用来描述不同LCD芯片的共同属性)。
(1)不同的LCD芯片(为LCD的共同属性定义一个结构体)
假如有两款不同的LCD芯片(比如3.5寸和4.3寸的LCD),我们如何在程序中表示它们?
首先抽象出3.5寸、4.3寸LCD的共同属性(例如都有初始化函数init、LCD的型号名字name),则我们可以新建一个上层文件lcd.c,在里面(更准确地说,应该在lcd.h文件中)定义一个struct lcd结构体,以及一个struct lcd *类型的指针:
//lcd.c
struct lcd
{
void (*init)(void);
char* name;
};
struct lcd *ptr;
然后在下一层文件lcd_3.5.c、lcd_4.3.c中,分别定义一个struct lcd类型的变量,用来表示对应LCD的属性:
//lcd_3.5.c
struct lcd lcd_3.5_ptr
{
.init = lcd_3.5_init;
.name = xxxy;
};
//lcd_4.3.c
struct lcd lcd_4.3_ptr
{
.init = lcd_4.3_init;
.name = yyy;
};
如此一来,我们只需要根据实际的 LCD 类型,将上层文件lcd.c中的ptr指针,指向下层文件Icd_3.5.c中的lcd_3.5_ptr变量,或者下层文件Icd_4.3.c中的lcd_4.3_ptr变量,就可以调用对应的init函数了。以后要添加其他LCD类型时(例如7寸的),只需要添加这款LCD自身特性层的代码即可(比如lcd_7.0.c),根本不会影响上一层lcd.c文件中的代码,这就是结构化编程好处的简单体现。
(2)不同SoC的LCD控制器(为LCD控制器的共同属性定义一个结构体)
假设在上层文件lcd_control.c中(其实应该在lcd_control.h)定义了一个结构体struct lcd_controller,它表示LCD控制器的共同属性:
struct lcd_controller{
void (*pin_init)(void);
void (*timeseq_init)(void);
void (*framebuffer_init)(void);
}
在下一层文件s5pv210_Icd_controller.c 、s3c2440_Icd_controller.c中都有它的实例化,分别表示s5pv210、s3c2440的LCD控制器:
//s5pv210_lcd_controller.c文件
struct lcd_controller s5pv210_lcd_controller
{
.pin_init = s5pv210_pin_init;
.timeseq_init = s5pv210_timeseq_init;
.framebuffer_init = s5pv210_framebuffer_init;
}
//s3c2440_lcd_controller.c文件
struct lcd_controller s3c2440_lcd_controller
{
.pin_init = s3c2440_pin_init;
.timeseq_init = s3c2440_timeseq_init;
.framebuffer_init = s3c2440_framebuffer_init;
}
在上层lcd_control.c文件中定义一个struct lcd_controller* 类型的指针ptr,当使用JZ2440开发板时,让ptr指针指向s3c2440_Icd_controller这个变量;当使用X210开发板时,让ptr指针s5pv210_Icd_controller这个变量。这样一来,无论我们更换哪款开发板,也不会影响上层的代码,这就是结构化编程好处的简单体现。
另外,假如struct lcd_controller结构体中还有一个name成员,则我们可以在上层文件lcd_control.c中根据名字来选择与之匹配的LCD控制器(见7.1节内容)。这种思想在驱动代码中很常见。
3.3 支持Nand与Nor启动的程序
之前的程序大小都没超过4K,因此可以Nor启动或者Nand启动;现在的LCD程序大小超过4K了,因此需要修改启动部分的代码。
目前还没有学习Nand Flash,因此将链接中与Nand Flash有关的代码复制到当前代码中即可(主要是5.5小节,涉及以下文件):
得到下图箭头所指的文件夹,它就是我们所准备的支持Nand、Nor启动的程序,后续我们将在这程序基础上修改。
四、LCD编程_抽象出重要结构体(Icd.h、Icd_controller.h)
根据3.1节的框架图,我们新建一个lcd文件夹,在里面新建以下空白文件:
- 测试层:Icd_test.c
- 纯软件:framebuffer.c(画点)、geometry.c(画线与画圆)、font.c(写字)
- LCD芯片自身特性层:Icd.c(上层,屏蔽了lcd_4.3.c的细节)、Icd_4.3.c(下层)
- LCD控制器层:Icd_controller.c(上层)、s3c2440_lcd_controller.c(下层)
我们先抽象出LCD芯片参数结构体、LCD控制器结构体,如下面4.1、4.2所示。
4.1 struct lcd_params结构体(表征LCD芯片参数,Icd.h)
LCD芯片的参数,包括引脚极性、时序、BPP、分辨率、FrameBuffer的首地址等内容。我们根据面向对象的思想,将这些内容封装成一个结构体struct lcd_params(如果是C++则是类的对象,对于C则是结构体),定义在lcd.c对应的头文件Icd.h中(从下往上看):
//lcd.h文件
#ifndef _LCD_H //防止头文件被重复包含
#define _LCD_H
enum {
NORMAL = 0, //正常极性
INVERT = 1, //反转极性
};
//引脚的极性
typedef struct pins_polarity {
int vclk; /* normal: 在下降沿获取数据 */
int rgb; /* normal: 高电平表示1 */
int hsync; /* normal: 高脉冲 */
int vsync; /* normal: 高脉冲 */
}pins_polarity, *p_pins_polarity;
//时序参数(包括垂直方向、水平方向)
typedef struct time_sequence {
/* 垂直方向 */
int tvp; /* vysnc脉冲宽度 */
int tvb; /* 上边黑框, Vertical Back porch */
int tvf; /* 下边黑框, Vertical Front porch */
/* 水平方向 */
int thp; /* hsync脉冲宽度 */
int thb; /* 左边黑框, Horizontal Back porch */
int thf; /* 右边黑框, Horizontal Front porch */
int vclk;
}time_sequence, *p_time_sequence;
typedef struct lcd_params {
/* 引脚极性 */
pins_polarity pins_pol;
/* 时序 */
time_sequence time_seq;
/* 分辨率, bpp */
int xres;
int yres;
int bpp;
/* framebuffer的地址 */
unsigned int fb_base;
//后续根据讲解的深入,还可能添加其他成员,比如:char* name;
}lcd_params, *p_lcd_params;
#endif /* _LCD_H */
然后我们需要在LCD_4.3.c文件中,创建一个struct lcd_params结构体的实例,表征着4.3寸LCD芯片的参数(见第六节的内容)。另外在lcd.c文件中(见第七节的内容)会有一个struct lcd_params*类型的指针,指向LCD_4.3.c文件中的实例,以屏蔽和封装LCD_4.3.c文件的细节,实现结构化编程。
4.1.1 struct pins_polarity结构体(引脚的极性)
引脚的极性,是指上升沿还是下降沿有效、高电平还是低电平有效。
代码中定义了两个宏NORMAL(=0)、INVERT(=1)。我们设置LCD控制器的寄存器时,需要设置某些引脚的极性(比如RGB、VSYNC、HSYNC、CLK引脚的极性),一般情况下都设置为NORMAL。至于NORMAL是对应着高电平还是低电平有效、是上升沿还是下降沿有效,就需要查看LCD芯片的时序图。时序图一般都是NORMAL对应的情形,如果你想与之相反,将极性设置为INVERT即可。
enum {
NORMAL = 0, //正常极性
INVERT = 1, //反转极性
};
//引脚的极性
typedef struct pins_polarity {
int vclk; /* normal: 在下降沿获取数据 */
int rgb; /* normal: 高电平表示1 */
int hsync; /* normal: 高脉冲 */
int vsync; /* normal: 高脉冲 */
//后面可能还会添加其他信号,这里只列出部分
}pins_polarity, *p_pins_polarity;
比如JZ2440所采用的LCD,是在像素时钟的下降沿获取像素数据(见1.3小节的时序分析)。在S3C2440数据手册P426有以下内容,可知LCDCON5[10]=0时表示在像素时钟的下降沿获取像素数据,=1则表示在上升沿,所以要将LCDCON5[10]设置为NORMAL(=0)。
4.1.2 struct time_sequence结构体(时序参数)
由1.3小节中的时序图可知,时序参数包括:水平方向的thp、thb、thd、thf,垂直方向的tvp、tvb、tvd、tvf。
//时序参数(包括垂直方向、水平方向)
typedef struct time_sequence {
/* 垂直方向 */
int tvp; /* vysnc脉冲宽度 */
int tvb; /* 上边黑框, Vertical Back porch */
int tvf; /* 下边黑框, Vertical Front porch */
/* 水平方向 */
int thp; /* hsync脉冲宽度 */
int thb; /* 左边黑框, Horizontal Back porch */
int thf; /* 右边黑框, Horizontal Front porch */
int vclk;
}time_sequence, *p_time_sequence;
4.2 struct lcd_controller结构体(表征LCD控制器,Icd_controller.h)
该结构体定义在Icd_controller.h文件中,包括LCD初始化函数、使能函数,表征着LCD控制器(的某些行为):
#ifndef _LCD_CONTROLLER_H
#define _LCD_CONTROLLER_H
#include "lcd.h"
typedef struct lcd_controller {
void (*init)(p_lcd_params plcdparams);
void (*enable)(void);
void (*disable)(void);
}lcd_controller, *p_lcd_controller;
#endif /* _LCD_CONTROLLER_H */
然后我们需要在s3c2440_lcd_controller.c文件中,定义一个struct lcd_controller结构体的实例,它表示S3C2440这款SoC的LCD控制器(见第五节开头代码的最后部分)。另外在lcd_controller.c文件中会有一个struct lcd_controller*类型的指针,指向s3c2440_lcd_controller.c文件中的实例,以屏蔽与封装s3c2440_lcd_controller.c文件的细节,实现结构化编程。
五、LCD编程_初始化LCD控制器(s3c2440_lcd_controller.c)
4.2小节最后提到,我们需要在s3c2440_lcd_controller.c文件中,定义一个struct lcd_controller结构体的实例,如下所示:
struct lcd_controller s3c2440_lcd_controller = {
.init = s3c2440_lcd_controller_init,
.enable = s3c2440_lcd_controller_enalbe,
.disable = s3c2440_lcd_controller_disable,
};
接下来我们需要实现这些函数指针所指向的函数。s3c2440_lcd_controller.c文件完整代码如下(从下往上看):
#define HCLK 100
void jz2440_lcd_pin_init(void)
{
/* 初始化引脚 : 背光引脚 */
GPBCON &= ~0x3;
GPBCON |= 0x01;
/* LCD专用引脚 */
GPCCON = 0xaaaaaaaa;
GPDCON = 0xaaaaaaaa;
/* PWREN */
GPGCON |= (3<<8);
}
/* 根据传入的LCD参数设置LCD控制器 */
void s3c2440_lcd_controller_init(p_lcd_params plcdparams)
{
int pixelplace;
unsigned int addr;
jz2440_lcd_pin_init();
/* [17:8]: clkval, vclk = HCLK / [(CLKVAL+1) x 2]
* 9 = 100M /[(CLKVAL+1) x 2], clkval = 4.5 = 5
* CLKVAL = 100/vclk/2-1
* [6:5]: 0b11, tft lcd
* [4:1]: bpp mode
* [0] : LCD video output and the logic enable/disable
*/
int clkval = (double)HCLK/plcdparams->time_seq.vclk/2-1+0.5;
int bppmode = plcdparams->bpp == 8 ? 0xb :\
plcdparams->bpp == 16 ? 0xc :\
0xd; /* 0xd: 24bpp */
LCDCON1 = (clkval<<8) | (3<<5) | (bppmode<<1) ;
/* [31:24] : VBPD = tvb - 1
* [23:14] : LINEVAL = line - 1
* [13:6] : VFPD = tvf - 1
* [5:0] : VSPW = tvp - 1
*/
LCDCON2 = ((plcdparams->time_seq.tvb - 1)<<24) | \
((plcdparams->yres - 1)<<14) | \
((plcdparams->time_seq.tvf - 1)<<6) | \
((plcdparams->time_seq.tvp - 1)<<0);
/* [25:19] : HBPD = thb - 1
* [18:8] : HOZVAL = 列 - 1
* [7:0] : HFPD = thf - 1
*/
LCDCON3 = ((plcdparams->time_seq.thb - 1)<<19) | \
((plcdparams->xres - 1)<<8) | \
((plcdparams->time_seq.thf - 1)<<0);
/*
* [7:0] : HSPW = thp - 1
*/
LCDCON4 = ((plcdparams->time_seq.thp - 1)<<0);
/* 用来设置引脚极性, 设置16bpp, 设置内存中象素存放的格式
* [12] : BPP24BL
* [11] : FRM565, 1-565
* [10] : INVVCLK, 0 = The video data is fetched at VCLK falling edge
* [9] : HSYNC是否反转
* [8] : VSYNC是否反转
* [7] : INVVD, rgb是否反转
* [6] : INVVDEN
* [5] : INVPWREN
* [4] : INVLEND
* [3] : PWREN, LCD_PWREN output signal enable/disable
* [2] : ENLEND
* [1] : BSWP
* [0] : HWSWP
*/
pixelplace = plcdparams->bpp == 24 ? (0) : |\
plcdparams->bpp == 16 ? (1) : |\
(1<<1); /* 8bpp */
LCDCON5 = (plcdparams->pins_pol.vclk<<10) |\
(plcdparams->pins_pol.rgb<<7) |\
(plcdparams->pins_pol.hsync<<9) |\
(plcdparams->pins_pol.vsync<<8) |\
(plcdparams->pins_pol.de<<6) |\
(plcdparams->pins_pol.pwren<<5) |\
(1<<11) | pixelplace;
/* framebuffer地址 */
/*
* [29:21] : LCDBANK, A[30:22] of fb
* [20:0] : LCDBASEU, A[21:1] of fb
*/
addr = plcdparams->fb_base & ~(1<<31);
LCDSADDR1 = (addr >> 1);
/*
* [20:0] : LCDBASEL, A[21:1] of end addr
*/
addr = plcdparams->fb_base + plcdparams->xres*plcdparams->yres*plcdparams->bpp/8;
addr >>=1;
addr &= 0x1fffff;
LCDSADDR2 = addr;//
}
void s3c2440_lcd_controller_enalbe(void)
{
/* 背光引脚 : GPB0 */
GPBDAT |= (1<<0);
/* pwren : 给LCD提供AVDD */
LCDCON5 |= (1<<3);
/* LCDCON1'BIT 0 : 设置LCD控制器是否输出信号 */
LCDCON1 |= (1<<0);
}
void s3c2440_lcd_controller_disable(void)
{
/* 背光引脚 : GPB0 */
GPBDAT &= ~(1<<0);
/* pwren : 给LCD提供AVDD */
LCDCON5 &= ~(1<<3);
/* LCDCON1'BIT 0 : 设置LCD控制器是否输出信号 */
LCDCON1 &= ~(1<<0);
}
struct lcd_controller s3c2440_lcd_controller = {
.init = s3c2440_lcd_controller_init,
.enalbe = s3c2440_lcd_controller_enalbe,
.disable = s3c2440_lcd_controller_disable,
};
5.1 s3c2440_lcd_controller_init函数
首先是s3c2440_lcd_controller_init函数,需要我们依次设置LCD控制器的寄存器。
LCD控制器的寄存器组,其位含义在S3C2440数据手册P422,包括LCDCON1~LCDCON5、LCDSADDR1~LCDSADDR3等寄存器。
通过这些寄存器,可以设置像素时钟的频率、LCD材质、BPP、是否使能LCD控制器,可以设置垂直方向与水平方向的相关参数(比如时序参数、分辨率),可以设置FrameBuffer的基地址等内容。
5.1.1 LCDCON1寄存器
该寄存器的位含义如下图所示,主要用来设置像素时钟的频率、LCD材质、BPP、是否使能LCD控制器。
(1)bit[27:18],是只读的数据位,不需要设置。
(2)bit[17:8],用来设置像素时钟的频率(注意计算公式中的VCLK才是像素时钟频率 )。
我们的LCD材质是TFT,所以计算公式为 VCLK = HCLK / [(CLKVAL+1) x 2] ( CLKVAL ≥ 0 )。
其中HCLK=100MHz(HCLK用于LCD控制器,在链接中被设置为100MHz);
VCLK是什么意思?在S3C2440数据手册P51有以下内容,说明它就是像素时钟的频率:
像素时钟频率VCLK是多少?在LCD芯片的数据手册P11有以下内容,说明5MHz<VCLK<12MHz都行,我们选择典型取值9MHz。
根据上面公式可以推算出CLKVAL≈4.6,这里取为5,则真正的VCLK=100/[(5+1)*2]=8.3MHz,满足5MHz<VCLK<12MHz。
也就是说我们直接往bit[17:8]中写入数值5就好,但这样就写死了。注意到VCLK是LCD芯片的一个参数(见4.1.2小节),那可以在程序中根据plcdparams这个传参,来设置LCDCON1[17:8]即CLKVAL的值。
由于VCLK=plcdparams->time_seq.vclk,根据计算公式有:plcdparams->time_seq.vclk=HCLK/ [(CLKVAL+1) x 2],所以可以得到:
CLKVAL=(HCLK/plcdparams->time_seq.vclk)/2-1+0.5(+0.5表示向上取整,是根据计算结果肉眼写的0.5)。
课程的代码最后选择直接往LCDCON1[17:8]写数值5,并没有在代码中写出上面的计算公式。
(3)bit[7]不用管,默认值即可。
(4)bit[6:5],表示外接LCD的材质。这里配置为0b11,表示是TFT材质的LCD。TFT和STN的区别可以自行百度,或者在配套书籍P197。我们现在用的LCD,一般都是TFT材质的。
(6)bit[4:1],用来设置BPP是多少。需要根据传入的plcdparams->bpp配置为相应的数值(假设只支持8BPP、16BPP、24BPP)。
(7)bit[0],表示是否使能LCD控制器(1表示使能,0表示关闭)。这里暂时不设置,在s3c2440_lcd_controller_enalbe函数中才设置。
综上所述,s3c2440_lcd_controller_init函数中,LCDCON1寄存器设置代码如下:
//int clkval = (double)HCLK/plcdparams->time_seq.vclk/2-1+0.5;
int clkval = 5;
int bppmode = plcdparams->bpp == 8 ? 0xb :\
plcdparams->bpp == 16 ? 0xc :\
0xd; /* 0xd: 24,32bpp */
LCDCON1 = (clkval<<8) | (3<<5) | (bppmode<<1) ;
//外接LCD材质
5.1.2 LCDCON2寄存器
该寄存器的位含义如下图所示,主要用来设置垂直方向有关的时序参数(垂直方向的移动时间、LCD芯片一共有多少行像素、垂直方向的等待时间、垂直方向同步信号的脉冲宽度)。
要理解与设置上图四个参数的含义,需要对比S3C2440的LCD控制器时序图(见数据手册P418)、具体LCD芯片的时序图(见1.3节)。
由此可知,VSPW+1=tvp(tvp表示垂直方向同步信号的脉冲宽度),VBPD+1=tvb(tvb表示垂直方向的移动时间),LINEVAL+1=lines(lines表示LCD一共有lines行像素),VFPD+1=tvf(tvf表示垂直方向的等待时间),则根据传参plcdparams可以得到各个量的设置值:
- bit[31:24] = VBPD = tvb-1 = plcdparams->time_seq.tvb - 1
- bit[23:14] = LINEVAL = lines-1 = plcdparams->yres - 1
- bit[13:6] = VFPD = tvf-1 = plcdparams->time_seq.tvf - 1
- bit[5:0] = VSPW = tvp-1 = plcdparams->time_seq.tvp - 1
综上所述,s3c2440_lcd_controller_init函数中,LCDCON2寄存器设置代码如下:
LCDCON2 = ((plcdparams->time_seq.tvb - 1)<<24) | \
((plcdparams->yres - 1)<<14) | \
((plcdparams->time_seq.tvf - 1)<<6) | \
((plcdparams->time_seq.tvp - 1)<<0);
5.1.3 LCDCON3寄存器
该寄存器的位含义如下图所示,主要用来设置水平方向的时序参数(水平方向移动时间、每行有多少个像素、水平方向的移动时间)。
图中参数的含义,需要对比S3C2440的LCD控制器时序图、具体LCD芯片的时序图(见LCDCON2寄存器中的图)。由此可得:
- bit[25:19] = HBPD = thb(水平方向的移动时间)-1 = plcdparams->time_seq.thb - 1
- bit[18:8] = HOZVAL = 列数(每行有多少个像素)-1 = plcdparams->xres - 1
- bit[7:0] = HFPD = thf(水平方向的等待时间)-1 = plcdparams->time_seq.thf - 1
综上所述,s3c2440_lcd_controller_init函数中,LCDCON3寄存器设置代码如下:
LCDCON3 = ((plcdparams->time_seq.thb - 1)<<19) | \
((plcdparams->xres - 1)<<8) | \
((plcdparams->time_seq.thf - 1)<<0);
5.1.4 LCDCON4寄存器
该寄存器的位含义如下图所示,主要用来设置水平方向同步信号的脉冲宽度。
对比S3C2440的LCD控制器时序图、具体LCD芯片的时序图,可知:bit[7:0] = HSPW = thp - 1 = plcdparams->time_seq.thp - 1
因此s3c2440_lcd_controller_init函数中,LCDCON4寄存器设置代码如下:
LCDCON4 = ((plcdparams->time_seq.thp - 1)<<0);
5.1.5 LCDCON5寄存器
该寄存器的位含义如下图所示,主要用来设置:像素数据在内存中的摆放方式(LSB还是MSB)、采用16BPP时的格式(5:6:5还是5:5:5:I)、引脚信号的极性(比如像素时钟等信号的极性)、是否为LCD供电(注意bit[3]是使能,bit[5]是设置极性)。
(1)bit[12]:当使用24BPP时,用来设置像素数据在内存中的摆放方式(是LSB还是MSB)。
在S3C2440数据手册中搜索“BPP24BL”,在P412有以下内容:
对于24BPP,我们选择将像素数据放在4字节中的低3字节,因此需要设置:BPP24BL=bit[12]=0、BSWP=bit[1]=0、HWSWP=bit[0]=0。
由于bit[12]默认值就是0,所以在代码中,对于24BPP,没有设置bit[12]=0,只设置bit[1:0]=0b00=0(BSWP=0、HWSWP=0):
//pixelplace 表示bit[1:0]的值
pixelplace = plcdparams->bpp == 24 ? (0) : |\
plcdparams->bpp == 16 ? (1) : |\
(1<<1); /* 8bpp */
(2)bit[11]:当使用16BPP时,用来设置是5:6:5格式,还是5:5:5:I格式。这里我们设置为5:6:5格式。
(3)bit[10]:用来设置像素时钟的极性(设置是在上升沿取数据,还是在下降沿取数据)。
(4)bit[9]:用来设置水平方向同步信号(即HSYNC)的极性(高电平还是低电平有效)。
(5)bit[8]:用来设置垂直方向同步信号(即VSYNC)的极性。
(6)bit[7]:用来设置RGB数据(即video data)的极性。
(7)bit[6]:用来设置VDEN引脚信号的极性。
由下面的原理图可知,VM引线接到了LCD芯片的DE引脚。我们在LCD芯片的数据手册中搜索“DE”,在P6得知它是“DATA ENABLE”的含义,那么DE引脚应该对应着S3C2440的VDEN引脚(原理图中引脚的名字起得很怪异,按理说,原理图中引脚的名字应该引用S3C2440数据手册P49中的名字,比如这里的VM应该写为VDEN才对)。
因此我们需要设置VDEN引脚,即设置LCDCON5寄存器的bit[6],为此我们在 struct pins_polarity 结构体中添加一个成员de:
//引脚的极性
typedef struct pins_polarity {
int vclk; /* normal: 在下降沿获取数据 */
int rgb; /* normal: 高电平表示1 */
int hsync; /* normal: 高脉冲 */
int vsync; /* normal: 高脉冲 */
//后面可能还会添加其他信号,这里只列出部分
int de; /* normal: 高脉冲 */
}pins_polarity, *p_pins_polarity;
在S3C2440的数据手册中查找“VDEN”,找到它的时序图,可知它的normal对应着高电平:
(8)bit[5]:用来设置LCD_PWREN信号的极性。bit[5]和bit[3]都是在设置LCD_PWREN引脚,不过bit[5]设置极性,bit[3]设置使能。
(9)bit[4]:用来设置LEND信号的极性(似乎没用到,不需要设置)。
(10)bit[3]:用来设置LCD_PWREN引脚是否使能,即是否为LCD提供电源。注意它和LCD背光灯是不同的含义。
我们查看原理图(在V2原理图中搜索“LCD_PWREN”),看一下LCD_PWREN这个引脚是否接到了LCD:
由此可知,S3C2440的LCD_PWREN引脚(与GPG4复用),间接地连接到了LCD(AVDD?)。所以我们需要设置这个引脚,为此我们在struct pins_polarity结构体中添加一个成员pwren(在s3c2440_lcd_controller_enable函数中我们直接将这个位设置为1,因此这里添加这个成员好像也没啥用):
//引脚的极性
typedef struct pins_polarity {
int vclk; /* normal: 在下降沿获取数据 */
int rgb; /* normal: 高电平表示1 */
int hsync; /* normal: 高脉冲 */
int vsync; /* normal: 高脉冲 */
//后面可能还会添加其他信号,这里只列出部分
int de; /* normal: 高脉冲 */
int pwren; /* normal: 高脉冲 */
}pins_polarity, *p_pins_polarity;
在S3C2440的数据手册中查找“LCD_PWREN”,找到它的时序图,可知它的normal对应着高电平:
(11)bit[2]:用来设置是否使能LEND引脚(似乎没用到,不需要设置)。
(12)bit[1]、bit[0]:当BPP=16或者8时,由这两个位决定像素数据在内存中的摆放格式;当BPP=24时,还需要配合bit[12]来决定。
下图中的红框,表示编程时的我们设置情况。
综上所述,s3c2440_lcd_controller_init函数中,LCDCON5寄存器设置代码如下:
pixelplace = plcdparams->bpp == 24 ? (0) : |\
plcdparams->bpp == 16 ? (1) : |\
(1<<1); /* 8bpp */
LCDCON5 = (plcdparams->pins_pol.vclk<<10) |\
(plcdparams->pins_pol.rgb<<7) |\
(plcdparams->pins_pol.hsync<<9) |\
(plcdparams->pins_pol.vsync<<8) |\
(plcdparams->pins_pol.de<<6) |\
(plcdparams->pins_pol.pwren<<5) |\ //这里为什么不用设置bit[3]
(1<<11) | pixelplace; //来开启LCD电源?
注意本函数还没有设置bit[3](因为本函数都是与初始化相关的内容,不应该包括enable功能),在s3c2440_lcd_controller_enalbe函数中才设置bit[3](该函数符合enable的含义)。
5.1.6 LCDSADDR1寄存器
LCDSADDR1寄存器的位含义如下,主要用来设置FrameBuffer的首地址。
假设FrameBuffer首地址是A[31:0],则LCDSADDR1[29:21]存储A[30:22],LCDSADDR1[20:0]存储A[21:1]。
即LCDSADDR1[29:0] 与 A[ 30:1] 是一一对应的。在编程时,我们需要将A[30:1]赋值给LCDSADDR1寄存器,体现在下面这段代码:
addr = plcdparams->fb_base & ~(1<<31);
LCDSADDR1 = (addr >> 1);//上面取出A[30:0]的数据,需要右移1bit得到A[30:1]
这里提一下,JZ2440开发板上的LCD,是 single-scan LCD。
5.1.7 LCDSADDR2寄存器
LCDSADDR2寄存器的位含义如下,主要用来设置FrameBuffer的结束地址。
假设FrameBuffer的结束地址是A[31:0],则LCDSADDR2[20:0]存储A[21:1]。
注意上图给出的计算方法可能有错(课程说的),可以根据S3C2440数据手册P429的例子自己来算(如下所示):
又或者根据常识来写:FB结束地址 = FB首地址 + (一共有多少个像素*每个像素占据多少位) / 8 。
因此LCDSADDR2寄存器的设置代码如下:
//addr = fb首地址+ (x*y*BPP)/8
addr = plcdparams->fb_base + plcdparams->xres*plcdparams->yres*plcdparams->bpp/8;
addr >>=1; //addr =A[21:0] => addr =A[21:1]
addr &= 0x1fffff; //
LCDSADDR2 = addr;
5.1.8 其他引脚的设置
(1)设置LCD背光灯电源
原理图中还有LED+、LED-引脚,我们在V2原理图中搜索“LED+”,进而继续搜索“KEYBOARD”:
由此可知GPB0控制着LCD背光灯,因此需要设置GPBCON寄存器。我们把GPB0配置成输出模式,如下所示:
/* 初始化背光引脚,设置为输出模式 */
GPBCON &= ~0x3; //清零
GPBCON |= 0x01; //设置为输出模式
(2)设置LCD电源(LCD_PWREN引脚)
由5.1.5(10)可知,LCD_PWREN引脚与GPG4复用,因此需要设置GPGCON寄存器。
我们把GPG4配置成LCD_PWREN功能,如下所示:
/* PWREN */
GPGCON |= (3<<8);
(3)设置LCD的专用引脚(如VD)
由下面的原理图可知,S3C2440中,VD[23:0]与GPCx、GPDx引脚复用,另外像VFRAM、VLINE等引脚也与GPCx引脚有关,所以我们需要分别设置GPCCON、GPDCON寄存器。
GPCCON、GPDCON寄存器的位含义如下图所示:
我们把GPCCON、GPDCON寄存器,都设置为0xaaaa_aaaa即可,如下所示:
/* 设置LCD的专用引脚 */
GPCCON = 0xaaaaaaaa; //0b 1010 1010 …… 1010 = 0x a a …… a (共8个a)
GPDCON = 0xaaaaaaaa;
综合(1)~(3)所述,其他引脚的设置代码如下所示:
void jz2440_lcd_pin_init(void)
{
/* 设置背光引脚 */
GPBCON &= ~0x3;
GPBCON |= 0x01;
/* 设置LCD的专用引脚 */
GPCCON = 0xaaaaaaaa;
GPDCON = 0xaaaaaaaa;
/* 设置LCD电源引脚 */
GPGCON |= (3<<8);
}
然后我们在s3c2440_lcd_controller_init函数开始位置,调用这个jz2440_lcd_pin_init函数即可。注意这里只进行初始化,不涉及开启,所以都不涉及GPXDAT;与GPXDAT有关的代码在下面s3c2440_lcd_controller_enalbe函数中。
5.2 s3c2440_lcd_controller_enalbe函数
此函数负责开启某些开关,比如开启背光灯、为LCD供电、使能LCD控制器:
void s3c2440_lcd_controller_enalbe(void)
{
/* 背光引脚 : GPB0 */
GPBDAT |= (1<<0);
/* pwren : 给LCD提供AVDD */
LCDCON5 |= (1<<3); //bit[3]=1
/* LCDCON1'BIT 0 : 设置LCD控制器是否输出信号 */
LCDCON1 |= (1<<0); //bit[0]=1
}
5.3 s3c2440_lcd_controller_disable函数
此函数与 s3c2440_lcd_controller_enalbe 函数功能相反:
void s3c2440_lcd_controller_disable(void)
{
/* 背光引脚 : GPB0 */
GPBDAT &= ~(1<<0);
/* pwren : 给LCD提供AVDD */
LCDCON5 &= ~(1<<3);
/* LCDCON1'BIT 0 : 设置LCD控制器是否输出信号 */
LCDCON1 &= ~(1<<0);
}
六、LCD编程_LCD芯片参数的设置 (lcd_4.3.c)
4.1节提到,我们需要在lcd_4.3.c文件中创建一个lcd_params类型的实例(表征4.3寸LCD芯片的参数),作为参数传入s3c2440_lcd_controller.c文件中的s3c2440_lcd_controller_init函数,这样就可以初始化LCD控制器了。
在4.1节中已经定义了lcd_params结构体(如下所示),我们需要确定它的实例中各个成员的值。
typedef struct lcd_params {
pins_polarity pins_pol; /* 引脚极性 */
time_sequence time_seq; /* 时序 */
/* 分辨率, bpp */
int xres;
int yres;
int bpp;
unsigned int fb_base; /* framebuffer的地址 */
//后续根据讲解的深入,还可能添加其他成员,比如:char* name;
}lcd_params, *p_lcd_params;
通过对比S3C2440的LCD控制器的时序图、JZ2440的4.3寸LCD的时序图、时序参数推荐值,可知lcd_4.3.c文件代码设置如下:
#define LCD_FB_BASE 0x33c00000
lcd_params lcd_4_3_params = {
.name = "lcd_4.3"//后面会说要加这个,这里先提前列出吧
.pins_polarity = {
.de = NORMAL, /* normal: 高电平时可以传输数据 */
.pwren = NORMAL, /* normal: 高电平有效 */
.vclk = NORMAL, /* normal: 在下降沿获取数据 */
.rgb = NORMAL, /* normal: 高电平表示1 */
.hsync = INVERT, /* normal: 高脉冲 */
.vsync = INVERT, /* normal: 高脉冲 */
},
.time_sequence = {
/* 垂直方向 */
.tvp= 10, /* vysnc脉冲宽度 */
.tvb= 2, /* 上边黑框, Vertical Back porch */
.tvf= 2, /* 下边黑框, Vertical Front porch */
/* 水平方向 */
.thp= 41, /* hsync脉冲宽度 */
.thb= 2, /* 左边黑框, Horizontal Back porch */
.thf= 2, /* 右边黑框, Horizontal Front porch */
.vclk= 9, /* MHz */
},
.xres = 480,
.yres = 272,
.bpp = 16,
.fb_base = LCD_FB_BASE,
};
以下是对上面代码的一些解释说明:
(1)首先是设置引脚极性。S3C2440数据手册中的时序图,其电平情形就属于normal;LCD芯片自身也有一个时序图,如果某引脚的电平和S3C2440的一样(都是高电平或者低电平有效),那么就把该引脚的极性设置为normal,如果和S3C2440是相反的,则把引脚的极性设置为invert。
从5.1.5中可知VDEN、LCD_PWREN的引脚极性是normal,从LCD控制器时序图可知VCLK的引脚极性也是normal;至于RGB数据,一般来说也不需要反转,所以其引脚极性是normal。最后由下面两张图可知,需要把hsync、vsync设置为INVERT:
(2)然后是时序参数的值。在LCD芯片数据手册P11中,有以下内容:
从上图可以得知除了 .bpp(这里设置为16)、.fb_base的所有取值。
(3).fb_base的取值为多少呢?我们需要看一下start.S文件中,是如何规划内存的使用的。经过分析,设置FB的首地址为0x33c00000。
lcd_4.3.c文件完整代码如下:
#define LCD_FB_BASE 0x33c00000
lcd_params lcd_4_3_params = {
.name = "lcd_4.3"
.pins_polarity = {
.de = NORMAL, /* normal: 高电平时可以传输数据 */
.pwren = NORMAL, /* normal: 高电平有效 */
.vclk = NORMAL, /* normal: 在下降沿获取数据 */
.rgb = NORMAL, /* normal: 高电平表示1 */
.hsync = INVERT, /* normal: 高脉冲 */
.vsync = INVERT, /* normal: 高脉冲 */
},
.time_sequence = {
/* 垂直方向 */
.tvp= 10, /* vysnc脉冲宽度 */
.tvb= 2, /* 上边黑框, Vertical Back porch */
.tvf= 2, /* 下边黑框, Vertical Front porch */
/* 水平方向 */
.thp= 41, /* hsync脉冲宽度 */
.thb= 2, /* 左边黑框, Horizontal Back porch */
.thf= 2, /* 右边黑框, Horizontal Front porch */
.vclk= 9, /* MHz */
},
.xres = 480,
.yres = 272,
.bpp = 16,
.fb_base = LCD_FB_BASE,
};
void lcd_4_3_add(void)
{
register_lcd(&lcd_4_3_params);
}
七、LCD编程_添加中间层文件(lcd.c、lcd_controller.c)
在第五、六节,我们编写了LCD控制器相关的设置代码(s3c2440_lcd_controller.c)、LCD芯片参数相关设置代码(lcd_4.3.c)。
7.1 编写Icd_controller.c文件
接下来我们需要编写一个中间层文件(Icd_controller.c),它负责接收上一层的LCD芯片参数(即lcd_4.3.c文件),然后利用这些参数来设置同层下一级的LCD控制器(即s3c2440_lcd_controller.c文件)。该文件的核心逻辑如下(后面需要修改,但逻辑是如此):
//lcd_controller.c文件
void lcd_controller_init(p_lcd_params plcdparams)
{
//s3c2440_lcd_controller.init(plcdparams);//调用S3C2440这款SoC的LCD控制器初始化函数
selected_lcd_controller.init(plcdparams);//上的写法没有分层思想,应该写成与具体SoC无关的代码。
}
Icd_controller.c文件的完整代码如下:
#define LCD_CONTROLLER_NUM 10 //假设最多支持10款SoC的LCD控制器
//指针数组p_array_lcd_controller,后续各个LCD控制器注册到该数组中
static p_lcd_controller p_array_lcd_controller[LCD_CONTROLLER_NUM];
//表示我们要设置哪款LCD控制器(通过名字来找到它)
static p_lcd_controller g_p_lcd_controller_selected;
//下面这个函数会被某个LCD控制器文件调用,用来将LCD控制器注册到数组中
//比如s3c2440_lcd_controller.c会调用该函数,将s3c2440的LCD控制器注册到数组中
int register_lcd_controller(p_lcd_controller plcdcon)
{
int i;
for (i = 0; i < LCD_CONTROLLER_NUM; i++)//遍历,哪个地方空的就注册到该地方
{
if (!p_array_lcd_controller[i])//如果是空的
{
p_array_lcd_controller[i] = plcdcon;
return i;
}
}
return -1;
}
//下面这个函数,通过名字来查找对应的LCD控制器
int select_lcd_controller(char *name)
{
int i;
for (i = 0; i < LCD_CONTROLLER_NUM; i++)
{
if (p_array_lcd_controller[i] && !strcmp(p_array_lcd_controller[i]->name, name))
{
g_p_lcd_controller_selected = p_array_lcd_controller[i];
return i;
}
}
return -1;
}
/* 向上: 接收不同LCD的参数
* 向下: 使用这些参数设置对应的LCD控制器
*/
int lcd_controller_init(p_lcd_params plcdparams)
{
/* 调用所选择的LCD控制器的初始化函数 */
if (g_p_lcd_controller_selected)
{
g_p_lcd_controller_selected->init(plcdparams);
return 0;
}
return -1;
}
void lcd_contoller_add(void)
{
s3c2440_lcd_contoller_add();//如果要更换其他SoC,需要在这里换一个函数
}
下面是对这段代码的一些解释说明(解释Icd_controller.c文件如何管理下一级LCD控制器):
(1)首先在Icd_controller.c文件中,设置一个指针数组p_array_lcd_controller,用数组来保存同层下一级的各种LCD控制器(数组的每个成员,都是一个指向LCD控制器结构体的指针,用来指向LCD控制器结构体的某个实例)。
由于这些结构体需要通过名字来区分,所以需要在lcd_controller.h文件中的struct lcd_controller结构体定义中添加成员name:
typedef struct lcd_controller {
char *name;//新添
void (*init)(p_lcd_params plcdparams);
void (*enable)(void);
void (*disable)(void);
}lcd_controller, *p_lcd_controller;
并且在s3c2440_lcd_controller.c文件中设置其值为“s3c2440”:
struct lcd_controller s3c2440_lcd_controller = {
.name = "s3c2440",//添加这一行
.init = s3c2440_lcd_controller_init,
.enable = s3c2440_lcd_controller_enalbe,
.disable = s3c2440_lcd_controller_disable,
};
(2)Icd_controller.c文件提供了一个注册函数register_lcd_controller,用来注册LCD控制器:
//下面这个函数会被某个LCD控制器调用,用来将LCD控制器注册到数组p_array_lcd_controller中
//比如在s3c2440_lcd_controller.c中调用该函数,将s3c2440的LCD控制器注册到数组中
int register_lcd_controller(p_lcd_controller plcdcon)
{
int i;
for (i = 0; i < LCD_CONTROLLER_NUM; i++)//遍历,哪个地方空的就注册到该地方
{
if (!p_array_lcd_controller[i])//如果是空的
{
p_array_lcd_controller[i] = plcdcon;
return i;
}
}
return -1;
}
与Icd_controller.c同层的下一级代码(即各个LCD控制器对应的文件,比如s3c2440_lcd_controller.c),需要通过调用该函数,将自己注册到p_array_lcd_controller数组中,如下所示(这里将该函数封装成s3c2440_lcd_contoller_add函数,在同层上一级代码lcd_controller.c中肯定会调用s3c2440_lcd_contoller_add函数,例如通过调用lcd_contoller_add函数来间接调用它):
//位于s3c2440_lcd_contoller.c文件中
void s3c2440_lcd_contoller_add(void)
{
register_lcd_controller(&s3c2440_lcd_controller);
}
(3)Icd_controller.c文件提供了一个select_lcd_controller函数(或者说接口),如下所示:
int select_lcd_controller(char *name)
{
int i;
for (i = 0; i < LCD_CONTROLLER_NUM; i++)
{
if (p_array_lcd_controller[i] && !strcmp(p_array_lcd_controller[i]->name, name))
{
g_p_lcd_controller_selected = p_array_lcd_controller[i];
return i;
}
}
return -1;
}
上层代码lcd.c文件(见7.2节内容)中通过调用这个函数来选择某个LCD控制器。如何选择呢?根据传参name来匹配。
7.2 编写lcd.c文件
lcd.c文件的完整代码如下:
#define LCD_NUM 10 //假设最多支持10款LCD芯片
static p_lcd_params p_array_lcd[LCD_NUM];//指针数组
static p_lcd_params g_p_lcd_selected;//表示选中哪款LCD芯片
int register_lcd(p_lcd_params plcd)//注册LCD芯片
{
int i;
for (i = 0; i < LCD_NUM; i++)
{
if (!p_array_lcd[i])
{
p_array_lcd[i] = plcd;
return i;
}
}
return -1;
}
int select_lcd(char *name)//根据名字来选中该款LCD芯片
{
int i;
for (i = 0; i < LCD_NUM; i++)
{
if (p_array_lcd[i] && !strcmp(p_array_lcd[i]->name, name))
{
g_p_lcd_selected = p_array_lcd[i];
return i;
}
}
return -1;
}
int lcd_init(void)
{
/* 注册LCD */ //这里注册4.3寸这款LCD芯片
lcd_4_3_add(); //位于lcd_4.3.c文件中
/* 注册LCD控制器 */ //这里注册s3c2440这款SoC的LCD控制器
lcd_contoller_add(); //位于lcd_controller.c文件中
/* 选择某款LCD */
select_lcd("lcd_4.3"); //位于lcd.c文件中
/* 选择某款LCD控制器 */
select_lcd_controller("s3c2440"); //位于lcd_controller.c文件中
/* 使用LCD的参数, 初始化LCD控制器 */
lcd_controller_init(g_p_lcd_selected);//位于lcd_controller.c文件中
}
可见lcd.c文件的思路,与编写lcd_controller.c文件是一样的。
(1)lcd.c文件中也有一个指针数组p_array_lcd,它的每个元素都是指针,用来指向(存储)各个LCD芯片参数结构体的实例;
(2)lcd.c文件中也有一个register_lcd函数。同层下一级的各个LCD芯片参数对应的文件(比如lcd_4.3.c文件),需要通过调用该函数,将LCD芯片注册到p_array_lcd数组中。
由此可知p_array_lcd、register_lcd都是供同层下一级文件使用的,那向上层提供了什么?提供了select_lcd函数。
(3)lcd.c文件中也有一个select_lcd函数。上一层(纯软件层)文件通过调用该函数(由代码可知不是直接调用该函数,而是调用包含该函数的lcd_init函数),根据传入的name来选择某款LCD芯片。
(4)纯软件层只访问到lcd.c文件(只使用到lcd.c文件中的函数),不关心它之后的下层文件(不关心LCD控制器层的文件;按理也不关心lcd_4.3.c文件,所以不应该用到lcd_4.3.c文件中的函数,但这里的代码似乎写得不是很完美,所以还是用到了lcd_4.3.c文件中的函数)。在lcd.c文件最后写了一个初始化函数lcd_init。该函数会被上一层(纯软件层)调用。
int lcd_init(void)
{
/*步骤1:能够选择某款LCD、某款LCD控制器的前提,是先注册到对应数组中。
*如果有多款则可以注册多款,然后在2中再选择。由于分别只有一款,所以这里分别只列一个函数。
*/
//注册LCD芯片,这里是注册4.3寸这款LCD芯片
/*lcd_4_3_add函数定义在lcd_4.3.c文件中,出现在这里不符合分层思想
*我觉得应该在lcd.c文件中写一个lcd_add函数,该函数调用lcd_4.3.c文件中的lcd_4_3_add函数
*如果有多款LCD芯片要注册,可以在lcd_add函数中继续添加其他lcd_x.x.c文件中的注册函数
*比如下面的lcd_contoller_add就是这样处理的。
*/
//lcd_add();//我觉得应该写成这样
lcd_4_3_add();
//注册LCD控制器,这里是注册s3c2440这款SoC的LCD控制器
/*这里原本的代码是调用s3c2440_lcd_contoller_add函数的
*但这样写不好,因为该函数位于s3c2440_lcd_contoller.c文件,出现在这里不符合分层思想
*所以这里改为调用lcd_controller.c文件中的lcd_contoller_add函数,
*然后lcd_contoller_add函数再调用s3c2440_lcd_contoller_add函数。
*/
//s3c2440_lcd_contoller_add();//不符合分层思想
lcd_contoller_add(); //位于lcd_controller.c文件中
/*步骤2:选择某款LCD、某款LCD控制器*/
//选择某款LCD
select_lcd("lcd_4.3"); //位于lcd.c文件中
//选择某款LCD控制器
select_lcd_controller("s3c2440"); //位于lcd_controller.c文件中
/*步骤3:选择某款LCD、某款LCD控制器之后,才能使用LCD的参数来初始化LCD控制器*/
lcd_controller_init(g_p_lcd_selected); //位于lcd_controller.c文件中
}
如此一来,如果后面我们想要添加其他LCD芯片(比如7寸的LCD),或者更改LCD控制器(比如更改为s5pv210的LCD控制器),只需要在LCD芯片特性层添加一个lcd_7.0.c文件、在LCD控制器层添加一个s5pv210_lcd_controller.c文件,并分别注册到相应数组中,然后在lcd.c中的lcd_init函数里,通过传入名字参数来选择某款LCD芯片和某款LCD控制器即可。
八、LCD编程_测试层文件(lcd_test.c)
8.1 编写测试层文件lcd_test.c
为了测试上面编写的代码(代码是否出错,需要有测试代码),我们需要编写测试层文件lcd_test.c,该文件内容如下:
void lcd_test(void)
{
unsigned int fb_base;
int xres, yres, bpp;
int x, y;
unsigned short *p;
/* 初始化LCD */
lcd_init();
/* 使能LCD */
lcd_enable();
/* 获得LCD的参数: fb_base, xres, yres, bpp */
get_lcd_params(&fb_base, &xres, &yres, &bpp);
/* 往framebuffer中写数据 */
if (bpp == 16)
{
/* 让LCD输出整屏的红色 */
/* 565: 0xf800 */
p = (unsigned short *)fb_base;
for (y = 0; y < yres; y++)
for (x = 0; x < xres; x++)
*p++ = 0xf800;
}
}
下面是对这段代码的一些说明:
(1)首先调用lcd.c文件中的lcd_init函数(见7.2小节),对LCD控制器进行初始化(核心是lcd_init函数的步骤3)。
(2)然后调用lcd.c文件中的lcd_enable函数来使能LCD。该函数内部调用lcd_controller.c文件中的lcd_controller_enable函数,最终调用s3c2440_lcd_controller.c文件中的s3c2440_lcd_controller_enalbe函数。
//位于lcd.c文件
void lcd_enable(void) //对lcd_controller_enable进行封装
{
lcd_controller_enable();
}
void lcd_disable(void)
{
lcd_controller_disable();
}
这里为什么要封装一下?这是因为我们希望,纯软件层(这里包括测试层、纯软件)的文件,只访问到它下一层(LCD芯片特性层)的文件(对应lcd.c文件),不去访问LCD控制器层的文件(对应lcd_controller.c文件),所以我们在lcd.c文件中,将lcd_controller.c文件中的lcd_controller_enable函数封装为lcd_enable函数。
(3)接着调用lcd.c文件中的get_lcd_params函数,来获取LCD芯片的参数(因为后续(4)要根据这些参数进行显示):
//位于lcd.c文件中
void get_lcd_params(unsigned int *fb_base, int *xres, int *yres, int *bpp)
{
*fb_base = g_p_lcd_selected->fb_base;
*xres = g_p_lcd_selected->xres;
*yres = g_p_lcd_selected->yres;
*bpp = g_p_lcd_selected->bpp;
}
注意在lcd.h中声明一下get_lcd_params函数。
(4)最后往FrameBuffer中写入数据,让LCD整个屏幕显示红色(因为这情况最简单)。
想让整个屏幕显示红色,那么就要从FrameBuffer首地址开始一直填充红色对应的数据。
假设采用16BPP且5:6:5(我们设置的就是如此),红色对应的数据是多少呢?
16BPP意味着一个像素数据占据16bit,5:6:5表示R占据5bit(即bit[15:11])、G占据6bit(即bit[10:5])、B占据5bit(即bit[4:0])。
当bit[15:11]全为1表示红色,bit[10:5]全为0表示没有绿色成分,bit[4:0]全为0表示没有蓝色成分。
所以全红色则为0b 11111 000000 00000 = 0x f800,如下图所示:
算出红色对应的数据之后,就需要从FrameBuffer首地址开始,填充一帧画面对应的数据:
p = (unsigned short *)fb_base;//注意一下这里的强制类型转换,则p++时表示移动2字节
for (y = 0; y < yres; y++) //遍历每一行
for (x = 0; x < xres; x++)//遍历每一行中的每一个像素
*p++ = 0xf800;
注意一下“p = (unsigned short *)fb_base;”,其强制类型转换为(unsigned short *)类型,则p+1则表示移动2个字节,刚好符合16BPP时每个像素在内存中占2个字节的情形。下面采用24BPP时,由于24BPP实际对应4字节(低3字节是有效数据位,高1字节无效),所以要强制类型转换为(unsigned init*)(见8.2中的(4)中的代码)。
8.2 编译烧写运行验证
(1)修改Makefile和main.c
完成上面工作之后,我们修改Makefile文件如下(好看一些而已):
然后在main.c文件修改:
int main(void)
{
led_init();
//interrupt_init(); /* 初始化中断控制器 */
key_eint_init(); /* 初始化按键, 设为中断源 */
//timer_init();
puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");
//nor_flash_test();
lcd_test();//----现在要测试lcd------------------
return 0;
}
最后得到待编译的代码文件夹(见005_simple_test_bad_017_007文件夹)。
(2)根据错误提示修改代码
1)我们执行make,然后根据错误提示来修改源码:
其中某些错误容易解决(缺少头文件等),但下面错误花了我一点时间解决(这两个unknown field表示结构体里面没有这两项内容):
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/005_simple_test_bad_017_007$ make
arm-linux-gcc -march=armv4 -c -o lcd/lcd_4.3.o lcd/lcd_4.3.c
lcd/lcd_4.3.c:7: error: unknown field `pins_polarity' specified in initializer
lcd/lcd_4.3.c:15: error: unknown field `time_sequence' specified in initializer
make: *** [lcd/lcd_4.3.o] Error 1
xjh@ubuntu:~/iot/embedded_basic/jz2440/armBareMachine/005_simple_test_bad_017_007$
后来发现,错误是因为在lcd_4_3.文件中,把数据类型pins_polarity、time_sequence当做变量进行赋值了,改回来即可:
2)修改完上面这个内容后,编译又弹出问题,解决呗!
首先也是缺少头文件,在lcd/lcd_controller.c文件开头添加头文件即可(注意一下包含s3c2440_soc.h这个头文件时的写法,这是因为它与lcd目录同级):
#include "lcd_controller.h"
#include "lcd.h"
#include "../s3c2440_soc.h"
3)解决完上面错误后,又会弹出关于lcd/s3c2440_lcd_controller.c文件的错误,也是缺少头文件,以及把enable错写成enalbe了、多添加了“|”符号。改过来就好。
4)重新编译又弹出问题,包括strcmp没有定义、除法错误等问题:
A、s3c2440_lcd_controller.c文件中,下面这部份代码用到了除法。为了简单起见,这里先直接赋值为5,后面再想办法解决这个除法的问题。这在5.1.1(2)中曾经说过这个问题。
//int clkval = (double)HCLK/plcdparams->time_seq.vclk/2-1+0.5;
int clkval = 5;
int bppmode = plcdparams->bpp == 8 ? 0xb :\
plcdparams->bpp == 16 ? 0xc :\
0xd; /* 0xd: 24,32bpp */
LCDCON1 = (clkval<<8) | (3<<5) | (bppmode<<1) ;
B、解决strcmp没有定义这个问题。打开string_utils.c文件(与lcd文件夹同层目录中),在里面实现一个strcmp函数即可(可以直接复制u-boot中的代码,比如下面代码就是直接拷贝过来的):
/**
* strcmp - Compare two strings
* @cs: One string
* @ct: Another string
*/
int strcmp(const char * cs,const char * ct)
{
register signed char __res;
while (1) {
if ((__res = *cs - *ct++) != 0 || !*cs++)
break;
}
return __res;
}
(3)实验现象
重新编译可以通过,烧写到开发板中运行。实验现象如预期所料,整个屏幕全红,见链接。
还可以这样玩,因为上面8.1(4)中已经说明,采用16BPP时:
- 全红色对应的数据是0b 11111 000000 00000 = 0x f800,
- 全绿色对应的数据是0b 00000 111111 00000 =0x7e0,
- 全蓝色对应的数据是0b 00000 000000 11111 = 0x1f。
则我们可以让屏幕轮流显示全红、全绿、全蓝(代码如下,没啥特别意义,单纯好看而已),实验现象见链接。
if (bpp == 16)
{
for(;;)
{
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0xf800;//红
delay(100000);
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0x07e0;//绿
delay(100000);
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0x001f;//蓝
delay(100000);
}
}
(4)补充说明
由2.3(1)可知,采用24BPP时,每个像素使用24bit来表示,但实际上每个像素在内存中会使用4个字节来表示(从这角度来说,24BPP和32BPP其实是一样的),而低3字节即bit[23:0]才是RGB数据:其中R占据8bit,即bit[23:16];G也是占据8bit,即bit[15:8];B占据8bit,即bit[7:0]。
但问题是,LCD控制器与LCD屏幕之间用于传输RGB数据的只有16根线,采用24BPP时,FrameBuffer中的数据是如何传送给LCD屏幕的?可以这样理解,采用24BPP时,LCD控制器从FrameBuffer取得4字节数据后(只有低3字节有效),发出8条红色、8条绿色、8条蓝色数据,但分别只用到其中的高5条红线、高6根绿线、高5根蓝线(由原理图可以得知,VD[23:19]表示红色数据,VD[15:10]表示绿色数据,VD[7:3]表示蓝色数据)。
当采用24BPP时,全红对应着数据0b xxxxxxxx 11111xxx 000000xx 00000xxx 。x表示1或0都行,这里让x=0时,则全红对应着数据0x00f80000。同理全绿对应着0x0000fc00,全蓝对应着0x000000f8。
这里插讲一下,当采用8BPP时,可以通过调色板转换为16BPP,这样就可以在LCD上显示了。
接下来我们试一下使用24BPP是否起效果。 首先将lcd_4.3.c文件中.bpp的值改为32(注意一定是改为32而非24,原因见注释):
#define LCD_FB_BASE 0x33c00000
lcd_params lcd_4_3_params = {
//篇幅缘故,这里省略部分代码
.xres = 480,
.yres = 272,
//.bpp = 16,
.bpp =32, //修改这里,由于24BPP和32BPP,在内存中一个像素都是32位,
//所以这里要写为32,而不能写为24
//另外后面(LCDSADDR2寄存器)要通过/8计算FB的结束地址,这里如果写24则不对了
.fb_base = LCD_FB_BASE,
};
然后在s3c2440_lcd_controller.c中修改如下:
//修改点1(其实不用修改)
int bppmode = plcdparams->bpp == 8 ? 0xb :\
plcdparams->bpp == 16 ? 0xc :\
0xd; /* 0xd: 24,32bpp */
//修改点2(LCDCON5寄存器处,将24改为32)
pixelplace = plcdparams->bpp == 32 ? (0) : \
plcdparams->bpp == 16 ? (1) : \
(1<<1); /* 8bpp */
然后在lcd_test.c文件中添加下面代码(在lcd_test函数开始位置记得定义"unsigned int *p2;"):
if (bpp == 16)
{
for(;;)
{
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0xf800;//红
delay(100000);
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0x07e0;//绿
delay(100000);
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0x001f;//蓝
delay(100000);
}
}
//在lcd_test()开始位置记得定义: unsigned int *p2;
if(bpp==32)
{
for(;;)
{
p2 = (unsigned int *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
//*p++ = 0xf800;//红
*p2++ = 0x00f80000;
delay(100000);
p2 = (unsigned int *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
//*p++ = 0x07e0;//绿
*p2++ = 0x0000fc00;
delay(100000);
p2 = (unsigned int *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
//*p++ = 0x001f;//蓝
*p2++ = 0x000000f8;
delay(100000);
}
}
重新编译烧写运行,发现实验结果符合预期。说明采用24BPP时也能起效。
九、LCD编程_画点、线、圆
9.1 实现画点
线条、圆、字母,都是以点为基础的,所以先实现画点。在3.1程序框架中,画点是在framebuffer.c中实现的。该文件内容如下:
#include "lcd.h"
/*获得LCD参数*/
static unsigned int fb_base;
static int xres, yres, bpp;
void fb_get_lcd_params(void)
{
get_lcd_params(&fb_base, &xres, &yres, &bpp);
}
/*将32bpp(其实是24bpp)转换为16bpp
*rgb是32位的,位含义是0x00RRGGBB
*/
unsigned short convert32bppto16bpp(unsigned int rgb)
{
int r = (rgb >> 16)& 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
/* rgb565 */
r = r >> 3;
g = g >> 2;
b = b >> 3;
return ((r<<11) | (g<<5) | (b));
}
/*功能:实现画点
*color是32bit的,位含义是0x00RRGGBB
*/
void fb_put_pixel(int x, int y, unsigned int color)
{
unsigned char *pc; /* 8bpp */
unsigned short *pw; /* 16bpp */
unsigned int *pdw; /* 32bpp */
unsigned int pixel_base = fb_base + (xres * bpp / 8) * y + x * bpp / 8;
switch (bpp)
{
case 8:
pc = (unsigned char *) pixel_base;
*pc = color;
break;
case 16:
pw = (unsigned short *) pixel_base;
*pw = convert32bppto16bpp(color);
break;
case 32:
pdw = (unsigned int *) pixel_base;
*pdw = color;
break;
}
}
下面是对这段代码的一些解释说明:
(1)framebuff.c文件中的fb_get_lcd_params函数,调用了lcd.c文件中的get_lcd_params函数(见8.1节的(3))来获取LCD芯片的一些参数,后续将根据这些参数进行一些设置。
为什么要将get_lcd_params函数封装成fb_get_lcd_params函数呢?因为纯软件层的画线画圆写字(把它们叫做“纯软件层的上级”),与lcd.c这层(LCD芯片特性层)之间隔着framebuffer.c这层(把它叫做“纯软件层的下级”)。根据分层思想,画线条画圆写字这层,不能跨越framebuffer.c这层去访问lcd.c文件这层,只能访问到framebuffer.c这层。
注意在framebuff.h中声明一下fb_get_lcd_params函数。
(2)如何显示某一个像素的颜色呢?
假设这个像素的坐标是(x,y),我们需要找到这个像素对应着FrameBuffer中哪个地址,然后在该地址写入这个像素颜色的数据即可。
1)首先明确地址:像素(x,y)的数据,应该放在 fb_base + (xres * bpp / 8) * y + x * bpp / 8 地址处。
2)然后明确在该地址填入什么样的数据:
- 对于8PP,每个像素在FrameBuffer中占据8位,因此采用unsigned char类型;
- 对于16PP,每个像素在FrameBuffer中占据16位,因此采用unsigned short类型;
- 对于32PP,每个像素在FrameBuffer中占据32位,因此采用unsigned int类型。
在编程时,我们一般将颜色的数据类型设置为int类型(其位含义是0x00RRGGBB),比如下面的color参数就是int类型。
void fb_put_pixel(int x, int y, unsigned int color)
如何将int类型的数据(其位含义是0x00RRGGBB),转换为符合要求的BPP数据,并写入FrameBuffer中进行显示呢?
如果需要转化为32BPP(或者24BPP,因为24BPP在内存中也是32位),则大小刚好对应,直接*pc = color即可。
如果需要转化为8BPP,由于8BPP需要通过调色板来实现,后面再讲解,这里先暂时设置为*pc = color。
如果需要转为16BPP,则使用convert32bppto16bpp函数,将32BPP(int类型数据其实可以看作是32BPP格式的数据)转换为16BPP。
(3)关于convert32bppto16bpp函数的说明如下(如何将RGB888转换为RGB565):
上面提到,表示颜色的int类型数据(其位含义是0x00RRGGBB)等同于32BPP格式的数据。
32BPP其实与24BPP差别不大,都有RGB888,R是bit[23:16]、G是bit[15:8]、B是bit[7:0],如下图所示:
16BPP的RGB568,是分别截取RGB888的bit[23:19]、bit[15:11]、bit[7:3]形成的,对应着下面代码:
/*函数功能:将32bpp转换为16bpp(将RGB888转换为RGB565)
*参数rgb是int类型(32位的),位含义是0x00RRGGBB
*注意返回值是unsigned short类型的,2个字节
*/
unsigned short convert32bppto16bpp(unsigned int rgb)
{
//分别取出RGB888中的组分
int r = (rgb >> 16)& 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
//形成RGB565
r = r >> 3;
g = g >> 2;
b = b >> 3;
return ((r<<11) | (g<<5) | (b)); //返回值是unsigned short类型,2字节
}
9.2 实现画线画圆
在完成画点函数之后,我们可以继续实现画线画圆,放在geometry.c文件中(注意在其对应头文件中声明一下有哪些函数)。
geometry.h文件如下:
#ifndef _GEOMETRY_H
#define _GEOMETRY_H
void draw_circle(int x, int y, int r, int color);
void draw_line(int x1,int y1,int x2,int y2,int color);
#endif /* _GEOMETRY_H */
geometry.c文件如下:
#include "framebuffer.h"
/*
* http://blog.csdn.net/p1126500468/article/details/50428613
*/
//-------------画圆函数。参数:圆心,半径,颜色----------
// 画1/8圆 然后其他7/8对称画
// ---------------->X
// |(0,0) 0
// | 7 1
// | 6 2
// | 5 3
// (Y)V 4
//
// L = x^2 + y^2 - r^2
void draw_circle(int x, int y, int r, int color)
{
int a, b, num;
a = 0;
b = r;
while(22 * b * b >= r * r) // 1/8圆即可
{
fb_put_pixel(x + a, y - b,color); // 0~1
fb_put_pixel(x - a, y - b,color); // 0~7
fb_put_pixel(x - a, y + b,color); // 4~5
fb_put_pixel(x + a, y + b,color); // 4~3
fb_put_pixel(x + b, y + a,color); // 2~3
fb_put_pixel(x + b, y - a,color); // 2~1
fb_put_pixel(x - b, y - a,color); // 6~7
fb_put_pixel(x - b, y + a,color); // 6~5
a++;
num = (a * a + b * b) - r*r;
if(num > 0)
{
b--;
a--;
}
}
}
//-----------画线。参数:起始坐标,终点坐标,颜色--------
void draw_line(int x1,int y1,int x2,int y2,int color)
{
int dx,dy,e;
dx=x2-x1;
dy=y2-y1;
if(dx>=0)
{
if(dy >= 0) // dy>=0
{
if(dx>=dy) // 1/8 octant
{
e=dy-dx/2;
while(x1<=x2)
{
fb_put_pixel(x1,y1,color);
if(e>0){y1+=1;e-=dx;}
x1+=1;
e+=dy;
}
}
else // 2/8 octant
{
e=dx-dy/2;
while(y1<=y2)
{
fb_put_pixel(x1,y1,color);
if(e>0){x1+=1;e-=dy;}
y1+=1;
e+=dx;
}
}
}
else // dy<0
{
dy=-dy; // dy=abs(dy)
if(dx>=dy) // 8/8 octant
{
e=dy-dx/2;
while(x1<=x2)
{
fb_put_pixel(x1,y1,color);
if(e>0){y1-=1;e-=dx;}
x1+=1;
e+=dy;
}
}
else // 7/8 octant
{
e=dx-dy/2;
while(y1>=y2)
{
fb_put_pixel(x1,y1,color);
if(e>0){x1+=1;e-=dy;}
y1-=1;
e+=dx;
}
}
}
}
else //dx<0
{
dx=-dx; //dx=abs(dx)
if(dy >= 0) // dy>=0
{
if(dx>=dy) // 4/8 octant
{
e=dy-dx/2;
while(x1>=x2)
{
fb_put_pixel(x1,y1,color);
if(e>0){y1+=1;e-=dx;}
x1-=1;
e+=dy;
}
}
else // 3/8 octant
{
e=dx-dy/2;
while(y1<=y2)
{
fb_put_pixel(x1,y1,color);
if(e>0){x1-=1;e-=dy;}
y1+=1;
e+=dx;
}
}
}
else // dy<0
{
dy=-dy; // dy=abs(dy)
if(dx>=dy) // 5/8 octant
{
e=dy-dx/2;
while(x1>=x2)
{
fb_put_pixel(x1,y1,color);
if(e>0){y1-=1;e-=dx;}
x1-=1;
e+=dy;
}
}
else // 6/8 octant
{
e=dx-dy/2;
while(y1>=y2)
{
fb_put_pixel(x1,y1,color);
if(e>0){x1-=1;e-=dy;}
y1-=1;
e+=dx;
}
}
}
}
}
9.3 测试与验证
修改一下测试程序lcd_test.c文件:
#include "geometry.h"
void lcd_test(void)
{
unsigned int fb_base;
int xres, yres, bpp;
int x, y;
unsigned short *p;
unsigned int *p2;
/* 初始化LCD */
lcd_init();
/* 使能LCD */
lcd_enable();
//这段代码有些怪异……不是使用一个应该就可以了,比如使用fb_这个?验证确实需要写两个
/* 获得LCD的参数: fb_base, xres, yres, bpp */
get_lcd_params(&fb_base, &xres, &yres, &bpp);
fb_get_lcd_params();
/* 往framebuffer中写数据 */
if (bpp == 16)
{
/* 让LCD输出整屏的红色 */
/* 565: 0xf800 */
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0xf800;
/* green */
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0x7e0;
/* blue */
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0x1f;
/* black */
p = (unsigned short *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p++ = 0;
}
else if (bpp == 32)
{
/* 让LCD输出整屏的红色 */
/* 0xRRGGBB */
p2 = (unsigned int *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p2++ = 0xff0000;
/* green */
p2 = (unsigned int *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p2++ = 0x00ff00;
/* blue */
p2 = (unsigned int *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p2++ = 0x0000ff;
/* black */
p2 = (unsigned int *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p2++ = 0;
}
delay(1000000);
/* 画线 */
draw_line(0, 0, xres - 1, 0, 0xff0000);//上边线
draw_line(xres - 1, 0, xres - 1, yres - 1, 0xffff00);//右边线
draw_line(0, yres - 1, xres - 1, yres - 1, 0xff00aa);//下边线
draw_line(0, 0, 0, yres - 1, 0xff00ef);//左边线
draw_line(0, 0, xres - 1, yres - 1, 0xff4500);//左上到右下
draw_line(xres - 1, 0, 0, yres - 1, 0xff0780);//右上到左下
delay(1000000);
/* 画圆 */
draw_circle(xres/2, yres/2, yres/4, 0xff00);//在屏幕中心画一个半径为yres/4的圆
}
从代码可知,我们想要下图这样的显示效果(六条线一个圆):
以上修改之后得到“007_dot_line_circle_017_008”文件夹,将之编译烧写与运行,发现实验现象如下所示,可见符合预期:
注意一下上面lcd_test()函数调用了两次获取LCD参数的函数:
//这段代码有些怪异……不是使用一个应该就可以了,比如使用fb_这个?验证确实需要写两个
/* 获得LCD的参数: fb_base, xres, yres, bpp */
get_lcd_params(&fb_base, &xres, &yres, &bpp);
fb_get_lcd_params();
我试过只用第一个不用第二个,发现可以显示红绿蓝全屏但不能显示六线一圆;只使用第二个时,现象很怪异(没有六线一圆,但也不是红绿蓝全屏),只有使用两个时才符合预期。有空解决一下。
十、LCD编程_显示字母
10.1 字母显示原理
本节实现如何在LCD上显示字母。
字母也是由像素点构成的,一个个像素点组成的点阵,在宏观上就表现为字母。那某个字母对应怎样的点阵呢?我们参考Linux内核源码中(在源码中搜索“font”),选择打开font_8x16.c文件(由文件名字可知,一个字母用8*16个像素点来表示),发现里面是一个数组。
以字符'A'为例,它对应的点阵如下图所示。这个图含义:8位的二进制数据中,1表示某个像素点有颜色,0则表示没有颜色。另外由图可知,一个字母对应着8*16个像素点。
10.2 实现字母显示
接下来实现字母的显示,代码放在lcd/font.c文件中。
首先要把上面的font_8x16.c文件复制到font.c同级目录中,然后修改一下font_8x16.c文件:删掉 #include <linux/font.h>,删除数组的static修饰符(否则别的文件无法访问它)。
font.c文件的内容如下(另外要编写对应的头文件font.h):
extern const unsigned char fontdata_8x16[];
/* 获得LCD参数 */
static unsigned int fb_base;
static int xres, yres, bpp;
void font_init(void)
{
get_lcd_params(&fb_base, &xres, &yres, &bpp);
}
/* 根据字母的点阵在LCD上描画文字 */
void fb_print_char(int x, int y, char c, unsigned int color)
{
int i, j;
/* 根据c的ascii码在fontdata_8x16中得到点阵数据 */
unsigned char *dots = &fontdata_8x16[c * 16];
unsigned char data;
int bit;
/* 根据点阵来设置对应象素的颜色 */
for (j = y; j < y+16; j++)
{
data = *dots++;
bit = 7;
for (i = x; i < x+8; i++)
{
/* 根据点阵的某位决定是否描颜色 */
if (data & (1<<bit))
fb_put_pixel(i, j, color);
bit--;
}
}
}
/* "abc\n\r123" */
void fb_print_string(int x, int y, char* str, unsigned int color)
{
int i = 0, j;
while (str[i])
{
if (str[i] == '\n')
y = y+16;
else if (str[i] == '\r')
x = 0;
else
{
fb_print_char(x, y, str[i], color);
x = x+8;
if (x >= xres) /* 换行 */
{
x = 0;
y = y+16;
}
}
i++;
}
}
下面是对这段代码的一些解析说明:
(1)下面这段代码,是因为每个字符在点阵数组中占据16字节。
/* 根据c的ascii码在fontdata_8x16中得到点阵数据 */
unsigned char *dots = &fontdata_8x16[c * 16];
(2)如何显示一个字符呢?由下图可知,一个字符对应8*16个像素(每行8个像素,共16行)。
我们可以根据font_8x16.c提供的点阵数据,一行行地描,所以对应着以下代码:
void fb_print_char(int x, int y, char c, unsigned int color)
{
int i, j;
/* 根据c的ascii码在fontdata_8x16中得到点阵数据 */
unsigned char *dots = &fontdata_8x16[c * 16];
unsigned char data;
int bit;
/* 根据点阵来设置对应象素的颜色 */
for (j = y; j < y+16; j++)//一次循环描一行,共有16行
{
data = *dots++;
bit = 7;
for (i = x; i < x+8; i++)//一次循环描一个点,每行有8个点
{
/* 根据点阵的某位决定是否描颜色 */
if (data & (1<<bit))
fb_put_pixel(i, j, color);
bit--;
}
}
}
(3)接着基于上面的 fb_print_char 函数,实现了一个打印字符串的函数,如下所示。
/*在(x,y)开始处打印字符串str,字符串的颜色是color
* 例如输出“abc\n\r123”
*/
void fb_print_string(int x, int y, char* str, unsigned int color)
{
int i = 0, j;
while (str[i])
{
if (str[i] == '\n')//如果遇到换行
y = y+16; //每行字符的高度是16像素点
else if (str[i] == '\r')//如果遇到回车
x = 0;//回到某行的0像素点
else//如果是其他字符,打印呗
{
fb_print_char(x, y, str[i], color);//打印一个字符
x = x+8;
if (x >= xres) /* 换行 */ //感觉这个换行的判断有些问题
{
x = 0;
y = y+16;
}
}
i++;
}
}
我觉得下面代码有问题:
else//如果是其他字符,打印呗
{
fb_print_char(x, y, str[i], color);//打印一个字符
x = x+8;
if (x >= xres) /* 换行 */ //感觉这个换行的判断有些问题
{
x = 0;
y = y+16;
}
}
应该改为:
else//如果是其他字符,打印呗
{
if((xres-x)<8)//先判断特殊情况:选取的x具距离边界不够8个像素,无法显示1个字符
{ //此时就应该直接换行
x=0;
y=y+16;
}
/* else if( (xres-x)>8 && (xres-x)<16 )
{
fb_print_char(x, y, str[i], color);//打印一个字符
x=0;
y=y+16;
}
*/
else
{
fb_print_char(x, y, str[i], color);//打印一个字符
x=x+8;
}
}
10.3 测试与验证
首先修改一下lcd_test.c文件,如下所示:
1)包含头文件font.h;
2)调用font_init函数来获取LCD参数;
3)调用fb_print_string函数输出某个字符串(比如“XJH is very handsome!\n\rXJH has no money!\n\rWhat a pity!”)。
然后修改一下Makefile文件,添加下面语句:
objs += lcd/font.o
objs += lcd/font_8x16.o
实验现象如下,可见符合预期:
至于如何显示汉字,或者显示更加复杂的矢量字符,在第三期项目(数码相框)中有更加详细的介绍(使用应用程序来显示字符,但原理和本文裸板显示字符是一样的)。
十一、LCD编程_解决除法问题
11.1 问题背景
在8.2(2)4)A中提到,因为缺失某些除法函数(__floatsidf、__divdf3、__subdf3、、__fixdfsi),编译时出错,如下所示:
lcd/s3c2440_lcd_controller.o(.text+0xb4): In function `s3c2440_lcd_controller_init':
: undefined reference to `__floatsidf'
lcd/s3c2440_lcd_controller.o(.text+0xd0): In function `s3c2440_lcd_controller_init':
: undefined reference to `__divdf3'
lcd/s3c2440_lcd_controller.o(.text+0xec): In function `s3c2440_lcd_controller_init':
: undefined reference to `__divdf3'
lcd/s3c2440_lcd_controller.o(.text+0x108): In function `s3c2440_lcd_controller_init':
: undefined reference to `__subdf3'
lcd/s3c2440_lcd_controller.o(.text+0x124): In function `s3c2440_lcd_controller_init':
: undefined reference to `__adddf3'
lcd/s3c2440_lcd_controller.o(.text+0x138): In function `s3c2440_lcd_controller_init':
: undefined reference to `__fixdfsi'
make: *** [all] Error 1
下面这段代码位于s3c2440_lcd_controller.c文件中:为了解决编译错误,我们注释掉除法计算的过程,直接给变量赋一个定值5。
//int clkval = (double)HCLK/plcdparams->time_seq.vclk/2-1+0.5;
int clkval = 5;
int bppmode = plcdparams->bpp == 8 ? 0xb :\
plcdparams->bpp == 16 ? 0xc :\
0xd; /* 0xd: 24,32bpp */
LCDCON1 = (clkval<<8) | (3<<5) | (bppmode<<1) ;
但采取这样的解决方法有个弊端,即如果换另一款LCD则需要修改这个定值。所以现在我们在第十节代码的基础上,将上面这段代码的注释打开,并删除给变量clkval赋定值5的语句,然后实现错误提示中所涉及的除法函数。
实际上,lib1funcs.S文件中是有除法的(该文件以及string_utils.c文件是在哪个章节引入的?),但功能不够强大。
11.2 解决问题
这里提供几个解决问题的思路:
- 可以参考集裸机程序大成者u-boot的源码(如果它里面有相关的实现)。
- 可以在内核源码中查找(它里面一般会实现这些除法函数)。
- 可以在库函数中查找(编译器一般会自带很多基本的库文件,比如数学库libm.a)。
- 可以参考网上别人的实现(如果有的话)。
(1)在u-boot中查找
比如,对于__floatsidf函数,我们在SI中打开u-boot的源码,查找是否有该函数的实现代码(没有);接着搜索__divdf3(也没有);接着搜索__subdf3(也没有)……居然一个也没有?!
(2)在内核源码中寻找
在内核源码中查找看看。比如在linux-2.6.22.6版本的代码中查找,只发现下面这段被注释掉的代码,没什么参考价值:
(3)链接时添加库文件
数学库中肯定实现了这些除法函数,但库文件一般以文件形式发布(比如.a或者.so文件),我们无法看到这些除法函数的源码。现在我们想找到这些除法函数的源码,因此在网上(百度与必应)搜寻“ undefined reference to `__floatsidf' ”,但是找不到靠谱的解决方法。
那就在链接时添加这些除法函数对应的库吧。流程如下:
- 通过“arm-linux-gcc -version”查看当前使用的交叉编译工具链(查看编译器的版本)。
- 通过“echo $PATH”查看当前使用的交叉编译工具链所在的路径(查看编译器在哪里)。
- 在交叉编译工具链所在路径搜索相关函数(例如“ grep "__floatsidf" ./ -nr ”),获知该函数在哪个库文件中实现。
- 该函数可能在很多库文件中都有实现,我们把其中的静态库文件(即.a后缀的文件)拷贝到代码文件夹中。
- 修改Makefile,即在arm-linux-ld命令中依次加入每个静态库文件,直至编译成功。然后在代码文件夹中保留能使编译成功的库文件,删掉其他拷贝过来的库文件(为了节省存储空间)。
比如我们要明确__floatsidf函数在哪个库文件中实现:
xjh@ubuntu:/$ echo $PATH
/opt/hisi-linux/x86-arm/arm-hisiv300-linux/target/bin:/usr/local/arm/gcc-3.4.5-glibc-2.3.6/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
xjh@ubuntu:/$ arm-linux-gcc --version
arm-linux-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
xjh@ubuntu:/$ cd /usr/local/arm/gcc-3.4.5-glibc-2.3.6
xjh@ubuntu:/usr/local/arm/gcc-3.4.5-glibc-2.3.6$ grep "__floatsidf" ./ -nr
Binary file ./include/c++/3.4.5/arm-linux/bits/stdc++.h.gch/O0g matches
Binary file ./include/c++/3.4.5/arm-linux/bits/stdc++.h.gch/O2g matches
Binary file ./tmp/arm-linux-hello2-static matches
./info/gccint.info:556: -- Runtime Function: double __floatsidf (int I)
./info/gccint.info:24593:* __floatsidf: Soft float library routines.
Binary file ./lib/gcc/arm-linux/3.4.5/libgcc.a matches
Binary file ./arm-linux/bin/localedef matches
Binary file ./arm-linux/bin/sprof matches
Binary file ./arm-linux/lib/libgcc_s.so.1 matches
Binary file ./arm-linux/lib/libm.a matches
Binary file ./arm-linux/lib/libc.a matches
Binary file ./arm-linux/lib/libiberty.a matches
Binary file ./arm-linux/lib/libc-2.3.6.so matches
Binary file ./arm-linux/lib/libm-2.3.6.so matches
xjh@ubuntu:/usr/local/arm/gcc-3.4.5-glibc-2.3.6$
可见在libm-2.3.6.so、libc-2.3.6.so、libiberty.a、libc.a、libm.a、libgcc_s.so.1、libgcc.a等文件中都有__floatsidf函数的实现。我们将静态库文件(因为我们使用静态链接)libiberty.a、libc.a、libm.a、libgcc.a文件拷贝到代码文件夹中。但具体哪个能用,需要一一尝试。
现在试一下将libc.a文件链接进去,看能否编译成功。将Makefile文件修改如下(在arm-linux-ld命令中加入参数libc.a),执行编译指令make时还是报同样的错误,可知libc.a不是合适的库文件。
//篇幅缘故,省略其他
all: $(objs)
arm-linux-ld -T sdram.lds $^ libc.a -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
经过一一尝试,最后发现链接时只有添加libgcc.a这个库文件,才可以使得编译通过。添加libm.a居然不行!看来这些除法函数的实现,是放在libgcc.a库文件中,而不是放在libm.a数学库文件中。
注意,如果你更换了交叉编译工具链,需要自己去交叉编译工具链目录下寻找对应的libgcc.a,有可能有多个libgcc.a,需要逐个尝试。
本节的问题,似乎在博文1、博文2中有相关的描述。
十二、LCD编程_调用调色板
12.1 调色板原理
前面写的程序都是采用16BPP或24BPP(即32BPP),假如我们要使用8PP,则需要使用调色板。
在2.4节中我们曾经讲过调色板的工作原理:采用8BPP时,某个像素的数据(此时称为索引)在FrameBuffer中占据8位,则调色板中有2^8=256项颜色数据,每项颜色数据占16位。LCD控制器把FrameBuffer中的8位数据当做索引,在调色板中找到它对应的16位数据,然后将这16位数据发给电子枪。
调色板是一块内存,有自己的地址和格式。在S3C2440数据手册中搜索“PALETTE”,在P416有以下内容,由此可知:调色板的起始地址0x4D00_0400(这个地址隶属于LCD控制器);一共有256项,每项占据4个字节,但只用到最低2个字节。
12.2 调用调色板
在硬件上,我们要初始化这个调色板,才能通过索引得到颜色。
根据第三节的软件框架,调色板初始化函数应该放在s3c2440_lcd_controller.c文件里面。
1、我们先在lcd_controller.h文件中,修改lcd_controller结构体的定义(添加一个函数指针成员):
#ifndef _LCD_CONTROLLER_H
#define _LCD_CONTROLLER_H
#include "lcd.h"
typedef struct lcd_controller {
char *name;
void (*init)(p_lcd_params plcdparams);
void (*enable)(void);
void (*disable)(void);
void (*init_palette)(void);//添加这个
}lcd_controller, *p_lcd_controller;
#endif /* _LCD_CONTROLLER_H */
(2)然后在s3c2440_lcd_controller.c文件中,给s3c2440_lcd_controller结构体的函数指针成员init_palette赋初值:
struct lcd_controller s3c2440_lcd_controller = {
.name = "s3c2440",
.init = s3c2440_lcd_controller_init,
.enable = s3c2440_lcd_controller_enalbe,
.disable = s3c2440_lcd_controller_disable,
.init_palette = s3c2440_lcd_controller_init_palette,
};
接下来我们要实现s3c2440_lcd_controller_init_palette函数,即调色板初始化函数:
/* 设置调色板之前, 先关闭lcd_controller */
void s3c2440_lcd_controller_init_palette(void)
{
volatile unsigned int *palette_base = (volatile unsigned int *)0x4D000400;
int i;
// 取出LCDCON1[0],下面的if判断它是否为1(是否已经打开LCD控制器,打开为1,关闭为0)
int bit = LCDCON1 & (1<<0);
/* LCDCON1'BIT 0 : 设置LCD控制器是否输出信号 */
if (bit)
LCDCON1 &= ~(1<<0);//如果打开了就先关闭LCD控制器
//填充调色板数据数组(调色板一共有256种颜色,它们的颜色数据是什么,这里来定义)
for (i = 0; i < 256; i++)
{
/* 低16位 : rgb565 */
*palette_base++ = i;
}
if (bit)//这里难道不是if(!bit)吗?
LCDCON1 |= (1<<0);
}
下面是对这段代码的一些说明:
1)设置调色板前,先判断LCD控制器是否打开,如果打开了就先关闭,且设置完成后再打开。
2)在网上没有找到调色板数据对应的数组,这里作为实验就随便设置了,比如我们让调色板数据等于i(i=0~255)。这样的设置,会让调色板的颜色整体偏蓝,因为红色成分为0(5bit全为0),绿色成分也很少(只用到了3bit),而蓝色用到了5bit,即相当于R0G3B5。
(3)修改lcd_4.3.c文件,将BPP改为8。
(4)再修改lcd_test.c文件,加入BPP=8的情形,让屏幕显示某种颜色:
if (bpp == 8)
{
/* bpp: palette[12] */
p0 = (unsigned char *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p0++ = 12;
/* palette[47] */
p0 = (unsigned char *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p0++ = 47;
/* palette[88] */
p0 = (unsigned char *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p0++ = 88;
/* palette[0] */
p0 = (unsigned char *)fb_base;
for (x = 0; x < xres; x++)
for (y = 0; y < yres; y++)
*p0++ = 0;
}
(5)再修改Icd_controller.c文件中lcd_controller_init函数,在里面加入调色板初始化函数:
/* 向上: 接收不同LCD的参数
* 向下: 使用这些参数设置对应的LCD控制器
*/
int lcd_controller_init(p_lcd_params plcdparams)
{
/* 调用所选择的LCD控制器的初始化函数 */
if (g_p_lcd_controller_selected)
{
g_p_lcd_controller_selected->init(plcdparams);
g_p_lcd_controller_selected->init_palette();//加入这行代码
return 0;
}
return -1;
}
(6)在lcd_test.c调用的画线画圆、显示字母的函数里,修改下颜色(其实只有低两位有效,因为调色板映射范围是0-255):
/* 画线 */
draw_line(0, 0, xres - 1, 0, 0x23ff77);
draw_line(xres - 1, 0, xres - 1, yres - 1, 0xffff);
draw_line(0, yres - 1, xres - 1, yres - 1, 0xff00aa);
draw_line(0, 0, 0, yres - 1, 0xff00ef);
draw_line(0, 0, xres - 1, yres - 1, 0xff45);
draw_line(xres - 1, 0, 0, yres - 1, 0xff0780);
delay(1000000);
/* 画圆 */
draw_circle(xres/2, yres/2, yres/4, 0xff);
/* 输出文字 */
fb_print_string(10, 10, "www.100ask.net\n\r100ask.taobao.com", 0xff);
12.3 测试与验证
编译通过,实验现象符合预期,见链接。
十三、作业预留
1、如果有MINI2440、TQ2440或是带3.5寸LCD的JZ2440,添加一个lcd_xxx.c文件,构造lcd_params结构体,体验一下结构化编程的优点:可以很轻松支持其他LCD。
2、找到汉字库点阵,在LCD上显示汉字。这需要你善用百度,找到汉字库,也许要阅读别人的代码了解汉字库点阵的存储方式。这是一个综合能力的体现。