矩阵扫描+呼吸灯效果,其实不难:从时序图说起

最近在搞一个 LED 控制的小项目,需求很简单:在矩阵扫描的基础上,实现呼吸灯效果。听起来有点复杂,但只要你理解了时序图,其实一点都不难。

今天这篇文章,我就结合自己写的代码和一张时序图,来给大家讲讲矩阵扫描中实现呼吸灯的原理和实现方法。

目录

一、先看时序图:理解矩阵扫描的本质

二、呼吸灯效果的实现思路

三、代码实现:数据结构定义

四、矩阵扫描函数实现

五、呼吸灯效果控制

六、最终实现效果

七、总结

八、源码

1.C源文件

2.C头文件


一、先看时序图:理解矩阵扫描的本质

我手上的硬件是一个典型的共阴极 LED 矩阵,时序图大概是这样的:

这个图看起来简单,但其实信息量很大:

  • COM1~COM4:四个公共端,控制 LED 的负极;

  • SEG0~SEG3:段选信号,控制 LED 的正极;

  • 每个 COM 口对应一组 LED,通过快速轮流拉低 COM 口,实现动态扫描;

  • 每个 SEG 口控制具体的 LED 是否点亮。

说白了,就是通过快速轮流选通每一行(COM),再配合 SEG 信号,点亮对应的 LED。

二、呼吸灯效果的实现思路

呼吸灯的本质是PWM(脉宽调制),也就是通过调节 LED 的亮灭时间比例(占空比),来控制亮度。

在矩阵扫描中实现呼吸灯,核心思想就是:

  • 在每个扫描周期内,根据当前 LED 的占空比,决定它是否点亮;

  • 通过周期性地调整占空比,实现渐亮渐暗的呼吸效果。

三、代码实现:数据结构定义

首先定义一个结构体,表示一个 LED 的状态:

typedef struct {
    unsigned char Led0:1; // 0:常亮, 1:呼吸灯
    unsigned char Led1:1;
    unsigned char Led2:1;
    unsigned char Led3:1;
    ......
} Typedef_Led8;

再定义一个结构体,保存每个 COM 口的呼吸灯参数:

typedef struct {
    Typedef_Led8 Leds;              // LED模式
    unsigned char Bre_Duty;         // 当前占空比
    unsigned char Bre_Duty_Cnt;     // 当前计数
    unsigned char Bre_Duty_Cnt_Max; // 呼吸周期
} Typedef_Led8_Bre;

四、矩阵扫描函数实现

我用一个定时器中断,每 125μs 调用一次扫描函数:

void Leds_Data_Drive2(void) {
    if (Show_Dptr_Temp != Show_Dptr) {
        Show_Dptr_Temp = Show_Dptr;

        // 消隐显示
        PORT_LEDS_SEG0 = 0; PORT_LEDS_SEG1 = 0; PORT_LEDS_SEG2 = 0; PORT_LEDS_SEG3 = 0;
        PORT_LEDS_COM1 = 1; PORT_LEDS_COM2 = 1; PORT_LEDS_COM3 = 1; PORT_LEDS_COM4 = 1;

        // COM口控制
        switch (Show_Dptr) {
            case 0: PORT_LEDS_COM1 = 0; break;
            case 1: PORT_LEDS_COM2 = 0; break;
            case 2: PORT_LEDS_COM3 = 0; break;
            case 3: PORT_LEDS_COM4 = 0; break;
        }
    }

    // 段选控制
    if (Show_Dptr < SHOW_NUMB_LENG) {
        //常量显示,一定要放在呼吸灯前面
        if ((Leds_Bre[Show_Dptr].Leds.Led0) == 0) { if (Show_Data[Show_Dptr] & 0x01) PORT_LEDS_SEG0 = 1; }
        if ((Leds_Bre[Show_Dptr].Leds.Led1) == 0) { if (Show_Data[Show_Dptr] & 0x02) PORT_LEDS_SEG1 = 1; }
        if ((Leds_Bre[Show_Dptr].Leds.Led2) == 0) { if (Show_Data[Show_Dptr] & 0x04) PORT_LEDS_SEG2 = 1; }
        if ((Leds_Bre[Show_Dptr].Leds.Led3) == 0) { if (Show_Data[Show_Dptr] & 0x08) PORT_LEDS_SEG3 = 1; }

        // 呼吸灯控制
        if (Leds_Bre[Show_Dptr].Bre_Duty_Cnt >= (Leds_Bre[Show_Dptr].Bre_Duty_Cnt_Max - Leds_Bre[Show_Dptr].Bre_Duty)) {
            if ((Leds_Bre[Show_Dptr].Leds.Led0) == 1) { if (Show_Data[Show_Dptr] & 0x01) PORT_LEDS_SEG0 = 1; }
            if ((Leds_Bre[Show_Dptr].Leds.Led1) == 1) { if (Show_Data[Show_Dptr] & 0x02) PORT_LEDS_SEG1 = 1; }
            if ((Leds_Bre[Show_Dptr].Leds.Led2) == 1) { if (Show_Data[Show_Dptr] & 0x04) PORT_LEDS_SEG2 = 1; }
            if ((Leds_Bre[Show_Dptr].Leds.Led3) == 1) { if (Show_Data[Show_Dptr] & 0x08) PORT_LEDS_SEG3 = 1; }
        }

        // 更新占空比计数
        if (++Leds_Bre[Show_Dptr].Bre_Duty_Cnt >= Leds_Bre[Show_Dptr].Bre_Duty_Cnt_Max) {
            Leds_Bre[Show_Dptr].Bre_Duty_Cnt = 0;
            if (++Show_Dptr >= SHOW_NUMB_LENG) {
                Show_Dptr = 0;
            }
        }
    }
}

