soc/intel/common/block/oc_wdt: Add OC watchdog common block

Add new block for handling overclocking watchdog. The watchdog is
present since Skylake or maybe even earlier so it is safe to use with
most of the microarchitectures utilizing intelblocks.

The patch adds the common block for initializing and feeding the
watchdog. Timeout is defined statically in Kconfig and should be set
high enough by the board or SoC Kconfig to let the board boot with
full memory training and avoid reset loops. Full training of 128GB
DDR5 DIMM memory on AlderLake takes about 5 minutes. Newer SoCs
with newer memory technologies and higher RAM capacity may take more.
The default has been set to 10 minutes.

The patch also adds support for feeding watchdog in driverless mode,
i.e. it utilizies periodic SMI to reload the timeout value and restart
the watchdog timer. This is optional and selectable by Kconfig option
as well. If the option is not enabled, payload and/or software must
ensure to keep feeding the watchdog, otherwise the platform will
reset.

TEST=Enable watchdog on MSI PRO Z690-A and see the platform resets
after some time. Enable the watchdog in driverless mode and see the
platform no longer resets and periodic SMI keeps feeding the watchdog.

Signed-off-by: Michał Żygowski <michal.zygowski@3mdeb.com>
Change-Id: Ib494aa0c7581351abca8b496fc5895b2c7cbc5bc
Reviewed-on: https://review.coreboot.org/c/coreboot/+/68944
Reviewed-by: Eric Lai <eric_lai@quanta.corp-partner.google.com>
Reviewed-by: Krystian Hebel <krystian.hebel@3mdeb.com>
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
diff --git a/src/soc/intel/common/block/include/intelblocks/oc_wdt.h b/src/soc/intel/common/block/include/intelblocks/oc_wdt.h
new file mode 100644
index 0000000..8a03a187
--- /dev/null
+++ b/src/soc/intel/common/block/include/intelblocks/oc_wdt.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef SOC_INTEL_COMMON_OC_WDT_H
+#define SOC_INTEL_COMMON_OC_WDT_H
+
+#include <stdbool.h>
+
+/*
+ * Starts and reloads the OC watchdog with given timeout.
+ *
+ * timeout - Time in seconds before OC watchdog times out. Supported range = 70 - 1024
+ */
+void oc_wdt_start(unsigned int timeout);
+
+/* Reloads the OC watchdog (if enabled) preserving the current settings. */
+void oc_wdt_reload(void);
+
+/* Disables the OC WDT */
+void oc_wdt_disable(void);
+
+/* Checks if OC WDT is enabled and returns true if so, otherwise false */
+bool is_oc_wdt_enabled(void);
+
+/* Returns currently programmed OC watchdog timeout in seconds */
+unsigned int oc_wdt_get_current_timeout(void);
+
+#endif
diff --git a/src/soc/intel/common/block/oc_wdt/Kconfig b/src/soc/intel/common/block/oc_wdt/Kconfig
new file mode 100644
index 0000000..f0966e0
--- /dev/null
+++ b/src/soc/intel/common/block/oc_wdt/Kconfig
@@ -0,0 +1,41 @@
+## SPDX-License-Identifier: GPL-2.0-only
+
+config SOC_INTEL_COMMON_BLOCK_OC_WDT
+	bool
+	depends on SOC_INTEL_COMMON_BLOCK_PMC
+	help
+	  Intel Processor common Overclocking Watchdog support
+
+config SOC_INTEL_COMMON_OC_WDT_ENABLE
+	bool "Enable overclocking watchdog during boot"
+	depends on SOC_INTEL_COMMON_BLOCK_OC_WDT
+	help
+	  Enables Intel chipset Overclocking Watchdog to count during system
+	  boot. The platform will reset during lockups if watchdog is not
+	  reloaded. Software/firmware is responsible for feeding the watchdog.
+
+	  If unsure, say N.
+
+config SOC_INTEL_COMMON_OC_WDT_TIMEOUT_SECONDS
+	int
+	depends on SOC_INTEL_COMMON_OC_WDT_ENABLE
+	range 70 1024
+	default 600
+	help
+	  The Intel chipset Overclocking Watchdog timeout value in seconds.
+	  coreboot will preload the watchdog with the timeout value specified
+	  in this option. Specify a high enough value so that the platform
+	  will have a chance to perform full memory training and boot. Default
+	  is 10 minutes. Boards and SoCs may override this value.
+
+config SOC_INTEL_COMMON_OC_WDT_RELOAD_IN_PERIODIC_SMI
+	bool "Reload the overclocking watchdog using periodic SMI"
+	depends on SOC_INTEL_COMMON_OC_WDT_ENABLE
+	depends on SOC_INTEL_COMMON_BLOCK_SMM
+	default y
+	help
+	  Enables Intel chipset Overclocking Watchdog reloading in the periodic
+	  SMI handler. Without this option the platform will keep power cycling
+	  unless the OS drivers are installed for this watchdog.
+
+	  If unsure, say Y.
diff --git a/src/soc/intel/common/block/oc_wdt/Makefile.inc b/src/soc/intel/common/block/oc_wdt/Makefile.inc
new file mode 100644
index 0000000..8f7282a
--- /dev/null
+++ b/src/soc/intel/common/block/oc_wdt/Makefile.inc
@@ -0,0 +1,4 @@
+## SPDX-License-Identifier: GPL-2.0-only
+
+all-$(CONFIG_SOC_INTEL_COMMON_BLOCK_OC_WDT) += oc_wdt.c
+smm-$(CONFIG_SOC_INTEL_COMMON_BLOCK_OC_WDT) += oc_wdt.c
diff --git a/src/soc/intel/common/block/oc_wdt/oc_wdt.c b/src/soc/intel/common/block/oc_wdt/oc_wdt.c
new file mode 100644
index 0000000..012621b
--- /dev/null
+++ b/src/soc/intel/common/block/oc_wdt/oc_wdt.c
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#include <arch/io.h>
+#include <console/console.h>
+#include <intelblocks/oc_wdt.h>
+#include <soc/iomap.h>
+#include <types.h>
+
+/* OC WDT configuration */
+#define PCH_OC_WDT_CTL				(ACPI_BASE_ADDRESS + 0x54)
+#define   PCH_OC_WDT_CTL_RLD			BIT(31)
+#define   PCH_OC_WDT_CTL_ICCSURV_STS		BIT(25)
+#define   PCH_OC_WDT_CTL_NO_ICCSURV_STS		BIT(24)
+#define   PCH_OC_WDT_CTL_FORCE_ALL		BIT(15)
+#define   PCH_OC_WDT_CTL_EN			BIT(14)
+#define   PCH_OC_WDT_CTL_ICCSURV		BIT(13)
+#define   PCH_OC_WDT_CTL_LCK			BIT(12)
+#define   PCH_OC_WDT_CTL_TOV_MASK		0x3FF
+
+/*
+ * Starts and reloads the OC watchdog with given timeout.
+ *
+ * timeout - Time in seconds before OC watchdog times out. Supported range = 70 - 1024
+ */
+void oc_wdt_start(unsigned int timeout)
+{
+	uint32_t oc_wdt_ctrl;
+
+	if (!CONFIG(SOC_INTEL_COMMON_OC_WDT_ENABLE))
+		return;
+
+	if ((timeout < 70) || (timeout > (PCH_OC_WDT_CTL_TOV_MASK + 1))) {
+		timeout = CONFIG_SOC_INTEL_COMMON_OC_WDT_TIMEOUT_SECONDS;
+		printk(BIOS_WARNING, "OC Watchdog: invalid timeout value,"
+				     " using config default: %ds\n", timeout);
+	}
+
+	printk(BIOS_SPEW, "OC Watchdog: start and relaod timer (timeout %ds)\n", timeout);
+
+	oc_wdt_ctrl = inl(PCH_OC_WDT_CTL);
+	oc_wdt_ctrl |= (PCH_OC_WDT_CTL_EN | PCH_OC_WDT_CTL_FORCE_ALL | PCH_OC_WDT_CTL_ICCSURV);
+
+
+	oc_wdt_ctrl &= ~PCH_OC_WDT_CTL_TOV_MASK;
+	oc_wdt_ctrl |= (timeout - 1);
+	oc_wdt_ctrl |= PCH_OC_WDT_CTL_RLD;
+
+	outl(oc_wdt_ctrl, PCH_OC_WDT_CTL);
+}
+
+/* Reloads the OC watchdog (if enabled) preserving the current settings. */
+void oc_wdt_reload(void)
+{
+	uint32_t oc_wdt_ctrl;
+
+	/* Reload only works if OC WDT enable bit is set */
+	if (!is_oc_wdt_enabled())
+		return;
+
+	oc_wdt_ctrl = inl(PCH_OC_WDT_CTL);
+	/* Unset write-1-to-clear bits and preserve other settings */
+	oc_wdt_ctrl &= ~(PCH_OC_WDT_CTL_ICCSURV_STS | PCH_OC_WDT_CTL_NO_ICCSURV_STS);
+	oc_wdt_ctrl |= PCH_OC_WDT_CTL_RLD;
+	outl(oc_wdt_ctrl, PCH_OC_WDT_CTL);
+}
+
+/* Disables the OC WDT. */
+void oc_wdt_disable(void)
+{
+	uint32_t oc_wdt_ctrl;
+
+	printk(BIOS_INFO, "OC Watchdog: disabling watchdog timer\n");
+
+	oc_wdt_ctrl = inl(PCH_OC_WDT_CTL);
+	oc_wdt_ctrl &= ~(PCH_OC_WDT_CTL_EN | PCH_OC_WDT_CTL_FORCE_ALL);
+	outl(oc_wdt_ctrl, PCH_OC_WDT_CTL);
+}
+
+/* Checks if OC WDT is enabled and returns true if so, otherwise false. */
+bool is_oc_wdt_enabled(void)
+{
+	return (inl(PCH_OC_WDT_CTL) & PCH_OC_WDT_CTL_EN) ? true : false;
+}
+
+/* Returns currently programmed OC watchdog timeout in seconds */
+unsigned int oc_wdt_get_current_timeout(void)
+{
+	return (inl(PCH_OC_WDT_CTL) & PCH_OC_WDT_CTL_TOV_MASK) + 1;
+}
diff --git a/src/soc/intel/common/block/smm/smihandler.c b/src/soc/intel/common/block/smm/smihandler.c
index 3cbdd9f..680ec87 100644
--- a/src/soc/intel/common/block/smm/smihandler.c
+++ b/src/soc/intel/common/block/smm/smihandler.c
@@ -15,6 +15,7 @@
 #include <device/pci_ops.h>
 #include <elog.h>
 #include <intelblocks/fast_spi.h>
