linux: Initial support for running Klipper in a Linux real-time process
Add support for compiling the Klipper micro-controller code as a real-time process capable of running on standard Linux systems. Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
parent
3ccecc568d
commit
d851882278
|
@ -399,7 +399,8 @@ class MCU:
|
|||
# Serial port
|
||||
self._serialport = config.get('serial', '/dev/ttyS0')
|
||||
baud = 0
|
||||
if not self._serialport.startswith("/dev/rpmsg_"):
|
||||
if not (self._serialport.startswith("/dev/rpmsg_")
|
||||
or self._serialport.startswith("/tmp/klipper_host_")):
|
||||
baud = config.getint('baud', 250000, minval=2400)
|
||||
self._serial = serialhdl.SerialReader(
|
||||
printer.reactor, self._serialport, baud)
|
||||
|
|
|
@ -10,6 +10,8 @@ choice
|
|||
bool "SAM3x8e (Arduino Due)"
|
||||
config MACH_PRU
|
||||
bool "Beaglebone PRU"
|
||||
config MACH_LINUX
|
||||
bool "Linux process"
|
||||
config MACH_SIMU
|
||||
bool "Host simulator"
|
||||
endchoice
|
||||
|
@ -17,6 +19,7 @@ endchoice
|
|||
source "src/avr/Kconfig"
|
||||
source "src/sam3x8e/Kconfig"
|
||||
source "src/pru/Kconfig"
|
||||
source "src/linux/Kconfig"
|
||||
source "src/simulator/Kconfig"
|
||||
|
||||
# The HAVE_GPIO_x options allow boards to disable support for some
|
||||
|
|
|
@ -186,7 +186,6 @@ command_sendf(const struct command_encoder *ce, ...)
|
|||
va_end(args);
|
||||
|
||||
writeb(&in_sendf, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# Kconfig settings for compiling and running the micro-controller code
|
||||
# in a Linux process
|
||||
|
||||
if MACH_LINUX
|
||||
|
||||
config BOARD_DIRECTORY
|
||||
string
|
||||
default "linux"
|
||||
|
||||
config CLOCK_FREQ
|
||||
int
|
||||
default 20000000
|
||||
|
||||
endif
|
|
@ -0,0 +1,8 @@
|
|||
# Additional linux build rules
|
||||
|
||||
dirs-y += src/linux src/generic
|
||||
|
||||
src-y += linux/main.c linux/timer.c linux/console.c linux/watchdog.c
|
||||
src-y += generic/crc16_ccitt.c generic/alloc.c
|
||||
|
||||
CFLAGS_klipper.elf += -lutil
|
|
@ -0,0 +1,210 @@
|
|||
// TTY based IO
|
||||
//
|
||||
// Copyright (C) 2017 Kevin O'Connor <kevin@koconnor.net>
|
||||
//
|
||||
// This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
#include <errno.h> // errno
|
||||
#include <fcntl.h> // fcntl
|
||||
#include <poll.h> // poll
|
||||
#include <pty.h> // openpty
|
||||
#include <stdio.h> // fprintf
|
||||
#include <string.h> // memmove
|
||||
#include <sys/stat.h> // chmod
|
||||
#include <sys/timerfd.h> // timerfd_create
|
||||
#include <time.h> // struct timespec
|
||||
#include <unistd.h> // ttyname
|
||||
#include "board/irq.h" // irq_poll
|
||||
#include "board/misc.h" // console_sendf
|
||||
#include "command.h" // command_find_block
|
||||
#include "internal.h" // console_setup
|
||||
#include "sched.h" // sched_wake_task
|
||||
|
||||
static struct pollfd main_pfd[2];
|
||||
#define MP_TIMER_IDX 0
|
||||
#define MP_TTY_IDX 1
|
||||
|
||||
// Report 'errno' in a message written to stderr
|
||||
static void
|
||||
report_errno(char *where, int rc)
|
||||
{
|
||||
int e = errno;
|
||||
fprintf(stderr, "Got error %d in %s: (%d)%s\n", rc, where, e, strerror(e));
|
||||
}
|
||||
|
||||
|
||||
/****************************************************************
|
||||
* Setup
|
||||
****************************************************************/
|
||||
|
||||
static int
|
||||
set_non_blocking(int fd)
|
||||
{
|
||||
int flags = fcntl(fd, F_GETFL);
|
||||
if (flags < 0) {
|
||||
report_errno("fcntl getfl", flags);
|
||||
return -1;
|
||||
}
|
||||
int ret = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
if (ret < 0) {
|
||||
report_errno("fcntl setfl", flags);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
set_close_on_exec(int fd)
|
||||
{
|
||||
int ret = fcntl(fd, F_SETFD, FD_CLOEXEC);
|
||||
if (ret < 0) {
|
||||
report_errno("fcntl set cloexec", ret);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
console_setup(char *name)
|
||||
{
|
||||
// Open pseudo-tty
|
||||
struct termios ti;
|
||||
memset(&ti, 0, sizeof(ti));
|
||||
int mfd, sfd, ret = openpty(&mfd, &sfd, NULL, &ti, NULL);
|
||||
if (ret) {
|
||||
report_errno("openpty", ret);
|
||||
return -1;
|
||||
}
|
||||
ret = set_non_blocking(mfd);
|
||||
if (ret)
|
||||
return -1;
|
||||
ret = set_close_on_exec(mfd);
|
||||
if (ret)
|
||||
return -1;
|
||||
ret = set_close_on_exec(sfd);
|
||||
if (ret)
|
||||
return -1;
|
||||
main_pfd[MP_TTY_IDX].fd = mfd;
|
||||
main_pfd[MP_TTY_IDX].events = POLLIN;
|
||||
|
||||
// Create symlink to tty
|
||||
unlink(name);
|
||||
char *tname = ttyname(sfd);
|
||||
if (!tname) {
|
||||
report_errno("ttyname", 0);
|
||||
return -1;
|
||||
}
|
||||
ret = symlink(tname, name);
|
||||
if (ret) {
|
||||
report_errno("symlink", ret);
|
||||
return -1;
|
||||
}
|
||||
ret = chmod(tname, 0660);
|
||||
if (ret) {
|
||||
report_errno("chmod", ret);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Make sure stderr is non-blocking
|
||||
ret = set_non_blocking(STDERR_FILENO);
|
||||
if (ret)
|
||||
return -1;
|
||||
|
||||
// Create sleep wakeup timer fd
|
||||
ret = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC|TFD_NONBLOCK);
|
||||
if (ret < 0) {
|
||||
report_errno("timerfd_create", ret);
|
||||
return -1;
|
||||
}
|
||||
main_pfd[MP_TIMER_IDX].fd = ret;
|
||||
main_pfd[MP_TIMER_IDX].events = POLLIN;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/****************************************************************
|
||||
* Console handling
|
||||
****************************************************************/
|
||||
|
||||
static struct task_wake console_wake;
|
||||
static char receive_buf[4096];
|
||||
static int receive_pos;
|
||||
|
||||
// Process any incoming commands
|
||||
void
|
||||
console_task(void)
|
||||
{
|
||||
if (!sched_check_wake(&console_wake))
|
||||
return;
|
||||
|
||||
// Read data
|
||||
int ret = read(main_pfd[MP_TTY_IDX].fd, &receive_buf[receive_pos]
|
||||
, sizeof(receive_buf) - receive_pos);
|
||||
if (ret < 0) {
|
||||
if (errno == EWOULDBLOCK) {
|
||||
ret = 0;
|
||||
} else {
|
||||
report_errno("read", ret);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (ret == 15 && receive_buf[receive_pos+14] == '\n'
|
||||
&& memcmp(&receive_buf[receive_pos], "FORCE_SHUTDOWN\n", 15) == 0)
|
||||
shutdown("Force shutdown command");
|
||||
|
||||
// Find and dispatch message blocks in the input
|
||||
int len = receive_pos + ret;
|
||||
uint8_t pop_count, msglen = len > MESSAGE_MAX ? MESSAGE_MAX : len;
|
||||
ret = command_find_block(receive_buf, msglen, &pop_count);
|
||||
if (ret > 0)
|
||||
command_dispatch(receive_buf, pop_count);
|
||||
if (ret) {
|
||||
len -= pop_count;
|
||||
if (len) {
|
||||
memmove(receive_buf, &receive_buf[pop_count], len);
|
||||
sched_wake_task(&console_wake);
|
||||
}
|
||||
}
|
||||
receive_pos = len;
|
||||
}
|
||||
DECL_TASK(console_task);
|
||||
|
||||
// Encode and transmit a "response" message
|
||||
void
|
||||
console_sendf(const struct command_encoder *ce, va_list args)
|
||||
{
|
||||
// Generate message
|
||||
char buf[MESSAGE_MAX];
|
||||
uint8_t msglen = command_encodef(buf, ce, args);
|
||||
command_add_frame(buf, msglen);
|
||||
|
||||
// Transmit message
|
||||
int ret = write(main_pfd[MP_TTY_IDX].fd, buf, msglen);
|
||||
if (ret < 0)
|
||||
report_errno("write", ret);
|
||||
}
|
||||
|
||||
// Sleep until the specified time (waking early for console input if needed)
|
||||
void
|
||||
console_sleep(struct timespec ts)
|
||||
{
|
||||
struct itimerspec its;
|
||||
its.it_interval = (struct timespec){0, 0};
|
||||
its.it_value = ts;
|
||||
int ret = timerfd_settime(main_pfd[MP_TIMER_IDX].fd, TFD_TIMER_ABSTIME
|
||||
, &its, NULL);
|
||||
if (ret < 0) {
|
||||
report_errno("timerfd_settime", ret);
|
||||
return;
|
||||
}
|
||||
ret = poll(main_pfd, ARRAY_SIZE(main_pfd), -1);
|
||||
if (ret <= 0) {
|
||||
report_errno("poll main_pfd", ret);
|
||||
return;
|
||||
}
|
||||
if (main_pfd[MP_TTY_IDX].revents)
|
||||
sched_wake_task(&console_wake);
|
||||
if (main_pfd[MP_TIMER_IDX].revents)
|
||||
irq_poll();
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
#ifndef __LINUX_INTERNAL_H
|
||||
#define __LINUX_INTERNAL_H
|
||||
// Local definitions for micro-controllers running on linux
|
||||
|
||||
#include <time.h> // struct timespec
|
||||
|
||||
// console.c
|
||||
int console_setup(char *name);
|
||||
void console_sleep(struct timespec ts);
|
||||
|
||||
// timer.c
|
||||
int timer_check_periodic(struct timespec *ts);
|
||||
|
||||
// watchdog.c
|
||||
int watchdog_setup(void);
|
||||
|
||||
#endif // internal.h
|
|
@ -0,0 +1,99 @@
|
|||
// Main starting point for micro-controller code running on linux systems
|
||||
//
|
||||
// Copyright (C) 2017 Kevin O'Connor <kevin@koconnor.net>
|
||||
//
|
||||
// This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
#include </usr/include/sched.h> // sched_setscheduler
|
||||
#include <stdio.h> // fprintf
|
||||
#include <string.h> // memset
|
||||
#include <unistd.h> // getopt
|
||||
#include "board/misc.h" // console_sendf
|
||||
#include "command.h" // DECL_CONSTANT
|
||||
#include "internal.h" // console_setup
|
||||
#include "sched.h" // sched_main
|
||||
|
||||
DECL_CONSTANT(MCU, "linux");
|
||||
|
||||
|
||||
/****************************************************************
|
||||
* Real-time setup
|
||||
****************************************************************/
|
||||
|
||||
static int
|
||||
realtime_setup(void)
|
||||
{
|
||||
struct sched_param sp;
|
||||
memset(&sp, 0, sizeof(sp));
|
||||
sp.sched_priority = 1;
|
||||
int ret = sched_setscheduler(0, SCHED_FIFO, &sp);
|
||||
if (ret < 0) {
|
||||
report_errno("sched_setscheduler", ret);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/****************************************************************
|
||||
* Restart
|
||||
****************************************************************/
|
||||
|
||||
static char **orig_argv;
|
||||
|
||||
void
|
||||
command_config_reset(uint32_t *args)
|
||||
{
|
||||
if (! sched_is_shutdown())
|
||||
shutdown("config_reset only available when shutdown");
|
||||
int ret = execv(orig_argv[0], orig_argv);
|
||||
report_errno("execv", ret);
|
||||
}
|
||||
DECL_COMMAND_FLAGS(config_reset, HF_IN_SHUTDOWN, "config_reset");
|
||||
|
||||
|
||||
/****************************************************************
|
||||
* Startup
|
||||
****************************************************************/
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
// Parse program args
|
||||
orig_argv = argv;
|
||||
int opt, watchdog = 0, realtime = 0;
|
||||
while ((opt = getopt(argc, argv, "wr")) != -1) {
|
||||
switch (opt) {
|
||||
case 'w':
|
||||
watchdog = 1;
|
||||
break;
|
||||
case 'r':
|
||||
realtime = 1;
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Usage: %s [-w] [-r]\n", argv[0]);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial setup
|
||||
if (watchdog) {
|
||||
int ret = watchdog_setup();
|
||||
if (ret)
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (realtime) {
|
||||
int ret = realtime_setup();
|
||||
if (ret)
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ret = console_setup("/tmp/klipper_host_mcu");
|
||||
if (ret)
|
||||
return -1;
|
||||
|
||||
// Main loop
|
||||
sched_main();
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
// Handling of timers on linux systems
|
||||
//
|
||||
// Copyright (C) 2017 Kevin O'Connor <kevin@koconnor.net>
|
||||
//
|
||||
// This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
#include <time.h> // struct timespec
|
||||
#include "autoconf.h" // CONFIG_CLOCK_FREQ
|
||||
#include "board/misc.h" // timer_from_us
|
||||
#include "board/irq.h" // irq_disable
|
||||
#include "basecmd.h" // stats_note_sleep
|
||||
#include "command.h" // DECL_CONSTANT
|
||||
#include "generic/timer_irq.h" // timer_dispatch_many
|
||||
#include "internal.h" // console_sleep
|
||||
#include "sched.h" // DECL_INIT
|
||||
|
||||
|
||||
/****************************************************************
|
||||
* Timespec helpers
|
||||
****************************************************************/
|
||||
|
||||
static uint32_t last_read_time_counter;
|
||||
static struct timespec last_read_time, next_wake_time;
|
||||
static time_t start_sec;
|
||||
|
||||
#define NSECS 1000000000
|
||||
#define NSECS_PER_TICK (NSECS / CONFIG_CLOCK_FREQ)
|
||||
|
||||
// Compare two 'struct timespec' times
|
||||
static inline uint8_t
|
||||
timespec_is_before(struct timespec ts1, struct timespec ts2)
|
||||
{
|
||||
return (ts1.tv_sec < ts2.tv_sec
|
||||
|| (ts1.tv_sec == ts2.tv_sec && ts1.tv_nsec < ts2.tv_nsec));
|
||||
}
|
||||
|
||||
// Convert a 'struct timespec' to a counter value
|
||||
static inline uint32_t
|
||||
timespec_to_time(struct timespec ts)
|
||||
{
|
||||
return ((ts.tv_sec - start_sec) * CONFIG_CLOCK_FREQ
|
||||
+ ts.tv_nsec / NSECS_PER_TICK);
|
||||
}
|
||||
|
||||
// Convert an internal time counter to a 'struct timespec'
|
||||
static inline struct timespec
|
||||
timespec_from_time(uint32_t time)
|
||||
{
|
||||
int32_t counter_diff = time - last_read_time_counter;
|
||||
struct timespec ts;
|
||||
ts.tv_sec = last_read_time.tv_sec;
|
||||
ts.tv_nsec = last_read_time.tv_nsec + counter_diff * NSECS_PER_TICK;
|
||||
if ((unsigned long)ts.tv_nsec >= NSECS) {
|
||||
if (ts.tv_nsec < 0) {
|
||||
ts.tv_sec--;
|
||||
ts.tv_nsec += NSECS;
|
||||
} else {
|
||||
ts.tv_sec++;
|
||||
ts.tv_nsec -= NSECS;
|
||||
}
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
// Add a given number of nanoseconds to a 'struct timespec'
|
||||
static inline struct timespec
|
||||
timespec_add(struct timespec ts, long ns)
|
||||
{
|
||||
ts.tv_nsec += ns;
|
||||
if (ts.tv_nsec >= NSECS) {
|
||||
ts.tv_sec++;
|
||||
ts.tv_nsec -= NSECS;
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
// Return the current time
|
||||
static struct timespec
|
||||
timespec_read(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts;
|
||||
}
|
||||
|
||||
// Periodically update last_read_time / last_read_time_counter
|
||||
void
|
||||
timespec_update(void)
|
||||
{
|
||||
last_read_time = timespec_read();
|
||||
last_read_time_counter = timespec_to_time(last_read_time);
|
||||
}
|
||||
DECL_TASK(timespec_update);
|
||||
|
||||
// Check if a given time has past
|
||||
int
|
||||
timer_check_periodic(struct timespec *ts)
|
||||
{
|
||||
if (timespec_is_before(next_wake_time, *ts))
|
||||
return 0;
|
||||
*ts = next_wake_time;
|
||||
ts->tv_sec += 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/****************************************************************
|
||||
* Timers
|
||||
****************************************************************/
|
||||
|
||||
DECL_CONSTANT(CLOCK_FREQ, CONFIG_CLOCK_FREQ);
|
||||
|
||||
// Return the number of clock ticks for a given number of microseconds
|
||||
uint32_t
|
||||
timer_from_us(uint32_t us)
|
||||
{
|
||||
return us * (CONFIG_CLOCK_FREQ / 1000000);
|
||||
}
|
||||
|
||||
// Return true if time1 is before time2. Always use this function to
|
||||
// compare times as regular C comparisons can fail if the counter
|
||||
// rolls over.
|
||||
uint8_t
|
||||
timer_is_before(uint32_t time1, uint32_t time2)
|
||||
{
|
||||
return (int32_t)(time1 - time2) < 0;
|
||||
}
|
||||
|
||||
// Return the current time (in clock ticks)
|
||||
uint32_t
|
||||
timer_read_time(void)
|
||||
{
|
||||
return timespec_to_time(timespec_read());
|
||||
}
|
||||
|
||||
// Activate timer dispatch as soon as possible
|
||||
void
|
||||
timer_kick(void)
|
||||
{
|
||||
next_wake_time = last_read_time;
|
||||
}
|
||||
|
||||
static struct timespec timer_repeat_until;
|
||||
#define TIMER_IDLE_REPEAT_NS 500000
|
||||
#define TIMER_REPEAT_NS 100000
|
||||
|
||||
#define TIMER_MIN_TRY_NS 1000
|
||||
#define TIMER_DEFER_REPEAT_NS 5000
|
||||
|
||||
// Invoke timers
|
||||
static void
|
||||
timer_dispatch(void)
|
||||
{
|
||||
struct timespec tru = timer_repeat_until;
|
||||
for (;;) {
|
||||
// Run the next software timer
|
||||
uint32_t next = sched_timer_dispatch();
|
||||
struct timespec nt = timespec_from_time(next);
|
||||
|
||||
struct timespec now = timespec_read();
|
||||
if (!timespec_is_before(nt, timespec_add(now, TIMER_MIN_TRY_NS))) {
|
||||
// Schedule next timer normally.
|
||||
next_wake_time = nt;
|
||||
return;
|
||||
}
|
||||
|
||||
if (unlikely(timespec_is_before(tru, now))) {
|
||||
// Check if there are too many repeat timers
|
||||
if (unlikely(timespec_is_before(timespec_add(nt, 100000000), now))
|
||||
&& !sched_is_shutdown())
|
||||
shutdown("Rescheduled timer in the past");
|
||||
if (sched_tasks_busy()) {
|
||||
timer_repeat_until = timespec_add(now, TIMER_REPEAT_NS);
|
||||
next_wake_time = timespec_add(now, TIMER_DEFER_REPEAT_NS);
|
||||
return;
|
||||
}
|
||||
timer_repeat_until = timespec_add(now, TIMER_IDLE_REPEAT_NS);
|
||||
}
|
||||
|
||||
// Next timer in the past or near future - wait for it to be ready
|
||||
while (unlikely(timespec_is_before(now, nt)))
|
||||
now = timespec_read();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
timer_init(void)
|
||||
{
|
||||
start_sec = timespec_read().tv_sec;
|
||||
timer_repeat_until.tv_sec = start_sec + 2;
|
||||
timespec_update();
|
||||
timer_kick();
|
||||
}
|
||||
DECL_INIT(timer_init);
|
||||
|
||||
|
||||
/****************************************************************
|
||||
* Interrupt wrappers
|
||||
****************************************************************/
|
||||
|
||||
void
|
||||
irq_disable(void)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
irq_enable(void)
|
||||
{
|
||||
}
|
||||
|
||||
irqstatus_t
|
||||
irq_save(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
irq_restore(irqstatus_t flag)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
irq_wait(void)
|
||||
{
|
||||
console_sleep(next_wake_time);
|
||||
}
|
||||
|
||||
void
|
||||
irq_poll(void)
|
||||
{
|
||||
if (!timespec_is_before(timespec_read(), next_wake_time))
|
||||
timer_dispatch();
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// Support for Linux watchdog
|
||||
//
|
||||
// Copyright (C) 2017 Kevin O'Connor <kevin@koconnor.net>
|
||||
//
|
||||
// This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
#include <fcntl.h> // open
|
||||
#include <unistd.h> // write
|
||||
#include "internal.h" // report_errno
|
||||
#include "sched.h" // DECL_TASK
|
||||
|
||||
static int watchdog_fd = -1;
|
||||
|
||||
int
|
||||
watchdog_setup(void)
|
||||
{
|
||||
int ret = open("/dev/watchdog", O_RDWR|O_CLOEXEC);
|
||||
if (ret < 0) {
|
||||
report_errno("watchdog open", ret);
|
||||
return -1;
|
||||
}
|
||||
watchdog_fd = ret;
|
||||
return set_non_blocking(watchdog_fd);
|
||||
}
|
||||
|
||||
void
|
||||
watchdog_task(void)
|
||||
{
|
||||
static struct timespec next_watchdog_time;
|
||||
if (watchdog_fd < 0 || !timer_check_periodic(&next_watchdog_time))
|
||||
return;
|
||||
int ret = write(watchdog_fd, ".", 1);
|
||||
if (ret <= 0)
|
||||
report_errno("watchdog write", ret);
|
||||
}
|
||||
DECL_TASK(watchdog_task);
|
Loading…
Reference in New Issue