bmc_monitor_app: Support `FirmwarePlusLoader` for Nuvoton SoC

Support `FirmwarePlusLoader` duration for NCPM7XX or newer Nuvoton SoC.
This will mitigate the inaccuracy caused by SoC not supporting Firmware
time and Loader time.

Tested:
\# Check FirmwarePlusLoader time is correct on the BMC with Nuvoton SoC
```
bmc:~# cat /usr/share/boot_time_monitor/bmc_durations.csv.completed
Firmware,0
Loader,0
Kernel,38706
InitRD,0
Userspace,381086
FirmwarePlusLoader,43480
```

Google-Bug-Id: 296530445
Change-Id: Ie340bd5f9d4637f0be1f2915887200b3c3d03e12
Signed-off-by: Michael Shen <gpgpgp@google.com>
diff --git a/include/utils.hpp b/include/utils.hpp
index 60a11aa..1b061e4 100644
--- a/include/utils.hpp
+++ b/include/utils.hpp
@@ -88,6 +88,15 @@
      */
     virtual std::string getDurPath(std::string_view nodeName,
                                    bool wantCompleted) = 0;
+
+    /**
+     * Read 4 bytes data from target address
+     *
+     * @param[in] target - Target address
+     * @return 4 bytes data from the target address or std::nullopt if any error
+     * happens during `readMem4Bytes`
+     */
+    virtual std::optional<uint32_t> readMem4Bytes(uint32_t target) = 0;
 };
 
 /**
@@ -103,6 +112,7 @@
                           bool wantCompleted) override;
     std::string getDurPath(std::string_view nodeName,
                            bool wantCompleted) override;
+    std::optional<uint32_t> readMem4Bytes(uint32_t target) override;
 };
 
 /**
diff --git a/meson.build b/meson.build
index cd5e04f..e6db5bc 100644
--- a/meson.build
+++ b/meson.build
@@ -17,6 +17,10 @@
 
 sdbusplus_dep = dependency('sdbusplus', required : false)
 
+if get_option('npcm7xx-or-newer').enabled()
+  add_project_arguments('-DNPCM7XX_OR_NEWER', language:'cpp')
+endif
+
 generated_sources = []
 generated_others = []
 if get_option('yocto').disabled()
diff --git a/meson_options.txt b/meson_options.txt
index ad4edc1..c173694 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,2 +1,3 @@
-option('tests', type: 'feature', description: 'Build tests')
-option('yocto', type : 'feature', value : 'disabled')
+option('tests', type: 'feature', description: 'Build tests', value : 'enabled')
+option('yocto', type : 'feature', description : 'Using yocto build or not', value : 'disabled')
+option('npcm7xx-or-newer', type : 'feature', description: 'The BMC is using Nuvoton NCPM7XX+ or not', value : 'disabled')
diff --git a/src/bmc_monitor_app.cpp b/src/bmc_monitor_app.cpp
index 96e0bb0..1c42b6f 100644
--- a/src/bmc_monitor_app.cpp
+++ b/src/bmc_monitor_app.cpp
@@ -130,6 +130,21 @@
                                  ? (times.finish - times.userspace) /
                                        kMillisecondPerSecond
                                  : 0);
+
+#ifdef NPCM7XX_OR_NEWER
+    // NPCM7XX or newer Nuvoton BMC has a register that starts counting from SoC
+    // power on. Also uptime starts counting when kernel is up. Thus we can get
+    // (Firmware + Loader) time by `value[SEC_CNT_ADDR] - uptime`.
+    constexpr uint32_t SEC_CNT_ADDR = 0xF0801068;
+
+    auto powerOnSec = util->readMem4Bytes(SEC_CNT_ADDR);
+    auto upTimeMS = util->getUpTimeInMS();
+    if (powerOnSec != std::nullopt && upTimeMS != std::nullopt)
+    {
+        bootManager->setDuration("FirmwarePlusLoader",
+                                 powerOnSec.value() * 1000 - upTimeMS.value());
+    }
+#endif
 }
 
 } // namespace boot_time_monitor
diff --git a/src/utils.cpp b/src/utils.cpp
index 15eae76..42783d8 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -2,6 +2,9 @@
 
 #include <fmt/printf.h>
 
+#include <boost/interprocess/file_mapping.hpp>
+#include <boost/interprocess/mapped_region.hpp>
+
 #include <array>
 #include <chrono>
 #include <filesystem>
@@ -60,6 +63,44 @@
            (wantCompleted ? kCompletedSuffix : "");
 }
 
+std::optional<uint32_t> Util::readMem4Bytes(uint32_t target)
+{
+    // Typically the pageSize will be 4K/8K for 32 bit operating systems
+    uint32_t pageSize = boost::interprocess::mapped_region::get_page_size();
+    const uint32_t kPageMask = pageSize - 1;
+
+    uint32_t pageOffset = target & (~kPageMask);
+    uint32_t offsetInPage = target & kPageMask;
+
+    // Map `/dev/mem` to a region.
+    std::unique_ptr<boost::interprocess::file_mapping> fileMapping;
+    std::unique_ptr<boost::interprocess::mapped_region> MappedRegion;
+    try
+    {
+        fileMapping = std::make_unique<boost::interprocess::file_mapping>(
+            "/dev/mem", boost::interprocess::read_only);
+        // No need to unmap in the end.
+        MappedRegion = std::make_unique<boost::interprocess::mapped_region>(
+            *fileMapping, boost::interprocess::read_only, pageOffset,
+            pageSize * 2);
+
+        // MappedRegion->get_address() returns (void*) which needs to covert
+        // into (char*) to make `+ offsetInPage` work.
+        // Then converts it again into (uint32_t*) since we read 4 bytes.
+        return *(reinterpret_cast<uint32_t*>(
+            static_cast<char*>(MappedRegion->get_address()) + offsetInPage));
+    }
+    catch (const std::exception& e)
+    {
+        fmt::print(stderr, "[{}]: Throw exception: %s\n", __FUNCTION__,
+                   e.what());
+        return std::nullopt;
+    }
+
+    fmt::print(stderr, "[{}]: Shouldn't go to this line.\n", __FUNCTION__);
+    return std::nullopt;
+}
+
 FileUtil::FileUtil(std::string_view filename) : filename(filename)
 {
     fs::path dir = fs::path(filename).remove_filename();
diff --git a/test/include/mockups.hpp b/test/include/mockups.hpp
index fedae10..387e006 100644
--- a/test/include/mockups.hpp
+++ b/test/include/mockups.hpp
@@ -21,6 +21,7 @@
     MOCK_METHOD(bool, isValidName, (std::string_view), (override));
     MOCK_METHOD(std::string, getCPPath, (std::string_view, bool), (override));
     MOCK_METHOD(std::string, getDurPath, (std::string_view, bool), (override));
+    MOCK_METHOD(std::optional<uint32_t>, readMem4Bytes, (uint32_t), (override));
 };
 
 class MockFileUtil : public FileUtilIface