第30章:RGB LCD字符和图片显示实验

本章对应原书第 719~739 页,属于「第五篇 实战篇」。

本章在讲什么?(先看这个)

上一章我们能在屏幕上画彩条了,但真正的界面要显示文字和图片。这一章解决两个问题:① 字符怎么显示——答案是"取模",把汉字变成 0/1 点阵数组;② 图片怎么显示——把图片像素数据转成 COE 文件,预先存进 FPGA 的 ROM IP 核,扫描时按地址读出来。

实验任务:在 LCD 屏左上角显示正点原子 Logo 图片(100×100)和"正点原子"四个汉字(每个 32×32)。工程的读 ID、时钟分频、LCD 驱动模块全部沿用彩条实验,只改 LCD 显示模块——这正是模块化设计的便利之处。

核心概念拆解

30.1 简介

本章基于"RGB LCD 彩条显示实验",LCD 屏的时序、同步方式、分辨率等概念完全相同,不再赘述;核心增量是字符点阵ROM 存图两个技术点。

30.2 硬件设计

硬件原理与管脚分配与彩条实验完全相同(RGB888 双向数据线 + DE/HS/VS/BL/CLK/RST 控制线),直接复用即可。

30.3 实验任务

通过 RGB LCD 接口,在 LCD 屏左上角显示正点原子 Logo 图片及"正点原子"4 个汉字。每个汉字大小 32×32,图片大小 100×100。

30.4 程序设计

整体模块设计

系统架构与彩条实验基本一样,只有 LCD 显示模块(lcd_display)的实现有差异:

图 30.4.1 系统架构图

图 30.4.2 RGB LCD 字符和图片显示实验系统框图

四个模块分工:

  • rd_id / clk_div / lcd_driver:与彩条实验完全一致,直接拿来用;
  • lcd_display:字符通过取模软件得到(存在 Verilog 数组里),图片存在 ROM IP 中;根据驱动模块传来的像素坐标,输出对应的 pixel_data。

字符图片的取模

字符的本质是点阵:字符显示区域内像素点的集合。字符大小决定点数,样式(字体、颜色)决定各像素颜色。提取字模("取模")的规则:每个像素点用 1bit 表示,字符笔画处为 1,背景为 0。这样描述的点阵不含颜色信息,颜色由代码在显示时再"上色"。

图 30.4.3 汉字"正"及其点阵描述

实操流程用取模软件 PCtoLCD2002(在开发板资料 A 盘"6_常用软件"目录下):

  1. 打开软件后字体/字宽/字高不可设置,先在菜单"模式"中切换到字符模式

图 30.4.4 取模软件PCtoLCD2002

图 30.4.5 切换到字符模式

  1. 字体选"宋体",字宽、字高设为 32,输入"正点原子":

图 30.4.6 字符设置

  1. 关键技巧:软件默认给每个字生成独立字模,但我们要把四个字当一个整体(形成 128×32 的"大字模"),便于一次性显示。做法:先把点阵"另存为" BMP 图片(存到工程 doc 文件夹):

图 30.4.7 点击"文件"并另存为图像

图 30.4.8 BMP 格式图片保存界面

  1. 用"画图"打开确认效果,然后把软件切到图形模式,重新打开这张 BMP:

图 30.4.10 切换至图形模式

图 30.4.11 PCtoLCD2002 图形模式

  1. 设置字模格式("选项"菜单)。注意"每行显示数据"以字节为单位,1 字节 = 8 个像素点;点阵每行 128 个像素,需要 16 字节,所以设置为 16:

图 30.4.12 字模格式配置界面

  1. "生成字模"并保存为 txt。数据为十六进制,每行 16 Byte(对应 128 个像素),共 32 行(对应 32 像素高):

图 30.4.13 生成字模

图 30.4.14 "正点原子"字模

图片的取模走另一条路:图片像素数据要存进 ROM IP(FPGA 片上 BRAM),格式必须是 Xilinx 能识别的 COE 文件。COE 文件开头定义数据进制,然后是初始化数据向量,最后一行以 ";" 结尾、其余以 "," 分隔:

图 30.4.15 COE 文件格式

100×100 图片按 RGB888 存储的大小 = 100×100×24bit = 240000bit ≈ 0.23Mbit。XC7Z020 的 BRAM 容量 4.9Mbit、XC7Z010 为 2.1Mbit,都放得下。

用正点原子提供的 PicToLCD 工具转换:先用"画图"把 Logo 调整为 100×100(尺寸必须精确,否则 ROM 访问的最大地址要改),然后加载图片 → 输出格式选 RGB888 → 文件类型选 COE → 一键转换,得到 "ZDYZ.coe":

