Piscando o LED Usando Interrupção Com o Watchdog do Arduino

Sumário

Muito provavelmente o seu primeiro projeto com o Arduino foi fazer um LED piscar, um momento de realização após aprender vários conceitos e novos paradigmas. Circuitos elétricos, IDE, linguagem de programação, eletrônica digital e microcontroladores programáveis. 

Que tal repetir este processo, mas dessa vez, ao invés de aprender como mudar o nível lógico em uma porta digital e usar a função delay() para piscar um LED, usar outros conceitos também importantes: Interrupções e Watchdog?

Mas primeiro, vamos revisar os conceitos de interrupção e watchdog.

Interrupções

Vou te passar apenas um apanhado geral, suficiente para entender os princípios que quero demonstrar nesse post. Se eu tiver uma oportunidade, mais adiante eu dedicarei um post apenas para tratar deste tópico.

Pois bem, uma interrupção é um mecanismo que permite o microcontrolador responder a um evento o mais rápido possível, sem ter aguardar até um momento no código onde o programa verifica se o respectivo evento ocorreu.

Imagine por exemplo que o seu projeto tenha um botão e que o laço loop() é responsável por verificar se este botão foi pressionado para então acender ou apagar uma luz. Mas dentro desse mesmo loop o programa é responsável por mandar uma sequência de dados para um periférico, um processo que pode levar alguns milisegundos. Agora vamos dizer que o usuário pressiona o botão exatamente enquanto o microcontrolador está enviando os dados para o periférico. Há a possibilidade real de que o loop() não irá detectar o botão pressionado, fazendo com o usuário tenha que pressionar o botão outra vez. Isso diminui a experiência do usuário com o sistema, o que pode te custar algumas estrelas nas resenhas no site de vendas do seu produto.

Botão e interrupção
– Achei que tivesse pressionado o botão…

A interrupção, como o próprio nome diz, interrompe o microcontrolador dizendo: ocorreu um evento que precisa ser registrado imediatamente. Assim, no nosso exemplo, mesmo que o usuário pressione o botão num momento em que o sketch não está checando a porta, o evento não será perdido e o programa terá a oportunidade de responder conforme necessário.

O uso de interrupções não só é mais eficiente, como também permite liberar o processador para executar outras atividades enquanto espera que um evento ocorra. Notando que nesse contexto um evento pode ser tanto como um sinal lógico externo ocorrendo em algumas portas específicas do Arduino, ou eventos internos, como temporizadores, watchdog, comunicação serial, etc. Para os eventos externos a detecção pode ocorrer pela presença de um sinal lógico 0, 1 ou na ocorrência de  transições do sinal de 0 para 1 (rising), de 1 para 0 (fall). E só para mencionar, no Arduino existem 26 interrupções distintas, duas diretamente por hardware e as outras internas. Outras variantes dos chips AVR podem ter mais portas de hardware habilitadas para interrupções.

Watchdog no Arduino

Analogia dead man's switch
Dead man’s switch: granada explode se o herói morrer

Watchdog é uma funcionalidade disponível em praticamente todos os microcontroladores e permite que este seja reiniciado nos casos em que o programa trave ou entre em um loop infinito. E como detectar se o microcontrolador travou e não consegue rodar nada? Aqui vale mais uma analogia. Lembra nos filmes quando o herói, já nas últimas, está prestes a ser atacado, e tira o pino e segura uma granada, aguardando apenas a morte para levar os vilões junto com ele? Isto é o chamado ‘gatilho do homem morto‘ (em inglês “dead man’s switch”). Aqui o mesmo conceito se aplica. O watchdog é constituido de um oscilador e um contador que ao atingir um limite, envia um sinal de reset ao processador. A chave é incluir no sketch do seu projeto uma instrução para zerar esse contador antes do contador chegar nesse limite. Se por algum motivo isso não ocorrer, o watchdog é acionado, o que dependendo de como foi configurado pode reiniciar o microcontrolador, ou gerar um sinal interno de interrupção, ou os dois. Esse contador pode ser zerado a qualquer momento através da instrução assembly WDR. 

Isso é legal pois provê uma maneira de se recuperar de situações inesperadas como um crash do programa, ou um famigerado loop infinito. Mas é possível usar essa funcionalidade de outras formas também. Como a frequência do oscilador interno é conhecida (128KHz), e com o uso de alguns divisores internos (prescallers), é possível prever aproximadamente quanto tempo levará para o watchdog gerar a interrupção. Permitindo portanto utilizar o como um temporizador. Os possíveis valores para este temporizador são no ATMega 328p e 32u4: 16ms, 32ms, 64ms, 125ms, 250ms, 1s, 2s, 4s e 8s. Note que estes são valores aproximados pois este oscilador interno não é super-preciso.

Diagrama watchdog microcontrolador ATMega 328p
Diagrama watchdog microcontrolador ATMega 328p

Usando o Watchdog do Arduino Como um Temporizador

 

A interrupção interna do watchdog é a de número 7, no endereço 0x000C (WDT). Internamente esta interrupção pode ser referenciada através da macro ISR(WDT_vect) já pre-definida nas bibliotecas padrão da IDE Arduino.

Para usar o  watchdog do Arduino é preciso primeiro configurar o seu registrador de controle WDTCSR (Watchdog Timer Control Register) em uma sequencia específica. Isso provê uma camada adicional de segurança na tentativa de evitar que um código aleatório e descontrolado (o que pode acontecer em casos de corrupção de memória) gere acidentalmente uma sequência de instruções capaz de alterar o funcionamento do watchdog. 

As seguintes instruções devem ser executadas nesta ordem:

  1. Desabilitar as interrupções
  2. Resetar o watchdog através da instrução WDR
  3. Setar simultaneamente os bits WDCE e WDE do registrador WDTCSR
  4. Setar, dentro de 4 ciclos, os bits WDIE, WDP3, WDP2, WDP1 e WDP0 do registrador WDTCSR
  5. Habilitar as interrupções 

No datasheet do 328p estes bits são descritos como:

WDCE e WDE: Controlam a habilidade de habilitar as interrupções e setar o prescaller no próximo passo (dentro de 4 ciclos de instrução).

WDIE: Este bit setado indica que o timer do watchdog vai gerar uma interrupção ao invés de reiniciar o microcontrolador.

WDP3, WDP2, WDP1 e WDP0: Estes quatro bits definem o divisor de frequência do prescaller, e consequentemente o tempo para que um timeout do contador ocorra.

Um aspecto interessante do watchdog é que ele não para de funcionar quando o microcontrolador é colocado em modo sleep. Após o timeout é possível usar a interrupção para acordar o chip e continuar com a execução do programa. Isso permite uma redução significativa no consumo de eletricidade, algo importante se o seu projeto depende de baterias para sobreviver. Note que o tempo máximo para este temporizador é de 8 segundos. Mas mesmo que o seu projeto não precise executar uma ação a cada 8 segundo, você pode manter um contador em uma variável e executar a ação necessária após um certo número de ciclos dormir-acordar. Por exemplo se você precisa ler um sensor de temperatura a cada 5 minutos basta acordar cerca de 37 vezes (5×60/8 = 4,93 minutos). 

Sketch de Exemplo

Para demonstrar o uso do watchdog, criei o sketch a seguir para piscar um led uma vez por segundo. Para isso uso o watchdog para gerar uma interrupção a cada 0.5s  para trocar o valor lógico de uma variável. Esta variável é verificada dentro de loop() o qual liga ou desliga o LED.

O código começa por declarar uma macro para facilitar zerar o contador to watchdog usando a instrução assembly WDR. Opcionalmente se você quiser omitir esta declaração, basta incluir o cabeçalho  #include <avr/wdt.h>.

				
					#define wdt_reset() __asm__ __volatile__ ("wdr")
				
			

Também aproveito para definir as diferentes combinações possíveis dos bits do prescaller, dessa forma podemos referir às diferentes combinações por seus respectivos nomes. 

				
					// Timer prescalers available for the watchdog.
#define WDT_16MS	B01000000
#define WDT_32MS	B01000001
#define WDT_64MS	B01000010
#define WDT_125MS	B01000011
#define WDT_250MS	B01000100
#define WDT_500MS	B01000101
#define WDT_1S		B01000110
#define WDT_2S		B01000111
#define WDT_4S		B01100000
#define WDT_8S		B01100001
				
			

Algumas outras definições são necessárias. O LED está conectado ao pino 10 do Arduino Uno. Também defino uma variável to tipo volátil para indicar se o LED deve estar aceso ou apagado, bem como o protótipo de uma função auxiliar para inicializar o watchdog.

				
					// LED conectado no pino 10 através de um restor de 220 Ohms
#define LED_PIN 10

// Varável global acessada pela interrupção do watchdog
volatile bool _ledToggle = false;

// Declaração de funções auxiliares
void watchdogInit(uint8_t);

				
			

Em setup() eu apenas defino o pino do LED como de OUTPUT e inicializo o watchdog (veja como mais abaixo). Em loop() eu apenas checo o valor da variável _ledToggle e acendo ou apago o LED de acordo. Observe que ao contrário do código típico para piscar o LED, não faço uso da função delay().

				
					void setup()
{
	pinMode(LED_PIN, OUTPUT);
	watchdogInit(WDT_500MS);
}

void loop()
{
	// Enquanto a flag = true mantém o led aceso
	// Enquanto a flag = false mantém o led apagado
	// A flag troca de estado toda vez que houver um timeout
	// do temporizador do watchdog.
	if (_ledToggle) digitalWrite(LED_PIN, HIGH);
	else digitalWrite(LED_PIN, LOW);
}
				
			

Esse é o código executado quando a interrupção do watchdog é disparada. Deve-se manter o código de manipulação de interrupções o mais simples possível. Aqui eu apenas troco o valor lógico da variável e zero o watchdog, para que este possa começar outro ciclo de temporização.

				
					ISR(WDT_vect)
{
	_ledToggle = !_ledToggle;
	wdt_reset();
}

				
			

