本文隶属于AVR单片机教程系列。

 

引子

定时/计数器(简称定时器)是单片机编程中至关重要的一部分,再简单的单片机也会带有定时器。

也许你会觉得我们已经在delay函数中接触过定时器了,然而并不是,它只是软件地通过“浪费时间”来实现延时。我们接触定时器在数码管中,segment_auto函数可以自动完成动态扫描,好像在main函数背后又开了一个线程,两者并行执行一样。这就用到了定时器中断。

中断是一种必要的程序流程控制方法,但这两讲我们先聚焦于利用定时器来输出波形。

本讲中,我们用定时器来输出一定频率的方波,让蜂鸣器发出声音。

定时/计数器

ATmega324PA提供了3个定时器:定时器0、定时器1、定时器2。其中,定时器0和2都是8位的,定时器1是16位的;定时器1支持输入捕获;定时器2有异步支持,即可以独立于CPU时钟工作。为了简单起见,本讲以定时器0为例。

定时器0有一个计数寄存器TCNT0,由CPU时钟的可配置分频驱动,每一定时器时钟周期增加1。

定时器0有4种工作模式:普通模式、CTC模式,还有两种放到下一讲。CTC模式下可以输出波形,后两种模式也有对应的波形。波形可以输出到引脚PB3和PB4上。定时器时钟、工作模式与波形输出在寄存器TCCR0ATCCR0B中配置。

在普通模式中,TCNT0持续增加,在值为255时再加1会溢出变成0,因此以256个定时器时钟周期为循环周期。这种模式一般用于产生定时器中断。

在CTC模式中,TCNT0增加到寄存器OCR0A的值时,发生比较匹配,此时TCNT0会被硬件清零,引脚电平可以被翻转、置低或置高。如果配置为翻转,则每匹配两次,引脚输出一个方波,而每次匹配需要OCR0A值+1个周期,所以输出方波的频率为:\(f_{OC0A} = \frac {f_{clk\_I/O}} {2 \cdot N \cdot (1 + OCR0A)}\),其中,\(f_{clk\_I/O}\)是外设IO时钟,频率与CPU时钟相同;\(N\)表示分频系数,对于定时器0,可以是1、8、64、256或1024。

以上是对数据手册部分信息的不完全概括。请参阅数据手册第15章,以完成作业题。

分频系数与OCR0A的值应该根据想要的波形频率来计算。首先,选择分频系数的原则是,在可选的值中选择最小的。最小的分频系数1往往是不能选的,因为计算下来OCR0A的值会超过其可接受的最大值255(开发板上单片机的CPU频率是25MHz);如果分频系数过大,OCR0A的值会比较小,由于计算出的通常是小数而实际只能取整数,较小的数会产生较大的误差。

比如,为了输出1kHz的方波,先计算最小的分频系数:\(N_{min} = \frac {f_{CPU}} {2 \cdot (1 + OCR0A_{max}) \cdot f_{OC0A}} = \frac {25000000} {2 \cdot 256 \cdot 1000} = 48.83\),因此分频系数应取64。再根计算OCR0A的值:\(OCR0A = \frac {f_{CPU}} {2 \cdot N \cdot f_{OC0A}} - 1 = \frac {25000000} {2 \cdot 64 \cdot 1000} - 1 = 194.31\),所以取OCR0A194。不妨再计算一下实际波形频率:\(f_{OC0A} = \frac {f_{CPU}} {2 \cdot N \cdot (1 + OCR0A)} = \frac {25000000} {2 \cdot 64 \cdot (1 + 194)} = 1001.6Hz\),只比预期的差3个音分,相当精确。

开发板上一共有4个可以输出波形的引脚,分别是引脚4~7,在库中被定义为WAVE_0WAVE_3。要输出波形,必须先调用wave_mode以指定输出何种波形,然后再调用tone_set输出一定频率的方波。

蜂鸣器

蜂鸣器有有源与无源两种,“源”指的是振荡源。有源蜂鸣器给一定电压就可以发出一定频率的声音,但不能改变;无源蜂鸣器需要方波才能发声,声音的频率与方波的相同,这是可以控制的。开发板上的是压电式无源蜂鸣器,两极都接出来了,所以可以同时发出两个频率的声音。如果只需要一个,一般把负极接地,正极接单片机引脚。

到这里你应该暂停一下,试着用tone_set函数使蜂鸣器发出523Hz的声音。

假设你已经实现了。程序很短吧?你也许会想当然地认为用tone_set函数控制蜂鸣器已经足够方便了,但实践证明不是的。试试这段代码:

#include <ee1/delay.h>
#include <ee1/button.h>
#include <ee1/wave.h>
#include <ee1/tone.h> int main()
{
button_init(PIN_NULL, PIN_NULL);
wave_mode(WAVE_0, WAVE_MODE_TONE);
tone_set(WAVE_0, 523);
delay(1000);
while (1)
{
if (button_down(BUTTON_0))
tone_set(WAVE_0, 523);
else
tone_set(WAVE_0, 0);
delay(10);
}
}

在程序开始时,你会听到一声清脆的Do,但是之后按键按下时,蜂鸣器的声音却没那么纯粹了。这是因为,每次调用tone_set时,波形都会从新的周期开始,而原来的周期可能只进行到一半,就使波形不是很完美——可别小看这半个周期,你不是听到这明显的噪音了吗?

buzzer_tone函数作为进一步的封装,在设计上避免了这个问题。它把蜂鸣器正在播放的频率保存起来,如果调用时参数与上次的相同,则不进行任何操作。

我们来实现播放复音的功能。

#include <ee1/delay.h>
#include <ee1/button.h>
#include <ee1/switch.h>
#include <ee1/buzzer.h> int main()
{
button_init(PIN_2, PIN_3);
switch_init(PIN_NULL, PIN_NULL);
buzzer_init(WAVE_0, WAVE_1);
uint16_t freq[] = {262, 330, 392, 523};
while (1)
{
if (switch_status(SWITCH_0))
{
uint16_t temp[2] = {0};
uint16_t* ptr = temp;
for (uint8_t i = BUTTON_COUNT; i-- && ptr != temp + 2;)
if (button_down(i))
*ptr++ = freq[i];
buzzer_tone(temp[0], temp[1]);
}
else
buzzer_tone(0, 0);
delay(40);
}
}

虽然蜂鸣器的声音本来就比较刺耳,但和声还是挺和谐的吧。不信?试试349和494,然后你就会觉得上面这个程序效果其实挺不错的。

作业

  1. 当定时器在引脚上输出波形时,原来的PORTDDR寄存器还有用吗?

  2. 阅读数据手册,使用寄存器,输出440Hz的方波。

  3. 用旋转编码器控制蜂鸣器,发出音阶中的音符。你可以用计算器或Excel计算好音符频率,然后直接写在程序中。

