LCD显示技术完全指南:原理·制造·驱动·FPGA实现之点屏一

篇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 上电顺序

  1. 先给FPGA和屏的数字电源(3.3V)上电
  2. 等待PLL锁定(内部逻辑自动延迟几微秒)
  3. 输出稳定的PCLK和同步信号
  4. 开启背光(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恢复出同步信号。这种单线同步方式简化了硬件连接。


相关推荐
風清掦17 小时前
【STM32学习笔记-14】WDG看门狗 - 14.2 WWDG窗口看门狗
笔记·stm32·单片机·嵌入式硬件·学习·fpga开发
尤老师FPGA20 小时前
HDMI数据的接收发送实验(十二)
fpga开发
坏孩子的诺亚方舟1 天前
FPGA神经网络数学基础0
人工智能·神经网络·线性代数·fpga开发
熠速1 天前
PolarBox高性能实时仿真系统
arm开发·fpga开发·嵌入式实时数据库·硬件在环半实物仿真
南檐巷上学1 天前
基于Zynq-7020的带有正弦波发生器的8051软核设计
单片机·嵌入式硬件·fpga开发·fpga
思尔芯S2C1 天前
FPGA原型验证中的内存模型应用:基于DDR5的Linux系统启动与测试
fpga开发·内存模型·ddr4·ddr5·memory model·hbm3·prototyping
hai3152475431 天前
RISC-V CVA6 AXI适配器+DMA桥蜂鸟E203处理器的总线接口单元(BIU)仲裁器
驱动开发·fpga开发·硬件架构·硬件工程·精益工程
高速上的乌龟2 天前
Lattice LFCPNX-100 HSB+Fpga开发详解:2.3 Hololink 顶层模块深度全解析
linux·fpga开发
ALINX技术博客2 天前
【FPGA 开发教程】基于 ALINX FPGA 开发板实现 USB3.2 高速通信(Z7-P+FL2010)
fpga开发·fpga·fmc子卡·usb3.2通信