☰
8×8 LED点阵滚动汉字:扫描原理与VHDL实现
2026/10/28 12:20:21 网站建设 项目流程

简介:这份资源面向电子信息、集成电路相关专业学生及FPGA初学者,提供基于FPGA的8×8点阵汉字滚动显示完整设计方案,可用于EDA课程实验、课程设计或自学练手。压缩包内仅含1个doc文档,约273KB,以VHDL/EDA实习报告形式组织,涵盖FPGA结构与特点概述、点阵LED行/列扫描原理、汉字字模存储与动态分时扫描思路、VHDL控制逻辑编写、QuartusII仿真下载流程及硬件电路方案,并给出预定义“北华电子”的显示任务与设计结论。读者可据此理解从字模建库、扫描时序到上板验证的完整链路,对照报告复现实验并撰写自己的实验文档。目前已有1672人学习下载,适合作为入门级FPGA显示类项目的参考范例。

1. 8×8 点阵滚动汉字:为什么一块“只有 64 个灯”的屏能骗过眼睛

一块 8×8 的 LED 点阵,总共只有 64 个发光点。按常理,它连一个像样的汉字都塞不下,更别说让多个汉字从右往左滚动起来。但实际做出来的效果是:字是完整的,滚动是连续的,人眼看上去没有任何闪烁。这背后不是屏幕变大了,而是扫描和视觉暂留这两个机制在联手“骗”眼睛。

搞懂这件事的价值在于:它几乎是所有动态显示系统的缩小版模型。数码管动态扫描、LCD 行扫描、甚至视频的帧刷新,底层逻辑都是同一套——用时间换空间,用高速切换换静态观感。FPGA 在这里扮演的角色,是把“什么时候该点亮哪一列、哪一列该给什么数据”这件事,用硬件并行地、精确地卡在时序上。一旦你理解了这套时序是怎么被 VHDL 描述成电路的,再看任何扫描显示,都不会觉得是玄学。

2. 扫描显示的底层机制:64 个灯怎么拼出一个字

2.1 行列偏压与共阴共阳:一个 LED 点亮的物理条件

8×8 点阵内部是 64 个红色发光二极管,按 8 行 8 列排布。每个 LED 要亮,必须满足正向偏压:阳极高、阴极低。点阵的接法决定了控制方式——行共阳、列共阴,意味着行线给高电平、列线给低电平时,交叉点上的那个 LED 才会导通发光。

这里有个容易被忽略的细节:限流电阻放在 X 轴还是 Y 轴。放在行线上,则同一行 8 个 LED 共享一个电阻,当一行同时点亮多个点时,每个 LED 分到的电流会下降,亮度不均;放在列线上同理。常见的实现思路是每列串一个电阻,因为扫描时一次只驱动一列,列电流相对固定,亮度更均匀。这个取舍在原理图阶段就要定下来,焊完板子再改就麻烦了。

从等效电路看,点阵可以理解为一个 8×8 的交叉开关矩阵。行选信号 H0~H7 是 8 根独立的位控制线,高电平有效;列选信号 SEL[2..0] 是 3 位向量,经 4 线-8 线译码器输出 8 列。任意时刻,3 位列选只能选中一列,所以硬件结构本身就“逼”着你用逐列扫描的方式工作——这不是设计者选的,是电路拓扑决定的。

2.2 视觉暂留的量化门槛:为什么频率不能低于 50Hz

人眼有视觉暂留特性:一个光点消失后,视网膜上的印象会保留约 1/16 秒。这意味着,只要整个画面的刷新周期小于这个时间,人就会觉得画面是连续稳定的。

对于 8×8 点阵的列扫描,一帧要扫完 8 列。如果每列停留时间相同,那么帧周期 = 8 × 单列停留时间。要满足视觉暂留,帧周期必须小于约 62.5ms(1/16 秒),对应帧刷新率大于 16Hz。但 16Hz 只是“不觉得是一帧一帧”的下限,实际观感要舒服,通常要求帧率在 50Hz 以上,也就是单列停留时间不超过 2.5ms。

