[SOLUCIONADO] Reemplazar millis por defecto

Hola a todos. Cómo el título dice, quiero reemplazar la implementación por defecto de millis, por una más precisa (que utiliza el timer1); así que mis dudas son las siguientes:

  • ¿Se puede deshabilitar la interrupción que millis necesita?
  • ¿Es posible redefinir una función sin necesidad de modificar los archivos fuente usados por la IDE?
  • ¿Podría realizar dichos cambios con solo incluir una librería?

La última pregunta es un poco más "técnica", así que ojalá alguien sepa respondérmela:
¿Necesito reiniciar "manualmente" el contador del timer1 cada vez que ocurre una interrupción por comparación de valores?

Agredezco cualquier respuesta... :slight_smile:

Hola Lucario448

Ahí van algunos datos básicos:

Lucario448:
... ¿Se puede deshabilitar la interrupción que millis necesita?

Sí, se puede, pero eso deja fuera de servicio a ,millis(), micros() y delay():

Lucario448:
¿Es posible redefinir una función sin necesidad de modificar los archivos fuente usados por la IDE?

Desde un punto de vista teórico, sí. Desde un punto de vista práctico, no.

Lucario448:
¿Podría realizar dichos cambios con solo incluir una librería?

Si creas una nueva funcion, mi_millis() por ejemplo, y la extiendes a mis_micros() y a mi_delay(), y la empaquetas en una libreria mis_tiempos.cpp, entonces sí que sería posible pero no sería compatible con lo que hay.

Lucario448:
La última pregunta es un poco más "técnica", así que ojalá alguien sepa respondérmela:
¿Necesito reiniciar "manualmente" el contador del timer1 cada vez que ocurre una interrupción por comparación de valores?

Agredezco cualquier respuesta... :slight_smile:

No es indispensable. Segun el modo de funcionamiento que utilizaras para el timer1 (es el que usarias, supongo), podria resetearse el contador en espera de volver a alcanzar la cuenta deseada.
Por cierto, hasta donde yo conozco la funciones de tiempo estandar (millis, micros y delay), son precisas hasta donde la programacion lo permite, y usan un timer para la cuenta de los tiempos.

Naturalmente esto se basa en lo que sé y no está libre de errores.
Estamos aprendiendo.

Quieres mejor la precisión de millis() y porque no usas micros()?

Intuyo que lo que quiere es crear una función millis más exacta y "natural".

Tal y como viene definido millis por defecto, para un reloj de 16 Mhz utiliza un prescaler de 64, con lo que cada conteo del timer equivale a 4 microsegundos. Por tanto la resolución de micros es esa. En cuanto al conteo de millis, se incrementa cada desbordamiento, es decir cada 4*256 = 1024 microsegundos, con lo que de tanto en tanto se suman dos milisegundos, en lugar de uno, a la cuenta de millis.

Tal vez lo que desea implementar Lucario sea, por ejemplo, un prescaler de 8, con lo que cada conteo equivaldría a medio microsegundo, e interrupción/reseteo cada 2000 conteos, con lo que se incrementaría millis exactamente cada milisegundo.

En cuanto a lo de redefinir millis, micros y delay, me temo que no es posible directamente. La solución que más podría acercarse, si es que funciona, sería una que mencionan, y no como respuesta principal, aquí.

Consistiría básicamente en copiar al directorio del proyecto la librería o librerías que contienen las funciones que deseamos modificar, y modificarlas dentro de nuestro proyecto (en otro lugar he leído que deben estar en una subcarpeta libraries del proyecto). De esta forma las modificaciones sólo afectarían a nuestro proyecto. Si alguien lo prueba sería interesante que exponga sus resultados.

Respecto a lo de reiniciar el timer1 al alcanzarse el valor de comparación, creo que sí es posible (modo CTC). Lo que no estoy totalmente seguro es de si el valor de comparación que debes poner es N o N-1 (2000 o 1999 en el ejemplo propuesto antes).

No queria entrar a tanto detalle, pero bueno si se necesita mas informacion ver aqui: explicacion de millis.
Para que cada uno pueda plantearse si la precision que da la funcion estandar le vale o no.

Efectivamente, el timer1 se resetea a un valor programado, si usas los modos NORMAL y CTC, pero si deseas resetear a 0, puedes considerar tambien los modos FAST PWM y PWM Phase Correct.

Por cierto, creo que seria N = 1999 y el siguiente clock lo pondria a cero (0 a 1999 son 2000 cuentas)).
@Lucario un factor a considerar si sigues adelante en tu idea es la sobrecarga que supone un tiempo demasiado corto entre interrupciones.

Para un cronometro podria ser buena idea, pero en general tambien usamosSerial, I2C, PWM y mas cosas todas juntas, y queremos que vaya todo fluido.
Quizás por eso los padres fundadores han buscado un equilibrio global, a fin de cuentas para la hora usamos modulos externos, y los milisegundos los usamos siempre en modo relativo ... millis()-tiempo ...

surbyte:
Quieres mejor la precisión de millis() y porque no usas micros()?

  • micros tiene el bendito overflow cada 71 minutos. Es buena la idea pero si el Arduino no opera por más de una hora continua.
  • Analizándolo bien, realmente no tenía pensado redefinir micros; desencadenar interrupciones que cuenten microsegundos con exactitud... imposible con 16 MHz.

A menos de que una interrupción de 974 hz no interfiera fuertemente con una de 1 KHz (de ahí un millis más "exacto y natural"); entonces no sería necesario deshabilitarla. Caso contrario, quedará como una opción para una librería (porque he notado que realmente paralizaría a micros).

noter:
Tal vez lo que desea implementar Lucario sea, por ejemplo, un prescaler de 8, con lo que cada conteo equivaldría a medio microsegundo, e interrupción/reseteo cada 2000 conteos, con lo que se incrementaría millis exactamente cada milisegundo.

Lo dices en timer1? Porque en timer0 o 2; está más complicado lidiar con no sub-múltiplos de 2000 (256).

Pero entiendo tu punto, 8 ciclos de reloj son exactamente 500 ns, entonces cada dos cuentas timer, hay un microsegundo (si tan solo se pudiera hacer el incremento completamente paralelo a una variable de la memoria RAM).
Yo tenía en mente hacerlo con el prescaler en 1, así el incremento tiene que ocurrir cada 15999 (16000) ciclos; o podría restarle los que se tarda en "moverse" del código principal a la ISR, que aparentemente (según Nick Gammon aquí, sección "How long does it take to execute an ISR?"), son 23 si no es interrupción externa.

Respecto a la redefinición... si no es algo que pueda hacer con un #include, entonces creo que bastará con deshabilitar la interrupción del overflow del timer0 (si el usuario así lo quisiera).

Alfaville:
Para un cronometro podria ser buena idea, pero en general tambien usamosSerial, I2C, PWM y mas cosas todas juntas, y queremos que vaya todo fluido.
Quizás por eso los padres fundadores han buscado un equilibrio global, a fin de cuentas para la hora usamos modulos externos, y los milisegundos los usamos siempre en modo relativo ... millis()-tiempo ...

Otro motivo más para no redefinir millis.

Leyendo el artículo del link que posteaste, me di cuenta que el contador de millis tiene lo que en el calendario gregoriano (con el que prácticamente todo el mundo vive, el de los 365 días) viene siendo el año bisiesto; esa unidad de más que se suma con el fin de compensar la desviación que provocan las cifras decimales. Vamos, para nadie es un secreto que el movimiento de traslación de la Tierra no dura exactamente 365 días; sino hasta el "29 de febrero" se podría usar como una indirecta de "nunca".

Si entonces millis tiene su razón de ser, entonces mejor dejarlo así; a menos de que se pueda cambiar con un #include <libreria.h>.

PD: creo que ya tengo claro que el modo CTC automáticamente reinicia el contador cuando viene la interrupción, pero ahora mi duda es: ¿Sigue contando aún dentro de una ISR, y aún si todas las interrupciones estuvieran deshabilitadas?

Yo supongo que sí, ya que los timers son piezas de hardware y no de software; que únicamente son contadores de pulsos (reloj interno o externo). No sé que piensan ustedes...

