i2c-vr: add support for MPS MP5998 VR firmware update over I2C
This commit introduces support for programming MPS MP5998 VR devices
over the I2C bus.
Support for the MP5998Firmware type in entity-manager was added in [1].
[1] https://gerrit.openbmc.org/c/openbmc/entity-manager/+/83114
Tested on the Santabarbara platform:
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/Santabarbara_PDB2_VR_MP5998_8692"
},
{...}
],
"Members@odata.count": 13,
"Name": "Software Inventory Collection"
}
```
2. Query version.
```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_PDB2_VR_MP5998_8692
```
```
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_PDB2_VR_MP5998_8692",
"@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
"Description": "Unknown image",
"Id": "Santabarbara_PDB2_VR_MP5998_8692",
"Name": "Software Inventory",
"Status": {
"Health": "OK",
"HealthRollup": "OK",
"State": "Enabled"
},
"Updateable": true,
"Version": "508F"
}
```
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/Santabarbara_PDB2_VR_MP5998_8692"],"@Redfish.OperationApplyTime":"OnReset"};type=application/json" \
-F "UpdateFile=@${fwpath};type=application/octet-stream" \
https://${bmc}/redfish/v1/UpdateService/update-multipart
```
4. Task is returned
```
{
"@odata.id": "/redfish/v1/TaskService/Tasks/0",
"@odata.type": "#Task.v1_4_3.Task",
"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."
}
],
"Name": "Task 0",
"Payload": {
"HttpHeaders": [],
"HttpOperation": "POST",
"TargetUri": "/redfish/v1/UpdateService/update-multipart"
},
"PercentComplete": 0,
"StartTime": "2025-08-22T08:41:47+00:00",
"TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/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-08-22T08:41:48+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 80 percent complete.",
"MessageArgs": [
"0",
"80"
],
"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-multipart"
},
"PercentComplete": 100,
"StartTime": "2025-08-22T08:41: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/Santabarbara_PDB2_VR_MP5998_5612"
},
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_PDB2_VR_MP5998_8692"
},
{...}
],
"Members@odata.count": 16,
"Name": "Software Inventory Collection"
}
```
7. Query the new fw version.
```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_PDB2_VR_MP5998_5612
```
```
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_PDB2_VR_MP5998_5612",
"@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
"Description": "Unknown image",
"Id": "Santabarbara_PDB2_VR_MP5998_5612",
"Name": "Software Inventory",
"Status": {
"Health": "OK",
"HealthRollup": "OK",
"State": "Enabled"
},
"Updateable": false,
"Version": "FCFF"
}
```
8. Do AC cycle to make sure the new fw is applied.
```
busctl set-property xyz.openbmc_project.State.Chassis0 /xyz/openbmc_project/state/chassis0 \
xyz.openbmc_project.State.Chassis RequestedPowerTransition s xyz.openbmc_project.State.Chassis.Transition.PowerCycle
```
9. Display the fw inventory after AC cycle.
```
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/Santabarbara_PDB2_VR_MP5998_302"
},
{...}
],
"Members@odata.count": 12,
"Name": "Software Inventory Collection"
}
```
10. Query the fw version after AC cycle.
```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_PDB2_VR_MP5998_302
```
```
{
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_PDB2_VR_MP5998_302",
"@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
"Description": "Unknown image",
"Id": "Santabarbara_PDB2_VR_MP5998_302",
"Name": "Software Inventory",
"Status": {
"Health": "OK",
"HealthRollup": "OK",
"State": "Enabled"
},
"Updateable": true,
"Version": "FCFF"
}
```
Change-Id: Ia973a142ac04e041989a74a87e7f1fd8bd16ae3d
Signed-off-by: Freddie Jheng <Freddie.Jheng@quantatw.com>
diff --git a/i2c-vr/mps/mp5998.cpp b/i2c-vr/mps/mp5998.cpp
new file mode 100644
index 0000000..c30540e
--- /dev/null
+++ b/i2c-vr/mps/mp5998.cpp
@@ -0,0 +1,453 @@
+#include "mp5998.hpp"
+
+#include "common/include/utils.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <fstream>
+
+PHOSPHOR_LOG2_USING;
+
+namespace phosphor::software::VR
+{
+
+static constexpr std::string_view crcUserRegName = "CRC_USER";
+static constexpr uint8_t eepromFaultBit = 0x01;
+static constexpr uint8_t unlockData = 0x00;
+static constexpr size_t statusByteLength = 1;
+
+enum class MP5998Cmd : uint8_t
+{
+ crcUser = 0xF8,
+ passwordReg = 0x0E,
+};
+
+sdbusplus::async::task<bool> MP5998::parseDeviceConfiguration()
+{
+ if (!configuration)
+ {
+ error("Device configuration not initialized");
+ co_return false;
+ }
+
+ configuration->vendorId = 0x4D5053;
+ configuration->productId = 0x35393938;
+
+ for (const auto& tokens : parser->lineTokens)
+ {
+ if (!parser->isValidDataTokens(tokens))
+ {
+ continue;
+ }
+
+ auto regName = parser->getVal<std::string>(tokens, ATE::regName);
+
+ if (regName == crcUserRegName)
+ {
+ configuration->configId =
+ parser->getVal<uint32_t>(tokens, ATE::configId);
+ configuration->crcUser =
+ parser->getVal<uint32_t>(tokens, ATE::regDataHex);
+ }
+ }
+
+ co_return true;
+}
+
+sdbusplus::async::task<bool> MP5998::verifyImage(const uint8_t* image,
+ size_t imageSize)
+{
+ if (!co_await parseImage(image, imageSize))
+ {
+ error("Image verification failed: image parsing failed");
+ co_return false;
+ }
+
+ if (configuration->registersData.empty())
+ {
+ error("Image verification failed - no register data found");
+ co_return false;
+ }
+
+ if (configuration->configId == 0)
+ {
+ error("Image verification failed - missing config ID");
+ co_return false;
+ }
+
+ co_return true;
+}
+
+sdbusplus::async::task<bool> MP5998::checkId(PMBusCmd idCmd, uint32_t expected)
+{
+ static constexpr size_t mfrIdLength = 3;
+ static constexpr size_t mfrModelLength = 5;
+
+ std::vector<uint8_t> tbuf;
+ std::vector<uint8_t> rbuf;
+
+ tbuf = buildByteVector(PMBusCmd::page, MPSPage::page0);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("MP5998: Failed to set page 0 for ID check");
+ co_return false;
+ }
+
+ size_t bufferSize;
+
+ if (idCmd == PMBusCmd::mfrId)
+ {
+ bufferSize = statusByteLength + mfrIdLength;
+ }
+ else if (idCmd == PMBusCmd::mfrModel)
+ {
+ bufferSize = statusByteLength + mfrModelLength;
+ }
+ else
+ {
+ error("MP5998: Unsupported ID command: 0x{CMD}", "CMD", lg2::hex,
+ static_cast<uint8_t>(idCmd));
+ co_return false;
+ }
+
+ tbuf = buildByteVector(idCmd);
+ rbuf.resize(bufferSize);
+
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("MP5998: I2C sendReceive failed for command 0x{CMD}", "CMD",
+ lg2::hex, static_cast<uint8_t>(idCmd));
+ co_return false;
+ }
+
+ auto idBytes = std::span(rbuf).subspan(statusByteLength);
+ uint32_t id;
+ if (idCmd == PMBusCmd::mfrModel)
+ {
+ auto productBytes = idBytes.subspan(1, 4);
+ id = bytesToInt<uint32_t>(productBytes);
+ }
+ else
+ {
+ id = bytesToInt<uint32_t>(idBytes);
+ }
+
+ debug("Check ID cmd {CMD}: Got={ID}, Expected={EXP}", "CMD", lg2::hex,
+ static_cast<uint8_t>(idCmd), "ID", lg2::hex, id, "EXP", lg2::hex,
+ expected);
+
+ co_return id == expected;
+}
+
+sdbusplus::async::task<bool> MP5998::unlockPasswordProtection()
+{
+ constexpr uint8_t passwordUnlockBit = 0x08;
+ constexpr uint16_t passwordData = 0x0000;
+
+ std::vector<uint8_t> tbuf;
+ std::vector<uint8_t> rbuf;
+
+ tbuf = buildByteVector(PMBusCmd::page, MPSPage::page0);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to set page 0 for password unlock");
+ co_return false;
+ }
+
+ tbuf = buildByteVector(MP5998Cmd::passwordReg, passwordData);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to write password");
+ co_return false;
+ }
+
+ tbuf = buildByteVector(PMBusCmd::statusCML);
+ rbuf.resize(statusByteLength);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to read STATUS_CML");
+ co_return false;
+ }
+
+ bool unlocked = (rbuf[0] & passwordUnlockBit) == 0;
+
+ co_return unlocked;
+}
+
+sdbusplus::async::task<bool> MP5998::unlockWriteProtection()
+{
+ std::vector<uint8_t> tbuf;
+ std::vector<uint8_t> rbuf;
+
+ tbuf = buildByteVector(PMBusCmd::page, MPSPage::page0);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to set page 0 for write protection unlock");
+ co_return false;
+ }
+
+ tbuf = buildByteVector(PMBusCmd::writeProtect, unlockData);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to unlock write protection");
+ co_return false;
+ }
+
+ debug("Write protection unlocked");
+ co_return true;
+}
+
+sdbusplus::async::task<bool> MP5998::programAllRegisters()
+{
+ uint8_t currentPage = 0xFF;
+
+ for (const auto& regData : configuration->registersData)
+ {
+ if (regData.page != currentPage)
+ {
+ std::vector<uint8_t> tbuf =
+ buildByteVector(PMBusCmd::page, regData.page);
+ std::vector<uint8_t> rbuf;
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to set page {PAGE}", "PAGE", regData.page);
+ co_return false;
+ }
+ currentPage = regData.page;
+ }
+
+ std::vector<uint8_t> tbuf;
+ std::vector<uint8_t> rbuf;
+
+ tbuf.push_back(regData.addr);
+
+ for (uint8_t i = 0; i < regData.length && i < 4; ++i)
+ {
+ tbuf.push_back(regData.data[i]);
+ }
+
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to write register 0x{REG} on page {PAGE}", "REG",
+ lg2::hex, regData.addr, "PAGE", regData.page);
+ co_return false;
+ }
+ }
+
+ debug("All registers programmed successfully");
+ co_return true;
+}
+
+sdbusplus::async::task<bool> MP5998::storeMTP()
+{
+ std::vector<uint8_t> tbuf;
+ std::vector<uint8_t> rbuf;
+
+ tbuf = buildByteVector(PMBusCmd::page, MPSPage::page0);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to set page 0 for MTP store");
+ co_return false;
+ }
+
+ tbuf = buildByteVector(PMBusCmd::storeUserCode);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to send STORE_USER_ALL command");
+ co_return false;
+ }
+
+ co_return true;
+}
+
+sdbusplus::async::task<bool> MP5998::waitForMTPComplete()
+{
+ constexpr uint16_t mtpStoreWaitmS = 1200;
+ co_await sdbusplus::async::sleep_for(
+ ctx, std::chrono::milliseconds(mtpStoreWaitmS));
+ std::vector<uint8_t> tbuf = buildByteVector(PMBusCmd::statusCML);
+ std::vector<uint8_t> rbuf;
+ rbuf.resize(statusByteLength);
+
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to read STATUS_CML after MTP store");
+ co_return false;
+ }
+
+ bool eepromFault = rbuf[0] & eepromFaultBit;
+
+ if (eepromFault)
+ {
+ error("EEPROM fault detected after MTP store");
+ co_return false;
+ }
+
+ co_return true;
+}
+
+sdbusplus::async::task<bool> MP5998::verifyCRC()
+{
+ uint32_t deviceCRC{0};
+ // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+ bool getCRCSuccess = co_await getCRC(&deviceCRC);
+ // NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+ if (!getCRCSuccess)
+ {
+ error("Failed to read CRC from device");
+ co_return false;
+ }
+
+ bool crcMatch = (deviceCRC == configuration->crcUser);
+
+ co_return crcMatch;
+}
+
+sdbusplus::async::task<bool> MP5998::getCRC(uint32_t* checksum)
+{
+ constexpr size_t crcLength = 2;
+
+ std::vector<uint8_t> tbuf;
+ std::vector<uint8_t> rbuf;
+
+ tbuf = buildByteVector(PMBusCmd::page, MPSPage::page0);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to set page 0 for CRC read");
+ co_return false;
+ }
+
+ tbuf = buildByteVector(MP5998Cmd::crcUser);
+ rbuf.resize(crcLength);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to read CRC_USER register");
+ co_return false;
+ }
+
+ *checksum = bytesToInt<uint32_t>(rbuf);
+
+ co_return true;
+}
+
+sdbusplus::async::task<bool> MP5998::sendRestoreMTPCommand()
+{
+ std::vector<uint8_t> tbuf;
+ std::vector<uint8_t> rbuf;
+
+ tbuf = buildByteVector(PMBusCmd::page, MPSPage::page0);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to set page 0 for MTP restore");
+ co_return false;
+ }
+
+ tbuf = buildByteVector(PMBusCmd::restoreUserAll);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to send RESTORE_ALL command");
+ co_return false;
+ }
+
+ co_return true;
+}
+
+sdbusplus::async::task<bool> MP5998::checkEEPROMFaultAfterRestore()
+{
+ std::vector<uint8_t> tbuf;
+ std::vector<uint8_t> rbuf;
+
+ tbuf = buildByteVector(PMBusCmd::page, MPSPage::page0);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to set page 0 for EEPROM fault check");
+ co_return false;
+ }
+
+ tbuf = buildByteVector(PMBusCmd::statusCML);
+ rbuf.resize(1);
+ if (!i2cInterface.sendReceive(tbuf, rbuf))
+ {
+ error("Failed to read STATUS_CML register");
+ co_return false;
+ }
+
+ bool eepromFault = (rbuf[0] & eepromFaultBit) != 0;
+
+ co_return !eepromFault;
+}
+
+sdbusplus::async::task<bool> MP5998::restoreMTPAndVerify()
+{
+ constexpr uint16_t mtpRestoreWait = 1600;
+
+ if (!co_await sendRestoreMTPCommand())
+ {
+ error("Failed to send RESTORE_ALL command");
+ co_return false;
+ }
+
+ co_await sdbusplus::async::sleep_for(
+ ctx, std::chrono::microseconds(mtpRestoreWait));
+ if (!co_await checkEEPROMFaultAfterRestore())
+ {
+ error("EEPROM fault detected after MTP restore");
+ co_return false;
+ }
+
+ co_return true;
+}
+
+bool MP5998::forcedUpdateAllowed()
+{
+ return true;
+}
+
+sdbusplus::async::task<bool> MP5998::updateFirmware(bool force)
+{
+ (void)force;
+
+ if (!co_await checkId(PMBusCmd::mfrId, configuration->vendorId))
+ {
+ co_return false;
+ }
+
+ if (!co_await checkId(PMBusCmd::mfrModel, configuration->productId))
+ {
+ co_return false;
+ }
+
+ if (!co_await unlockWriteProtection())
+ {
+ co_return false;
+ }
+
+ if (!co_await programAllRegisters())
+ {
+ co_return false;
+ }
+
+ if (!co_await storeMTP())
+ {
+ co_return false;
+ }
+
+ if (!co_await waitForMTPComplete())
+ {
+ co_return false;
+ }
+
+ if (!co_await verifyCRC())
+ {
+ co_return false;
+ }
+
+ if (!co_await restoreMTPAndVerify())
+ {
+ co_return false;
+ }
+
+ co_return true;
+}
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/mps/mp5998.hpp b/i2c-vr/mps/mp5998.hpp
new file mode 100644
index 0000000..c30ca47
--- /dev/null
+++ b/i2c-vr/mps/mp5998.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "common/include/pmbus.hpp"
+#include "mps.hpp"
+
+namespace phosphor::software::VR
+{
+
+class MP5998 : public MPSVoltageRegulator
+{
+ public:
+ MP5998(sdbusplus::async::context& ctx, uint16_t bus, uint16_t address) :
+ MPSVoltageRegulator(ctx, bus, address)
+ {}
+
+ sdbusplus::async::task<bool> verifyImage(const uint8_t* image,
+ size_t imageSize) final;
+ sdbusplus::async::task<bool> updateFirmware(bool force) final;
+ sdbusplus::async::task<bool> getCRC(uint32_t* checksum) final;
+ sdbusplus::async::task<bool> parseDeviceConfiguration() final;
+ bool forcedUpdateAllowed() final;
+
+ private:
+ sdbusplus::async::task<bool> checkId(PMBusCmd pmBusCmd, uint32_t expected);
+ sdbusplus::async::task<bool> unlockPasswordProtection();
+ sdbusplus::async::task<bool> unlockWriteProtection();
+ sdbusplus::async::task<bool> storeMTP();
+ sdbusplus::async::task<bool> waitForMTPComplete();
+ sdbusplus::async::task<bool> sendRestoreMTPCommand();
+ sdbusplus::async::task<bool> restoreMTPAndVerify();
+ sdbusplus::async::task<bool> checkEEPROMFaultAfterRestore();
+ sdbusplus::async::task<bool> verifyCRC();
+ sdbusplus::async::task<bool> programAllRegisters();
+};
+
+} // namespace phosphor::software::VR