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

Necesito medir la frecuencia de onda cuadrada que puede variar entre 0 y 1MHz, y tiene una resolucion 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 frecuencia más baja sería mediante el uso de dos temporizadores, uno configurado en el modo de captura del temporizador para interrumpir, por ejemplo, los flancos ascendentes de la señal externa y otro temporizador configurado para interrumpir cada segundo, por lo tanto, los temporizadores anteriores registran el valor 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 oscilan entre 0 y 1MHz con una resolución de 0.25Hz para esto, necesitaría un contador de 22 bits (AFAIK micros de 8 bits solo tienen contadores de 8/16 bits).

Una idea que tuve fue dividir la señal antes de aplicarla al micro, pero esto no sería práctico ya que la señal tendría que dividirse por 61, por lo tanto, la frecuencia solo podría actualizarse cada 61 segundos donde me gustaría que fuera cada pocos segundos. .

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


Actualizar:

La solución más simple es utilizar una interrupción externa o una captura de temporizador para interrumpir en el flanco ascendente de la señal y hacer que el isrincremento sea una variable de tipo long int. Lea la variable cada 4 segundos (para permitir la medición de frecuencias de hasta 0,25 Hz).


Actualización 2:

Como señaló JustJeff, una MCU de 8 bits no podrá mantenerse al día con una señal de 1 MHz, por lo que descarta la interrupción en cada flanco ascendente e incrementa un long int...

He elegido el método sugerido por timororr. Una vez que llegue a implementarlo, publicaré de nuevo y compartiré los resultados. Gracias a todos por sus sugerencias.


Informe de progreso:

He comenzado a probar algunas de las ideas presentadas aquí. Primero probé el código de vicatcu. Hubo un problema obvio de que TCNT1 no se eliminó después de calcular la frecuencia, no es gran cosa...

Luego, al depurar el código, me di cuenta de que aproximadamente cada 2 a 7 veces la frecuencia se calculaba en el temporizador 1 (el temporizador configurado para contar eventos externos), el recuento de desbordamiento se acortaría en dos. Atribuí esto a la latencia del ISR del temporizador 0 y decidí mover el bloque de declaración if del ISR al principal (ver el fragmento a continuación) y simplemente establecer una bandera en el ISR. Algunas depuraciones mostraron que la primera medición estaría bien, pero con cada lectura subsiguiente, el conteo de desbordamiento del temporizador 1 terminaría en 2. Lo cual no puedo explicar, 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 implementar la sugerencia de timrorrs. Para generar el intervalo necesario (de aproximadamente 15 ms entre cada interrupción de timer_isr), tendría que conectar en cascada los dos temporizadores de 8 bits, ya que el único temporizador de 16 bits en el Atmega16 se utiliza para capturar los flancos ascendentes 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 transfiere a los temporizadores y solo queda un isr corto para que lo maneje la CPU. Sin embargo, no fue tan preciso como esperaba, las mediciones cambiaron de un lado a otro aproximadamente 70 Hz, lo que no me importaría en frecuencias altas, pero definitivamente no es aceptable en frecuencias más bajas. No dediqué mucho tiempo a analizar el problema, pero supongo que la disposición en cascada del temporizador no es tan precisa, ya que implementé una disposición 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 movido el cálculo de frecuencia al Timer 0 isr (vea el fragmento a continuación ), este código ha producido mediciones consistentes y razonablemente precisas. Con un poco de precisión de calibración, debería ser de aproximadamente +/-10 Hz.

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, pero prefiero no tener que usar rangos... Tampoco tengo la intención de obtener una resolución del 0,25 %, no parece tener mucho sentido con el nivel de precisión que tengo en este momento. .

Hay una manera relativamente fácil de hacer esto utilizando una interrupción de captura en un PIC y el temporizador 1 funcionando a una velocidad muy alta. Si todavía está interesado en otros métodos, hágamelo saber y puedo resumirlo en una respuesta.
Todavía no he comenzado a trabajar en esto, así que sí, todavía estoy interesado.
Por alguna razón, nunca me dejó saber que había comentado mi comentario.
@Kortuk: El software solo te notifica si dejo un comentario en una de tus respuestas o preguntas. También podría notificarle este comentario, porque puse @Kortuk delante. Pero ese es un cambio de software de StackOverflow, y no sé si se filtró en el código base de StackExchange o no.
no, no me avisó que habías respondido, ni siquiera con el @kortuk. Sin preocupaciones. Parece que se ha encontrado una respuesta.

Respuestas (7)

Si es posible, sugeriría seleccionar un microcontrolador que admita una operación de contador utilizando 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), permite que el hardware maneje el conteo. En este punto, su código simplemente se convierte en una cuestión de esperar su interrupción periódica y luego calcular la frecuencia.

Para extender el rango y hacer que el contador de frecuencia sea más generalizado (eliminando la necesidad de múltiples rangos a expensas de un poco más de trabajo para la MCU), puede usar la siguiente técnica.

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

Incluya una variable para contar interrupciones periódicas (debe inicializarse en 0 al inicio); para este ejemplo me referiré a esta variable como "isr_count". El período de interrupción está contenido en la constante "isr_period".

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

