51 单片机 拖尾流水灯

先看视频

拖尾流水灯

51单片机LED拖尾流水灯代码详解(每步+原理)

本文针对51单片机(基于11.0592MHz晶振)LED拖尾流水灯代码,逐行、逐函数拆解,讲解每一步操作的作用、底层原理,结合51单片机寄存器、中断、PWM等核心知识点,让初学者能完全理解代码逻辑和运行机制。

先明确核心功能:8个LED(接P0口)实现"拖尾流水"效果------1个亮灯头部带动3个渐变暗的拖尾,循环移动,亮度由PWM(脉冲宽度调制)控制,流动速度由延时函数控制。

一、头文件与宏定义(代码开头部分)

c 复制代码
#include <REGX51.H>
#include <intrins.h>  

#define LED_PORT        P0          
#define LED_NUM         8                    
#define PWM_PERIOD      20

1. 头文件说明(原理+作用)

  • #include <REGX51.H>:51单片机核心寄存器定义头文件,必须包含。该头文件已经定义了51单片机所有寄存器(如TMOD、TH0、TL0、ET0等)和I/O口(如P0、P1等),无需我们手动定义,直接使用即可。

  • #include <intrins.h>:51单片机内置函数头文件,包含_nop_()(空操作函数)的声明,这里用于延时函数中提升延时精度。

2. 宏定义说明(原理+作用)

宏定义的核心作用是"替换",把常用的常量、I/O口用简单的名字代替,方便后续修改(比如想把LED改到P1口,只需修改LED_PORT即可,无需逐行改代码)。

  • #define LED_PORT P0:将P0口定义为LED_PORT,后续操作LED_PORT就等同于操作P0口(8个LED一端接P0口,另一端接地,共阳极LED:高电平灭、低电平亮)。

  • #define LED_NUM 8:定义LED的数量为8个(对应P0口8个引脚,P0.0~P0.7分别控制1个LED)。

  • #define PWM_PERIOD 20:定义PWM(脉冲宽度调制)的周期,单位是"定时器中断次数"。这里PWM周期为20,后续通过定时器中断计数,实现PWM的占空比控制(亮度控制核心)。

二、全局变量定义(代码全局部分)

c 复制代码
unsigned char brightness[LED_NUM] = {0};  // 每个LED的亮度等级
unsigned char head = 0;                    // 拖尾头部位置
unsigned char pwm_count = 0;               // PWM周期计数器
unsigned char tail_bright[4] = {0, 8, 20, 17};

变量作用+原理

全局变量的特点:整个程序(所有函数)都能访问和修改,用于存储程序运行过程中的核心数据,这里全部用unsigned char(无符号字符型,范围0~255),因为LED亮度、计数无需负数,且数值不大。

  • brightness[LED_NUM]:长度为8的数组,对应8个LED,每个元素存储对应LED的"亮度等级"(0~20,因为PWM_PERIOD=20),数值越大,LED越亮(原理:PWM占空比越高,亮的时间越长,视觉上越亮)。初始值全为0,即初始全灭。

  • head:存储"拖尾头部"的LED位置(0~7,对应8个LED),头部是拖尾中最亮的部分,后续通过改变head的值,实现拖尾移动。

  • pwm_count:PWM周期计数器,范围0~19(因为取模PWM_PERIOD=20),用于配合brightness数组,实现PWM亮度控制(后续中断函数中核心使用)。

  • tail_bright[4]:拖尾的4个亮度等级(0、8、20、17),对应"头部+3个拖尾"的亮度:头部最亮(20),后面拖尾依次变暗(17、8、0),形成渐变拖尾效果(0表示灭,20表示最亮)。

三、定时器0初始化函数(Timer0_Init)

c 复制代码
void Timer0_Init(void)
{
    TMOD &= 0xF0; 
    TMOD |= 0x01; 
    TH0 = 0xFE; 
    TL0 = 0x33; 
    ET0 = 1;      
    TR0 = 1;      
    EA = 1;       
}

函数作用

初始化51单片机的定时器0,配置为"16位定时模式",开启定时器中断,让定时器0开始工作------核心目的是产生固定时间的中断,用于刷新PWM(控制LED亮度)和计数。

