AVR单片机教程——PWM调光

发布者:SereneDreamer最新更新时间:2020-09-09 来源: eefocus关键字:AVR  单片机  PWM调光 手机看文章 扫描二维码
随时随地手机看文章

PWM

两位数码管的驱动方式是动态扫描,每一位都只有50%的时间是亮的,我们称这个数值为其占空比。让引脚输出高电平点亮LED,占空比就是100%。


在驱动数码管时,我们迫不得已使占空比为50%,因为不能让两位真正同时地显示不同的数字。但是,我们也可以有意地让LED的占空比不到100%,以降低其亮度。


占空比是可以用程序来调节的。下面的程序允许用户用按键调整蓝色LED的占空比,并通过数码管来显示。


#include


#define DUTY_MAX 9


int main()

{

    led_init();

    button_init(PIN_NULL, PIN_NULL);

    segment_init(PIN_NULL, PIN_8);

    uint8_t duty = 0;

    while (1)

    {

        if (button_pressed(BUTTON_0) && duty > 0)

            --duty;

        if (button_pressed(BUTTON_1) && duty < DUTY_MAX)

            ++duty;

        segment_dec(duty);

        segment_display(SEGMENT_DIGIT_R);

        for (uint8_t i = 0; i != DUTY_MAX; ++i)

        {

            if (i < duty)

                led_set(LED_BLUE, true);

            else

                led_set(LED_BLUE, false);

            delay(1);

        }

    }

}

duty是一个整数,取值范围为0到9,分别表示LED的占空比为0/9到9/9。比如,当占空比为4/9时,在9毫秒的周期中,前4毫秒LED亮,后5毫秒LED不亮。


可以看见,占空比越大,LED亮度也越高。原来,在亮与暗之间,LED还有中间的状态。我们不是通过让引脚输出一个0V和5V之间的电压,而是让引脚电平迅速地在高低之间变化来实现的。


这种通过电平的快速跳变来实现模拟量效果的技术,称为脉冲宽度调制,简称PWM。


定时器

大多数单片机的定时器都可以输出PWM波,外设丰富的AVR单片机自然不例外。上一讲提到定时器0有四种工作模式,后两种就是快速PWM模式与相位修正PWM模式。


在快速PWM模式中,TCNT0寄存器的动作与普通模式相同,但还可以把OCR0A作为上限。对于非反转输出,TCNT0达到上限并清零后,引脚会输出高电平;而当TCNT0与OCR0A或OCR0B匹配时,OC0A和OC0B会分别输出低电平;对于反转输出,前者为低,后者为高。一般使用非反转,输出PWM波的频率为fCPU/256N(对于上限为255的情况;N为分频系数),占空比为(OCR0x+1)/256。由于占空比分母256为2的8次方,这个PWM输出是8位分辨率的。


相位修正PWM主要用于电机控制等对PWM波的形状要求比较严格的场合,这里不细讲。定时器1有更多工作模式,定时器2的时钟系统更为丰富,你可以在数据手册中一探究竟。


在占空比公式(OCR0x+1)/256中,OCR0x可以取0到255的值,因此占空比可以达到1,PWM模式下LED可达最大亮度;占空比不能达到0,因此用PWM控制的LED不能全暗。这有点麻烦,必须关掉PWM才能使LED暗,而不仅仅是往OCR0x中写入一个值了。为了日后使用方便,我们用函数把寄存器操作包装起来(整个库都在做这件事)。


在Atmel Studio中,静态库与可执行程序都属于project,可以并列存在于solution中。在上面软件PWM的程序所属的solution中,点击菜单栏File->New->Project(或Ctrl+Shift+N),选择“GCC C Static Library Project”,命名为“pwm”,在“Solution:”中选择“Add to solution”,“OK”后选择MCU型号,静态库项目就创建好了,默认带有一个library.c文件。


在“Solution Explorer”中,将library.c重命名为oc0a.c。选中“pwm”项目,右键->Add->New Item或菜单栏->Project->Add New Item或Ctrl+Shift+A,选择“Include File”,命名为oc0a.h(通常取相同的名字,但不是必须的)。


这个库需要提供两个函数:oc0a_init用于将OC0A引脚配置为PWM输出,oc0a_pwm设置输出PWM占空比,参数为一个无符号8位整数。


// oc0a.h


#ifndef OC0A_H

#define OC0A_H


#include


/*

 * 函数:oc0a_init

 * 参数:无

 * 返回:void

 * 功能:将OC0A引脚配置为PWM输出,占空比为0。

 */

void oc0a_init();


