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.
2. Estructura de la Trama UART
Cada byte transmitido sigue un formato específico que los dispositivos deben acordar antes de comunicarse:
(1 bit)
(5-9 bits)
(0-1 bit)
(1-2 bits)
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.
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 |
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.
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;
}
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;
}
- 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;
}
}
}