☰
单片机驱动16×16点阵屏:74HC595串行扩展与字模编码详解
2026/10/16 1:31:29 网站建设 项目流程

1. 先搞清楚点阵屏到底是怎么“画”出数据的

单片机驱动点阵屏,本质上干的事情是“用有限的控制引脚,把一块由LED颗粒组成的像素网格点亮成特定的图案”。这块像素网格常见的有8×8、16×16、32×64等规格,你听到的“P10单红”“P5双色”“8x8点阵模块”,指的都是这种LED阵列。核心任务就是把要显示的数据——一个数字、一屏汉字或者一组滚动字符——转换成这个网格上每个LED的亮灭状态,再通过扫描刷新把它稳定地呈现出来。

这个项目为什么值得折腾?因为点阵显示是单片机里少有的“既要处理数据编码,又要处理时序控制,还得懂点硬件驱动”的综合活。一个简单的16×16点阵,256颗LED,单片机的IO口根本不可能每颗灯接一个引脚,于是就有了行扫描、列移位这些经典方案。做完这个项目,你对GPIO控制、时序协议、数据编码、刷新率这些嵌入式基础概念的理解,会比刷十遍理论课都扎实。

最适合做这个项目的,是已经会点C语言、写过几个GPIO点灯程序、手头有一块51或者STM32开发板的人。如果你连“上拉电阻”和“延时函数”都还没概念,建议先跑几个流水灯例程再回来。下面我从方案选型一直讲到调试排障,全程按我实际做过的路子走,代码和接线都是可以直接抄的。

2. 先拆需求再选方案:Direct IO、串行扩展还是专用驱动芯片

2.1 点阵屏驱动方案的底层逻辑

先说点阵屏驱动的一个根本矛盾:32×64的点阵有2048颗LED,直接每个LED一个引脚,任何单片机都扛不住。所以工程上从来都是“矩阵扫描”——把LED排列成行和列的交叉矩阵,某一瞬间只点亮一行的若干LED,靠人眼视觉暂留把快速切换的多行画面“融合”成完整一屏。既然是一行行点亮,那控制结构就变成两部分:行选择电路(决定当前扫到第几行)和列数据电路(决定这一行的哪些LED亮)。

行选择好解决,用IO口直接选,或者用译码器把3根线扩展成8根。列数据麻烦一点,因为一行的LED数量多,数据得一批批送过去。于是派生出三种公认的驱动流派:

第一种是直接驱动,适用于8×8这种小型点阵模块。共16个引脚,8行8列全部接单片机IO,用8个引脚选行,8个引脚送列数据。优点是程序最简单,逻辑直白,适合入门理解扫描原理。缺点是IO占用极高,16×16就得32个引脚,基本要挑引脚多的单片机才行。

第二种是串行扩展驱动,也就是用74HC595这类移位寄存器。列数据通过一根数据线、一根时钟线、一根锁存线,一位位“挤”进去,串转并之后一次性输出给整行列脚。这是目前小中规模点阵项目的主流方案——IO占用少了,数据传递方式变成了串行协议,这是嵌入式里最重要的技能点之一。

第三种是专用LED驱动芯片,比如MAX7219、TM1640、DP芯片这类。MAX7219内置了BCD译码、显示RAM、扫描逻辑,你只需要把数据通过SPI接口发过去,芯片自己负责扫描刷新。好处是程序极其简单,坏处是灵活性低,只能驱动它定义的矩阵结构,且不适合做大屏拼接。

2.2 16×16点阵的选型决策:为什么我推荐74HC595方案

16×16点阵是学习性价比极高的规格,既能显示完整的汉字,又不会像32×64那样对时序要求太苛刻。按我的经验,如果目标是“显示16×16汉字+滚动效果”,优先选74HC595串行扩展方案,

原因有三个:一是IO口占用量合理,8行+2个数据口就能搞定列数据,剩下的IO还能接按键、传感器;二是74HC595芯片极便宜,几毛钱一片,到处都买得到,拆机件也能用;三是它逼你理解“串行转并行”的数据链路,这知识后面接LCD屏、接LED灯带、接各种外设都用得上。

