A UART (Universal Asynchronous Receiver/Transmitter) é um dos blocos periféricos mais usados em microcontroladores STM32 para depuração, comunicação com PCs, módulos Bluetooth, GPS, entre outros. Na prática, é “a porta serial” clássica, só que implementada diretamente dentro do microcontrolador, permitindo enviar e receber bytes de forma assíncrona, usando poucos pinos.
Neste tutorial vamos usar como referência a placa Nucleo STM32F411RE, que traz o microcontrolador STM32F411RE e já possui uma interface ST-LINK/V2-1 que expõe uma porta serial virtual (Virtual COM Port) via USB. Isso é perfeito para testes, porque você só precisa de um cabo USB conectado ao PC e um terminal serial (PuTTY, TeraTerm, minicom, etc.) para enxergar os dados enviados pelo printf ou por funções que escrevem na UART, e também para enviar comandos de volta ao microcontrolador.
O foco aqui é mostrar, de forma didática e minuciosa, como:
- Configurar a UART no STM32CubeMX (usando o CubeIDE ou o CubeMX standalone);
- Enviar dados pela UART (modo bloqueante e não bloqueante);
- Receber dados pela UART com:
- Polling (consulta ativa);
- Interrupção (callback
HAL_UART_RxCpltCallback); - Um pequeno “buffer” de recepção para montar strings/comandos.
Ao final, você terá uma aplicação completa rodando na Nucleo STM32F411RE, capaz de:
- Enviar mensagens de texto para o PC (por exemplo, mensagens de debug);
- Receber caracteres ou comandos vindos do terminal serial;
- Fazer um “eco” dos dados recebidos (o clássico “echo server” na UART);
- Servir de base para protocolos mais complexos (menus, comandos AT, etc.).
Visão geral do hardware usado
- Placa: NUCLEO-F411RE
- UART usada: normalmente USART2, mapeada pelos jumpers da placa para o ST-LINK (Virtual COM Port).
- Conexão com o PC:
- Cabo USB ligado ao conector ST-LINK da Nucleo.
- No PC, aparecerá uma porta COM (Windows) ou
/dev/ttyACM*//dev/ttyUSB*(Linux) correspondente.
Importante: A UART física (pinos PA2/PA3 na Nucleo F411RE, em geral) já está ligada internamente ao ST-LINK. Em muitos casos não é necessário conectar fios extras para fazer depuração via serial: basta configurar a USART correta no CubeMX e abrir o terminal no PC.
2. Configurando a UART no STM32CubeMX para a Nucleo STM32F411RE
Nesta seção vamos configurar a USART2, que é a interface serial já conectada ao ST-LINK e que aparece no PC como uma porta serial virtual. Esse é o caminho padrão para depuração via UART no NUCLEO-F411RE.
2.1 Selecionando a UART correta no CubeMX
- Abra o STM32CubeIDE e crie um novo projeto para a NUCLEO-F411RE.
- Na aba Pinout & Configuration, localize a categoria Connectivity → USART2.
- Clique em USART2 e selecione Asynchronous.
O CubeMX automaticamente habilitará os pinos:
- PA2 – USART2_TX
- PA3 – USART2_RX
Esses pinos já estão roteados para o ST-LINK, permitindo enviar e receber dados via USB.

2.2 Configurando parâmetros da UART
Com a USART2 selecionada, abra seu painel de configurações:

• Baud rate
- Valor recomendado: 115200 bps
- Rápido, estável e amplamente suportado por terminais seriais.
• Palavra de dados (Word Length)
- 8 Bits (padrão universal e ideal para ASCII).
• Paridade (Parity)
- None (sem paridade).
• Bits de parada (Stop Bits)
- 1 Stop Bit.
• Modo
- TX and RX (necessário tanto para enviar quanto para receber).
• Hardware Flow Control
- None (a Nucleo não implementa RTS/CTS no ST-LINK).
2.3 Configurando as interrupções (NVIC)
Como iremos utilizar recepção por interrupção (mais robusta que polling), precisamos ativá-las:

- No painel da USART2, clique na aba NVIC Settings.
- Marque: USART2 global interrupt.
- Prioridade recomendada:
- Preemption Priority: 0
- Sub Priority: 0
Ponto para inserir imagem 3 (Configurar NVIC da USART2)
2.4 Clock e velocidade da UART
A UART usa como referência o clock do barramento APB1. GARANTA que:

