Author

Topic: [ANN] Hardware watchdog using an AVR (Read 1430 times)

full member
Activity: 140
Merit: 100
1221iZanNi5igK7oAA7AWmYjpsyjsRbLLZ
January 18, 2013, 11:06:37 PM
#1
After my rigs were down last night for most of the night Angry I decided to bite the bullet and create a hardware watchdog.

The irony is that the chipset on these motherboards has a watchdog! But because the manufacturer won't share the chipset datasheet, no driver exists. What I did was easier than reversing the BIOS (but I might still do that):

After a night of tinkering:
Code:
#include "VirtualSerial.h"

static CDC_LineEncoding_t LineEncoding = { .BaudRateBPS = 0,
                                           .CharFormat  = CDC_LINEENCODING_OneStopBit,
                                           .ParityType  = CDC_PARITY_None,
                                           .DataBits    = 8
                                         };

uint8_t led_pwm = 255;
int8_t led_pwm_delta = 0;
uint16_t led_pwm_delta_delay = 0;
uint8_t reset_countdown = 6;

enum {
watchdog_off = 0,
watchdog_armed = 1,
watchdog_cleared = 2,
watchdog_timeout = 3,
watchdog_reset_wait = 4,
};

uint8_t watchdog_state = watchdog_off;
char * report = 0;

int main(void)
{
MCUSR &= ~(1 << WDRF);
wdt_disable();
clock_prescale_set(clock_div_1);
USB_Init();
sei();
DDRC = 0;
PORTC = 0;
DDRD = 255;
PORTD = 0;

uint16_t count = 63;
for (;;) {
count += 64; // 64 controls the update rate of the LED by speeding or slowing the overflow to 0

// square led_pwm: fast linearization of the nonlinear response of the LED
PORTD = (count > ((uint16_t) led_pwm*led_pwm)) ?
0 : (1 << PORTD6);

if (watchdog_state == watchdog_timeout) {
led_pwm_delta_delay += 2;
if (!led_pwm_delta_delay) {
if (!reset_countdown) {
led_pwm = 64;
DDRC = 1 << PORTC7;
reset_countdown = 7;
} else if (reset_countdown == 7) {
led_pwm = 10;
reset_countdown = 6;
watchdog_state = watchdog_reset_wait;
} else {
led_pwm ^= 255;
reset_countdown--;
DDRC = 0;
}
}
} else {
if (watchdog_state == watchdog_cleared) watchdog_state = watchdog_armed;
led_pwm_delta_delay += 64;
if (!led_pwm_delta_delay && led_pwm_delta) {
int16_t next_pwm = led_pwm;
next_pwm += led_pwm_delta;
if (next_pwm > 255) {
led_pwm_delta = -1;
led_pwm = 255;
} else if (next_pwm < 0) {
led_pwm_delta = 1;
led_pwm = 0;
if (watchdog_state == watchdog_armed) {
watchdog_state = watchdog_timeout;
led_pwm_delta = 0;
led_pwm = 255;
}
} else {
led_pwm = next_pwm;
}
}
}

CDC_Task();
USB_USBTask();
}
}

void EVENT_USB_Device_Connect(void)
{
led_pwm = 0;
led_pwm_delta = 0;
}

void EVENT_USB_Device_Disconnect(void)
{
led_pwm = 255;
led_pwm_delta = 0;
watchdog_state = watchdog_off;
}

void EVENT_USB_Device_ConfigurationChanged(void)
{
if (Endpoint_ConfigureEndpoint(CDC_NOTIFICATION_EPADDR, EP_TYPE_INTERRUPT, CDC_NOTIFICATION_EPSIZE, 1) &&
Endpoint_ConfigureEndpoint(CDC_TX_EPADDR, EP_TYPE_BULK, CDC_TXRX_EPSIZE, 1) &&
Endpoint_ConfigureEndpoint(CDC_RX_EPADDR, EP_TYPE_BULK,  CDC_TXRX_EPSIZE, 1))
{
led_pwm = 0;
led_pwm_delta = 0;
} else {
led_pwm = 128;
led_pwm_delta = 0;
}
LineEncoding.BaudRateBPS = 0;
}

