Guardar y cargar variables en la EEPROM

Wemos D1 R1 ESP8266 eeprom

La utilización de la EEPROM en los proyectos de Arduino solía ser un tema relativamente escabroso:

Guardar y cargar variables en la EEPROMEn mi caso, recuerdo terminar desarmando cada variable en bytes y guardando así byte a byte su contenido.

Luego debía repetir el proceso a la inversa, leyendo uno a uno los bytes almacenados en la EEPROM y recuperando los valores tipo integer, double, o char[] que en su momento había almacenado allí.

No obstante hay un mecanismo tremendamente sencillo (y útil) que una vez lo integras a tu código, seguramente te acompañará en cada proyecto que realices.

Hoy, este es mi mecanismo favorito para almacenar parámetros de configuración y valores que requieren ser preservados ante un reset de mis ESP8266 (o básicamente cualquier otro  microcontrolador compatible con Arduino, quizás con alguna pequeñísima modificación).

Como primer paso, al inicio de nuestro código no debemos olvidarnos de incluir la librería EEPROM.h:

#include <EEPROM.h> // Manejo de la EEPROM

Una struct encapsulando las variables en EEPROM

En nuestro código vamos a definir una estructura que contenga las variables necesarias por ejemplo para la configuración de nuestro proyecto.

/* Estructura que carga y guarda en eeprom */
struct config_t {
	int puerto,retries=0;
	char ssid[12];
	byte valor1,valor2,valor3 = 0;
	IPAddress sts;
} config;

Y luego agrego estas dos funciones a mi código. Bastará llamarlas cada vez que sea necesario guardar o cargar el contenido de ese struct en la EEPROM.

//Guardo en la eeprom el struct config
void eepromsave() {
	EEPROM.begin(sizeof(config));
	EEPROM.put(0, config); //Guardo la config
	EEPROM.end();
}

//Cargo de la eeprom el struct config
void eepromload() {
	EEPROM.begin(sizeof(config));
	EEPROM.get(0, config); //Cargo config
	EEPROM.end();
}

Por si no estás ducho en el tema de estructuras, puedes utilizar las variables como si fuera cualquier variable, pero dentro de la estructura.

Ejemplo: para guardar un valor en la variable INT llamada puerto, haces

config.puerto = valor;

¿ Cómo funciona una struct en este contexto ?

En la interna, nuestra estructura se almacena en memoria con su contenido ordenado en forma contigua, en bloque.

El nombre de la estructura ( tal como ocurre con el nombre de cualquier variable) actúa como un simple puntero donde se inicia su espacio de almacenamiento en memoria.

El otro dato necesario es la cantidad de bytes que ocupa la estructura, y eso se obtiene haciendo un simple sizeof(config).

Esos dos datos son los únicos necesarios para identificar ese bloque de memoria que contiene nuestra estructura.

Por eso es tan simple, guardar todo de una sola vez en la EEPROM: Simplemente nos limitamos a guardar o cargar el paquete de bytes definido por la estructura, con su inicio y tamaño.

Lo de adentro, se resuelve solo, porque las variables están declaradas explícitamente en su interior. Si necesitas agregar más variables en tu proyecto, simplemente los agregas dentro de tu estructura config.

Consejo a considerar al inicio de tu proyecto

Debes considerar que la EEPROM puede contener cualquier valor arbitrario (basura) tal como viene de fábrica.

Por este motivo te conviene inicializar previamente tu estructura con valores conocidos, “por defecto” y guardarlos en la EEPROM, antes de leer nada desde allí, la primera vez que corre tu código

Para lograr esto puedes agregar en tu código una función como la siguiente:

//Inicializo valores para eeprom
void eeprominit() {
        config.eepromok=49834;
	config.puerto=2828;
        config.retries=3;
	strncpy(config.ssid, "Miproyecto01", 12);
        config.valor1=10;
        config.valor2=127;
        config.valor3=255;
	config.sts[0] = 0;
	config.sts[1] = 0;
	config.sts[2] = 0;
	config.sts[3] = 0;
    //Guardo valores por defecto en la eeprom 
	eepromsave(); 
}

 

