MCU & FPGA STM32 Como Ler e Escrever Dados pela UART no STM32F411RE: Guia Completo com STM32CubeMX

Como Ler e Escrever Dados pela UART no STM32F411RE: Guia Completo com STM32CubeMX

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

  1. Abra o STM32CubeIDE e crie um novo projeto para a NUCLEO-F411RE.
  2. Na aba Pinout & Configuration, localize a categoria ConnectivityUSART2.
  3. 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:

  1. No painel da USART2, clique na aba NVIC Settings.
  2. Marque: USART2 global interrupt.
  3. 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:

  1. No menu Clock Configuration:
    • APB1 esteja configurado para 50 MHz ou 42 MHz (valores usuais no F411).
  2. 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:

  1. Clique em Project Manager → confirme o nome do projeto.
  2. Selecione HAL como driver.
  3. Clique em GENERATE CODE.

O CubeIDE criará arquivos importantes:

  • usart.c → contém MX_USART2_UART_Init(), definições do handle huart2, etc.
  • usart.h → exposições das funções e do handle.
  • Inicialização da UART será chamada automaticamente dentro de main.c pela função MX_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 Timeout estourou.
  • 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 callback HAL_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(...) - 1 toda 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_IT novamente enquanto txBusy == 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:

  1. Lê caracteres um a um.
  2. Guarda em um buffer.
  3. Quando encontrar \r ou \n, considera que a “linha” terminou.
  4. 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_Receive repetidamente. 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:

  1. 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.
  2. 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.
  3. A flag uartLineReady é lida no while(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, STATUS e respostas.


5.6. Resumo do fluxo RX por interrupção

  1. Configura USART2 no CubeMX (TX/RX, NVIC habilitado).
  2. Gera o código, verifica MX_USART2_UART_Init().
  3. Cria:
    • uartRxByte (buffer de 1 byte);
    • uartRxBuffer[] (buffer maior);
    • Índice uartRxIndex e flag uartLineReady.
  4. Inicia HAL_UART_Receive_IT(&huart2, &uartRxByte, 1); no main().
  5. Implementa HAL_UART_RxCpltCallback:
    • Copia uartRxByte para uartRxBuffer;
    • Detecta \r/\n;
    • Atualiza índice/flags;
    • Reagenda HAL_UART_Receive_IT(...).
  6. No while(1), verifica uartLineReady e 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étodoVantagensDesvantagens
PollingSimples; ideal para menusTravamento do CPU; risco de perda de dados
Interrupção (IT)Rápido; seguro; ideal para comandosRequer cuidado com buffers e callbacks
DMAMáxima eficiência; ideal para grandes fluxosMais 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.cinicialização gerada pela HAL
Core/Inc/usart.h

Core/Src/serial.cfunçõ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.

0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

Como Funcionam os Fuse Bits (Option Bytes) nos Microcontroladores STM32: Parametrização e Segurança do FirmwareComo Funcionam os Fuse Bits (Option Bytes) nos Microcontroladores STM32: Parametrização e Segurança do Firmware

Os microcontroladores STM32 utilizam Option Bytes como um sistema equivalente aos fuse bits tradicionais, permitindo configurar parâmetros essenciais de operação e segurança do firmware. Esses recursos controlam proteção de leitura,

0
Adoraria saber sua opinião, comente.x