Skip to content

Orion O6 / CIX P1 SBSA watchdog refresh frame appears non-functional: WDIOC_KEEPALIVE does not refresh, WDIOC_SETTIMEOUT does #25

@qqice

Description

@qqice

Summary

On Radxa Orion O6 with CIX P1, using the official Debian 13 image and the official CIX 7.0 mainline kernel, the SBSA Generic Watchdog is successfully enumerated through ACPI GTDT and exposed as /dev/watchdog0. The watchdog can be opened, armed, configured with a longer timeout, queried through WDIOC_GETTIMELEFT, and it can reset the board when the counter expires. However, the standard watchdog keepalive path does not appear to work: both userspace write ping and WDIOC_KEEPALIVE return successfully, but the watchdog counter continues decreasing and is not refreshed.

Interestingly, repeatedly calling WDIOC_SETTIMEOUT with the same timeout value does refresh the counter. This suggests that the watchdog control path is working, while the SBSA watchdog refresh frame or WRR access path may not be functional. This may indicate an incorrect ACPI GTDT refresh frame resource, an MMIO access/permission issue, or a platform-specific incompatibility in the current SBSA watchdog description.

Environment

Board: Radxa Orion O6 16GB
SoC: CIX P1
OS: Official Debian 13 image
BIOS: 1.2.1
Kernel: Official CIX 7.0.0-rc5 mainline kernel
Boot mode: ACPI / UEFI
Watchdog driver: sbsa-gwdt
Watchdog device: /dev/watchdog0

Please find the exact environment information below:

uname -a
Linux orion-o6 7.0.0-generic #1 SMP PREEMPT Tue Apr 21 05:59:24 EDT 2026 aarch64 GNU/Linux
cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-7.0.0-generic root=UUID=a8a1d2fa-cbef-44eb-abba-bc9a391a5109 ro quiet pcie_aspm=off nvidia.NVreg_EnableGpuFirmware=0 acpi=force clk_ignore_unused

Kernel detection log

The kernel detects one SBSA Generic Watchdog through ACPI GTDT and initializes sbsa-gwdt successfully.

sudo dmesg | grep -Ei 'ACPI GTDT|sbsa|gwdt|watchdog'
[    0.107239] ARMH0011:02: ttyAMA0 at MMIO 0x40d0000 (irq = 21, base_baud = 0) is a SBSA
[    0.326141] ACPI GTDT: found 1 SBSA generic Watchdog(s).
[    1.580989] sbsa-gwdt sbsa-gwdt.0: Initialized with 10s timeout @ 1000000000 Hz, action=0.

wdctl output

wdctl reports the device as SBSA Generic Watchdog. The device supports keepalive ping and magic close. The timeout can also be changed successfully.

sudo wdctl /dev/watchdog0
Device:        /dev/watchdog0
Identity:      SBSA Generic Watchdog [version 0]
Timeout:       300 seconds
Timeleft:      256 seconds
Pre-timeout:    0 seconds
Pre-timeout governor: noop
Available pre-timeout governors: noop
FLAG           DESCRIPTION                   STATUS BOOT-STATUS
CARDRESET      Card previously reset the CPU      0           0
KEEPALIVEPING  Keep alive ping reply              1           0
MAGICCLOSE     Supports magic close char          0           0
SETTIMEOUT     Set timeout (in seconds)           0           0

Problem 1: systemd can arm the watchdog, but the system still resets

When enabling systemd hardware watchdog with RuntimeWatchdogSec, systemd successfully opens the device and configures the hardware timeout. However, the watchdog counter appears not to be refreshed afterwards, and the board eventually resets.

Example systemd configuration used:

[Manager]
WatchdogDevice=/dev/watchdog0
RuntimeWatchdogSec=30s
RebootWatchdogSec=off
KExecWatchdogSec=off

Problem 2: userspace write ping does not refresh the watchdog

The following simple userspace test opens /dev/watchdog0, writes to the watchdog device every 10 seconds, and reads timeleft from sysfs. The watchdog becomes active, but timeleft continues decreasing and does not jump back to the timeout value after each write.

