1
1

realtek: add Hasivo MCU watchdog driver

Add a watchdog driver for the external management MCU on Hasivo /
Horaco network switches, reachable over I2C. Without periodic
keepalive the MCU resets the board every ~3 minutes.

The driver arms the MCU at probe and registers a struct
watchdog_device with WDOG_HW_RUNNING so the watchdog core feeds the
chip via a kernel timer until userspace opens the watchdog node.
Timeout is fixed at 15s; the hardware threshold is baked into MCU
firmware and is not software-configurable.

The I2C address is supplied per-board in the device tree via the
`reg` property. The driver does not constrain or probe a specific
address. Known addresses across current Hasivo / Horaco silicon:

  - 0x6F: Hasivo S1300WP-8XGT-4S+, Hasivo F5800W-12S+,
          Horaco ZX-SW82TS-L2P (default / most common)
  - 0x6E: alternate Hasivo / Horaco variant

The driver, its device-tree binding and the Kconfig/Makefile wiring
are added to the kernel tree as a realtek target patch and exposed as
the kmod-hasivo-mcu-wdt KernelPackage. Keeping the binding in the
kernel tree lets dt_binding_check exercise it during the build and
makes the whole driver easy to drop once it lands upstream.

Tested on Hasivo S1300WP-8XGT-4S+ (RTL9313). Unbinding the driver
causes the MCU to power-cycle the board within ~15s.

Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
Link: https://github.com/openwrt/openwrt/pull/23418
Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
This commit is contained in:
Carlo Szelinsky 2026-05-18 22:41:21 +02:00 committed by Markus Stockhausen
parent c9406df918
commit d048137e85
8 changed files with 301 additions and 0 deletions

View File

@ -6,6 +6,24 @@
# #
MFD_MENU:=MultiFunction Device (MFD) Support MFD_MENU:=MultiFunction Device (MFD) Support
WATCHDOG_MENU:=Watchdog Timer Support
define KernelPackage/hasivo-mcu-wdt
SUBMENU:=$(WATCHDOG_MENU)
TITLE:=Hasivo MCU watchdog driver
KCONFIG:=CONFIG_HASIVO_MCU_WATCHDOG
FILES:=$(LINUX_DIR)/drivers/watchdog/hasivo-mcu-wdt.ko
DEPENDS:=@TARGET_realtek +kmod-i2c-core
AUTOLOAD:=$(call AutoProbe,hasivo-mcu-wdt,1)
endef
define KernelPackage/hasivo-mcu-wdt/description
Hardware watchdog driver for the external management MCU found on
Hasivo / Horaco network switches. Registers a Linux watchdog device;
the kernel watchdog core feeds it automatically via its own timer.
endef
$(eval $(call KernelPackage,hasivo-mcu-wdt))
define KernelPackage/mfd-hasivo-stc8 define KernelPackage/mfd-hasivo-stc8
SUBMENU:=$(MFD_MENU) SUBMENU:=$(MFD_MENU)

View File

