01 · Punto de partida
Qué hace Serial.begin()
En el entorno de Arduino, Serial.begin(9600) parece una orden mágica. En realidad escribe cuatro registros del periférico USART0: fija el divisor que genera la velocidad, activa el transmisor y el receptor y elige el formato de la trama. Serial.print() coloca bytes en un buffer; una interrupción los entrega al registro de datos UDR0 a medida que el hardware termina de enviar el anterior. Lo que hace el core de Arduino es lo que vamos a escribir nosotros, en pocas líneas de C.
En el tema 31 vimos qué es una trama asíncrona y por qué la velocidad de los dos extremos debe coincidir. Acá vemos cómo se implementa en el ATmega328P y qué decisiones de programa conviene tomar: sondeo o interrupciones, qué tamaño de buffer, cómo se procesan los comandos.
Este periférico se llama USART (Universal Synchronous and Asynchronous Receiver Transmitter) porque puede funcionar también en modo síncrono con un reloj. Aquí usamos solo el modo asíncrono, que es la UART clásica.
02 · Los bloques
El USART0 por dentro
Cada sentido tiene su propia cadena. En transmisión, la CPU escribe un byte en UDR0; el hardware lo pasa a un registro de desplazamiento que lo saca bit a bit por el pin TXD (PD1) con su bit de inicio, la paridad y la parada. En recepción, el pin RXD (PD0) entra a otro desplazador que muestrea cada bit 16 veces y reconstruye el byte; cuando está completo lo deposita en UDR0 (el mismo nombre, otro registro físico) y avisa con una bandera.
Hay tres banderas que gobiernan la conversación: UDRE0 («el registro de datos está vacío, podés escribir otro byte»), TXC0 («terminó de salir el último bit de la trama») y RXC0 («hay un byte recibido sin leer»). Cada una puede generar una interrupción (USART_UDRE_vect, USART_TX_vect y USART_RX_vect).
El doble almacenamiento en transmisión (UDR0 más desplazador) permite que la CPU prepare el byte siguiente mientras el actual todavía sale: la línea no queda con huecos. En recepción ocurre algo similar: el hardware retiene unos pocos bytes (un pequeño FIFO) además del que está en el desplazador, pero si el programa no los lee a tiempo se pierden y se activa la bandera de desbordamiento.
En el Arduino Uno, los pines PD0 y PD1 (los pines digitales 0 y 1) están conectados también a un segundo microcontrolador que traduce a USB. Por eso la UART del chip sirve tanto para hablar con la computadora como para recibir un módulo externo, pero no ambas cosas a la vez.
03 · La velocidad
Calcular los baudios
El generador de baudios divide el reloj del sistema con un contador de 12 bits, el registro UBRR0. En modo normal el receptor muestrea cada bit 16 veces, así que:
Con el bit U2X0 en 1 (doble velocidad) el receptor muestrea 8 veces por bit y el divisor es 8 en lugar de 16: se alcanzan velocidades más altas a cambio de menos muestras por bit (más sensible al ruido). Como UBRR0 es un entero, la velocidad real casi nunca coincide con la pedida y hay un error. Dos UART con errores de signos opuestos se alejan entre sí: la suma de ambos debe quedar en unos ±2 % (tema 31).
| fosc | Baudios | Normal | Con U2X0 |
|---|---|---|---|
| 16 MHz | 9600 | UBRR0 = 103 (+0,16 %) | 207 (+0,16 %) |
| 16 MHz | 57 600 | 16 (+2,12 %) | 34 (−0,79 %) |
| 16 MHz | 115 200 | 8 (−3,55 %) | 16 (+2,12 %) |
| 16 MHz | 250 000 | 3 (0 %) | 7 (0 %) |
| 8 MHz | 115 200 | 3 (+8,51 %, no sirve) | 8 (−3,55 %) |
| 20 MHz | 115 200 | 10 (−1,36 %) | 21 (−1,36 %) |
| 18,432 MHz | 115 200 | 9 (0 %) | 19 (0 %) |
Los números enseñan una lección de diseño: el 16 MHz es un reloj cómodo para el procesador pero incómodo para la UART, porque 16 000 000 no es múltiplo de 115 200 × 16. Un cristal de 18,432 MHz (o de 7,3728 MHz, 14,7456 MHz) es múltiplo exacto de las velocidades estándar y da error cero. El máximo posible es fosc/16 (1 Mbaudio con 16 MHz) o fosc/8 con U2X0 (2 Mbaudios).
En C, el archivo <util/setbaud.h> calcula el divisor en tiempo de compilación a partir de F_CPU y BAUD y decide automáticamente si conviene U2X0. La calculadora del laboratorio hace lo mismo y muestra el error.
04 · Control
Los registros
| Bit | Nombre | Significado |
|---|---|---|
| 7 | RXC0 | Hay un byte recibido sin leer (se borra al leer UDR0) |
| 6 | TXC0 | Terminó de salir la trama (se borra escribiendo 1 o al atender la interrupción) |
| 5 | UDRE0 | UDR0 de transmisión vacío: se puede escribir el próximo byte |
| 4 | FE0 | Error de trama: el bit de parada llegó en 0 |
| 3 | DOR0 | Desbordamiento: llegó un byte y no había lugar |
| 2 | UPE0 | Error de paridad |
| 1 | U2X0 | Doble velocidad |
| 0 | MPCM0 | Modo multiprocesador |
| Registro | Bits | Función |
|---|---|---|
| UCSR0B | RXCIE0, TXCIE0, UDRIE0 | Habilitan las interrupciones de recepción, fin de transmisión y registro vacío |
| UCSR0B | RXEN0, TXEN0 | Habilitan el receptor y el transmisor (toman el control de los pines PD0 y PD1) |
| UCSR0B | UCSZ02 | Con UCSZ01:0 selecciona 5 a 9 bits de datos |
| UCSR0C | UMSEL01:0 | Modo: 00 asíncrono (el que usamos), 01 síncrono |
| UCSR0C | UPM01:0 | Paridad: 00 ninguna, 10 par, 11 impar |
| UCSR0C | USBS0 | Bits de parada: 0 = 1; 1 = 2 |
| UCSR0C | UCSZ01:0 | Bits de datos: 11 con UCSZ02 = 0 son 8 bits; 10 son 7 |
Además, UBRR0H y UBRR0L (0xC5 y 0xC4) forman el divisor de 12 bits, y UDR0 (0xC6) es el registro de datos. El formato 8N1 corresponde a UCSR0C = (1 << UCSZ01) | (1 << UCSZ00), es decir 0x06, que es también el valor de reset; el transmisor y el receptor se habilitan con UCSR0B = (1 << RXEN0) | (1 << TXEN0) (0x18).
Una regla de lectura: al recibir, primero se lee UCSR0A (para ver los errores) y después UDR0, porque leer el dato borra las banderas de error asociadas.
05 · Enviar bytes
Transmitir
La forma más sencilla es el sondeo: esperar que UDRE0 esté en 1 y escribir el byte.
#define F_CPU 16000000UL
#define BAUD 9600
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <stdio.h>
#include <util/setbaud.h> // calcula UBRR_VALUE y USE_2X
static void uart_init(void) {
UBRR0H = UBRRH_VALUE;
UBRR0L = UBRRL_VALUE;
#if USE_2X
UCSR0A |= (1 << U2X0);
#else
UCSR0A &= ~(1 << U2X0);
#endif
UCSR0B = (1 << RXEN0) | (1 << TXEN0);
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // 8N1
}
static void uart_tx(uint8_t dato) {
while (!(UCSR0A & (1 << UDRE0))) { } // espera el registro libre
UDR0 = dato;
}
static void uart_texto(const char *s) { while (*s) uart_tx((uint8_t)*s++); }
static void uart_texto_P(const char *s) { // cadena guardada en Flash
char c;
while ((c = pgm_read_byte(s++))) uart_tx((uint8_t)c);
}
static void uart_decimal(uint16_t v) { // convierte a texto sin printf
char buf[5];
uint8_t n = 0;
do { buf[n++] = (char)('0' + v % 10); v /= 10; } while (v);
while (n) uart_tx((uint8_t)buf[--n]);
}
Las cadenas constantes se guardan en Flash con PROGMEM (tema 8), porque la SRAM de 2 KB es escasa: un mensaje de 40 caracteres ocupa 40 bytes de SRAM si se declara de forma ordinaria. Para imprimir con formato se puede enlazar printf de avr-libc con una función de salida propia:
static int uart_putchar(char c, FILE *flujo) {
(void)flujo;
if (c == '\n') uart_tx('\r'); // \n -> \r\n para los terminales
uart_tx((uint8_t)c);
return 0;
}
static FILE salida = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);
/* en main(): stdout = &salida; printf("T=%d.%d C\n", t / 10, t % 10); */
printf es cómodo pero pesado (del orden de 1 a 2 KB de Flash y un rato de CPU), y la versión mínima de avr-libc no formatea números en coma flotante. En microcontroladores pequeños suele convenir convertir a texto a mano, como uart_decimal().
El costo oculto del sondeo es el tiempo. A 9600 baudios cada carácter tarda 1,04 ms: una línea de 30 caracteres detiene el programa 31 ms. Con 115 200 baudios son 2,6 ms. Si el programa tiene que atender un sensor cada milisegundo, imprimir por sondeo lo arruina. La solución son las interrupciones (sección 7).
Otro detalle: UDRE0 se pone en 1 cuando el último byte pasó al desplazador, pero la trama todavía no terminó de salir. Si hay que saber cuándo se enviaron todos los bits (para dormir el microcontrolador o para invertir el sentido de un transceptor RS-485), se espera la bandera TXC0.
06 · Recibir bytes
Recibir
// Devuelve el byte recibido, o -1 si no hay ninguno o llegó con error
static int16_t uart_rx(void) {
if (!(UCSR0A & (1 << RXC0))) return -1; // nada recibido
uint8_t estado = UCSR0A; // primero el estado
uint8_t dato = UDR0; // leer UDR0 borra RXC0 y los errores
if (estado & ((1 << FE0) | (1 << DOR0) | (1 << UPE0))) return -1;
return dato;
}
// Eco: devuelve cada carácter recibido (útil para probar el enlace)
/* for (;;) { int16_t c = uart_rx(); if (c >= 0) uart_tx((uint8_t)c); } */
El programa de eco es la primera prueba de cualquier enlace serie: si lo que escribís en el monitor vuelve idéntico, el cableado, la velocidad y el formato están bien. Ante un error, las tres banderas indican la causa: FE0 (error de trama) suele significar velocidad equivocada o ruido; DOR0 (desbordamiento), que el programa no lee a tiempo; UPE0, que el formato de paridad no coincide.
El sondeo de la recepción tiene la limitación de siempre: el programa debe preguntar con la frecuencia suficiente. A 115 200 baudios llega un carácter cada 87 µs; si el lazo principal tarda más, se pierden bytes. Por eso casi toda recepción real se hace con la interrupción USART_RX_vect.
07 · En segundo plano
Interrupciones y buffers circulares
La idea es la del tema 21: la interrupción coloca cada byte recibido en un buffer circular y el programa lo consume cuando puede. Un buffer circular es un arreglo con dos índices: la cabeza, donde escribe el productor, y la cola, de donde lee el consumidor. Cuando un índice llega al final, vuelve al principio; con un tamaño potencia de 2 basta una máscara para eso. El buffer está vacío si los dos índices coinciden, y lleno si avanzar la cabeza la haría coincidir con la cola (por eso la capacidad útil es el tamaño menos uno).
#include <avr/interrupt.h>
#define RX_TAM 64 // potencia de 2
static volatile uint8_t rx_buf[RX_TAM];
static volatile uint8_t rx_cab, rx_cola; // cab: escribe la ISR; cola: lee el programa
ISR(USART_RX_vect) {
uint8_t estado = UCSR0A;
uint8_t dato = UDR0; // leer UDR0 borra RXC0
uint8_t sig = (uint8_t)((rx_cab + 1) & (RX_TAM - 1));
if (estado & ((1 << FE0) | (1 << DOR0) | (1 << UPE0))) return; // descarta el byte con error
if (sig != rx_cola) { rx_buf[rx_cab] = dato; rx_cab = sig; } // si está lleno, se pierde
}
static int16_t uart_leer(void) {
if (rx_cab == rx_cola) return -1; // vacío
uint8_t dato = rx_buf[rx_cola];
rx_cola = (uint8_t)((rx_cola + 1) & (RX_TAM - 1));
return dato;
}
#define TX_TAM 64
static volatile uint8_t tx_buf[TX_TAM];
static volatile uint8_t tx_cab, tx_cola; // cab: escribe el programa; cola: lee la ISR
static void uart_escribir(uint8_t dato) {
uint8_t sig = (uint8_t)((tx_cab + 1) & (TX_TAM - 1));
while (sig == tx_cola) { } // lleno: espera (bloquea solo si el buffer se llena)
tx_buf[tx_cab] = dato;
tx_cab = sig;
UCSR0B |= (1 << UDRIE0); // activa la interrupción de «registro libre»
}
ISR(USART_UDRE_vect) {
if (tx_cola == tx_cab) { UCSR0B &= ~(1 << UDRIE0); return; } // nada más que enviar
UDR0 = tx_buf[tx_cola];
tx_cola = (uint8_t)((tx_cola + 1) & (TX_TAM - 1));
}
/* uart_init() más: UCSR0B |= (1 << RXCIE0); sei(); */
Cada índice lo modifica un único actor (la cabeza del buffer de recepción, la ISR; su cola, el programa), así que no hacen falta secciones críticas: los accesos de 8 bits son atómicos (tema 21). La interrupción de registro libre (UDRIE0) tiene una particularidad: mientras UDRE0 valga 1 y la interrupción esté habilitada, se dispara una y otra vez. Por eso la ISR la desactiva cuando no queda nada para enviar, y uart_escribir() la vuelve a activar cuando agrega un byte.
El tamaño del buffer sale de una cuenta: a la velocidad de la línea llegan baudios / 10 bytes por segundo; en el tiempo máximo que el programa puede pasar sin leer, llegan tiempo · baudios / 10 bytes, y el buffer debe contenerlos. La segunda herramienta del laboratorio la calcula. El core de Arduino usa buffers de 64 bytes en cada sentido.
Con el buffer de transmisión, uart_escribir() vuelve en microsegundos aunque la línea tarde milisegundos en vaciarse: el programa sigue trabajando y los caracteres salen en segundo plano. El precio es la memoria (64 bytes de SRAM por buffer) y el tiempo de las interrupciones: a 115 200 baudios, una cada 87 µs durante la transmisión, de unos 3 a 4 µs cada una (tema 19), alrededor del 4 % de la CPU.
08 · Hablar con la computadora
El monitor serie y los comandos de texto
El monitor serie del IDE de Arduino, y programas como PuTTY, CoolTerm o screen, abren el puerto USB como una UART: eligen la velocidad, muestran los bytes que llegan y envían lo que escribís. Tres detalles evitan horas de confusión:
- La velocidad del monitor debe ser la misma que la del programa. Con una velocidad distinta aparecen símbolos extraños.
- El terminador de línea. El monitor ofrece cuatro opciones: ninguno,
\n,\ro\r\n. El programa tiene que esperar el mismo que el monitor envía. - Al abrir el puerto, el Uno se reinicia. La línea DTR del adaptador USB provoca un reset (es lo que permite cargar programas sin tocar el botón). Los primeros mensajes de
setup()a veces se pierden mientras el monitor termina de abrirse.
Con eso se arma un intérprete de comandos: una máquina que ensambla líneas de texto y las ejecuta.
#include <string.h>
static char linea[24];
static uint8_t largo;
static void ejecutar(const char *cmd) {
if (strcmp(cmd, "LED ON") == 0) { PORTB |= (1 << PB5); uart_texto("OK\r\n"); }
else if (strcmp(cmd, "LED OFF") == 0) { PORTB &= ~(1 << PB5); uart_texto("OK\r\n"); }
else if (strcmp(cmd, "TEMP?") == 0) { /* leer el sensor e imprimir */ uart_texto("T=25.1 C\r\n"); }
else uart_texto("ERROR\r\n");
}
static void comandos(void) { // llamar en cada vuelta del lazo principal
int16_t c;
while ((c = uart_leer()) >= 0) {
if (c == '\r') continue; // acepta \n y \r\n
if (c == '\n') { linea[largo] = '\0'; ejecutar(linea); largo = 0; }
else if (largo < sizeof linea - 1) linea[largo++] = (char)c; // si no cabe, descarta
}
}
El mismo esquema sirve para controlar un robot desde un teléfono por Bluetooth, para leer un GPS (que envía líneas de texto $GPGGA,...) o para configurar un módulo con comandos AT. Siempre aparecen las mismas decisiones: terminador, tamaño máximo de línea, qué hacer si la línea se desborda y cómo informar errores.
09 · Diagnóstico
Cuando no se entiende nada
| Síntoma | Causa probable |
|---|---|
| Símbolos extraños y aleatorios | Velocidad distinta entre los dos extremos, o error de baudios demasiado grande (por ejemplo 115 200 con 16 MHz sin U2X0) |
| No llega nada | TX y RX sin cruzar (TX con TX), masa sin unir, receptor sin habilitar (RXEN0) o niveles de 3,3 V y 5 V mal combinados |
| Se pierden caracteres | Buffer demasiado chico, programa que lee tarde, DOR0 activo |
| Texto con los renglones superpuestos o con «escalones» | El terminal espera \r\n y el programa envía solo \n (o al revés) |
| Los comandos nunca coinciden | Un \r invisible al final, o el monitor sin terminador de línea |
| El programa se vuelve lento al imprimir | Impresión por sondeo a baja velocidad bloqueando la CPU |
| No se puede cargar el programa | Un módulo externo conectado a los pines 0 y 1 ocupa la línea que usa el cargador |
| Funciona unos minutos y falla | Error de baudios al límite, que el calor o la tensión empeoran (con resonador cerámico en lugar de cristal) |
Una receta de depuración: empezá con el eco (sección 6) a baja velocidad; cuando funcione, subí la velocidad; luego agregá el formato real y el buffer. Un analizador lógico o un osciloscopio sobre TX permiten ver la trama y medir el ancho de un bit (el tema 35 lo hace con un analizador).
10 · Analogía con programación
Baudios, buffers y líneas en Python
Este programa calcula el divisor y el error de baudios para varios cristales, simula la recepción con un buffer circular que se desborda cuando el programa está ocupado, compara enviar una lectura como texto o como binario, mide cuánto bloquea imprimir por sondeo y ensambla líneas de comando aceptando \n y \r\n.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
def ubrr_y_error(fclk, baud, u2x):
div = 8 if u2x else 16
ubrr = round(fclk / (div * baud) - 1)
if not 0 <= ubrr <= 4095:
return None
real = fclk / (div * (ubrr + 1))
return ubrr, real / baud - 1
# 1) tabla de errores de baudios, con y sin doble velocidad
print("fclk (MHz) baudios normal U2X0")
for fclk in (16e6, 8e6, 20e6, 18.432e6, 1e6):
for baud in (9600, 57600, 115200, 250000):
celdas = []
for u2x in (False, True):
r = ubrr_y_error(fclk, baud, u2x)
celdas.append("fuera de rango" if r is None else f"UBRR={r[0]:3d} {r[1]:+6.2%}")
print(f"{fclk / 1e6:9.3f} {baud:8d} {celdas[0]:18s} {celdas[1]}")
# 2) buffer circular de recepcion: un programa ocupado no lee durante 5 ms
def perdidos(baud, ocupado_s, tam, duracion_s=1.0):
por_caracter = 10 / baud
ocupado_hasta = 0.0
cola = 0
perdidos = 0
llegada = 0.0
while llegada < duracion_s:
llegada += por_caracter
if llegada >= ocupado_hasta and cola:
cola = 0 # el programa vacia el buffer cuando termina su trabajo
ocupado_hasta = llegada + ocupado_s
if cola < tam - 1:
cola += 1
else:
perdidos += 1
return perdidos
print()
print("bytes perdidos en 1 s a 115200 baudios con el programa ocupado 5 ms (ciclo de trabajo):")
for tam in (16, 32, 64, 128):
print(f" buffer de {tam:3d}: {perdidos(115200, 0.005, tam)} perdidos")
# 3) texto frente a binario para enviar una lectura de 10 bits
for valor in (7, 512, 1023):
texto = len(str(valor)) + 2 # digitos + \r\n
print(f"lectura {valor:4d}: texto {texto} bytes, binario 2 bytes")
# 4) cuanto bloquea imprimir una linea por sondeo
linea = "T=25.1 C\r\n"
for baud in (9600, 115200):
print(f"imprimir {linea.strip()!r} ({len(linea)} bytes) a {baud}: {len(linea) * 10 / baud * 1000:.2f} ms")
# 5) ensamblar lineas de comando desde un flujo de bytes, con \n o \r\n
def lineas(flujo):
actual = bytearray()
for b in flujo:
if b == 10: # \n termina la linea
yield actual.decode("ascii").strip()
actual.clear()
elif b != 13: # ignora el \r
actual.append(b)
flujo = b"LED ON\r\nLED OFF\nTEMP?\r\nXYZ"
for comando in lineas(flujo):
print("comando:", repr(comando), "->", "valido" if comando in ("LED ON", "LED OFF", "TEMP?") else "desconocido")
print("quedan sin terminar:", flujo.split(b"\n")[-1])
Resultado: con 16 MHz, 115 200 baudios da −3,55 % en modo normal y +2,12 % con U2X0; con 18,432 MHz el error es 0 en todas las velocidades; con 1 MHz los 9600 baudios dan −6,99 % y no sirven. En la simulación, con el programa ocupado 5 ms a 115 200 baudios llegan 57 bytes: un buffer de 16 o de 32 pierde miles de bytes por segundo y uno de 64 o de 128 no pierde ninguno. Imprimir 10 bytes por sondeo bloquea 10,4 ms a 9600 y 0,87 ms a 115 200. Y el intérprete de líneas separa tres comandos válidos y deja pendiente el último, sin terminador.
El modelo del buffer es simple (el consumidor vacía todo de una vez al terminar su trabajo); un programa real lee de a poco, pero el dimensionamiento por el peor caso es el mismo.
11 · Exploración interactiva
Baudios, buffers y líneas
La primera herramienta calcula UBRR0 y el error para tu reloj y velocidad, y arma los registros. La segunda dimensiona los buffers. La tercera muestra los bytes exactos que viajan por la línea para un texto y su terminador.
Banco de la UART
1. Divisor de baudios y registros
2. Dimensionar los buffers
3. Qué viaja por la línea
Recorrido guiado
- Con 16 MHz y 115 200 baudios en modo normal: UBRR0 = 8, error −3,55 % y una nota de «marginal». Activá U2X0: UBRR0 = 16 y +2,12 %. Cambiá el reloj a 18,432 MHz: error 0.
- Con 1 MHz y 9600 baudios sin U2X0 el error es −6,99 %; con U2X0 baja a +0,16 %. Con 1 MHz y 115 200 el error es −45,75 % (UBRR0 no puede ser menor que 0): imposible.
- Elegí 7 bits, paridad par y 2 bits de parada:
UCSR0Cpasa a 0x2C (UPM01 + USBS0 + 7 bits). - En la herramienta 2, a 115 200 baudios y 5 ms de espera llegan 57 bytes: un buffer de 32 desborda (se pierden 26) y uno de 64 alcanza.
- Con 9600 baudios y 30 bytes a enviar: 31 ms de bloqueo por sondeo y 0 con el buffer de 64.
- En la herramienta 3, «T=25,1 C» con
\r\nson 10 bytes y 10,4 ms a 9600 baudios. El número 1023 ocupa 6 bytes como texto y 2 en binario.
Desafío: un GPS a 9600 baudios
Un módulo GPS envía unas 5 líneas de unos 80 caracteres por segundo. ¿Cuánto ocupa la línea y alcanza un buffer de 64 bytes? 400 bytes por segundo son el 42 % de los 960 bytes por segundo de un enlace de 9600 baudios en 8N1. Una línea de 80 caracteres tarda 83 ms en llegar; si el programa pasa más de 66 ms sin leer (64 bytes a 1,04 ms cada uno), se pierden caracteres. Con 128 bytes hay margen para una línea completa (80), pero no para dos. La solución es atender la UART en cada vuelta del lazo y procesar cada línea apenas termina.
12 · Comprensión
Confusiones frecuentes
«Serial.print() envía el dato inmediatamente»
Lo copia en un buffer de salida (64 bytes en Arduino) y una interrupción lo va enviando. Si el buffer se llena, print bloquea hasta que haya lugar. Por eso un programa que imprime demasiado se vuelve lento.
«UBRR0 = 8 es 115 200 baudios»
Es aproximadamente 111 111 con 16 MHz. Funciona con ciertos dispositivos tolerantes y falla con otros. Verificá el error antes de elegir una velocidad.
«Leo UDR0 dos veces para asegurarme»
Cada lectura de UDR0 saca un byte del FIFO: una segunda lectura devuelve el siguiente byte, o basura si no hay. Se lee una vez por byte, después de consultar el estado.
«UDRE0 en 1 significa que ya se envió todo»
Significa que el registro está libre. La trama aún puede estar saliendo. Para saber que terminó, se usa TXC0.
«Puedo usar los pines 0 y 1 para otra cosa mientras uso el monitor serie»
Con RXEN0 y TXEN0 activados, el USART toma el control de PD0 y PD1. Y en el Uno están conectados al chip USB, así que cualquier otra carga en esos pines interfiere.
13 · Práctica guiada
Predecí antes de comprobar
1. El divisor
Con 8 MHz y 9600 baudios en modo normal, ¿cuánto vale UBRR0 y cuál es el error?
Ver solución
8 000 000 / (16 × 9600) − 1 = 51,08 → UBRR0 = 51. Velocidad real: 8 000 000 / (16 × 52) = 9615 baudios: +0,16 %.
2. Doble velocidad
¿Qué divisor y qué error resultan para 115 200 baudios con 16 MHz y U2X0 = 1?
Ver solución
16 000 000 / (8 × 115 200) − 1 = 16,36 → UBRR0 = 16. Real: 16 000 000 / (8 × 17) = 117 647 baudios: +2,12 %.
3. Un buffer de recepción
A 57 600 baudios, un programa se bloquea hasta 20 ms sin leer la UART. ¿Qué tamaño mínimo de buffer necesita, con tamaño potencia de 2?
Ver solución
Cada carácter tarda 10 / 57 600 = 173,6 µs. En 20 ms llegan 20 / 0,1736 = 115 bytes. Como la capacidad útil es el tamaño menos uno, hace falta al menos 116: 128 bytes.
4. Qué envía println
¿Qué bytes se envían con Serial.println(42)?
Ver solución
Los caracteres «4», «2» y el terminador: 0x34 0x32 0x0D 0x0A, cuatro bytes. No viaja el número 42 binario (0x2A).
5. Para qué sirve TXC0
Un programa envía un mensaje y enseguida pone al microcontrolador a dormir. ¿Qué bandera conviene esperar y por qué?
Ver solución
TXC0. UDRE0 sube cuando el último byte pasa al desplazador, pero sus bits todavía están saliendo; si el chip se duerme o se corta el reloj en ese momento, el último carácter sale cortado. TXC0 indica que la trama completa, incluido el bit de parada, ya salió.
14 · Síntesis
Ideas para recordar
- El USART0 tiene un transmisor y un receptor independientes en PD1 y PD0, con registro de datos
UDR0y tres banderas:UDRE0,TXC0yRXC0. - El divisor es
UBRR0 = f / (16 · baudios) − 1(o 8 con U2X0). El error de redondeo importa: 16 MHz con 115 200 baudios solo es seguro con U2X0 o con un cristal de 18,432 MHz. - 8N1 se configura con
UCSR0C = 0x06y la UART se activa conRXEN0yTXEN0enUCSR0B. - Por sondeo, transmitir bloquea la CPU (1 ms por carácter a 9600); con buffer circular e interrupciones el programa sigue trabajando.
- Al recibir, se lee primero el estado y después
UDR0; los erroresFE0,DOR0yUPE0indican velocidad, desbordamiento y paridad. - El tamaño del buffer sale de la cuenta tiempo × baudios / 10; la capacidad útil de un buffer circular es el tamaño menos uno.
- Un intérprete de comandos de texto ensambla líneas, ignora el
\ry valida lo recibido; terminador y velocidad deben coincidir con el monitor.
¿Listo para el tema 33?
Antes de avanzar, calculá el divisor para un reloj y una velocidad, escribí de memoria uart_init() y uart_tx(), y explicá cuándo conviene el buffer con interrupciones. Si podés justificar las tres respuestas, dominás la UART.
En el próximo tema vemos el bus I2C: cómo hablar con muchos dispositivos por solo dos cables, con direcciones, confirmaciones (ACK) y resistencias de pull-up.