Contador de horas de trabajo (por interrupción)

Hola a todos:
Actualmente me encuentro realizando un proyecto de telemetría para visualizar remotamente las horas de trabajo y posición de una máquina.

Todo el código correspondiente al envío de la información a la web lo hace sin ningún inconveniente, sin embargo tengo problemas con la adquisición de datos de las horas de trabajo.

La forma de contabilizar las horas de trabajo se realiza en base a una señal de activación (24 volts) de un relé de un motor eléctrico (conectado a una bomba hidráulica). Esta señal la adapto para tener 5 volts a la entrada digital 2 del Arduino Uno, que a su vez es utilizada como interrupción (0). Dicha interrupción detecta un flanco de subida de la señal de entrada (motor encendido), la cual comienza a contabilizar hasta que se detecte un flanco de bajada (motor apagado), el tiempo resultante entre la diferencia del flanco de bajada y subida se guarda en una variable que cada cierto tiempo se almacena en la memoria EEPROM.

El problema es que el programa funciona muy bien cuando lo emulo en mi casa bajo condiciones ideales de la señal de entrada (PWM), pero cuando lo llevo a la máquina, en algunos momentos (no siempre) contabiliza mal debido a que aveces al realizar movimientos hidráulicos simultáneos aparecen flancos de bajada y subida, siendo que el motor trabaja continuamente entre dichos movimientos. Esto causa que se produzcan desfases entre las horas de trabajo real y las que envía a la web el Arduino.

Lo que necesito mejorar del programa, es que a través de una interrupción (debido a que el programa es largo), el Arduino comience a contar sólo cuando la entrada sea "1" y no a través de flancos. Una vez que esté en "0" debe realizar la resta y posteriormente sumarlo a la contabilización general.

¿Alguien tiene alguna idea de como realizar esto?

A continuación, un extracto del programa donde se contabiliza el tiempo:

volatile unsigned long pwm_value = 0;
volatile unsigned long prev_time = 0;
float HourMeter = 223.0;
float hourcalc = 0.0;


void rising() {
  attachInterrupt(0, falling, FALLING);
  prev_time = millis();
}


void falling() {
  attachInterrupt(0, rising, RISING);
  pwm_value = millis()-prev_time;
  calculo();
}

void calculo (){

hourcalc = pwm_value / 3600000.0;
HourMeter = HourMeter +  hourcalc;
Serial.print(F("Segundos agregados a horometro:  ")); Serial.println(pwm_value / 1000.0);
Serial.print(F("Nuevo horometro:  ")); Serial.println(HourMeter, 6);

}


void setup()
{
Serial.begin(115200);
attachInterrupt(0, rising, RISING);
}

void loop()
{
// programa
}

Muchas gracias!!

Hola OHMero

Has probado a definir la INT0 con nivel y no con flancos ??

La idea seria usar la misma señal que usas ahora pero pasandola atraves de un inversor con lo cual tendrias dos señales opuestas: la actual y la del inversor, y esta ultima iria a la entrada de la interrupcion externa 1 (INT1).
Como ves en el codigo las ISR trabajan cada una por su lado y no tienes que andar redefiniendo el vector de interrupcion.

volatile unsigned long pwm_value = 0;
volatile unsigned long prev_time = 0;
float HourMeter = 223.0;
float hourcalc = 0.0;


void rising() {
//  attachInterrupt(1, falling, LOW);
  prev_time = millis();
}


void falling() {
//  attachInterrupt(0, rising, LOW);
  pwm_value = millis()-prev_time;
  calculo();
}

void calculo (){

hourcalc = pwm_value / 3600000.0;
HourMeter = HourMeter +  hourcalc;
Serial.print(F("Segundos agregados a horometro:  ")); Serial.println(pwm_value / 1000.0);
Serial.print(F("Nuevo horometro:  ")); Serial.println(HourMeter, 6);

}


void setup()
{
Serial.begin(115200);
attachInterrupt(0, rising, LOW);
attachInterrupt(1, falling, LOW);
}

void loop()
{
// programa
}

... y tres.

Por si decides probar la idea anterior y te ocurren cosas "raras", ahi va una modificacion del sketch.

Contempla la particularidad de que una interrupcion por nivel se mantiene tanto tiempo como dure el estado LOW en el pin:

