【电子电路基础实验】无源蜂鸣器

本文通过控制无源蜂鸣器模拟电影中的报警器声音,介绍了如何使用51单片机和ULN2003A驱动器进行硬件和软件设计。硬件部分涉及蜂鸣器、ULN2003A的工作原理,软件部分则探讨了多路复用器和定时器的运用,以实现不同频率和间隔的报警音效。

本文记录一段旅程–控制无源蜂鸣器嗡嗡作响。

背景

小时候,看科幻电影都是瞪大了眼睛,竖直了耳朵,生怕错过了精彩的情节。仙女座星系,宇宙飞船距某颗类地行星10千公里;南极圈,海冰和冰山在海平面上肆意分布,科考船即将抵达科考站。忽然,飞船,科考船报警器发出间断性的低鸣,各仓位笼罩在频闪的红光中,飞船即将与一颗陨星交汇,必须要尽快脱离当前轨道;科考船前方3公里的冰山海平面以下的结构会对科考船造成威胁,必须重新规划路线。舰长召集大副,领航员快速拟定应对计划,各部门紧急就位,齐心协力,化险为夷。
我决定使用手里的资源模拟报警器的运作方式。这次的主角是声音,而声源是一颗无源蜂鸣器,还是拿出手里的51单片机开发板,泡上一杯茶,开始一段新的旅行。


一、硬件驱动器

先看一看开发板原理图中关于蜂鸣器的部分。器件的驱动顺序是:单片机IO->ULN2003D->蜂鸣器。
在这里插入图片描述
在这里插入图片描述
我不知道ULN2003D是如何驱动蜂鸣器工作的,需要先了解一下ULN2003D的信息做好攻略再下手。看了一眼开发板上实际用的芯片是ULN2003A,因此查也应该查ULN2003A,不然可能因为一些细节问题抓耳挠腮自找无趣。查了一下数据手册,自我感觉有两张图比较重要。第一个是下面的芯片内部框图。
在这里插入图片描述
可以看到这芯片其实由7组相同的基本单元组成,不言而喻,另一个重要的是基本单元的描述图。
在这里插入图片描述
通过上面的信息加上一点点手册上的细节,可以产生新的两张图,一张用来描述这个蜂鸣器电路的硬件电气特性,一张用来描述它的数字特性。
请添加图片描述
我只想知道怎么使用这个芯片,所以我只关注ULN2003A的传输特性。手册中Iin最大值是25mA,意味这它不希望前端提供过大的电流把它冲烂,Iout最大值为500mA意味着当负载工作需要的电流大于500mA时,驱动器会表示它无能为力,可究竟蜂鸣器需要多大的驱动电流我也不知道,因为没找到相应的手册,只能在实际工况测试一下满不满足。

接下来是蜂鸣器模块的数字特性。芯片内部框图中特意描述的续流二极管我给省去了,因为我觉得作者的意图是想告诉我当应用于感性负载时你应该把这个续流二极管用起来,这属于电气特性。5V,TTL电平意味着我可以不加思考地将STC89C52单片机的IO口连到达林顿管的输入管脚上;建立时间限制了驱动器能跟踪输入信号的最大频率,否则输出会根据输入信号的频率与这个临界值的差值大小会产生不同程度的失真,再深究下去就要把拉普拉斯请出来了,还是不麻烦他老人家了。
请添加图片描述
无源蜂鸣器的激励信号并不是简单的逻辑1或逻辑0,而是具有一定频率的激励源,因为声音是物体机械震动产生的。但究竟这个激励源是正弦波还是方波还是什么别的稀奇古怪的东西就不得而知,遇事不决先看攻略,打开demo,先ctrl+c,ctrl+v再说。然后我就搞清楚了demo里面是用方波来驱动无源蜂鸣器的,然而具体别的波行不行,我暂时也不在乎。demo里面给的驱动信号是像下面这个样子的。
请添加图片描述
这是一个500Hz的方波信号,持续一段时间之后就停了,或许是怕一直响扰民被投诉,也或许是自己听多了都上头,总的来说考虑得还是比较周到。

二、软件驱动器

设计软件之前先重新泡一杯茶。就好像旅途中准备去下一个目的地之前总是想先找点东西填肚子喝点水才有劲一样,看着一片片茶叶在茶汤中慢慢落下总有说不出的惬意。
请添加图片描述
出发前回顾一下目的地在哪里。飞船里的报警器的声音有缓和一点的,也有急促一点的,我应该同时包含几种不同蜂鸣间隔的报警音。还有就是demo里面发出来的声音音调和我在电影里听到的也不太一样,也得试出来。所以我决定走下面图示的路线。
请添加图片描述
通过多路复用器可以选择要关闭报警还是其他什么花里胡哨的报警类型,为了让人能明显听得出蜂鸣器在响一下停一下,这些间隔时间是100ms起步的,然后复用器输出与蜂鸣器的发声驱动信号相与得到实际的蜂鸣器驱动信号,目的地就确定下来了。至于使用一个定时器Timer0作为脉冲的时基,一是为了节约定时器,二是为了方便修改时间参数。

