Watchdogs são fundamentais em produtos, são eles que devem reiniciar o sistema a um estado conhecido na ocorrência de um comportamento anômalo. Entretanto, existe uma dificuldade considerável em fazer com que o sistema sempre seja reiniciado na ocorrência de travamentos e, no final das contas, os watchdogs são difíceis de usar e frequentemente vemos produtos congelando com seus watchdogs não atuando. Neste post eu vou apresentar a maneira correta de se programar um bom watchdog e, inclusive, vou fornecer o código da minha biblioteca de watchdog. Basta copiar e usar!
Como não fazer com Watchdog
- Servir o watchdog numa interrupção de timer: se a execução do código entrar em loop ou deadlock o watchdog continuará a ser alimentado, pois as interrupções continuarão ocorrendo;
- Servir várias vezes o watchdog dentro do código: quanto mais vezes a função que serve o watchdog for chamada no código, maior a probabilidade de se ter uma delas na fila de execução quando o código trancar em loop;
- Colocar as rotinas de delay para servir o watchdog: pelo mesmo motivo do item 2, essas rotinas são usadas largamente e provavelmente serão usadas por um software rodando em loop também.
O jeito certo
Primeiro a biblioteca de watchdog deve ser desenvolvida de maneira a fornecer um watchdog de software a cada uma das diferentes tarefas do firmware (execução da main é uma tarefa, execução em interrupção de timer é outra tarefa, assim como execução em interrupção de ethernet, e etc.), então cada tarefa terá seu próprio watchdog simulado em software.
Para implementar a solução de diversos watchdogs em software, a biblioteca do watchdog fornece as seguintes funções:
/* Abre id: Se o id não for atualizado em timeoutMs ms a partir da chamada, o watchdog de hardware não será servido. */ void wdOpen(enum eWdClient id, uint32_t timeoutMs); /* Serve o watchdog de software id */ void wdRefresh(enum eWdClient id); /* Fecha o watchdog de software id, agora não é mais necessário servir este watchdog */ void wdClose(enum eWdClient id);
Com essas três funções fica fácil usar o watchdog em diferentes tarefas. Na main, configura-se o timeout para o tempo suficiente (5 segundos, no caso) e chama-se apenas uma vez a função de refresh, que fica no início do loop infinito e, preferencialmente, logo antes da interface do usuário, pois um dos sintomas de um código congelado é a interface travada. Tendo o watchdog da main servido junto à interface garante que se a interface não rodar, o watchdog também não será servido e o sistema será reiniciado.
...
wdOpen(wdMain, 5000);
while (1)
{
wdRefresh(wdMain);
ifPeriodicHook(); /* Call UI processor */
...
}
A main não deve usar a função wdClose, pois nunca para de executar. Ao contrário das demais tarefas que interrompem a execução, fazendo suas funções e depois se cessando, ou seja, rodam em interrupções. Para essas fica necessário usar a função wdClose logo antes da interrupção retornar, como mostra o snippet abaixo. No início da execução da interrupção wdOpen é usado para ativar o watchdog wdEthernet a um timeout de 1 segundo. Com isso, dentro de 1 segundo a função wdClose ou wdRefresh deve ser chamada para que o watchdog não dispare o sinal de reiniciar o sistema. No caso ilustrado o watchdog wdEthernet não é servido, apenas aberto e fechado.
void ethernetTcpCallback(void) //Called by the Ethernet ISR
{
wdOpen(wdEthernet, 1000);
... /* Do Tcp Stuff within 1 secound */
wdClose(wdEthernet);
}
Na verdade, apenas o watchdog virtual da função main seria o suficiente para garantir a eficácia da atuação do watchdog. Entretanto, frequentemente precisamos colocar timeouts na ordem de diversos segundos na main, e usando watchdogs distribuídos por tarefas atinge-se menor tempo de resposta na ocorrência de travamentos.
Quando se desenvolve uma biblioteca bem estruturada ela fica simples de usar, como os snippets mostram. Por trás dessas funções a biblioteca executa o seu algoritmo no seu próprio escopo eliminando complexidades das camadas superiores do software. Inserir esses níveis de abstração no código é sempre uma excelente prática de programação de sistemas embarcados.
Por fim, vimos que as funções wdOpen, wdRefresh e wdClose são usadas para controlar o watchdog simulado em software. Entretanto, o verdadeiro watchdog é um só, o de hardware, e que geralmente precisa ser servido a uma periodicidade na ordem de centenas de milésimos de segundos.
Para isso torna-se necessário usar uma função que verifica se algum dos watchdogs simulados em software extrapolou o tempo configurado e, por seguinte, sirva o watchdog de hardware. Essa função precisa garantir latência e deve ser chamada por uma interrupção de alta prioridade de timer. Abaixo o algoritmo da função.
void wdPeriodicHook(void) /* Must be called frequent */
{
if (!gWdClientStatus.wdEnabled)
return;
if (gWdClientStatus.wdEthernetEnabled &&
timeoutCheck_ms(gWdClientStatus.wdEthernetToutMs,
gWdClientStatus.wdEthernetToutSeed))
return;
if (gWdClientStatus.wdMainEnabled &&
timeoutCheck_ms(gWdClientStatus.wdMainToutMs,
gWdClientStatus.wdMainToutSeed))
return;
_WdHwToggle(); /* Feed the real watchdog */
}
Como eu havia prometido, abaixo segue o código completo da biblioteca. Sintam-se convidados para usá-la da maneira que lhes convir.
watchdog.h
#ifndef WATCHDOG_H
#define WATCHDOG_H
enum eWdClient {
wdGlobal=1,
wdMain,
wdEthernet
};
void wdOpen(enum eWdClient id, uint32_t timeoutMs);
void wdRefresh(enum eWdClient id);
void wdClose(enum eWdClient id);
void wdPeriodicHook(void);
void wdInit(void);
#endif //WATCHDOG_H
watchdog.c
static void _WdHwInit(void)
{
...
}
static void _WdHwToggle(void)
{
...
}
struct sWdClientStatus
{
uint8_t wdMainEnabled;
uint32_t wdMainToutSeed;
uint32_t wdMainToutMs;
uint8_t wdEthernetEnabled;
uint32_t wdEthernetToutSeed;
uint32_t wdEthernetToutMs;
uint8_t wdEnabled;
};
static struct sWdClientStatus gWdClientStatus;
void wdOpen(enum eWdClient id, uint32_t timeoutMs)
{
switch (id)
{
case wdGlobal:
gWdClientStatus.wdEnabled = 1;
break;
case wdMain:
gWdClientStatus.wdMainEnabled = 1;
gWdClientStatus.wdMainToutSeed = timeoutInit();
gWdClientStatus.wdMainToutMs = timeoutMs;
break;
case wdEthernet:
gWdClientStatus.wdEthernetEnabled = 1;
gWdClientStatus.wdEthernetToutSeed = timeoutInit();
gWdClientStatus.wdEthernetToutMs = timeoutMs;
break;
}
}
void wdRefresh(enum eWdClient id)
{
switch (id)
{
case wdMain:
gWdClientStatus.wdMainToutSeed = timeoutInit();
break;
case wdEthernet:
gWdClientStatus.wdEthernetToutSeed = timeoutInit();
break;
}
}
void wdClose(enum eWdClient id)
{
switch (id)
{
case wdGlobal:
gWdClientStatus.wdEnabled = 0;
break;
case wdMain:
gWdClientStatus.wdMainEnabled = 0;
gWdClientStatus.wdMainToutSeed = 0;
gWdClientStatus.wdMainToutMs = 0;
break;
case wdEthernet:
gWdClientStatus.wdEthernetEnabled = 0;
gWdClientStatus.wdEthernetToutSeed = 0;
gWdClientStatus.wdEthernetToutMs = 0;
break;
}
}
void wdPeriodicHook(void)
{
if (!gWdClientStatus.wdEnabled)
return;
if (gWdClientStatus.wdEthernetEnabled &&
timeoutCheck_ms(gWdClientStatus.wdEthernetToutMs,
gWdClientStatus.wdEthernetToutSeed))
return;
if (gWdClientStatus.wdMainEnabled &&
timeoutCheck_ms(gWdClientStatus.wdMainToutMs,
gWdClientStatus.wdMainToutSeed))
return;
_WdHwToggle();
}
void wdInit(void) {
gWdClientStatus.wdEnabled = 1;
gWdClientStatus.wdEthernetEnabled = 0;
gWdClientStatus.wdMainEnabled = 0;
_WdHwInit();
}
Conclusão
Neste post foi apresentado a maneira adequada de se utilizar watchdogs. Como muitas vezes pudemos descobrir, geralmente da pior maneira, que apenas o watchdog real, o de hardware, não é suficiente para que o sistema sempre seja reiniciado quando trancar e, assim, torna-se necessário uma solução criativa e efetiva como a apresentada.





Olá Felipe, essa sua biblioteca de Watchdog é ótima, está salvando meu projeto! Porém estou com algumas dúvidas em relação as funções “timeoutInit()” e “timeoutCheck_ms(…,…)”. Como não entendi a forma de implementá-las, fiz umas alterações e gostaria que você me desse sua opinião a respeito. Estou usando o TI-RTOS, da Texas em um placa Tiva C Connected Launchpad. A princípio está tudo funcionando, mas fiquei na dúvida se é ‘sorte’ ou a bliblioteca está trabalhando como a sua original. As alterações que fiz são essas: void wdOpen(enum eWdClient id, uint32_t timeoutMs) { switch (id) { case wdGlobal: gWdClientStatus.wdEnabled = 1;… Leia mais »