volatile unsigned long pwm_value = 0;
volatile unsigned long prev_time = 0;
float HourMeter = 223.0;
float hourcalc = 0.0;

// ISR de la INT0
void rising() {
  detachInterrupt(0);      // Evitamos engancharnos a la interrupcion
  attachInterrupt(1, falling, LOW);   // Habilitamos el cambio para cuando suceda
  prev_time = millis();
}


// ISR de la INT1
void falling() {
  detachInterrupt(1);      // Evitamos engancharnos a la interrupcion
  attachInterrupt(0, rising, LOW);   // Habilitamos el cambio para cuando suceda
  pwm_value = millis()-prev_time;
  calculo();
}

void calculo (){

hourcalc = pwm_value / 3600000.0;
HourMeter = HourMeter +  hourcalc;
Serial.print(F("Segundos agregados a horometro:  ")); Serial.println(pwm_value / 1000.0);
Serial.print(F("Nuevo horometro:  ")); Serial.println(HourMeter, 6);

}


void setup()
{
Serial.begin(115200);
attachInterrupt(0, rising, LOW);
}

void loop()
{
// programa
}

Hola.
Lo primero que deberías mirar es cómo reducir esos flancos no deseados. Seguro que los expertos en hardware te pueden recomendar algún sistema de filtrado para evitar esos micropulsos.
Lo segundo, es que en una rutina de interrupción deben ejecutarse cosas básicas y que no requieran mucho tiempo de proceso. Por lo tanto en tu rutina falling sobra el llamado a la función cálculo, y sobre todo los serial.print que tienes en ella, que deberían hacerse en todo caso en el programa principal, y no dentro de una interrupción.
Por último, en lugar de alternar la interrupción entre falling/rising, sencillamente establécela en el setup como change (saltará en ambos flancos) y en la rutina de interrupción lees el pin, y dependiendo de si es low o high realizas el inicio o final de medición de tiempo.

Un contador de una máquina que al final lleva un conteo de dias:horas:minutos:segundos no se verá muy alterada por una rutina de debounce.
Yo no usará jamas una interrupción para inciar una cuenta con un rele que rebota!! Para qué hacerlo?
Usaría el pin 2 con una simple detección de 0 a 1 y luego como te dijo Alfaville o Noter verifico el estado 1 por 50 o 100 mseg que sacrifico y aseguro un buen comienzo.

Elimina la interrupción y usa un simple debounce (antirebote) y asunto solucionado.

Hola surbyte.

Segun lo que OHMero manifiesta:

El problema es que el programa funciona muy bien cuando lo emulo en mi casa bajo condiciones ideales de la señal de entrada (PWM), pero cuando lo llevo a la máquina, en algunos momentos (no siempre) contabiliza mal debido a que aveces al realizar movimientos hidráulicos simultáneos aparecen flancos de bajada y subida, siendo que el motor trabaja continuamente entre dichos movimientos. Esto causa que se produzcan desfases entre las horas de trabajo real y las que envía a la web el Arduino.

No es un problema de rebotes, sino de movimientos que originan flancos de subida y bajada, lo que lleva a confusion en la medida.

Lo que necesito mejorar del programa, es que a través de una interrupción (debido a que el programa es largo), el Arduino comience a contar sólo cuando la entrada sea "1" y no a través de flancos. Una vez que esté en "0" debe realizar la resta y posteriormente sumarlo a la contabilización general.

Y parece ser que lo que quiere es contar tiempos durante estados concretos, es decir medir el ancho de los impulsos que genera la actividad.

Es lo que a mi me parece, por supuesto, y en ese sentido le he indicado mi version de sketch.

Parece que tienes razon.
El mejor código que vi es el ultimo de Alfaville pero con una corrección.
calculo() no puede incluir envios al monitor serie dentro de una interrupción JAMAS!!!!

Que tal asi

volatile unsigned long pwm_value = 0;
volatile unsigned long prev_time = 0;
float HourMeter = 223.0;
float hourcalc = 0.0;
bool flag = false;

void setup() {
    Serial.begin(115200);
    attachInterrupt(0, rising, LOW);
}

void loop() {
    // programa
    if (flag) {
        Serial.print(F("Segundos agregados a horometro:  ")); 
        Serial.println(pwm_value / 1000.0);
        Serial.print(F("Nuevo horometro:  ")); 
        Serial.println(HourMeter, 6);
        flag = false;
    }
}

