8 votos

Medición de 0 - 1MHz (resolución de 0,25Hz) de onda cuadrada utilizando una MCU

Necesito medir la frecuencia de la onda cuadrada que puede variar entre 0 y 1MHz, y tiene una resolución de 0,25Hz.

Todavía no he decidido qué controlador, pero lo más probable es que sea uno de los Attiny de 20 pines.

Normalmente, la forma en que mediría las señales de baja frecuencia sería utilizando dos temporizadores, uno configurado en modo de captura de temporizador para interrumpir, por ejemplo, en los bordes ascendentes de la señal externa y otro temporizador configurado para interrumpir cada segundo, por lo que el valor del registro del contador de los primeros temporizadores después de 1 segundo sería igual a la frecuencia de la señal.

Sin embargo, este método obviamente no funcionará para capturar señales que oscilen entre 0 y 1MHz con una resolución de 0.25Hz para esto necesitaría un contador de 22Bit (AFAIK los micros de 8bit solo tienen contadores de 8/16bit).

Una idea que tenía era dividir la señal antes de aplicarla al micro, pero esto sería impracticable, ya que la señal tendría que ser dividida por 61, por lo que la frecuencia sólo podría actualizarse cada 61 segundos, cuando me gustaría que fuera cada pocos segundos.

¿Existe otro método que permita actualizar la frecuencia, por ejemplo, cada 4 segundos?


Actualización:

La solución más sencilla es utilizar una interrupción externa o una captura del temporizador para interrumpir en el flanco de subida de la señal y tener el isr incrementar una variable de tipo long int . Lea la variable cada 4 segundos (para permitir que se midan frecuencias de hasta 0,25Hz).


Actualización 2:

Como ha señalado JustJeff, una MCU de 8 bits no podrá seguir el ritmo de una señal de 1MHz, por lo que se descarta la interrupción en cada flanco ascendente y el incremento de un long int ...

He elegido el método sugerido por timororr. Una vez que lo ponga en práctica, enviaré un mensaje y compartiré los resultados. Gracias a todos por sus sugerencias.


Informe de progreso:

He empezado a probar algunas de las ideas presentadas aquí. En primer lugar he probado el código de vicatcu. Hubo un problema obvio de TCNT1 no se borra después de la frecuencia se calcula - no es un gran problema ...

Luego me di cuenta al depurar el código que aproximadamente cada 2 a 7 veces que se calculaba la frecuencia la cuenta de desbordamiento del temporizador 1 (el temporizador configurado para contar los eventos externos) se quedaba corta por dos. Lo achaqué a la latencia del ISR del temporizador 0 y decidí trasladar el bloque de la sentencia if del ISR al main (ver el fragmento de abajo) y simplemente poner una bandera en el ISR. Un poco de depuración mostró que la primera medición estaría bien pero con cada lectura subsiguiente la cuenta de desbordamiento del Timer 1 se sobrepasaría en 2. Lo cual no puedo explicar - yo hubiera esperado que estuviera por debajo y no por encima...

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

        .....
    }
}

A continuación decidí que intentaría poner en práctica la sugerencia de timrorrs. Para generar el intervalo necesario (de aproximadamente 15ms entre cada interrupción timer_isr) tendría que conectar en cascada los dos temporizadores de 8 bits ya que el único temporizador de 16 bits del Atmega16 está siendo utilizado para capturar los flancos de subida de la señal externa.

Pensé que esta solución funcionaría y sería mucho más eficiente ya que la mayor parte de la sobrecarga se desplaza a los temporizadores y sólo queda un isr corto para que la cpu lo maneje. Sin embargo, no fue tan preciso como esperaba, las mediciones se desplazaron hacia adelante y hacia atrás por aproximadamente 70Hz que no me importa en las frecuencias altas, pero definitivamente no es aceptable en las frecuencias más bajas. No pasé mucho tiempo analizando el problema, pero supongo que la disposición en cascada del temporizador no es tan precisa ya que he implementado un arreglo similar a la sugerencia de timrorrs en un controlador 8051 mucho más lento que tenía 2 temporizadores de 16 bits y los resultados fueron bastante precisos.

Ahora he vuelto a la sugerencia de vicatcu, pero he trasladado el cálculo de la frecuencia a la isr del temporizador 0 (véase el fragmento siguiente ), este código ha producido mediciones consistentes y razonablemente precisas. Con un poco de calibración, la precisión debería ser de 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;
    }                                                 
}       

Si alguien tiene alguna otra sugerencia estoy abierto a ellas aunque prefiero no tener que usar rangos... Además, ya no quiero conseguir una resolución del 0,25%, no parece que tenga mucho sentido con el nivel de precisión que tengo en este momento.

8voto

JW. Puntos 145