图 30.4.16 PicToLCD 转换界面

图 30.4.17 转换得到的COE 文件

创建ROM IP

RAM 和 ROM 都由 BMG IP 核(Block Memory Generator) 配置生成:

  1. Basic 选项卡选 Single Port ROM

图 30.4.18 "Single Port ROM"选项

  1. Port A Options 选项卡:像素数据是 RGB888,端口位宽设 24;COE 文件共 10000 个数据(100×100),端口深度设 10000;不使用流水线寄存器:

图 30.4.19 "Port A Options"选项卡

  1. Other Options 选项卡:加载刚生成的 .coe 文件:

图 30.4.20 加载".coe"文件

LCD显示模块设计

模块框图与彩条实验相同,只是内部逻辑换了"内容"——按坐标区域决定输出图片、字符还是背景色:

图 30.4.21 LCD 显示模块框图

显示区域的划分逻辑(波形图思路):

图 30.4.22 显示驱动模块波形图

  • 图片部分:显示在左上角,横坐标 1~100、纵坐标 1~100。ROM 共 10000 个数据,rom_addr 每行 0~99 递增,读到坐标 (100,100) 时完成一次整图刷新:

图 30.4.23 读取ROM 图片部分波形

  • 字符部分:显示在图片下方,大小 128×32,横坐标起始为 1、纵坐标起始为 110(可自定)。用两个计数器 x_cnt/y_cnt 表示像素点相对字符区域的偏移,有效范围 0~127 和 0~31:

图 30.4.24 显示字符部分波形图

核心代码:

module lcd_display(
    input             lcd_pclk,      //时钟
    input             rst_n,         //复位,低电平有效
    input      [10:0] pixel_xpos,    //像素点横坐标
    input      [10:0] pixel_ypos,    //像素点纵坐标
    output reg [23:0] pixel_data     //像素点数据
);

//parameter define
localparam PIC_X_START = 11'd10;     //图片起始点横坐标
localparam PIC_Y_START = 11'd10;     //图片起始点纵坐标
localparam PIC_WIDTH   = 11'd100;    //图片宽度
localparam PIC_HEIGHT  = 11'd100;    //图片高度

localparam CHAR_X_START= 11'd10;     //字符起始点横坐标
localparam CHAR_Y_START= 11'd120;    //字符起始点纵坐标
localparam CHAR_WIDTH  = 11'd128;    //字符宽度,4 个字符:32*4
localparam CHAR_HEIGHT = 11'd32;     //字符高度

localparam BACK_COLOR  = 24'hE0FFFF; //背景色,浅蓝色
localparam CHAR_COLOR  = 24'hff0000; //字符颜色,红色

//reg define
reg   [127:0] char[31:0];  //字符数组
reg   [13:0]  rom_addr  ;  //ROM 地址

//wire define
wire  [10:0]  x_cnt;       //横坐标计数器
wire  [10:0]  y_cnt;       //纵坐标计数器
wire          rom_rd_en ;  //ROM 读使能信号
wire  [23:0]  rom_rd_data ;//ROM 数据

//像素点相对于字符区域起始点坐标
assign  x_cnt = pixel_xpos + 11'd1 - CHAR_X_START;
assign  y_cnt = pixel_ypos - CHAR_Y_START;
assign  rom_rd_en = 1'b1;                  //读使能拉高,即一直读ROM 数据

//给字符数组赋值,显示汉字"正点原子",每个汉字大小为32*32
always @(posedge lcd_pclk) begin
    char[0 ]  <= 128'h00000000000000000000000000000000;
    char[1 ]  <= 128'h00000000000000000000000000000000;
    char[2 ]  <= 128'h00000000000100000000002000000000;
    char[3 ]  <= 128'h000000100001800002000070000000C0;
    char[4 ]  <= 128'h000000380001800003FFFFF803FFFFE0;
    char[5 ]  <= 128'h07FFFFFC0001800003006000000001E0;
    // ……(此处省略char[6]~char[30],为PCtoLCD2002生成的32行点阵数据)
    char[31]  <= 128'h00000000000000000000000000000000;
end