逐行拆解(原理+作用)

  1. TMOD &= 0xF0;:清空定时器0的工作模式(TMOD是定时器模式寄存器,高4位控制定时器1,低4位控制定时器0)。0xF0是16进制,二进制为11110000,与TMOD做"与运算",会把低4位(定时器0的模式位)清0,高4位(定时器1)不变,避免之前的模式干扰。

  2. TMOD |= 0x01;:设置定时器0为"16位定时模式"。0x01二进制为00000001,与TMOD做"或运算",将定时器0的模式位(低4位)设为01,即16位定时模式(定时器0由TH0和TL0两个8位寄存器组成,合起来16位,最大计数范围0~65535)。

  3. TH0 = 0xFE; TL0 = 0x33;:设置定时器0的初值,决定定时器的"定时时间"(核心原理:51单片机晶振为11.0592MHz,1个机器周期=12个晶振周期,即12/(11.0592×10^6) ≈ 1.085μs)。

计算:TH0=0xFE(254),TL0=0x33(51),初始计数=254×256 + 51 = 65027;16位定时器最大计数65535,所以定时时间=(65535 - 65027)×1.085μs ≈ 508μs ≈ 500μs(简化为0.5ms)。即每0.5ms,定时器0触发一次中断。

  1. ET0 = 1;:开启定时器0的中断允许(ET0是定时器0中断允许位,1=允许中断,0=禁止中断),允许定时器0计数溢出后,向CPU发送中断请求。

  2. TR0 = 1;:启动定时器0(TR0是定时器0运行控制位,1=启动计数,0=停止计数),启动后,TH0和TL0开始累加计数。

  3. EA = 1;:开启CPU总中断允许(EA是总中断允许位,1=允许所有已开启的中断,0=禁止所有中断),如果不开启总中断,即使ET0=1,定时器0也无法触发中断。

四、定时器0中断服务函数(Timer0_ISR)

c 复制代码
void Timer0_ISR(void) interrupt 1
{
    unsigned char i;
    unsigned char output = 0xFF; // 初始全灭(高电平)
    
    TH0 = 0xFE; 
    TL0 = 0x33; 
     
    pwm_count = (pwm_count + 1) % PWM_PERIOD;
    
 
    for(i = 0; i < LED_NUM; i++)
    {
        if(brightness[i] > pwm_count)
        {
            output &= ~(1 << i);   // 对应位拉低,点亮LED
        }
    }
    
    LED_PORT = output; 
}

函数作用

这是定时器0触发中断后,CPU自动执行的函数(中断服务函数),核心功能:1. 重装定时器初值(保证定时精度);2. 更新PWM计数器;3. 根据亮度等级,控制每个LED的亮灭(PWM亮度控制),刷新LED显示。

关键知识点

interrupt 1:51单片机中断优先级编号,定时器0的中断编号是1(外部中断0是0,定时器1是3,外部中断1是2),必须写对,否则CPU无法识别中断来源。

逐行拆解(原理+作用)

  1. unsigned char i; unsigned char output = 0xFF;:定义循环变量i,定义output变量并初始化为0xFF(二进制11111111)。因为LED是共阳极(高电平灭、低电平亮),output初始为0xFF,意味着P0口8个引脚全为高电平,所有LED初始全灭。

  2. TH0 = 0xFE; TL0 = 0x33;:重装定时器0初值。因为定时器0是16位定时,计数到65535后会溢出,触发中断,此时TH0和TL0会清零,若不重装初值,下次定时时间会变长(从0开始计数),所以每次中断后必须重装初值,保证每次中断的时间都是0.5ms。

  3. pwm_count = (pwm_count + 1) % PWM_PERIOD;:更新PWM计数器。pwm_count每次中断加1,加1后对PWM_PERIOD(20)取模,意味着pwm_count的范围始终是0~19,循环往复------这就是PWM的"周期"(20次中断,每次0.5ms,总周期=20×0.5ms=10ms,人眼看不到闪烁,视觉上是稳定亮度)。

  4. for(i = 0; i < LED_NUM; i++):循环遍历8个LED(i从0到7,对应P0.0~P0.7),逐个判断每个LED的亮度等级,决定是否点亮。

  5. if(brightness[i] > pwm_count):PWM亮度控制的核心逻辑。brightnessi是第i个LED的亮度等级(020),pwm_count是当前PWM周期的计数(019)。