/*

 * 函数:oc0a_pwm

 * 参数:uint8_t _duty - 占空比的整数表示

 * 返回:void

 * 功能:将OC0A引脚输出PWM波的占空比设置为(_duty / 256)。

 */

void oc0a_pwm(uint8_t _duty);


#endif

在头文件oc0a.h中,我们定义了这两个函数,并以注释形式提供了说明,包括参数、返回值与功能。


然后,在oc0a.c中提供这些函数的实现。


// oc0a.c


#include "oc0a.h"

#include


void oc0a_init()

{

    PORTB &=   ~(1 << PORTB3); // PB3 low level

    DDRB  |=     1 << DDB3;    // PB3 output mode

    TCCR0A =  0b00 << COM0A0   // normal port operation

           |  0b11 << WGM00;   // fast PWM mode

    TCCR0B =   0b0 << WGM02    // fast PWM mode

           | 0b010 << CS00;    // divide by 8

}


void oc0a_pwm(uint8_t _duty)

{

#define COMA_MASK (~(0b11 << COM0A0)) // mask for COMnA bits

    if (_duty)                        // fast PWM mode

        TCCR0A = (TCCR0A & COMA_MASK) // protect other bits

               | 0b10 << COM0A0,      // non-inverting mode

        OCR0A  = _duty - 1;           // duty = (OCRnx + 1) / 256

    else                              // turn PWM off

        PORTB &= ~(1 << PORTB3);      // PB3 low level

        TCCR0A = (TCCR0A & COMA_MASK) // protect other bits

               | 0b00 << COM0A0;      // normal port operation

}

实现文件应该首先包含对应的头文件,以确保函数接口一致。


作为底层操作的封装,这些函数中涉及到很多寄存器。对寄存器的操作没有写成直接用一个数字来赋值,而是由多种位运算组合起来,这是单片机编程特有的。比如,PORTB3宏定义在中,值为3,意义为PORTB的第3位(最低位为第0位)控制PB3引脚;1 << PORTB3生成一个这一位为1,其余位为0的数;对它取~,得到只有这一位为0,其余位为1的数;让PORTB与这个数进行&=运算,可以保持其他位不变而这一位变成0,这是因为0与一位“与”的结果是0,而1与一位“与”的结果就是那位的值。再比如,COM0A0为6,0b00 << COM0A0把COM0A1:0两位填00,同理0b11 << WGM00把WGM1:0填11,两数|运算,就把TCCR0A中的这两段同时填好了(参考数据手册查看位定义)。


并且,这样写是有多种原因的:对于PORTB等寄存器,函数只负责其中的一位,而赋值语句会影响其他位;对于OCR0A等寄存器,代码中明确写出每一位的名称与值,可以增强可读性。


如果是开源库,注释是写给想深入了解的用户看的;如果是闭源库,以头文件与库文件的形式发布,注释是写给以后的自己看的;总之,需要有注释。注释的目的是消除读者(包括自己)的疑惑。读者不知道0b010 << CS00的意义,就注明“8分频”,这是数据手册写的;读者不明白为什么OCR0A的赋值语句中需要-1,就把占空比的公式放上去,其中有+1。


还需要提醒的是,以上代码的可移植性有些欠缺,因为0b前缀的二进制数是GCC的扩展,不属于C语言标准。最贴近二进制的标准表示方法是十六进制,但是需要手动地转换(在0b0000到0b1111和0x0到0xF之间建立映射,就像涂答题卡时的F-AB到K-BD一样),这也是把寄存器赋值展开写的理由。


呼吸灯

为了测试这个库,我们再新建一个项目,这次选择“GCC C Executable Project”,之后的过程想必你已经做过很多遍了。不同的是引用头文件的写法有点变化,之前写的oc0a.h位于../pwm/目录下,../意为上级目录;以及,需要手动添加这个库,在“Solution Explorer”中该项目的“Libraries”上右键,点击“Add Library”,在“Project Libraries”一页中勾选“pwm”项目;这样就可以使用刚才写的两个函数了。


我们来实现呼吸灯的效果,即LED从暗慢慢变亮,再变暗,像呼吸一样。


#include

#include "../pwm/oc0a.h"


int main()

{

    oc0a_init();

    int brightness = 0, fadeAmount = 5;

    while (1)

    {

        oc0a_pwm(brightness);

        brightness = brightness + fadeAmount;

        if (brightness <= 0 || brightness >= 255)

            fadeAmount = -fadeAmount;

        delay(30);

    }

}

把OC0A引脚连接到开发板左侧RGBW中任意一个,你就会看到对应的LED有呼吸灯的效果。


RGBW

RGBW代表红绿蓝白。理论上,红绿蓝即可组合出所有颜色,而白色的加入即提供了纯正的白光,也能增强整个LED的亮度。