//为LCD 不同显示区域绘制图片、字符和背景色
always @(posedge lcd_pclk or negedge rst_n) begin
    if (!rst_n)
        pixel_data <= BACK_COLOR;
    else if((pixel_xpos>=PIC_X_START - 11'd1) && (pixel_xpos<PIC_X_START + PIC_WIDTH - 11'd1)
          && (pixel_ypos >= PIC_Y_START) && (pixel_ypos < PIC_Y_START + PIC_HEIGHT) )
        pixel_data <= rom_rd_data ;  //显示图片
    else if((pixel_xpos>=CHAR_X_START-11'd1) && (pixel_xpos<CHAR_X_START+CHAR_WIDTH-11'd1)
         && (pixel_ypos >= CHAR_Y_START) && (pixel_ypos < CHAR_Y_START + CHAR_HEIGHT)) begin
        if(char[y_cnt][CHAR_WIDTH - 11'd1 - x_cnt])
            pixel_data <= CHAR_COLOR;    //显示字符
        else
            pixel_data <= BACK_COLOR;    //显示字符区域的背景色
    end
    else
        pixel_data <= BACK_COLOR;        //屏幕背景色
end

//根据当前扫描点的横纵坐标为ROM 地址赋值
always @(posedge lcd_pclk or negedge rst_n) begin
    if(!rst_n)
        rom_addr <= 14'd0;
    //当横纵坐标位于图片显示区域时,累加ROM 地址
    else if((pixel_ypos >= PIC_Y_START) && (pixel_ypos < PIC_Y_START + PIC_HEIGHT)
        && (pixel_xpos >= PIC_X_START-11'd2) && (pixel_xpos < PIC_X_START + PIC_WIDTH-11'd2))
        rom_addr <= rom_addr + 14'd1;
    //当横纵坐标位于图片区域最后一个像素点时,ROM 地址清零
    else if((pixel_ypos >= PIC_Y_START + PIC_HEIGHT))
        rom_addr <= 14'd0;
end

//ROM:存储图片
blk_mem_gen_0  blk_mem_gen_0 (
  .clka  (lcd_pclk),    // input wire clka
  .ena   (rom_rd_en),   // input wire ena
  .addra (rom_addr),    // input wire [13 : 0] addra
  .douta (rom_rd_data)  // output wire [23 : 0] douta
);

endmodule

代码要点解读:

  • 字符显示char[y_cnt][CHAR_WIDTH-1-x_cnt] 从 128 位点阵行中取出当前像素对应的 bit——为 1 则显示字符颜色(红色),为 0 则显示背景色。这就是"点阵上色"。
  • 图片显示:只要坐标落入图片区域,直接输出 rom_rd_data;rom_addr 在图片区域内随扫描递增、扫出区域后清零,实现循环刷新。
  • 背景色:其余区域全部输出浅蓝色 24'hE0FFFF。

图 30.4.25 显示图片仿真图a

图 30.4.29 显示字符仿真图a

一个仿真现象要提前说明:ROM 有效地址是 0~9999,地址 10000 没有赋初值,仿真中会出现不定态,这是正常现象,不是 bug。

顶层模块

顶层 lcd_rgb_char 与彩条实验唯一的区别是显示模块换成了新版(少了 h_disp/v_disp 连接),双向引脚方向切换逻辑一模一样:

module  lcd_rgb_char(
    input              sys_clk ,
    input              sys_rst_n ,
    //RGB LCD 接口
    output             lcd_hs  ,       //LCD 行同步信号
    output             lcd_vs  ,       //LCD 场同步信号
    output             lcd_de  ,       //LCD 数据输入使能
    inout      [23:0]  lcd_rgb ,       //LCD RGB888 颜色数据
    output             lcd_bl  ,       //LCD 背光控制信号
    output             lcd_rst,        //LCD 复位
    output             lcd_clk         //LCD 采样时钟
);

//wire define
wire  [15:0]  lcd_id    ;
wire          lcd_pclk  ;
wire  [10:0]  pixel_xpos;
wire  [10:0]  pixel_ypos;
wire  [23:0]  pixel_data;
wire  [23:0]  lcd_rgb_o ;
wire  [23:0]  lcd_rgb_i ;

//像素数据方向切换
assign lcd_rgb = lcd_de ?  lcd_rgb_o :  {24{1'bz}};
assign lcd_rgb_i = lcd_rgb;

//读LCD ID 模块
rd_id u_rd_id(
    .clk          (sys_clk  ),
    .rst_n        (sys_rst_n),
    .lcd_rgb      (lcd_rgb_i),
    .lcd_id       (lcd_id   )
    );

//时钟分频模块
clk_div u_clk_div(
    .clk           (sys_clk  ),
    .rst_n         (sys_rst_n),
    .lcd_id        (lcd_id   ),
    .lcd_pclk      (lcd_pclk )
    );

//LCD 显示模块
lcd_display u_lcd_display(
    .lcd_pclk       (lcd_pclk  ),
    .rst_n          (sys_rst_n ),
    .pixel_xpos     (pixel_xpos),
    .pixel_ypos     (pixel_ypos),
    .pixel_data     (pixel_data)
    );

//LCD 驱动模块
lcd_driver u_lcd_driver(
    .lcd_pclk      (lcd_pclk  ),
    .rst_n         (sys_rst_n ),
    .lcd_id        (lcd_id    ),
    .pixel_data    (pixel_data),
    .pixel_xpos    (pixel_xpos),
    .pixel_ypos    (pixel_ypos),
    .lcd_de        (lcd_de    ),
    .lcd_hs        (lcd_hs    ),
    .lcd_vs        (lcd_vs    ),
    .lcd_bl        (lcd_bl    ),
    .lcd_clk       (lcd_clk   ),
    .lcd_rst       (lcd_rst   ),
    .lcd_rgb       (lcd_rgb_o )
    );

endmodule

仿真验证

TB 与彩条实验相同,用 4.3 寸 800×480 屏,lcd_de 为低时给 lcd_rgb 赋 24'h80 提供屏幕 ID 激励:

`timescale 1ns / 1ps

module tb_lcd_rgb_char();

reg     sys_clk;
reg     sys_rst_n;

wire          lcd_de ;
wire          lcd_hs ;
wire          lcd_vs ;
wire          lcd_bl ;
wire          lcd_clk;
wire  [23:0]  lcd_rgb;

always #10 sys_clk = ~sys_clk;
assign lcd_rgb = lcd_de ?  {24{1'bz}} :  24'h80;

initial begin
    sys_clk = 1'b0;
    sys_rst_n = 1'b0;
    #200
    sys_rst_n = 1'b1;
end

lcd_rgb_char u_lcd_rgb_char(
    .sys_clk          (sys_clk  ),
    .sys_rst_n        (sys_rst_n),
    .lcd_de           (lcd_de ),
    .lcd_hs           (lcd_hs ),
    .lcd_vs           (lcd_vs ),
    .lcd_bl           (lcd_bl ),
    .lcd_clk          (lcd_clk),
    .lcd_rgb          (lcd_rgb)
    );

endmodule

图 30.4.33 RGB LCD 字符和图片显示仿真波形

波形显示:lcd_de 拉高范围内,图片和字符区域之外均为背景色 24'he0ffff,符合设计。

30.5 下载验证

引脚约束与"RGB LCD 彩条显示实验"完全相同,直接复用该工程的 XDC。

上板步骤:FPC 排线连接 LCD 模块 J1 接口与开发板 RGB LCD 接口;JTAG 下载器连电脑与开发板;上电下载程序。屏幕左上角显示 Logo 图片、下方显示红色"正点原子"字样、背景为浅蓝色,即验证成功:

图 30.5.1 RGB LCD 字符和图片显示

初学者容易踩的坑

  • 取模顺序别搞反:代码里是 char[y_cnt][CHAR_WIDTH-1-x_cnt],高位在前(行内第 0 个像素对应 bit127),字模配置界面里的"取模方式"选项如果和代码不一致,屏幕上会出现镜像/乱码字符。
  • "大字模"技巧:逐字取模会得到 4 个独立 32×32 字模;要一行显示四个字,先把点阵存成 BMP 再用图形模式整体取模(128 宽),省去拼接麻烦。
  • 每行显示数据的单位是字节:128 像素宽 ÷ 8 = 16 字节/行,配置成 16;配错会导致点阵错位。
  • 图片尺寸必须严格 100×100:ROM 深度按 10000 配置,图片尺寸变了 ROM 地址计算就会越界或留白。
  • 仿真中 ROM 地址 10000 出现不定态是正常的:该地址未初始化,不是设计错误,上板无影响。
  • BRAM 容量有限:100×100 RGB888 约 0.23Mbit 没问题,但想放大图就要压缩格式或外挂存储,别盲目把大图塞进 ROM。

小结 & 下一步

本章掌握了屏幕显示"内容"的两条数据通路:字符走取模 → Verilog 数组 → 按坐标查 bit 上色;图片走 PicToLCD 转 COE → ROM IP → 按扫描地址读数据。加上彩条实验的时序框架,你就具备了在 LCD 上绘制任意静态界面的能力。

动手建议:完成拓展训练——用取模软件生成自己喜欢的字符或图片,修改工程把它显示在屏幕中间位置(调整 PIC/CHAR 的 X_START、Y_START 参数即可),加深对坐标映射的理解。