Lucario448:
PD: creo que ya tengo claro que el modo CTC automáticamente reinicia el contador cuando viene la interrupción, pero ahora mi duda es: ¿Sigue contando aún dentro de una ISR, y aún si todas las interrupciones estuvieran deshabilitadas?

Yo supongo que sí, ya que los timers son piezas de hardware y no de software; que únicamente son contadores de pulsos (reloj interno o externo). No sé que piensan ustedes...

Efectivamente no te equivocas.
El timer sigue contando y si (aunque no es el caso) genera una interrupcion, no se podrá atender hasta no retornar de la ISR.
Y aunque nuestro timer no va a generar una interrupcion porque precisamente estamos atendiendo la que acaba de generar, si fuese necesario poriamos habilitar las interrupciones dentro de la ISR con un sei() y sí que seriamos interrumpidos.
Esta tecnica es fuente de muchos problemas potenciales, y debe evitarse salvo extrema necesidad.

Creo que la idea de hacerte una funcion personal para medida "exacta" de tiempos sería mejor que complicarte parcheando librerias.
Por eso en mi primer post te hablé de "mis_tiempos.cpp" y demás.

Bueno.
Me parece que más que solucionar algo concreto, lo que estamos haciendo todos es más bien teorizar, elucubrar, etc., cosa que no me parece mal, sino sumamente constructiva, aunque el lector que entre en este hilo sin un conocimiento un poco avanzado puede caer en la locura. :slight_smile:
Así que una vez realizado el aviso, pongo algunos flecos más ;).

Creo que esto es sabido por Alfaville, y no sé si también por Lucario, pero de la lectura del último post podría deducirse que micros también genera una interrupción, lo que sería una locura, pues con 16 Mhz sería imposible atender una interrupción de 1 Mhz; y lo sería doblemente pretender que se ejecutara cada medio microsegundo. La única interrupción que se atiende es la de desbordamiento cada 1024 us, en la que se incrementa en uno (o dos) la cuenta de millis(). Esto quiere decir que, efectivamente, hay una variable "paralela" en memoria RAM. No así con micros(), que consulta el timer0 y realiza el cálculo sólo cuando es llamado.

De esta forma, el teórico millis y micros propuesto que, como decía Lucario, debería realizarse sobre timer1; incrementaría directamente millis sin correcciones, y podría proporcionar un cálculo de micros más preciso. Podríamos decir: ¿Por qué los padres de Arduino no implementaron millis así? Pues supongo que por aprovechar mejor los recursos disponibles y no comprometer el único timer de 16bit de muchos micros. También porque este cálculo tan chulo está muy bien para un reloj de 16 Mhz, pero se nos vendrá abajo con otras frecuencias. En las librerías del core se toma en cuenta la frecuencia definida para ajustar el incremento de millis y el cómputo de micros.

Ya metidos en harina, creo que para quien haya llegado hasta aquí puede ser interesante ver la implementación en el core de las tres funciones descritas; así que me tomo la libertad de ponerlas con algún comentario añadido.

Esta sería la definición de la ISR para desbordamiento del timer0. Vemos que hay tres variables afectadas llamadas timer0_millis, que sería la variable que contiene propiamente los millis, y timer0_fract, que acumula la fracción que se produce en cada desbordamiento (el 24 de los 1024 microsegundos para 16 Mhz). Cuando la fracción acumulada supera FRACT_MAX, vemos que se incrementa el milisegundo adicional. Por último, también se incrementa la variable timer0_overflow_count que, como su nombre indica, lleva la cuenta del número de desbordamientos, que será parte del cálculo de micros().

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
	// copy these to local variables so they can be stored in registers
	// (volatile variables must be read from memory on every access)
	unsigned long m = timer0_millis;
	unsigned char f = timer0_fract;

	m += MILLIS_INC;
	f += FRACT_INC;
	if (f >= FRACT_MAX) {
		f -= FRACT_MAX;
		m += 1;
	}

	timer0_fract = f;
	timer0_millis = m;
	timer0_overflow_count++;
}