// ISR de la INT0
void rising() {
    detachInterrupt(0);      // Evitamos engancharnos a la interrupcion
    prev_time = millis();
    attachInterrupt(1, falling, LOW);   // Habilitamos el cambio para cuando suceda
}

// ISR de la INT1
void falling() {
    detachInterrupt(1);      // Evitamos engancharnos a la interrupcion
    pwm_value = millis()-prev_time;
    calculo();
    attachInterrupt(0, rising, LOW);   // Habilitamos el cambio para cuando suceda
}

void calculo () {
    hourcalc = pwm_value / 3600000.0;
    HourMeter = HourMeter +  hourcalc;

    flag = true;
}

Me parece acertado surbyte.

Solo hice un copy-paste del sketch de OHMero y modifiqué el tema de las interrupciones, sin pararme a analizar el resto del código.

Ahora solo falta que OHMero lo vea todo, pruebe lo que le parezca y nos cuente como ha ido :slight_smile:

Hola a todos:

Muchas gracias por cada respuesta que han enviado, la verdad es que con cada comentario la mente se abre un poco mas y nuevas ideas llegan!!

@Alfaville:
Gracias por las opciones que enviaste, durante esta semana las implementaré y te comento.

@noter:
Si no es en la ISR, dónde hago el cálculo cada vez que el motor se energice?, este tipo de máquinas funcionan esporádicamente y por tiempos no muy prolongados. Ej. Movimientos de 5 o 10 o 20 segundos cada 3 minutos. Si es así, debiera crear una rutina que detecte si hay aumento en la variable "pwm_value", sin embargo en la función loop está leyendo constantemente la posición GPS y enviando datos a la web, lo que podría provocar que no alcance a calcular el valor de "HourMeter" antes que se haga otro movimiento.

@surbyte:
En un principio pensé no hacerlo como ISR, pero tendría que estar preguntando por el estado 0 o 1 de una entrada muchas veces dentro de un programa de casi 900 líneas.
Respecto a la función print, he leído muchos consejos como el tuyo de no usarlo, pero no he tenido inconvenientes al tener el PC conectado al arduino. ¿Será posible que se vea afectado cuando no está conectado ningún monitor?
Muchas gracias por contribuir a esta mejora.

Lamentablemente el equipo lo tengo instalado muy lejos de donde vivo, tendré que esperar hasta la próxima semana para implementar las mejoras que me están recomendando. Mientras tanto estaré probando con otro arduino aquí en mi casa.

Saludos a todos!!

Bienvenido OHMero.

Ya nos lo contaras cuando toque, jejeje.

Por supuesto que puedes hacer el cálculo dentro de la ISR. Lo que no debes es hacer Serial.print dentro de dicha ISR, porque potencialmente puede detener la ejecución del programa dentro de la interrupción durante un tiempo prolongado. Luego, siendo un poco puntilloso con los tiempos de ejecución de la ISR, aunque no sea estrictamente necesario, se puede acortar unos ciclos evitando llamar a una rutina, especialmente si la misma no necesita ser llamada desde otros lugares.
La solución que yo proponía era aproximadamente esta:

volatile unsigned long pwm_value = 0;
volatile unsigned long prev_time = 0;
float HourMeter = 223.0;
float hourcalc = 0.0;
bool flag = false;

void setup() {
  Serial.begin(115200);
  attachInterrupt(0, change, CHANGE);
}

void loop() {
    // programa
    if (flag) { 
      Serial.print(F("Segundos agregados a horometro:  "));
      Serial.println(pwm_value / 1000.0);
      Serial.print(F("Nuevo horometro:  "));
      Serial.println(HourMeter, 6);
      flag = false;
    }
  }

// ISR de la INT0
void change() {
  // tardo menos en esta verificación que en hacer detach y attach interrupt
  if (digitalRead(2)) { 
    prev_time = millis();  
  }
  else {
    pwm_value = millis()-prev_time;
    // Y ahorramos un par de ciclos evitando poner en rutina aparte estas sentencias
    hourcalc = pwm_value / 3600000.0;
    HourMeter = HourMeter +  hourcalc;
    flag = true;
  }
}

Hola noter

Mirando con detenimiento el codigo que propones creo que podria cumplir con los objetivos de OHMero, no obstante hay algunas cosillas discutibles:

