写在前面:这篇文章是写给那些真正把手弄脏、去焊接杜邦线、去对着万用表皱眉头的嵌入式工程师看的。别指望读完就能写代码,但如果你卡在“为什么屏不亮”或者“为什么花屏”上,这篇文章能救你的命。
一、 为什么要死磕 S3C2440 的 LCD 驱动?
你可能觉得,现在都是 ARM Cortex-M4/M7 或者高性能 SoC 了,谁还用 2003 年发布的 Samsung S3C2440?
但我必须告诉你:S3C2440 是嵌入式 Linux 驱动开发的“Hello World”进阶版,也是理解现代 GPU/DISP 子系统底层原理的最佳教科书。
它的 LCD 控制器架构清晰、寄存器命名规范、文档详尽。搞懂它,再看高通、瑞昱、甚至苹果芯片的显示驱动,你会发现本质都一样。而且,很多工控机、智能电表、车载仪表还在用这类裸机或嵌入式 Linux 开发,它的坑,你未来还会遇到。
二、 硬件基础:先搞清楚你的“屏幕”是什么
在碰代码之前,你必须从屏幕供应商那里拿到两样东西:
- 数据手册(Datasheet):重点看“电气特性”和“时序参数”章节。
- 参考设计原理图:确认 RGB 数据线接到 S3C2440 的哪些 GPIO,VSYNC/HSYNC 接哪里。
2.1 S3C2440 LCD 控制器支持的模式
S3C2440 的 LCDC(LCD Controller)支持多种接口模式,最常见的是:
- TFT 模式(RGB 接口):这是最常用、最复杂的。需要 HSYNC、VSYNC、DEN(数据使能)、PCLK(像素时钟)和 RGB565/888 数据线。
- STN 模式:老式黑白屏,现在很少见。
- 8 位并行接口:用于简单的字符屏。
我们重点讲 TFT 模式,因为 99% 的实战问题都出在这里。
2.2 关键时序参数(你必须理解的)
看屏幕手册,你会看到一堆数字。别慌,我来翻译成人话:
| 参数名 | 含义 | 常见值 | 影响 |
|---|---|---|---|
| H-Sync Pulse Width | 行同步脉冲宽度 | 4~10 像素 | 决定每一行什么时候开始 |
| V-Sync Pulse Width | 帧同步脉冲宽度 | 1~5 行 | 决定每一帧什么时候开始 |
| HBP (Horizontal Back Porch) | 行消隐后肩 | 2~20 像素 | 同步信号后的延迟 |
| VBP (Vertical Back Porch) | 帧消隐后肩 | 2~10 行 | 同步信号后的延迟 |
| HFP (Horizontal Front Porch) | 行消隐前肩 | 0~10 像素 | 行数据结束后的延迟 |
| VFP (Vertical Front Porch) | 帧消隐前肩 | 0~5 行 | 帧数据结束后的延迟 |
| PCLK (Pixel Clock) | 像素时钟频率 | 6MHz~40MHz | 决定刷新率和分辨率 |
| RES_X, RES_Y | 分辨率 | 240x320, 320x240 | 屏幕的宽和高 |
举个例子:一个 240x320 的屏幕,假设:
- H-Sync: 高电平 1 个 PCLK 周期
- V-Sync: 高电平 1 个 PCLK 周期
- HBP: 2 个 PCLK
- HFP: 2 个 PCLK
- VBP: 2 行
- VFP: 1 行
那么,一行实际占用的时钟数 = 240 + 1 + 2 + 2 = 245 一帧实际占用的行数 = 320 + 1 + 2 + 1 = 324
这个计算结果将直接用于后续寄存器配置。
三、 寄存器详解:S3C2440 LCD 控制的“七剑”
S3C2440 的 LCD 控制器主要通过以下几个寄存器工作。我们不念经,直接说每个寄存器要设置什么,为什么。
3.1 LCDCON1 - 控制主开关和时钟分频
// 地址:0x4D000000
// 典型配置值:0x00000100 (假设模式为 TFT, 16bpp)
| Bit | 字段 | 说明 |
|---|---|---|
| [31:26] | CLKVAL | 最关键! 决定 PCLK 频率。计算公式复杂,见后文。 |
| [25:16] | Reserved | 保留 |
| [15] | BPPMODE | 位深度模式。0=TFT 16bpp(5:6:5), 1=TFT 24bpp(8:8:8) |
| [14:12] | VIDMODE | 视频模式。0x0=Video Sync 0, 0x1=Video Sync 1, 0x3=TFT LCD |
| [11:10] | HWSWP | 硬件双缓冲。关闭通常用于简单驱动 |
| [5:4] | VSTATUS | VSYNC 状态 |
| [3:2] | HSTATUS | HSYNC 状态 |
| [1] | PNRMODE | 页面扫描模式。0=逐行扫描, 1=交错扫描 |
| [0] | ENVID | 主使能位。置 1 后,LCD 控制器开始工作,屏幕才会亮! |
重点:ENVID 必须最后置 1,否则前面所有配置都无效。
3.2 LCDCON2 - 水平时序控制
// 地址:0x4D000004
// 典型配置值:根据 H-Sync, HBP, HFP 计算
| Bit | 字段 | 说明 |
|---|---|---|
| [31:24] | VBPD | 垂直消隐后肩(行数 - 1) |
| [23:14] | LINEVAL | 垂直分辨率(行数 - 1) |
| [13:6] | HBPD | 水平消隐后肩(PCLK 数 - 1) |
| [5:0] | HFP | 水平消隐前肩(PCLK 数 - 1) |
注意:LINEVAL 和 HBPD 等字段通常需要减 1,这是 0-based 计数。
3.3 LCDCON3 - 垂直时序控制
// 地址:0x4D000008
// 典型配置值:根据 V-Sync, VBP, VFP 计算
| Bit | 字段 | 说明 | | :— | -- | :— | | [31:19] | VPED | V-Sync 脉冲宽度(行数 - 1) | | [18:8] | VBP | 垂直消隐后肩(行数 - 1) | | [7:0] | VFP | 垂直消隐前肩(行数 - 1) |
同样,VPED 通常也是减 1。
3.4 LCDCON4 - 水平同步脉冲宽度
// 地址:0x4D00000C
| Bit | 字段 | 说明 |
|---|---|---|
| [15:8] | MVSP | 混合扫描模式选择 |
| [7:0] | HMSP | H-Sync 脉冲宽度(PCLK 数 - 1) |
HMSP 也要减 1。
3.5 LCDCON5 - 极性控制(极性错了,屏幕全白或全黑)
// 地址:0x4D000010
这是最容易出错的地方!
| Bit | 字段 | 说明 |
|---|---|---|
| [13] | BPP24BL | 24bpp 时低位填充 |
| [12] | FRM565 | 当 BPP=565 时,调整色彩格式 |
| [11] | INVVCLK | 像素时钟极性:0=下降沿有效, 1=上升沿有效 |
| [10] | INVVLINE | 行同步极性:0=低电平有效, 1=高电平有效 |
| [9] | INVVFRAME | 帧同步极性:0=低电平有效, 1=高电平有效 |
| [8] | INVVDEN | 数据使能极性:0=高电平有效, 1=低电平有效 |
| [7] | INVVD | 数据信号极性(通常不用) |
| [6] | INVPWREN | 电源使能极性 |
| [5] | INVOD | 输出数据极性 |
| [4] | INVODEN | 输出数据使能极性 |
| [3] | HWSWP | 硬件双缓冲开关 |
| [2:0] | BPPMODE | 位深度模式(与 LCDCON1 重复,以 LCDCON1 为准) |
常见极性组合:
- 大多数 240x320 TFT 屏:
- VSYNC: 高电平有效 (1)
- HSYNC: 高电平有效 (1)
- DEN: 高电平有效 (0)
- PCLK: 上升沿采样 (0)
- 对应 LCDCON5 设置:
0x0700(假设其他位为 0)
3.6 LCDSADDR1 - 显存起始地址(上半部分)
// 地址:0x4D000014
| Bit | 字段 | 说明 |
|---|---|---|
| [31:21] | STARTADDRB3 | 显存起始地址的高 11 位(字节单位) |
| [20:0] | STARTADDRB2 | 显存起始地址的低 21 位(字节单位) |
注意:地址必须是 32 字节对齐 的。显存通常用 DMA 或 mmap 分配。
3.7 LCDSADDR2 - 显存起始地址(下半部分)
// 地址:0x4D000018
| Bit | 字段 | 说明 |
|---|---|---|
| [19:0] | ENDADDRB1 | 显存结束地址的低 20 位(字节单位) |
显存大小 = (ENDADDRB1 - STARTADDR) / 2(因为是 16bpp,每个像素 2 字节)。
四、 PCLK 时钟计算公式(最头疼的部分)
S3C2440 的 PCLK 由 MCLK(主时钟)经过分频得到。
4.1 时钟树简图
MCLK (100MHz)
|
v
UCLK (由 LCDCON1 的 CLKVAL 决定)
|
v
PCLK (由 UCLK 和 LCD 控制器内部逻辑决定)
4.2 计算公式
PCLK = MCLK / (CLKVAL + 1) / (TCON 分频系数)
但 S3C2440 的 TCON 分频系数是固定的,取决于 LCDCON1 的 TCON 字段(bit [15:16])。
更实用的方法是反向推导:
- 假设你需要 PCLK = 9MHz(常见于 240x320@60Hz)
- MCLK = 100MHz(常见值)
- 公式:
CLKVAL = (MCLK / PCLK) - 1
但注意,S3C2440 的 CLKVAL 还受到 VCLKEN 和 TCON 分频的影响。
官方推荐公式(来自《S3C2440A Manual》第 12 章):
PCLK = MCLK / (CLKVAL + 1)
前提是:LCDCON1 的 TCON 字段为 0b00(无分频)。
如果 TCON 为其他值,则需要额外分频。
4.3 实战示例
假设:
- MCLK = 100MHz
- 屏幕分辨率:240x320
- 刷新率:60Hz
- 时序参数:
- H-Sync: 1
- V-Sync: 1
- HBP: 2
- HFP: 2
- VBP: 2
- VFP: 1
计算总像素时钟数:
Horizontal total = 240 + 1 + 2 + 2 = 245
Vertical total = 320 + 1 + 2 + 1 = 324
Total pixels per frame = 245 * 324 = 79,380
计算所需 PCLK:
PCLK = Total pixels per frame * Refresh rate
= 79,380 * 60
= 4,762,800 Hz ≈ 4.76 MHz
计算 CLKVAL:
CLKVAL = (MCLK / PCLK) - 1
= (100,000,000 / 4,762,800) - 1
≈ 20.99 - 1
≈ 19
所以,LCDCON1 的 CLKVAL 应该设为 19(0x13)。
五、 代码实战:从零点亮屏幕
以下代码基于 Linux 字符设备驱动框架,你可以直接嵌入到你的项目中。
5.1 头文件和数据定义
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/dma-mapping.h>
#include <linux/fb.h>
#include <asm/uaccess.h>
#include <mach/regs-lcd.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>
#define LCD_NAME "s3c2440_lcd"
#define LCD_MEM_SIZE (240 * 320 * 2) // 240x320, 16bpp
// 屏幕时序参数(根据你的屏幕手册修改!)
#define H_Sync_Pulse_Width 1
#define V_Sync_Pulse_Width 1
#define H_BP 2
#define V_BP 2
#define H_FP 2
#define V_FP 1
#define RES_X 240
#define RES_Y 320
// 极性配置(根据你的屏幕手册修改!)
#define INV_VCLK 0 // 下降沿有效
#define INV_VLINE 1 // HSYNC 高电平有效
#define INV_VFRAME 1 // VSYNC 高电平有效
#define INV_VDEN 0 // DEN 高电平有效
5.2 驱动结构体
struct s3c2440_lcd_driver {
void __iomem *lcd_base;
void __iomem *gpio_base;
dma_addr_t dma_addr;
unsigned char *vidmem;
struct platform_device *pdev;
struct fb_info *fbi;
};
static struct s3c2440_lcd_driver *g_lcd_dev;
5.3 时钟分频值计算函数
// 计算 CLKVAL,确保 PCLK 接近目标值
static unsigned int calc_clkval(unsigned int mclk, unsigned int target_pclk)
{
return (mclk / target_pclk) - 1;
}
// 计算 PCLK 的实际频率
static unsigned int calc_pclk(unsigned int mclk, unsigned int clkval)
{
return mclk / (clkval + 1);
}
5.4 初始化 LCD 寄存器
”`c static int lcd_init_registers(void) {
unsigned long lcdcon1, lcdcon2, lcdcon3, lcdcon4, lcdcon5;
unsigned int mclk = 100000000; // 100MHz,根据你的实际时钟修改
unsigned int pclk = 4762800; // 计算出的目标 PCLK
// 1. 计算 CLKVAL
unsigned int clkval = calc_clkval(mclk, pclk);
// 2. 配置 LCDCON1