Tacómetro y efecto no deseado en el código.

Hola a todos, os pongo en situación.

Realicé un tacometro con un display 1602, un sensor óptico ranurado y dos relés
de salida: uno para mínima velocidad y otro para máxima. Las vueltas son pocas,
en regimen de trabajo son 375 rpm y los reles se activan a 350 y 380 rpm. En
teoría está trabajando perfectamente ya unos años.

Pero ahora, me encontré con la necesidad de hacer uno nuevo. En este caso con las
mismas características, pero sustityendo la pantalla de cristal liquido por un
display de 7 segmentos; entre otras cosas para que se vea a distancia.

Así que hice el diseño de la PCB en dos partes. Por un lado el display, botones
para interfaz y un par de leds indicadores. Por otro lado la placa con un atmega328p,
la entrada del sensor, fuente y salidas de relé. Monté la placa y me puse a hacer
pruebas.

Aquí empieza la aventura.

Conectas el tacómetro y parece que bien, pero de vez en cuando el led indicador de
mínima se enciende, subes las rpm y la cosa empeora, a tal punto que cuando está
por encima del máximo se vuelven locos los reles. La cosa fue a peor. Para ver que
relé es el que se enciende, coloqué un led en sus contactos que se enciende al
cerrarlos. Más loco aún.

De momento en el hardware he llegado a la conclusión de que tengo un mal diseño de
placa y alguna línea esta cerca de algo que no debería. No es lógico que un simple
led te provoque un problema gordo. Toca hacerlo de nuevo.

Pero claro, antes hice pruebas de software por si en algún sitio habia metido la
pata. Así que armé en la protoboard un display, un adaptador de señal para el generador
de funciones y el arduino para ver que ocurre, he hice el siguiente programa:

uint32_t tactual;    // Coge el tiempo en el que se produce un flanco.
uint32_t tanterior;  // Guardará el tiempo actual para poder restar en el 
                     // siguiente flanco.
uint32_t periodo;    // Tiempo entre flancos.
float    frecuencia; // La inversa del periodo

// Se ejecuta en cada flanco ascendente de la señal, leyendo el momento en el
// que se produce con micros y restando el tiempo transcurrido desde el pulso
// anterior obteniendo el periodo.
void rutinaInterrupcion() {
  tactual = micros();
  periodo = tactual - tanterior;
  tanterior = tactual;
}

// Configuración.
void setup() {
  Serial.begin(9600);
  // Usamos el pin 2 que se corresponde con la interrupción 0, asi que lo 
  // configuramos con entrada y aprovechamos la resistencia interna de pull-up
  pinMode(2, INPUT_PULLUP); 

  // Ajustamos el vector de interrupción a nuestra función.
  attachInterrupt(0, rutinaInterrupcion, RISING);
}

// loop.
void loop() {
  // Para evitar dividir por cero comprobamos que el periodo no sea 0.
  if ( periodo == 0 ) frecuencia = 0;

  // Cuando la frecuencia es muy baja no se produciran interrupciones y puede
  // que el periodo se quede fijo, por eso observamos micros y la ultima vez 
  // que lo guardamos, si el tiempo es muy alto lo ponemos a 0. Si no 
  // calculamos la inversa del periodo para obtener la frecuencia.
  if ( micros()-tactual > 1000000 ) frecuencia = 0;
  else
     // Importante!! Ese millon debe ser un float, si ponemos un entero hará
     // una división entera y nos truncará la frecuencia.
     frecuencia = 1000000.0 / periodo;

  // A partir de aquí, podemos mostrar la información en un display, serie, y
  // realizar acciones con el código. 
  if ( frecuencia == 0 ) {
    Serial.println(F("*"));
  }
}

Cómo veis el código es muy simple.

Por un lado la funcion rutinaInterrupción, lo unico que hace es medir el tiempo
que ha transcurrido entre pulsos, guardando el valor en la variable periodo. He
medido la distancia entre pulsos, ya que si la maquina gira a 375 rpm, nos da una
frecuencia de 6.25Hz.

Por otro lado en el código, para obtener la frecuencia, hago la inversa y para
obtener hertzios utilizo 1000000.0. Hasta ahi bien.

¿Cómo controlo cuando la máquina va muy despacio? En el loop se compara el tiempo
transcurrido desde que se actualizó la variable, si es superior a un tiempo se
pone la frecuencia a cero. Si no hiciera esto cuando la máquina este parada en el
display se mostrará la última velocidad y no cero.

El código funciona bien y tengo una precisión de 0.01 Hz que casi no se nota. Pero
ocurre algo misterioso que no llego a comprender bien y es donde haber si podéis
iluminarme.

