espi-control: Add program to disable eSPI

This commit introduces a program that can disable and re-enable eSPI on
a Nuvoton BMC.

Tested:
- Ran "npcm7xx_espi_control -d", and confirmed that IPMI stopped
  working.
- Ran "npcm7xx_espi_control", and confirmed that IPMI started working
  again.

Change-Id: I9637ac4d84b82931f107946ff53e7ef16a965d25
Signed-off-by: John Wedig <johnwedig@google.com>
diff --git a/.gitignore b/.gitignore
index fbedebd..27676e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 subprojects/*
 !subprojects/acpi-power-state-daemon/
 !subprojects/dhcp-done/
+!subprojects/espi-control/
 !subprojects/libcr51sign/
 !subprojects/metrics-ipmi-blobs/
 !subprojects/ncsid/
diff --git a/espi-control b/espi-control
new file mode 120000
index 0000000..eacd114
--- /dev/null
+++ b/espi-control
@@ -0,0 +1 @@
+subprojects/espi-control/
\ No newline at end of file
diff --git a/meson.build b/meson.build
index e405f0c..7f54a82 100644
--- a/meson.build
+++ b/meson.build
@@ -43,4 +43,5 @@
   subproject('nemora-postd')
   subproject('libcr51sign')
   subproject('dhcp-done')
+  subproject('espi-control')
 endif
diff --git a/subprojects/espi-control/meson.build b/subprojects/espi-control/meson.build
new file mode 100644
index 0000000..9e45228
--- /dev/null
+++ b/subprojects/espi-control/meson.build
@@ -0,0 +1,23 @@
+project(
+  'espi_control',
+  'cpp',
+  version: '0.1',
+  meson_version: '>=1.1.1',
+  default_options: [
+    'warning_level=3',
+    'werror=true',
+    'cpp_std=c++23',
+  ],
+)
+
+executable(
+  'npcm7xx-espi-control',
+  'npcm7xx_espi_control.cpp',
+  implicit_include_directories: false,
+  dependencies:
+  [
+    dependency('stdplus'),
+  ],
+  install: true,
+  install_dir: get_option('libexecdir'),
+)
diff --git a/subprojects/espi-control/npcm7xx_espi_control.cpp b/subprojects/espi-control/npcm7xx_espi_control.cpp
new file mode 100644
index 0000000..661a48c
--- /dev/null
+++ b/subprojects/espi-control/npcm7xx_espi_control.cpp
@@ -0,0 +1,227 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+ * Disclaimer: This binary is only intended to be used on Nuvoton NPCM7xx BMCs.
+ *
+ * It could also be extended to support NPCM8xx, but it hasn't been tested with
+ * that model BMC.
+ *
+ * This binary is NOT intended to support Aspeed BMCs.
+ */
+
+#include <unistd.h>
+
+#include <stdplus/fd/create.hpp>
+#include <stdplus/fd/intf.hpp>
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/mmap.hpp>
+
+#include <cassert>
+#include <cstdint>
+#include <cstdio>
+#include <format>
+#include <stdexcept>
+
+/* Base address for Nuvoton's global control register space. */
+#define NPCM7XX_GLOBAL_CTRL_BASE_ADDR 0xF0800000
+/* Offset of the PDID register and expected PDID value. */
+#define PDID_OFFSET 0x00
+#define NPCM7XX_PDID 0x04A92750
+
+/* Register width in bytes. */
+#define REGISTER_WIDTH 4
+
+/* Base address for Nuvoton's eSPI register space. */
+#define NPCM7XX_ESPI_BASE_ADDR 0xF009F000
+/*
+ * Offset of the eSPI config (ESPICFG) register, along with host channel enable
+ * mask and core channel enable mask.
+ */
+#define ESPICFG_OFFSET 0x4
+#define ESPICFG_HOST_CHANNEL_ENABLE_MASK 0xF0
+#define ESPICFG_CORE_CHANNEL_ENABLE_MASK 0x0F
+/*
+ * Offset of the host independence (ESPIHINDP) register and automatic ready bit
+ * mask.
+ */
+#define ESPIHINDP_OFFSET 0x80
+#define ESPI_AUTO_READY_MASK 0xF
+
+namespace
+{
+
+using stdplus::fd::MMapAccess;
+using stdplus::fd::MMapFlags;
+using stdplus::fd::OpenAccess;
+using stdplus::fd::OpenFlag;
+using stdplus::fd::OpenFlags;
+using stdplus::fd::ProtFlag;
+using stdplus::fd::ProtFlags;
+
+static void usage(char* name)
+{
+    std::fprintf(stderr, "Usage: %s [-d]\n", name);
+    std::fprintf(stderr, "Enable or disable eSPI bus on NPCM7XX BMC\n");
+    std::fprintf(stderr, "This program will enable eSPI by default, unless "
+                         "the -d option is used.\n");
+    std::fprintf(stderr, "  -d   Disable eSPI\n");
+}
+
+/*
+ * Get a pointer to the register at the given offset, within the provided
+ * memory-mapped I/O space.
+ */
+static inline volatile uint32_t* getReg(stdplus::fd::MMap& map,
+                                        size_t regOffset)
+{
+    uintptr_t regPtr = reinterpret_cast<uintptr_t>(map.get().data()) +
+                       regOffset;
+    /* Make sure the register pointer is properly aligned. */
+    assert((regPtr & ~(REGISTER_WIDTH - 1)) == regPtr);
+
+    return reinterpret_cast<volatile uint32_t*>(regPtr);
+}
+
+static void modifyESPIRegisters(bool disable)
+{
+    /*
+     * We need to make sure this is running on a Nuvoton BMC. To do that, we'll
+     * read the product identification (PDID) register.
+     */
+
+    /* Find the page that includes the Product ID register. */
+    size_t pageSize = sysconf(_SC_PAGE_SIZE);
+    size_t pageBase = NPCM7XX_GLOBAL_CTRL_BASE_ADDR / pageSize * pageSize;
+    size_t pageOffset = NPCM7XX_GLOBAL_CTRL_BASE_ADDR - pageBase;
+    size_t mapLength = pageOffset + PDID_OFFSET + REGISTER_WIDTH;
+
+    auto fd = stdplus::fd::open(
+        "/dev/mem", OpenFlags(OpenAccess::ReadWrite).set(OpenFlag::Sync));
+    stdplus::fd::MMap pdidMap(fd, mapLength, ProtFlags().set(ProtFlag::Read),
+                              MMapFlags(MMapAccess::Shared), pageBase);
+
+    volatile uint32_t* const pdidReg = getReg(pdidMap,
+                                              pageOffset + PDID_OFFSET);
+
+    /*
+     * Read the PDID register to make sure we're running on a Nuvoton NPCM7xx
+     * BMC.
+     * Note: This binary would probably work on NPCM8xx, as well, if we also
+     * allowed the NPCM8xx PDID, since the register addresses are the same. But
+     * that hasn't been tested.
+     */
+    if (*pdidReg != NPCM7XX_PDID)
+    {
+        throw std::runtime_error(
+            std::format("Unexpected product ID {:#x} != {:#x}", *pdidReg,
+                        NPCM7XX_PDID)
+                .data());
+    }
+
+    /*
+     * Find the start of the page that includes the start of the eSPI register
+     * space.
+     */
+    pageBase = NPCM7XX_ESPI_BASE_ADDR / pageSize * pageSize;
+    pageOffset = NPCM7XX_ESPI_BASE_ADDR - pageBase;
+
+    mapLength = pageOffset + ESPIHINDP_OFFSET + REGISTER_WIDTH;
+
+    stdplus::fd::MMap espiMap(
+        fd, mapLength, ProtFlags().set(ProtFlag::Read).set(ProtFlag::Write),
+        MMapFlags(MMapAccess::Shared), pageBase);
+
+    /* Read the ESPICFG register. */
+    volatile uint32_t* const espicfgReg = getReg(espiMap,
+                                                 pageOffset + ESPICFG_OFFSET);
+    uint32_t espicfgValue = *espicfgReg;
+
+    if (disable)
+    {
+        /*
+         * Check if the automatic ready bits are set in the eSPI host
+         * independence register (ESPIHINDP).
+         */
+        volatile uint32_t* const espihindpReg =
+            getReg(espiMap, pageOffset + ESPIHINDP_OFFSET);
+        uint32_t espihindpValue = *espihindpReg;
+        if (espihindpValue & ESPI_AUTO_READY_MASK)
+        {
+            /*
+             * If any of the automatic ready bits are set, we need to disable
+             * them, using several steps:
+             *   - Make sure the host channel enable and core channel bits are
+             *     consistent, in the ESPICFG register, i.e. copy the host
+             *     channel enable bits to the core channel enable bits.
+             *   - Clear the automatic ready bits in ESPIHINDP.
+             */
+            uint32_t hostChannelEnableBits = espicfgValue &
+                                             ESPICFG_HOST_CHANNEL_ENABLE_MASK;
+            espicfgValue |= (hostChannelEnableBits >> 4);
+            *espicfgReg = espicfgValue;
+
+            espihindpValue &= ~ESPI_AUTO_READY_MASK;
+            *espihindpReg = espihindpValue;
+        }
+
+        /* Now disable the core channel enable bits in ESPICFG. */
+        espicfgValue &= ~ESPICFG_CORE_CHANNEL_ENABLE_MASK;
+        *espicfgReg = espicfgValue;
+
+        fprintf(stderr, "Disabled eSPI bus\n");
+    }
+    else
+    {
+        /* Enable eSPI by setting the core channel enable bits in ESPICFG. */
+        espicfgValue |= ESPICFG_CORE_CHANNEL_ENABLE_MASK;
+        *espicfgReg = espicfgValue;
+
+        fprintf(stderr, "Enabled eSPI bus\n");
+    }
+}
+
+} // namespace
+
+int main(int argc, char** argv)
+{
+    int opt;
+    bool disable = false;
+    while ((opt = getopt(argc, argv, "d")) != -1)
+    {
+        switch (opt)
+        {
+            case 'd':
+                disable = true;
+                break;
+            default:
+                usage(argv[0]);
+                return -1;
+        }
+    }
+
+    try
+    {
+        /* Update registers to enable or disable eSPI. */
+        modifyESPIRegisters(disable);
+    }
+    catch (const std::exception& e)
+    {
+        fprintf(stderr, "Failed to %s eSPI bus: %s\n",
+                disable ? "disable" : "enable", e.what());
+        return -1;
+    }
+
+    return 0;
+}