Este documento detalha a implementação de drivers para telas seriais TJC (Touch Job Control) utilizando o framework HAL (Hardware Abstraction Layer) do STM32. A solução emprega técnicas avançadas como buffers circulares, DMA e a funcionalidade de interrupção Idle para otimizar a comunicação serial.
- Arquitetura Geral
A arquitetura foi projetada para oferecer um manuseio eficiente tanto da recepção quanto da transmissão de dados, priorizando a não-bloqueio e a minimização do uso da CPU.
Fluxo de Recepção
A recepção de dados utiliza o modo DMA Circular em conjunto com a interrupção Idle do UART. Quando o barramento UART fica ocioso (indicando o fim de uma trama), um callback é acionado. Este callback apenas sinaliza a presença de novos dados para a aplicação de camada superior, que então os processa em sua própria lógica de loop.
Fluxo de Transmissão
A transmissão é realizada através de uma função de impressão formatada (similar a printf) que primeiro formata a string e anexa os terminadores específicos do protocolo TJC (0xFF 0xFF 0xFF). A transmissão em si é realizada de forma blouqeante usando HAL_UART_Transmit, o que é adequado para comandos de tela que geralmente são curtos e exigem confirmação.
Bufferização
Dois buffers circulares independentes são mantidos para cada interface UART (um para TJC e outro para BLE, por exemplo). Essa separação garante que a comunicação entre as diferentes portas seriais não interfira uma na outra.
- Estrutura de Dados do Buffer Circular
Para gerenciar os dados recebidos de forma eficiente, um buffer circular é empregado. A estrutura básica é definida da seguinte forma:
typedef struct {
volatile uint16_t read_ptr; // Ponteiro de leitura do software (head)
uint8_t buffer_data[BUFFER_SIZE]; // Área de dados real do buffer
} CircularBuffer_t;
// Instâncias dos buffers, alocadas em memória DMA para acesso otimizado
DMA_BUFFER CircularBuffer_t uart_rx_buffer;
DMA_BUFFER uint8_t tx_temp_buffer[TX_BUFFER_SIZE];
O campo read_ptr é marcado como volatile para assegurar que o compilador não aplique otimizações que possam levar a leituras desatualizadas por parte da aplicação.
- Funções Principais e Análise Detalhada
3.1. Inicialização (HMI_Init)
A função de inicialização configura os buffers e inicia a recepção DMA.
void HMI_Init(UART_HandleTypeDef* huart, CircularBuffer_t* rx_buffer) {
rx_buffer->read_ptr = 0; // Zera o ponteiro de leitura
// Inicia a recepção DMA no modo Idle, usando o buffer circular
HAL_UARTEx_ReceiveToIdle_DMA(huart, rx_buffer->buffer_data, BUFFER_SIZE);
// Desabilita interrupções de Half Transfer (HT) do DMA, pois são desnecessárias
// no modo circular e consomem recursos de CPU indevidamente.
__HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT);
}
Pontos Chave:
ReceiveToIdle_DMA: Preferível aoReceive_DMApadrão, pois só gera um evento ao final de uma trama de dados, adaptando-se a tamanhos de pacote variáveis.- Desabilitação de
DMA_IT_HT: A interrupção de transferência pela metade (Half Transfer) não traz benefícios no contexto de buffers circulares e é desativada para reduzir a sobrecarga do sistema. - Inicialização do buffer: Apenas o ponteiro de leitura é zerado. Os dados do buffer são continuamente sobrescritos pelo DMA, dispensando a necessidade de limpar a área de memória.
3.2. Contagem de Dados Disponíveis (HMI_Get_DataCount)
Calcula quantos bytes estão prontos para serem lidos no buffer circular.
uint16_t HMI_Get_DataCount(CircularBuffer_t* rx_buffer, UART_HandleTypeDef* huart) {
// O contador DMA (NDTR) representa os bytes *restantes* a serem transmitidos.
// Portanto, o ponteiro de escrita (write_ptr) é o tamanho total menos o contador.
uint16_t write_ptr = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// Cálculo do número de bytes disponíveis, tratando o caso de 'wrap-around' do buffer.
return (write_ptr - rx_buffer->read_ptr + BUFFER_SIZE) % BUFFER_SIZE;
}
A fórmula (write_ptr - read_ptr + BUFFER_SIZE) % BUFFER_SIZE é um padrão para calcular o espaço utilizável em buffers circulares, garantindo a correção mesmo quando write_ptr é menor que read_ptr (indicando que o buffer "deu a volta").
3.3. Leitura e Consumo de Dados (HMI_ConsumeData e HMI_PeekData)
Estas funções permitem que a aplicação acesse os dados no buffer sem a necessidade de cópias intermediárias.
HMI_ConsumeData(size): Simplesmente avança oread_ptrpelo número de bytes consumidos. Não envolve cópia de dados.HMI_PeekData(index): Retorna o byte na posiçãoindexa partir doread_ptratual, utilizando a lógica de endereçamento circular(read_ptr + index) % BUFFER_SIZE. Isso permite que a aplicação implemente máquinas de estado para analisar os pacotes diretamente no buffer DMA.
3.4. Envio de Dados (HMI_SendFormatted)
Responsável por formatar e enviar dados para a tela.
void HMI_SendFormatted(UART_HandleTypeDef* huart, const char* format_string, ...) {
// Seleciona o buffer de transmissão temporário baseado na instância do UART
uint8_t* tx_buffer_ptr = (huart->Instance == TJC_UART_INSTANCE) ? tx_buffer_tjc : tx_buffer_ble;
// Verifica se o UART já está transmitindo para evitar sobreposição de comandos
if (huart->gState == HAL_UART_STATE_BUSY_TX) {
return;
}
va_list arguments;
va_start(arguments, format_string);
// Formata a string, reservando espaço para os terminadores 0xFF 0xFF 0xFF
int bytes_written = vsnprintf((char*)tx_buffer_ptr, TX_BUFFER_SIZE - 3, format_string, arguments);
va_end(arguments);
// Verifica se a formatação foi bem-sucedida e se não excedeu o buffer
if (bytes_written <= 0) {
return;
}
if (bytes_written > TX_BUFFER_SIZE - 3) {
bytes_written = TX_BUFFER_SIZE - 3; // Trunca se for muito grande
}
// Anexa os terminadores do protocolo TJC
tx_buffer_ptr[bytes_written++] = 0xFF;
tx_buffer_ptr[bytes_written++] = 0xFF;
tx_buffer_ptr[bytes_written++] = 0xFF;
// Realiza a transmissão bloqueante
HAL_UART_Transmit(huart, tx_buffer_ptr, bytes_written, HAL_MAX_DELAY);
}
- Espaço para Terminadores: O buffer temporário de transmissão tem espaço reservado para garantir que os
0xFFfinais sempre caibam. - Proteção contra Reentrada: A verificação
HAL_UART_STATE_BUSY_TXprevine que comandos sejam enviados em sequência rápida demais, o que poderia corromper a comunicação. - Suporte a Ponto Flutuante: Para utilizar formatação de ponto flutuante (e.g.,
%f), pode ser necessário habilitar a opção-u _printf_floatnas configurações do compilador.
- Mecanismo de Callback de Recepção
O callback HAL_UARTEx_RxEventCallback é acionado quando a interrupção Idle ocorre.
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t received_size) {
// Apenas define um flag para indicar que novos dados estão disponíveis.
// O processamento real ocorre no loop principal da aplicação.
if (huart->Instance == TJC_UART_INSTANCE) {
tjc_data_ready_flag = 1;
} else if (huart->Instance == BLE_UART_INSTANCE) {
ble_data_ready_flag = 1;
}
// Reativa a recepção DMA Idle para continuar monitorando o barramento
HAL_UARTEx_ReceiveToIdle_DMA(huart, /* buffer, size */);
}
Este callback é deliberadamente simples, apenas atualizando um flag. A lógica de processamento dos dados é delegada ao loop principal da aplicação, promovendo um design mais limpo e desacoplado.
- Considerações Importantes
- Configuração CubeMX: No STM32CubeMX, configure o modo DMA do canal de recepção do UART para Circular. Caso contrário, será necessário gerenciar manualmente o início e o fim das transferências DMA.
- Tamanho do Buffer: Para desempenho ótimo, o
BUFFER_SIZEdo buffer circular deve ser uma potência de dois (e.g., 256, 512, 1024). - Gerenciamento do Ponteiro de Leitura: É crucial que a aplicação chame
HMI_ConsumeDataapós processar os dados recebidos. Falhar em fazer isso fará com que o buffer circular pareça "cheio" (read_ptrnão avança), resultando na perda de novos dados.