← Volver al inicio

Laboratorio 5: Comunicación Serie UART

Transmisión y recepción asíncrona de datos

1. ¿Qué es el Puerto Serie (UART)?

UART (Universal Asynchronous Receiver-Transmitter) es un protocolo de comunicación serie asíncrona que permite la transmisión de datos entre dispositivos utilizando solo dos líneas: TX (transmisión) y RX (recepción).

Características Principales:

Asíncrono

No requiere una señal de reloj compartida. Los dispositivos acuerdan previamente la velocidad (baudrate).

Full-Duplex

Permite transmitir y recibir simultáneamente gracias a las líneas TX y RX independientes.

Simple

Solo necesita 2 cables (TX/RX) más una referencia común (GND). Ideal para distancias cortas.

Configurable

Velocidad, bits de datos, paridad y bits de parada son configurables según la aplicación.

Aplicaciones comunes: Depuración de código (printf), comunicación con módulos GPS, Bluetooth, sensores, pantallas LCD, etc.

2. Estructura de la Trama UART

Cada byte transmitido sigue un formato específico que los dispositivos deben acordar antes de comunicarse:

START
(1 bit)
DATA
(5-9 bits)
PARITY
(0-1 bit)
STOP
(1-2 bits)
🛠️ Simulador Interactivo UART Pantalla completa ↗

Bit de START

Siempre es un 0 lógico. Indica al receptor que comienza la transmisión de un nuevo byte.

Bits de DATOS

Normalmente 8 bits (1 byte). Pueden ser 5, 6, 7 o 9 bits según configuración. Se transmiten LSB primero.

Bit de PARIDAD

Opcional. Permite detectar errores de transmisión. Puede ser Par (Even), Impar (Odd) o Ninguna.

Bits de STOP

Siempre en 1 lógico. Marca el final de la trama. Pueden ser 1, 1.5 o 2 bits.

3. Cálculo del Baudrate (BRR)

La velocidad de comunicación se configura mediante el registro USART_BRR (Baud Rate Register). Este registro divide la frecuencia del reloj del periférico para generar la velocidad deseada.

BRR = fPCLK / (16 × Baudrate)

Donde fPCLK es la frecuencia del bus APB (36MHz para USART1/2/3 en APB1 con reloj a 72MHz)

Ejemplos de Cálculo:

Baudrate Deseado fPCLK (Hz) Cálculo BRR (Dec) BRR (Hex)
9600 bps 36,000,000 36M / (16 × 9600) 234.375 ≈ 234 0x00EA
115200 bps 36,000,000 36M / (16 × 115200) 19.53 ≈ 19.5 0x0138
9600 bps 72,000,000 72M / (16 × 9600) 468.75 ≈ 469 0x01D5
Nota: El STM32F103RB tiene USART1 en APB2 (72MHz) y USART2/3 en APB1 (36MHz). Verifica qué bus usa tu USART antes de calcular BRR.

4. Control de Paridad

El bit de paridad es un mecanismo simple de detección de errores. Se añade un bit extra cuyo valor hace que la cantidad total de '1's en la trama cumpla una regla específica.

Paridad Par (Even)

El bit de paridad se ajusta para que el número total de '1's sea par.

Ejemplo: Dato = 0b10110010 (4 unos, par) → Paridad = 0

Paridad Impar (Odd)

El bit de paridad se ajusta para que el número total de '1's sea impar.

Ejemplo: Dato = 0b10110010 (4 unos, par) → Paridad = 1

Sin Paridad (None)

No se envía ningún bit de paridad. Configuración más común para comunicaciones confiables.

Limitación: La paridad solo detecta errores impares (1, 3, 5... bits erróneos). Si hay 2 bits erróneos, la paridad no lo detecta. Para mayor seguridad, usa CRC o checksums en el protocolo de aplicación.

5. Configuración Paso a Paso: USART2 a 115200 bps

Vamos a configurar USART2 (pines PA2=TX, PA3=RX) para comunicación a 115200 baudios, 8 bits de datos, sin paridad, 1 bit de stop (configuración estándar 8N1).

Paso 1: Habilitar Relojes (RCC)

