记得我刚入行嵌入式那会儿,对着三星 S3C2440 那本厚得像砖头一样的参考手册(Datasheet),盯着 LCD 控制器那一章看了整整一下午,整个人都是懵的。寄存器名字长得要命,时序参数眼花缭乱,代码写了一堆,屏幕要么花屏、要么黑屏、要么颜色颠倒。
今天我不给你整那些枯燥的教科书式定义,咱们就像两个工程师在工位上聊天一样,把这块“老古董”里的 LCD 控制器从头到尾扒一遍。S3C2440 虽然是 2005 年左右的产品,但它的 LCD 接口逻辑非常经典,搞懂了它,再去搞 6410、S5PV210 甚至是其他 ARM Cortex 系列的处理器的 LCD 驱动,都是举一反三的事。
先摸清家底:S3C2440 的 LCD 控制器是个啥?
在动手写代码之前,你得知道我们要控制的是什么。S3C2440 内部集成了一个 LCD 控制器,它的主要工作就一件事:把内存里的图像数据,按照 LCD 屏幕的要求,准时准点地送出去。
你可以把它想象成一个“搬运工头”。你的显存(VRAM)就是仓库,LCD 屏幕就是客户。这个搬运工头负责从仓库里拿货,并且严格按照客户规定的时间(时序)交货。
它支持哪些屏幕?
S3C2440 的 LCD 控制器支持两类接口,这是你选型时首先要确定的:
- MCP(Mono Color Panel)接口:
- 这是最简单的,用于单色屏或 STN 屏。
- 特点:数据位宽固定,通常只用于黑白或绿底黑字的简易屏。如果你买的是那种十几块钱的 2.8 寸单色屏,用这个。
- TFT(Thin Film Transistor)接口:
- 这才是我们今天要聊的重点,彩色液晶屏幕。
- 特点:支持多种色彩深度(1bpp, 4bpp, 8bpp, 16bpp, 24bpp),引脚灵活,时序可配置。
重要提示:S3C2440 的 LCD 控制器不支持 24bpp(RGB888)的全真彩色输出,它最高只支持 16bpp(RGB565)。这意味着每个像素用 16 位(2 字节)来表示,格式是 RRRRRGGGGGGBBBBB。如果你的屏幕是 24 位的,你需要外接一个屏 IC(解码芯片),但在 2440 上直接用 16 位模式配合 16 位宽度的屏幕是最常见、最稳定的方案。
关键引脚回顾
在原理图上,你会看到一堆标着 LCD_ 的引脚,别慌,其实就分三类:
- 控制信号:
LCDCON1~LCDCON5对应的输出:VN(垂直同步)、VM(帧同步/帧率)、VLACC(水平脉冲)、VCLK(像素时钟)、VENB(显示使能)。LPCSEL:选择 LCD 类型(TFT 或 STN)。
- 数据信号:
D[15:0]:16 位数据线,传输 RGB 数据。
- 电源与背光:
LCD_PWREN:背光使能,很多时候屏幕不亮,不是 LCD 控制器没配好,而是这个脚没拉高,背光没开。这点一定要记住,这是新手最容易踩的坑之一。
灵魂所在:LCDCON1 寄存器详解
配置 LCD,第一步永远是配置 LCDCON1。这个寄存器决定了 LCD 的工作模式、位宽、时钟分频等核心参数。我们把它拆开来看。
假设我们使用一块 320x240 的 TFT 屏幕,色彩模式 16bpp (RGB565),像素时钟 10MHz。
// 假设 FCLK:HCLK:PCLK = 1:1:1,即 PCLK = FCLK / 1
// 如果 PCLK = 10MHz,我们需要计算 CLKVAL
// LCDCON1 的位定义:
// [23:16] : CLKVAL (像素时钟分频系数)
// [15:12] : 保留
// [11:8] : VBPD (垂直后沿)
// [7:4] : LINEVAL (行数 - 1)
// [3:0] : VSPW (垂直同步脉冲宽度)
// 等等,上面的描述有点简化,实际 LCDCON1 的 bit 定义如下(以 S3C2440 手册为准):
// Bit[23:16] : CLKVAL (对于 TFT mode)
// Bit[15:12] : 保留
// Bit[11:8] : VBPD
// Bit[7:4] : LINEVAL (Active lines - 1)
// Bit[3:0] : VSPW
// 但是!S3C2440 的 LCDCON1 实际上是一个复合寄存器,不同的 bit 段控制不同的东西。
// 让我们看一个典型的配置代码:
#define LCD_CLKVAL 13 // 根据 PCLK 和 VCLK 计算得出
#define LCD_VBPD 1 // 垂直后沿周期数
#define LCD_LINEVAL 239 // 240 行,所以是 239
#define LCD_VSPW 0 // 垂直同步脉冲宽度
// 实际上,S3C2440 的 LCDCON1 寄存器结构更复杂,它同时包含了时钟分频和时序参数。
// 更准确的做法是直接写值。
// 假设 PCLK = 10MHz, 需要 VCLK = 10MHz / (CLKVAL + 1)
// 如果 CLKVAL = 1, VCLK = 5MHz (太快了,通常 TFT 屏幕需要 5-10MHz 左右的 VCLK)
// 让我们假设我们需要 VCLK = 6.5MHz, PCLK = 10MHz
// CLKVAL = (PCLK / VCLK) - 1 = (10 / 6.5) - 1 ≈ 0.53, 取整数 0 或 1
// 通常我们会调整 PCLK 或者接受一个近似的 VCLK。
// 在代码中,我们通常这样配置:
// LCDCON1 = (CLKVAL << 16) | (VBPD << 8) | (LINEVAL << 4) | VSPW;
// 但等等,S3C2440 的 LCDCON1 的 bit 23:16 是 CLKVAL,bit 15:12 是保留,
// bit 11:8 是 VBPD,bit 7:4 是 LINEVAL,bit 3:0 是 VSPW。
// 这是一个常见的误解点,请务必查阅最新的手册确认。
// 正确的 S3C2440 LCDCON1 位定义(TFT Mode):
// [23:16] : CLKVAL
// [15:12] : 保留
// [11:8] : VBPD (Vertical Back Porch)
// [7:4] : LINEVAL (Number of active lines - 1)
// [3:0] : VSPW (Vertical Sync Pulse Width)
// 所以配置代码如下:
unsigned int lcd_con1 = (13 << 16) | (1 << 8) | (239 << 4) | 0;
这里有个大坑:很多人只配了 LCDCON1,然后就发现屏幕花屏。为什么?因为 LCDCON1 里的 CLKVAL 计算错了。
CLKVAL 的计算公式是:
VCLK = PCLK / (CLKVAL + 1)
其中:
PCLK是FCLK经过CLKDIVN分频后得到的时钟,通常连接 LCD 控制器的时钟。VCLK是像素时钟,必须和你的 LCD 屏幕规格书匹配。
如何确定 PCLK?
你需要在初始化时钟部分(clock.h 或 clock.c)设置 CLKDIVN 寄存器。比如,你希望 FCLK:HCLK:PCLK = 1:4:8,那么 CLKDIVN 的值就是 5 (二进制 101)。假设 FCLK = 400MHz,那么 PCLK = 400 / 8 = 50MHz。
如果屏幕需要 VCLK = 10MHz,那么 CLKVAL = (50 / 10) - 1 = 4。
时序参数:让屏幕“不花屏”的秘密
LCD 屏幕不是连续传输数据的,它是“行”和“帧”的结构。想象你在画一幅画,你得先画一行,然后换到下一行开头;画完所有行,再刷新整个画面。这个过程中的“空白时间”就是时序参数。
垂直时序(Vertical Timing)
- VSYNC (垂直同步):告诉 LCD “这一帧开始了”。
- VBP (垂直后沿):VSYNC 结束后,到有效数据显示开始之间的行数。
- VFP (垂直前庭):有效数据显示结束后,到下一帧开始之间的行数。
- VBPD:在 S3C2440 中,
VBPD对应的是 VSYNC 结束到有效数据开始之间的行数减 1。 - LINEVAL:有效行数减 1。
- VSPW:VSYNC 脉冲的宽度(行数)。
水平时序(Horizontal Timing)
- HSYNC (水平同步):告诉 LCD “这一行开始了”。
- HBP (水平后沿):HSYNC 结束后,到有效数据显示开始之间的时钟周期数。
- HFP (水平前庭):有效数据显示结束后,到下一行开始之间的时钟周期数。
- HBPD:在 S3C2440 的
LCDCON2寄存器中配置。 - HOZVAL:有效列数减 1。
- HSPW:HSYNC 脉冲宽度(时钟周期数)。
实战技巧:这些参数你必须从 LCD 屏幕的规格书(Datasheet) 里找!每个屏幕的时序都不一样。比如,常见的 3.5 寸 320x240 屏幕,规格书里会有一个表格,列出 HSPW, HBPD, HFP, HOZVAL 等值。
颜色模式与数据格式
S3C2440 支持多种颜色模式,最常用的就是 16bpp (RGB565)。
RGB565 格式
每个像素 16 位:
R(红色): 5 位 (Bit 15-11)G(绿色): 6 位 (Bit 10-5)B(蓝色): 5 位 (Bit 4-0)
注意:S3C2440 的 LCD 控制器可以配置数据端子的排列顺序(HSYNC, VSYNC, VEN 的极性,以及 RGB 数据的顺序)。如果你发现屏幕颜色不对(比如红色和蓝色互换了),你需要调整 LCDCON3 寄存器中的 BPPMODE 和 INVVCLK、INVVLINE 等位。
例如,如果颜色反转,尝试将 LCDCON3 中的 INVVFRAME 或 INVVLINE 取反。
显存(VRAM)的配置
LCD 控制器不会自动知道图像数据在哪里,你需要告诉它显存的起始地址。
WINCON 和 WINxADDR
S3C2440 有 5 个窗口(Window 0 - Window 4),其中 Window 0 是最常用的,通常用于背景显示。
- WINCON0:使能 Window 0,并设置颜色模式和 DMA 通道。
- WINxADDRx (x=0,1,2):设置显存的起始地址、大小和位置。
// 假设显存起始地址为 0x33C00000
// WINCON0: 使能 Window 0, 16bpp, RGB565
#define WINCON0_VAL ((0 << 7) | (0 << 6) | (4 << 2) | (1 << 0))
// 位[7:6]: 关闭 Alpha blending (不使用)
// 位[5:4]: 保留
// 位[3:2]: BPPMODE = 4 (16bpp)
// 位[1:0]: ENWIN = 1 (使能)
// WIN0ADDR1: 显存起始地址的高 20 位
// WIN0ADDR2: 显存起始地址的低 18 位
// WIN0ADDR3: 显存大小和位置
// 计算显存大小:320 * 240 * 2 bytes = 153600 bytes = 0x25800
关键点:显存的大小必须大于或等于 (宽度 * 高度 * 颜色深度)。如果你的显存太小,画面就会错位或者显示错误的内容。
完整的初始化流程(代码示例)
下面是一个典型的 S3C2440 LCD 初始化代码框架,你可以参考这个结构:
”`c
#include
#define LCD_XSIZE 320 #define LCD_YSIZE 240 #define LCD_BPP 16 // 16bpp
// 显存地址,建议放在 SDRAM 中,且地址要 256 字节对齐 unsigned int *pLCDBuffer = (unsigned int *)0x33C00000;
void lcd_init(void) {
// 1. 配置 GPIO 为 LCD 功能
// GPCCON 和 GPDCON 需要配置为 LCD 相关信号
rGPCCON = (3 << 0) | (3 << 4) | (3 << 8) | (3 << 12) | (3 << 16) | (3 << 20); // LCDCON1[5:0] -> GPC0-GPC5
rGPDCON = (3 << 0) | (3 << 4) | (3 << 8) | (3 << 12) | (3 << 16) | (3 << 20) | (3 << 24) | (3 << 28); // LCDCON2[7:0] -> GPD0-GPD7
// 2. 配置 LCDCON1 - 时钟和垂直时序
// 假设 PCLK = 10MHz, VCLK = 10MHz, 所以 CLKVAL = 0
// VBPD = 1, LINEVAL = 239, VSPW = 0
rLCDCON1 = (0 << 16) | (1 << 8) | (239 << 4) | (0 << 0);
// 3. 配置 LCDCON2 - 水平时序
// 假设 HBP = 2, HOZVAL = 319, HFP = 2, HSPW = 0
rLCDCON2 = (2 << 24) | (319 << 14) | (2 << 6) | (0 << 0);
// 4. 配置 LCDCON3 - 颜色深度和极性
// BPPMODE = 4 (16bpp), INVVFRAME = 0, INVVLINE = 0, ...
rLCDCON3 = (0 << 8) | (319 << 0); // HOZVAL 也可以在这里配置,取决于具体型号,通常以 LCDCON2 为准
// 5. 配置 LCDCON4 - 水平同步极性
// MVAL = 0, INVVFRAME = 0, ...
rLCDCON4 = 0;
// 6. 配置 LCDCON5 - 控制信号极性
// INVVCLK = 0, HSYNC 极性, VSYNC 极性, ...
rLCDCON5 = (1 << 6) | (1 << 5) | (1 << 4) | (1 << 3); // 根据屏幕手册调整极性
// 7. 配置 WINCON0 - 使能 Window 0
rWINCON0 = (0 << 7) | (0 << 6) | (4 << 2) | (1 << 0);
// 8. 配置显存地址
// 假设显存大小为 320*240*2 = 153600 字节
rWINxADDR1 = (unsigned int)pLCDBuffer >> 2; // 地址右移 2 位
rWINxADDR2 = (unsigned int)pLCDBuffer >> 2;
rWINxADDR3 = (LCD_XSIZE * LCD_BPP / 8) | ((LCD_XSIZE * LCD_BPP / 8) << 14); // 设置每行字节数和显存大小
// 9. 使能 LCD 控制器
rLCDCON1 |= (1 << 0); // ENVID =