news 2026/10/19 13:41:06

51汇编驱动74HC595实现16x16点阵汉字左移全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
51汇编驱动74HC595实现16x16点阵汉字左移全解析

简介:一份围绕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 RET

RLC 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-DSP3.0串行数据输入
595-SH_CPP3.1移位时钟
595-ST_CPP3.2锁存时钟
ROW0~ROW7P1.0~P1.7低 8 行行选
ROW8~ROW15P2.0~P2.7高 8 行行选
电源VCC/GND595 与点阵共地

工程骨架方面,汇编源文件里最前面必须放好中断向量跳转:

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 点阵汉字左移这条路走通,后面的玩法就靠你自己发挥了。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 13:35:13

数学PRM为何不适合Agent工具调用?过程监督的边界与务实评估方案

最近在给某智能体项目做训练和评估时&#xff0c;我踩了一个特别典型的坑&#xff1a;团队里有人提议&#xff0c;把数学推理场景里训练 Process Reward Model&#xff08;PRM&#xff0c;过程奖励模型&#xff09;那套方法直接搬过来&#xff0c;给 Agent 每一步的工具调用打分…

作者头像 李华
网站建设 2026/10/19 13:35:05

DivideMix:将带噪标签学习转化为半监督学习

干了几轮带噪声标签的项目之后&#xff0c;我第一次看到DivideMix这篇论文&#xff0c;心里那种"老办法总差一口气"的感觉终于有了出口。之前处理标签噪声&#xff0c;常用的思路无非是&#xff1a;拿小损失样本当干净样本、按置信度截断、给样本重新加权。思路没错&…

作者头像 李华
网站建设 2026/10/19 13:34:50

open-code-review:基于Git+CLI+LLM的可审计代码审查工作流

1. 项目概述&#xff1a;这不是又一个“AI写代码”工具&#xff0c;而是一套可嵌入开发流程的开源代码审查工作流“open-code-review”这个名字乍看平平无奇&#xff0c;但拆开来看——open不是指“开源”&#xff0c;而是指“开放式、可插拔、不绑定任何特定模型或平台”&…

作者头像 李华
网站建设 2026/10/19 13:34:28

基于PyQt5与ResNet18的京剧脸谱识别系统设计实践

1. 项目整体设计与技术选型1.1 这个项目解决的是什么问题京剧脸谱识别系统&#xff0c;本质上是一个图像分类任务。说白了&#xff0c;就是让电脑看一眼京剧脸谱的图片&#xff0c;然后告诉我们这是哪个人物、属于什么谱式&#xff0c;或者对应什么颜色类型。很多人在拿到这个题…

作者头像 李华
网站建设 2026/10/19 13:32:59

递归可视化:用JavaScript Canvas绘制谢尔宾斯基分形图形

递归不是玄学&#xff0c;它是一种把大问题拆成形状相同、规模更小的子问题的思维习惯。而谢尔宾斯基地毯、谢尔宾斯基三角形、分段线&#xff0c;正好是理解递归最直观的“视觉化教材”——你不需要去背复杂的递推公式&#xff0c;盯着一张图不断放大&#xff0c;看到它每一部…

作者头像 李华
网站建设 2026/10/19 13:28:46

车机双系统ASAN实战:QNX+Android9内存崩溃定位与构建配置

简介&#xff1a;这份资源面向高通8155p平台下从事QNXAndroid9双系统开发的工程师&#xff0c;聚焦于开启ASAN&#xff08;AddressSanitizer&#xff09;以定位内存越界、泄漏等疑难问题的编译配置方法。包内同时提供Android.mk与Android.bp两套构建脚本示例&#xff0c;覆盖li…

作者头像 李华