很多初学51单片机的朋友,第一次碰LED点阵时都会问:“点阵上的心形到底是怎么点出来的?”其实原理不复杂,无非是把一颗一颗LED当作像素,按坐标去点亮。但真上手做的时候,大家普遍卡在一个叫“取字模软件”的环节——心形图案画好了,却不知道怎么变成单片机认识的数组。这篇内容就围绕51单片机点阵和取字模软件的使用方法,从原理、取模、接线到代码显示,把完整流程拆开讲一遍。心形图案只是个例子,把方法学会,换汉字、箭头、字母都只是换一组数据的事。
1. 点阵显示的核心原理
1.1 从点亮一颗LED说起
点阵本质就是许多LED按行和列排在一起。8×8点阵共64颗LED,内部通过行线和列线交叉连接。拿一颗LED来说,只有行线和列线之间满足导通条件时它才会亮。如果某一行给高电平,某一列给低电平,对应交叉点的LED就会点亮;反过来也一样,取决于点阵是共阳还是共阴。
这个思想很像一个电影院座位图。你想让某一排、某一列的观众站起来,就同时通知那一排和那一列。点阵中的LED就是观众,行线和列线就是通知信号。心形图案本质上就是一张“座位表”,某个位置该亮,某个位置不该亮,按这张表去点,图案就出来了。
1.2 动态扫描与视觉暂留
如果一次把64颗LED全点亮,需要同时控制64个状态,51单片机IO口根本不够用。所以实际项目里都会用动态扫描:同一时刻只点亮一行,或者只点亮一列,然后快速切换,利用人眼的视觉暂留效应让整幅画面看起来是同时亮的。
动态扫描的时间要把握合适。8行点阵,每一行点亮1ms左右,扫描完8行只需要8ms,一秒钟能刷新约125次。这个刷新率远高于人眼能分辨闪烁的临界值,所以看起来不会闪。如果每行点得太久,比如10ms以上,就能明显看到一行一行在闪,这个后面排查问题时要特别注意。
2. 取字模软件的使用方法
2.1 取模到底在干什么
电脑上的一幅黑白图片,放大之后就是一个个像素点。放到8×8点阵上,每行8个像素,8行一共64个像素。取模软件要做的,就是把“哪个位置是黑的、哪个位置是白的”翻译成0和1。8个0/1合在一起就是一个字节,8行就得到8个字节,这8个字节就是字模数组。
比如说心形图案第一行是“两边空,中间两个点,再空,再两个点,再空”,转换成二进制就是00111100,十六进制就是0x3C。取模软件生成的一串十六进制数,其实就是好多这样的二进制状态。把它写到单片机程序里,再配合扫描代码,就能控制点阵按这些状态点亮LED。
2.2 关键参数到底怎么设置
取模软件有不少版本,界面五花八门,核心参数却是通用的。新手容易懵的也是这些参数。以8×8心形为例,推荐这样设置:
| 参数项 | 推荐值 | 说人话的解释 |
|---|---|---|
| 字宽/字高 | 8 / 8 | 对应8×8点阵,宽高必须和硬件一致 |
| 取模方式 | 逐行式 | 按行扫描,每行生成一个字节 |
| 取模走向 | 顺向/正向 | 数组第0个字节对应点阵第0行 |
| 阴码/阳码 | 阳码 | 1表示亮,0表示灭,调试时更直观 |
| 字节内顺序 | 高位在前 | 字节的最高位对应最左边那一列 |
这里面最容易糊涂的是“阴码/阳码”。阴码是0表示亮、1表示灭,阳码是1表示亮、0表示灭。如果选择了阴码,心形会显示成“心形镂空”,因为亮灭关系完全反了。我在实际使用中更推荐“阳码”,因为代码里直接写1就是亮,逻辑和硬件状态一一对应,排查问题的时候少绕弯。
字节内顺序也很关键。同一个8×8图案,高位在前和低位在前生成的数组内容完全不同。代码里发送数据时,如果发送函数从最高位开始逐位移出,那取模软件就要选“高位在前”。如果软件生成的是“低位在前”,要么把取模设置改成高位在前,要么把发送函数改成从最低位开始发。这里没有谁对谁错,关键是要匹配。
2.3 手把手生成心形字模数组
为了把过程说清楚,我按最常用的两种做法走一遍。第一种是在取模软件自带的绘图区里直接点格子,第二种是先用系统画图工具做一张8×8黑白图再导入。
用画图工具制作时,先把画布宽度和高度都改成8像素。8像素小图基本看不清,可以把显示比例放大到800%以上,再用铅笔工具一格一格地涂黑,画出心形轮廓。画好后另存为BMP单色位图。保存过程中如果提示会丢失颜色信息,直接确认,因为我们只需要黑白两色。
然后把位图导入取模软件。如果没有导入功能,就照着同样的形状在软件绘图区里临摹一遍。导入或画完后,一定要确认图片尺寸是8×8,不要多一格少一格。接着按上面表格设置好参数,点击“生成字模”或“取模”按钮,软件就会输出类似下面这样的数组:
0x3C, 0x42, 0xA5, 0x81, 0x81, 0x66, 0x24, 0x18如果逐行读一下这8个字节,会发现每一行都对应心形轮廓的一部分。第0行是0x3C,二进制是00111100,也就是中间4个点亮,左右各空两格;第1行是0x42,左边一个、右边一个,正好是心形的两边向下延伸。到第5行0x66,中间两个点、两边各两个点,最后第7行0x18收尾。整体一串数据下来,就是一个完整的心形。
2.4 为什么同一个图案会有不同取模结果
如果你把同一个心形分别用“逐行式”和“逐列式”取模,得到的数据完全不同。逐行式生成的是每一行8个点的状态,适合按行扫描的硬件;逐列式生成的是每一列8个点的状态,适合按列扫描的硬件。两种方式没有优劣,只看你的扫描方式。代码里如果是逐行扫描,就用逐行式;如果是逐列扫描,就用逐列式。一旦选错,显示出来的图案就是乱的,东一块西一块。
还有“倒序”问题。有的软件把数组按从下到上输出,有的按从上到下输出。如果硬件从第0行开始扫描,数组第0个元素就应该是第0行。若发现心形上下颠倒,把数组倒序放一遍,或者把“取模走向”改成反向即可。
3. 硬件连接与驱动方案
3.1 为什么选择74HC595
51单片机IO口不多,8×8点阵本身就有16根引出脚,直接全用IO口显然不现实。而且动态扫描要求不断刷新行和列数据,如果占用大量IO口,其他功能就不用做了。所以常见做法是用74HC595来扩展输出。74HC595是一个8位串入并出移位寄存器,只需要3根控制线:数据线SER、移位时钟SRCLK、输出锁存时钟RCLK。通过这3根线,可以把8位数据从串行转成并行输出,驱动8个引脚。
更复杂的项目会把两个74HC595级联,一个控制行、一个控制列。心形这个项目用一个74HC595控制列数据,行选择用P0口控制,简单而且够用。这样单片机只占用P2口的3根线和P0口的8根线,程序结构也很清晰。
3.2 实际接线与极性确认
我这里给出一套可行的参考接法。假设点阵是共阴类型,也就是行引脚为公共阴极,列引脚接LED阳极,那么点亮条件是:行给低电平,列给高电平。这样P0口输出低电平来选通行,列数据由74HC595输出高电平来点亮LED。
| 74HC595引脚 | 接法 | 说明 |
|---|---|---|
| SER | P2.2 | 串行数据输入 |
| SRCLK | P2.1 | 移位时钟,上升沿移入数据 |
| RCLK | P2.0 | 锁存时钟,上升沿输出到并行脚 |
| Q0~Q7 | 点阵列引脚 | 依次对应点阵左到右 |
行引脚直接接到P0.0~P0.7。注意P0口内部没有上拉电阻,输出高电平时能力很弱,所以这里正好利用它输出低电平选通行,让列数据由595去负责高电平,电流路径比较合理。为了控制LED电流,需要在列引脚或行引脚串入限流电阻,一般取220Ω~470Ω比较常见。电阻太小电流过大,容易伤害LED;电阻太大亮度不够,动态扫描时看起来更暗。
接线之前先确认模块上哪边是行、哪边是列,市面上的8×8点阵引脚排列不统一。拿不准的时候,用万用表二极管档去测,或者做一个最简单的“单点亮”测试程序,把某一位置成点亮状态,看实际是哪颗LED亮。这一步花几分钟,但能帮你省掉后面几个小时的瞎猜。
如果手里是共阳点阵,情况会相反:行给高电平,列给低电平点亮。这种情况下两种处理方式:一是在取模软件里把“阳码”改成“阴码”,取出来的数组正好是反相的,和共阳点阵匹配;二是在代码里把列数据取反发送,也就是写成Hc595SendByte(~heart[i]),同时把行选通改成高电平有效。代码本身并不复杂,但一定要明白自己的硬件属于哪种极性。
4. 单片机代码实现:让心形亮起来
4.1 发送一个字节到74HC595
74HC595的数据发送原理,就是按照时钟节奏,一位一位地把数据从SER引脚送进内部移位寄存器。每来一次上升沿,当前位就被移入一位。8位数据全部送完后,再给RCLK一个上升沿,把移位寄存器里的8位数据一次性锁存到并行输出脚上。这样输出的8个引脚状态就是稳定的,不会在移位过程中乱跳。
发送函数可以写成这样:
void Hc595SendByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { SER = (dat & 0x80) ? 1 : 0; dat <<= 1; SRCLK = 0; _nop_(); SRCLK = 1; } RCLK = 0; _nop_(); RCLK = 1; }这里判断dat & 0x80,每次取最高位输出,然后把数据左移一位。对应取模软件里“高位在前”的设置。如果软件设置的是“低位在前”,就把判断改成dat & 0x01,数据每次右移一位。发送完8位后,统一产生锁存信号,所有并行引脚同时更新,这也是消除乱闪的关键。
4.2 逐行扫描显示代码
有了发送函数,就可以实现逐行扫描了。每一行处理流程是:先把所有行关闭,避免上一行内容残留;再发送当前行的列数据;最后选通当前行,延时一小段时间;然后切换到下一行。
完整示例代码如下:
#include <reg51.h> #include <intrins.h> sbit SER = P2^2; sbit SRCLK = P2^1; sbit RCLK = P2^0; unsigned char code heart[] = { 0x3C, 0x42, 0xA5, 0x81, 0x81, 0x66, 0x24, 0x18 }; void Delay(unsigned int t) { while (t--); } void Hc595SendByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { SER = (dat & 0x80) ? 1 : 0; dat <<= 1; SRCLK = 0; _nop_(); SRCLK = 1; } RCLK = 0; _nop_(); RCLK = 1; } void DisplayOneFrame(void) { unsigned char i; for (i = 0; i < 8; i++) { P0 = 0xFF; // 全部行关闭,消隐 Hc595SendByte(heart[i]); // 发送当前行列数据 P0 = ~(0x01 << i); // 第i行输出低电平,选通 Delay(100); // 行显示时间 } P0 = 0xFF; } void main() { while (1) { DisplayOneFrame(); } }注意这里P0 = ~(0x01 << i)适用于共阴点阵:只有被选中的那一路输出低电平,其余行都是高电平。如果改用共阳点阵,就换成P0 = (0x01 << i),同时列数据要取反发送。代码里的延时Delay(100)在12MHz晶振下大约1ms左右,8行扫描一圈约8ms,视觉效果稳定。如果觉得亮度不够,可以适当增加延时;如果觉得闪烁,就要减小延时。
4.3 数组顺序与硬件行序的对应关系
heart数组第0个字节是0x3C,表示第0行的状态;第1个字节表示第1行,依此类推。所以取模软件里的“取模走向”必须和代码中的行序一致。如果发现心形上下反了,就把数组倒序,或者把扫描循环改成从7到0。如果发现心形左右反了,多半是高位在前/低位在前设置反了,或者硬件接线时Q0对应的是最右列。
实际操作里,我建议先只显示数组第一个字节,看看0x3C对应硬件上哪一行。如果这一行亮的位置和预期一致,说明行序没问题。再看第二个字节,如果也正常,说明整体方向没错。这样一步步验证,比直接烧一整幅图猜问题要快得多。
5. 常见问题与排查技巧
5.1 画面闪烁,一行一行地亮
这是动态扫描项目里最常遇到的问题。最常见原因是每行延时太长,导致扫描周期变长,刷新率下降。可以先把延时从100降到20或者直接去掉,看闪烁是否改善。另一个原因是没有做消隐处理。如果切换到下一行之前没有把所有行关闭,上一行的列数据可能会在行切换瞬间串到下一行去,造成残影和亮度不均。代码里先执行P0 = 0xFF再发送新数据,就是为了保证切换干净。
如果去掉延时后还是闪,检查是不是只有一行在反复循环,那就是扫描函数没真正遍历8行,或者数组长度计算有误。
5.2 显示出来的心形乱七八糟
心形图案乱,大概率是取模方式和扫描方式不匹配。比如用“逐列式”取模,却用“逐行式”扫描,数据完全对不上。先把取模软件里的“逐行式/逐列式”和代码里的扫描顺序统一。如果图案像是斜着错位,检查字节内“高位在前/低位在前”是否一致,以及发送函数是不是从最高位开始发送。
还有一个很低级的坑:数组元素顺序被改乱了。比如从取模软件复制出来的时候漏了一个字节,或者多了一个0x00,整张图就会错位。排查方法就是打印或者仿真看数组前几个值,和取模软件的输出逐行对照。
5.3 心形缺笔画或亮度太暗
缺笔画先检查硬件连接,可能是行线或列线虚焊,也可能是595某个输出脚接触不良。用万用表测一下对应引脚是否导通,或者把测试图案换成全亮,看哪些点不亮,就能快速定位。
亮度暗有两个方向:一是限流电阻太大,把电流压得太低;二是每行点亮时间太短,LED平均功率不够。可以适当减小限流电阻,或者增加延时时间。但要注意动态扫描下LED峰值电流可以比静态大一些,只要不超过器件最大额定值就行,这也算动态显示的一个好处。
5.4 点阵极性彻底搞反
如果整屏一个点都不亮,先别急着改代码。拿万用表二极管档,分别测点阵的行和列引脚,摸清是共阳还是共阴。如果是共阳,行是公共正极,列是分列负极;如果是共阴,行是公共负极,列是分列正极。确定了极性之后,再按对应的代码逻辑去修改。
我见过有人卡在这里很久,因为模块上丝印没有标注,网上资料又对应不上。最稳的办法就是自己写一个单点亮测试程序:给某一行选通,某一列给数据,看能不能亮。能亮就说明极性判断正确,不能亮就换反逻辑再试。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 整屏全灭 | 极性接反、供电不足、595没锁存 | 确认共阳/共阴,检查RCLK和供电 |
| 一行亮一行不亮 | 行引脚虚焊或P0口驱动问题 | 测试每一行选通,检查接线 |
| 心形上下颠倒 | 数组顺序反了 | 倒序显示,或取模时改反向 |
| 心形左右颠倒 | 高位/低位顺序匹配错误 | 统一取模设置和发送顺序 |
| 图案乱错位 | 取模方式与扫描方式不匹配 | 统一逐行/逐列、顺序、阴阳码 |
| 屏幕闪烁 | 刷新率低、缺少消隐 | 减小延时,扫描前关闭所有行 |
| 亮度暗 | 限流电阻大或点亮时间短 | 减小电阻,适当增加行延时 |
6. 从心形还能扩展出什么
6.1 一维数组变成多帧动画
heart数组存的是一个心形,如果连续存两个不同大小的心形,比如第0~7字节是普通心形,第8~15字节是小一点的心形,代码里循环显示两个子数组,就是心跳动画。做法也很简单,扫描函数加一个帧号参数,根据帧号取不同数组区域的起始地址。这个思路可以做滚动文字、闪烁图案、箭头跳动等等,本质上都是“多组字模按顺序刷新”。
6.2 从8×8扩展到16×16汉字
16×16点阵的汉字显示,可以用4块8×8点阵拼起来,或者直接用成品16×16点阵模块。取模软件里把字宽字高改成16×16,生成的字模数组就不是8个字节,而是32个字节。驱动电路需要两个或更多74HC595级联,分别控制行和列。代码结构不变,只是发送字节数变多、扫描行数变多。
我的建议是,先把8×8心形这个流程完整走通,取模软件的参数、硬件极性、扫描顺序这些关键点都搞清楚,再上16×16会顺利很多。否则一次加太多变量,出了问题很难判断是硬件还是软件还是取模的锅。
6.3 一点个人实操体会
我自己第一次调点阵心形时,最耗时间的不是写代码,而是取模软件和硬件方向不匹配。后来养成一个习惯:新做一个点阵项目,第一步永远是用“边框测试图”确认行列方向。比如第一行全亮,看它是不是点阵最上面一行;第一列全亮,看它是不是最左边一列。这一步确认了,后面不管取模软件怎么折腾,都能快速绕回来。
另外建议大家把每个图案的字模文件单独保存,最好带上备注,比如“8x8_心形_逐行_高位在前_阳码”。别看这个备注简单,隔两周再翻出来看,你会非常感谢当时的自己。取模软件生成的数据本质上是0和1的组合,但不同硬件和扫描方式带来的差异,不亲自动手试一次,真的很难体会到那种“明明数据对,但图案就是不对”的无奈。
最后再分享一个调试小技巧:点亮心形之前,先用unsigned char code test[] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xFF};做一个方框。方框方向对了,方向问题就排除了;方框方向不对,心形再怎么调都是白费劲。这个方框测试我几乎每次都用,推荐你也试试。