Измерение 0 - 1 МГц (разрешение 0,25 Гц) SquareWave с использованием MCU


8

Мне нужно измерить частоту прямоугольной волны, которая может варьироваться от 0 до 1 МГц, и имеет разрешение 0,25 Гц.

Я еще не определился с тем, какой контроллер, но скорее всего это будет один из 20-контактных Attiny.

Обычно я измерял бы низкочастотные сигналы, используя два таймера, один из которых сконфигурирован в режиме захвата таймера, чтобы прерывать, например, нарастающие фронты внешнего сигнала, и другой таймер, настроенный на прерывание каждую секунду, поэтому значение счетчика бывших таймеров регистрируется через 1 секунду. будет равна частоте сигнала.

Однако этот метод, очевидно, не будет работать для захвата сигналов в диапазоне от 0 до 1 МГц с разрешением 0,25 Гц, для этого мне понадобится 22-битный счетчик (8-битные микросхемы AFAIK имеют только 8/16-битные счетчики).

Одна из идей, которая у меня была, заключалась в том, чтобы разделить сигнал, прежде чем применять его к микро, но это было бы нецелесообразно, так как сигнал пришлось бы делить на 61, поэтому частоту можно было обновлять только каждые 61 секунду, где бы я хотел, чтобы она была каждые несколько секунд. ,

Есть ли другой метод, который позволил бы обновлять частоту, скажем, каждые 4 секунды?


Обновить:

Самое простое решение - использовать внешнее прерывание или захват таймера для прерывания по переднему фронту сигнала и isrувеличения значения переменной типа long int. Считывайте переменную каждые 4 секунды (чтобы можно было измерять частоты до 0,25 Гц).


Обновление 2:

Как указал JustJeff, 8-битный MCU не сможет справиться с сигналом 1 МГц, так что исключается прерывание на каждом переднем фронте и увеличение long int...

Я выбрал метод, предложенный Тиморорром. Как только я приступлю к его реализации, я отправлю ответ и поделюсь результатами. Спасибо всем за ваши предложения.


Отчет о проделанной работе:

Iv'e начал тестировать некоторые идеи, представленные здесь. Сначала я попробовал код Викацу. Была очевидная проблема TCNT1, которая не была очищена после того, как была рассчитана частота - ничего страшного ...

Затем я заметил, что при отладке кода примерно каждые 2-7 раз вычислялась частота, когда счетчик переполнения таймера 1 (таймера, настроенного на подсчет внешних событий) был бы коротким на два. Я отложил это до времени ожидания таймера 0 ISR и решил переместить блок оператора if из ISR в главное (см. Фрагмент ниже) и просто установить флаг в ISR. Некоторая отладка показала, что первое измерение будет в порядке, но при каждом последующем чтении счетчик переполнения Таймера 1 будет превышен на 2., что я не могу объяснить - я ожидал, что оно будет ниже, чем не более ...

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

        .....
    }
}

Затем я решил, что попытаюсь реализовать предложение тиморра. Чтобы сгенерировать необходимый интервал (около 15 мс между каждым прерыванием timer_isr), мне пришлось бы каскадировать два 8-битных таймера, поскольку единственный 16-битный таймер на Atmega16 используется для захвата нарастающих фронтов внешнего сигнала.

Я думал, что это решение будет работать и будет гораздо более эффективным, поскольку большая часть служебных данных переносится на таймеры, и для обработки процессором остается только один короткий isr. Однако это было не так точно, как я надеялся, измерения сместились назад и вперед примерно на 70 Гц, что я не возражал бы на высоких частотах, но это определенно не приемлемо на низких частотах. Я не потратил много времени на анализ проблемы, но я предполагаю, что механизм каскадирования таймера не настолько точен, поскольку я реализовал механизм, подобный предложению тиморра, на гораздо более медленном контроллере 8051, который имел 2 16-разрядных таймера, и результаты были довольно точными.

Теперь я вернулся к предложению Викачу, но я перенес расчет частоты в таймер 0 (см. Фрагмент ниже ), этот код дал последовательные и достаточно точные измерения. При небольшой калибровке точность должна быть примерно +/- 10 Гц.

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, работающем с очень высокой скоростью. Если вы все еще заинтересованы в других методах, дайте мне знать, и я могу изложить это в ответе.
Кортук

Я еще не начал работать над этим, так что да, я все еще заинтересован.
2010 года

По какой-то причине он никогда не давал мне знать, что вы прокомментировали мой комментарий.
Кортук

@Kortuk: Программное обеспечение уведомляет вас, только если я оставлю комментарий к одному из ваших ответов или вопросов. Он также может уведомить вас об этом комментарии, потому что я поставил @Kortuk перед ним. Но это изменение программного обеспечения StackOverflow, и я не знаю, попало ли оно в кодовую базу StackExchange или нет.
Роберт Харви

нет, это не дало мне знать, что вы ответили, даже с @kortuk. Не беспокойся. Похоже, ответ был найден.
Кортук

Ответы:


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
Превосходный тиморор, он сделает то, что я хочу, без затрат на дополнительные микросхемы, что всегда хорошо, я думаю, я был слишком быстр, чтобы упустить возможность решения проблемы в программном обеспечении. Спасибо
вольт

@timrorr, мне интересны ваши мысли о моем ответе ниже, если вы хотите прочитать его
vicatcu

7

Возможно, вы захотите рассмотреть два (или более) диапазона. Проблемы с захватом очень низких частот несколько отличаются от проблем с более высокими. Как вы уже заметили, на верхнем уровне вашего диапазона возникают проблемы с переполнением счетчика.