原理:在一个PWM周期(20次中断)内,若brightnessi=20,那么每次中断(pwm_count 0~19)都满足"20>pwm_count",LED会一直亮(占空比100%,最亮);若brightnessi=8,那么只有pwm_count 0~7时满足条件,LED亮8次(4ms),灭12次(6ms),占空比40%,亮度变暗;若brightnessi=0,永远不满足条件,LED全灭。

  1. output &= ~(1 << i);:点亮对应LED。1 << i 表示将1左移i位(比如i=0,1<<0=0x01;i=1,1<<1=0x02),~(1<<i) 表示按位取反(比如i=0,~0x01=0xFE);output与该值做"与运算",会将output的第i位(对应P0.i引脚)拉低为0,其余位不变------低电平点亮LED。

  2. LED_PORT = output;:将output的值写入P0口(LED_PORT),更新所有LED的亮灭状态,完成一次PWM刷新。

五、拖尾更新函数(Update_Tail)

c 复制代码
void Update_Tail(void)
{
    unsigned char i;
    
    // 清空所有亮度
    for(i = 0; i < LED_NUM; i++)
    {
        brightness[i] = 0;
    }
    

    for(i = 0; i < 4; i++)
    {
        unsigned char pos = ((head + i) % LED_NUM);
        brightness[pos] = tail_bright[i];
    }
}

函数作用

更新8个LED的亮度等级(brightness数组),实现"拖尾效果"------以head为头部,后面跟随3个渐变暗的拖尾,循环覆盖8个LED位置。

逐行拆解(原理+作用)

  1. for(i = 0; i < LED_NUM; i++) { brightness[i] = 0; }:清空所有LED的亮度等级,将brightness数组全部设为0,即先让所有LED灭,避免上一次的拖尾残留,为新的拖尾位置做准备。

  2. for(i = 0; i &lt; 4; i++):循环4次(对应拖尾的4个部分:头部+3个拖尾),给4个位置的LED赋值亮度。

  3. unsigned char pos = ((head + i) % LED_NUM);:计算拖尾每个部分的LED位置。head是头部位置,i从0到3,所以pos依次是head(头部)、head+1(拖尾1)、head+2(拖尾2)、head+3(拖尾3);% LED_NUM(取模8)是为了实现"循环"------当head+3超过7(比如head=7,head+3=10,10%8=2),位置会绕回前面,避免数组越界,实现无限循环流水。

  4. brightness[pos] = tail_bright[i];:给计算出的位置pos赋值对应的亮度。tail_bright0=0(拖尾最末端,灭)、tail_bright1=8(拖尾2,较暗)、tail_bright2=20(头部,最亮)、tail_bright3=17(拖尾1,较亮),最终形成"灭→较暗→最亮→较暗"的渐变拖尾(注意:i=0对应拖尾末端,i=2对应头部,形成从头部到末端逐渐变暗的效果)。

六、软件延时函数(Delaynms)

c 复制代码
void Delaynms(unsigned int xms)	//@11.0592MHz
{
	unsigned char data i, j;
    while(xms--)
    {
        _nop_();
        i = 2;
        j = 199;
        do
        {
            while (--j);
        } while (--i);
    }
}

函数作用

实现"毫秒级延时",控制拖尾的流动速度(延时时间越长,拖尾移动越慢),括号内的xms是延时时间(单位:ms)。

关键知识点

//@11.0592MHz:说明该延时函数是基于11.0592MHz晶振编写的,延时时间才准确(不同晶振,延时函数的i、j初始值需要调整);_nop_()是内置空操作函数,执行1个机器周期(≈1.085μs),用于微调延时精度,避免延时误差过大。

原理拆解

  1. unsigned char data i, j;:定义局部变量i、j,加data修饰,指定变量存储在51单片机的内部数据存储器(data区),访问速度更快(适合延时函数这种高频循环的场景)。

  2. while(xms--):外层循环,xms是传入的延时毫秒数,每循环一次,xms减1,直到xms=0,循环结束------比如xms=50000,就会循环50000次,每次循环实现1ms的延时,总延时50000ms=50秒(这里注意:50秒延时较长,实际可修改为500ms,拖尾移动更流畅)。

  3. 内层循环(i=2; j=199; do{while(--j);}while(--i);):实现1ms的延时核心。基于11.0592MHz晶振,通过循环消耗机器周期,经过计算,i=2、j=199的循环组合,刚好能实现约1ms的延时(具体计算:每个循环语句消耗固定机器周期,叠加后总周期≈1ms)。

  4. _nop_();:空操作,增加1个机器周期的延时,微调延时精度,让1ms的延时更准确(避免因晶振误差导致延时偏差)。

七、主函数(main)------ 程序入口

