Arduino. Volcado de la memoria RAM, flash y EEPROM usando Dump

Ya hemos visto como leer un byte de los distintos tipos de memoria de la placa Arduino. Sabiendo leer un byte podemos recorriendo y mostrando todos los bytes entre dos direcciones para conseguir un volcado. Pero si queremos algo con «mejor aspecto» podemos usar la librería Dump .

Tenemos dos funciones, una devuelve un volcado de la SRAM y otra un volcado de la memoria flash.

Volcado de la SRAM: dumpRam(out, addr, size);

Volcado de la memoria flash: dumpPgm(out, addr, size);

Los parámetros que se les pasa son los siguientes:

  • out: Serial al que enviar los datos
  • addr: dirección en la que comenzar el volcado
  • size: cantidad de bytes que se mostraran

Faltaría un volcado de la memoria EEPROM, por suerte es sencillo crear nuestra propia función fijándonos en el código de las que ya tiene:

unsigned char memByteEeprom(const void* x) {
    return *(char*)x;
}

void dumpEeprom(Print& out,void const*at,int sz) {
    return dump(out,at,sz,memByteEeprom);
}

Veamos un ejemplo completo de su uso:

#include <Dump.h>

const unsigned int* ramTest = (unsigned int*) 0x100;
const unsigned int* pgmTest = (unsigned int*) 0x100;

void setup() {
  Serial.begin(9600);
  while(!Serial);
  Serial.println("Dump SRAM");
  dumpRam(Serial,ramTest,64);
  Serial.println("Dump Flash");
  dumpPgm(Serial,pgmTest,64);
  Serial.println("Dump EEPROM");
  dumpEeprom(Serial,0,64);
}

void loop(){}

unsigned char memByteEeprom(const void* x) {
  return *(char*)x;
}

void dumpEeprom(Print& out, void const* at, int sz) {
  return dump(out,at,sz,memByteEeprom);
}

La salida obtenida:

Dump SRAM
0x0100: ......e. ..R..... 00 00 00 00 05 01 65 00  92 00 52 01 C3 00 A1 00 
0x0110: .....000 .0x.: .D B5 00 0D 0A 00 30 30 30  00 30 78 00 3A 20 00 44 
0x0120: ump SRAM .Dump Fl 75 6D 70 20 53 52 41 4D  00 44 75 6D 70 20 46 6C 
0x0130: ash.Dump  EEPROM. 61 73 68 00 44 75 6D 70  20 45 45 50 52 4F 4D 00 
Dump Flash
0x0100: ...-.... .+y..... F0 81 E0 2D C6 01 09 95  89 2B 79 F7 C5 01 DF 91 
0x0110: ........ ........ CF 91 1F 91 0F 91 FF 90  EF 90 DF 90 CF 90 BF 90 
0x0120: ......S. D.%/0../ AF 90 08 95 FC 01 53 8D  44 8D 25 2F 30 E0 84 2F 
0x0130: ......T. ........ 90 E0 82 1B 93 0B 54 17  10 F0 CF 96 08 95 01 97 
Dump EEPROM
0x0000: ........ ...._... A3 00 BE BF BF 00 00 00  00 00 36 00 5F 00 0E 00 
0x0010: ...... . ..J.0... 00 00 00 00 01 00 20 08  18 00 4A 01 30 00 1E 00 
0x0020: ........ ........ B8 B8 B8 1F 00 00 02 00  00 01 00 00 B8 B8 B8 B8 
0x0030: ....5... ........ B8 B8 B8 B8 B8 06 07 07  B8 B8 B8 00 00 00 00 00 

Arduino. Leer datos de la memoria RAM, flash y EEPROM

Vamos a ver como leer un byte de cada uno de los tres tipos de memoria que tiene una placa Arduino.

Leer un byte de la memoria flash

Podemos usar la función (realmente es una macro) pgm_read_byte(addr) que devuelve el byte de la dirección addr (una dirección de 16 bits más que suficiente para los 32 KB de memoria FLASH de la placa Arduino UNO).

Serial.print(pgm_read_byte(addr));

Leer un byte de la memoria EEPROM

Para leer la EEPROM podemos usar el el array EEPROM[addr] siendo addr la dirección del byte de memoria a leer. Para acceder a este array hay que incluir la librería EEPROM.h

#include <EEPROM.h>

...

Serial.print(EEPROM[addr])

Leer un byte de la memoria RAM

