锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

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

时间:2023-04-11 05:07:00 330ma电感器970ma电感器790ma电感器490ma电感器

文章目录

  • 背景
  • 一、硬件驱动器
  • 二、软件驱动器

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

背景

当我还是个孩子的时候,看科幻电影时,我睁大了眼睛,竖直了耳朵,生怕错过了精彩的情节。仙女星系,宇宙飞船距某类地行星1000公里;南极圈,海冰冰山在海平面上肆意分布,科研船即将到达科研站。突然,飞船和科研船报警器发出间歇性低鸣,每个位置都笼罩在闪闪发光的红光中。飞船即将与陨星相遇,必须尽快脱离当前轨道;科研船前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是复用器的三个输入端口。
  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数一个数,说明时钟不准。
锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章