28. LCD—液晶显示中英文

本章参考资料:《STM32F10X-中文参考手册》、《STM32F103增强型系列数据手册》。

在前面我们学习了如何使用FSMC外设控制液晶屏并用它显示各种图形,本章讲解如何控制液晶屏显示文字。使用液晶屏显示文字时,涉及到字符编码与字模的知识。

28.1. 字符编码

由于计算机只能识别0和1,文字也只能以0和1的形式在计算机里存储,所以我们需要对文字进行编码才能让计算机处理,编码的过程就是规定特定的01数字串来表示特定的文字,最简单的字符编码例子是ASCII码。

28.1.1. ASCII编码

学习C语言时,我们知道在程序设计中使用ASCII编码表约定了一些控制字符、英文及数字。它们在存储器中,本质也是 二进制数,只是我们约定这些二进制数可以表示某些特殊意义,如以ASCII编码解释数字“0x41”时,它表示英文字符“A”。ASC II码表分为两部分,第一部分是控制字符或通讯专用字符,它们的数字编码从0~31,见表 27‑1,它们并没有特定的图形显示,但会根据不同的应用程序,而对文本显示有不同的影响。ASCII码 的第二部分包括空格、阿拉伯数字、标点符号、大小写英文字母以及“DEL(删除控制)”,这部分符号的 数字编码从32~127,除最后一个DEL符号外,都能以图形的方式来表示,它们属于传统文字书写系统的一部分。

表 27‑1 ASCII码中的控制字符或通讯专用字符

十进制

十六进制

缩写/字符

解释

0

0

NUL(null)

空字符

1

1

SOH(start of headline)

标题开始

2

2

STX (start of text)

正文开始

3

3

ETX (end of text)

正文结束

4

4

EOT (end of transmission)

传输结束

5

5

ENQ (enquiry)

请求

6

6

ACK (acknowledge)

收到通知

7

7

BEL (bell)

响铃

8

8

BS (backspace)

退格

9

9

HT (horizontal tab)

水平制表符

10

0A

LF (NL line feed, new line)

换行键

11

0B

VT (vertical tab)

垂直制表符

12

0C

FF (NP form feed, new page)

换页键

13

0D

CR (carriage return)

回车键

14

0E

SO (shift out)

不用切换

15

0F

SI (shift in)

启用切换

16

10

DLE (data link escape)

数据链路转义

17

11

DC1 (device control 1)

设备控制1

18

12

DC2 (device control 2)

设备控制2

19

13

DC3 (device control 3)

设备控制3

20

14

DC4 (device control 4)

设备控制4

21

15

NAK (negative acknowledge)

拒绝接收

22

16

SYN (synchronous idle)

同步空闲

23

17

ETB (end of trans. block)

传输块结束

24

18

CAN (cancel)

取消

25

19

EM (end of medium)

介质中断

26

1A

SUB (substitute)

替补

27

1B

ESC (escape)

换码(溢出)

28

1C

FS (file separator)

文件分割符

29

1D

GS (group separator)

分组符

30

1E

RS (record separator)

记录分离符

31

1F

US (unit separator)

单元分隔符

表 27‑2 ASCII码中的字符及数字

十进制

十六进制

缩写/字符

十进制

十六进制

缩写/字符

32

20

(space)空格

80

50

P

33

21

!

81

51

Q

34

22

82

52

R

35

23

#

83

53

S

36

24

$

84

54

T

37

25

%

85

55

U

38

26

&

86

56

V

39

27

87

57

W

40

28

(

88

58

X

41

29

)

89

59

Y

42

2A

*

90

5A

Z

43

2B

91

5B

[

44

2C

,

92

5C

45

2D

93

5D

]

46

2E

.

94

5E

^

47

2F

/

95

5F

_

48

30

0

96

60

`

49

31

1

97

61

a

50

32

2

98

62

b

51

33

3

99

63

c

52

34

4

100

64

d

53

35

5

101

65

e

54

36

6

102

66

f

55

37

7

103

67

g

56

38

8

104

68

h

57

39

9

105

69

i

58

3A

:

106

6A

j

59

3B

;

107

6B

k

60

3C

<

108

6C

l

61

3D

=

109

6D

m

62

3E

>

110

6E

n

63

3F

?

111

6F

o

64

40

@

112

70

p

65

41

A

113

71

q

66

42

B

114

72

r

67

43

C

115

73

s

68

44

D

116

74

t

69

45

E

117

75

u

70

46

F

118

76

v

71

47

G

119

77

w

72

48

H

120

78

x

73

49

I

121

79

y

74

4A

J

122

7A

z

75

4B

K

123

7B

{

76

4C

L

124

7C


77

4D

M

125

7D

}

78

4E

N

126

7E

~

79

4F

O

127

7F

DEL

(delete)

删除

后来,计算机引进到其它国家的时候,由于他们使用的不是英语,他们使用的字母在ASCII码表中没有定义,所以他们采用127号之后的位来表示这些新的字母,还加入了各种形状,一直编号到255。从128到255这些字符被称为ASCII扩展字符集。至此基本存储单位Byte(char)能表示的编号都被用完了。

28.1.2. 中文编码

由于英文书写系统都是由26个基本字母组成,利用26个字母组可合出不同的单词,所以用ASCII码表就能表达整个英文书写系统。而中文书写系统中的汉字是独立的方块,若参考单词拆解成字母的表示方式,汉字可以拆解成部首、笔画来表示,但这样会非常复杂(可参考五笔输入法编码),所以中文编码直接对方块字进行编码,一个汉字使用一个号码。

由于汉字非常多,常用字就有6000多个,如果像ASCII编码表那样只使用1个字节最多只能表示256个汉字,所以我们使用2个字节来编码。

28.1.2.1. GB2312标准

我们首先定义的是GB2312标准。它把ASCII码表127号之后的扩展字符集直接取消掉,并规定小于127的编码按原来ASCII标准解释字符。当2个大于127的字符连在一起时,就表示1个汉字,第1个字节使用 (0xA1-0xFE) 编码,第2个字节使用(0xA1-0xFE)编码,这样的编码组合起来可以表示了7000多个符号,其中包含6763个汉字。在这些编码里,我们还把数学符号、罗马字母、日文假名等都编进表中,就连原来在ASCII里原本就有的数字、标点以及字母也重新编了2个字节长的编码,这就是平时在输入法里可切换的“全角”字符,而标准的ASCII码表中127号以下的就被称为“半角”字符。

表 27‑3说明了GB2312是如何兼容ASCII码的,当我们设定系统使用GB2312标准的时候,它遇到一个字符串时,会按字节检测字符值的大小,若遇到连续两个字节的数值都大于127时就把这两个连续的字节合在一起,用GB2312解码,若遇到的数值小于127,就直接用ASCII把它解码。

表 27‑3 GB2312兼容ASCII码的原理

第1字节

第2字节

表示的字符

说明

0x68

0x69

(hi)

两个字节的值都小于127(0x7F),使用ASCII解码

0xB0

0xA1

(啊)

两个字节的值都大于127(0x7F),使用GB2312解码

28.1.2.1.1. 区位码

在GB2312编码的实际使用中,有时会用到区位码的概念,见 图27_1。GB2312编码对 所收录字符进行了“分区”处理,共94个区,每区含有94个位,共8836个码位。而区位码实际是GB2312编码的内部形式,它规定对收录的每个字符采用两个字节表示,第一个字节为“高字节”,对应94个区;第二个字节为“低字节”,对应94个位。所以它的区位码范围是:0101-9494。为兼容ASCII码,区号和位号分别加上0xA0偏移就得到GB2312编码。在区位码上加上0xA0偏移,可求得GB2312编码范围:0xA1A1-0xFEFE,其中汉字的编码范围为0xB0A1-0xF7FE,第一字节0xB0-0xF7(对应区号:16-87),第二个字节0xA1-0xFE(对应位号:01-94)。

例如,“啊”字是GB2312编码中的第一个汉字,它位于16区的01位,所以它的区位码就是1601,加上0xA0偏移,其GB2312编码为0xB0A1。其中区位码为0101的码位表示的是“空格”符。

图 27‑1 GB2312 的部分区位码

图 27‑1 GB2312 的部分区位码

28.1.2.2. GBK编码

据统计,GB2312编码中表示的6763个汉字已经覆盖中国大陆99.75%的使用率,单看这个数字已经很令人满意了,但是我们不能因为那些文字不常用就不让它进入信息时代,而且生僻字在人名、文言文中的出现频率是非常高的。为此我们在GB2312标准的基础上又增加了14240个新汉字(包括所有后面介绍的Big5中的所有汉字)和符号,这个方案被称为GBK标准。增加这么多字符,按照GB2312原来的格式来编码,2个字节已经没有足够的编码,我们聪明的程序员修改了一下格式,不再要求第2个字节的编码值必须大于127,只要第1个字节大于127就表示这是一个汉字的开始,这样就做到了兼容ASCII和GB2312标准。

表27‑4说明了GBK是如何兼容ASCII和GB2312标准的,当我们设定系统使用GBK标准的时候,它按顺序遍历字符串,按字节检测字符值的大小,若遇到一个字符的值大于127时,就再读取它后面的一个字符,把这两个字符值合在一起,用GBK解码,解码完后,再读取第3个字符,重新开始以上过程,若该字符值小于127,则直接用ASCII解码。

表 27‑4 GBK兼容ASCII和GB2312的原理

第1字节

第2字节

第3字节

表示的字符

说明

0x68(<7F)

0xB0(>7F)

0xA1(>7F)

(h啊)

第1个字节小于127,使用ASCII解码,每2个字节大于127,直接使用GBK解码,兼容GB2312

0xB0(>7F)

0xA1(>7F)

0x68(<7F)

(啊h)

第1个字节大于127,直接使用GBK码解释,第3个字节小于127,使用ASCII解码

0xB0(>7F)

0x56(<7F)

0x68(<7F)

(癡h)

第1个字节大于127,第2个字节虽然小于127,直接使用GBK解码,第3个字节小于127,使用ASCII解码

28.1.2.3. GB18030

随着计算机技术的普及,我们后来又在GBK的标准上不断扩展字符,这些标准被称为GB18030,如GB18030-2000、GB18030-2005等(“-”号后面的数字是制定标准时的年号),GB18030的编码使用4个字节,它利用前面标准中的第2个字节未使用的“0x30-0x39”编码表示扩充四字节的后缀,兼容GBK、GB2312及ASCII标准。

GB18030-2000主要在GBK基础上增加了“CJK(中日韩)统一汉字扩充A”的汉字。加上前面GBK的内容,GB18030-2000一共规定了27533个汉字(包括部首、部件等)的编码,还有一些常用非汉字符号。

GB18030-2005的主要特点是在GB18030-2000基础上增加了“CJK(中日韩)统一汉字扩充B”的汉字。增加了42711个汉字和多种我国少数民族文字的编码(如藏、蒙古、傣、彝、朝鲜、维吾尔文等)。加上前面GB18030-2000的内容,一共收录了70244个汉字。

GB2312、GBK及GB18030是汉字的国家标准编码,新版向下兼容旧版,各个标准简要说明见表 27‑5,目前比较流行的是GBK编码,因为每个汉字只占用2个字节,而且它编码的字符已经能满足大部分的需求,但国家要求一些产品必须支持GB18030标准。

表 27‑5 汉字国家标准

类别

编码范围

汉字编码范围

扩充汉字数

说明

GB2312

第一字节0xA1-0xFE

第一字节0xB0-0xF7

6763

除汉字外,还包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符

第二字节0xA1-0xFE

第二字节0xA1-0xFE

GBK

第一字节0x81-0xFE

第一字节0x81-0xA0

6080

包括部首和构件,中日韩汉字,包含了BIG5编码中的所有汉字,加上GB2312的原内容,一共有21003个汉字

第二字节0x40-0xFE

第二字节0x40-0xFE

第一字节0xAA-0xFE

8160

第二字节0x40-0xA0

GB18030-2000

第一字节0x81-0xFE

第一字节0x81-0x82

6530

在GBK基础上增加了中日韩统一汉字扩充A的汉字,加上GB2312、GBK的内容,一共有27533个汉字

第二字节0x30-0x39

第二字节0x30-0x39

第三字节0x81-0xFE

第三字节0x81-0xFE

第四字节0x30-0x39

第四字节0x30-0x39

GB18030-2005

第一字节0x81-0xFE

第一字节0x95-0x98

42711

在GB18030-2000的基础上增加了42711中日韩统一汉字扩充B中的汉字和多种我国少数民族文字的编码(如藏、蒙古、傣、彝、朝鲜、维吾尔文等),加上前面GB2312、GBK、GB18030-2000的内容,一共70244个汉字

第二字节0x30-0x39

第二字节0x30-0x39

第三字节0x81-0xFE

第三字节0x81-0xFE

第四字节0x30-0x39

第四字节0x30-0x39

28.1.2.4. Big5编码

在台湾、香港等地区,使用较多的是Big5编码,它的主要特点是收录了繁体字。而从GBK编码开始,已经把Big5中的所有汉字收录进编码了。即对于汉字部分,GBK是Big5的超集,Big5能表示的汉字,在GBK都能找到那些字相应的编码,但他们的编码是不一样的,两个标准不兼容,如GBK中的“啊”字编码是“0xB0A1”,而Big5标准中的编码为“0xB0DA”。

28.1.3. Unicode字符集和编码

由于各个国家或地区都根据使用自己的文字系统制定标准,同一个编码在不同的标准里表示不一样的字符,各个标准互不兼容,而又没有一个标准能够囊括所有的字符,即无法用一个标准表达所有字符。国际标准化组织(ISO)为解决这一问题,它舍弃了地区性的方案,重新给全球上所有文化使用的字母和符号进行编号,对每个字符指定一个唯一的编号(ASCII中原有的字符编号不变),这些字符的号码从0x000000到0x10FFFF,该编号集被称为Universal Multiple-Octet Coded Character Set,简称UCS,也被称为Unicode。最新版的Unicode标准还包含了表情符号(聊天软件中的部分emoji表情),可访问Unicode官网了解:http://www.unicode.org

Unicode字符集只是对字符进行编号,但具体怎么对每个字符进行编码,Unicode并没指定,因此也衍生出了如下几种unicode编码方案(Unicode Transformation Format)。

28.1.4. UTF-32

对Unicode字符集编码,最自然的就是UTF-32方式了。编码时,它直接对Unicode字符集里的每个字符都用4字节来表示,转换方式很简单,直接将字符对应的编号数字转换为4字节的二进制数。如表 27‑6,由于UTF-32把每个字符都用要4字节来存储,因此UTF-32不兼容ASCII编码,也就是说ASCII编码的文件用UTF-32标准来打开会成为乱码。

表 27‑6 UTF-32编码示例

字符

GBK编码

Unicode编号

UTF-32编码

A

0x41

0x0000 0041

大端格式0x0000 0041

0xB0A1

0x0000 554A

大端格式0x0000 554A

对UTF-32数据进行解码的时候,以4个字节为单位进行解析即可,根据编码可直接找到Unicode字符集中对应编号的字符。

UTF-32的优点是编码简单,解码也很方便,读取编码的时候每次都直接读4个字节,不需要加其它的判断。它的缺点是浪费存储空间,大量常用字符的编号只需要2个字节就能表示。其次,在存储的时候需要指定字节顺序,是高位字节存储在前(大端格式),还是低位字节存储在前(小端格式)。

28.1.5. UTF-16

针对UTF-32的缺点,人们改进出了UTF-16的编码方式,它采用2字节或4字节的变长编码方式(UTF-32定长为4字节)。对Unicode字符编号在0到65535的统一用2个字节来表示,将每个字符的编号转换为2字节的二进制数,即从0x0000到0xFFFF。而由于Unicode字符集在0xD800-0xDBFF这个区间是没有表示任何字符的,所以UTF-16就利用这段空间,对Unicode中编号超出0xFFFF的字符,利用它们的编号做某种运算与该空间建立映射关系,从而利用该空间表示4字节扩展,感兴趣的读者可查阅相关资料了解具体的映射过程。

表 27‑7 UTF-16编码示例

字符

GB18030编码

Unicode编号

UTF-16编码

A

0x41

0x0000 0041

大端格式0x0041

0xB0A1

0x0000 554A

大端格式0x554A

𧗌

0x9735 F832

0x0002 75CC

大端格式0xD85D DDCC

注:𧗌 五笔:TLHH(不支持GB18030码的输入法无法找到该字,感兴趣可搜索它的Unicode编号找到)

UTF-16解码时,按两个字节去读取,如果这两个字节不在0xD800到0xDFFF范围内,那就是双字节编码的字符,以双字节进行解析,找到对应编号的字符。如果这两个字节在0xD800到 0xDFFF之间,那它就是四字节编码的字符,以四字节进行解析,找到对应编号的字符。

UTF-16编码的优点是相对UTF-32节约了存储空间,缺点是仍不兼容ASCII码,仍有大小端格式问题。

28.1.6. UTF-8

UTF-8是目前Unicode字符集中使用得最广的编码方式,目前大部分网页文件已使用UTF-8编码,如使用浏览器查看百度首页源文件,可以在前几行HTML代码中找到如下代码:

1 <meta http-equiv=Content-Type content=”text/html;charset=utf-8”>

其中“charset”等号后面的“utf-8”即表示该网页字符的编码方式UTF-8。

UTF-8也是一种变长的编码方式,它的编码有1、2、3、4字节长度的方式,每个Unicode字符根据自己的编号范围去进行对应的编码,见表 27‑8。它的编码符合以下规律:

  • 对于UTF-8单字节的编码,该字节的第1位设为0(从左边数起第1位,即最高位),剩余 的位用来写入字符的Unicode编号。即对于Unicode编号从0x0000 0000-0x0000 007F的字符,UTF-8编码只需要1个字节,因为这个范围Unicode编号的字符与ASCII码完全相同,所以UTF-8兼容了ASCII码表。

  • 对于UTF-8使用N个字节的编码(N>1),第一个字节的前N位设为1,第N+1 位设为0,后面字节的前两位都设为10,这N个字节的其余空位填充该字符的Unicode编号,高位用0补足。

表 27‑8 UTF-8编码原理(x的位置用于填充Unicode编号)

Unicode(16进制)

UTF-8(2进制)

编号范围

第一字节

第二字节

第三字节

第四字节

第五字节

00000000-0000007F

0xxxxxxx

00000080-000007FF

110xxxxx

10xxxxxx

00000800-0000FFFF

1110xxxx

10xxxxxx

10xxxxxx

00010000-0010FFFF

11110xxx

10xxxxxx

10xxxxxx

10xxxxxx

111110xx

10xxxxxx

10xxxxxx

10xxxxxx

10xxxxxx

注:实际上utf-8编码长度最大为四个字节,所以最多只能表示Unicode编码值的二进制数为21位的Unicode字符。但是已经能表示所有的Unicode字符,因为Unicode的最大码位0x10FFFF也只有21位。

UTF-8解码的时候以字节为单位去看,如果第一个字节的bit位以0开头,那就是ASCII字符,以单字节进行解析。如果第一个字节的数据位以“110”开头,就按双字节进行解析,3、4字节的解析方法类似。

UTF-8的优点是兼容了ASCII码,节约空间,且没有字节顺序的问题,它直接根据第1个字节前面数据位中连续的1个数决定后面有多少个字节。不过使用UTF-8编码汉字平均需要3个字节,比GBK编码要多一个字节。

28.1.7. BOM

由于UTF系列有多种编码方式,而且对于UTF-16和UTF-32还有大小端的区分,那么计算机软件在打开文档的时候到底应该用什么编码方式去解码呢?有的人就想到在文档最前面加标记,一种标记对应一种编码方式,这些标记就叫做BOM(Byte Order Mark),它们位于文本文件的开头,见表 27‑9。注意BOM是对Unicode的几种编码而言的,ANSI编码没有BOM。

表 27‑9 BOM标记

BOM标记

表示的编码

0xEF 0xBB 0xBF

UTF-8

0xFF 0xFE

UTF-16 小端格式

0xFE 0xFF

UTF-16 大端格式

0xFF 0xFE 0x00 0x00

UTF-32 小端格式

0x00 0x00 0xFE 0xFF

UTF-32 大端格式

但由于带BOM的设计很多规范不兼容,不能跨平台,所以这种带BOM的设计没有流行起来。Linux系统下默认不带BOM。

28.2. 什么是字模?

有了编码,我们就能在计算机中处理、存储字符了,但是如果计算机处理完字符后直接以编码的形式输出,人类将难以识别。来,在2秒内告诉我ASCII编码的“0x25”表示什么字符?不容易吧?要是觉得容易,再来告诉我GBK编码的“0xBCC6”表示什么字符。因此计算机与人交互时,一般会把字符转化成人类习惯的表现形式进行输出,如显示、打印的时候。

但是如果仅有字符编码,计算机还不知道该如何表达该字符,因为字符实际上是一个个独特的图形,计算机必须把字符编码转化成对应的字符图形人类才能正常识别,因此我们要给计算机提供字符的图形数据,这些数据就是字模,多个字模数据组成的文件也被称为字库。计算机显示字符时,根据字符编码与字模数据的映射关系找到它相应的字模数据,液晶屏根据字模数据显示该字符。

28.2.1. 字模的构成

已知字模是图形数据,而图形在计算机中是由一个个像素点组成的,所以字模实质是一个个像素点数据。为方便处理,我们把字模定义 成方块形的像素点阵,且每个像素点只有0和1这两种状态(可以理解为单色图像数据)。见 图27_2,这是两个宽、高为16x 16的像素点阵组成的两个汉字图形,其中的黑色像素点即为文字 的笔迹。计算机要表示这样的图形,只需使用16x16个二进制数据位,每个数据位记录一个像素点的状态,把黑色像素点以“1”表示,无色像素点以“0”表示即可。这样的一个汉字图形,使用16x16/8=32个字节来就可以记录下来。

图 27‑2 字模

16x16的“字”的字模数据以C语言数组的方式表示,见 代码清单27_1。在这样的字模中,以两个字节表示一行像素点,16行构成一个字模。

代码清单27‑1:“字”的字模数据
   1.  /* 字 */

   2.  unsigned char code Bmp003[]=

   3.  {

   4.  /*------------------------------------------------------------

   5.  ;  源文件 / 文字 : 字

   6.  ;  宽×高(像素): 16×16

   7.  ;  字模格式/大小 : 单色点阵液晶字模,横向取模,字节正序/32字节

   8.  ----------------------------------------------------------*/

   9.

   10. 0x02,0x00,0x01,0x00,0x3F,0xFC,0x20,0x04,0x40,0x08,0x1F,0xE0,0x00,0x40,0x00,0x80,

   11. 0xFF,0xFF,0x7F,0xFE,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x05,0x00,0x02,0x00,

   12. };

28.2.2. 字模显示原理

如果使用LCD的画点函数,按位来扫描这些字模数据,把为1的位以黑色来显示(也可以使用其它颜色),为0的数据位以白色来显示,即可把整个点阵还原出来,显示在液晶屏上。

为便于理解,我们编写了一个使用串口printf利用字模打印字符到串口上位机,见 代码清单27_2 中演示的字模显示原理。

代码清单 27‑2 使用串口利用字模打印字到上位机
1

2

3 /*"当"字符的字模16x16 */

4 unsigned char charater_matrix[] = {

5 /*"当",0*/

6 0x01,0x00,0x21,0x08,0x11,0x08,0x09,0x10,

7 0x09,0x20,0x01,0x00,0x7F,0xF8,0x00,0x08,

8 0x00,0x08,0x00,0x08,0x3F,0xF8,0x00,0x08,

9 0x00,0x08,0x00,0x08,0x7F,0xF8,0x00,0x08,

10 };

11

12 /**

13 * @brief 使用串口在上位机打印字模

14 * 演示字模显示原理

15 * @retval 无

16 */

17 void Printf_Charater(void)

18 {

19 int i,j;

20 unsigned char kk;

21

22 /*i用作行计数*/

23 for ( i=0; i<16; i++) {

24 /*j用作一字节内数据的移位计数*/

25 /*一行像素的第一个字节*/

26 for (j=0; j<8; j++) {

27 /*一个数据位一个数据位地处理*/

28 kk = charater_matrix[2*i] << j ; //左移J位

29 if ( kk & 0x80) {

30 printf("*"); //如果最高位为1,输出*号,表示笔迹

31 } else {

32 printf(" "); //如果最高位为0,输出空格,表示空白

33 }

34 }

35 /*一行像素的第二个字节*/

36 for (j=0; j<8; j++) {

37 kk = charater_matrix[2*i+1] << j ; //左移J位

38

39 if ( kk & 0x80) {

40 printf("*"); //如果最高位为1,输出*号,表示笔迹

41 } else {

42 printf(" "); //如果最高位为0,输出空格,表示空白

43 }

44 }

45 printf("\n"); //输出完一行像素,换行

46 }

47 printf("\n\n"); //一个字输出完毕

48 }

49

在main函数中运行这段代码,连接好开发板到上位机,可以看到 图27_3 中的现象。该函数中利用printf函数对字模数据中为1的数据位打印“*”号,为0的数据 位打印出“空格”,从而在串口接收区域中使用“*”号表达出了一个“当”字。

图 27‑3 使用串口打印字模

28.2.3. 如何制作字模

以上只是某几个字符的字模,为方便使用,我们需要制作所有常用字符的字模,如程序只需要英文显示,那就需要制作包含ASCII码表 27‑2中所有字符的字模,如程序只需要使用一些常用汉字,我们可以选择制作GB2312编码里所有字符的字模,而且希望字模数据与字符编码有固定的映射关系,以便在程序中使用字符编码作为索引,查找字模。在网上搜索可找到一些制作字模的软件工具,可满足这些需求。在我们提供的《液晶显示中英文》的工程目录下提供了一个取模软件“PCtoLCD”,这里以它为例讲解如何制作字模,其它字模软件也是类似的。

  1. 配置字模格式

打开取模软件,点击“选项”菜单,会弹出一个对话框,见图 27‑4。

  • 选项“点阵格式”中的阴、阳码是指字模点阵中有笔迹像素位的状态是“1”还是“0”,像我们前文介绍的那种就是阴码,反过来就是阳码。本工程中使用阴码。

  • 选项“取模方式”是指字模图形的扫描方向,修改这部分的设置后,选项框的右侧会有相应的说明及动画显示,这里我们依然按前文介绍的字模类型,把它配置成“逐行式”

  • 选项“每行显示的数据”里我们把点阵和索引都配置成16,设置这个点阵的像素大小为16x16。

字模选项的格式保持不变,设置完我们点击确定即可,字模选项的这些配置会影响到显示代码的编 写方式,即类似前文 代码清单27_2 中的程序。

图 27‑4 配置字模格式
  1. 生成GB2312字模

配置完字模选项后,点击软件中的导入文本图标,会弹出一个“生成字库”的对话框,点击右下角的生成国标汉字库按钮即可生成包含了GB2312编码里所有字符的字模文件。在《液晶显示中英文》的工程目录下的《GB2312_H1616.FON》是我用这个取模软件生成的字模原文件,若不想自己制作字模,可直接使用该文件。

图 27‑5 生成国标汉字库

28.2.4. 字模寻址公式

使用字模软件制作的字模数据一般会按照编码格式排列。如我们利用以上软件生成的字模文件《GB2312_H2424.FON》中的数据,是根据GB2312的区位码表的顺序存储的,它存储了区位码为0101-9494的字符,每个字模的大小为16x16/8=32字节。其中第一个字符“空格”的区位码为0101,它是首个字符,所以文件的前32字节存储的是它的字模数据;同理,32-64字节存储的则是0102字符“、”的字模数据。所以我们可以导出任意字符的寻址公式:

Addr = (((Code:sub:H-0xA0-1)*94) +(CodeL-0xA0-1))*16*16/8

其中CodeH和CodeL分别是GB2312编码的第一字节和 第二字节;94是指一个区中有94个位(即94个字符)。公式的实质是根据字符 的GB2312编码,求出区位码,然后区位码乘以每个字符占据的字节数,求出地址偏移。

28.2.5. 存储字模文件

上面生成的《GB2312_H1616.FON》文件的大小为256KB,比很多STM32芯片内部的所有FLASH空间都大,如果我们还是在程序中直接以C语言数组的方式存储字模数据,STM32芯片的程序空间会非常紧张,一般的做法是把字模数据存储到外部存储器,如SD卡或SPI-FLASH芯片,当需要显示某个字符时,控制器根据字符的编码算好字模的存储地址,再从存储器中读取,而FLASH芯片在生产前就固化好字模内容,然后直接把FLASH芯片贴到电路板上,作为整个系统的一部分。

28.3. 各种模式的液晶显示字符实验

本小节讲解如何利用字模使用在液晶屏上显示字符。

根据编码或字模存储位置、使用方式的不同,讲解中涉及到多个工程,见表 27‑10中的说明,在讲解特定实验的时候,请读者打开相应的工程来阅读

表 27‑10 各种模式的液晶显示字符实验

工程名称

说明

液晶显示

仅包含ASCII码字符显示功能,字库直接以C语言常量数组的方式存储在STM32芯片的内部FLASH空间

液晶显示中英文(字库在外部FLASH)

包含ASCII码字符及GB2312码字符的显示功能,ASCII码字符存储在STM32内部FLASH,GB2312码字符存储在外部SPI-FLASH芯片

液晶显示中英文(字库在SD卡)

包含ASCII码字符及GB2312码字符的显示功能,ASCII码字符存储在STM32内部FLASH,GB2312码字符直接以文件的格式存储在SD卡中

液晶显示中英文(任意大小)

在基础字库的支持下,使用字库缩放函数,使得只用一种字库,就能显示任意大小的字符。包含ASCII码字符及GB2312码字符的显示功能,ASCII码字符存储在STM32内部FLASH,GB2312码字符存储在外部SPI-FLASH芯片

这些实验是在《液晶显示》工程的基础上修改的,主要添加字符显示相关的内容,本小节只讲解这部分新增的函数。关于液晶驱动的原理在此不再赘述,不理解这部分的可阅读前面的章节。

28.3.1. 硬件设计

针对不同模式的液晶显示字符工程,需要有不同的硬件支持。字库存储在STM32芯片内部FLASH的工程跟普通液晶显示的硬件需求无异。需要外部字库的工程,要有额外的SPI-FLASH、SD支持,使用外部FLASH时,我们的实验板上直接用板子上的SPI-FLASH芯片存储字库,出厂前我们已给FLASH芯片烧录了前面的《GB2312_H1616.FON》字库文件,如果您想把我们的程序移植到您自己设计产品上,请确保该系统包含有存储了字库的FLASH芯片,才能正常显示汉字使用SD卡时,需要给板子接入存储有《GB2312_H1616.FON》字库文件的MicroSD卡,SD卡的文件系统格式需要是FAT格式,且字库文件所在的目录需要跟程序里使用的文件目录一致。

关于SPI-FLASH和SD卡的原理图及驱动说明可参考其他的章节。给外部SPI-FLASH和SD卡存储字库的操作我们将在另一个文档中说明,本章的教程默认您已配置好SPI-FLASH和SD卡相关的字库环境。

28.3.2. 显示ASCII编码的字符

我们先来看如何显示ASCII码表中的字符,请打开“液晶显示”(即)的工程文件。本工程中我们把字库数据相关的函数代码写在“fonts.c”及“fonts.h”文件中,字符显示的函数仍存储在LCD驱动文件“bsp_ili9341_lcd.c”及“bsp_ ili9341_lcd.h”中。

28.3.2.1. 编程要点

  1. 获取字模数据;

  2. 根据字模格式,编写液晶显示函数;

  3. 编写测试程序,控制液晶英文。

28.3.2.2. 代码分析

28.3.2.2.1. ASCII字模数据

要显示字符首先要有字库数据,在工程的“fonts.c”文件中我们定义了一系列大小为24x32、16x24、8x16的ASCII码表的 字模数据,其形式见 代码清单27_3

代码清单 27‑3 部分英文字库16x24大小(fonts.c文件)
/*
* 常用ASCII表,偏移量32,大小:24(高度)* 16 (宽度)
*/
//@conslons字体,阴码点阵格式,逐行顺向取摸
const uint8_t ASCII16x24_Table [ ] = {
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x03,0x80,0x01,0x80,0x01,0x80,
   0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,
   0x01,0x80,0x01,0x80,0x01,0x80,0x00,0x00,
   0x00,0x00,0x03,0xc0,0x03,0xc0,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,

   /*以下部分省略.....,包含从 空格 至 波浪号 的ASCII码图形字模数据*/

由于ASCII中的字符并不多,所以本工程中直接以C语言数组的方式存储这些字模数据,C语言的const数组是作为常量直接存储到STM32芯片的内部FLASH中的,所以如果您不需要显示中文,可以不用外部的SPI-FLASH芯片,可省去烧录字库的麻烦。以上代码定义的ASCII16x24_Table数组是16x24大小的ASCII字库。

28.3.2.2.2. 管理英文字模的结构体

为了方便使用各种不同的字体,工程中定义了一个“sFont”结构体类型,并利用它定义存 储了不同字体信息的结构体变量,见 代码清单27_4

代码清单 27‑4 管理英文字模的结构体(fonts.c文件)
/*字体格式*/
typedef struct _tFont {
   const uint8_t *table; /*指向字模数据的指针*/
   uint16_t Width;   /*字模的像素宽度*/
   uint16_t Height;    /*字模的像素高度*/

} sFONT;

sFONT Font8x16 = {
   ASCII8x16_Table,
   8, /* 字模宽 */
   16, /* 字模高 */
};

sFONT Font16x24 = {
   ASCII16x24_Table,
   16, /* 字模宽 */
   24, /* 字模高 */
};

sFONT Font24x32 = {
   ASCII24x32_Table,
   24, /* 字模宽 */
   32, /* 字模高 */
};

这个结构体类型定义了三个变量,第一个是指向字模数据的指针,即前面提到的C语言数组,每二、三个变量存储了该字模单个字符的像素宽度和高度。利用这个类型定义了Font8x16、Font16x24之类的变量,方便显示时寻址。

28.3.2.2.3. 切换字体

在程序中若要方便切换字体,还需要定义一个存储了当前选择字 体的变量LCD_Currentfonts,代码清单27_5

代码清单 27‑5 切换字体(bsp_lcd.c文件)
/*用于存储当前选择的字体格式的全局变量*/
static sFONT *LCD_Currentfonts;
/**
* @brief  设置字体格式(英文)
* @param  fonts: 选择要设置的字体格式
* @retval None
*/
void LCD_SetFont(sFONT *fonts)
{
   LCD_Currentfonts = fonts;
}

使用LCD_SetFont可以切换LCD_Currentfonts指向的字体类型,函数的可输入参数即前面的Font8x16、Font16x24之类的变量。

28.3.2.2.4. ASCII字符显示函数

利用字模数据以及上述结构体变量,我们可以编写一个能显示各 种字体的通用函数,见 代码清单27_6

代码清单 27‑6 ASCII字符显示函数
/**
* @brief  在 ILI9341 显示器上显示一个英文字符
* @param  usX :在特定扫描方向下字符的起始X坐标
* @param  usY :在特定扫描方向下该点的起始Y坐标
* @param  cChar :要显示的英文字符
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_DispChar_EN ( uint16_t usX, uint16_t usY, const char cChar )
{
   uint8_t  byteCount, bitCount,fontLength;
   uint16_t ucRelativePositon;
   uint8_t *Pfont;

   //对ascii码表偏移(字模表不包含ASCII表的前32个非图形符号)
   ucRelativePositon = cChar - ' ';

   //每个字模的字节数
   fontLength = (LCD_Currentfonts->Width*LCD_Currentfonts->Height)/8;

   //字模首地址
   /*ascii码表偏移值乘以每个字模的字节数,求出字模的偏移位置*/
   Pfont = (uint8_t *)&LCD_Currentfonts->table[ucRelativePositon * fontLength];

   //设置显示窗口
ILI9341_OpenWindow ( usX, usY, LCD_Currentfonts->Width, LCD_Currentfonts->Height);

   ILI9341_Write_Cmd ( CMD_SetPixel );

   //按字节读取字模数据
   //由于前面直接设置了显示窗口,显示数据会自动换行
   for ( byteCount = 0; byteCount < fontLength; byteCount++ ) {
      //一位一位处理要显示的颜色
      for ( bitCount = 0; bitCount < 8; bitCount++ ) {
            if ( Pfont[byteCount] & (0x80>>bitCount) )
               ILI9341_Write_Data ( CurrentTextColor );
            else
               ILI9341_Write_Data ( CurrentBackColor );
      }
   }
}

这个函数与前文中的串口打印字模到上位机的那个函数原理是一样的,只是这个函数要使用液晶显示,且字模数据并不是一个个独立的数组,而是所有字符的字模都放到同一个数组里,所以显示时,要根据字符编码计算字模数据的偏移,并把串口打印的处理改成像素点显示。该函数的说明如下:

  1. 输入参数

这个字符显示函数有usX、usY及cChar参数。其中usX和usY分别表示字符显示位置的(X,Y)坐标;而输入参数cChar是要显示的英文字符,如字符’A’,字符’空格’等。

  1. 根据字符计算字模的数组偏移

在显示前,首先要提取出字符相应的字模数据。由于ASCII码中的编码0~31(NULL~US符号)是不存在图形表示的,为节省空间,字模表中只包含图形符号相关的数据,例如,对于Font8x16的字模表,每个字符的字模数据长度=8*16/8=16字节,那么0~15字节表示的是’空格’字模,16~31字节表示的是’!’号字模,32~47字节表示的是’ “ ’号字模。

因此,显示函数中通过cChar获知要显示的字符后,使用ucRelativePositon变量存储cChar减去字符’空格’的ASCII码值,即求出图形ASCII码的偏移值;然后使用fontLength变量存储根据当前选择的字体宽度与高度计算出的单个字模数据长度;最后根据它在字模数组表中的偏移值(ucRelativePositon与fontLength的乘积)求出该字模的地址指针,存储在指针变量Pfont中,后面就可以直接使用该指针获取字模数据了,从Pfont至(Pfont+fontLength-1)范围内的都是输入字符cChar的字模数据。

  1. 设置显示窗口并发送显示命令

计算出字模数据的指针后,可以准备开始显示,函数中使用ILI9341_OpenWindow函数根据输入的显示坐标及字模的宽高设置一个 字符的显示窗口,并使用ILI9341_Write_Cmd函数发送设置像素点的命令(CMD_SetPixel),有了这两个操作后,下面使 用的ILI9341_Write_Data函数发送的像素点数据将会一行一行地显示到窗口中(沿X方向,到达X结尾后沿Y方向显 示下一行),见 图27_6

图 27‑6 设置显示窗口后的像素数据扫描过程

图 27‑6 设置显示窗口后的像素数据扫描过程

  1. 行循环与列循环

由于根据字模大小设置了显示窗口,使用ILI9341_Write_Data函数发送像素数据时到达单行的结尾它会自动换行,所以在发送数据时不需要再考虑换行。代码中直接使用两层循环处理字模数据,其中外层for循环用于遍历字模的字节数据,一个字节一个字节地读取,而字节数据的处理则交给内层for循环,当外层for循环遍历完fontLength个字节表示处理完一个字符的字模,即显示完一个字符;内层for循环用于遍历字模单个字节数据的每个数据位,数据位为1时就发送一个点的字体颜色(CurrentTextColor),数据位为0时就发送背景颜色(CurrentBackColor)。

经过ILI9341_DispChar_EN函数的处理,可显示一个英文字符,要显示字符串时,重复调用本函数即可。

28.3.2.2.5. 显示字符串

对ILI9341_DispChar_EN函数进行封装,我们可以得到ASCII字符的字符串显示函 数,见 代码清单27_7

代码清单 27‑7字符串显示函数
/**
* @brief  在 ILI9341 显示器上显示英文字符串
* @param  usX :在特定扫描方向下字符的起始X坐标
* @param  usY :在特定扫描方向下字符的起始Y坐标
* @param  pStr :要显示的英文字符串的首地址
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_DispString_EN (  uint16_t usX ,uint16_t usY,  char * pStr )
{
   while ( * pStr != '\0' ) {
if ( ( usX - ILI9341_DispWindow_X_Star + LCD_Currentfonts->Width )> LCD_X_LENGTH) {
            usX = ILI9341_DispWindow_X_Star;
            usY += LCD_Currentfonts->Height;
         }

if ( ( usY -ILI9341_DispWindow_Y_Star + LCD_Currentfonts->Height )>LCD_Y_LENGTH ) {
            usX = ILI9341_DispWindow_X_Star;
            usY = ILI9341_DispWindow_Y_Star;
         }

         ILI9341_DispChar_EN ( usX, usY, * pStr);

         pStr ++;
         usX += LCD_Currentfonts->Width;
   }
}

本函数中的输入参数pStr为指向要显示的字符串的指针,在函数的内部利用while循环把字符串中的字符一个个地利用ILI9341_DispChar_EN函数显示到液晶屏上,当遇到字符串结束符’0’时完成显示,退出while循环,结束函数。在while循环的开头,有两个if判断操作,它们分别用于判断显示字符时的X及Y坐标是否超出屏幕的边沿,若超出了则换到下一行,使用这个函数,可以很方便地利用“ILI9341_DispString_EN (10,20,”test”)”这样的格式在液晶屏上直接显示一串字符。

28.3.2.2.6. 使用宏计算Y坐标

使用ILI9341_DispString_EN函数显示时,需要注意字Y方向字符覆盖的问题,例如在(10,20)坐标处显示了一行字体高度为16 像素的字符串,如果再显示另一串字符时指定的坐标为(10,25),那么由于高度预留不足,会出现字符覆盖的现象,因此 调用时需要小心计算Y方向的坐标。为了简化操作,代码中定义了一个宏LINE及函数ILI9341_DispStringLine_EN来显 示字符串,见 代码清单27_8

代码清单 27‑8 使用宏计算Y坐标
#define LINE(x) ((x) * (((sFONT *)LCD_GetFont())->Height))
/**
* @brief  在 ILI9341 显示器上显示英文字符串
* @param  line :在特定扫描方向下字符串的起始Y坐标
*   本参数可使用宏LINE(0)、LINE(1)等方式指定文字坐标,
*   宏LINE(x)会根据当前选择的字体来计算Y坐标值。
*   显示中文且使用LINE宏时,需要把英文字体设置成Font8x16
* @param  pStr :要显示的英文字符串的首地址
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_DispStringLine_EN (  uint16_t line,  char * pStr )
{
   uint16_t usX = 0;

   while ( * pStr != '\0' ) {
if(( usX - ILI9341_DispWindow_X_Star + LCD_Currentfonts->Width ) > LCD_X_LENGTH ) {
            usX = ILI9341_DispWindow_X_Star;
            line += LCD_Currentfonts->Height;
      }

if((line - ILI9341_DispWindow_Y_Star + LCD_Currentfonts->Height )> LCD_Y_LENGTH ) {
            usX = ILI9341_DispWindow_X_Star;
            line = ILI9341_DispWindow_Y_Star;
      }

      ILI9341_DispChar_EN ( usX, line, * pStr);

      pStr ++;

      usX += LCD_Currentfonts->Width;
   }
}

本函数主体与ILI9341_DispString_EN差异不大,主要是删减了X方向的坐标,另外使用输入参数line来指定Y方向的坐标,调用本函数时,一般配合上面的LINE宏来使用,该宏会根据当前选择的字体高度来计算字符Y方向的间隔,如当前字体为8x16时,字体高度为16像素,所以LINE(1)会返回数值16,LINE(3)会返回数值48。利用它配合本函数可以使用“ ILI9341_DispStringLine_EN (LINE(1),”test”); ”的形式来使字符串显示在第1行,即Y方向为16像素处,当然,使用前面的函数“ ILI9341_DispString_EN (10,LINE(1),”test”); ”同样也是可以的,并且可指定X坐标。

28.3.2.2.7. 清除屏幕字符

在实际应用中,还经常需要把当前屏幕显示的内容清除掉,这可以使用 代码清单27_9 中的函数。

代码清单 27‑9 清除屏幕字符
/**
* @brief  对ILI9341显示器的某一窗口以某种颜色进行清屏
* @param  usX :在特定扫描方向下窗口的起点X坐标
* @param  usY :在特定扫描方向下窗口的起点Y坐标
* @param  usWidth :窗口的宽度
* @param  usHeight :窗口的高度
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_Clear ( uint16_t usX, uint16_t usY,
uint16_t usWidth, uint16_t usHeight )
{
   ILI9341_OpenWindow ( usX, usY, usWidth, usHeight );

   ILI9341_FillColor ( usWidth * usHeight, CurrentBackColor );

}

/**
   * @brief  清除某行文字
   * @param  Line: 指定要删除的行
   *   本参数可使用宏LINE(0)、LINE(1)等方式指定要删除的行,
   *   宏LINE(x)会根据当前选择的字体来计算Y坐标值,并删除当前字体高度的第x行。
   * @retval None
   */
void LCD_ClearLine(uint16_t Line)
{/* 清屏,显示背景颜色 */
   ILI9341_Clear(0,Line,LCD_X_LENGTH,((sFONT *)LCD_GetFont())->Height);
}

代码中的ILI9341_Clear函数可以直接清除一个指定的矩形,它会把该矩形显示成当前设置的背景颜色CurrentBackColor,实现清除图像的效果,而LCD_ClearLine函数对它进行了封装,使用LINE宏配合,可以比较方便地清除单行字符串,如调用LCD_ClearLine(LINE(1))可以清除第1行的字符串。

28.3.2.2.8. 显示ASCII码示例

下面我们再来看main文件是如何利用这些函数显示ASCII码字符的, 见 代码清单27_10

代码清单 27‑10 显示ASCII码的main函数
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main ( void )
{
   ILI9341_Init ();         //LCD 初始化

   USART_Config();

   printf("\r\n ********** 液晶屏英文显示程序*********** \r\n");
   printf("\r\n 本程序不支持中文,显示中文的程序请学习下一章 \r\n");

//其中0、3、5、6 模式适合从左至右显示文字,
//不推荐使用其它模式显示文字 其它模式显示文字会有镜像效果
//其中 6 模式为大部分液晶例程的默认显示方向
   ILI9341_GramScan ( 6 );
   while ( 1 ) {
      LCD_Test();
   }
}

main函数中主要是对液晶屏初始化,初始化完成后就能够显示ASCII码字符了,无需利用SPI-FLASH及SD卡。在while 循环中调用的LCD_Test函数包含了显示字符的函数调用示例,见 代码清单27_11

代码清单 27‑11 LCD_Test函数中的ASCII码显示示例
/*用于测试各种液晶的函数*/
void LCD_Test(void)
{
   /*演示显示变量*/
   static uint8_t testCNT = 0;
   char dispBuff[100];

   testCNT++;

   LCD_SetFont(&Font8x16);
   LCD_SetColors(RED,BLACK);

   ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
   /********显示字符串示例*******/
   ILI9341_DispStringLine_EN(LINE(0),"BH 3.2 inch LCD para:");
   ILI9341_DispStringLine_EN(LINE(1),"Image resolution:240x320 px");
   ILI9341_DispStringLine_EN(LINE(2),"ILI9341 LCD driver");
   ILI9341_DispStringLine_EN(LINE(3),"XPT2046 Touch Pad driver");

   /********显示变量示例*******/
   LCD_SetFont(&Font16x24);
   LCD_SetTextColor(GREEN);

   /*使用cHAL库把变量转化成字符串*/
   sprintf(dispBuff,"Count : %d ",testCNT);
   LCD_ClearLine(LINE(4)); /* 清除单行文字 */

   /*然后显示该字符串即可,其它变量也是这样处理*/
   ILI9341_DispStringLine_EN(LINE(4),dispBuff);
   /*以下省略其它液晶演示示例*/
}

这段代码包含了使用字符串显示函数显示常量字符和变量的示例。显示常量字符串时,直接使用双引号括起要显示的字符串即可,根据C语言的语法,这些字符串会被转化成常量数组,数组内存储对应字符的ASCII码,然后存储到STM32的FLASH空间,函数调用时通过指针来找到对应的ASCII码,液晶显示函数使用前面分析过的流程,转换成液晶显示输出。

在很多场合下,我们可能需要使用液晶屏显示代码中变量的内容,这时很多用户就不知道该如何解决了,上面的LCD_Test函数结尾处演示了如何处理。它主要是使用一个C语言HAL库里的函数sprintf,把变量转化成ASCII码字符串,转化后的字符串存储到一个数组中,然后我们再利用液晶显示字符串的函数显示该数组的内容即可。spritnf函数的用法与printf函数类似,使用它时需要包含头文件string.h。

28.3.3. 显示GB2312编码的字符

显示ASCII编码比较简单,由于字库文件小,甚至都不需要使用外部的存储器,而显示汉字时,由于我们的字库是存储到外部存储器上的,这涉及到额外的获取字模数据的操作。

我们分别制作了两个工程来演示如何显示汉字,以下部分的内容请打开“液晶显示中英文(字库在外部FLASH)”和“液晶显示中英文(字库在SD卡)”工程阅读理解。这两个工程使用的字库文件内容相同,只是字库存储的位置不一样,工程中我们把获取字库数据相关的函数代码写在“fonts.c”及“fonts.h” 文件中,字符显示的函数仍存储在LCD驱动文件“bsp_ili9341_lcd.c”及“bsp_ili9341_lcd.h”中。

28.3.3.1. 编程要点

  1. 获取字模数据;

  2. 根据字模格式,编写液晶显示函数;

  3. 编写测试程序,控制液晶汉字。

28.3.3.2. 代码分析

28.3.3.2.1. 显示汉字字符

由于我们的GB2312字库文件与ASCII字库文件是使用同一种方式生成的,但字符的编码不同导致字 模偏移地址计算有区别,且字模数据存储的位置不同,所以为了显示汉字,需要另外编写 一个字符显示函数,它利用前文生成的《GB2312_H1616.FON》字库显示GB2312编码里 的字符,见 代码清单27_12

代码清单 27‑12 显示GB2312编码字符的函数(bsp_ili9341_ldc.c文件)
#define      WIDTH_CH_CHAR                    16      //中文字符宽度
#define      HEIGHT_CH_CHAR                   16      //中文字符高度

/**
* @brief  在 ILI9341 显示器上显示一个中文字符
* @param  usX :在特定扫描方向下字符的起始X坐标
* @param  usY :在特定扫描方向下字符的起始Y坐标
* @param  usChar :要显示的中文字符(国标码)
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_DispChar_CH ( uint16_t usX, uint16_t usY, uint16_t usChar )
{
   uint8_t rowCount, bitCount;
   uint8_t ucBuffer [ WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8 ];
   uint16_t usTemp;

   //设置显示窗口
   ILI9341_OpenWindow ( usX, usY, WIDTH_CH_CHAR, HEIGHT_CH_CHAR );

   ILI9341_Write_Cmd ( CMD_SetPixel );

   //取字模数据
   GetGBKCode ( ucBuffer, usChar );

   for ( rowCount = 0; rowCount < HEIGHT_CH_CHAR; rowCount++ ) {
      /* 取出两个字节的数据,在lcd上即是一个汉字的一行 */
      usTemp = ucBuffer [ rowCount * 2 ];
      usTemp = ( usTemp << 8 );
      usTemp |= ucBuffer [ rowCount * 2 + 1 ];

      for ( bitCount = 0; bitCount < WIDTH_CH_CHAR; bitCount ++ ) {
            if ( usTemp & ( 0x8000 >> bitCount ) )  //高位在前
               ILI9341_Write_Data ( CurrentTextColor );
            else
               ILI9341_Write_Data ( CurrentBackColor );
      }
   }
}

这个GB2312码的显示函数与ASCII码的显示函数是很类似的,它的输入参数有usX,usY及usChar。其中usX和usY用于设定字符的显示位置,usChar是字符的编码,这是一个16位的变量,因为GB2312编码中每个字符是2个字节的。函数的执行流程介绍如下:

  1. 使用ILI9341_OpenWindow和ILI9341_Write_Cmd来设置显示窗口并发送显示像素命令。

  2. 使用量macGetGBKCode函数获取字模数据,向该函数输入usChar参数(字符的编码),它会从外部SPI-FLASH芯 片或SD卡中读取该字符的字模数据,读取得的数据被存储到数组ucBuffer中。关于GetGBKCode函数我们在后面详细讲解。

  3. 遍历像素点。这个代码在遍历时还使用了usTemp变量用来缓存一行的字模数据(本字模一行有2个字节),然后一位一 位地判断这些数据,数据位为1的时候,像素点就显示字体颜色,否则显示背景颜色。原理是跟ASCII字符显示一样的。

28.3.3.2.2. 显示中英文字符串

类似地,我们希望希望汉字也能直接以字符串的形式来调用函数显示,而且最好是中英文字符可以混在一个字符串里。为此,我们编写了LCD_DisplayStringLine_EN_CH 函数,代码清单27_13

代码清单 27‑13 显示中英文的字符串
/**
* @brief  在 ILI9341 显示器上显示中英文字符串
* @param  usX :在特定扫描方向下字符的起始X坐标
* @param  usY :在特定扫描方向下字符的起始Y坐标
* @param  pStr :要显示的字符串的首地址
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_DispString_EN_CH (uint16_t usX , uint16_t usY, char * pStr )
{
   uint16_t usCh;

   while ( * pStr != '\0' ) {
      if ( * pStr <= 126 ) {            //英文字符
if(( usX - ILI9341_DispWindow_X_Star + LCD_Currentfonts->Width ) > LCD_X_LENGTH ) {
               usX = ILI9341_DispWindow_X_Star;
               usY += LCD_Currentfonts->Height;
            }

if(( usY - ILI9341_DispWindow_Y_Star + LCD_Currentfonts->Height )> LCD_Y_LENGTH ) {
               usX = ILI9341_DispWindow_X_Star;
               usY = ILI9341_DispWindow_Y_Star;
            }

            ILI9341_DispChar_EN ( usX, usY, * pStr );

            usX +=  LCD_Currentfonts->Width;
            pStr ++;
      }

      else {                            //汉字字符
if ( ( usX - ILI9341_DispWindow_X_Star + WIDTH_CH_CHAR ) > LCD_X_LENGTH ) {
               usX = ILI9341_DispWindow_X_Star;
               usY += HEIGHT_CH_CHAR;
            }

if ( ( usY - ILI9341_DispWindow_Y_Star + HEIGHT_CH_CHAR ) > LCD_Y_LENGTH ) {
               usX = ILI9341_DispWindow_X_Star;
               usY = ILI9341_DispWindow_Y_Star;
            }

            usCh = * ( uint16_t * ) pStr;

            usCh = ( usCh << 8 ) + ( usCh >> 8 );

            ILI9341_DispChar_CH ( usX, usY, usCh );

            usX += WIDTH_CH_CHAR;
            pStr += 2;           //一个汉字两个字节
      }
   }
}

这个函数根据字符串中的编码值,判断它是ASCII码还是国标码中的字符,然后作不同处理。英文部分与前方中的英文字符串显示函数是一样的,中文部分也很类似,需要注意的是中文字符每个占2个字节,而且由于STM32芯片的数据是小端格式存储的,国标码是大端格式存储的,所以函数中对输入参数pStr指针获取的编码usCh交换了字节顺序,再输入到单个字符的显示函数LCD_DispChar_CH中。

28.3.3.2.3. 获取SPI-FLASH中的字模数据

前面提到的GetGBKCode函数用于获取汉字字模数据,它根据字库文件的存储位置,有SPI-FLASH和SD卡两个版本,我们先来分析比 较简单的SPI-FLASH版本,代码清单27_14。该函数定义在“液晶显示中英文(字库在外部FLASH)”工程的“fonts.c”和“fonts.h”文件中。

代码清单 27‑14 从SPI-FLASH获取字模数据(“液晶显示中英文(字库在外部FLASH)“工程)
/*************fonts.h文件中的定义 **********************************/

/*使用FLASH字模*/
/*中文字库存储在FLASH的起始地址*/
/*FLASH*/
#define GBKCODE_START_ADDRESS   387*4096


/*获取字库的函数*/
//定义获取中文字符字模数组的函数名,ucBuffer为存放字模数组名,usChar为中文字符(国标码)

#define GetGBKCode( ucBuffer, usChar )  GetGBKCode_from_EXFlash( ucBuffer, usChar )
int GetGBKCode_from_EXFlash( uint8_t * pBuffer, uint16_t c);

/*********************************************************************/
/************fonts.c文件中的字义**************************************/

/*使用FLASH字模*/
//字模GB2312_H1616配套的函数

/**
   * @brief  获取FLASH中文显示字库数据
   * @param  pBuffer:存储字库矩阵的缓冲区
   * @param  c : 要获取的文字
   * @retval None.
   */
int GetGBKCode_from_EXFlash( uint8_t * pBuffer, uint16_t c)
{
   unsigned char High8bit,Low8bit;
   unsigned int pos;

   static uint8_t everRead=0;

   /*第一次使用,初始化FLASH*/
   if (everRead == 0) {
         SPI_FLASH_Init();
         everRead = 1;
   }

   High8bit= c >> 8;     /* 取高8位数据 */
   Low8bit= c & 0x00FF;  /* 取低8位数据 */

   /*GB2312 公式*/
   pos = ((High8bit-0xa1)*94+Low8bit-0xa1)*WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8;
   //读取字库数据
SPI_FLASH_BufferRead(pBuffer,GBKCODE_START_ADDRESS+pos,WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8);
   return 0;
}

这个GetGBKCode实质上是一个宏,当使用SPI-FLASH作为字库数据源时,它等效于GetGBKCode_from_EXFlash函数,它的执行过程如下:

  1. 初始化SPI外设,以确保后面使用SPI读取FLASH内容时SPI已正常工作,并且初始化后 做一个标记,以后再读取字模数据的时候就不需要再次初始化SPI了;

  2. 取出要显示字符的GB2312编码的高位字节和低位字节,以便后面用于计算字符的字模地址偏移;

  3. 根据字符的编码及字模的大小导出的寻址公式,计算当前要显示字模数据在字库中的地址偏移;

  4. 利用SPI_FLASH_BufferRead函数,从SPI-FLASH中读取该字模的数据,输入参数 中的GBKCODE_START_ADDRESS是在代码头部定义的一个宏,它是字库文件存储在SPI-FLASH芯片的 基地址,该基地址加上字模在字库中的地址偏移,即可求出字模在SPI-FLASH中存储的实际位置。这个基地址具体数值是在我们烧录FLASH字库时决定的,程序中定义的是实验板出厂时默认烧录的位置。

  5. 获取到的字模数据存储到pBuffer指针指向的存储空间,显示汉字的函数直接利用它来显示字符。

28.3.3.2.4. 获取SD卡中的字模数据

类似地,从SD卡中获取字模数据时,使用GetGBKCode_from_sd函数,见 代码清单27_15。该函数定义在“液晶显示中英文(字库在SD卡)”工程的“fonts.c”和“fonts.h”文件中。

代码清单27‑15从SD卡中获取字模数据(“液晶显示中英文(字库在SD卡)”工程)
/*************fonts.h文件中的定义 **********************************/

/*使用SD字模*/

/*SD卡字模路径*/
#define GBKCODE_FILE_NAME     "0:/srcdata/GB2312_H1616.FON"

/*获取字库的函数*/
//定义中文字符字模数组的函数名,ucBuffer为存放字模数组名,usChar为中文字符(国标码)

#define GetGBKCode( ucBuffer, usChar )  GetGBKCode_from_sd( ucBuffer, usChar )
int GetGBKCode_from_sd ( uint8_t * pBuffer, uint16_t c);

/*********************************************************************/

/************fonts.c文件中的字义**************************************/

/*使用SD字模*/

static FIL fnew;                          /* 文件句柄 */
static FATFS fs;                          /* 文件系统句柄 */
static FRESULT res_sd;
static UINT br;                     /* 文件 R/W 计数 */

/**
   * @brief  获取SD卡中文显示字库数据
   * @param  pBuffer:存储字库矩阵的缓冲区
   * @param  c : 要获取的文字
   * @retval None.
   */
int GetGBKCode_from_sd ( uint8_t * pBuffer, uint16_t c)
{
   unsigned char High8bit,Low8bit;
   unsigned int pos;

   static uint8_t everRead = 0;

   High8bit= c >> 8;     /* 取高8位数据 */
   Low8bit= c & 0x00FF;  /* 取低8位数据 */

   pos = ((High8bit-0xa1)*94+Low8bit-0xa1)*WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8;

   /*第一次使用,挂载文件系统,初始化sd*/
   if (everRead == 0) {
         res_sd = f_mount(&fs,"0:",1);
         everRead = 1;

   }

res_sd = f_open(&fnew , GBKCODE_FILE_NAME, FA_OPEN_EXISTING | FA_READ);

   if ( res_sd == FR_OK ) {
         f_lseek (&fnew, pos);   //指针偏移

         //16*16大小的汉字 其字模 占用16*16/8个字节
   res_sd = f_read( &fnew, pBuffer, WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8, &br );

         f_close(&fnew);

         return 0;
   } else
         return -1;
}

当字库的数据源在SD卡时,GetGBKCode宏指向的是这个GetGBKCode_from_sd函数。由于字库是使用SD卡的文件系统存储的,从SD卡中获取字模数据实质上是直接读取字库文件,利用f_lseek函数偏移文件的读取指针,使它能够读取特定字符的字模数据。

由于使用文件系统的方式读取数据比较慢,而SD卡大多数都会使用文件系统,所以我们一般使用SPI-FLASH直接存储字库(不带文件系统地使用),市场上有一些厂商直接生产专用的字库芯片,可以直接使用,省去自己制作字库的麻烦。

28.3.3.2.5. 显示GB2312字符示例

下面我们再来看main文件是如何利用这些函数显示GB2312的字符,由于我们用GetGBKCode宏屏蔽了差异,所以在上层使用 字符串函数时,不需要针对不同的字库来源写不同的代码,见 代码清单27_16

代码清单 27‑16 main函数
int main(void)
{
   ILI9341_Init ();         //LCD 初始化

   /* USART config */
   USART_Config();

   printf("\r\n ****** 液晶屏中文显示程序(字模文件在SD卡)**** \r\n");
   printf("\r\n 实验前请阅读工程中的readme.txt文件说明,存储字模数据到SPI-FLASH或SD卡\r\n");

//其中0、3、5、6 模式适合从左至右显示文字,
//不推荐使用其它模式显示文字 其它模式显示文字会有镜像效果
//其中 6 模式为大部分液晶例程的默认显示方向
   ILI9341_GramScan ( 6 );

   while ( 1 ) {
         LCD_Test();
   }

}

main文件中的初始化流程与普通的液晶初始化没有区别,这里也不需要初始化SPI或SDIO,因为我们在获 取字库的函数中包含了相应的初始化流程。在while循环里调用的LCD_Test包含了显示GB2312字符串 的示例,见 代码清单27_17

代码清单 27‑17 显示GB2312字符示例
/*用于测试各种液晶的函数*/
void LCD_Test(void)
{
   /*演示显示变量*/
   static uint8_t testCNT = 0;
   char dispBuff[100];

   testCNT++;

   LCD_SetFont(&Font8x16);
   LCD_SetColors(RED,BLACK);

   ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
   /********显示字符串示例*******/
   ILI9341_DispStringLine_EN_CH(LINE(0),"野火3.2寸LCD参数:");
   ILI9341_DispStringLine_EN_CH(LINE(1),"分辨率:240x320 px");
   ILI9341_DispStringLine_EN_CH(LINE(2),"ILI9341液晶驱动");
   ILI9341_DispStringLine_EN_CH(LINE(3),"XPT2046触摸屏驱动");

   /********显示变量示例*******/
   LCD_SetTextColor(GREEN);

   /*使用cHAL库把变量转化成字符串*/
   sprintf(dispBuff,"显示变量: %d ",testCNT);
   LCD_ClearLine(LINE(5)); /* 清除单行文字 */

   /*然后显示该字符串即可,其它变量也是这样处理*/
   ILI9341_DispStringLine_EN_CH(LINE(5),dispBuff);

   /******其它液晶演示示例省略******/
}

在调用字符串显示函数的时候,我们也是直接使用双引号括起要显示的中文字符即可,为什么这样就能正常显示呢?我们的字符串显示函 数需要的输入参数是字符的GB2312编码,编译器会自动转化这些中文字符成相应的GB2312编码吗?为什么编译器不把它转化成UTF-8编码?这跟我 们的开发环境配置有关,在MDK软件中,可在“Edit->Configuration->Editor->Encoding”选项设定编码,见 图27_7

图 27‑7 MDK中的字符编码选项

编译环境会把文件中的字符串转换成这里配置的编码,然后存储到STM32的程序空间中,所以这里的设定要与您的字库编码格式一样。如果您的实验板显示的时候出现乱码,请确保以下所有环节都正常:

  • SPI-FLASH或SD卡中是否有字库文件?

  • 文件存储的位置或路径是否与程序的配置一致?

  • 开发环境中的字符编码选项是否与字库的编码一致?

28.3.4. 显示任意大小的字符

前文中无论是ASCII字符还是GB2312的字符,都只能显示字库中设定的字体大小,例如,我们想显示一些像素大小为48x48的字符,那我们又得制作相应的字库,非常麻烦。为此我们编写了一些函数,简便地实现显示任意大小字符的目的。本小节的内容请打开“液晶显示中英文(任意大小)”工程来配合阅读。

28.3.4.1. 编程要点

  1. 编写缩放字模数据的函数;

  2. 编写利用缩放字模的结果进行字符显示的函数;

  3. 编写测试程序,控制显示不同大小的字符。

28.3.4.2. 代码分析

28.3.4.2.1. 缩放字模数据

显示任意大小字符的功能,其核心是缩放字模,通过LCD_zoomChar函数对原始字模数据进行缩放,见代码清单27‑1。

代码清单 27‑18 缩放字模数据
/***********************缩放字体****************************/
#define ZOOMMAXBUFF 16384
uint8_t zoomBuff[ZOOMMAXBUFF] = {0};  //用于缩放的缓存,最大支持到128*128
uint8_t zoomTempBuff[1024] = {0};

/**
* @brief  缩放字模,缩放后的字模由1个像素点由8个数据位来表示
                     0x01表示笔迹,0x00表示空白区
* @param  in_width :原始字符宽度
* @param  in_heig :原始字符高度
* @param  out_width :缩放后的字符宽度
* @param  out_heig:缩放后的字符高度
* @param  in_ptr :字库输入指针 注意:1pixel 1bit
* @param  out_ptr :缩放后的字符输出指针 注意: 1pixel 8bit
*    out_ptr实际上没有正常输出,改成了直接输出到全局指针zoomBuff中
* @param  en_cn :0为英文,1为中文
* @retval 无
*/
void ILI9341_zoomChar(uint16_t in_width,  //原始字符宽度
                     uint16_t in_heig,   //原始字符高度
                     uint16_t out_width, //缩放后的字符宽度
                     uint16_t out_heig,  //缩放后的字符高度
                     uint8_t *in_ptr,  //字库输入指针  注意:1pixel 1bit
                  uint8_t *out_ptr, //缩放后的字符输出指针 注意: 1pixel 8bit
                     uint8_t en_cn)    //0为英文,1为中文
{
   uint8_t *pts,*ots;
   //根据源字模及目标字模大小,设定运算比例因子,左移16是为了把浮点运算转成定点运算

   unsigned int xrIntFloat_16=(in_width<<16)/out_width+1;
   unsigned int yrIntFloat_16=(in_heig<<16)/out_heig+1;

   unsigned int srcy_16=0;
   unsigned int y,x;
   uint8_t *pSrcLine;

   uint16_t byteCount,bitCount;

   //检查参数是否合法
   if (in_width >= 32) return;             //源字库不允许超过32像素
   if (in_width * in_heig == 0) return;
   if (in_width * in_heig >= 1024 ) return;          //限制输入最大 32*32

   if (out_width * out_heig == 0) return;
if (out_width * out_heig >= ZOOMMAXBUFF ) return; //限制最大缩放 128*128
   pts = (uint8_t*)&zoomTempBuff;

   //为方便运算,字库的数据由1 pixel/1bit 映射到1pixel/8bit
   //0x01表示笔迹,0x00表示空白区
   if (en_cn == 0x00) { //英文
         //英文和中文字库上下边界不对,可在此处调整。需要注意tempBuff防止溢出
         for (byteCount=0; byteCount<in_heig*in_width/8; byteCount++) {
            for (bitCount=0; bitCount<8; bitCount++) {
               //把源字模数据由位映射到字节
               //in_ptr里bitX为1,则pts里整个字节值为1
               //in_ptr里bitX为0,则pts里整个字节值为0
               *pts++ = (in_ptr[byteCount] & (0x80>>bitCount))?1:0;
            }
         }
   } else { //中文
      for (byteCount=0; byteCount<in_heig*in_width/8; byteCount++) {
            for (bitCount=0; bitCount<8; bitCount++) {
               //把源字模数据由位映射到字节
               //in_ptr里bitX为1,则pts里整个字节值为1
               //in_ptr里bitX为0,则pts里整个字节值为0
               *pts++ = (in_ptr[byteCount] & (0x80>>bitCount))?1:0;
            }
      }
   }

   //zoom过程
   pts = (uint8_t*)&zoomTempBuff;  //映射后的源数据指针
   ots = (uint8_t*)&zoomBuff;  //输出数据的指针
   for (y=0; y<out_heig; y++) {  /*行遍历*/
      unsigned int srcx_16=0;
      pSrcLine=pts+in_width*(srcy_16>>16);
      for (x=0; x<out_width; x++) { /*行内像素遍历*/
            ots[x]=pSrcLine[srcx_16>>16]; //把源字模数据复制到目标指针中
            srcx_16+=xrIntFloat_16;     //按比例偏移源像素点
      }
      srcy_16+=yrIntFloat_16;         //按比例偏移源像素点
      ots+=out_width;
   }
   /*!!!缩放后的字模数据直接存储到全局指针zoomBuff里了*/
   //out_ptr没有正确传出,后面调用直接改成了全局变量指针!
   out_ptr = (uint8_t*)&zoomBuff;
   /*实际中如果使用out_ptr不需要下面这一句!!!
      只是因为out_ptr没有使用,会导致warning。强迫症*/
   out_ptr++;
}

缩放字模的本质是按照缩放比例,减少或增加矩阵中的像素点,见 图27_8,只要把左侧的矩阵隔一行、隔一列地取出像素点,即可得到右侧按比例缩小了的矩阵,而右侧的小 矩阵按比例填复制像素点即可得到左侧放大的矩阵,上述函数就是完成了这样的工作。

图 27‑8 缩放矩阵

该函数的说明如下:

  1. 输入参数

函数包含输入参数源字模、缩放后字模的宽度及高度:in_width、inheig、out_width、out_heig。源字模数据指针in_ptr,缩放后的字符指针out_ptr以及用于指示字模是英文还是中文的标志en_cn。其中out_ptr指针实质上没有用到,这个函数缩放后的数据最后直接存储在全局变量zoomBuff中了。

  1. 计算缩放比例

根据输入字模与要求的输出字模大小,计算出缩放比例到xrIntFloat_16及yrIntFloat_16变量中,运算式中的左移16位是典型的把浮点型运算转换成定点运算的处理方式。理解的时候可把左移16位的运算去掉,把它当成一个自然的数学小数运算即可。

  1. 检查输入参数

由于运算变量及数组的一些限制,函数中要检查输入参数的范围,本函数限制最大输出字模的大小为128*128像素,输入字模限制不可以超过32*32像素。

  1. 映射字模

输入源的字模都是1个数据位表示1个像素点的,为方便后面的运算,函数把输入字模转化成1个字节(8个数据位)表示1个像素点,该字节的值为0x01表示笔迹像素,0x00表示空白像素。把字模数据的1个数据位映射为1个字节,可以方便后面直接使用指针和数组索引运算。

  1. 缩放字符

缩放字符这部分代码比较难理解,但总的来说它就是利用前面计算得的比例因子,以它为步长复制源字模的数据到目标字模的缓冲区中,具体的抽象运算只能意会了。其中的右移16位是把比例因子由定点数转换回原始的数值。如果还是觉得难以理解,可以把函数的宽度及高度输入参数in_width、inheig、out_width及out_heig都设置成16,然后代入运算来阅读这段代码。

  1. 缩放结果

经过运算,缩放的结果存储在zoomBuff中,它只是存储了一个字模的缩放结果,所以每显示一个字模都需要先调用这个函数更新zoomBuff中的字模数据,而且它也是用1个字节表示1个像素位的。

28.3.4.2.2. 利用缩放的字模数据显示字符

由于缩放后的字模数据格式与我们原来用的字模数据格式不一样,所以我们也 要重新编写字符显示函数,见 代码清单27_19

代码清单 27‑19 利用缩放的字模显示字符
/**
* @brief  利用缩放后的字模显示字符
* @param  Xpos :字符显示位置x
* @param  Ypos :字符显示位置y
* @param  Font_width :字符宽度
* @param  Font_Heig:字符高度
* @param  c:要显示的字模数据
* @param  DrawModel :是否反色显示
* @retval 无
*/
void ILI9341_DrawChar_Ex(uint16_t usX, //字符显示位置x
                        uint16_t usY, //字符显示位置y
                        uint16_t Font_width, //字符宽度
                        uint16_t Font_Height,  //字符高度
                        uint8_t *c,            //字模数据
                        uint16_t DrawModel)    //是否反色显示
{
   uint32_t index = 0, counter = 0;

   //设置显示窗口
   ILI9341_OpenWindow ( usX, usY, Font_width, Font_Height);

   ILI9341_Write_Cmd ( CMD_SetPixel );

   //按字节读取字模数据
   //由于前面直接设置了显示窗口,显示数据会自动换行
   for ( index = 0; index < Font_Height; index++ ) {
      //一位一位处理要显示的颜色
      for ( counter = 0; counter < Font_width; counter++ ) {
            //缩放后的字模数据,以一个字节表示一个像素位
            //整个字节值为1表示该像素为笔迹
            //整个字节值为0表示该像素为背景
            if ( *c++ == DrawModel )
               ILI9341_Write_Data ( CurrentBackColor );
            else
               ILI9341_Write_Data ( CurrentTextColor );
      }
   }
}

注意在这个函数中,它并没有对中英文模区分显示代码,因为本函数的字模是由输入参数c指针中获取的,在调用本函数时,需要输入要显示的字模数据指针,而不是字符编码。在其它方面这个函数主体与前面介绍的字符显示函数都很类似,只是它在判断字模数据位的时候,直接用一整个字节来判断,区分显示分支,而且还支持了反色显示模式。

28.3.4.2.3. 利用缩放的字模显示字符串

单个字符显示的函数并不包含字模的获取过程,为便于使用,我们把它直接封装成字符串显示函数,见 代码清单27_20

代码清单 27‑20 利用缩放的字模显示字符串
/**
* @brief  利用缩放后的字模显示字符串
* @param  Xpos :字符显示位置x
* @param  Ypos :字符显示位置y
* @param  Font_width :字符宽度,英文字符在此基础上/2。注意为偶数
* @param  Font_Heig:字符高度,注意为偶数
* @param  c :要显示的字符串
* @param  DrawModel :是否反色显示
* @retval 无
*/
void ILI9341_DisplayStringEx(uint16_t x,    //字符显示位置x
                              uint16_t y,        //字符显示位置y
                              uint16_t Font_width,
                              //要显示的字体宽度,英文字符在此基础上/2。注意为偶数
                        uint16_t Font_Height,  //要显示的字体高度,注意为偶数
                              uint8_t *ptr,          //显示的字符内容
                              uint16_t DrawModel)  //是否反色显示

{
uint16_t Charwidth = Font_width; //默认为Font_width,英文宽度为中文宽度的一半
   uint8_t *psr;
   uint8_t Ascii;  //英文
   uint16_t usCh;  //中文
   uint8_t ucBuffer [ WIDTH_CH_CHAR*HEIGHT_CH_CHAR/8 ];

   while ( *ptr != '\0') {
      /****处理换行*****/
if ( ( x - ILI9341_DispWindow_X_Star + Charwidth ) > LCD_X_LENGTH ) {
            x = ILI9341_DispWindow_X_Star;
            y += Font_Height;
      }

if ( ( y - ILI9341_DispWindow_Y_Star + Font_Height ) > LCD_Y_LENGTH ) {
            x = ILI9341_DispWindow_X_Star;
            y = ILI9341_DispWindow_Y_Star;
      }

      if (*ptr > 0x80) { //如果是中文
            Charwidth = Font_width;
            usCh = * ( uint16_t * ) ptr;
            usCh = ( usCh << 8 ) + ( usCh >> 8 );
            GetGBKCode ( ucBuffer, usCh );  //取字模数据
            //缩放字模数据,源字模为16*16
            ILI9341_zoomChar(WIDTH_CH_CHAR,HEIGHT_CH_CHAR,Charwidth,
Font_Height,(uint8_t *)&ucBuffer,psr,1);
            //显示单个字符
            ILI9341_DrawChar_Ex(x,y,Charwidth,Font_Height,
(uint8_t*)&zoomBuff,DrawModel);
            x+=Charwidth;
            ptr+=2;
      } else {//英文
            Charwidth = Font_width / 2;
            Ascii = *ptr - 32;
            //使用16*24字体缩放字模数据
      ILI9341_zoomChar(16,24,Charwidth,Font_Height,
(uint8_t *)&Font16x24.table[Ascii * Font16x24.Height*Font16x24.Width/8],psr,0);
            //显示单个字符
ILI9341_DrawChar_Ex(x,y,Charwidth,Font_Height,(uint8_t*)&zoomBuff,DrawModel);
            x+=Charwidth;
            ptr++;
      }
   }
}

这个函数包含了从字符编码到源字模获取、字模缩放及单个字符显示的过程,多个这样的过程组合起来,就实现了简单易用的字符串显示函数。而且可了解到它使用的英文源字模数据是Font16x24字体,而中文源字模数据仍是采用GetGBKCode函数获取,使得数据源的获取与上层分离,支持从SPI FLASH及SD卡中获取数据源。

28.3.4.2.4. 利用缩放的字模显示示例

利用缩放的字模显示时,液晶的初始化过程与前面的工程无异,以下我们给出LCD_Test函数 中调用字符串函数显示不同字符时的示例,见 代码清单27_21

代码清单 27‑21 利用缩放的字模显示示例
/*用于测试各种液晶的函数*/
void LCD_Test(void)
{
   /*演示显示变量*/
   static uint8_t testCNT = 0;
   char dispBuff[100];

   testCNT++;

   LCD_SetFont(&Font8x16);
   LCD_SetColors(RED,BLACK);

   ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
   /********显示字符串示例*******/
   ILI9341_DispStringLine_EN_CH(LINE(0),"野火BH");
   //显示指定大小的字符
   ILI9341_DisplayStringEx(0,1*24,24,24,(uint8_t *)"野火BH",0);
   ILI9341_DisplayStringEx(2*48,0*48,48,48,(uint8_t *)"野火BH",0);

   /********显示变量示例*******/
   LCD_SetTextColor(GREEN);

   /*使用cHAL库把变量转化成字符串*/
   sprintf(dispBuff,"显示变量: %d ",testCNT);
   LCD_ClearLine(LINE(5)); /* 清除单行文字 */

   /*然后显示该字符串即可,其它变量也是这样处理*/
   ILI9341_DispStringLine_EN_CH(LINE(5),dispBuff);
   /*...以下部分省略*/
}

28.3.5. 下载验证

用USB线连接开发板,编译程序下载到实验板,并上电复位,各个不同的工程会有不同的的液晶屏显示字符示例。