35- Arduino, Construyendo Grandes Proyectos


Escribir un programa.

En esta lección, hablaremos sobre cómo los makers arduino se diferencian de los programadores, por qué no se entienden y no se gustan entre sí, y también cómo un «boceto» se diferencia de un programa.

Empecemos de lejos: Lenguajes de programación. El microcontrolador en sí está programado en lenguaje ensamblador (assembler, asm), y cada MC tiene su propio conjunto limitado de comandos. Cada comando se ejecuta en un ciclo del procesador, es decir, al programar en ASM tenemos el máximo control sobre la velocidad de ejecución del código, su tamaño y en general los procesos que ocurren dentro del Mc: por eso se le llama -lenguaje de bajo nivel. Es muy difícil programar en lenguaje ensamblador, porque el conjunto de instrucciones es muy pequeño, estos comandos son elementales (no en términos de uso, sino en la esencia misma) y muchas cosas estándar para otros lenguajes tienen que reinventarse literalmente y describirse manualmente: navegar manualmente por el espacio de direcciones, manipular la memoria y trabajar en conjunto con la documentación para un MC específico y conocimiento de su arquitectura. Hay mucho código y parece incomprensible por decirlo suavemente:

// código de ejemplo en asme
loop:
st X, %[set_hi]
sbrs %[LEDbuffer], 7
st X, %[set_lo]
lsl  %[LEDbuffer]
dec   %[counter]
rjmp .+0
rjmp .+0
rjmp .+0
brcc to_end
st  X,%[set_lo]
to_end:
brne  loop
: [counter] "=&d" (ctr)
: [LEDbuffer] "r" (*data_ptr++), "x" (ws2812_port_1)

En algunos casos, es prácticamente imposible leer y comprender el código ASM desconocido sin los comentarios de los desarrolladores y el conocimiento de la documentación del microcontrolador. Por eso programar microcontroladores solía ser una ocupación muy difícil, y como “hobby” era inaccesible para una persona sin una especialidad correspondiente.

Ahora, el compilador genera el código ensamblador a partir de lenguajes de nivel superior, que son fáciles y agradables de escribir: el mismo Arduino se puede programar en C, C ++, Basic, así como un montón de shells de programación visual. Usamos funciones y herramientas de lenguaje listas para usar y comprensibles, escribimos un par de decenas o cientos de líneas en ellas y el compilador las convierte en decenas de miles de líneas de código en ensamblador que funcionarán en el Mc.

En el IDE de Arduino, programamos en C++. ¿o C? Buena pregunta, porque de hecho escribimos en ambos: el lenguaje C ++ es el lenguaje C, en el que se agregaron clases, objetos, herencia y todo lo relacionado con ellos, C++ originalmente se llamó incluso “ C con clases ”. C++ le permite utilizar todas las delicias de la programación orientada a objetos: programación orientada a objetos, gracias a la cual se puede organizar un programa enorme de manera muy agradable, legible, puede crear varios módulos y bibliotecas independientes entre sí, construir la estructura del proyecto de la manera más eficiente, y también proporciona su conveniente revisión y edición. Y ahora pasamos a la parte donde diré “ recuerda todos los ejemplos de las lecciones anteriores y las fuentes de mis proyectos. No puedes hacer eso«

En general, se pueden distinguir dos enfoques para el desarrollo de programas: procedimental y orientado a objetos (hay un tercero – estilo Arduino, cuando todas las funciones y variables se mezclan en un archivo). En términos generales, el enfoque procedimental es un montón de funciones y variables, a veces esparcidas en archivos separados, y OOP encapsula todo el código en clases que interactúan entre sí y con el programa principal. En la comunidad de Arduino, casi siempre se encuentra el primer tipo, porque es mucho más fácil para un principiante trabajar de esta manera, los programas en general son pequeños y sencillos: el programa principal en sí está escrito de manera procedimental, pero usa «bibliotecas», que son básicamente clases. Todos los ejemplos oficiales y no oficiales se construyen de esta manera, y el propio IDE de Arduino llama a sus documentos bocetos (del inglés. sketch – sketch), porque Arduino está concebido como una plataforma de entrenamiento y prototipado rápido, y no para el desarrollo de proyectos grandes y serios. En el IDE de Arduino, en lugar de un administrador de documentos normal, tenemos pestañas que no funcionan de la manera más obvia y están claramente diseñadas para un enfoque de procedimiento. Echemos un vistazo más de cerca a las características de los enfoques.


