利用Arduino剖析PWM脉宽调制

本文为转载文章,来自博客园,提过下面链接可以查看原文
作者:代码钢琴家-lulipro
出处:[link]http://www.cnblogs.com/lulipro/

PWM(Pulse Width Modulation)简介

PWM,也就是脉冲宽度调制,用于将一段信号编码为脉冲信号(方波信号)。是在数字电路中 达到 模拟输出效果的一种手段。即:使用数字控制产生占空比不同的方波(一个不停在开与关之间切换的信号)来控制模拟输出。我们要在数字电路中输出模拟信号,就可以使用PWM技术实现。

在单片机中,我们常用PWM来驱动LED的暗亮程度,电机的转速等。

原理 

我们知道,在数字电路中,电压信号是离散的: 不是 0(0V)  就是 1(5V或者3.3V), 那么如何输出介于 0v 和  5V之间的某个电压值呢?

我们先来举个实际的例子,一看就懂,胜过千言万语。

如下图,要让让数字信号模拟出 3.7V 的电压输出。可以先假想:3.7V的电压输出是由多个周期时间为T ,电压值 都 为3.7V的信号 持续输出形成的。

 

根据PWM原理,我画出下面等效脉冲信号波形图(红色) 。PWM的理念是:连续的信号可以使用独立的方波信号模拟出来,手段就是调整输出的脉冲宽度,以达到同样的效果。你会怀疑:这真的可以达到同样的效果吗?

别急,让我们继续往后看。

 如果一个周期T内的均值电压等于3.7V,那么,整体的输出就是3.7V,因为整体只不过是n个周期不断的重复而已。那么我们的主要问题就是如何让一个调制周期T时间内的均值电压等于3.7V。下面就开始计算。

 

设:脉冲信号的值随时间变化的函数为:

因为这里是数字电路的背景下的,所以v的值只能取 0v 或者   5v。

 

设:在一个周期T时间中,高电平持续时间占T的百分比为 D,则低电平持续时间在周期T中占的百分比为 1- D。

我们对 一个调制周期T内的电压值对时间积分,然后除以周期T,就得到了这个周期的输出电压均值。

由于这个积分图形是方波,所以很好计算(就是面积 除以T)。

 

 

可以看出,1个调制周期内,输出的电压均值只和D有关。也就是高电平信号占持续时间占这个周期的百分比决定这个周期内的输出电压。

上面说了,要让这个均值等于3.7v,则求出D为:0.74  。

那也就是说:如果在一个调制周期中,高电平持续时间占周期的百分比为74%,则整体输出的信号就是3.7V。这个百分比就是下面要说的占空比

 

 

占空比(duty cycle)

有了前面的知识,相信你已经对占空比理解了,其实很好理解 ,占空比就是 在一段调制周期时间内,某个信号持续的时间占这个时间段的百分比。

下面给出占空比的公式

 

 
D: 占空比
PW: 脉冲宽度(调制周期中脉冲持续时间)
T:  一个调制周期

 

下面是一个占空比不断变化的图示

 

所以我们可以很自然的得出结论:

低占空比意味着输出的能量低,因为在一个周期内大部分时间信号处于关闭状态,如果pwm控制的负载为led,则具体表现例如led灯很暗。

高占空比意味着输出的能量高,在一个周期内,大部分时间信号处于on状态,具体表现为LED比较亮。

当占空比为100%时,表示 fully on,也就是在一个周期内,信号都处于on状态,具体表现为led亮度到达100%。

为0%时则表示 totally off,在一个周期内,一直处于off状态,具体表现为led熄灭。

 现在一切都明了了:脉宽调制,脉宽调制,脉宽调制,这个宽,不是物体的宽度,而是高电平信号在一个调制周期中持续时间长短,它可以用占空比去衡量,占空比越大,脉冲宽度越宽。

 

占空比随时间变化的PWM调制

扩展:用PWM模拟出如下的正玄波(假设仅仅用3个周期去调制出这段正弦波)

道理和前面是一样的,只不过,因为从图中看出,模拟信号(黑色曲线)随着时间不断加强,因此,占空比要变化,也就是逐渐增加。下面3个调制周期中,占空比D逐渐增大。

 

 

 

PWM的频率 (PWM frequency)

pwm的频率决定了输出的数字信号on ,1 和 off,0 的切换速度。频率越高,切换就越快。频率的大小就是前面提到的调制周期T的倒数  : f = 1/T。

1秒内,0.5秒开,0.5秒灭,占空比是50%。那么,1毫秒内,0.5毫秒开,0.5毫秒灭,占空比也是50%,对于前者,频率就是1HZ,而后者,是1毫秒,频率就是1KHZ。

