Introdução ao Cenário de Comunicação Serial
Em sistemas embarcados, é comum enviar comandos através de uma enterface serial e aguardar por uma resposta. Embora o uso de vTaskDelay() para inserir um atraso fixo possa parecer simples, ele apresenta limitações importantes que comprometem a eficiência e a confiabilidade do sistema.
Desvantagens do Uso de vTaskDelay()
Utilizar vTaskDelay() para sincronizar comandos e respostas seriais resulta em várias ineficiências:
- Tempo de resposta ineficiente: O atraso é fixo, mesmo que a resposta chegue rapidamente.
- Latência desnecessária: Se a resposta chegar durante o período de espera, o processamento é adiado até o final do atraso.
- Falta de adaptabilidade: Condições de rede ou carga do sistema podem variar, tornando um atraso fixo inadequado.
- Uso subótimo da CPU: Durante o atraso, a tarefa fica bloqueada, permitindo que outras tarefas exceutem, mas mecanismos de sincronização específicos são mais eficientes.
Exemplo de Código com vTaskDelay() (Não Recomendado)
void EnviarComandoEEsperaResposta(char *comando) {
TransmitirDadosSeriais(comando);
AguardarTarefa(pdMS_TO_TICKS(100));
if (VerificarDadosSeriais()) {
LerDadosSeriais(resposta);
}
}
Neste exemplo, a função aguarda 100 milissegundos independentemente do tempo real de resposta, o que pode levar a atrasos ou perda de dados.
Alternativas Recomendadas
Para uma comunicação serial mais robusta, é preferível utilizar mecanismos de sincronização do FreeRTOS, como filas, notificações de tarefa ou semáforos.
Alternativa 1: Uso de Filas (Mais Flexível)
Filas permitem a transferência de dados entre interrupções e tarefas, oferecendo controle de timeout preciso.
FilaHandle_t FilaRespostaSerial;
void InicializarComunicacaoSerial(void) {
FilaRespostaSerial = xQueueCreate(5, sizeof(DadosResposta));
ConfigurarInterrupcaoSerial(&huart, buffer_rx, 1);
}
int EnviarComandoComFila(char *comando, DadosResposta *resposta, uint32_t tempo_maximo_ms) {
DadosResposta temp;
TransmitirDadosSeriais(comando, strlen(comando));
if (xQueueReceive(FilaRespostaSerial, &temp, pdMS_TO_TICKS(tempo_maximo_ms)) == pdTRUE) {
memcpy(resposta, &temp, sizeof(DadosResposta));
return 0;
}
return -1;
}
void InterrupcaoSerial(void) {
DadosResposta dados_recebidos;
ColetarDadosRecebidos(&dados_recebidos);
BaseType_t xAcordouTarefa = pdFALSE;
xQueueSendFromISR(FilaRespostaSerial, &dados_recebidos, &xAcordouTarefa);
portYIELD_FROM_ISR(xAcordouTarefa);
}
Alternativa 2: Uso de Notificações de Tarefa (Mais Rápido)
Notificações são leves e ideais para sinalizar eventos simples, como a chegada de dados.
int EnviarComandoComNotificacao(char *comando, uint8_t *buffer_resposta, uint32_t tempo_maximo_ms) {
TransmitirDadosSeriais(comando, strlen(comando));
uint32_t valor_notificacao;
if (xTaskNotifyWait(0, ULONG_MAX, &valor_notificacao, pdMS_TO_TICKS(tempo_maximo_ms)) == pdTRUE) {
int tamanho = LerDadosSeriais(buffer_resposta);
return tamanho;
}
return -1;
}
void InterrupcaoSerial(void) {
TaskHandle_t tarefa_destino = xTaskGetHandle("Tarefa_Serial");
ColetarDadosRecebidos();
BaseType_t xAcordouTarefa = pdFALSE;
vTaskNotifyGiveFromISR(tarefa_destino, &xAcordouTarefa);
portYIELD_FROM_ISR(xAcordouTarefa);
}
Alternativa 3: Uso de Semáforos Binários (Cenários Simples)
Semáforos são adequados quando apenas a sinalização de um evento é necessária, sem transferência de dados.
SemaforoHandle_t SemaforoSerial;
void InicializarSemaforo(void) {
SemaforoSerial = xSemaphoreCreateBinary();
}
int EnviarComandoComSemaforo(char *comando, uint8_t *buffer_resposta, uint32_t tempo_maximo_ms) {
TransmitirDadosSeriais(comando, strlen(comando));
if (xSemaphoreTake(SemaforoSerial, pdMS_TO_TICKS(tempo_maximo_ms)) == pdTRUE) {
return LerDadosSeriais(buffer_resposta);
}
return -1;
}
void InterrupcaoSerial(void) {
ColetarDadosRecebidos();
BaseType_t xAcordouTarefa = pdFALSE;
xSemaphoreGiveFromISR(SemaforoSerial, &xAcordouTarefa);
portYIELD_FROM_ISR(xAcordouTarefa);
}
Comparação das Alternativas
| Método | Vantagens | Desvantagens | Cenário Ideal |
|---|---|---|---|
| vTaskDelay() | Implementação simples | Resposta lenta, imprecisa, desperdiça CPU | Apenas para testes ou aplicações não críticas |
| Filas | Transferência de dados, suporte a múltiplas mensagens | Maior consumo de memória | Comunicação complexa com necessidade de transferência de dados |
| Notificações de Tarefa | Alta eficiência (~45% mais rápido), baixo uso de memória | Limitada a uma única tarefa | Sinalização simples, comunicação um-para-um |
| Semáforos Binários | Simplicidade, uso moderado de recursos | Apenas sinalização, sem transferência de dados | Cenários onde basta saber que uma resposta foi recebida |
Exemplo Prático de Aplicação
typedef struct {
uint8_t comando;
uint8_t dados[256];
uint16_t comprimento;
} MensagemSerial;
void TarefaComunicacaoSerial(void *parametros) {
FilaHandle_t fila_rx = (FilaHandle_t)parametros;
MensagemSerial mensagem;
while (1) {
uint8_t consulta[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A};
TransmitirDadosSeriais(consulta, sizeof(consulta));
if (xQueueReceive(fila_rx, &mensagem, pdMS_TO_TICKS(500)) == pdTRUE) {
ProcessarResposta(&mensagem);
} else {
TratarTimeout();
}
AguardarTarefa(pdMS_TO_TICKS(1000));
}
}
Conclusão
Para o cenário de envio de comandos e espera por respostas seriais, é desaconselhável utilizar vTaskDelay(). Em vez disso, adote mecanismos de sincronização do FreeRTOS como filas, notificações de tarefa ou semáforos, que permitem controle preciso de timeout, resposta imediata, uso eficiente da CPU e melhor manutenibilidade do código.