OOP y enfoque procedimental.

Umbral de entrada

Comenzamos a pensar y escribir de manera procedimental desde las primeras lecciones, porque es simple. Trabajar con OOP requiere un conocimiento mucho más profundo de C++, y el trabajo eficiente requiere el máximo.

Tamaño del código

Envolver todo en una fila en clases conduce a la generación de una gran cantidad de código como texto: solo tienes que escribir mucho más, respectivamente, leerlo también. Hacer esto en un IDE nativo, por cierto, es muy desagradable en comparación con los entornos de desarrollo más antiguos, que tienen sustitución automática de palabras y miembros de la clase, así como un «árbol» del proyecto y sus archivos.

Peso y velocidad de ejecución de código

Los compiladores se actualizan y mejoran constantemente, incluso el IDE de Arduino integrado en avr-gcc: las nuevas versiones optimizan el código cada vez mejor, haciéndolo más ligero y rápido. Sin embargo, una gran cantidad de clases anidadas y largas cadenas de datos se ejecutarán inevitablemente un poco más lento y ocuparán más espacio que un enfoque de procedimiento más compacto.

Alcance y nombre

En un enfoque de procedimiento, debe monitorear constantemente el uso de nombres de variables y funciones y debe evitar repeticiones, si es posible, esconderlas dentro de documentos separados y tenerlas siempre en cuenta. Un pequeño error, como usar una variable global en lugar de una local, puede generar errores que son difíciles de rastrear. Al envolver partes del código en clases, obtenemos, hablando en términos generales, programas separados cuyos nombres de funciones y variables están separados del resto del código y no les importa que otras clases o en el programa principal tengan lo mismo.

Proyectos mayores

Es mucho más agradable escribir un programa grande con un montón de subrutinas con OOP, y no solo escribir, sino también mejorar en el futuro.

Pequeños proyectos y prototipos

Escribir un pequeño programa en un estilo de procedimiento es mucho más fácil y más rápido que en C++ clásico, por lo que Arduino IDE es sólo un bloc de notas que guarda un dibujo en su extensión .ino, en lugar de los archivos de biblioteca .h y .cpp: a nosotros no nos es necesario pensar en la estructura del archivo del proyecto, solo escribimos el código y listo. Para la creación rápida de prototipos y la depuración de pequeños algoritmos, esto funciona muy bien, pero con un proyecto grande pueden comenzar problemas e inconvenientes.

Bibliotecas y compatibilidad

Si abre cualquier biblioteca para Arduino, entonces, con un 99,9% de probabilidad, verá una clase allí. El poder de OOP es que tomamos una biblioteca (o simplemente algún tipo de clase), la agregamos a nuestro código y no obtenemos ningún conflicto de nombres ni intersecciones de código en general (en el 99% de los casos): la clase funciona por sí sola, es un programa separado. Si toma, por ejemplo, algún fragmento de código «desnudo» y lo inserta en su mismo código desnudo, habrá una probabilidad bastante alta de intersecciones y conflictos, y lo peor es cuando el compilador no ve errores pero el programa no funciona adecuadamente.


¿Qué hacer y cómo escribir a continuación?