+#include <intelblocks/oc_wdt.h>
 #include <intelblocks/pmclib.h>
 #include <intelblocks/smihandler.h>
 #include <intelblocks/tco.h>
@@ -480,6 +481,9 @@
 	if ((reg32 & PERIODIC_EN) == 0)
 		return;
 	printk(BIOS_DEBUG, "Periodic SMI.\n");
+
+	if (CONFIG(SOC_INTEL_COMMON_OC_WDT_RELOAD_IN_PERIODIC_SMI))
+		oc_wdt_reload();
 }
 
 void smihandler_southbridge_gpi(
diff --git a/src/soc/intel/common/block/smm/smm.c b/src/soc/intel/common/block/smm/smm.c
index 2fd97da..b0bb378 100644
--- a/src/soc/intel/common/block/smm/smm.c
+++ b/src/soc/intel/common/block/smm/smm.c
@@ -3,9 +3,12 @@
 #include <console/console.h>
 #include <cpu/x86/smm.h>
 #include <cpu/intel/smm_reloc.h>
+#include <device/mmio.h>
+#include <intelblocks/oc_wdt.h>
 #include <intelblocks/pmclib.h>
 #include <intelblocks/systemagent.h>
 #include <soc/pm.h>
+#include <soc/pmc.h>
 
 void smm_southbridge_clear_state(void)
 {
@@ -23,6 +26,36 @@
 	pmc_clear_all_gpe_status();
 }
 
+static void configure_periodic_smi_interval(void)
+{
+	uint32_t gen_pmcon;
+	uint32_t gen_pmcon_reg;
+
+	if (CONFIG(PERIODIC_SMI_RATE_SELECTION_IN_GEN_PMCON_B))
+		gen_pmcon_reg = GEN_PMCON_B;
+	else
+		gen_pmcon_reg = GEN_PMCON_A;
+
+	/*
+	 * Periodic SMIs have +/- 1 second error, to be safe add few seconds
+	 * more. Also we do not allow timeouts lower than 70s by Kconfig
+	 * definition, so we need to handle one case.
+	 */
+	gen_pmcon = read32p(soc_read_pmc_base() + gen_pmcon_reg);
+	gen_pmcon &= ~PER_SMI_SEL_MASK;
+	gen_pmcon |= SMI_RATE_64S;
+	write32p(soc_read_pmc_base() + gen_pmcon_reg, gen_pmcon);
+
+	/*
+	 * We don't know when SMI timer is started or even if this is
+	 * architecturally defined, but in worst case we may get SMI 64
+	 * seconds (+ any error) from now, which may be more than OC watchdog
+	 * timeout since it was last kicked, so we should kick it here, just
+	 * in case.
+	 */
+	oc_wdt_reload();
+}
+
 static void smm_southbridge_enable(uint16_t pm1_events)
 {
 	uint32_t smi_params = ENABLE_SMI_PARAMS;
@@ -48,6 +81,7 @@
 	 *  - on writes to GBL_RLS (bios commands)
 	 *  - on eSPI events, unless disabled (does nothing on LPC systems)
 	 *  - on TCO events (TIMEOUT, case intrusion, ...), if enabled
+	 *  - periodically, if watchdog feeding through SMI is enabled
 	 * No SMIs:
 	 *  - on microcontroller writes (io 0x62/0x66)
 	 */
@@ -57,6 +91,11 @@
 	if (CONFIG(SOC_INTEL_COMMON_BLOCK_SMM_TCO_ENABLE))
 		smi_params |= TCO_SMI_EN;
 
+	if (CONFIG(SOC_INTEL_COMMON_OC_WDT_RELOAD_IN_PERIODIC_SMI)) {
+		smi_params |= PERIODIC_EN;
+		configure_periodic_smi_interval();
+	}
+
 	/* Enable SMI generation: */
 	pmc_enable_smi(smi_params);
 }