
简介基于FPGA实现点阵显示的设计资源面向数字电路与嵌入式系统学习者演示如何用Xilinx EP1C3T144C8芯片驱动LED点阵显示字符与图形。资源覆盖控制器设计关键模块包括数据存储器、扫描控制、时序状态机、SPI/I2C接口以及驱动电路并提供VHDL/Verilog源码与实验流程适合从入门到动手实践的硬件设计人群。压缩包仅209KB含113个文件以.v源码、.bdf原理图、.qsf工程配置、.rpt报告及.bsf模块封装为主还有.cdb和.hdb元件数据库文件便于在Quartus中直接打开学习。工程文件可完整复现点阵动态扫描过程同时附带多版本备份与仿真日志有助于深入理解行列扫描机制、刷新时序和状态机设计是FPGA点阵显示实验与课程设计的实用参考。已有1210人学习下载。 前两天翻柜子翻出一块放了挺久的8x8点阵模块排针都焊好了却一直没派上用场。网上一搜点阵显示项目十个里有七八个是51单片机的例程什么8x8点阵箭头滚动显示、数字时钟代码套路基本都是查表、循环扫描、delay一下。原理挺清晰但我手里正好放着块FPGA开发板就想着换个方式用FPGA把这块小点阵屏驱动起来。这一折腾下来才发现动态扫描刷新、行驻留时间、IO约束、上板调试这些概念全被串到了一起比单纯看教程有用得多。这篇东西就来聊聊我是怎么从零写扫描控制器、怎么算刷新参数、又是怎么一步步把屏幕上的一团乱码调成稳定画面的给同样手里有板子、想找个能看见实际效果的小项目的朋友做个参考。1. 为什么点阵显示我选了FPGA而不是单片机1.1 单片机刷屏方案的两处短板网上80%的51点阵例程长这样主循环里for一下依次把某行的字模数据放到P0口再用P2口选中对应行延时就完事了。这个写法在纯显示场景下没问题但一进真实项目就会露怯——系统里但凡还有按键扫描、串口通信、传感器读数主循环一忙刷屏节奏立刻变得断断续续屏幕肉眼可见地闪。你可能觉得那把扫描挪到定时器中断里不就行了我试过确实比主循环稳但单片机是单核串行执行中断里要保护现场、搬数据、恢复现场刷新时序依然存在抖动。而且中断服务程序一旦写得啰嗦CPU时间被大量吃掉整个系统的实时响应能力都会受影响。说白了扫描这件事占用了CPU的脑子CPU就没法专心干别的了。1.2 FPGA做点阵显示的真正价值FPGA的思路从根本上不一样它用硬件电路去干“扫描”这件事烧进芯片的本身就是一套专门负责刷屏的状态机。显示逻辑和主逻辑天然并行互不干扰。你只管把要显示的字模数据更新到寄存器里扫描电路自己会按节拍把每一行点亮刷新率完全由晶振和计数器决定不受任何外部任务影响。很多人觉得拿FPGA点个8x8点阵是杀鸡用牛刀我一开始也这么想做完才发现这个项目其实是性价比很高的FPGA入门练习。它涵盖了FPGA开发最常用的一串基本功分频计数器、行扫描状态机、字模ROM查表、IO约束、上板调试。后面如果你想碰FPGA图像处理、卡尔曼滤波FPGA实现或者写更复杂的接口协议回来再看这个小项目很多概念都是相通的。说白了这是一次把理论架构落地成实际硬件的完整闭环。2. 动态扫描背后的电气原理和参数计算2.1 共阳行、共阴列与逐行点亮一个8x8点阵模块内部其实就8行8列共64个LED外部引出16个引脚。常见的模块是行引脚做公共阳极、列引脚做公共阴极——某一行引脚拉高、某一列引脚拉低交叉点的LED才会亮。正因如此我们不可能同时把所有LED点亮只能让8行以极快的速度轮流导通这就是“动态扫描”存在的根本原因。驱动流程其实很朴素选第一行把第一行要显示的8位列数据一次性送上去保持一小段时间然后切到第二行送第二行的数据如此循环。只要整屏循环速度够快人眼的视觉暂留效应就会把8行图像合成一幅完整的画面。注意列是共阴的所以列数据是低电平点亮字模数据里0表示亮、1表示灭。第一次写代码的人十个有九个栽在这里看到显示出来的画面是反色第一反应不是取反数据而是怀疑板子坏了。2.2 刷新率、行驻留时间和亮度怎么权衡动态扫描的一切参数都要围绕一个词刷新率。人眼对低于50到60Hz的光脉冲会明显感知到闪烁所以整屏刷新率至少要留出余量我习惯取125Hz。有了行数和刷新率行切换频率就很好算了行切换频率 行数 × 刷新率 8 × 125 1000Hz。也就是说每行驻留1ms跑完8行正好8ms对应125Hz刷新率。主时钟如果是常见的50MHz周期20ns行切换需要的计数次数就是1ms ÷ 20ns 50000。这个数值直接决定了Verilog里分频计数器的溢出边界。不同刷新率对应的参数大概是这个关系目标刷新率行切换频率每行驻留时间50MHz下计数器值60Hz480Hz2.08ms约104166125Hz1kHz1ms50000250Hz2kHz0.5ms25000这里有个绕不开的矛盾扫描方式下每行LED只有1/8时间导通平均亮度只有静态直通的1/8左右。想提高亮度不能靠无脑降低刷新率——刷新率低了闪烁感立刻回来。正确思路是提高行驱动电流或者换更高亮的LED。小屏实验无所谓一块8x8点阵直接用IO口驱动也能看得很清楚但心里得有这本账做大了迟早要面对。3. 扫描控制器的Verilog实现3.1 顶层接口与外部电路连接我的顶层端口设计得很精简50MHz系统时钟、低电平复位的复位按键、3位行选信号row_sel、8位列数据col_data。行选信号只占3个引脚是因为外部接了3-8译码器比如74HC238FPGA输出3位行地址译码器输出8路行驱动信号。如果嫌麻烦也可以直接输出8位行选数据但那样要占8个IO而且跳线也多不如译码器方案干净。列数据8位直接并接到点阵屏的8根列线上每根列线串一颗限流电阻。电阻值按LED压降和期望电流算一般100到330Ω都能用。行驱动部分按常见做法加一片ULN2803或者PNP三极管阵列因为一行LED导通时的总电流不小长期用FPGA引脚硬灌不现实开发阶段偶尔点个几百毫秒没问题跑久了稳妥起见还是要加驱动。3.2 核心代码分频、行计数与字模查表直接看核心代码我用一个X形图案做演示对角线方向的LED都亮方便验证每一行的行列映射关系对不对。module dot_matrix_scan ( input wire clk_50m, input wire rst_n, output wire [2:0] row_sel, output wire [7:0] col_data ); // 50MHz - 1kHz 行切换整屏刷新率 125Hz parameter ROW_SWITCH_CNT 24d49_999; reg [23:0] timer_cnt; reg [2:0] row_cnt; reg [2:0] row_sel_reg; reg [7:0] col_data_reg; // 行切换计数器 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) timer_cnt 24d0; else if (timer_cnt ROW_SWITCH_CNT) timer_cnt 24d0; else timer_cnt timer_cnt 24d1; end // 行地址循环递增3bit自动回卷 wire [2:0] next_row_cnt row_cnt 3d1; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) row_cnt 3d0; else if (timer_cnt ROW_SWITCH_CNT) row_cnt next_row_cnt; end // 行选与列数据同步更新0亮1灭 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin row_sel_reg 3d0; col_data_reg 8hFF; end else if (timer_cnt ROW_SWITCH_CNT) begin row_sel_reg next_row_cnt; case (next_row_cnt) 3d0: col_data_reg 8b10000001; 3d1: col_data_reg 8b01000010; 3d2: col_data_reg 8b00100100; 3d3: col_data_reg 8b00011000; 3d4: col_data_reg 8b00011000; 3d5: col_data_reg 8b00100100; 3d6: col_data_reg 8b01000010; 3d7: col_data_reg 8b10000001; default: col_data_reg 8hFF; endcase end end assign row_sel row_sel_reg; assign col_data col_data_reg; endmodule这段代码里最关键的一点是把行选和列数据放在同一个时钟沿更新。为什么要这么做因为如果行选先跳、列数据晚到一拍切换的瞬间上一行的数据会漏到下一行去结果就是屏幕上出现拖影或者斜向的残影。我在第一次实现时吃过这个亏后面第三章会专门展开说。3.3 行选和列数据的同步性一个大屏驱动的隐患上一节代码里我用了next_row_cnt这个组合逻辑目的就是让行地址、行选信号、列数据在同一个时钟沿一起更新。如果改成直接拿row_cnt去查表由于非阻塞赋值的特点row_sel和col_data会看到旧的行地址等效于整个画面的行列整体错位一个周期。单块小屏上可能看不出多大问题但当你把行数做到16行、32行这种错位会被放大成明显的位移和花屏。严格的大屏驱动流程应该是关闭显示OE拉高→ 切换行选 → 更新列数据 → 打开显示OE拉低。我的小屏方案没有OE控制只能做到行选和列数据同沿更新实验下来没问题。如果你后面要做更大面积的屏或者排线很长、亮度很高就要在状态机里主动插入消隐周期先把列数据全置为全灭状态切换完行选后再送真实数据。这一步不加残影问题迟早会回来找你。提示这里说的OE消隐不是玄学而是LED驱动芯片标准流程。共阳行、共阴列的大屏一行LED电流可能到几百毫安如果切换瞬间列数据还没稳定就打开驱动不仅显示错乱长期看还会加重驱动芯片负担。4. 约束配置和上板调试实录4.1 引脚约束、时钟约束怎么给Vivado工程里引脚约束和时钟约束最好不要省。即使点阵显示这种设计对时序要求不算苛刻没有正确引脚约束也会导致布局布线的Pin位置完全对不上板子。我的XDC约束习惯是先声明时钟再逐个约束IOcreate_clock -period 20.000 -name sys_clk [get_ports clk_50m] set_property PACKAGE_PIN U18 [get_ports clk_50m] set_property IOSTANDARD LVCMOS33 [get_ports clk_50m] set_property PACKAGE_PIN J15 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] for {set i 0} {$i 3} {incr i} { set_property PACKAGE_PIN [lindex {T14 T15 T16} $i] [get_ports {row_sel[$i]}] set_property IOSTANDARD LVCMOS33 [get_ports {row_sel[$i]}] } for {set i 0} {$i 8} {incr i} { set_property PACKAGE_PIN [lindex {R11 R12 T11 T12 U11 U12 V11 V12} $i] [get_ports {col_data[$i]}] set_property IOSTANDARD LVCMOS33 [get_ports {col_data[$i]}] }注意这些引脚号只是示意具体以你手里开发板的原理图为准千万别照抄。IO电平标准LVCMOS33也要和开发板bank电压一致不然逻辑电平不匹配信号上板就变形。另外如果你用的板载FPGA型号在Vivado里显示未安装需要先安装对应的器件支持包不然综合实现阶段会直接报芯片不支持。4.2 从上板花屏到流畅显示的排查过程我第一次下载完程序屏幕全亮一个图案都看不出。这个现象非常典型原因是复位阶段col_data_reg复位值为8hFF也就是全灭逻辑上没错但如果你在顶层把复位释放后的初始状态设计成全零列数据全是0低电平点亮当然整屏全亮。排查方法很简单先用内部分频计数器制造一个很慢的切换用板载LED观察row_cnt有没有在循环再抓一下col_data的复位值。第二次下载出现了规律性亮点在屏幕上上下飘动。我最开始怀疑硬件接触不良后来用ILA抓了row_sel和col_data发现行选和列数据更新的确存在一拍的错位恰好印证了第三章说的同步问题。把代码改成next_row_cnt同沿更新后残影消失。这里分享一个真实经验遇到问题先别急着怀疑板子用ILA或逻辑分析仪确认FPGA内部信号对不对再把问题缩小到外部电路这样排查链路会短很多。第三次问题出在显示内容左右颠倒。原因很简单字模数据是从左往右写的但点阵模块的列引脚顺序正好相反。这种问题没什么高深解法把列数据按位镜像一下或者把线序反过来试一次就定了。上板调试还有个小技巧强烈建议掌握初次验证时不要一上来就显示完整图形先固定显示某一行的一个点比如row_cnt0、col_data8b11111110确认这个LED能亮再逐步扩展到整行、整屏。这样如果出了问题你能快速定位是逻辑问题还是接线问题。现象可能原因排查建议整屏全亮复位/默认列数据为全0检查col_data初始值是否全灭0xFF亮点上下飘动行选与列数据不同步让两者在同一时钟沿更新内容左右/上下颠倒字模数据方向与引脚接线不一致镜像位序或调整接线画面闪烁明显刷新率偏低提高行切换频率确保刷新率大于60Hz亮度极低扫描占空比只有1/8提高行驱动电流或换高亮LED5. 从8x8到16x16、灰度、全彩的扩展思考5.1 多片级联与HUB08串行接口8x8做完之后很自然想拼个16x16。如果继续用并行方式16根列数据线加4根行选线连线还能接受但到了32x32、64x32的屏并行走线就不现实了。市售LED屏普遍采用串行接口最常见的HUB08协议核心只有几个信号数据线可能分R/G双色或R1/G1/R2/G2四组、CLK、LAT锁存、OE使能以及A/B/C/D行地址。HUB08的驱动思路和我的并行扫描完全一致只是数据通路从并行变成了串行移位每个CLK上升沿移入1bit数据一行数据全部移完后拉一个LAT脉冲锁存到输出寄存器再把OE拉低使能显示同时更新行地址。对FPGA来说这就是“移位寄存器锁存器行译码器”的组合理解了8x8的动态扫描写HUB08驱动只是换个接口形式而已。5.2 PWM灰度控制的实现思路单色点阵想要显示灰度不能靠让LED半亮——数字电路的输出只有0和1唯一能做的是时间调制。思路很直接把每一行的驻留时间按二进制权重切分成多个时间片比如8级灰度就按1:2:4切三段时间每个像素根据灰度值的对应位决定在这个时间片里亮还是灭。整行驻留时间内亮的时长比例不同人眼平均出来就是不同灰度。这个方法做大了就是全彩LED屏驱动的地基也是FPGA图像显示方向最常见的技术起点。灰度级数越高时间片越细对FPGA时钟频率的要求就越高。8级灰度用50MHz轻松搞定256级灰度就要开始认真算时序余量了。顺着这个方向走下去你就会碰到帧缓存、图像处理、色彩空间转换这些更深的主题点阵显示这个项目就像第一级台阶站上去才能看到后面的路。5.3 容易被忽略的驱动与布线细节最后说几个我实际踩过、但常规教程几乎不会提的细节。行驱动电流不能全靠FPGA硬扛。开发阶段跑一跑没事长时间运行特别是多行同时点亮时引脚电流会超标。加ULN2803或者4953这类驱动芯片不只是“建议”而是做工程的基本常识。排线太长时信号完整性会很头疼。点阵屏和FPGA板之间的排线超过十几厘米建议在FPGA侧把IO的输出翻转速度调低一点Vivado里设SLEW属性再串一个33Ω左右的小电阻做阻抗匹配。我用示波器看过不处理的话CLK和LAT线上有很明显的过冲毛刺偶尔会导致显示错乱。字模数据不要手写在case里。写个Python脚本把图片转成二进制数组再生成Verilog头文件是更工程化的做法。手写字模在8x8还能应付一旦做16x16或者动画手写效率太低且容易出错。这个脚本本质上就是数据格式转换和后面做图像处理时的数据预处理是一个套路。说实话做完这个项目最大的收获不是“点亮了一块屏”这么一件事而是终于把计数分频、IO约束、上板排错这些概念从纸面上拽进了现实里。后来我再做图像采集或者更复杂的接口协议遇到问题第一反应都是先抓内部信号、分清边界再动手查电路这个习惯就是在这块8x8点阵上养成的。如果你也在入门FPGA别急着追那些看起来很高大上的项目先找块点阵屏把一行一行刷明白很多困惑会在一次上板亮屏之后豁然开朗。本文还有配套的精品资源点击获取