Si es un principiante y recién se está sumergiendo en el idioma, es mejor escribir como está escrito. Se considera una buena práctica tener un número mínimo, o mejor aún, la ausencia de variables globales para todo el programa: muy a menudo la global puede hacerse local estática y ocultarse del resto del código sin perder funcionalidad. Las partes separadas e independientes del programa (botones de sondeo, procesamiento de valores, envío y análisis de datos, etc.) pueden agruparse en una clase, colocarse en un archivo separado y, por lo tanto, el código del programa principal se puede reducir, aumentando su legibilidad y estructuración. Pero no olvides que un mismo problema se puede resolver de infinitas formas, y no todas son óptimas.

Si el programa tiene los mismos «bloques» que requieren el mismo conjunto de variables, será mucho más conveniente envolverlos en una clase. Además, con el tiempo, se acumulará un conjunto de estas minibibliotecas y será muy conveniente utilizarlas en trabajos futuros. En mis lecciones hay una lección sobre clases y sobre escritura de bibliotecas, pero solo se analiza una pequeña y más básica parte de las capacidades de OOP. Para escribir herramientas poderosas y versátiles, estudia cualquier lección de C++, después de estudiar mis lecciones estarás listo para ellas y todo estará claro allí.

Además, las clases en C++ tienen una característica tan poderosa como la herencia: una clase puede heredar las capacidades de otra clase. Por ejemplo, casi todas las bibliotecas de visualización, así como Serial y Soft Serial, tienen un método «todo terreno». print(), que muestra variables de cualquier tipo, puede mostrar un número en diferentes representaciones, formatear la salida de números flotantes, etc. Un punto interesante aquí es que todas estas capacidades se implementan en la clase Print estándar, que se encuentra entre el resto de los archivos en el núcleo IDE de Arduino, y todas las demás bibliotecas simplemente heredan todas las capacidades de salida. De hecho, solo se debe implementar la biblioteca de visualización / serial.print(), y absolutamente todo el resto de la versatilidad de salida es proporcionada por la «cooperación» con la clase de impresión. En este ciclo de lecciones, no analizaremos la herencia y otras herramientas de programación orientada a objetos, porque es poco probable que sea útil para usted y ya se entiende perfectamente en cualquier libro o en cualquier tutorial de C++ en Internet.

Envolverse en una clase no es una panacea: si un programa no implica la creación y el uso de múltiples instancias de sí mismo, entonces simplemente se puede colocar en un archivo separado, como un conjunto de funciones y variables. En este caso, las variables globales deben hacerse estáticas para que no sean «visibles» desde otros archivos de programa.

¿Por qué y cómo trabajar con él? Al crear grandes proyectos (y en general), se debe adherir al concepto de «datos por separado, código por separado», es decir, no debe haber variables globales que estén en el alcance de todo el programa, al menos su número debe ser minimizado. Las variables globales se pueden ocultar dentro del archivo, que es proporcionado por la palabra clave státic (las variables deben ser declaradas en un .c o .cpp archivo!), y usted puede compartir sus valores con el resto del código del programa y establecer un nuevo valor utilizando funciones separadas. Como ejemplo de un proyecto muy grande realizado con una estructura de archivos comprensible y sin usar OOP y clases, firmware GRBL. Además, la mayoría de las variables globales se pueden ocultar dentro de funciones donde se necesiten (es decir, si se necesitan sólo dentro de una función específica), nuevamente usando estátic. Hablamos de esto al principio, en la lección sobre tipos de datos.

Ejemplo 1

Echemos un vistazo a un ejemplo de cómo convertir un código «vinigrette» terrible con un montón de variables globales y un desorden en el ciclo principal en un programa comprensible con rutinas independientes separadas.

En este ejemplo, tenemos dos botones conectados (en los pines D2 y D3) y un LED (usamos el pin D13). Escribamos un programa que hará parpadear un LED y sondeará de forma asincrónica los botones con amortiguación programada del rebote de contacto. Usando los botones, puede cambiar la frecuencia de parpadeo del LED. No tiene sentido comentar el código en detalle, porque hemos analizado todas las construcciones utilizadas más de una vez en el ciclo de lecciones.

