JZ2440开发板——LCD

以下内容源于韦东山嵌入式课程的学习与整理,如有侵权请告知删除。

之前在博文中学习过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上显示汉字。这需要你善用百度,找到汉字库,也许要阅读别人的代码了解汉字库点阵的存储方式。这是一个综合能力的体现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/906972.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024 Rust现代实用教程:Ownership与结构体、枚举

文章目录 一、Rust的内存管理模型1.GC&#xff08;Stop the world&#xff09;2.C/C内存错误大全3.Rust的内存管理模型 二、String与&str1.String与&str如何选择2.Example 三、枚举与匹配模式1.常见的枚举类型&#xff1a;Option和Result2.匹配模式 四、结构体、方法、…

数据结构C语言描述1(图文结合)--顺序表讲解,实现,表达式求值应用,考研可看

前言 这个专栏将会用纯C实现常用的数据结构和简单的算法&#xff1b;用C基础即可跟着学习&#xff0c;代码均可运行&#xff1b;准备考研的也可跟着写&#xff0c;个人感觉&#xff0c;如果时间充裕&#xff0c;手写一遍比看书、刷题管用很多&#xff0c;这也是本人采用纯C语言…

Oracle视频基础1.3.7练习

1.3.7 看oracle是否启动构造一个pfile:boobooke.ora看spfilewilson内容修改alert log file里拷贝的参数内容&#xff0c;创建一个pfile boobooke.ora用新创建的pfile启动数据库&#xff0c;并创建新的spfile:spfilebbk.ora启动数据库&#xff0c;监听&#xff0c;看新的进程解…

数据转换 | Matlab基于SP符号递归图(Symbolic recurrence plots)一维数据转二维图像方法

目录 基本介绍程序设计参考资料获取方式 基本介绍 Matlab基于SP符号递归图&#xff08;Symbolic recurrence plots&#xff09;一维数据转二维图像方法 符号递归图(Symbolic recurrence plots)是一种一维时间序列转图像的技术&#xff0c;可用于平稳和非平稳数据集;对噪声具有…

将android studio3.4.1改为as 2024的方法

将android studio3.4.1改为as 2024的方法 先正常的open项目&#xff0c;不用等gradle build完&#xff0c;就先改这2个地方&#xff1a; 1.替换as2024新建一个空的正常的项目的build.gradle和config.gradle 2.在build.gradle(app)中 删除3处kotlin相关的提示&#xff08;2个…

【温酒笔记】DMA

参考文档&#xff1a;野火STM32F103 网友资料整理 1. Direct Memory Access-直接内存访问 DMA控制器独立于内核 是一个单独的外设 DMA1有7个通道DMA2有5个通道DMA有四个等级&#xff0c;非常高&#xff0c;高&#xff0c;中&#xff0c;低四个优先级如果优先等级相同&#xf…

Stable Diffusion Web UI 1.9.4常用插件扩展-WD14-tagger

Stable Diffusion Web UI 1.9.4 运行在 WSL 中的 Docker 容器中 tagger 插件的作用是&#xff0c;上传一张图片&#xff0c;反推这张图片可能的提示词。 使用场景就是&#xff0c;想要得到类似的图片内容时使用。 WD14-tagger 安装 Stable Diffusion WebUI WD14-tagger GitH…

VS 中使用c#高版本语言方法

方法如下&#xff0c;打开项目工程文件&#xff08;记事本&#xff09;&#xff0c;然后添加如下语句&#xff1a;保存&#xff0c;重新加载即可使用最新C#语法。

Golang--DOS命令、变量、基本数据类型、标识符

1、DOS命令 切换盘符&#xff08;大小写没有区别&#xff09;&#xff1a; c: //切换到c盘 d: //切换到d盘 e: //切换到e盘 显示文件内信息&#xff1a; 指令&#xff1a;dir 切换路径&#xff1a; 指令&#xff1a;cd 绝对路径 / 相对路径 . 表示当前…

正反向滤波的简述和MATLAB代码

