Medindo 0 - 1MHz (resolução de 0,25Hz) Squarewave usando um MCU


8

Preciso medir a frequência da onda quadrada que pode variar entre 0 e 1 MHz e tem uma resolução de 0,25 Hz.

Ainda não decidi em qual controlador, mas provavelmente será um dos 20 pinos da Attiny.

Normalmente, como eu mediria os sinais de frequência mais baixa seria usando dois temporizadores, um configurado no modo de captura do temporizador, para interromper, por exemplo, as bordas ascendentes do sinal externo e outro temporizador configurado para interromper a cada segundo, portanto, os temporizadores anteriores registram o valor após 1 segundo seria igual à frequência do sinal.

No entanto, este método obviamente não funcionará para capturar sinais que variam entre 0 e 1 MHz com uma resolução de 0,25 Hz para isso. Eu precisaria de um contador de 22 bits (os micros AFAIK de 8 bits possuem apenas contadores de 8/16 bits).

Uma idéia que tive foi dividir o sinal antes de aplicá-lo ao micro, mas isso seria impraticável, pois o sinal teria que ser dividido por 61. Portanto, a frequência só poderia ser atualizada a cada 61 segundos, onde eu gostaria que fosse a cada poucos segundos .

Existe outro método que permita a atualização da frequência, digamos a cada 4 segundos?


Atualizar:

A solução mais simples é usar uma interrupção externa ou uma captura de temporizador para interromper a borda ascendente do sinal e fazer com que o isrincremento seja uma variável do tipo long int. Leia a variável a cada 4 segundos (para permitir frequências de até 0,25Hz a serem medidas).


Atualização 2:

Como apontado por JustJeff, um MCU de 8 bits não será capaz de acompanhar um sinal de 1 MHz, de modo que exclui a interrupção em cada borda ascendente e o incremento de um long int...

Eu escolhi o método sugerido por timororr. Depois que eu começar a implementá-lo, vou postar de volta e compartilhar os resultados. Obrigado a todos por suas sugestões.


Relatório de progresso:

Iv'e começou a testar algumas das idéias apresentadas aqui. Primeiramente, tentei o código do vicatcu. Havia um problema óbvio de TCNT1 que não foi resolvido depois que a frequência foi calculada - não é grande coisa ...

Percebi então, ao depurar o código, que a cada 2 a 7 vezes a frequência era calculada no temporizador 1 (o temporizador configurado para contar eventos externos) a contagem de transbordamento seria curta em dois. Coloquei isso na latência do Timer 0 ISR e decidi mover o bloco if do ISR para o principal (veja o trecho abaixo) e apenas defina uma sinalização no ISR. Alguma depuração mostrou que a primeira medição seria boa, mas a cada leitura subseqüente a contagem de transbordamento do Timer 1 terminaria em 2. o que não posso explicar - eu esperava que ela estivesse abaixo do limite ...

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

        .....
    }
}

Em seguida, decidi que tentaria implementar a sugestão de timrorrs. Para gerar o intervalo necessário (de aproximadamente 15ms entre cada interrupção timer_isr), eu precisaria cascatear os dois temporizadores de 8 bits, pois o único temporizador de 16 bits do Atmega16 está sendo utilizado para capturar as bordas ascendentes do sinal externo.

Eu pensei que essa solução funcionaria e seria muito mais eficiente, pois a maior parte da sobrecarga é deslocada para os temporizadores e apenas um isr curto resta para a CPU lidar. No entanto, não foi tão preciso quanto eu esperava, as medições mudaram para frente e para trás em aproximadamente 70Hz, o que eu não me importaria em altas frequências, mas definitivamente não é aceitável em frequências mais baixas. Eu não gastei muito tempo analisando o problema, mas acho que o arranjo em cascata do temporizador não é tão preciso, já que implementei um arranjo semelhante à sugestão de timrorrs em um controlador 8051 muito mais lento que tinha 2 temporizadores de 16 bits e os resultados foram bastante precisos.

Voltei agora à sugestão do vicatcu, mas mudei o cálculo de frequência para o temporizador 0 isr (veja o trecho abaixo ), esse código produziu medições consistentes e razoavelmente precisas. Com um pouco de calibração, a precisão deve ser aproximadamente +/- 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;
    }                                                 
}       

Se alguém tiver outras sugestões, estou aberto a eles, embora eu prefira não usar intervalos ... Também não tenho mais a intenção de obter uma resolução de 0,25%, não parece haver muito sentido no nível de precisão que tenho no momento. .


