From d048137e85e1434ede70f48a91b6b4fe9b57eae1 Mon Sep 17 00:00:00 2001 From: Carlo Szelinsky Date: Mon, 18 May 2026 22:41:21 +0200 Subject: [PATCH] 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 Link: https://github.com/openwrt/openwrt/pull/23418 Signed-off-by: Markus Stockhausen --- target/linux/realtek/modules.mk | 18 ++ .../patches-6.18/810-add-hasivo-mcu-wdt.patch | 277 ++++++++++++++++++ target/linux/realtek/rtl838x/config-6.18 | 1 + target/linux/realtek/rtl839x/config-6.18 | 1 + target/linux/realtek/rtl930x/config-6.18 | 1 + target/linux/realtek/rtl930x_nand/config-6.18 | 1 + target/linux/realtek/rtl931x/config-6.18 | 1 + target/linux/realtek/rtl931x_nand/config-6.18 | 1 + 8 files changed, 301 insertions(+) create mode 100644 target/linux/realtek/patches-6.18/810-add-hasivo-mcu-wdt.patch diff --git a/target/linux/realtek/modules.mk b/target/linux/realtek/modules.mk index c6065a9395..e3a85b75b0 100644 --- a/target/linux/realtek/modules.mk +++ b/target/linux/realtek/modules.mk @@ -6,6 +6,24 @@ # 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 SUBMENU:=$(MFD_MENU) diff --git a/target/linux/realtek/patches-6.18/810-add-hasivo-mcu-wdt.patch b/target/linux/realtek/patches-6.18/810-add-hasivo-mcu-wdt.patch new file mode 100644 index 0000000000..8330b88f3b --- /dev/null +++ b/target/linux/realtek/patches-6.18/810-add-hasivo-mcu-wdt.patch @@ -0,0 +1,277 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Carlo Szelinsky +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 +Signed-off-by: Manuel Stocker +--- +--- /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 ++ - Manuel Stocker ++ ++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 ++ * Copyright (C) 2026 Carlo Szelinsky ++ */ ++ ++#include ++#include ++#include ++ ++#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 "); ++MODULE_AUTHOR("Carlo Szelinsky "); diff --git a/target/linux/realtek/rtl838x/config-6.18 b/target/linux/realtek/rtl838x/config-6.18 index 97c4508304..9020be7c04 100644 --- a/target/linux/realtek/rtl838x/config-6.18 +++ b/target/linux/realtek/rtl838x/config-6.18 @@ -97,6 +97,7 @@ CONFIG_GPIO_WATCHDOG=y # CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set CONFIG_GRO_CELLS=y CONFIG_HARDWARE_WATCHPOINTS=y +# CONFIG_HASIVO_MCU_WATCHDOG is not set CONFIG_HAS_DMA=y CONFIG_HAS_IOMEM=y CONFIG_HAS_IOPORT=y diff --git a/target/linux/realtek/rtl839x/config-6.18 b/target/linux/realtek/rtl839x/config-6.18 index c0912804c2..243e0ac5b2 100644 --- a/target/linux/realtek/rtl839x/config-6.18 +++ b/target/linux/realtek/rtl839x/config-6.18 @@ -100,6 +100,7 @@ CONFIG_GPIO_WATCHDOG=y # CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set CONFIG_GRO_CELLS=y CONFIG_HARDWARE_WATCHPOINTS=y +# CONFIG_HASIVO_MCU_WATCHDOG is not set CONFIG_HAS_DMA=y CONFIG_HAS_IOMEM=y CONFIG_HAS_IOPORT=y diff --git a/target/linux/realtek/rtl930x/config-6.18 b/target/linux/realtek/rtl930x/config-6.18 index 730b35e16f..ce48ee74d9 100644 --- a/target/linux/realtek/rtl930x/config-6.18 +++ b/target/linux/realtek/rtl930x/config-6.18 @@ -86,6 +86,7 @@ CONFIG_GPIO_WATCHDOG=y # CONFIG_GPIO_WATCHDOG_ARCH_INITCALL is not set CONFIG_GRO_CELLS=y CONFIG_HARDWARE_WATCHPOINTS=y +# CONFIG_HASIVO_MCU_WATCHDOG is not set CONFIG_HAS_DMA=y CONFIG_HAS_IOMEM=y CONFIG_HAS_IOPORT=y diff --git a/target/linux/realtek/rtl930x_nand/config-6.18 b/target/linux/realtek/rtl930x_nand/config-6.18 index 8f0e183d44..2c0595f17e 100644 --- a/target/linux/realtek/rtl930x_nand/config-6.18 +++ b/target/linux/realtek/rtl930x_nand/config-6.18 @@ -88,6 +88,7 @@ CONFIG_GPIO_REALTEK_OTTO=y CONFIG_GPIO_REGMAP=y CONFIG_GRO_CELLS=y CONFIG_HARDWARE_WATCHPOINTS=y +# CONFIG_HASIVO_MCU_WATCHDOG is not set CONFIG_HAS_DMA=y CONFIG_HAS_IOMEM=y CONFIG_HAS_IOPORT=y diff --git a/target/linux/realtek/rtl931x/config-6.18 b/target/linux/realtek/rtl931x/config-6.18 index 3f5a7289c6..8d97246e31 100644 --- a/target/linux/realtek/rtl931x/config-6.18 +++ b/target/linux/realtek/rtl931x/config-6.18 @@ -88,6 +88,7 @@ CONFIG_GPIO_REALTEK_OTTO=y CONFIG_GPIO_REGMAP=y CONFIG_GRO_CELLS=y CONFIG_HARDWARE_WATCHPOINTS=y +# CONFIG_HASIVO_MCU_WATCHDOG is not set CONFIG_HAS_DMA=y CONFIG_HAS_IOMEM=y CONFIG_HAS_IOPORT=y diff --git a/target/linux/realtek/rtl931x_nand/config-6.18 b/target/linux/realtek/rtl931x_nand/config-6.18 index bc08c5f30e..75129075f5 100644 --- a/target/linux/realtek/rtl931x_nand/config-6.18 +++ b/target/linux/realtek/rtl931x_nand/config-6.18 @@ -90,6 +90,7 @@ CONFIG_GPIO_REALTEK_OTTO=y CONFIG_GPIO_REGMAP=y CONFIG_GRO_CELLS=y CONFIG_HARDWARE_WATCHPOINTS=y +# CONFIG_HASIVO_MCU_WATCHDOG is not set CONFIG_HAS_DMA=y CONFIG_HAS_IOMEM=y CONFIG_HAS_IOPORT=y