如果你在室内光下观察上面程序的效果,你会发现,尽管变量brightness,所谓亮度,是随时间线性变化的,但是视觉效果上,在整个亮起的过程中,明显是前半段亮度变化快,后面亮度几乎不变。而如果你用手电筒去照着它然后观察,就能感受到后半段的亮度变化。这可能是因为人眼对弱光环境下的强光变化不敏感。


rgbw_set函数解决了这个问题。它不是直接把参数转发给pwm_set,而是用映射后的参数调用;这个映射作为数学上的函数,在x较小时y增长较慢,较大时增长较快,从而抵消人眼的错觉。


#include

#include


void init();

void breathe();

void flash();


int main()

{

    init();

    while (1)

        breathe(), flash();

}


void init()

{

    rgbw_init(PIN_4, PIN_5, PIN_6, PIN_7);

}


void breathe_phase(uint8_t* _status, int8_t* _alter)

{

    for (uint8_t step = 0; step != 200; ++step)

    {

        for (uint8_t which = 0; which != 4; ++which)

            rgbw_set(which, _status[which] += _alter[which]);

        delay(5);

    }

}


void breathe()

{

    uint8_t status[4] = {0, 0, 0, 0};

    int8_t pre[4] = {1, 0, 0, 0};

    int8_t loop[][4] =

    {

        {-1, 1, 0, 0},

        {0, -1, 1, 0},

        {1, 0, -1, 0},

    };

    int8_t post[4] = {-1, 0, 0, 0};

    breathe_phase(status, pre);

    for (uint8_t cnt = 2; cnt--;)

        for (uint8_t pha = 0; pha != sizeof(loop) / sizeof(*loop); ++pha)

            breathe_phase(status, loop[pha]);

    breathe_phase(status, post);

}


void flash_phase(bool* _pattern)

{

    for (uint8_t which = 0; which != 4; ++which)

        rgbw_set(which, _pattern[which] ? 200 : 0);

    delay(500);

}


void flash()

{

    bool extra[4] = {0, 0, 0, 0};

    bool loop[][4] =

    {

        {1, 0, 0, 0},

        {1, 1, 0, 0},

        {0, 1, 0, 0},

        {0, 1, 1, 0},

        {0, 0, 1, 0},

        {1, 0, 1, 0},

    };

    flash_phase(extra);

    for (uint8_t cnt = 2; cnt--;)

        for (uint8_t pha = 0; pha != sizeof(loop) / sizeof(*loop); ++pha)

            flash_phase(loop[pha]);

    flash_phase(extra);

}

这段代码把灯变化的模式用数字表示,而不是用一定参数的函数调用来硬编码,使程序易于修改与扩展。


关键字:AVR  单片机  PWM调光 引用地址:AVR单片机教程——PWM调光

上一篇:AVR单片机教程——LCD1602
下一篇:基于ATMEGAl6和分级转向模块实现智能寻迹车模系统的设计

推荐阅读最新更新时间:2024-11-10 10:12

