AVR单片机教程——PWM调光
本文隶属于AVR单片机教程系列。
PWM
两位数码管的驱动方式是动态扫描,每一位都只有50%的时间是亮的,我们称这个数值为其占空比。让引脚输出高电平点亮LED,占空比就是100%。
在驱动数码管时,我们迫不得已使占空比为50%,因为不能让两位真正同时地显示不同的数字。但是,我们也可以有意地让LED的占空比不到100%,以降低其亮度。
占空比是可以用程序来调节的。下面的程序允许用户用按键调整蓝色LED的占空比,并通过数码管来显示。
#include <ee1/ee.h>
#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波的频率为\(f_{CPU} / 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 <stdint.h>
/*
* 函数: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 <avr/io.h>
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宏定义在<avr/io.h>中,值为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 <ee1/delay.h>
#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 <ee1/delay.h>
#include <ee1/rgbw.h>
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);
}
这段代码把灯变化的模式用数字表示,而不是用一定参数的函数调用来硬编码,使程序易于修改与扩展。
作业
阅读数据手册,实现在
OC1A引脚上输出12位分辨率的、带相位与频率修正的PWM波。注意占空比为0和1的情况。玩玩灯吧!
AVR单片机教程——PWM调光的更多相关文章
- AVR单片机教程——示波器
本文隶属于AVR单片机教程系列. 在用DAC做了一个稍大的项目之后,我们来拿ADC开开刀.在本讲中,我们将了解0.96寸OLED屏,移植著名的U8g2库到我们的开发板上,学习在屏幕上画直线的算法, ...
- AVR单片机教程——ADC
ADC 计算机的世界是0和1的.单片机可以通过读取0和1来确定按键状态,也可以输出0和1来控制LED.即使是看起来不太0和1的PWM,好像可以输出0到5V之间的电压一样,达到0和1之间的效果,但本质上 ...
- AVR单片机教程——数字输入
我们已经学习了如何使用按键和拨动开关,不知你有没有好奇 button_down 和 switch_status 等函数是如何实现的.本篇教程带你一探究竟,让我们从按键的原理开始. 在原理图中,按键的符 ...
- AVR单片机教程——定时器中断
本文隶属于AVR单片机教程系列. 中断,是单片机的精华. 中断基础 当一个事件发生时,CPU会停止当前执行的代码,转而处理这个事件,这就是一个中断.触发中断的事件成为中断源,处理事件的函数称为中断 ...
- AVR单片机教程——LCD1602
本文隶属于AVR单片机教程系列. 显示屏 开发板套件里有两块屏幕,大的是LCD(液晶显示),小的是OLED(有机发光二极管).正与你所想的相反,短小精悍的比较贵,而本讲的主题--LCD1602-- ...
- AVR单片机教程——DAC
本文隶属于AVR单片机教程系列. 单片机的应用场景时常涉及到模拟信号.我们已经会使用ADC把模拟信号转换成数字信号,本讲中我们要学习使用DAC把数字信号转换成模拟信号.我们还将搭建一个简单的功率放 ...
- AVR单片机教程——走向高层
本文隶属于AVR单片机教程系列. 在系列教程的最后一篇中,我将向你推荐3个可以深造的方向:RTOS.C++.事件驱动.掌握这些技术可以帮助你更快.更好地开发更大的项目. 本文涉及到许多概念性的内容 ...
- AVR单片机教程——第三期导语
背景(一) 寒假里做了一个灯带控制器: 理想情况下我应该在一个星期内完成这个项目,但实际上它耗费了我几乎一整个寒假,因为涉及到很多未曾尝试的方案.在这种不是很赶时间的.可以自定目标.自由发挥的项目中, ...
- AVR单片机教程——旋转编码器
好久没写这个系列了.今天讲讲旋转编码器. 旋转编码器好像不是单片机玩家很常用的器件,但是我们的开发板上有,原因如下: 旋转编码器挺好用的.电位器能旋转的角度有限,旋转编码器可以无限圈旋转:旋转时不连续 ...
随机推荐
- 使用vuex来管理数据
最近一直工作比较忙,博客已经鸽了好久了,趁着今天是周末,写点东西吧 使用vuex来管理数据 最近一直在用vue做项目,但是却从来没真正去用过vuex,因为一直感觉很复杂,其实真正去研究一下啊,就会发现 ...
- H3C 端口隔离基本配置
- IDEA 创建普通的maven+java项目(入门)
配置IDEA2017,maven3.5.2 ,Java1.8 其实大部分的IDEA都差不多,不用纠结版本之前的差异. 第一步:File-->New-->Project 第二步:出现new ...
- dotnet 获取用户设备安装了哪些 .NET Framework 框架
从注册表可以拿到当前用户安装的 .NET Framework 版本,本文告诉大家如何解析这些信息 在注册表的当前设备的 SOFTWARE\Microsoft\NET Framework Setup\N ...
- quartz关闭DBUG日志
引用了Quartz组件后,打印日志时,整天都有相应的调试信息打印出来,严重影响了查找日志效率,谷歌一番后,修改nlog配置文件即可 <?xml version="1.0" e ...
- SQLite3的使用(封装很长,直接使用sqlite3_open函数,LIBS += sqlite3.dll 即可)good
1.下载sqlite3相关文件sqlite3.dll.sqlite3.h(可从http://download.csdn.net/detail/mingxia_sui/5249070下载),添加到工程的 ...
- 38.CSS
转载:https://www.cnblogs.com/yuanchenqi/articles/5977825.html css概述 CSS是Cascading Style Sheets的简称,中文称为 ...
- Batch Normalization批量归一化
BN的深度理解:https://www.cnblogs.com/guoyaohua/p/8724433.html BN: BN的意义:在激活函数之前将输入归一化到高斯分布,控制到激活函数的敏感区域,避 ...
- 百度地图addEventListener“赋值”参数
实现点击百度地图上的覆盖物,然后获取覆盖上的属性,进而实现数据传送. var pointArray=new Array();//创建一个数组存储坐标 /*在地图上标点*/ function ShowA ...
- $Poj2956/AcWing116\ The\ Pilots\ Brothers'Refrigerator$ 二进制
AcWing $Sol$ 假设改变$[x1,y1]$和$[x2,y2]$的状态就可以达到目的.注意到先改变谁对结果是没有影响的!! 所以就可以直接枚举改变状态的结点而不需要注意顺序. $4*4$的矩阵 ...