前言
本章接触的硬件依然与LED息息相关,它是多个LED按矩阵形式封装的一个显示模块。有了它,我们就可以制作出流动字幕,自定义动画等效果了。
本节完整工程文件已上传GitHub,仓库地址,欢迎下载交流!
硬件介绍
LED点阵的基本单元由LED组成,常见点阵屏大小为8x8或16x16。LED点阵根据行的连接方式进行区分。共阳极点阵的特点是每行LED共阳极,每列LED共阴极。共阴极点阵的特点是每行LED共阴极,每列LED共阳极。
![]() | ![]() |
诚然,与数码管类似,我们不可以通过IO口去直接驱动LED点阵,原因有两点:
- 单片机引脚为弱上拉,灌电流较大,但拉电流很小,难以直接驱动。
- 点阵屏极其消耗IO口资源,小规格的8x8点阵就要消耗16个IO引脚,这显然是不合理的。
因此,我们一般采用驱动芯片来实现LED点阵屏的驱动。
驱动芯片
在数码管控制中,为节省IO资源,采用了动态扫描的方式实现多个数码管的同时显示。LED点阵的控制思想也与之类似。但稍有不同的是,在数码管中我们采用的是74HC245驱动芯片和74HC138译码器的解决方案(当然也可以应用于LED点阵),而LED点阵采用了74HC595芯片,它可以实现仅使用3个IO口就控制8个引脚,还可以级联。
需要明确一点,任何元器件的控制方案都不止一个,可以通过查阅芯片手册来选择合适的芯片。仅从学习角度而言,不要太局限自己的认知。
1. 74HC595芯片(串转并)
![]() | ![]() |
IO扩展芯片,可以实现将8位串行输入转为三态并行输出。内置移位寄存器和存储寄存器,由各自的时钟控制。
SER
:串行输入(8bit
数据)。SRCLK(SCK)
:移位寄存器时钟引脚。当接收上升沿时,将串行数据由高位至低位依次存进移位寄存器(8位)中(一个上升沿移一位,类似于压栈过程)。最大工作频率可达50MHz。RCLK(RCK)
:存储寄存器时钟引脚。当接收上升沿时,将移位寄存器中的所有数据一次性全部存进存储寄存器中。OE(G)
:使能引脚。低电平使能,高电平并行输出为高阻态。SRCLR(SCLR)
:复位引脚。低电平复位(清除移位寄存器)。QH'
:串行输出。用于74HC595级联(可以实现3个IO控制多个LED点阵)。最大传输延时仅20ns。
注:时钟最小脉宽16ns
2. MAX7219芯片
MAX7219也是LED点阵屏控制芯片,它内部有8x8的数据寄存器,可以自动扫描显示一张静态图形。这显然优于74HC595,它只有8位的数据寄存器,显示一张静态图形,需要靠动态扫描完成。
现在市面上比较大型的LED点阵基本采用MAX7219芯片(缺点就是贵!),它同样也只用3个IO口,但可以自动完成扫描。
![]() | ![]() |
DIN
:串行输入(16bit
数据)。LOAD(CS)
:片选引脚。该引脚状态不影响串行数据加载至移位寄存器中(MAX7219)。拉高时将最后16bit
数据锁定。CLK
:时钟引脚。当接收上升沿时,串行数据由高位至低位加载至移位寄存器中。当接收下升沿时,数据从DOUT
输出。最大频率10MHz。DOUT
:串行输出。用于级联,在DIN
接收数据后16.5个时钟周期后有效。最大传输延时仅20ns。ISET
:亮度调节。通过一个可调电阻连接到V+
,可实现段电流调节。DIG0~7
:位选引脚。用于连接共阴极接受灌电流。当关闭时,上拉至V+
(高电平)SEGA~G,SEGDP
:段选引脚。根据待显示段码提供驱动电流。当该段关闭时,下拉至GND
。
原理分析
LED点阵可以显示静态画面和滚动画面,实现的细节稍有不同,总结如下:
- 规则矩形:无需动态扫描,想要点亮哪排哪列直接给对应的引脚赋电平即可。例如,点亮LED点阵中的某个LED。
- 自定义图形:由于自定义图形往往不规则(非矩形),必须采用动态扫描的方式。图案的设计可以自己手算,或者借助取模工具。例如,点亮一个爱心图形。
- 自定义动画:以上都是静态画面,LED点阵也可以实现比较简陋的动画效果。每一帧的画面需要提前保存在数组中,每一帧都采用动态扫描显示,一段时间后跳至下一帧,连贯成动画。例如,滚动字幕、旋转风车、螺旋线等等。
对于自定义动画,由于片内RAM只有128B,而片内ROM有8KB,可以通过code
关键字将动画数据存放在ROM中。另外,有些具备数学规律的动画可以通过算法计算下一帧的数据,而不用将所有的动画帧都存放在数组中占用空间。
软件实现
开发板上集成的是74HC595芯片,故主要按该芯片编写对应代码。
1. 爱心图片
实现的效果:静态显示一个爱心
#include <REGX52.H>
#define LED_PORT P0
typedef unsigned char u8;
typedef unsigned int u16;
sbit SER = P3^4; // 串行输入
sbit ST = P3^5; // 存储寄存器时钟引脚
sbit SH = P3^6; // 移位存储器时钟引脚
u8 code LED_portX_Array[] = {0x7e,0xbd,0xdb,0xe7};
u8 code LED_portY_Array[] = {0x38,0x7c,0x7e,0x3f};
void delay(u16 t){
while(t--);
}
void LED_control(u8 dat){
u8 i;
// 将一个字节拆分成串行输入
for(i=0;i<8;i++){
SER = dat >> 7; // 先将最高位送入SER中
dat <<= 1; // 左移1位(去掉最高位)更新数据
SH = 0; // 给移位寄存器时序脉冲
delay(1);
SH = 1; // 检测到上升沿时将SER数据读入移位寄存器中
delay(1);
}
ST = 0; // 当一个字节传输完毕,此时移位寄存器已满。给存储寄存器时序脉冲
delay(1);
ST = 1; // 检测到上升沿时将移位寄存器中的8位数据全部读入存储寄存器中。通过并行输出引脚可以直接检测到
delay(1);
}
int main(void){
u8 i; // 必须先定义,放在第一个
P0 = 0xff; // 初始全熄灭
while(1){
for(i=0;i<4;i++){
LED_control(0x00);
LED_control(LED_portY_Array[i]);
P0 = LED_portX_Array[i];
delay(100); //1ms
}
}
}
代码中比较关键的就是LED_control
函数中的内容,只要理解74HC595芯片的工作原理,就不难理解代码的逻辑。需要注意的是,采用了动态扫描就必然会有重影的问题,要记得消影。
2. 旋转大风车
这个旋转风车比较抽象,哈哈哈哈哈,主要是点阵数太少。
为了提高移植性和复用性,我将74HC595驱动显示和延时的代码抽取出来(延时代码与之前篇章一致,不再给出)。
LED_Matrix.h
#ifndef _LED_MATRIX_
#define _LED_MATRIX_
#include "delay.h"
#define LED_PORT P0
sbit SER = P3^4; // 串行输入
sbit ST = P3^5; // 存储寄存器时钟引脚
sbit SH = P3^6; // 移位存储器时钟引脚
void LED_Init(void);
void LED_Animation_Show(u8, u8);
#endif
LED_Matrix.c
#include "LED_Matrix.h"
/**
** @brief 封装了74HC595芯片的LED点阵代码
** @author QIU
** @date 2023.08.23
**/
/*-------------------------------------------------------------------*/
/**
** @brief 74HC595串转并驱动代码
** @param dat:8位串行数据
** @retval 返回值:无
**/
void LED_control(u8 dat){
u8 i;
// 将一个字节拆分成串行输入
for(i=0;i<8;i++){
SER = dat >> 7; // 先将最高位送入SER中
dat <<= 1; // 左移1位(去掉最高位)更新数据
SH = 0; // 给移位寄存器时序脉冲
delay_10us(1);
SH = 1; // 检测到上升沿时将SER数据读入移位寄存器中
delay_10us(1);
}
ST = 0; // 当一个字节传输完毕,此时移位寄存器已满。给存储寄存器时序脉冲
delay_10us(1);
ST = 1; // 检测到上升沿时将移位寄存器中的8位数据全部读入存储寄存器中。通过并行输出引脚可以直接检测到
delay_10us(1);
}
/**
** @brief LED点阵初始化
** @param 无
** @retval 返回值:无
**/
void LED_Init(void){
LED_PORT = 0xff;
}
/**
* @brief 显示对应静态画面(8*8)
* @param datX:阴极,datY:阳极
* @retval
*/
void LED_Animation_Show(u8 datX, u8 datY){
LED_control(datY); //阳极码
LED_PORT = ~(0x80>>datX);
delay_10us(100);
LED_Init(); //消影
}
main.c
#include "LED_Matrix.h"
#define SPEED 8 // 动画速度
// 大风车动画数组
u8 code WindMill_Animation_Array[] = {
0x40,0x63,0x36,0x1C,0x38,0x6C,0xC6,0x02,
0x0C,0x18,0x90,0xDE,0x7B,0x09,0x18,0x30,
0x02,0xC6,0x6C,0x38,0x1C,0x36,0x63,0x40,
};
int main(void){
// i为当前点亮的LED列号;
// t为计数器,用于记录进入下一帧画面的时间;
// step为当前帧数(8的倍数)
u8 i, t=0, step=0;
while(1){
for(i=0;i<8;i++){
LED_Animation_Show(i, WindMill_Animation_Array[i+step]);
}
t++;
if(t > SPEED){
t = 0;
step += 8;
if(step > sizeof(WindMill_Animation_Array)/sizeof(WindMill_Animation_Array[0]) - 8){
step = 0;
}
}
}
}
其中,WindMill_Animation_Array[]
数组由取模软件生成。
确定好动画由几帧组成,对于每一帧,都是采用动态扫描显示。而动画的流畅度取决于每一帧停留的时间,可以通过调节SPEED
来测试。
3. 滚动日期
#include "LED_Matrix.h"
#define SPEED 50
// 2022年12月30日
u8 code Animation_Array[] = {
0x61,0x83,0x85,0x89,0x71,0x00,0x7E,0x81,
0x81,0x7E,0x00,0x61,0x83,0x85,0x89,0x71,
0x00,0x61,0x83,0x85,0x89,0x71,0x00,0x44,
0xDC,0x54,0x7F,0x54,0x44,0x00,0x40,0xFF,
0x00,0x61,0x83,0x85,0x89,0x71,0x00,0x01,
0xFE,0xA8,0x82,0xFF,0x00,0x42,0x91,0x99,
0x66,0x00,0x7E,0x81,0x81,0x7E,0x00,0xFF,
0x91,0x91,0xFF,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,};
int main(void){
// i为当前点亮的LED列号;
// count为计数器,用于记录进入下一帧画面的时间;
// j为当前帧数(偏移量)
u8 i, count=0, j=0;
LED_Init();
while(1){
for(i=0;i<8;i++){
LED_Animation_Show(i, Animation_Array[i+j]);
}
count++;
if(count > SPEED){
count = 0;
j++; // 移动一列
if(j > sizeof(Animation_Array)/sizeof(Animation_Array[0]) - 8){
j = 0;
}
}
}
}
实现原理和旋转风车类似,只不过旋转风车是一帧一帧移动的(一帧需要8个字节),滚动字幕是一个字节一个字节的移动。
4. 螺旋线动画
PS:所有代码都写在了这一个C文件里。
#include <REGX52.H>
#include <INTRINS.H>
#define LED_PORT P0
#define SPEED 30
typedef unsigned char u8;
typedef unsigned int u16;
sbit SER = P3^4; // 串行数据输入引脚
sbit ST_CP = P3^5; // 存储寄存器时钟引脚
sbit SH_CP = P3^6; // 移位寄存器时钟引脚
// 全局变量
u8 x, y, num_step, count, dir, posY[8] = {0};
// 延时函数
void delay(u16 t){
while(t--);
}
// 生成对应位置的二进制代码
u8 produce_B_Code(u8 pos){
return 0x01<<pos;
}
//串行数据生成对应并行数据
void Ser2Para(u8 dat){
u8 i;
for(i=0;i<8;i++){
SER = dat>>7;
dat <<= 1;
SH_CP = 0;
_nop_(); // 延迟一个机器周期
SH_CP = 1; //获得一个上升沿
_nop_();
}
ST_CP = 0;
_nop_();
ST_CP = 1;
_nop_();
}
void clear_LED(){
LED_PORT = 0xff; // 清屏
Ser2Para(0x00);
}
// 显示每帧画面
void display(u8 XDATA,u8 YDATA){
LED_PORT = ~produce_B_Code(XDATA); // 按列由右往左扫描
Ser2Para(YDATA);
delay(100);
clear_LED(); // 消影
}
// 更新方向与数组
void update_posY(u8 dir){
// 判断方向
switch(dir%4){
case 0: // 向下
y = y - 1; // 更新当前点坐标
posY[x] += produce_B_Code(y); // 更新需要点亮的Y坐标码
break;
case 1: // 向右
x = x - 1;
posY[x] += produce_B_Code(y); // 更新需要点亮的Y坐标码
break;
case 2: // 向上
y = y + 1; // 更新当前点坐标
posY[x] += produce_B_Code(y); // 更新需要点亮的Y坐标码
break;
case 3: // 向左(往高位走)
x = x + 1; // 更新当前点坐标
posY[x] += produce_B_Code(y); // 更新需要点亮的Y坐标码
break;
}
}
// 重启
void reset(){
u8 k;
x = 4, y = 4, num_step = 1, count = 0, dir = 0;
for(k=0;k<8;k++){
if(k == x){
posY[x] = produce_B_Code(y); // 记录初始化的值
}else{
posY[k] = 0;
}
}
}
/**
** @brief 螺旋线
** @param count: 记录每个动作(上下,左右)需要执行几次
** @param x, y:记录当前点的坐标
** @param posY[]:记录当前需要点亮的灯的十六进制码
** @retval 返回值
**/
int main(void){
u8 i, j=0;
reset(); // 初始化
while(1){
for(i=0;i<8;i++){
// 逆时针
display(i,posY[i]); // 把数组传过去
}
count++;
// 下一帧
if(count > SPEED){
count = 0;
// 更新一下posY数组,显示下一个点
update_posY(dir);
// 记录每更改一次方向需要几帧
j++;
// 每两组换向多走一步
num_step = dir/2 + 1;
// 更新方向
if(j>=num_step){
dir++;
j=0; // 复位
}
}
// 结束条件
if(x==0 && y==8){
delay(10000); // 保持画面
clear_LED(); // 清屏
delay(50000);
reset(); // 重新开始
}
}
}
这个动画我没有提前将每一帧都计算出来,而是每次计算出下一步要点亮的灯,同时保留当前的灯。
5. LED点阵模块功能封装
主要封装了74HC595芯片驱动,提供了延时法和定时器法刷新LED点阵的接口。详见仓库地址。
总结
LED点阵相对来说显示的东西还是很丰富的吧!发挥你的想象力,你也可以创新很多有趣的图案和动画的。