第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)的实现有差异:


四个模块分工:
- rd_id / clk_div / lcd_driver:与彩条实验完全一致,直接拿来用;
- lcd_display:字符通过取模软件得到(存在 Verilog 数组里),图片存在 ROM IP 中;根据驱动模块传来的像素坐标,输出对应的 pixel_data。
字符图片的取模
字符的本质是点阵:字符显示区域内像素点的集合。字符大小决定点数,样式(字体、颜色)决定各像素颜色。提取字模("取模")的规则:每个像素点用 1bit 表示,字符笔画处为 1,背景为 0。这样描述的点阵不含颜色信息,颜色由代码在显示时再"上色"。

实操流程用取模软件 PCtoLCD2002(在开发板资料 A 盘"6_常用软件"目录下):
- 打开软件后字体/字宽/字高不可设置,先在菜单"模式"中切换到字符模式:


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

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


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


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

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


图片的取模走另一条路:图片像素数据要存进 ROM IP(FPGA 片上 BRAM),格式必须是 Xilinx 能识别的 COE 文件。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":


创建ROM IP
RAM 和 ROM 都由 BMG IP 核(Block Memory Generator) 配置生成:
- Basic 选项卡选 Single Port ROM:

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

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

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

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

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

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

核心代码:
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。


一个仿真现象要提前说明: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

波形显示:lcd_de 拉高范围内,图片和字符区域之外均为背景色 24'he0ffff,符合设计。
30.5 下载验证
引脚约束与"RGB LCD 彩条显示实验"完全相同,直接复用该工程的 XDC。
上板步骤:FPC 排线连接 LCD 模块 J1 接口与开发板 RGB LCD 接口;JTAG 下载器连电脑与开发板;上电下载程序。屏幕左上角显示 Logo 图片、下方显示红色"正点原子"字样、背景为浅蓝色,即验证成功:

初学者容易踩的坑
- 取模顺序别搞反:代码里是
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 参数即可),加深对坐标映射的理解。