历史上的今天
返回首页

历史上的今天

今天是:2026年01月30日(星期五)

正在发生

2023年01月30日 | PWM视频代码剖析与解释

2023-01-30 来源:zhihu

1、不同频率LED灯闪烁

接下来我们以下面LED灯的闪烁代码为例子,改变延时长短来看LED灯的效果

void setup()

{

  pinMode(2, OUTPUT);

}


void loop()

{

  digitalWrite(2, HIGH);

  delay(50); // Wait for xx millisecond(s)

  digitalWrite(2, LOW);

  delay(50); // Wait for xx millisecond(s)

}

动图封面

500ms延时闪烁(1Hz频率)


动图封面

200ms延时闪烁(2.5Hz频率)

动图封面

50延时ms闪烁(10Hz频率)

通过三个对比实验我们发现随着频率的升高,我们的LED灯慢慢的开始感觉不到闪烁,由于我们人眼的视觉停留效应,一般大于50Hz的刷新率就能满足我们的要求


2、高频率LED闪烁变形过程

我们还是以这个代码,将频率固定在50Hz,然后保持周期不变即高低电平加起来的时间等于40ms,然后改变高低电平的占空比(高低电平占总周期的百分比),我们通过调节高低电平的延时的长度来调节亮度的占比

代码部分:

void setup()

{

  pinMode(2, OUTPUT);

}


void loop()

{

  digitalWrite(2, HIGH);

  delay(10); // Wait for 1000 millisecond(s)

  digitalWrite(2, LOW);

  delay(10); // Wait for 1000 millisecond(s)

}

动图封面

50Hz频率最大亮度(即灯灭的情况下延时为0,亮的情况下延时20ms)

动图封面

50Hz频率50%亮度(即灯灭的情况下延时为10ms,灯亮的情况下延时10ms)

动图封面

50Hz频率25%亮度(即灯灭的情况下延时为15ms,灯亮的情况下延时5ms)

动图封面

50Hz频率10%亮度(即灯灭的情况下延时为18ms,灯亮的情况下延时2ms)

我们把上面的动作连贯起来,也就是说把亮度延时做成连续变化,为了在实际中效果更好,我们将延时改成延时200个us,这样连续变化效果更好

代码部分:

void setup()

{

  pinMode(2, OUTPUT);

}


int count = 0;

int PWM_Time = 50;

  

void loop()

{

  digitalWrite(2, HIGH); // LED灯灭

  delayMicroseconds(200-PWM_Time); // Wait for xx millisecond(s)

  digitalWrite(2, LOW); // LED灯亮

  delayMicroseconds(PWM_Time); // Wait for xx millisecond(s)

  

  count++;

  if(count==50)

  {

      count = 0;

      PWM_Time++;

      if(PWM_Time>=200) PWM_Time = 0;

  }

}

动图封面

LED灯渐亮效果

我们再进一步修改下,让它变成一个呼吸的效果

代码部分:

void setup()

{

  pinMode(2, OUTPUT);

}


int count = 0;            // 因为延时比较短,直接使用会变化的

      // 太快看不到效果,说以加个计数的变量

int PWM_Time = 0;         // LED占空比变量

int LED_Togle_Flag = 0;   // LED逐渐亮灭翻转标志

  

void loop()

{

  if(LED_Togle_Flag)

  {

    digitalWrite(2, HIGH); // LED灯灭

    delayMicroseconds(200-PWM_Time); // Wait for xx millisecond(s)

    digitalWrite(2, LOW); // LED灯亮

    delayMicroseconds(PWM_Time); // Wait for xx millisecond(s)

  }

  else

  {

    digitalWrite(2, HIGH); // LED灯灭

    delayMicroseconds(PWM_Time); // Wait for xx millisecond(s)

    digitalWrite(2, LOW); // LED灯亮

    delayMicroseconds(200-PWM_Time); // Wait for xx millisecond(s)

  }

  

  

  count++;

  if(count==50)

  {

  count = 0;

    PWM_Time++;

    // 切换亮暗变化逻辑

    if(PWM_Time>=200) 

    {

    PWM_Time = 0;

        LED_Togle_Flag = ~LED_Togle_Flag;

    }

  }

}

动图封面

LED灯呼吸灯


LED“流星雨”

首先我们先来分析下流星雨的逻辑:

首先我们要实现一个这样的效果,第一个最亮,然后后一个是前一个的45%的亮度

代码部分:

// ----------------------------------------------------------------------------

// LED_Rains.ino

//

// 数字引脚实现的雨滴流动效果

// 雨滴流动效果与流水灯(跑马灯)的区别在于雨滴流水效果有拖尾效果,即亮过的灯是慢慢熄灭的