具体怎么扩展,16×16点阵一行16个LED需要16位列数据。串两片74HC595,一片管前8位、一片管后8位,把两片的串行数据口级联起来,就组成了一个16位的移位寄存器组。单片机每次往这个链路里“推”16位数据,推完给一个锁存上升沿,16个输出引脚同时刷新,这一行的亮灭状态就定了。

顺带提一下行驱动的选择。16行如果直接接IO,就得用16个引脚,不划算。用一个74HC154(4线转16线译码器),4根IO选行引脚就能控制16行。这样整个板子需要的IO是:4根行选+1根串行数据+1根移位时钟+1根锁存时钟,一共7个引脚,任何单片机都毫无压力。

2.3 不推荐的弯路

有人上来就想搞32×64全彩、用FPGA驱动、上LVDS接口——我劝你先冷静。全彩屏意味着RGB三色数据各占一组通道,数据量是单色屏的三倍,扫描频率要求也高,零基础直接做大概率被调试折磨到放弃。还有一个常见的坑是买那种“带MAX7219的8×8模块”,如果你目标只是点亮一块小屏,那个确实省事,但如果要做16×16甚至更大,MAX7219的级联能力和刷新灵活性远不如595,到时候又要换方案重写代码。

3. 拆解显示原理:汉字怎么变成点阵数据

3.1 数据编码的本质:把图案翻译成一串二进制

点阵显示的核心问题是:你想显示“好”这个汉字,怎么让256颗LED按正确的坐标亮起来?答案是取模。在16×16点阵里,一个汉字占16行×16列,每一行16个点,用两个字节的二进制表示:某一位是1,就表示这一列的这个点要亮。一个汉字总共32个字节,这32字节就是它的“字模数据”。

取模过程通常靠取模软件自动生成,不需要你手工数格子。以常用的取模软件为例,设置“纵向取模,字节倒序”或“横向取模,字节正序”,把汉字输入进去,软件直接输出形如0x10, 0x10, ...的十六进制数组。但这里有个极其容易踩的坑:取模模式的设置必须和你的显示扫描方向一一对应。如果软件默认“横向取模”,而你的代码按“纵向取模”解析,出来的汉字一定是乱的。

我自己常用的模式是“列行式,低位在前”。意思是:每一个字节的bit0对应点阵的最上面一行,bit1对应第二行,取完一个字节后,下一个字节接在下面继续往下,共16个字节覆盖16行,然后第二个字节对对应该是第二列。这和你代码里的移位输出顺序要严格一致,否则出现“旋转90度”“镜像翻转”这类问题。

3.2 字模数据在程序里怎么存

拿到字模数组后,存储结构一般是二维数组或者结构体数组。国家标准GB2312的汉字编码是按区码、位码排列的,但自己做项目不需要管那么深,直接建立一个连续的字模表就行。比如做“温度显示”项目,建一个code unsigned char temp_dis[],把0~9十个数字、冒号、字母C的字模按顺序放进去,显示的时候按索引取出来用。code关键字别漏,51单片机里这会把数据放到程序存储器而不是RAM——RAM本来就紧张,256字节的字模表放code可以省掉一大块内存开销。

一个16×16汉字占32字节,10个数字加符号也就四五百字节,对单片机毫无压力。注意:字模数组的排列顺序,一定要跟取模软件界面上看到的点阵排列顺序一致,不然就是“取对了模,却用错了方向”。

3.3 一行行扫描时,列数据是怎么算出来的

扫描执行的顺序是这样的:

  1. 选中第0行,把第0行的16位列数据通过595送入;
  2. 锁存输出,这一行的LED点亮;
  3. 延时一段时间(通常几百微秒到1毫秒),让眼睛“看”到这一行;
  4. 关闭当前行,选通第1行,送第1行列数据;
  5. 依次扫完16行,再回到第0行,开始下一帧。

这里的核心是:每次选通一行,送的永远是这一行在字模里对应的那两个字节。如果你把整个字模表看成16行×16列的一个矩阵,那第i行的列数据就是字模数组的第2*i和2*i+1个字节。

