☰
FPGA驱动LCD1602:手写Verilog状态机的完整工程实践
2026/10/13 22:41:21 网站建设 项目流程

简介:面向FPGA初学者与嵌入式系统开发者,这是一份基于Verilog语言在FPGA平台上驱动LCD1602液晶显示屏的完整工程设计包,重点解决字符型液晶显示中的时序控制、指令配置与数据刷新等核心问题。资源共95个文件,压缩包仅654KB,核心内容包含LCD1602.v等Verilog源文件、Quartus工程配置文件(qpf/qsf)、编译下载文件(sof/pof)以及综合布局布线报告(rpt、summary)等,覆盖从源码编写到工程实现与验证的完整流程。已有481人学习下载。该工程提供完整的LCD1602驱动实现,读者可直接查看源码中时序控制、数据传输、指令组织等模块的编写思路,也可依据编译报告了解资源占用与设计收敛情况,适合FPGA入门阶段学习液晶显示驱动原理与Verilog工程实践。

1. LCD1602 的 FPGA 驱动:为什么放着现成 IP 不用,非要自己写 Verilog

很多人第一次在 FPGA 上点亮 LCD1602,以为跟单片机一样,对着数据手册把引脚电平拉一拉、延时时长凑一凑就能出字。真上手才发现,1602 的时序逻辑并不复杂,但它对“建立时间、保持时间、忙标志”这些边界条件的敏感程度,足够让一块刚跑起来的板子黑屏半天。用 Verilog 在 FPGA 上驱动 LCD1602,本质是在用硬件状态机去还原一颗慢速外设的时序协议,而且这颗外设慢到 FPGA 的 50MHz 主频对它来说简直是降维打击——你根本不是“跑不满”,而是要想办法“慢下来”。

这个方案的现实价值在于:FPGA 工程里经常需要一块廉价、通用、可视化的人机交互窗口,LCD1602 不依赖操作系统、不占 CPU、接口电平 3.3V/5V 兼容,是调试和展示状态的优选器件。本文就把完整的 Verilog 驱动、初始化序列、状态机设计和常见翻车点一次说清,适合刚能跑通流水灯的入门者,也适合资源紧张、不想引入现成 IP 核的进阶开发者。直接讲能落地的方案,不绕弯子。

2. 从数据手册到时序图:先把 1602 的脾气摸透再动手

2.1 1602 的接口电平与引脚规划:哪些必须接、哪些可以省

设计驱动之前,先确认信号的物理连接。LCD1602 的标准接口是 16 脚,但真正需要 FPGA 控制的信号只有 6 个:RS(寄存器选择)、RW(读写选择)、E(使能)、以及 8 位数据总线 D0-D7。在绝大多数只写不读的应用场景里,RW 可以直接接地,数据总线可以只接高 4 位 D4-D7,用 4 位模式传输,这样能省下 4 个 FPGA 引脚。实际工程里我一般这样规划:

信号方向作用接法建议
RSFPGA -> LCD0 写命令 / 1 写数据普通 IO
RWFPGA -> LCD0 写 / 1 读只写时接地
EFPGA -> LCD使能脉冲,下降沿锁存普通 IO
D4-D7FPGA -> LCD4 位数据总线普通 IO
V0模拟对比度调节接电位器分压
A/K电源背光串电阻接电源

这里有个容易误判的地方:很多人会把 RW 也接出来控制读取忙标志(BF),但在 FPGA 驱动里,初始化完成后写入命令的间隔可以用固定延时替代,完全不需要读忙。这样既省了引脚,也让状态机更简单——纯写模式不存在三态门控制问题,数据总线方向永远是输出。

2.2 时序参数拆解:用表格定死状态机的时间约束

LCD1602 数据手册里最核心的是一组建立/保持时间参数:E 脉冲宽度、RS 和 RW 相对 E 的建立时间、数据相对 E 下降沿的保持时间。把这些参数从手册抽象出来,才能变成状态机的状态转移条件。常用写操作时序参数如下:

