使用MCU测量0-1 MHz(0.25Hz分辨率)方波


8

我需要测量方波的频率,该频率可以在0到1MHz之间变化,分辨率为0.25Hz。

我还没有决定使用哪个控制器,但是它很可能是20pin Attiny的控制器之一。

通常,我如何测量低频信号将通过使用两个定时器来配置,其中一个配置为定时器捕获模式以在外部信号的上升沿中断,另一个定时器设置为每秒中断一次,因此前一个定时器在1秒后计数寄存器值将等于信号的频率。

但是,这种方法显然无法以0.25Hz的分辨率捕获0到1MHz范围内的信号,为此,我需要一个22Bit计数器(AFAIK 8bit micros仅具有8 / 16bit计数器)。

我曾经想过的一个想法是在将信号应用于微控制器之前先对其进行分频,但这是不切实际的,因为必须将信号除以61,因此频率只能每61秒更新一次,而我希望每隔几秒更新一次。

还有另一种方法可以让频率每4秒更新一次吗?


更新:

最简单的解决方案是使用外部中断或定时器捕获在信号的上升沿中断,并使isr增量为type变量long int。每4秒钟读取一次变量(以允许测量低至0.25Hz的频率)。


更新2:

正如JustJeff所指出的,一个8位MCU将无法跟上1MHz的信号,从而排除了在每个上升沿中断并增加long int...

我选择了timororr建议的方法。一旦我实现了它,我会回发并分享结果。感谢大家的建议。


进度报告:

Iv'e开始测试此处提出的一些想法。首先,我尝试了vicatcu的代码。存在一个明显的问题,即TCNT1在计算频率后仍未清除-没什么大不了的...

然后,我在调试代码时注意到,大约每2到7倍的频率被计算出来,定时器1(配置为对外部事件进行计数的定时器)的溢出计数将减少2。我将其归结为定时器0 ISR的等待时间,并决定将ISR的if语句块移至主要(请参见下面的代码段),并在ISR中设置一个标志。一些调试显示,第一次测量是可以的,但随后的每次读取都将使Timer 1的溢出计数超过2。这我无法解释-我希望它不会超过...。

int main()
{
    while(1)
    {
        if(global_task_timer_ms > 0 && (T0_overflow == 1))
        {
            global_task_timer_ms--;
            T0_overflow = 0;
        }

        .....
    }
}

接下来,我决定尝试实施timrorrs建议。要生成必要的间隔(每个timer_isr中断之间大约15毫秒),我将必须将两个8位定时器级联,因为Atmega16上仅有的16位定时器被用来捕获外部信号的上升沿。

我认为此解决方案将行之有效,并且效率会更高,因为大部分开销都转移到了计时器上,并且仅剩一个简短的isr供CPU处理。但是它不如我所希望的那样准确,测量值来回移动了约70Hz,这在高频下我不介意,但在低频下绝对不能接受。我没有花太多时间来分析问题,但是我猜想计时器的级联设置不太准确,因为我在一个速度较慢的具有2个16位计时器的8051控制器上实现了与调速建议类似的安排,结果相当准确。

现在,我又回到了vicatcu的建议,但是将频率计算移到了Timer 0 isr (请参见下面的代码)中,此代码产生了一致且合理的测量结果。稍加校准后,精度应约为+/- 10Hz。

ISR(TIMER0_OVF_vect)
{            

    TCNT0 = TIMER0_PRELOAD;         //Reload timer for 1KHz overflow rate

    if(task_timer_ms > 0)
    {
        task_timer_ms--;
    }
    else
    {     
        frequency_hz = 1.0 * TCNT1;
        TCNT1 = 0;
        frequency_hz += global_num_overflows * 65536.0;
        global_num_overflows  = 0;
        frequency_hz /= (TASK_PERIOD_MS / 1000.0);
        task_timer_ms = TASK_PERIOD_MS;
    }                                                 
}       