Al iniciar la ejecución de tu código, cargas la config desde la eeprom.

A continuación,  verificas que una variable INT que has puesto dentro de la estructura config para este propósito tenga un valor arbitrario conocido:

Como ejemplo, digamos el valor 49834 en la variable config.eepromok

Si la variable tiene ese valor, quiere decir que está todo bien. Pero si hay cualquier otro valor, llamas a la función eeprominit() porque eso significa que la eeprom aún no fue inicializada.

Reloj DS3231 de alta precisión y eficiencia

Reloj-DS3231-i2c

El reloj DS3231 es un integrado bastante versátil capaz de llevar cuenta del tiempo y fecha con una precisión de +/- 2 minutos por año.

Reloj-DS3231-i2c-bateria-arduinoPuedes conseguirlo por un costo de 5 a 10 USD en la tienda de Amazon.

El integrado DS3231 es presentado usualmente en un pequeño circuito que incluye, entre otros componentes, una pila de litio CR2032 (en la parte de abajo del circuito, por eso no se ve en la foto) que le brinda autonomía de hasta unos 5 años, aún sin otra fuente de alimentación.

El DS3231 cuenta con un interfaz i2c desde el cual podremos ajustar y consultar su información.

Puede ser alimentado tanto con 3.3v como 5v lo que da buena comodidad en nuestros proyectos Arduino independientemente de que controladora y voltajes utilicemos.

Manipulando el DS3231 desde Arduino

Hay numerosas librerías disponibles para controlar al DS3231 en nuestros proyectos Arduino.

En mi caso, me destilé por la librería SODAQ DS3231 que está disponible para su integración desde el propio gestor de librerías del IDE oficial de Arduino.

NOTA: La librería SODAQ no compila (al menos la versión que tengo en este momento) correctamente para el ESP8266. Tuve que buscar el archivo Sodaq_DS3231.cpp y al principio donde se hace el #include avr\pgmspace.h cambiarlo por:

#if (defined(__AVR__))
#include <avr\pgmspace.h>
#else
#include <pgmspace.h>
#endif

Para mi actual proyecto, conecté el reloj DS3231 a una controladora ESP8266 para así obtener buena precisión horaria en  el registro histórico de datos.

Cómo vuelta de tuerca, hago que el ESP8266 consulte via Internet con un servidor de NTP al ser encendido y así ajuste el reloj del DS3231.

Ajustando el reloj DS3231 con un servidor NTP via Internet

Antes de presentar el código que utilizo para ajustar el DS3231 via Internet con un servidor NTP, necesito explicar algunas cosas que verán allí:

Primero, el objeto ssd es simplemente una pantalla OLED que tengo conectada -también vía i2c a mi ESP8266.

Segundo, la función que presento aquí abajo es llamada dentro de mi código general luego de confirmar que el ESP8266 se ha conectado a mi red WIFI.  No estoy poniendo toda esa parte del software porque serían cientos de líneas de código que no considero “centrales” al tema de este artículo.

Por último, hay una serie de variables y objetos que se inicializan para que puedan ser consumidos por esta función, que voy a intentar detallar aquí debajo e iría en los inicios de tu proyecto de software para Arduino:

/* ==== Includes ==== */
#include <WiFiUdp.h> //Se necesita para consultar el NTP y ajustar el reloj
#include <Sodaq_DS3231.h> //Librería para el DS3231

/*==== Objetos-libs ====*/
Sodaq_DS3231 clock;  //RTC DS3231
WiFiUDP udp;	//Instancia UDP para enviar y recibir paquetes al Servidor NTP