Por supuesto que puedes hacer el cálculo dentro de la ISR. Lo que no debes es hacer Serial.print dentro de dicha ISR, .porque potencialmente puede detener la ejecución del programa dentro de la interrupción durante un tiempo prolongado Luego, siendo un poco puntilloso con los tiempos de ejecución de la ISR, aunque no sea estrictamente necesario, se puede acortar unos ciclos evitando llamar a una rutina, especialmente si la misma no necesita ser llamada desde otros lugares.

No acabo de entender cual es el problema. Una vez disparada la ISR y salvada por lo tanto la lectura del tiempo no parece preocupante que dure un poco mas o un poco menos, maxime si tenemos en cuenta que el Serial se configura a 115200 baudios (unos 9 microsegundos por bit). Y considerando tambien que el tiempo entre llamadas a la ISR podria ser de varios segundos (idealmente, claro).

Por otro lado es discutible el ahorro de ciclos que prevees en el cambio detach-attach por if-else. Hay que tener en cuenta que el attach-detach solo actualizan un puntero int en un array, y quitan/ponen el bit de mascara correspondiente en el registro EIMSK.
El if-else lleva acompañado un digitalRead() que realiza tres llamadas a funciones y para pines con timer asociado una cuarta, todo ello ademas del proceso interno de testear el pin.
Discutible.

Pero lo mas importante es que me acabo de dar cuenta de que si hay rebotes no vale el sketch de noter ni el mio, porque como decia surbyte faltaria un debouncer.

Hay tema para seguir analizando.
Por cierto habria que modificar el comienzo del sketch asi:

volatile unsigned long pwm_value = 0;
volatile unsigned long prev_time = 0;
// Cambiar a volatile
volatile float HourMeter = 223.0;   // Usada dentro de la ISR
volatile float hourcalc = 0.0;        // Usada dentro de la ISR
volatile bool flag = false;             // Usada dentro de la ISR

No os parece?

tienes razón, no le puse volatile a la variable flag.
Por otro lado, ya sabíamos que la rutina funcionaba aceptablemente salvo por algunos momentos ya descriptos.
Mi idea del debouncer es que tampoco hace falta usar ISR en algo como ésto ya que hablamos de actuadores hidráulicos, asi que ver flancos por interrupciones es como demasiado poro si funciona porque cambiarlo?
Solo corregir el hecho de quitar las impresiones en el puerto serie a algo fuera de las interrupciones.
Con eso ya estamos casi perfectos.
La idea de Noter también esta bien porque usa solo un pin.

No acabo de entender cual es el problema. Una vez disparada la ISR y salvada por lo tanto la lectura del tiempo no parece preocupante que dure un poco mas o un poco menos, maxime si tenemos en cuenta que el Serial se configura a 115200 baudios (unos 9 microsegundos por bit). Y considerando tambien que el tiempo entre llamadas a la ISR podria ser de varios segundos (idealmente, claro).

El principal problema es que no sabemos en qué estado estará el buffer de transmisión cuando entramos en la interrupción; pero sobre todo que el envío Serial hace uso de otra interrupción y en nuestra ISR lo normal es que estén deshabilitadas.

Por otro lado es discutible el ahorro de ciclos que prevees en el cambio detach-attach por if-else. Hay que tener en cuenta que el attach-detach solo actualizan un puntero int en un array, y quitan/ponen el bit de mascara correspondiente en el registro EIMSK.
El if-else lleva acompañado un digitalRead() que realiza tres llamadas a funciones y para pines con timer asociado una cuarta, todo ello ademas del proceso interno de testear el pin.

En este caso, como bien dices, es discutible.Tanto attach, detach, o digitalRead llevan aparejada la llamada a una función, con lo que mi cálculo se reduce a comparar dos llamadas a función VS una llamada a función y una comparación. A lo que no llego es a dirimir si durará más un attachInterrupt que un digitalRead, pues desconozco cómo se implementan en detalle, pero ambas se reducen a acceder a un registro (de hecho el digitalRead se podría haber realizado mediante lectura directa del puerto).

En lo de declarar volatile todas las variables que se tocan en la interrupción, totalmente de acuerdo.

Hola noter.

Por orden.
Respecto al Serial, llevas razón y habria que afinar para decidirnos a usarlo dentro de la ISR. Por lo tanto… mejor evitarlo.

