简介:本项目基于FPGA平台,使用Verilog硬件描述语言实现对128×64分辨率LCD显示器的驱动控制,不依赖预置字符库,支持自定义图形与文本显示。通过手动控制每个像素,项目提升了显示内容的灵活性和可定制性。核心文件包括LCD控制模块(lcd.v)及用于存储显示数据的页数据文件(pagedata1.txt、pagedata2.txt),涵盖时序控制、数据传输、像素缓冲和命令生成等关键逻辑。该项目适用于学习FPGA开发流程、Verilog设计方法以及嵌入式显示技术的实际应用。
1. FPGA基本原理与开发流程
1.1 FPGA内部架构与核心组件
FPGA由可配置逻辑块(CLB)、输入输出块(IOB)、片上存储器(Block RAM)和丰富的布线资源构成,其并行架构支持硬件级并发执行。每个CLB包含多个查找表(LUT)、触发器和多路复用器,可用于实现组合或时序逻辑。IOB支持多种电平标准与通信协议,适配不同外设接口需求。
// 示例:一个简单的CLB功能建模(2输入逻辑门)
assign out = a & b; // LUT实现AND逻辑
布线资源连接各模块,虽带来延迟但保证灵活性;Block RAM常用于存储显示数据或指令缓存,如12864 LCD所需的图形缓冲区。
1.2 基于HDL的FPGA开发流程
FPGA设计遵循“需求分析 → 建模 → 综合 → 实现 → 下载验证”的闭环流程。首先使用Verilog/VHDL进行行为建模,随后通过综合工具(如Xilinx Vivado)将代码映射为网表,再经布局布线生成比特流文件。
开发工具链(以Vivado为例)提供集成环境支持仿真、约束设置与时序分析。关键步骤包括:
1. 创建工程并定义顶层模块;
2. 编写Verilog代码与测试平台;
3. 添加引脚约束(XDC文件);
4. 综合、实现并生成比特流;
5. 下载至FPGA进行硬件验证。
该流程确保从逻辑设计到物理实现的精确映射,为后续LCD控制器开发奠定基础。
1.3 无处理器的纯逻辑显示控制架构
在不依赖嵌入式核(如MicroBlaze)的前提下,FPGA可通过纯组合与时序逻辑构建完整的12864 LCD控制器。系统采用状态机驱动初始化序列,配合定时计数器满足ST7920的严格时序要求。
控制器模块需生成RS、RW、E等控制信号,并按指令时序发送命令与数据。例如,清屏操作需在E上升沿前稳定保持数据总线至少450ns(依据ST7920手册)。通过有限状态机(FSM)调度各阶段,结合延时计数器实现精准节拍控制。
此类设计充分发挥FPGA并行优势,避免CPU轮询开销,实现低延迟、高可靠性的独立显示系统。
2. Verilog硬件描述语言应用
Verilog作为电子设计自动化(EDA)领域中最广泛使用的硬件描述语言之一,其核心价值在于能够以行为级、寄存器传输级(RTL)乃至门级的方式精确建模数字电路。在FPGA开发中,Verilog不仅用于实现基本逻辑功能,更是构建复杂状态机、控制时序和模块化系统的关键工具。本章将深入探讨Verilog在实际项目中的关键应用技术,涵盖从基础语法到高级建模方法的完整知识链条,尤其聚焦于如何利用该语言高效实现组合与时序逻辑、有限状态机设计以及可验证性强的测试平台编写。
通过典型实例——如12864 LCD控制器的设计背景——展示Verilog如何支撑从抽象逻辑构思到物理实现的全过程。重点分析语法特性背后的硬件映射机制,使开发者不仅能“写出代码”,更能理解“代码生成了什么电路”。这为后续章节中具体外设驱动开发提供了坚实的语言基础与工程思维框架。
2.1 Verilog基础语法与建模方式
Verilog的语法结构遵循模块化设计原则,允许工程师以自顶向下或自底向上的方式组织大型数字系统。掌握其基本语法是进入FPGA开发的第一步,而真正难点在于理解不同语句所对应的硬件资源分配与综合结果。以下从模块定义、数据类型、赋值方式三个维度展开详述,并结合流程图与代码实例说明其在实际设计中的意义。
2.1.1 模块定义与端口声明
Verilog程序的基本单位是模块( module ),每个模块代表一个独立的功能单元,可以包含内部信号、子模块实例化以及逻辑描述。模块之间通过端口进行通信,形成层次化结构。
module lcd_controller (
input clk,
input rst_n,
output rs,
output rw,
output e,
inout [7:0] data_bus
);
上述代码定义了一个名为 lcd_controller 的模块,具有五个端口:两个输入( clk , rst_n )、三个输出( rs , rw , e )和一个双向总线( data_bus )。其中:
-
input表示该信号仅由外部驱动,进入本模块; -
output表示该信号由本模块产生并输出至外部; -
inout用于三态总线接口,常见于LCD、SRAM等共享数据总线设备。
参数说明 :
-clk: 系统主时钟,通常为50MHz或更高频率;
-rst_n: 低电平有效的复位信号;
-rs: 寄存器选择线,高电平表示数据,低电平表示命令;
-rw: 读写控制,高电平读,低电平写;
-e: 使能信号,上升沿锁存数据;
-data_bus[7:0]: 8位并行数据总线,支持双向传输。
该模块结构清晰地体现了与12864 LCD的物理连接关系,是顶层控制器的基础骨架。
模块结构解析与综合映射
当上述代码被综合工具处理后,会生成相应的网表(netlist),其中每个端口对应FPGA引脚约束(Pin Constraint),内部逻辑则映射为查找表(LUT)、触发器(FF)等资源。例如, rs 和 rw 输出可能直接来自寄存器输出,确保时序稳定; data_bus 因为是双向端口,需配合三态缓冲器( tri 类型)使用,如下所示:
wire [7:0] data_out;
reg [7:0] data_reg;
assign data_bus = (bus_enable) ? data_reg : 8'bz;
此处使用条件连续赋值实现三态控制:当 bus_enable 有效时, data_bus 驱动为 data_reg 值;否则呈现高阻态( 8'bz ),避免总线冲突。
2.1.2 数据类型与常量表示(wire、reg、parameter)
Verilog提供多种数据类型,正确选择类型对功能正确性和资源利用率至关重要。
| 类型 | 用途说明 | 是否可存储值 | 典型应用场景 |
|---|---|---|---|
wire | 连接模块间或逻辑间的连线 | 否(瞬时传递) | 组合逻辑输出、模块端口互联 |
reg | 存储状态,在 always 块中赋值 | 是(保持值) | 时序逻辑、状态寄存器 |
integer | 整数变量,用于循环计数 | 是 | 参数计算、for循环索引 |
parameter | 编译时常量 | 是(不可变) | 定义地址、指令码、状态编码 |
示例:状态编码与参数定义
parameter CMD_CLEAR_DISPLAY = 8'h01;
parameter CMD_RETURN_HOME = 8'h02;
parameter STATE_IDLE = 3'b000;
parameter STATE_INIT_1 = 3'b001;
这些 parameter 定义提升了代码可读性,并便于后期修改。例如,在LCD初始化序列中,可通过比较当前状态是否等于 STATE_INIT_1 来决定执行哪条指令。
reg 并不总是触发器?
一个常见的误解是认为所有 reg 类型都会综合成触发器。实际上,只有在 always @(posedge clk) 或带有时钟边沿敏感的块中赋值的 reg 才会被综合为寄存器。若在组合逻辑块中使用非阻塞赋值,也可能导致不必要的锁存器生成。
// ❌ 错误用法:产生锁存器
always @(*) begin
if (sel)
out_reg = a;
// else 分支缺失 → 综合出锁存器
end
// ✅ 正确做法:补全else或初始化
always @(*) begin
out_reg = 8'd0;
if (sel)
out_reg = a;
end
因此,必须明确区分组合逻辑与时序逻辑的建模范式,防止意外引入异步反馈路径。
2.1.3 连续赋值与过程赋值的区别与应用场景
Verilog提供两种主要赋值方式: 连续赋值 ( assign )和 过程赋值 (在 always 块中)。它们分别对应不同的硬件结构与建模层次。
连续赋值(Continuous Assignment)
wire eq_flag;
assign eq_flag = (a == b) ? 1'b1 : 1'b0;
assign 语句用于描述纯组合逻辑,其右侧表达式一旦变化,左侧立即更新。综合后生成的是门电路网络(如AND、OR、MUX等),无记忆元件。
适用场景 :多路选择器、比较器、地址译码、简单算术运算。
过程赋值(Procedural Assignment)
reg [7:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 8'd0;
else
counter <= counter + 1;
end
always 块内的赋值发生在特定事件触发时(如时钟上升沿),适用于时序逻辑建模。注意此处使用了非阻塞赋值 <= ,保证多个寄存器同步更新。
适用场景 :计数器、状态机、移位寄存器、寄存器堆。
赋值方式对比表格
| 特性 | 连续赋值 ( assign ) | 过程赋值 ( always ) |
|---|---|---|
| 触发机制 | 任意右端信号变化 | 显式敏感列表(event-driven) |
| 支持的数据类型 | wire | reg |
| 是否需要过程块 | 否 | 是 |
| 综合结果 | 组合逻辑 | 组合或时序逻辑 |
| 可否嵌套条件语句 | 有限(三目运算符) | 完整 if-else , case 结构 |
| 建议使用位置 | 模块顶层连接、简单逻辑 | 状态控制、时序逻辑 |
流程图:赋值方式选择决策树
graph TD
A[需要建模的逻辑?] --> B{是否依赖时钟?}
B -- 否 --> C[使用 assign 连续赋值]
B -- 是 --> D{是否为组合逻辑?}
D -- 否 --> E[使用 always @(posedge clk) + reg + 非阻塞赋值]
D -- 是 --> F[使用 always @(*) + reg + 阻塞赋值]
F --> G[注意避免锁存器]
此流程图为初学者提供了清晰的建模指导路径。例如,在LCD控制器中,地址译码部分采用 assign 实现,而状态转移逻辑则必须使用 always 块与时钟同步。
2.2 组合逻辑与时序逻辑的实现
在FPGA设计中,正确区分并实现组合逻辑与时序逻辑是保证系统稳定性与可预测性的关键。两者在延迟特性、毛刺敏感度及资源占用方面存在本质差异。本节将结合代码实例深入剖析其实现机制与最佳实践。
2.2.1 使用assign语句实现组合逻辑电路
assign 是最直接的组合逻辑建模手段,适用于无需状态保持的即时计算任务。
// 多路选择器
wire [3:0] mux_out;
assign mux_out = sel ? in_a : in_b;
// 地址译码
wire cs_lcd;
assign cs_lcd = (addr == 16'hA000) && chip_enable;
// 简单加法器
wire [7:0] sum;
assign sum = op_a + op_b;
以上均为典型的组合逻辑片段。综合工具会将其映射为LUT或专用加法器资源(如DSP块)。需要注意的是,所有输入必须出现在敏感路径上,否则可能导致仿真与综合不一致。
陷阱提示 :未完全列出敏感信号会导致综合后电路行为异常。SystemVerilog中可用
always_comb替代传统always @(*)来自动管理敏感列表。
2.2.2 使用always块构建触发器与时序状态机
时序逻辑依赖于时钟信号来同步状态变化,最常见的形式是D触发器。
reg q;
always @(posedge clk) begin
q <= d;
end
该结构综合为单个D触发器,用于构建寄存器、计数器、状态寄存器等。扩展一下,可用于实现有限状态机(FSM):
typedef enum logic [1:0] {
IDLE = 2'b00,
SEND_CMD = 2'b01,
WAIT_ACK = 2'b10,
DONE = 2'b11
} state_t;
state_t current_state, next_state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
always @(*) begin
case (current_state)
IDLE: next_state = SEND_CMD;
SEND_CMD: next_state = WAIT_ACK;
WAIT_ACK: next_state = (ack_received) ? DONE : WAIT_ACK;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
这是一个标准的两段式状态机设计:第一段负责状态寄存,第二段组合逻辑决定下一状态。优点是状态转换清晰、易于调试。
2.2.3 阻塞与非阻塞赋值的正确使用原则
这是Verilog中最容易出错的概念之一。简而言之:
- 阻塞赋值
=:顺序执行,适用于组合逻辑; - 非阻塞赋值
<=:并行更新,适用于时序逻辑。
对比示例
// ❌ 危险:混合使用导致竞争
always @(posedge clk) begin
a = b;
c <= a; // c 取的是旧值还是新值?不确定!
end
// ✅ 正确:统一使用非阻塞
always @(posedge clk) begin
a <= b;
c <= a;
end
在时序块中应始终使用非阻塞赋值,以确保所有寄存器在同一时钟边沿同步更新,避免仿真与综合结果偏差。
原则总结表
| 场景 | 推荐赋值方式 | 理由 |
|---|---|---|
always @(posedge clk) | <= 非阻塞 | 保证并发更新,避免竞争 |
always @(*) 组合逻辑 | = 阻塞 | 体现因果顺序,防止锁存器 |
| 初始化或复位分支 | <= | 与时序一致 |
| 多条赋值需同时生效 | <= | 模拟真实硬件同步行为 |
2.3 状态机设计与控制逻辑建模
状态机是控制类逻辑的核心建模工具,尤其适用于LCD初始化这类有序操作。
2.3.1 有限状态机(FSM)的三种编码方式(One-hot, Binary, Gray)
不同编码方式影响面积、速度与功耗。
| 编码方式 | 示例(4状态) | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Binary | 00, 01, 10, 11 | 节省寄存器 | 翻转多位,EMI大 | 小规模状态机 |
| One-hot | 0001, 0010, 0100, 1000 | 单bit翻转,速度快 | 占用更多FF | 高速FPGA设计 |
| Gray | 00, 01, 11, 10 | 相邻仅一位变,抗噪声 | 不通用 | 编码器、计数器 |
// One-hot 编码示例
localparam S0 = 4'b0001, S1 = 4'b0010, S2 = 4'b0100, S3 = 4'b1000;
Xilinx FPGA推荐使用One-hot编码,因其布线延迟低且便于静态时序分析。
2.3.2 基于状态机的LCD初始化序列控制设计实例
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= INIT_1;
cmd <= 8'h30;
end else begin
case (state)
INIT_1: begin
cmd <= 8'h30; // Function Set
state <= INIT_2;
end
INIT_2: begin
cmd <= 8'h30;
state <= INIT_3;
end
INIT_3: begin
cmd <= 8'h30;
state <= INIT_4;
end
INIT_4: begin
cmd <= 8'h28; // 4-bit mode, 2-line
state <= DISPLAY_ON;
end
DISPLAY_ON: begin
cmd <= 8'h0C; // Display ON, Cursor OFF
state <= CLEAR;
end
CLEAR: begin
cmd <= 8'h01; // Clear Display
state <= READY;
end
default: state <= INIT_1;
endcase
end
end
该状态机严格按照ST7920初始化流程执行三次0x30发送,再切换至4位模式,最终开启显示。每步延时由外部定时器控制。
2.3.3 多模块协同下的同步复位与异步事件处理
在多模块系统中,全局复位需同步释放,防止亚稳态。
reg [1:0] sync_rst;
always @(posedge clk) begin
sync_rst[0] <= ~rst_n;
sync_rst[1] <= sync_rst[0];
end
wire synced_rst_n = sync_rst[1];
采用双触发器同步法,将异步复位信号同步到本地时钟域,提升可靠性。
对于按键中断等异步事件,也应先同步再处理。
2.4 测试平台(Testbench)编写方法
2.4.1 初始块(initial)与仿真激励生成
initial begin
clk = 0;
forever #10 clk = ~clk; // 50MHz时钟
end
initial begin
rst_n = 0;
repeat(10) @(posedge clk);
rst_n = 1;
// 发送测试指令
send_command(8'h01); // 清屏
send_command(8'h80); // 地址0
send_data("H");
$finish;
end
initial 块用于生成激励和控制仿真流程。
2.4.2 信号监测与波形调试技巧
使用 $monitor 或 $dumpfile 输出关键信号:
initial $monitor("%t | state=%b cmd=%h", $time, state, cmd);
结合Vivado Simulator或ModelSim查看波形,定位时序问题。
2.4.3 自动化验证与预期结果比对机制
task expect_output;
input [7:0] expected;
begin
@(posedge lcd.e);
if (lcd.data_bus === expected)
$display("PASS: Got %h", expected);
else
$error("FAIL: Expected %h, got %h", expected, lcd.data_bus);
end
endtask
实现自动断言检查,提升验证效率。
3. 12864 LCD显示屏工作原理
12864图形液晶显示器(LCD)作为嵌入式系统中常见的可视化输出设备,广泛应用于工业控制、人机交互界面和便携式设备。其分辨率为128列×64行,支持文本与图形混合显示模式,具备低功耗、高对比度和宽视角等优点。本章将深入剖析12864 LCD的核心控制器架构、内部存储器组织机制、初始化流程及指令集执行逻辑,并结合ST7920控制器芯片详细解析并行接口下的通信协议与时序约束。通过对硬件引脚功能与数据传输机制的系统性分析,为后续在FPGA平台上实现无MCU依赖的自主驱动提供理论支撑。
3.1 12864 LCD硬件结构与引脚功能
12864 LCD模块通常集成ST7920或兼容控制器芯片,采用COB(Chip on Board)封装技术,直接将驱动IC绑定于玻璃基板之上,简化外围电路设计。该类屏幕支持并行8位数据总线接口,适用于高速数据写入场景,尤其适合由FPGA等可编程逻辑器件直接控制。理解其物理引脚定义与电气特性是构建稳定通信链路的前提。
3.1.1 ST7920或其他常用控制器芯片特性分析
ST7920是一款专用于128×64点阵LCD的内置汉字字库图形控制器,由中国晶门科技(Solomon Systech)开发,具备完整的字符生成引擎与图形显示RAM管理能力。其核心特性包括:
- 内置GB2312简体中文字符集 :包含一级汉字3755个、二级汉字6763个,共支持10518个字符。
- 双显示模式支持 :支持文本模式(Text Mode)与图形模式(Graphic Mode),可通过指令切换。
- 串/并双接口可选 :支持4/8位并行接口和串行SPI接口,灵活性强。
- 片上GDRAM容量 :256×32 bits = 1024 bytes,用于图形显示;DDRAM共4行×16字符位置,每字符占2字节。
- 工作电压范围 :VDD = 4.5V ~ 5.5V,典型值5V,符合TTL电平标准。
尽管ST7920自带字库,但在FPGA设计中若不使用MCU进行复杂指令调度,则需绕过自动字符映射机制,转而以“纯图形模式”直接操作GDRAM,实现自定义内容绘制。这要求开发者精确掌握底层寄存器配置方式与状态反馈机制。
| 特性 | 描述 |
|---|---|
| 控制器型号 | ST7920 |
| 分辨率 | 128 × 64 点阵 |
| 接口类型 | 8位并行 / 串行SPI |
| 内置字库 | GB2312简体中文 |
| GDRAM大小 | 256 × 32 bits (1024 Bytes) |
| DDRAM行数 | 4行 |
| 字符宽度 | 每行最多16个字符 |
说明 :GDRAM即图形显示RAM,按页分层组织;DDRAM用于存放ASCII或汉字编码,由控制器自动转换为点阵输出。
// 示例:ST7920基本配置参数定义(Verilog常量)
parameter CLK_FREQ = 50_000_000; // FPGA主频50MHz
parameter T_CYCLE_MIN = 200; // E周期最小时间(ns),对应5MHz写入速率
parameter INIT_DELAY_MS = 100; // 上电延时等待时间
代码解释 :
- CLK_FREQ 表示FPGA系统时钟频率,用于计算延时计数器初值;
- T_CYCLE_MIN 是使能信号E的最小周期,根据ST7920手册规定不低于200ns;
- INIT_DELAY_MS 保证上电后VCC达到稳定所需的时间,避免过早发送指令导致失败。
此参数设定为后续状态机延时控制提供了基准参考,确保所有操作满足时序要求。
3.1.2 数据总线(DB0~DB7)、RS、RW、E、RST等关键信号作用详解
ST7920通过一组标准控制信号与主控单元通信。各引脚功能如下表所示:
| 引脚名 | 方向 | 功能描述 |
|---|---|---|
| DB0~DB7 | 双向 | 8位双向数据总线,用于传输命令或显示数据 |
| RS (Register Select) | 输入 | 高电平表示数据,低电平表示指令 |
| RW (Read/Write) | 输入 | 高电平读操作,低电平写操作 |
| E (Enable) | 输入 | 使能信号,上升沿锁存数据 |
| RST (Reset) | 输入 | 复位信号,低电平有效 |
| V0 | —— | 对比度调节电压输入(接电位器) |
其中最关键的是 RS、RW 和 E 的组合决定了当前操作类型:
| RS | RW | 操作含义 |
|---|---|---|
| 0 | 0 | 写入指令(如清屏、设置地址) |
| 1 | 0 | 写入显示数据(到DDRAM或GDRAM) |
| 0 | 1 | 读取忙标志BF与地址计数器AC |
| 1 | 1 | 从显存读取数据(极少使用) |
⚠️ 注意:在大多数FPGA应用中仅使用“写模式”,即
RW = 0固定拉低,避免读取总线竞争问题。
E信号时序行为分析 :
E引脚作为同步使能端,在每个写周期中必须产生一个正脉冲(高电平持续至少450ns),且上升沿前数据必须稳定建立(setup time ≥ 140ns)。典型波形如下:
___________________________
E: | |
____| |______
←--- t_high ≥ 450ns ---→
←t_setup≥140ns→
Data: X X X X X X X X (稳定)
上述时序对FPGA逻辑设计提出了明确约束:在发出数据后,必须插入足够的延迟再触发E上升沿,否则可能导致命令丢失。
3.1.3 并行接口模式下的电气特性与时序要求
为了确保可靠通信,必须严格遵守ST7920数据手册中的交流参数规范。以下是关键时序参数(基于5V供电):
| 参数 | 符号 | 最小值 | 典型值 | 单位 | 条件 |
|---|---|---|---|---|---|
| E脉冲宽度高电平 | t_EH | 450 | - | ns | |
| E脉冲宽度低电平 | t_EL | 450 | - | ns | |
| 地址建立时间 | t_AS | 140 | - | ns | E上升沿前 |
| 数据保持时间 | t_DH | 10 | - | ns | E下降沿后 |
这些参数直接影响FPGA中状态机的状态跳变间隔设计。例如,在Verilog中模拟一次写操作需遵循以下步骤:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
E <= 1'b0;
state <= IDLE;
end else case(state)
IDLE: begin
if (wr_cmd_valid) begin
DB <= cmd_data; // 设置数据总线
RS <= 1'b0; // 写指令
RW <= 1'b0;
E <= 1'b0;
state <= SETUP;
end
end
SETUP: begin
#140; // 建立时间等待
E <= 1'b1;
state <= PULSE;
end
PULSE: begin
#450; // E高电平维持
E <= 1'b0;
state <= HOLD;
end
HOLD: begin
#450; // E低电平恢复
state <= IDLE;
end
endcase
end
逐行逻辑分析 :
1. always @(posedge clk...) :同步时序逻辑,抗毛刺干扰;
2. if (!rst_n) :异步复位,强制进入空闲态;
3. IDLE 状态检测是否有新指令请求;
4. SETUP 阶段延迟140ns确保数据建立;
5. PULSE 阶段维持E高电平不少于450ns;
6. HOLD 阶段完成低电平恢复期,准备下一次操作。
该结构虽用 # 延迟模拟,实际应替换为计数器实现跨平台综合兼容性。但其体现了精确时序控制的基本思想。
stateDiagram-v2
[*] --> IDLE
IDLE --> SETUP : wr_cmd_valid
SETUP --> PULSE : delay ≥140ns
PULSE --> HOLD : delay ≥450ns (E high)
HOLD --> IDLE : delay ≥450ns (E low)
note right of SETUP
必须保证DB[7:0]、RS、RW
在E↑前已稳定
end note
上图展示了写指令操作的状态迁移流程,强调了各阶段的时间约束条件,是FPGA控制器设计的重要参考模型。
3.2 显示存储器组织与地址映射机制
12864 LCD的显示内容由内部存储器统一管理,主要包括 DDRAM (Display Data RAM)和 GDRAM (Graphics Display RAM)。两者分别服务于字符显示与图形绘制,具有不同的寻址方式与布局结构。
3.2.1 GDRAM(图形显示RAM)布局与坐标系对应关系
GDRAM是图形模式下的核心显存区域,大小为1024字节(256×32位),被划分为两个半屏:左半(64列)和右半(64列),每半屏又分为8页(Page),每页8行,构成垂直分页结构。
每一“页”代表8行像素的高度,横向128列,因此每页占用128字节空间。整个GDRAM分布如下:
| 页面编号 | Y范围(行) | 起始地址(十六进制) |
|---|---|---|
| Page 0 | Y=0~7 | 0x80 |
| Page 1 | Y=8~15 | 0x81 |
| … | … | … |
| Page 7 | Y=56~63 | 0x87 |
实际上,页面地址通过“设置GDRAM Y地址指针”指令(
1000 0000 + Y_addr)来选择,然后X方向自动递增写入。
在X方向上,地址从0x00到0x7F连续排列,对应128列。每个字节的8位分别控制当前页内的8行像素(bit7为顶行,bit0为底行)。
例如:向左半屏Page 0写入字节 8'hFF ,将在第0~7行、第0列处点亮一竖条8像素高的线段。
这种“纵向字节排列”的特点要求图像数据必须经过转置处理才能正确显示。
3.2.2 DDRAM(数据显存)用于字符模式的定位规则
DDRAM用于存储待显示的字符编码,共4行,每行最多容纳16个字符地址,总计64个位置。每个字符位置占用2字节:
- 第一字节:字符高位地址(用于定位字库)
- 第二字节:字符低位地址
地址映射如下:
| 行号 | 起始地址(hex) | 地址范围 |
|---|---|---|
| 第1行 | 0x80 | 0x80 ~ 0x8F |
| 第2行 | 0x90 | 0x90 ~ 0x9F |
| 第3行 | 0x88 | 0x88 ~ 0x8F |
| 第4行 | 0x98 | 0x98 ~ 0x9F |
注意:第3、4行起始地址并非连续递增,而是与第1、2行交错分布。
当写入一个汉字编码(如“中”=0xD6D0)时,需先发送地址设置指令,再依次写入两个字节:
// 示例:在第一行第一个位置显示“中”
send_cmd(8'h80); // 设置地址为0x80
send_data(8'hD6); // 高字节
send_data(8'hD0); // 低字节
此时ST7920会自动查找内置字库并渲染点阵图像。
然而,在FPGA纯逻辑设计中,由于缺乏动态编码解析能力,一般禁用DDRAM模式,转而全屏使用GDRAM进行像素级绘图。
3.2.3 Z地址指针与行扫描顺序的影响
Z地址指针(也称行偏移指针)在部分控制器中用于调整显示起始行,实现滚动效果。但在ST7920中,Z地址不可编程,固定为从顶行开始逐行扫描。
这意味着屏幕刷新顺序为:
- 从Page 0开始,X=0→127,逐字节写入;
- 切换至Page 1,重复;
- 直至Page 7完成。
这种静态扫描方式简化了地址控制逻辑,但也限制了垂直滚动性能。若需实现滑动动画,必须手动重写整页内容。
以下表格总结了GDRAM与DDRAM的关键差异:
| 特性 | GDRAM | DDRAM |
|---|---|---|
| 用途 | 图形显示 | 字符显示 |
| 容量 | 1024 Bytes | 64 × 2 = 128 Bytes |
| 地址单位 | 字节 → 控制8行×1列 | 字符位置 → 自动查表 |
| 是否需要外部点阵 | 是(用户自行提供) | 否(内置字库) |
| 支持中文 | 取决于点阵数据 | 支持GB2312 |
| 更新粒度 | 像素级 | 字符级 |
graph TD
A[GDRAM] --> B[Page 0: Y=0~7]
A --> C[Page 1: Y=8~15]
A --> D[...]
A --> E[Page 7: Y=56~63]
B --> F[X=0 to 127, 128 bytes]
C --> G[X=0 to 127, 128 bytes]
E --> H[X=0 to 127, 128 bytes]
style A fill:#f9f,stroke:#333
style F fill:#bbf,stroke:#333,color:white
上图为GDRAM的层级结构示意,清晰展示“页-列-像素”三级映射关系,有助于理解图像数据组织方式。
3.3 初始化流程与指令集解析
正确初始化ST7920是确保12864 LCD正常工作的前提。上电后控制器处于未知状态,必须按照特定顺序发送一系列配置命令。
3.3.1 复位后必须执行的配置命令序列(清屏、开显示、设置模式等)
标准初始化流程如下:
- 延时 >10ms(等待电源稳定)
- 发送
0x30:进入8位基本指令集模式 - 延时 >100μs
- 发送
0x0C:开启显示(无光标、无闪烁) - 延时 >100μs
- 发送
0x01:清屏,地址归零 - 延时 >10ms(清屏耗时较长)
- 可选:进入扩展指令集设置绘图模式
// 初始化状态机片段
typedef enum logic [2:0] {
WAIT_POWER,
SEND_FUNC_SET,
SEND_DISPLAY_ON,
SEND_CLEAR_SCREEN,
INIT_DONE
} init_state_t;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
current_state <= WAIT_POWER;
cnt <= 0;
end else begin
case(current_state)
WAIT_POWER: begin
if (cnt >= POWER_DELAY_CNT) // 例如10ms @50MHz
current_state <= SEND_FUNC_SET;
else
cnt <= cnt + 1;
end
SEND_FUNC_SET: begin
send_command(8'h30);
current_state <= SEND_DISPLAY_ON;
end
SEND_DISPLAY_ON: begin
send_command(8'h0C);
current_state <= SEND_CLEAR_SCREEN;
end
SEND_CLEAR_SCREEN: begin
send_command(8'h01);
current_state <= INIT_DONE;
end
INIT_DONE: ; // 终止
endcase
end
end
参数说明 :
- POWER_DELAY_CNT = 10e-3 * CLK_FREQ ≈ 500,000(50MHz下)
- send_command() 为封装好的带E时序的写指令函数
该状态机能自动完成上电配置,无需外部干预。
3.3.2 扩展指令集启用与绘图开关操作
要启用图形模式,需进入扩展指令集:
- 发送
0x34:开启扩展指令集(RE=1) - 发送
0x36:开启绘图显示(GDRAM读写模式) - 后续可设置XY地址进行绘图
退出时发送 0x30 恢复基本指令集。
注:扩展指令集中还包括温度补偿、反转显示等功能。
3.3.3 指令执行时间与延时等待策略
不同指令执行时间各异,特别是清屏(0x01)和归位(0x02)需等待至少1.6ms。若未充分延时即发下一条指令,会导致操作失败。
解决方案:
- 使用固定延时计数器
- 或读取“忙标志”(BF)判断是否就绪
但由于FPGA通常不连接DB总线为输入,推荐采用保守延时策略。
| 指令 | 执行时间 | 推荐延时 |
|---|---|---|
| 0x01 (清屏) | 1.6ms | ≥2ms |
| 0x02 (归位) | 1.6ms | ≥2ms |
| 其他指令 | ~72μs | ≥100μs |
合理安排延时可显著提升系统稳定性。
sequenceDiagram
participant FPGA
participant LCD
FPGA->>LCD: 上电 (VCC稳定)
Note right of FPGA: 延时10ms
FPGA->>LCD: 写指令 0x30
Note right of FPGA: 延时100μs
FPGA->>LCD: 写指令 0x0C
Note right of FPGA: 延时100μs
FPGA->>LCD: 写指令 0x01
Note right of FPGA: 延时2ms
FPGA->>LCD: 初始化完成
时序图展示了完整的初始化过程,强调了各阶段之间的必要等待窗口。
3.4 文本与图形混合显示机制
虽然ST7920支持混合显示,但在FPGA独立驱动中更倾向于统一使用GDRAM进行全图形化渲染。
3.4.1 字符模式与图形模式切换条件
模式切换依赖指令控制:
-
0x30:基本指令集 → 支持DDRAM写入 -
0x34+0x36:扩展指令集 → 允许GDRAM访问
二者不可同时激活,需通过状态机协调。
3.4.2 中文字符点阵取模原理与显示限制
无内置字库访问权限时,需预先提取汉字点阵数据。常用工具如PCtoLCD2002可生成16×16像素的二进制数组。
例如,“赵”字的点阵数据为:
const unsigned char zhao[] = {
0x04,0x20,0x08,0x20,0x1F,0xFC,0x20,0x20,0x40,0x20,0x9F,0xFE,
0x00,0x20,0x00,0x20,0x00,0x20,0xFF,0xFE,0x00,0x20,0x00,0x20,
0x40,0x20,0x80,0x20,0x7F,0xFE,0x00,0x00
};
该数据需按列分割写入GDRAM相邻两页(因16行跨越两个8行页)。
3.4.3 图像数据写入GDRAM的分页策略
由于每页仅8行,超过8行的图像需拆分写入多个页面。例如16×16图标需:
- 设置Page 0, X=col
- 写入前8行数据(每列1字节)
- 设置Page 1, X=col
- 写入后8行数据
此过程可通过循环自动完成,形成通用图像绘制引擎的基础。
4. LCD控制器模块设计与实现
在FPGA系统中驱动12864 LCD显示屏,尤其是基于ST7920等并行接口控制器的型号,需要构建一个高度时序敏感、状态可控且具备良好扩展性的专用控制模块。该模块不仅要完成对LCD初始化流程的精确执行,还需持续管理数据/命令的发送节拍、地址映射以及显示内容更新机制。本章将围绕“ LCD控制器模块的设计与实现 ”展开深入分析,从顶层架构划分到子模块逻辑细节,系统化地阐述如何利用Verilog HDL语言构建一个可复用、高稳定性的纯逻辑驱动方案。
不同于嵌入式处理器通过固件轮询或中断方式操作外设的方式,FPGA中的LCD控制器必须以完全同步、流水线化的方式运行,所有操作均需由状态机精确调度,并满足严格的建立/保持时间约束。此外,由于12864 LCD采用并行8位总线接口,涉及多个控制信号(如RS、RW、E)和数据总线DB[7:0]之间的协同工作,因此跨时钟域处理、信号同步、总线冲突避免等问题成为设计难点。
本章内容将以Xilinx Artix-7系列FPGA为平台背景,结合Vivado开发环境进行综合与布局布线验证,确保所设计模块不仅功能正确,而且满足实际硬件运行所需的时序要求。
4.1 顶层模块划分与接口定义
在复杂数字系统设计中,合理的模块划分是保证可维护性、可测试性和性能优化的基础。对于LCD控制器而言,其顶层模块应作为整个显示系统的中枢,负责协调各个子模块之间的工作流,同时对外提供清晰的标准接口,便于与其他逻辑单元(如图像生成器、CPU接口或DMA引擎)集成。
4.1.1 控制器主模块与子模块间的数据通路规划
LCD控制器的内部结构可分为以下几个核心子模块:
| 子模块 | 功能描述 |
|---|---|
init_fsm | 管理上电后LCD初始化命令序列的自动发送 |
cmd_gen | 生成具体要发送的指令或数据字节 |
timing_ctrl | 提供符合ST7920规范的E使能脉冲及延时控制 |
data_path | 数据缓冲与总线驱动逻辑,支持三态输出 |
state_machine | 主控状态机,协调写命令、写数据、等待等操作 |
这些模块通过明确的数据通路连接,形成一条从“命令请求 → 命令打包 → 时序控制 → 总线输出”的完整路径。例如,当外部逻辑请求写入字符“A”时,该请求首先进入 cmd_gen 模块,被封装成对应的ASCII码值;随后传递至 data_path 暂存;最后由 state_machine 在合适的时机触发 timing_ctrl 产生E脉冲,完成一次写操作。
// 示例:顶层模块端口声明
module lcd_controller (
input clk, // 系统主时钟(50MHz)
input rst_n, // 低电平有效复位
input en, // 模块使能信号
input [7:0] data_in, // 待写入的数据/命令
input wr_cmd, // 写命令标志(1=命令,0=数据)
input wr_req, // 写请求有效
output reg busy, // 模块忙信号
output rs, // 寄存器选择
output rw, // 读/写控制
output e, // 使能信号
inout [7:0] db // 双向数据总线
);
上述代码展示了顶层模块的关键输入输出端口。其中, wr_req 用于触发一次写操作, wr_cmd 决定本次操作类型(命令 or 数据),而 busy 信号则反映当前控制器是否处于繁忙状态(如正在执行延时或发送过程)。这种接口设计使得外部模块可以像访问寄存器一样调用LCD控制器,提升了抽象层次。
数据流向图(Mermaid)
graph TD
A[External Logic] -->|data_in, wr_req| B(cmd_gen)
B --> C[state_machine)
C --> D[timing_ctrl]
D --> E[e]
C --> F[data_path]
F --> G[db]
C --> H[rs/rw]
D --> C
style B fill:#f9f,stroke:#333
style C fill:#bbf,stroke:#333,color:#fff
此流程图清晰表达了各子模块间的依赖关系与数据流动方向。主状态机处于中心位置,接收来自命令生成模块的信息,并依据当前状态调用时序控制器产生E信号,同时配置RS/RW和驱动数据总线。
4.1.2 时钟、复位、使能信号的全局同步机制
在一个多模块协作的系统中,时钟和复位信号的同步至关重要。若不同模块使用异步复位或未对齐的时钟边沿采样,极易引发亚稳态问题,导致状态跳变异常或数据错乱。
为此,在本设计中统一采用 同步复位策略 ,即所有子模块的状态寄存器均在 clk 上升沿检测 rst_sync 信号(已同步后的复位信号):
reg rst_sync1, rst_sync2;
always @(posedge clk) begin
rst_sync1 <= ~rst_n; // 异步捕获原始复位
rst_sync2 <= rst_sync1; // 第二级打拍同步
end
wire sys_rst = rst_sync2;
该双触发器同步法广泛应用于跨时钟域信号同步场景,也可用于消除复位抖动带来的不确定性。经两级D触发器采样后, sys_rst 已成为与时钟完全同步的低电平有效复位信号,可用于驱动所有状态机和寄存器。
此外, en 使能信号也需经过同步处理,防止在非使能状态下误触发操作。只有当 en == 1 && wr_req == 1 时,才允许进入写操作流程,从而实现对外部访问的精确控制。
4.1.3 写数据/命令接口与外部逻辑的交互协议
为了提高兼容性与易用性,LCD控制器定义了一套简洁但完备的通信协议。该协议基于“请求-响应”模式,具体如下:
- 外部模块将目标数据放入
data_in总线; - 设置
wr_cmd以指示操作类型; - 拉高
wr_req一个时钟周期; - 控制器拉高
busy表示开始处理; - 完成后自动释放
busy,允许下一次请求。
// 在主状态机中判断写请求
always @(posedge clk) begin
if (sys_rst)
busy <= 1'b0;
else if (en && wr_req && !busy)
busy <= 1'b1; // 锁定总线,防止并发访问
else if (current_state == IDLE && next_state == SEND_DATA)
busy <= 1'b1;
else if (current_state == DONE)
busy <= 1'b0;
end
参数说明:
- sys_rst : 同步复位信号,优先级最高。
- en : 全局使能,关闭时忽略所有请求。
- wr_req : 脉冲信号,宽度至少为一个 clk 周期。
- busy : 输出状态信号,用于流量控制。
该机制确保了即使外部逻辑连续发送请求,控制器也能逐个处理,避免总线竞争或命令丢失。同时, busy 信号还可作为握手信号接入DMA或CPU直连接口,实现高效批量写入。
4.2 命令与数据生成逻辑实现
LCD控制器能否正常工作,关键在于能否准确生成所需命令并按序发送。特别是上电初始化阶段,必须严格按照ST7920手册规定的顺序执行一系列配置命令,否则屏幕可能无法点亮或进入错误模式。
4.2.1 初始化命令队列的状态机驱动设计
ST7920的初始化流程通常包括以下步骤:
1. 延时 >40ms(上电稳定)
2. 发送 0x30 (基本指令集)
3. 延时 >100us
4. 发送 0x0C (开显示,光标关闭)
5. 发送 0x01 (清屏)
6. 延时 >1.6ms
这一系列操作不能由软件循环完成,而必须由FPGA内部状态机自动推进。我们设计一个有限状态机(FSM)来管理整个初始化流程:
typedef enum logic [3:0] {
IDLE,
INIT_WAIT_40MS,
SEND_CMD_30,
WAIT_100US,
SEND_CMD_0C,
SEND_CMD_01,
INIT_DONE
} init_state_t;
init_state_t current_init_state;
always @(posedge clk) begin
if (sys_rst) begin
current_init_state <= IDLE;
cmd_to_send <= 8'hxx;
init_done <= 1'b0;
end else case (current_init_state)
IDLE:
if (global_en)
current_init_state <= INIT_WAIT_40MS;
INIT_WAIT_40MS:
if (delay_counter == 40_000) // 假设clk=50MHz
current_init_state <= SEND_CMD_30;
SEND_CMD_30: begin
cmd_to_send <= 8'h30;
send_pulse <= 1'b1;
current_init_state <= WAIT_100US;
end
WAIT_100US:
if (delay_counter == 5_000) // 100us @50MHz
current_init_state <= SEND_CMD_0C;
SEND_CMD_0C: begin
cmd_to_send <= 8'h0C;
send_pulse <= 1'b1;
current_init_state <= SEND_CMD_01;
end
SEND_CMD_01: begin
cmd_to_send <= 8'h01;
send_pulse <= 1'b1;
current_init_state <= INIT_DONE;
end
INIT_DONE: begin
init_done <= 1'b1;
send_pulse <= 1'b0;
end
endcase
end
逻辑逐行分析:
- 使用 typedef enum 定义状态枚举类型,增强可读性;
- delay_counter 为独立计数器模块,根据当前状态加载不同延时值;
- send_pulse 为单周期脉冲信号,触发主写状态机执行一次传输;
- 所有状态转移均发生在 posedge clk 下,确保完全同步;
- 最终 init_done 信号可用于通知其他模块“LCD已就绪”。
该状态机完全自动化运行,无需外部干预,极大简化了系统集成难度。
4.2.2 动态文本刷新与静态图像加载的调度机制
在实际应用中,LCD常需同时显示动态信息(如时间、传感器数值)和静态内容(如标题、图标)。为此,控制器需具备任务调度能力。
设计思路如下:
- 将显示区域划分为多个“页面”或“区块”;
- 每个区块绑定特定更新频率;
- 使用定时器触发对应刷新动作;
- 高优先级任务可抢占低优先级任务。
// 区块刷新调度示例
localparam BLOCK_TITLE = 0;
localparam BLOCK_TIME = 1;
localparam BLOCK_SENSOR= 2;
reg [1:0] next_block = 0;
always @(posedge clk) begin
if (sys_rst) next_block <= 0;
else if (tick_1s && !busy) begin // 每秒刷新一次时间
case (next_block)
BLOCK_TITLE: {wr_req_o, wr_cmd_o} <= {1, 8'h80}; // 写地址
BLOCK_TIME: {wr_req_o, wr_data_o} <= gen_time_str();
BLOCK_SENSOR:{wr_req_o, wr_data_o}<= read_sensor();
endcase
next_block <= next_block + 1'd1;
end
end
该机制实现了轻量级的时间分片调度,适用于资源受限的FPGA系统。
4.2.3 命令打包与发送节拍控制
每条命令或数据在发送前需打包成标准格式,包含:
- 数据字节本身;
- 对应的RS/RW设置;
- 必要的前置延时;
- E脉冲宽度控制。
// 打包逻辑片段
assign rs = (wr_cmd) ? 1'b0 : 1'b1; // 命令:RS=0;数据:RS=1
assign rw = 1'b0; // 固定写操作
随后交由 timing_ctrl 模块生成宽度为450ns~1.6μs的E脉冲(依据ST7920手册),并通过状态机严格控制最小间隔(≥200ns)。
4.3 时钟同步与时序控制设计
4.3.1 多时钟域下的跨时钟信号同步技术(双触发器同步法)
若LCD控制器工作在50MHz时钟域,而外部数据源来自另一异步时钟(如UART接收器@115200bps),则 wr_req 等控制信号存在亚稳态风险。采用双触发器同步链可显著降低失效率:
reg sync1, sync2;
always @(posedge clk) begin
sync1 <= async_wr_req;
sync2 <= sync1;
end
wire synced_wr_req = sync2;
注意 :仅适用于单比特信号。多比特信号应使用FIFO或握手协议。
4.3.2 E使能信号高电平宽度与周期约束满足
根据ST7920手册,E信号需满足:
- 高电平宽度 ≥ 450ns
- 周期 ≥ 1μs
- 下降沿后需 ≥ 20ns 才能改变数据
sequenceDiagram
Controller->>LCD: RS/RW/DB set
Controller->>LCD: E ↑ (t ≥ 450ns)
Controller->>LCD: E ↓
Note right of LCD: Data latched on falling edge
Verilog实现:
reg [9:0] e_width_ctr = 0;
always @(posedge clk) begin
if (e_width_ctr != 0) begin
e_sig <= 1'b1;
e_width_ctr <= e_width_ctr - 1;
end else
e_sig <= 1'b0;
end
// 触发E脉冲
always @(posedge clk) begin
if (send_pulse)
e_width_ctr <= 24; // 24*20ns=480ns @50MHz
end
4.3.3 关键路径时序优化与建立/保持时间保障
在Vivado中添加时序约束:
create_clock -name lcd_clk -period 20.000 [get_ports clk]
set_max_delay -from [get_pins data_path/db_reg[*]/C] -to [get_ports db] 15.0
使用IOB寄存器锁定( (* IOB="TRUE" *) )减少布线延迟,提升稳定性。
4.4 接口通信协议实现细节
4.4.1 数据/控制信号(RS/RW)的组合逻辑判定
见前述 rs/rw 赋值逻辑,完全由 wr_cmd 决定。
4.4.2 写操作时序图转化为Verilog行为描述
参考手册时序图建模:
// 写操作状态机片段
SEND: begin
db <= data_to_write; // 提前1个周期准备数据
e_sig <= 1'b1;
next_state <= HOLD;
end
HOLD: begin
if (hold_count == 2) // 保持40ns以上
next_state <= RELEASE;
end
RELEASE: begin
e_sig <= 1'b0;
next_state <= DONE;
end
4.4.3 总线冲突避免与三态缓冲控制
使用三态门控制双向总线:
assign db = (lcd_output_enable) ? db_internal : 8'bz;
防止多个驱动源同时驱动总线造成短路。
5. 无字库显示机制与自定义字符编码
在现代FPGA驱动12864 LCD的应用中,一个核心挑战是缺乏内置中文或图形字库支持。传统嵌入式系统依赖于LCD控制器(如ST7920)内部集成的CGROM来实现字符快速输出,但该ROM通常仅包含ASCII字符集和有限的汉字库(如GB2312基本区),且不可扩展。当设计需求涉及非标准字符、图标或动态图形时,这种依赖成为功能瓶颈。因此,构建一套脱离固有字库限制的“无字库”显示机制,成为高灵活性人机界面开发的关键技术路径。
本章将系统性地阐述如何通过自定义点阵数据编码、构建渲染引擎以及实现图形绘制能力,完成从抽象文本到像素级图像的完整映射过程。整个方案不依赖任何外部处理器或操作系统,完全由FPGA逻辑自主完成,适用于资源受限但对实时性和可控性要求较高的工业控制、仪器仪表等场景。
5.1 传统字库依赖问题与解决方案
5.1.1 内置字库缺失导致的中文无法直接显示问题
12864 LCD模块常采用ST7920作为主控芯片,其支持两种工作模式:字符模式(Character Mode)和图形模式(Graphic Mode)。在字符模式下,用户发送一个字节(如‘A’),控制器会自动从内部CGROM查找对应的5×8或8×16点阵并渲染;而在图形模式下,则需手动写入GDRAM(图形显示RAM)进行逐像素操作。
然而,ST7920虽然宣称支持简体中文,但实际上其内建汉字库仅覆盖部分常用汉字(约8,000个),并且访问方式受限于特定地址映射规则。更严重的问题在于: 这些字库固化在ROM中,无法添加新字符或修改样式 。例如,在需要显示公司Logo、特殊符号或少数民族文字时,传统方法失效。
此外,许多低成本12864模块并未真正搭载具备中文字库的ST7920版本,而是使用兼容芯片甚至模拟协议的行为模型,导致即使指令正确也无法正常显示汉字。这使得开发者必须放弃对硬件字库的信任,转向全软件化控制策略。
5.1.2 使用外部点阵数据替代ROM字库的技术路线
为突破此限制,可采用“外置点阵+逻辑渲染”的技术路线:
- 将所需字符/图形预先转换为16×16或更高分辨率的二进制点阵;
- 存储于FPGA片上Block RAM或常量数组中;
- 设计专用状态机按行扫描方式将其写入GDRAM;
- 实现坐标定位、换行处理、叠加合成等高级功能。
该方法的核心优势在于 完全自主控制显示内容 。无论是自定义字体、动画帧还是矢量图标,均可通过预处理生成点阵后交由FPGA执行。同时,由于所有数据驻留在逻辑内部,响应速度快,无需外部存储器访问延迟。
自定义字库流程示意图(Mermaid)
graph TD
A[原始字符/图像] --> B{选择工具}
B --> C[PCtoLCD2002]
B --> D[Image2Lcd]
C --> E[生成C语言格式点阵]
D --> F[导出Hex数组]
E --> G[转换为Verilog常量]
F --> G
G --> H[FPGA ROM LUT]
H --> I[渲染引擎调用]
I --> J[写入GDRAM]
J --> K[屏幕显示]
该流程实现了从视觉内容到可编程逻辑的端到端映射,构成无字库系统的基石。
5.1.3 资源占用与灵活性之间的权衡分析
尽管外置点阵提供了极大的自由度,但也带来资源开销问题。以16×16点阵为例,每个汉字需32字节(2B × 16行),若需支持100个常用字符,总空间为3.2KB。对于Xilinx Artix-7系列FPGA,BRAM容量充足(数十KB以上),此类开销可接受。但在低端CPLD或小规模FPGA上,则需优化策略。
| 字符尺寸 | 每字符字节数 | 100字符总占用 | 适用场景 |
|---|---|---|---|
| 8×16 | 16 | 1.6 KB | 数码管替代、菜单标签 |
| 16×16 | 32 | 3.2 KB | 中文界面、小型GUI |
| 32×32 | 128 | 12.8 KB | 图标、LOGO显示 |
注:每行数据按字节对齐,高位补零。
为了平衡资源与表现力,常见做法包括:
- 分页加载:仅将当前页面所需字符载入BRAM;
- 共享结构:利用点阵相似性合并重复图案;
- 压缩编码:使用RLE(Run-Length Encoding)压缩空白区域;
- 动态生成:通过算法生成简单图形(如进度条)而非存储全图。
上述决策直接影响系统架构设计,应在项目初期评估清楚。
5.2 自定义字符点阵生成方法
5.2.1 点阵提取工具(如PCtoLCD2002)使用流程
PCtoLCD2002是一款广泛使用的点阵生成工具,支持多种输出格式和编码方式。以下是其典型使用步骤:
- 打开软件,选择“取模方式” → “阴码 + 列行式(低位在前)”;
- 输入目标字符(如“智”、“能”)或导入BMP图像;
- 设置字体类型(宋体)、大小(16×16);
- 点击“生成”按钮,得到十六进制数组;
- 复制结果并粘贴至Verilog代码中。
示例:生成“科”字的16×16点阵
在PCtoLCD2002中设置如下参数:
- 字模类型:16×16
- 排列方式:纵向取模,字节倒序
- 输出格式:C51格式
生成结果如下:
0x22,0x22,0xFA,0x22,0x22,0x22,0xFA,0x22,
0x22,0x22,0xFA,0x22,0x22,0x22,0xFE,0x00,
0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,
0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x00
此数组表示“科”字的每一列的位分布(共16列,每列2字节)。
5.2.2 16×16汉字点阵数据格式解析
理解点阵排列顺序至关重要。ST7920的GDRAM按页组织,每页8行(Y=0~7),共8页。X方向为0~127列。当写入图形模式时,每次传输两个字节代表一列中的上下8位。
具体映射关系如下表所示:
| GDRAM 地址 | 行号(Y) | 数据含义 |
|---|---|---|
| Page 0 | Y=0~7 | 上半部分 |
| Page 1 | Y=8~15 | 下半部分 |
因此,16×16汉字需分两次写入:先写Page 0对应上8行,再写Page 1对应下8行。
每个字节的bit7~bit0对应垂直方向上的像素值(bit7为顶部)。例如,若某列为 8'h10 ,则表示第4行为亮(从上往下数第4行)。
5.2.3 点阵数组在Verilog中的常量存储方式(case语句或memory block)
有两种主流方式在Verilog中组织点阵数据:
方法一:使用 case 语句实现字符查找表(LUT)
function [31:0] get_char_data;
input [7:0] ch;
begin
case (ch)
"A": get_char_data = {8'h7E, 8'h81, 8'h81, 8'h81, 8'h7E, 8'h00, 8'h00, 8'h00};
"B": get_char_data = {8'hFE, 8'h91, 8'h91, 8'h91, 8'h6E, 8'h00, 8'h00, 8'h00};
"科": get_char_data = {
8'h22, 8'h22, 8'hFA, 8'h22, 8'h22, 8'h22, 8'hFA, 8'h22,
8'h22, 8'h22, 8'hFA, 8'h22, 8'h22, 8'h22, 8'hFE, 8'h00,
8'h08, 8'h08, 8'h08, 8'h08, 8'h08, 8'h08, 8'h08, 8'h08,
8'h08, 8'h08, 8'h08, 8'h08, 8'h08, 8'h08, 8'h08, 8'h00
};
default: get_char_data = 32'd0;
endcase
end
endfunction
逻辑分析 :
- 函数输入为字符ASCII码或自定义ID;
- 输出为32字节拼接的向量,便于后续拆包;
- 支持混合英文与中文,通过唯一键区分;
- 缺点是编译后占用组合逻辑较多,不适合大字符集。
方法二:使用分布式RAM(Block RAM)作为只读存储器
(* ram_style = "block" *) reg [7:0] char_rom [0:255][0:31];
initial begin
// 初始化“科”字
char_rom["科"][ 0] = 8'h22; char_rom["科"][ 1] = 8'h22;
// ...其余30项省略...
char_rom["科"][31] = 8'h00;
// 可继续初始化其他字符
end
逻辑分析 :
- 利用FPGA原语块RAM实现高效存储;
-ram_style="block"提示综合器优先使用BRAM;
- 支持更大字符集(最多256个字符 × 32B = 8KB);
- 访问需通过地址索引,适合配合状态机批量读取。
5.3 字符渲染引擎设计
5.3.1 字符索引查找表(LUT)构建
为提高检索效率,应建立字符索引表,将输入字符映射为其在ROM中的偏移地址。例如:
reg [7:0] char_index [256];
initial begin
char_index["A"] = 0;
char_index["B"] = 1;
char_index["科"] = 2;
// ...
end
配合上述 char_rom 使用,即可实现 char_rom[char_index[ch]][line] 的快速访问。
5.3.2 坐标定位与逐行写入GDRAM策略
GDRAM写入需遵循严格的地址设置流程:
- 发送“设置页地址”命令(0xB0 ~ 0xB7);
- 发送“设置列地址”命令(0x00 ~ 0x0F,低4位;0x10 ~ 0x1F,高4位);
- 连续写入数据,自动递增列指针。
以下为Verilog状态机片段,用于在指定(X,Y)位置绘制16×16字符:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
x_cnt <= 0;
y_page <= 0;
byte_idx <= 0;
end else begin
case (state)
IDLE: if (draw_req) begin
x_base <= pos_x;
y_base <= pos_y >> 3; // 转换为页号
char_id <= char_index[in_char];
state <= SET_PAGE;
end
SET_PAGE: begin
lcd_cmd <= 8'hB0 + (y_base + (byte_idx >> 4)); // 高8行切换页
lcd_rs <= 1'b0; // 命令模式
en_send <= 1;
state <= SET_COL;
end
SET_COL: begin
lcd_cmd <= 8'h00 + (x_base[3:0]); // 设置低列
en_send <= 1;
state <= SET_COL_HI;
end
SET_COL_HI: begin
lcd_cmd <= 8'h10 + (x_base[6:4]); // 设置高列
en_send <= 1;
state <= WRITE_DATA;
end
WRITE_DATA: begin
lcd_data <= char_rom[char_id][byte_idx];
lcd_rs <= 1'b1; // 数据模式
en_send <= 1;
byte_idx <= byte_idx + 1;
if (byte_idx == 31) state <= DONE;
else state <= SET_COL; // 继续下一列
end
DONE: begin
draw_done <= 1;
state <= IDLE;
end
endcase
end
end
参数说明 :
-pos_x,pos_y:起始坐标(左上角)
-byte_idx:当前写入的第几个字节(0~31)
-y_base:起始页号(Y / 8)
-en_send:触发LCD控制器发送模块
-lcd_cmd/lcd_data:输出至驱动层的信号逻辑分析 :
- 每次写入一个字节对应一列的8个像素;
- 当byte_idx < 16时写Page Y,>=16时写Page Y+1;
- 列地址在每次写入后自动加1,故只需设置一次;
- 整个过程由状态机精确控制时序,避免冲突。
5.3.3 多字符拼接与换行处理机制
实际应用中常需连续显示字符串。为此,需引入字符间距管理与换行判断逻辑:
reg [15:0] cursor_x, cursor_y;
always @(posedge clk) begin
if (new_line) begin
cursor_y <= cursor_y + 16;
cursor_x <= 0;
end else if (char_sent) begin
cursor_x <= cursor_x + 16; // 假设固定宽度
if (cursor_x + 16 > 128) begin
cursor_x <= 0;
cursor_y <= cursor_y + 16;
end
end
end
结合DMA式批量推送机制,可大幅提升刷新效率。
5.4 图形元素绘制支持
5.4.1 线条、矩形、图标等基本图形的像素级绘制算法
除文本外,图形界面还需绘制边框、进度条等元素。以画线为例,Bresenham算法可在纯逻辑中高效实现:
// 简化版水平线绘制
task draw_hline;
input [6:0] x_start, x_end;
input [6:0] y;
begin
for (int i = x_start; i <= x_end; i = i + 1) begin
plot_pixel(i, y);
end
end
endtask
其中 plot_pixel 更新缓存或直写GDRAM。
5.4.2 位操作合成多图层内容
多个图形可能重叠,需支持“或”、“异或”合成:
wire new_pixel = old_pixel | incoming;
可用于闪烁光标、选中高亮等效果。
5.4.3 显示缓存更新粒度与刷新频率平衡
直接操作GDRAM会导致撕裂现象。推荐引入双缓冲机制(见第六章),定时批量刷新,保持画面稳定。
图形绘制模块结构(表格)
| 模块名称 | 功能描述 | 输入信号 | 输出信号 |
|---|---|---|---|
| LineDrawer | Bresenham直线绘制 | x1,y1,x2,y2, color | write_addr, write_data |
| RectRenderer | 矩形填充 | x,y,w,h | 同上 |
| IconBlitter | 图标贴图 | base_addr, offset | |
| Compositor | 多图层合成 | layer1, layer2, mode | final_buffer |
通过模块化封装,形成可复用的图形库,显著提升开发效率。
6. 像素缓冲区设计与管理
6.1 显示缓冲区的必要性与架构选择
在FPGA驱动12864 LCD的系统中,直接对GDRAM(图形显示RAM)进行实时写操作会带来显著的问题:首先,LCD控制器(如ST7920)执行指令存在严格的时序延迟(例如清屏指令需约1.6ms),频繁访问会导致显示更新卡顿;其次,多个逻辑模块(如字符渲染、图形绘制)并发请求写入时容易引发总线竞争。因此,引入 像素级显示缓冲区 (Frame Buffer)成为必要。
一种高效的架构是采用 双缓冲机制 :即设置两个独立的128×64位(共8KB)BRAM存储区域,分别命名为 Buffer_A 和 Buffer_B 。当前显示的是 Buffer_A 内容,而新画面可安全写入 Buffer_B 。当帧更新完成,通过控制信号切换读取源,实现“乒乓切换”,避免撕裂现象。
Xilinx FPGA中的Block RAM(BRAM)支持单端口或双端口模式。为支持同时读写,我们选用 True Dual Port BRAM ,其资源占用估算如下表所示:
| 缓冲区类型 | 容量 (bit) | 所需BRAM 36K数量 | 分布方式 |
|---|---|---|---|
| 单缓冲 | 128×64 = 8192 bit ≈ 8KB | ceil(8 / 36) = 1 | 单个BRAM即可容纳 |
| 双缓冲 | 16KB | 1(共享同一BRAM块,分区域映射) | 利用BRAM深度扩展 |
实际设计中,可通过IP核生成器配置一个16-bit宽、512深度的双端口RAM(共1024×16=16KB),前8KB用于 Buffer_A ,后8KB用于 Buffer_B ,地址映射关系如下:
// 地址映射宏定义
localparam BUFFER_SIZE = 512; // 每页512字节(128列 × 64行 / 8)
localparam PAGE_A_BASE = 12'h000;
localparam PAGE_B_BASE = 12'h200;
该结构允许CPU接口或内部状态机按坐标寻址写入,同时扫描逻辑持续从另一页面读出送往LCD控制器。
6.2 缓冲区读写访问控制
为了实现高效且无冲突的数据访问,需设计精细的读写仲裁机制。本系统采用 固定优先级+乒乓切换 策略。
写操作:局部更新机制
外部模块(如文本引擎)通过以下接口更新缓冲区:
interface buffer_write_if;
logic [6:0] x; // X坐标 (0–127)
logic [5:0] y; // Y坐标 (0–63)
logic wr_en; // 写使能
logic data; // 单bit像素值
endinterface
写操作将(x,y)转换为字节地址与位偏移:
always_comb begin
byte_addr = {y[5:3], x[6:0]}; // 高3位行号 + 7位列号 → 字节索引
bit_pos = y[2:0]; // 行内bit位置 (0~7)
end
读操作:逐行扫描输出
读操作由LCD驱动模块发起,在水平消隐期间按行读取,每次输出一个字节(8行并行)。使用状态机控制扫描顺序:
stateDiagram-v2
[*] --> Idle
Idle --> ReadLine : Start_Scan
ReadLine --> ShiftOut : Load_Byte
ShiftOut --> ShiftOut : Shift_Clock++
ShiftOut --> ReadLine : Shift_Done?
ReadLine --> Done : Line_Count == 64
Done --> [*]
读写冲突仲裁
当读写同时请求同一内存单元时,采用“ 读优先 ”原则,写操作插入等待周期。若启用双缓冲,则写入目标页面与当前显示页面分离,从根本上规避冲突。
乒乓切换通过顶层控制信号实现:
reg current_page; // 0: Page A, 1: Page B
always @(posedge clk) begin
if (update_done && !displaying_update_page)
current_page <= ~current_page;
end
6.3 外部数据文件加载与解析
预设图像(如开机LOGO)通常以十六进制文本文件形式提供,如 pagedata1.txt ,格式示例如下:
0x00 0xFF 0x81 0xBD ... // 第0行
0x00 0x00 0xFE 0x02 ...
0xFF 0xFF 0xFF 0xFF // 第63行
每行包含16个字节(128列 / 8),共64行,总计1024字节。
数据解析模块实现
设计一个有限状态机,在初始化阶段自动加载文件内容至指定缓冲区:
typedef enum logic[1:0] {
IDLE, WAIT_ACK, READ_BYTE, UPDATE_MEM
} load_state_t;
always_ff @(posedge clk or posedge rst) begin
if (rst) state <= IDLE;
else case (state)
IDLE:
if (load_req) state <= WAIT_ACK;
WAIT_ACK:
if (uart_rx_done) state <= READ_BYTE;
READ_BYTE:
if (valid_hex) state <= UPDATE_MEM;
UPDATE_MEM:
state <= (addr < BUFFER_SIZE-1) ? WAIT_ACK : IDLE;
endcase
end
ASCII到二进制转换函数(伪代码):
function byte ascii_to_byte(input [7:0] hi, lo);
byte out = 0;
out |= (hi >= 'A') ? (hi - 'A' + 10) << 4 : (hi - '0') << 4;
out |= (lo >= 'A') ? (lo - 'A' + 10) : (lo - '0');
return out;
endfunction
该模块通过UART接收上位机发送的HEX流,边解析边填充BRAM,支持远程动态更新画面。
6.4 完整嵌入式显示系统集成方案
各模块整合后的顶层结构如下图所示:
graph TD
A[自定义字符编码] --> F[像素缓冲区]
B[图形绘制引擎] --> F
C[外部HEX加载] --> F
F --> D[LCD控制器模块]
D --> E[12864 LCD]
G[系统时钟/复位] --> All
上电流程包括:
1. 初始化双缓冲区,加载 pagedata1.txt 至Page A
2. 启动LCD控制器状态机执行初始化序列
3. 开启自动扫描,从Page A读取数据
4. 动态信息(时间、状态)叠加至Page B
5. 帧同步后切换显示至Page B
为支持多页面切换,可扩展地址映射:
// 支持4个预设页面
localparam PAGE_ID_WIDTH = 2;
assign bram_addr = {page_sel, byte_addr};
此外,预留SPI从机接口,允许MCU远程更新任意区域内容,提升系统可维护性。
简介:本项目基于FPGA平台,使用Verilog硬件描述语言实现对128×64分辨率LCD显示器的驱动控制,不依赖预置字符库,支持自定义图形与文本显示。通过手动控制每个像素,项目提升了显示内容的灵活性和可定制性。核心文件包括LCD控制模块(lcd.v)及用于存储显示数据的页数据文件(pagedata1.txt、pagedata2.txt),涵盖时序控制、数据传输、像素缓冲和命令生成等关键逻辑。该项目适用于学习FPGA开发流程、Verilog设计方法以及嵌入式显示技术的实际应用。

8076


被折叠的 条评论
为什么被折叠?



