| /* |
| * 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; |
| |
| #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()); |
| |
| 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); |
| 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; |
| } |