FPGA驱动16x2字符LCD:从HD44780协议到Verilog状态机实战

📅 2026/8/6 3:39:53
FPGA驱动16x2字符LCD:从HD44780协议到Verilog状态机实战
1. 项目概述当FPGA遇上经典字符屏如果你手头有一块Mojo v3 FPGA开发板又恰好从某个旧设备上拆下来一块经典的16x2字符液晶屏LCD想把它们连起来玩点花样比如显示个“Hello World”、做个简易计数器或者显示传感器数据那你来对地方了。这个项目看似简单却是打通FPGA数字逻辑世界与外部物理设备通信的绝佳入门实践。它不涉及复杂的图像处理核心在于理解并实现一种古老但极其通用的通信协议——HD44780兼容接口这对于任何想用FPGA做嵌入式控制、人机交互的开发者来说都是必须掌握的基本功。Mojo v3基于Xilinx Spartan-6 FPGA以其简洁的硬件设计和活跃的社区生态著称特别适合学习和原型开发。而16x2 LCD模块以其低廉的成本、清晰的显示和简单的接口历经数十年依然是仪表盘、小型设备的首选。将这两者结合你不仅能学会如何用Verilog或VHDL去“模拟”一个微控制器的时序更能深刻理解FPGA并行处理、精确时序控制的优势。接下来我会带你从原理到引脚连接从状态机设计到代码调试完整走通整个流程。你会发现用FPGA驱动LCD比用单片机更底层、更自由也更能锻炼你的硬件描述语言和数字设计思维。2. 核心原理与接口协议深度解析2.1 16x2 LCD模块与HD44780控制器内幕我们常说的16x2 LCD模块其核心是一块名为HD44780或其兼容品如KS0066、ST7066的控制器芯片。这块芯片内部集成了显示RAMDDRAM、字符发生器ROMCGROM以及控制逻辑。对我们而言它就像一个拥有简单指令集的“外设”我们通过发送指令和数据来控制它。模块通常提供两种接口模式8位并行和4位并行。为了节省宝贵的FPGA I/O引脚本项目采用4位模式。这意味着数据分两次高4位、低4位传输。模块背面的16个引脚中最关键的是VSS/VDD/VEE 地、电源5V、对比度调节通常接电位器到地。RS (Register Select) 寄存器选择。0表示发送的是指令如清屏、光标移动1表示发送的是要显示的字符数据。RW (Read/Write) 读写选择。0写1读。在绝大多数驱动场景下我们只写不读所以此引脚可以直接接地。E (Enable) 使能信号。这是一个下降沿触发的锁存信号是通信的“快门”。数据在E引脚的高电平期间建立在E的下降沿被LCD控制器锁存。D0-D7 8位数据线。在4位模式下我们只使用高4位D4-D7。通信的本质就是在正确的时序下控制RS、E和D4-D7这6个信号线的电平变化。2.2 FPGA驱动LCD的核心挑战与设计思路用FPGA驱动LCD与用单片机有本质区别。单片机有顺序执行的CPU可以方便地用delay_ms()函数产生微秒级的延时。而FPGA是并行执行的我们需要用硬件逻辑来构建精确的时序状态机。核心挑战在于生成满足HD44780时序规范的各种脉冲宽度尤其是E使能信号的脉冲宽度典型值450ns和命令/数据建立保持时间典型值40ns。在Mojo v3上其系统时钟通常是50MHz或100MHz周期20ns或10ns我们可以通过计数器来产生这些延时。基本设计思路是构建一个有限状态机FSM。这个状态机负责初始化序列 上电后必须按照HD44780数据手册的规定执行一系列固定的指令来设置4位模式、显示行数、字体等。指令/数据发送状态 将待发送的8位指令或数据分成两个4位“半字节”Nibble按照“设置RS和RW - 放置高4位数据 - 产生E脉冲 - 放置低4位数据 - 产生E脉冲”的流程通过状态跳转依次完成。忙状态检测与等待 在发送某些指令如清屏、移光标后LCD内部需要时间执行此时它会置忙标志BF。我们可以通过读取BF位来等待但更常见的简化做法是发送指令后插入一个固定的、足够长的延时例如清屏后延时1.6ms以上。对于FPGA初学者采用固定延时法更简单可靠。因此我们的FPGA设计将包含几个核心模块一个顶层模块负责连接Mojo板载资源一个LCD驱动状态机核心模块一个可能存在的分频或延时计数器模块以及一个用于生成待显示字符数据的逻辑可能是ROM、计数器或来自其他模块的数据。3. 硬件连接与Mojo v3引脚分配实战3.1 物料清单与电路连接详解你需要准备Mojo v3 FPGA开发板。16x2字符LCD模块确保是5V工作电压3.3V的也有但较少见。一个10kΩ的电位器用于调节对比度。若干杜邦线母对母。连接电路如下这是最常用的4位模式接法LCD VSS (Pin 1)-Mojo GND。LCD VDD (Pin 2)-Mojo VUSB (5V)。注意Mojo的VUSB在USB供电时提供5V这是LCD需要的电压。切勿接到3.3V引脚可能导致显示暗淡或不工作。LCD VEE (Pin 3)-电位器的中间抽头。电位器另外两端分别接VUSB和GND。旋转电位器可以调节屏幕对比度直到字符清晰显示。LCD RS (Pin 4)-连接到Mojo任意一个IO引脚例如P77对应Mojo板上标号77的引脚。LCD RW (Pin 5)-直接连接到GND。因为我们只写不读。LCD E (Pin 6)-连接到Mojo另一个IO引脚例如P78。LCD D4 (Pin 11)-连接到Mojo IO引脚例如P79。LCD D5 (Pin 12)-连接到Mojo IO引脚例如P80。LCD D6 (Pin 13)-连接到Mojo IO引脚例如P81。LCD D7 (Pin 14)-连接到Mojo IO引脚例如P82。LCD背光阳极A (Pin 15)-通过一个约100Ω的限流电阻接VUSB (5V)。如果不接电阻长期使用可能损坏背光LED。LCD背光阴极K (Pin 16)-接GND。注意 务必仔细核对LCD模块的引脚图不同厂家的模块引脚顺序可能略有差异。上述引脚编号是基于最常见的标准。3.2 Mojo v3约束文件.ucf编写要点在FPGA开发中仅仅在代码里给信号命名是不够的必须通过约束文件告诉工具哪个逻辑信号对应物理板上的哪个引脚。对于Mojo v3我们使用Xilinx的UCFUser Constraints File格式。你需要创建一个.ucf文件例如mojo.ucf并在其中添加如下内容假设使用上述引脚连接NET “lcd_rs” LOC P77; NET “lcd_e” LOC P78; NET “lcd_db4” LOC P79; # D4 NET “lcd_db5” LOC P80; # D5 NET “lcd_db6” LOC P81; # D6 NET “lcd_db7” LOC P82; # D7这里lcd_rs、lcd_e、lcd_db[7:4]是你Verilog代码中对应输出信号的名称。LOC属性指定了物理引脚。Mojo v3的引脚编号印在板子上非常方便。实操心得 连接硬件时先不要插LCD。用万用表测量一下VUSB和GND之间是否为5V确保电源正常。接上LCD后如果屏幕全黑或全白方块首先调节电位器改变对比度。如果背光不亮检查背光电路和限流电阻。4. Verilog驱动状态机设计与实现4.1 状态定义与顶层模块设计我们首先定义状态机的所有状态。一个健壮的驱动通常包含以下状态localparam [3:0] S_POWER_ON_WAIT 4‘d0, // 上电延时等待约15ms S_FUNC_SET_A 4’d1, // 功能设置指令第一次8位模式尝试 S_FUNC_SET_B 4‘d2, // 功能设置指令第二次 S_FUNC_SET_C 4’d3, // 功能设置指令第三次确定4位模式 S_FUNC_SET_4BIT 4‘d4, // 设置4位模式、2行显示、5x8字体 S_DISP_CTRL 4’d5, // 显示控制开显示关光标不闪烁 S_DISP_CLEAR 4‘d6, // 清屏指令 S_ENTRY_MODE 4’d7, // 进入模式设置地址递增不移位 S_IDLE 4‘d8, // 初始化完成等待发送数据 S_SEND_HIGH 4’d9, // 发送高4位数据并拉高E S_SEND_LOW 4‘d10, // 发送低4位数据并拉高E S_PULSE_E_H 4’d11, // 产生E下降沿高4位 S_PULSE_E_L 4‘d12, // 产生E下降沿低4位 S_DELAY 4’d13; // 指令执行后延时顶层模块lcd_driver的接口大致如下module lcd_driver ( input wire clk, // 50MHz系统时钟 input wire rst, // 复位信号 input wire [7:0] data_in, // 要发送的字符数据ASCII码 input wire rs_in, // 当前的RS值0指令1数据 input wire start, // 发送启动信号 output reg lcd_rs, // 输出到LCD的RS output reg lcd_e, // 输出到LCD的E output reg [3:0] lcd_db, // 输出到LCD的D7-D4 output reg busy // 忙标志为1时表示状态机正在处理无法接收新命令 );4.2 初始化序列的精确实现初始化是驱动成功与否的关键。HD44780上电后内部状态不确定必须遵循严格的时序。以下是4位模式的初始化流程在状态机中的实现逻辑上电等待(S_POWER_ON_WAIT) 上电或复位后进入此状态。启动一个计数器计数到750_00050MHz时钟下15ms。时间到则转入下一步。三次功能设置8位模式(S_FUNC_SET_A/B/C) 这是为了强制LCD进入已知状态。依次发送三次0x30即数据线D7-D4 3‘b0011。注意此时我们仍假设是8位模式但只操作高4位低4位忽略。每次发送后需要等待至少4.1ms或更长。设置4位模式(S_FUNC_SET_4BIT) 发送0x20即0010。从此条指令开始LCD正式进入4位模式后续所有指令和数据都必须分两次发送。功能设置详细 发送指令0x28。注意在4位模式下发送0x28需要分两次先发高4位0x2再发低4位0x8。这条指令的含义是4位接口、2行显示、5x8点阵字体。显示开关控制(S_DISP_CTRL) 发送指令0x0C先0x0后0xC。含义打开显示D1关闭光标C0关闭光标闪烁B0。清屏(S_DISP_CLEAR) 发送指令0x01。发送后必须等待至少1.64ms。进入模式设置(S_ENTRY_MODE) 发送指令0x06。含义读写后地址指针自动加1I/D1整屏显示不移动S0。完成以上步骤后状态机进入S_IDLE等待上层逻辑通过start信号触发字符或指令的发送。注意事项 初始化过程中的延时如15ms, 4.1ms, 1.64ms必须得到保证。一个常见的错误是计数器位宽不够导致计数溢出后实际延时远小于预期。例如50MHz时钟下计数1ms需要50_000个周期15ms就需要750_000这需要至少20位的计数器2^201,048,576。务必根据时钟频率和所需延时计算清楚计数器的最大值。4.3 数据发送状态机与时序控制当状态机在S_IDLE且检测到start信号为高时开始发送流程。假设要发送的8位数据data_in和寄存器选择信号rs_in已经准备好。锁存输入 立即将data_in和rs_in锁存到内部寄存器并将busy信号置为1。进入S_SEND_HIGH 将lcd_rs设置为锁存的rs_in值。将lcd_db设置为data_in[7:4]高4位。然后转入S_PULSE_E_H。S_PULSE_E_H 将lcd_e拉高。启动一个短延时计数器例如计数10个周期约200ns远大于E脉冲宽度要求的最小450ns。延时结束后将lcd_e拉低产生下降沿锁存高4位数据。转入S_SEND_LOW。S_SEND_LOW 将lcd_db设置为data_in[3:0]低4位。然后转入S_PULSE_E_L。S_PULSE_E_L 同样拉高lcd_e延时再拉低锁存低4位数据。S_DELAY 数据/指令发送完成后需要根据发送的内容插入不同的延时。如果发送的是清屏0x01或归位0x02指令需要长延时1.64ms。如果发送的是其他指令或数据短延时即可40us。这里可以简化处理对于绝大多数指令和数据统一使用一个约50us的延时对于清屏指令在发送后特殊处理一个长延时。返回S_IDLE 延时结束后将busy信号拉低表示一次发送完成可以接收下一次发送请求。这个状态机确保了每个信号的变化都满足HD44780数据手册中规定的建立时间t_DS、保持时间t_H和E脉冲宽度t_PW。5. 上层逻辑与字符显示示例5.1 构建字符发送控制器有了底层的lcd_driver模块我们还需要一个上层模块例如lcd_top来组织要显示的内容并按照正确的顺序和时机调用驱动模块。这个控制器也是一个状态机或者是一个简单的序列发生器。例如我们想在第一行显示“Hello, Mojo!”在第二行显示“Count: 1234”。首先需要发送指令将DDRAM地址设置到第一行开头地址0x80。然后依次发送‘H’‘e’‘l’‘l’‘o’‘’‘ ’‘M’‘o’‘j’‘o’‘!’的ASCII码。接着发送指令将DDRAM地址设置到第二行开头地址0xC0。最后发送字符串“Count: ”以及分解后的数字‘1’‘2’‘3’‘4’。在Verilog中我们可以用一个ROM来存储这个初始化信息序列包括指令和字符数据以及对应的RS标志。控制器状态机按顺序从ROM中读取并启动lcd_driver进行发送等待busy变低后再读取下一个。5.2 动态内容显示以计数器为例显示静态字符串只是开始。更实用的场景是显示动态变化的数据比如一个每秒递增的计数器。我们可以设计一个每秒触发一次的定时器用系统时钟分频得到1Hz信号。每当定时器触发将一个4位的BCD计数器加1。然后上层控制器需要执行以下操作将计数器的值0-9转换为对应的ASCII码‘0’的ASCII码是0x30所以转换就是8‘h30 counter_value。将光标定位到显示数字的固定位置例如第二行第7列地址0xC6。启动驱动模块发送定位指令。发送转换后的ASCII码字符。这要求上层控制器能中断当前的显示序列处理动态更新。一种设计是让控制器有两个模式初始化模式和更新模式。初始化模式完成首次静态内容的显示后进入空闲状态。当检测到计数器更新事件时切换到更新模式执行上述定位和发送字符的短序列完成后又回到空闲状态。实操心得 在调试动态显示时最常见的错误是更新太快导致LCD来不及处理。务必确保在发送每条指令或数据后都等待了足够的延时或者严格检查了busy标志如果实现了忙检测。另一个技巧是在更改某一行的一部分内容时最好只重写需要更改的部分而不是重写整行这样可以避免屏幕闪烁。6. 仿真、调试与常见问题排查实录6.1 使用ModelSim/ISim进行行为仿真在烧录到板子之前仿真是验证逻辑正确性的关键。你需要编写一个测试平台Testbenchtb_lcd_driver.v。实例化被测模块 将lcd_driver模块在testbench中实例化。生成时钟和复位initial begin clk 0; forever #10 clk ~clk; // 50MHz时钟周期为20ns end initial begin rst 1; #100 rst 0; // 复位100ns后释放 end设计测试序列 在复位结束后模拟上层控制器的行为。例如等待初始化完成监测busy信号然后发送一个设置地址的指令rs_in0, data_in8‘h80再发送一个字符数据rs_in1, data_in8’h41即‘A’。观察波形 在仿真波形中重点观察lcd_e信号 是否产生了宽度足够的正脉冲两个脉冲对应高4位和低4位的间隔是否合理lcd_rs信号 在发送指令和数据时电平是否正确lcd_db信号 在lcd_e高电平期间数据是否稳定发送0x41‘A’时是否先出现0100高4位后出现0001低4位状态机state 是否按照预期的顺序跳转在每个延时状态停留的时间是否足够通过仿真你可以在不依赖硬件的情况下排除大部分逻辑设计和时序控制上的错误。6.2 板上调试与逻辑分析仪ILA的使用当代码通过仿真后就可以综合并烧录到Mojo v3了。如果LCD没有按预期显示硬件调试就至关重要。基础检查电源与对比度 再确认一遍。用万用表量VDD和VEE电压。旋转电位器覆盖整个范围。背光 背光是否亮起如果不亮检查限流电阻和连接。连线 杜邦线是否接触不良最好用万用表通断档逐一检查。使用集成逻辑分析仪ILA 这是最强大的调试工具。你可以在Vivado/ISE中实例化一个ILA IP核将lcd_rslcd_elcd_db 以及驱动模块内部的statebusy等关键信号连接到ILA的探针上。设置合适的触发条件例如在start信号上升沿时触发。重新生成比特流并下载到FPGA。运行后ILA会捕获这些信号的实际波形。将捕获到的波形与仿真波形或HD44780时序图进行对比。关键排查点E脉冲宽度是否太窄应450ns数据在E下降沿前后是否稳定建立/保持时间初始化序列的波形是否完整发出特别是前三次0x30和后续的0x28。状态机是否卡在某个状态如延时状态可能是计数器位宽或终值设置错误。6.3 常见问题速查表现象可能原因排查步骤屏幕全黑背光或电源问题1. 检查VDD(5V)和GND连接。2. 检查背光引脚(A/K)及限流电阻。3. 测量VEE电压调节电位器。屏幕全白有背光但无字符对比度设置不当或初始化失败1. 大幅调节电位器。2. 用ILA抓取初始化阶段的波形检查E、RS、DB信号是否正常。显示乱码或部分字符错误数据线连接错误或时序不佳1. 核对D4-D7连线是否与代码定义顺序一致。2. 用ILA检查发送具体字符时的DB数据是否正确。3. 检查E脉冲宽度和数据建立/保持时间是否满足要求。仅第一行显示第二行不显示第二行地址设置错误或初始化未设置2行模式1. 确认初始化指令0x28已正确发送设置2行显示。2. 确认第二行起始地址为0xC0。显示内容闪烁或不稳定刷新过快或电源噪声1. 确保指令/数据间有足够延时。2. 在VDD和GND之间靠近LCD引脚处加一个10uF-100uF的电解电容滤波。FPGA配置后无任何反应引脚约束错误或顶层模块未正确例化1. 检查.ucf文件中的引脚分配与实际连线是否一致。2. 检查代码中输出信号名与约束文件中NET名是否完全一致大小写敏感。3. 确认Mojo的mojo_top模块正确例化了你的LCD驱动模块。一个关键的调试技巧如果ILA显示波形完全正确但LCD还是不亮可以尝试一个最简单的测试——绕过状态机用拨码开关或写一个最简单的计数器循环输出固定的初始化序列。例如用一个计数器每隔几秒循环输出0x280x0C0x01等指令的波形。这可以排除状态机设计复杂性的干扰直接验证FPGA引脚输出能力和LCD模块本身的好坏。