16x16 LED点阵字符显示
约 2184 字大约 7 分钟
2025-01-06
一切的开始
应本学期课程《单片机原理及其应用》的课设要求,折腾了两天,终于把这事了了。课设题目非常经典,网上有不少类似的博客,但多半没有详尽的思路和配置过程,遂作此记录。
设计题目
使用AT89C52 / AT89S52 , MATRIX 16 * 16 ,74HC154 , RESPACK-8,74HC04,CRYSTAL,BUTTON,CAP,RES,CAP-ELEC以内的元器件设计电路,完成功能要求。
功能要求
使用开关控制 16 * 16 点阵显示内容。按下开关 K1:以四个 16 * 16 点阵动态汉字的形式显示自己的名字一名字为四个字直接显示自己名字,三个字的同学第四个汉字为“好"即显示***好'名字中只有两个字的同学,第三四个汉字为''你好',显示**你好。按下开关 K2 :以四个 16 * 16 点阵动态汉字的形式“生日快乐'或“新年快乐'“我爰中华"等其也不少于 4 个汉字的内容。
实现思路
电路原理图
程序源代码和注释
data1 EQU 30H;
data2 EQU 31H;
DLC_1 EQU 32H;
DLC_2 EQU 33H;
CURRENT_ROW EQU 34H;当前行下标
FUNC_DLEAY_ARG EQU 35H;延时函数参数
SCAN_COUNTER EQU 36H;行扫描计数器
CURRENT_CHAR EQU 37H;当前字符下标
CHAR_OFFSET EQU 38H;字符偏移量
CHAR_COUNTER EQU 39H;字符计数器
CHAR_DELAY EQU 40H;字符显示延时
MODE_SELECTED EQU R2 ;
OUT1 EQU P0 ;
OUT2 EQU P2 ;
ORG 0000H;
LJMP MAIN
ORG 0003H;
LJMP INT_0
ORG 0013H;
LJMP INT_1
INT_0:
MOV MODE_SELECTED,#1 ;已选择模式
MOV DPTR,#TABLE_1 ;选择表1
MOV CURRENT_CHAR,#0 ;重新置零字符下标
RETI
INT_1:
MOV MODE_SELECTED,#1 ;已选择模式
MOV DPTR,#TABLE_2 ;选择表2
MOV CURRENT_CHAR,#0 ;重新置零字符下标
RETI
ORG 0030H;
MAIN:
SETB EA ;使能总中断
SETB EX0 ;使能外部中断0
SETB IT0 ;设置外部中断0为下降沿触发
SETB EX1 ;使能外部中断1
SETB IT1 ;设置外部中断1为下降沿触发
CJNE MODE_SELECTED,#1,MAIN ;未选择模式则不运行
MOV CHAR_COUNTER,#4 ;初始化字符数
MOV CURRENT_CHAR,#0 ;当前字符下标值0
LOOP1:
MOV CHAR_DELAY,#100
LOOP2:
LCALL SCAN
DJNZ CHAR_DELAY,LOOP2
INC CURRENT_CHAR
DJNZ CHAR_COUNTER,LOOP1
SJMP MAIN
SCAN: ;逐行扫描函数
MOV SCAN_COUNTER,#16 ;扫描16行
MOV CURRENT_ROW,#0 ;当前行下标置为0
SCAN_LOOP: ;逐行扫描函数内部循环
LCALL CHECK_COLUM_DATA ;取出列数据
MOV OUT1,data1
MOV OUT2,data2
MOV P1,CURRENT_ROW ;逐行扫描
INC CURRENT_ROW ;扫描行号自增
MOV FUNC_DLEAY_ARG,#1 ;为延时程序设置参数
LCALL DELAY ;调用延时程序
MOV OUT1,#0FFH ;保证显示
MOV OUT2,#0FFH ;清晰可见
DJNZ SCAN_COUNTER,SCAN_LOOP ;符合循环条件则继续循环
RET
CHECK_COLUM_DATA: ;取出列数据
PUSH ACC ;保护现场
PUSH PSW
MOV A,CURRENT_CHAR ;将当前字符下标存入A
MOV B,#32 ;每个字符需要32字节的数据
MUL AB ;计算字符偏移量
MOV CHAR_OFFSET,A ;偏移量存至CHAR_OFFSET
MOV A,CURRENT_ROW ;将当前行下标存入A
RL A ;行下标 *=2,因为逐行扫描时
;需要提供2字节的列数据
ADD A,CHAR_OFFSET ;相加得到低字节的列数据偏移量
MOVC A,@A+DPTR ;一个很正常的相对寻址
MOV data1,A ;将低字节列数据放入data1
MOV A,CURRENT_ROW ;为高字节列数据
RL A ;计算偏移量,高
ADD A,CHAR_OFFSET ;字节偏移量比低
INC A ;字节多1,故自增
MOVC A,@A+DPTR
MOV data2,A ;将高字节列数据放入data2
POP ACC ;恢复现场
POP PSW
RET
DELAY:
PUSH DLC_1
PUSH PSW
MOV DLC_1, #2
DELAY_LOOP1:
MOV DLC_2, #248
DELAY_LOOP2:
DJNZ DLC_2, DELAY_LOOP2 ; 内层循环延时0.5毫秒
DJNZ DLC_1, DELAY_LOOP1 ; 外层循环延时1毫秒
POP PSW
POP DLC_1
DJNZ FUNC_DLEAY_ARG, DELAY ; 循环n次实现n毫秒延时
RET
TABLE_1:
;——生——;
DB 07FH,0FFH,077H,0FFH,077H,0FFH,077H,0FFH,003H,0C0H,07BH,0FFH,07DH,0FFH,07EH,0FFH
DB 07FH,0FFH,003H,0E0H,07FH,0FFH,07FH,0FFH,07FH,0FFH,07FH,0FFH,000H,080H,0FFH,0FFH
;——日——;
DB 0FFH,0FFH,007H,0F0H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,007H,0F0H
DB 0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,007H,0F0H,0F7H,0F7H
;——快——;
DB 0F7H,0FDH,0F7H,0FDH,0F7H,0FDH,037H,0E0H,0E7H,0EDH,0D5H,0EDH,0F5H,0EDH,0F5H,0EDH
DB 016H,080H,0F7H,0FDH,0F7H,0FAH,0F7H,0FAH,077H,0F7H,077H,0F7H,0B7H,0EFH,0D7H,09FH
;——乐——;
DB 0FFH,0FBH,0FFH,0F0H,007H,0FFH,0F7H,0FFH,077H,0FFH,07BH,0FFH,07BH,0FFH,003H,0C0H
DB 07FH,0FFH,06FH,0FBH,06FH,0F7H,077H,0EFH,07BH,0DFH,07DH,0DFH,05FH,0FFH,0BFH,0FFH
TABLE_2:
;——生——;
DB 07FH,0FFH,077H,0FFH,077H,0FFH,077H,0FFH,003H,0C0H,07BH,0FFH,07DH,0FFH,07EH,0FFH
DB 07FH,0FFH,003H,0E0H,07FH,0FFH,07FH,0FFH,07FH,0FFH,07FH,0FFH,000H,080H,0FFH,0FFH
;——日——;
DB 0FFH,0FFH,007H,0F0H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,007H,0F0H
DB 0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,0F7H,007H,0F0H,0F7H,0F7H
;——快——;
DB 0F7H,0FDH,0F7H,0FDH,0F7H,0FDH,037H,0E0H,0E7H,0EDH,0D5H,0EDH,0F5H,0EDH,0F5H,0EDH
DB 016H,080H,0F7H,0FDH,0F7H,0FAH,0F7H,0FAH,077H,0F7H,077H,0F7H,0B7H,0EFH,0D7H,09FH
;——乐——;
DB 0FFH,0FBH,0FFH,0F0H,007H,0FFH,0F7H,0FFH,077H,0FFH,07BH,0FFH,07BH,0FFH,003H,0C0H
DB 07FH,0FFH,06FH,0FBH,06FH,0F7H,077H,0EFH,07BH,0DFH,07DH,0DFH,05FH,0FFH,0BFH,0FFH
END
调试与实现过程(包含KEIL与protues的配置,联调)
Keil5基础配置
在Options For Target窗口Target选项卡中设置晶振频率与电路图一致,修改内存模型(是否支持XDATA)以及ROM大小,同时操作系统选项选择None,因为这里编写的是裸板程序。