虚拟定时器在前一篇《【电子电路基础实验】LED闪烁实验》中已经提到过了,就不说了。

虚拟逻辑多路复用器是这次要建造的新的工具。

  1. selfActionSignal是自身动作信号,顾名思义,当自己的输出发生变化时,复用器会设置这个标志来通知别的虚拟器件,至于别的器件作什么响应跟自己没有关系,反正我通知到了。
  2. currentOutput就是我自己当前的实际输出。
  3. 枚举变量linkCH用来确定currentOutput是链接到的哪个输入通道。
  4. inputChannel1-3就是复用器的3个输入端口。
  5. step函数,让复用器完整地运行一次,获取输入,更新输出。
  6. processSignal函数,看一看当前输出端口应该链接到哪个输入端口。
typedef struct logicMUX			//逻辑复用器
{
	unsigned char selfActionSignal;
	unsigned char currentOutput;
	enum
	{
		OUT_LINK_TO_CH1 = 0,
		OUT_LINK_TO_CH2,
		OUT_LINK_TO_CH3,
	}linkCH;
	unsigned char inputChannel1;
	unsigned char inputChannel2;
	unsigned char inputChannel3;
	void (*step)(struct logicMUX* multiplexer);
	void (*processSignal)(unsigned char signal, unsigned char value, struct logicMUX* multiplexer);
}logicMUX_t;

尽管c语言中有逻辑与运算“&&”,但是我还是需要将它显式地表达出来,以构造完备的虚拟逻辑器件集和。

uint8_t logic_and2Input(uint8_t a, uint8_t b)
{
	if(a && b)
		return 1;
	else
		return 0;
}

新的设备到这里就介绍完成了,接下来就可以构造整个工程的代码了。

#include "reg52.h"

#define NULL 0
//理论11.0592MHz晶振,400us溢出一次,定时器数367个数
//实际1.198us数一个数,计满400us要数334个数
#define TIMER0_MIDDLE_VALUE 334
/*********************************************************************
 * TYPEDEF
 */
typedef unsigned int uint16_t;
typedef unsigned char uint8_t;

typedef struct virtualTimer		//虚拟定时器,定时时间与使用的定时器定时间隔有关系
{
	unsigned short counter;
	unsigned short reloadTime;
	unsigned short reloadFlag;
}virtualTimer_t;

typedef struct logicMUX			//逻辑复用器
{
	unsigned char selfActionSignal;
	unsigned char currentOutput;
	enum
	{
		OUT_LINK_TO_CH1 = 0,
		OUT_LINK_TO_CH2,
		OUT_LINK_TO_CH3,
	}linkCH;
	unsigned char inputChannel1;
	unsigned char inputChannel2;
	unsigned char inputChannel3;
	void (*step)(struct logicMUX* multiplexer);
	void (*processSignal)(unsigned char signal, unsigned char value, struct logicMUX* multiplexer);
}logicMUX_t;
/*********************************************************************
 * VIRTUAL DEVICE
 */
//virtual timers
#define BEEP_BLINK_PERIOD1    1000    //1000*400us=400000us = 400ms
#define BEEP_BLINK_PERIOD2    3000    //3000*400us=1200000us=1.2s
#define BEEP_DRIVE_PERIOD     10      //value*0.4ms = 4ms -> 驱动频率的倒数

sbit BEEP=P2^5;	//将P2.5管脚定义为BEEP
uint8_t beepDriveSignal = 0;
uint8_t beepBlinkSignal1 = 0;
uint8_t beepBlinkSignal2 = 0;
virtualTimer_t beepDriveTimer = {BEEP_DRIVE_PERIOD,BEEP_DRIVE_PERIOD,1};
virtualTimer_t beepBlinkTimer1 = {BEEP_BLINK_PERIOD1,BEEP_BLINK_PERIOD1,1};
virtualTimer_t beepBlinkTimer2 = {BEEP_BLINK_PERIOD2,BEEP_BLINK_PERIOD2,1};

