Blog

Latch-up anyone?

(meditations on academically correct content in technical articles and application notes)

Some days ago, I was chatting with an old friend and mentor about electronic design stuff, and he raised the latch-up subject. Later, while I was reviewing an article inside my head (one I haven’t written yet), I recalled a similar example, on a different realm, that brought this to my mind, and so it takes form in this post.

My thoughts were pondering how bad some features and ease of use can be, when we consider teaching and educational aspects. Around 2004, I attended a developer’s conference from a famous company selling “easy” microcontrollers (that are in reality the hardest thing to approach for someone who already knows what a microcontroller is and happens to work on many of them from different brand names). The presenter was showing an application note on which I could see a relay connected to a microcontroller I/O port (GPIO), to which I respectfully questioned if, at least, wasn’t it a bit convenient to add a flywheel (or “protection”) diode in there, and he confidently answered “all our microcontrollers have protection diodes in GPIO pins”.

Let’s start by saying that I’m one of those engineers who don’t use built-in weak pull-ups unless there is a reason to use them. Why? Because I try to keep my design as independent from manufacturer details as possible (in the future I might port this design to a “difficult” microcontroller, not having these cute add-ons), and also firmware independent; saving those tenths of a cent (setting PCB real estate aside) does not justify me being attentive to remembering to enable pull-ups or later find unintended operation up to the point when the code hits that instruction. In addition, in ultra-low power designs those internal weak pull-ups are unpredictable and can even be counterproductive. Furthermore, to anyone not having the design log (if it exists), there is no clue on the “academic” reason why that resistor has to be there, and all it says about the rest of the circuit; to me that is very important in an application note, as it is an engineering document.

Back to this relay being controlled by a GPIO, let’s say this leads to an enthusiast probably learning conflictive techniques that he may later replicate in other realms when he advances (I feel tempted to remind you all of the effects of using only BASIC to teach youngsters how to program in the 80’s).

First issue: the current through the relay. GPIOs usually present a MOS push-pull structure, that is, a P-channel MOSFET sources current from the power supply while a N-channel MOSFET drains current to ground. These MOSFETs usually have different areas, being the N-channel one bigger and so having more current capacity, and in many cases even not enough to drive a relay. Depending on how this relay is connected, it may operate correctly because the GPIO does not raise the output voltage too much, as the N-channel MOSFET is able to handle that current; or it may operate erratically because the GPIO lowers the output voltage too much, as the P-channel MOSFET is not able to handle that current. Other side of this is that, depending on how this relay is connected, its current will flow through the GND pin or the VDD pin, that is, it will enter the microcontroller through the power pin, coming from the power supply, or will return to it through the ground pin, creating, among other things, current circulation in unintended paths, and this can affect operation during relay turn-on and turn-off transients. This can be caused by parasitic inductance in the PCB tracks, and by loop antennas formed by the current flowing on one path and returning on a different path, that irradiate in all directions with an intensity that is proportional to the loop area. The current flowing on the ground pin can also affect measurements on a built-in analog-to-digital converter (the current on the power pin can also do it if the microcontroller saves the analog section power pin, VDDA).

Second issue: the current that should stop flowing through the relay when it opens, but due to it being an inductor, Faraday and Lenz conspire against us: V = -L di/dt.

When we want to open the relay, we need to set the GPIO on a high-impedance state, or at least the opposite state to which we used to turn it on. Let’s suppose we activate it by connecting it to ground, so ideally we should now configure an open-drain pin or at least connect it to the positive power supply. Being the relay coil an inductor, its current can’t stop flowing immediately, it will try to keep flowing. Depending on how hard it is for the inductor “to keep this current constant” (di/dt), it will develop a voltage on its terminals, and this will have the polarity it needs to cause that current to flow, and a value proportional to the inductance. That polarity causes the voltage at the microcontroller pin to be higher than its power supply voltage, as much as it needs in order to cause that current to flow. On which path will this current flow ? Ideally through a flywheel diode, connected in parallel with the relay coil, using the appropriate polarity. Being it absent, through wherever the induced high-voltage may find the path; in this very case, through the protection diode in the GPIO, returning to the relay by the power trace that connects them (in the opposite direction, producing a voltage raise in the microcontroller power supply), if we set the pin in a high-impedance state. If, on the other hand, we set the pin as a positive output, it will have to flow through the P-channel MOSFET, also in the opposite direction, probably through the intrinsic diode that can be found in the crystalline structure, and usually behaves as protection diode; or trough a real protection diode, whatever presents the lower impedance, and following the same path we already mentioned.

Let’s assume those protection diodes are designed to withstand the current flowing through the relay coil, and the rest of the circuit can cope with these effects without issues. What happens if we replicate this design using a microcontroller with no built-in protection diodes ? (These diodes are often undesirable, but this article will become too long…)

Some paragraphs above we said “through wherever the induced high-voltage may find the way”. Sometimes that path can be through some internal peripheral, producing erratic behavior; others, something much more risky can happen.

In semiconductor chip structures, some parasitic PNPN structures can be formed; this is an undesired thyristor. This thyristor may trigger via a trigger current if something is able to inject enough current through the block operating as the gate (depending on polarity); or via overvoltage, if enough voltage develops at the ends of this structure, exceeding its breakdown voltage. This parasitic thyristor then turns on and will stay on until the power supply is removed; most likely the crystalline structure will melt as it isn’t able to dissipate the excess of heat, forming a short circuit. Latch-up.

Therefore, it is often much more educational to control the relay using a transistor and place the necessary flywheel diode, carrying the relay current away from the A/D converter. Otherwise, we can place a comment warning us on the side effects we might have and the components we’ve been able to remove from the circuit thanks to this fancy feature; this might arise enough curiosity on the reader and push him to investigate further.

Even though new fabrication technologies minimize latch-up possibility, it is always convenient to place series resistors on those pins that might be exposed to voltages higher than the power supply voltage, in order to limit a possible trigger current and also help to extinguish current through the thyristor, or at least limit it so it does not destroy the crystalline structure.

TLS, Transport Layer Security

Those who want to connect to a server often require having the possibility to verify it is the desired server and not an impersonator. Those who offer a service might require some tool to authenticate the identity of incoming connections, that is, to determine those really belong to authorized users.

