Mini2440裸机开发之LCD编程(GB2312、ASCII字库制作)
在上一节我们介绍了LCD的硬件基础知识、以及S3C2440 LCD控制器相关的寄存器。这一节我们将会动手在LCD上显示一幅日落的图片。
一、LCD初始化编程步骤
1.1 初始化GPIO,引脚复用
在上一节我们介绍了S3C2440这些引脚对应的LCD TFT上的引脚。这里就不在重复介绍了。我们需要配置Port C和Port D为LCD功能。
1.1.1GPIOC
端口C相关寄存器的相关信息:
寄存器 | 地址 | R/W | 描述 | 复位值 |
GPCCON | 0x56000020 | R/W | 配置端口C的引脚 | 0x00 |
GPCDAT | 0x56000024 | R/W | 配置C的数据寄存器 | - |
GPCUP | 0x56000028 | R/W | 端口C的上拉使能寄存器 | 0x00 |
保留 | 0x5600002C | - | 保留 | - |
(1) GPCCON
GPCCON | 位 | 描述 | 初始状态 |
GPC15 | [31:30] | 00 = 输入 01 = 输出 10 = VD[7] 11 = 保留 | 0 |
GPC14 | [29:28] | 00 = 输入 01 = 输出 10 = VD[6] 11 = 保留 | 0 |
GPC13 | [27:26] | 00 = 输入 01 = 输出 10 = VD[5] 11 = 保留 | 0 |
GPC12 | [25:24] | 00 = 输入 01 = 输出 10 = VD[4] 11 = 保留 | 0 |
GPC11 | [23:22] | 00 = 输入 01 = 输出 10 = VD[3] 11 = 保留 | 0 |
GPC10 | [21:20] | 00 = 输入 01 = 输出 10 = VD[2] 11 = 保留 | 0 |
GPC9 | [19:18] | 00 = 输入 01 = 输出 10 = VD[1] 11 = 保留 | 0 |
GPC8 | [17:16] | 00 = 输入 01 = 输出 10 = VD[0] 11 = 保留 | 0 |
GPC7 | [15:14] | 00 = 输入 01 = 输出 10 = LCD_LPCREVB 11 = 保留 | 0 |
GPC6 | [13:12] | 00 = 输入 01 = 输出 10 = LCD_LPCREV 11 = 保留 | 0 |
GPC5 | [11:10] | 00 = 输入 01 = 输出 10 = LCD_LPCOE 11 = 保留 | 0 |
GPC4 | [9:8] | 00 = 输入 01 = 输出 10 = VM 11 = 保留 | 0 |
GPC3 | [7:6] | 00 = 输入 01 = 输出 10 = VFRAME 11 = 保留 | 0 |
GPC2 | [5:4] | 00 = 输入 01 = 输出 10 = VLINE 11 = 保留 | 0 |
GPC1 | [3:2] | 00 = 输入 01 = 输出 10 = VCLK 11 = 保留 | 0 |
GPC0 | [1:0] | 00 = 输入 01 = 输出 10 = LEND 11 = 保留 | 0 |
由上表可知,B端口的控制寄存器可以将每个引脚配置为四种模式:
- 00:输入模式
- 01:输出模式
- 10:功能扩展模式
- 11:保留模式
配置端口C功能复用为LCD:
GPCCON=0xaaaaaaaa
(2) GPCDAT
GPCDAT | 位 | 描述 |
GPC[15:0] | [15:0] |
当端口配置为输入端口时,相应位为引脚状态。当端口配置为输出端口时,引脚状态将与相应位相同。 当端口配置为功能引脚,将读取到未定义值。 |
(3) GPCUP
GPCUP | 位 | 描述 |
GPC[15:0] | [15:0] |
0:使能附加上拉功能到相应端口引脚 1:禁止附加上拉功能到相应端口引脚 |
禁止上拉:
GPCUP = 0xffffffff;
1.1.2 GPIOD
端口D相关寄存器的相关信息:
寄存器 | 地址 | R/W | 描述 | 复位值 |
GPDCON | 0x56000030 | R/W | 配置端口D的引脚 | 0x00 |
GPDDAT | 0x56000034 | R/W | 配置D的数据寄存器 | - |
GPDUP | 0x56000038 | R/W | 端口D的上拉使能寄存器 | 0xF000 |
保留 | 0x5600003C | - | 保留 | - |
(1) GPDCON
GPDCON | 位 | 描述 | 初始状态 |
GPD15 | [31:30] | 00 = 输入 01 = 输出 10 = VD[23] 11 = 保留 | 0 |
GPD14 | [29:28] | 00 = 输入 01 = 输出 10 = VD[22] 11 = 保留 | 0 |
GPD13 | [27:26] | 00 = 输入 01 = 输出 10 = VD[21] 11 = 保留 | 0 |
GPD12 | [25:24] | 00 = 输入 01 = 输出 10 = VD[20] 11 = 保留 | 0 |
GPD11 | [23:22] | 00 = 输入 01 = 输出 10 = VD[19] 11 = 保留 | 0 |
GPD10 | [21:20] | 00 = 输入 01 = 输出 10 = VD[18] 11 = 保留 | 0 |
GPD9 | [19:18] | 00 = 输入 01 = 输出 10 = VD[17] 11 = 保留 | 0 |
GPD8 | [17:16] | 00 = 输入 01 = 输出 10 = VD[16] 11 = 保留 | 0 |
GPD7 | [15:14] | 00 = 输入 01 = 输出 10 = VD[15] 11 = 保留 | 0 |
GPD6 | [13:12] | 00 = 输入 01 = 输出 10 = VD[14] 11 = 保留 | 0 |
GPD5 | [11:10] | 00 = 输入 01 = 输出 10 = VD[13] 11 = 保留 | 0 |
GPD4 | [9:8] | 00 = 输入 01 = 输出 10 = VD[12] 11 = 保留 | 0 |
GPD3 | [7:6] | 00 = 输入 01 = 输出 10 =VD[11] 11 = 保留 | 0 |
GPD2 | [5:4] | 00 = 输入 01 = 输出 10 = VD[10] 11 = 保留 | 0 |
GPD1 | [3:2] | 00 = 输入 01 = 输出 10 = VD[9] 11 = 保留 | 0 |
GPD0 | [1:0] | 00 = 输入 01 = 输出 10 = VD[8] 11 = 保留 | 0 |
由上表可知,B端口的控制寄存器可以将每个引脚配置为四种模式:
- 00:输入模式
- 01:输出模式
- 10:功能扩展模式
- 11:保留模式
配置端口D功能复用为LCD:
GPDCON=0xaaaaaaaa
(2) GPDDAT
GPCDAT | 位 | 描述 |
GPD[15:0] | [15:0] |
当端口配置为输入端口时,相应位为引脚状态。当端口配置为输出端口时,引脚状态将与相应位相同。 当端口配置为功能引脚,将读取到未定义值。 |
(3) GPDUP
GPCUP | 位 | 描述 |
GPD[15:0] | [15:0] |
0:使能附加上拉功能到相应端口引脚 1:禁止附加上拉功能到相应端口引脚 |
禁止上拉:
GPDUP = 0xffffffff;
总结下来:
void _lcd_gpio_init() { GPCUP = 0xffffffff; /* Disable Pull - up register */ GPCCON = 0xaaaaaaaa; /* Initialize VD[7:0], VM, VFRAME, VLINE, VCLK */ GPDUP = 0xffffffff; /* Disable Pull - up register */ GPDCON = 0xaaaaaaaa; /* Initialize VD[15:8] */ }
1.2 打开LCD电源
LCD_PWR对应的引脚,所以设置GPG4就可以控制LED背光电源了。(GPGCON:9-8写入11),这时候LCD电源的打开/关闭可以通过LCDCON5位3来控制:
LCD_PWREN 输出信号使能/禁止:0 = 禁止PWREN 信号 1 = 允许PWREN信号:
void _lcd_power_on() { /* 打开LCD电源 */ GPGUP |= (1 << 4); /* Pull - up disable */ GPGCON |= (3 << 8); /* 设置GPG4引脚为LCD_PWREN模式 */ LCDCON5 |= (1 << 3); /* LCD_PWREN输出信号使能 */ }
1.3 设置其他信号线
其他信号线包括VD0-VD23和VFRAME、VLINE、VCLK等,分别在GPCCON,GPDCON中选择相应功能。这些在上一节已经介绍过。这里就稍微提一下
1.3.1 设置VCLK、BPPMODE、PNRMODE
(VCLK)LCD的Datasheet上一般会写有一个推荐的频率,上一节已经介绍过我使用的屏幕推荐频率为6.4M,我们通过计算得到的CLKVAL=7比较合适。写入LCDCON1位17-8;
位4-1选择BPP(位每像素)模式 1100 = TFT 的16 BPP;
位6-5选择显示模式 11 = TFT LCD 面板
1.3.2 设置其他相关参数
LCD相关的参数主要还有这几个:
- LINEVAL: LCD水平像素-1,如240-1 = 239;
- HOZVAL: LCD垂直像素-1,如320-1 = 319;
- HFPD: 行开始前的VCLK时钟数(LCD屏幕的Datasheet一般有推荐值);
- HBPD: 行结束后的VCLK时钟数(LCD屏幕的Datasheet一般有推荐值);
- HSPW: 行之间水平同步的无效VCLK时钟数(LCD屏幕的Datasheet一般有推荐值);
- VFPD: 帧数据开始前的空白行数(LCD屏幕的Datasheet一般有推荐值);
- VBPD: 帧数据结束后的空白行数(LCD屏幕的Datasheet一般有推荐值);
- VSPW: 帧之间垂直同步的无效行数(LCD屏幕的Datasheet一般有推荐值);
(相关寄存器LCDCON2, LCDCON3, LCDCON4)
1.3.3 设置视频缓冲区的地址
S3C2440支持虚拟屏幕,可以通过改变LCD寄存器实现屏幕快速移动;
- PAGEWIDTH:虚拟屏幕一行的字节数,如果不使用虚拟屏幕,设置为实际屏幕的行半字数,如16位宽320像素,设为320 ;
- OFFSIZE:虚拟屏幕左侧偏移的字节数,如果不使用虚拟屏幕,设置为0;
- LCDBANK: 视频帧缓冲区内存地址30-22位;
- LCDBASEU: 视频帧缓冲区的开始地址21-1位;
- LCDBASEL: 视频帧缓冲区的结束地址21-1位;
(相关寄存器LCDSADDR1,LCDSADDR2,LCDSADDR3)
1.3.4 确定信号的极性
S3C2440的LCD控制器允许设置VCLK、VLINE、VFRAME等信号的极性(上升沿有效还是下降沿有效),需要对照LCD的Datasheet一一确认。
(相关寄存器LCDCON5)
1.3.5 关中断
设置LCDINTMSK位1-0。
1.3.6 禁止LPC3600/LCC3600模式
如果不是使用三星LPC3600/LCC3600 LCD,必须禁止LPC3600/LCC3600模式(写入0到TCONSEL位4、0)。
1.3.7 关闭临时调色板
TPAL位24设置为0。
1.3.8 代码
总体设置代码如下:
void _lcd_control_init() { /* [17:8] CLKVAL * [6:5] PNRMODE;选择显示模式 * 00 = 4 位双扫描显示模式 01 = 4 位单扫描显示模式(STN) * 10 = 8 位单扫描显示模式 11 = TFT LCD 面板 * [4:1] BPPMODE 选择BPP(位每像素)模式 1100 = TFT 的16 BPP * [0] ENVID LCD 视频输出和逻辑使能/禁止。 * 0 = 禁止视频输出和LCD 控制信号 1 = 允许视频输出和LCD 控制信号 */ LCDCON1 = (CLKVAL<<8)| (3<<5)|(0xC<<1); /* 16 bpp for TFT */ /* [31:24] VBPD:帧同步信号的后肩 * [23:14] LINEVAL:LCD面板的垂直尺寸 * [13:6] VFPD:帧同步信号的前肩 * [5:0] VSPW:同步信号的脉宽 */ LCDCON2 = (VBPD<<24)|(LINEVAL<<14)|(VFPD<<6)|(VSPW); /* [25:19] HBPD: 行同步信号的后肩 * [18:8] HOZVAL: LCD面板的水平尺寸 * [7:0] HFPD: 行同步信号的前肩 */ LCDCON3 = (HBPD<<19)|(HOZVAL<<8)|(HFPD); LCDCON4 = (HSPW); /* [11] FRM565: 此位选择16 BPP 输出视频数据的格式 0 = 5:5:5:1 格式 1= 5:6:5 格式 * [10] STN/TFT: 此位控制VCLK 有效沿的极性 * [9] INVVLINE: STN/TFT:此位表明VLINE/HSYNC 脉冲极性 0 = 正常 1 = 反转 * [8] INVVFRAME: STN/TFT:此位表明VFRAME/VSYNC 脉冲极性 0 = 正常 1 = 反转 * VLINE/HSYNC 脉冲极性、VFRAME/VSYNC 脉冲极性反转(LCD型号决定) * [0] HWSWP: STN/TFT:半字节交换控制位 0 = 交换禁止 1 = 交换使能 */ LCDCON5 = ((1<<11) | (1<<10) | (1 << 9) | (1 << 8) | (1 << 0)); #define M5D(n) ((n)&0x1fffff) #define LCD_ADDR ((u32)LCD_BUFFER) /* FrameBuffer地址 */ /* [29:21] LCDBANK:存放帧缓冲起始地址的[30:22] * [20:0] LCDBASEU: 存放帧缓冲起始地址的[21:1] */ LCDSADDR1 = ((LCD_ADDR >> 22) << 21) | (M5D(LCD_ADDR >> 1)) ; /* 存放帧结束地址[21:1] */ LCDSADDR2 = M5D((LCD_ADDR + (LCD_WIDTH * LCD_HEIGHT * 2 )) >> 1); /* [21:11] OFFSIZE:表示虚拟屏偏移尺寸 即上一行最后像素点到下一行第一个像素点之间间隔多少个像素点 * [10:0] PAGEWIDTH:表示行的宽度(半字为单位 16bit) */ LCDSADDR3 = LCD_WIDTH; LCDINTMSK |= 3; TCONSEL &= ~((1 << 4) | 1); TPAL = 0x00; /* 关闭临时调色板 */ }
1.4 打开视频输出
ENVID设为1 (LCDCON1:0写入1)。
/* LCD使能 */ LCDCON1 |= 1;
到此LCD初始化步骤就介绍完成了,整体代码如下:
void lcd_init() { _lcd_gpio_init(); _lcd_control_init();
_lcd_power_on(); /* LCD使能 */ LCDCON1 |= 1; }
二、显示BMP图片
如果想在LCD上显示一幅图片,只需要向LCD_BUFFER缓冲区写入图片数据即可。
void lcd_draw_bmp(u16 x0,u16 y0,u16 width,u16 height,const u8 *bmp) { u16 x,y; u16 c; u32 p = 0; /* 绘制每一行 */ for( y = 0 ; y < height; y++ ) { /* 绘制每一点 */ for( x = 0 ; x < width; x++ ) { c = bmp[p + 1] | (bmp[p] << 8); /* bmp.c中的数组元素大小是8bit,屏幕像素设置16bit */ if (((x0 + x) < LCD_WIDTH) && ( (y0 + y) < LCD_HEIGHT)) /* LCD尺寸范围,根据实际LCD设置 */ { LCD_BUFFER[y0 + y][x0 + x] = c; } p = p + 2 ; /* 屏幕像素设置16bit,所以要+2 */ } } }
在Ecllispe调试模式下,将代码直接下载SDRAM地址0x30000000运行,图片显示正常:
但是通过MiniTools下载到SDRAM地址0x30000000运行,图片却显示异常:
目前原因还未排查清楚。
三、显示字符
3.1 字符显示原理
既然我们能使用LCD去显示图片,那么去显示字符其实也是很简单的道理。我们可以把每个字符看成一个比较小的图片。
比如16*16的汉字显示原理就是在一个长宽16的正方形中绘制若干个点,这样就只有弄清楚哪些点是要显示的就行了。比如第一行要显示一个点我们就可以 xxxxxxxoxxxxxxxx, 我们只有把中间的圈显示,其余的点不显示就可以了。
这样我们就可以用一个数组来保存这些点,每一位表示是否要显示,当然这一位要显示什么颜色, 我们可以自行设置。
3.2 字符制作
对于字模数组的提取现在已经有很多好用的字模提取软件,字库软件的,到网上搜一个下载下来,只要在字模提取软件输入想要显示的汉字软件就帮你把字模的数组显示出来,你只要把这个字模数组放到程序当中去就可以了。
当然这里要注意字模提取的顺序,还有有些字模软件中可以设置要不要倒序的问题,这里我用的是 PCtoLCD2002.EXE 这个软件,我的LCD的取模方式是横向取模,字节不倒序,C51格式 。
结果为:
你(0) DB 08H 80H 08H 80H 08H 80H 11H FEH 11H 02H 32H 04H 34H 20H 50H 20H; DB 91H 28H 11H 24H 12H 24H 12H 22H 14H 22H 10H 20H 10H A0H 10H 40H;"你",0
可以看到一共生成32个字节,每两个字节对应图片中一行的16个点,我们以第一行对应的两个字节为例 0x08 0x80,转换成二进制就是0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0,也就是说将第一行16个像素中的第5个、9个像素点亮。
当然要是有很多汉字要显示的话一个一个字去提取就不容易了。现在已经有人或者有一些标准已经把汉字弄成了一些字库,像16*16的话就标准库GB2312 ,但是注意,这些库好像要么是没有后缀名要么是数据库形式,对于裸机还是比较不方便,这里的话可以去找一下有人把这些做成了C语言数组形式这样就比较好用 了,不过这样有点占内存,16*16 / 8 = 32 就是一个字占32个字节,常用汉字字符库的话一般有6 7千个,那么简单算一下应该就需要差不多200k的内存,对于单片机来说还是比较难以消化的。
3.3 字符原理
有了字库我们就只需要找个我们需要的汉字然后取出来显示就好了,但是怎么找这么汉字,当然这么问题别人早已经解决。
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了