// pines
const byte btn1 = 2;
const byte btn2 = 3;
const byte led = 13;
//cambiar paso
const int step = 50;
//temporizadores de rebote de botón
uint32_t btn1Tmr;
uint32_t btn2Tmr;
// banderas para sondeo de botones  
bool btn1Flag;
bool btn2Flag;
//  variable para el led
uint32_t ledTmr;
int ledPeriod = 1000; // período inicial 1 segundo
bool ledState = false;
void setup () {
  //configurar pines
  pinMode(btn1, INPUT_PULLUP);
  pinMode(btn2, INPUT_PULLUP);
  pinMode(led, OUTPUT);
}
void loop() {
  // temporizacion led
  if (millis() - ledTmr >= ledPeriod) {
    ledTmr = millis();
    ledState = !ledState;
    digitalWrite(led, ledState);
  }
  // encuesta para el primer botón con 100ms antirrebote
  bool btn1State = digitalRead(btn1);
  if (!btn1State && !btn1Flag && millis() - btn1Tmr >= 100) {
    btn1Flag = true;    
    btn1Tmr = millis();
    ledPeriod += step;    // aumentar el período
  }
  if (btn1State && btn1Flag) {
    btn1Flag = false;
    btn1Tmr = millis();
  }
  // encuesta para el segundo botón con 100ms antirrebote
  bool btn2State = digitalRead(btn2);
  if (!btn2State && !btn2Flag && millis() - btn2Tmr >= 100) {
    btn2Flag = true;    
    btn2Tmr = millis();
    ledPeriod -= step;    //  disminuir el período
  }
  if (btn2State && btn2Flag) {
    btn2Flag = false;
    btn2Tmr = millis();
  }
}

Agregar botones adicionales o funcionalidad LED adicional al programa generará una gran confusión y un aumento en la cantidad de código, será mucho más difícil de entender. Envuelva el procesamiento del botón en una clase, porque ya tenemos dos botones idénticos, y en el futuro será posible agregar más cosas al programa. Movamos inmediatamente la clase a un archivo separado y diseñémoslo como una biblioteca:

button.h

// clase de botón
#pragma once
#include <Arduino.h>
#define _BTN_DEB_TIME 100  // iempo de espera anti-rebote
class Button {
  public:
    Button (byte pin) : _pin(pin) {
      pinMode(_pin, INPUT_PULLUP);
    }
    bool click() {
      bool btnState = digitalRead(_pin);
      if (!btnState && !_flag && millis() - _tmr >= _BTN_DEB_TIME) {
        _flag = true;
        _tmr = millis();
        return true;
      }
      if (btnState && _flag) {
        _flag = false;
        _tmr = millis();
      }
      return false;
    }
  private:
    const byte _pin;
    uint32_t _tmr;
    bool _flag;
};

El controlador de botones ahora funciona así: devuelve True si se presionó el botón correspondiente. En el programa principal pondremos el método clic () en la condición y cambiaremos el período del LED de acuerdo con él.

Planeo que en este proyecto solo tendré un LED parpadeante, y no lo envolveré en una clase: simplemente pondré las funciones en un archivo (para un ejemplo de implementación del proyecto de esta manera).

led.h

// LED parpadeante
#pragma once
#include <Arduino.h>
void LEDinit(byte pin, int period);
void LEDblink();
void LEDadjust(int val);

led.cpp

#include "led.h"
//las variables estáticas serán "visibles" solo en este archivo
static int _period;
static byte _pin;
static uint32_t _tmr;
static bool _flag;
void LEDinit(byte pin, int period) {
  _pin = pin;
  _period = period;
  pinMode(_pin, OUTPUT);
}
void LEDblink() {
  if (millis() - _tmr >= _period) {
    _tmr = millis();
    _flag = !_flag;
    digitalWrite(_pin, _flag);
  }
}
void LEDadjust(int val) {
  _period += val;
}