TLS(Transport Layer Security) allows mutually authenticating two parties at a conversation, and keeping its integrity. Authentication is done by means of X.509 certificates, and after that, keys are derived that allow transferred information to be encrypted. This means that whatever is sent can’t be observed with a network sniffer, and the only way to impersonate one of those entities is by having its certificate and the private key from which it’s been derived. TLS is a complex authentication scheme, requiring some certifying authority who generates keys and certificates used in this process. Due to it being a follow up to SSL, it is common to find references to this name.

The simplest form of operation, and maybe the most used, is server authentication; this happens for example when our web browser requests a page in the Internet using HTTPS (HTTP Secure). Before proceeding with the HTTP session, a handshake takes place that allows for validating the authenticity and establishing a secure connection, in the sense that the other end is identified and information integrity is protected. Web browsers know several certification authorities (have their certificates) and can validate a site by checking the signature in their credentials.

The other form of operation takes place in many IoT networks, when a device connects to “the cloud”. This time not only the server certificate is validated, the device certificate is validated too, that is there is a mutual authentication.

The details of this validation process depend on the method both parties agree to use, though roughly it consists in sending some information encrypted with the receiver’s public key, the receiver can only be able to decrypt and answer if he has the corresponding private key.

Even though authentication is a slow process involving Public Key Cryptography operations, the data transfer phase uses symmetric key encryption based on keys derived from the former process.

As we suggested at the beginning of this article, the security in this scheme resides on the impossibility of impersonating the Certification Authority, whose private key must be safely stored and protected from prying eyes.

MQTT sobre TLS-PSK con ESP32 y Mongoose-OS

Este artículo es una extensión de este otro; te recomiendo que lo veas primero.

Para mayor información sobre el cliente MQTT y la operación sin TLS, existe este artículo. Para más información sobre TLS, éste.

TLS-PSK

Existe una solución un poco más simple a la que vimos en el primer artículo; consiste en utilizar, en vez de certificados, una clave pre-compartida entre el broker y el dispositivo que se conecta.

El proceso de conexión es similar al empleado con certificados, sólo que en vez de validarse éstos y luego generarse una clave derivada, se utiliza como punto de partida la clave pre-compartida. Según el esquema utilizado, se evitan las costosas operaciones de clave pública/privada (Public Key Cryptography), y la logística de manejo de claves suele ser más simple, además del hecho de no requerir una CA (Certification Authority).

Si bien existen al momento tres formas de operar, una de las cuales permite validar al broker mediante certificados y al dispositivo mediante clave pre-compartida (PSK), sólo hemos probado la forma simple.

Configuración

Configuramos el ESP32 con Mongoose-OS para operar mediante TLS-PSK.

libs:
  - origin: https://github.com/mongoose-os-libs/mqtt  # incorpora el cliente MQTT
config_schema:
  - ["mqtt.enable", true]             # Habilita el cliente MQTT
  - ["mqtt.server", "address:port"]   # Dirección IP del broker a utilizar
  - ["mqtt.ssl_psk_identity", "bob"]  # identidad para la clave elegida
  - ["mqtt.ssl_psk_key", "000000000000000000000000deadbeef"] # clave AES-128 (ó 256)
  - ["mqtt.ssl_cipher_suites", "TLS-PSK-WITH-AES-128-CCM-8:TLS-PSK-WITH-AES-128-GCM-SHA256:TLS-ECDHE-PSK-WITH-AES-128-CBC-SHA256:TLS-PSK-WITH-AES-128-CBC-SHA"] # claves utilizables

El puerto comúnmente utilizado es 8883. El broker puede además solicitar que utilicemos nombre de usuario y password, aunque lo más común es configurarlo para aprovechar la identidad que se envía, como hicimos para estas pruebas (ejemplo más abajo).

Respecto a las cipher suites, se trata de indicar un conjunto de esquemas criptográficos que el sistema soporta y entre ellos debemos acertar a elegir uno que esté soportado por el broker. Este parámetro es obligatorio, sin él el dispositivo no anuncia ningún esquema criptográfico compatible con TLS-PSK y la conexión TLS no se establece. Lo habitual es acordarlo con el administrador del broker.

Al iniciar la conexión TLS, el dispositivo incluye los esquemas soportados en el mensaje ClientHello; el broker elige uno compatible y lo indica en el mensaje ServerHello. Dado que esta lista la obtuvimos analizando el código fuente, decidimos publicarla aquí para mayor utilidad. En nuestro caso en particular sólo uno de los esquemas coincidió con los disponibles en el broker: TLS-PSK-WITH-AES-128-CCM-8. El handshake de inicialización conteniendo estos mensajes puede observarse en la captura Wireshark que figura más abajo.

Por último, la clave pre-compartida (PSK, parámetro ssl_psk_key) debe tener una longitud de 128-bits (16-bytes) para sets basados en AES-128 y de 256-bits (32-bytes) para sets basados en AES-256.

Operación

Al iniciar el procesador, observaremos en el log si todo funciona como debe, o los errores que se hayan producido. En este último caso, resulta bastante difícil determinar las causas sin un sniffer dado que las pistas de ambos lados no suelen ser muy precisas. Tanto el handshake como la operación MQTT dentro de TLS pueden observarse en un sniffer, ingresando dicha clave en el mismo (por ejemplo Wireshark, ejemplo más abajo).

[Mar 31 17:28:32.349] mgos_mqtt_conn.c:435    MQTT0 connecting to 192.168.5.3:8883
[Mar 31 17:28:32.368] mongoose.c:4906         0x3ffc76ac ciphersuite: TLS-PSK-WITH-AES-128-CBC-SHA
[Mar 31 17:28:32.398] mgos_mqtt_conn.c:188    MQTT0 TCP connect ok (0)
[Mar 31 17:28:32.411] mgos_mqtt_conn.c:235    MQTT0 CONNACK 0
[Mar 31 17:28:32.420] init.js:34              MQTT connected
[Mar 31 17:28:32.434] init.js:26              Published:OK topic:/this/test/esp32_807A98 msg:CONNECTED!