Exporadicamente la condición:

if ( micros()-tactual > 1000000 ) frecuencia=0;

Se cumple y entonces se imprime el "*" por el puerto serie. Tened en cuenta que
ocurre a regimenes de frecuencia fijos donde el periodo es mucho menor a ese millón
de microsegundos.

Cosas:

  • El generador de señales es chinesco y dudo mucho que sea preciso a altas frecuencias,
    pero hasta 100Hz si lo es.

  • La salida del generador es una señal cuadrada de 20V de amplitud que ataca el led de
    un optoacoplador. La salida de dicho optoacoplador se conecta al pin D2 usando la
    resistencia interna de PULL-UP, para pruebas sin condesandor de filtro.

  • El tiempo entre "*" es también un poco aleatorio, no parece ser constante, si parece
    que se ve afectado por la frecuencia.

  • En el ejemplo no he declarado las variables de la rutina como "volatile", pero también
    lo he probado obteniendo el mismo resultado.

  • Para frecuencias pequeñas creo que es mejor medir distancia entre pulsos. Podría
    medir número de pulsos en un intervalo de tiempo, pero para obtener precisión debería
    tener mas sensores o un tiempo de intervalo muy alto.

  • En el código de ejemplo he quitado el código del display, dado que no es importante
    para la comprensión del código y lo único que hace es mostrar la frecuencia.

Supongo que el problema debe venir de alguna situación donde se produce la interrupción
justo cuando se hace la comparación, aún así, no logro ver como puede afectar.

PD. luego subo fotos y esquemas del circuito.

Una primer observación puede ser muy simple pero tal vez ayude:

En lugar de

if ( micros()-tactual > 1000000 ) frecuencia = 0;

usa

if ( micros()-tactual > 1000000UL ) frecuencia = 0;

y con ello no le complicas la vida al compilador.

Sigo observando y responderé en este mismo post como hago siempre.

Edito 1: también aquí aunque tal vez tenga menos peso

if ( periodo == 0 ) frecuencia = 0;
if ( periodo == 0UL ) frecuencia = 0.0; // lo importante es el UL para tener una buena comparación que dudo falle pero..

Para frecuencias pequeñas creo que es mejor medir distancia entre pulsos. Podría
medir número de pulsos en un intervalo de tiempo, pero para obtener precisión debería
tener mas sensores o un tiempo de intervalo muy alto.

Para todo lo que esta debajo de 1khz siempre conviene medir distancia entre pulsos con un tick o TIMER a toda velocidad pero es un poco lo que haces.
Si quiere mejorar usa el TIME_CAPTURE del pin 7 del ATmega328 y con el podrás hacerlo del mejor modo. Nick Gammon en su página lo tiene todo resuelto y muy bien explicado.
Te facilitaría mucho las cosas y al usar TIMER te alejas de las incertidumbres de si es esto o lo otro.

De todos modos es curioso el problema.

Edito 2: viendo la pagina de Nick Gammon, ahora no me convencen las soluciones.
Me disculpo por eso Victor.
Lo que veo a baja frecuencia no me parece preciso pero hace falta un poco mas de trabajo. Recuerdo haber trabajado en el pasado con alguien en este tema y finalmente logramos o logró medir bien.
Ya lo encuentro y comparto.

Edito 3: bueno, se nota que no es buena mañana para mi.
Mirando la rutina de INPUT_CAPTURE veo que es lo que necesitas.

Put another way, it captured an interval of 5 µs (80 x 62.5 ns). Since it takes about 2.5 µs to enter and leave an ISR (interrupt service routine) then this sounds about right (you need two interrupts to capture the interval: the "start of interval" interrupt and the "end of interval" interrupt).

Los ticks de 62.5 o sea el ciclo de reloj del arduino te dan la mejor precisión.
Lo que me confundió fuen una lectura a 200khz y solo 80 cuentas pero en tu caso serán muchísimas y tendras la mejor precisión en la lectura.
Sigue por ahi si te parece la idea.

Aquí dejo una foto de la protoboard, para que veais el montaje de pruebas, midiendo una frecuencia de 50 Hz, en pantalla imprime 4999 (o sea que la frecuencia medida es 49.99Hz).

IMG_20210224_151741.jpg

Y el esquema sería más o menos este (omito todas las conexiones del display).

montaje.jpg

El código completo, incluidas las nuevas sugerencias de Surbyte.

/* PRUEBA DE MEDIDA DE FRECUENCIA */
#include <LiquidCrystal.h>

LiquidCrystal lcd(A5,A4,A3,A2,A1,A0);

volatile uint32_t tactual;
volatile uint32_t tanterior;
volatile uint32_t periodo=0xffff;
float    frecuencia;

char     linea[16];

int rpm;
int minrpm=350;
int maxrpm=380;
uint32_t  t;

void isr() {
  tactual = micros();
  periodo = tactual - tanterior;
  tanterior = tactual;
}

unsigned int onDelayTimer(bool condicion, unsigned long &timer, unsigned long duracion) {
  if (condicion == false) { timer=0; return 0; }                   
  else                     
    if (timer == 0) { timer = millis(); return 0; }                 
    else              
      if (millis() - timer >= duracion ) return 1;  
      else return 0;
}

void setup() {
  Serial.begin(115200);
  pinMode(2,INPUT_PULLUP);
  lcd.begin(20,4);
  pinMode(7, OUTPUT);
  digitalWrite(7,HIGH);
  attachInterrupt(0, isr, RISING);
}

void loop() {
  if ( periodo == 0UL ) frecuencia = 0;
  if ( micros()-tactual > 1000000UL ) frecuencia = 0;
  else
    frecuencia = 1000000.0 / periodo;
  rpm = ceil(frecuencia*100);
  lcd.setCursor(0,0);
  sprintf(linea, "%8d", rpm);
  lcd.print(linea);
  if ( rpm < minrpm  ) 
    digitalWrite(7,LOW);
  else
    digitalWrite(7,HIGH);
   
  if ( rpm == 0 ) {
    Serial.print(F("*"));
  }
}

Sigue habiendo el mismo problema, mucho * en pantalla.

IMG_20210224_151741.jpg

montaje.jpg

Otro enfoque, cuando es posible que superes 1millon de micros = 1 segundo ?
La única posibilidad sería que se pierda pulsos, qué opinas?

Si se pierden pulsos es porque o tienes un problema a la entrada y digamos es el generador o a la salida.
Puedes con el generador olvidarte del opto, colocarlo a 5V y simplemente ver si se comporta mejor cuando lo conectas directamente a la entrada de interrupción?

Aqui dejo fotos del dichoso aparato que me ha hecho ver el fallo.

frontal.jpg

trasera.jpg

lateral.jpg

frontal.jpg

trasera.jpg

lateral.jpg

La verdad es que uso el optoacoplador para no meter tensión negativa en el arduino. Al generar la señal es una onda cuadrada, que puedo ajustar la amplitud, frecuencia, pero no me deja subir la tensión para generar una señal que sea compatible ttl., aunque si puedo usar otro arduino como generador de señales aunque no sea muy preciso :slight_smile: :slight_smile: :slight_smile:

Para descartar el optoacoplador he probado con otro Arduino. He realizado el siguiente montaje:

simulando.jpg

Como veis un arduino alimenta el otro, y sin mayor problemas. No me ha gustado unir los pines directamente y he colocado una resistencia para evitar "problemas".

He usado el siguiente código en el generador:

/* GENERADOR DE FRECUENCIA SIMPLE CON ARDUINO */

float f = 10.00;
float periodoS;
uint32_t periodo;

void setup() {
  // put your setup code here, to run once:
  pinMode(13, OUTPUT);
  periodoS = 1.0 / f;
  periodo = periodoS * 1000;
  periodo = periodo>>1;
}

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(13, HIGH);
  delay(periodo);
  digitalWrite(13, LOW);
  delay(periodo);
}

No os asusteis que haya usado delay, simplemente he ajustado el programa del blink para que parpadee cambiando tan solo el valor de la variable f (frecuencia). La señal no es muy precisa, por ejemplo, a 50Hz ya da unos 49 y algo, pero a 10 Hz es estable.

Aquí no hay acondicionamiento de señal, lo que se genera en uno está en el otro, TTL puro y duro.

He probado con y sin resistencia por si acaso y el resultado sigue siendo el mismo * de vez en cuando.

Asi que sospecho que debe ser cosa del código o de algo que ocurre cuando se produce la interrupción.

simulando.jpg

Hi,
victorjam recuerda que si no me equivoco la resistencia interna de "PULLUP" creo que es de 50K. En otra palabra estas usando la resistencia interna del pin D2 de 50K. Yo trataria de usar una resistencia externa de 10K. Si calcula la corriente usando una de 50k la corriente es de 5/50000 = .0001 amp

He probado con y sin resistencia por si acaso y el resultado sigue siendo el mismo * de vez en cuando.

Si eso te pasa a ti, entonces es un problema reproducible.
Intentaré hacerlo a ver que puedo comentarte de tenerlo en vivo y en directo.

Para Tauro: He probado en la protoboard cambiando INPUT_PULLUP por INPUT y colocando una resistencia externa de 10k y por bajar un poco una de 2k2. El resultado sigue siendo el mismo un "*" en pantalla.