Tenga en cuenta que dentro de los archivos usé variables con los mismos nombres, pero hice que estas variables fueran estáticas u ocultas en una clase. Esto es muy bueno, porque nunca se cruzan entre sí y puede usar lo mismo para denotar variables con un significado similar. Esto nos da dos módulos separados, dos subrutinas separadas con las que se puede interactuar desde el núcleo del programa. Cambié el período de parpadeo del LED a través de la función LEDadjust (), que lleva la corrección al valor actual. El valor «actual» inicial se establece durante la inicialización en LEDinit ().

Bueno, pongamos nuestras bibliotecas al lado del archivo principal del programa, inclúyelos en el código y veamos cómo se ve nuestro proyecto ahora:

// cambiar paso
const int step = 50;
// biblioteca de LED
#include "led.h"
// biblioteca de button
#include "button.h"
Button btn1(2);
Button btn2(3);
void setup() {
  // specifica el pin y el período de inicio
  LEDinit(13, 1000);
}
void loop() {
  LEDblink();   // parpadea
  if (btn1.click()) LEDadjust(step);
  if (btn2.click()) LEDadjust(-step);
}

Bueno, ¡esto es otra cosa! Ahora las moscas están separadas de las chuletas y podemos refinar cuidadosamente ambos módulos independientemente uno del otro. Por cierto, ¿qué pasa con el tamaño del código? El primer ejemplo toma 1306 bytes de Flash y 26 bytes de RAM, y el nuevo … 1216 bytes de Flash y 29 bytes de RAM. La cantidad de código (el número de líneas) ha aumentado, ¡pero su peso ha disminuido en 100 bytes! El hecho es que tenemos dos instancias del botón, que se sondean esencialmente de la misma manera. Hicimos la encuesta como un método de clase y el compilador no la duplicó para diferentes botones.

Desarrollemos un poco el programa y agreguemos otro botón con el que podrás encender y apagar el LED, para lo cual agregaremos esta característica a su biblioteca. Y agregue un método a la clase de botón que devolverá True mientras mantiene presionado el botón para cambiar la frecuencia manteniendo presionado el botón en consecuencia.

Agregue una condición complicada al controlador de botones que volverá True por temporizador, si el botón se mantiene presionado, es decir, aún no se ha soltado después de presionar. Así, será posible cambiar el valor una vez con un “clic”, o mantenerlo presionado y cambiará paso a paso, como en cualquier reloj chino.

button.h

//  clase button
#pragma once
#include <Arduino.h>
#define _BTN_DEB_TIME 100  // tiempo de espera anti-rebote
#define _BTN_HOLD_TIME 400  // тtiempo de espera pulso
class Button {
  public:
    Button (byte pin) : _pin(pin) {
      pinMode(_pin, INPUT_PULLUP);
    }
    bool click() {
      bool btnState = digitalRead(_pin);
      if (!btnState && !_flag && millis() - _tmr >= _BTN_DEB_TIME) {
        _flag = true;
        _tmr = millis();
        return true;
      }
      if (!btnState && _flag && millis() - _tmr >= _BTN_HOLD_TIME) {
        _tmr = millis();
        return true;
      }
      if (btnState && _flag) {
        _flag = false;
        _tmr = millis();
      }
      return false;
    }
  private:
    const byte _pin;
    uint32_t _tmr;
    bool _flag;
};

Puede implementar el LED de encendido / apagado como el «estado» de todo el módulo del programa usando una bandera (el método principal blink () se ejecutará en él), agregarlo a las variables. Hay varias formas de tirar de la bandera:

  • Haga una función toggle () que simplemente invertirá la bandera
  • Haga que las funciones habiliten () y deshabiliten (), la bandera respectivamente
  • Leer el estado actual

Etc. Detengámonos en la configuración manual y la lectura del estado como una opción universal.

led.h