如果有人有其他建议,尽管我可以向他们提出,但我宁可不必使用范围...我也不再打算获得0.25%的分辨率,我目前的准确性水平似乎没有多大意义。


使用PIC上的捕获中断和以极高的速度运行的定时器1是一种相对简单的方法。如果您仍然对其他方法感兴趣,请告诉我,我可以在答复中进行概述。
科尔图克

我尚未开始这项工作,所以是的,我仍然很感兴趣。
投票

由于某种原因,它从未让我知道您对我的评论发表了评论。
Kortuk 2010年

@Kortuk:只有当我对您的答案或问题之一发表评论时,软件才会通知您。它也可能会通知您此评论,因为我将@Kortuk放在了它前面。但这是StackOverflow软件的更改,我不知道它是否被滴入了StackExchange代码库中。
罗伯特·哈维

不,即使使用@kortuk,它也没有让我知道您的回应。别担心。似乎已找到答案。
Kortuk'9

Answers:


4

如果可能的话,我建议选择一个使用定时器输入支持计数器操作的微控制器。而不是在ISR内部手动增加一个计数器(在高频时,它很快就会使微控制器的活动饱和),而是让硬件来处理计数。这时,您的代码就变成了等待周期性中断然后计算频率的问题。

为了扩展范围并使频率计数器更通用(无需增加多个范围,而以牺牲MCU的更多工作为代价),您可以使用以下技术。

选择一个周期性中断率,以允许在最高输入频率下的测量精度;这应该考虑到您的计数器大小(您需要选择计时器周期,以使计时器计数器不会在最大输入频率下溢出)。对于此示例,我假设可以从变量“ timer_input_ctr”中读取输入计数器值。

包括一个用于计数周期性中断的变量(应在启动时初始化为0);在本例中,我将此变量称为“ isr_count”。中断周期包含在常量“ isr_period”中。

您的周期性中断应实现为(C伪代码):

void timer_isr()
{
  isr_count++;
  if (timer_input_ctr > 0)
  {
    frequency = timer_input_ctr / (isr_count * isr_period).
    timer_input_ctr = 0;
    isr_count = 0;
  }
}

显然,这个粗糙的示例依赖于某些浮点数学运算,这些浮点运算可能与低端微控制器不兼容,有一些技术可以克服这一点,但它们不在此答案的范围之内。


1
优秀的timororr,这将完全满足我的需求,而无需花费额外的IC,而这总是很好的,我想我太快了,无法排除解决软件问题的可能性。谢谢
10年

@timrorr,如果您想阅读以下内容,我对您对我的回答的看法感兴趣
vicatcu 2010年

7

您可能要考虑具有两个(或更多)范围。捕获非常低的频率的问题与捕获较高频率的问题有些不同。正如您已经指出的那样,在范围的高端,您会遇到计数器溢出问题。

但是考虑到范围的低端,寄存器中的计数不足会影响精度。不知道您是否真的要在0.25Hz和0.5Hz之间进行区分,但是如果这样做,则实际上必须数四秒钟才能做到。

另外,严格解释为指定0.25Hz的平坦分辨率意味着您可以从500,000.25Hz分辨出500,000.00Hz,这是相当高的精度。

由于这些原因,针对不同范围进行设计可以减轻计数器尺寸的问题。举例来说,随机抽取数字,对于低端,例如0到100Hz,以10秒为间隔计数,您将获得0.1Hz的分辨率,您的计数器只需要增加到1000,甚至不需要10位。然后从100Hz到10kHz,间隔1秒计数一次;您只能获得1Hz的分辨率,但您的计数器只需要运行高达10,000个(仍小于16位)。10kHz到1MHz的上限范围仅可计算0.01秒,最大计数仍将仅为10,000,尽管您的分辨率为100Hz,这将是一个合理的精度。


