简介:一份围绕16x16点阵汉字左移显示的汇编开发资源,基于74HC595移位寄存器设计,面向电子设计、嵌入式系统初学者及对点阵显示感兴趣的开发者,完整演示了通过汇编语言操作DS、SHCP、STCP信号,完成汉字点阵数据加载、左移滚动与刷新显示的全过程,可应用于小型信息屏、仪表盘等简单显示设备。包体共11个文件,约57KB,核心为asm汇编源码与hex烧录文件,另含c辅助程序、dsn仿真电路、dbk备份及lst列表文件,便于在Proteus中对照仿真、检查时序。已有754人浏览学习。借助这些代码与电路,读者可以理解16x16汉字点阵的取模保存、逐位移入与锁存输出机制,掌握左移循环的完整流程,并学会通过调整延时控制滚动速度;同时,c源文件提供了辅助控制逻辑,与汇编主程序配合可扩展显示功能,汇编与高级语言对照,也便于初学者理解。对于课程设计、电子竞赛或入门单片机显示控制,都是不错的参考。
1. 一块 16x16 点阵屏,为什么值得用汇编从 595 开始做
“16x16 点阵显示汉字左移”听起来是个课程设计标配题,但真上手的人体会很深:网上一搜一大把 C 语言例程,全是调库函数,一旦换成汇编,数据到底怎么从 74HC595 串进去、左移一列到底移的是哪个字节的哪个位,立刻卡壳。这套项目就是用 51 汇编从底层驱动两片级联的 74HC595,实现 16x16 点阵汉字的滚动左移:定时器中断负责动态扫描,字模左移通过“当前字和下一字拼接补位”完成,整屏滚动不闪不跳。适合正在做点阵课程设计、或者想把“串转并、动态扫描、字模窗口”一次吃透的开发者,新手能照接线跑到滚屏,熟手能在此基础上改右移、改上移。
2. 硬件链路与驱动原理:16x16 点阵、级联 595 与动态扫描
2.1 一块 16x16 面板是怎么拼出来的
常见的 16x16 点阵屏不是一块现成的大屏,而是把 4 个 8x8 点阵模块拼成 2x2 的矩阵。每个 8x8 模块有 16 个引脚,8 根行线加 8 根列线,拼起来之后整屏变成 16 根行线(ROW0~ROW15)和 16 根列线(COL0~COL15)。
要让某个点亮起来,必须让对应的行线和列线同时“导通”。比如让第 3 行第 5 列的灯亮,就得选中 ROW3,再把 COL5 置成有效电平。这个道理很简单,难在数据怎么送给 16 根列线——如果直接用单片机 IO 口去驱动,至少要占 16 个引脚,那就没法玩了,于是 74HC595 就该上场了。
这里先把几个容易混淆的概念钉死:行选是“逐行扫描”的对象,一次只选一行;列数据是“这一行 16 个点亮的图案”,由两片 595 同时输出。我用的方案是:列数据走两片级联的 595,行选直接由单片机的 P1、P2 两组 8 位 IO 口各管 8 行。这样单片机只占 3 个引脚给 595(数据、移位时钟、锁存时钟),剩下 16 个引脚全留给行选,接线非常直观。
2.2 两片 74HC595 级联:串行数据怎么变成 16 列并行输出
74HC595 本质是一个“串入并出”移位寄存器,内部有 8 位移位寄存器加 8 位存储寄存器。数据从 DS 引脚一位一位送进去,每来一个 SH_CP 上升沿,移位寄存器里的 8 位数据整体往左挪一位,DS 上的新位补到最低位;8 位送完后,给 ST_CP 一个上升沿,移位寄存器里的数据一次性锁存到存储寄存器,并从 QA~QH 并行输出。
两片级联的关键在“串出引脚 Q7S”:第一片 595 的 Q7S 接到第二片 595 的 DS。发送数据时,先送入的位会随着移位一点点被推到第二片里。也就是说,想最终在屏幕上看是“左半列”的数据,应该最后发送,让它留在靠近输出端的这一片;先发送的数据会被推到后面那一片,对应屏幕右半列。所以两片级联的发送顺序是:先发送右半字节,再发送左半字节,每个字节内部按高位到低位发送。
2.3 动态扫描与定时器中断:为什么看起来每一行都常亮
人眼有视觉暂留,16 行不可能同时送数据点亮,所以实际做法是逐行扫描:先送第 0 行的列数据并选通行 0,点亮一小段时间;然后送第 1 行的列数据,切换到行 1…… 如此循环 16 行算一帧。只要每行刷新得够快,人眼看到的就是一块完整的屏。
我一般把扫描放在定时器中断里:定时器 T0 每 2ms 中断一次,中断里送一行数据并切换一行,16 行一轮就是 32ms,算下来帧率约 31Hz,肉眼不闪。这个参数可以调:如果想让画面更稳,就把定时器缩到 1ms,帧率到 62Hz,但每行点亮时间变短,整体亮度会下降;如果追求亮度,可以放宽到 3ms 一行,帧率约 20Hz,能感受到轻微闪烁。具体选几毫秒,取决于你的点阵是共阳还是共阴、以及用的三极管驱动能力。
3. 字模与左移算法:32 字节怎么排,左移一列到底移什么
3.1 16x16 汉字字模的内存排列:每行两字节,高字节在前
16x16 点阵要表示一个汉字,需要 16 行,每行 16 个像素点,总共 16x16=256 个点。按字节算,每行需要 2 个字节,一个字正好 32 字节。取模软件通常默认“横向取模、字节高位在前”的格式:每一行的 16 个点拆成左半 8 位和右半 8 位,左半字节的 bit7 对应这一行最左边那列,字节的 bit0 对应左边第 8 列;右半字节的 bit7 对应第 9 列,bit0 对应最右边第 16 列。
内存里一个字模的排布就是连续 32 个字节:
| 偏移 | 内容 |
|---|---|
| +0 | 第 0 行左字节 |
| +1 | 第 0 行右字节 |
| +2 | 第 1 行左字节 |
| +3 | 第 1 行右字节 |
| ... | ... |
| +30 | 第 15 行左字节 |
| +31 | 第 15 行右字节 |
这个排列顺序直接决定了取模时要用什么格式。如果换一种取模软件,导出的可能是“列行式”或者“低位在前”,那左移算法里的补位方向就全反了。所以拿到字模表后,第一件事是验证第 0 行左字节的 bit7 是不是对应屏幕左上角那个点,这一步错了后面全屏幕镜像。
3.2 左移一列的窗口逻辑:当前字与下一字拼接补位
很多人以为左移就是把整屏 32 字节按字节整体往左挪,其实不是。16x16 屏的“列”是每个 2 字节内部的一个 bit,左移一列意味着 16 行里每一行都要做一次 16 位移位。位移出的最高位丢弃,最低位要补进来的数据不是本行原样的,而是“下一行”或者“下一个字”的同行的最高位。
实际项目中,字库表是多个汉字连续存放的。要显示“欢迎光临”四个字左移,屏上任意时刻显示的其实是某个字的某一段。一列一列往左移时,最终会从当前字移动到下一个字。我的做法是:准备两个缓冲区,一个是当前显示帧 DISP_BUF(32 字节),一个是当前字的下一字 NEXT_BUF(32 字节)。每次左移一列,对这 16 行分别执行:
新第 i 行 16 位 = (当前字第 i 行 16 位 << 1) | (下一字第 i 行最高位)翻译成人话就是:当前这一行整体往左挪一个点,最左边挤出去的丢掉,最右边空出来的位置用下一个字同一行的最左边那一位补上。这样从左往右滚动,汉字边界处天衣无缝。
3.3 汇编里的更新子程序:16 行循环、进位链与补位
8051 是 8 位机,没有 16 位移位指令,所以需要把一行两个字节拆开处理。左移一列的实际字节操作是:
- 右字节先左移一位,它最高位移进进位标志 C
- 左字节再左移一位,C 补到左字节最低位,左字节自己的最高位被挤掉
- 把下一字左字节的最高位取出来,加到右字节的最低位
对应的汇编子程序如下:
; 显示缓冲首地址定在内部 RAM 0x30 DISP_BUF EQU 30H ; 下一字缓冲紧跟在显示缓冲后面 NEXT_BUF EQU 50H ; 每调用一次,整屏左移一列 UPDATE_DISP: MOV R0, #DISP_BUF ; R0 指向当前字第 0 行左字节 MOV R1, #NEXT_BUF ; R1 指向下一字第 0 行左字节 MOV R2, #16 ; 一共处理 16 行 U_ROW: MOV A, @R1 ; 取下一字左字节 MOV C, ACC.7 ; 补位 = 下一字最高位 MOV A, @R0 ; 取当前字左字节(高 8 位) RLC A ; 左移一位,C 补入 bit0 MOV @R0, A ; 写回左字节 INC R0 MOV A, @R0 ; 取当前字右字节(低 8 位) RLC A ; 左移一位,上一字节的进位进来 MOV @R0, A ; 写回右字节 ADDC A, #00H ; 用 ADDC 把补位加到右字节最低位 MOV @R0, A ; 再次写回右字节 INC R0 ; 指向下一行左字节 INC R1 ; 指向下一字的下一行左字节 INC R1 ; 跳过右字节 DJNZ R2, U_ROW RET这里有个很容易翻车的细节:右字节的补位不能直接写,要先经过 ADDC 指令。8051 的 ADDC A, #00H 会把进位标志 C 加上去,而进位标志正好在上一步 RLC 右字节时存了右字节的最高位。所以顺序必须是:左字节 RLC、右字节 RLC、再 ADDC 补位,三步缺一不可。
还要说明一点:上述代码更新的是当前帧,左移的节拍由主循环控制。每左移一列,就得从字模表重新填一次 NEXT_BUF,否则滚到第二个字后补位还是第一个字,屏幕会鬼畜。NEXT_BUF 的填充在换字时做一次:先把当前 DISP_BUF 清掉,再从 FONT 表把第 N+1 个字的 32 字节复制进来。
4. 核心代码逐段拆解:595 发送、扫描中断与左移主循环
4.1 SEND_BYTE:字节发送的完整时序
595 的时序不复杂,但顺序不能错。先把 DS 引脚置成要发送的电平,再给 SH_CP 一个上升沿,数据就被移进去了。我习惯写成“先置数据、再打时钟”:
DS BIT P3.0 SH_CP BIT P3.1 ST_CP BIT P3.2 ; 发送 A 中的一个字节,高位先出 SEND_BYTE: MOV R2, #8 ; 一个字节 8 位 SB_LOOP: RLC A ; 最高位移入进位 C,A 同时左移 MOV DS, C ; 把这一位送到数据引脚 SETB SH_CP ; 移位时钟上升沿 CLR SH_CP ; 拉低,准备下一位 DJNZ R2, SB_LOOP RETRLC A 是带进位的循环左移,执行完后 ACC.7 进入 C,下一位自然成了新的最高位,所以循环 8 次刚好把字节从高位到低位全部送出去。SH_CP 先 SETB 再 CLR,制造一个上升沿,595 在上升沿采样 DS 引脚,这个步骤相当于“拍照保存当前位”。
参数方面,DS、SH_CP、ST_CP 接哪三个 IO 完全可以自定义,只要 BIT 伪指令处改一下即可。需要注意的是 595 工作电压和单片机一致时才能直接驱动,5V 单片机带 5V 的 595 没问题,如果点阵模块是 3.3V 电平,就得加电平转换或者换用 3.3V 单片机。
4.2 扫描中断 ISR:一行数据的送出与行选通
SEND_BYTE 只是发送一个字节,真正显示一行需要两片 595 同时输出 16 位。我用 R3 存左半字节、R4 存右半字节,调用 SEND_16BIT 完成一次行数据输出:
; 发送一行 16 位列数据,R3=左半字节,R4=右半字节 SEND_16BIT: MOV A, R4 ; 先发右半字节 LCALL SEND_BYTE MOV A, R3 ; 再发左半字节 LCALL SEND_BYTE SETB ST_CP ; 锁存上升沿 CLR ST_CP RET注意发送顺序:先送右半字节,会被推到第二片 595;后送左半字节,留在第一片 595。这样从屏幕左边看,第一片 595 的 Q7 对应最左列,数据方向正确。
定时器 T0 每 2ms 中断一次,中断服务函数里完成“取当前扫描行的数据、送 595、切换行选”:
ORG 000BH LJMP ISR_T0 ; 内部 RAM SCAN_ROW EQU 3FH ; 当前扫描行 0~15 DISP_BUF EQU 30H ISR_T0: MOV TH0, #0F8H ; 12MHz、12T 模式,2ms 重装初值 MOV TL0, #30H MOV A, SCAN_ROW RL A ; 行号 * 2 ADD A, #DISP_BUF ; 加上缓冲区起始地址 MOV R0, A MOV R3, @R0 ; 左半字节 INC R0 MOV R4, @R0 ; 右半字节 LCALL SEND_16BIT ; 先把这一行的列数据送出去 ; 行选通:低 8 行用 P1,高 8 行用 P2,高电平选通 MOV A, SCAN_ROW ANL A, #08H JZ ISR_LOW MOV A, SCAN_ROW ANL A, #07H MOV R2, A MOV A, #01H ISR_SHIFT: JZ ISR_HIGH_OK RL A DJNZ R2, ISR_SHIFT ISR_HIGH_OK: MOV P1, #00H ; 先灭低 8 行 MOV P2, A ; 高 8 行选中目标行 SJMP ISR_NEXT ISR_LOW: MOV A, SCAN_ROW ANL A, #07H MOV R2, A MOV A, #01H ISR_SHIFT2: JZ ISR_LOW_OK RL A DJNZ R2, ISR_SHIFT2 ISR_LOW_OK: MOV P2, #00H ; 先灭高 8 行 MOV P1, A ; 低 8 行选中目标行 ISR_NEXT: INC SCAN_ROW MOV A, SCAN_ROW ANL A, #0FH MOV SCAN_ROW, A RETI这段代码里有一个时序原则:先送列数据,再切换行选。如果反过来,切换行的瞬间列数据还是上一行的,当前行会亮一下错误的图案,表现为屏幕边缘有“拖影”。灭掉另外 8 行再点亮目标行,是为了防止行与行之间短暂串扰。
如果你用的是 4-16 译码器做行选,可以把上面一大段行选逻辑换成四个 IO 输出行号,代码能短一半。原理不变,关键是行号来源始终是 SCAN_ROW 这个 0~15 的循环变量。
4.3 左移主循环:节拍控制与显示缓冲更新
扫描中断负责“显示”,主循环负责“移动”。我的主程序结构很简单:
ORG 0000H LJMP MAIN ORG 0030H MAIN: MOV SP, #70H MOV TMOD, #01H ; T0 模式 1,16 位定时器 MOV TH0, #0F8H MOV TL0, #30H MOV SCAN_ROW, #00H SETB ET0 SETB EA SETB TR0 ; 启动扫描 MAIN_LOOP: CLR EA ; 关闭中断,防止扫描读到一半的数据 LCALL UPDATE_DISP ; 左移一列 SETB EA LCALL DELAY_600MS ; 控制左移速度 SJMP MAIN_LOOP这里有个非常关键的操作:CLR EA 关闭总中断,等 UPDATE_DISP 执行完再 SETB EA。原因在于,扫描中断每 2ms 就会读取 DISP_BUF 里的 32 字节去刷新屏幕,如果主循环在改写缓冲区的过程中被打断,中断可能读到“半新半旧”的行数据,屏幕上这一行就会闪一下错位。关中断的时间很短,只有执行 UPDATE_DISP 那几十条指令,影响可以忽略。
DELAY_600MS 就是简单的软件延时,循环里套循环。想加快滚动速度就把延时改小,比如 200ms;想慢一点就加大循环次数。更平滑的做法是用一个计数器在中断里累加,每累计 N 次 T0 中断调用一次 UPDATE_DISP,这样滚动速度和扫描刷新互不干扰,后面二次开发部分会说。
5. 工程搭建与避坑排查:从接线到烧录的血泪记录
5.1 最小硬件接线与汇编工程骨架
以两片 595 驱动列、P1/P2 驱动 16 行选为接线方案,连线表如下:
| 信号 | 单片机引脚 | 说明 |
|---|---|---|
| 595-DS | P3.0 | 串行数据输入 |
| 595-SH_CP | P3.1 | 移位时钟 |
| 595-ST_CP | P3.2 | 锁存时钟 |
| ROW0~ROW7 | P1.0~P1.7 | 低 8 行行选 |
| ROW8~ROW15 | P2.0~P2.7 | 高 8 行行选 |
| 电源 | VCC/GND | 595 与点阵共地 |
工程骨架方面,汇编源文件里最前面必须放好中断向量跳转:
ORG 0000H LJMP MAIN ORG 000BH LJMP ISR_T0 ORG 0030H MAIN: ; 主程序从这里开始ORG 0000H 是复位入口,ORG 000BH 是定时器 T0 的中断向量地址。单片机复位后从 0000H 执行,通过 LJMP 跳到主程序;T0 中断触发时硬件跳到 000BH,再通过 LJMP 跳到 ISR_T0。很多人第一次写汇编点阵工程,忘了放 000BH 的中断跳转,结果一开定时器就飞程序,屏幕乱闪。
汇编源文件写好后,用你习惯的 8051 汇编工具链编译生成 HEX 文件,再用 51 烧录工具下载到单片机。烧录时注意选择正确的单片机型号和晶振频率,下载线连好波特率设置对,一般都能一次成功。
5.2 常见问题与避坑记录
先说说我调试这套电路时踩过的三个最典型的坑,每条都按“现象、原因、解决”写清楚。
第一个坑:屏幕整屏左移时,某一行出现明显的“断裂”或“抽动”。现象是画面大部分正常,但中间有一行图案明显错位,像是左半边和右半边来自不同时刻。原因是主循环的 UPDATE_DISP 改缓冲区时,T0 中断正好读到了正在更新的那一行数据,导致该行显示内容是新旧拼接。解决方法是更新缓冲区前关中断,更新完再开。这个操作看着不起眼,但没有它动态扫描和逻辑刷新一定会打架。
第二个坑:屏幕上汉字是“镜像”的,或者字的左右两半互换。现象是字能显示但是反的,或者左半边跑到右半边去。原因有两类:一是取模软件导出的字节位序和 SEND_BYTE 的发送顺序不一致,比如取模是低位在前,代码按高位先发;二是两片 595 级联的发送顺序写反了,先发了左半字节。解决方法是先画一个竖线或横线的测试字模,单独显示它,通过调试交换 R3/R4 的发送顺序,或者把 RLC A 改成 RRC A,验证哪个方向对,再批量改代码。
第三个坑:屏幕亮度很低,或者只有部分行亮。现象是显示内容能看见但整体很暗,或者总是隔几行才亮。原因是扫描周期太长,每行点亮时间占比太低;或者行选切换时列数据还没稳定,595 输出了错误数据导致该行几乎不亮。解决方法是把 T0 中断周期从 2ms 改到 1ms,帧率翻倍;同时确认行选切换前先完成了列数据锁存。
还有一个容易被忽视的问题:延时控制滚动速度时,不要用 TR0=0 暂停定时器来实现停顿,一旦定时器停了,扫描中断也停了,屏幕立刻只剩一行亮,体验非常差。正确做法是保持 T0 一直跑,主循环在两次更新之间做延时,保证扫描从不中断。
6. 二次开发方向:换字库、调速度、改右移与上移
这个项目拿到手之后,最容易做的改动是换字库。取模软件里选“横向取模、字节高位在前、每字 32 字节”,生成的字模数据按 CN1、CN2、CN3 的顺序粘进 FONT 表即可。要注意的是:字库表必须是完整字的连续数据,不能只取用到的字,否则左移到最后一个字时补位会拿到空的随机数。
想改成右移左移方向的滚动,不需要动扫描中断,只需要改 UPDATE_DISP 的补位方向。左移补的是“下一字左字节的 bit7”,右移补的是“上一字右字节的 bit0”,把 R1 指向的目标从 NEXT_BUF 换成 PREV_BUF,并把 RLC 逻辑整体反转。改上移则是另一个思路:左移是列方向移动,上移是行方向移动,每上移一行的本质是显示窗口在字模表里往下跳一行,扫描中断里给行地址加一个“行偏移量”就行,建议单独维护一个 ORIGIN_ROW 变量,中断取值时加进去。
从那以后,我每次接点阵屏项目,都强制自己先做一件事:把取模格式、字节发送顺序、行选极性这三个参数写死在程序头部的注释里,再开始写代码。这三个参数任何一对不匹配,屏幕表现都是各种鬼畜镜像,查起来非常烧时间。希望这篇拆解能帮你把 16x16 点阵汉字左移这条路走通,后面的玩法就靠你自己发挥了。
本文还有配套的精品资源,点击获取