//LED parpadeante
#pragma once
#include <Arduino.h>
void LEDinit(byte pin, int period);
void LEDblink();
void LEDadjust(int val);
void LEDsetState(bool state);
bool LEDgetState();

led.cpp

#include "led.h"
//as variables estáticas serán "visibles" solo en este archivo
static int _period;
static byte _pin;
static uint32_t _tmr;
static bool _flag;
static bool _state = true;
void LEDinit(byte pin, int period) {
  _pin = pin;
  _period = period;
  pinMode(_pin, OUTPUT);
}
void LEDblink() {
  if (_state && millis() - _tmr >= _period) {
    _tmr = millis();
    _flag = !_flag;
    digitalWrite(_pin, _flag);
  }
}
void LEDadjust(int val) {
  _period += val;
}
void LEDsetState(bool state) {
  _state = state;
  if (_state) digitalWrite(_pin, 0);
}
bool LEDgetState() {
  return _state;
}

Agregue un botón más al programa principal en el pin D4 y cambie el estado del LED:

// cambiar paso
const int step = 50;
//biblioteca de LED
#include "led.h"
//  biblioteca de botones
#include "button.h"
Button btn1(2);
Button btn2(3);
Button btn3(4);
void setup() {
  // especifica el pin y el período de inicio
  LEDinit(13, 1000);
}
void loop() {
  LEDblink();   //  parpadea
  if (btn1.click()) LEDadjust(step);
  if (btn2.click()) LEDadjust(-step);
  if (btn3.click()) LEDsetState(!LEDgetState());
}

Ahora los botones en los pines 2 y 3 hacen clic para aumentar y disminuir la frecuencia de parpadeo del LED, cuando se mantiene presionado, la frecuencia cambia automáticamente con el mismo paso y se configura en el período button.h, y al hacer clic en el botón del pin 4, puede activar o desactivar el proceso de parpadeo.

Así, ya hemos obtenido algunos desarrollos, que pueden insertarse completamente en otro proyecto, directamente en un archivo, y utilizarse. Ésta es la belleza de la programación orientada a objetos y, en general, el concepto de separar datos del código y rechazar variables globales para todo el programa.

Ejemplo 2

A continuación, recordemos el ejemplo con la estación meteorológica de la lección sobre cómo escribir un boceto en el curso básico y vamos a tratar de «peinarlo» un poco: envolver todo en clases, dispersarlo en archivos separados y separar los datos entre sí. Aunque todavía crearemos una clase para el temporizador milis, porque allí se usa en tres lugares, y con un mayor refinamiento, es posible que se necesiten más temporizadores. El proyecto original tiene 10.078 bytes de Flash y 511 RAM.

Estación meteorológica