是的,我提到在更新我的问题(之前)时,我必须数到4秒才能...。是的,我希望能够区分500,000.00Hz和500,000.25Hz。我曾考虑过使用不同的范围,因为信号有6个可选范围,所以我可以轻松地将其与其他硬件结合使用,因此我可以设计一个简单的6到3个编码器来指示哪个范围...但是我不是如果我使用硬件计数器并结合4秒钟的更新时间,请确定是否有必要,这应该解决频谱两端的问题
2010年

5

您可以通过计算ISR中硬件计数器的溢出来混合硬件和软件计数器。

Counting every edge of the signal in an ISR will be too slow for a 1 MHz signal. I think you could do up to about 50kHz that way.


是的,您可能是正确的-它对于1MHz而言太慢了,但我想像一下20MIPS RISC处理器的性能会比50KHz好。无论如何,我还考虑为信号提供8位二进制计数器的时钟,并将计数器的进位连接到MCU的外部中断引脚,然后读取信号的频率,作为进位位中断与o / p计数之和。我每隔n秒就会获得该计数器的值,我想这就是您说硬件和软件计数器的组合时得到的结果。
沃尔特

我认为OP指的是内置硬件计数器。它们都有溢出中断,可用于改善计数范围。
jpc

@starblue,我写的代码是否与您的答案一致?
vicatcu 2010年

2

与其做一个1秒的计数器,不如做一个0.1秒的计数器,然后将计数乘以10?

如果仅是存储计数器号的问题,您是否可以使用其他代码来跟踪计数器何时将溢出并写入另一个存储位置以保持计数?


2
我想我必须冻结大脑。我认为最简单的解决方案是,每当检测到上升沿时就增加long int类型的变量。每秒读取一次该值,然后将其重置为零。
2010年

2
实际上,我将需要每4秒读取一次该值以测量到0.25Hz的频率
2010年

2

您不能只使用16位定时器的输入捕获和溢出中断(加上变量)来进行测量吗?这是我如何使用带有AVR-GCC的ATTiny24A进行的操作(当然,这是未经测试的,并且可能有故障):

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#define TIMER1_BITS           16    // 16 bit timer
#define TIMER1_HZ             8.0e6 // 8MHz crystal
#define TIMER1_OVF_PERIOD_SEC (1.0 * (1 << TIMER1_BITS) / TIMER1_HZ)
#define TIMER1_SEC_PER_TICK   (1.0 / TIMER1_HZ)

//global variables for time keeping
double total_period_sec = 0.0;
uint16_t  num_overflows = 0;

void setup_timer1_capture(){
   // set the ICP (input caputure pin) to a floating input
   DDRA  &= ~_BV(7); // it's A7 on the ATTiny24A...
   PORTA &= ~_BV(7);

   TIMSK1 =   _BV(ICIE1)  // enable input pin capture interrupt
            | _BV(TOIE1); // enable overflow interrupt

   TCCR1B =   _BV(ICNC1)  // activate the input noise canceller
            | _BV(ICES1)  // capture on rising edge of ICP
            | _BV(CS10);  // run the timer at full speed

}

ISR(TIM1_CAPT_vect, ISR_NOBLOCK){ //pin capture interrupt
  uint16_t capture_value_ticks = ICR1; // grab the captured value
  // do some floating point math
  total_period_sec =   1.0 * num_overflows * TIMER1_OVF_PERIOD_SEC
                     + 1.0 * capture_value_ticks / TIMER1_SEC_PER_TICK; 

  num_overflows = 0; // clear helper variable to be ready for next time
}

ISR(TIM1_OVF_vect){   //timer overflow interrupt
    num_overflows++;
}

int main(int argc, char *argv[]){
   setup_timer1_capture();

   sei(); // enable interrupts!

   for(;;){ //forever
      // do whatever you want...
      // the most recently calculated period is available in the 
      // total_period_sec variable 
      // (obviously 1.0 / total_period_sec is the frequency in Hz)
   }

   return 0;
} 

...无论如何,它会编译:)


