大家好,我是潮打空城,前几天本人所在的大学在临近期末考试的时候出其意外的布置了一道频率计设计的题目。这个题目拿到手之后,我初步判断要用到单片机,如果用其他种类的单片机,都不可避免的需要进行程序控制字的编程,着实需要对照手册好好钻研一番,然而在考试周这种比较特殊的情况下,为了投机取巧,只好选用了提供了大量的库函数的Arduino平台进行频率计的设计。
题目的要求如下:

针对这种要求我设计了这样的解决方案:

从框图能够看出来,核心的实现功能的部分由单片机负责,所有的硬件电路都是给单片机服务,提供方便。
所以从出题角度来说,这个设计的核心在软件上,而不在硬件上,所以我还是先介绍软件的部分。
软件/逻辑部分的框图:

由于该设计功能比较单一,用比较简单的流程图就可以将逻辑描述清楚。对于这种较为简单的功能,实际上也可以尝试做一个纯模拟或者模拟+数字的系统进行实现,如果要动用单片机,一定要加入单片机比较优势的部分功能。但是本人比较懒,用Arduino开发可以省很多事,所以就不想那么多了。
下面是具体实现所用的代码:
1 void InputVoltageMeasurement() //测量输入电压,并发送至串口 2 { 3 int SingleCycleSamplingPoints; //采样点数 4 if(Frequency<=100) //保证在频率范围内都能够采到至少100个点 5 SingleCycleSamplingPoints=1/(SamplingTime_s*Frequency); //根据频率智能计算采样点数 6 else 7 SingleCycleSamplingPoints=100/(SamplingTime_s*Frequency); 8 float Peak=(((analogRead(InputVoltagePin))/1023.0)*5.0)/InputPartialRatio; //在众多的采样点中找到峰值 9 for(int p=1;p<SingleCycleSamplingPoints;p++) 10 { 11 float CurrentValue=(((analogRead(InputVoltagePin))/1023.0)*5.0)/InputPartialRatio; 12 if(CurrentValue>Peak) 13 Peak=CurrentValue; 14 } 15 InputVoltage=Peak; //认为输入电压就是峰值电压 16 Serial.println(); //串口输出 17 Serial.print("InputVoltage="); 18 Serial.print(InputVoltage); 19 Serial.println("V"); 20 }
输入信号并不是直流信号,讲道理如果要用单片机直接测量需要用到峰值检波电路,本人曾经焊过一个,可以实现结果,值得注意的一点就是对于低频一定要加滤波,但是在后期进行硬件联调的时候,发现这个模块一和其它模块级联就工作不正常,最后作罢,索性纯粹用软件进行峰值检波。
这段代码的核心函数是Arduino官方库中的analogRead()函数,以此作为单片机和外界联系的桥梁。
用软件进行峰值检波的思路是这样:考虑一个交流信号一个周期的情况,在其中采大量的点,如果点的数量比较多,那么就很有可能能够采到一个周期中的峰值,这样就实现了交流信号的峰值检波。
1 void FrequencyDutyMeasurement() //测量输入信号的频率、占空比,并将测量值发送到串口 2 { 3 volatile int i=digitalRead(SquareWave); //先看现在是高电平还是低电平 4 while(1) 5 { 6 if(i!=digitalRead(SquareWave)) //发现第一次跳变 7 { 8 i=digitalRead(SquareWave); 9 CurrentTime=micros(); //记录第一次跳变的时间 10 goto S1; 11 } 12 } 13 S1:FirstTime=CurrentTime; 14 int m=1; //存放跳变的次数,次数越多精度越高,测量时间也就会越长 15 if(Shift==1) //分档处理,低频的时候点取少,高频的时候点取多 16 { 17 while(1) 18 { 19 if(i!=digitalRead(SquareWave)) 20 { 21 m++; 22 i=digitalRead(SquareWave); 23 if(m==200) 24 { 25 CurrentTime=micros(); //记录200次跳变的时间 26 goto S2; 27 } 28 } 29 } 30 S2:SecondTime=CurrentTime; 31 Frequency=100000000/(SecondTime-FirstTime); //计算频率 32 } 33 if(Shift==2) 34 { 35 while(1) 36 { 37 if(i!=digitalRead(SquareWave)) 38 { 39 m++; 40 i=digitalRead(SquareWave); 41 if(m==1000) 42 { 43 CurrentTime=micros(); //记录1000次跳变的时间 44 goto S3; 45 } 46 } 47 } 48 S3:SecondTime=CurrentTime; 49 Frequency=500000000/(SecondTime-FirstTime); //计算频率 50 } 51 Serial.println(); //将频率值发送至串口 52 Serial.print("The frequency is "); 53 Serial.print(Frequency); 54 Serial.println("HZ"); 55 if(Frequency<=1000) //根据测得的频率智能换挡 56 Shift=1; 57 else 58 Shift=2; 59 60 volatile int j=digitalRead(SquareWave); //先看现在是高电平还是低电平 61 while(1) 62 { 63 if(j!=digitalRead(SquareWave)) //发现第一次跳变 64 { 65 j=digitalRead(SquareWave); 66 CurrentTime=micros(); //记录第一次跳变的时间 67 goto S4; 68 } 69 } 70 S4:FirstTime=CurrentTime*1.0; 71 int n=0; 72 while(1) 73 { 74 if(j!=digitalRead(SquareWave)) 75 { 76 n++; 77 j=digitalRead(SquareWave); 78 if(n==99) 79 { 80 CurrentTime=micros(); 81 Time_99=CurrentTime-FirstTime; //记录99次跳变的时间 82 } 83 if(n==199) 84 { 85 CurrentTime=micros(); 86 Time_199=CurrentTime-FirstTime; //记录199次跳变的时间 87 goto S5; 88 } 89 } 90 } 91 S5: 92 if(j) //根据第一次测的电平值来计算占空比 93 DutyRatio=((Time_199-2*Time_99)*Frequency)/10000.0; 94 else 95 DutyRatio=100-((Time_199-2*Time_99)*Frequency)/10000.0; 96 Serial.print("The duty ratio is "); //串口输出占空比 97 Serial.print(DutyRatio); 98 Serial.println("%"); 99 }
经过了硬件的整形,这时所有的输入信号都被整成了振幅为5V的方波信号,所以就可以被单片机测量频率。
这里所用的核心的函数是Arduino官方库里面的时间函数micros(),这个函数可以返回Arduino开机的时间。
用这个函数进行频率和占空比的测量是这样:这个函数可以认为是提供了表的作用,也就是我们都有的钟点的概念。众所周知,单片机可以检测上下沿电平的变化,而我们通过micros()函数也可以知道发生电平跳转时的时刻,考虑一个单周期的情况,如果发生了两个电平跳变,对方波来说,实际上这两个跳变的时间差就等于周期的一半的时间,所以我们就能够知道周期的长短,知道了周期,就能够通过简单的数学变换得到频率。而占空比,可以测量三次电平跳变,其中总有一段时间是高电平持续的时间,只要知道高电平持续的时间以及周期,根据占空比的定义就可以算出占空比。
理论上是这样,实际上还是这样吗?未必,通过这种思路写的代码,也许能够测量出一个值,但是这个值本身就不稳定,无法说明问题。出现这种现象的原因就是对于频率比较高的信号,测量单周期的时间对单片机来说太难了,解决办法就是,把很多的周期的时间加到一起进行计算,这样单片机就能够反应的过来。
这种思路进行测量会导致一个比较明显的缺陷,那就是输入信号越快,那单片机也得测得快,这样一来,频率一高,输出的东西太快,人眼会看不清,这就是一种缺陷。
1 void OverVoltageAlarm() //过压报警 2 { 3 tone(Buzzer,1000); //蜂鸣器报警 4 delay(1000); //延时1s 5 noTone(Buzzer); //蜂鸣器停止报警 6 for(int k=0;k<3;k++) //屏幕显示三次过压警告 7 { 8 Serial.println("OVER VOLTAGE!!!"); 9 } 10 }
这段代码相对来说就比较简单了,核心函数是Arduino官方扩展库的tone()和noTone()函数,这两个函数是专门用来输出蜂鸣器驱动信号的。
整体代码如下:
1 /* 2 代码名称:FrequencyMeter 3 代码功能:实现对输入信号的电压、频率及占空比的测量。 4 作者:马彭康 5 时间:2016 6 25 6 版本:4.0 7 作者邮箱:1710945152@qq.com 8 */ 9 10 /***********************声明功能函数********************/ 11 12 void InputVoltageMeasurement(); //测量输入电压,并发送至串口 13 void FrequencyDutyMeasurement(); //测量输入信号的频率、占空比,并将测量值发送到串口 14 void OverVoltageAlarm(); //过压报警 15 /*********************************************************/ 16 17 /************************定义引脚************************/ 18 19 #define InputVoltagePin A0 20 #define SquareWave 2 21 #define Buzzer 3 22 23 /*********************************************************/ 24 25 /***********************定义变量*************************/ 26 27 float InputVoltage; 28 volatile unsigned long CurrentTime; //当前时间 29 volatile unsigned long FirstTime; //第一次跳变的时间 30 volatile unsigned long SecondTime; //第二次跳变的时间 31 unsigned long Frequency; 32 unsigned long DutyRatio; //占空比 33 volatile unsigned long Time_99; //计数99次跳变的时间 34 volatile unsigned long Time_199; //计数199次跳变的时间 35 int Shift; //测量档位 36 37 /*********************************************************/ 38 39 /***********************定义常量*************************/ 40 41 #define InputPartialRatio (0.3429) //定义输入分压比 42 #define SamplingTime_s (0.0001) //ADC采样一次所花费的时间 43 44 /*********************************************************/ 45 46 void setup() 47 { 48 pinMode(SquareWave,INPUT); 49 Serial.begin(9600); 50 Serial.println("This is a frequency meter!"); 51 Shift=1; //默认档位是1挡 52 Frequency=20; //默认频率是20HZ 53 } 54 55 void loop() 56 { 57 InputVoltageMeasurement(); 58 if(InputVoltage<=7.5) 59 FrequencyDutyMeasurement(); 60 else 61 OverVoltageAlarm(); 62 } 63 64 void InputVoltageMeasurement() //测量输入电压,并发送至串口 65 { 66 int SingleCycleSamplingPoints; //采样点数 67 if(Frequency<=100) //保证在频率范围内都能够采到至少100个点 68 SingleCycleSamplingPoints=1/(SamplingTime_s*Frequency); //根据频率智能计算采样点数 69 else 70 SingleCycleSamplingPoints=100/(SamplingTime_s*Frequency); 71 float Peak=(((analogRead(InputVoltagePin))/1023.0)*5.0)/InputPartialRatio; //在众多的采样点中找到峰值 72 for(int p=1;p<SingleCycleSamplingPoints;p++) 73 { 74 float CurrentValue=(((analogRead(InputVoltagePin))/1023.0)*5.0)/InputPartialRatio; 75 if(CurrentValue>Peak) 76 Peak=CurrentValue; 77 } 78 InputVoltage=Peak; //认为输入电压就是峰值电压 79 Serial.println(); //串口输出 80 Serial.print("InputVoltage="); 81 Serial.print(InputVoltage); 82 Serial.println("V"); 83 } 84 85 void FrequencyDutyMeasurement() //测量输入信号的频率、占空比,并将测量值发送到串口 86 { 87 volatile int i=digitalRead(SquareWave); //先看现在是高电平还是低电平 88 while(1) 89 { 90 if(i!=digitalRead(SquareWave)) //发现第一次跳变 91 { 92 i=digitalRead(SquareWave); 93 CurrentTime=micros(); //记录第一次跳变的时间 94 goto S1; 95 } 96 } 97 S1:FirstTime=CurrentTime; 98 int m=1; //存放跳变的次数,次数越多精度越高,测量时间也就会越长 99 if(Shift==1) //分档处理,低频的时候点取少,高频的时候点取多 100 { 101 while(1) 102 { 103 if(i!=digitalRead(SquareWave)) 104 { 105 m++; 106 i=digitalRead(SquareWave); 107 if(m==200) 108 { 109 CurrentTime=micros(); //记录200次跳变的时间 110 goto S2; 111 } 112 } 113 } 114 S2:SecondTime=CurrentTime; 115 Frequency=100000000/(SecondTime-FirstTime); //计算频率 116 } 117 if(Shift==2) 118 { 119 while(1) 120 { 121 if(i!=digitalRead(SquareWave)) 122 { 123 m++; 124 i=digitalRead(SquareWave); 125 if(m==1000) 126 { 127 CurrentTime=micros(); //记录1000次跳变的时间 128 goto S3; 129 } 130 } 131 } 132 S3:SecondTime=CurrentTime; 133 Frequency=500000000/(SecondTime-FirstTime); //计算频率 134 } 135 Serial.println(); //将频率值发送至串口 136 Serial.print("The frequency is "); 137 Serial.print(Frequency); 138 Serial.println("HZ"); 139 if(Frequency<=1000) //根据测得的频率智能换挡 140 Shift=1; 141 else 142 Shift=2; 143 144 volatile int j=digitalRead(SquareWave); //先看现在是高电平还是低电平 145 while(1) 146 { 147 if(j!=digitalRead(SquareWave)) //发现第一次跳变 148 { 149 j=digitalRead(SquareWave); 150 CurrentTime=micros(); //记录第一次跳变的时间 151 goto S4; 152 } 153 } 154 S4:FirstTime=CurrentTime*1.0; 155 int n=0; 156 while(1) 157 { 158 if(j!=digitalRead(SquareWave)) 159 { 160 n++; 161 j=digitalRead(SquareWave); 162 if(n==99) 163 { 164 CurrentTime=micros(); 165 Time_99=CurrentTime-FirstTime; //记录99次跳变的时间 166 } 167 if(n==199) 168 { 169 CurrentTime=micros(); 170 Time_199=CurrentTime-FirstTime; //记录199次跳变的时间 171 goto S5; 172 } 173 } 174 } 175 S5: 176 if(j) //根据第一次测的电平值来计算占空比 177 DutyRatio=((Time_199-2*Time_99)*Frequency)/10000.0; 178 else 179 DutyRatio=100-((Time_199-2*Time_99)*Frequency)/10000.0; 180 Serial.print("The duty ratio is "); //串口输出占空比 181 Serial.print(DutyRatio); 182 Serial.println("%"); 183 } 184 185 void OverVoltageAlarm() //过压报警 186 { 187 tone(Buzzer,1000); //蜂鸣器报警 188 delay(1000); //延时1s 189 noTone(Buzzer); //蜂鸣器停止报警 190 for(int k=0;k<3;k++) //屏幕显示三次过压警告 191 { 192 Serial.println("OVER VOLTAGE!!!"); 193 } 194 }
硬件电路相对来说是比较简单的。信号整形电路如下:

该电路有一个前置的滤波器,通过迟滞比较器将不同振幅和不同波形的信号整成统一振幅的方波信号。
输入信号分压电路如下:

通过比较简单的电阻串联关系进行分压,后面接了一个跟随器和单片机引脚进行阻抗匹配。
报警电路是在网上找的,蜂鸣器用的是大家都比较喜闻乐见的三极管驱动方式:

基本没什么可说的。
最后的实物电路部分如下:

最终的实物测试结果如下图:



实践表明该设计可以完成要求的指标,大家可以再加一些其他的功能,可以写一些简单的函数来解决很多系统上本来就存在的缺陷。
比如可以写一个显示函数,如果这次的结果和上次一样,那么就不用显示这一次的结果,会对高频的那种体验有一种很好的改善。
如果想要提升精度,可以尝试用一个更精确的频率计进行测量,然后把两个频率计之间的差值记录下来,在matlab中进行拟合,求出解析函数,然后每次显示的时候,显示测量值+校正值,应该还可以把精度提升一个数量级。
不说了,考试去了……
posted on
浙公网安备 33010602011771号