En este casos recurriremos a crear un puntero y apuntarlo a la dirección de memoria que queramos.

Asignando la dirección de memoria directamente:

const byte* ramPtr = (const byte*) 0x100; 

Usando una variable que almacena la dirección de memoria:

unsigned int address = 0x100;
const byte* ramPtr = (const byte*) address;

Para mostrarlo:

Serial.print(*ramPtr, HEX);

Librería LowPower para Arduino

Una de las librerías más usadas para el ahorro de energía es LowPower. Esta librería unifica un poco todo lo que hemos visto hasta ahora bajo una interface simple y fácil de usar. Su principal problema es que en algunos casos es demasiado exhaustiva y eso hace que sea tediosa de programar.

Tras descargar la librería en el IDE de Arduino hay que incluirla en el programa:

#include "LowPower.h"

El objecto LowPower tiene un método para cada uno de los modos de sleep:

  • idle
  • adcNoiseReduction
  • powerDown
  • powerSave
  • powerStandby
  • powerExtStandby
  • standby

El primer parámetro «period» indica cuanto tiempo va permanecer dormido tras llamar a esta función, los posible valores son:

  • SLEEP_15MS
  • SLEEP_30MS
  • SLEEP_60MS
  • SLEEP_120MS
  • SLEEP_250MS
  • SLEEP_500MS
  • SLEEP_1S
  • SLEEP_2S
  • SLEEP_4S
  • SLEEP_8S
  • SLEEP_FOREVER

Los siguientes parámetros permiten apagar distintos módulos de la placa mientras esta dormida. Cada modo tiene unos parámetros distintos:

void adcNoiseReduction(period_t period, adc_t adc, timer2_t timer2) 
void powerDown(period_t period, adc_t adc, bod_t bod) 
void powerSave(period_t period, adc_t adc, bod_t bod, timer2_t timer2) 
void powerStandby(period_t period, adc_t adc, bod_t bod) 
void powerExtStandby(period_t period, adc_t adc, bod_t bod, timer2_t timer2)

El estado Idle tiene una larga lista de parámetros, que ademas cambia según para que placa se compile:

// ATmega328P, ATmega168, ATmega168P, AVR_ATmega88
void idle(period_t period, adc_t adc, timer2_t timer2,
     timer1_t timer1, timer0_t timer0, spi_t spi,
     usart0_t usart0, twi_t twi);

// ATmega644P, AVR_ATmega1284P
void idle(period_t period, adc_t adc, timer2_t timer2,
     timer1_t timer1, timer0_t timer0, spi_t spi,
     usart1_t usart1, usart0_t usart0, twi_t twi);

// ATmega2560
void idle(period_t period, adc_t adc, timer5_t timer5,
     timer4_t timer4, timer3_t timer3, timer2_t timer2,
     timer1_t timer1, timer0_t timer0, spi_t spi,
     usart3_t usart3, usart2_t usart2, usart1_t usart1,
     usart0_t usart0, twi_t twi);

// ATmega256RFR2
void idle(period_t period, adc_t adc, timer5_t timer5,
     timer4_t timer4, timer3_t timer3, timer2_t timer2,
     timer1_t timer1, timer0_t timer0, spi_t spi,
     usart1_t usart1, usart0_t usart0, twi_t twi);

// ATmega32U4
void idle(period_t period, adc_t adc, timer4_t timer4,
     timer3_t timer3, timer1_t timer1, timer0_t timer0,
     spi_t spi, usart1_t usart1, twi_t twi, usb_t usb);

Los parámetros son enumeraciones, cuyos posibles valores pueden ser el nombre del modulo al que res refieren seguido de ON u OFF. Por ejemplo adc puede tener los valores ADC_ON o ADC_OFF, timer2 puede tener los valores TIMER2_ON y TIMER2_OFF

Veamos dos ejemplos, el primero duerme la placa durante 8 segundo en modo powerDown y ademas desactiva los módulos ADC y BOD, similar a ejecutar las instrucciones: power_adc_disable() y sleep_bod_disable()

#include "LowPower.h"

void setup(){ 

}

void loop(){
    LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); 
		// codigo cuando se despierta la placa
}

El siguiente ejemplo duerme la placa hasta que sea despertada por una interrupción en el pin 2:

#include "LowPower.h"

const int wakeUpPin = 2;

void wakeUp(){
	// codigo a ejecutar cuando se despierta la placa
}