AVR单片机教程——蜂鸣器的更多相关文章

  1. AVR单片机教程——定时器中断

    本文隶属于AVR单片机教程系列.   中断,是单片机的精华. 中断基础 当一个事件发生时,CPU会停止当前执行的代码,转而处理这个事件,这就是一个中断.触发中断的事件成为中断源,处理事件的函数称为中断 ...

  2. AVR单片机教程——LCD1602

    本文隶属于AVR单片机教程系列.   显示屏 开发板套件里有两块屏幕,大的是LCD(液晶显示),小的是OLED(有机发光二极管).正与你所想的相反,短小精悍的比较贵,而本讲的主题--LCD1602-- ...

  3. AVR单片机教程——DAC

    本文隶属于AVR单片机教程系列.   单片机的应用场景时常涉及到模拟信号.我们已经会使用ADC把模拟信号转换成数字信号,本讲中我们要学习使用DAC把数字信号转换成模拟信号.我们还将搭建一个简单的功率放 ...

  4. AVR单片机教程——旋转编码器

    好久没写这个系列了.今天讲讲旋转编码器. 旋转编码器好像不是单片机玩家很常用的器件,但是我们的开发板上有,原因如下: 旋转编码器挺好用的.电位器能旋转的角度有限,旋转编码器可以无限圈旋转:旋转时不连续 ...

  5. AVR单片机教程——数码管

    先解答之前一个思考题:如果不把引脚配置为输出而写高电平,连接LED会怎样? 实验结果是,LED会亮,但相比于输出高电平的情况,亮度很低.这是为什么呢? 通过上一篇教程我们知道,引脚输入输出模式是由寄存 ...

  6. AVR单片机教程——数字输出

    从上一篇教程中我们了解到,按键与开关的输入本质上就是数字信号的读取.这一篇教程要讲的是,控制LED的原理是数字信号的输出.数字IO是单片机编程之有别于桌面编程的各项内容中最简单.最基础的. 在讲数字信 ...

  7. AVR单片机教程——数字输入

    我们已经学习了如何使用按键和拨动开关,不知你有没有好奇 button_down 和 switch_status 等函数是如何实现的.本篇教程带你一探究竟,让我们从按键的原理开始. 在原理图中,按键的符 ...

  8. AVR单片机教程——拨动开关

    在按键的上方有4个拨动开关.开关与按键,在原理和使用方法上都是很类似的,但有不同的用途——按键按下后松开就会弹起,而开关可以保存其状态. <switch.h> 定义了与开关相关的函数.sw ...

  9. AVR单片机教程——按键动作

    上一篇教程中我们学习了如何读取按键状态.而按键的动作,比如单击,至少需要两个状态才能判定,长按.双击的判定更加复杂.今天我们来学习如何使用库函数判断按键单击,以及其实现原理. 我们要实现的是:当一个按 ...

随机推荐

  1. H3C 环路避免机制一:路由毒化

  2. Vue 子组件传父组件

    vue中的传值是个很烦的问题,记录一下自己完成的这个需求. 首先,被引用的称之为子组件,当前页面为父组件,这个不能搞错. 子组件传值,要用到this.$emit. 子组件页面,需要在一个函数中使用th ...

  3. P1072 城市轰炸

    题目描述 一个大小为N*M的城市遭到了X次轰炸,每次都炸了一个每条边都与边界平行的矩形. 在轰炸后,有Y个关键点,指挥官想知道,它们有没有受到过轰炸,如果有,被炸了几次,最后一次是第几轮. 输入格式 ...

  4. 【2016常州一中夏令营Day7】

    序列(sequence)[题目描述]蛤布斯有一个序列,初始为空.它依次将 1-n 插入序列,其中 i插到当前第 ai 个数的右边 (ai=0 表示插到序列最左边).它希望你帮它求出最终序列.[输入数据 ...

  5. JMeter JMS测试计划

    在本节中,我们将学习如何编写一个简单的测试计划来测试Java Messaging Service(JMS). 出于测试目的,我们使用Apache ActiveMQ.有各种JMS服务器,如:glassf ...

  6. SpringBoot如何优雅的使用RocketMQ

    目录 SpringBoot如何优雅的使用RocketMQ SpringBoot如何优雅的使用RocketMQ MQ,是一种跨进程的通信机制,用于上下游传递消息.在传统的互联网架构中通常使用MQ来对上下 ...

  7. 叶子的颜色---经典树上dp

    挺简单的一个dp #include<iostream> #include<cstring> #include<cstdio> #include<algorit ...

  8. c++ 屏幕截图指定窗口句柄后台截图返回位图句柄

    /根据窗口句柄后台截图保存成BMP位图文件并且显示到picture 控件上 void GetScreenBmp(HWND hwnd, int left, int top, int width, int ...

  9. 洛谷$P1345\ [USACO5.4]$ 奶牛的电信$Telecowmunication$ 网络流

    正解:最小割 解题报告: 传送门$QwQ$ $QwQ$好久没做网络流了来复健下. 这个一看就很最小割趴?考虑咋建图?就把点拆成边权为$1$的边,然后原有的边因为不能割所以边权为$inf$. 然后跑个最 ...

  10. 使用宝塔搭建nextcloud的过程(搭建、优化、问题)

    宝塔部署教程 参考网址: 使用NextCloud来搭建我们的私有网盘.并结合Redis优化性能(宝塔) https://www.moerats.com/archives/175/ 宝塔面板下nextc ...