/* Variables para el RTC y acceso NTP */
unsigned int localPort = 2390;      //puerto local para recibir paquete UDP desde el NTP server
IPAddress timeServerIP; // donde voy a guardar la IP del servidor NTP
const char* ntpServerName = "0.pool.ntp.org"; //el servidor NTP
const int NTP_PACKET_SIZE = 48; // tamaño del paquete NTP que nos interesa
byte packetBuffer[NTP_PACKET_SIZE]; //buffer para paquetes entrantes y salientes al NTP
const double timezone = -3.0;

/* ==== Setup ==== */
void setup() {
        //aquí va todo el código de tu Setup, y al final, activas el reloj:
	clock.begin();
}

Esta es la función que hace la magia de consultar al servidor NTP y así ajustar el DS3231 con los datos precisos del tiempo y fecha:

//Actualizar reloj RTC con datos del NTP
void actualizareloj()
{
	ssd.clearDisplay();
	ssd.setCursor(0, 0);
	ssd.println("Actualizando RTC");
	ssd.println(ntpServerName);
	ssd.display();
	if (udp.begin(localPort) == 0) //activo puerto udp 2390 para los paquetes del servidor NTP
	{
		ssd.print("\nERROR PUERTO UDP");
	}
	else {
		while (udp.parsePacket() > 0); // discard any previously received packets
		WiFi.hostByName(ntpServerName, timeServerIP); //Busco IP del servidor NTP
		if (timeServerIP[0] == 0)
		{
			ssd.println("\nERROR NO IP");
		}
		else
		{
			ssd.println("IP " + (String)timeServerIP[0] + '.' + (String)timeServerIP[1] + '.' + (String)timeServerIP[2] + '.' + (String)timeServerIP[3]);
			ssd.display();
			yield();
			// Envio paquete NTP para pedir hora
			memset(packetBuffer, 0, NTP_PACKET_SIZE);
			packetBuffer[0] = 0b11100011;   // LI, Version, Mode
			packetBuffer[1] = 0;     // Stratum, or type of clock
			packetBuffer[2] = 6;     // Polling Interval
			packetBuffer[3] = 0xEC;  // Peer Clock Precision
									 // 8 bytes of zero for Root Delay & Root Dispersion
			packetBuffer[12] = 49;
			packetBuffer[13] = 0x4E;
			packetBuffer[14] = 49;
			packetBuffer[15] = 52;
			udp.beginPacket(timeServerIP, 123); //NTP requests are to port 123
			udp.write(packetBuffer, NTP_PACKET_SIZE);
			udp.endPacket();
			yield();
			//Ahora a esperar la respuesta del servidor NTP:
			int cb = 0;
			unsigned long mi = millis();
			while (millis() - mi < 4000 && !cb)
			{
				cb = udp.parsePacket();
				yield();
			}
			if (cb != 0)
			{
				//Vino paquete con hora
				udp.read(packetBuffer, NTP_PACKET_SIZE); // Levantamos paquete al buffer
				unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
				unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
				// combine the four bytes (two words) into a long integer
				// this is NTP time (seconds since Jan 1 1900):
				unsigned long secsSince1900 = highWord << 16 | lowWord;
				// now convert NTP time into everyday time:
				// Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
				const unsigned long seventyYears = 2208988800UL;
				// subtract seventy years:
				unsigned long epoch = secsSince1900 - seventyYears;
				// adjust to user timezone
				epoch += timezone * 3600;
				ssd.println("\nOK");
				clock.setEpoch(epoch);
			}
			else {
				ssd.print("\nERROR NO RESPONDE");
			}
		}
		udp.stopAll();
	}
	ssd.display();
	delay(3000);
}

Como siempre, por favor espero dudas y comentarios acerca de este código para gestionar el DS3231, o simplemente para que cuenten en que andan con Arduino.

Sensor de flujo de agua

Llegó el turno del sensor de flujo de agua, otra pieza más del rompecabezas que estoy armando para mi módulo IOT de registro de consumo de agua en mi domicilio.