void setup(){
    pinMode(wakeUpPin, INPUT);  
		attachInterrupt(0, wakeUp, LOW); 
}

void loop(){    
    LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);    
}

Ejemplo de fragmentación de memoria con Arduino

En dispositivos con poca memoria RAM, como es el caso de Arduino, cuando se asigna memoria de forma dinámica puede surgir el problema de la fragmentación. Se produce cuando la memoria libre esta fragmentada en trozos tan pequeños que no se puede reservar suficiente memoria contigua libre aunque la suma total de memoria libre es mayor que la que se trata de reservar.

Veamos un ejemplo de como se produce esta fragmentación, para ello empezaremos reservando un bloque de 1000 bytes de memoria (memory1) para limitar la memoria disponible. Luego reservamos un bloque de memoria de 300 bytes (memory2) otro de un byte (memory3) y otro de 300 (memory4) de nuevo. La función de ese bloque de un único byte es evitar que al liberar los otros dos bloques estos puedan unirse en un mismo bloque. Liberamos los dos bloques de 300 bytes por lo que ahora debería haber libre un mínimo de 600 bytes libres, sin embargo si tratamos de reservar 500 bytes (memory5) y no puedo (el puntero tiene valor 0). Hay memoria libre suficiente pero esta divida en bloques más pequeños del que necesito. Con solo un byte estratégicamente colocado hemos causado un problema de fragmentación. Los bloques más pequeños (memory6) pueden reservar memoria sin problemas.

byte* memory1;
byte* memory2;
byte* memory3;
byte* memory4;
byte* memory5;
byte* memory6;

void setup() {  
  Serial.begin(9600);
  while (!Serial) {
    ; // esperamos a que el puerto este inicializado 
  }

  memory1 = (byte*) calloc (1000, sizeof(byte)); //reservamos 1000
  memory2 = (byte*) calloc (300, sizeof(byte)); //reservamos 300
  memory3 = (byte*) calloc (1, sizeof(byte)); //reservamos 1
  memory4 = (byte*) calloc (300, sizeof(byte)); //reservamos 300
  free(memory2); // liberamos 300
  free(memory4); //liberamos 300 
  memory5 = (byte*) calloc (500, sizeof(byte)); //reservamos 500
  memory6 = (byte*) calloc (300, sizeof(byte)); //reservarmos 300

  Serial.println((long)memory1);
  Serial.println((long)memory2);
  Serial.println((long)memory3);
  Serial.println((long)memory4);
  Serial.println((long)memory5);
  Serial.println((long)memory6);
}
 
void loop() {

}

Veamos un ejemplo de salida:

466
1468
1770
1774
0
1468

Se puede ver que memory5 no puede reservar memoria mientras que a memory6 se le asigna el primer bloque libre que coincide con el memory2 que acaba de ser liberado.

Este ejemplo esta pensado para una placa Arduino UNO en caso de usarlo en otra placa habrá que ajustar el tamaño del primer bloque.

Arduino, leer puerto serie usando serialEvent

SerialEvent es una función que se llama automáticamente cuando hay datos esperando a ser leídos en el puerto serie. Se invoca tras cada iteración de la función loop, por lo que cualquier delay o bloqueo en la función loop retrasa su invocación. Es una forma sencilla de integrar la gestión de los datos por puerto serie nuestro código.

void setup() {
  Serial.begin(9600);
}

void loop() {

}

void serialEvent() {
  //mientras tenga datos que leer
  while (Serial.available()) {
	//leer datos
  }	
}

Para las placas que tienen más de un puerto serie existe una función serialEvent distinta para cada uno:

  • Serial – serialEvent
  • Serial1- serialEvent1
  • Serial2 – serialEvent2
  • Serial3 – serialEvent3

Esta función no esta disponible para todos los puerto serie de todas las placas, por ejemplo no esta disponible para las placas basadas en microcontroladores SAMD, para Arduino Due o las placas Leonardo, Micro, o Yún que no funciona serialEvent (serialEvent1 si que funciona).

Arduino, configurar puerto serie.

