La UART es un protocolo serie punto a punto para enviar bytes entre dos dispositivos usando líneas TX y RX.
La UART permite que el microcontrolador intercambie datos con módulos y otros equipos mediante una conexión sencilla.
El protocolo Serie (UART) es el estándar de facto en la industria electrónica desde hace décadas. Aunque tu ordenador ya no tenga aquel conector DB9 gigante, internamente el mundo sigue funcionando con TX y RX.
En esta entrada aprenderemos a usar la librería System.IO.Ports para establecer comunicaciones bidireccionales. Lo usaremos para dos cosas fundamentales:
- Comunicación M2M: Conectar módulos como GPS (NMEA), GSM/4G, o lectores de códigos de barras.
- Comunicación con PC: Crear una consola de comandos personalizada para controlar nuestro dispositivo.
Para este tutorial necesitas instalar el paquete NuGet: nanoFramework.System.IO.Ports
Conceptos básicos: TX y RX
La idea más importante de la UART es el cruce de cables. La UART es una comunicación asíncrona (no hay cable de reloj) que usa dos líneas de datos:
- TX (Transmit): Por donde salen los datos.
- RX (Receive): Por donde entran los datos.
Por tanto, si conectas dos dispositivos (A y B), siempre debes cruzar las líneas:
- TX de A va al RX de B.
- RX de A va al TX de B.
- GND: Ambos deben compartir la misma referencia de tierra.
Configuración del puerto serie
En nanoFramework, la clase estrella es SerialPort.
Lo primero que debemos saber es qué puerto COM corresponde a qué pines en nuestra placa.
- ESP32:
COM1: Generalmente reservado para el Log/Debug y flasheo (UART0).COM2: Suele ser UART1 o UART2 (dependiendo de la asignación de pines).COM3: Otro puerto disponible.
En ESP32 podemos reasignar las funciones de los pines mediante nanoFramework.Hardware.Esp32.Configuration. Consulta siempre el mapeo del target que has flasheado, porque el nombre del puerto no determina por sí solo los GPIO físicos.
Inicialización
using System.IO.Ports;
// 1. Instanciamos el puerto
// Parámetros: Nombre del puerto, BaudRate (Velocidad)
SerialPort miSerie = new SerialPort("COM2", 9600);
// 2. Configuración opcional (pero recomendada)
miSerie.Parity = Parity.None;
miSerie.StopBits = StopBits.One;
miSerie.Handshake = Handshake.None;
miSerie.DataBits = 8;
// 3. Abrimos la conexión
miSerie.Open();La velocidad (BaudRate) debe ser idéntica en ambos extremos. Los valores típicos son 9600 (GPS antiguos) o 115200 (Módulos modernos). Si ves caracteres extraños (“garabatos”), es que la velocidad está mal.
Enviar datos (TX)
Enviar información es trivial. Tenemos métodos para enviar texto o bytes crudos.
// Enviar texto simple
miSerie.Write("Hola Mundo");
// Enviar una línea (añade \r\n al final automáticamente)
miSerie.WriteLine("Comando AT");
// Enviar bytes (para protocolos binarios)
byte[] buffer = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
miSerie.Write(buffer, 0, buffer.Length);Recibir datos mediante eventos (RX)
Podríamos usar un bucle infinito y preguntar if (miSerie.BytesToRead > 0), pero eso es volver al Polling que ya acordamos abandonar.
La clase SerialPort nos ofrece el evento DataReceived. Este evento se dispara cuando llegan datos al buffer de entrada.
Ejemplo completo: eco serie
Vamos a crear un programa que escuche todo lo que le enviemos y nos lo devuelva convertido a mayúsculas. (Para probar esto, necesitarás un adaptador USB-Serial conectado a los pines TX/RX de tu ESP32, o puentear el TX con el RX para hacer un “loopback”).
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Diagnostics;
namespace UartEjemplo
{
public class Program
{
private static SerialPort _serial;
public static void Main()
{
// Configura el puerto según tu placa (COM2 suele ser seguro en ESP32 para pines extra)
// Asegúrate de saber qué pines físicos son COM2 en tu dispositivo
_serial = new SerialPort("COM2", 115200);
// Nos suscribimos al evento
_serial.DataReceived += Serial_DataReceived;
try
{
_serial.Open();
Debug.WriteLine("Puerto abierto. Esperando datos...");
_serial.WriteLine("Sistema Iniciado. Escribe algo:");
}
catch (Exception ex)
{
Debug.WriteLine($"Error abriendo puerto: {ex.Message}");
}
Thread.Sleep(Timeout.Infinite);
}
private static void Serial_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
// Opción A: Leer todo como string (si sabemos que es texto)
string textoRecibido = sp.ReadExisting();
Debug.WriteLine($"RX: {textoRecibido}");
// Procesar y Responder (Echo en mayúsculas)
if (textoRecibido.Length > 0)
{
sp.WriteLine($"RECIBIDO: {textoRecibido.ToUpper()}");
}
// Opción B: Leer bytes (si es un protocolo binario)
/*
int bytesToRead = sp.BytesToRead;
byte[] buffer = new byte[bytesToRead];
sp.Read(buffer, 0, bytesToRead);
*/
}
}
}Caso de uso: leer un GPS
Uno de los usos más comunes es leer un módulo GPS (como el NEO-6M). Estos módulos escupen líneas de texto continuamente usando el estándar NMEA.
Una sentencia típica se ve así:
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
Para procesar esto en nanoFramework:
- Escuchamos el evento
DataReceived. - Acumulamos los caracteres en un
StringBuilderhasta encontrar un salto de línea (\n). - Cuando tenemos la línea completa, la analizamos.
private static StringBuilder _buffer = new StringBuilder();
private static void Serial_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
string incoming = sp.ReadExisting();
foreach (char c in incoming)
{
if (c == '\n') // Fin de línea NMEA
{
ProcesarLineaGPS(_buffer.ToString());
_buffer.Clear();
}
else
{
_buffer.Append(c);
}
}
}
private static void ProcesarLineaGPS(string linea)
{
if (linea.StartsWith("$GPGGA"))
{
// Separar por comas
string[] partes = linea.Split(',');
// partes[2] es Latitud, partes[4] es Longitud...
Debug.WriteLine($"Localización GPS detectada: {linea}");
}
}Fragmentación de Datos
Ten en cuenta que el evento DataReceived no garantiza que te llegue la línea completa de golpe. Puede dispararse con la mitad del mensaje. Por eso siempre debemos usar un buffer intermedio y buscar el carácter delimitador (como \n) antes de procesar.