Sensor-de-flujo-de-agua-parametros-de-funcionamiento-ArduinoPrimero tuve que dar caza a un sensor de consumo de agua apropiado. Hay diversas tecnologías y métricas en el mercado de venta de sensores de “water flow“.

(Lamentablemente) opté por un sensor de flujo de agua de bajo costo, debido a que los verdaderamente fiables cuestan varios cientos de dólares (o más).

Adquirí en la tienda de amazon.com un “Water Flow Sensor” de la marca DIGITEN, que mide caudales de hasta 60 litros por minuto, con un pase de rosca de 3/4 de pulgada.

En su configuración final, este sensor irá colocado después de la válvula o llave automática de la que hablé en mi post anterior.

Por otra parte, es el sensor preferido por los entusiastas de Arduino y otros microcontroladores, por lo que hay una cantidad de ejemplos de uso.

¿ Cómo funciona este sensor de flujo de agua ?

sensor de flujo de agua adentroEl sensor de flujo de agua es muy similar al  contador de agua que coloca la compañía a la entrada de nuestro domicilio:

Dentro del sensor existe un eje con una serie de paletas, convenientemente colocadas en el medio del paso del agua, que girarán con la circulación del líquido.

En la parte superior del eje se produce la medición gracias a un sensor de “hall effect” que al girar y por medio de un imán,  cierra un circuito mandando impulsos al Arduino.

El sensor de flujo de agua tiene en su etiqueta superior una serie de datos, entre los cuales se incluye la presión de trabajo (menor a 1.2MPa), capacidad (1 a 6 litros por minuto) y coeficiente para calcular litros (se multiplica la cantidad de impulsos por 5.5 para obtener los litros por minuto).

Utilizando el sensor de flujo de agua en un proyecto Arduino

De los tres cables que salen del sensor de flujo de agua, dos se utilizan para alimentarlo:

Si bien dice funcionar con 5 volts, yo lo he conectado a la salida de 3.3v de mi ESP8266 y parece funcionar perfectamente.

El tercer cable se conecta a una pata digital del Arduino compatible que pueda funcionar con interrupciones.

//El sensor de flow de agua va al gpio5 (D1)
#define aguapin 5 </code>

//Variable que va a ir sumando impulsos
unsigned long aguacont=0;
//preparo timer cada 1 segundo
unsigned long mspasadosseg = 0;
const unsigned long millisxseg = 1000;

//Inicializo el WATER FLOW METER:
pinMode(aguapin, INPUT_PULLUP);
attachInterrupt(aguapin, agua, CHANGE);

void setup()
{
	Serial.begin(115200);
	//Inicializo el WATER FLOW METER:
	pinMode(aguapin, INPUT_PULLUP);
	attachInterrupt(aguapin, agua, CHANGE);
}

void loop()
{
	yield(); //para el ESP8266
	if ((millis() - mspasadosseg) &gt; millisxseg) //Si ya pasó un segundo ...
	{
		mspasadosseg = millis();
		Serial.println((String)aguacont);
	}
}

//Cada vez que el hall sensor del molinete del medidor de agua se activa
void agua()
{
	aguacont += 1;
}

 

La idea es activar una interrupción al inicio de nuestro código, para que llame a una función que contabilice cada impulso que el sensor de flujo de agua genere.

Evidentemente el código de arriba es básico y solamente sirve para ilustrar los rudimentos de funcionamiento. En futuros artículos entraremos a pulir este sensor de flujo de agua.

Bus i2c Scanner

Este pequeño programa para Arduino buscará entre las direcciones del bus i2c en busca de componentes conectados que respondan, oficiando asi de escáner de bus i2c.

Scanner de bus i2cMe ha ocurrido ya un par de veces que al adquirir un componente que se conecta al bus de datos i2c,  la dirección explicitada en la documentación no es correcta.

