1. 16*16点阵显示原理与硬件连接
第一次接触16*16点阵的朋友可能会觉得它很神秘,其实拆解开来特别简单。想象一下,这就是256个LED灯整齐排列成的方阵。我手头这块点阵模块,正面看是整齐的发光点,翻过来看背面,你会发现它其实是由16行(R0-R15)和16列(C0-C15)的引脚组成的。
点亮原理特别直观:当某列接高电平(比如C3=1),某行接低电平(比如R5=0)时,这两个坐标交叉点的LED就会亮起。这就像在Excel表格里定位单元格一样简单。不过要注意的是,这种共阳接法的点阵(市面上也有共阴的),列是阳极,行是阴极,这个细节直接影响后续的字模提取方式。
实际接线时有个坑我踩过:点阵模块的引脚顺序可能不按常理出牌。有次我按照文档顺序接线,结果显示图案全乱了,后来用万用表逐个测试才发现,这个模块的物理引脚和逻辑编号居然是错位的。所以建议拿到新模块先做个简单测试,用杜邦线手动点亮几个点确认引脚对应关系。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态显示的核心:视觉暂留与扫描技巧
为什么我们看到的画面是稳定的?这要感谢人眼的"延迟"特性——视觉暂留效应。实测发现,当扫描频率超过60Hz时(也就是每列停留时间小于1/960秒,因为16列×60Hz=960Hz),人眼就会认为所有点同时亮着。这个原理和电影院放映机切换帧画面的道理一模一样。
我常用的列扫描方案是这样的:
- 通过计数器循环生成0-15的列选择信号
- 每个时钟周期激活一列(输出高电平)
- 同时给该列对应的16行输出字模数据
- 循环速度要足够快,建议扫描整个点阵的帧率至少80Hz
这里有个性能平衡的技巧:扫描太快会导致LED亮度不足,太慢又会出现闪烁。经过多次实验,我发现将系统时钟分频到约1.5kHz(16列×90Hz)效果最理想。具体数值可以用这个公式计算:
verilog复制// 假设系统时钟50MHz,目标扫描频率90Hz
parameter DIVIDER = 50_000_000 / (16 * 90);
3. 字模提取的实战技巧
提取字模是项目中最有"艺术感"的环节。我试过各种字模软件,推荐使用PCtoLCD2002这个经典工具。配置时要注意三个关键点:
- 取模方式选"横向取模,高位在前"(和我们的扫描方向匹配)
- 一定要选"阳码",因为我们的点阵是0点亮
- 取模走向要和FPGA代码中的处理逻辑一致
比如要显示"FPGA"这个单词:
- 在软件中输入文字后,会得到类似这样的十六进制数据:
code复制0x00,0x00,0x7F,0xFE... // 每两个字节代表一列16个点 - 每组数据对应一列LED的亮灭状态,1表示灭,0表示亮(因为是阳码)
遇到汉字显示时更要注意:不同编码方式会导致显示错乱。有次我显示"你好"变成了乱码,后来发现是软件编码没设置为GB2312。建议先用ASCII字符测试,再逐步过渡到中文。
4. 字模存储的优化方案
当需要显示大量字符时,直接写在代码里显然不现实。我的方案是使用FPGA的ROM核,具体操作分三步:
第一步:准备mif文件
这是存储字模的关键文件,格式如下:
code复制WIDTH=16; // 每个数据16位宽
DEPTH=1024; // 存储容量
ADDRESS_RADIX=HEX;
DATA_RADIX=HEX;
CONTENT BEGIN
0 : 0000; // 地址:数据
1 : 7FFE;
...
END;
第二步:Quartus中配置ROM
- 在IP Catalog中选择ROM:1-PORT
- 数据宽度设为16位
- 导入准备好的mif文件
- 注意勾选"允许在系统运行时通过文件更新内容",这样调试时修改mif文件不用重新编译
第三步:代码中例化ROM
verilog复制rom_module rom_inst (
.address(rom_addr),
.clock(clk),
.q(rom_data)
);
实际项目中,我还会在ROM里预留前256个地址给ASCII字符,从256开始存储汉字,这样方便用ASCII码直接作为地址偏移量。
5. 动态显示控制逻辑实现
让字符动起来的关键在于地址生成逻辑。以滚动显示为例,核心思路是:
- 设置一个显示窗口寄存器(比如16位宽,对应16列)
- 每个滚动周期将窗口向前移动1列
- 从ROM中读取窗口覆盖的字模数据
具体代码框架如下:
verilog复制reg [15:0] scroll_counter;
always @(posedge clk) begin
if(scroll_enable) begin
if(scroll_counter >= SCROLL_SPEED) begin
window_start <= window_start + 1;
scroll_counter <= 0;
end else begin
scroll_counter <= scroll_counter + 1;
end
end
end
// 计算当前显示列对应的ROM地址
wire [9:0] rom_addr = window_start + column_index;
对于倒计时功能,可以采用预存数字0-9的字模,然后通过状态机控制显示切换。按键交互部分建议用消抖处理,这里分享一个我常用的简单消抖方案:
verilog复制reg [19:0] debounce_cnt;
reg key_stable;
always @(posedge clk) begin
if(key_in != key_stable)
debounce_cnt <= debounce_cnt + 1;
else
debounce_cnt <= 0;
if(debounce_cnt >= 500000) // 约10ms@50MHz
key_stable <= key_in;
end
6. 系统集成与调试心得
把所有模块组装起来时,时钟域处理是个重点。我的经验是:
- 扫描时钟用单独的分频器生成
- 按键检测用系统主时钟
- 滚动控制用另一个低速时钟(如1Hz)
调试时遇到最头疼的问题是显示残影,表现为切换画面时前一个画面的痕迹隐约可见。后来发现是列切换时没有完全关闭前一列导致的,解决方法是在列切换间隙插入1us的全灭间隔。
另一个实用技巧是加入亮度调节功能。通过PWM控制每个列的开启时间,可以实现整体亮度调节。这在不同的环境光线下特别有用:
verilog复制reg [3:0] brightness; // 0-15级亮度
reg [3:0] pwm_cnt;
always @(posedge clk) begin
pwm_cnt <= pwm_cnt + 1;
column_enable <= (pwm_cnt <= brightness);
end
最后分享一个布线优化技巧:将点阵的行信号用寄存器打一拍再输出,可以显著减少毛刺。我在多个项目中发现,直接组合逻辑输出会导致边缘LED偶尔误亮,这个方案完美解决了问题。