Excelente timororr, eso hará exactamente lo que quiero sin costo de circuitos integrados adicionales, lo cual siempre es bueno, creo que me apresuré a descartar la posibilidad de resolver el problema en el software. Gracias
@timrorr, estoy interesado en sus pensamientos sobre mi respuesta a continuación si tiene ganas de leerla

Es posible que desee considerar tener dos (o más) rangos. Los problemas con la captura de frecuencias muy bajas son algo diferentes de los problemas con las más altas. Como ya ha notado, en el extremo superior de su rango tiene problemas de desbordamiento del contador.

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

Además, especificar una resolución plana de 0,25 Hz, estrictamente interpretada, significa que podrá distinguir 500 000,00 Hz de 500 000,25 Hz, que es un grado de precisión bastante alto.

Por esas razones, el diseño para distintos rangos podría aliviar el problema del tamaño del mostrador. Sacando números al azar por ejemplo, para el extremo inferior, digamos de 0 a 100 Hz, cuente durante intervalos de 10 segundos y obtendrá una resolución de 0,1 Hz, y su contador solo necesita subir a 1000, ni siquiera 10 bits. Luego, de 100 Hz a 10 kHz, cuente por intervalos de 1 segundo; solo obtiene una resolución de 1 Hz, pero su contador solo necesita ejecutar hasta 10,000 aún más pequeños que 16 bits. El rango superior de 10kHz a 1MHz podría contar por solo 0.01 segundos, y el conteo máximo aún sería de solo 10,000 y aunque su resolución sería de 100Hz, esta sería una precisión razonable.

Sí, mencioné que en la actualización de mi pregunta (anterior) tendría que contar hasta 4 segundos para... y sí, me gustaría poder diferenciar entre, digamos, 500 000,00 Hz y 500 000,25 Hz. Había pensado en usar diferentes rangos, podría vincular esto fácilmente con el resto del hardware, ya que la señal tiene 6 rangos seleccionables, por lo que probablemente podría diseñar un codificador simple de 6 a 3 para indicar qué rango... pero no estoy seguro que sería necesario si uso un contador de hardware junto con un tiempo de actualización de 4 segundos, esto debería solucionar los problemas en cualquier extremo del espectro

Puede mezclar un contador de hardware y software contando los desbordamientos del contador de hardware en un ISR.

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

Sí, probablemente tengas razón: será demasiado lento para 1 MHz, pero me imagino que un procesador RISC de 20 MIPS podría funcionar mejor que 50 KHz. De todos modos, también estaba considerando cronometrar un contador binario de 8 bits con la señal y conectar el acarreo del contador al pin de interrupción externo de la MCU, luego leer la frecuencia de la señal como la suma de las interrupciones del bit de acarreo más el conteo o/p valor del contador cada n segundos, supongo que eso es a lo que se refería cuando dijo una combinación de contadores de hardware y software.
Creo que el OP se refería al contador de hardware incorporado. Todos tienen interrupciones de desbordamiento que se pueden usar para mejorar el rango de conteo.
@starblue, ¿el código que escribí a continuación es lo que tenía en mente con su respuesta?

En lugar de hacer un contador de 1 segundo, conviértalo en un contador de 0,1 segundos y multiplique el conteo por 10.

Si solo se trata de almacenar el número del contador, ¿no puede usar un código adicional para realizar un seguimiento de cuándo el contador está a punto de desbordarse y escribir en otra ubicación de la memoria para llevar la cuenta?

Creo que debo haberme congelado el cerebro ... creo que la solución más simple es simplemente incrementar una variable de tipo long int cada vez que se detecta un borde ascendente. Lea ese valor una vez cada segundo y luego reinícielo a cero.
En realidad, tendré que leer el valor cada 4 segundos para medir hasta 0,25 Hz.

¿No puede simplemente usar la captura de entrada de un temporizador de 16 bits y las interrupciones de desbordamiento (más una variable) para hacer la medición? Así es como lo haría con el ATTiny24A con AVR-GCC (no probado y potencialmente defectuoso, 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, compila :)


EDITAR Miré la salida del archivo lss de mi código, y el código generado tiene demasiadas instrucciones para no tropezarse a 1 MHz con un reloj de 8 MHz... ¡incluso el simple incremento de una línea en TIM1_OVF_vect genera 19 instrucciones! Entonces, para manejar eventos de 1 MHz, definitivamente necesitaría optimizar, probablemente registrar, asignar 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 al principal bucle siempre que sea posible.

La medición de una frecuencia usando el período funciona bastante bien con formas de onda lentas (depende de que el temporizador interno sea mucho más rápido que la señal externa), pero alcanza un límite rápidamente a medida que aumenta la frecuencia de la señal de entrada. Básicamente, como ha descubierto, el tiempo pasado dentro de la interrupción de captura del temporizador se vuelve dominante; no queda tiempo para que se ejecuten otras partes del código. Si bien no estoy tan familiarizado con ATTiny, un vistazo rápido a la hoja de datos muestra que el temporizador/contador1 admite el conteo de eventos externos, así que deje que el hardware maneje el conteo.
@timrorr, wow, sí, esa es una forma mucho más inteligente de hacerlo :) Publiqué el código AVR-GCC actualizado en una publicación separada. ¿Te importaría echar un vistazo y ver lo que piensas?

Publicar este código como una alternativa según la sugerencia de @timrorr a mi publicación anterior. Esto se compila para ATTiny24A usando 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 publicación original.