原文里给出的数字是:行扫描或列扫描频率必须大于 16×8=128Hz,周期小于 7.8ms。这里的 128Hz 指的是列切换频率,即每秒切换 128 次列,8 列一轮,帧率就是 16Hz。而设计程序里又强调“扫描频率不得低于 50Hz”,指的是帧率。两个数字不矛盾,只是口径不同:一个说列切换,一个说整帧刷新。实际做的时候,我一般会把帧率做到 60Hz 以上,单列停留约 2ms,这样滚动时边缘不会发虚。

提示:扫描频率太低,除了闪烁,滚动汉字还会出现“拖影”——因为人眼在字移动的过程中捕捉到了不连续的列。

2.3 列扫描的时序模型:3 位列选 + 8 位行数据的配合

把扫描过程拆成时序,是这样的:每个时钟周期(或分频后的扫描节拍),列选信号 SEL 输出一个值,选中某一列;同时,行数据端口输出这一列上 8 个 LED 的亮灭信息,1 表示亮、0 表示灭(共阴接法下,行给高电平点亮)。下一拍,列选切换到下一列,行数据换成新的一列字模。8 拍走完,一帧结束,再从头开始。

这个模型的关键在于:列选是“地址”,行数据是“内容”。汉字被拆成 8 列,每列 8 位,存成一个字节。扫描时按列地址依次取出对应的字节送到行端口,就完成了显示。原文里“0000”表示第 0 列、“00000001”表示第一行点亮的例子,说的就是这个对应关系。

用 VHDL 描述时,列选的循环可以用一个 3 位信号做减计数或加计数。原文方案一的列循环扫描程序里,lie信号从 “111” 递减到 “000” 再回绕,每个时钟上升沿且使能有效时变化一次。这里有个细节:If lie>"000" then Lie<=lie-"001"; Else Lie<="111";用的是字符串比较,综合工具会把它翻译成比较器和减法器。对于 3 位信号,这种写法能工作,但资源上不如直接lie <= lie - 1让工具自己处理回绕来得干净。不过在教学场景下,显式写出回绕逻辑,可读性更好。

3. 字模存储与滚动控制:数据从哪来、怎么动起来

3.1 汉字点阵的二进制化:从笔画到 8 个字节

要在 8×8 点阵上显示一个汉字,第一步是把汉字“数字化”。做法是:把汉字放进一个 8×8 的方格,有笔画经过的格子填 1,空白填 0,然后按列(或按行)读出来,每列 8 位组成一个字节。一个汉字就是 8 个字节。

原文里“北”字的字模数据是:

d0 <= "00010100"; -- 北 第0列 d1 <= "00010100"; -- 北 第1列 d2 <= "01111110"; -- 北 第2列 d3 <= "00000000"; -- 北 第3列 d4 <= "01111110"; -- 北 第4列 d5 <= "01010000"; -- 北 第5列 d6 <= "01010000"; -- 北 第6列 d7 <= "01100000"; -- 北 第7列

这 8 个字节就是“北”字在 8×8 网格上的列投影。注意第 3 列是全 0,说明这一列没有笔画,扫描到这一列时 8 个 LED 全灭。这种“空列”在字模里很常见,它不影响显示,只是扫描时多了一拍全灭的时间。

字模的生成方式,常见的有两种:手工在方格纸上画完再转二进制,或者用字模提取软件。原文提到“将若干个汉字的数据贮存在存贮器内”,在 FPGA 里这个“存贮器”通常不是真的 ROM 块,而是用 VHDL 的 case 语句或常量数组实现的查找表。综合工具会把常量数组映射到 LUT 或 Block RAM,具体取决于工具设置和容量。

3.2 滚动效果的数学模型:列偏移与字库索引

滚动显示的本质,是显示窗口在字库数据上滑动。假设字库里有 N 个汉字,每个汉字 8 列,拼成一个 8N 列的长条数据。8×8 点阵是一个宽度为 8 的窗口,窗口在长条上从左向右(或从右向左)移动,每移动一列,窗口里看到的内容就变一列,视觉上就是字在滚动。