五、呼吸灯效果控制

我用另一个定时器,每 200ms 调用一次函数,动态调整占空比:

void Leds_Brea_Func() {
    if (Tcnt_Disp_Dptr != 0) return;
    if (++Show_Bre_Cout <= 50) return;
    Show_Bre_Cout = 0;

    // COM1 呼吸灯
    if (!Show_Bre_Dir0) {
        if (++Leds_Bre[0].Bre_Duty >= Leds_Bre[0].Bre_Duty_Cnt_Max) {
            Leds_Bre[0].Bre_Duty = Leds_Bre[0].Bre_Duty_Cnt_Max;
            Show_Bre_Dir0 = 1;
        }
    } else {
        if (--Leds_Bre[0].Bre_Duty >= Leds_Bre[0].Bre_Duty_Cnt_Max) {
            Leds_Bre[0].Bre_Duty = 0;
            Show_Bre_Dir0 = 0;
        }
    }

    // 其他 COM 口类似...
}

六、最终实现效果

通过这套代码,我实现了:

  • 每个 LED 可以单独设置为常亮或呼吸灯;

  • 呼吸灯效果平滑自然;

  • 所有 LED 可以同时运行,互不干扰。

七、总结

矩阵扫描 + PWM 实现呼吸灯,其实没你想的那么难。只要掌握了扫描原理和 PWM 控制方法,代码写起来很顺手。希望我的分享能给你带来一些启发。

如果你也在做类似的项目,欢迎一起交流。

八、源码

1.C源文件

#define __LEDS_C__
#include "Leds.H"
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
void Leds_Data_Drive(void)
{
	////消隐显示
	PORT_LEDS_SEG0 = 0;PORT_LEDS_SEG1 = 0;PORT_LEDS_SEG2 = 0;PORT_LEDS_SEG3 = 0;
	PORT_LEDS_COM1 = 1;PORT_LEDS_COM2 = 1;PORT_LEDS_COM3 = 1;PORT_LEDS_COM4 = 1;

	////COM口控制
	switch (Show_Dptr)
	{
		case 0:
		{
			PORT_LEDS_COM1 = 0;
		}break;
		case 1:
		{
			PORT_LEDS_COM2 = 0;
		}break;
		case 2:
		{
			PORT_LEDS_COM3 = 0;
		}break;
		case 3:
		{
			PORT_LEDS_COM4 = 0;
		}
		
		default:
			break;
	}

	////段选控制
	if(Show_Dptr < SHOW_NUMB_LENG)
	{
		if(Show_Data[Show_Dptr] & 0x01) PORT_LEDS_SEG0 = 1;
		if(Show_Data[Show_Dptr] & 0x02) PORT_LEDS_SEG1 = 1;
		if(Show_Data[Show_Dptr] & 0x04) PORT_LEDS_SEG2 = 1;
		if(Show_Data[Show_Dptr] & 0x08) PORT_LEDS_SEG3 = 1;
	}

	////显示周期(时间)控制
	if(++Show_Dptr >= SHOW_NUMB_DUTY)
	{
		Show_Dptr = 0;
	}


    
}
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
void Send_Data_Show(void)
{////4ms执行一次
	Show_Data[0] = Disp_Buf[0];
	Show_Data[1] = Disp_Buf[1];
	Show_Data[2] = Disp_Buf[2];
	Show_Data[3] = Disp_Buf[3];
}
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
/////扫描显示实现常亮与呼吸灯效果
/////1.常亮可以实现一直亮与闪烁(闪烁周期不能太短)
/////2.呼吸灯效果,每一LED灯的呼吸效果功能可以独立控制,不影响其他LED灯的常量与闪烁效果
/////3.有四个COM口,每个COM口有四个LED灯,共16个LED灯,每个LED灯的呼吸效果可以独立控制
/////4.每一个COM口执行的时间最大能占到1/4总周期的时间,即1/4的周期T,设T=4ms,可调整的周期为3ms,2ms,1ms,....
/////5.每一个COM口执行的时间最小能占到1/32总周期的时间,即1/32的周期T,即T/32;因为125us的定时器,最大能执行32次
/////6.让实现LED实现常亮与呼吸灯效果