同理,如果要滚动显示一串文字,比如“欢迎光临”,那就不是一帧把所有字都点亮,而是每次只显示当前窗口里能看到的16列。每次刷新后,把取字模的起始列位置往右移一列,再把后续16列的数据送进去,就实现了平滑左移的效果。这个逻辑初看有点绕,但理解了“窗口滑动”就好办了——屏幕上永远只有16列可见,你的数据指针在整个字模长条上移动。

4. 完整实操:74HC595驱动的16×16点阵,从接线到代码

4.1 硬件接线与器件清单

我实测用的这套配置是很经典的组合,器件便宜、好买、容错率高:

器件型号/规格数量用途
单片机STC89C52RC(或任何51)1主控
点阵屏16×16共阳点阵(单色即可)1显示
移位寄存器74HC5952串转并,送列数据
译码器74HC15414线转16线,选行
三极管阵列8550(PNP)或ULN280316路行驱动,增强电流
电阻470Ω~1kΩ16列限流

接线这一块有个细节要重视:16×16共阳还是共阴,决定了列数据是高电平有效还是低电平有效。我下面给的是共阳方案——所有LED的阳极接行线,阴极接列线,选中某一行给高电平,列数据给低电平,对应LED才会亮。如果是共阴屏,逻辑反过来,驱动三极管的类型也要换。

具体引脚这么接:

  • 74HC595的DS(串行数据输入)接P3.4
  • SHCP(移位时钟)接P3.5
  • STCP(锁存时钟)接P3.6
  • 第一片595的Q7'(串行输出级联脚)接第二片595的DS
  • 两片的OE接地,MR接VCC(复位脚拉高才能正常工作)
  • 74HC154的四个选择输入A、B、C、D分别接P2.0~P2.3
  • 154的输出接8550基极,8550的发射极接VCC,集电极接点阵行公共端

我建议你先用杜邦线在面包板上搭,不要一上来就画PCB。第一次做点阵项目如果直接打板,出了问题很难分清楚是焊接问题还是逻辑问题,面包板插拔方便,调试效率高一倍。

4.2 关键代码:扫描主循环

下面这段代码是我自己的一个精简版本,去掉了没用的花活,保留了最核心的逻辑。芯片是STC89C52,晶振11.0592MHz。

#include <reg52.h> #include <intrins.h> // 三个控制引脚 sbit SRCLK = P3^5; // 移位时钟 sbit RCLK = P3^6; // 锁存时钟 sbit SER = P3^4; // 串行数据 // 数字和符号的字模(16x16点阵,col-major:先16行,再下一列) code unsigned char font[][32] = { // 0 的字模数据 {0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF, 0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0xE0,0xE0,0x00,0x00, 0x00,0x00,0xE0,0xE0,0x00,0x00,0x00,0x00}, // 1 的字模数据,此处省略,按需填充 }; // 行选:74HC154的4个输入 sbit A0 = P2^0; sbit B0 = P2^1; sbit C0 = P2^2; sbit D0 = P2^3; // 延时,约1ms void delay1ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 123; j++); } // 向595链路推入16位数据:先送高位字节,再送低位字节 void hc595_send16(unsigned char high_byte, unsigned char low_byte) { unsigned char i; for (i = 0; i < 8; i++) { SER = (high_byte & 0x80) ? 1 : 0; high_byte <<= 1; SRCLK = 0; _nop_(); _nop_(); SRCLK = 1; } for (i = 0; i < 8; i++) { SER = (low_byte & 0x80) ? 1 : 0; low_byte <<= 1; SRCLK = 0; _nop_(); _nop_(); SRCLK = 1; } // 上升沿锁存,16位输出同步刷新 RCLK = 0; _nop_(); _nop_(); RCLK = 1; } // 选中第row行,row取值0~15 void select_row(unsigned char row) { A0 = row & 0x01; B0 = (row >> 1) & 0x01; C0 = (row >> 2) & 0x01; D0 = (row >> 3) & 0x01; } // 显示一次完整帧:16行逐行扫描 void display_frame(unsigned char char_index) { unsigned char i; for (i = 0; i < 16; i++) { // 取第i行的两个字节(第i列对应font[char_index][16+i]?注意这里按你的取模方式定) unsigned char hi = font[char_index][i]; unsigned char lo = font[char_index][i + 16]; hc595_send16(hi, lo); select_row(i); delay1ms(1); // 每行停留约1ms,16行一帧约16ms,约62Hz刷新 } } void main() { while (1) { display_frame(0); // 一直刷新显示‘0’ } }