c 复制代码
void main(void)
{
    Timer0_Init();
    LED_PORT = 0xFF; 
    
    while(1)
    {
        Update_Tail();                 // 更新拖尾亮度分布
        head = (head + 1) % LED_NUM;   // 移动拖尾头部
        Delaynms(500);                 // 控制流动速度
    }
}

函数作用

主函数是程序的入口(51单片机程序从main函数开始执行),核心逻辑:初始化定时器→设置LED初始状态→进入死循环,不断更新拖尾、移动头部、延时,实现持续的拖尾流水效果。

逐行拆解(原理+作用)

  1. Timer0_Init();:调用定时器0初始化函数,启动定时器和中断,让PWM刷新开始工作(此时中断会每隔0.5ms触发一次,不断刷新LED亮度)。

  2. LED_PORT = 0xFF;:设置P0口初始状态为0xFF(全高电平),所有LED初始全灭,避免程序启动时LED乱亮。

  3. while(1):死循环(无限循环),程序会一直执行循环内的代码,实现持续的拖尾效果(51单片机主函数必须有死循环,否则程序执行完会乱跑)。

  4. Update_Tail();:调用拖尾更新函数,根据当前head位置,更新8个LED的亮度等级,形成新的拖尾分布。

5.head = (head + 1) % LED_NUM;:移动拖尾头部,head每次加1,取模8(LED_NUM=8),实现头部从0→1→2→...→7→0的循环移动,带动拖尾一起移动。

  1. Delaynms(500);:调用延时函数,延时500,控制拖尾移动速度------延时时间越长,头部移动越慢,拖尾流动越慢;(实际使用时可根据需求调整)。

八、整体运行流程(核心逻辑串联)

  1. 程序启动,执行main函数,先调用Timer0_Init(),初始化定时器0(定时0.5ms,开启中断),LED初始全灭。

  2. 进入死循环,先调用Update_Tail(),根据当前head位置,给4个位置的LED赋值亮度(形成拖尾),其余LED灭。

  3. head加1,准备下一次拖尾位置,然后延时50秒(拖尾停留50秒)。

  4. 在整个过程中,定时器0每0.5ms触发一次中断,执行Timer0_ISR():重装初值→更新PWM计数器→根据brightness数组控制LED亮灭(PWM亮度刷新),让人眼看到稳定的拖尾亮度。

  5. 延时结束后,重复步骤2~4,head不断循环移动,拖尾也随之循环流动,形成持续的LED拖尾流水效果。

九、关键注意点(补充)

  1. LED接法:代码默认是"共阳极LED"(阳极接VCC,阴极接P0口),若用共阴极LED,需修改代码(output初始为0x00,点亮逻辑改为"brightnessi>pwm_count时,output |= (1<<i)")。

  2. 延时时间越小 ,拖尾移动更流畅。

  3. 拖尾效果调整:修改tail_bright数组的数值,可改变拖尾的亮度渐变效果(比如改为{0,10,20,15},拖尾亮度更柔和);修改循环次数(i<4),可改变拖尾长度(比如i<5,拖尾更长)。

  4. 定时器初值:若晶振不是11.0592MHz,需重新计算TH0和TL0的初值(保证定时时间准确,PWM刷新稳定)。

相关推荐
清风6666662 小时前
基于单片机与DAC0832的双路波形信号发生系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
azwsm4 小时前
电路元器件和GPIO控制器
单片机·嵌入式硬件
kebidaixu7 小时前
FreeRTOS 移植到 STM32F407VETX 记录(一)
stm32·单片机·嵌入式硬件
CSDN官方博客7 小时前
「谁说嵌入式只是调包和焊板子?」—— 2026嵌入式全栈技术征锋令
嵌入式硬件·物联网·embedding
点灯小铭8 小时前
基于单片机的数码管定时插座设计与定时开关功能实现
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
云栖梦泽8 小时前
玩转RK3506SDK
linux·嵌入式硬件
数智工坊10 小时前
机器人四大主控板系统分层选型指南:树莓派、ESP32、STM32与Arduino的能力边界与实战定位
stm32·嵌入式硬件·机器人
进击的小头10 小时前
第8篇:IGBT 从零到精通:核心原理、关键参数、选型指南与工业级应用要点
经验分享·嵌入式硬件·学习
点灯小铭10 小时前
基于单片机的多模式智能洗衣机设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
项目題供诗11 小时前
STM32-AD单通道&AD多通道(十九)
stm32·单片机·嵌入式硬件