// ////结构体8位定义
// typedef struct
// {
//     unsigned char Led0:1; ////0-常亮,1-呼吸灯
//     unsigned char Led1:1;
//     unsigned char Led2:1;
//     unsigned char Led3:1;
//     unsigned char Led4:1;
//     unsigned char Led5:1;
//     unsigned char Led6:1;
//     unsigned char Led7:1;
// }Typedef_Led8;

// ////结构体定义
// typedef struct
// {
// 	Typedef_Led8  Leds;		//LED灯状态:位0-常亮,1-呼吸灯
// 	unsigned char Bre_Duty;	//呼吸灯占空比
// 	unsigned char Bre_Duty_Cnt;//呼吸灯占空比计数
// 	unsigned char Bre_Duty_Cnt_Max;//呼吸灯占空比计数最大值
// }Typedef_Led8_Bre;////呼吸灯结构体
// Typedef_Led8_Bre Leds_Bre[4];////4个COM口,每个COM口4个LED灯,共16个LED灯
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
void Leds_Data_Drive2(void)
{////125us执行一次
	if (Show_Dptr_Temp != Show_Dptr)
	{
		Show_Dptr_Temp = Show_Dptr;
		////消隐显示
		PORT_LEDS_SEG0 = 0;PORT_LEDS_SEG1 = 0;PORT_LEDS_SEG2 = 0;PORT_LEDS_SEG3 = 0;
		PORT_LEDS_COM1 = 1;PORT_LEDS_COM2 = 1;PORT_LEDS_COM3 = 1;PORT_LEDS_COM4 = 1;

		////COM口控制
		switch (Show_Dptr)
		{
			case 0:
			{
				PORT_LEDS_COM1 = 0;
			}break;
			case 1:
			{
				PORT_LEDS_COM2 = 0;
			}break;
			case 2:
			{
				PORT_LEDS_COM3 = 0;
			}break;
			case 3:
			{
				PORT_LEDS_COM4 = 0;
			}
			
			default:
				break;
		}
	}
    

	////段选控制
	if(Show_Dptr < SHOW_NUMB_LENG)
	{
		////常量显示,一定要放在呼吸灯前面
		if((Leds_Bre[Show_Dptr].Leds.Led0) == 0){if(Show_Data[Show_Dptr] & 0x01){PORT_LEDS_SEG0 = 1;}}
		if((Leds_Bre[Show_Dptr].Leds.Led1) == 0){if(Show_Data[Show_Dptr] & 0x02){PORT_LEDS_SEG1 = 1;}}
		if((Leds_Bre[Show_Dptr].Leds.Led2) == 0){if(Show_Data[Show_Dptr] & 0x04){PORT_LEDS_SEG2 = 1;}}
		if((Leds_Bre[Show_Dptr].Leds.Led3) == 0){if(Show_Data[Show_Dptr] & 0x08){PORT_LEDS_SEG3 = 1;}}

		////呼吸灯控制
		if(Leds_Bre[Show_Dptr].Bre_Duty_Cnt >= (Leds_Bre[Show_Dptr].Bre_Duty_Cnt_Max-Leds_Bre[Show_Dptr].Bre_Duty))////减少消隐操作
		{
		    if((Leds_Bre[Show_Dptr].Leds.Led0) == 1) {if(Show_Data[Show_Dptr] & 0x01) PORT_LEDS_SEG0 = 1;}
			if((Leds_Bre[Show_Dptr].Leds.Led1) == 1) {if(Show_Data[Show_Dptr] & 0x02) PORT_LEDS_SEG1 = 1;}
			if((Leds_Bre[Show_Dptr].Leds.Led2) == 1) {if(Show_Data[Show_Dptr] & 0x04) PORT_LEDS_SEG2 = 1;}
			if((Leds_Bre[Show_Dptr].Leds.Led3) == 1) {if(Show_Data[Show_Dptr] & 0x08) PORT_LEDS_SEG3 = 1;}

			
		}
		if(++Leds_Bre[Show_Dptr].Bre_Duty_Cnt >= Leds_Bre[Show_Dptr].Bre_Duty_Cnt_Max){Leds_Bre[Show_Dptr].Bre_Duty_Cnt = 0;if(++Show_Dptr >= SHOW_NUMB_LENG) {Show_Dptr = 0;}}
		
	}


}
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
void Leds_xBre_Init(void)
{
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	Leds_Bre[0].Leds.Led0 = 0;
	Leds_Bre[0].Leds.Led1 = 0;
	Leds_Bre[0].Leds.Led2 = 0;
	Leds_Bre[0].Leds.Led3 = 0;

	Leds_Bre[1].Leds.Led0 = 0;
	Leds_Bre[1].Leds.Led1 = 0;
	Leds_Bre[1].Leds.Led2 = 0;
	Leds_Bre[1].Leds.Led3 = 0;

	Leds_Bre[2].Leds.Led0 = 0;
	Leds_Bre[2].Leds.Led1 = 0;
	Leds_Bre[2].Leds.Led2 = 0;
	Leds_Bre[2].Leds.Led3 = 0;

	Leds_Bre[3].Leds.Led0 = 0;
	Leds_Bre[3].Leds.Led1 = 0;
	Leds_Bre[3].Leds.Led2 = 0;
	Leds_Bre[3].Leds.Led3 = 0;
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/

	

	
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	Show_Dptr = 0;
	Show_Dptr_Temp = 1;
	Leds_Bre[0].Bre_Duty = 0;
	Leds_Bre[0].Bre_Duty_Cnt = 0;
	Leds_Bre[0].Bre_Duty_Cnt_Max = 12;
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	Leds_Bre[1].Bre_Duty = 0;
	Leds_Bre[1].Bre_Duty_Cnt = 0;
	Leds_Bre[1].Bre_Duty_Cnt_Max = 12;
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	Leds_Bre[2].Bre_Duty = 0;
	Leds_Bre[2].Bre_Duty_Cnt = 0;
	Leds_Bre[2].Bre_Duty_Cnt_Max = 12;
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	Leds_Bre[3].Bre_Duty = 0;
	Leds_Bre[3].Bre_Duty_Cnt = 0;
	Leds_Bre[3].Bre_Duty_Cnt_Max = 12;
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	
}
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
void Leds_Brea_Func()
{////调节PWM宽度
	if (Tcnt_Disp_Dptr != 0){return;}////4ms执行一次
	if(++Show_Bre_Cout<= 50){return;}	////4ms*50=200ms	
	Show_Bre_Cout = 0;
    if(!Show_Bre_Dir0){if(++Leds_Bre[0].Bre_Duty >= Leds_Bre[0].Bre_Duty_Cnt_Max){Leds_Bre[0].Bre_Duty = Leds_Bre[0].Bre_Duty_Cnt_Max;Show_Bre_Dir0 = TRUE;}}
	if(Show_Bre_Dir0){if(--Leds_Bre[0].Bre_Duty >= Leds_Bre[0].Bre_Duty_Cnt_Max){Leds_Bre[0].Bre_Duty = 0;Show_Bre_Dir0 = FALSE;}}

    if(!Show_Bre_Dir1){if(++Leds_Bre[1].Bre_Duty >= Leds_Bre[1].Bre_Duty_Cnt_Max){Leds_Bre[1].Bre_Duty = Leds_Bre[1].Bre_Duty_Cnt_Max;Show_Bre_Dir1 = TRUE;}}
	if(Show_Bre_Dir1){if(--Leds_Bre[1].Bre_Duty >= Leds_Bre[1].Bre_Duty_Cnt_Max){Leds_Bre[1].Bre_Duty = 0;Show_Bre_Dir1 = FALSE;}}

    if(!Show_Bre_Dir2){if(++Leds_Bre[2].Bre_Duty >= Leds_Bre[2].Bre_Duty_Cnt_Max){Leds_Bre[2].Bre_Duty = Leds_Bre[2].Bre_Duty_Cnt_Max;Show_Bre_Dir2 = TRUE;}}
	if(Show_Bre_Dir2){if(--Leds_Bre[2].Bre_Duty >= Leds_Bre[2].Bre_Duty_Cnt_Max){Leds_Bre[2].Bre_Duty = 0;Show_Bre_Dir2 = FALSE;}}

    if(!Show_Bre_Dir3){if(++Leds_Bre[3].Bre_Duty >= Leds_Bre[3].Bre_Duty_Cnt_Max){Leds_Bre[3].Bre_Duty = Leds_Bre[3].Bre_Duty_Cnt_Max;Show_Bre_Dir3 = TRUE;}}
	if(Show_Bre_Dir3){if(--Leds_Bre[3].Bre_Duty >= Leds_Bre[3].Bre_Duty_Cnt_Max){Leds_Bre[3].Bre_Duty = 0;Show_Bre_Dir3 = FALSE;}}

}
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/

