1. 项目思路:点阵显示为什么非得“扫描”
1.1 8×8点阵的内部结构
很多初学者拿到8×8点阵模块,第一反应是“不就是64个LED嘛,直接控制不就行了”。问题在于,64个LED如果每个都单独接一个引脚,那单片机引脚再多也不够用。厂家把64个LED接成行列矩阵,行和列交叉共用引脚,最终只引出16个引脚:8个行引脚、8个列引脚。点亮某个点的本质,就是在对应的行引脚和列引脚之间建立正向导通回路。
所以这里首先要搞清楚模块是“行共阳”还是“行共阴”。共阳的意思是每一行所有LED的阳极接在一起,共阴则相反,是阴极接在一起。很多教程一上来就给你一套程序,结果你接上实物发现显示不对,大概率就是极性没对上。最简单的判断方法是用万用表二极管档:红表笔接一个引脚,黑表笔挨个扫其他引脚,看哪个LED亮了,记录下来,很快就能摸清行和列的对应关系。别嫌麻烦,这一步省了,后面得折腾半天。
1.2 为什么要动态扫描而不是一次全亮
64个LED只有16个引脚,任意时刻你最多只能让一行或一列的LED同时导通,根本没办法让64个灯一次性全部按你的意愿点亮。这时候就要用到动态扫描:让8行LED轮流点亮,每行只亮一小会儿,然后迅速切到下一行。人眼有视觉暂留效应,只要切换得够快,看起来就是整幅画面同时亮着。
这个原理和放电影一模一样,电影胶片其实也是一帧一帧快速切换,因为每帧停留时间极短,人眼根本反应不过来。对8×8点阵来说,8行轮流扫一圈,如果每行点亮1ms,一圈就是8ms,换算成刷新率大约是125Hz。这个刷新率已经远超肉眼分辨闪烁的临界值,看起来相当稳定。要是每行延时拉到5ms甚至更久,一圈变成40ms以上,刷新率掉到25Hz左右,屏幕就会明显“一闪一闪”,这也是新手最常见的问题来源。
1.3 为什么需要取字模软件
人眼看到的是“心形”这个图案,但单片机不认识图案,它只认十六进制字节。拿8×8点阵来说,一行8个LED正好对应一个字节的8个bit:某个bit是1,代表这一列点亮;是0,代表熄灭。8行就需要8个字节。把这些字节按顺序发给行扫描电路,就能拼出完整图案。
用手算也能把图案转成字节,但8×8还算小,要是换成16×16、32×32,手算效率太低还容易出错。取字模软件就是把“画图”和“生成字节数据”这两件事合并了:你在软件里像画格子一样点出心形,它直接帮你生成C语言数组。这个项目里,核心工作其实就是两件事:把图案变成正确的字节数据,再用动态扫描把这些字节快速刷出来。
2. 取字模:把心形变成8个字节
2.1 工具里必须理清的4个参数
打开任意一款取字模软件(界面大同小异),先别急着画图,把生成选项设对。我见过太多人卡在这里,生成的数组长得跟加密文件一样,烧进去完全不是想要的样子。需要重点确认的是下面4项。
第一,取模方式选“逐行式”,也就是从上到下、一行一行生成字节。有些工具叫“行扫描”或“水平扫描”,意思一样。如果选成“逐列式”,生成的第一个字节是第一列的点亮状态,配合逐行扫描代码就会完全乱套。
第二,取模走向选“顺向”。顺向的意思是第一个字节对应第一行,第二个字节对应第二行,以此类推。选成“逆向”就会上下颠倒。
第三,位序选“低位在前”。这对应的是字节内bit0接左侧第0列,bit7接右侧第7列。如果硬件上P0.0接的是点阵最左边一列,那就必须低位在前。如果你接成了P0.0接最右边一列,那就得选“高位在前”。这个没有绝对标准,关键在于和你的硬件接线保持“位”与“列”的一一对应。
第四,输出格式选“阴码”,也就是1代表点亮、0代表熄灭。有些软件里叫“前景色为1”或者“白底黑字”,反正记着一点:我们想要的是“逻辑数组”,1就是亮,0就是灭。如果选了“阳码”,生成的数据会反过来,效果直接变成“暗的地方亮、亮的地方暗”。
2.2 在8×8画布上画心形
设置好之后,新建一个8×8的画布,把网格放大到最大,然后用鼠标左键逐格点出心形。因为是8×8,网格很小,画的时候要稍微有点耐心。我这里给出一个可以直接照抄的心形图案,用#表示点亮,用.表示熄灭:
..##..## ######## ######## ######## .######. ..####.. ...##... ........对应到每一行的十六进制数据就是:
0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00从第一行开始看,0x66就是0110 0110,也就是第一行只有左右两个位置各亮了2个点,这是心形的两个“圆弧顶部”。接下来三行全是0xFF,心形最宽的部分。第五行0x7E是0111 1110,左右各预留一列不亮,开始收窄。第六行0x3C是0011 1100,第七行0x18是0001 1000,最后一行全灭。如果你手里工具生成的字节和这个不完全一样,先检查是不是“逐行式、低位在前、阴码”这三个关键选项出了问题。
有的教程喜欢心形顶部留两行空白,那数组就会变成0x00, 0x00, 0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,相当于把心形图案整体往下挪,但底部尖端就放不下了。8×8点阵就这么多像素,怎么取舍全看你个人审美,但原理完全一样。我这里选择的是带底部尖端的紧凑版,显示出来更接近“一颗心”的直觉印象。
2.3 生成数据后必须做的一件事
从取模软件复制数据出来之后,别急着烧录,先做一个“反向验证”。把生成的十六进制数一个个拆成二进制,再按“#”和“.”还原成图形,看是不是跟你在画布里点出来的心形一致。这招特别管用,能挡掉大部分“数据看起来对、显示起来乱”的诡异问题。很多取模软件自带“预览”功能,可以直接看模拟效果,那就更方便。没有预览功能的话,就手动拆字节,8个字节拆完也就几分钟,比烧录后对着花屏猜原因快多了。
3. 硬件与代码:让心形真正亮起来
3.1 引脚连接方案与选型原因
代码只占一半,另一半在硬件接线。点阵有16个引脚,8个列引脚用来送数据,8个行引脚用来选通行。列引脚直接接P0口,每个引脚串一个220Ω到330Ω的限流电阻,防止LED过流。8个行引脚如果也直接接P1口或P2口,那确实简单,但太浪费引脚了,做个8×8就吃掉16个IO,以后啥也别干了。
更合理的做法是用三八译码器,也就是74HC138。它的原理和“点名器”差不多:3个输入引脚A、B、C可以组合出8种状态,每种状态对应一个输出引脚Y0到Y7被拉低,其余保持高电平。这样只需要占用P2.0、P2.1、P2.2三个引脚,就能控制8行,省下5个IO给其他外设。如果你想省事,直接用P2口8个引脚接行也没问题,但核心原理完全一样,只是把“选通行”的动作从查表变成了端口赋值。
具体接线如下:P0.0到P0.7分别接点阵的第1到第8列引脚(注意实际引脚顺序要以模块资料或万用表测量结果为准)。P2.0、P2.1、P2.2接74HC138的A、B、C输入。74HC138的G1接高电平,G2A和G2B接地,这样才能让译码器正常工作,这个使能引脚特别容易被漏接。Y0到Y7按顺序接点阵的第1到第8行引脚。
这里还要说清楚一个极性匹配问题:我这个接法默认点阵是“行共阴、列高电平点亮”的极性。也就是说,某一行被138选中拉低,该行LED阴极接地,P0口输出高电平点亮对应列。如果你的模块是“行共阳”,即行公共端是LED阳极,那么P0口的输出就要取反,代码里把P0 = heart[row]改成P0 = ~heart[row],或者在行驱动线上加一级三极管反向。万变不离其宗:只要LED的阳极和阴极之间能形成正向压降,灯就会亮。
3.2 完整可运行代码
#include <reg51.h> code unsigned char heart[8] = { 0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00 }; void delay(unsigned int t) { unsigned char i; while (t--) { for (i = 0; i < 120; i++); } } void main(void) { unsigned char row; while (1) { for (row = 0; row < 8; row++) { P0 = 0x00; // 消影:先把所有列灭掉 P2 = row & 0x07; // 译码器选中当前行,输出低电平 P0 = heart[row]; // 送入当前行的列数据,1点亮 delay(1); // 维持约1ms } } }代码逻辑其实很直白。heart数组存的就是取模生成的8个字节。主循环里,row从0到7依次循环,P2 = row & 0x07让74HC138按顺序选中第0行、第1行……第7行。每次选通行之前,先把P0置成0x00,把所有列的LED先熄灭,再送当前行的数据,这就是消影。
消影这一步非常关键。如果不做,上一行的数据会残留在P0口上,当切换行选通时,上一行的点亮状态会“串”到新行上,画面就会出现明显的拖影或者残影。你可能觉得“反正下一行数据马上也会覆盖P0”,但P0赋值和行选通切换之间有一个极短的瞬间,高速扫描下这点误差就会被视觉放大。先灭再点亮,是最稳妥的做法。
3.3 动态扫描的时序取舍
延时函数里delay(1)的实际时长和晶振频率、编译器优化级别都有关系,不必追求绝对精确。按12MHz晶振算,这个延时大约在1ms量级,8行扫描一轮就是8ms左右,刷新率大约125Hz,效果已经很好。
延时长短要按实际效果调整:如果你看到明显的闪烁感,就把延时调短;如果整体亮度偏暗,就把延时调长一点。但注意,延时不能无脑拉长,超过5ms后闪烁感会非常明显,超过10ms基本就没法看了。这和刷新率的道理是一样的:每行点亮时间越长,单轮扫描周期就越长,刷新率就越低。想要亮度高又不闪烁,更好的路线是改善驱动电路,比如用恒流驱动或加强行驱动能力,而不是单纯靠拖长延时。
4. 常见问题与排查技巧实录
4.1 画面明显闪烁
绝大多数情况是延时长到离谱,或者主循环里多了不必要的耗时操作。先把延时缩到1ms以内试试。还有一种可能:你用的是开发板上的点阵,但点阵和单片机之间隔了大把杜邦线,接触不良导致某些行实际没有被点亮,看起来也会像在闪。排查时先量一下138输出引脚,看看Y0到Y7是否在按顺序逐行拉低。用万用表直流电压档或者示波器看波形是最直接的,没有仪器的话,把延时拉长到50ms,强制让每一行“慢慢走”,肉眼看一遍就知道是行选通问题还是数据问题。
4.2 图案左右镜像或上下颠倒
这是让最多人崩溃的问题。左右镜像通常是列接线顺序接反了,或者取模时“低位在前”和“高位在前”选反了。硬件上怎么查?让程序只送一个字节0x01给P0口,理论上应该只有最左(或最右)一列亮。看它实际亮在哪一列,你就知道列顺序对不对。上下颠倒同理,送一个0xFF给第0行,看实际亮的是不是最上面一行。这种“点一个列、亮一个行”的定位法,比烧录后反复猜快得多。
4.3 亮度不均匀或只有一部分亮
如果是亮度整体偏暗,先看P0口有没有加上拉电阻。P0口是开漏结构,输出高电平能力很弱,直接推LED阵列很容易被拉垮,接一个10k×8的排阻到VCC会立竿见影。如果是上面几行亮下面几行暗,那就是行驱动能力不足,74HC138直接驱动整行LED的灌电流能力有限,实物中最好在138输出端加三极管或者专用的行驱动芯片。这个问题在仿真软件里看不出来,但一上实物就特别明显,可以说是从仿真到实物的“第一道坎”。
4.4 数据看起来对但显示成一团糟
先别怀疑代码,先怀疑“极性”。用同一套代码,把P0 = heart[row]改成P0 = ~heart[row]试试,很多时候问题瞬间就解决了。因为取模软件生成的“阴码”并不保证和你的硬件极性配套,它只保证“1代表点亮”的逻辑关系。至于LED的公共端到底吃高电平还是低电平,取决于硬件。另一点是确认74HC138的G2A和G2B有没有接地,如果这两个引脚悬空,138的输出状态会变得不可控,表现就是“时不时有几行乱亮”。
4.5 快速排查对照表
以下是我自己整理的一张排查表,收藏级别,适合从仿真到实物的全流程参考。
| 现象 | 最常见原因 | 排查与解决 |
|---|---|---|
| 画面明显闪烁 | 延时过长、刷新率过低 | 将每行延时控制在1ms以内 |
| 图案左右反 | 列接线顺序反或位序设置反 | 送0x01定位实际列位置 |
| 图案上下反 | 行接线顺序反或顺/逆向设置反 | 送0xFF定位实际行位置 |
| 显示一团糟但代码没报错 | 极性不匹配 | 尝试P0输出取反 |
| 只有部分行亮 | 138使能引脚没接好 | 检查G1高电平、G2A/G2B接地 |
| 整体亮度低 | P0口开漏驱动弱 | 加排阻上拉,或加74HC245缓冲 |
| 有残影拖尾 | 没有消影或P0数据切换过慢 | 在切换行前先将P0清0 |
4.6 我调试这种小项目时的独家习惯
第一次做点阵炸了很多次之后,我养成一个习惯:拿到任何新点阵模块,先写一个“全亮测试程序”,让8行全部点亮1秒。如果全亮正常,说明硬件基本健壮;然后让每一行轮流亮1秒,确认行的顺序;再让每一列单独亮,确认列的顺序。这一步只花几分钟,但能把“硬件问题”和“软件问题”彻底切开。很多人一上来就烧心形程序,结果图不对,又怀疑代码又怀疑硬件,最后发现是引脚顺序搞反了,白白浪费时间。先定位,再显示,做任何带点阵的项目都能省一大半调试时间。