El código de la función millis, vemos que básicamente consulta la variable timer0_millis, con la precaución de deshabilitar interrupciones para evitar que en medio de la lectura se produzca una actualización de dicha variable y nos de un resultado incongruente.

unsigned long millis()
{
	unsigned long m;
	uint8_t oldSREG = SREG;

	// disable interrupts while we read timer0_millis or we might get an
	// inconsistent value (e.g. in the middle of a write to timer0_millis)
	cli();
	m = timer0_millis;
	SREG = oldSREG;

	return m;
}

En la función micros, vemos que se hace el cálculo con el número de desbordamientos(m) y el valor actual del timer0 (t):

unsigned long micros() {
	unsigned long m;
	uint8_t oldSREG = SREG, t;
	
	cli();
	m = timer0_overflow_count;
#if defined(TCNT0)
	t = TCNT0;
#elif defined(TCNT0L)
	t = TCNT0L;
#else
	#error TIMER 0 not defined
#endif

#ifdef TIFR0
	if ((TIFR0 & _BV(TOV0)) && (t < 255))
		m++;
#else
	if ((TIFR & _BV(TOV0)) && (t < 255))
		m++;
#endif

	SREG = oldSREG;
	
	return ((m << 8) + t) * (64 / clockCyclesPerMicrosecond());
}

Si señor, así funciona.
Los recien llegados que pueden caer en un estado de "locura" pasajera ante estos post, pueden ambientarse aquí: enlace a explicaciones sobre millis que aunque ya lo posteé, ha quedado semioculto entre los últimos post.
Y llegados a este punto, y a falta de saber si el moderador considera que deberiamos abrir un nuevo hilo en otra sección, creo que debemos preguntar a Lucario ¿ quieres realmente hacer estas funciones de tiempos ?.
Si la respuesta es sí, aquí estamos por si en algo podemos facilitarte la tarea.

noter:
pues con 16 Mhz sería imposible atender una interrupción de 1 Mhz; y lo sería doblemente pretender que se ejecutara cada medio microsegundo.

A eso me refería:

Lucario448:
desencadenar interrupciones que cuenten microsegundos con exactitud... imposible con 16 MHz.

noter:
La única interrupción que se atiende es la de desbordamiento cada 1024 us, en la que se incrementa en uno (o dos) la cuenta de millis(). Esto quiere decir que, efectivamente, hay una variable "paralela" en memoria RAM.

Bueno... yo hablaba de que ambos incrementos ocurrieran en el mismo ciclo de reloj (cuenta de timer con variable en RAM); pero creo que no es el caso.

noter:
De esta forma, el teórico millis y micros propuesto que, como decía Lucario, debería realizarse sobre timer1; incrementaría directamente millis sin correcciones, y podría proporcionar un cálculo de micros más preciso.

O al menos deshabilitar sólo el millis original; digo, que la interrupción únicamente incremente el "overflow count".
Con 16 MHz pedir más resolución en el conteo de microsegundos (ir de 3 en 3 o menos) veo que es demasiado pedir.

noter:
También porque este cálculo tan chulo está muy bien para un reloj de 16 Mhz, pero se nos vendrá abajo con otras frecuencias. En las librerías del core se toma en cuenta la frecuencia definida para ajustar el incremento de millis y el cómputo de micros.

De hecho, lo debo tomar en cuenta en una versión mejorada de mi librería. La frecuencia del reloj también me altera el valor del OCR para intentar mantener un constante 1 KHz.

Alfaville:
Y llegados a este punto, y a falta de saber si el moderador considera que deberiamos abrir un nuevo hilo en otra sección

Lo dices por la discución/lección de timers en AVR?.
Al menos a mi no me incomoda siempre y cuando el desvirtuado no sea tan extremo; y no haya intercambio de insultos/groserías.

Alfaville:
creo que debemos preguntar a Lucario ¿ quieres realmente hacer estas funciones de tiempos ?

