The goal is to be able to use the Task Watchdog on a system that is also using power management to reach low-power modes. In some low-power modes, the watchdog channels can't be feed anymore. The task_wdt_suspend() function allows to prepare the Task Watchdog for a system low-power mode, in which the hardware watchdog (if enabled) is also suspended. The task_wdt_resume() function will reschedule the internal timer that manages the channels, feed all channels and also the hardware watchdog. Thus, the application is good to go and has enough time to feed the channels by itself. Signed-off-by: Adrien Ricciardi <aricciardi@baylibre.com>
318 lines
8.2 KiB
C
318 lines
8.2 KiB
C
/*
|
|
* Copyright (c) 2020 Libre Solar Technologies GmbH
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <zephyr/task_wdt/task_wdt.h>
|
|
|
|
#include <zephyr/drivers/watchdog.h>
|
|
#include <zephyr/sys/reboot.h>
|
|
#include <zephyr/device.h>
|
|
#include <errno.h>
|
|
|
|
#define LOG_LEVEL CONFIG_WDT_LOG_LEVEL
|
|
#include <zephyr/logging/log.h>
|
|
LOG_MODULE_REGISTER(task_wdt);
|
|
|
|
/*
|
|
* This dummy channel is used to continue feeding the hardware watchdog if the
|
|
* task watchdog timeouts are too long for regular updates
|
|
*/
|
|
#define TASK_WDT_BACKGROUND_CHANNEL UINTPTR_MAX
|
|
|
|
/*
|
|
* Task watchdog channel data
|
|
*/
|
|
struct task_wdt_channel {
|
|
/* period in milliseconds used to reset the timeout, set to 0 to
|
|
* indicate that the channel is available
|
|
*/
|
|
uint32_t reload_period;
|
|
/* abs. ticks when this channel expires (updated by task_wdt_feed) */
|
|
int64_t timeout_abs_ticks;
|
|
/* user data passed to the callback function */
|
|
void *user_data;
|
|
/* function to be called when watchdog timer expired */
|
|
task_wdt_callback_t callback;
|
|
};
|
|
|
|
/* array of all task watchdog channels */
|
|
static struct task_wdt_channel channels[CONFIG_TASK_WDT_CHANNELS];
|
|
static struct k_spinlock channels_lock;
|
|
|
|
/* timer used for watchdog handling */
|
|
static struct k_timer timer;
|
|
|
|
/* Tell whether the Task Watchdog has been fully initialized. */
|
|
static bool task_wdt_initialized;
|
|
|
|
#ifdef CONFIG_TASK_WDT_HW_FALLBACK
|
|
/* pointer to the hardware watchdog used as a fallback */
|
|
static const struct device *hw_wdt_dev;
|
|
static int hw_wdt_channel;
|
|
static bool hw_wdt_started;
|
|
#endif
|
|
|
|
static void schedule_next_timeout(int64_t current_ticks)
|
|
{
|
|
uintptr_t next_channel_id; /* channel which will time out next */
|
|
int64_t next_timeout; /* timeout in absolute ticks of this channel */
|
|
|
|
#ifdef CONFIG_TASK_WDT_HW_FALLBACK
|
|
next_channel_id = TASK_WDT_BACKGROUND_CHANNEL;
|
|
next_timeout = current_ticks +
|
|
k_ms_to_ticks_ceil64(CONFIG_TASK_WDT_MIN_TIMEOUT);
|
|
#else
|
|
next_channel_id = 0;
|
|
next_timeout = INT64_MAX;
|
|
#endif
|
|
|
|
/* find minimum timeout of all channels */
|
|
for (int id = 0; id < ARRAY_SIZE(channels); id++) {
|
|
if (channels[id].reload_period != 0 &&
|
|
channels[id].timeout_abs_ticks < next_timeout) {
|
|
next_channel_id = id;
|
|
next_timeout = channels[id].timeout_abs_ticks;
|
|
}
|
|
}
|
|
|
|
/* update task wdt kernel timer */
|
|
k_timer_user_data_set(&timer, (void *)next_channel_id);
|
|
k_timer_start(&timer, K_TIMEOUT_ABS_TICKS(next_timeout), K_FOREVER);
|
|
|
|
#ifdef CONFIG_TASK_WDT_HW_FALLBACK
|
|
if (hw_wdt_started) {
|
|
wdt_feed(hw_wdt_dev, hw_wdt_channel);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* @brief Task watchdog timer callback.
|
|
*
|
|
* If the device operates as intended, this function will never be called,
|
|
* as the timer is continuously restarted with the next due timeout in the
|
|
* task_wdt_feed() function.
|
|
*
|
|
* If all task watchdogs have longer timeouts than the hardware watchdog,
|
|
* this function is called regularly (via the background channel). This
|
|
* should be avoided by setting CONFIG_TASK_WDT_MIN_TIMEOUT to the minimum
|
|
* task watchdog timeout used in the application.
|
|
*
|
|
* @param timer_id Pointer to the timer which called the function
|
|
*/
|
|
static void task_wdt_trigger(struct k_timer *timer_id)
|
|
{
|
|
uintptr_t channel_id = (uintptr_t)k_timer_user_data_get(timer_id);
|
|
bool bg_channel = IS_ENABLED(CONFIG_TASK_WDT_HW_FALLBACK) &&
|
|
(channel_id == TASK_WDT_BACKGROUND_CHANNEL);
|
|
|
|
/* If the timeout expired for the background channel (so the hardware
|
|
* watchdog needs to be fed) or for a channel that has been deleted,
|
|
* only schedule a new timeout (the hardware watchdog, if used, will be
|
|
* fed right after that new timeout is scheduled).
|
|
*/
|
|
if (bg_channel || channels[channel_id].reload_period == 0) {
|
|
schedule_next_timeout(sys_clock_tick_get());
|
|
return;
|
|
}
|
|
|
|
if (channels[channel_id].callback) {
|
|
channels[channel_id].callback(channel_id,
|
|
channels[channel_id].user_data);
|
|
} else {
|
|
sys_reboot(SYS_REBOOT_COLD);
|
|
}
|
|
}
|
|
|
|
int task_wdt_init(const struct device *hw_wdt)
|
|
{
|
|
if (hw_wdt) {
|
|
#ifdef CONFIG_TASK_WDT_HW_FALLBACK
|
|
struct wdt_timeout_cfg wdt_config;
|
|
|
|
wdt_config.flags = WDT_FLAG_RESET_SOC;
|
|
wdt_config.window.min = 0U;
|
|
wdt_config.window.max = CONFIG_TASK_WDT_MIN_TIMEOUT +
|
|
CONFIG_TASK_WDT_HW_FALLBACK_DELAY;
|
|
wdt_config.callback = NULL;
|
|
|
|
hw_wdt_dev = hw_wdt;
|
|
hw_wdt_channel = wdt_install_timeout(hw_wdt_dev, &wdt_config);
|
|
if (hw_wdt_channel < 0) {
|
|
LOG_ERR("hw_wdt install timeout failed: %d", hw_wdt_channel);
|
|
return hw_wdt_channel;
|
|
}
|
|
#else
|
|
return -ENOTSUP;
|
|
#endif
|
|
}
|
|
|
|
k_timer_init(&timer, task_wdt_trigger, NULL);
|
|
schedule_next_timeout(sys_clock_tick_get());
|
|
|
|
task_wdt_initialized = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int task_wdt_add(uint32_t reload_period, task_wdt_callback_t callback,
|
|
void *user_data)
|
|
{
|
|
k_spinlock_key_t key;
|
|
|
|
if (reload_period == 0) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* k_spin_lock instead of k_sched_lock required here to avoid being interrupted by a
|
|
* triggering other task watchdog channel (executed in ISR context).
|
|
*/
|
|
key = k_spin_lock(&channels_lock);
|
|
|
|
/* look for unused channel (reload_period set to 0) */
|
|
for (int id = 0; id < ARRAY_SIZE(channels); id++) {
|
|
if (channels[id].reload_period == 0) {
|
|
channels[id].reload_period = reload_period;
|
|
channels[id].user_data = user_data;
|
|
channels[id].timeout_abs_ticks = K_TICKS_FOREVER;
|
|
channels[id].callback = callback;
|
|
|
|
#ifdef CONFIG_TASK_WDT_HW_FALLBACK
|
|
if (!hw_wdt_started && hw_wdt_dev) {
|
|
/* also start fallback hw wdt */
|
|
wdt_setup(hw_wdt_dev,
|
|
WDT_OPT_PAUSE_HALTED_BY_DBG
|
|
#ifdef CONFIG_TASK_WDT_HW_FALLBACK_PAUSE_IN_SLEEP
|
|
| WDT_OPT_PAUSE_IN_SLEEP
|
|
#endif
|
|
);
|
|
hw_wdt_started = true;
|
|
}
|
|
#endif
|
|
/* must be called after hw wdt has been started */
|
|
task_wdt_feed(id);
|
|
|
|
k_spin_unlock(&channels_lock, key);
|
|
|
|
return id;
|
|
}
|
|
}
|
|
|
|
k_spin_unlock(&channels_lock, key);
|
|
|
|
return -ENOMEM;
|
|
}
|
|
|
|
int task_wdt_delete(int channel_id)
|
|
{
|
|
k_spinlock_key_t key;
|
|
|
|
if (channel_id < 0 || channel_id >= ARRAY_SIZE(channels)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
key = k_spin_lock(&channels_lock);
|
|
|
|
channels[channel_id].reload_period = 0;
|
|
|
|
k_spin_unlock(&channels_lock, key);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int task_wdt_feed(int channel_id)
|
|
{
|
|
int64_t current_ticks;
|
|
|
|
if (channel_id < 0 || channel_id >= ARRAY_SIZE(channels)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* We need a critical section instead of a mutex while updating the
|
|
* channels array in order to prevent priority inversion. Otherwise,
|
|
* a low priority thread could be preempted before releasing the mutex
|
|
* and block a high priority thread that wants to feed its task wdt.
|
|
*/
|
|
k_sched_lock();
|
|
|
|
current_ticks = sys_clock_tick_get();
|
|
|
|
/* feed the specified channel */
|
|
channels[channel_id].timeout_abs_ticks = current_ticks +
|
|
k_ms_to_ticks_ceil64(channels[channel_id].reload_period);
|
|
|
|
schedule_next_timeout(current_ticks);
|
|
|
|
k_sched_unlock();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void task_wdt_suspend(void)
|
|
{
|
|
k_spinlock_key_t key;
|
|
|
|
/*
|
|
* Allow the function to be called from a custom PM policy callback, even when
|
|
* the Task Watchdog was not initialized yet.
|
|
*/
|
|
if (!task_wdt_initialized) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Prevent all task watchdog channels from triggering.
|
|
* Protect the timer access with the spinlock to avoid the timer being started
|
|
* concurrently by a call to schedule_next_timeout().
|
|
*/
|
|
key = k_spin_lock(&channels_lock);
|
|
k_timer_stop(&timer);
|
|
k_spin_unlock(&channels_lock, key);
|
|
|
|
#ifdef CONFIG_TASK_WDT_HW_FALLBACK
|
|
/*
|
|
* Give a whole hardware watchdog timer period of time to the application to put
|
|
* the system in a suspend mode that will pause the hardware watchdog.
|
|
*/
|
|
if (hw_wdt_started) {
|
|
wdt_feed(hw_wdt_dev, hw_wdt_channel);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void task_wdt_resume(void)
|
|
{
|
|
k_spinlock_key_t key;
|
|
int64_t current_ticks;
|
|
|
|
/*
|
|
* Allow the function to be called from a custom PM policy callback, even when
|
|
* the Task Watchdog was not initialized yet.
|
|
*/
|
|
if (!task_wdt_initialized) {
|
|
return;
|
|
}
|
|
|
|
key = k_spin_lock(&channels_lock);
|
|
|
|
/*
|
|
* Feed all enabled channels, so the application threads have time to resume
|
|
* feeding the channels by themselves.
|
|
*/
|
|
current_ticks = sys_clock_tick_get();
|
|
for (size_t id = 0; id < ARRAY_SIZE(channels); id++) {
|
|
if (channels[id].reload_period != 0) {
|
|
channels[id].timeout_abs_ticks = current_ticks +
|
|
k_ms_to_ticks_ceil64(channels[id].reload_period);
|
|
}
|
|
}
|
|
|
|
/* Restart the Task Watchdog timer */
|
|
schedule_next_timeout(current_ticks);
|
|
|
|
k_spin_unlock(&channels_lock, key);
|
|
}
|