用数学模型描述:设字库总列数为 L = 8N,窗口起始列偏移为 offset,则第 k 列(k=0..7)显示的是字库中第 (offset + k) mod L 列的数据。offset 随时间递增(或递减),就实现了滚动。原文里用cont信号在 0 到 30 之间循环(if cont="11110" then cont<="00000"),对应 31 个状态,每个状态切换一组 d0~d7 数据,本质上就是在做这个偏移索引。

这里有个设计取舍:偏移量是“整列跳变”还是“逐列滑动”。整列跳变实现简单,但滚动会有台阶感;逐列滑动需要更细的偏移控制,通常用计数器在字模列之间插值。原文的方案是每个cont状态对应一个完整的 8 列字模组合,滚动步进是整列,对于 8×8 这种低分辨率,台阶感不明显,可以接受。

3.3 换字延时:为什么字与字之间要“停一下”

如果字库直接首尾相连滚动,两个汉字之间没有间隔,读起来会连成一片。原文在方案一里用了一个int计数器(0 到 10000)来控制换字节奏,方案二里用clk_1hz驱动cont变化。这个延时的作用是:让每个汉字在窗口里完整显示一段时间,再开始滚出、滚入下一个字。

延时的长度需要权衡。太短,字还没看清就过去了;太长,滚动显得拖沓。原文用 1Hz 的脉冲控制换字,意味着每秒切换一次显示内容,配合列扫描的高频刷新,观感是“字停一秒,然后滚到下一个”。这个 1Hz 不是扫描频率,而是内容更新频率,两者在代码里是分开的:clk_1khz管扫描节拍,clk_1hz管换字节拍。这种双时钟域的做法在显示控制里很常见,但要注意跨时钟域的信号同步,否则cont的跳变可能被扫描进程采到亚稳态。

4. VHDL 实现拆解:从进程划分看硬件并行性

4.1 方案一为什么仿真失败:模块化拆分中的信号归属问题

原文提到方案一采用模块化思想,但仿真没成功,检查源程序也没发现问题,最后改用方案二。这是一个很典型的血泪经验:VHDL 的模块化拆分,难点不在语法,而在信号的作用域和驱动关系。

方案一把列扫描和字符样式设计拆成两个 entity,各自有独立的进程。问题在于:列扫描模块输出Sel,字符模块输出h0、h8,但两个模块之间没有明确的握手或同步机制。在仿真时,如果两个模块的时钟使能信号相位不一致,或者lie信号在两个模块里各有一份、值不同步,就会出现列选和数据对不上的情况——仿真波形上看就是某些列的数据错位,但单看每个模块的代码都“没毛病”。

常见的排查思路是:先确认所有模块用的是同一个时钟和同一个复位;再检查跨模块的信号是否在顶层正确连接;最后看综合后的网表里有没有被优化掉的信号。教学项目里,模块化拆分如果没处理好这些,很容易翻车。方案二放弃模块化、把所有逻辑塞进一个 architecture,虽然代码冗长,但信号都在同一个作用域里,驱动关系一目了然,反而一次跑通。这不算优雅,但说明了一个事实:在中小规模 FPGA 设计里,结构清晰比模块划分更重要。

4.2 方案二的核心进程:分频、列计数、数据选择

方案二的 architecture 里有几个关键进程,各司其职,靠信号连接:

-- 分频进程:把 1kHz 时钟再分频,产生扫描节拍 FenPin: process(clk_1khz) begin if clk_1khz'EVENT AND clk_1khz='1' then if Q="111" then Q<=(OTHERS=>'0'); else Q<=Q+'1'; end if; end if; end process FenPin; clk <= Q(0); -- 取最低位作为扫描时钟