Existe uma maneira relativamente fácil de fazer isso usando uma interrupção de captura em um PIC e Timer 1 executando em uma velocidade muito alta. Se você ainda estiver interessado em outros métodos, informe-me e posso descrevê-lo em uma resposta.
Kortuk 31/05

Ainda não comecei a trabalhar nisso, então sim, ainda estou interessado.
volting 01/06/10

Por alguma razão, nunca me avise que você comentou o meu comentário.
Kortuk

@ Kortuk: O software só o notifica se eu deixar um comentário em uma de suas respostas ou perguntas. Também pode notificá-lo desse comentário, porque eu coloquei o @Kortuk na frente dele. Mas essa é uma alteração do software StackOverflow, e não sei se foi introduzida na base de código do StackExchange ou não.
Robert Harvey

não, não me avise que você respondeu, mesmo com o @kortuk. Não se preocupe. Parece que uma resposta foi encontrada.
Kortuk

Respostas:


4

Se possível, sugiro selecionar um microcontrolador que suporte uma operação de contador usando as entradas do timer; em vez de incrementar manualmente um contador dentro de um ISR (que em altas frequências acaba rapidamente saturando a atividade do microcontrolador), você permite que o hardware lide com a contagem. Nesse ponto, seu código simplesmente se torna uma questão de aguardar sua interrupção periódica e calcular a frequência.

Para estender a faixa e tornar o contador de frequência mais generalizado (removendo a necessidade de várias faixas à custa de um pouco mais de trabalho para o MCU), você pode usar a seguinte técnica.

Selecione uma taxa de interrupção periódica que permita a precisão da medição na frequência de entrada mais alta; isso deve levar em consideração o tamanho do seu contador (é necessário selecionar o período do temporizador para que o contador não transborde na freqüência máxima de entrada). Neste exemplo, assumirei que o valor do contador de entrada pode ser lido da variável "timer_input_ctr".

Inclua uma variável para contar interrupções periódicas (deve ser inicializada como 0 na inicialização); neste exemplo, vou me referir a essa variável como "isr_count". O período de interrupção está contido na constante "isr_period".

Sua interrupção periódica deve ser implementada como (pseudocódigo 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;
  }
}

Obviamente, este exemplo aproxima-se de algumas matemáticas de ponto flutuante que podem não ser compatíveis com microcontroladores de gama baixa, existem técnicas para superar isso, mas estão fora do escopo desta resposta.


1
Excelente timororr, isso fará exatamente o que eu quero, sem o custo de ICs extras, o que é sempre bom, acho que fui rápido demais para descartar a possibilidade de resolver o problema no software. Obrigado
volting

@timrorr, Estou interessado em seus pensamentos sobre a minha resposta abaixo se você sentir vontade de lê-lo
vicatcu

7

Você pode considerar ter dois (ou mais) intervalos. Os problemas com a captura de frequências muito baixas são um pouco diferentes dos problemas com as mais altas. Como você já observou, na extremidade superior do seu intervalo, você tem problemas com o estouro de contador.

Mas considere que na extremidade inferior do seu intervalo, sua precisão sofrerá por não ter contagens suficientes no registro. Não tenho certeza se você realmente deseja discriminar entre 0,25Hz e 0,5Hz, mas se o fizer, precisará contar quatro segundos para fazer isso.

Além disso, especificar uma resolução plana de 0,25Hz, estritamente interpretada, significa que você poderá discernir 500.000,00Hz de 500.000,25Hz, o que é um alto grau de precisão.

Por esses motivos, o design de intervalos distintos pode aliviar o problema do tamanho do contador. Puxar números aleatoriamente, por exemplo, para os mais baixos, digamos de 0 a 100Hz, conte com intervalos de 10 segundos e você obtém uma resolução de 0,1Hz, e o contador precisa apenas subir até 1000, nem mesmo 10 bits. Em seguida, de 100Hz a 10kHz, conte por intervalos de 1 segundo; você obtém apenas uma resolução de 1 Hz, mas seu contador precisa executar até 10.000 ainda menores que 16 bits. O intervalo superior de 10kHz a 1MHz pode contar por apenas 0,01 s, e a contagem máxima ainda seria de apenas 10.000 e, embora a sua resolução fosse 100Hz, seria uma precisão razoável.