一般pwm频率都是因硬件设计而固定的,是由pwm发生器决定的。PWM频率越高,调制出来的输出曲线就更加的smooth,效果越好,完成一个调制周期的时间越短。这个和手机的ppi越高,显示越清晰是一个道理。当然我想PWM的频率越高,对硬件的要求就也越高。

下图中,右边的频率是左边的2倍,调制出的曲线更加圆滑,贴近理想波形。

 

 

 

使用Arduino来实战!

首先要确定你的Arduino 的哪些引脚支持PWM输出,数字引脚上标记了 ~ 符号的就是支持PWM的。Arduino主控芯片为ATmega168或者ATmega328的3, 5, 6, 9, 10, 和 11引脚支持PWM,Arduino Mega的 2~13 , 44~46引脚支持PWM,老板子ATmega8的9,10,11脚支持PWM。

Arduino的库中通过analogWrite函数来完成PWM输出。

 

analogWrite(pin,value)

作用:让一个支持PWM输出的引脚持续输出指定脉冲宽度的方波。

参数:

     pin:PWM输出的引脚编号。

     value:用于控制占空比,范围:0~255。值为0表示占空比为0,值为255表示占空比为100%,值为127表示占空比为50%。

 

当调用一次此函数后,引脚就会持续稳定地输出指定占空比的PWM方波,直到下一次对同一个引脚的新的调用来修改脉冲宽度的值,就会再持续输出新的脉冲宽度的PWM波。

Arduino板的PWM输出频率一般是490Hz,意味着一个调制周期的完成需要2ms的时间。在Uno或者与Uno相似的板子上,其5和6引脚PWM的频率约为980Hz(一个调制周期的完成需要1ms的时间)。

注意

1、analogWrite和analogRead没有任何关系,他们虽然都属于模拟驱动函数,但是他们使用的技术不同,一个是PWM,一个是A/D转换。

2、在调用analogWrite前,无需对引脚设置pinMode为输出,因为函数实现中已经完成了这个设置,见下面源代码。

3、从源代码中也可以发现,当value的值为0时(占空比为0),等价于持续输出低电平,当value值为255时(占空比为100%),等价于持续输出高电平。当value不是0也不是255时,是通过设置定时器/计数器的比较寄存器的值来完成的,可以看出PWM技术依赖单片机内部的Timer。正是这个原因,引脚5和6实际的占空比可能比设置的高,特别是在value值很小的时候,例如value值为0时,PWM的占空比实际却可能不为0而比0高一点,因为5和6的PWM生成器器依赖的Timer同时也被millis()和delay()函数使用。