void EVENT_USB_Device_ControlRequest(void)
{
switch (USB_ControlRequest.bRequest)
{
case CDC_REQ_GetLineEncoding:
if (USB_ControlRequest.bmRequestType == (REQDIR_DEVICETOHOST | REQTYPE_CLASS | REQREC_INTERFACE))
{
Endpoint_ClearSETUP();
Endpoint_Write_Control_Stream_LE(&LineEncoding, sizeof(CDC_LineEncoding_t));
Endpoint_ClearOUT();
}
break;
case CDC_REQ_SetLineEncoding:
if (USB_ControlRequest.bmRequestType == (REQDIR_HOSTTODEVICE | REQTYPE_CLASS | REQREC_INTERFACE))
{
Endpoint_ClearSETUP();
Endpoint_Read_Control_Stream_LE(&LineEncoding, sizeof(CDC_LineEncoding_t));
Endpoint_ClearIN();
}
break;
case CDC_REQ_SetControlLineState:
if (USB_ControlRequest.bmRequestType == (REQDIR_HOSTTODEVICE | REQTYPE_CLASS | REQREC_INTERFACE))
{
Endpoint_ClearSETUP();
Endpoint_ClearStatusStage();
}
break;
}
}

void CDC_Task(void)
{
static char * last_report   = 0;

if (USB_DeviceState != DEVICE_STATE_Configured)
 return;

report = (watchdog_state == watchdog_timeout) ? "watchdog timeout\r\n" : "watchdog armed\r\n";

if ((report != 0) && (last_report != report) && LineEncoding.BaudRateBPS)
{
last_report = report;
if (watchdog_state == watchdog_off) {
watchdog_state = watchdog_armed;
led_pwm_delta = 1;
}

Endpoint_SelectEndpoint(CDC_TX_EPADDR);
Endpoint_Write_Stream_LE(report, strlen(report), NULL);
bool IsFull = (Endpoint_BytesInEndpoint() == CDC_TXRX_EPSIZE);
Endpoint_ClearIN();

if (IsFull)
{
Endpoint_WaitUntilReady();
Endpoint_ClearIN();
}
}

Endpoint_SelectEndpoint(CDC_RX_EPADDR);
if (Endpoint_IsOUTReceived())
{
uint8_t  Buffer[Endpoint_BytesInEndpoint()];
uint16_t DataLength = Endpoint_BytesInEndpoint();
Endpoint_Read_Stream_LE(&Buffer, DataLength, NULL);
Endpoint_ClearOUT();
Endpoint_SelectEndpoint(CDC_TX_EPADDR);
Endpoint_Write_Stream_LE(&Buffer, DataLength, NULL);
Endpoint_ClearIN();
Endpoint_WaitUntilReady();
Endpoint_ClearIN();

watchdog_state = watchdog_cleared;
led_pwm = 0;
led_pwm_delta = 1;
led_pwm_delta_delay = 0;
}
}
How this works: download LUFA, then either WinAVR or Linux has gcc-avr in your package manager. Unzip LUFA and copy the VirtualSerial demo to another directory, then replace VirtualSerial.c with the above code. Now edit the makefile and specify your MCU and BOARD. Then type 'make' to create a .hex file you can pass to avrdude or a similar flash programmer for your board. If you get to this point but 'make' reports an error and does not spit out a .hex file, feel free to ask questions.

The code uses 1 LED for status, and one GPIO connected to the reset switch on your motherboard. The pins are correct for a Teensy 2 ($16). The reset line can be connected to both the GPIO and your manual reset button: the line is open drain with a pullup so any number of reset drivers can be wired to it.

The AVR emulates a USB serial adapter, and must receive 1 byte every 4 seconds (if your AVR crystal runs at 16 MHz), or it will hit the reset button. I have Windows and Linux code that does this for me - it's quite simple. To reduce the number of moving parts for reliability, I recommend against a simple echo "" >/dev/ttyACM0 because it will open and close the port, causing unnecessary USB traffic. A slightly better way is to leave the fd (handle) open:
Code:
exec 5>/dev/ttyACM0
while true; do
echo "" >&5
sleep 4
done

The status LED stays off until the serial port is configured. For Windows this happens when the serial port is opened. For Linux, this happens when the cdc_acm module is loaded. Then the status LED indicates the countdown, starting over each time a byte is received. During the reset process, the LED shows a "warning pattern," asserts reset with a different intensity, and then waits for the system to reset the serial port.

There's still lots that needs work! A watchdog should be as reliable as possible, and the serial port code is more complicated than necessary. It does help when debugging things. Please donate if you want me to improve on this. I also want to port Coin Control to Bitcoin-Qt head - send me a PM if that would be useful to you.

Here are some older threads on watchdogs:
Please donate if you find this useful: 1KBMjP645vz9HGyUru22V5RgSyacdJHsfh
Jump to: