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