代码长得不复杂,但注释里面有句话我得展开说:取模方式和上面代码的对应关系。上面这个代码块里我用的是“列式取模,高位在前”——font[char_index][0]到[15]是第一列从上到下的16个点,[16]到[31]是第二列的16个点。送数据时第一列字节先送,并且最高位对应点阵第一行。

如果你用取模软件生成的是“行式取模”的数据,那上面代码里的索引逻辑就必须改:第0行的两个字节对应[0]和[1],第1行对应[2]和[3]。方向反了显示就是花的。

4.3 刷新率怎么算:为什么我坚持每行1ms

这段延时参数是可以算出来的。16行依次点亮一次算一帧,如果每行延时1ms,一帧16ms,刷新率约62.5Hz。人眼对闪烁的感知临界频率大概在50~60Hz,所以62Hz已经够了,不会看到明显闪烁。每行延时加到2ms,一帧32ms,刷新率约31Hz,这时候肉眼看单色屏已经能感觉到闪了,拍照更是严重色斑。所以我个人建议的行延时范围是0.5ms~1.5ms。

但是51单片机跑11.0592MHz,一条空指令约1.085us,上面delay1ms其实是软件延时,功耗和精度都一般。如果你用定时器中断来做扫描,比如每1ms进一次中断,在中断里切换下一行,效果会更好,主程序就能空出来干别的。这个后面可以自己优化,先把架构跑通最重要。

5. 常见问题与排查实录:点阵项目我踩过的那些坑

5.1 屏“糊”成一片,全是乱码

现象:上电之后所有LED全亮,或者显示出来的图案完全不是预期形状。

排查顺序:

  1. 先查行选和列数据是否接反。点阵屏引脚顺序不规范是祖传问题,不同厂家的引脚排序完全不一样。用万用表二极管档,或者写一个最简单的“全屏点亮”测试程序,依次选通行、送全列数据,确认哪一根行线对应哪个区域。
  2. 查取模方向和代码索引。见过太多次,字模是对的,像“0”这种对称字形看不出来,但显示“A”就翻车了。用对称数字自测没用,换不对称字符“L”“F”来验。
  3. 查扫描顺序。按上述代码,行应该从0到15排列,如果你反着扫(15到0),整个图案会上下颠倒,如果154的接线顺序错乱,会左右错位或者行序乱跳。

5.2 亮度不均匀:有些行亮有些行暗

这个多半不是代码问题,而是硬件驱动能力不足。一个16×16共阳屏,一行16个LED,如果每个LED电流按5mA算,一行就80mA。只靠74HC595的输出引脚直接驱动(595每个IO的灌电流能力有限),后面几颗LED就会明显变暗。解决办法有两个:

  • 行驱动加三极管。我用的是PNP 8550,基极接154输出,发射极接VCC,集电极接行公共端。选通哪一行,就让对应的三极管导通,供电电流的瓶颈从595转移到三极管上。
  • 列输出串电阻限流。电阻阻值根据LED压降和电源电压算,红色LED压降取2V,5V供电,3V落在电阻上。想让每LED电流8mA,就用3V/8mA≈375Ω,取330Ω或470Ω都能亮。电阻太小LED寿命短,太大亮度不够,330~1k之间调整。

5.3 显示有拖影和“鬼影”

现象是:这一行关了,下一行没亮的时候,上一行的LED还带着余光,或者出现了不该亮的行列残影。

关键原因是对“消隐”处理不当。595的输出锁存之后,即便你切换到下一行,上一行的数据其实还挂在输出上。在切换行选之前,必须先把列数据清零或者用OE引脚拉低来关断输出。代码上,我的习惯是在select_row()之后,调用一次hc595_send16(0x00, 0x00),先送全0数据,再切换下一行,这样595输出全灭,然后才亮下一行。如果你有硬件条件,把595的OE引脚接到PWM引脚,还可以顺便做亮度调节,拉低就是全灭,占空比调亮度。

每次扫描结束后,还应该在最后一行显示完,把OE引脚拉高一下,等下一轮开始时再拉低。这段消隐时序写在中断里,调试拖影问题就基本消失了。

