本文转自www.technoblogy.com,原文名字:Frequency Probe
这个频率探头是一个便携式的工具,能够通过显示频率和电压帮助你调试电路,它能够测出1Hz到5MHz的周期性波形,准确度达到0.3%,当然也可以当电压表使用,能够告诉你当前的电压值。
检查一个4MHz晶体振荡器的频率
它是基于一个8位的AVR单片机ATtiny84,由一个Lipo电池供电。
简介
制作一个频率表最直接的方式就是数一下一秒内有多少个脉冲,它直接给出的是频率 - 也就是工作于频率模式。这种方式的缺点就是对于低频率的信号要进行准确测量需要较长的时间。另一种方式就是测量输入信号两个脉冲之间的间隔,它的倒数就是频率 - 也就是间隔模式,例如脉冲之间的间隔位1秒,其频率也就是1Hz。这种方式的缺点是对于高频信号,你需要非常准确滴测量两个脉冲之间的间隔。
理想的方式是用频率模式测量高频信号,用间隔模式测量低频信号,那问题就在于两种模式中间的平衡点如何确定?
处理器
我选用了ATtiny84这个处理器是因为它由一个8位的定时器/计数器0, 这样你可以通过在T0管脚上用外部信号作为时钟,提供了一种测量外部频率的准确的方式,它同时还有一个16位定时器/计数器1用以内部捕获信号,它能够在输入捕获管脚ICP1的上升沿或下降沿的时候将计数器的内容复制到寄存器里,这样就可以非常精准地计算波形的周期。
它使用USI外设驱动一个I2C OLED显示屏,以6*8字体的双倍大小显示在显示屏上。
频率范围
在“频率模式”下能测到的信号的频率上限取决于你在T0输入端能够采样的最高频率,数据手册指明了为CLK/2.5,CLK为处理器的频率,在3V供电的情况下处理器能够工作的最高频率为12MHz,因此能够量测到的最高频率为4.8MHz;
在”间隔模式“下能够测到的信号的频率下限取决于你打算花多久的时间得到答案,我选择1Hz,也就是每秒刷新一次显示的信息。
精度
在频率模式下,量测频率的精度随着频率的增高而增加,因为取样的时候能够获得更多的频率周期,在下面的程序中我用了定时器/计数器0在有定时器/计数器1的中断里来数周期数。系统时钟为12MHz,除以16384*8, 每秒产生9.16次中断,因此测量的频率精度为:
100x12000000/(8x16384xf)%
例如,对于100KHz的信号,其分辨率精度为0.09%.
在间隔模式下,随着频率的升高准确度会降低,这是因为在两次变换之间的周期数在减少,在程序中我使用了定时器/计数器1的输入捕获来测量输入信号波形的两次变化之间的间隔,测量的频率精度为:
100xf/12000000 %
例如,对于10KHz的信号,其分辨率精度为0.083%.
用一个图来展示一下每种方式能够达到的精度:
两线相交的地方定义为本系统能够达到的“最佳的精度”,在30KHz以下我们使用间隔模式,高于30KHz则使用频率模式,在全频率范围内都能给出超过0.3%的测量精度。
电压表
如果在探头上有稳定的电压,频率探头则可以显示该电压值,从0到5V,准确度为1/1024 ~ 0.1%。电压是通过模拟输入ADC1读取的,它使用了内部的1.1V参考电压,需要一个电阻分压奖0-5V的输入降到0-1.1V。
电路
下面就是这个探头的电路:
基于ATtiny84的频率探头的电路构成
用到的器件
输入IN对应于探针的尖端, 与GND分开的飞线连接到要测试的电路的GND。220Ω电阻和两个肖特基二极管可保护探头输入免受高于Vcc的电压或低于GND的负电压的影响。然后通过分压器将其馈入频率模式的T0输入(PA3),用于间隔模式的ICP输入(PA7)和用于电压模式的ADC1(PA1)。在探头上使用5V电压时,分压器在PA1处提供6800/(6800 + 24000)x 5 = 1.10 V,以匹配电压模式下使用的1.1V参考电压。分压器设计为匹配ADC输入,该输入针对10kΩ或更小的输入阻抗进行了优化。
当使用USI外设提供I2C接口时,不可能在SCL和SDA上使用内部上拉,因此该电路提供4.7kΩ的外部上拉。
剩下的唯一器件是晶振电路,还有一个按钮,用于重置处理器并使它退出睡眠状态。
组装
ATtiny84A采用SOIC封装,电阻,电容器和二极管均为0805尺寸,因此手工焊接应相对容易。
晶体为5x3.2mm
显示屏为SSD1306驱动的OLED 128x32 I2C显示模块
用普通的制衣针作为探针
使用了一个40mAh的Lipo电池通过双面自粘泡沫垫固定在板上给板子供电
频率探头的元器件面
程序
该程序包括一个用于控制OLED显示的显示接口,实现这三种模式的例程以及一个主循环,该主循环选择用于给定输入信号的模式。
显示界面使用与我以前的Tiny Function Generator相同的例程,后者使用了类似的I2C OLED显示器。使用6x8像素字符集在显示屏上绘制文本,但使用我在``平滑大文本''中介绍的平滑例程,将比例尺放大一倍以提供12x16像素的有效字符大小。
当前模式由变量模式决定,该模式在间隔模式下具有常数INTERVAL,在频率模式下具有常数FREQUENCY。电路以频率模式启动。
程序首先配置Timer / Counter0,TimerCounter1,看门狗定时器和ADC:
// Set up 8-bit Timer/Counter0
TCCR0A = 0< TCCR0B = 0< OCR0A = 255; // Compare match at top
TIMSK0 = 0; // Interrupts initially off
//
// Set up 16-bit Timer/Counter1
TCCR1A = 0< TCCR1B = 1< TIMSK1 = 0; // Interrupts initially off
//
// Set up Watchdog timer for 1 Hz interrupt for display update
WDTCSR = 0< //
// Set up ADC to use ADC1 (PA1)
ADMUX = 2< ADCSRA = 1<}
最初,定时器/计数器时钟停止,看门狗定时器被禁用,中断被关闭。
要运行频率模式或间隔模式,程序会调用GetFrequency()或GetInterval()来分别为每种模式设置外设。
频率模式
调用GetFrequency()在“频率模式”下进行测量:
unsigned long GetFrequency () {
Capture = false;
FreqHigh = 0;
TCNT0 = 0;
TCNT1 = 0;
OCR1A = 16383; // Divide by 16384
TIFR1 = 1< TIMSK0 = 1< TIMSK1 = 1< GTCCR = 1; // Clear prescaler
TCCR1B = TCCR1B | 2< TCCR0B = TCCR0B | 7< while (!Capture); // Wait for Timer/Counter1 timeout
TCCR1B = TCCR1B & ~(2< TCCR0B = TCCR0B & ~(7< return Count;
}
它首先清除计数器,使能定时/计数器中断,并通过外部时钟输入T0(PA3)上的输入信号设置8位定时器/计数器0的时钟。
为了获得16位计数,我们在Timer / Counter0寄存器溢出时生成一个比较中断,并对变量FreqHigh中的溢出进行计数(我们无法使用更具逻辑性的TIM0_OVF_vect中断向量,因为它已被Arduino内核使用):
ISR(TIM0_COMPA_vect) {
FreqHigh++;
}
16位定时器/计数器1设置为系统时钟的1/8。OCR1A值为16383时,每16384 x 8个周期就会产生一个比较中断,即频率为91.6Hz。
中断服务程序关闭定时/计数器中断,并将变量Count设置为通过结合Timer / Counter0计数寄存器和FreqHigh获得的值:
ISR(TIM1_COMPA_vect) {
uint8_t temp = TCNT0;
TIMSK1 = 0; // Turn off interrupt
TIMSK0 = 0; // Turn off interrupt
Count = (unsigned long)FreqHigh<<8 | temp;
Capture = true;
}
通过下面的公式得到实际的频率,单位是Hz
(count * 46875)/512;
其中46875/512近似为91.6Hz。
然后调用例程“ Plot Frequency()”,以适当的单位在显示器上绘制频率:
void PlotFrequency (unsigned long c, int line) {
unsigned long f = (c * 46875)/512;
if (f >= 1000000) {
PlotValue(f/100, line, Col, 4);
PlotText(PSTR(" MHz"), line, Col+6*6*Scale);
} else if (f >= 100000) {
PlotValue(f/10, line, Col, 2);
PlotText(PSTR(" kHz"), line, Col+6*6*Scale);
} else if (f >= 10000) {
PlotValue(f, line, Col, 3);
PlotText(PSTR(" kHz"), line, Col+6*6*Scale);
} else PlotText(PSTR("----------"), line, Col);
}
间隔模式
调用GetInterval()在间隔模式下进行测量:
unsigned long GetInterval () {
Captures = 0;
Count = 0;
IntHigh = 0;
TCNT1 = 0;
OCR1A = 65535; // Divide by 65536
TCCR1B = TCCR1B | 1< TIFR1 = 1< TIMSK1 = 1< DelaySecond(); // Allow 1 second for captures
TCCR1B = TCCR1B & ~(1< TIMSK1 = 0; // Turn off interrupts
return Count;
}
该模式仅使用Timer / Counter1。它首先清除计数器,使能定时/计数器捕获和溢出中断,并将计数器设置为以系统时钟速率计时。
溢出中断将变量IntHigh递增:
ISR(TIM1_OVF_vect) {
IntHigh++;
}
输入捕获中断对两个输入捕获进行计数,然后将变量Count设置为它们之间的差:
ISR(TIM1_CAPT_vect) {
unsigned int temp = ICR1;
Count0 = Count1;
Count1 = (unsigned long)IntHigh<<16 | temp;
Captures++;
if (Captures == 2) {
Count = Count1 - Count0;
TIMSK1 = 0;
}
}
如果探头上没有信号,则由于没有捕获输入,GetInterval()例程将锁定。因此,使用8Hz的看门狗中断来对Timer变量进行递减计数:
ISR(WDT_vect) {
WDTCSR = WDTCSR | 1< Timer--;
}
例程DelaySecond()使用此函数生成一秒钟的延迟,并在经过此时间后从GetInterval()返回:
void DelaySecond () {
WDTCSR = WDTCSR | 1<<WDIE;Timer = 8;while (Timer > 0);
WDTCSR = WDTCSR & ~(0<}
电压模式
最后,电压模式仅使用ADC来测量模拟输入ADC1上的电压:
unsigned long GetVoltage () {
ADCSRA = ADCSRA | 1< while(ADCSRA & 1< int low, high;
low = ADCL;
high = ADCH;
return high<<8 | low;
}
主循环
主循环在当前选择的模式下反复获取读数,如果读数超出该模式的范围,则在必要时切换模式:
do {
if (mode == FREQUENCY) {
c = GetFrequency();
if (c > 320) {
PlotFrequency(c, 1);
Sleep = Timeout;
DelaySecond();
} else if (c == 0) mode = VOLTAGE;
else mode = INTERVAL;
} else if (mode == INTERVAL) {
c = GetInterval();
if (c > 390) {
PlotInterval(c, 1);
Sleep = Timeout;
} else if (c == 0) mode = VOLTAGE;
else mode = FREQUENCY;
} else if (mode == VOLTAGE) {
c = GetVoltage();
PlotVoltage(c, 1);
if (c < 40) Sleep--;
DelaySecond();
mode = FREQUENCY;
}
} while (Sleep > 0);
切换点存在一些滞后现象,以避免探头在切换点的频率的模式之间切换。
睡眠超时
为了避免需要开/关开关,如果探头上没有输入,并且电压小于0.20V,则程序会在大约30秒后超时。然后,它关闭显示屏,并使处理器进入睡眠状态。睡眠电流消耗约为7.1µA,使用40mAh的Lipo电池可提供大约一年的电池寿命。按下按钮将重置处理器并唤醒探头。
进一步建议
添加一个在间隔模式下显示标记/间距比率的显示非常容易;实际上,我已经在早期的原型机上进行了这项工作。您需要做的是在输入捕捉中断服务程序中获取三个读数,通过更改寄存器TCCR1B中的ICES1在读数之间切换输入捕捉边沿。
您可以通过在20MHz的外部晶振上运行ATtiny84,将频率探头的上限频率提高到8MHz。为此,您需要使用至少4.5V的电源供电。