Brokers: Mosquitto

Detallamos, a modo instructivo y como ayuda rápida, la configuración mínima necesaria para operar en Mosquitto. El path está en formato GNU/Linux y se espera que pongamos nuestro archivo con las claves allí.

listener 8883 192.168.5.1
log_dest syslog
use_identity_as_username true # evitamos que requiera usuario en el header MQTT
psk_file /etc/mosquitto/pskfile
psk_hint cualquiera           # cualquier nombre

El parámetro psk_hint, en el caso que el cliente se conecte a varios lugares, le da un indicio (hint) de qué identidad utilizar para identificarse. No tiene utilidad en nuestro caso.

El archivo pskfile, por su parte, debe contener las identidades y claves pre-compartidas; por ejemplo, para el usuario bob:

bob:000000000000000000000000deadbeef

Ejemplo

El código de ejemplo se encuentra en Github.

Sniffers: Wireshark

En todo sniffer, veremos el tráfico TLS pero no lo que éste transporta

Captura simple

En Wireshark, poder decodificar el tráfico TLS-PSK es tan simple como ingresar la clave compartida en Edit->Preferences->Protocols->TLS->Pre-Shared-Key.

Ingresamos la clave pre-compartida

El sniffer es entonces capaz de descifrar el contenido y nos permite observar el tráfico MQTT.

Captura descifrada

MQTT sobre TLS con ESP32 y Mongoose-OS

Mongoose-OS incorpora un cliente MQTT con soporte TLS. Esto nos permite por un lado autenticar al servidor, y por otro que la infraestructura del servidor pueda autenticar la identidad de esas conexiones, es decir, que realmente sean usuarios autorizados.

Para mayor información sobre el cliente MQTT y la operación sin TLS, existe este artículo. Para más información sobre TLS, éste.

Configuración

Configuramos el ESP32 con Mongoose-OS para tener un cliente MQTT y conectarse como cliente a una red WiFi. Dado que debemos conectarnos a un servidor, ésta nos resulta la forma tal vez más rápida y simple de realizar las pruebas y explicar la operación. La configuración puede realizarse manualmente mediante RPC, en un archivo de configuración JSON; o definirla en el archivo YAML que describe el proyecto. Para las pruebas elegimos esta última opción.

libs:
  - origin: https://github.com/mongoose-os-libs/mqtt  # incorpora el cliente MQTT
config_schema:
  - ["mqtt.enable", true]            # Habilita el cliente MQTT
  - ["mqtt.server", "address:port"]  # Dirección IP del broker a utilizar
  - ["mqtt.ssl_ca_cert", "ca.crt"]   # Certificado del broker, requerido
  - ["mqtt.ssl_cert", "sandboxclient.crt"]  # nuestro certificado, para autenticación mutua
  - ["mqtt.ssl_key", "sandboxclient.key"]   # nuestra clave, para autenticación mutua

El puerto comúnmente utilizado es 8883. El broker puede además solicitar que utilicemos nombre de usuario y password, los detalles de la configuración completa los podemos encontrar en la página de Mongoose-OS. Finalmente, quien decide qué tipo de autenticación (simple o mutua) utilizamos es el broker, aunque nosotros debemos proveer el certificado cuando es requerido.

Operación

Antes de compilar y ejecutar, es conveniente tener un cliente conectado al broker para poder observar el mensaje enviado al momento de conexión. Hemos utilizado un broker instalado en nuestro lab, y nos conectamos a él sin autenticación, por lo que lo mostrado en el artículo sobre MQTT vale para éste también

Luego de compilado el código (mos build) y grabado el microcontrolador (mos flash) mediante mos tool, observaremos en el log si todo funciona como debe, o los errores que se hayan producido. Recordemos que debemos configurar las credenciales para conectarnos por WiFi a nuestra red (SSID y clave) y la dirección y port del broker MQTT que vayamos a utilizar. Al final de la nota comentamos cómo utilizar Mosquitto.

Autenticación simple

De esta forma autenticamos al servidor, es decir, podemos confiar en que es el que esperamos que sea, pero el servidor no tiene idea de quién podemos llegar a ser nosotros.

Es la configuración más simple y no debemos tomar ningún recaudo más allá de informar el nombre del archivo que contiene el certificado de la CA almacenado en el directorio fs en el parámetro ssl_ca_cert, esto indica que se deberá validar al broker.

[Mar 31 17:21:18.360] mgos_mqtt_conn.c:435    MQTT0 connecting to 192.168.5.3:8883
[Mar 31 17:21:18.585] mongoose.c:4906         0x3ffca41c ciphersuite: TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256
[Mar 31 17:21:20.404] mgos_mqtt_conn.c:188    MQTT0 TCP connect ok (0)
[Mar 31 17:21:20.414] mgos_mqtt_conn.c:235    MQTT0 CONNACK 0
[Mar 31 17:21:20.423] init.js:34              MQTT connected
[Mar 31 17:21:20.437] init.js:26              Published:OK topic:/this/test/esp32_807A98 msg:CONNECTED!

Autenticación mutua

De esta forma autenticamos tanto al servidor como al cliente, es decir, ahora el servidor sabe que deberíamos ser quien nuestro certificado dice que somos. Este doble proceso requiere bastante procesamiento y lo notaremos en el tiempo de establecimiento de la conexión. En esta configuración deberemos proveer nuestro certificado y clave en el directorio fs, además de configurar los parámetros ssl_cert y ssl_key; caso contrario el broker puede que nos dé una pista en el log, como lo hace Mosquitto:

Mar 31 17:30:52 Server mosquitto[2694]: OpenSSL Error: error:140890C7:SSL routines:ssl3_get_client_certificate:peer did not return a certificate 

Brokers: Mosquitto

Detallamos, a modo instructivo y como ayuda rápida, las configuraciones mínimas necesarias para operar en Mosquitto. Los paths están en formato GNU/Linux y se espera que pongamos nuestros certificados allí.

Autenticación simple (del broker)

listener 8883 192.168.5.1
log_dest syslog
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key

Autenticación mutua

listener 8883 192.168.5.1
log_dest syslog
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
require_certificate true

Ejemplo

El código de ejemplo se encuentra en Github, aquí y aquí. Vamos a necesitar además las credenciales del servidor, dejé unas aquí para simplificar la operatoria.

TLS, seguridad en la capa de transporte

Quienes desean conectarse a un servidor suelen requerir la posibilidad de verificar que se trata del servidor deseado y no de impostor. Quienes dan un servicio pueden requerir una herramienta para autenticar la identidad de las conexiones, es decir, que realmente sean usuarios autorizados.

TLS(Transport Layer Security) permite autenticar mutuamente dos entidades en una conversación y mantener la integridad de la misma. La autenticación se realiza por certificados X.509, y luego de realizada, se derivan claves que permiten encriptar la información que viaja. Esto significa que lo que se envía no puede ser observado con un sniffer y la única forma de impersonar a una entidad es mediante la posesión del certificado y la clave privada de la que se deriva. Se trata de un esquema de autenticación complejo, con un ente certificador, quien genera las claves y certificados que se utilizan en el proceso. Dado que TLS se deriva de SSL, el esquema original (Secure Sockets Layer), es común que la nomenclatura refiera a este nombre.

La operación más simple y tal vez la más utilizada es la autenticación del servidor, operación que sucede cuando por ejemplo nuestro navegador solicita en Internet una página mediante HTTPS (HTTP Secure). Antes de proceder a la sesión HTTP, se realiza el handshake que permite la validación de autenticidad y el establecimiento de la conexión segura (“segura” en el sentido de que se identifica a la contraparte y se cuida la integridad de la información que viaja). El navegador conoce una serie de entes certificadores (tiene sus certificados) y puede validar mediante la firma al sitio solicitado cuando éste “le presenta sus credenciales”.

La otra operación posible es realizada en muchas redes IoT, cuando un dispositivo se conecta “a la nube”. En esta oportunidad no sólo se valida el certificado del servidor sino que éste a su vez valida el certificado del dispositivo, es decir, se establece una doble validación.

El proceso de validación en detalle depende del método acordado entre las contrapartes, aunque a grandes rasgos consiste en enviar una cierta información basada en la encripción con la clave pública del receptor, quien sólo puede descifrarla y así contestar si posee la clave privada correspondiente.

Si bien la autenticación es un proceso lento que involucra clave pública/privada (Public Key Cryptography), la comunicación transcurre encriptada por una clave simétrica derivada.

Como insinuamos al inicio, toda la seguridad del esquema radica en la imposibilidad de impersonar al ente certificador (Certification Authority), cuya clave privada es celosamente guardada y protegida.

Timer expired, o eso creo…

A menudo disponemos de varias formas de realizar una misma tarea. A veces la elección es por gusto o simpatía y otras el contexto suele dictar algunas normas que limitan las elecciones seguras.

En el caso de los timers por software, mi elección personal suele ser emplear un viejo esquema de timers individuales que luego de visitar assembler de varios micros ha terminado en C. Otras veces, dado que dispongo de 32-bits, por algún motivo u otro me inclino a simplemente chequear el valor de un contador. En ambos casos, tanto los timers individuales como el contador suelen ser decrementados e incrementado (respectivamente) por un interrupt handler.

Esto requiere que, por un lado, (en C) declaremos la variable de interés como volatile para que el compilador sepa que aún dentro de una función esta variable puede cambiar su valor. Caso contrario, loops de espera serán eliminados por el optimizador.

extern volatile uint32_t tickcounter;

Por otra parte, y por la misma razón, es necesario que el acceso a dicha variable sea atómico, es decir, ocurra en una instrucción del microprocesador o microcontrolador (o una secuencia no interrumpible de éstas). Caso contrario, podría aceptarse una interrupción en medio de la cadena de instrucciones que lee los bytes individuales que componen dicha variable y si dicha interrupción altera su contenido, obtendríamos un valor equivocado. Esto ocurre por ejemplo si utilizamos timers/contadores de 32-bits en la mayoría de los micros de 8-bits; ahondo en este tema en otro post alegórico.

Una vez resueltos estos temas, nos queda el problema de la finitud. No sólo de nuestra existencia, me refiero a la cantidad de bits que dan la longitud de los enteros que forman los timers.

El esquema de timers que les comenté es muy simple: cada timer es decrementado si su valor es distinto de cero, por lo que su uso se remite a escribir un valor y revisar frecuentemente si llegó a cero. Existirá obviamente un jitter dado por el tiempo que transcurre entre la escritura y el primer decremento en el interrupt handler, y luego desde que el timer llega a cero hasta que la función interesada lo observa.

El observar el valor de un contador, por el contrario, presenta algunos inconvenientes cuya solución motiva este post. Por problemas de atomicidad podemos vernos forzados a emplear una variable más chica y nos vamos a encontrar con que el contador desborda en un tiempo corto.

¿Qué ocurre cuando el contador desborda? Bueno, puede que nada si tomamos las precauciones necesarias.

Todo funciona perfectamente y pasa todos los tests si el contador no desborda entre chequeos, es decir, si el contador es lo suficientemente “largo” como para soportar todo el tiempo de vida de la aplicación. Por ejemplo, un contador de segundos de 32-bits tarda unos 136 años en desbordar. Sin embargo, un contador de milisegundos de 16-bits desborda una vez cada poco más de un minuto (65536 milisegundos).

Si el tiempo entre chequeos de dicho contador es mayor, existe la posibilidad de que el contador desborde más de una vez entre que lo chequeamos una y otra vez. En un caso como éste, deberemos valernos de otro recurso dado que, así solo, esto no nos permite controlar el tiempo.

Si en cambio podemos asegurar que el contador a lo sumo sólo desbordará una vez entre chequeos, deberemos escribir correctamente el código para que nuestra comparación sobreviva al desborde del contador. Caso contrario, como dijimos, todo funcionará bien pero “a veces” (cada vez que el tiempo deseado involucre un desborde intermedio), ocurrirán “cosas raras” con la operación del timer.

A los fines prácticos consideremos el siguiente esquema de variables, donde usamos mayúsculas violando tal vez algunos coding styles pero lo hacemos para resaltar el contador:

extern volatile uint32_t MS_TIMER;
uint32_t interval = 5000; // milisegundos
uint32_t last_time;   

Las opciones mostradas en la siguiente porción de código funcionan correctamente, dado que la diferencia sobrevive al overflow del contador y es comparada correctamente con el valor deseado:

	last_time = MS_TIMER;

	if(MS_TIMER - last_time > interval)
	    printf("Timer expired\n");

	if(MS_TIMER - interval > last_time)
	    printf("Timer expired\n");

Por el contrario, esto no funciona correctamente

uint32_t deadline;

	/* no debo hacer esto
	deadline = MS_TIMER + interval;
	if(MS_TIMER > deadline)
	    printf("Timer expired\n");
	*/

Si, por ejemplo, MS_TIMER tiene el valor 0xF8000000 e interval es 0x10000000, entonces deadline será igual a 0x08000000 y MS_TIMER es claramente mayor a deadline desde el inicio, lo cual ocasiona que el timer expire inmediatamente (no es lo que queremos…).

Una solución de compromiso para utilizar un esquema como el anterior requiere emplear enteros con signo y funciona para intervalos de hasta MAXINT32 unidades:

int32_t deadline;
int32_t interval = 5000; // milisegundos

	deadline = MS_TIMER + interval;
	if((int32_t)(MS_TIMER - deadline) > 0)
	    printf("Timer expired\n");

Otras formas más creativas pueden resultar en timers que nunca expiran cuando el contador supera MAXINT32, dado que se ve como un número negativo y es por ende menor que un tiempo solicitado (que es positivo); pero que sin embargo funcionan cuando se inicia (o reinicia) el equipo. En general las podemos agrupar en errores al elegir el largo de la variable (incluyendo al signo) y los vamos a dejar de lado.

Como pudimos observar, algunos problemas se manifiestan al referirse a tiempos MAXINT32 unidades temporales luego de iniciado el sistema. Por lo general este valor es 2^31, lo cual, para un contador de milisegundos son unos 25 días y para uno de segundos unos 68 años. Algunos errores en aplicaciones Linux y similares se manifestarán cuando reporten a tiempos iniciando a partir del año 2038 (el contador de segundos cuenta a partir del inicio del año 1970).

Si por el contrario utilizamos esquemas de 16 ú 8-bits…

En los ejemplos vistos, el uso de la comparación por ‘mayor’ nos garantiza que el intervalo va a ser mayor al solicitado. Si por ejemplo lo seteamos en este momento pidiendo un intervalo de un segundo y un milisegundo después el contador de segundos es incrementado, si utilizáramos comparación por ‘mayor o igual’ nuestro timer expiraría inmediatamente (bueno, en 1ms…). De modo similar, si lo solicitamos 1ms luego de la interrupción demorará 1,999s en expirar en vez de 1s. En un esquema donde la comparación y el incremento no son asíncronos, podemos evitar el jitter y emplear comparación por ‘igual o mayor’.

Sobre átomos y relojes

La particularidad de las interrupciones es que se caracterizan (como su nombre lo indica) por interrumpir al procesador en cualquier momento, de forma indistinta, aunque a veces pareciera que lo hacen de forma intencional sobre la tarea que más nos complica. La razón fundamental de su existencia es permitir que el micro pueda atender eventos que no es conveniente estar esperando o consultando, y/o que deben ser atendidos rápidamente y sin demoras.

Cuidadosamente utilizadas, son un poderoso aliado, y hasta es posible armar esquemas de tareas múltiples asignando a cada tarea una interrupción periódica independiente.

Su uso indiscriminado puede generar más inconvenientes que beneficios; particularmente no es posible compartir variables o subrutinas sin tomar las debidas precauciones de accesibilidad, atomicidad y reentrabilidad.

Una interrupción puede ocurrir en cualquier momento, y si ocurre en medio de la actualización de una variable multi-byte puede causar (y de hecho lo hace) estragos a las demás tareas, que quedan con valores “parcialmente alterados”, que en el mundo real son valores incorrectos, y probablemente de peligrosa incoherencia para el software. Un ejemplo típico y frecuentemente olvidado, son las variables de temporización, o fecha y hora. El programa principal se pone alegremente a descomponerlas byte a byte, de forma de mostrar su contenido en el display, sin pensar que, si son actualizadas por interrupciones, éstas no tienen por qué ocurrir antes o después de nuestro acceso y no durante el mismo. Si se muestran los valores en un display no hay mayor problema, al cabo de un segundo volveremos a la normalidad, pero si esto va a un log o informe vamos a tener que dar muchas explicaciones, como en el ejemplo siguiente, en el que aparece un registro con casi un año de diferencia

Utilicé alguna vez un entorno orientado a microcontroladores que “para subsanar este inconveniente”, incorporaba el concepto de ‘variables compartidas’ (shared variables). Ante un acceso o modificación de una variable así definida, se produce una inhibición de las interrupciones durante el transcurso de la misma. El problema que encuentro en este tipo de enfoques es que esto es malo para la didáctica como los pull-ups y diodos de protección en los GPIOs (como ya he comentado en otro artículo), el usuario no entiende qué pasa, no sabe, no aprende. Además, accesos reiterados a este tipo de variables producen inhibiciones reiteradas en las interrupciones, lo que se traduce como variaciones en la latencia de atención de éstas.

Si bien en una variable multi-byte como la del ejemplo esto resulta más que evidente, lo mismo ocurre en una variable de 32-bits o de 16-bits en micros de 8-bits que no disponen de instrucciones para acceder a variables de 32-bits (la gran mayoría) o 16-bits. La atomicidad, es decir, la propiedad de acceder a la variable como una unidad indivisible, se pierde, y cuando los accesos a estas variables son compartidos por tareas asíncronas existe la posibilidad de que se superpongan y entonces ocurra lo que hemos visto.

Si el entorno para desarrollar en C que utilizamos incorpora signal.h, deberíamos encontrar allí la definición del tipo sig_atomic_t, que identifica un tamaño de variable que puede ser accedido de forma atómica. Caso contrario, y como recomendación en estos entornos, el usuario debe conocer el micro con el que opera y obrar en consecuencia.

En entornos multiprocesados aparecen además otros inconvenientes, los cuales no abordaremos aquí.

Corolario

Una interrupción puede ocurrir en cualquier momento, siempre que esté habilitada. La probabilidad de que suceda justo en el instante en que se está produciendo la lectura de una variable cuyo acceso no es atómico es bastante baja, particularmente si ésta es breve y ocurre de forma no sincronizada con las interrupciones, y de manera no frecuente. Sin embargo, es mayor que cero, y por ende, en un número lo suficientemente alto de repeticiones, es de esperarse que ocurra. El dejar librado el correcto funcionamiento de un equipo a la función de distribución de Poisson no debería ser considerada buena práctica de diseño…

Este post contiene algunos extractos del libro “El Camino del Conejo“, con permiso del autor.

¿ Alguien ordenó un latch-up ?

(meditaciones sobre lo académicamente correcto en artículos y notas de aplicación)

Días atrás conversaba con un viejo amigo y maestro sobre esas cosas del diseño electrónico, y surgió el tema del latch-up. Mientras revisaba en mi cabeza un artículo que todavía no había escrito, recordé un ejemplo similar en otro ámbito que me trajo a colación este tema, y se materializa en este post.
Pensaba por ese entonces en lo malo que pueden resultar algunas conveniencias y facilidades a la hora de la didáctica… Por allá por el año 2004 estaba yo asistiendo a una conferencia para desarrolladores de una famosa empresa de microcontroladores “fáciles” (que son en realidad lo más difícil que se pueda inventar para alguien que ya conoce otros microcontroladores). El presentador estaba desarrollando sobre una nota de aplicación en la cual se veía un relé conectado directamente a un puerto de entrada/salida (GPIO) de un microcontrolador, a lo cual respetuosamente interrogué si, al menos, no convenía poner allí un diodo de flywheel (o “de protección”), a lo cual me respondió “todos nuestros microcontroladores tienen diodos de protección en los pines de entrada/salida”.

En primer lugar, soy de los que no utilizan los pull-ups incorporados en los micros excepto cuando alguna razón lo justifica. ¿Por qué? Porque intento que el diseño de hardware no dependa de detalles del fabricante (el día de mañana puedo portar el diseño a un microcontrolador “difícil” que no tenga estas “facilidades”) ni del firmware, y ahorrar esas décimas de centavo (despreciando el espacio en PCB) no me justifica el estar pendiente de recordar habilitar el pull-up o encontrarme con efectos no deseados hasta que la ejecución del código llega a ese punto. Eso sin contar que trabajando en muy bajo consumo esos pull-ups internos son impredecibles y hasta suelen ser contraproducentes. En segundo lugar, para quien no dispone del log de diseño (si existe), se pierde la razón “académica” por la cual ese resistor debe existir allí, todo lo que eso dice sobre el resto del circuito, lo cual me parece muy importante en una nota de aplicación, que es un documento de ingeniería.

Volviendo al relé controlado por un GPIO, digamos que esto propicia que un aficionado probablemente aprenda técnicas conflictivas que tal vez replique en otros ámbitos cuando progrese (me siento tentado a recordar el efecto de enseñar a programar a los jóvenes en los 80’s usando BASIC solamente).

Primer aspecto: la corriente que circula por el relé. Los GPIOs suelen presentar una estructura MOS del tipo push-pull, es decir, un MOSFET de canal P provee la corriente desde el positivo mientras que un MOSFET de canal N recibe la corriente que retorna hacia masa. Dichos MOSFETs suelen ser de áreas diferentes, siendo el de canal N de mayor capacidad de corriente, y en muchos casos tal vez insuficiente para operar un relé. Según cómo esté conectado el relé, puede operar correctamente porque el GPIO no eleva demasiado la tensión de salida pues el MOSFET de canal N es capaz de manejar esa corriente; u operar erráticamente porque el GPIO disminuye demasiado la tensión de salida pues el MOSFET de canal P es incapaz de manejar esa corriente. La segunda parte de esto es también, según cómo esté conectado el relé, que la corriente de éste circulará por el pin de GND o por el pin de VDD, es decir, entrará por el pin positivo desde la fuente o volverá a ésta por el pin de masa, creando, entre otras cosas, una circulación por caminos no deseados que puede afectar el funcionamiento en los transitorios de encendido y apagado del relé, por un lado por inductancias parásitas en las pistas, y por otro dado que cuando las corrientes van por un camino y vuelven por otro, se forma un lazo de corriente, una antena de cuadro, que irradia en todas direcciones con intensidad proporcional al área del lazo. La corriente por el pin de masa puede afectar además las mediciones de un conversor analógico-digital incluido en el chip (y la corriente por el pin de alimentación también si el microcontrolador economiza el pin de alimentación de la sección analógica, VDDA).

Segundo aspecto: la corriente que debería dejar de circular por el relé cuando éste se abre; pero dado que es un inductor, Faraday y Lenz conspiran contra nosotros: V = -L di/dt.

Cuando se desea abrir el relé, se debe poner el GPIO en estado de alta impedancia, idealmente circuito abierto, o al menos el estado opuesto al usado para activarlo. Supongamos que lo activamos conectándolo a masa, ahora debemos idealmente configurar un open-drain o al menos conectarlo a positivo. Al ser la bobina del relé un inductor, la corriente no puede cesar instantáneamente sino que intentará seguir circulando por ésta; dependiendo de qué tan difícil le resulte al inductor “mantener la corriente constante” (di/dt), aparecerá sobre sus bornes una tensión de la polaridad necesaria para hacer circular esa corriente que además es proporcional a la inductancia. Dicha polaridad hace que la tensión en el pin sea superior en este caso a la de alimentación del microcontrolador, tanto como sea necesario para que circule esa corriente. ¿Por dónde circulará esa corriente? Idealmente por un diodo que se denomina “de fly-wheel”, traducido en ocasiones como “rueda libre”, que se conecta con la polaridad apropiada en paralelo con la bobina del relé. En su ausencia por donde la alta tensión inducida encuentre el camino; en este caso en particular por el diodo de protección del GPIO y volviendo hasta el relé por la traza de alimentación que los une (en sentido inverso, generando una subida de tensión en la alimentación del micro), si colocamos el pin en alta impedancia. Si en cambio ponemos el pin en estado alto, deberá circular por el MOSFET de canal P en sentido inverso, probablemente por el diodo intrínseco que suele formarse en la estructura cristalina, y que suele oficiar de diodo de protección, o por un diodo de protección a tal efecto, lo que presente menos impedancia; suguiendo de allí igual camino al anterior.

Asumamos que los diodos de protección están diseñados para soportar la corriente que circula por la bobina, y que el resto del circuito tolera dichos efectos sin mayores complicaciones. ¿Qué sucede si replicamos este diseño en un microcontrolador que no tiene diodos de protección? (Dichos diodos muchas veces no son deseables, pero este artículo ya se haría muy largo…)

Bien, como dijimos, “por donde la alta tensión inducida encuentre el camino”. Algunas veces el camino se encuentra a través de algún periférico ocasionando comportamientos erráticos, otras ocurre algo mucho más riesgoso.

En las estructuras de semiconductor de los chips, se forman estructuras parásitas PNPN, lo cual es un tiristor no deseado. Este tiristor se puede disparar por corriente de disparo si algo logra inyectar una corriente suficiente por el bloque que de acuerdo a la polaridad oficie como “compuerta”, o por sobretensión, si aparece una tensión suficiente entre extremos de la estructura que supera su tensión de ruptura. El tiristor parásito comienza entonces a conducir y no deja de hacerlo hasta que se retira la alimentación, siendo lo más probable que se funda la estructura cristalina al no poder disipar el exceso de calor, formando un cortocircuito. Latch-up.

Por esta razón, suele ser mucho más didáctico controlar el relé con un transistor y colocar el correspondiente diodo de fly-wheel, llevando la corriente del relé lejos del conversor A/D. Caso contrario, un comentario indicando los efectos que podríamos tener y los componentes que eliminamos gracias a las bondades de esta facilidad, tal vez logren generar la curiosidad necesaria en el lector para investigar.

Si bien las nuevas tecnologías de fabricación minimizan la posibilidad de latch-up, siempre es conveniente colocar resistores en serie con los pines que puedan estar expuestos a tensiones más elevadas que la de alimentación, a fin de limitar la posible corriente de disparo y a la vez facilitar la extinción de la corriente por el tiristor o al menos limitarla para evitar la destrucción de la estructura cristalina.

Conectándonos a HiveMQ Cloud Basic con Mongoose-OS

HiveMQ Cloud es un servicio de broker MQTT basado en HiveMQ, un excelente broker MQTT desarrollado por la empresa del mismo nombre. Es posible utilizar HiveMQ ejecutándolo en nuestros servidores, o podemos aprovechar la infraestructura de HiveMQ Cloud y pagar un plan de acuerdo a nuestras necesidades.

HiveMQ Cloud Basic es gratuito para hasta 100 conexiones concurrentes (al momento de escribir este artículo), lo cual es muy útil para nosotros los desarrolladores, a fin de poder realizar rápidas pruebas de concepto en un ambiente estable. Incluso también es posible comenzar con la opción gratuita y luego migrar al servicio pago una vez que el negocio comienza a ganar momento.

El broker cumple con el standard MQTT 5, y contiene todo lo que se espera de él, incluyendo QoS2, mensajes “de última voluntad” y retención de mensajes. Las conexiones utilizan TLS en modalidad de autenticación del servidor, no se utilizan certificados para los clientes, las credenciales válidas son un nombre de usuario y un password creados en la consola web.

Para más información, podemos leer la Quick Start Guide.

Configuración

En mos.yml, agregamos el nombre del broker y el puerto, el certificado de la CA para TLS, y la pareja usuario/password. El nombre del broker lo obtenemos al ver los detalles de nuestro cluster en la consola de HiveMQ. La pareja de credenciales usuario/password la configuramos en dicha consola, en la sección de control de acceso (Access Management).

config_schema:
  - ["mqtt.enable", true]
  - ["mqtt.server", "blablabla.hivemq.cloud:8883"]
  - ["mqtt.user", "username"]                        
  - ["mqtt.pass", "password"]                       
  - ["mqtt.ssl_ca_cert", "trustid-x3-root.pem"]      

Para mayor información sobre MQTT sobre TLS te recomiendo este otro artículo.

Ejemplo

En Github disponemos de un ejemplo funcional. Es un simple script mJS que publica un pequeño mensaje cuando se establece la conexión con el broker.

El certificado de la CA lo hemos incluido entre los archivos de dicho ejemplo, en caso que tengamos algún problema con él, la documentación de HiveMQ Cloud nos explica cómo obtenerlo, pero al parecer debemos estar registrados para acceder porque es parte de la consola. Mongoose-OS requiere que se configure el certificado para saber que debe iniciar TLS y validar a su vez el certificado del broker.

Si todo sale bien, luego de compilar ejecutando mos build y grabar ejecutando mos flash veremos esto ejecutando mos console:

[May 21 14:57:38.806] mgos_mqtt_conn.c:468    MQTT0 connecting to numerolargo enhexa.s1.eu.hivemq.cloud:8883
[May 21 14:57:38.841] mgos_mongoose.c:66      New heap free LWM: 210876
[May 21 14:57:39.350] mg_ssl_if_mbedtls.c:30  0x3ffc8818 ciphersuite: TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256
[May 21 14:57:40.741] SW ECDH curve 3
[May 21 14:57:41.170] mgos_mongoose.c:66      New heap free LWM: 191588
[May 21 14:57:41.467] mgos_mqtt_conn.c:227    MQTT0 TCP connect ok (0)
[May 21 14:57:41.733] mgos_mqtt_conn.c:271    MQTT0 CONNACK 0
[May 21 14:57:41.742] init.js:34              MQTT connected
[May 21 14:57:41.756] init.js:26              Published:OK topic:/this/test/esp32_807A98 msg:CONNECTED!

Si disponemos de un cliente como mosquitto_sub client, o algún otro de nuestro agrado, podemos conectarnos al broker para ver que todo funcione bien. En nuestro caso, hemos utilizado CentOS 7 y el cliente mosquitto requiere que además se le especifique dónde hallar el certificado de la CA:

$ mosquitto_sub -h somelonghexnumber.s1.eu.hivemq.cloud -p 8883 -t "#" -u yourusername -P yourpassword --cafile pathto/isrgrootx1.pem -v
	/this/test/esp32_807A98 CONNECTED!

MQTT with the ESP32 and Mongoose-OS

Mongoose-OS has a built-in MQTT client and a simple mJS (a reduced JavaScript) interpreter. These tools allow us to quickly and easily connect to an MQTT broker and perform our proof-of-concept tests for more complex applications.

Configuration

We’ll configure the ESP32 running Mongoose-OS to use the MQTT client and connect to a WiFi network. Since we are going to connect to a server (the MQTT broker), this is perhaps the simplest and fastest way to run our tests. This action can be done manually using RPCs (Remote Procedural Calls), writing a JSON config file, or it can be defined in the YAML file that describes how the project will be built. We’ve chosen this last option for our tests.

libs:
  - origin: https://github.com/mongoose-os-libs/mqtt  # Include the MQTT client
config_schema:
  - ["mqtt.enable", true]            # Enable the MQTT client
  - ["mqtt.server", "address:port"]  # Broker IP address (and port)

The most common port for MQTT is 1883. The broker can also ask us to send a username and password, full configuration details are available at the corresponding Mongoose-OS doc page.

Operation

Before we build our project and execute our code, it is perhaps convenient to have an MQTT client connected to the broker, so we can see the message the device will send at connect time. We’ve used a broker we installed in a server in our lab:

$ mosquitto_sub -h mqtt.lab -p 1883 -t "#" -v

In addition (if daily traffic allows), it is also possible to use Eclipse’s open MQTT broker, in that case we should be a bit more specific in the topic we subscribe to, in order to try to receive only our messages

$ mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/this/#" -v

After the code is compiled and linked (mos build) and the microcontroller’s flash is programmed (mos flash) using mos tool, we’ll watch the log and check if everything is going on as it should, or catch any possible errors. Remember we need to configure the proper access credentials to connect to our WiFi network of choice (SSID and password), and the address and port for the MQTT broker we are going to use.

[Feb 11 15:19:42.353] mgos_mqtt_conn.c:435    MQTT0 connecting to mqtt.sensors.lab:1883
[Feb 11 15:19:42.370] mgos_mqtt_conn.c:188    MQTT0 TCP connect ok (0)
[Feb 11 15:19:42.380] mgos_mqtt_conn.c:235    MQTT0 CONNACK 0
[Feb 11 15:19:42.385] mgos_mqtt_conn.c:168    MQTT0 sub /this/sub @ 1
[Feb 11 15:19:42.393] init.js:38              MQTT connected
[Feb 11 15:19:42.407] init.js:26              Published:OK topic:/this/pub/esp32_807A98 msg:CONNECTED!

At this time we’ll see that message in our MQTT client window:

$ mosquitto_sub -h mqtt.lab -p 1883 -t "#" -v
/this/pub/esp32_807A98 CONNECTED!

Then, we can send a message to our device by publishing to the topic to which it is subscribed:

$ mosquitto_pub -t /this/sub -h mqtt.lab -m "This message"

At this time we’ll see that message in our MQTT client window:

$ mosquitto_sub -h mqtt.lab -p 1883 -t "#" -v
/this/pub/esp32_807A98 CONNECTED!
/this/sub This message

and in our device’s log

[Feb 11 15:20:02.647] init.js:32              Got a msg: This message

Finally, we’ll power off our ESP32 and after a while (minutes) the broker will detect the disconnection; at that time we’ll see the last-will message in our MQTT client window

$ mosquitto_sub -h mqtt.lab -p 1883 -t "#" -v
/this/pub/esp32_807A98 CONNECTED!
/this/sub This message
/this/test lost

The mJS code

Whenever we want to publish a message, we’ll call MQTT.pub(). There’s no need to wait nor perform any handshakes, just invoke that method and see the results. Internally, the message is queued and will be sent as soon as possible; the value returned by the publishing method is an indicator of the result of the queuing action, not the actual transmission of the message.

This article has companion code, there we’ve written a simple function as a usage example:

let publish = function (topic, msg) {
    let ok = MQTT.pub(topic, msg, 1, false); // QoS = 1, do not retain
    Log.print(Log.INFO, 'Published:' + (ok ? 'OK' : 'FAIL') + ' topic:' + topic + ' msg:' +  msg);
    return ok;
};

let device_id = Cfg.get('device.id');
publish('/this/pub/'+device_id,'CONNECTED!');

Should we want our message to be retained, the fourth parameter for the MQTT.pub() method call must be true.

To receive messages, we subscribe to the desired topic invoking the MQTT.sub() method. Its second parameter is a handler for a callback function that will be run when we get a message. The following example can also be found in the companion code:

MQTT.sub('/this/sub', function(conn, topic, msg) {
    Log.print(Log.INFO, 'Got a msg: ' + msg);
}, null);

Last-will messages

If we want to configure a “last-will message”, that is, a message that will be published by the broker in our behalf when it detects we are no longer connected, we can do it this way:

  - ["mqtt.will_topic", "/this/test"]
  - ["mqtt.will_message", "lost"]

For some applications it is required that this message is also retained by the broker, so if other clients connect after we disconnected and before we reconnect, they can receive that message, which will in turn inform them that we are not available at that time.

  - ["mqtt.will_retain", true]

Retain, persistence

Message retention (be them last-will or common messages with the retain flag set) requires an MQTT broker with persistence capability, that is, with a database in which to store the latest messages that have been marked as persistent in every topic and send them to whoever client subscribes to that topic when it does so.

In our case, we’ve used Eclipse Mosquitto as our broker, but there are other available options.

When using Mosquitto, to configure persistence we need to add

persistence true

to the config file (if we search for it, we’ll find it). It is also possible that we have to add the name and path to the persistence database (paths in GNU/Linux environment form):

persistence_file mosquitto.db
persistence_location /var/lib/mosquitto/

but that will depend on our distro, it may already be configured as default. The user running the Mosquitto process has to have write permission on this database.

Example

The companion example code is available at Github.