//virtual logic MUX
void beepSignalMUXStep(struct logicMUX* multiplexer);
void beepSignalMUXStepprocessSignal(unsigned char signal, unsigned char value, struct logicMUX* multiplexer);
logicMUX_t beepSignalMUX = 
{
	0,  //selfActionSignal
	0,  //currentOutput
	OUT_LINK_TO_CH2,
	0,0,0,    //input signal
	beepSignalMUXStep,
	beepSignalMUXStepprocessSignal
};

//logic and
uint8_t logic_and2Input(uint8_t a, uint8_t b);

/*********************************************************************
 * FUNCTION BODY
 */
//function entry
void main(){	
	TMOD = 0x01;        //T0方式1->16位不自动重装定时器
    TMOD &= ~(1<<2);    //T0定时器模式
    TMOD &= ~(1<<3);    //T0启停仅受TCON的TR0控制
    TH0 = (65535 - TIMER0_MIDDLE_VALUE) / 256;    
    TL0 = (65535 - TIMER0_MIDDLE_VALUE) % 256;
    ET0 = 1;            //T0中断使能
    EA = 1;             //总中断使能
    TR0 = 1;            //T0使能
	while(1){                       	
	}		
}

//sub function for virtual logic MUX
void beepSignalMUXStep(struct logicMUX* multiplexer){
	switch((uint8_t)(multiplexer->linkCH)){
		case (uint8_t)OUT_LINK_TO_CH1:
			multiplexer->currentOutput = multiplexer->inputChannel1;
			break;
		case (uint8_t)OUT_LINK_TO_CH2:
			multiplexer->currentOutput = multiplexer->inputChannel2;
			break;
		case (uint8_t)OUT_LINK_TO_CH3:
			multiplexer->currentOutput = multiplexer->inputChannel3;
			break;
		default:
			break;
	}
	multiplexer->selfActionSignal = 1;
}

void beepSignalMUXStepprocessSignal(unsigned char signal, unsigned char value, struct logicMUX* multiplexer){
	if(signal){
		switch(value){
			case 0:
				multiplexer->linkCH = OUT_LINK_TO_CH1;
				break;
			case 1:
				multiplexer->linkCH = OUT_LINK_TO_CH2;
				break;
			case 2:
				multiplexer->linkCH = OUT_LINK_TO_CH3;
				break;
			default:
				break;
		}
	}
}

//logic and
uint8_t logic_and2Input(uint8_t a, uint8_t b){
	if(a && b)
		return 1;
	else
		return 0;
}

//时基, 400us中断一次
void T0_interrupt(void) interrupt 1 {   //至多211.58us
    TH0 = (65535 - TIMER0_MIDDLE_VALUE) / 256;   //重装初值
    TL0 = (65535 - TIMER0_MIDDLE_VALUE) % 256;

    if(beepDriveTimer.counter != 0)
		--beepDriveTimer.counter;
    if(beepBlinkTimer1.counter != 0)
		--beepBlinkTimer1.counter;
    if(beepBlinkTimer2.counter != 0)
		--beepBlinkTimer2.counter;
    //更新需要重载的虚拟定时器
    if(!beepDriveTimer.counter && beepDriveTimer.reloadFlag)
	    beepDriveTimer.counter = beepDriveTimer.reloadTime;
    if(!beepBlinkTimer1.counter && beepBlinkTimer1.reloadFlag)
	    beepBlinkTimer1.counter = beepBlinkTimer1.reloadTime;
    if(!beepBlinkTimer2.counter && beepBlinkTimer2.reloadFlag)
	    beepBlinkTimer2.counter = beepBlinkTimer2.reloadTime;
    //产生信号源
    if(beepDriveTimer.counter > BEEP_DRIVE_PERIOD / 2)       //蜂鸣器驱动信号
        beepDriveSignal = 1;
    else
        beepDriveSignal = 0;
    if(beepBlinkTimer1.counter > (BEEP_BLINK_PERIOD1 / 2))   //蜂鸣器闪烁信号1
        beepBlinkSignal1 = 1;
    else
        beepBlinkSignal1 = 0; 
    if(beepBlinkTimer2.counter > BEEP_BLINK_PERIOD2 / 3)    //蜂鸣器闪烁信号2
        beepBlinkSignal2 = 1;
    else
        beepBlinkSignal2 = 0;
    //逻辑复用器工作
    //beepSignalMUX.processSignal(0,0,&beepSignalMUX);      //复用器截获通道选择信号并修改通道值
    beepSignalMUX.inputChannel1 = 0;                      //复用器抽取输入
    beepSignalMUX.inputChannel2 = beepBlinkSignal1;
    beepSignalMUX.inputChannel3 = beepBlinkSignal2;
    beepSignalMUX.step(&beepSignalMUX);                 //复用器运行一次,更新输出  
    //与非门工作,更新蜂鸣器驱动信号
    BEEP = logic_and2Input(beepSignalMUX.currentOutput,beepDriveSignal);
    //回收各逻辑器件的动作信号
    beepSignalMUX.selfActionSignal = 0;    	
}