/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/

2.C头文件

#ifndef __LEDS_H__
#define __LEDS_H__

#include "Common.H"
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
////共阴极
// #define	PORT_LEDS_COM1    PA4
// #define	PORT_LEDS_COM2    PA1
// #define	PORT_LEDS_COM3    PA0
// #define	PORT_LEDS_COM4    PB0

#define	PORT_LEDS_COM1    LATA4
#define	PORT_LEDS_COM2    LATA1
#define	PORT_LEDS_COM3    LATA0
#define	PORT_LEDS_COM4    LATB0

////SEG口
// #define	PORT_LEDS_SEG0    PC4
// #define	PORT_LEDS_SEG1    PB2
// #define	PORT_LEDS_SEG2    PB3
// #define	PORT_LEDS_SEG3    PB4

#define	PORT_LEDS_SEG0    LATC4
#define	PORT_LEDS_SEG1    LATB2
#define	PORT_LEDS_SEG2    LATB3
#define	PORT_LEDS_SEG3    LATB4
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
#define LEDS_DUTY		4
#define LEDS_CYCL       4
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
#define SHOW_NUMB_LENG    4
#define SHOW_NUMB_DUTY    4
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
////结构体8位定义
typedef struct
{
    unsigned char Led0:1; ////0-常亮,1-呼吸灯
    unsigned char Led1:1;
    unsigned char Led2:1;
    unsigned char Led3:1;
    unsigned char Led4:1;
    unsigned char Led5:1;
    unsigned char Led6:1;
    unsigned char Led7:1;
}Typedef_Led8;