- No menu Clock Configuration:
- APB1 esteja configurado para 50 MHz ou 42 MHz (valores usuais no F411).
- O cubemx indicará se o baud rate pode ser gerado com boa precisão.
Se a precisão for baixa (<2%), o CubeMX exibirá um alerta. Ajuste o clock se necessário.
2.5 Geração do Código
Quando tudo estiver configurado:
- Clique em Project Manager → confirme o nome do projeto.
- Selecione HAL como driver.
- Clique em GENERATE CODE.
O CubeIDE criará arquivos importantes:
- usart.c → contém
MX_USART2_UART_Init(), definições do handlehuart2, etc. - usart.h → exposições das funções e do handle.
- Inicialização da UART será chamada automaticamente dentro de
main.cpela funçãoMX_USART2_UART_Init().
Ponto para inserir imagem 5 (Tela de Generate Code)
2.6 Confirmando a inicialização no main.c
Após gerar o código, abra o arquivo main.c e verifique:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init(); // <-- UART inicializada aqui
while (1)
{
}
}
E veja o handle da UART definido em usart.c:
UART_HandleTypeDef huart2;
2.7 Teste rápido da UART (primeiro envio)
Antes mesmo de escrever as funções detalhadas, podemos testar a UART:
uint8_t msg[] = "UART funcionando!\r\n";
HAL_UART_Transmit(&huart2, msg, sizeof(msg)-1, HAL_MAX_DELAY);
Abra o terminal serial (115200, 8N1) no PC, reset a placa, e confirme se a mensagem aparece.
Se aparecer, a UART está configurada corretamente.
3. Enviando dados pela UART no STM32F411RE
Agora que a USART2 está configurada no CubeMX, vamos ver na prática como transmitir dados. Vamos começar pelo modo mais simples (bloqueante) e depois comentar alternativas.
Vou assumir sempre:
#include "usart.h" // gera o handle huart2
extern UART_HandleTypeDef huart2;
3.1. Conceito rápido: transmissão bloqueante vs não bloqueante
Na HAL temos basicamente três famílias de funções para TX:
- Bloqueante:
HAL_UART_Transmit(&huart2, pData, Size, Timeout);
A função só retorna quando:- Todos os bytes foram enviados, ou
- O tempo de
Timeoutestourou.
- Por interrupção:
HAL_UART_Transmit_IT(&huart2, pData, Size);
A função retorna “rápido”, e o envio acontece em background.
Quando termina, a HAL chama o callbackHAL_UART_TxCpltCallback(). - Por DMA:
HAL_UART_Transmit_DMA(&huart2, pData, Size);
Similar ao IT, mas usando DMA. Ideal para grandes volumes de dados.
(Vou citar aqui, mas podemos deixar o uso detalhado para uma seção futura, se você quiser.)
Para debug e exemplos simples, o bloqueante é o mais direto e suficiente.
3.2. Primeiro exemplo: “Hello, UART!” (modo bloqueante)
No main.c, após a inicialização, podemos mandar uma mensagem a cada segundo:
#include "main.h"
#include "usart.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
uint8_t msg[] = "Hello, UART!\r\n";
while (1)
{
HAL_UART_Transmit(&huart2,
msg,
sizeof(msg) - 1, // sem o '\0'
HAL_MAX_DELAY); // espera o tempo que precisar
HAL_Delay(1000); // 1 segundo
}
}
Abra o terminal serial (115200, 8N1) e você deve ver a mensagem sendo impressa a cada segundo.
Pontos importantes:
sizeof(msg) - 1: envia só até o\n, sem incluir o byte nulo final.HAL_MAX_DELAY: faz a função esperar até o fim da transmissão (modo bem simples para debug).- Esse padrão é muito usado para mensagens curtas de status.
Ponto para inserir imagem 6: captura do terminal exibindo
"Hello, UART!".
3.3. Função utilitária para enviar strings (Serial_SendString)
Na prática, é comum criar uma função de conveniência para enviar strings ASCII (char *).
Você pode colocá-la, por exemplo, no final de usart.c ou em um módulo serial.c:
#include "usart.h"
#include <string.h>
/**
* @brief Envia uma string ASCII terminada em '\0' pela USART2 (bloqueante).
*/
void Serial_SendString(const char *str)
{
if (str == NULL)
return;
HAL_UART_Transmit(&huart2,
(uint8_t *)str,
strlen(str),
HAL_MAX_DELAY);
}
Uso em main.c:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
Serial_SendString("Sistema iniciado...\r\n");
while (1)
{
Serial_SendString("Loop principal rodando.\r\n");
HAL_Delay(1000);
}
}
Vantagens:
- Você não precisa ficar calculando
sizeof(...) - 1toda hora. - Facilita a criação de logs e mensagens de debug.
3.4. Enviando buffers binários (não só texto)
Nem sempre você quer mandar texto; muitas vezes vai mandar dados binários (por exemplo, struct, amostras de ADC, etc.). O uso é o mesmo, apenas com um uint8_t * e um tamanho:
void Serial_SendBuffer(uint8_t *buffer, uint16_t length)
{
if (buffer == NULL || length == 0)
return;
HAL_UART_Transmit(&huart2, buffer, length, HAL_MAX_DELAY);
}
Exemplo de uso:
uint8_t data[4] = {0xDE, 0xAD, 0xBE, 0xEF};
Serial_SendBuffer(data, sizeof(data));
No terminal “puro” você verá caracteres estranhos, pois não são ASCII, mas do ponto de vista da camada física tudo está correto.
3.5. Transmissão por interrupção (HAL_UART_Transmit_IT)
Quando você começa a mandar mensagens mais longas e quer não travar o CPU durante o envio, a versão por interrupção faz sentido.
3.5.1. Buffer global e flag de estado
No main.c ou em um módulo próprio:
#include "usart.h"
#include <string.h>
uint8_t txBuffer[64];
volatile uint8_t txBusy = 0;
3.5.2. Função para enviar string de forma não bloqueante
HAL_StatusTypeDef Serial_SendString_IT(const char *str)
{
if (str == NULL)
return HAL_ERROR;
if (txBusy) // ainda está transmitindo algo
return HAL_BUSY;
size_t len = strlen(str);
if (len > sizeof(txBuffer))
len = sizeof(txBuffer); // truncar se for muito grande
memcpy(txBuffer, str, len);
txBusy = 1;
return HAL_UART_Transmit_IT(&huart2, txBuffer, len);
}
3.5.3. Callback de fim de transmissão
A HAL chama esse callback automaticamente ao terminar o envio:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
txBusy = 0; // libera para próxima transmissão
}
}
3.5.4. Uso no main
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
Serial_SendString_IT("Inicio não bloqueante...\r\n");
while (1)
{
if (!txBusy)
{
Serial_SendString_IT("Outra mensagem via IT.\r\n");
}
// Aqui seu código principal roda enquanto a UART transmite em background
HAL_Delay(500);
}
}
Observações importantes:
- Não chame
Serial_SendString_ITnovamente enquantotxBusy == 1, ou os dados podem se misturar. - Esse padrão é o mesmo que o artigo da ST usa para RX: um buffer global + callback para notificar fim da operação. (STMicroelectronics)
- Para sistemas maiores, normalmente se implementa uma fila (queue) de mensagens ou um ring buffer para TX.
3.6. Quando considerar DMA para transmissão
De forma resumida:
- Mensagens curtas, debug, comandos esporádicos:
HAL_UART_Transmit(bloqueante) é mais do que suficiente e muito simples. - Mensagens médias que não podem travar o CPU, mas não são contínuas:
HAL_UART_Transmit_IT. - Streams contínuos ou muito longos (telemetria, logs pesados, etc.):
HAL_UART_Transmit_DMA+ um buffer circular costuma ser a melhor solução.
Se quiser, podemos dedicar uma seção futura só a TX por DMA.
4. Lendo dados pela UART usando polling (HAL_UART_Receive)
Vamos começar a recepção de dados pela forma mais simples: polling. Ou seja, o código “espera” (bloqueia) até que um ou mais bytes cheguem e só então continua a execução.
A função principal da HAL para isso é:
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
huart→ normalmente&huart2.pData→ ponteiro para o buffer onde os dados recebidos serão armazenados.Size→ quantidade de bytes a receber.Timeout→ tempo máximo de espera em milissegundos.
Se os Size bytes chegarem antes do timeout, a função retorna HAL_OK.
Se o tempo estourar, retorna HAL_TIMEOUT.
Importante: Em polling, o processador fica parado esperando a UART. Então essa abordagem é boa para testes simples, menus que só fazem algo quando o usuário digita, ou para sistemas muito pequenos. Para aplicações mais reativas, usaremos interrupção (próxima seção).
4.1. Exemplo simples: eco de um único caractere
Vamos fazer o clássico echo: tudo o que chega é enviado de volta. Isso ajuda a testar se TX e RX estão OK.
No main.c:
#include "main.h"
#include "usart.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
uint8_t rxByte;
// Mensagem inicial
uint8_t msg[] = "Digite qualquer coisa: eu irei ecoar.\r\n";
HAL_UART_Transmit(&huart2, msg, sizeof(msg) - 1, HAL_MAX_DELAY);
while (1)
{
// Espera um byte chegar (sem limite de tempo)
if (HAL_UART_Receive(&huart2, &rxByte, 1, HAL_MAX_DELAY) == HAL_OK)
{
// Ecoa o mesmo byte de volta
HAL_UART_Transmit(&huart2, &rxByte, 1, HAL_MAX_DELAY);
}
}
}
Abra o terminal serial, digite caracteres: eles devem aparecer de volta na tela (às vezes duplicados visualmente se o terminal também ecoar localmente). Desative o local echo no terminal se quiser ver apenas o eco do microcontrolador.
4.2. Recebendo uma “linha” de texto até \r ou \n
Na prática, você geralmente quer receber comandos (por exemplo, LED ON, PWM 100, etc.), que chegam em forma de texto terminados com Enter (\r e/ou \n). Vamos montar um exemplo simples que:
- Lê caracteres um a um.
- Guarda em um buffer.
- Quando encontrar
\rou\n, considera que a “linha” terminou. - Ecoa a linha completa e limpa o buffer.
#include "main.h"
#include "usart.h"
#include <string.h>
#define RX_LINE_BUFFER_SIZE 64
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
uint8_t rxByte;
uint8_t rxLine[RX_LINE_BUFFER_SIZE];
uint16_t rxIndex = 0;
const char *startMsg = "Digite um comando e pressione ENTER:\r\n";
HAL_UART_Transmit(&huart2, (uint8_t *)startMsg, strlen(startMsg), HAL_MAX_DELAY);
while (1)
{
// Aguarda um byte (sem timeout, bloqueante)
if (HAL_UART_Receive(&huart2, &rxByte, 1, HAL_MAX_DELAY) == HAL_OK)
{
// Se receber \r ou \n, considera fim de linha
if (rxByte == '\r' || rxByte == '\n')
{
// Fecha a string
rxLine[rxIndex] = '\0';
// Se houver algo no buffer, processa
if (rxIndex > 0)
{
const char *prefix = "\r\nVoce digitou: ";
HAL_UART_Transmit(&huart2,
(uint8_t *)prefix,
strlen(prefix),
HAL_MAX_DELAY);
HAL_UART_Transmit(&huart2,
rxLine,
rxIndex,
HAL_MAX_DELAY);
const char *suffix = "\r\nDigite outro comando:\r\n";
HAL_UART_Transmit(&huart2,
(uint8_t *)suffix,
strlen(suffix),
HAL_MAX_DELAY);
// Aqui é onde você chamaria uma função para interpretar o comando:
// Processar_Comando((char *)rxLine);
// Limpa índice para próxima linha
rxIndex = 0;
}
}
else
{
// Armazena o byte no buffer se há espaço
if (rxIndex < RX_LINE_BUFFER_SIZE - 1)
{
rxLine[rxIndex++] = rxByte;
// (Opcional) ecoa o caractere para feedback imediato
HAL_UART_Transmit(&huart2, &rxByte, 1, HAL_MAX_DELAY);
}
else
{
// Buffer cheio: descarta e avisa
const char *overflowMsg =
"\r\n[ERRO] Linha muito longa, descartada.\r\n";
HAL_UART_Transmit(&huart2,
(uint8_t *)overflowMsg,
strlen(overflowMsg),
HAL_MAX_DELAY);
rxIndex = 0;
}
}
}
}
}
Esse padrão (buffer de linha + fim em \r/\n) é exatamente a base de interfaces de comando em UART (menus de terminal, CLI serial, comandos AT etc.).
4.3. Usando timeout para não travar o sistema
O uso de HAL_MAX_DELAY torna a função bloqueante “para sempre”, o que pode ser ruim se o seu programa tiver outras tarefas importantes para fazer em paralelo.
Você pode usar um timeout finito, por exemplo 10 ms, e testar o retorno:
uint8_t rxByte;
HAL_StatusTypeDef status = HAL_UART_Receive(&huart2,
&rxByte,
1,
10); // 10 ms
if (status == HAL_OK)
{
// Chegou algum dado
HAL_UART_Transmit(&huart2, &rxByte, 1, HAL_MAX_DELAY);
}
else if (status == HAL_TIMEOUT)
{
// Não chegou nada nesse intervalo; toque a vida normalmente
// Atualize sensores, rode outras tarefas, etc.
}
else
{
// Outros erros de UART (framing, overrun, etc.)
const char *err = "[ERRO] UART Receive falhou.\r\n";
HAL_UART_Transmit(&huart2, (uint8_t *)err, strlen(err), HAL_MAX_DELAY);
}
Esse esquema já melhora a responsividade: o laço principal não fica parado indefinidamente esperando dados.
4.4. Limitações do polling e motivação para interrupções
Problemas do polling puro:
- Se você usar
HAL_MAX_DELAY, o loop principal só “acorda” quando chega dado. Ótimo para aplicações tipo “terminal interativo” simples, mas ruim quando há outras tarefas a executar (por exemplo, ler sensores, controlar motores). - Mesmo com timeouts pequenos, você fica chamando
HAL_UART_Receiverepetidamente. Isso consome CPU sem necessidade se os dados chegam esporadicamente. - Se vários bytes chegam enquanto o código está fazendo outra coisa (sem chamar
HAL_UART_Receive), você pode ter overrun ou perda de dados se não tratar corretamente.
Por isso, para aplicações robustas, é comum usar:
- Recepção por interrupção (
HAL_UART_Receive_IT) com um buffer de 1 byte que é realimentado a cada callback, ou - Recepção por DMA em buffers circulares, dependendo do volume de dados.
Essas estratégias são justamente o núcleo do artigo de referência da ST e serão a base da próxima seção.
5. Recebendo dados pela UART com interrupção (HAL_UART_Receive_IT)
Agora vamos para o “jeito certo” de receber dados em aplicações reais: interrupção.
A ideia é simples e poderosa:
- A UART gera uma interrupção sempre que um byte chega.
- No callback da HAL, você:
- Lê esse byte;
- Armazena num buffer maior;
- Toma alguma decisão (ecoar, montar comando, etc.);
- Agenda a próxima recepção com
HAL_UART_Receive_IT.
Assim, a CPU fica livre para fazer outras coisas no while(1) e só é interrompida quando realmente chega dado na UART.
5.1. Estrutura básica da recepção por interrupção
Vamos montar uma estrutura típica:
- Um buffer de 1 byte para a chamada de
HAL_UART_Receive_IT. - Um buffer maior de linha ou de comando.
- Flags e índices globais para sinalizar “linha pronta”.
No main.c ou em um módulo serial.c:
#include "usart.h"
#include <string.h>
#define RX_BUFFER_SIZE 64
// Byte único usado pela HAL para recepção via IT
uint8_t uartRxByte;
// Buffer maior para montar uma linha/comando
uint8_t uartRxBuffer[RX_BUFFER_SIZE];
volatile uint16_t uartRxIndex = 0;
volatile uint8_t uartLineReady = 0; // flag de "linha completa"
5.2. Iniciando a recepção no main
No main() depois de inicializar tudo, iniciamos uma primeira recepção de 1 byte:
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
const char *startMsg = "UART RX por interrupcao iniciada.\r\n";
HAL_UART_Transmit(&huart2,
(uint8_t *)startMsg,
strlen(startMsg),
HAL_MAX_DELAY);
// Inicia recepcao de 1 byte via interrupcao
HAL_UART_Receive_IT(&huart2, &uartRxByte, 1);
while (1)
{
// Se uma linha completa foi recebida:
if (uartLineReady)
{
uartLineReady = 0;
// Garante que o buffer termine em '\0'
uartRxBuffer[uartRxIndex] = '\0';
const char *prefix = "\r\n[IT] Linha recebida: ";
HAL_UART_Transmit(&huart2,
(uint8_t *)prefix,
strlen(prefix),
HAL_MAX_DELAY);
HAL_UART_Transmit(&huart2,
uartRxBuffer,
uartRxIndex,
HAL_MAX_DELAY);
const char *suffix = "\r\nDigite outra linha:\r\n";
HAL_UART_Transmit(&huart2,
(uint8_t *)suffix,
strlen(suffix),
HAL_MAX_DELAY);
// Aqui você chamaria o parser de comando:
// Processar_Comando((char *)uartRxBuffer);
// Zera índice para próxima linha
uartRxIndex = 0;
}
// Seu código principal roda livremente aqui:
// - Atualizar sensores
// - Controlar motores
// - Ler outros periféricos, etc.
}
}
Repare que o while(1) não chama nenhuma função de recepção.
A recepção real acontece no callback de interrupção, que veremos agora.
5.3. Callback HAL_UART_RxCpltCallback: núcleo da lógica de RX
A HAL chama automaticamente HAL_UART_RxCpltCallback() quando a recepção iniciada por HAL_UART_Receive_IT termina (no nosso caso, sempre que chega 1 byte).
Implemente esse callback em stm32f4xx_it.c ou em main.c (onde preferir, desde que tenha o protótipo certo):
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
uint8_t received = uartRxByte;
// Exemplo opcional: eco imediato do caracter
HAL_UART_Transmit(&huart2, &received, 1, HAL_MAX_DELAY);
// Verifica se é fim de linha (ENTER)
if (received == '\r' || received == '\n')
{
// Marca que uma linha completa foi recebida
if (uartRxIndex > 0) // ignora ENTERs vazios
{
uartLineReady = 1;
}
}
else
{
// Armazena no buffer se ainda houver espaço
if (uartRxIndex < RX_BUFFER_SIZE - 1)
{
uartRxBuffer[uartRxIndex++] = received;
}
else
{
// Buffer lotado: descarta e sinaliza erro (opcional)
const char *overflowMsg =
"\r\n[ERRO] Buffer RX lotado, descartando.\r\n";
HAL_UART_Transmit(&huart2,
(uint8_t *)overflowMsg,
strlen(overflowMsg),
HAL_MAX_DELAY);
uartRxIndex = 0;
}
}
// Reagenda a recepcao de mais 1 byte
HAL_UART_Receive_IT(&huart2, &uartRxByte, 1);
}
}
Pontos críticos:
- Sempre reagendar a recepção ao final do callback:
HAL_UART_Receive_IT(&huart2, &uartRxByte, 1);Se esquecer essa linha, você recebe apenas o primeiro byte e depois nunca mais. - Nunca faça um loop demorado dentro do callback.
O callback deve ser curto e objetivo: copiar o byte, atualizar índice/flags e reagendar RX. - A flag
uartLineReadyé lida nowhile(1)e pode sinalizar que algo está pronto para processamento mais pesado fora da interrupção.
5.4. Tratando erros de UART com callback de erro (opcional mas recomendado)
Além de RxCplt, a HAL também chama HAL_UART_ErrorCallback em caso de:
- Overrun (chega outro byte antes de você ler o anterior);
- Framing error (problemas de sincronismo);
- Noise etc.
Podemos colocar algo simples:
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
const char *errMsg = "\r\n[ERRO] UART2 - ErrorCallback.\r\n";
HAL_UART_Transmit(&huart2,
(uint8_t *)errMsg,
strlen(errMsg),
HAL_MAX_DELAY);
// Limpa estados de recepcao, se for o caso
uartRxIndex = 0;
uartLineReady = 0;
// Reagenda recepcao
HAL_UART_Receive_IT(&huart2, &uartRxByte, 1);
}
}
Isso ajuda a não “morrer” silenciosamente em casos de erro.
5.5. Interpretando comandos recebidos (exemplo simples)
Vamos supor que você queira três comandos via UART:
LED ON→ acende um LED na placa.LED OFF→ apaga o LED.STATUS→ imprime uma mensagem dizendo se o LED está ligado ou apagado.
Crie uma função simples de parser, por exemplo em main.c:
void Processar_Comando(const char *cmd)
{
if (strcmp(cmd, "LED ON") == 0)
{
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
const char *resp = "\r\n[OK] LED ligado.\r\n";
HAL_UART_Transmit(&huart2, (uint8_t *)resp, strlen(resp), HAL_MAX_DELAY);
}
else if (strcmp(cmd, "LED OFF") == 0)
{
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
const char *resp = "\r\n[OK] LED desligado.\r\n";
HAL_UART_Transmit(&huart2, (uint8_t *)resp, strlen(resp), HAL_MAX_DELAY);
}
else if (strcmp(cmd, "STATUS") == 0)
{
GPIO_PinState state = HAL_GPIO_ReadPin(LD2_GPIO_Port, LD2_Pin);
const char *respOn = "\r\n[STATUS] LED esta LIGADO.\r\n";
const char *respOff = "\r\n[STATUS] LED esta DESLIGADO.\r\n";
if (state == GPIO_PIN_SET)
HAL_UART_Transmit(&huart2, (uint8_t *)respOn, strlen(respOn), HAL_MAX_DELAY);
else
HAL_UART_Transmit(&huart2, (uint8_t *)respOff, strlen(respOff), HAL_MAX_DELAY);
}
else
{
const char *resp = "\r\n[ERRO] Comando desconhecido.\r\n";
HAL_UART_Transmit(&huart2, (uint8_t *)resp, strlen(resp), HAL_MAX_DELAY);
}
}
E no while(1) (reaproveitando o código da seção 5.2):
if (uartLineReady)
{
uartLineReady = 0;
uartRxBuffer[uartRxIndex] = '\0';
// Chama o parser
Processar_Comando((char *)uartRxBuffer);
// Redefine para próxima linha
uartRxIndex = 0;
}
Agora você tem um mini “terminal de comandos” via UART, completamente assíncrono:
- O microcontrolador pode rodar outras tarefas no
while(1). - Os bytes são recebidos em background por interrupção.
- Quando uma linha é fechada com ENTER, o comando é interpretado.
Ponto para inserir imagem 10: terminal mostrando
LED ON,LED OFF,STATUSe respostas.
5.6. Resumo do fluxo RX por interrupção
- Configura USART2 no CubeMX (TX/RX, NVIC habilitado).
- Gera o código, verifica
MX_USART2_UART_Init(). - Cria:
uartRxByte(buffer de 1 byte);uartRxBuffer[](buffer maior);- Índice
uartRxIndexe flaguartLineReady.
- Inicia
HAL_UART_Receive_IT(&huart2, &uartRxByte, 1);nomain(). - Implementa
HAL_UART_RxCpltCallback:- Copia
uartRxByteparauartRxBuffer; - Detecta
\r/\n; - Atualiza índice/flags;
- Reagenda
HAL_UART_Receive_IT(...).
- Copia
- No
while(1), verificauartLineReadye processa o comando.
Com isso, você tem uma base sólida para qualquer protocolo simples baseado em texto ou mesmo binário (adaptando o parser).
6. Boas práticas, extensões e organização avançada para UART no STM32F411RE
Nesta seção final do tutorial, consolidaremos o conhecimento e ampliaremos para cenários reais de engenharia embarcada, abordando:
- Boas práticas essenciais para evitar perda de dados e travamentos.
- Quando usar DMA, buffers circulares (ring buffers) e filas (queues).
- Estratégias de modularização do código para projetos maiores.
- Cuidados elétricos e físicos ao trabalhar com UART.
- Finalização do tutorial.
6.1. Boas práticas essenciais
6.1.1. Reagendar sempre o HAL_UART_Receive_IT
Esse é o erro mais comum de iniciantes:
HAL_UART_Receive_IT(&huart2, &uartRxByte, 1);
Se faltar isso no final do callback, sua UART vai “parar de receber” após o primeiro byte.
No fluxo de recepção por interrupção, essa linha é obrigatória.
6.1.2. Mantenha o callback curto e livre de delays
Nunca faça:
HAL_Delay(100);
processamento_pesado();
calculo_matricial();
O callback deve simplesmente:
- ler o byte recebido,
- salvar no buffer,
- ajustar índices e flags,
- reagendar a recepção.
Todo o processamento pesado deve ocorrer no while(1) ou em tasks de RTOS.
6.1.3. Proteja buffers globais se houver múltiplos contextos
Se você estiver rodando FreeRTOS ou outro sistema multitarefa:
- Use mutex para escrever em buffers compartilhados.
- Ou use um queue para comunicação UART → tarefa de parser.
6.1.4. Use timeouts realistas nos modos bloqueantes
HAL_MAX_DELAY é aceitável para testes, mas ruim em aplicações reais.
Se você precisa manter o sistema reativo, use timeouts pequenos e teste o retorno.
6.1.5. Trate o callback de erro
Erros de framing, overrun e noise podem acontecer em ambientes ruidosos.
Tenha sempre:
void HAL_UART_ErrorCallback(...)
para reinicializar buffers e reagendar recepção.
6.2. Quando usar DMA e buffers circulares (ring buffers)
6.2.1. DMA: ideal para fluxos contínuos e altos volumes
A DMA libera totalmente a CPU durante a recepção ou transmissão, útil quando:
- Recebe grandes volumes de dados (por exemplo, GPS NMEA, telemetria, logs extensos).
- Envia pacotes longos com alta frequência.
- Há risco de perda de bytes se o callback de interrupção for lento.
Com DMA, você pode usar double buffering ou ring buffers para processar dados de forma eficiente.
6.2.2. Buffer circular (ring buffer)
Para RX contínuo, uma abordagem profissional é:
- Criar um buffer circular grande (por exemplo, 256 ou 512 bytes).
- A DMA grava nele continuamente.
- Um ponteiro de leitura (
readIndex) processa os dados conforme chegam. - A DMA apenas avança o ponteiro de escrita (
writeIndex).
Essa técnica é usada em sistemas onde a UART não pode perder nenhum dado.
6.2.3. Comparação dos métodos de RX
| Método | Vantagens | Desvantagens |
|---|---|---|
| Polling | Simples; ideal para menus | Travamento do CPU; risco de perda de dados |
| Interrupção (IT) | Rápido; seguro; ideal para comandos | Requer cuidado com buffers e callbacks |
| DMA | Máxima eficiência; ideal para grandes fluxos | Mais complexo; exige ring buffer |
6.3. Organização modular do código
Em projetos maiores, não é prático manter todo o código UART em main.c.
A recomendação é separar em módulos:
Core/Src/usart.c ← inicialização gerada pela HAL
Core/Inc/usart.h
Core/Src/serial.c ← funções Serial_SendString, Processar_Comando, callbacks
Core/Inc/serial.h
Estrutura sugerida no serial.h
#ifndef SERIAL_H
#define SERIAL_H
#include <stdint.h>
void Serial_Init(void);
void Serial_SendString(const char *str);
void Serial_Process(void); // chamada no while(1)
void Processar_Comando(const char *cmd);
#endif
No serial.c
- Implementa as funções de transmissão.
- Implementa o callback
HAL_UART_RxCpltCallback. - Implementa parsers e tratamento de erros.
No main.c
Fica apenas:
Serial_Init();
while (1)
{
Serial_Process();
// outras tarefas
}
Essa abordagem mantém o código do projeto limpo e facilita testes unitários e reutilização.
6.4. Cuidados elétricos ao usar UART externamente
Embora a Nucleo tenha conversão USB ↔ UART embutida, quando você usar UART em conectores externos, observe:
6.4.1. Tensão dos sinais
O STM32F411RE opera em 3.3V.
- Nunca conecte diretamente uma UART em 5V.
- Use conversores de nível (level shifters) se necessário.
6.4.2. GND comum
Sempre conecte o GND do dispositivo externo ao GND do STM32.
6.4.3. Comprimento dos cabos
Cabos muito longos (>2m) podem introduzir:
- Ruídos
- Cross-talk
- Desalinhamento de clock serial
Para cabos longos, considere RS485.
6.4.4. Ruído e indução
Ambientes industriais exigem:
- Cabos blindados
- Filtros EMI
- Evitar passar UART próximo a motores e trilhos de potência
6.5. Checklist final do UART para STM32
✔ UART configurada no CubeMX
✔ Interrupção habilitada na NVIC
✔ Buffer de 1 byte para RX via IT
✔ Reagendar recepção automaticamente
✔ Parser de comandos isolado
✔ Código modularizado
✔ Testado com terminal serial no PC
Com isso, você tem uma UART profissional, robusta e escalável.