Activar el reloj del GPIOA (pines TX/RX) y del USART2.

RCC->APB2ENR |= (1 << 0);  // Habilitar GPIOA
RCC->APB1ENR |= (1 << 17); // Habilitar USART2
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;    // Habilitar GPIOA
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;  // Habilitar USART2

Paso 2: Configurar Pines GPIO

PA2 (TX) como Alternate Function Push-Pull. PA3 (RX) como Input Floating.

// PA2 (TX): AF Push-Pull, 50MHz
GPIOA->CRL &= ~(0xF << 8);
GPIOA->CRL |= (0xB << 8);  // CNF=10, MODE=11

// PA3 (RX): Input Floating
GPIOA->CRL &= ~(0xF << 12);
GPIOA->CRL |= (0x4 << 12); // CNF=01, MODE=00
// PA2 (TX): AF Push-Pull, 50MHz
GPIOA->CRL &= ~(GPIO_CRL_MODE2 | GPIO_CRL_CNF2);
GPIOA->CRL |= GPIO_CRL_MODE2 | GPIO_CRL_CNF2_1;

// PA3 (RX): Input Floating
GPIOA->CRL &= ~(GPIO_CRL_MODE3 | GPIO_CRL_CNF3);
GPIOA->CRL |= GPIO_CRL_CNF3_0;

Paso 3: Configurar Baudrate (BRR)

Para 115200 bps con APB1 a 36MHz: BRR = 36M / (16 × 115200) ≈ 19.53 → 0x138

USART2->BRR = 0x138;  // 115200 bps @ 36MHz

Para otros baudrates, recalcula según la fórmula vista anteriormente.

Paso 4: Configurar Formato (CR1)

Definir longitud de palabra, paridad y habilitar TX/RX.

// CR1: Control Register 1
USART2->CR1 = 0;           // Reset
USART2->CR1 |= (1 << 13);  // UE: USART Enable
USART2->CR1 |= (1 << 3);   // TE: Transmitter Enable
USART2->CR1 |= (1 << 2);   // RE: Receiver Enable
// Bits de datos = 8 (M=0), Sin paridad (PCE=0)
// Configuración con CMSIS
USART2->CR1 = 0;                           // Reset
USART2->CR1 |= USART_CR1_UE;               // USART Enable
USART2->CR1 |= USART_CR1_TE | USART_CR1_RE; // TX y RX habilitados

Paso 5: Configurar Bits de Stop (CR2)

Por defecto, CR2 = 0 configura 1 bit de stop, que es lo estándar.

USART2->CR2 = 0;  // 1 stop bit (STOP[1:0] = 00)

6. Transmisión y Recepción de Datos

Transmitir un Byte

Escribir en el registro DR y esperar a que el flag TXE (Transmit Data Register Empty) se active.

void UART_SendByte(uint8_t data) {
    while (!(USART2->SR & (1 << 7)));  // Esperar TXE
    USART2->DR = data;  // Escribir dato
}
void UART_SendByte(uint8_t data) {
    while (!(USART2->SR & USART_SR_TXE));
    USART2->DR = data;
}

Recibir un Byte

Leer del registro DR cuando el flag RXNE (Receive Data Register Not Empty) esté activo.

uint8_t UART_ReceiveByte(void) {
    while (!(USART2->SR & (1 << 5)));  // Esperar RXNE
    return USART2->DR;  // Leer dato
}
uint8_t UART_ReceiveByte(void) {
    while (!(USART2->SR & USART_SR_RXNE));
    return USART2->DR;
}
Importante: Leer el registro DR limpia automáticamente el flag RXNE. Asegúrate de procesar o guardar el dato antes de la siguiente lectura, o se perderá.

7. Ejemplos Básicos (Polling)

A continuación se muestran dos programas independientes que funcionan por polling (preguntando continuamente). Son ideales para entender el funcionamiento básico.

📤 Programa 1: Transmisor Simple

Este programa envía continuamente un contador incrementado cada segundo por UART.

#include "stm32f10x.h"

void UART2_Init(void);
void UART_SendByte(uint8_t data);
void Delay_ms(uint32_t ms);