参数最小值说明
E 脉冲高电平宽度400ns这是核心时间,决定 E 翻转节奏
RS 建立时间100nsRS 必须先于 E 上升沿稳定
RS 保持时间30nsE 下降沿后 RS 需保持
数据建立时间80nsD4-D7 须先于 E 下降沿稳定
数据保持时间30nsE 下降沿后数据保持
命令间隔40us(绝大多数命令)两条命令之间最短间隔
清屏等待1.5ms0x01 清屏指令后的特殊等待

FPGA 是 50MHz 时,一个周期就是 20ns。上面最短的时间参数也都大于一个时钟周期,所以驱动代码里几乎不需要担心“来不及建立”,反而要担心“延续太久”——比如 E 高电平如果保持了几百个时钟周期,虽然看起来没错,但会让整条显示刷新链路变慢。正确做法是让状态机给每个阶段分配精确的计数器周期数,既不超时也不浪费。时序参数最终全部折算成“在多少个时钟周期后拉高/拉低某个信号”,这就是状态机设计的全部依据。

3. 驱动架构拆解:分频器、状态机与顶层模块怎么组织

3.1 模块划分与数据流:让每个 Verilog 文件只干一件事

一个可维护的 1602 驱动工程,通常拆成 4 个文件:顶层模块负责例化;系统分频模块把 50MHz 降到 1kHz 左右的“节拍时钟”或者直接用计数器产生周期标志;核心驱动模块是一个大状态机,负责初始化、写命令、写数据;显示数据来源可以是 ROM、拨码开关或者 UART 收来的数据。这里给出最常用的顶层例化结构:

module lcd1602_top( input wire clk_50m, input wire rst_n, output wire lcd_rs, output wire lcd_rw, output wire lcd_e, output wire [3:0] lcd_data ); wire clk_1kHz; // 分频模块:把 50MHz 分频得到慢速节拍 clk_div #(.DIV_N(50000)) u_clk_div( .clk_in (clk_50m), .rst_n (rst_n), .clk_out(clk_1kHz) ); // 核心驱动:状态机 + 显示数据生成 lcd1602_driver u_driver( .clk (clk_1kHz), .rst_n (rst_n), .lcd_rs (lcd_rs), .lcd_rw (lcd_rw), .lcd_e (lcd_e), .lcd_data (lcd_data) ); endmodule

这里的关键决定是:驱动模块的时钟直接用分频后的慢时钟,而不是在状态机里再用计数器去卡时序。这样做的直接好处是状态机每个状态恰好对应一个慢时钟周期,仿真和调试时波形一目了然。lcd_rw 在顶层直接拉低,也可以是 wire 型常量,但建议保留端口,方便以后要接读操作时只改驱动不换顶层。

3.2 核心状态机的设计:十一个状态把“慢时序”拆成“快逻辑”

状态机的核心任务是把初始化序列、写命令、写数据三个工作阶段统一成一张状态转移表。每个状态的名字对应一个时间阶段,状态转移条件就是计数器是否数满。下面给出我常用的状态定义与转移逻辑:

// 参数说明: // IDLE:等待启动,拉高 E,保持总线空闲 // SETUP:拉高 RS 和设置数据,等待 tAS // E_HIGH:拉高 E,等待脉冲宽度 // HOLD:保持数据,等待 tAH // NEXT:计数器递减,决定整条命令结束后回到初始还是进入下一条 localparam IDLE = 4'd0; localparam SETUP = 4'd1; localparam E_HIGH = 4'd2; localparam HOLD = 4'd3; localparam NEXT = 4'd4; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else begin case (state) IDLE: begin lcd_e <= 1'b0; if (start_cmd) state <= SETUP; end SETUP: begin lcd_rs <= cmd_rs; lcd_data <= cmd_data; state <= E_HIGH; end E_HIGH: begin lcd_e <= 1'b1; // 这里用慢时钟,1 个周期即 1ms,满足 400ns 脉冲要求 state <= HOLD; end HOLD: begin lcd_e <= 1'b0; state <= NEXT; end NEXT: begin state <= IDLE; end endcase end end

每个状态在慢时钟下都是 1ms,远超手册要求的最小时间。SETUP 到 E_HIGH 之间没有额外延时,因为慢时钟本身已经保证了建立时间;E_HIGH 只持续一个周期,之后立即拉低形成下降沿,数据在 HOLD 阶段继续保持。这个设计思路的关键是把“时间长度”转化为“状态数量”,而不是在一个状态里反复计数。