Pues bien, la solución es cargar momentáneamente este pequeño programa y descubrir asi donde se esconde el componente en cuestión.

Indudablemente un ahorro de tiempo importante, a mi me ha resultado útil y por eso quería compartirlo con ustedes para que lo tengan presente:

Código del Scanner de bus i2c

Simple en su funcionamiento, no necesita mayor detalle este pequeño scanner de i2c:

#include <Wire.h>
 
void setup()
{
  Wire.begin();
  Serial.begin(115200);
  while (!Serial);             // Esperamos a que el serial se active
  Serial.println("\nI2C Scanner");
}
 
void loop()
{
  byte error, address;
  int nDevices;
  Serial.println("Escaneando...");
  nDevices = 0;
  for(address = 1; address < 127; address++ )
  {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0)
    {
      Serial.print("Componente I2C encontrado en 0x");
      if (address<16)
        Serial.print("0");
      Serial.print(address,HEX);
      Serial.println("  !");
 
      nDevices++;
    }
    else if (error==4)
    {
      Serial.print("Error en 0x");
      if (address<16)
        Serial.print("0");
      Serial.println(address,HEX);
    }    
  }
  if (nDevices == 0)
    Serial.println("No encontré nada en I2C\n");
  else
    Serial.println("TERMINADO\n");
 delay(5000);        
}

Al ejecutarlo, el programa les listará las direcciones donde encontró componentes i2c.

Enviar email con un Ethernet Shield W5100

Ethernet Shield W5100

El Ethernet Shield W5100 es compatible con los pines del Arduino UNO, Arduino Mega y algunos otros, siempre y cuando el módulo Arduino incluya el conector ICSP de 6 pines.

Ethernet Shield W5100El Ethernet Shield W5100 es accesible por el entorno de los 10 USD en todas las tiendas electrónicas:  Por ejemplo puedes encontrar infinidad de marcas y presentaciones del W5100 en amazon.com.

Como soy un tanto más rebuscado, para este pequeño proyecto Arduino voy a conectar el Ethernet Shield W5100 a un módulo WEMOS D1 R2 (basado en el ESP8266) compatible con la programación Arduino, que justamente carece del conector ICSP.

El conector ICSP expone en forma estandarizada la conectividad SPI entre el módulo Arduino y sus periféricos. Pero si el módulo compatible con Arduino que quieres utilizar carece de conector ICSP, entonces será simplemente cuestión de recablear correctamente tu proyecto para acceder al bus SPI.

Conectando el Ethernet Shield W5100 al Wemos D1 R2

El Ethernet Shield W5100 puede ser fácilmente conectado al módulo Wemos D1 R2 con 8 cables, de acuerdo a este pinout:

WEMOS   W5100
GND --- GND (pin 1 ICSP)
5V  --- 5V  (pin 3 ICSP)
3v3 --- 3v3
RST --- RST (pin 4 ICSP, abajo del GND)
SS  --- SS (pin 10)
MOSI--- MOSI (pin 2 ICSP, entre 5V y GND)
MISO--- MISO (pin 6 ICSP, abajo 5V)
CLK --- CLK (pin 5 ICSP, entre MISO y RST)

Queda claro entonces que en vez de montar el Ethernet W5100 sobre el WEMOS D1 R2, los ponemos uno al lado del otro y realizamos ese cableado.

Si estás utilizando un Arduino Mega, u otro, con conector ICSP, simplemente colocas el Shield Ethernet W5100 como lo harías normalmente con cualquier Shield.

En ese caso toma precaución que ninguna parte de metal (o soldaduras) del Ethernet Shield toquen alguna parte del circuito del módulo Arduino:

En mi caso, durante otro proyecto, cuando monté el Ethernet Shield al Arduino MEGA, el conector de ETHERNET facilmente podía hacer cortocircuito con una parte del MEGA, por lo que tuve que separarlos unos milímetros (igual hacían contacto para funcionar correctamente).