En Arduino para configurar la conexión serie de la placa se usa la instrucción Serial.begin(speed). El parámetro indica la velocidad de transmisión en bits por segundo (baudios), tiene que ser uno de estos valores: 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600 o 115200. Puede aceptar un parámetro más Serial.begin(speed, config). Este parámetro se usa para configurar el tamaño de bits por dato, la paridad y el número de bit de parada. Para ello hay definidas un conjunto de constantes con un convenio para sus nombres. Veamos como funciona.

  • SERIAL_ : Todas las constante empiezan así
  • 5,6,7,8 : bits por dato
  • N,E,O : Paridad. Ninguna (None), par (Even) o impar (Odd). La paridad es un mecanismo para verificar si los datos recibidos son correctos.
  • 1, 2: bits de parada. Se usan para indicar el fin de un dato.

Por ejemplo, el valor por defecto es SERIAL_8N1 que indica 8 bits por dato, sin paridad y con un bit de parada. El listado de posibles valores es:

  • SERIAL_5N1
  • SERIAL_6N1
  • SERIAL_7N1
  • SERIAL_8N1
  • SERIAL_5N2
  • SERIAL_6N2
  • SERIAL_7N2
  • SERIAL_8N2
  • SERIAL_5E1
  • SERIAL_6E1
  • SERIAL_7E1
  • SERIAL_8E1
  • SERIAL_5E2
  • SERIAL_6E2
  • SERIAL_7E2
  • SERIAL_8E2
  • SERIAL_5O1
  • SERIAL_6O1
  • SERIAL_7O1
  • SERIAL_8O1
  • SERIAL_5O2
  • SERIAL_6O2
  • SERIAL_7O2
  • SERIAL_8O2

Por ejemplo estas dos instrucciones hacen lo mismo ya que SERIAL_8N1 es el valor por defecto:

Serial.begin(9600);

Serial.begin(9600,  SERIAL_8N1);

En el caso de placas con varios puertos serie como MEGA o NANO 33 existe un nombre distinto para cada uno. Se nombran «Serial» y el número de puerto empezando en 0 y teniendo en cuenta que «Serial0» es simplemente «Serial»:

  • Serial
  • Serial1
  • Serial2
  • Serial3

Se usan exactamente igual que Serial:

void setup() {
  Serial.begin(9600);
  Serial1.begin(9600);
  Serial2.begin(9600, SERIAL_8N1);
  Serial3.begin(9600, SERIAL_8N1);
}

En alguna placas como Leonardo o Micro puede ser necesario esperar a que el puerto serie este correctamente inicializado puedes leer más sobre eso en esta entrada.

Evitar que Arduino se bloquee esperando al puerto serie

En las placas Arduino que controlan el puerto USB desde el propio microcontrolador como Leonardo o Micro hay que esperar a que el puerto serie se inicialice para evitar que se pierdan datos. Esto habitualmente se hace en la función Setup tras Serial.begin() con:

while(!Serial){ }

El problema de hacer esto es que se queda esperando a que el puerto USB este conectado y si no se conecta el programa no avanza. ¿Y si queremos tener un programa que funcione correctamente aunque no este conectado a un puerto USB?. Hay dos maneras de solucionar esto. La más simple reemplazar un delay() que espere el tiempo suficiente para que se haya iniciado (entre 500 y 1000 milisegundos suele ser una buena elección). La otra opción es, dentro del bucle, poner un tiempo limite y cuando pase «romper» el bucle con la instrucción break:

const long breakTime = 1000;//un segundo
unsigned long startTime = millis();

void setup(){
  Serial.begin(9600);
  while(!Serial){ 
	if (millis() - startTime >= breakTime) {
	  break; //sale del bucle
	}
  }
}

En las placas como Arduino UNO que el soporte USB es externo al microcontrolador Serial siempre es valido y nunca entra en el bucle.

Usar Arduino como si fuera un ratón

Para simular un ratón con una placa Arduino Leonardo contamos con la librería Mouse.h. Al igual que vimos con el teclado hay que usar las funciones Mouse.begin() y Mouse.end() para indicar que se empieza y se termina la simulación de ratón.

Se distingue entre la acción de pulsar un botón del ratón y la de presionar y luego soltar. Para simular la pulsación de un botón se puede usar la función Mouse.click(button). Mientras que para presionar se usa Mouse.press(button) y para liberar Mouse.release(button) (no existe un releaseAll para el ratón). La variable button hace referencia a una de las siguientes constantes definidas en la libreria Mouse.h :

  • MOUSE_LEFT
  • MOUSE_RIGHT
  • MOUSE_MIDDLE

