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

电器工程 频率测量 测量 微控制器 服装 avr
2022-01-28 20:21:10

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

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

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

然而,这种方法显然不适用于以 0.25Hz 的分辨率捕获 0 到 1MHz 之间的信号,为此我需要一个 22 位计数器(AFAIK 8 位微控制器只有 8/16 位计数器)。

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

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


更新:

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


更新 2:

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

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


进度报告:

我已经开始测试这里提出的一些想法。首先我尝试了vicatcu的代码。有一个明显的问题是TCNT1在计算频率后没有被清除——没什么大不了的……

然后我注意到,在调试代码时,大约每 2 到 7 次计算一次频率,计时器 1(配置为计数外部事件的计时器)溢出计数会减少 2。我将其归结为 Timer 0 ISR 的延迟,并决定将 if 语句块从 ISR 移至 main(见下面的片段),并在 ISR 中设置一个标志。一些调试表明第一次测量是可以的,但随后每次读取计时器 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 中断之间大约 15ms),我必须级联两个 8 位定时器,因为 Atmega16 上唯一的 16 位定时器用于捕获外部信号的上升沿。

我认为这个解决方案会起作用并且效率更高,因为大部分开销都转移到了计时器上,并且只剩下一个短的 isr 供 cpu 处理。然而它并不像我希望的那样准确,测量值来回移动了大约 70Hz,我不介意在高频下,但在低频下绝对不能接受。我没有花太多时间分析问题,但我猜测定时器级联安排并不那么准确,因为我已经在一个速度慢得多的 8051 控制器上实现了与 timrorrs 建议类似的安排,该控制器具有 2 个 16 位定时器,结果非常准确。

我现在回到了 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% 的分辨率,我目前的准确度水平似乎没有多大意义.

4个回答

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

但是考虑到您的范围的低端,您的准确性将受到寄存器中没有足够计数的影响。不确定您是否真的要区分 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,但这将是合理的精度。

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

对于 1 MHz 信号,计算 ISR 中信号的每个边沿将太慢。我认为您可以通过这种方式达到大约 50kHz。

如果可能的话,我建议选择一个支持使用定时器输入进行计数器操作的微控制器;而不是手动增加 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;
  }
}

显然,这个粗略的例子依赖于一些可能与低端微控制器不兼容的浮点数学,有一些技术可以克服这个问题,但它们超出了这个答案的范围。