Enviar un correo con el Ethernet Shield W5100

Es posible enviar un correo con el Ethernet Shield W5100 y aquí debajo te dejo el código que yo utilicé con suceso.

#include <SPI.h>
#include <Ethernet.h>     //Ethernet Shield

EthernetClient client;

//Shield requiere un MACADDRESS UNICO
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; 

//Configura una IP, gateway y subnet para tu red local:
IPAddress ip(192, 168, 2, 19);  
IPAddress gateway(192, 168, 2, 1);
IPAddress subnet(255, 255, 255, 0);

//Reemplaza las xxx con la IP de tu servidor de correo
IPAddress servidormail(xxx, xxx, xxx, xxx);

void setup()
{
	Serial.begin(9600);
	delay(1000);
	Serial.println("INICIO SISTEMA");
	Ethernet.begin(mac, ip, gateway, gateway, subnet);
	delay(2000);
	Serial.println("Ethernet Inicializado");

        //Cambia las cuentas de correo de remitente y destino por las que quieras probar tu!
	byte resultado = mandaremail(servidormail, "Hola desde ARDUINO", "Rutina de correo funciona", "remitente@correo.com", "destino@correo.com");
	Serial.println("RESULTADO DE ENVIAR EMAIL: " + (String)resultado);
}

void loop()
{
	/* add main program code here */
}


byte mandaremail(IPAddress smtp, String titulo, String mensaje, String de, String para)
{
	byte thisByte = 0;
	byte respCode;
	Serial.println("Mandar correo iniciado");
	if (client.connect(smtp, 25)) //Me conecto o devuelvo error
	{
		Serial.println(F("connectado"));
	}
	else {
		Serial.println(F("No pudo conectarse"));
		return 0;
	}

	if (!eRcv()) return 1;

	// coloca una ip pública tuya
	client.println(F("helo 1.2.3.4"));
	if (!eRcv()) return 2;
	client.println("MAIL From: <" + de + ">");
	if (!eRcv()) return 3;
	client.println("RCPT To: " + para);
	if (!eRcv()) return 4;
	client.println(F("DATA"));
	if (!eRcv()) return 5;
	client.println("To: You <" + para + ">");
	// change to your address
	client.println("From: cuenta <" + de + ">");
	client.println("Subject: " + titulo + "\r\n");
	client.println(mensaje);
	client.println(F("."));
	if (!eRcv()) return 6;
	client.println(F("QUIT"));
	if (!eRcv()) return 7;
	client.stop();
	return 255; // Email enviado
}

byte eRcv()
{
	byte respCode;
	byte thisByte;
	int loopCount = 0;
	while (!client.available()) {
		delay(1);
		loopCount++;
		// Si no recibo nada en 10 segs, doy timeout
		if (loopCount > 10000) {
			client.stop();
			Serial.println(F("\r\nTimeout"));
			return 0;
		}
	}
	respCode = client.peek();
	while (client.available())
	{
		thisByte = client.read();
		Serial.write(thisByte);
	}
	if (respCode >= '4')
	{
		efail();
		return 0;
	}
	return 1;
}

void efail()
{
	byte thisByte = 0;
	int loopCount = 0;

	client.println(F("QUIT"));

	while (!client.available()) {
		delay(1);
		loopCount++;

		// Si no recibo nada en 10 segs, doy timeout
		if (loopCount > 10000) {
			client.stop();
			Serial.println(F("\r\nTimeout"));
			return;
		}
	}

	while (client.available())
	{
		thisByte = client.read();
		Serial.write(thisByte);
	}
	client.stop();
	Serial.println(F("desconectado"));
}

Este código ejecuta su magia durante el setup {} y  es un ejemplo, nada más, de una funcionalidad básica para enviar correos desde tu Arduino.

Igualmente considera que necesitarás un servidor de correos (protocolo SMTP, sin encriptar) convencional, que confíe en la IP desde donde le conversará tu Arduino.