sudo bash -c '
exec 3>/dev/watchdog0
echo "opened watchdog"
for i in $(seq 1 20); do
  printf "." >&3
  echo
  date
  echo -n "timeleft="
  cat /sys/class/watchdog/watchdog0/timeleft
  sleep 10
done
echo "closing watchdog fd"
printf V >&3
exec 3>&-
'
opened watchdog

Mon May 11 12:55:42 PM PDT 2026
timeleft=299

Mon May 11 12:55:52 PM PDT 2026
timeleft=289

Mon May 11 12:56:02 PM PDT 2026
timeleft=279

Mon May 11 12:56:12 PM PDT 2026
timeleft=269

Mon May 11 12:56:22 PM PDT 2026
timeleft=259

Mon May 11 12:56:32 PM PDT 2026
timeleft=249

Mon May 11 12:56:42 PM PDT 2026
timeleft=239

Mon May 11 12:56:52 PM PDT 2026
timeleft=229

Mon May 11 12:57:02 PM PDT 2026
timeleft=219

Mon May 11 12:57:12 PM PDT 2026
timeleft=209

Mon May 11 12:57:22 PM PDT 2026
timeleft=199

Mon May 11 12:57:32 PM PDT 2026
timeleft=189

Mon May 11 12:57:42 PM PDT 2026
timeleft=179

Mon May 11 12:57:52 PM PDT 2026
timeleft=169

Mon May 11 12:58:02 PM PDT 2026
timeleft=159

Mon May 11 12:58:12 PM PDT 2026
timeleft=149

Mon May 11 12:58:22 PM PDT 2026
timeleft=139

Mon May 11 12:58:32 PM PDT 2026
timeleft=129

Mon May 11 12:58:42 PM PDT 2026
timeleft=119

Mon May 11 12:58:52 PM PDT 2026
timeleft=109
closing watchdog fd

Expected behavior: each write ping should refresh the watchdog counter, so timeleft should jump back close to the configured timeout.

Actual behavior: timeleft keeps decreasing monotonically.

Problem 3: WDIOC_KEEPALIVE returns success but does not refresh the counter

The following C test explicitly sets the timeout to 300 seconds, then calls WDIOC_KEEPALIVE every 10 seconds. The ioctl returns success, but WDIOC_GETTIMELEFT shows that the counter is not refreshed.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <linux/watchdog.h>

static int fd = -1;

static void magic_close(void) {
    if (fd >= 0) {
        write(fd, "V", 1);
        close(fd);
        fd = -1;
    }
}

static void on_signal(int sig) {
    fprintf(stderr, "\nCaught signal %d, magic-closing watchdog\n", sig);
    magic_close();
    _exit(128 + sig);
}

static int get_timeleft(void) {
    int t = -1;
    if (ioctl(fd, WDIOC_GETTIMELEFT, &t) < 0) {
        perror("WDIOC_GETTIMELEFT");
        return -1;
    }
    return t;
}

int main(int argc, char **argv) {
    int requested_timeout = 300;
    int interval = 10;
    int rounds = 20;

    if (argc >= 2)
        requested_timeout = atoi(argv[1]);
    if (argc >= 3)
        interval = atoi(argv[2]);
    if (argc >= 4)
        rounds = atoi(argv[3]);

    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    fd = open("/dev/watchdog0", O_RDWR | O_CLOEXEC);
    if (fd < 0) {
        perror("open /dev/watchdog0");
        return 1;
    }

    printf("Opened /dev/watchdog0\n");

    int timeout = requested_timeout;
    if (ioctl(fd, WDIOC_SETTIMEOUT, &timeout) < 0) {
        perror("WDIOC_SETTIMEOUT");
        fprintf(stderr, "Failed to set timeout. Magic-closing watchdog and exiting.\n");
        magic_close();
        return 1;
    }

    printf("Requested timeout=%d, actual timeout=%d\n", requested_timeout, timeout);

    if (timeout < 60) {
        fprintf(stderr, "Actual timeout is too short for safe testing. Magic-closing watchdog and exiting.\n");
        magic_close();
        return 1;
    }

    int got_timeout = -1;
    if (ioctl(fd, WDIOC_GETTIMEOUT, &got_timeout) == 0)
        printf("WDIOC_GETTIMEOUT reports timeout=%d\n", got_timeout);
    else
        perror("WDIOC_GETTIMEOUT");

    for (int i = 0; i < rounds; i++) {
        sleep(interval);

        int before = get_timeleft();

        int dummy = 0;
        int ret = ioctl(fd, WDIOC_KEEPALIVE, &dummy);
        if (ret < 0)
            printf("[%02d] KEEPALIVE failed: %s\n", i + 1, strerror(errno));
        else
            printf("[%02d] KEEPALIVE ok\n", i + 1);

        int after = get_timeleft();

        printf("[%02d] timeleft before=%d, after keepalive=%d\n",
               i + 1, before, after);
        fflush(stdout);
    }

    printf("Writing magic close 'V' and closing watchdog fd\n");
    magic_close();
    return 0;
}

