简介:本资源为基于AT89C51单片机的16x16点阵LED电子显示屏设计说明文档,面向电子信息、自动化等专业的单片机课程设计、毕业设计学习者及入门开发者。文档围绕以AT89C51为核心的控制系统,讲解如何用一片74HC154行驱动器配合两片74HC595列驱动器,由4块8x8点阵模块拼成16x16点阵,实现全屏显示一个汉字的动态显示方案。压缩包内仅1个doc文件,约392KB,集中收纳了硬件设计思路、行与列驱动电路原理、总体电路、汇编与C语言程序设计及使用说明、附录程序清单等章节内容。读者可据此理清点阵屏行列扫描与动态显示的实现脉络,掌握点阵数据提取、字库调用和显示点阳极阴极电平控制方法,并参考电路图与程序完成自己的课程设计或实物调试。目前已有139人学习下载,适合需要完整设计报告与排错思路的初学者参照。
1. 32 根 IO 不是真正的问题
16 行加 16 列,16x16 点阵看起来刚好吃掉 51 单片机 P0 到 P3 全部 32 根 IO。很多人就是照这个式子开始画原理图的,焊完才发现 P0 是开漏结构、不加上拉推不动,串口烧录又占了 P3.0/P3.1,再想挂个按键或者蜂鸣器就没位置了。
真正卡住的是电流。一行 16 个 LED 同时发光,峰值电流能到 200mA 量级,任何一款 51 的单个 IO 都撑不住,整颗芯片的总灌电流也就几十毫安。所以 16x16 点阵的设计重点从来不是把 32 根线接满,而是用尽量少的 IO 把大电流移到片外去解决。
这份设计说明要走的链路是这样:先用 4 片 74HC595 把 32 路输出压到 3 根 IO 上,再把行扫描时序、消隐、字模取模方式讲清楚,然后是 Proteus 仿真怎么拼出 16x16,最后落到实物上电后的排查顺序。
2. 16x16 点阵的极性判断与驱动链路选型
2.1 共阳还是共阴:先确定扫描极性再画图
市面上一体化的 16x16 点阵模块不多,更常见的做法是拿 4 块 8x8 模块(1088AS、1088BS 这类)拼。每块 8x8 是 8 行加 8 列共 16 个引脚,4 块拼起来之后,行线按上下分成 16 路,列线按左右分成 16 路,总数还是 32 根。
这里必须先确认模块的公共端极性,因为它直接决定扫描时行和列各输出什么电平。很多人抄了一份代码发现全屏亮反、该亮的不亮,八成就是共阳共阴搞混了。
| 模块类型 | 公共端 | 点亮条件 | 行驱动应输出 | 列驱动应输出 |
|---|---|---|---|---|
| 共阴(1088AS) | 行 = 阴极 | 行低、列高 | 低电平(灌电流) | 高电平(拉电流) |
| 共阳(1088BS) | 行 = 阳极 | 行高、列低 | 高电平(拉电流) | 低电平(灌电流) |
判断方法很土但有效:用万用表二极管档,红表笔固定接某一脚,黑表笔依次扫其余引脚,如果有一组能点亮且红笔那脚是公共端,那就是共阳;反过来黑笔固定、红笔扫就是共阴。
连线时要特别注意跨模块的并联关系。左上和右上共享行 0 到行 7,左下和右下共享行 8 到行 15;左列 0 到 7 由左上和左下并联,右列 8 到 15 由右上和右下并联。这样接完,任意时刻选中一行,只有这一行上的 16 个 LED 可能点亮,另外三块因为行没被选中而保持熄灭。
2.2 为什么用 74HC595 级联,而不是 74HC138 译码
行选常见的另一种做法是 74HC138 加反相器,或者直接用 74HC154 做 4-16 译码。译码方案的优点是行选逻辑简单,缺点是输出低有效,如果点阵是共阳的还得再加一级反相,而且列数据仍然要另外找 16 路输出。
| 方案 | 占用 IO | 行选实现 | 列数据实现 | 主要问题 |
|---|---|---|---|---|
| 直接 IO 驱动 | 32 | P1/P2 八位 | P0/P3 八位 | 电流超限,IO 全占满 |
| 74HC138 x2 译码 | 4 + 3 | 4 根地址线 | 2 片 595 | 低有效需反相,行电流仍要外扩 |
| 74HC595 x4 级联 | 3 | 2 片 595 | 2 片 595 | 需要一次性移 32 位,时序稍复杂 |
4 片 595 串成一条链,只需要数据(SER)、移位时钟(SRCLK)、锁存时钟(RCLK)三根线,行和列的数据一起移进去再统一锁存。省下来的 IO 可以接按键、温度传感器或者串口,这是我在实际项目里更倾向的方案。
需要注意的是 74HC595 本身的电流能力。它的每个输出脚连续电流上限大约 35mA,但 VCC 和 GND 引脚的总电流上限只有 70mA 左右。一片 595 带 8 列,如果 8 列同时点亮、每列 12mA,总电流就到 96mA 了,已经越界。所以列驱动后面要接 ULN2803 这类达林顿阵列,行驱动因为要输出高电平,得用 UDN2981 或者 8550 三极管。
2.3 限流电阻和峰值电流的计算
扫描显示的平均亮度取决于峰值电流和占空比。16 行扫描的占空比是 1/16,如果按静态 5mA 去设计,实际看上去会非常暗,所以峰值电流要往上提。
# 16x16 点阵限流电阻与功耗估算 VCC = 5.0 # 系统供电 Vf = 2.0 # 红色 LED 正向压降,绿/蓝约 3.0~3.2 Vce = 0.3 # 驱动器饱和压降(ULN2803 约 0.9~1.2,三极管约 0.3) If_peak = 0.012 # 目标峰值电流 12mA R = (VCC - Vf - Vce) / If_peak print("限流电阻计算值:%.1f 欧" % R) print("取标称值 220 欧,实际峰值电流 %.1f mA" % ((VCC - Vf - Vce) / 220 * 1000)) # 单行全亮时的总电流 I_row = 16 * (VCC - Vf - Vce) / 220 print("单行全亮总电流:%.0f mA" % (I_row * 1000))按 220Ω 算下来峰值电流约 12mA,一整行 16 个 LED 全亮时行驱动要过 190mA 左右,这就是为什么行驱动必须上三极管或专用驱动芯片。如果点阵是共阳、行接阳极,建议用 UDN2981,它 8 通道、每通道 500mA,两片正好覆盖 16 行;如果是共阴、行接阴极,ULN2803 两片就够了。
限流电阻放在列线上,每列一个,不要图省事整片共用一个。因为每一列在同一时刻只对应一个 LED,单列电流就是 12mA,220Ω 的 1/4W 电阻完全够用。
3. 51 单片机 16x16 点阵的 C51 扫描代码
3.1 74HC595 的字节发送函数与时序
595 的移位是上升沿有效,数据在 SRCLK 上升沿被读入移位寄存器,32 位全部移完之后给 RCLK 一个上升沿,数据才出现在输出脚上。整个过程中 OE 要保持低电平使能输出。
#include <reg52.h> sbit SER = P1^0; // 74HC595 串行数据输入 sbit SRCLK = P1^1; // 移位寄存器时钟,上升沿移入 sbit RCLK = P1^2; // 存储寄存器时钟,上升沿锁存输出 sbit OE = P1^3; // 输出使能,低有效(用于 PWM 调光) /* 发送一个字节,先发最高位 */ void HC595_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { SER = (dat & 0x80) ? 1 : 0; // 取最高位送到数据线 dat <<= 1; // 左移,准备下一位 SRCLK = 0; SRCLK = 1; // 上升沿,595 采样数据线 } } /* 一次发送整帧:行高、行低、列高、列低,共 32 位 */ void HC595_SendFrame(unsigned char rowH, unsigned char rowL, unsigned char colH, unsigned char colL) { HC595_WriteByte(rowH); // 最先发的位会走到链尾,顺序由硬件接线决定 HC595_WriteByte(rowL); HC595_WriteByte(colH); HC595_WriteByte(colL); RCLK = 0; RCLK = 1; // 锁存,32 路输出同时更新 }这里最容易出错的是发送顺序。74HC595 是串入并出,第一个发出去的位会一路移到链的最末端。假设硬件连接是 MCU → 列低 → 列高 → 行低 → 行高,那么最后发出去的那一字节才会留在第一片 595 上。调整顺序的办法只有两个:要么按硬件实测结果调换调用次序,要么在原理图阶段就把 4 片的串联顺序定义清楚再写代码。
如果发现显示出来整个画面是镜像或者上下颠倒,不用改代码,直接交换 rowH 和 rowL 的调用位置就能验证是不是字节序问题。
3.2 定时器 T0 中断里的行扫描与消隐
扫描周期要控制在 16ms 以内,否则人眼会看到明显闪烁。16 行分下来每行 1ms 对应约 60Hz 刷新率,为了留余量,我一般把 T0 设成 2ms 一行、8 位行序列分两轮走完,或者直接 1ms 一行。
unsigned char code RowMask[16] = { 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80, // 行 0~7,对应行低字节 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80 // 行 8~15,对应行高字节 }; unsigned char xdata FrameBuf[32]; // 16 行 x 2 字节的显示缓冲区 unsigned char scanRow = 0; void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; // T0 方式1,16 位定时 TH0 = (65536 - 1000) / 256; // 12MHz 晶振下定时 1ms TL0 = (65536 - 1000) % 256; ET0 = 1; EA = 1; TR0 = 1; } void Timer0_ISR(void) interrupt 1 { unsigned char rowL = 0, rowH = 0; TH0 = (65536 - 1000) / 256; TL0 = (65536 - 1000) % 256; /* 1. 先消隐:把行输出全部关掉,避免上一行的残影串到下一行 */ HC595_SendFrame(0x00, 0x00, 0x00, 0x00); /* 2. 组装当前行的行选信号 */ if (scanRow < 8) rowL = RowMask[scanRow]; else rowH = RowMask[scanRow]; /* 3. 发送当前行的列数据(两字节)和行选信号 */ HC595_SendFrame(rowH, rowL, FrameBuf[scanRow * 2], FrameBuf[scanRow * 2 + 1]); scanRow++; if (scanRow >= 16) scanRow = 0; }中断函数里有三件事必须按顺序做:先消隐、再组装行选、最后发帧。消隐这一步不能省,否则上一行的列数据会和新行选叠加,出现一条竖着的“鬼影”。发帧时列数据的两个字节顺序和字模高低位要对上,如果显示出来左右反了,把FrameBuf[scanRow*2]和FrameBuf[scanRow*2+1]互换即可。
3.3 字模怎么取:让 32 字节和扫描顺序对齐
16x16 汉字每个字符 32 字节,按逐行式取模时,每行 2 字节、共 16 行。取模软件设置必须和扫描代码里的列字节顺序一致,这是新手最容易踩的坑。
| 设置项 | 推荐值 | 说明 |
|---|---|---|
| 点阵格式 | 阴码 | 点亮位为 1,与 FrameBuf 直接对应 |
| 取模方式 | 逐行式 | 每行 2 字节,共 16 行,和扫描顺序一致 |
| 取模走向 | 顺向(高位在前) | 对应代码里dat & 0x80先发最高位 |
| 输出格式 | C51 数组 | 直接贴进code区,省手动改格式 |
/* “单”字 16x16 阴码、逐行式、顺向,共 32 字节 */ unsigned char code Zi_Dan[32] = { 0x00,0x00,0x1F,0xF0,0x10,0x10,0x10,0x10, 0x1F,0xF0,0x10,0x10,0x10,0x10,0x1F,0xF0, 0x00,0x00,0x3F,0xF8,0x01,0x00,0x01,0x00, 0x01,0x00,0x01,0x00,0x7F,0xFE,0x00,0x00 }; void LoadZi(unsigned char *zi) { unsigned char i; for (i = 0; i < 32; i++) FrameBuf[i] = zi[i]; // 直接平移到扫描缓冲区 }如果取模时选了“逆向(低位在前)”,显示出来会变成横条纹错位的乱码,这时候要么改取模设置,要么在发送前对每个字节做一次位反转。实测下来,直接把取模软件设置改对,比在代码里加位反转函数稳妥得多。
4. Proteus 仿真搭建与实物调试排查
4.1 用 4 个 8x8 元件拼出 16x16 点阵
Proteus 元件库里通常找不到现成的 16x16 点阵。常见做法是在元件搜索框里找MATRIX-8X8-RED(共阳)或MATRIX-8X8-GREEN(共阴),放 4 个,按象限拼成 2x2 阵列,然后手动连线。
连线时注意 Proteus 的矩阵元件引脚方向和实物模块可能相反,元件属性里能看到ROW和COL的说明,先点进去确认公共端是哪一侧。4 个模块的行线按上下两组并联,列线按左右两组并联,总线用BUS工具画,比一根根拉线清晰。
驱动部分用74HC595加ULN2803,时钟源选AT89C52加 12MHz 晶振与复位电路。如果嫌 4 片 595 连线太乱,仿真阶段也可以只画 1 片 595 加 8x8 点阵先验证扫描逻辑,跑通之后再扩到 16x16。
4.2 不亮、鬼影、闪烁的排查表
| 现象 | 常见原因 | 处理方法 |
|---|---|---|
| 全屏不亮 | OE 悬空、RCLK 没接、P0 无上拉 | 测 OE 电平,量锁存脚波形,P0 加 10k 上拉排阻 |
| 只有一行亮 | 行选只发了一个字节、串接顺序反了 | 检查 HC595 链上 4 片的输出对应哪 8 路 |
| 有竖条鬼影 | 换行前未消隐 | 在中断开头补一次全零发送再发新帧 |
| 整屏闪烁 | 扫描周期大于 20ms | 把 T0 重载值改小,或减少每行停留时间 |
| 亮度不均、某列偏暗 | 该列限流电阻偏大、595 输出脚虚焊 | 逐列量电阻,补焊 595 输出脚 |
| 显示左右镜像 | 字模取模走向反了,或列字节顺序颠倒 | 交换列高、列低两个字节 |
排查时建议把扫描频率先降下来,比如改成 100ms 一行。这样人眼能直接看到当前扫到哪一行、哪一列亮,鬼影和错位会变得非常明显,比盯着 60Hz 的模糊画面猜要快得多。
4.3 从仿真到烧录:上电自检的正确顺序
仿真跑通不等于实物能跑,因为实物的电流和线阻是仿真里没有的。我的习惯是先写一段自检程序,按固定顺序点亮,上电后用肉眼逐项确认。
/* 上电自检:全亮 -> 逐行走点 -> 逐列走点 */ void SelfTest(void) { unsigned char i, j; unsigned char *p; /* 1. 全屏点亮 1 秒,检查所有 LED 是否都能亮 */ for (i = 0; i < 32; i++) FrameBuf[i] = 0xFF; for (i = 0; i < 200; i++) Delay1ms(); /* 2. 逐行走点,每行停 300ms,检查行驱动是否逐路正常 */ for (i = 0; i < 16; i++) { for (j = 0; j < 32; j++) FrameBuf[j] = 0x00; FrameBuf[i * 2] = 0x80; // 每行最左一个点 DelayMs(300); } /* 3. 逐列走点,检查列驱动和限流电阻 */ for (i = 0; i < 16; i++) { for (j = 0; j < 32; j++) FrameBuf[j] = 0x00; p = &FrameBuf[0]; FrameBuf[(i / 8) * 2 + (i % 8) * 2] = 0x01; // 按列位置置位 DelayMs(300); } }自检时先测电源电压,4 片 595 加两片驱动芯片全亮的时候电流会到几百毫安,用 5V/1A 的电源比较稳。上电前先用万用表量 VCC 和 GND 之间有没有短路。烧录器下载时记得把 P3.0/P3.1 上的外设断开,否则下载容易失败。
5. 16x16 点阵做滚动显示与 PWM 调光的落地写法
滚动显示的关键是把每一行当成一个 16 位无符号数来移位,而不是按字节移。按字节移会破坏跨字节的进位,出现一列一列跳着走的情况。
/* 整屏左移一列,每行独立移位,不跨行进位 */ void ScrollLeft(void) { unsigned char i; unsigned int v; EA = 0; // 移位期间关中断,避免扫描到半帧数据 for (i = 0; i < 16; i++) { v = ((unsigned int)FrameBuf[i * 2] << 8) | FrameBuf[i * 2 + 1]; v <<= 1; FrameBuf[i * 2] = (unsigned char)(v >> 8); FrameBuf[i * 2 + 1] = (unsigned char)(v & 0xFF); } EA = 1; }16 位拼接再左移的做法保证高位字节的低位能正确进到下一个字节。每次调用移一列,滚动速度靠调用间隔控制:把这段放进 T0 中断里每 16 帧移一次,效果就是每秒移动约 4 列,视觉上比较舒服。注意移位前关中断,否则可能出现半帧新数据、半帧旧数据同时显示。
调光用 595 的 OE 脚最省事。把 OE 从 P1.3 接出来,用定时器 T1 产生一个 1kHz 左右的 PWM,占空比越大、点亮时间越长,画面越亮。
unsigned char duty = 8; // 占空比 0~10,10 为最亮 void Timer1_Init(void) { TMOD &= 0x0F; TMOD |= 0x10; // T1 方式1 TH1 = (65536 - 100) / 256; // 100us 一步,10 步一个 PWM 周期 TL1 = (65536 - 100) % 256; ET1 = 1; TR1 = 1; } unsigned char pwmCnt = 0; void Timer1_ISR(void) interrupt 3 { TH1 = (65536 - 100) / 256; TL1 = (65536 - 100) % 256; pwmCnt++; if (pwmCnt >= 10) pwmCnt = 0; OE = (pwmCnt < duty) ? 0 : 1; // 0 使能,1 关闭 }T1 和 T0 同时跑的时候要注意中断优先级。扫描中断负责刷新,PWM 中断负责亮度,两者都不要写太长,否则会互相拖慢。如果发现调光时画面开始抖,先把 T0 的重载值算准,确认 1ms 的扫描间隔没有被 T1 打断太久。
亮度调节不建议直接改限流电阻,因为那是固定值,改起来要动硬件。用 OE 做软件调光,可以在同一个固件里针对不同环境亮度切换占空比,白天调到 10、夜里调到 3 都不需要重新焊板子。
本文还有配套的精品资源,点击获取