De hecho, no me preocupa tanto el opto, ya que a las malas utilizaré un h11l1m que tiene salida trigger "smith" (que no se escribirlo :smiley: ) con lo cual la salida me garantizo una señal ttl dura y pura. Pero claro, estoy hablo de frecuencias irrisorias, como mucho no llega la señal a 100Hz y tengo pc817 funcionando para transimisión serie a 9600 baudios. Asi que el opto puede que tenga algo que ver, pero debería influir poco.

Ya me contarás Surbyte.

¿Cuantas ranuras tiene el disco? Es decir la ¿frecuencia de pulsos es igual a la de giro?

El disco va tener dos puntos de detección. Por lo tanto doble de pulsos por vuelta, para ser exactos en trabajo serian unos 12.5 Hz.

Si aumentara el número de ranuras, aumento la frecuencia y entonces convendría utilizar la otra alternativa: contar pulsos en determinado tiempo. Con la ayuda de la libreria TimerOne es fácil de hacer.

Pero no es el caso. Lo que aquí ocurre es algo que hace el código que a simple vista no parece lógico que ocurra.

Es más. Se me ha escapado una función en el segundo código llamada onDelayTimer (retraso al encendido). Con esta función compruebo si una condición es cierta durante un determinado tiempo. Así por ejemplo, el rele de maxima puedo hacer que se active solo si ha estado 200 ms activo.

digitalWrite(relemaxima, onDelayTimer( rpm < minrpm, temporizador, 200));

Con lo cual absorvo el fallo de que las rpm sean 0. De hecho lo he probado con el led y con solo 20 segundos y cuando se produce el fallo no se enciende. Pero claro... quiero saber que ocurre y sabiendolo quizás pueda solucionarlo.

Peguntaba, porque el generador de pulsos que implementaste (si no calcule mal) esta a 20 Hz. Me desconcertó el desplazamiento de bits en el codigo.

¿Esto?

periodo = periodo>>1;

Es dividir por dos.

El periodo es la inversa de la frecuencia. Pero el periodo es desde el inicio de la señal hasta el final, es decir, toda la "onda". Asi que divido el periodo en dos, para generar un ciclo de trabajo del 50%. Asi la señal esta en alto un tiempo y bajo el mismo tiempo.

Si no divido por dos, entonces al hacer el delay si estaría haciendo el doble de la frecuencia.

Pido perdón, pero desde que suspendí un examen por no haber hecho una división con desplazamientos, mi cerebro ha asumido que cuando vea un unsigned que haya que dividir por una potencia de dos, desplaza antes de dividir. Lo mismo para multiplicar.

volatile uint32_t tactual;
volatile uint32_t tanterior;
volatile uint32_t periodo=0xffff;
float    frecuencia;
int rpm;

void isr() {
  tactual = micros();
  periodo = tactual - tanterior;
  tanterior = tactual;
   
}


void setup() {
  // put your setup code here, to run once:
  pinMode(13, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(3), isr, RISING);
  Serial.begin(115200);
}

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(13, HIGH);
  delay(50);
  digitalWrite(13, LOW);
  delay(50);
   if ( micros()-tactual > 1000000UL ) frecuencia = 0;
    else     frecuencia = 1000000.0 / periodo;
   rpm = ceil(frecuencia*100);
   if ( rpm == 0 ) {
    Serial.print(F("*"));
  }
  Serial.println(rpm);
}

No pude repetir el error. Como solo tengo un arduino (al alcance de la mano) ,coloque un cable entre el pin 3 y el 13 (mega), se autointerrumpe :). Este código no me genera la impresión de "*", imprime casi de continuo 998

Probalo sin volatile a ver que hace @Peter.

Eliminando los volatile, no hay cambios.

interesante, llevando el delay hasta un segundo, se produce el error.

volatile uint32_t tactual;
volatile uint32_t tanterior;
volatile uint32_t periodo=0xffff;
float    frecuencia;
int rpm;

void isr() {
  tactual = micros();
  periodo = tactual - tanterior;
  tanterior = tactual;
   
}


void setup() {
  // put your setup code here, to run once:
  pinMode(13, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(3), isr, RISING);
  Serial.begin(115200);
}

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
   if ( micros()-tactual > 1000000UL ) frecuencia = 0;
    else     frecuencia = 1000000.0 / periodo;
   rpm = ceil(frecuencia*100);
   if ( rpm == 0 ) {
    Serial.print(F("*"));
  }
  Serial.println(rpm);
}

Lógico. Un segundo es un millón de microsegundos.

El limite que funciona sin generar el error es un delay de 499 milisegundos es decir 1 hz