单片机PID程序
刚刚做的一个恒温箱,采用PID算法,3秒一个输出周期,输出量为0-150,各参数量化到0-99,便于用户修改,为抵消散热,加入维持功率系数,程序如下(温度放大了10倍) typedef struct DeltPID { char PIDTemp;//pid开关温差 char P; char I; char D; char PIDKc;//维持功率 }data DELTPID; int data PID_Buff ;//温度缓冲区-参与PID运算 int data PID_Value;//输出控制量 DELTPID PID= { 50, 50, 50, 50, 50 }; //增量pid结构体
[单片机]
建立一个属于自己的AVR的RTOS(第二篇:人工堆栈)
第二篇:人工堆栈 在单片机的指令集中,一类指令是专门与堆栈和PC指针打道的,它们是 rcall相对调用子程序指令 icall间接调用子程序指令 ret子程序返回指令 reti中断返回指令 对于ret和reti,它们都可以将堆栈栈顶的两个字节被弹出来送入程序计数器PC中,一般用来从子程序或中断中退出。其中reti还可以在退出中断时,重开全局中断使能。 有了这个基础,就可以建立我们的人工堆栈了。 例: #include avr/io.h voidfun1(void) { unsignedchari=0; while(1) { PORTB=i++; PORTC=0x01 (i%8); } } unsignedcharSta
[单片机]
PIC单片机学习遇到的一些问题
1、中断优先级 中断优先级的设置虽然能够比较合理的管理资源,但是如果在中断里写太多内容会导致另一个中断可能永远进不去的现象。比如在定时器中断里定20ms,但是中断里面处理的代码量太多超过20ms,则会出现另一个中断优先级低的刚等待定时器执行完要开始执行时,定时时间到了又进定时中断。如此循环低优先级的中断永远也进不去。(给低优先级中断IE位重新复位可以在短时间无视优先级执行,不过过一段时间也会出现上述情况。作者亲身试过的bug) 2、AD采样出现波动大的问题 有时程序出现AD采样的数值波动大往往是因为可能这时候的供电情况不同,可能你跟原先状态比关闭了什么开启了什么。如开关GPS、开关功放导致的。 3、XC编译器与系统不兼容问
[单片机]
51单片机按键控制数码管0~9_51单片机外部中断
前面为大家介绍的点亮LED灯、数码管、按键只用到了51单片机的IO资源,我们要是仅仅用单片机点灯、操作数码管,那可真是大才小用了。这些都只是51单片机资源的冰山一角,51单片机还有好多的功能,我后面将为大家一一介绍。今天为大家介绍单片机一个重要的外设——中断。 中断 没接触过单片机的朋友听到这个词肯定很陌生,大家对打断这个词应该不陌生吧,中断字面意思可以理解为中途被打断。大家可以思考一下,什么的中途被什么给打断了呢?想明白了这个问题就说明理解中断了。下面看看百度的解释: 中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情 况的程序,处理完毕后又返回原被暂停的程
[单片机]
51<font color='red'>单片机</font>按键控制数码管0~9_51<font color='red'>单片机</font>外部中断
51单片机数据类型int,float,指针所占字节数
1.int===2个字节 2.sfr===特殊功能寄存器,也是一种扩充数据类型,占用1个内存单元,利用它可以访问51单片机内的所有特殊功能寄存器。 sfr P1 = 0x90;/////////这一句定义P1为P1端口在片内的寄存器。 3.sfr16===16位特殊功能的寄存器。用于定时器T0,T1 4.sbit===可录址位,也是一种扩充数据类型。利用它可访问芯片内部RAM中的可寻址位或特殊功能寄存器的可寻址位。 sfr P1 = 0x90;/////////因P1端口的寄存器是可寻址位的,所以我们可以定义 sbit P1_1 = P1^1;///////P1_1为P1中的P1.1引脚 ////////== sbit P1_1
[单片机]
51<font color='red'>单片机</font>数据类型int,float,指针所占字节数
单片机成长之路(51基础篇) - 008 C51 的标示符和关键字
标准 C 语言定义了 32 个关键字,如下表(ANSI C的32个关键字):   C51在此基础上针对单片机功能进行了扩展,详情见下表(C51编译器扩充关键字): C 51的数据类型 51单片机使用的C语言的存储器类型分为以下几种:
[单片机]
<font color='red'>单片机</font>成长之路(51基础篇) - 008 C51 的标示符和关键字
Spansion 推出全新汽车微控制器产品家族
基于ARM® Cortex® R5 内核的 Traveo™ MCU 家族可为汽车应用提供先进的 HMI、安全和网络功能。 2014年5月21日,中国北京 –——全球行业领先的嵌入式市场闪存解决方案创新厂商 Spansion 公司(NYSE:CODE)今日宣布推出一个针对汽车应用市场全新微控制器家族。Spansion® Traveo™ 微控制器家族基于ARM Cortex®-R5 内核,能够针对电气化、车身电子、电池管理、汽车仪表盘、供热通风与空调(HVAC)、先进驾驶辅助系统 (ADAS) 等一系列广泛的汽车应用提供高性能、先进的人机交互界面、高安全性以及先进的网络系统协议。 新的产品家族结合了Spansion
[汽车电子]
Spansion 推出全新汽车<font color='red'>微控制器</font>产品家族
高性价比的单片机应用系统结构设计方案
  MSP430系列单片机作为一个性能优异的MCU在大陆已经得到了广泛的应用。MSP430在高整合性与高性能方面与其他MCU比较有较大优势。该系列芯片的价格也较为合理,目前整合性最好的MAP430F44X系列,整合了60K字节程序存储(可记录数据)、2K字节片内RAM、6个I/O端口(P1、P2能中断)、160段液晶驱动、两个串行端口、4个定时器(其中TB带有7个捕获/比较器、包括看门狗)、模拟比较器、硬件乘法器、8路12位A/D转换器、还有频率调整电路FLL+、系统复位SVS模块等。而较为基本型的MSP430F1101、MSP430C1101只有1K字节程序存储、128字节片内RAM、模拟比较器、两个定时器等。他们的性能比较可参考
[电源管理]
高性价比的<font color='red'>单片机</font>应用系统结构设计方案
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
随便看看

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved