SPI device code updater

This code updater is for updating spi flash devices.
It can for example update the host firmware on different server boards
and has following features:

- power down the host before update
- set mux gpios to access spi flash
- (very limited) communication with ME (Management Engine)
- use flashrom to utilize fw with IFD (Intel Flash Descriptor)
- otherwise directly write to the flash chip.

The behavior of this code updater can be configured via EM.

Tested: on Tyan S8030 and Tyan S5549 Board. Steps below.

1. Display the fw inventory

```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/717f6f4d"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active"
    }
  ],
  "Members@odata.count": 3,
  "Name": "Software Inventory Collection"
}
```

2. Query BIOS version.
The version is "unknown" here since currently there is no interface
enabled via which to query it.

```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "HostSPIFlash_4950",
  "Name": "Software Inventory",
  "Status": {
    "Health": "Warning",
    "HealthRollup": "OK",
    "State": "Disabled"
  },
  "Updateable": true,
  "Version": "unknown"
}
```

3. Trigger the fw update via redfish.

```
curl -k ${creds} \
	-H "Content-Type:multipart/form-data" \
	-X POST \
	-F UpdateParameters="{\"Targets\":[\"/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_6041\"],\"@Redfish.OperationApplyTime\":\"Immediate\"};type=application/json" \
	-F "UpdateFile=@${fwpath};type=application/octet-stream" \
	https://${bmc}/redfish/v1/UpdateService/update
```

4. Task is returned

```
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task",
  "Id": "0",
  "TaskState": "Running",
  "TaskStatus": "OK"
}
```

5. Query Task status
```
curl --silent $creds https://$bmc/redfish/v1/TaskService/Tasks/0
```

```
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task",
  "EndTime": "2025-02-18T14:05:46+00:00",
  "HidePayload": false,
  "Id": "0",
  "Messages": [
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has started.",
      "MessageArgs": [
        "0"
      ],
      "MessageId": "TaskEvent.1.0.TaskStarted",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 10 percent complete.",
      "MessageArgs": [
        "0",
        "10"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 20 percent complete.",
      "MessageArgs": [
        "0",
        "20"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 70 percent complete.",
      "MessageArgs": [
        "0",
        "70"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 100 percent complete.",
      "MessageArgs": [
        "0",
        "100"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has completed.",
      "MessageArgs": [
        "0"
      ],
      "MessageId": "TaskEvent.1.0.TaskCompletedOK",
      "MessageSeverity": "OK",
      "Resolution": "None."
    }
  ],
  "Name": "Task 0",
  "Payload": {
    "HttpHeaders": [],
    "HttpOperation": "POST",
    "JsonBody": "null",
    "TargetUri": "/redfish/v1/UpdateService/update"
  },
  "PercentComplete": 90,
  "StartTime": "2025-02-18T14:04:47+00:00",
  "TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/0",
  "TaskState": "Completed",
  "TaskStatus": "OK"
}
```

6. Display the fw inventory with newly updated fw.
```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/717f6f4d"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active"
    }
  ],
  "Members@odata.count": 3,
  "Name": "Software Inventory Collection"
}
```

7. Query the new fw version.

The version is 'mycompversion' since that's what has been set in the
pldm fw update package for testing.

```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "HostSPIFlash_8728",
  "Name": "Software Inventory",
  "Status": {
    "Health": "OK",
    "HealthRollup": "OK",
    "State": "Enabled"
  },
  "Updateable": false,
  "Version": "mycompversion"
}
```

Change-Id: I27803b7fded71af2364c2f55fad841a410603dec
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/bios/README.md b/bios/README.md
new file mode 100644
index 0000000..a18017d
--- /dev/null
+++ b/bios/README.md
@@ -0,0 +1,44 @@
+# SPI Device Update Daemon
+
+This daemon is for updating SPI flash chips commonly used for Host Bios.
+
+## Configuration Example 1 (Tyan S8030)
+
+This is an example EM Exposes record which can appear on dbus as
+
+```
+xyz.openbmc_project.Configuration.SPIFlash
+```
+
+```json
+{
+  "Name": "HostSPIFlash",
+  "SPIControllerIndex": "1",
+  "SPIDeviceIndex": "0",
+  "HasME": false,
+  "MuxOutputs": ["BMC_SPI_SEL"],
+  "MuxGPIOValues": [1],
+  "VendorIANA": "6653",
+  "Layout": "Flat",
+  "Tool": "flashrom",
+  "FirmwareInfo": {
+    "VendorIANA": "6653",
+    "CompatibleHardware": "com.tyan.Hardware.S8030.SPI.Host"
+  },
+  "Type": "SPIFlash"
+}
+```
+
+- 'HasME' is referring to the Intel Management Engine.
+
+## Layout information
+
+Sometimes another tool is needed if one does not have a flat image. Use "Layout"
+property to give that hint. Possible values:
+
+- "Flat" : No tool, flat image. This can be used for example when we want to
+  write a flash image which was previously dumped.
+
+## Tool information
+
+We can directly write to the mtd device or use flashrom to do the writing.
diff --git a/bios/bios_software_manager.cpp b/bios/bios_software_manager.cpp
new file mode 100644
index 0000000..61a9fef
--- /dev/null
+++ b/bios/bios_software_manager.cpp
@@ -0,0 +1,104 @@
+#include "bios_software_manager.hpp"
+
+#include "common/include/dbus_helper.hpp"
+#include "common/include/software_manager.hpp"
+#include "spi_device.hpp"
+
+#include <gpiod.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/bus.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+
+using namespace phosphor::software;
+
+PHOSPHOR_LOG2_USING;
+
+BIOSSoftwareManager::BIOSSoftwareManager(sdbusplus::async::context& ctx,
+                                         bool isDryRun) :
+    SoftwareManager(ctx, configTypeBIOS), dryRun(isDryRun)
+{}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> BIOSSoftwareManager::initDevice(
+    const std::string& service, const std::string& path, SoftwareConfig& config)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    std::string configIface =
+        "xyz.openbmc_project.Configuration." + config.configType;
+
+    std::optional<int64_t> spiControllerIndex =
+        co_await dbusGetRequiredProperty<uint64_t>(
+            ctx, service, path, configIface, "SPIControllerIndex");
+
+    std::optional<int64_t> spiDeviceIndex =
+        co_await dbusGetRequiredProperty<uint64_t>(
+            ctx, service, path, configIface, "SPIDeviceIndex");
+
+    const std::string configIfaceMux = configIface + ".MuxOutputs";
+
+    std::vector<std::string> names;
+    std::vector<uint64_t> values;
+
+    for (size_t i = 0; true; i++)
+    {
+        const std::string iface = configIfaceMux + std::to_string(i);
+
+        std::optional<std::string> name =
+            co_await dbusGetRequiredProperty<std::string>(ctx, service, path,
+                                                          iface, "Name");
+
+        std::optional<std::string> polarity =
+            co_await dbusGetRequiredProperty<std::string>(ctx, service, path,
+                                                          iface, "Polarity");
+
+        if (!name.has_value() || !polarity.has_value())
+        {
+            break;
+        }
+
+        names.push_back(name.value());
+        values.push_back((polarity == "High") ? 1 : 0);
+    }
+
+    if (!spiControllerIndex.has_value() || !spiDeviceIndex.has_value())
+    {
+        error("Error: Missing property");
+        co_return false;
+    }
+
+    enum FlashLayout layout = flashLayoutFlat;
+    enum FlashTool tool = flashToolNone;
+
+    debug("SPI device: {INDEX1}:{INDEX2}", "INDEX1", spiControllerIndex.value(),
+          "INDEX2", spiDeviceIndex.value());
+
+    std::unique_ptr<SPIDevice> spiDevice;
+    try
+    {
+        spiDevice = std::make_unique<SPIDevice>(
+            ctx, spiControllerIndex.value(), spiDeviceIndex.value(), dryRun,
+            names, values, config, this, layout, tool);
+    }
+    catch (std::exception& e)
+    {
+        co_return false;
+    }
+
+    std::unique_ptr<Software> software =
+        std::make_unique<Software>(ctx, *spiDevice);
+
+    // enable this software to be updated
+    std::set<RequestedApplyTimes> allowedApplyTimes = {
+        RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset};
+
+    software->enableUpdate(allowedApplyTimes);
+
+    spiDevice->softwareCurrent = std::move(software);
+
+    spiDevice->softwareCurrent->setVersion(SPIDevice::getVersion());
+
+    devices.insert({config.objectPath, std::move(spiDevice)});
+
+    co_return true;
+}
diff --git a/bios/bios_software_manager.hpp b/bios/bios_software_manager.hpp
new file mode 100644
index 0000000..7e52bb7
--- /dev/null
+++ b/bios/bios_software_manager.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "common/include/software_manager.hpp"
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/context.hpp>
+
+using namespace phosphor::software::manager;
+
+const std::string configTypeBIOS = "BIOS";
+
+class BIOSSoftwareManager : public SoftwareManager
+{
+  public:
+    BIOSSoftwareManager(sdbusplus::async::context& ctx, bool isDryRun);
+
+    sdbusplus::async::task<bool> initDevice(const std::string& service,
+                                            const std::string& path,
+                                            SoftwareConfig& config) final;
+
+  private:
+    bool dryRun;
+};
diff --git a/bios/main.cpp b/bios/main.cpp
new file mode 100644
index 0000000..abed9bc
--- /dev/null
+++ b/bios/main.cpp
@@ -0,0 +1,40 @@
+#include "bios_software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+
+void run(bool dryRun)
+{
+    sdbusplus::async::context ctx;
+
+    std::vector<std::string> configIntfs = {
+        "xyz.openbmc_project.Configuration.SPIFlash",
+    };
+
+    BIOSSoftwareManager spidcu(ctx, dryRun);
+
+    ctx.spawn(spidcu.initDevices(configIntfs));
+
+    ctx.run();
+}
+
+int main(int argc, char* argv[])
+{
+    bool dryRun = false;
+
+    for (int i = 1; i < argc; i++)
+    {
+        std::string arg = std::string(argv[i]);
+        if (arg == "--dryrun")
+        {
+            dryRun = true;
+        }
+    }
+
+    run(dryRun);
+
+    return 0;
+}
diff --git a/bios/meson.build b/bios/meson.build
new file mode 100644
index 0000000..358e5d8
--- /dev/null
+++ b/bios/meson.build
@@ -0,0 +1,32 @@
+
+bios_spi_src = files('bios_software_manager.cpp'
+, 'spi_device.cpp')
+
+bios_spi_include = include_directories('.')
+
+executable(
+    'phosphor-bios-software-update',
+    'main.cpp',
+    bios_spi_src,
+    include_directories: [common_include, bios_spi_include],
+    dependencies: [
+        sdbusplus_dep,
+        phosphor_logging_dep,
+        pdi_dep,
+        boost_dep,
+        libgpiod,
+        libpldm_dep,
+    ],
+    link_with: [libpldmutil, software_common_lib],
+    install: true,
+)
+
+systemd_system_unit_dir = dependency('systemd').get_variable(
+    'systemdsystemunitdir',
+    pkgconfig_define: ['prefix', get_option('prefix')],
+)
+
+install_data(
+    'xyz.openbmc_project.Software.BIOS.service',
+    install_dir: systemd_system_unit_dir,
+)
diff --git a/bios/spi_device.cpp b/bios/spi_device.cpp
new file mode 100644
index 0000000..93cb9a3
--- /dev/null
+++ b/bios/spi_device.cpp
@@ -0,0 +1,519 @@
+#include "spi_device.hpp"
+
+#include "common/include/NotifyWatch.hpp"
+#include "common/include/device.hpp"
+#include "common/include/host_power.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <gpiod.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+#include <xyz/openbmc_project/State/Host/client.hpp>
+
+#include <cstddef>
+#include <fstream>
+#include <random>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace std::literals;
+using namespace phosphor::software;
+using namespace phosphor::software::manager;
+using namespace phosphor::software::host_power;
+
+SPIDevice::SPIDevice(sdbusplus::async::context& ctx,
+                     uint64_t spiControllerIndex, uint64_t spiDeviceIndex,
+                     bool dryRun, const std::vector<std::string>& gpioLinesIn,
+                     const std::vector<uint64_t>& gpioValuesIn,
+                     SoftwareConfig& config, SoftwareManager* parent,
+                     enum FlashLayout layout, enum FlashTool tool,
+                     const std::string& versionDirPath) :
+    Device(ctx, config, parent,
+           {RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset}),
+    NotifyWatchIntf(ctx, versionDirPath), dryRun(dryRun),
+    gpioLines(gpioLinesIn),
+    gpioValues(gpioValuesIn.begin(), gpioValuesIn.end()),
+    spiControllerIndex(spiControllerIndex), spiDeviceIndex(spiDeviceIndex),
+    layout(layout), tool(tool)
+{
+    // To probe the driver for our spi flash, we need the memory-mapped address
+    // of the spi peripheral. These values are specific to aspeed BMC.
+    // https://github.com/torvalds/linux/blob/master/arch/arm/boot/dts/aspeed/aspeed-g6.dtsi
+    std::map<uint32_t, std::string> spiDevAddr = {
+        {0, "1e620000.spi"},
+        {1, "1e630000.spi"},
+        {2, "1e631000.spi"},
+    };
+
+    if (spiControllerIndex >= spiDevAddr.size())
+    {
+        throw std::invalid_argument("SPI controller index out of bounds");
+    }
+
+    spiDev = spiDevAddr[spiControllerIndex];
+
+    ctx.spawn(readNotifyAsync());
+
+    debug(
+        "SPI Device {NAME} at {CONTROLLERINDEX}:{DEVICEINDEX} initialized successfully",
+        "NAME", config.configName, "CONTROLLERINDEX", spiControllerIndex,
+        "DEVICEINDEX", spiDeviceIndex);
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::updateDevice(const uint8_t* image,
+                                                     size_t image_size)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    // NOLINTBEGIN(readability-static-accessed-through-instance)
+    // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Branch)
+    auto prevPowerstate = co_await HostPower::getState(ctx);
+
+    if (prevPowerstate != stateOn && prevPowerstate != stateOff)
+    {
+        co_return false;
+    }
+
+    // NOLINTBEGIN(readability-static-accessed-through-instance)
+    bool success = co_await HostPower::setState(ctx, stateOff);
+    // NOLINTEND(readability-static-accessed-through-instance)
+    if (!success)
+    {
+        error("error changing host power state");
+        co_return false;
+    }
+    setUpdateProgress(10);
+
+    success = co_await writeSPIFlash(image, image_size);
+
+    if (success)
+    {
+        setUpdateProgress(100);
+    }
+
+    // restore the previous powerstate
+    const bool powerstate_restore =
+        co_await HostPower::setState(ctx, prevPowerstate);
+    if (!powerstate_restore)
+    {
+        error("error changing host power state");
+        co_return false;
+    }
+
+    // return value here is only describing if we successfully wrote to the
+    // SPI flash. Restoring powerstate can still fail.
+    co_return success;
+    // NOLINTEND(readability-static-accessed-through-instance)
+}
+
+const std::string spiAspeedSMCPath = "/sys/bus/platform/drivers/spi-aspeed-smc";
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::bindSPIFlash()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("binding flash to SMC");
+
+    if (SPIDevice::isSPIFlashBound())
+    {
+        debug("flash was already bound, unbinding it now");
+        bool success = co_await SPIDevice::unbindSPIFlash();
+
+        if (!success)
+        {
+            error("error unbinding spi flash");
+            co_return false;
+        }
+    }
+
+    std::ofstream ofbind(spiAspeedSMCPath + "/bind", std::ofstream::out);
+    ofbind << spiDev;
+    ofbind.close();
+
+    const int driverBindSleepDuration = 2;
+
+    co_await sdbusplus::async::sleep_for(
+        ctx, std::chrono::seconds(driverBindSleepDuration));
+
+    const bool isBound = isSPIFlashBound();
+
+    if (!isBound)
+    {
+        error("failed to bind spi device");
+    }
+
+    co_return isBound;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::unbindSPIFlash()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("unbinding flash from SMC");
+    std::ofstream ofunbind(spiAspeedSMCPath + "/unbind", std::ofstream::out);
+    ofunbind << spiDev;
+    ofunbind.close();
+
+    // wait for kernel
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(2));
+
+    co_return !isSPIFlashBound();
+}
+
+bool SPIDevice::isSPIFlashBound()
+{
+    std::string path = spiAspeedSMCPath + "/" + spiDev;
+
+    return std::filesystem::exists(path);
+}
+
+static std::unique_ptr<::gpiod::line_bulk> requestMuxGPIOs(
+    const std::vector<std::string>& gpioLines,
+    const std::vector<int>& gpioValues, bool inverted)
+{
+    std::vector<::gpiod::line> lines;
+
+    for (const std::string& lineName : gpioLines)
+    {
+        const ::gpiod::line line = ::gpiod::find_line(lineName);
+
+        if (line.is_used())
+        {
+            error("gpio line {LINE} was still used", "LINE", lineName);
+            return nullptr;
+        }
+
+        lines.push_back(line);
+    }
+
+    ::gpiod::line_request config{"", ::gpiod::line_request::DIRECTION_OUTPUT,
+                                 0};
+
+    debug("[gpio] requesting gpios to mux SPI to BMC");
+
+    auto lineBulk = std::make_unique<::gpiod::line_bulk>(lines);
+
+    if (inverted)
+    {
+        std::vector<int> valuesInverted;
+        valuesInverted.reserve(gpioValues.size());
+
+        for (int value : gpioValues)
+        {
+            valuesInverted.push_back(value ? 0 : 1);
+        }
+
+        lineBulk->request(config, valuesInverted);
+    }
+    else
+    {
+        lineBulk->request(config, gpioValues);
+    }
+
+    return lineBulk;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlash(const uint8_t* image,
+                                                      size_t image_size)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("[gpio] requesting gpios to mux SPI to BMC");
+
+    std::unique_ptr<::gpiod::line_bulk> lineBulk =
+        requestMuxGPIOs(gpioLines, gpioValues, false);
+
+    if (!lineBulk)
+    {
+        co_return false;
+    }
+
+    bool success = co_await SPIDevice::bindSPIFlash();
+    if (success)
+    {
+        if (dryRun)
+        {
+            info("dry run, NOT writing to the chip");
+        }
+        else
+        {
+            if (tool == flashToolFlashrom)
+            {
+                const int status =
+                    co_await SPIDevice::writeSPIFlashWithFlashrom(image,
+                                                                  image_size);
+                if (status != 0)
+                {
+                    error(
+                        "Error writing to SPI flash {CONTROLLERINDEX}:{DEVICEINDEX}, exit code {EXITCODE}",
+                        "CONTROLLERINDEX", spiControllerIndex, "DEVICEINDEX",
+                        spiDeviceIndex, "EXITCODE", status);
+                }
+                success = (status == 0);
+            }
+            else
+            {
+                success =
+                    co_await SPIDevice::writeSPIFlashDefault(image, image_size);
+            }
+        }
+
+        success = success && co_await SPIDevice::unbindSPIFlash();
+    }
+
+    lineBulk->release();
+
+    // switch bios flash back to host via mux / GPIO
+    // (not assume there is a pull to the default value)
+    debug("[gpio] requesting gpios to mux SPI to Host");
+
+    lineBulk = requestMuxGPIOs(gpioLines, gpioValues, true);
+
+    if (!lineBulk)
+    {
+        co_return success;
+    }
+
+    lineBulk->release();
+
+    co_return success;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<int> asyncSystem(sdbusplus::async::context& ctx,
+                                        const std::string& cmd)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    int pipefd[2];
+    if (pipe(pipefd) == -1)
+    {
+        perror("pipe");
+        co_return -1;
+    }
+
+    pid_t pid = fork();
+    if (pid == -1)
+    {
+        perror("fork");
+        close(pipefd[0]);
+        close(pipefd[1]);
+        co_return -1;
+    }
+    else if (pid == 0)
+    {
+        close(pipefd[0]);
+        int exitCode = std::system(cmd.c_str());
+
+        ssize_t status = write(pipefd[1], &exitCode, sizeof(exitCode));
+        close(pipefd[1]);
+        exit((status == sizeof(exitCode)) ? 0 : 1);
+    }
+    else
+    {
+        close(pipefd[1]);
+
+        sdbusplus::async::fdio pipe_fdio(ctx, pipefd[0]);
+
+        co_await pipe_fdio.next();
+
+        int status;
+        waitpid(pid, &status, 0);
+        close(pipefd[0]);
+
+        co_return WEXITSTATUS(status);
+    }
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<int> SPIDevice::writeSPIFlashWithFlashrom(
+    const uint8_t* image, size_t image_size) const
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    // randomize the name to enable parallel updates
+    const std::string path = "/tmp/spi-device-image-" +
+                             std::to_string(Software::getRandomId()) + ".bin";
+
+    int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
+    if (fd < 0)
+    {
+        error("Failed to open file: {PATH}", "PATH", path);
+        co_return 1;
+    }
+
+    const ssize_t bytesWritten = write(fd, image, image_size);
+
+    close(fd);
+
+    setUpdateProgress(30);
+
+    if (bytesWritten < 0 || static_cast<size_t>(bytesWritten) != image_size)
+    {
+        error("Failed to write image to file");
+        co_return 1;
+    }
+
+    debug("wrote {SIZE} bytes to {PATH}", "SIZE", bytesWritten, "PATH", path);
+
+    auto devPath = getMTDDevicePath();
+
+    if (!devPath.has_value())
+    {
+        co_return 1;
+    }
+
+    std::string cmd = "flashrom -p linux_mtd:dev=" + devPath.value();
+
+    if (layout == flashLayoutFlat)
+    {
+        cmd += " -w " + path;
+    }
+    else
+    {
+        error("unsupported flash layout");
+
+        co_return 1;
+    }
+
+    debug("[flashrom] running {CMD}", "CMD", cmd);
+
+    const int exitCode = co_await asyncSystem(ctx, cmd);
+
+    std::filesystem::remove(path);
+
+    co_return exitCode;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlashDefault(
+    const uint8_t* image, size_t image_size)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    auto devPath = getMTDDevicePath();
+
+    if (!devPath.has_value())
+    {
+        co_return false;
+    }
+
+    int fd = open(devPath.value().c_str(), O_WRONLY);
+    if (fd < 0)
+    {
+        error("Failed to open device: {PATH}", "PATH", devPath.value());
+        co_return false;
+    }
+
+    // Write the image in chunks to avoid blocking for too long.
+    // Also, to provide meaningful progress updates.
+
+    const size_t chunk = static_cast<size_t>(1024 * 1024);
+    ssize_t bytesWritten = 0;
+
+    const int progressStart = 30;
+    const int progressEnd = 90;
+
+    for (size_t offset = 0; offset < image_size; offset += chunk)
+    {
+        const ssize_t written =
+            write(fd, image + offset, std::min(chunk, image_size - offset));
+
+        if (written < 0)
+        {
+            error("Failed to write to device");
+            co_return false;
+        }
+
+        bytesWritten += written;
+
+        setUpdateProgress(
+            progressStart + int((progressEnd - progressStart) *
+                                (double(offset) / double(image_size))));
+    }
+
+    close(fd);
+
+    if (static_cast<size_t>(bytesWritten) != image_size)
+    {
+        error("Incomplete write to device");
+        co_return false;
+    }
+
+    debug("Successfully wrote {NBYTES} bytes to {PATH}", "NBYTES", bytesWritten,
+          "PATH", devPath.value());
+
+    co_return true;
+}
+
+std::string SPIDevice::getVersion()
+{
+    std::string version{};
+    try
+    {
+        std::ifstream config(biosVersionPath);
+
+        config >> version;
+    }
+    catch (std::exception& e)
+    {
+        error("Failed to get version with {ERROR}", "ERROR", e.what());
+        version = versionUnknown;
+    }
+
+    if (version.empty())
+    {
+        version = versionUnknown;
+    }
+
+    return version;
+}
+
+// NOLINTNEXTLINE(readability-static-accessed-through-instance)
+auto SPIDevice::processUpdate(std::string versionFileName)
+    -> sdbusplus::async::task<>
+{
+    if (biosVersionFilename != versionFileName)
+    {
+        error(
+            "Update config file name '{NAME}' (!= '{EXPECTED}') is not expected",
+            "NAME", versionFileName, "EXPECTED", biosVersionFilename);
+        co_return;
+    }
+
+    if (softwareCurrent)
+    {
+        softwareCurrent->setVersion(getVersion());
+    }
+
+    co_return;
+}
+
+std::optional<std::string> SPIDevice::getMTDDevicePath() const
+{
+    const std::string spiPath =
+        "/sys/class/spi_master/spi" + std::to_string(spiControllerIndex) +
+        "/spi" + std::to_string(spiControllerIndex) + "." +
+        std::to_string(spiDeviceIndex) + "/mtd/";
+
+    if (!std::filesystem::exists(spiPath))
+    {
+        error("Error: SPI path not found: {PATH}", "PATH", spiPath);
+        return "";
+    }
+
+    for (const auto& entry : std::filesystem::directory_iterator(spiPath))
+    {
+        const std::string mtdName = entry.path().filename().string();
+
+        if (mtdName.starts_with("mtd") && !mtdName.ends_with("ro"))
+        {
+            return "/dev/" + mtdName;
+        }
+    }
+
+    error("Error: No MTD device found for spi {CONTROLLERINDEX}.{DEVICEINDEX}",
+          "CONTROLLERINDEX", spiControllerIndex, "DEVICEINDEX", spiDeviceIndex);
+
+    return std::nullopt;
+}
diff --git a/bios/spi_device.hpp b/bios/spi_device.hpp
new file mode 100644
index 0000000..d05ab89
--- /dev/null
+++ b/bios/spi_device.hpp
@@ -0,0 +1,117 @@
+#pragma once
+
+#include "common/include/NotifyWatch.hpp"
+#include "common/include/device.hpp"
+#include "common/include/software.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async/context.hpp>
+
+#include <string>
+
+class SPIDevice;
+
+using namespace phosphor::software;
+using namespace phosphor::software::manager;
+using namespace phosphor::notify::watch;
+
+using NotifyWatchIntf = phosphor::notify::watch::NotifyWatch<SPIDevice>;
+
+const std::string biosVersionDirPath = "/var/bios/";
+const std::string biosVersionFilename = "host0_bios_version.txt";
+const std::string biosVersionPath = biosVersionDirPath + biosVersionFilename;
+
+const std::string versionUnknown = "Unknown";
+
+enum FlashLayout
+{
+    flashLayoutFlat,
+    flashLayoutIntelFlashDescriptor,
+};
+
+enum FlashTool
+{
+    flashToolNone,     // write directly to the mtd device
+    flashToolFlashrom, // use flashrom, to handle e.g. IFD
+};
+
+class SPIDevice : public Device, public NotifyWatchIntf
+{
+  public:
+    using Device::softwareCurrent;
+    SPIDevice(sdbusplus::async::context& ctx, uint64_t spiControllerIndex,
+              uint64_t spiDeviceIndex, bool dryRun,
+              const std::vector<std::string>& gpioLinesIn,
+              const std::vector<uint64_t>& gpioValuesIn, SoftwareConfig& config,
+              SoftwareManager* parent, enum FlashLayout layout,
+              enum FlashTool tool,
+              const std::string& versionDirPath = biosVersionDirPath);
+
+    sdbusplus::async::task<bool> updateDevice(const uint8_t* image,
+                                              size_t image_size) final;
+
+    // @returns       the bios version which is externally provided.
+    static std::string getVersion();
+
+    /** @brief Process async changes to cable configuration */
+    auto processUpdate(std::string versionFileName) -> sdbusplus::async::task<>;
+
+  private:
+    bool dryRun;
+
+    std::vector<std::string> gpioLines;
+
+    std::vector<int> gpioValues;
+
+    uint64_t spiControllerIndex;
+    uint64_t spiDeviceIndex;
+
+    // e.g. "1e631000.spi"
+    std::string spiDev;
+
+    enum FlashLayout layout;
+
+    enum FlashTool tool;
+
+    // @returns          true on success
+    sdbusplus::async::task<bool> bindSPIFlash();
+
+    // @returns          true on success
+    sdbusplus::async::task<bool> unbindSPIFlash();
+
+    bool isSPIFlashBound();
+
+    // @description preconditions:
+    // - host is powered off
+    // @returns   true on success
+    sdbusplus::async::task<bool> writeSPIFlash(const uint8_t* image,
+                                               size_t image_size);
+
+    // @description preconditions:
+    // - host is powered off
+    // - gpio / mux is set
+    // - spi device is bound to the driver
+    // we write the flat image here
+    // @param image           the component image
+    // @param image_size      size of 'image'
+    // @returns               true on success
+    sdbusplus::async::task<bool> writeSPIFlashDefault(const uint8_t* image,
+                                                      size_t image_size);
+
+    // @description preconditions:
+    // - host is powered off
+    // - gpio / mux is set
+    // - spi device is bound to the driver
+    // we use 'flashrom' here to write the image since it can deal with
+    // Intel Flash Descriptor
+    // @param image           the component image
+    // @param image_size      size of 'image'
+    // @returns               0 on success
+    sdbusplus::async::task<int> writeSPIFlashWithFlashrom(
+        const uint8_t* image, size_t image_size) const;
+
+    // @returns nullopt on error
+    std::optional<std::string> getMTDDevicePath() const;
+};
diff --git a/bios/xyz.openbmc_project.Software.BIOS.service b/bios/xyz.openbmc_project.Software.BIOS.service
new file mode 100644
index 0000000..c26f1f2
--- /dev/null
+++ b/bios/xyz.openbmc_project.Software.BIOS.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=Host FW Code Update Daemon
+Wants=xyz.openbmc_project.State.Chassis@0.service
+After=xyz.openbmc_project.State.Chassis@0.service
+Requires=xyz.openbmc_project.EntityManager.service
+After=xyz.openbmc_project.EntityManager.service
+
+[Service]
+Restart=yes
+Type=oneshot
+RemainAfterExit=no
+ExecStart=/usr/bin/phosphor-bios-software-update
+
+[Install]
+WantedBy=multi-user.target
diff --git a/common/include/NotifyWatch.hpp b/common/include/NotifyWatch.hpp
new file mode 100644
index 0000000..8b5c795
--- /dev/null
+++ b/common/include/NotifyWatch.hpp
@@ -0,0 +1,121 @@
+#pragma once
+#include <sys/inotify.h>
+#include <unistd.h>
+
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/async/fdio.hpp>
+#include <sdbusplus/async/task.hpp>
+
+#include <array>
+#include <cerrno>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <filesystem>
+#include <memory>
+#include <span>
+#include <string>
+#include <system_error>
+namespace phosphor::notify::watch
+{
+namespace fs = std::filesystem;
+template <typename Instance>
+class NotifyWatch
+{
+  public:
+    NotifyWatch() = delete;
+    NotifyWatch(const NotifyWatch&) = delete;
+    NotifyWatch& operator=(const NotifyWatch&) = delete;
+    NotifyWatch(NotifyWatch&&) = delete;
+    NotifyWatch& operator=(NotifyWatch&&) = delete;
+
+    explicit NotifyWatch(sdbusplus::async::context& ctx,
+                         const std::string& dir) : notifyCtx(ctx)
+    {
+        std::error_code ec = {};
+        fs::path dirPath(dir);
+        if (!fs::create_directories(dirPath, ec))
+        {
+            if (ec)
+            {
+                throw std::system_error(ec,
+                                        "Failed to create directory " + dir);
+            }
+        }
+        fd = inotify_init1(IN_NONBLOCK);
+        if (-1 == fd)
+        {
+            throw std::system_error(errno, std::system_category(),
+                                    "inotify_init1 failed");
+        }
+        wd = inotify_add_watch(fd, dir.c_str(), IN_CLOSE_WRITE);
+        if (-1 == wd)
+        {
+            close(fd);
+            throw std::system_error(errno, std::system_category(),
+                                    "inotify_add_watch failed");
+        }
+        fdioInstance = std::make_unique<sdbusplus::async::fdio>(ctx, fd);
+    }
+    ~NotifyWatch()
+    {
+        if (-1 != fd)
+        {
+            if (-1 != wd)
+            {
+                inotify_rm_watch(fd, wd);
+            }
+            close(fd);
+        }
+    }
+    sdbusplus::async::task<> readNotifyAsync()
+    {
+        co_await fdioInstance->next();
+        constexpr size_t maxBytes = 1024;
+        std::array<uint8_t, maxBytes> buffer{};
+        auto bytes = read(fd, buffer.data(), maxBytes);
+        if (0 > bytes)
+        {
+            throw std::system_error(errno, std::system_category(),
+                                    "Failed to read notify event");
+        }
+        auto offset = 0;
+        while (offset < bytes)
+        {
+            // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast)
+            std::span<uint32_t> mask{
+                reinterpret_cast<uint32_t*>(
+                    buffer.data() + offset + offsetof(inotify_event, mask)),
+                1};
+            std::span<uint32_t> len{
+                reinterpret_cast<uint32_t*>(
+                    buffer.data() + offset + offsetof(inotify_event, len)),
+                1};
+            // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)
+            if (((mask[0] & IN_CLOSE_WRITE) != 0U) &&
+                ((mask[0] & IN_ISDIR) == 0U))
+            {
+                // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast)
+                std::span<char> name{
+                    reinterpret_cast<char*>(
+                        buffer.data() + offset + offsetof(inotify_event, name)),
+                    len[0]};
+                // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)
+                co_await static_cast<Instance*>(this)->processUpdate(
+                    std::string(name.begin(), name.end()));
+            }
+            offset += offsetof(inotify_event, name) + len[0];
+        }
+        if (!notifyCtx.stop_requested())
+        {
+            notifyCtx.spawn(readNotifyAsync());
+        }
+    }
+
+  private:
+    sdbusplus::async::context& notifyCtx;
+    int wd = -1;
+    int fd = -1;
+    std::unique_ptr<sdbusplus::async::fdio> fdioInstance;
+};
+} // namespace phosphor::notify::watch
diff --git a/common/include/software.hpp b/common/include/software.hpp
index d3e8ffc..849e959 100644
--- a/common/include/software.hpp
+++ b/common/include/software.hpp
@@ -98,6 +98,8 @@
     std::unique_ptr<SoftwareActivationProgress> softwareActivationProgress =
         nullptr;
 
+    static long int getRandomId();
+
   protected:
     // @returns        a random software id (swid) for that device
     static std::string getRandomSoftwareId(device::Device& parent);
diff --git a/common/src/software.cpp b/common/src/software.cpp
index 31b2b75..504a9ae 100644
--- a/common/src/software.cpp
+++ b/common/src/software.cpp
@@ -55,7 +55,7 @@
           "OBJPATH", objPath);
 };
 
-static long int getRandomId()
+long int Software::getRandomId()
 {
     struct timespec ts;
     clock_gettime(CLOCK_REALTIME, &ts);
diff --git a/meson.build b/meson.build
index f5420ea..5b8f78c 100644
--- a/meson.build
+++ b/meson.build
@@ -68,15 +68,24 @@
 
 common_include = include_directories('.')
 
-common_build = build_tests.allowed()
+common_build = build_tests.allowed() or get_option('bios-software-update').enabled()
 
 if common_build
     libpldm_dep = dependency('libpldm')
 
+    libgpiod = dependency(
+        'libgpiodcxx',
+        default_options: ['bindings=cxx'],
+        version: '>=1.1.2',
+    )
+
     subdir('common')
 endif
 
+if get_option('bios-software-update').enabled()
+    subdir('bios')
+endif
+
 if build_tests.allowed()
     subdir('test')
 endif
-
diff --git a/meson.options b/meson.options
index 0907c38..b88eac6 100644
--- a/meson.options
+++ b/meson.options
@@ -56,6 +56,13 @@
     description: 'Automatic flash side switch on boot',
 )
 
+option(
+    'bios-software-update',
+    type: 'feature',
+    value: 'enabled',
+    description: 'Enable BIOS/Host firmware update',
+)
+
 # Variables
 option(
     'active-bmc-max-allowed',
diff --git a/subprojects/libgpiod.wrap b/subprojects/libgpiod.wrap
new file mode 100644
index 0000000..e85aa49
--- /dev/null
+++ b/subprojects/libgpiod.wrap
@@ -0,0 +1,12 @@
+[wrap-file]
+directory = libgpiod-1.6.3
+source_url = https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/snapshot/libgpiod-1.6.3.tar.gz
+source_filename = libgpiod-1.6.3.tar.gz
+source_hash = eb446070be1444fd7d32d32bbca53c2f3bbb0a21193db86198cf6050b7a28441
+patch_filename = libgpiod_1.6.3-1_patch.zip
+patch_url = https://wrapdb.mesonbuild.com/v2/libgpiod_1.6.3-1/get_patch
+patch_hash = 76821c637073679a88f77593c6f7ce65b4b5abf8c998f823fffa13918c8761df
+
+[provide]
+libgpiod = gpiod_dep
+libgpiodcxx = gpiodcxx_dep