fw update: 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 S5549 Board

Change-Id: I27803b7fded71af2364c2f55fad841a410603dec
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/bios-spi/README.md b/bios-spi/README.md
new file mode 100644
index 0000000..78ba6f6
--- /dev/null
+++ b/bios-spi/README.md
@@ -0,0 +1,100 @@
+# 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
+```
+
+This config is untested.
+
+```json
+{
+  "Name": "HostSPIFlash",
+  "Path": "1e630000.spi",
+  "HasME": false,
+  "Layout": "Flat",
+  "MuxGpios": [
+    {
+      "Name": "BMC_SPI_SEL",
+      "Polarity": "High"
+    }
+  ],
+  "VendorIANA": "6653",
+  "Compatible": "com.tyan.Hardware.S8030.SPI.Host",
+  "Type": "SPIFlash"
+}
+```
+
+- 'HasME' is referring to the Intel Management Engine.
+
+## Configuration Example 2 (Tyan S5549)
+
+Here we are writing a previously dumped flash image using flashrom.
+
+```json
+{
+  "Name": "HostSPIFlash",
+  "Path": "1e630000.spi",
+  "HasME": true,
+  "Layout": "Flat",
+  "Tool": "flashrom",
+  "MuxGpios": [
+    {
+      "Name": "FM_BIOS_SPI_SWITCH",
+      "Polarity": "High"
+    },
+    {
+      "Name": "FM_BMC_FLASH_SEC_OVRD_N",
+      "Polarity": "High"
+    }
+  ],
+  "VendorIANA": "6653",
+  "Compatible": "com.tyan.Hardware.S5549.SPI.Host",
+  "Type": "SPIFlash"
+}
+```
+
+## Layout information
+
+Sometimes another tool is needed if one does not have a flat image.
+Configuration fragments below.
+
+No tool, flat image. This can be used for example when we want to write a flash
+image which was previously dumped.
+
+```json
+{
+  "Layout": "Flat"
+}
+```
+
+Use flashrom to write with information from an intel flash descriptor
+
+```json
+{
+  "Layout": "IFD"
+}
+```
+
+## Tool information
+
+We can directly write to the mtd device or use flashrom to do the writing. In
+case you want to use Intel Flash Descriptor (not flat layout) or do additional
+verification, flashrom can be used.
+
+```json
+{
+  "Tool": "flashrom"
+}
+```
+
+```json
+{
+  "Tool": "None"
+}
+```
diff --git a/bios-spi/main.cpp b/bios-spi/main.cpp
new file mode 100644
index 0000000..0a63d92
--- /dev/null
+++ b/bios-spi/main.cpp
@@ -0,0 +1,136 @@
+#include "spi_device_code_updater.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>
+
+#include <fstream>
+#include <iostream>
+
+// implementing the design
+// https://github.com/openbmc/docs/blob/377ed14df4913010752ee2faff994a50e12a6316/designs/code-update.md
+
+// NOLINTNEXTLINE
+sdbusplus::async::task<> startManualUpdate(sdbusplus::async::context& ctx,
+                                           SPIDeviceCodeUpdater& spidcu,
+                                           const std::string& imageFilename)
+{
+    if (spidcu.devices.empty())
+    {
+        lg2::error("no device available for manual update");
+        co_return;
+    }
+
+    const std::unique_ptr<Device>& device = *spidcu.devices.begin();
+
+    std::ifstream file(imageFilename, std::ios::binary | std::ios::ate);
+
+    if (!file.good())
+    {
+        lg2::error("error opening file {FILENAME}", "FILENAME", imageFilename);
+        co_return;
+    }
+
+    std::streamsize size = file.tellg();
+    file.seekg(0, std::ios::beg);
+
+    auto buffer = std::make_unique<uint8_t[]>(size);
+
+    if (!file.read(reinterpret_cast<char*>(buffer.get()), size))
+    {
+        throw std::runtime_error("Error reading file: " + imageFilename);
+    }
+
+    // TODO: find the proper object path here
+    auto sap =
+        std::make_unique<SoftwareActivationProgress>(ctx, "/dummyActivation");
+
+    co_await device->updateDevice(buffer.get(), size, sap);
+
+    co_return;
+}
+
+// NOLINTNEXTLINE
+sdbusplus::async::task<> start(sdbusplus::async::context& ctx,
+                               SPIDeviceCodeUpdater& spidcu, bool manual,
+                               const std::string& imageFilename)
+{
+    std::vector<std::string> configIntfs = {
+        "xyz.openbmc_project.Configuration." + configTypeSPIDevice};
+
+    co_await spidcu.getInitialConfiguration(configIntfs);
+
+    if (manual)
+    {
+        co_await startManualUpdate(ctx, spidcu, imageFilename);
+    }
+
+    co_return;
+}
+
+void printHelpText()
+{
+    std::cout << "--help              : print help" << std::endl;
+    std::cout << "--manual            : start a manual update" << std::endl;
+    std::cout << "--image <filename>  : filename for manual update"
+              << std::endl;
+}
+
+int main(int argc, char* argv[])
+{
+    // getting a really unspecific error from clang-tidy here
+    // about an uninitialized / garbage branch. Happy to discuss.
+
+    // NOLINTBEGIN
+
+    sdbusplus::async::context ctx;
+
+    bool manualUpdate = false;
+    bool printHelp = false;
+    bool dryRun = false;
+    bool debug = false;
+    std::string imageFilename = "";
+
+    for (int i = 1; i < argc; i++)
+    {
+        std::string arg = std::string(argv[i]);
+        if (arg == "--manual")
+        {
+            manualUpdate = true;
+        }
+        if (arg == "--image" && i < argc - 1)
+        {
+            imageFilename = std::string(argv[i + 1]);
+            i++;
+        }
+        if (arg == "--help")
+        {
+            printHelp = true;
+        }
+        if (arg == "--dryrun")
+        {
+            dryRun = true;
+        }
+        if (arg == "-debug")
+        {
+            debug = true;
+        }
+    }
+
+    if (printHelp)
+    {
+        printHelpText();
+    }
+
+    SPIDeviceCodeUpdater spidcu(ctx, dryRun, debug);
+
+    ctx.spawn(start(ctx, spidcu, manualUpdate, imageFilename));
+
+    ctx.run();
+
+    // NOLINTEND
+
+    return 0;
+}
diff --git a/bios-spi/meson.build b/bios-spi/meson.build
new file mode 100644
index 0000000..e2d6853
--- /dev/null
+++ b/bios-spi/meson.build
@@ -0,0 +1,27 @@
+
+bios_spi_src = files(
+    'spi_device.cpp',
+    'spi_device_code_updater.cpp'
+)
+
+bios_spi_include = include_directories('.')
+
+executable(
+    'phosphor-fw-update-bios',
+    '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
+)
diff --git a/bios-spi/spi_device.cpp b/bios-spi/spi_device.cpp
new file mode 100644
index 0000000..46fe1dc
--- /dev/null
+++ b/bios-spi/spi_device.cpp
@@ -0,0 +1,405 @@
+#include "spi_device.hpp"
+
+#include "common/include/device.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 <fstream>
+
+using namespace std::literals;
+
+SPIDevice::SPIDevice(
+    sdbusplus::async::context& ctx, const std::string& spiDevName, bool dryRun,
+    bool hasME, const std::vector<std::string>& gpioLines,
+    const std::vector<uint8_t>& gpioValues, DeviceConfig& config,
+    SoftwareManager* parent, bool layoutFlat, bool toolFlashrom, bool debug) :
+    Device(ctx, dryRun, config, parent), hasManagementEngine(hasME),
+    gpioLines(gpioLines), gpioValues(gpioValues), spiDev(spiDevName),
+    layoutFlat(layoutFlat), toolFlashrom(toolFlashrom), debug(debug)
+{
+    lg2::debug("initialized SPI Device instance on dbus");
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<std::string> SPIDevice::getInventoryItemObjectPath()
+// NOLINTEND
+{
+    // TODO: we currently do not know how to access the object path of the
+    // inventory item here
+    co_return "";
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::updateDevice(
+    const uint8_t* image, size_t image_size,
+    std::unique_ptr<SoftwareActivationProgress>& activationProgress)
+// NOLINTEND
+{
+    // NOLINTBEGIN
+    bool success =
+        co_await this->writeSPIFlash(image, image_size, activationProgress);
+    // NOLINTEND
+    co_return success;
+}
+
+constexpr const char* IPMB_SERVICE = "xyz.openbmc_project.Ipmi.Channel.Ipmb";
+constexpr const char* IPMB_PATH = "/xyz/openbmc_project/Ipmi/Channel/Ipmb";
+constexpr const char* IPMB_INTF = "org.openbmc.Ipmb";
+
+// NOLINTBEGIN
+sdbusplus::async::task<> SPIDevice::setManagementEngineRecoveryMode()
+// NOLINTEND
+{
+    lg2::info("[ME] setting Management Engine to recovery mode");
+    auto m = ctx.get_bus().new_method_call(IPMB_SERVICE, IPMB_PATH, IPMB_INTF,
+                                           "sendRequest");
+
+    // me address, 0x2e oen, 0x00 - lun, 0xdf - force recovery
+    uint8_t cmd_recover[] = {0x1, 0x2e, 0x0, 0xdf};
+    for (unsigned int i = 0; i < sizeof(cmd_recover); i++)
+    {
+        m.append(cmd_recover[i]);
+    }
+    std::vector<uint8_t> remainder = {0x04, 0x57, 0x01, 0x00, 0x01};
+    m.append(remainder);
+
+    m.call();
+
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(5));
+
+    co_return;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<> SPIDevice::resetManagementEngine()
+// NOLINTEND
+{
+    lg2::info("[ME] resetting Management Engine");
+    auto m = ctx.get_bus().new_method_call(IPMB_SERVICE, IPMB_PATH, IPMB_INTF,
+                                           "sendRequest");
+
+    // me address, 0x6 App Fn, 0x00 - lun, 0x2 - cold reset
+    uint8_t cmd_recover[] = {0x1, 0x6, 0x0, 0x2};
+    for (unsigned int i = 0; i < sizeof(cmd_recover); i++)
+    {
+        m.append(cmd_recover[i]);
+    }
+    std::vector<uint8_t> remainder;
+    m.append(remainder);
+
+    m.call();
+
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(5));
+
+    co_return;
+}
+
+const std::string spiAspeedSMCPath = "/sys/bus/platform/drivers/spi-aspeed-smc";
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::bindSPIFlash()
+// NOLINTEND
+{
+    lg2::info("[SPI] binding flash to SMC");
+    std::ofstream ofbind(spiAspeedSMCPath + "/bind", std::ofstream::out);
+    ofbind << this->spiDev;
+    ofbind.close();
+
+    // wait for kernel
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(2));
+
+    co_return isSPIFlashBound();
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::unbindSPIFlash()
+// NOLINTEND
+{
+    lg2::info("[SPI] unbinding flash from SMC");
+    std::ofstream ofunbind(spiAspeedSMCPath + "/unbind", std::ofstream::out);
+    ofunbind << this->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 + "/" + this->spiDev;
+    lg2::debug("[SPI] checking {PATH}", "PATH", path);
+
+    return std::filesystem::exists(path);
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlash(
+    const uint8_t* image, size_t image_size,
+    const std::unique_ptr<SoftwareActivationProgress>& activationProgress)
+// NOLINTEND
+{
+    auto currentPowerstateOpt = co_await parent->getHostPowerstate();
+
+    if (!currentPowerstateOpt.has_value())
+    {
+        co_return false;
+    }
+
+    const bool prevPowerstate = currentPowerstateOpt.value();
+
+    // NOLINTBEGIN
+    bool success = co_await parent->setHostPowerstate(false);
+    // NOLINTEND
+    if (!success)
+    {
+        lg2::error("error changing host power state");
+        co_return false;
+    }
+    activationProgress->progress(10);
+
+    if (hasManagementEngine)
+    {
+        co_await setManagementEngineRecoveryMode();
+    }
+    activationProgress->progress(20);
+
+    success = co_await writeSPIFlashHostOff(image, image_size);
+
+    if (success)
+    {
+        activationProgress->progress(70);
+    }
+
+    if (hasManagementEngine)
+    {
+        co_await resetManagementEngine();
+    }
+
+    if (success)
+    {
+        activationProgress->progress(90);
+    }
+
+    // restore the previous powerstate
+    const bool powerstate_restore =
+        co_await parent->setHostPowerstate(prevPowerstate);
+    if (!powerstate_restore)
+    {
+        lg2::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;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool>
+    SPIDevice::writeSPIFlashHostOff(const uint8_t* image, size_t image_size)
+// NOLINTEND
+{
+    gpiod::chip chip;
+    try
+    {
+        // TODO: make it work for multiple chips
+        chip = gpiod::chip("/dev/gpiochip0");
+    }
+    catch (std::exception& e)
+    {
+        lg2::error(e.what());
+        co_return false;
+    }
+
+    std::vector<unsigned int> offsets;
+
+    for (const std::string& lineName : gpioLines)
+    {
+        const ::gpiod::line line = ::gpiod::find_line(lineName);
+
+        if (line.is_used())
+        {
+            lg2::error("gpio line {LINE} was still used", "LINE", lineName);
+            co_return false;
+        }
+        offsets.push_back(line.offset());
+    }
+
+    auto lines = chip.get_lines(offsets);
+
+    ::gpiod::line_request config{"", ::gpiod::line_request::DIRECTION_OUTPUT,
+                                 0};
+    std::vector<int> values;
+    std::vector<int> valuesInverted;
+    values.reserve(gpioValues.size());
+
+    for (uint8_t value : gpioValues)
+    {
+        values.push_back(value);
+        valuesInverted.push_back(value ? 0 : 1);
+    }
+
+    lg2::debug("[gpio] requesting gpios to mux SPI to BMC");
+    lines.request(config, values);
+
+    co_await writeSPIFlashHostOffGPIOSet(image, image_size);
+
+    lines.release();
+
+    // switch bios flash back to host via mux / GPIO
+    // (not assume there is a pull to the default value)
+    lg2::debug("[gpio] requesting gpios to mux SPI to Host");
+    lines.request(config, valuesInverted);
+
+    lines.release();
+
+    co_return true;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlashHostOffGPIOSet(
+    const uint8_t* image, size_t image_size)
+// NOLINTEND
+{
+    bool success = true;
+
+    if (SPIDevice::isSPIFlashBound())
+    {
+        lg2::debug("[SPI] flash was already bound, unbinding it now");
+        success = co_await SPIDevice::unbindSPIFlash();
+
+        if (!success)
+        {
+            lg2::error("[SPI] error unbinding spi flash");
+            co_return false;
+        }
+    }
+
+    success = co_await SPIDevice::bindSPIFlash();
+
+    if (!success)
+    {
+        lg2::error("[SPI] failed to bind spi device");
+        co_await SPIDevice::unbindSPIFlash();
+        co_return false;
+    }
+
+    if (dryRun)
+    {
+        lg2::info("[SPI] dry run, NOT writing to the chip");
+    }
+    else
+    {
+        if (this->toolFlashrom)
+        {
+            co_await SPIDevice::writeSPIFlashFlashromHostOffGPIOSetDeviceBound(
+                image, image_size);
+        }
+        else
+        {
+            co_await SPIDevice::writeSPIFlashHostOffGPIOSetDeviceBound(
+                image, image_size);
+        }
+    }
+
+    success = co_await SPIDevice::unbindSPIFlash();
+
+    co_return success;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool>
+    SPIDevice::writeSPIFlashFlashromHostOffGPIOSetDeviceBound(
+        const uint8_t* image, size_t image_size)
+// NOLINTEND
+{
+    // TODO: randomize the name to enable parallel updates
+    std::string path = "/tmp/spi-device-image.bin";
+    int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
+    if (fd < 0)
+    {
+        lg2::error("[SPI] Failed to open file: {PATH}", "PATH", path);
+        co_return false;
+    }
+
+    const ssize_t bytesWritten = write(fd, image, image_size);
+
+    close(fd);
+
+    if (bytesWritten < 0 || static_cast<size_t>(bytesWritten) != image_size)
+    {
+        lg2::error("[SPI] Failed to write image to file");
+        co_return false;
+    }
+
+    // TODO: do not hardcode the mtd device
+    std::string cmd = "flashrom -p linux_mtd:dev=6 ";
+
+    if (this->layoutFlat)
+    {
+        cmd += "-w " + path;
+    }
+    else
+    {
+        cmd += "--ifd -i fd -i bios -i me -w " + path;
+    }
+
+    lg2::info("[flashrom] running {CMD}", "CMD", cmd);
+
+    const int exitCode = std::system(cmd.c_str());
+
+    if (exitCode != 0)
+    {
+        lg2::error("[SPI] error running flaashrom");
+    }
+
+    // in debug mode we do not delete the raw component image
+    if (!this->debug)
+    {
+        std::filesystem::remove(path);
+    }
+
+    co_return exitCode;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlashHostOffGPIOSetDeviceBound(
+    const uint8_t* image, size_t image_size)
+// NOLINTEND
+{
+    // TODO: not hardcode the mtd device
+    std::string devPath = "/dev/mtd6";
+    int fd = open(devPath.c_str(), O_WRONLY);
+    if (fd < 0)
+    {
+        lg2::error("[SPI] Failed to open device: {PATH}", "PATH", devPath);
+        co_return false;
+    }
+
+    ssize_t bytesWritten = write(fd, image, image_size);
+
+    close(fd);
+
+    if (bytesWritten < 0)
+    {
+        lg2::error("[SPI] Failed to write to device");
+        co_return false;
+    }
+
+    if (static_cast<size_t>(bytesWritten) != image_size)
+    {
+        lg2::error("[SPI] Incomplete write to device");
+        co_return false;
+    }
+
+    lg2::info("[SPI] Successfully wrote {NBYTES} bytes to {PATH}", "NBYTES",
+              bytesWritten, "PATH", devPath);
+
+    co_return true;
+}
diff --git a/bios-spi/spi_device.hpp b/bios-spi/spi_device.hpp
new file mode 100644
index 0000000..f28c4ac
--- /dev/null
+++ b/bios-spi/spi_device.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "common/include/device.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 Software;
+
+class SPIDevice : public Device
+{
+  public:
+    SPIDevice(sdbusplus::async::context& ctx, const std::string& spiDevName,
+              bool dryRun, bool hasME,
+              const std::vector<std::string>& gpioLines,
+              const std::vector<uint8_t>& gpioValues, DeviceConfig& config,
+              SoftwareManager* parent, bool layoutFlat, bool toolFlashrom,
+              bool debug);
+
+    sdbusplus::async::task<bool> updateDevice(
+        const uint8_t* image, size_t image_size,
+        std::unique_ptr<SoftwareActivationProgress>& activationProgress) final;
+
+    sdbusplus::async::task<std::string> getInventoryItemObjectPath() final;
+
+  private:
+    // Management Engine specific members and functions
+    bool hasManagementEngine;
+    sdbusplus::async::task<> setManagementEngineRecoveryMode();
+    sdbusplus::async::task<> resetManagementEngine();
+
+    std::vector<std::string> gpioLines;
+    std::vector<uint8_t> gpioValues;
+
+    // SPI specific members and functions
+    std::string spiDev;
+
+    // does the spi flash have a flat layout?
+    // Otherwise, we have to use Intel Flash Descriptor
+    // or another descriptor to figure out which regions should be written
+    bool layoutFlat;
+
+    // do we use flashrom?
+    // if not, write directly to the mtd device.
+    bool toolFlashrom;
+
+    // @param spi_dev    e.g. "1e630000.spi"
+    // @returns          true on success
+    sdbusplus::async::task<bool> bindSPIFlash();
+
+    // @param spi_dev    e.g. "1e630000.spi"
+    // @returns          true on success
+    sdbusplus::async::task<bool> unbindSPIFlash();
+
+    // @param spi_dev    e.g. "1e630000.spi"
+    bool isSPIFlashBound();
+
+    bool debug;
+
+    sdbusplus::async::task<bool> writeSPIFlash(
+        const uint8_t* image, size_t image_size,
+        const std::unique_ptr<SoftwareActivationProgress>& activationProgress);
+
+    // this function assumes:
+    // - host is powered off
+    sdbusplus::async::task<bool>
+        writeSPIFlashHostOff(const uint8_t* image, size_t image_size);
+
+    // this function assumes:
+    // - host is powered off
+    // - gpio / mux is set
+    sdbusplus::async::task<bool>
+        writeSPIFlashHostOffGPIOSet(const uint8_t* image, size_t image_size);
+
+    // this function assumes:
+    // - 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> writeSPIFlashHostOffGPIOSetDeviceBound(
+        const uint8_t* image, size_t image_size);
+
+    // this function assumes:
+    // - 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
+    // TODO: look into using libflashrom instead
+    // @param image           the component image
+    // @param image_size      size of 'image'
+    // @returns               true on success
+    sdbusplus::async::task<bool> writeSPIFlashFlashromHostOffGPIOSetDeviceBound(
+        const uint8_t* image, size_t image_size);
+};
diff --git a/bios-spi/spi_device_code_updater.cpp b/bios-spi/spi_device_code_updater.cpp
new file mode 100644
index 0000000..888d5c8
--- /dev/null
+++ b/bios-spi/spi_device_code_updater.cpp
@@ -0,0 +1,157 @@
+#include "spi_device_code_updater.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>
+
+SPIDeviceCodeUpdater::SPIDeviceCodeUpdater(sdbusplus::async::context& ctx,
+                                           bool isDryRun, bool debug) :
+    SoftwareManager(ctx, configTypeSPIDevice, isDryRun), debug(debug)
+{}
+
+// NOLINTBEGIN
+sdbusplus::async::task<>
+    SPIDeviceCodeUpdater::getInitialConfigurationSingleDevice(
+        const std::string& service, const std::string& path,
+        DeviceConfig& config)
+// NOLINTEND
+{
+    std::optional<std::string> optSpiPath =
+        co_await SoftwareManager::dbusGetRequiredConfigurationProperty<
+            std::string>(service, path, "Path", config);
+
+    std::optional<bool> optHasME =
+        co_await SoftwareManager::dbusGetRequiredConfigurationProperty<bool>(
+            service, path, "HasME", config);
+
+    std::optional<std::string> optLayout =
+        co_await SoftwareManager::dbusGetRequiredConfigurationProperty<
+            std::string>(service, path, "Layout", config);
+
+    std::optional<std::string> optTool =
+        co_await SoftwareManager::dbusGetRequiredConfigurationProperty<
+            std::string>(service, path, "Tool", config);
+
+    if (!optSpiPath.has_value() || !optHasME.has_value())
+    {
+        co_return;
+    }
+
+    std::string spiPath = optSpiPath.value();
+    bool hasME = optHasME.value();
+
+    lg2::debug("[config] spi device: {SPIDEV}", "SPIDEV", spiPath);
+
+    std::vector<std::string> gpioLines;
+    std::vector<uint8_t> gpioValues;
+
+    std::string configIntfMuxGpios;
+
+    std::vector<std::string> configIntfs = {
+        "xyz.openbmc_project.Configuration." + configTypeSPIDevice};
+
+    for (auto& iface : configIntfs)
+    {
+        configIntfMuxGpios = iface + ".MuxGpios";
+    }
+
+    for (size_t i = 0; true; i++)
+    {
+        std::string intf = configIntfMuxGpios + std::to_string(i);
+        std::optional<std::string> optGpioName =
+            co_await SoftwareManager::dbusGetRequiredProperty<std::string>(
+                service, path, intf, "Name");
+        std::optional<std::string> optGpioPolarity =
+            co_await SoftwareManager::dbusGetRequiredProperty<std::string>(
+                service, path, intf, "Polarity");
+
+        if (!optGpioName.has_value() || !optGpioPolarity.has_value())
+        {
+            break;
+        }
+
+        gpioLines.push_back(optGpioName.value());
+        gpioValues.push_back((optGpioPolarity.value() == "High") ? 1 : 0);
+
+        lg2::debug("[config] gpio {NAME} = {VALUE}", "NAME",
+                   optGpioName.value(), "VALUE", optGpioPolarity.value());
+    }
+
+    lg2::debug("[config] extracted {N} gpios from EM config", "N",
+               gpioLines.size());
+
+    bool layoutFlat;
+
+    if (!optLayout.has_value())
+    {
+        lg2::info("[config] error: no flash layout chosen (property 'Layout')");
+        co_return;
+    }
+
+    const std::string& layout = optLayout.value();
+    if (layout == "Flat")
+    {
+        layoutFlat = true;
+    }
+    else if (layout == "IFD")
+    {
+        layoutFlat = false;
+    }
+    else
+    {
+        lg2::error("[config] unsupported flash layout config: {OPTION}",
+                   "OPTION", layout);
+        lg2::info("supported options: 'Flat', 'IFD'");
+        co_return;
+    }
+
+    bool toolFlashrom;
+    if (!optTool.has_value())
+    {
+        lg2::error("[config] error: no tool chose (property 'Tool')");
+        co_return;
+    }
+
+    const std::string& tool = optTool.value();
+
+    if (tool == "flashrom")
+    {
+        toolFlashrom = true;
+    }
+    else if (tool == "None")
+    {
+        toolFlashrom = false;
+    }
+    else
+    {
+        lg2::error("[config] unsupported Tool: {OPTION}", "OPTION", tool);
+        co_return;
+    }
+
+    auto spiDevice = std::make_unique<SPIDevice>(
+        ctx, spiPath, dryRun, hasME, gpioLines, gpioValues, config, this,
+        layoutFlat, toolFlashrom, this->debug);
+
+    // we do not know the version on startup, it becomes known on update
+    std::string version = "unknown";
+
+    std::unique_ptr<Software> bsws =
+        std::make_unique<Software>(ctx, "spi_swid_unknown", *spiDevice);
+
+    bsws->setVersion("unknown");
+
+    // enable this software to be updated
+    std::set<RequestedApplyTimes> allowedApplyTimes = {
+        RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset};
+
+    bsws->enableUpdate(allowedApplyTimes);
+
+    spiDevice->softwareCurrent = std::move(bsws);
+
+    devices.insert(std::move(spiDevice));
+}
diff --git a/bios-spi/spi_device_code_updater.hpp b/bios-spi/spi_device_code_updater.hpp
new file mode 100644
index 0000000..fbe39f9
--- /dev/null
+++ b/bios-spi/spi_device_code_updater.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "common/include/software_manager.hpp"
+#include "sdbusplus/async/context.hpp"
+
+#include <sdbusplus/async.hpp>
+
+const std::string configTypeSPIDevice = "SPIFlash";
+
+class SPIDeviceCodeUpdater : public SoftwareManager
+{
+  public:
+    SPIDeviceCodeUpdater(sdbusplus::async::context& ctx, bool isDryRun,
+                         bool debug);
+
+    sdbusplus::async::task<> getInitialConfigurationSingleDevice(
+        const std::string& service, const std::string& path,
+        DeviceConfig& config) final;
+
+    bool debug;
+
+  private:
+};
diff --git a/meson.build b/meson.build
index 8db7902..370b489 100644
--- a/meson.build
+++ b/meson.build
@@ -66,7 +66,7 @@
 
 common_include = include_directories('.')
 
-common_build = build_tests.allowed()
+common_build = build_tests.allowed() or get_option('code-updater-spi-device').enabled()
 
 if common_build
   libpldm_dep = dependency('libpldm')
@@ -80,7 +80,10 @@
   subdir('common')
 endif
 
+if get_option('code-updater-spi-device').enabled()
+  subdir('bios-spi')
+endif
+
 if build_tests.allowed()
   subdir('test')
 endif
-
diff --git a/meson.options b/meson.options
index 605786d..b477342 100644
--- a/meson.options
+++ b/meson.options
@@ -133,3 +133,8 @@
     value: '/run/media/rwfs-alt/cow',
     description: 'The dir for alt-rwfs partition.',
 )
+
+option(
+    'code-updater-spi-device', type: 'feature', value: 'enabled',
+    description: 'enable update of spi device / host fw',
+)
diff --git a/test/bios-spi/meson.build b/test/bios-spi/meson.build
new file mode 100644
index 0000000..149f4a0
--- /dev/null
+++ b/test/bios-spi/meson.build
@@ -0,0 +1,23 @@
+e = executable(
+    'test_bios_spi',
+    'test_bios_spi.cpp',
+    bios_spi_src,
+    include_directories: [
+      common_include,
+      bios_spi_include,
+    ],
+    dependencies: [
+      libgpiod,
+      libpldm_dep,
+      sdbusplus_dep,
+      pdi_dep,
+      phosphor_logging_dep,
+    ],
+    link_with: [libpldmutil, software_common_lib],
+)
+
+test(
+        'test_bios_spi',
+        e,
+        workdir: meson.current_source_dir(),
+)
diff --git a/test/bios-spi/test_bios_spi.cpp b/test/bios-spi/test_bios_spi.cpp
new file mode 100644
index 0000000..aea6ebc
--- /dev/null
+++ b/test/bios-spi/test_bios_spi.cpp
@@ -0,0 +1,43 @@
+#include "bios-spi/spi_device.hpp"
+#include "spi_device_code_updater.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>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+#include <iostream>
+#include <memory>
+
+int main()
+{
+    sdbusplus::async::context ctx;
+
+    try
+    {
+        SPIDeviceCodeUpdater spidcu(ctx, true, false);
+        uint32_t vendorIANA = 0x0000a015;
+        std::string compatible = "com.testvendor.testcomponent";
+        SPIDeviceCodeUpdater* cu = &spidcu;
+        std::vector<std::string> gpioNames;
+        std::vector<uint8_t> gpioValues;
+
+        DeviceConfig config(vendorIANA, compatible, "SPIFlash", "HostSPI");
+
+        auto sd = std::make_unique<SPIDevice>(
+            ctx, "1e630000.spi", true, true, gpioNames, gpioValues, config, cu,
+            true, true, false);
+
+        spidcu.devices.insert(std::move(sd));
+    }
+    catch (std::exception& e)
+    {
+        std::cerr << e.what() << std::endl;
+        return 1;
+    }
+
+    return 0;
+}
diff --git a/test/meson.build b/test/meson.build
index 6c436e2..bf0ca44 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -7,3 +7,4 @@
 )
 
 subdir('common')
+subdir('bios-spi')