5.4 用放大镜都看不清的“触点虚焊”

还有一个经常被忽视的坑:点阵屏模块的引脚孔距比较密,杜邦线插上去松垮垮的。排查数据错位问题如果查代码没问题,拿万用表蜂鸣档去量每一个引脚从模块到595输出的通路,十有八九有一两个引脚在软排线或焊点处断了。这一条相当费时间,建议从一开始就养成模块到手先全测一遍的习惯。

5.5 实时数据接入:让点阵显示温度传感器数值

项目标题里的“显示数据”不止是显示固定的汉字,更常见的是显示动态数值。比如把字模表做好后,再接到DS18B20温度传感器上,定时读取温度,把温度的每一位数字分别取出来,查找对应的字模,拼在一起滚动刷新。注意处理负号和超范围显示,这是显示系统里最容易被忽略的边界条件。我建议先把“静态显示字符”跑通,再考虑动态数据,分阶段做,出问题也好定位。

6. 显示滚动的正确姿势:从静态单字到多字滑动

6.1 静态显示到滚动显示的架构升级

前面说的是固定显示一个字符,如果你想显示“HELLO”这样一行字,就得把多个字模横向拼成一个长条形数据区。显示的逻辑从“每行取两个字节”,变成“每行取N个字中的某两字节”。

简单算一下:5个16×16汉字,横向排布是80列(5×16)。每次显示时,屏幕可视区域只有16列。假设当前左移偏移量offset是从0到64变化,每一帧扫描时,屏幕第0列显示的是长条数据的第offset列,第1列显示第offset+1列……第15列显示第offset+15列。每扫描一定次数帧后,offset加1,再继续刷新,就实现了平滑滚动。

滚动代码的关键点是:offset需要在中断或主循环里持续累加,而不是每次刷新都重置。做滚动显示时还要注意刷新率,每帧仍然要保证50Hz以上,不然画面一边滚一边闪,很容易头晕。

6.2 做滚动代码时容易犯的错误

最容易犯的错误是字模拼接时索引越界。5个字的字模长度是5×32=160字节,当offset偏到64时,屏幕最后一列取的是长条数据的第79列的数据,对应第二个字节索引还在160以内,没问题。但你要是把边界条件算错一位,直接访问font[5]这种不存在的元素,取出来的是乱数据。调试滚动效果时建议先在代码里加一个门限判断:if (offset >= max_offset) offset = 0;,并且把max_offset写成字模数*16 - 16,防止溢出。

6.3 定时器中断版扫描的好处

前面代码都是主循环软件延时扫描,做静态显示没问题,但一加滚动逻辑,主循环就要同时干两件事:计算offset和刷新屏幕。如果还用阻塞延时,滚动速度会和扫描耦合在一起,很难调到平滑。

我的建议是把“刷新一帧”搬进定时器中断,主循环只负责更新数据指针和offset。51单片机用定时器0做1ms中断,中断里送一行的数据,16次中断完成一帧。这样主循环和刷新互不干扰,后面接按键、接传感器、调节亮度都方便得多。实测下来,同样的硬件,用中断版扫描后显示稳定性明显提升,CPU占用也低了一大截。

7. 最后分享两个小经验

第一件是取模软件的配置问题。我最终采用的配置是“纵向取模、低位在前、每字节高位对应点阵上方”,但这个配置在不同软件里的叫法不一样。我的办法是先随便写一个字母“L”,生成字模数组,再写一个测试程序把数据按字节bit依次打印在串口,对照取模软件里的预览图,来回试两三次,找到自己代码和软件匹配的那套配置,然后把它固定下来,以后所有的字模都按这个配置出。

第二件是电源问题。16×16全亮时电流可能超过500mA,某些USB转TTL模块根本带不动,单片机就会不断复位闪烁。别怀疑自己代码,先检查供电——USB供电的板子尽量用独立5V电源给点阵供电,地和单片机共地就行。这问题我在好几个项目里都遇到过,排查了半小时代码,最后发现只是电源不够。点阵项目是少有的“程序写得好也得硬件扛得住”的活,软件和硬件两头都得扎实,这才是做这项目最大的收获。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询