软件实现了在4.3寸LCD左上角显示一个数字时钟,效果如下图所示。本文针对VGA/LCD控制时许有一定基础的人群,博主的开发环境为Quartus13.1和一个随便哪家的开发板,使用4.3寸LCD(RGB565接口),兼容VGA,但是相关参数需要更改。软件中部分代码模块借用野火电子的软件,感谢。后文以LCD进行说明。
获取源代码、字模软件、rom初始化文件等点击此处
LCD在显示时是从左到右、从上到下进行刷新显示,4.3寸lcd显示区域大小为480x272,软件在坐标(24,32)处划定了一个长24宽32的显示区域,将此区域命名为square1,显示的内容为时的十位(22:15中左边第一个2),如下图所示。
在显示一个字符1时先进行取模,如下图所示,点阵大小也为24x32,与上图square1对应,因此lcd在square1中一行一行刷新时也判断字模的值是否为1,是则点亮lcd对应位置,反之则变暗。下面举例进行说明。square1在扫描前三行时,字模为0,第四行的第13个像素点为1,那么此时lcd对应的位置点亮。软件中lcd点亮即将该点像素变红。
软件实现时,将字模存储在宽度为24bit,深度为32的rom中,square1的32行读出rom地址0的值,在本行的第24列开始判断rom地址0读出值rom_dat的最高位bit23,然后判断bit23值是否为1,是则将lcd该点变红,反之变黑,之后继续判断bit30、bit29、bit28…bit0,一行判断完成后则判断下一行。square1的第33行读出rom的地址1的数据,然后进行判断。重复以上过程直到square1最后一行,此时对应rom地址31的数据。由此完成一个字符的显示。
部分代码如下:
parameter POINTX ='d24, //squr1
POINTY ='d32;
parameter WIDTH ='d24,
HEIGH ='d32;
else if(squr1_rden)begin
if(squrrom_dat[WIDTH+POINTX-pix_x])
o_tft_dat<=RED ;
else
o_tft_dat<=0;//BLUE;
end
squrrom_rdaddr =(pix_y-HEIGH)
完成一个字符显示后多个字符显示就很简单,继续划定剩下3个字符和冒号区域,4个字符的点阵大小都是24x32,冒号点阵大小为16x32。软件使用两个rom存放字模,一个存储0-9字符,地址0-31为字符“0”,32-63为字符“1”,后续依次递增;另一个存储“:”。4个字符共用一个rom,字符寻址、显示部分代码如下图:
always@(*)begin
if(!rst_n)
squrrom_rdaddr='d0;
else if(squr1_rden)
squrrom_rdaddr=suqr1_addr;
else if(squr2_rden)
squrrom_rdaddr=suqr2_addr;
else if(squr3_rden)
squrrom_rdaddr=suqr3_addr;
else if(squr4_rden)
squrrom_rdaddr=suqr4_addr;
end
assign suqr1_addr=(pix_y-HEIGH)+ squr1*32;
assign suqr2_addr=(pix_y-HEIGH)+ squr2*32;
assign suqr3_addr=(pix_y-HEIGH)+ squr3*32;
assign suqr4_addr=(pix_y-HEIGH)+ squr4*32;
always@(posedge clk,negedge rst_n)
if(!rst_n)
o_tft_dat<='d0;
else if(squr1_rden)begin
if(squrrom_dat[WIDTH+POINTX-pix_x])
o_tft_dat<=RED ;
else
o_tft_dat<=0;//BLUE;
end else if(squr2_rden)begin
if(squrrom_dat[WIDTH+POINTX2-pix_x])
o_tft_dat<=RED ;
else
o_tft_dat<=0;//YELLOW;
end else if(dot_rden)begin
if(dotrom_dat[WIDTHDOT+POINTXDOT-pix_x] && dot_vld)
o_tft_dat<=RED ;
else
o_tft_dat<=0;//CYAN;
end else if(squr3_rden)begin
if(squrrom_dat[WIDTH+POINTX3-pix_x])
o_tft_dat<=RED ;
else
o_tft_dat<=0;//PURPPLE;
end else if(squr4_rden)begin
if(squrrom_dat[WIDTH+POINTX4-pix_x])
o_tft_dat<=RED ;
else
o_tft_dat<=0;//GRAY;
end else
o_tft_dat<='d0;
软件输出小时、分钟两个数值,直接对时钟计数产生,部分代码如下:
always@(posedge clk,negedge rst_n)
if(!rst_n)
cnt<='d0;
else if(cnt==MS-1)
cnt<='d0;
else
cnt<=cnt+'d1;
always@(posedge clk,negedge rst_n)
if(!rst_n)
cntms<='d0;
else if(cnt==MS-1)begin
if(cntms=='d1000-1)
cntms<='d0;
else
cntms<=cntms+'d1;
end else
cntms<=cntms;
always@(posedge clk,negedge rst_n)
if(!rst_n)
cnts<='d0;
else if((cnt==MS-1) && (cntms=='d1000-1))begin
if(cnts=='d60-1)
cnts<='d0;
else
cnts<=cnts+'d1;
end else
cnts<=cnts;
assign o_sec_fg = (cnt==MS-1) && (cntms=='d1000-1);
always@(posedge clk,negedge rst_n)
if(!rst_n)
cntmin<='d0;
else if(min_ctl) begin
if(cntmin>='d60-1)
cntmin<='d0;
else
cntmin<=cntmin+'d1;
end else if(o_sec_fg && (cnts=='d60-1)) begin
if(cntmin=='d60-1)
cntmin<='d0;
else
cntmin<=cntmin+'d1;
end else
cntmin<=cntmin;
always@(posedge clk,negedge rst_n)
if(!rst_n)
cnthour<='d0;
else if(hour_ctl)begin
if(cnthour>='d24-1)
cnthour<='d0;
else
cnthour<=cnthour+'d1;
end else if(o_sec_fg && (cnts=='d60-1) &&(cntmin=='d60-1))begin
if(cnthour=='d24-1)
cnthour<='d0;
else
cnthour<=cnthour+'d1;
end else
cnthour<=cnthour;
对于一个两位数的数字,常用的提取个位和十位的方法是除以10得到十位,除以10取余数得到个位,但是对于FPGA来说以上方法将使用更多资源,并且不符合FPGA编程习惯,因此一般使用加3移位法实现二进制转BCD码,部分代码如下:
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_shift <= 5'd0;
else if((cnt_shift == 5'd7) && (shift_flag == 1'b1))
cnt_shift <= 5'd0;
else if(shift_flag == 1'b1)
cnt_shift <= cnt_shift + 1'b1;
else
cnt_shift <= cnt_shift;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_shift <= 14'b0;
else if(cnt_shift == 5'd0)
data_shift <= {8'b0,data};
else if((cnt_shift <= 6) && (shift_flag == 1'b0))
begin
data_shift[09:06] <= (data_shift[09:06] > 4) ? (data_shift[09:06] + 2'd3) : (data_shift[09:06]);
data_shift[13:10] <= (data_shift[13:10] > 4) ? (data_shift[13:10] + 2'd3) : (data_shift[13:10]);
end
else if((cnt_shift <= 6) && (shift_flag == 1'b1))
data_shift <= data_shift << 1;
else
data_shift <= data_shift;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
shift_flag <= 1'b0;
else
shift_flag <= ~shift_flag;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
unit <= 4'b0;
ten <= 4'b0;
end
else if(cnt_shift == 5'd7)
begin
unit <= data_shift[09:06];
ten <= data_shift[13:10];
end
软件使用两个按键对小时、分钟进行控制,功能比较简单,只能进行加一调整,即按下某个按键对应数值加一。因为轻触按键在按下时存在机械抖动,所以不能简单判断下降沿就认为按键按下,需要在下降沿后延时20ms在判断按键情况,部分代码如下:
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_20ms <= 20'b0;
else if(key_in == 1'b1)
cnt_20ms <= 20'b0;
else if(cnt_20ms == CNT_MAX && key_in == 1'b0)
cnt_20ms <= cnt_20ms;
else
cnt_20ms <= cnt_20ms + 1'b1;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key_flag <= 1'b0;
else if(cnt_20ms == CNT_MAX - 1'b1)
key_flag <= 1'b1;
else
key_flag <= 1'b0;
LCD驱动时序与VGA类似,因此在使用VGA是只需要更改时钟、行列计数等参数,驱动代码如下:
//parameter define
parameter H_SYNC = 10'd41 , //行同步
H_BACK = 10'd2 , //行时序后沿
H_VALID = 10'd480 , //行有效数据
H_FRONT = 10'd2 , //行时序前沿
H_TOTAL = 10'd525 ; //行扫描周期
parameter V_SYNC = 10'd10 , //场同步
V_BACK = 10'd2 , //场时序后沿
V_VALID = 10'd272 , //场有效数据
V_FRONT = 10'd2 , //场时序前沿
V_TOTAL = 10'd286 ; //场扫描周期
//wire define
wire rgb_valid ; //VGA有效显示区域
wire pix_data_req ; //像素点色彩信息请求信号
//reg define
reg [9:0] cnt_h ; //行扫描计数器
reg [9:0] cnt_v ; //场扫描计数器
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//o_tft_clk,o_tft_de,o_tft_bl:TFT像素时钟、数据使能、背光信号
assign o_tft_clk = tft_clk_9m ;
assign o_tft_de = rgb_valid ;
assign o_tft_bl = sys_rst_n ;
//cnt_h:行同步信号计数器
always@(posedge tft_clk_9m or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_h <= 10'd0 ;
else if(cnt_h == H_TOTAL - 1'd1)
cnt_h <= 10'd0 ;
else
cnt_h <= cnt_h + 1'd1 ;
//o_hsync:行同步信号
assign o_hsync = (cnt_h <= H_SYNC - 1'd1) ? 1'b1 : 1'b0 ;
//cnt_v:场同步信号计数器
always@(posedge tft_clk_9m or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_v <= 10'd0 ;
else if((cnt_v == V_TOTAL - 1'd1) && (cnt_h == H_TOTAL-1'd1))
cnt_v <= 10'd0 ;
else if(cnt_h == H_TOTAL - 1'd1)
cnt_v <= cnt_v + 1'd1 ;
else
cnt_v <= cnt_v ;
//o_vsync:场同步信号
assign o_vsync = (cnt_v <= V_SYNC - 1'd1) ? 1'b1 : 1'b0 ;
//rgb_valid:VGA有效显示区域
assign rgb_valid = (((cnt_h >= H_SYNC + H_BACK)
&& (cnt_h < H_SYNC + H_BACK + H_VALID))
&&((cnt_v >= V_SYNC + V_BACK)
&& (cnt_v < V_SYNC + V_BACK + V_VALID)))
? 1'b1 : 1'b0;
//pix_data_req:像素点色彩信息请求信号,超前rgb_valid信号一个时钟周期
assign pix_data_req = (((cnt_h >= H_SYNC + H_BACK - 1'b1)
&& (cnt_h < H_SYNC + H_BACK + H_VALID - 1'b1))
&&((cnt_v >= V_SYNC + V_BACK)
&& (cnt_v < V_SYNC + V_BACK + V_VALID)))
? 1'b1 : 1'b0;
//o_pix_x,o_pix_y:VGA有效显示区域像素点坐标
assign o_pix_x = (pix_data_req == 1'b1)
? (cnt_h - (H_SYNC + H_BACK - 1'b1)) : 10'h3ff;
assign o_pix_y = (pix_data_req == 1'b1)
? (cnt_v - (V_SYNC + V_BACK )) : 10'h3ff;
//o_rgb_tft:输出像素点色彩信息
assign o_rgb_tft = (rgb_valid == 1'b1) ? pix_data : 16'b0 ;
整个软件不复杂,RTL视图如下,主要由timer产生小时、分钟数值,经过BCD转换后输入到pic_char模块,然后将rgb输出到tft_ctl模块,最终实现上图效果,软件工程如下:源代码、字模软件、rom初始化文件等。