// ajustes
#define ONE_WIRE_BUS 2  // пин ds18b20
// biblotecas
#include <RTClib.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <OneWire.h>
#include <DallasTemperature.h>
// OBJETOS Y VARIABLES
// la dirección puede ser 0x27 o 0x3f
LiquidCrystal_I2C lcd(0x3f, 16, 2); //// Configurar la pantalla
RTC_DS3231 rtc;
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
uint32_t myTimer1, myTimer2, myTimer3;
boolean LEDflag = false;
float tempSum = 0, temp;
byte tempCounter;
void setup() {
  Serial.begin(9600); // para depurar
  pinMode(13, 1);
  // mostrar
  lcd.init();
  lcd.backlight();  //Enciende la luz de fondo de la pantalla
  // termometro
  sensors.begin();
  sensors.setWaitForConversion(false);  //obtener datos de forma asincrónica
  // reloj
  rtc.begin();
  // establecer el tiempo para compilar el tiempo
  if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
}
void loop() {
  //2 veces por segundo
  if (millis() - myTimer1 >= 500) {
    myTimer1 = millis(); //  reiniciar el temporizador
    toggleLED();
  }
  // 5 veces por segundo
  if (millis() - myTimer2 >= 200) {
    myTimer2 = millis(); //  reiniciar el temporizador
    getTemp();
  }
  // cada segundo
  if (millis() - myTimer3 >= 1000) {
    myTimer3 = millis(); //  reiniciar el temporizador
    redrawDisplay();
  }
}
void toggleLED() {
  digitalWrite(13, LEDflag); //  encendido apagado
  LEDflag = !LEDflag; //invertir flag
}
void getTemp() {
  // suma la temperatura en una variable común
  tempSum += sensors.getTempCByIndex(0);
  sensors.requestTemperatures();
  //contador de medidas
  tempCounter++;
  if (tempCounter >= 5) { // si es 5
    tempCounter = 0;  // cero
    temp = tempSum / 5; // promedio
    tempSum = 0;  // cero
  }
}
void redrawDisplay() {
  // hora
  DateTime now = rtc.now(); //consigue tiempo
  lcd.setCursor(0, 0);      // cursor a 0,0
  lcd.print(now.hour());    // reloj
  lcd.print(':');
  // el primer cero es para decorar
  if (now.minute() < 10) lcd.print(0);
  lcd.print(now.minute());
  lcd.print(':');
  // el primer cero es para decorar
  if (now.second() < 10) lcd.print(0);
  lcd.print(now.second());
  // TEMP
  lcd.setCursor(11, 0);    // cursor en 11.0
  lcd.print("Temp:");
  lcd.setCursor(11, 1);    // cursor en 11,1
  lcd.print(temp);
  // fecha
  lcd.setCursor(0, 1);      //cursor en 0,1
  //el primer cero es para decorar
  if (now.day() < 10) lcd.print(0);
  lcd.print(now.day());
  lcd.print('.');
  //el primer cero es para decorar
  if (now.month() < 10) lcd.print(0);
  lcd.print(now.month());
  lcd.print('.');
  lcd.print(now.year());
}

Entonces, envolví todo en clases e hice estáticos los objetos de las bibliotecas externas conectadas, para que no fueran «visibles» desde el programa principal y no pudieran mezclarse con nada. No toqué la pantalla, toda la salida permaneció como estaba, la pantalla en sí «se enlaza» en el boceto principal.

Boceto principal

// BIBLIOTECAS
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2); // Configurar la pantalla
#include "led.h"
Led led(13);  // LED en el pin 13
#include "timer.h"
Timer ledTimer(500);      // Temporizador LED para 500ms
Timer tempTimer(800);     //  temporizador del sensor 800 ms
Timer displayTimer(1000); // muestra la salida 1 segundo
#include "realTime.h"
RealTime rtc;
#include "temperature.h"
Temperature dallas;
void setup() {
  Serial.begin(9600); //para depurar
  dallas.begin();
  rtc.begin();
  lcd.init();
  lcd.backlight();  // Enciende la luz de fondo de la pantalla
}
void loop() {
  if (ledTimer.ready()) led.toggle();
  if (tempTimer.ready()) dallas.filter();
  if (displayTimer.ready()) redrawDisplay();
}
void redrawDisplay() {
  // hora
  rtc.update(); // pilla tiempo
  lcd.setCursor(0, 0);      //cursor en 0,0
  lcd.print(rtc.hour());    // reloj
  lcd.print(':');
  
  //  el primer cero es para decorar
  if (rtc.minute() < 10) lcd.print(0);
  lcd.print(rtc.minute());
  lcd.print(':');
  
  // el primer cero es para decorar
  if (rtc.second() < 10) lcd.print(0);
  lcd.print(rtc.second());
  
  // TEMP
  lcd.setCursor(11, 0);    // cursor en 11.0
  lcd.print("Temp:");
  lcd.setCursor(11, 1);    // cursor en  11,1
  lcd.print(dallas.get());
  
  // fecha
  lcd.setCursor(0, 1);      // cursor en  0,1
  
  // el primer cero es para decorar
  if (rtc.day() < 10) lcd.print(0);
  lcd.print(rtc.day());
  lcd.print('.');
  
  //el primer cero es para decorar
  if (rtc.month() < 10) lcd.print(0);
  lcd.print(rtc.month());
  lcd.print('.');
  lcd.print(rtc.year());
}

