所用板子为altera DE2板子,FPGA为Cyclone II:EP2C35F672C6,quartus版本为13.0
1.LCD规格与接口
DE2板子上的LCD为16*2,是最简单的LCD显示屏。
数据储存器地址为第一行00H~0fH,第二行40H~4fH。但是需要注意的是,在需要向数据存贮器赋值时,需要赋值为80H~8fH和c0H~cfH,因为只有第一位置1数据存贮器地址地址输入才为有效。
接下来看下LCD用户接口,在此截取DE2用户手册。
用户接口标注如下:
[7:0]Data //8位数据总线 LCD_EN //使能信号 LCD_RW //读/写选择信号 LCD_RS //数据/命令选择信号 LCD_BLON //背光灯亮灭 LCD_ON //总开关
其中,LCD_ON LCD_BLON可暂时不关注,使用时置为1即可。
LCD_EN LCD_RS LCD_RW Data在时序中需要格外关注。
2.LCD时序图
LCD的时序图极为简单,概括来讲就是:
在LCD_EN=1时进行数据交互(因此可令LCD_EN = CLK_LCD,数据操作均发生在时钟高有效时段)
在LCD_RW=0时写入数据,LCD_RW=1时读出数据(基本没有用到)
在LCD_RS=0时操作指令,LCD_RS=1时操作数据
整体时序图如下(来自网络),里面的细节时序可暂时忽略。
3.LCD基本操作指令
LCD的操作整体上分为两类:指令配置与数据读写显示。指令表如下(来源网络)。
指令的解读(摘取自网络):
指令1:清屏,光标同时复位至00H位置(左上角);
指令2:光标复位,即光标复位至00H;
指令3:光标与显示移动设置。I/D:光标移动方向,高电平右移,低电平左移;S:屏幕上所有文字是否左移或右移,高电平表示有效,低电平表示无效;
指令4:显示开关控制。D:控制整体的显示开与关,高电平表示开显示,低电平表示关显示。C:控制光标的开与关,高电平表示有光标,低电平表示无光标 B:控制光标是否闪烁,高电平闪烁,低电平不闪烁;
指令5:光标或显示移位 S/C :高电平时显示移动的文字,低电平时移动光标;
指令6:功能设置命令 DL:高电平时为4位总线,低电平时为8位总线 N:低电平时为单行显示,高电平时为双行显示,F:低电平时显示5X7的点阵字符,高电平时显示5X10的显示字符;功能设置命令 DL:高电平时为4位总线,低电平时为8位总线 N:低电平时为单行显示,高电平时为双行显示,F:低电平时显示5X7的点阵字符,高电平时显示5X10的显示字符。
指令7:字符发生器RAM地址设置;
指令8:DDRAM地址设置;
指令9:读忙信号和光标地址 BF:忙标志位,高电平表示忙,此时模块不能接收命令或数据,如果为低电平表示不忙。
4.如何显示一个字符
字符产生器CGROM中内置了192个常用字符,自定义字符产生器CGRAM还允许用户自定义8个字符。内置的192个字符如下
如要显示“A”,则需要在指令10阶段时候写入数据为8‘b0100_0001。
本次将大小写字母以参数形式写在了character.v中。主程序中可以直接调用。
parameter space = 8'b0010_0000; parameter CAPITAL_A = 8'b0100_0001; parameter CAPITAL_B = 8'b0100_0010; parameter CAPITAL_C = 8'b0100_0011; parameter CAPITAL_D = 8'b0100_0100; parameter CAPITAL_E = 8'b0100_0101; parameter CAPITAL_F = 8'b0100_0110; parameter CAPITAL_G = 8'b0100_0111; parameter CAPITAL_H = 8'b0100_1000; parameter CAPITAL_I = 8'b0100_1001; parameter CAPITAL_J = 8'b0100_1010; parameter CAPITAL_K = 8'b0100_1011; parameter CAPITAL_L = 8'b0100_1100; parameter CAPITAL_M = 8'b0100_1101; parameter CAPITAL_N = 8'b0100_1110; parameter CAPITAL_O = 8'b0100_1111; parameter CAPITAL_P = 8'b0101_0001; parameter CAPITAL_Q = 8'b0101_0001; parameter CAPITAL_R = 8'b0101_0010; parameter CAPITAL_S = 8'b0101_0011; parameter CAPITAL_T = 8'b0101_0100; parameter CAPITAL_U = 8'b0101_0101; parameter CAPITAL_V = 8'b0101_0110; parameter CAPITAL_W = 8'b0101_0111; parameter CAPITAL_X = 8'b0101_1000; parameter CAPITAL_Y = 8'b0101_1001; parameter CAPITAL_Z = 8'b0101_1010; parameter LOWERCASE_a = 8'b0110_0001; parameter LOWERCASE_b = 8'b0110_0010; parameter LOWERCASE_c = 8'b0110_0011; parameter LOWERCASE_d = 8'b0110_0100; parameter LOWERCASE_e = 8'b0110_0101; parameter LOWERCASE_f = 8'b0110_0110; parameter LOWERCASE_g = 8'b0110_0111; parameter LOWERCASE_h = 8'b0110_1000; parameter LOWERCASE_i = 8'b0110_1001; parameter LOWERCASE_j = 8'b0110_1010; parameter LOWERCASE_k = 8'b0110_1011; parameter LOWERCASE_l = 8'b0110_1100; parameter LOWERCASE_m = 8'b0110_1101; parameter LOWERCASE_n = 8'b0110_1110; parameter LOWERCASE_o = 8'b0110_1111; parameter LOWERCASE_p = 8'b0111_0001; parameter LOWERCASE_q = 8'b0111_0001; parameter LOWERCASE_r = 8'b0111_0010; parameter LOWERCASE_s = 8'b0111_0011; parameter LOWERCASE_t = 8'b0111_0100; parameter LOWERCASE_u = 8'b0111_0101; parameter LOWERCASE_v = 8'b0111_0110; parameter LOWERCASE_w = 8'b0111_0111; parameter LOWERCASE_x = 8'b0111_1000; parameter LOWERCASE_y = 8'b0111_1001; parameter LOWERCASE_z = 8'b0111_1010;
5.程序分析
1)接口
module work( ClOCK_50, KEY, LCD_RW, LCD_EN, LCD_RS, LCD_DATA, LCD_ON, LCD_BLON ); input ClOCK_50; input [3:0]KEY; output LCD_RW, LCD_EN, LCD_RS, LCD_ON, LCD_BLON; output [8:0]LCD_DATA; assign LCD_RW = rw; assign LCD_EN = CLK_500Hz; assign LCD_RS = rs; assign LCD_ON = 1; assign LCD_BLON = 1; assign LCD_DATA = data; `include "character.v"
主时钟为50MHz,之后需要进行手动分频,LCD时钟不能过快。
!KEY[1]作为复位信号。其余信号均输出至LCD。
2)状态与参数设置
/*共14个状态:1个空闲状态,9个设置状态,两行读写包括地址与数据共计4个状态*/ parameter s_idle = 4'd0; parameter s_clear = 4'd1; parameter s_cursor = 4'd2; parameter s_inputmode = 4'd3; parameter s_switchmode = 4'd4; parameter s_shiftmode = 4'd5; parameter s_setfunction = 4'd6; parameter s_setgeneraddr = 4'd7; parameter s_setdataaddr1 = 4'd8; parameter s_readbasy = 4'd9; parameter s_writecgram1 = 4'd10; parameter s_readram = 4'd11; parameter s_setdataaddr2 = 4'd12; parameter s_writecgram2 = 4'd13; parameter IDLE = 8'bzzzz_zzzz; parameter CLEAR = 8'b0000_0001; //清屏 parameter CURSOR = 8'b0000_0010; //光标返回 parameter INPUTMODE = 8'b0000_0110; //置输入模式 _01(I/D)S parameter SWITCHMODE = 8'b0000_1111; //显示开关控制 _1DCB parameter SHIFTMODE = 8'b0001_1100; //光标或字符移位 _(S/R)(R/L)** parameter SETFUNCTION = 8'b0011_1100; //置功能 00_001(DL)_NF** parameter SETGENERADDR = 8'b0100_0000; //置字符发生存贮地址 parameter SETDATAADDR = 8'b1000_0000; //置数据存贮器地址 起始地址
3)状态机跳转,并非每个状态都是必须的,参考了其他程序后,状态机如下
reg [3:0]state; reg rw, rs; reg [7:0]data; reg [7:0]addr; always @(posedge clk or posedge rst) begin if (rst) begin // reset state <= s_idle; data <= 8'b0; rw <= 1'b0; rs <= 1'b0; addr <= 8'b1000_0000; end else begin case (state) s_idle :begin state <= s_clear; data <= IDLE; rw <= rw; rs <= rs; end s_clear :begin state <= s_inputmode; data <= CLEAR; rw <= 1'b0; rs <= 1'b0; end s_inputmode :begin state <= s_switchmode; data <= INPUTMODE; rw <= 1'b0; rs <= 1'b0; end s_switchmode :begin state <= s_shiftmode; data <= SWITCHMODE; rw <= 1'b0; rs <= 1'b0; end s_shiftmode :begin state <= s_setfunction; data <= SHIFTMODE; rw <= 1'b0; rs <= 1'b0; end s_setfunction :begin state <= s_setdataaddr1; data <= SETFUNCTION; rw <= 1'b0; rs <= 1'b0; end s_setdataaddr1:begin state <= s_writecgram1; data <= addr; rw <= 1'b0; rs <= 1'b0; end s_writecgram1 :begin if (addr == 8'b1000_1111) begin state <= s_setdataaddr2; data <= lcd_data(addr); rw <= 1'b0; rs <= 1'b1; addr <= 8'b1100_0000; end else begin state <= s_writecgram1; data <= lcd_data(addr); rw <= 1'b0; rs <= 1'b1; addr <= addr + 1'b1; end end s_setdataaddr2:begin state <= s_writecgram2; data <= addr; rw <= 1'b0; rs <= 1'b0; end s_writecgram2 :begin if (addr == 8'b1100_1111) begin state <= s_setdataaddr1; data <= lcd_data(addr); rw <= 1'b0; rs <= 1'b1; addr <= 8'b1000_0000; end else begin state <= s_writecgram2; data <= lcd_data(addr); rw <= 1'b0; rs <= 1'b1; addr <= addr + 1'b1; end end default :begin state <= state; data <= data; rw <= rw; rs <= rs; end endcase end end
4)函数:设置每一个地址所显示数据(此处可DIY)
function [7:0]lcd_data; input [7:0]lcd_addr; begin case(lcd_addr) 8'h81 :lcd_data = CAPITAL_I; 8'h83 :lcd_data = LOWERCASE_a; 8'h84 :lcd_data = LOWERCASE_m; 8'h86 :lcd_data = CAPITAL_G; 8'h87 :lcd_data = CAPITAL_J; 8'h88 :lcd_data = CAPITAL_M; 8'hc1 :lcd_data = CAPITAL_W; 8'hc2 :lcd_data = LOWERCASE_h; 8'hc3 :lcd_data = LOWERCASE_o; 8'hc5 :lcd_data = LOWERCASE_a; 8'hc6 :lcd_data = LOWERCASE_r; 8'hc7 :lcd_data = LOWERCASE_e; 8'hc9 :lcd_data = LOWERCASE_y; 8'hca :lcd_data = LOWERCASE_o; 8'hcb :lcd_data = LOWERCASE_u; default:lcd_data = space; endcase end endfunction
5)时钟分频
parameter CLK_LCD_times = 19'd030000; reg [22:0]cnt; reg CLK_LCD; always@(posedge ClOCK_50 or posedge rst) begin if(rst) begin cnt <= 19'd0; CLK_500Hz <= 1'b0; end else if(cnt == CLK_LCD_times) begin cnt <= 19'd0; CLK_LCD <= ~CLK_LCD; end else begin cnt <= cnt+1'b1; end end
至此程序完成。文末附上完整代码。
6.程序结果
ps.完整代码如下
module work( ClOCK_50, KEY, LCD_RW, LCD_EN, LCD_RS, LCD_DATA, LCD_ON, LCD_BLON ); input ClOCK_50; input [3:0]KEY; output LCD_RW, LCD_EN, LCD_RS, LCD_ON, LCD_BLON; output [8:0]LCD_DATA; assign LCD_RW = rw; assign LCD_EN = CLK_LCD; assign LCD_RS = rs; assign LCD_ON = 1; assign LCD_BLON = 1; assign LCD_DATA = data; `include "character.v" /*共14个状态:1个空闲状态,9个设置状态,两行读写包括地址与数据共计4个状态*/ parameter s_idle = 4'd0; parameter s_clear = 4'd1; parameter s_cursor = 4'd2; parameter s_inputmode = 4'd3; parameter s_switchmode = 4'd4; parameter s_shiftmode = 4'd5; parameter s_setfunction = 4'd6; parameter s_setgeneraddr = 4'd7; parameter s_setdataaddr1 = 4'd8; parameter s_readbasy = 4'd9; parameter s_writecgram1 = 4'd10; parameter s_readram = 4'd11; parameter s_setdataaddr2 = 4'd12; parameter s_writecgram2 = 4'd13; parameter IDLE = 8'bzzzz_zzzz; parameter CLEAR = 8'b0000_0001; //清屏 parameter CURSOR = 8'b0000_0010; //光标返回 parameter INPUTMODE = 8'b0000_0110; //置输入模式 _01(I/D)S parameter SWITCHMODE = 8'b0000_1111; //显示开关控制 _1DCB parameter SHIFTMODE = 8'b0001_1100; //光标或字符移位 _(S/R)(R/L)** parameter SETFUNCTION = 8'b0011_1100; //置功能 00_001(DL)_NF** parameter SETGENERADDR = 8'b0100_0000; //置字符发生存贮地址 parameter SETDATAADDR = 8'b1000_0000; //置数据存贮器地址 起始地址 wire clk, rst; assign clk = CLK_LCD; assign rst = !KEY[1]; reg [3:0]state; reg rw, rs; reg [7:0]data; reg [7:0]addr; always @(posedge clk or posedge rst) begin if (rst) begin // reset state <= s_idle; data <= 8'b0; rw <= 1'b0; rs <= 1'b0; addr <= 8'b1000_0000; end else begin case (state) s_idle :begin state <= s_clear; data <= IDLE; rw <= rw; rs <= rs; end s_clear :begin state <= s_inputmode; data <= CLEAR; rw <= 1'b0; rs <= 1'b0; end s_inputmode :begin state <= s_switchmode; data <= INPUTMODE; rw <= 1'b0; rs <= 1'b0; end s_switchmode :begin state <= s_shiftmode; data <= SWITCHMODE; rw <= 1'b0; rs <= 1'b0; end s_shiftmode :begin state <= s_setfunction; data <= SHIFTMODE; rw <= 1'b0; rs <= 1'b0; end s_setfunction :begin state <= s_setdataaddr1; data <= SETFUNCTION; rw <= 1'b0; rs <= 1'b0; end s_setdataaddr1:begin state <= s_writecgram1; data <= addr; rw <= 1'b0; rs <= 1'b0; end s_writecgram1 :begin if (addr == 8'b1000_1111) begin state <= s_setdataaddr2; data <= lcd_data(addr); rw <= 1'b0; rs <= 1'b1; addr <= 8'b1100_0000; end else begin state <= s_writecgram1; data <= lcd_data(addr); rw <= 1'b0; rs <= 1'b1; addr <= addr + 1'b1; end end s_setdataaddr2:begin state <= s_writecgram2; data <= addr; rw <= 1'b0; rs <= 1'b0; end s_writecgram2 :begin if (addr == 8'b1100_1111) begin state <= s_setdataaddr1; data <= lcd_data(addr); rw <= 1'b0; rs <= 1'b1; addr <= 8'b1000_0000; end else begin state <= s_writecgram2; data <= lcd_data(addr); rw <= 1'b0; rs <= 1'b1; addr <= addr + 1'b1; end end default :begin state <= state; data <= data; rw <= rw; rs <= rs; end endcase end end function [7:0]lcd_data; input [7:0]lcd_addr; begin case(lcd_addr) 8'h81 :lcd_data = CAPITAL_I; 8'h83 :lcd_data = LOWERCASE_a; 8'h84 :lcd_data = LOWERCASE_m; 8'h86 :lcd_data = CAPITAL_G; 8'h87 :lcd_data = CAPITAL_J; 8'h88 :lcd_data = CAPITAL_M; 8'hc1 :lcd_data = CAPITAL_W; 8'hc2 :lcd_data = LOWERCASE_h; 8'hc3 :lcd_data = LOWERCASE_o; 8'hc5 :lcd_data = LOWERCASE_a; 8'hc6 :lcd_data = LOWERCASE_r; 8'hc7 :lcd_data = LOWERCASE_e; 8'hc9 :lcd_data = LOWERCASE_y; 8'hca :lcd_data = LOWERCASE_o; 8'hcb :lcd_data = LOWERCASE_u; default:lcd_data = space; endcase end endfunction parameter CLK_LCD_times = 19'd030000; reg [22:0]cnt; reg CLK_LCD; always@(posedge ClOCK_50 or posedge rst) begin if(rst) begin cnt <= 19'd0; CLK_500Hz <= 1'b0; end else if(cnt == CLK_LCD_times) begin cnt <= 19'd0; CLK_LCD <= ~CLK_LCD; end else begin cnt <= cnt+1'b1; end end endmodule