Y por qué no? Como todo, tiene sus pros y sus contras.
Como decía Alfaville, sería útil para cronómetros, pero para ciertos proyectos más bien es un dolor de cabeza. Es más, admito que tengo tres librerías que no van a ser compatibles con mi implementación de millis: Servo, TMRpcm (a menos que se configure para timer2) y TimerOne. Hasta incluso se vería alterado al hacer analogWrite a cual sea el pin que está asociado al OCR1A (si no me equivoco creo que es el 10 "Arduino" en ATmega328P).

Muy posiblemente tenga que encapsularlo en una clase, porque necesito que la variable que lleva los milisegundos no pueda ser accesada desde afuera.

PD: quizá esta pregunta también este fuera de tema, pero aquí va: ¿qué tan nocivo es para el microcontrolador hacerle "overclocking"?
Sé que quizá desatiempe los tres tipos de comunicación serial (USART, I2C Y SPI) y por supuesto que hasta los mismos timers. Y que genera más calor, requiere mayor voltaje, bla bla bla.
Mi pregunta es si (como todo microprocesador), practicarle "overclocking" a un pobre ATmega328P a la larga lo puede matar.

Yo para algo como lo que plantea Lucario simplemente me voy al Atmel Studio y programo el AVR y me olvide del C Arduino style. Y asunto terminado.
Facilmente genero una interrupción con timerX en 10kHz o 100khz.
El resto es lo mismo si usar Arduino, estilo COMO ANTES o como ha dicho Alfaville que trabaja !!

surbyte:
Yo para algo como lo que plantea Lucario simplemente me voy al Atmel Studio y programo el AVR y me olvide del C Arduino style. Y asunto terminado.

Si lo dices porque me estoy yendo muy "adentro", pues... tiene sentido.

... Lo dices por la discución/lección de timers en AVR?.
Al menos a mi no me incomoda siempre y cuando el desvirtuado no sea tan extremo; y no haya intercambio de insultos/groserías.

No, para nada.
Considero que estamos en terreno cortés, no de discusión.
¿ Insultos ?, ¿ groserias ?. Dios no lo permita.
Lo decía porque el tema dá para mucho y ahonda en las partes intimas del Auduino (librerias del core), y puede parecer demasiado técnico para el tópico elegido.
Vamos que cae mas en el campo de un tuto.

Sobre el tema del overclock, el ATmega 328P admite hasta 20 MHz que, por cierto, es una frecuencia mas agradecida a la hora de encontrar divisores para obtener mili / micro segundos.

Por otro lado los calculos internos usan la constante F_CPU

#define clockCyclesPerMicrosecond() ( F_CPU / 1000000L )
#define clockCyclesToMicroseconds(a) ( (a) / clockCyclesPerMicrosecond() )
#define microsecondsToClockCycles(a) ( (a) * clockCyclesPerMicrosecond() )

por lo que pese al cambio de frecuencia, se pueden recalcular los tiempos.
Es un tema a considerar mas despacio.

Como ejercicio teórico no está mal, pero los beneficios prácticos creo que no serán notables respecto al uso de millis/micros implementado en el core. Como se ha dicho, la precisión de millis es de +-1ms, y la precisión de micros de +-4us- Teóricamente esta precisión se mantiene tanto si queremos medir un tiempo breve como uno largo. Creo que para la mayoría de aplicaciones es más que suficiente, y para las que necesiten un temporizado más fino (generador de frecuencias, medir anchura de un pulso, etc) es preferible programar específicamente un timer, bien directamente, bien con apoyo de alguna librería.
No obstante, si alguien se quiere embarcar en la aventura, se intentará ayudar ;).

Efectivamente, es un ejercicio teórico.
Pero creo que llegados hsta aquí merece la pena diferenciar entre micros() que no recibe interrupciones cada 4 useg, sino que calcula los microsegundos haciendo una extrapolacion a partir de los milisegundos obtenidos ( como funciona micros() ):