//

// 使用 UNO 的所有引脚用模拟 PWM 实现雨滴流动的效果,包括模拟输入口也可以用做数字输出

// 各引脚接 LED 正极,LED 负极接 GND

// ----------------------------------------------------------------------------


const unsigned char leds[] = { A4, A5, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; // 所有的引脚按 LED 接线顺序排列

const unsigned int maxPwm       = 100;    // 手工模拟 PWM,可以自己定义最大的 PWM 值是多少,所以定义一个整百整千的数比较方便计算


unsigned int ledPwm[12] = { 1, 3, 4, 6, 9, 13, 18, 25, 35, 50, 70, 100}; // 存放运行时每一个 LED 的亮度 PWM 值



void setup()

{

    for (char i = 0; i < 12; ++i)

    {

        pinMode(leds[i], OUTPUT);

    }

}


void loop()

{

    unsigned int i, j;


    for (i = 0; i < 12; ++i)      // 先亮灯,等占空比到切换点的时候灭灯

    {

        digitalWrite(leds[i], LOW);

    }

   

    for( i=0; i    {

    for (j = 0; j < 12; ++j)

        {

            if (i == ledPwm[j])

                digitalWrite(leds[j], HIGH);

        }

        delayMicroseconds(1);

    }

}

动图封面

静态流星雨效果

代码解释:

我门首先给亮度数组ledPwm[12]储存按比例分配的数值,这里我是按70%的一个比例来计算

比如说最暗是100,那么次暗的就是100*70% = 70,以此类推,然后我们就按照分配的亮度来把灯分别点亮

这部分代码是把所有LED灯先点亮

    for (i = 0; i < 12; ++i)      // 先亮灯,等占空比到切换点的时候灭灯

    {

        if (ledPwm[i] == 0)

            continue;

        digitalWrite(leds[i], LOW);

    }

这部分代码根据LED灯的亮暗程度来分别控制灭的时间,我们先根据最大亮度值“maxPWM”来将亮度分为100份,每份的延时是1us,然后在内部的循环里面检查当前的亮度值是否到达分配的份数,如果到达了,那就熄灭,没有到达,就继续保持亮


 for( i=0; i    {

    for (j = 0; j < 12; ++j)

        {

            if (i == ledPwm[j])

                digitalWrite(leds[j], HIGH);

        }

        delayMicroseconds(1);

    }



让LED”流星雨“运动

显然这样静态的流星雨还是满足不了我们的要求,接下来我们让流星雨先动起来

我们需要它这样动

LED流星雨动态分解示意图

我们先试着让它动一位,我只需要把ledPwm[12]这个数组里面的值重新进行排列就可以了,这其实就是对数组操作

unsigned int ledPwm[12] = { 3, 4, 6, 9, 13, 18, 25, 35, 50, 70, 100, 1}; // 存放运行时每一个 LED 的亮度 PWM 值

LED流星雨移动一位的效果



LED“流星雨”连续运动

从上面我们知道,我们如果有办法对数组进行连续的操作,那么就能实现“流星雨”流动的效果


其中下面这一点代码是arduino自带的时间计数器,可以直接读取里面的数值,用来辅助计数用的,其实你也可以不用这个,可以自己直接在里面计数也可以的


extern volatile unsigned long timer0_millis;


完整版代码:把所有需要改动的数值变量全部放在最前面,这是编写可复用程序常用的一种做法,可以灵活适应多个灯,同时可以调节速度,调节亮度比例

// ----------------------------------------------------------------------------

// LED_Rains.ino

//

// 数字引脚实现的雨滴流动效果

// 雨滴流动效果与流水灯(跑马灯)的区别在于雨滴流水效果有拖尾效果,即亮过的灯是慢慢熄灭的

//

// 使用 UNO 的所有引脚用模拟 PWM 实现雨滴流动的效果,包括模拟输入口也可以用做数字输出

// 各引脚接 LED 正极,LED 负极接 GND

// ----------------------------------------------------------------------------


const unsigned char leds[] = { A4, A5, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; // 所有的引脚按 LED 接线顺序排列

const unsigned int maxPwm       = 100;    // 手工模拟 PWM,可以自己定义最大的 PWM 值是多少,所以定义一个整百整千的数比较方便计算

const unsigned int initPwm      = 100;    // 最亮的灯的 pwm 值,即移动的时候在最头一个灯的亮度

const unsigned int deltaPwm     = 1;      // 灯慢慢熄灭的 pwm 值,后一个灯比前一个灯暗多少。这相当于是一个等差队列。等差的亮度感觉不大好,所以引入下一个等比的因素

const unsigned int deltaPercent = 45;     // 后一个灯比前一个灯暗,其亮度是前一个灯的百分之几。相对于前面的递减,这个相当于是等比级数

const unsigned long delayMs     = 100;     // 移动延迟,单位 ms

const unsigned char ledNum      = sizeof(leds) / sizeof(leds[0]);  // 引脚数量,即 LED 个数


unsigned int ledPwm[ledNum]; // 存放运行时每一个 LED 的亮度 PWM 值


void setup()

{

    for (char i = 0; i < ledNum; ++i)

    {

        pinMode(leds[i], OUTPUT);

        ledPwm[i] = 0;

    }

}


extern volatile unsigned long timer0_millis;    // 声明外部变量 timer0_millis 以便在程序中使用,其实就是 millis() 的返回值——程序运行的毫秒数


void loop()

{

    static unsigned char head = 0;

    static unsigned long lastTick = timer0_millis;

    unsigned int i, j;


    for (i = 0; i < ledNum; ++i)      // 先亮灯,等占空比到切换点的时候灭灯

    {

        if (ledPwm[i] == 0)

            continue;

        digitalWrite(leds[i], LOW);

    }

   

    ledPwm[head] = initPwm;         // 水滴头是最亮的


    for (i = 0; i < maxPwm; ++i)    // 这里就是数字口模拟的 PWM 程序了

    {

        for (j = 0; j < ledNum; ++j)

        {

            if (i == ledPwm[j])

                digitalWrite(leds[j], HIGH);

        }

        delayMicroseconds(1);

    }


    // 如果延时时间还没到,先跳出,不进行水滴的移动 

    if (timer0_millis - lastTick < delayMs) // 由于是用数字口模拟的 PWM,程序要不停的跑,不能使用 delay() 来延时,会卡住的

        return;


    lastTick = timer0_millis;


    for (i = 0; i < ledNum; ++i)      // 处理每一个灯的亮度

    {

        ledPwm[i] = ledPwm[i] * deltaPercent / 100;

        if (ledPwm[i] <= deltaPwm){ ledPwm[i] = 0; }           

        else                      { ledPwm[i] -= deltaPwm; }

            

        if (i == head){ ledPwm[i] = initPwm; }            

    }

    

    head = (head + 1) % ledNum;  // 移动水滴头

}

动图封面

完整版LED“流星雨”效果


总结:

1、我们了解了PWM的实现方式

2、LED在不同的频率下,会“欺骗”我们的眼睛,这样是玩单片机中惯用的一种思路

3、通过剖析LED"流星雨",我们发现其实它就用了一些简单的处理方式实现的,没有我们想象中的那么复杂

4、LED"流星雨"里面有一点简单的算法,算法是独立于单片机的,在其他平台(51,STM32等)上面同样可以实现,同时算法也是一个程序的灵魂

推荐阅读

史海拾趣

台湾双羽公司的发展小趣事

在成为全球领先的电子企业之后,富士通并没有停下脚步。公司开始积极拓展海外市场,将先进的技术和产品带到世界各地。通过在全球各地设立分支机构、与当地企业建立合作关系等方式,富士通成功地将自己的业务版图扩展到了全球60多个国家和地区。这一过程中,富士通不仅为当地市场带来了先进的技术和产品,也促进了全球电子产业的交流与合作。

汇顶科技(GOODiX)公司的发展小趣事

富士通的故事始于1935年,当时它作为一家电信设备制造公司在日本成立。在那个通信技术刚刚起步的时代,富士通凭借其创新精神和卓越的技术实力,迅速在电信设备领域崭露头角。公司最初专注于电话交换机的生产,随着技术的不断进步,富士通逐渐扩大了业务范围,为日本的电信基础设施建设做出了重要贡献。这一阶段的成功,为富士通后续在电子行业的蓬勃发展奠定了坚实的基础。

Digital Core Design公司的发展小趣事

1988年,Core Design由Jeremy Heath-Smith创立,起初仅有8名员工和1万6千英镑的注册资金。这家新兴的游戏开发公司很快凭借其首款游戏《Rick Dangerous》在英国游戏市场崭露头角。这款游戏凭借其独特的游戏机制和引人入胜的故事情节,迅速登上英国游戏销量榜的榜首,并赢得了欧洲年度游戏奖,为Core Design的未来发展奠定了坚实的基础。

EnerSys公司的发展小趣事

为了进一步拓展市场并加强合作伙伴关系,EnerSys积极寻求与行业领先企业的合作。例如,EnerSys与Verkor携手打造美国锂电超级工厂,这一合作项目将有助于提高EnerSys在美国市场的竞争力,并加速全球清洁能源的转型。通过与合作伙伴的紧密合作,EnerSys在电子行业中的影响力逐渐增强。

Anadigm公司的发展小趣事

EnerSys一直致力于技术创新和研发投入。公司拥有一支专业的研发团队,不断推出具有竞争力的新产品和解决方案。例如,EnerSys在锂电池领域取得了重要突破,成功开发出高能量密度、长寿命的锂电池产品。这些创新产品不仅满足了客户的多样化需求,还推动了公司在电子行业中的持续发展。

力芯微(ETEK)公司的发展小趣事

2024年第一季度,力芯微公司实现了营业总收入2.20亿元,同比增长23.57%;归母净利润5347.70万元,同比增长93.35%。这一业绩的取得,不仅体现了公司强大的市场竞争力和盈利能力,也为公司未来的发展奠定了坚实的基础。同时,公司在资产结构、现金流量等方面也取得了显著的改善。

请注意,以上故事均基于事实描述,未对力芯微公司进行评价或褒贬。每个故事都力求全面、客观地展现力芯微公司在电子行业中的发展历程和成就。

问答坊 | AI 解惑

LINUX 内核学习

希望大家多多支持…

查看全部问答>

求助高手:大家帮忙看看这个模拟电路到底是什么问题

如图所示,VCC=+5V;VDC=+29V左右,单片机给PE0一个宽度为 1us  的脉冲信号使得 TIP42 开通,但是测量开通后 TIP42 和二极管D2连接那端的信号起码有的 10us 宽度,这非常影响我后面接个1:1:1变压器出来的信号。查了资料,TIP42开通时0. ...…

查看全部问答>

IP高清监控系统的技术探讨和设计实践

by 千家网 jandar 作者按:面前已经陆续发表了若干个有关IP高清监控系统的基础介绍文章,与一些国外IPC产品的点评,现应一些朋友的要求,发一个IP高清监控系统的设计实例,里面有许多本人在设计过程中碰到的问题,和一些解决的方法,与广大安防从 ...…

查看全部问答>

一个很诡异的现象,在场的所有人包括电工都不能解释

前段时间去过一个工厂测试:一个车间就一台80KVA的变压器单独供电,三相四线由变压器配电房直接拉到车间,线路大约有120米,没有补偿。在车间总进线处测量得到的数据正常: A相:有功11.55KW,功率因素0.723,视在功率16.32KVA,无功11.30KW,电压2 ...…

查看全部问答>

u-boot在skyeye下运行报错!!!!!

SKYEYE:Error in mem_read_word, no bank found, NumInstrs 4182, mem_read_word addr = 4160 no bank SKYEYE:Error in mem_read_word, no bank found, NumInstrs 4183, mem_read_word addr = 4164 no bank SKYEYE:Error in mem_read_word, no ba ...…

查看全部问答>

IssueVendorTransfer USB_DEVICE_REQUEST

IssueVendorTransfer执行对Device的写操作失败,GetLastError为31 我的USB_DEVICE_REQUEST是这样的          req.bmRequestType = USB_TYPE_VENDOR|USB_RECIP_DEVICE;//(0x02hDevice,       &nbs ...…

查看全部问答>

单片机C语言程序该这样写!不是教科书上教的那样!

写单片机程序也是程序,也要遵循写软件的一些基本原则,不是为了完成功能那么简单。我看过的所有的C语言单片机书籍基本都不注重模块化思想,完全是拿着C当汇编用,简直是在糟蹋C语言! 如下问题,几乎所有的单片机书籍中都大量存在(更别说网上的和 ...…

查看全部问答>

请教PC104下 中断的使用方法?

请高手给我详细讲下中断的使用方法例程.我想把我的DLL和相应的中断响应关联起来,不知道要怎么做.我现在只知道 先Oalintr.h里加一个中断定义.#define SYSINTR_TOUCH   (SYSINTR_FIRMWARE+20)然后就不清楚要做什么了? 希望高手给我提供 ...…

查看全部问答>

wince 如何修改网关?

请问在不接屏的情况下通过网络怎么修改wince5.0的网关?系统定制的时候要加哪些东西?我自己定制了一个web server在里面,能过访问设备IP进去,可以修改IP,但找不到修改网关的,知道的或做过的请给说说,谢谢!…

查看全部问答>

关于双核与中断描述表IDT的简单问题

我似乎在网上看到如果是双核的话,那每个处理器都有一个中断描述表IDT。那我想问一下每一个处理器他们的中断号都是统一的吗?也就是说第一处理器IDT[0x93]是键盘中断,那第二处理器0x93的IDT[0x93]也一定是键盘中断吗?…

查看全部问答>