1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00
RIOT/cpu/qn908x/periph/wdt.c

192 lines
6.2 KiB
C
Raw Normal View History

/*
* Copyright (C) 2020 iosabi
*
* This file is subject to the terms and conditions of the GNU Lesser General
* Public License v2.1. See the file LICENSE in the top level directory for more
* details.
*/
/**
* @ingroup cpu_qn908x
* @ingroup cpu_qn908x_wdt
*
* @{
*
* @file
* @brief Low-level WDOG driver implementation
*
* @author iosabi <iosabi@protonmail.com>
*
* @}
*/
#include "periph/wdt.h"
#include <stdint.h>
#include <stdbool.h>
#include "vendor/drivers/fsl_clock.h"
#include "debug.h"
/* The number of cycles to refresh the WDT with when kicked. */
static uint32_t wdt_load_value = 0xffffffff;
/* The maximum value the WDT counter could have when kicked. The WDT counter
* always decrements starting from wdt_load_value, so when used in WINDOW mode
* a counter value larger than this means that WDT was kicked before the lower
* bound of the window.
*/
static uint32_t wdt_load_window_value = 0xffffffff;
/* The value that will be loaded in the WDT counter after the first interrupt
* triggers. The WDT doesn't not reset the device automatically once the WDT
* counter reaches 0, instead it first triggers an interrupt and restarts the
* count again. This value will be loaded by the ISR after it triggers. A value
* of 0 means to reset immediately after the ISR triggers, which is used if no
* callback is provided. */
static uint32_t wdt_load_after_isr = 0;
#ifdef MODULE_PERIPH_WDT_CB
static wdt_cb_t wdt_cb;
static void *wdt_arg;
static bool wdt_kick_disabled = false;
#endif /* MODULE_PERIPH_WDT_CB */
/**
* Before making any change to the WDT it is required to "unlock" it by writing
* this value to the LOCK register. Call @ref _wdt_lock() to lock it again.
*/
static void _wdt_unlock(void)
{
WDT->LOCK = 0x1acce551;
}
/**
* @brief Lock the WDT block to prevent accidental changes.
*/
static void _wdt_lock(void)
{
WDT->LOCK = 0x10c1ced; /* Any other value is as good. */
}
void wdt_start(void)
{
CLOCK_EnableClock(kCLOCK_Wdt);
}
void wdt_stop(void)
{
/* This only stops the clock of the watchdog and therefore the counter
* stops, but leaves it otherwise configured. */
CLOCK_DisableClock(kCLOCK_Wdt);
}
void wdt_kick(void)
{
#ifdef MODULE_PERIPH_WDT_CB
if (wdt_kick_disabled) {
return;
}
#endif /* MODULE_PERIPH_WDT_CB */
if (WDT->VALUE > wdt_load_window_value) {
DEBUG("wdt_kick() called before the minimum window time.");
/* In this condition we simulate the WDT triggering immediately by
* setting its LOAD value to 0. This will cause the ISR (and the
* callback if there is one) if it wasn't called yet, which will update
* the LOAD value with the time derived from CONFIG_WDT_WARNING_PERIOD
* if needed and reset the device afterwards. */
_wdt_unlock();
WDT->LOAD = 0;
_wdt_lock();
while (true) {}
}
_wdt_unlock();
WDT->LOAD = wdt_load_value;
_wdt_lock();
}
static void wdt_setup(uint32_t min_time, uint32_t max_time, uint32_t isr_time)
{
/* Check reset time limit */
assert((max_time > NWDT_TIME_LOWER_LIMIT)
&& (max_time < NWDT_TIME_UPPER_LIMIT));
assert(min_time <= max_time);
/* The clock is stopped after setup and calling wdt_start() is required. */
wdt_stop();
const uint32_t tick_per_ms = CLOCK_GetFreq(kCLOCK_WdtClk) / 1000;
wdt_load_value = tick_per_ms * max_time;
wdt_load_window_value = tick_per_ms * (max_time - min_time);
wdt_load_after_isr = tick_per_ms * isr_time;
/* The ISR is always called as it is needed to trigger the reset after the
* appropriate delay even in the no callback case. */
_wdt_unlock();
WDT->LOAD = wdt_load_value;
WDT->CTRL = WDT_CTRL_RESEN_MASK | WDT_CTRL_INTEN_MASK;
_wdt_lock();
NVIC_EnableIRQ(WDT_IRQn);
}
void wdt_setup_reboot(uint32_t min_time, uint32_t max_time)
{
#ifdef MODULE_PERIPH_WDT_CB
wdt_cb = NULL;
wdt_arg = NULL;
#endif /* MODULE_PERIPH_WDT_CB */
wdt_setup(min_time, max_time, 0);
}
#ifdef MODULE_PERIPH_WDT_CB
void wdt_setup_reboot_with_callback(uint32_t min_time, uint32_t max_time,
wdt_cb_t cb, void *arg)
{
wdt_cb = cb;
wdt_arg = arg;
assert(max_time >= CONFIG_WDT_WARNING_PERIOD);
/* We don't support having a min_time that falls within the
* CONFIG_WDT_WARNING_PERIOD since that would mean that you can't call
* wdt_kick() until some time after the callback is called which is pretty
* useless considering that the purpose of the callback is to perform
* "specific safety operations of data logging before the actual reboot."
* before the reboot happens. After the callback is called the reboot is
* inevitable and calling wdt_kick() has no effect. This code moves the
* min_time back to at least the point where the callback is called which is
* a similar behavior. */
if (max_time - CONFIG_WDT_WARNING_PERIOD < min_time) {
min_time = max_time - CONFIG_WDT_WARNING_PERIOD;
}
wdt_setup(min_time, max_time - CONFIG_WDT_WARNING_PERIOD,
CONFIG_WDT_WARNING_PERIOD);
}
#endif /* MODULE_PERIPH_WDT_CB */
void isr_wdt(void)
{
DEBUG("[wdt] isr_wdt with LOAD=%" PRIu32 "\n", WDT->LOAD);
/* Set the timer to reset the device after CONFIG_WDT_WARNING_PERIOD but not
* clear the interrupt bit in the WDT->INT_CLR register. This will cause the
* WDT to reset the device the next time the counter reaches 0, which now
* will happen again in CONFIG_WDT_WARNING_PERIOD ms.
* Since the wdt_cb may return before the new WDT counter triggers, which is
* the normal case if we expect the callback to do some short safety
* operations, we would exit the ISR without clearing the interrupt bit in
* WDT->INT_CLR which would cause the interrupt to be called again. To avoid
* this situation, we disable the IRQ source in the NVIC.
* After this ISR triggers, further wdt_kicks() are ignored to prevent the
* software from kicking the WDT during the CONFIG_WDT_WARNING_PERIOD. */
NVIC_DisableIRQ(WDT_IRQn);
_wdt_unlock();
WDT->LOAD = wdt_load_after_isr;
_wdt_lock();
#ifdef MODULE_PERIPH_WDT_CB
wdt_kick_disabled = true;
if (wdt_cb) {
wdt_cb(wdt_arg);
}
#endif /* MODULE_PERIPH_WDT_CB */
cortexm_isr_end();
}