编辑我查看了我的代码中的lss文件输出,并且生成的代码有太多指令,以至于1MHz不会以8MHz的时钟跳闸……即使在TIM1_OVF_vect中简单的一行递增也会生成19条指令!因此,要处理1MHz事件,您肯定需要进行优化,可能需要注册分配一些内容(可能是num_overflows和capture_value_ticks),使用内联汇编程序(从lss文件中窃取重要内容),并将处理移出中断并移至主程序中。尽可能地循环。


使用该周期来测量频率对于缓慢的波形非常有效(您所依赖的内部计时器比外部信号要快得多),但是随着输入信号频率的增加,它很快达到极限。基本上,正如您所发现的,在定时器捕获中断中花费的时间占主导地位。没有其他代码可以运行的时间了。虽然我对ATTiny不太熟悉,但快速浏览一下数据表可以看到Timer / Counter1确实支持外部事件计数,因此让硬件来处理计数。
timrorr 2010年

@timrorr,哇,那是 这样做的更聪明的方式:)我发布更新的AVR-GCC代码在一个单独的职位。关心看看您的想法吗?
vicatcu 2010年

2

根据@timrorr的建议,将此代码发布到我以前的帖子中。它将使用c99语言标准为ATTiny24A进行编译,但是除此以外,我还没有进行任何实际测试。

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#define TIMER0_PRELOAD   0x83 // for 8MHz crystal and overflow @ 1kHz
#define TIMER0_PRESCALE 0x03  // divide by 64
#define TASK_PERIOD_MS 4000   // execute task every 4 seconds

//global variables for time keeping
volatile uint16_t  global_num_overflows = 0;
volatile uint16_t  global_task_timer_ms = TASK_PERIOD_MS;

void setup_timers(){
    // set the T1 pin (PA.4) to a floating input (external event)
    DDRA  &= ~_BV(4);
    PORTA &= ~_BV(4);

    // set Timer1 to count external events
    TIMSK1 = _BV(TOIE1);      // enable overflow interrupt
    TCCR1B =   _BV(CS10)      // clock on external positive edge of T1 pin
        | _BV(CS11)
        | _BV(CS12);

    // set Timer0 for task timing (overflow once per ms)
    TCCR0B = TIMER0_PRESCALE;
    TCNT0  = TIMER0_PRELOAD;  // setup appropriate timeout
    TIMSK0 = _BV(TOIE0);      // enable timer0 overflow interrupt
}


ISR(TIM1_OVF_vect){   //timer1 overflow interrupt
    global_num_overflows++;
}

ISR(TIM0_OVF_vect){            //timer0 overflow interrupt @ 1kHz
    TCNT0 = TIMER0_PRELOAD;   // preload timer for 1kHz overflow rate
    if(global_task_timer_ms > 0){
        global_task_timer_ms--;
    }
}

int main(int argc, char *argv[]){
    double frequency_hz = 0;
    uint16_t num_overflows = 0;
    uint16_t num_positive_edges  = 0;
    setup_timers();
    sei(); // enable interrupts!
    for(;;){ //forever
        if(global_task_timer_ms == 0){ // wait for task to be scheduled
            ATOMIC_BLOCK(ATOMIC_FORCEON){
                num_overflows        = global_num_overflows; // copy the volatile variable into a local variable
                global_num_overflows = 0;                    // clear it for next time
                num_positive_edges   = TCNT1;                // copy num positive edge events to local variable
            }

            // calculate the 'average' frequency during this task period
            frequency_hz  = 1.0 * num_positive_edges;  // num edges since last overflow
            frequency_hz += num_overflows * 65536.0;   // edges per overflow of 16 bit timer
            frequency_hz /= (TASK_PERIOD_MS / 1000.0); // over the task interval in seconds

            global_task_timer_ms = TASK_PERIOD_MS;     // reschedule task
        }

        // use frequency_hz for whatever other processing you want to do
    }
    return 0;
}

与我的原始文章相比,这很好地利用了Timer1的硬件功能,并释放了大量的处理周期。


2
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.