这个进程把 1kHz 时钟 8 分频,得到 125Hz 的扫描节拍。Q是 3 位计数器,Q(0)每两个 1kHz 周期翻转一次,所以clk的频率是 500Hz?这里需要仔细算:Q从 000 到 111 循环,每个clk_1khz上升沿加 1,Q(0)的翻转频率是clk_1khz的 1/2,即 500Hz。但原文注释说用于扫描,实际列切换由cnt8控制,cnt8每个clk上升沿加 1,所以列切换频率是 500Hz/8 = 62.5Hz?这个数字偏低,但原文说“扫描频率不得低于 50Hz”,62.5Hz 刚好过线。如果实际观感有闪烁,可以把分频比调小,让clk更快。

-- 列计数进程:3 位循环,控制当前扫描到第几列 P2 : process(clk) begin if clk'event and clk ='1' then cnt8 <= cnt8+1; end if; end process P2; -- 数据选择进程:根据列计数,把对应列的字模送到行端口 P1: process(clk) begin if (clk'event and clk='1') then case cnt8 is when "000"=> data<=d0; when "001"=> data<=d1; -- ... when "111"=> data<=d7; when others => null; end case; end if; end process P1;

P2是列地址发生器,P1是数据选择器(多路复用器)。两者用同一个clk,所以cnt8变化后的下一个周期,data才更新到新列的数据。这里有一拍延迟:cnt8从 000 变到 001 的那个时钟沿,P1采到的还是旧的cnt8值?不对,VHDL 进程在时钟沿同时触发,P1里case cnt8读到的是cnt8的当前值(即变化前的值),所以data会比cnt8晚一拍更新。对于扫描显示,这一拍延迟意味着列选和数据有一拍错位,但下一拍就对齐了,实际观感无影响。如果严格对齐,可以把cnt8的更新和data的选择放在同一个进程里,用变量做中间值。