¿No se puede utilizar la captura de entrada y las interrupciones de desbordamiento de un temporizador de 16 bits (más una variable) para realizar la medición? Así es como yo lo haría con el ATTiny24A con AVR-GCC (no probado y potencialmente con errores, por supuesto):

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

... en cualquier caso, se compila :)


EDITAR He mirado la salida del archivo lss de mi código, y el código generado tiene demasiadas instrucciones para no tropezar consigo mismo a 1MHz con un reloj de 8MHz... ¡incluso el simple incremento de una línea en el TIM1_OVF_vect genera 19 instrucciones! Así que para manejar los eventos de 1MHz, definitivamente tendrías que optimizar, probablemente registrar algunas cosas (probablemente num_overflows y capture_value_ticks), usar ensamblador en línea (robar las cosas importantes del archivo lss), y mover el procesamiento fuera de las interrupciones y dentro del bucle principal siempre que sea posible.

3voto

mahler Puntos 161

Si es posible, yo sugeriría seleccionar un microcontrolador que soporte una operación de contador usando las entradas del temporizador; en lugar de incrementar manualmente un contador dentro de un ISR (que a altas frecuencias rápidamente termina saturando la actividad del microcontrolador) permites que el hardware maneje el conteo. En este punto tu código se convierte simplemente en una cuestión de esperar tu interrupción periódica y luego calcular la frecuencia.

Para ampliar el rango y hacer que el contador de frecuencias sea más generalizado (eliminando la necesidad de múltiples rangos a costa de un poco más de trabajo para la MCU) podrías utilizar la siguiente técnica.

Seleccione una tasa de interrupción periódica que permita la precisión de la medición a la frecuencia de entrada más alta; esto debe tener en cuenta el tamaño de su contador (necesita seleccionar el período del temporizador de tal manera que el contador del temporizador no se desborde a la frecuencia de entrada máxima). Para este ejemplo asumiré que el valor del contador de entrada puede ser leído desde la variable "timer_input_ctr".

Incluya una variable para contar las interrupciones periódicas (debe ser inicializada a 0 en el arranque); para este ejemplo me referiré a esta variable como "isr_count". El periodo de interrupción está contenido en la constante "isr_period".

Su interrupción periódica debe 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 ejemplo se basa en algunas matemáticas de punto flotante que pueden no ser compatibles con los microcontroladores de gama baja, hay técnicas para superar esto, pero están fuera del alcance de esta respuesta.

2voto

JW. Puntos 145

Publicando este código como alternativa por la sugerencia de @timrorr a mi post anterior. Esto compila para el ATTiny24A utilizando el estándar de lenguaje c99, pero en realidad no lo he probado de ninguna manera más allá de eso.

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

Este es un buen uso de las capacidades de hardware del Timer1 y libera una tonelada de ciclos de procesamiento en comparación con mi post original.

0voto

SQLMenace Puntos 68670

Quizás quieras considerar tener dos (o más) gamas. Los problemas de captación de frecuencias muy bajas son algo diferentes a los problemas de las más altas. Como ya has señalado, en el extremo superior de tu rango tienes problemas de desbordamiento del contador.

Pero considere que en el extremo inferior de su rango, su precisión se verá afectada por no tener suficiente cuenta en el registro. No estoy seguro de si realmente quieres discriminar entre 0,25Hz y 0,5Hz, pero si lo haces, entonces tendrás que contar durante cuatro segundos para hacerlo.

Además, especificar una resolución plana de 0,25Hz, interpretada estrictamente, significa que serías capaz de discernir 500.000,00Hz de 500.000,25Hz, lo cual es un grado de precisión bastante alto.

Por estas razones, el diseño de rangos distintos podría aliviar el problema del tamaño del contador. Tomando números al azar para el ejemplo, para el extremo inferior, digamos de 0 a 100Hz, cuenta para intervalos de 10 segundos, y obtienes una resolución de 0,1Hz, y tu contador sólo necesita llegar hasta 1000, ni siquiera 10 bits. Luego, de 100Hz a 10kHz, cuenta para intervalos de 1 segundo; sólo obtienes una resolución de 1Hz, pero tu contador sólo necesita llegar hasta 10.000, todavía más pequeño que 16 bits. El rango superior de 10kHz a 1MHz podría contar por sólo 0.01 seg, y el conteo máximo seguiría siendo sólo de 10,000 y aunque tu resolución sería de 100Hz, esto sería una precisión razonable.

0voto

reconbot Puntos 1670

Puedes mezclar un contador hardware y uno software contando los desbordamientos del contador hardware en un ISR.

Contar cada flanco de la señal en un ISR será demasiado lento para una señal de 1 MHz. Creo que se podría hacer hasta unos 50kHz de esa manera.

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X