简介:面向嵌入式电子设计与单片机初学者的16×16点阵汉字左移显示资料包,基于74HC595与汇编语言实现点阵驱动与动态滚动。资源解决小型LED点阵高效显示汉字及平滑左移的硬件时序问题,适合学习汇编指令和Proteus仿真的开发者。压缩包共11个文件,含asm汇编源码、c语言辅助程序、hex烧录文件、Proteus设计的DSN仿真工程及DBK备份、lst列表、SDI调试信息等,整体仅57KB,结构紧凑,便于按需查阅。已有754人学习,可用于课设或竞赛备赛。资料既有可直接编译的代码,又有仿真工程与备份文件。通过阅读汇编与C源码、烧录hex并在Proteus中打开DSN,可理解74HC595的DS/SHCP/STCP时序控制、16×16点阵字模组织和左移滚动实现思路,对提升底层硬件调试能力有直接帮助。
1. 16x16 点阵显示汉字左移:为什么这玩法值得用汇编和595
拿到16x16点阵屏,想让汉字平滑左移,很多人第一反应是C语言里写一长串延时和位操作,但烧进某款51内核单片机后才发现,动态扫描、消隐、字模方向、595级联顺序这些坑能把人绕晕。这套资源用纯汇编控制74HC595级联,四块8x8点阵拼成16x16,定时器中断做行扫描,主循环做左移窗口,用最少的IO口把“显示”和“移动”拆干净。适合电子设计、嵌入式课程设计的从业者,也适合所有想把点阵驱动原理看清楚的人。
2. 硬件连接与驱动选型:4块8x8拼屏,595级联的行列分配
2.1 点阵屏的物理结构:共阴共阳与行列扫描原理
16x16点阵不是一块现成的屏,而是四块8x8点阵模块拼出来的。常见模块分共阳和共阴两种,内部每一行或每一列的LED引脚已经连到公共端。拼屏时,四块模块的行列引脚需要重新规划:通常把左上、右上、左下、右下四块的行和列分别并联,组成16行16列。
驱动方式必须用矩阵动态扫描。所谓扫描,就是每次只选中一行,把这一行上应该亮的列置为有效电平,其余行全部关闭,然后快速轮换下一行。假设每行持续1ms,16行扫完一帧需要16ms,刷新率大约62Hz,人眼看到的就是稳定图像。如果每行5ms,整帧80ms,刷新率只有12.5Hz,屏幕会明显闪烁,看起来很累。
本项目我采用共阳接法:行是公共阳极,列是LED阴极。选中某一行时,行驱动输出有效电平(低电平,由PNP三极管反相),列数据通过595输出高电平点亮对应的LED。如果手头是共阴模块,行和列的有效电平全部取反,程序里把行选码和列数据取反就行。这个极性搞反是最早的坑,后面避障章会细说。
2.2 595级联电路原理:两片控制行、两片控制列
74HC595是一片8位串入并出移位寄存器,带输出锁存。它只需要三根控制线:串行数据DS、移位时钟SCK、锁存时钟RCK。数据在SCK上升沿逐位移入,锁存时RCK上升沿把移位寄存器里的内容同时送到并行输出Q0-Q7。
16行需要两片595级联提供16位行选,16列也需要两片595级联提供16位列数据。级联的方法是前一片的串行输出Q7'接到后一片的串行输入DS。这样四片595总共只需要3根IO线,而不是直接占用20多个引脚。
级联顺序很容易出错。数据先从第一片进入,再通过Q7'传给第二片。锁存后,第一片的Q0-Q7对应二进制数的低8位,第二片对应高8位。也就是你最后发送的那个字节,出现在输出端口的低字节位置。如果两片列595的数据顺序反了,汉字会被从中间劈成两半,左右互换。这个在代码部分会给出明确结论。
为什么不用138译码器选行?138只能做3-8译码,16行需要两片,再加上列数据的锁存器,电路复杂度不比595低。595同时解决行和列,级联结构还能继续扩展到大屏,所以这套方案更通用。
2.3 硬件参数表:引脚分配、限流电阻、电源注意
下面是我实际使用的接线,不是唯一方案,但照着接可以少走弯路。
| 信号名 | 单片机引脚 | 作用 |
|---|---|---|
| DS_DATA | P1.0 | 串行数据,行和列595共用 |
| SHIFT_CLK | P1.1 | 移位时钟SCK |
| LATCH_CLK | P1.2 | 锁存时钟RCK |
| ROW_OE | P1.3 | 行595输出使能,低有效,用于消隐 |
| COL_OE | 接地 | 列595常开,靠行OE统一消隐 |
行595的16位输出经过PNP三极管接到16行公共端;列595的16位输出经过300Ω限流电阻接各列LED阴极。电源用5V/2A稳压,靠近点阵处并接1000μF电解电容和104陶瓷电容,避免扫描时电流尖峰导致电压跌落。
需要重点检查的是限流电阻阻值。16行扫描时,每个LED的真实导通占空比只有1/16,平均亮度不高,所以瞬时电流可以适当加大。300Ω在5V供电下,LED压降约2V,电流约10mA,瞬时功率在安全范围内。如果电阻加到1k,电流只有3mA,整个屏会显得非常暗。不要为了省电把电阻调大,点阵扫描不是这样算的。
3. 字模提取与取模方向:汉字在16x16点阵中的坐标约定
3.1 字模数据结构:16x16=256bit,32字节
一个16x16汉字就是16行乘16列,一共256个点,每个点占用1bit,所以总共32字节。通常按行存储:每行16个点拆成两个字节,第一个字节表示行的左半部分(列0到列7),第二个字节表示右半部分(列8到列15)。
以“左”字为例,前几行字模长这样:
; “左”字前4行,每行两个字节,MSB对应最左列 DB 0x04, 0x00 DB 0x04, 0x00 DB 0x08, 0x00 DB 0x7F, 0xFE第一字节0x04换算成二进制是00000100,表示该行从左数第5个点被点亮。第二字节0x00表示右半部分全灭。到了第四行,0x7F表示左侧7个点全亮,0xFE表示右侧8个点除了最右一位全亮,连起来就是一条横线。
这个MSB在左的约定必须贯穿取模、扫描、左移三个环节。任何一个环节把位序理解反了,汉字就会镜像或者旋转。我见过最典型的错误是取模时选了“逐列式”,得到的字模数据按列排列,行扫描时字体会变成横着躺,需要额外做转置。
3.2 取模软件设置:逐行式 vs 逐列式,MSB在左
取模软件里通常会有一堆选项:横向取模、纵向取模、逐行式、逐列式、MSB在左、MSB在右。本项目必须选择:
- 逐行式(一行一行取,每行16位)
- 横向取模(同一行里按左到右排列)
- MSB在左(每个字节最高位对应左边第一个点)
如果你选了逐列式,得到的数据是按列顺序存储的,适合列扫描电路。但本方案是行扫描,强行使用逐列式就会让汉字旋转90度。补救办法是写一个转置函数,把32字节按位重排,但51单片机上做位重排开销很大,刷新率会掉到30Hz以下,不建议。
有一个快速验证方法:先取一个“日”字,看看前两个字节。一个字第一行应该是顶部横线,如果是逐行取模,第一个字节至少应该包含高位的若干1;如果看到第一个字节是0x00,紧接着大量连续1,说明取模方向很可能反了。实际看一眼取模软件的预览比反复读文档更快。
3.3 字模与扫描的匹配:行扫描时列数据的顺序
扫描程序每刷新第N行,就把字模表里第N行的两个字节送进列595。字模表必须按行顺序存放,不能打乱。扫描时,列595的Q0对应屏幕最左列,Q15对应最右列。由于级联顺序,发送数据时先发右半字节,再发左半字节,锁存后左半字节会落到Q7-Q0,右半字节落到Q15-Q8。这个顺序在代码里是用发送字节的先后控制的,不是靠硬件改线。
再强调一次:如果发送顺序反了,左半字节会跑到右边,汉字左右颠倒。这种错误在点阵上非常显眼,因为笔画多的字会立刻看出来。调试时用“王”字或者“日”字,横竖对称的字不容易看出问题,最好用一个像“洲”这样左右结构明显的字。
4. 汇编程序实现左移显示:动态扫描、移动速度与内存管理
4.1 主循环与定时中断:扫描频率与视觉残留
动态扫描不能靠主循环里的大延时,那会让整屏亮度波动。正确的做法是用定时器0产生1ms中断,每次中断刷新一行,16ms完成一帧。中断服务程序只做两件事:把当前行的列数据送出去、把行选码送出去、锁存并开启该行。主循环里只处理“要不要左移”的判断,不做任何耗时操作。
如果每行刷新间隔大于2ms,整帧刷新率就低于31Hz,闪动会很明显。有些人把延时写在主循环里,以为扫描速度够快,实际一测量发现每行刷新时间长达3-5ms,屏幕像坏了一样。解决办法是直接用定时器,把每行时间压到1ms。
4.2 左移算法:从字模缓冲区到显示缓冲区的移位操作
资源里设计了一个32字节的显示缓冲区,存放当前窗口的16列数据。左移的本质是:显示缓冲区整体向左移动一列,最左一列从屏幕上移出,最右边补入下一列的数据。
位序上,我们约定每个字节的MSB对应屏幕左侧。因此整行向左移动一位,二进制上实际是执行“带进位循环左移”。以下面为例:
; R0指向当前行低字节,R1指向当前行高字节 ; 进入前C已经放入了下一列要补入的位 MOV A, @R0 ; 先处理低字节 RLC A ; 低字节左移,bit7进入C,bit0接收外部位 MOV @R0, A MOV A, @R1 ; 再处理高字节 RLC A ; 高字节左移,bit0接收低字节bit7,bit7进入C后丢弃 MOV @R1, A为什么必须先低字节后高字节?因为低字节的bit7需要搬到高字节的bit0。如果先处理高字节,高字节bit0接收的是当前进位(外部新位),而不是低字节bit7,数据链就断了。这个顺序是左移算法最容易写错的地方。
处理完16行后,原来的显示缓冲区整体向左移动了一个像素。外部补入的位从哪里来?一般会维护一个列偏移量OFS,每左移一次OFS加1,然后从原始字模数组里取每一行对应OFS列的位。取位操作可以用查表或者循环右移完成,资源里提供了完整宏。
4.3 完整流程代码:关闭点阵、送行数据、送列数据、刷新行
这是定时器0中断刷新一行的核心框架,可以直接套进任何51汇编工程:
T0_ISR: PUSH ACC PUSH PSW ; 1. 消隐:关闭所有行,避免拖影 MOV A, #0FFH MOV ROW_DATA_L, A MOV ROW_DATA_H, A ACALL SEND_ROW_BYTE ; 发送行全无效 ; 2. 从显示缓冲区取当前行数据 MOV A, ROW_NO ; 当前行号0-15 RL A ADD A, #BUF_START_L MOV R0, A MOV A, @R0 ; 低字节 MOV COL_DAT_L, A INC R0 MOV A, @R0 ; 高字节 MOV COL_DAT_H, A ; 3. 发送列数据到列595 MOV A, COL_DAT_L ; 先发低字节 ACALL SEND_COL_BYTE MOV A, COL_DAT_H ; 再发高字节 ACALL SEND_COL_BYTE ; 4. 发送行选码到行595 MOV DPTR, #ROW_TAB MOV A, ROW_NO RL A MOVC A, @A+DPTR MOV ROW_DAT_H, A MOV A, ROW_NO RL A INC A MOVC A, @A+DPTR MOV ROW_DAT_L, A MOV A, ROW_DAT_L ACALL SEND_ROW_BYTE MOV A, ROW_DAT_H ACALL SEND_ROW_BYTE ; 5. 同时锁存行和列 CLR LATCH_CLK SETB LATCH_CLK ; 6. 开启当前行(行OE拉低) CLR ROW_OE ; 7. 行号递增并回卷 INC ROW_NO CJNE ROW_NO, #16, T0_EXIT MOV ROW_NO, #0 T0_EXIT: POP PSW POP ACC RETISEND_COL_BYTE和SEND_ROW_BYTE是两个字节发送子程序,把A里的8位逐位移到DS引脚。由于行和列共用一个DS和SCK,发送行数据时列595也会收到脉冲,但因为还没有锁存,列595内部只是在移位,不影响输出。锁存信号RCK只在全部数据发送完成后才产生,这样行和列才同时有效。
行选码表ROW_TAB存放16组两字节行码。第一组对应“只选通第0行”,共阳接法下行有效电平为0,所以行码是0xFFFE,第二行0xFFFD,依次类推。也可以用软件计算,但查表更快。
4.4 汇编与C的共同调试:结合仿真器验证时序
如果整个工程是汇编写的,在调试器里单步执行时可以看到每次进入中断大约多长时间。每个SEND_COL_BYTE执行8次循环,每次SCK低高翻转,加上MOV、DJNZ这些指令,大约需要20多个机器周期。一次刷新最多发送4个字节,加上其他操作,总时长在1ms定时器周期内是够的。
如果发现中断程序还没执行完,下一次中断就来了,说明定时器初值设得太小或者发送子程序太慢。可以把定时器周期改成1.5ms,或者优化发送子程序,用硬件SPI替代软件模拟。但在51上,硬件SPI的速度也不一定匹配595的时序,很多开发者还是用IO模拟。
仿真器只能验证逻辑,不能验证电平驱动能力。上板之后用逻辑分析仪抓SCK、RCK和OE,确认每一帧有16个锁存脉冲,并且每个脉冲之间正好16位,这样才算真正跑通。
5. 避坑常见问题:左移边界、残影、闪烁、硬件接线
5.1 现象:左移后最右边出现一条竖线
原因:左移时高字节bit7被丢进进位后没有清掉,下一次左移它又循环回来,相当于一列数据堆积在右侧。还有可能是低字节和高字节处理顺序反了,导致跨字节位没被正确传递。
解决:严格按“先低字节RLC,再高字节RLC”的顺序。每次左移前先把进位C清零,并确认外部补入位已经放进C。调试时让一个只有单列亮点的测试图案左移,观察亮点是否平滑移动到右侧,而不是像分形一样扩散。
5.2 现象:屏幕亮度低,整个字发暗
原因:列端电流不够。16行扫描时,单个LED实际照亮时间只有总时间的1/16,平均亮度本来就很低。如果限流电阻选了1kΩ,瞬时电流不到3mA,平均亮度就更低。另一个常见原因是595输出高电平的拉电流能力有限,直接驱动LED时可能只有几个毫安。
解决:把限流电阻降到300Ω左右,并测量LED两端的压降。如果压差小于1.8V,说明电流被限制得很小。可以加一级ULN2003或三极管列驱动,让595只输出逻辑信号,由三极管提供真实电流。记住,点阵扫描的亮度从来不是靠加大电阻保护的。
5.3 现象:行切换时有重影,字边缘像被墨水浸过
原因:行选通和列数据切换重叠了。上一行还在亮,列595已经开始装载新数据,这时LED上会短暂出现上一行和下一行的混合图案。扫描速度越快,重影越明显。
解决:必须做到“先断行,再送列,后选行”。在定时中断开头立刻关闭所有行(行OE置高或行码全无效),之后发送列数据和新的行码,最后锁存并开启该行。我习惯用行OE硬件消隐,因为软件发送行码可能还需要几个微秒,OE拉高可以瞬间切断所有行电流。
5.4 现象:汇编程序跑飞,进入中断后死循环
原因:中断服务程序破坏了主循环的寄存器现场,或者堆栈溢出。51单片机上电时SP默认是0x07,如果代码区和堆栈共用低地址,中断一深就会把返回地址冲掉。另一个常见问题是定时器0中断向量处没有长跳转指令,而是直接写了一段程序,导致违反中断机制。
解决:汇编程序开头设置SP,#40H,把堆栈挪到内存高区。中断服务程序里至少压栈ACC、PSW,如果用到DPTR也要压栈。检查0x000B地址处是否有一条LJMP T0_ISR。我用这个排查方法解决过至少三个“跑飞”问题,每次都是先把SP抬上去再看。
5.5 现象:左移速度不均匀,间隔时间长短不一
原因:左移操作直接放在中断里运行,扫描行数和左移耗时的叠加导致每行刷新时间不稳定。还有可能主循环里用了软件延时,延时期间中断扫描会正常跑,但主循环被阻塞,左移动作就会卡顿。
解决:左移动作从中断里移出去,放到主循环。中断里只维护一个帧计数器FRAME_CNT,每完成一帧加1。主循环轮询FRAME_CNT,达到阈值后执行一次左移并清零。这样移动速度完全由阈值控制,非常均匀。阈值越大,移动越慢,最小单位是“每N帧移动一列”。
6. 进阶验证:用逻辑分析仪测时序,调整移动速度的一个小技巧
调试稳定之后,最容易踩的是接触不良和突发性花屏。我习惯用逻辑分析仪直接抓DS、SCK、RCK和ROW_OE四路信号。把触发设为RCK上升沿,屏幕正常刷新时,波形应该是:RCK上升沿前SCK上有连续16个脉冲,间隔均匀;随后还有16个脉冲用于行数据。如果看到SCK脉冲数量不是32,或者RCK前后波形对不上,基本可以确定是595级联方向或者字节发送顺序错了。这个验证比肉眼观察屏幕更早暴露问题,因为电气信号总是先于视觉结果出错。
移动速度调整不必依赖软件延时。细腻的做法是用帧计数。假设每行刷新1ms,一帧16ms,需要“每10帧移动一列”就表示160ms移动一列。在中断里设一个RAM变量FRAME_CNT,行号回卷时递增。主循环判断:
MAIN_LOOP: MOV A, FRAME_CNT CJNE A, #10, MAIN_LOOP ; 每10帧移动一次 MOV FRAME_CNT, #0 ACALL SHIFT_LEFT JMP MAIN_LOOP这里SPEED=10是一个立即数,你可以把它换成RAM变量,这样运行中可以随时调节速度。如果移动太快导致闪烁,就先提高帧刷新率,即把每行扫描时间从1ms缩短到0.8ms,再增加SPEED值。注意不要在主循环里死等计数器,否则其他按键响应会被卡住;更好的方式是把判断放在主循环的某条分支里。
有一次我调试左移时,字模表末尾没有补冗余数据,窗口移到最后直接跑进未初始化区域,屏幕上出现一团乱码。后来我在字模表末尾强制加了一组的0x00占位,并把窗口偏移量做了上限判定,从那以后凡是改到字模或者缓冲区相关代码,我都会先检查越界问题。希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取