Compile and run:

gcc /tmp/wd-ioctl-test.c -o /tmp/wd-ioctl-test
sudo /tmp/wd-ioctl-test 300 10 4

Observed output:

Requested timeout=300, actual timeout=300
WDIOC_GETTIMEOUT reports timeout=300
[01] KEEPALIVE ok
[01] timeleft before=289, after keepalive=289
[02] KEEPALIVE ok
[02] timeleft before=279, after keepalive=279
[03] KEEPALIVE ok
[03] timeleft before=269, after keepalive=269
[04] KEEPALIVE ok
[04] timeleft before=259, after keepalive=259

Expected behavior: after WDIOC_KEEPALIVE, timeleft should return close to 300 seconds.

Actual behavior: WDIOC_KEEPALIVE returns success, but timeleft remains unchanged and keeps decreasing.

Problem 4: WDIOC_SETTIMEOUT refreshes the counter

As a comparison, repeatedly calling WDIOC_SETTIMEOUT with the same timeout value does refresh the counter. This suggests that the watchdog control frame is functional.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <linux/watchdog.h>

static int fd = -1;

static void magic_close(void) {
    if (fd >= 0) {
        write(fd, "V", 1);
        close(fd);
        fd = -1;
    }
}

static void on_signal(int sig) {
    fprintf(stderr, "\nCaught signal %d, magic-closing watchdog\n", sig);
    magic_close();
    _exit(128 + sig);
}

static int get_timeleft(void) {
    int t = -1;
    if (ioctl(fd, WDIOC_GETTIMELEFT, &t) < 0) {
        perror("WDIOC_GETTIMELEFT");
        return -1;
    }
    return t;
}

int main(void) {
    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    fd = open("/dev/watchdog0", O_RDWR | O_CLOEXEC);
    if (fd < 0) {
        perror("open /dev/watchdog0");
        return 1;
    }

    int timeout = 300;
    if (ioctl(fd, WDIOC_SETTIMEOUT, &timeout) < 0) {
        perror("initial WDIOC_SETTIMEOUT");
        magic_close();
        return 1;
    }

    printf("initial timeout=%d\n", timeout);

    for (int i = 0; i < 20; i++) {
        sleep(10);

        int before = get_timeleft();

        int t = 300;
        int ret = ioctl(fd, WDIOC_SETTIMEOUT, &t);

        int after = get_timeleft();

        if (ret < 0)
            printf("[%02d] SETTIMEOUT failed: %s\n", i + 1, strerror(errno));
        else
            printf("[%02d] SETTIMEOUT ok, actual timeout=%d\n", i + 1, t);

        printf("[%02d] timeleft before=%d, after settimeout=%d\n",
               i + 1, before, after);
        fflush(stdout);
    }

    printf("Writing magic close 'V' and closing watchdog fd\n");
    magic_close();
    return 0;
}

Compile and run:

gcc /tmp/wd-settimeout-refresh-test.c -o /tmp/wd-settimeout-refresh-test
sudo /tmp/wd-settimeout-refresh-test

Observed output:

initial timeout=300
[01] SETTIMEOUT ok, actual timeout=300
[01] timeleft before=290, after settimeout=299
[02] SETTIMEOUT ok, actual timeout=300
[02] timeleft before=290, after settimeout=299
[03] SETTIMEOUT ok, actual timeout=300
[03] timeleft before=290, after settimeout=299