文章目录 MATLAB 例程:卡尔曼滤波与反向滤波运行结果代码说明总结P.S. 相关公式1. 正向滤波(Forward Filtering)2. 反向滤波(Backward Filtering)3. 总结以下是一个简单的MATLAB例程,演示如何使用卡尔曼滤波进行状态估计,包括正向滤波和反向滤波的实现。这个例程模拟了一…

Django目录结构最佳实践

Django项目目录结构 项目目录结构配置文件引用修改创建自定义子应用方法修改自定义注册目录从apps目录开始 项目目录结构 └── backend # 后端项目目录&#xff08;项目名称&#xff09;├── __init__.py├── logs # 项目日志目录├── manage.py #…

KVM 使用主机 GPU

KVM 如何使用主机的 GPU&#xff0c;首先安装 KVM。 配置Grub vi /etc//etc/default/grub GRUB_CMDLINE_LINUX"amd_iommuon iommupt videoefifb:off vfio_pci.ids10de:1e07,10de:10f7,10de:1ad6,10de:1ad7"查看主机显卡信息 lspci -nnk | grep -A 3 VGA 找到GP…

b站小土堆PyTorch视频学习笔记(二)

Dataloader:提供不同类型的数据集&#xff1b;为后面的网络提供不同的数据形式 Dataset&#xff1a;提供一种方式去获取数据及其label&#xff08;标签&#xff09; 主要实现以下两个功能&#xff1a; {如何获取每一个数据及其lable&#xff1b;告诉我们总共有多少数据} fr…

nginx的proxy_next_upstream使用中的一个坑

今天线上系统出了点问题&#xff0c;机房的电信出口突然不通了&#xff0c;原本以为能自动切换的nginx配置&#xff0c;居然没有生效&#xff0c;导致了业务告警&#xff0c;手工紧急处理了才解决了。 当时的设想是&#xff0c;如果这个服务的访问&#xff0c;出现了500或者超…

【Git】SSH密钥

目录 1 前言2 SSH密钥2.1 生成密钥2.2 查看密钥2.3 关联Git服务器 3 小结 1 前言 许多Git服务器都使用SSH公钥进行认证&#xff0c;为了向Git服务器提供SSH公钥&#xff0c;如果某系统用户尚未拥有密钥&#xff0c;必须事先为其生成一份。 2 SSH密钥 2.1 生成密钥 在Window…

【Seed-Labs】SQL Injection Attack Lab

Overview SQL 注入是一种代码注入技术&#xff0c;利用的是网络应用程序与数据库服务器之间接口的漏洞。当用户输入的信息在发送到后端数据库服务器之前没有在网络应用程序中进行正确检查时&#xff0c;就会出现这种漏洞。 许多网络应用程序从用户那里获取输入&#xff0c;然…

ClkLog企业版(CDP)预售开启,更有鸿蒙SDK前来助力

新版本发布 ClkLog在上线近1年后&#xff0c;获得了客户的一致肯定与好评&#xff0c;并收到了不少客户对功能需求的反馈。根据客户的反馈&#xff0c;我们在今年三季度对ClkLog的版本进行了重新的规划与调整&#xff0c;简化了原有的版本类型&#xff0c;方便客户进行选择。 与…

T矩阵其实就是pauli基的乘,S矩阵中hv是体散射分量

注意什么是面散射&#xff0c;二次散射和体散射。 ShhSvv表示单次散射的电压&#xff0c;|ShhSvv|^2是功率

群控系统服务端开发模式-应用开发-上传配置功能开发

下面直接进入上传配置功能开发&#xff0c;废话不多说。 一、创建表 1、语句 CREATE TABLE cluster_control.nc_param_upload (id int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 编号,upload_type tinyint(1) UNSIGNED NOT NULL COMMENT 上传类型 1&#xff1a;本站 2&a…

HarmonyOS NEXT 应用开发实战(九、知乎日报项目详情页实现详细介绍)

在本篇博文中&#xff0c;我们将探讨如何使用 HarmonyOS Next 框架开发一个知乎日报的详情页&#xff0c;逐步介绍所用到的组件及代码实现。知乎日报是个小巧完整的小项目&#xff0c;这是一个循序渐进的过程&#xff0c;适合初学者和有一定开发经验的工程师参考。 1. 项目背景…