4. 初始化序列与显示写入:把手册命令变成可复现的 Verilog 逻辑

4.1 初始化序列的完整流程:从 8 位模式切换到 4 位模式的玄机

1602 上电后默认是 8 位模式,要切换成 4 位模式必须按特定顺序发三条特殊指令,这是最容易写错也最不容易排查的部分。常见做法是:上电延时 >15ms 后,先写 0x03 三次,再写 0x02 进入 4 位模式,然后依次发送功能设置、显示开关、清屏、进入模式设置。用 Verilog 实现时,这几条命令要放进一个 8 个状态的初始化数组里:

// 初始化命令序列:{RS, 数据} // 注意前三条 0x03 走的是 8 位模式,数据要放到高 4 位 localparam INIT_CMD_CNT = 7; reg [7:0] init_seq [0:INIT_CMD_CNT-1]; initial begin init_seq[0] = 8'b00_0011; // 8位模式指令1 init_seq[1] = 8'b00_0011; // 8位模式指令2 init_seq[2] = 8'b00_0011; // 8位模式指令3 init_seq[3] = 8'b00_0010; // 切换4位模式 init_seq[4] = 8'b00_0010; // 功能设置:4位、2行、5x7 init_seq[5] = 8'b00_0000; // 显示开关:显示开、光标关、闪烁关 init_seq[6] = 8'b00_0000; // 清屏 init_seq[7] = 8'b00_0000; // 进入模式:地址递增、不移动 end

注意 init_seq[4] 到 [7] 的高 4 位里其实还藏着低位数据,比如功能设置的完整指令是 0x28,在 4 位模式下要分两次发送:先发高 4 位 0x2,再发低 4 位 0x8。上面数组里只列了高 4 位,实际驱动代码里每条指令都要拆成两个状态去写。这是 4 位模式最容易踩的坑——数组里的数据看着像完整命令,实际只发送了一半。

4.2 写命令与写数据的状态机统一:用 RS 信号区分两种操作

初始化完成后,显示字符的流程就变成固定的“两个 4 位写周期”。假设要在第一行第一个位置显示字符 A,ASCII 码为 0x41,那么发送顺序是:写命令 0x80(设置 DDRAM 地址为 0)→ 写数据高 4 位 0x4 → 写数据低 4 位 0x1。这个“高低分裂”的过程可以直接用一个统一的写入状态机处理:

// 统一的写入状态机,适用于初始化命令和显示数据 // cmd_data 是完整 8 位数据,cmd_rs 区分命令/数据 // 状态机分为发送高4位和发送低4位两个阶段 reg [1:0] nibble_phase; // 0:高4位 1:低4位 reg [4:0] cmd_index; // 当前命令在数组中的索引 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin nibble_phase <= 2'b00; end else begin case (state) SETUP: begin // 根据当前阶段选择发送高4位或低4位 if (nibble_phase == 2'b00) lcd_data <= cmd_data[7:4]; else lcd_data <= cmd_data[3:0]; nibble_phase <= nibble_phase + 1'b1; end NEXT: begin // 低4位发送完成后,命令结束,更新命令索引 if (nibble_phase == 2'b11 && init_done == 1'b0) begin cmd_index <= cmd_index + 1'b1; end end endcase end end

这个状态机最大的好处是初始化序列和后期数据显示共用一套逻辑,所谓“写命令”和“写数据”的区别只在于 cmd_rs 的电平,以及命令结束后是否要跳转到下一个初始化步骤。显示数据来源可以是 ROM 里预存的字符串,也可以是 UART 接收模块写入的 RAM,只要在 cmd_index 和 cmd_data 的赋值逻辑上做切换。

5. LCD1602 驱动避坑指南:现象、原因、解决办法的 4 条实录

5.1 上电后第一行出现黑色方块,第二行无显示