led.h

#pragma once
#include <Arduino.h>
// Clase de LED
class Led {
  public:
    // crear un pin
    Led (byte pin) {
      _pin = pin;
      pinMode(_pin, OUTPUT);
    }
    // cambiar de estado
    void toggle() {
      _state = !_state;
      digitalWrite(_pin, _state);
    }
  private:
    byte _pin;
    bool _state;
};

timer.h

#pragma once
#include <Arduino.h>
//  clase de temporizador por milisegundos
class Timer {
  public:
    //  crear con un período específico
    Timer (int period) {
      _period = period;
    }
    //devuelve verdadero cuando se activa el período
    bool ready() {
      if (millis() - _tmr >= _period) {
        _tmr = millis();
        return true;
      }
      return false;
    }
  private:
    uint32_t _tmr;
    int _period;
};

realTime.h

#pragma once
#include <Arduino.h>
#include <RTClib.h>
#include <Wire.h>
class RealTime {
  public:
    void begin();
    void update();
    byte hour();
    byte minute();
    byte second();
    byte day();
    byte month();
    int year();
  private:
    byte _h, _m, _s;
    byte _day, _month;
    int _year;
};

realTime.cpp

#include "realTime.h"
static RTC_DS3231 rtc;
void RealTime::begin() {
  rtc.begin();
  // establecer el tiempo para compilar la hora
  if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
}
void RealTime::update() {
  DateTime now = rtc.now();
  _h = now.hour();
  _m = now.minute();
  _s = now.second();
  _day = now.day();
  _month = now.month();
  _year = now.year();
}
byte RealTime::hour() {
  return _h;
}
byte RealTime::minute() {
  return _m;
}
byte RealTime::second() {
  return _s;
}
byte RealTime::day() {
  return _day;
}
byte RealTime::month() {
  return _month;
}
int RealTime::year() {
  return _year;
}

temperature.h

#pragma once
// clase de sondeo y filtrado del sensor
#define ONE_WIRE_BUS 2  // pin ds18b20
#include <Arduino.h>
#include <OneWire.h>
#include <DallasTemperature.h>
class Temperature {
  public:
    void begin();
    void filter();
    float get();
  private:
    float tempSum = 0, temp = 0;
    byte tempCounter = 0;
};

temperature.cpp

#include "temperature.h"
static OneWire oneWire(ONE_WIRE_BUS);
static DallasTemperature sensors(&oneWire);
void Temperature::begin() {
  // termómetro
  sensors.begin();
  sensors.setWaitForConversion(false);   // obtener datos de forma asincrónica
}
void Temperature::filter() {
  // suma la temperatura en una variable común
  tempSum += sensors.getTempCByIndex(0);
  sensors.requestTemperatures();
  //  contador de medidas
  tempCounter++;
  if (tempCounter >= 5) {   // si más de 5  
    tempCounter = 0;        // si 0
    temp = tempSum / 5;     // promedio
    tempSum = 0;            // si 0
  }
}
float Temperature::get() {
  return temp;
}

Sí, el código se ha hecho mucho más grande, lo escribimos más largo, ahora toma 10322 y 539 bytes de Flash y RAM, respectivamente (240 y 28 bytes más), pero nuestro boceto se  ha convertido en un proyecto completo: puedes trabajar y completar cada «módulo» por separado y no tener miedo de interferir con el código principal, puede reemplazar muy convenientemente el sensor o el reloj en tiempo real con cualquier otro, y así sucesivamente. Será agradable y comprensible trabajar con dicho código incluso después de varios años, cuando todo esté olvidado, y será más fácil para otra persona entenderlo. Esta es la esencia de este enfoque para escribir programas grandes.


Deja un comentario