int main(void)
{
    UART2_Init();
    uint8_t counter = 0;
    
    while (1)
    {
        UART_SendByte(counter);  // Enviar valor del contador
        counter++;               // Incrementar
        Delay_ms(1000);         // Esperar 1 segundo
    }
}

void UART2_Init(void)
{
    // Habilitar relojes
    RCC->APB2ENR |= (1 << 0);   // GPIOA
    RCC->APB1ENR |= (1 << 17);  // USART2
    
    // Configurar PA2 (TX) - No necesitamos RX en transmisor
    GPIOA->CRL &= ~(0xF << 8);
    GPIOA->CRL |= (0xB << 8);   // PA2: AF Push-Pull 50MHz
    
    // Configurar USART2
    USART2->BRR = 0x138;        // 115200 bps @ 36MHz
    USART2->CR1 = (1 << 13) | (1 << 3);  // UE, TE (solo transmisor)
}

void UART_SendByte(uint8_t data)
{
    while (!(USART2->SR & (1 << 7)));  // Esperar TXE
    USART2->DR = data;
}

void Delay_ms(uint32_t ms)
{
    // Delay simple (ajustar según frecuencia de reloj)
    for (uint32_t i = 0; i < ms * 8000; i++) {
        __NOP();
    }
}
#include "stm32f10x.h"

void UART2_Init(void);
void UART_SendByte(uint8_t data);
void Delay_ms(uint32_t ms);

int main(void)
{
    UART2_Init();
    uint8_t counter = 0;
    
    while (1)
    {
        UART_SendByte(counter);
        counter++;
        Delay_ms(1000);
    }
}

void UART2_Init(void)
{
    // Habilitar reloj
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    
    // Configurar PA2 (TX)
    GPIOA->CRL &= ~(GPIO_CRL_MODE2 | GPIO_CRL_CNF2);
    GPIOA->CRL |= GPIO_CRL_MODE2 | GPIO_CRL_CNF2_1;
    
    // Configurar USART2
    USART2->BRR = 0x138;
    USART2->CR1 = USART_CR1_UE | USART_CR1_TE;
}

void UART_SendByte(uint8_t data)
{
    while (!(USART2->SR & USART_SR_TXE));
    USART2->DR = data;
}

void Delay_ms(uint32_t ms)
{
    for (uint32_t i = 0; i < ms * 8000; i++) {
        __NOP();
    }
}

📥 Programa 2: Receptor Simple (Polling)

Este programa espera activamente la llegada de bytes. Limitación: Mientras espera, no puede hacer nada más.

#include "stm32f10x.h"

void UART2_Init(void);
void GPIO_Init(void);
uint8_t UART_ReceiveByte(void);

int main(void)
{
    UART2_Init();
    GPIO_Init();  // Configurar PC13 como salida (LED)
    
    while (1)
    {
        // BLOQUEANTE: La CPU se queda aquí hasta que llegue un dato
        uint8_t received = UART_ReceiveByte();  
        
        // Conmutar LED si el valor es par
        if ((received % 2) == 0) {
            GPIOC->ODR ^= (1 << 13);  // Toggle PC13
        }
    }
}

void GPIO_Init(void)
{
    RCC->APB2ENR |= (1 << 4);      // Habilitar GPIOC
    GPIOC->CRH &= ~(0xF << 20);
    GPIOC->CRH |= (0x2 << 20);     // PC13: Output 2MHz
}

void UART2_Init(void)
{
    // Habilitar relojes
    RCC->APB2ENR |= (1 << 0);   // GPIOA
    RCC->APB1ENR |= (1 << 17);  // USART2
    
    // Configurar PA3 (RX) - No necesitamos TX en receptor
    GPIOA->CRL &= ~(0xF << 12);
    GPIOA->CRL |= (0x4 << 12);  // PA3: Input Floating
    
    // Configurar USART2
    USART2->BRR = 0x138;        // 115200 bps @ 36MHz
    USART2->CR1 = (1 << 13) | (1 << 2);  // UE, RE (solo receptor)
}

uint8_t UART_ReceiveByte(void)
{
    while (!(USART2->SR & (1 << 5)));  // Esperar RXNE
    return USART2->DR;
}
#include "stm32f10x.h"
// ... (mismo código usando definiciones CMSIS) ...