@ -0,0 +1,277 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Carlo Szelinsky <github@szelinsky.de>
Date: Sat, 30 May 2026 12:00:00 +0200
Subject: watchdog: add Hasivo MCU watchdog driver
Hardware watchdog driver for the external management MCU found on
Hasivo / Horaco network switches, reachable over I2C. Without periodic
keepalive the MCU resets the board every ~3 minutes.
The driver arms the MCU at probe and registers a struct watchdog_device
with WDOG_HW_RUNNING so the watchdog core feeds the chip via a kernel
timer until userspace opens the watchdog node. Timeout is fixed at 15s;
the hardware threshold is baked into MCU firmware and is not
software-configurable.
Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
Signed-off-by: Manuel Stocker <mensi@mensi.ch>
---
--- /dev/null
+++ b/Documentation/devicetree/bindings/watchdog/hasivo,mcu-wdt.yaml
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/watchdog/hasivo,mcu-wdt.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Hasivo / Horaco external management MCU watchdog
+
+maintainers:
+ - Carlo Szelinsky <github@szelinsky.de>
+ - Manuel Stocker <mensi@mensi.ch>
+
+description: |
+ Management microcontroller found on Hasivo and Horaco network switches,
+ exposed on the system I2C bus. Provides a hardware watchdog that must be
+ armed and periodically retriggered by the host via I2C writes.
+
+allOf:
+ - $ref: watchdog.yaml#
+
+properties:
+ compatible:
+ const: hasivo,mcu-wdt
+
+ reg:
+ maxItems: 1
+
+required:
+ - compatible
+ - reg
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ watchdog@6f {
+ compatible = "hasivo,mcu-wdt";
+ reg = <0x6f>;
+ };
+ };
+...
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -302,6 +302,18 @@ config MENF21BMC_WATCHDOG
This driver can also be built as a module. If so the module
will be called menf21bmc_wdt.
+config HASIVO_MCU_WATCHDOG
+ tristate "Hasivo MCU Watchdog"
+ depends on I2C
+ select WATCHDOG_CORE
+ help
+ Hardware watchdog driver for the external management MCU found
+ on Hasivo and Horaco network switches, reachable over I2C. The
+ MCU resets the board unless it is periodically retriggered.
+
+ This driver can also be built as a module. If so the module
+ will be called hasivo-mcu-wdt.
+
config MENZ069_WATCHDOG
tristate "MEN 16Z069 Watchdog"
depends on MCB
--- a/drivers/watchdog/Makefile
+++ b/drivers/watchdog/Makefile
@@ -239,6 +239,7 @@ obj-$(CONFIG_NCT6694_WATCHDOG) += nct669
obj-$(CONFIG_ZIIRAVE_WATCHDOG) += ziirave_wdt.o
obj-$(CONFIG_SOFT_WATCHDOG) += softdog.o
obj-$(CONFIG_MENF21BMC_WATCHDOG) += menf21bmc_wdt.o
+obj-$(CONFIG_HASIVO_MCU_WATCHDOG) += hasivo-mcu-wdt.o
obj-$(CONFIG_MENZ069_WATCHDOG) += menz69_wdt.o
obj-$(CONFIG_RAVE_SP_WATCHDOG) += rave-sp-wdt.o
obj-$(CONFIG_STPMIC1_WATCHDOG) += stpmic1_wdt.o
--- /dev/null
+++ b/drivers/watchdog/hasivo-mcu-wdt.c
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Hasivo MCU Watchdog Driver
+ *
+ * Hardware watchdog driver for the external management MCU found on
+ * Hasivo / Horaco network switches. Communicates over I2C.
+ *
+ * Protocol reverse-engineered from the stock firmware ("imi" daemon +
+ * i2c-poe.ko kernel module):
+ * - Arm: write 0x4F -> reg 0x09 (unlock), 0x4F -> reg 0x0A
+ * - Disarm: write 0x4F -> reg 0x09 (unlock), 0x3F -> reg 0x0A
+ * - Keepalive: write 0xEE -> reg 0x0B (no unlock needed)
+ *
+ * Copyright (C) 2026 Manuel Stocker <mensi@mensi.ch>
+ * Copyright (C) 2026 Carlo Szelinsky <github@szelinsky.de>
+ */
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/watchdog.h>
+
+#define HASIVO_REG_WDT_UNLOCK 0x09
+ #define HASIVO_WDT_UNLOCK_MAGIC 0x4f
+
+#define HASIVO_REG_WDT_CMD 0x0a
+ #define HASIVO_WDT_CMD_ARM 0x4f
+ #define HASIVO_WDT_CMD_DISARM 0x3f
+
+#define HASIVO_REG_WDT_KEEPALIVE 0x0b
+ #define HASIVO_WDT_KEEPALIVE_MAGIC 0xee
+
+/*
+ * Hardware timeout is fixed in MCU firmware and not software-
+ * configurable. Stock keepalive cadence is 2 s; the actual reset
+ * threshold is empirically in the 10-30 s range. Pick a conservative
+ * value so the kernel-core auto-feed (which fires at timeout/2) leaves
+ * generous slack.
+ */
+#define HASIVO_WDT_TIMEOUT 15
+
+struct hasivo_mcu {
+ struct i2c_client *client;
+ struct watchdog_device wdd;
+};
+
+static int hasivo_mcu_wdt_send_cmd(struct hasivo_mcu *mcu, u8 cmd)
+{
+ int ret;
+
+ ret = i2c_smbus_write_byte_data(mcu->client, HASIVO_REG_WDT_UNLOCK,
+ HASIVO_WDT_UNLOCK_MAGIC);
+ if (ret)
+ return ret;
+
+ return i2c_smbus_write_byte_data(mcu->client, HASIVO_REG_WDT_CMD, cmd);
+}
+
+static int hasivo_mcu_wdt_start(struct watchdog_device *wdd)
+{
+ struct hasivo_mcu *mcu = watchdog_get_drvdata(wdd);
+
+ return hasivo_mcu_wdt_send_cmd(mcu, HASIVO_WDT_CMD_ARM);
+}
+
+static int hasivo_mcu_wdt_stop(struct watchdog_device *wdd)
+{
+ struct hasivo_mcu *mcu = watchdog_get_drvdata(wdd);
+
+ return hasivo_mcu_wdt_send_cmd(mcu, HASIVO_WDT_CMD_DISARM);
+}
+
+static int hasivo_mcu_wdt_ping(struct watchdog_device *wdd)
+{
+ struct hasivo_mcu *mcu = watchdog_get_drvdata(wdd);
+
+ return i2c_smbus_write_byte_data(mcu->client,
+ HASIVO_REG_WDT_KEEPALIVE,
+ HASIVO_WDT_KEEPALIVE_MAGIC);
+}
+
+static const struct watchdog_info hasivo_mcu_wdt_info = {
+ .identity = "Hasivo MCU Watchdog",
+ .options = WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE,
+};
+
+static const struct watchdog_ops hasivo_mcu_wdt_ops = {
+ .owner = THIS_MODULE,
+ .start = hasivo_mcu_wdt_start,
+ .stop = hasivo_mcu_wdt_stop,
+ .ping = hasivo_mcu_wdt_ping,
+};
+
+static int hasivo_mcu_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct hasivo_mcu *mcu;
+ int ret;
+
+ mcu = devm_kzalloc(dev, sizeof(*mcu), GFP_KERNEL);
+ if (!mcu)
+ return -ENOMEM;
+
+ mcu->client = client;
+
+ /*
+ * Arm the watchdog. The stock bootloader normally leaves it
+ * already armed (and the chip's timer has been counting since
+ * then); re-arm explicitly so unexpected boot paths still leave
+ * it in a known state. It is not specified whether the arm
+ * sequence resets the chip's timeout counter, so send an
+ * explicit keepalive afterwards to guarantee a fresh window
+ * before the kernel-side feeder takes over at timeout/2.
+ */
+ ret = hasivo_mcu_wdt_send_cmd(mcu, HASIVO_WDT_CMD_ARM);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to arm watchdog\n");
+
+ ret = i2c_smbus_write_byte_data(client, HASIVO_REG_WDT_KEEPALIVE,
+ HASIVO_WDT_KEEPALIVE_MAGIC);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "initial watchdog ping failed\n");
+
+ mcu->wdd.info = &hasivo_mcu_wdt_info;
+ mcu->wdd.ops = &hasivo_mcu_wdt_ops;
+ mcu->wdd.parent = dev;
+ mcu->wdd.timeout = HASIVO_WDT_TIMEOUT;
+ mcu->wdd.min_timeout = HASIVO_WDT_TIMEOUT;
+ mcu->wdd.max_timeout = HASIVO_WDT_TIMEOUT;
+
+ /*
+ * Tell the watchdog core the hardware is already running so it
+ * pings via its own timer at timeout/2 until userspace (procd)
+ * opens /dev/watchdog0 and takes over.
+ */
+ set_bit(WDOG_HW_RUNNING, &mcu->wdd.status);
+
+ watchdog_set_drvdata(&mcu->wdd, mcu);
+ watchdog_stop_on_unregister(&mcu->wdd);
+
+ ret = devm_watchdog_register_device(dev, &mcu->wdd);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to register watchdog\n");
+
+ dev_info(dev, "Hasivo MCU watchdog armed at 0x%02x (timeout %us)\n",
+ client->addr, mcu->wdd.timeout);
+
+ return 0;
+}
+
+static const struct of_device_id hasivo_mcu_of_match[] = {
+ { .compatible = "hasivo,mcu-wdt" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, hasivo_mcu_of_match);
+
+static const struct i2c_device_id hasivo_mcu_id[] = {
+ { "hasivo-mcu-wdt" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, hasivo_mcu_id);
+
+static struct i2c_driver hasivo_mcu_driver = {
+ .driver = {
+ .name = "hasivo-mcu-wdt",
+ .of_match_table = hasivo_mcu_of_match,
+ },
+ .probe = hasivo_mcu_probe,
+ .id_table = hasivo_mcu_id,
+};
+module_i2c_driver(hasivo_mcu_driver);
+
+MODULE_DESCRIPTION("Hasivo MCU Watchdog Driver");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Manuel Stocker <mensi@mensi.ch>");
+MODULE_AUTHOR("Carlo Szelinsky <github@szelinsky.de>");

View File

@ -97,6 +97,7 @@ CONFIG_GPIO_WATCHDOG=y
# CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set # CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set
CONFIG_GRO_CELLS=y CONFIG_GRO_CELLS=y
CONFIG_HARDWARE_WATCHPOINTS=y CONFIG_HARDWARE_WATCHPOINTS=y
# CONFIG_HASIVO_MCU_WATCHDOG is not set
CONFIG_HAS_DMA=y CONFIG_HAS_DMA=y
CONFIG_HAS_IOMEM=y CONFIG_HAS_IOMEM=y
CONFIG_HAS_IOPORT=y CONFIG_HAS_IOPORT=y

View File

@ -100,6 +100,7 @@ CONFIG_GPIO_WATCHDOG=y
# CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set # CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set
CONFIG_GRO_CELLS=y CONFIG_GRO_CELLS=y
CONFIG_HARDWARE_WATCHPOINTS=y CONFIG_HARDWARE_WATCHPOINTS=y
# CONFIG_HASIVO_MCU_WATCHDOG is not set
CONFIG_HAS_DMA=y CONFIG_HAS_DMA=y
CONFIG_HAS_IOMEM=y CONFIG_HAS_IOMEM=y
CONFIG_HAS_IOPORT=y CONFIG_HAS_IOPORT=y

View File

@ -86,6 +86,7 @@ CONFIG_GPIO_WATCHDOG=y
# CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set # CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set
CONFIG_GRO_CELLS=y CONFIG_GRO_CELLS=y
CONFIG_HARDWARE_WATCHPOINTS=y CONFIG_HARDWARE_WATCHPOINTS=y
# CONFIG_HASIVO_MCU_WATCHDOG is not set
CONFIG_HAS_DMA=y CONFIG_HAS_DMA=y
CONFIG_HAS_IOMEM=y CONFIG_HAS_IOMEM=y
CONFIG_HAS_IOPORT=y CONFIG_HAS_IOPORT=y

View File

@ -88,6 +88,7 @@ CONFIG_GPIO_REALTEK_OTTO=y
CONFIG_GPIO_REGMAP=y CONFIG_GPIO_REGMAP=y
CONFIG_GRO_CELLS=y CONFIG_GRO_CELLS=y
CONFIG_HARDWARE_WATCHPOINTS=y CONFIG_HARDWARE_WATCHPOINTS=y
# CONFIG_HASIVO_MCU_WATCHDOG is not set
CONFIG_HAS_DMA=y CONFIG_HAS_DMA=y
CONFIG_HAS_IOMEM=y CONFIG_HAS_IOMEM=y
CONFIG_HAS_IOPORT=y CONFIG_HAS_IOPORT=y

View File

@ -88,6 +88,7 @@ CONFIG_GPIO_REALTEK_OTTO=y
CONFIG_GPIO_REGMAP=y CONFIG_GPIO_REGMAP=y
CONFIG_GRO_CELLS=y CONFIG_GRO_CELLS=y
CONFIG_HARDWARE_WATCHPOINTS=y CONFIG_HARDWARE_WATCHPOINTS=y
# CONFIG_HASIVO_MCU_WATCHDOG is not set
CONFIG_HAS_DMA=y CONFIG_HAS_DMA=y
CONFIG_HAS_IOMEM=y CONFIG_HAS_IOMEM=y
CONFIG_HAS_IOPORT=y CONFIG_HAS_IOPORT=y

View File

@ -90,6 +90,7 @@ CONFIG_GPIO_REALTEK_OTTO=y
CONFIG_GPIO_REGMAP=y CONFIG_GPIO_REGMAP=y
CONFIG_GRO_CELLS=y CONFIG_GRO_CELLS=y
CONFIG_HARDWARE_WATCHPOINTS=y CONFIG_HARDWARE_WATCHPOINTS=y
# CONFIG_HASIVO_MCU_WATCHDOG is not set
CONFIG_HAS_DMA=y CONFIG_HAS_DMA=y
CONFIG_HAS_IOMEM=y CONFIG_HAS_IOMEM=y
CONFIG_HAS_IOPORT=y CONFIG_HAS_IOPORT=y