至于定时器0为什么要设置成400us中断一次,为什么蜂鸣器的驱动频率是250Hz,为什么设置400ms和1.2s的闪烁周期,为什么要把所有的函数都写在中断函数里面,这样会不会出问题,我是怎么知道时钟频率有误差的,且听我娓娓道来。

  1. 定时器0为什么要设置成400us中断一次?
    在构造程序的时候先随便设置一个中断时间,比如100us,当然这个间隔要足以为系统中最高频率的虚拟定时器服务,比如我认为我的最高频率有可能到500Hz,2ms周期,那么100us=0.1ms的中断间隔凭第六感感觉就行,实际行不行还得实际试。
    设置一个初步的中断时间之后就暂时不管了,编辑完所有的其他部分的代码,最后来看看中断服务程序里面的代码段全部执行一轮需要多长时间,比如上面的定时器0的中断服务程序跑完最多(确保所有最长的条件分支得到执行)需要花费211.58us,那么在这种情况下,100us中断一次就显得不合适,毕竟上一次中断服务还没结束这一次中断就来了显得有些愚蠢,至少应当保证中断间隔大于等于中断服务程序的最大时间,但是如果两个时间相等,那么主程序就永远得不到执行了,毕竟一直在被打断,所以还是要留出一点冗余时间让cpu干点主循环的活。
    折个衷,就定了个400us,因为300us不容易算出整数,这让我很恼火。
    下面是上面的代码的主循环和中断对cpu占用的时间流图。这里又引出了另一个值得探讨的问题,假如说生产者会在定时器中断函数中会获取一些信息,而主循环中有某个消费者中会使用这些信息,假设在下图的条件下主循环跑完一轮需要1s,那么主循环会在发生若干次中断后才能完整地执行一轮,出现供过于求的情况,为了维系稳定,生产者把获取的信息扔掉那消费者获取的信息就会脱节,最后冒出各种各样的问题。显然时间协调对于构造系统来说很重要,需要刻意地去处理,搞一刀切坚决不在中断中处理过多的东西就高枕无忧的行为绝不可取。
    请添加图片描述
  1. 为什么蜂鸣器的驱动频率是250Hz,为什么设置400ms和1.2s的蜂鸣器闪烁周期?
    因为我试了一下感觉这样和电影里面比较像,感觉可以就可以。
  1. 为什么要把所有的处理都写在中断函数里面,这样会不会出问题?
    首先这样做在这个工程中不会出问题,在第一个问题中有所讨论。但是不同的工程情况不同,当有中断嵌套的时候事情就没那么容易把握了,目前我还没找到控制这种复杂度的方法,坚持下去肯定会找到的。
    所有的处理语句都写在中断函数中是因为定时器打断主循环的时机是任意的,更新驱动输出的代码放在主循环中会造成蜂鸣器的驱动信号更新时间不确定,产生下面图示的驱动频率不稳定的现象。
    请添加图片描述
    观察下面的伪代码,更新蜂鸣器的驱动信号是在主循环的step3进行的,假设定时器中断运行完之后,下一次中断来临之前主循环能跑完两轮,以确保定时器更新以后主循环一定会紧接着更新输出。定时器在驱动信号刚好更新输出结束的语句和驱动信号即将更新前的语句两个不同的位置打断主循环,两个位置更新输出会产生step4+step1+step2的运行时差,如果时差足够短,蜂鸣器的驱动频率就是稳定的,耳朵听不出来蜂鸣器抖动,否则耳朵会告诉你你该修改程序了。
void main(void){
	//初始化
	while(1){
		//step1:产生信号源
		//step2:逻辑复用器工作
		//step3:与非门工作,更新蜂鸣器驱动信号
		//step4:回收各逻辑器件的动作信号
	}
}
void T0_interrupt(void) interrupt 1 { 
	//重载定时器
	//虚拟定时器的计数器自减
	//更新需要重载的虚拟定时器
}
  1. 我怎么知道时钟不准。
    按精准的11.0592MHz晶振计算,定时器数一个数要花1.09us的时间,计满400us大约数367个数。那么要计满100ms就需要计400us时间单位250次,可实际情况并非如此。计满250次实际延时了110ms,则定时器440us溢出一次,反推回去定时器应该是1.198us数一个数,说明时钟不准。
    请添加图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值