Y para concretar y que no quede indefinido el digitalRead():

int digitalRead(uint8_t pin)
{
 uint8_t timer = digitalPinToTimer(pin);   // Llamada numero 1 (forzosa)
 uint8_t bit = digitalPinToBitMask(pin);   // Llamada numero 2 (forzosa)
 uint8_t port = digitalPinToPort(pin);   // Llamada numero 3 (forzosa)

 if (port == NOT_A_PIN) return LOW;

 // If the pin that support PWM output, we need to turn it off
 // before getting a digital reading.
 if (timer != NOT_ON_TIMER) turnOffPWM(timer);   // Llamada no realizada si el pin no es de timer

 if (*portInputRegister(port) & bit) return HIGH;   // Ademas llamada a funcion para lectura del port
 return LOW;
}

Y los attach y detach:

void attachInterrupt(uint8_t interruptNum, void (*userFunc)(void), int mode) {
  if(interruptNum < EXTERNAL_NUM_INTERRUPTS) {

    intFunc[interruptNum] = userFunc;      // Pasa puntero a array
    
    // Configure the interrupt mode (trigger on low input, any change, rising
    // edge, or falling edge).  The mode constants were chosen to correspond
    // to the configuration bits in the hardware register, so we simply shift
    // the mode into place.
      
    // Enable the interrupt.
      
    switch (interruptNum) {
    case 0:
      EICRA = (EICRA & ~((1 << ISC00) | (1 << ISC01))) | (mode << ISC00);
      EIMSK |= (1 << INT0);
      break;

    case 1:
      EICRA = (EICRA & ~((1 << ISC10) | (1 << ISC11))) | (mode << ISC10);
      EIMSK |= (1 << INT1);
      break;
    }
  }
}

void detachInterrupt(uint8_t interruptNum) {
  if(interruptNum < EXTERNAL_NUM_INTERRUPTS) {

    // Disable the interrupt.  (We can't assume that interruptNum is equal
    // to the number of the EIMSK bit to clear). 

    switch (interruptNum) {
    case 0:
      EIMSK &= ~(1 << INT0);
      break;

    case 1:
      EIMSK &= ~(1 << INT1);
      break;
    }
    intFunc[interruptNum] = 0;    // Asigna puntero NULL
  }
}

Que como puede verse van “justitos”.
Al final esto son disquisiciones casi filosoficas que poco afectaran al sketch de OHMero, pero bueno es saber como van por si acaso algun dia tenemos que hilar mas fino.

Perdon por esta disquisicion que no forma parte del sketch original, pero que puede resultar interesante.

Una cosa más que sé. Cierto que había leído en varios sitios que digitalRead era una función lenta, pero no sabía que tenía tanta "paja". Hacer lo mismo mediante acceso directo a puerto sólo necesitaría las dos últimas instrucciones.
También de acuerdo en que es una disquisición casi filosófica y que para el caso actual tiene poca influencia, pero no está de más saberlo.

Hola noter.

El problema de la “paja” es que cuando le decimos al compilador que queremos un < digitalRead(pin) >, a priori no se sabe donde esta ese pin, ni si tiene asociada alguna funcionalidad que distorsione la lectura (p.ej. que sea un pin asociado a una funcion de timer, que este trabajando como ADC, etc,…).
Eso le plantea al compilador la necesidad de averiguar mediante el numero suministrado ( pin ) el port y el pin I/O del port ( es decir la máscara ) para poder hacer esas dos ultimas instrucciones que comentas.

SI trabajasemos en assembler (avrasm32 o avr-as) seria mucho mas sencillo, directo, y por supuesto rápido.

No. Si entiendo toda la secuencia que se desarrolla en la función. Lo que no sabía es que realmente tomaba tantísimo más proceso que el acceso directo a puerto. Hasta ahora no solía considerar acceder directamente a los puertos salvo en casos muy críticos. Ahora creo que abriré un poco más el abanico.

Simple noter, modificala en tu IDE y cuando tengas problemas recuerda que lo hiciste, jajajaja

Tu crees que esto esta de más?

// If the pin that support PWM output, we need to turn it off
 // before getting a digital reading.
 if (timer != NOT_ON_TIMER) turnOffPWM(timer);   // Llamada no realizada si el pin no es de timer