This behavior strongly suggests that the control path is working, but the standard SBSA watchdog refresh path used by WDIOC_KEEPALIVE is not refreshing the hardware counter.

Expected behavior

Opening /dev/watchdog0 and periodically calling either userspace write ping or WDIOC_KEEPALIVE should refresh the watchdog counter. The value reported by WDIOC_GETTIMELEFT or /sys/class/watchdog/watchdog0/timeleft should jump back close to the configured timeout after each keepalive.

systemd RuntimeWatchdogSec should also be usable, because systemd relies on the standard Linux watchdog keepalive mechanism.

Actual behavior

The watchdog can be armed and can reset the board after timeout expiration. The timeout can be changed successfully, and WDIOC_SETTIMEOUT appears to reload the counter. However, both userspace write ping and WDIOC_KEEPALIVE do not refresh the counter, even though WDIOC_KEEPALIVE returns success.

This makes the watchdog dangerous to enable, because it behaves like a countdown reset timer rather than a usable watchdog.

Suspected cause

This looks like a possible issue in the SBSA watchdog refresh path on Orion O6 / CIX P1. Since WDIOC_SETTIMEOUT refreshes the counter but WDIOC_KEEPALIVE does not, the control frame appears to work while the refresh frame may not.

Possible causes include:

  1. The ACPI GTDT SBSA watchdog refresh frame base address may be incorrect.
  2. The refresh frame MMIO region may not be accessible to the OS.
  3. The platform firmware may expose an SBSA watchdog resource that does not fully match the standard Linux sbsa-gwdt driver's expectations.
  4. The sbsa-gwdt driver may need a CIX P1 specific quirk if this hardware requires refreshing through the control frame instead of the standard refresh frame.

Additional ACPI GTDT dump