int main(void)
{
    UART2_Init();
    GPIO_Init();
    
    while (1)
    {
        uint8_t received = UART_ReceiveByte();
        
        if ((received % 2) == 0) {
            GPIOC->ODR ^= GPIO_ODR_ODR13;
        }
    }
}

void GPIO_Init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
    GPIOC->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13);
    GPIOC->CRH |= GPIO_CRH_MODE13_1;  // 2MHz Output
}

void UART2_Init(void)
{
    // Habilitar relojes
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    
    // Configurar PA3 (RX)
    GPIOA->CRL &= ~(GPIO_CRL_MODE3 | GPIO_CRL_CNF3);
    GPIOA->CRL |= GPIO_CRL_CNF3_0;  // Input Floating
    
    // Configurar USART2
    USART2->BRR = 0x138;
    USART2->CR1 = USART_CR1_UE | USART_CR1_RE;
}

uint8_t UART_ReceiveByte(void)
{
    while (!(USART2->SR & USART_SR_RXNE));
    return USART2->DR;
}
Conexión entre dos STM32:
  • Conectar TX del transmisor → RX del receptor (PA2 → PA3)
  • Conectar GND común entre ambos dispositivos
  • Ambos deben estar configurados al mismo baudrate (115200 bps)

8. Gestión Avanzada: Interrupciones

Usar polling (como en el ejemplo anterior) desperdicia recursos. La forma profesional es usar Interrupciones.

Configuración Diferencial

Necesitamos habilitar el bit RXNEIE en control del USART y la línea IRQ en el NVIC.

// Habilitar Interrupciones en inicialización
USART2->CR1 |= (1 << 5);     // Habilitar RXNE Interrupt
NVIC_EnableIRQ(USART2_IRQn); // Habilitar en NVIC

⚡ Programa 3: Receptor por Interrupción

En este ejemplo, el main() está libre (o en modo bajo consumo __WFI()). El LED cambia solo cuando la interrupción "salta".

#include "stm32f10x.h"

void UART2_Init_IRQ(void);
void GPIO_Init(void);

int main(void)
{
    UART2_Init_IRQ();
    GPIO_Init();
    
    while (1)
    {
        // El programa principal está LIBRE
        // Podemos hacer otras cosas o dormir la CPU
        __WFI(); 
    }
}

void UART2_Init_IRQ(void)
{
    // ... (Configuración de relojes y GPIO igual) ...
    RCC->APB2ENR |= (1 << 0);
    RCC->APB1ENR |= (1 << 17);
    GPIOA->CRL |= (0x4 << 12); // PA3 RX

    // USART2 Config:
    USART2->BRR = 0x138;       // 115200
    
    // Habilitar RX, UART y RX INTERRUPT (Bit 5, 2, 13)
    USART2->CR1 = (1 << 13) | (1 << 2) | (1 << 5);
    
    // Habilitar NVIC
    NVIC_EnableIRQ(USART2_IRQn);
}

// Rutina de Servicio de Interrupción
void USART2_IRQHandler(void)
{
    // Verificar si es interrupción de recepción
    if (USART2->SR & (1 << 5)) {
        
        // Al leer DR, el flag se limpia solo
        uint8_t received = USART2->DR;
        
        // Procesar dato recibido
        if ((received % 2) == 0) {
            GPIOC->ODR ^= (1 << 13);
        }
    }
}
#include "stm32f10x.h"

// ... (Prototipos) ...

void UART2_Init_IRQ(void)
{
    // ... (Relojes y GPIO) ...

    USART2->BRR = 0x138;
    
    // Habilitar RXNEIE (Interrupción RX Not Empty)
    USART2->CR1 = USART_CR1_UE | USART_CR1_RE | USART_CR1_RXNEIE;
    
    NVIC_EnableIRQ(USART2_IRQn);
}

void USART2_IRQHandler(void)
{
    if (USART2->SR & USART_SR_RXNE) {
        uint8_t received = USART2->DR;
        
        if ((received % 2) == 0) {
            GPIOC->ODR ^= GPIO_ODR_ODR13;
        }
    }
}