Но обратите внимание на нижнюю границу вашего диапазона, ваша точность пострадает от недостатка подсчетов в реестре. Не уверен, что вы действительно хотите различать между 0,25 Гц и 0,5 Гц, но если вы это сделаете, то вам действительно придется рассчитывать на четыре секунды, чтобы сделать это.

Кроме того, если указать строгое интерпретированное плоское разрешение 0,25 Гц, вы сможете отличить 500 000,00 Гц от 500 000,25 Гц, что является довольно высокой степенью точности.

По этим причинам проектирование для различных диапазонов может облегчить проблему размера счетчика. Вытягивание случайных чисел, например, для нижнего уровня, скажем, от 0 до 100 Гц, считается за 10-секундные интервалы, и вы получаете разрешение 0,1 Гц, и вашему счетчику нужно только подняться до 1000, а не до 10 бит. Затем от 100 Гц до 10 кГц считайте интервалы в 1 секунду; вы получаете разрешение только 1 Гц, но ваш счетчик должен работать до 10000, все же меньше 16 бит. Верхний диапазон от 10 кГц до 1 МГц может считаться всего за 0,01 с, а максимальный счет будет по-прежнему только 10 000, и хотя ваше разрешение будет 100 Гц, это будет разумной точностью.


Да, я упомянул это в обновлении моего вопроса (ранее), что мне нужно считать до 4 секунд для ... и я да, я хотел бы иметь возможность различать, скажем, 500 000,00 Гц и 500 000 25 Гц. Я думал об использовании разных диапазонов, я мог бы легко связать это с остальной частью аппаратного обеспечения, поскольку сигнал имеет 6 выбираемых диапазонов, так что я мог бы, вероятно, разработать простой 6–3 кодировщик, чтобы указать, какой диапазон ... но я не конечно, если будет необходимо, если я использую аппаратный счетчик в сочетании с 4-секундным временем обновления, это должно решить проблемы на обоих концах спектра
вольт

5

Вы можете смешать аппаратный и программный счетчик, посчитав переполнения аппаратного счетчика в ISR.

Подсчет каждого фронта сигнала в ISR будет слишком медленным для сигнала 1 МГц. Я думаю, что вы можете сделать до 50 кГц таким образом.


Да, вы, вероятно, правы - он будет слишком медленным для 1 МГц, но я предполагаю, что процессор 20MIPS RISC может работать лучше, чем 50 КГц. В любом случае я также рассматривал возможность синхронизации 8-битного двоичного счетчика с помощью сигнала и подключения выноса счетчика к внешнему выводу прерывания блока MCU, а затем считывания частоты сигнала в виде суммы прерываний бита переноса плюс счет o / p. Значение счетчика каждые n секунд, я предполагаю, что это то, что вы получили, когда вы сказали комбинацию аппаратных и программных счетчиков.
volting

Я думаю, что OP имел в виду встроенный аппаратный счетчик. Все они имеют прерывания по переполнению, которые можно использовать для улучшения диапазона счета.
jpc

@starblue, код, который я написал ниже, что вы имели в виду с вашим ответом?
Викацу

2

Вместо того чтобы делать 1-секундный счетчик, сделайте его 0,1-секундным счетчиком и умножьте счет на 10?

Если это просто вопрос сохранения номера счетчика, разве вы не можете использовать дополнительный код для отслеживания того, когда счетчик собирается переполниться, и записи в другое место в памяти, чтобы сохранить счет?


2
Я думаю, что у меня должно было замерзнуть мозг ... самое простое решение, которое я думаю, это просто увеличивать переменную типа long int каждый раз, когда обнаруживается нарастающий фронт. Читайте это значение раз в секунду, а затем обнуляйте его.
volting

2
На самом деле мне нужно будет читать значение каждые 4 секунды, чтобы измерить частоту до 0,25 Гц
напряжение

2

Разве вы не можете использовать прерывания захвата и переполнения 16-битного таймера (плюс переменная) для измерения? Вот как я мог бы сделать это с ATTiny24A с AVR-GCC (непроверенный и, возможно, с ошибками):

#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 из моего кода, и в сгенерированном коде слишком много инструкций, чтобы не отключиться на частоте 1 МГц с тактовой частотой 8 МГц ... даже простое увеличение на одну строку в TIM1_OVF_vect генерирует 19 инструкций! Таким образом, для обработки событий с частотой 1 МГц вам, безусловно, необходимо оптимизировать, вероятно, зарегистрировать выделение некоторых вещей (например, num_overflows и capture_value_ticks), использовать встроенный ассемблер (украсть важные вещи из файла lss) и перенести обработку из прерываний в главное петля везде, где это возможно.


Измерение частоты с использованием периода работает очень хорошо с медленными сигналами (вы полагаетесь на то, что внутренний таймер намного быстрее внешнего сигнала), но быстро достигает предела по мере увеличения частоты входного сигнала. По сути, как вы обнаружили, время, проведенное внутри прерывания захвата таймера, становится доминирующим; нет времени для запуска других частей кода. Хотя я не очень знаком с ATTiny, быстрый просмотр таблицы данных показывает, что timer / counter1 поддерживает подсчет внешних событий, поэтому пусть аппаратное обеспечение обрабатывает подсчет.
Timrorr

@timrorr, ничего себе да , что это способ умнее способ сделать это :) Я отправил обновленный код AVR-GCC в отдельном посте. Хотите посмотреть и посмотреть, что вы думаете?
Викацу

2

Публикация этого кода в качестве альтернативы предложению @ timrorr для моего предыдущего поста. Он компилируется для ATTiny24A с использованием стандарта языка c99, но я на самом деле не тестировал его никоим образом.

#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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.