Para simular el movimiento del ratón podemos usar la función Mouse.move(xVal, yVal, wheel) siendo los dos primeros parámetros la cantidad de movimiento (no, la posición) en el eje X y en el Y de la pantalla. El tercero indica el desplazamiento de la rueda central del ratón. Estos desplazamientos pueden ser positivos o negativos en un rango de -128 y 127 (izquierda-derecha, arriba-abajo). Su valor es un poco confuso ya que se refiere a «lo que se ha movido el ratón en ese eje» y afecta al cursor desde su posición actual. No es fácil trasladar ese valor a pixeles ya que también depende, entre otras cosas, de como el ordenador al que este conectado interprete esos valores. Una de las pegas de esta forma de trabajar es que el movimiento del ratón es relativo a las coordenadas actuales del cursor en la pantalla por lo que es difícil situar el ratón en un punto exacto de la misma. El truco para hacerlo con cierta precisión es llevar el ratón a una esquina de la pantalla y desde ahí tratar de moverlo al punto deseado. Vemoa sun ejemplo muy básico de esta idea:

import "Mouse.h"

void setup() {
  Mouse.begin();
}

void loop() {

  //Mover a la esquina superior izquierda
  for(int i = 0; i < 20; i++){
	Mouse.move(-128, -128, 0);
  }

  //Mover a un punto determinado
  for(int i = 0; i < 10; i++){
	Mouse.move(50, 20, 0);
  }
  
  Mouse.click(MOUSE_LEFT);

  delay(5000);
}

Usar Arduino como si fuera un teclado

Las placas Arduino basados en los microcontroladores 32u4 o SAMD (Leonardo, Esplora, Zero, Due y MKR) pueden simular ser un teclado conectado al puerto USB. Para ello es necesario usar la librería Keyboard.h. Lo primero para simular un teclado es llamar a la función Keyboard.begin() y para finalizar la simulación Keyboard.end().

Antes de comenzar con el resto de las funciones hay que hacer una aclaración. No es lo mismo pulsar que presionar, presionar es solo el gesto de bajar la tecla sin liberarla mientras que pulsar consiste en presionar y liberar la tecla.

Para simular la pulsación de una tecla podemos usar Keyboard.write(char) a la que se pasa como parámetro el código ASCII del carácter. El código del carácter se puede pasar de varias formas:

//Simular la pulsación de la tecla A
Keyboard.write('A');        //Character
Keyboard.write(65);         //Decimal          
Keyboard.write(0x41);       //Hexadecimal       
Keyboard.write(0b01000001); //Binario

Al permitir enviarle el código de la tecla en diversos formatos podremos usarlo para simular teclas que no impriman un carácter como pueden ser la teclas con flechas de dirección. Para ello la librería incluye un listado de constantes que representan el valor de estas teclas.