////结构体定义
typedef struct
{
	Typedef_Led8  Leds;		//LED灯状态:位0-常亮,1-呼吸灯
	unsigned char Bre_Duty;	//呼吸灯占空比
	unsigned char Bre_Duty_Cnt;//呼吸灯占空比计数
	unsigned char Bre_Duty_Cnt_Max;//呼吸灯占空比计数最大值
}Typedef_Led8_Bre;////呼吸灯结构体
// Typedef_Led8_Bre Leds_Bre[4];////4个COM口,每个COM口4个LED灯,共16个LED灯
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	#ifdef __LEDS_C__
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
		// xchar Tcnt_Disp_Dptr;
		//xchar Tcnt_Tuch_Freh;////用于触摸扫描
		// vsbit Fdsp_Data_Alon;
		// xchar Disp_Buf[6];
		xchar Show_Data[SHOW_NUMB_LENG];
		xchar Show_Dptr;	////索引
		xchar Show_Dptr_Temp;	////索引
		Typedef_Led8_Bre Leds_Bre[4];////4个COM口,每个COM口4个LED灯,共16个LED灯
		vsbit Show_Bre_Dir0;////呼吸灯方向
		vsbit Show_Bre_Dir1;
		vsbit Show_Bre_Dir2;
		vsbit Show_Bre_Dir3;
		uuint Show_Bre_Cout;	
		// uuint Show_Cout;	
		// uuint Show_Duty;
		// uuint Show_Cycl;
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	#else
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
		// extern xchar Tcnt_Disp_Dptr;
		//extern xchar Tcnt_Tuch_Freh;////用于触摸扫描
		// extern vsbit Fdsp_Data_Alon;
		// extern xchar Disp_Buf[6];
		extern xchar Show_Data[SHOW_NUMB_LENG];
		extern xchar Show_Dptr;	////索引
		extern xchar Show_Dptr_Temp;	////索引
		extern Typedef_Led8_Bre Leds_Bre[4];////4个COM口,每个COM口4个LED灯,共16个LED灯
		extern vsbit Show_Bre_Dir0;////呼吸灯方向
		extern vsbit Show_Bre_Dir1;
		extern vsbit Show_Bre_Dir2;
		extern vsbit Show_Bre_Dir3;
		extern uuint Show_Bre_Cout;

/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
		extern uuint Show_Cout;
		extern uuint Show_Duty;
		extern uuint Show_Cycl;
/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
	#endif

/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
extern void Leds_Data_Drive(void);
extern void Send_Data_Show(void);
extern void Leds_Data_Drive2(void);
extern void Leds_xBre_Init(void);
extern void Leds_Brea_Func();

/*------------------------------------------------------------------------------------------------------------------------------------------20250703A*/
#endif




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值