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/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;
+}