Keyboard.write(KEY_UP_ARROW);    //Flecha arriba
Keyboard.write(KEY_DOWN_ARROW);  //Flecha abajo          
Keyboard.write(KEY_RIGHT_ARROW); //Flecha derecha      
Keyboard.write(KEY_LEFT_ARROW;   //Flecha Izquierda

Si queremos simular la pulsación de varios caracteres alfanuméricos podemos usar la funciones Keyboard.print(string) y Keyboard.println(string) que reciben como parámetro un String y simulan la pulsación de todos sus caracteres, println ademas añade un salto de linea al final.

Para simular una tecla presionada se puede usar Keyboard.press(char) se pueden pulsar varias teclas a la vez. Incluso combinar funciones, por ejemplo el siguiente código seria como teclear «hola» con la tecla «Mayús» pulsada.

Keyboard.press(KEY_RIGHT_SHIFT);
Keyboard.print("hola"); //HOLA
Keyboard.release(KEY_RIGHT_SHIFT);

Para liberar una tecla tenemos dos funciones: Keyboard.release(char) que libera la tecla que le pases como parámetro y Keyboard.releaseAll() que libera todas las teclas que estén presionadas.

Veamos el ejemplo completo:

#include <Keyboard.h>

void setup() {
  Keyboard.begin();
}

void loop(){
  Keyboard.press(KEY_RIGHT_SHIFT);
  Keyboard.print("hola"); //HOLA
  Keyboard.release(KEY_RIGHT_SHIFT);
}

Listado de constantes definidas en Keyboard.h

  • KEY_LEFT_CTRL
  • KEY_LEFT_SHIFT
  • KEY_LEFT_ALT
  • KEY_LEFT_GUI
  • KEY_RIGHT_CTRL
  • KEY_RIGHT_SHIFT
  • KEY_RIGHT_ALT
  • KEY_RIGHT_GUI
  • KEY_UP_ARROW
  • KEY_DOWN_ARROW
  • KEY_LEFT_ARROW
  • KEY_RIGHT_ARROW
  • KEY_BACKSPACE
  • KEY_TAB
  • KEY_RETURN
  • KEY_ESC
  • KEY_INSERT
  • KEY_DELETE
  • KEY_PAGE_UP
  • KEY_PAGE_DOWN
  • KEY_HOME
  • KEY_END
  • KEY_CAPS_LOCK
  • KEY_F1
  • KEY_F2
  • KEY_F3
  • KEY_F4
  • KEY_F5
  • KEY_F6
  • KEY_F7
  • KEY_F8
  • KEY_F9
  • KEY_F10
  • KEY_F11
  • KEY_F12
  • KEY_F13
  • KEY_F14
  • KEY_F15
  • KEY_F16
  • KEY_F17
  • KEY_F18
  • KEY_F19
  • KEY_F20
  • KEY_F21
  • KEY_F22
  • KEY_F23
  • KEY_F24

Usar el watchdog de Arduino como temporizador

Se puede usar el watchdog de la placa para que en lugar de reiniciar el programa ejecute una función que nosotros le definamos. Para cambiar el modo hay que modificar el registro WDTCSR que almacena la configuración del watchdog. Este registro ocupa un byte donde cada bit tiene el siguiente significado:

Bit 7 6 5 4 3 2 1 0
nombre WDIF WDIE WDP3 WDCE WDE WDP2 WDP1 WDP0
  • WDIF: Es un flag que indica si el watchdog ha alnzado la interrupción.
  • WDIE: Si se pone a 1 cuando el tiempo de espera pase el watchdog ejecutara la ISR asociada
  • WDCE: Tiene que ponerse a 1 para poder cambiar al valorde WDE o WDP3..0. Pasados cuatro ciclos de reloj su estado vuelve a 0.
  • WDE: Si se pone a 1 cuando el tiempo de espera pase el watchdog reiniciara la placa
  • WDP3..0: Fija el valor de tiempo de espera del watchdog

Para fijar el valor del tiempo de espera se pueden usar los siguientes valores:

WDP3 WDP2 WDP1 WDP0 Tiempo
0 0 0 0 16 ms
0 0 0 1 32 ms
0 0 1 0 64 ms
0 0 1 1 125 ms
0 1 0 0 250 ms
0 1 0 1 500 ms
0 1 1 0 1 sg
0 1 1 1 2 sg
1 0 0 0 4 sg
1 0 0 1 8 sg

Veamos como configurar WDTCSR correctamente. El funcionamiento es algo extraño, primero hay que habilitar al escritura de los registros poniendo los bits WDCE y WDE a 1:

WDTCSR = (1 << WDCE) | (1 << WDE);

Luego inmediatamente hay que poner WDIE a 1, WDE a 0 y los bits WDP3, WDP2, WDP1, WDP0 con el valor de tiempo que deseemos mirando la tabla. Por ejemplo supongamos que queremos poner 2 segundos de tiempo de espera, miramos la tabla (0111) y colocamos a 1 los bits que correspondan:

WDTCSR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1) | (1<<WDP0)

Si fuera 8 segundos (1001):

WDTCSR = (1<<WDIE) | (1<<WDP3) | (1<<WDP0);

Cada constante contiene el número de bit al que representa por ejemplo WDP3 es 5 al hacer 1<<WDP3 estamos poniendo a 1 el bit 5 y hacemos la operación | (or) para unir todos esos bits en un solo byte. Todo esto hay que hacerlo deshabilitando todas las interrupciones.

noInterrupts();
WDTCSR = (1 << WDCE) | (1 << WDE);
WDTCSR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1) | (1<<WDP0);
interrupts();

La función que selanzará se define usando la macro ISR:

ISR (WDT_vect){ 
//aqui va el codigo de tu ISR para el watchdog
}

Veamos un ejemplo:

#include <avr/wdt.h>
 
int count = 0;

void setup() {
  noInterrupts();
  WDTCSR = (1 << WDCE) | (1 << WDE);
  WDTCSR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1) | (1<<WDP0);
  interrupts();
  Serial.begin(9600);
}
 
void loop() {
  Serial.println(count);
  delay(400);
}
 
ISR(WDT_vect) { // WDT interrupt vector
  count++;  
}