E finalmente, a função responsável por inicializar o watchdog na ordem que mencionei anteriormente, ou seja:

  1. Desabilitar as interrupções
  2. Resetar o watchdog através da instrução WDR
  3. Setar simultaneamente os bits WDCE e WDE do registrador WDTCSR
  4. Setar, dentro de 4 ciclos, os bits WDIEWDP3WDP2WDP1 e WDP0 do registrador WDTCSR
  5. Habilitar as interrupções
				
					void watchdogInit(byte timeout_mask = WDT_1S)
{
	cli();                  
	wdt_reset();
	WDTCSR |= B00011000;
	WDTCSR = timeout_mask;
	sei();
}
				
			

Resultados

O LED Piscando através do watchdog
O LED Piscando através do watchdog

Ao executar este sketch em um Arduino Uno, um LED propriamente conectado através de um resistor de 220Ω no pino 10 irá piscar a cada segundo, ficando meio segundo aceso e meio segundo apagado.

Simulação

Use o serviço de emulação de kits de microcontroladores provido pelo site wokwi.com para rodar este sketch. Clique na imagem abaixo para acessar um simulador do Arduino Uno executando o sketch 002_PiscarLED_watchdog.ino. No site, basta selecionar o botão de play () para executar o sketch.

Código Completo

Incluo o código completo a seguir para que você possa copiar e testar no seu Arduino. Também disponível no github: https://github.com/jjjowens/Arduando/blob/master/002_PiscarLED_watchdog/002_PiscarLED_watchdog.ino

				
					/**
 * Copyright (c) 2022 - James Owens <jjo(at)arduando.com.br>
 * 
 * Arquivo:     002_PiscarLED_watchdog.ino
 * Arquivo:     29/11/2022 15:24:06
 * Versão:      
 * Fonte:       https://github.com/jjjowens/Arduando/tree/master/002_PiscarLED_watchdog
 * Website:     https://arduando.com.br
 *
 * Descrição: Esta é uma variação do sketch básico de fazer um LED piscar.
 * No entanto nesta demonstração fazemos uso do recurso de watchdog
 * incluido nos chips ATMega 328p e 32U4 entre outros. Para mais detalhes, 
 * veja o datasheet do 328p, seção  10.9.2 (WDTCSR – Watchdog Timer 
 * Control Register WDTCSR – Watchdog Timer Control Register:
 * https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf#page=47
 * 
  *
 * DISCLAIMER:
 * The author is in no way responsible for any problems or damage caused by
 * using this code. Use at your own risk.
 *
 * LICENSE:
 * This code is distributed under the GNU Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 * More details can be found at http://www.gnu.org/licenses/gpl.txt
 */


// Macro to executed assembly instruction 'wdr' (reset watchdog)
// Also defined in "avr/wdt.h"
#define wdt_reset() __asm__ __volatile__ ("wdr")

// Timer prescalers available for the watchdog.
#define WDT_16MS	B01000000
#define WDT_32MS	B01000001
#define WDT_64MS	B01000010
#define WDT_125MS	B01000011
#define WDT_250MS	B01000100
#define WDT_500MS	B01000101
#define WDT_1S		B01000110
#define WDT_2S		B01000111
#define WDT_4S		B01100000
#define WDT_8S		B01100001

// LED conectado no pino 10 através de um restor de 220 Ohms
#define LED_PIN 10

// Varável global acessada pela interrupção do watchdog
volatile bool _ledToggle = false;

// Declaração de funções auxiliares
void watchdogInit(uint8_t);



void setup()
{
	pinMode(LED_PIN, OUTPUT);
	watchdogInit(WDT_500MS);
}

void loop()
{
	// Enquanto a flag = true mantém o led aceso
	// Enquanto a flag = false mantém o led apagado
	// A flag troca de estado toda vez que houver um timeout
	// do temporizador do watchdog.
	if (_ledToggle) digitalWrite(LED_PIN, HIGH);
	else digitalWrite(LED_PIN, LOW);
}

//////////////////////////////////////////////

// Executado toda vez que houver uma interrupção gerada
// pelo contador do watchdog, o tempo pode ser controlado
// de acordo com divisores específicos da frequência base
// do oscilador interno de 128KHz. Após trocar o estado
// lógico da flag, reseta o contador do watchdog para 
// que ele possa repetir o processo.
ISR(WDT_vect)
{
	_ledToggle = !_ledToggle;
	wdt_reset();
}


// Inicializa o controlador do watchdog de acordo com o valor
// do divisor/prescaler passado como argumento. Caso um valor
// não seja passado, assume 1s para o timeout. Para iniciar 
// o Watchdog como interrupção, sem causar o processador a 
// reiniciar, é necessário executar esta sequencia:
//  1: desabilitar interrupções
//  2: resetar o watchdog
//  3: habilitar o watchdog e o prescaler (registrador WDTCSR)
//  4. habilitar a interrupção e setar o prescaler (registrador WDTCSR)
//  5. habilitar interrupções
void watchdogInit(byte timeout_mask = WDT_1S)
{
	cli();
	wdt_reset();
	WDTCSR |= B00011000;
	WDTCSR = timeout_mask;
	sei();
}