void analogWrite(uint8_t pin, int val)
{

    // We need to make sure the PWM output is enabled for those pins
    // that support it, as we turn it off when digitally reading or
    // writing with them.  Also, make sure the pin is in output mode
    // for consistenty with Wiring, which doesn't require a pinMode
    // call for the analog output pins.
    
    pinMode(pin, OUTPUT);
    if (val == 0)
    {
        digitalWrite(pin, LOW);
    }
    else if (val == 255)
    {
        digitalWrite(pin, HIGH);
    }
    else
    {
        switch(digitalPinToTimer(pin))
        {
            // XXX fix needed for atmega8
            #if defined(TCCR0) && defined(COM00) && !defined(__AVR_ATmega8__)
            case TIMER0A:
                // connect pwm to pin on timer 0
                sbi(TCCR0, COM00);
                OCR0 = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR0A) && defined(COM0A1)
            case TIMER0A:
                // connect pwm to pin on timer 0, channel A
                sbi(TCCR0A, COM0A1);
                OCR0A = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR0A) && defined(COM0B1)
            case TIMER0B:
                // connect pwm to pin on timer 0, channel B
                sbi(TCCR0A, COM0B1);
                OCR0B = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR1A) && defined(COM1A1)
            case TIMER1A:
                // connect pwm to pin on timer 1, channel A
                sbi(TCCR1A, COM1A1);
                OCR1A = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR1A) && defined(COM1B1)
            case TIMER1B:
                // connect pwm to pin on timer 1, channel B
                sbi(TCCR1A, COM1B1);
                OCR1B = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR1A) && defined(COM1C1)
            case TIMER1C:
                // connect pwm to pin on timer 1, channel B
                sbi(TCCR1A, COM1C1);
                OCR1C = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR2) && defined(COM21)
            case TIMER2:
                // connect pwm to pin on timer 2
                sbi(TCCR2, COM21);
                OCR2 = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR2A) && defined(COM2A1)
            case TIMER2A:
                // connect pwm to pin on timer 2, channel A
                sbi(TCCR2A, COM2A1);
                OCR2A = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR2A) && defined(COM2B1)
            case TIMER2B:
                // connect pwm to pin on timer 2, channel B
                sbi(TCCR2A, COM2B1);
                OCR2B = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR3A) && defined(COM3A1)
            case TIMER3A:
                // connect pwm to pin on timer 3, channel A
                sbi(TCCR3A, COM3A1);
                OCR3A = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR3A) && defined(COM3B1)
            case TIMER3B:
                // connect pwm to pin on timer 3, channel B
                sbi(TCCR3A, COM3B1);
                OCR3B = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR3A) && defined(COM3C1)
            case TIMER3C:
                // connect pwm to pin on timer 3, channel C
                sbi(TCCR3A, COM3C1);
                OCR3C = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR4A)
            case TIMER4A:
                //connect pwm to pin on timer 4, channel A
                sbi(TCCR4A, COM4A1);
                #if defined(COM4A0)        // only used on 32U4
                cbi(TCCR4A, COM4A0);
                #endif
                OCR4A = val;    // set pwm duty
                break;
            #endif
            
            #if defined(TCCR4A) && defined(COM4B1)
            case TIMER4B:
                // connect pwm to pin on timer 4, channel B
                sbi(TCCR4A, COM4B1);
                OCR4B = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR4A) && defined(COM4C1)
            case TIMER4C:
                // connect pwm to pin on timer 4, channel C
                sbi(TCCR4A, COM4C1);
                OCR4C = val; // set pwm duty
                break;
            #endif
                
            #if defined(TCCR4C) && defined(COM4D1)
            case TIMER4D:                
                // connect pwm to pin on timer 4, channel D
                sbi(TCCR4C, COM4D1);
                #if defined(COM4D0)        // only used on 32U4
                cbi(TCCR4C, COM4D0);
                #endif
                OCR4D = val;    // set pwm duty
                break;
            #endif

                            
            #if defined(TCCR5A) && defined(COM5A1)
            case TIMER5A:
                // connect pwm to pin on timer 5, channel A
                sbi(TCCR5A, COM5A1);
                OCR5A = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR5A) && defined(COM5B1)
            case TIMER5B:
                // connect pwm to pin on timer 5, channel B
                sbi(TCCR5A, COM5B1);
                OCR5B = val; // set pwm duty
                break;
            #endif

            #if defined(TCCR5A) && defined(COM5C1)
            case TIMER5C:
                // connect pwm to pin on timer 5, channel C
                sbi(TCCR5A, COM5C1);
                OCR5C = val; // set pwm duty
                break;
            #endif

            case NOT_ON_TIMER: 
            default:
                if (val < 128) {
                    digitalWrite(pin, LOW);
                } else {
                    digitalWrite(pin, HIGH);
                }
        }
      }
    }

 

一个例子

 

试验连线线图

 

const byte  ledPin = 3; //pwm输出引脚
const byte  button = 6; //按键引脚
byte pwmVal = 0;
bool isKeyPressed(byte pin);

void setup() {
  pinMode(button,INPUT_PULLUP);    //配置为数字输入,且使能内部上拉电阻 
  Serial.begin(9600);
}

void loop() {
  if(isKeyPressed(button))     //如果检测到按键按下,就让pwmVal 增加2
  {
       pwmVal+=2;         //pwmVal 的类型为byte,到了256会自动溢出回0,所以为没做检查,不过不要过度依赖这个技巧啊,规范些好
  }
  analogWrite(ledPin,pwmVal);
  Serial.println(map(pwmVal,0,255,0,5));    //使用map函数映射为 0~5v的电压信号
  delay(30);
}

bool isKeyPressed(byte pin)  //按键检测函数
{
	bool pre = false;
    if(digitalRead(pin)==LOW)
    {
        delay(10);
        if(digitalRead(pin)==LOW)
        {
             pre = true;
             for(int a = 5;digitalRead(pin)==LOW&&a;--a)
             {
                 delay(5);
             }
        }
    } 
    return pre;
 }

 

在IDE的串口绘图器中查看输出的波形。因为是手动按键来调整占空比的,所以波形不好看。用电位器调更加好。

 

我们去掉map函数,直接输出pwmVal的值,可以看到更加细腻。

 

夜晚的效果。

 

 

本文转自博客园,版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。为了获得更好的阅读体验,请访问原博客地址。

本文为转载文章,来自博客园,提过下面链接可以查看原文
作者:代码钢琴家-lulipro
出处:http://www.cnblogs.com/lulipro/

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值