FPGA新手避坑指南:用Verilog在DE2-115上驱动LCD1602,从静态到滚动显示(附完整代码)
FPGA实战:DE2-115开发板驱动LCD1602的Verilog全流程解析
第一次接触FPGA驱动LCD显示器的开发者,往往会在时序控制、状态机设计和硬件调试等环节遇到各种"坑"。本文将基于Altera DE2-115开发板和LCD1602字符液晶模块,通过完整的Verilog实现案例,带你从硬件连接到动态显示,逐步攻克FPGA外设驱动开发中的典型难题。
1. 硬件连接与基础配置
1.1 DE2-115开发板接口定义
DE2-115开发板上的LCD1602接口采用16引脚单排插座,关键信号线包括:
| 信号名称 | FPGA引脚号 | 说明 |
|---|---|---|
| LCD_E | PIN_Y4 | 使能信号,上升沿触发 |
| LCD_RS | PIN_Y3 | 数据/指令选择(1/0) |
| LCD_RW | PIN_W4 | 读写控制(固定接地) |
| LCD_DB | PIN_W1-PIN_W7 | 8位数据总线 |
注意:DE2-115的LCD模块不含背光控制电路,LCD_BLON信号无需连接
1.2 初始化参数实测优化
根据LCD1602数据手册,典型初始化时序要求:
// 初始参数(理论值) parameter T_PW_E = 150; // E脉冲宽度(ns) parameter T_CYCLE = 1000; // 指令周期(ns)但在实际测试中发现,DE2-115开发板需要更保守的时序配置:
// 优化后参数(实测稳定值) parameter T_PW_E = 1000; // E脉冲宽度调整为1μs parameter T_CYCLE = 2000; // 完整周期延长至2μs这种差异主要源于:
- FPGA时钟抖动带来的时序不确定性
- 开发板走线引入的信号延迟
- LCD模块个体差异
2. Verilog状态机设计与实现
2.1 多状态控制架构
采用三段式状态机实现LCD驱动,包含11个主要状态:
localparam IDLE = 4'd0, // 上电延时 INIT = 4'd1, // 模式设置 S0 = 4'd2, // 关闭显示 S1 = 4'd3, // 清屏 S2 = 4'd4, // 光标设置 S3 = 4'd5, // 显示控制 ROW1_ADDR = 4'd6, // 首行起始地址 WRITE = 4'd7, // 数据写入 ROW2_ADDR = 4'd8, // 次行起始地址 STOP = 4'd9, // 静态显示保持 DYNAMIC = 4'd10; // 动态移位控制2.2 关键状态转换逻辑
动态显示模式的状态跳转条件:
always @(*) begin if(mode) begin // 动态模式 case(state_c) STOP: state_n = (cnt_400ms == 25'd20_000_000) ? DYNAMIC : STOP; DYNAMIC: state_n = STOP; // 其他状态转换... endcase end else begin // 静态模式 case(state_c) STOP: state_n = STOP; // 其他状态转换... endcase end end3. 时序调试技巧与排错
3.1 典型时序问题排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 显示乱码 | E脉冲宽度不足 | 增大T_PW_E参数 |
| 仅第一行显示正常 | 行地址切换时序错误 | 检查ROW1_ADDR到ROW2_ADDR跳转 |
| 字符显示不全 | 状态保持时间不够 | 增加各状态延时计数器 |
| 动态显示卡顿 | 移位间隔时间设置不当 | 调整cnt_400ms阈值 |
3.2 SignalTap II实时调试
在Quartus Prime中配置SignalTap逻辑分析仪,监控关键信号:
# SignalTap配置示例 set_instance_assignment -name SYNCHRONIZER_IDENTIFICATION AUTO -to lcd_en set_instance_assignment -name USE_SIGNALTAP_FILE stp1.stp -to * set_instance_assignment -name SIGNALTAP_FILE stp1.stp -to *捕获信号包括:
- 状态机当前状态(state_c)
- 使能信号(lcd_en)脉冲波形
- 数据/指令选择(lcd_rs)变化
- 数据总线(lcd_data)传输内容
4. 工程优化与进阶功能
4.1 固化程序到Flash存储器
DE2-115开发板程序固化步骤:
生成.jic文件:
quartus_cpf -c -d EPCS64 -s 10 -o auto_create_ram=on output_file.sof output_file.jic使用Programmer下载:
quartus_pgm -m jtag -o "p;output_file.jic@1"验证固化效果:
- 断开USB-Blaster
- 重启开发板观察自动加载
4.2 自定义字符生成技术
LCD1602支持8个5×8点阵自定义字符,生成步骤:
计算字符点阵数据:
# Python点阵转换示例 char_map = [ 0b01110, # 自定义字符数据 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001, 0b00000 ]Verilog中写入CGRAM:
// 写入自定义字符到CGRAM地址 lcd_data <= 8'h40; // CGRAM起始地址 lcd_rs <= 0; // 随后写入8字节点阵数据在DDRAM中调用:
lcd_data <= 8'h00; // 自定义字符0的地址 lcd_rs <= 1;
5. 完整工程代码解析
5.1 顶层模块接口定义
module lcd1602( input clk, // 50MHz系统时钟 input rst_n, // 低电平复位 input mode, // 显示模式选择 output lcd_on, // LCD电源控制 output reg lcd_rs, // 寄存器选择 output lcd_rw, // 读写控制(固定写模式) output reg lcd_en, // 使能信号 output reg [7:0] lcd_data // 数据总线 );5.2 动态显示核心逻辑
// 400ms移位间隔定时器 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_400ms <= 25'd0; end else if(state_c == STOP) begin if(cnt_400ms == 25'd20_000_000 - 1) begin cnt_400ms <= 25'd0; end else begin cnt_400ms <= cnt_400ms + 1'b1; end end end // 动态移位控制 always @(*) begin if (state_c == STOP && cnt_400ms == 25'd20_000_000 - 1) begin state_n = DYNAMIC; end else if (state_c == DYNAMIC) begin state_n = STOP; end end5.3 双行显示地址管理
// 行地址切换逻辑 always @(*) begin case(state_c) WRITE: begin if (char_cnt == 5'd15) begin state_n = ROW2_ADDR; // 切换到第二行 end else if (char_cnt == 5'd22) begin state_n = STOP; // 显示完成 end end ROW2_ADDR: begin lcd_data <= 8'hC4; // 第二行中间起始地址 lcd_rs <= 0; end endcase end6. 硬件调试实战记录
6.1 常见问题解决方案
问题1:下载后无任何显示
- 检查LCD对比度调节电位器
- 确认开发板供电正常(5V/2A)
- 测量LCD_VCC引脚电压(应为5V±0.5V)
问题2:显示内容错位
- 重新校准初始化时序
- 检查状态机中地址设置指令(0x80/0xC4)
- 确认字符计数器(char_cnt)位宽匹配
问题3:动态显示闪烁
- 优化移位间隔时间(300-500ms为宜)
- 增加状态转换时的消抖处理
- 检查模式切换信号(mode)的同步处理
6.2 性能优化建议
时钟分频优化:
// 将50MHz主时钟分频为1MHz reg [5:0] clk_div; always @(posedge clk) begin clk_div <= clk_div + 1'b1; end wire clk_1M = clk_div[5];资源共享设计:
// 共用延时计数器 always @(posedge clk) begin if (state_c != state_n) begin common_delay <= 0; end else begin common_delay <= common_delay + 1'b1; end end时序约束添加:
# SDC时序约束示例 create_clock -name lcd_clk -period 2000 [get_ports lcd_en] set_output_delay -clock lcd_clk 500 [get_ports {lcd_data[*] lcd_rs}]
7. 扩展应用与进阶方向
7.1 多语言显示实现
通过CGRAM自定义字符实现非ASCII字符显示:
- 设计汉字字模数据
- 分段写入CGRAM存储区
- 建立字符映射表:
reg [7:0] hanzi_table [0:7]; initial begin hanzi_table[0] = 8'h00; // 自定义字符0地址 // ...其他字符初始化 end
7.2 基于Nios II的软核控制
将LCD驱动封装为Avalon-MM外设:
// Nios II软件控制示例 void lcd_print(char* str) { IOWR(LCD_BASE, 0, 0x01); // 清屏 usleep(2000); while(*str) { IOWR(LCD_BASE, 1, *str++); usleep(100); } }7.3 实时数据显示系统
结合传感器实现动态更新:
- ADC数据采集模块
- 数字滤波处理单元
- ASCII转换逻辑:
always @(posedge clk) begin case(adc_data[3:0]) 4'h0: digit <= "0"; // ...其他数字转换 4'hF: digit <= "F"; endcase end
在完成这个项目的过程中,最耗时的部分不是Verilog代码编写,而是硬件调试阶段对各种时序参数的微调。建议开发者在仿真阶段就建立严格的时序检查机制,可以节省大量板上调试时间。LCD1602虽然是个简单的外设,但把它作为FPGA入门练手项目,能系统性地掌握状态机设计、时序分析和硬件调试等核心技能。
