先看视频
拖尾流水灯
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亮度)和计数。
逐行拆解(原理+作用)
-
TMOD &= 0xF0;:清空定时器0的工作模式(TMOD是定时器模式寄存器,高4位控制定时器1,低4位控制定时器0)。0xF0是16进制,二进制为11110000,与TMOD做"与运算",会把低4位(定时器0的模式位)清0,高4位(定时器1)不变,避免之前的模式干扰。 -
TMOD |= 0x01;:设置定时器0为"16位定时模式"。0x01二进制为00000001,与TMOD做"或运算",将定时器0的模式位(低4位)设为01,即16位定时模式(定时器0由TH0和TL0两个8位寄存器组成,合起来16位,最大计数范围0~65535)。 -
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触发一次中断。
-
ET0 = 1;:开启定时器0的中断允许(ET0是定时器0中断允许位,1=允许中断,0=禁止中断),允许定时器0计数溢出后,向CPU发送中断请求。 -
TR0 = 1;:启动定时器0(TR0是定时器0运行控制位,1=启动计数,0=停止计数),启动后,TH0和TL0开始累加计数。 -
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无法识别中断来源。
逐行拆解(原理+作用)
-
unsigned char i; unsigned char output = 0xFF;:定义循环变量i,定义output变量并初始化为0xFF(二进制11111111)。因为LED是共阳极(高电平灭、低电平亮),output初始为0xFF,意味着P0口8个引脚全为高电平,所有LED初始全灭。 -
TH0 = 0xFE; TL0 = 0x33;:重装定时器0初值。因为定时器0是16位定时,计数到65535后会溢出,触发中断,此时TH0和TL0会清零,若不重装初值,下次定时时间会变长(从0开始计数),所以每次中断后必须重装初值,保证每次中断的时间都是0.5ms。 -
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,人眼看不到闪烁,视觉上是稳定亮度)。 -
for(i = 0; i < LED_NUM; i++):循环遍历8个LED(i从0到7,对应P0.0~P0.7),逐个判断每个LED的亮度等级,决定是否点亮。 -
if(brightness[i] > pwm_count):PWM亮度控制的核心逻辑。brightness[i]是第i个LED的亮度等级(020),pwm_count是当前PWM周期的计数(019)。
原理:在一个PWM周期(20次中断)内,若brightness[i]=20,那么每次中断(pwm_count 0~19)都满足"20>pwm_count",LED会一直亮(占空比100%,最亮);若brightness[i]=8,那么只有pwm_count 0~7时满足条件,LED亮8次(4ms),灭12次(6ms),占空比40%,亮度变暗;若brightness[i]=0,永远不满足条件,LED全灭。
-
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。 -
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位置。
逐行拆解(原理+作用)
-
for(i = 0; i < LED_NUM; i++) { brightness[i] = 0; }:清空所有LED的亮度等级,将brightness数组全部设为0,即先让所有LED灭,避免上一次的拖尾残留,为新的拖尾位置做准备。 -
for(i = 0; i < 4; i++):循环4次(对应拖尾的4个部分:头部+3个拖尾),给4个位置的LED赋值亮度。 -
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),位置会绕回前面,避免数组越界,实现无限循环流水。 -
brightness[pos] = tail_bright[i];:给计算出的位置pos赋值对应的亮度。tail_bright[0]=0(拖尾最末端,灭)、tail_bright[1]=8(拖尾2,较暗)、tail_bright[2]=20(头部,最亮)、tail_bright[3]=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),用于微调延时精度,避免延时误差过大。
原理拆解
-
unsigned char data i, j;:定义局部变量i、j,加data修饰,指定变量存储在51单片机的内部数据存储器(data区),访问速度更快(适合延时函数这种高频循环的场景)。 -
while(xms--):外层循环,xms是传入的延时毫秒数,每循环一次,xms减1,直到xms=0,循环结束------比如xms=50000,就会循环50000次,每次循环实现1ms的延时,总延时50000ms=50秒(这里注意:50秒延时较长,实际可修改为500ms,拖尾移动更流畅)。 -
内层循环(
i=2; j=199; do{while(--j);}while(--i);):实现1ms的延时核心。基于11.0592MHz晶振,通过循环消耗机器周期,经过计算,i=2、j=199的循环组合,刚好能实现约1ms的延时(具体计算:每个循环语句消耗固定机器周期,叠加后总周期≈1ms)。 -
_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初始状态→进入死循环,不断更新拖尾、移动头部、延时,实现持续的拖尾流水效果。
逐行拆解(原理+作用)
-
Timer0_Init();:调用定时器0初始化函数,启动定时器和中断,让PWM刷新开始工作(此时中断会每隔0.5ms触发一次,不断刷新LED亮度)。 -
LED_PORT = 0xFF;:设置P0口初始状态为0xFF(全高电平),所有LED初始全灭,避免程序启动时LED乱亮。 -
while(1):死循环(无限循环),程序会一直执行循环内的代码,实现持续的拖尾效果(51单片机主函数必须有死循环,否则程序执行完会乱跑)。 -
Update_Tail();:调用拖尾更新函数,根据当前head位置,更新8个LED的亮度等级,形成新的拖尾分布。
5.head = (head + 1) % LED_NUM;:移动拖尾头部,head每次加1,取模8(LED_NUM=8),实现头部从0→1→2→...→7→0的循环移动,带动拖尾一起移动。
Delaynms(500);:调用延时函数,延时500,控制拖尾移动速度------延时时间越长,头部移动越慢,拖尾流动越慢;(实际使用时可根据需求调整)。
八、整体运行流程(核心逻辑串联)
-
程序启动,执行main函数,先调用Timer0_Init(),初始化定时器0(定时0.5ms,开启中断),LED初始全灭。
-
进入死循环,先调用Update_Tail(),根据当前head位置,给4个位置的LED赋值亮度(形成拖尾),其余LED灭。
-
head加1,准备下一次拖尾位置,然后延时50秒(拖尾停留50秒)。
-
在整个过程中,定时器0每0.5ms触发一次中断,执行Timer0_ISR():重装初值→更新PWM计数器→根据brightness数组控制LED亮灭(PWM亮度刷新),让人眼看到稳定的拖尾亮度。
-
延时结束后,重复步骤2~4,head不断循环移动,拖尾也随之循环流动,形成持续的LED拖尾流水效果。
九、关键注意点(补充)
-
LED接法:代码默认是"共阳极LED"(阳极接VCC,阴极接P0口),若用共阴极LED,需修改代码(output初始为0x00,点亮逻辑改为"brightness[i]>pwm_count时,output |= (1<<i)")。
-
延时时间越小 ,拖尾移动更流畅。
-
拖尾效果调整:修改tail_bright数组的数值,可改变拖尾的亮度渐变效果(比如改为{0,10,20,15},拖尾亮度更柔和);修改循环次数(i<4),可改变拖尾长度(比如i<5,拖尾更长)。
-
定时器初值:若晶振不是11.0592MHz,需重新计算TH0和TL0的初值(保证定时时间准确,PWM刷新稳定)。