sudo acpidump -n GTDT -b
iasl -d gtdt.dat
grep -n -i -A120 -B20 'watchdog\|watch dog\|GTDT' gtdt.dsl
1-/*
2- * Intel ACPI Component Architecture
3- * AML/ASL+ Disassembler version 20250404 (64-bit version)
4- * Copyright (c) 2000 - 2025 Intel Corporation
5- * 
6: * Disassembly of gtdt.dat
7- *
8: * ACPI Data Table [GTDT]
9- *
10- * Format: [HexOffset DecimalOffset ByteLength]  FieldName : FieldValue (in hex)
11- */
12-
13:[000h 0000 004h]                   Signature : "GTDT"    [Generic Timer Description Table]
14-[004h 0004 004h]                Table Length : 00000084
15-[008h 0008 001h]                    Revision : 03
16-[009h 0009 001h]                    Checksum : C7
17-[00Ah 0010 006h]                      Oem ID : "CIXTEK"
18-[010h 0016 008h]                Oem Table ID : "SKY1EDK2"
19-[018h 0024 004h]                Oem Revision : 01000101
20-[01Ch 0028 004h]             Asl Compiler ID : "CIX "
21-[020h 0032 004h]       Asl Compiler Revision : 00000001
22-
23-[024h 0036 008h]       Counter Block Address : FFFFFFFFFFFFFFFF
24-[02Ch 0044 004h]                    Reserved : 00000000
25-
26-[030h 0048 004h]        Secure EL1 Interrupt : 0000001D
27-[034h 0052 004h]   EL1 Flags (decoded below) : 00000002
28-                                Trigger Mode : 0
29-                                    Polarity : 1
30-                                   Always On : 0
31-
32-[038h 0056 004h]    Non-Secure EL1 Interrupt : 0000001E
33-[03Ch 0060 004h]  NEL1 Flags (decoded below) : 00000002
34-                                Trigger Mode : 0
35-                                    Polarity : 1
36-                                   Always On : 0
37-
38-[040h 0064 004h]     Virtual Timer Interrupt : 0000001B
39-[044h 0068 004h]    VT Flags (decoded below) : 00000002
40-                                Trigger Mode : 0
41-                                    Polarity : 1
42-                                   Always On : 0
43-
44-[048h 0072 004h]    Non-Secure EL2 Interrupt : 0000001A
45-[04Ch 0076 004h]  NEL2 Flags (decoded below) : 00000002
46-                                Trigger Mode : 0
47-                                    Polarity : 1
48-                                   Always On : 0
49-[050h 0080 008h]  Counter Read Block Address : FFFFFFFFFFFFFFFF
50-
51-[058h 0088 004h]        Platform Timer Count : 00000001
52-[05Ch 0092 004h]       Platform Timer Offset : 00000068
53-[060h 0096 004h]      Virtual EL2 Timer GSIV : 0000001C
54-[064h 0100 004h]     Virtual EL2 Timer Flags : 00000002
55-
56:[068h 0104 001h]               Subtable Type : 01 [Generic Watchdog Timer]
57-[069h 0105 002h]                      Length : 001C
58-[06Bh 0107 001h]                    Reserved : 00
59-[06Ch 0108 008h]       Refresh Frame Address : 0000000016008000
60-[074h 0116 008h]       Control Frame Address : 0000000016003000
61-[07Ch 0124 004h]             Timer Interrupt : 00000198
62-[080h 0128 004h] Timer Flags (decoded below) : 00000000
63-                                Trigger Mode : 0
64-                                    Polarity : 0
65-                                    Security : 0
66-
67-Raw Table Data: Length 132 (0x84)
68-
69:    0000: 47 54 44 54 84 00 00 00 03 C7 43 49 58 54 45 4B  // GTDT......CIXTEK
70-    0010: 53 4B 59 31 45 44 4B 32 01 01 00 01 43 49 58 20  // SKY1EDK2....CIX 
71-    0020: 01 00 00 00 FF FF FF FF FF FF FF FF 00 00 00 00  // ................
72-    0030: 1D 00 00 00 02 00 00 00 1E 00 00 00 02 00 00 00  // ................
73-    0040: 1B 00 00 00 02 00 00 00 1A 00 00 00 02 00 00 00  // ................
74-    0050: FF FF FF FF FF FF FF FF 01 00 00 00 68 00 00 00  // ............h...
75-    0060: 1C 00 00 00 02 00 00 00 01 1C 00 00 00 80 00 16  // ................
76-    0070: 00 00 00 00 00 30 00 16 00 00 00 00 98 01 00 00  // .....0..........
77-    0080: 00 00 00 00                                      // ....

Linux resource mapping

The ACPI GTDT reports the following watchdog frames:

Refresh Frame Address : 0x16008000
Control Frame Address : 0x16003000

Linux also registers both MMIO regions under sbsa-gwdt.0:

ls -l /sys/bus/platform/devices/ | grep -i sbsa
cat /sys/bus/platform/devices/sbsa-gwdt.0/resource 2>/dev/null || true
cat /proc/iomem | grep -i -E '16003000|16008000|watchdog|gwdt|sbsa'
lrwxrwxrwx 1 root root 0 May 11 13:41 sbsa-gwdt.0 -> ../../../devices/platform/sbsa-gwdt.0
16003000-16003fff : sbsa-gwdt.0
  16003000-16003fff : sbsa-gwdt.0 sbsa-gwdt.0
16008000-16008fff : sbsa-gwdt.0
  16008000-16008fff : sbsa-gwdt.0 sbsa-gwdt.0

This confirms that the watchdog platform device is created and both the control frame and refresh frame MMIO regions are reserved by sbsa-gwdt.0.

However, runtime behavior indicates that only the control path is functional. WDIOC_SETTIMEOUT refreshes/reloads the counter, while WDIOC_KEEPALIVE does not. Therefore, this no longer looks like a userspace, systemd, or resource-order issue. The issue appears to be specifically related to the SBSA watchdog refresh frame at 0x16008000, its access permissions, or the platform's implementation of the standard WRR refresh mechanism.

Request

Could you please check whether the ACPI GTDT watchdog refresh frame address and access permissions are correct on Orion O6 / CIX P1? It would also be helpful to confirm whether the SBSA Generic Watchdog on this platform is expected to be refreshed through the standard WRR refresh frame, or whether a platform-specific workaround is required.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions