目录

基于FPGA的点阵滚动程序设计

刚刚学完数电课,对VHDL语言的使用还不是非常熟练,于是趁热打铁,用学校提供的试验箱,编写了一个基于FPGA的点阵滚动显示的驱动程序。

FPGA器件因具有可现场编程的优点,已经被广泛地应用于大屏幕点阵控制器中。因此我使用Verilog HDL语言实现16*16点阵的静态显示特定字符功能和流动显示特定字符功能。主要原理为使用LCD取模工具生成16位的数据存入ROM中,经高频分频器和扫描计数器实现行扫描,经地址发生器实现列显示,经低频分频器实现滚动显示。

硬件与系统设计

这一次的硬件,使用的是学校提供的试验箱。这个试验箱上有FPGA核心板、开关和点阵驱动电路,可以直接拿来使用,免去了硬件的设计。系统硬件设计框图如下图所示。

系统整体由硬件电路与FPGA内部数字电路逻辑构成。外部硬件电路包括外部时钟输入、按键输入以及点阵输出;内部数字电路逻辑则包括分频器、计数器、地址产生器、ROM等构成。外部时钟输入为试验箱上的50MHz有源晶振;显示方式与显示内容分别通过两个开关选择(key1key2);点阵则为试验箱上内置的16*16点阵。当显示方式(key1)开关为关闭状态时,静态显示每一个字符,一定时间以后切换显示内容;当其为开启状态时,流动显示字符,可以模拟类似广告屏的效果。

分频模块

由外部直接输入的50MHz时钟信号显然不能直接输入到扫描电路与地址计数器中,因此需要在输入之前进行分频。在本设计中,点阵的扫描使用了5000分频,扫描频率为10KHz,经测试具有良好的效果。用于产生点阵流动及帧切换的频率为输入时钟经过500000分频后的10Hz时钟信号。

扫描计数器

扫描计数器为一循环计数器。由于点阵的分辨率为16*16,有16行需要扫描,因此扫描计数器为4位的计数器,加满自动溢出归零。扫描计数器除了计数功能外,还可以向外输出行扫描信号。扫描模块及高频分频模块的verilog源代码如下:

always @(posedge clk)
begin
    clkdiv <= (clkdiv == 32'd5000) ? 32'd0 : (clkdiv + 1); //对时钟进行5000分频,10KHz
    if(clkdiv == 32'd2500)
    begin
        column_sel <= (column_sel == 6'd15) ? 6'd0 : column_sel + 6'd1;//扫描计数器
        column <= 16'd1 << column_sel; //输出行扫描信号
        rom_addr <= column_sel + column_offset; //给rom地址并从rom中取出数据,地址是二者之和
        if(key2 == 1'b0) //根据外部开关选择信号来源
            line <= rom_data1;
        else
            line <= rom_data2;
    end
end

ROM

ROM为Quartus提供的IP核,可以为我们提供一个可以预置的ROM。由于点阵每一行具有16位数据,我们所生成的ROM具有16位字宽;我们设计的每一条字幕具有816*16的字符,也就是具有128个数据,因此我们为ROM分配了128byte容量,ROM与外部的连接包括一个时钟信号,一个8位的地址输入和8位的数据输出,其中地址输入连接到地址产生模块,数据输出即为正弦波的数据,连接到后续的比较器。 在生成ROM的过程中,我们使用了LCD取模工具,如下图所示。

https://img.yuanze.wang/posts/fpga-martix/screenshot1.png
LCD取模工具

该工具可以根据字形生成16进制数据。考虑到点阵屏幕显示的方式,每一行扫描时需要一个16位的数据,因此在取模时选用逐列式,并令高位数据在前,这样两个相邻的数据可以直接合成一个16位的数据。由于点阵屏幕是共阴连接的,点阵格式选择阳码,这样1代表对应LED熄灭,0代表点亮,如下图所示。

https://img.yuanze.wang/posts/fpga-martix/screenshot2.png
取模工具的设置

生成完毕字幕数据以后,需要手工处理成Quartus能够识别的mif文件,该文件主要包含ROM的数据字长、位宽以及地址与数据。然后,通过Quartus自带的MegaWizard Plugin Manager添加一个1-Port ROM的IP核,参数选择与mif文件一致,如图3-3所示,并选择mif文件为预置数据文件,这样就生成了一个具有字模数据的ROM。

https://img.yuanze.wang/posts/fpga-martix/screenshot3.png
配置IP核

地址产生器

地址产生模块的本质为一计数器。在可变频率分频模块之后,每一个脉冲计数值加1,并将此计数值送到ROM,这样即可按照一定的频率将ROM内的每一个数据输出。 除了低频率分频器给地址产生器的计数脉冲外,由于点阵扫描时显示一帧数据需要获取16个数据,因此低频率分频器送给地址产生器的时钟实质上是用于对显示地址进行偏移,主要的地址是由扫描的行来决定的。也就是说,读取的ROM地址为扫描偏移地址与扫描行号之和。这样即可实现在整个点阵显示的同时,显示数据不断地变化。

点阵驱动电路

由于程序需要实现纵向和横向显示的切换,因此程序中也需要对扫描方式进行变化。在对图像取模的时候,需要纵向滚动的字符实际上是横着存储的,在显示纵向滚动字符时,通过判断按键将列与行交换并按位取反,即可实现将内容旋转180度显示,即通过横向平移的代码实现总线平移。部分代码如下所示:

always @(posedge clk)
begin
    if(key2 == 1'b0)
    begin
        column <= column_out;
        line <= line_out; 
    end
    else
    begin
        column <= ~line_out;
        line <= ~column_out;
    end
end

实物展示

https://img.yuanze.wang/posts/fpga-martix/demo1.jpg
静态显示姓名

https://img.yuanze.wang/posts/fpga-martix/demo2.jpg
滚动显示学号

总结

通过编写点阵滚动显示的驱动程序,我对Verilog语言有了更加深刻的认识,在不断的改错过程中,自己对Verilog HDL语言的语法结构有了深刻的理解,对编译过程中常见的错误也有了全面的认识。在熟悉了基于FPGA设计的同时,也学到了很多在学习课本知识时所体会不到的东西。

 Quartus工程

完整程序:

module Matrix(clk,
            key1,
            key2,
            column,
            line);
input clk;
input key1;
input key2;
output [15:0] column;
output [15:0] line;

reg [15:0] column;
reg [15:0] line;
reg [31:0] clkdiv;
reg [31:0] clkdiv_offset;
reg [31:0] clkdiv_show;
reg [6:0] column_sel;
reg [6:0] column_offset;
reg [6:0] column_cnt;
reg [6:0] rom_addr;
reg [15:0] rom_data;
wire [15:0] rom_data1;
wire [15:0] rom_data2;

ROM1 ROM_name(
  .clock(clk), //输入系统时钟
  .address(rom_addr), //地址输入
  .q(rom_data1) //数据输出
);

ROM2 ROM_number(
  .clock(clk),//输入系统时钟
  .address(rom_addr),//地址输入
  .q(rom_data2)//数据输出
);

always @(posedge clk)
begin
    clkdiv_offset <= (clkdiv_offset == 32'd5000000) ? 32'd0 : (clkdiv_offset + 32'd1);//分频
    if(clkdiv_offset == 32'd2500000)
    begin
        if(key1 == 1'b0)//模式选择按键
        begin
            column_cnt <= column_cnt + 1;
            if(column_cnt % 16 == 0)//逐字显示模式
                column_offset = column_cnt;
        end
        else if(key1 == 1'b1)//滚动显示模式
            column_offset <= (column_offset == 111) ? 6'd0 : column_offset + 6'd1;
        else
            column_offset <= 6'd0;
    end
end

always @(posedge clk)
begin
    clkdiv <= (clkdiv == 32'd5000) ? 32'd0 : (clkdiv + 1);//对时钟进行5000分频,10KHz
    if(clkdiv == 32'd2500)
    begin
        column_sel <= (column_sel == 6'd15) ? 6'd0 : column_sel + 6'd1;//扫描计数器
        column <= 16'd1 << column_sel;//输出行扫描信号
        rom_addr <= column_sel + column_offset;//给rom地址 从rom中取出数据
        if(key2 == 1'b0)//根据外部开关选择信号来源
            line <= rom_data1;
        else
            line <= rom_data2;
    end
end
endmodule