-- 换字进程:1Hz 脉冲驱动,切换显示内容 third: process(osd) begin if (osd='1' and osd'event) then if cont="11110" then cont<="00000"; else cont<=cont + '1'; end if; end if; end process third;

osd是 1Hz 信号,cont在 0 到 30 之间循环,每个值对应一组 d0~d7 字模数据。这个进程和扫描进程是异步的:cont变化时,扫描可能正扫到中间某一列,下一帧才会用新数据。这种异步在视觉上表现为“换字瞬间可能有一列旧数据残留”,但因为换字频率低(1Hz),人眼几乎察觉不到。

4.3 端口与信号定义:red 和 com 的位宽陷阱

方案二的 entity 定义里:

port ( clk_1khz, clk_1hz: in std_logic; red: out std_logic_vector(2 downto 0); -- 列共阳红色输出 com: out std_logic_vector(7 downto 0) -- 行共阴输出 );

red是 3 位,对应列选 SEL[2..0];com是 8 位,对应行数据 H0~H7。但注释写的是“列共阳红色输出控制端口 red[7..0]”,位宽写成了 8 位,和声明里的 3 位矛盾。这是一个典型的注释与代码不同步的坑。实际综合时以声明为准,red只有 3 位,接到 4 线-8 线译码器的输入端。如果照着注释去连 8 位,综合会报位宽不匹配。

另一个细节是st1、st2信号声明了但没用到,osc被注释掉了。这些冗余信号在综合时会被优化掉,不影响功能,但说明代码是迭代修改的产物,清理不彻底。读这类代码时,要以实际被进程驱动的信号为准,别被声明列表迷惑。

5. 避坑与排查:点阵显示翻车实录

5.1 现象:整屏全亮或全灭,列选完全不起作用

原因通常是列选译码器的使能端接错,或者red信号位宽不匹配导致译码器输入悬空。共阴接法下,如果列选一直输出全 0,译码器可能选中第 0 列并保持,行数据全 1 时就是整列全亮;如果译码器使能无效,所有列都不选,就是全灭。

解决:先用万用表量译码器输出,确认 3 位输入变化时 8 个输出依次有效;再检查 VHDL 里red的赋值是否真的连到了顶层端口,中间有没有被同名信号覆盖。

5.2 现象:显示的字左右颠倒或上下颠倒

原因是字模的列顺序或行位序与硬件接法相反。比如字模按“从左到右”存列,但硬件列选是从右到左译码;或者行数据 bit0 对应最上面一行,而实际接法是 bit7 对应最上面。

解决:不用改硬件,在 VHDL 里把列选计数方向反过来(加计数改减计数),或者把行数据的位序翻转(data <= data(0) & data(1) & ...)。更省事的办法是重新生成字模,按实际硬件的行列顺序提取。

5.3 现象:滚动时字与字之间出现半截字或错位

原因是换字时cont的跳变和扫描节拍不同步,导致某一帧里 d0~d7 有一部分是旧字、一部分是新字。原文方案二里cont由 1Hz 时钟驱动,扫描由 500Hz 时钟驱动,两者异步,换字瞬间必然有一帧数据混合。

解决:在cont变化时,强制扫描计数器复位到 0,等下一帧从头开始用新数据。或者用同一个时钟源分频出 1Hz,让换字发生在帧边界上。教学项目里如果不追求完美,混合一帧肉眼看不出来,可以不管。

5.4 现象:仿真波形正确,下载到板子后不亮

原因可能是引脚分配没做,或者时钟引脚接错。Quartus 综合后需要分配引脚(Pin Planner),把clk_1khz、red、com映射到实际 FPGA 管脚。如果没分配,工具会随机分配或报错;如果时钟接到了普通 I/O 而不是专用时钟引脚,内部时钟网络可能驱动不起来。

解决:下载前检查 Pin Planner 里的分配是否和实验箱原理图一致;用示波器量时钟引脚有没有波形;确认配置模式(JTAG 还是 AS)和下载线连接正常。

5.5 现象:亮度明显不足,尤其是一行点亮多个点时

原因是限流电阻太大,或者驱动电流不够。8×8 点阵一行 8 个 LED 同时亮时,总电流是单个的 8 倍,如果 FPGA 管脚直接驱动,电流能力有限(通常几 mA 到十几 mA),亮度会掉。

解决:加三极管或专用驱动芯片(如 ULN2803)做行驱动;或者把限流电阻减小,但要注意别超过 LED 和驱动器的额定电流。扫描显示下,每个 LED 只亮 1/8 时间,平均亮度本来就低,驱动能力要给足。

6. 设计取舍:为什么“丑”的方案二反而跑通了

回头看这个项目,最值得琢磨的不是扫描算法,而是方案一和方案二的取舍。方案一试图模块化,把列扫描和字模存储拆成独立 entity,思路是对的,但仿真失败。方案二把所有逻辑塞进一个 architecture,代码冗长、注释和代码不同步、有冗余信号,却一次跑通。这不是说模块化不好,而是说明:在资源有限、验证手段有限的教学场景里,降低结构复杂度比追求架构优雅更实际。

模块化的代价是接口定义和跨模块时序对齐。两个 entity 之间,时钟、使能、数据、地址每一根线都要在顶层正确连接,任何一根错位,仿真波形上就是一片混乱。而单 architecture 里,所有信号共享作用域,进程之间的驱动关系在代码里一目了然,出了问题也容易定位到具体进程。对于 8×8 点阵这种规模的设计,单文件完全放得下,拆模块带来的收益抵不过调试成本。

另一个取舍是字模的存储方式。原文用 case 语句硬编码字模,换字时改代码重新综合。更灵活的做法是用常量数组或初始化 ROM,把字模和逻辑分离,改字模不用动控制逻辑。但在教学项目里,硬编码让“数据长什么样”直接可见,便于理解字模和显示效果的对应关系。我一般会建议:先硬编码跑通,再把字模抽成独立包文件,这样既理解了原理,又留了扩展空间。

验证方法上,仿真波形只能看时序对不对,看不出亮度、闪烁、滚动流畅度这些观感指标。所以下载到实验箱上实际看,是不可跳过的一步。我自己的习惯是:仿真确认列选和数据对齐后,下载到板子,用手机慢动作录像看扫描有没有丢列,再调分频比直到滚动边缘不抖。从那以后我每次做扫描显示,都会先把帧率算清楚、把单列停留时间标在代码注释里,再动手写进程——这个习惯帮我省掉了不少反复下载调试的时间。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询