在Output选项卡中选择Create Executable,勾选Debug Info和Create HEX File,其它选项默认,再选择一个合适的输出文件夹,这里我选择了项目路径下的output文件夹。

在Project窗口中右键Source Group1,添加汇编源码文件。


这里文件名我使用了MAIN.a51。
然后在MAIN.a51中编写代码。

编写完毕后点击即可编译构建HEX文件。

此即生成的HEX文件。
Protues基础配置

新建工程,布置电路。由于默认不存在16*16的LED点阵,故采用4块8*8点阵组合。
联调配置
联合调试需要VDM51.dll动态链接库。将VDM51.dll放在Proteus安装路径下的MODELS文件夹中。

同时放在Keil安装路径下的C51/BIN文件夹中。

配置Keil安装路径下的TOOLS.INI文件,添加VDM51.dll。

启动Proteus中的远程编译监视器。

同时在Keil中调整Options For Target的DEBUG选项卡。

选择USE Proteus VSM Monitor-51。
在Keil中点击,启动调试。
此时Keil进入调试状态,可设置断点,监视表达式等。

同时Proteus中无法暂停停止仿真,只能由Keil步进、步过、停止运行等控制。

效果展示

双击单片机,选择Program File加载生成的HEX文件。

启动仿真,此时没有显示,因为没有按下按钮决定显示模式。
可以随时按下按钮K2,切换到另一模式。




实验总结
- 如何保持代码思路清晰,可读性高?
- 我认为保持汇编代码思路清晰,直观易懂的一点关键是尽量少用寄存器。使用内存地址的别名可以提高代码的可读性,但是这仅仅适用于非常小型的程序。
- 关于LED点阵数据
- LED点阵数据即为逐行扫描时需要显示的列的位置,在某些特定的电路连接方式下可能导致LED的实际位置与点阵数据不一致。比如在此报告中LED点阵的列为低电平有效,故需要在原始的点亮LED列位置上按位取反,否则将会得到类似镂空显示的效果。同时由于我在此报告中旋转了LED,所以在取反后还需对称交换字节中的位。
- 关于Keil调试
- 一定要学会使用表达式监视功能,这对观察程序是否正常运行非常有帮助,特别是在调试循环和子程序调用的时候。
- 关于逐行扫描的一些细节
- 逐行扫描时容易出现重影,可以通过在输出正常内容之后清除屏幕(当前扫描的行)来消除重影。此报告中我使用了MOV OUT1,#0FFH MOV OUT2,#0FFH来消除重影。
- 关于建表
- 一开始我选择在MAIN之前建表,编译却得到了地址位置太小的错误。原因在于MAIN之前的ORG 0030H语句要求编译器把MAIN存储到从0030H地址开始的位置,但在MAIN之前我建立的两个表有256个字节,自然会与MAIN从0030H地址开始的要求冲突。比起修改0030H为其它合适的值,我认为把表建在程序末尾是更好的选择,这样表占用的储存就不会产生任何冲突。
贡献者
- SunRt233