Sim, eu mencionei que, na atualização da minha pergunta (anterior), eu teria que contar até 4 segundos por ... e sim, eu gostaria de poder diferenciar entre, digamos, 500.000,00Hz e 500.000,25Hz. Eu tinha pensado em usar faixas diferentes, eu poderia facilmente vincular isso ao resto do hardware, pois o sinal tem 6 faixas selecionáveis, então eu provavelmente poderia projetar um codificador simples de 6 a 3 para indicar qual faixa ... mas eu não sou se necessário, se eu usar um contador de hardware juntamente com um tempo de atualização de 4 segundos, isso deve resolver os problemas em cada extremidade do espectro
voltando

5

Você pode misturar um contador de hardware e software contando os estouros do contador de hardware em um ISR.

A contagem de cada extremidade do sinal em um ISR será muito lenta para um sinal de 1 MHz. Eu acho que você poderia fazer até 50kHz dessa maneira.


Sim, você provavelmente está certo - será muito lento para 1MHz, mas eu imaginaria que um processador RISC de 20MIPS poderia fazer melhor que 50KHz. De qualquer forma, eu também estava pensando em registrar um contador binário de 8 bits com o sinal e conectar a execução do contador ao pino de interrupção externo do MCU, e depois ler a frequência do sinal enquanto a soma do bit de transporte interrompe mais a contagem o / p valor do contador a cada n segundos, acho que é isso que você estava conseguindo quando disse uma combinação de contadores de hardware e software.
volting

Eu acho que o OP estava se referindo ao contador de hardware embutido. Todos eles têm interrupções de transbordamento que podem ser usadas para melhorar o intervalo de contagem.
JPC

@ starblue, o código que escrevi abaixo é o que você tinha em mente com sua resposta?
vicatcu

2

Em vez de fazer um contador de 1 segundo, faça um contador de 0,1 segundo e multiplique a contagem por 10?

Se for apenas uma questão de armazenar o número do contador, você não pode usar um código adicional para acompanhar quando o contador está prestes a transbordar e gravar em outro local de memória para manter o registro?


2
Eu acho que devo ter congelado o cérebro .. a solução mais simples que eu acho é apenas incrementar uma variável do tipo long int toda vez que uma borda crescente é detectada. Leia esse valor uma vez a cada segundo e, em seguida, redefina-o para zero.
volting

2
Na verdade, precisarei ler o valor a cada 4 segundos para medir até 0,25Hz
voltando

2

Você não pode simplesmente usar a captura de entrada de um temporizador de 16 bits e as interrupções de transbordamento (mais uma variável) para fazer a medição? Aqui está como eu faria isso com o ATTiny24A com AVR-GCC (não testado e possivelmente com erros, é claro):

#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;
} 

... de qualquer forma, compila :)


EDIT Eu olhei para a saída do arquivo lss do meu código, e o código gerado tem muitas instruções para não tropeçar em 1MHz com um relógio de 8MHz ... até o simples incremento de uma linha no TIM1_OVF_vect gera 19 instruções! Portanto, para lidar com eventos de 1 MHz, você definitivamente precisaria otimizar, provavelmente registrar alocar algumas coisas (provavelmente num_overflows e capture_value_ticks), usar o assembler inline (roubar as coisas importantes do arquivo lss) e mover o processamento das interrupções para o principal loop sempre que possível.


Medir uma frequência usando o período funciona muito bem com formas de onda lentas (você conta com o timer interno muito mais rápido que o sinal externo), mas atinge rapidamente um limite à medida que a frequência do sinal de entrada aumenta. Basicamente, como você descobriu, o tempo gasto dentro da interrupção da captura do timer se torna dominante; não resta tempo para que outras partes do código sejam executadas. Embora eu não esteja tão familiarizado com o ATTiny, uma rápida olhada na folha de dados mostra que o timer / counter1 suporta a contagem de eventos externos; portanto, deixe o hardware lidar com a contagem.
timrorr

@timrorr, uau, sim, essa é a maneira mais inteligente de fazê-lo :) Postei o código AVR-GCC atualizado em um post separado. Gostaria de dar uma olhada e ver o que você pensa?
vicatcu

2

Publicar este código como uma alternativa por sugestão de @ timrorr à minha postagem anterior. Isso é compilado para o ATTiny24A usando o padrão da linguagem c99, mas na verdade não o testei além disso.

#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;
}

Esse é um ótimo uso dos recursos de hardware do Timer1 e libera uma tonelada de ciclos de processamento em comparação com a minha postagem original.


2
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.