蓝桥杯单片机组的点阵部分,很多人一开始会被74HC595和38译码器绕晕,尤其当你用的是STC15F2K60S2这颗1T增强型51单片机时,时序和端口复用又跟传统51有些区别。这篇笔记是系列第十三篇,专门把8x8点阵从硬件结构、扫描原理到完整代码、调试踩坑一条线讲明白,适合刚学到点阵模块、准备省赛或者想搞懂动态扫描机制的选手参考。
点阵模块在蓝桥杯开发板上几乎是必考外设,它不像数码管那样直接给段码就行,真正难的地方在于:引脚不够用要扩展、8行8列要动态扫描、还要跟数码管、LED抢同一组IO口。搞明白这一块,后面做贪吃蛇、跑马灯、图形界面就顺手很多。
1. 整体硬件结构与驱动方案拆解
1.1 8x8点阵的物理结构
LED点阵模块看似神秘,本质就是64个发光二极管按8行8列排成方阵。内部连接方式分两种:行共阳、列共阴,或者行共阴、列共阳。蓝桥杯CT107D开发板上用的是行共阳、列共阴,也就是说每一行的8个LED阳极连在一起引出,每一列的8个LED阴极连在一起引出,对外总共16个引脚,8个行引脚、8个列引脚。
这个结构带来一个直接问题:如果我想让某一个点亮,只需要让该行给高电平、该列给低电平,电流从行流过LED到列,灯就亮了。但是想让不同行不同列的多个点同时亮,引脚就会打架。比如第一行和第二行同时要点亮不同列的灯,行引脚怎么给电平?所以64个LED只靠16个引脚无法实现任意组合的静态点亮,必须引入扫描机制。
CT107D这块板的点阵接口设计也比较特殊,它没有把16个引脚直接引到单片机IO口,而是通过一片74HC245做电流驱动,再配合74HC138译码器选通行、74HC595移位寄存器送列数据。这样的好处是单片机IO口只需少数几根线就能控制整块点阵,代价是驱动时序变复杂。这也正是蓝桥杯要考的点:你不仅要会写LED点灯,还要会操作串行转并行的芯片。
1.2 74HC595和38译码器各自负责什么
先说38译码器。它是一个3-8译码器,输入3根地址线A、B、C,输出8根线Y0到Y7,同一时刻只有一路输出有效。在点阵电路里,138的作用是选择当前要扫描哪一行。开发板上P2.3、P2.4、P2.5作为地址线接入138,通过给这三个引脚编码,就能从Y0到Y7中挑出一行来。
再说74HC595。这是一片串行输入、并行输出的移位寄存器,8位数据通过一根数据线逐位送入,送完8位后用一个锁存信号把数据同时输出到8个并行引脚上。点阵电路里,595的8个输出脚接到8条列线,用来控制当前选中的这一行里哪些列要点亮。它解决了点阵列数太多、IO口不够用的问题,让单片机只需要3根线(数据、移位时钟、锁存时钟)就能控制全部8列。
这两个芯片的分工可以简单类比:138是“选行开关”,决定点亮哪一行;595是“列数据仓库”,决定这一行里哪些列亮。扫描时先送一行列数据到595,再让138切到对应行,亮一小段时间,然后切下一行,如此反复。由于切换速度足够快,人眼看到的就是一个完整的画面,而不是8行在轮流闪。
2. 动态扫描原理与字模数据处理
2.1 为什么必须动态扫描,亮度怎么控制
如果一次只点亮一行,点阵最多同时亮8个LED,而我们要看到完整图形,就必须让8行以极快的速度轮流点亮。这个过程和专业术语里的“动态扫描”是一回事,显示器、LED广告屏也是这么干的。
人眼的视觉暂留效应大约持续几十毫秒,只要一帧画面在20ms以内刷新完,人就感觉不到闪烁。8行轮流点一遍算一帧,意味着每行的点亮时间不能超过2.5ms。通常我习惯把每行显示时间控制在1ms到2ms,这样一帧在8ms到16ms之间,刷新率在60Hz以上,肉眼看很稳定。
亮度跟每行点亮时间成正比。点亮时间越长越亮,但刷新率会下降,超过20ms就会出现闪烁;反过来点亮时间太短,虽然不闪但亮度很低。这里需要找一个平衡点。实际调试时我一般先定刷新率,反过来算每行时间:想要100Hz刷新率,一帧10ms,每行约1.2ms,然后再微调延时补足亮度。
另外要注意,STC15F2K60S2是1T增强型单片机,指令执行速度是传统12T单片机的12倍,同样的延时循环写出来实际延时差距很大。从传统51板子移植点阵代码时,延时函数一定重新标定,否则扫描频率要么快得异常、要么慢到闪屏。
2.2 字模怎么取,为什么经常是反的
点阵显示的字模是一组8字节数据,每个字节代表一行的8个LED亮灭状态,对应位为1表示亮。比如要在最上面一行点亮最左边一个点,字模第一字节就是0x80。把8个字节按行排列,就组成一个8x8的图案。
手工对阵型图案很痛苦,实际开发都用取模软件。PCtoLCD2002是最常用的,打开后设置取模方式为“纵向取模、字节正序、高位在前”,逐行取模,生成C语言数组。用的时候直接复制进代码,加上code关键字存到Flash,不占用宝贵的RAM。STC15F2K60S2虽然RAM有2K,但点阵动画如果帧数多,最好还是把静态图案放code区。
取模方向最坑。不同软件默认方向不一样,有的从左到右,有的从右到左;有的高位在左,有的低位在左。取出来的数据如果不匹配硬件接线,最直观的表现就是图形左右颠倒或者上下颠倒。解决方法是先取一个最简单的测试图案,比如只亮左上角一个点,看实际亮的是哪个位置,据此调整取模方向或者发送函数里的移位顺序。我在调试时都会先跑这个测试程序,五分钟就能确定方向,比瞎猜快太多。
2.3 多帧动画和滚动显示怎么扩展
静态图案只是基础,蓝桥杯题目经常要显示动态效果,比如心形跳动、箭头切换、字幕滚动。多帧动画就是多组字模数组,按时间顺序交替刷新。比如做心形跳动,准备两个不同大小的心形图案,每隔一段时间切换一次,视觉上就是心跳效果。
滚动字幕稍微复杂一点,核心思想是“窗口滑动”。比如一个8x8点阵要显示“AB”两个字的滚动效果,可以先把两个字拼成一个16x8的完整位图,然后每次取其中8列显示,每移动一帧右移一列,连续显示就形成字幕平滑滚动。这需要对字节数组做位移拼接,具体做法是:准备一个8字节的显示缓冲区,每次循环从完整位图中截取8列填入缓冲区,再整体刷新点阵。
这个扩展思路在比赛里很实用,很多题目表面上考点阵显示,实际考的是数组操作和状态切换。点阵本身只是最后的呈现工具。
3. 完整代码实现(STC15F2K60S2平台)
3.1 595时序驱动函数怎么写得稳
74HC595的时序是整个点阵驱动的核心,必须理解到位。数据线SER上的数据在SRCLK上升沿被移入移位寄存器,送完8位后,RCLK上升沿把移位寄存器内容锁存到并行输出引脚。所以发送一个字节的标准流程是:循环8次,每次把数据的最高位放到SER上,然后给SRCLK一个上升沿,最后数据左移一位。8位都送完后,给RCLK一个上升沿,数据才能出现在595的输出脚上。
这里有个新手容易犯的错:很多人只给SRCLK高电平,不先拉低再拉高,导致没有产生上升沿,数据一直进不去。正确做法是每次先把SRCLK清零,再置一,中间加极短延时。STC15F2K60S2速度快,如果驱动不够稳定,可以在每个时钟沿之间加几个空操作,保证芯片能稳定采样。
我常用的发送函数长这样:
void Send595(unsigned char dat) { unsigned char i; for(i = 0; i < 8; i++) { SER = (dat & 0x80) ? 1 : 0; dat <<= 1; SRCLK = 0; _nop_(); _nop_(); SRCLK = 1; _nop_(); _nop_(); } RCLK = 0; _nop_(); RCLK = 1; }nop()就是一条空指令,STC15在1T模式下大约占一个时钟周期,加这两个空操作是为了让数据线和时钟线之间保持足够的建立时间。虽然驱动595的要求没那么苛刻,但养成加空操作的习惯能减少很多莫名其妙的问题。
3.2 行选通道切换,别和数码管打架
点阵行选在CT107D上是通过38译码器选锁存通道实现的,具体到代码就是改P2口的地址线。我封装了一个选择函数:
void SelectHC138(unsigned char ch) { P2 = (P2 & 0x1F) | (ch << 5); }这个函数的意思是把P2.5、P2.6、P2.7作为138的地址输入,ch的值从0到7对应Y0到Y7。开发板上点阵行选接在Y6C通道,所以显示时调用SelectHC138(6)。如果你的板子通道不一样,改参数就行。
重点说一下为什么每次送列数据前要先关闭行选。如果不关,595在串行送数过程中,当前选中的行会一直亮着,并且列数据在变化,表现出来就是一行高亮乱闪的横线,还会把其他行拖出残影。正确顺序是:先SelectHC138(0)关闭所有行,然后送595数据并锁存,再打开Y6C选通行,延时,再关闭行选进入下一行。这个“先消隐、再送数、后点亮”的顺序,是点阵显示不闪不糊的关键。
还要提醒一点:P0口在开发板上同时连着数码管、LED、点阵等多个外设,切换外设前必须把上一外设的锁存通道关闭。否则你在刷新点阵时,P0上变化的行选数据可能同时被数码管锁存器捕捉,造成数码管乱码。我的习惯是每次切换外设后紧接着调用一次SelectHC138(0),把138所有通道关闭,从硬件上隔离干扰。
3.3 完整显示和主循环代码
下面是一份可以直接运行的心形图案显示代码,主循环不停地刷新点阵,实现静态心形显示。刷新函数里每行延时大约1ms,整屏刷新一轮8ms,刷新率125Hz,效果非常稳。
#include <STC15F2K60S2.H> typedef unsigned char u8; typedef unsigned int u16; sbit SER = P0^0; sbit SRCLK = P0^1; sbit RCLK = P0^2; u8 code heart[8] = { 0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00 }; void SelectHC138(u8 ch) { P2 = (P2 & 0x1F) | (ch << 5); } void Send595(u8 dat) { u8 i; for(i = 0; i < 8; i++) { SER = (dat & 0x80) ? 1 : 0; dat <<= 1; SRCLK = 0; SRCLK = 1; } RCLK = 0; RCLK = 1; } void Delay1ms() { u16 i; for(i = 0; i < 1200; i++); } void DisplayHeart() { u8 row; for(row = 0; row < 8; row++) { SelectHC138(0); // 关闭行选,消隐 Send595(heart[row]); // 送第row行的列数据 SelectHC138(6); // 打开点阵行选通道 P0 = ~(0x01 << row); // 选中第row行,低电平有效 Delay1ms(); // 点亮维持时间 SelectHC138(0); // 关闭行选,进入下一行 } } void main() { while(1) { DisplayHeart(); } }这份代码里的Delay1ms延时值是我在12MHz主频下大致估算的,实际用示波器看或者凭肉眼调也可以。注意P0行的取反操作是因为行选低电平有效,如果你的板子相反,去掉取反就好。整个核心就是DisplayHeart这个函数里的顺序,千万不要把送数据和选行的顺序搞反。
3.4 进阶:定时器中断扫描告别阻塞
用延时函数刷新点阵,主程序会被占住,啥都干不了。比赛题目往往要同时检测按键、刷新数码管、处理逻辑,这时候再用delay刷点阵就非常被动。正确的做法是把点阵刷新放进定时器中断,每隔固定时间中断一次,刷新一行,主程序只管业务逻辑。
我用STC15F2K60S2的定时器0做1ms中断,设置成16位自动重装模式。1T模式下定时器计数频率等于主频,按12MHz计算,1ms需要计数12000次,重装初值就是65536减12000等于53536,十六进制是0xD120。所以TH0写0xD1,TL0写0x20。如果你的系统主频不一样,按这个公式重新算一遍就行。
定时器中断扫描的结构是这样:中断服务函数里只做一件事——刷新当前行,然后行号加一。显示数据用一个全局数组存放,主程序想显示什么图案,直接改这个数组内容,中断自动刷新,完全不用干预。
u8 disBuf[8] = { 0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00 }; u8 curRow = 0; void Timer0_Init() { AUXR |= 0x80; // 定时器0工作在1T模式 TMOD &= 0xF0; TMOD |= 0x00; // 16位自动重装 TL0 = 0x20; TH0 = 0xD1; // 1ms @ 12MHz ET0 = 1; EA = 1; TR0 = 1; } void Timer0_ISR() interrupt 1 { SelectHC138(0); Send595(disBuf[curRow]); SelectHC138(6); P0 = ~(0x01 << curRow); curRow++; if(curRow >= 8) curRow = 0; }主程序里只需要初始化定时器,然后随便写业务逻辑,点阵始终稳定刷新。这套结构也直接对应蓝桥杯的评分点:功能正确、无闪烁、外设互不干扰。学到这个程度,点阵部分基本就通了。
4. 常见问题与排查技巧实录
4.1 点阵完全不亮或者整行常亮
点阵完全不亮,先别急着改代码,用万用表量一下点阵模块的供电,再确认595的VCC和GND有没有接对。排除硬件问题后,逐段检查程序:先不送列数据,手动把某一行选通、某一列拉低,看对应LED能不能亮。如果手动能亮、程序不亮,问题基本在595时序上。
整行常亮是很典型的锁存问题。可能原因是RCLK一直为高,595输出跟随移位寄存器内容变化,而不是稳定锁存。检查RCLK是否在每次送完8位后才产生上升沿,以及初始化时RCLK是否处于低电平。还有一种可能是138通道没有关闭,导致上一行数据一直保持。每次消隐时SelectHC138(0)务必调用,不能省。
4.2 显示闪烁、亮度不均匀
闪烁先看刷新率。如果每行延时过长,一帧超过20ms,人眼必然看到闪烁。用定时器中断法把刷新周期压到8ms以内,闪烁问题基本消失。
亮度不均匀的原因通常是扫描顺序和延时不均。比如最后一行的点亮时间明显短于其他行,是循环边界没处理好,curRow到7之后立刻清零,最后一行没有完整的延时。解决方法是把关闭行选的操作放在显示完最后一行之后,或者在清行操作后统一延时。再一个容易被忽略的点:进入中断刷行时,如果关中断时间过长,也会造成某一行被跳过,导致亮度不均。
4.3 图形左右或上下颠倒
这个问题八成是字模数据和硬件极性不匹配。先做一个测试:显示一个只有左上角亮的图案,如果实际亮在右上角,就是左右反了;如果亮在左下角,就是上下反了。左右反了需要把发送595的移位顺序改成先发最低位,或者对每个字模字节做位反转;上下反了则把字模数组倒序排列。
我个人的习惯是把方向校正放在发送函数里做,不在取模数据里改,这样换字模时不用每次都调整。比如统一在Send595里反转位序,或者写一个位反转函数,把所有字节转换一次。虽然多花几条指令,但换来的是字模数据统一,方便后续维护。
4.4 点阵一刷,数码管跟着乱码
这是CT107D端口共用的典型症状。前面说过,P0口和138通道被多个外设共享,点阵刷新时如果不把138切回空通道,P0上的数据就可能被数码管的锁存器锁存进去,导致数码管显示乱码。解决方法是在每次刷新点阵行之前,先SelectHC138(0)把所有通道关掉,再操作595和行选。每次退出点阵操作后,同样切回空通道。
另外,如果主程序里数码管和点阵都在刷新,两者共用同一个定时器中断或者主循环,必须保证二者不同时操作P0。我习惯把外设刷新拆成时间片:定时器中断里点阵优先,主循环里处理数码管和按键,点阵刷新在中断里独占,这样从根本上避免竞争。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全不亮 | 供电问题、时序错误 | 量电压、手动点亮测试、检查SRCLK上升沿 |
| 整行常亮 | RCLK锁存异常、行选没关闭 | 检查锁存时序、消隐函数是否调用 |
| 显示闪烁 | 刷新率太低 | 每行延时缩短、改用定时器中断 |
| 亮度不均 | 某行延时缺失、中断被打断 | 检查循环边界、关中断时间 |
| 图形颠倒 | 取模方向、位序不对 | 左上角测试图、位反转函数 |
| 数码管乱码 | 端口复用冲突 | 切换外设后关闭138通道、时序隔离 |
以上这些坑我基本都踩过一遍,尤其端口复用那个问题,省赛练题时调了快一个小时才反应过来。点阵模块一旦跑通,成就感是很强的,因为它把IO扩展、动态扫描、时序控制这些单片机核心概念全串起来了。后面做蓝桥杯的复杂题目,比如菜单界面加动画、状态机配合按键切换点阵图案,其实就是在这套基础上堆逻辑而已。