篇8:从零开始------FPGA如何"点亮"第一块LCD
前七篇我们讲了液晶原理、TFT阵列、制造工艺、接口协议、DDIC内部结构......理论堆了不少。现在终于到了动手的时刻。
这一篇,我们真的用FPGA点亮一块LCD。你会学到:怎么读懂规格书的时序图,怎么用PLL产生精确的像素时钟,怎么用Verilog写出一个最小可行的RGB接口控制器。最后,屏幕上会出现彩条------这是你从数字世界跨入显示世界的第一步。
一、准备工作:选一块"友好"的屏
如果你是第一次做FPGA驱动LCD,强烈建议选 RGB接口 、 不带GRAM 、不需要复杂初始化的屏。这类屏通常被称为"傻瓜屏"或"直驱屏"。
推荐型号举例 :
- 4.3寸 480×272 (如AT043TN24)
- 5寸 800×480 (如KD50G21-40NB-A)
- 7寸 800×480 (如AT070TN92)
这些屏的共性是:上电后只要有时钟和同步信号,就会显示数据线上的内容。不需要通过SPI/I2C写寄存器。
硬件连接 :
- FPGA开发板(带足够多的IO,3.3V电平)
- LCD屏(带FPC排线,需要转接板)
- 逻辑分析仪或示波器(强烈建议)
- 背光电源(通常需要单独供电,3.3V或5V,几十mA)
二、读懂规格书的时序表
拿到一块屏的规格书,翻到"Timing Characteristics"章节。你会看到类似这样的表格:
| 参数 | 符号 | 最小值 | 典型值 | 最大值 | 单位 |
|---|---|---|---|---|---|
| 像素时钟 | PCLK | - | 33.26 | - | MHz |
| 水平有效像素 | thd | - | 800 | - | PCLK |
| 水平前沿 | thf | 8 | 40 | - | PCLK |
| 水平后沿 | thb | 8 | 216 | - | PCLK |
| 水平同步宽度 | thpw | 1 | 32 | - | PCLK |
| 垂直有效行 | tvd | - | 480 | - | 行 |
| 垂直前沿 | tvf | 2 | 10 | - | 行 |
| 垂直后沿 | tvb | 2 | 23 | - | 行 |
| 垂直同步宽度 | tvpw | 1 | 12 | - | 行 |
关键理解 :
- 一行总周期数 = thd + thf + thb + thpw = 800 + 40 + 216 + 32 = 1088(此例)
- 一帧总行数 = tvd + tvf + tvb + tvpw = 480 + 10 + 23 + 12 = 525
- 像素时钟 PCLK = 总像素 × 帧率 = 1088 × 525 × 60 ≈ 34.3 MHz(接近典型值33.26MHz,可能取整差异)
注意 :不同规格书的命名可能不同:
- HFP = thf, HBP = thb, HSYNC width = thpw
- VFP = tvf, VBP = tvb, VSYNC width = tvpw
- DE模式有时不单独列出HFP/HBP,而是给出"总周期"和"有效周期"。
三、时钟是命脉:用PLL生成PCLK
FPGA内部没有高精度时钟源,需要用PLL(锁相环) 从板载晶振(如50MHz)生成所需的PCLK。
3.1 计算PLL参数
假设目标PCLK = 33.26MHz,晶振 = 50MHz。分频比 = 50 / 33.26 ≈ 1.503。不一定能精确得到,取最接近的整数分频和倍频组合。
在Vivado中打开Clock Wizard:
- 输入时钟 50MHz
- 输出时钟频率 33.26MHz
- 允许±5%误差(屏通常能容忍1~2%的频率偏差)
- 生成后查看实际频率,比如可能得到33.333MHz(误差0.2%),完全可以接受。
重要 :PLL输出时钟要连接到 专用时钟引脚 (如MMCM/PLL的输出),不能随便从普通IO走线,否则抖动大,屏幕可能闪烁。
3.2 生成锁定指示
PLL的locked信号需要等待稳定(通常几微秒到几毫秒)。在locked为高之前,不要输出PCLK到屏,否则不稳定的时钟会损坏屏或导致异常。
verilog
// PLL模块例化(Vivado IP)
clk_wiz_0 u_pll (
.clk_in1(clk_50m),
.clk_out1(pclk), // 33.33MHz
.locked(plk_locked)
);
四、最小RGB控制器设计
我们需要生成三个核心信号: HSYNC, VSYNC, DE ,以及输出RGB数据。对于"傻瓜屏",通常DE模式最简单:只要DE为高时,每个PCLK上升沿送出一个像素数据即可。
4.1 模块划分
text
┌─────────────────────────────────────────┐
│ RGB Controller │
├─────────────┬───────────────────────────┤
│ H计数器 │ → HSYNC, H_DE, 行结束脉冲 │
│ V计数器 │ → VSYNC, V_DE, 帧结束脉冲 │
│ 组合逻辑 │ → DE = H_DE & V_DE │
│ 数据生成器 │ → RGB[23:0] (彩条/测试图) │
└─────────────┴───────────────────────────┘
4.2 水平计数器
水平计数器以PCLK为时钟,计数范围0 ~ H_total-1。
定义参数:
verilog
parameter H_SYNC = 32; // HSYNC脉冲宽度
parameter H_BACK = 216; // 后沿
parameter H_ACT = 800; // 有效像素
parameter H_FRONT = 40; // 前沿
parameter H_TOTAL = H_SYNC + H_BACK + H_ACT + H_FRONT; // 1088
生成HSYNC和H_DE:
- HSYNC:计数器在 0 ~ H_SYNC-1 期间为低(或高,看规格书,通常低有效)
- H_DE:计数器在 H_SYNC+H_BACK ~ H_SYNC+H_BACK+H_ACT-1 期间为高
verilog
always @(posedge pclk) begin
if (hcnt < H_SYNC)
hsync <= 1'b0; // 低有效
else
hsync <= 1'b1;
h_de <= (hcnt >= H_SYNC + H_BACK) && (hcnt < H_SYNC + H_BACK + H_ACT);
if (hcnt == H_TOTAL - 1)
hcnt <= 0;
else
hcnt <= hcnt + 1;
end
4.3 垂直计数器
垂直计数器以 行结束脉冲 (即hcnt达到H_TOTAL-1的时刻)为时钟。计数范围0 ~ V_TOTAL-1。
参数:
verilog
parameter V_SYNC = 12; // VSYNC宽度(行数)
parameter V_BACK = 23; // 后沿
parameter V_ACT = 480; // 有效行数
parameter V_FRONT = 10; // 前沿
parameter V_TOTAL = V_SYNC + V_BACK + V_ACT + V_FRONT; // 525
生成VSYNC和V_DE:
- VSYNC:计数器在0~V_SYNC-1期间为低
- V_DE:计数器在V_SYNC+V_BACK ~ V_SYNC+V_BACK+V_ACT-1期间为高
verilog
always @(posedge pclk) begin
if (hcnt == H_TOTAL - 1) begin // 一行结束
if (vcnt == V_TOTAL - 1)
vcnt <= 0;
else
vcnt <= vcnt + 1;
end
end
always @(posedge pclk) begin
if (vcnt < V_SYNC)
vsync <= 1'b0;
else
vsync <= 1'b1;
v_de <= (vcnt >= V_SYNC + V_BACK) && (vcnt < V_SYNC + V_BACK + V_ACT);
end
4.4 DE信号
verilog
wire de = h_de & v_de;
当DE为高时,每个PCLK上升沿,RGB数据线上必须是有效的像素数据。
4.5 产生测试图案(彩条)
最简单的测试图案:水平方向8条彩条(白、黄、青、绿、紫、红、蓝、黑)。
每条宽度 = H_ACT / 8 = 100像素(800/8)。用一个计数器在DE期间计数像素位置,选择颜色。
verilog
reg [9:0] x_cnt; // 水平像素坐标(0~799)
reg [23:0] rgb_data;
always @(posedge pclk) begin
if (de) begin
if (x_cnt == H_ACT - 1)
x_cnt <= 0;
else
x_cnt <= x_cnt + 1;
end else begin
x_cnt <= 0;
end
end
always @(*) begin
case (x_cnt / 100) // 整除,得到0~7
0: rgb_data = 24'hFFFFFF; // 白
1: rgb_data = 24'hFFFF00; // 黄
2: rgb_data = 24'h00FFFF; // 青
3: rgb_data = 24'h00FF00; // 绿
4: rgb_data = 24'hFF00FF; // 紫
5: rgb_data = 24'hFF0000; // 红
6: rgb_data = 24'h0000FF; // 蓝
7: rgb_data = 24'h000000; // 黑
default: rgb_data = 24'h000000;
endcase
end
五、完整代码框架
将上述模块整合成一个顶层模块:
verilog
module lcd_rgb_controller (
input wire clk_50m, // 板载50MHz
input wire rst_n, // 复位
output wire pclk, // 像素时钟(输出到屏)
output wire hsync,
output wire vsync,
output wire de,
output wire [23:0] rgb,
output wire lcd_rst, // 屏硬件复位(高有效或低有效?看规格)
output wire lcd_bl_en // 背光使能
);
// PLL产生33.33MHz pclk
wire pll_locked;
clk_wiz_0 u_pll (
.clk_in1(clk_50m),
.clk_out1(pclk),
.locked(pll_locked)
);
// 复位信号:上电后等待PLL锁定,再释放内部复位
wire internal_rst = ~(pll_locked & rst_n);
// 时序参数定义
localparam H_SYNC = 32;
localparam H_BACK = 216;
localparam H_ACT = 800;
localparam H_FRONT = 40;
localparam H_TOTAL = H_SYNC + H_BACK + H_ACT + H_FRONT; // 1088
localparam V_SYNC = 12;
localparam V_BACK = 23;
localparam V_ACT = 480;
localparam V_FRONT = 10;
localparam V_TOTAL = V_SYNC + V_BACK + V_ACT + V_FRONT; // 525
// 计数器和信号
reg [11:0] hcnt; // 0~1087
reg [10:0] vcnt; // 0~524
reg hsync_reg, vsync_reg, de_reg;
reg [9:0] x_cnt;
reg [23:0] rgb_reg;
// 水平计数
always @(posedge pclk or posedge internal_rst) begin
if (internal_rst) begin
hcnt <= 0;
hsync_reg <= 1'b1;
end else begin
hsync_reg <= (hcnt < H_SYNC) ? 1'b0 : 1'b1;
if (hcnt == H_TOTAL - 1)
hcnt <= 0;
else
hcnt <= hcnt + 1;
end
end
// 垂直计数
wire h_end = (hcnt == H_TOTAL - 1);
always @(posedge pclk or posedge internal_rst) begin
if (internal_rst) begin
vcnt <= 0;
vsync_reg <= 1'b1;
end else begin
vsync_reg <= (vcnt < V_SYNC) ? 1'b0 : 1'b1;
if (h_end) begin
if (vcnt == V_TOTAL - 1)
vcnt <= 0;
else
vcnt <= vcnt + 1;
end
end
end
// DE信号
wire h_active = (hcnt >= H_SYNC + H_BACK) && (hcnt < H_SYNC + H_BACK + H_ACT);
wire v_active = (vcnt >= V_SYNC + V_BACK) && (vcnt < V_SYNC + V_BACK + V_ACT);
assign de = h_active & v_active;
// 像素坐标和彩条数据
always @(posedge pclk or posedge internal_rst) begin
if (internal_rst) begin
x_cnt <= 0;
rgb_reg <= 0;
end else begin
if (de) begin
if (x_cnt == H_ACT - 1)
x_cnt <= 0;
else
x_cnt <= x_cnt + 1;
end else begin
x_cnt <= 0;
end
// 彩条生成
case (x_cnt / (H_ACT/8))
0: rgb_reg <= 24'hFFFFFF;
1: rgb_reg <= 24'hFFFF00;
2: rgb_reg <= 24'h00FFFF;
3: rgb_reg <= 24'h00FF00;
4: rgb_reg <= 24'hFF00FF;
5: rgb_reg <= 24'hFF0000;
6: rgb_reg <= 24'h0000FF;
7: rgb_reg <= 24'h000000;
default: rgb_reg <= 24'h000000;
endcase
end
end
// 输出赋值
assign hsync = hsync_reg;
assign vsync = vsync_reg;
assign rgb = rgb_reg;
// 背光和复位(简单处理,上电后使能)
assign lcd_rst = 1'b1; // 假设高有效复位,常高表示不复位
assign lcd_bl_en = pll_locked; // PLL锁定后打开背光
endmodule
六、约束文件与上板调试
6.1 引脚分配(XDC示例)
根据你的硬件连接,将顶层端口映射到FPGA引脚。注意电平标准:RGB接口通常是3.3V LVCMOS。
tcl
set_property PACKAGE_PIN R4 [get_ports pclk]
set_property IOSTANDARD LVCMOS33 [get_ports pclk]
set_property PACKAGE_PIN T5 [get_ports hsync]
set_property IOSTANDARD LVCMOS33 [get_ports hsync]
# ... 其他引脚
6.2 上电顺序
- 先给FPGA和屏的数字电源(3.3V)上电
- 等待PLL锁定(内部逻辑自动延迟几微秒)
- 输出稳定的PCLK和同步信号
- 开启背光(lcd_bl_en拉高)
⚠️ 千万不要在FPGA未配置完成时就给屏供PCLK。未配置完成的FPGA引脚可能处于不定状态,输出毛刺会损坏屏。解决方法是:在PLL的locked信号有效之前,将所有输出引脚置为高阻或固定电平。
6.3 第一次上电看不到图像怎么办?
按照以下顺序排查:
| 现象 | 检查方法 |
|---|---|
| 背光不亮 | 测量背光供电和使能引脚是否正确 |
| 屏幕全黑(背光亮) | 检查偏光片是否贴反?常白屏无信号时应为白屏,如果全黑说明偏光片问题或Vcom异常 |
| 屏幕全白 | 可能是HSYNC/VSYNC极性反了(规格书是低有效,你给了高有效) |
| 屏幕有杂乱雪花 | PCLK频率不对或时序参数偏差太大 |
| 显示彩条但位置偏移 | HBP/HFP参数与规格书不符 |
| 显示几行后重复 | VSYNC周期或脉冲宽度不对 |
调试利器 :用示波器测量HSYNC和VSYNC的频率。HSYNC频率 = PCLK / H_total。VSYNC频率 = HSYNC频率 / V_total。如果VSYNC是60Hz左右,说明时序基本正确。
七、从彩条到动态显示
一旦彩条能稳定显示,你就打通了FPGA到LCD的物理链路。下一步可以:
- 用BRAM存储一幅小图片,从BRAM中读取RGB数据代替彩条
- 实现简单的滚动文字
- 连接外部摄像头或DDR,实现动态帧缓冲(篇9预告)
八、☕ 工程师私房话
为什么我的屏需要初始化序列,而这里的代码没有?
如果你选的屏是"智能屏"(如ILI9341、ST7789),上电后必须先通过SPI或I2C写寄存器初始化(设置伽马、反转模式、扫描方向等)。这类屏不能直接用本篇的代码点亮。建议初次调试时避开它们,或者先找一份别人验证过的初始化序列。
像素时钟频率可以降低吗?
可以,但降太多会导致刷新率降低,人眼会看到闪烁。一般不低于50Hz。如果你的屏允许,可以降到30MHz,PCLK=30M,那么刷新率 = 30M / (H_total × V_total) ≈ 30M/570k ≈ 52.6Hz,勉强可用。
一个冷知识:为什么RGB接口的DE模式比HV模式更受欢迎?
因为DE信号包含了有效区域的信息,HSYNC和VSYNC可以只作为"辅助"甚至省略。很多主控芯片(如某些MCU的LCD控制器)只输出DE和PCLK,不输出HSYNC/VSYNC。屏端的TCON会从DE恢复出同步信号。这种单线同步方式简化了硬件连接。