现象是背光亮、对比度可调,但第一行 16 个格子全是黑块。原因是初始化序列中的清屏命令没有正确执行,或 4 位模式切换没有完成——最常见的是第三条 0x03 没有发送足三次,导致 1602 内部还在等待 8 位模式的后续数据。解决方法是严格复现数据手册的上电时序,前三次 0x03 之间加 5ms 以上延时,第三次发完后必须等待 4.1ms 再发 0x02。这个坑的特征是:换一块 1602 也一样,重新上电也一样,可以确诊为初始化流程问题。

5.2 字符显示乱码:明明发的是 A,显示出来却是随机字符

现象是字符能点亮但内容不对,有时同一个位置反复显示不同字符。原因大概率是 4 位模式高低 nibble 发送顺序反了,或者数据建立时间不够。需要再次强调:4 位模式下必须先发高 4 位再发低 4 位,中间还要保证一个完整的 E 下降沿;如果两个 nibble 在同一个 E 脉冲里连发,1602 会把它们当成一个 8 位数据同时锁存。解决方法是检查状态机中每个 SETUP 状态是否严格对应一次 E 拉高拉低,不要贪图省状态把两次发送合并。

5.3 初始化偶尔成功,断电重来又失败

现象极具迷惑性:刚下载比特流时显示正常,按一下复位键或者断电再上电就黑屏。原因是复位信号拉低时间不够,1602 内部的上电复位还没有完成,FPGA 就开始发指令;或者慢时钟分频系数在复位期间没有归零,导致第一条指令在错误的节拍上发出。解决方法是让 FPGA 复位后先保持驱动状态机在 IDLE 至少 20ms,用一个专门的延时计数器确保 1602 内部电源稳定后再开始初始化。这个坑最容易出现在用到 PLL 的工程里,PLL 锁定时间本身就有几十毫秒。

5.4 显示稳定但周期性地闪一下

现象是字符整体正常,但每隔几秒会有一个极短的闪烁或暗一下。原因不是 1602 本身,而是 FPGA 里其他模块在动态刷新 LCD 总线——比如有人在顶层做了引脚复用,或者 GPIO 输出寄存器被意外写入了新值。排查方法很简单,把驱动模块的 lcd_data 和 lcd_e 在 ChipScope / SignalTap 里单独观察,如果发现异常毛刺,再往上层追。另一个常见原因是分频器复位不彻底,导致慢时钟出现毛刺。解决方法是给所有输出信号加寄存器打拍,不要在组合逻辑里直接驱动 LCD 引脚。

6. 让驱动更实用:显示多行滚动文本与自定义字符的进阶方案

初始化跑通只是第一步,实际工程里更多人需要的是“动态刷新”——比如显示传感器读数、CPU 利用率或调试信息。LCD1602 的 DDRAM 只有 80 字节,每次只能显示 16 个字符,所以动态刷新的核心是管理好地址指针和刷新节奏。我常用的技巧是定义一个双端口 RAM,UART 或 ADC 模块往 RAM 里写,LCD 驱动状态机每隔一定时间把 RAM 内容整体搬到屏幕上。刷新周期建议控制在 200ms 以上,否则字符切换时的残影会让显示看起来像在抖动。

自定义字符是另一个值得做的小功能。1602 内置 CGRAM 空间可以定义 8 个 5x8 点阵字符,用普通命令序列就能写入。做法是先发送命令 0x40 + 字符编号 × 8 设置 CGRAM 地址,然后按行写入 8 字节点阵数据,最后在 DDRAM 里用对应编号显示。这个功能在显示进度条、信号强度、箭头图标时特别有用,比拼接 ASCII 字符效果专业得多。写 Verilog 时可以在 ROM 里预存点阵模板,初始化完成后自动写入。

最后说一个我自己的操作习惯:每次改完驱动,先做行为仿真,把 E 引脚、RS、数据的波形对照数据手册的时序图逐项核对,确认每个时间参数都比最小值大两倍以上再上板。这个习惯帮我避开过无数次“看起来能工作但稍降温就罢工”的边界问题。1602 是慢速器件,FPGA 是快速器件,两者之间所有问题的本质都是“快的不肯等慢的”。希望这些方案和踩坑记录能帮你少走几轮弯路,把时间花在功能调试而不是和液晶屏较劲上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询