micros = (Timer #0 counter + (number of times timer #0 has overflowed * 256)) * 4

y delayMicroseconds() que para el caso de 16 MHz de clock arroja una precision muy cercana a 1 useg, y nos permite medir ese milisegundo en mejores condiciones que con millis().
Lastima que el rango maximo tenga que ser de 65535 useg.
Pero para aplicaciones mas criticas con la precision y tiempos cortos puede ser mas que suficiente.

Un punto en contra, ya que todo no puede ser bueno, es que mientras ejecutamos delayMicroseconds() el programa queda detenido en un bucle, pero ya se sabe: interrupciones o detencion:

Alfaville:
Lastima que el rango maximo tenga que ser de 65535 useg.

En realidad, el parámetro de delayMicroseconds es unsigned long (32 bits); así que el máximo es el equivalente a 71 minutos.

Pero bueno, más bien trataré de no deshabilitar micros al cambiar de millis. Para esto, necesito saber: ¿puedo redefinir una ISR con una librería? Algo como:

#ifdef DISABLE_ORIGINAL_MILLIS
ISR(TIMER0_OVF_vect) {
  // Solo incrementar el "overflow count"
}
#endif

Otras preguntas (son para finalizar la librería y probarla en un ejemplo de un cronómetro):

  • Para hacer lo anterior, supongo que debo incluir el archivo que hace referencia a la variable usada por micros, ¿cierto?
  • La constante F_CPU es accesible para todo archivo fuente que se utilice bajo la IDE de Arduino, o también tengo que incluir (#include) el archivo correspondiente?
  • ¿Hay algún lugar donde pueda yo saber el nombre de las de definiciones que indican el microcontrolador a usar? La idea es que la librería no compile en arquitecturas diferentes a AVR o carentes de timer1.

PD: agradecer las respuestas que me han dado hasta el momento, y las que están por venir.
Se siente tan bien el saber que no me han "desamparado"... :slight_smile:

Hola Lucario, por un momento me has hecho dudar, pero:

/* Delay for the given number of microseconds.  Assumes a 8 or 16 MHz clock. */
void delayMicroseconds(unsigned int us)
{
 // calling avrlib's delay_us() function with low values (e.g. 1 or
 // 2 microseconds) gives delays longer than desired.
 //delay_us(us);

Parece ser que no.

Por otro lado las preguntas que pones sobre la mesa son muy interesantes, pero al mismo tiempo complejas (para mí al menos), por lo tanto para darte una respuesta que no sea incorrecta tengo que estudiar las interacciones de lo que quieres, con lo que hay.
El entorno IDE está bastante "enmarañado" y tiene multitud de dependencias.

Como las preguntas son para todo el foro, no descartes respuestas mas rápidas que las mias

Por cierto, se me ha pasado.

La constante F_CPU la coloca el IDE de forma automatica cuando llama al compilador, y su valor va directamente ligado al tipo de placa que has seleccionado para el sketch.

Por lo tanto no hay nada de que preocuparse, si usas Arduino UNO (por ejemplo) se pone automaticamente a 16 MHz

Alfaville:
por un momento me has hecho dudar, pero:

/* Delay for the given number of microseconds.  Assumes a 8 or 16 MHz clock. */

void delayMicroseconds(unsigned int us)
{
// calling avrlib's delay_us() function with low values (e.g. 1 or
// 2 microseconds) gives delays longer than desired.
//delay_us(us);



Parece ser que no.

Admito mi equivocación :sweat_smile: , aunque la verdad tiene sentido; por estas dos razones:

  • Es muy descabellado requerir "retardos de precisión" si son para lapsos muy prolongados. Más del segundo considero yo que es ir demasiado lejos para con microsegundos.
  • Son microprocesadores de 8 bits, trabajar con variables de 32 bits pues lógicamente le tomará cuatro veces más tiempo procesar que uno de su tamaño; y claramente aquí el tiempo importa mucho. Un único byte tampoco pueden pedir, porque 255 microsegundos pueden tornarse demasiado cortos para ciertas aplicaciones.

Alfaville:
La constante F_CPU la coloca el IDE de forma automatica cuando llama al compilador, y su valor va directamente ligado al tipo de placa que has seleccionado para el sketch.

Por lo tanto no hay nada de que preocuparse, si usas Arduino UNO (por ejemplo) se pone automaticamente a 16 MHz

¡Uno menos, quedan tres!

¡Gracias por la ayuda! :smiley: :wink: