i2c-vr: add support for MPS MP2X6XX VR firmware update over I2C

This commit introduces support for programming MPS MP2X6XX VR devices
over the I2C bus. It enables firmware updates for VR models such as
MP2869(A), MP29612(A), and MP29608(A).
Support for the MP2X6XXFirmware type in EM was added in [1].

[1] https://gerrit.openbmc.org/c/openbmc/entity-manager/+/82575

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_MB_VR_CPU1_4228"
    },
    {...}
  ],
  "Members@odata.count": 13,
  "Name": "Software Inventory Collection"
}
```

2. Query version.
```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_MB_VR_CPU1_4228
```

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

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_MB_VR_CPU1_4228\"],\"@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-07-29T08:51:41+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-07-29T08:51: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 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 50 percent complete.",
      "MessageArgs": [
        "0",
        "50"
      ],
      "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 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-07-29T08:51:41+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_MB_VR_CPU1_4228"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_MB_VR_CPU1_5453"
    },
    {...}
  ],
  "Members@odata.count": 14,
  "Name": "Software Inventory Collection"
}
```

7. Query the new fw version.
```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_MB_VR_CPU1_5453
```

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

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_MB_VR_CPU1_2249"
    },
    {...}
  ],
  "Members@odata.count": 13,
  "Name": "Software Inventory Collection"
}
```

10. Query the fw version after AC cycle.
```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_MB_VR_CPU1_2249
```

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

Change-Id: Ifa6fdd79f5d087adb6037e0043897fa6199ae097
Signed-off-by: Kevin Tung <Kevin.Tung@quantatw.com>
diff --git a/common/include/pmbus.hpp b/common/include/pmbus.hpp
new file mode 100644
index 0000000..d62db4d
--- /dev/null
+++ b/common/include/pmbus.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <cstdint>
+
+enum class PMBusCmd : uint8_t
+{
+    page = 0x00,
+    writeProtect = 0x10,
+    storeUserCode = 0x17,
+    mfrId = 0x99,
+    mfrSerial = 0x9E,
+    icDeviceId = 0xAD,
+};
diff --git a/common/include/utils.hpp b/common/include/utils.hpp
index 65d7022..9325678 100644
--- a/common/include/utils.hpp
+++ b/common/include/utils.hpp
@@ -14,3 +14,92 @@
 sdbusplus::async::task<bool> asyncSystem(
     sdbusplus::async::context& ctx, const std::string& cmd,
     std::optional<std::reference_wrapper<std::string>> result = std::nullopt);
+
+/**
+ * @brief Convert bytes to an integer of the given type.
+ *
+ * @tparam IntegerType Output integer type (e.g., uint16_t, uint32_t).
+ * @tparam Container A container of uint8_t bytes.
+ * @param data Byte data to convert.
+ * @param bigEndian Set true for big-endian order; false for little-endian.
+ * @return Converted integer.
+ */
+template <typename IntegerType, typename Container>
+IntegerType bytesToInt(const Container& data, bool bigEndian = false)
+{
+    static_assert(std::is_integral_v<IntegerType>,
+                  "IntegerType must be an integral type");
+    static_assert(std::is_same_v<typename Container::value_type, uint8_t>,
+                  "Container must hold uint8_t elements");
+
+    constexpr size_t maxBytes = sizeof(IntegerType);
+    size_t size = std::min(data.size(), maxBytes);
+
+    IntegerType result = 0;
+    for (size_t i = 0; i < size; ++i)
+    {
+        size_t shift = bigEndian ? (size - 1 - i) * 8 : i * 8;
+        result |= static_cast<IntegerType>(data[i]) << shift;
+    }
+
+    return result;
+}
+
+template <typename>
+inline constexpr bool always_false = false;
+
+/**
+ * @brief Constructs a vector of bytes (`std::vector<uint8_t>`) from a variable
+ *        number of arguments, which can include enums, integral values,
+ *        and initializer lists.
+ *
+ * This function is useful when building byte packets or command sequences
+ * to be sent over communication protocols (e.g., I2C, UART, SPI).
+ *
+ * @tparam Args Types of arguments to convert into bytes
+ * @param args The values to encode into the byte vector
+ * @return std::vector<uint8_t> A flattened list of bytes
+ *
+ * @note Passing unsupported types will trigger a compile-time static_assert.
+ * @note Endianness: Multi-byte integers use little-endian order.
+ *
+ * @code
+ * enum class Command : uint8_t { Start = 0x01 };
+ * auto buf = buildByteVector(Command::Start, 0x1234, {0xAA, 0xBB});
+ * // Result: { 0x01, 0x34, 0x12, 0xAA, 0xBB }
+ * @endcode
+ */
+template <typename... Args>
+std::vector<uint8_t> buildByteVector(Args&&... args)
+{
+    std::vector<uint8_t> buf;
+
+    auto append = [&](auto&& value) {
+        using T = std::decay_t<decltype(value)>;
+
+        if constexpr (std::is_enum_v<T>)
+        {
+            buf.push_back(static_cast<uint8_t>(value));
+        }
+        else if constexpr (std::is_integral_v<T>)
+        {
+            for (size_t i = 0; i < sizeof(T); ++i)
+            {
+                buf.push_back(static_cast<uint8_t>(value >> (i * 8)));
+            }
+        }
+        else if constexpr (std::is_same_v<T, std::initializer_list<uint8_t>>)
+        {
+            buf.insert(buf.end(), value.begin(), value.end());
+        }
+        else
+        {
+            static_assert(always_false<T>,
+                          "Unsupported type in buildByteVector");
+        }
+    };
+
+    (append(std::forward<Args>(args)), ...);
+
+    return buf;
+}
diff --git a/i2c-vr/i2cvr_software_manager.cpp b/i2c-vr/i2cvr_software_manager.cpp
index 691a20d..45955f0 100644
--- a/i2c-vr/i2cvr_software_manager.cpp
+++ b/i2c-vr/i2cvr_software_manager.cpp
@@ -20,8 +20,8 @@
 namespace ManagerInf = phosphor::software::manager;
 
 const std::string configDBusName = "I2CVR";
-const std::vector<std::string> emConfigTypes = {"XDPE1X2XXFirmware",
-                                                "ISL69269Firmware"};
+const std::vector<std::string> emConfigTypes = {
+    "XDPE1X2XXFirmware", "ISL69269Firmware", "MP2X6XXFirmware"};
 
 I2CVRSoftwareManager::I2CVRSoftwareManager(sdbusplus::async::context& ctx) :
     ManagerInf::SoftwareManager(ctx, configDBusName)
diff --git a/i2c-vr/meson.build b/i2c-vr/meson.build
index 0e292f5..483dfd7 100644
--- a/i2c-vr/meson.build
+++ b/i2c-vr/meson.build
@@ -1,6 +1,11 @@
 i2cvr_src = files('i2cvr_device.cpp', 'i2cvr_software_manager.cpp', 'vr.cpp')
 
-regulators_src = files('isl69269/isl69269.cpp', 'xdpe1x2xx/xdpe1x2xx.cpp')
+regulators_src = files(
+    'isl69269/isl69269.cpp',
+    'mps/mp2x6xx.cpp',
+    'mps/mps.cpp',
+    'xdpe1x2xx/xdpe1x2xx.cpp',
+)
 
 i2cvr_include = include_directories('.')
 
diff --git a/i2c-vr/mps/mp2x6xx.cpp b/i2c-vr/mps/mp2x6xx.cpp
new file mode 100644
index 0000000..a558f04
--- /dev/null
+++ b/i2c-vr/mps/mp2x6xx.cpp
@@ -0,0 +1,437 @@
+#include "mp2x6xx.hpp"
+
+#include "common/include/utils.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+namespace phosphor::software::VR
+{
+
+static constexpr size_t vendorIdLength = 3;
+static constexpr size_t deviceIdLength = 4;
+static constexpr size_t configIdLength = 2;
+static constexpr size_t statusByteLength = 1;
+static constexpr size_t crcLength = 2;
+
+static constexpr std::string_view productIdRegName = "TRIM_MFR_PRODUCT_ID2";
+static constexpr std::string_view crcUserRegName = "CRC_USER";
+
+static constexpr uint8_t pageMask = 0x0F;
+static constexpr uint8_t configMask = 0xF0;
+
+static constexpr uint8_t disableWriteProtect = 0x00;
+static constexpr uint16_t disablePage2WriteProtect = 0x128C;
+static constexpr uint16_t disablePage3WriteProtect = 0x0082;
+
+enum class MP2X6XXCmd : uint8_t
+{
+    // Page 0 commands
+    readCRCReg = 0xED,
+    // Page 1 commands
+    mfrMTPMemoryCtrl = 0xCC,
+    // Page 2 commands
+    selectConfigCtrl = 0x1A,
+    // Page 3 commands
+    mfrMTPMemoryCtrlPage3 = 0x81,
+};
+
+sdbusplus::async::task<bool> MP2X6XX::parseDeviceConfiguration()
+{
+    if (!configuration)
+    {
+        error("Device configuration not initialized");
+        co_return false;
+    }
+
+    configuration->vendorId = 0x4D5053;
+
+    for (const auto& tokens : parser->lineTokens)
+    {
+        if (!parser->isValidDataTokens(tokens))
+        {
+            continue;
+        }
+
+        auto regName = parser->getVal<std::string>(tokens, ATE::regName);
+        if (regName == productIdRegName)
+        {
+            configuration->productId =
+                parser->getVal<uint32_t>(tokens, ATE::regDataHex);
+            configuration->configId =
+                parser->getVal<uint32_t>(tokens, ATE::configId);
+        }
+        else if (regName == crcUserRegName)
+        {
+            configuration->crcUser =
+                parser->getVal<uint32_t>(tokens, ATE::regDataHex);
+            break;
+        }
+    }
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::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 data found");
+        co_return false;
+    }
+
+    if (configuration->productId == 0 || configuration->configId == 0)
+    {
+        error("Image verification failed - missing product or config ID");
+        co_return false;
+    }
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::checkId(PMBusCmd pmBusCmd,
+                                              uint32_t expected)
+{
+    const uint8_t cmd = static_cast<uint8_t>(pmBusCmd);
+    size_t idLen = 0;
+    bool blockRead = false;
+
+    switch (pmBusCmd)
+    {
+        case PMBusCmd::mfrId:
+            idLen = vendorIdLength;
+            blockRead = true;
+            break;
+        case PMBusCmd::icDeviceId:
+            idLen = deviceIdLength;
+            blockRead = true;
+            break;
+        case PMBusCmd::mfrSerial:
+            idLen = configIdLength;
+            break;
+        default:
+            error("Invalid command for ID check: {CMD}", "CMD", lg2::hex, cmd);
+            co_return false;
+    }
+
+    std::vector<uint8_t> rbuf;
+    std::vector<uint8_t> tbuf;
+
+    tbuf = buildByteVector(PMBusCmd::page, MPSPage::page0);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to set page 0 for ID check");
+        co_return false;
+    }
+
+    tbuf = {cmd};
+    rbuf.resize(idLen + (blockRead ? statusByteLength : 0));
+
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("I2C failure during ID check, cmd {CMD}", "CMD", lg2::hex, cmd);
+        co_return false;
+    }
+
+    auto idBytes = std::span(rbuf).subspan(blockRead ? statusByteLength : 0);
+    uint32_t id = bytesToInt<uint32_t>(idBytes);
+
+    if (id != expected)
+    {
+        error("ID check failed for cmd {CMD}: got {ID}, expected {EXP}", "CMD",
+              lg2::hex, cmd, "ID", lg2::hex, id, "EXP", lg2::hex, expected);
+        co_return false;
+    }
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::unlockWriteProtect()
+{
+    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 unlocking write protect");
+        co_return false;
+    }
+
+    tbuf = buildByteVector(PMBusCmd::writeProtect, disableWriteProtect);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to disable write protect");
+        co_return false;
+    }
+
+    // unlock page 2 write protect
+    tbuf = buildByteVector(PMBusCmd::page, MPSPage::page1);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to set page 1 for unlocking write protect for page 2");
+        co_return false;
+    }
+
+    tbuf =
+        buildByteVector(MP2X6XXCmd::mfrMTPMemoryCtrl, disablePage2WriteProtect);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to unlock page 2 write protect");
+        co_return false;
+    }
+
+    // unlock page 3 write protect
+    tbuf = buildByteVector(PMBusCmd::page, MPSPage::page3);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to set page 3 for unlocking write protect for page 3");
+        co_return false;
+    }
+
+    tbuf = buildByteVector(MP2X6XXCmd::mfrMTPMemoryCtrlPage3,
+                           disablePage3WriteProtect);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to unlock page 3 write protect");
+        co_return false;
+    }
+
+    debug("Unlocked write protect");
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::selectConfig(uint8_t config)
+{
+    // MPS config select command:
+    // Writes to Page 2 @ 0x1A: value = 0x0F00 | ((config + 7) << 4)
+    // For config 1–6 → result: 0x0F80 to 0x0FD0
+
+    std::vector<uint8_t> tbuf;
+    std::vector<uint8_t> rbuf;
+
+    tbuf = buildByteVector(PMBusCmd::page, MPSPage::page2);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to set page 2 for configuration switch");
+        co_return false;
+    }
+
+    constexpr uint8_t baseOffset = 7;
+    uint8_t encodedNibble = static_cast<uint8_t>((config + baseOffset) << 4);
+    uint16_t command = 0x0F00 | encodedNibble;
+
+    tbuf = buildByteVector(MP2X6XXCmd::selectConfigCtrl, command);
+
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to write config select command {CMD} for config {CONFIG}",
+              "CMD", lg2::hex, command, "CONFIG", config);
+        co_return false;
+    }
+
+    debug("Switched to config {CONFIG}", "CONFIG", config);
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::programConfigData(
+    const std::vector<MPSData>& gdata)
+{
+    std::vector<uint8_t> tbuf;
+    std::vector<uint8_t> rbuf;
+
+    for (const auto& data : gdata)
+    {
+        uint8_t page = data.page & pageMask;
+
+        tbuf = buildByteVector(PMBusCmd::page, page);
+        if (!i2cInterface.sendReceive(tbuf, rbuf))
+        {
+            error("Failed to set page {PAGE} for register {REG}", "PAGE", page,
+                  "REG", lg2::hex, data.addr);
+            co_return false;
+        }
+
+        tbuf = {data.addr};
+        tbuf.insert(tbuf.end(), data.data.begin(),
+                    data.data.begin() + data.length);
+
+        if (!i2cInterface.sendReceive(tbuf, rbuf))
+        {
+            error(
+                "Failed to write data {DATA} to register {REG} on page {PAGE}",
+                "DATA", lg2::hex, bytesToInt<uint32_t>(data.data), "REG",
+                lg2::hex, data.addr, "PAGE", page);
+            co_return false;
+        }
+    }
+
+    if (!co_await storeUserCode())
+    {
+        error("Failed to store user code after programming config data");
+        co_return false;
+    }
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::configAllRegisters()
+{
+    for (const auto& [config, gdata] : getGroupedConfigData(configMask, 4))
+    {
+        debug("Configuring registers for config {CONFIG}", "CONFIG", config);
+
+        // Select the appropriate config before programming its registers
+        if (config > 0 && !co_await selectConfig(config))
+        {
+            co_return false;
+        }
+
+        if (!co_await programConfigData(gdata))
+        {
+            error("Failed to program configuration {CONFIG}", "CONFIG", config);
+            co_return false;
+        }
+
+        debug("Configured {SIZE} registers for config {CONFIG}", "SIZE",
+              gdata.size(), "CONFIG", config);
+    }
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::storeUserCode()
+{
+    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 storing user code");
+        co_return false;
+    }
+
+    tbuf = buildByteVector(PMBusCmd::storeUserCode);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to store user code");
+        co_return false;
+    }
+
+    // Wait store user code
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::milliseconds(500));
+
+    debug("Stored user code");
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::getCRC(uint32_t* checksum)
+{
+    if (checksum == nullptr)
+    {
+        error("getCRC() called with null checksum pointer");
+        co_return false;
+    }
+
+    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(MP2X6XXCmd::readCRCReg);
+    rbuf.resize(crcLength);
+    if (!i2cInterface.sendReceive(tbuf, rbuf))
+    {
+        error("Failed to read CRC from device");
+        co_return false;
+    }
+
+    if (rbuf.size() < crcLength)
+    {
+        error("CRC read returned insufficient data");
+        co_return false;
+    }
+
+    *checksum = bytesToInt<uint32_t>(rbuf);
+
+    debug("Read CRC: {CRC}", "CRC", lg2::hex, *checksum);
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::checkMTPCRC()
+{
+    uint32_t crc = 0;
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    if (!co_await getCRC(&crc))
+    // NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    {
+        error("Failed to get CRC for MTP check");
+        co_return false;
+    }
+
+    debug("MTP CRC: {CRC}, Expected: {EXP}", "CRC", lg2::hex, crc, "EXP",
+          lg2::hex, configuration->crcUser);
+
+    co_return configuration->crcUser == crc;
+}
+
+bool MP2X6XX::forcedUpdateAllowed()
+{
+    return true;
+}
+
+sdbusplus::async::task<bool> MP2X6XX::updateFirmware(bool force)
+{
+    (void)force;
+
+    if (!co_await checkId(PMBusCmd::mfrId, configuration->vendorId))
+    {
+        co_return false;
+    }
+
+    if (!co_await checkId(PMBusCmd::icDeviceId, configuration->productId))
+    {
+        co_return false;
+    }
+
+    if (!co_await checkId(PMBusCmd::mfrSerial, configuration->configId))
+    {
+        co_return false;
+    }
+
+    if (!co_await unlockWriteProtect())
+    {
+        co_return false;
+    }
+
+    if (!co_await configAllRegisters())
+    {
+        co_return false;
+    }
+
+    if (!(co_await checkMTPCRC()))
+    {
+        co_return false;
+    }
+
+    co_return true;
+}
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/mps/mp2x6xx.hpp b/i2c-vr/mps/mp2x6xx.hpp
new file mode 100644
index 0000000..8c504a4
--- /dev/null
+++ b/i2c-vr/mps/mp2x6xx.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "common/include/pmbus.hpp"
+#include "mps.hpp"
+
+namespace phosphor::software::VR
+{
+
+class MP2X6XX : public MPSVoltageRegulator
+{
+  public:
+    MP2X6XX(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> unlockWriteProtect();
+    sdbusplus::async::task<bool> storeUserCode();
+    sdbusplus::async::task<bool> checkMTPCRC();
+    sdbusplus::async::task<bool> selectConfig(uint8_t config);
+    sdbusplus::async::task<bool> configAllRegisters();
+    sdbusplus::async::task<bool> programConfigData(
+        const std::vector<MPSData>& gdata);
+};
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/mps/mps.cpp b/i2c-vr/mps/mps.cpp
new file mode 100644
index 0000000..ee0abf9
--- /dev/null
+++ b/i2c-vr/mps/mps.cpp
@@ -0,0 +1,93 @@
+#include "mps.hpp"
+
+namespace phosphor::software::VR
+{
+
+bool MPSImageParser::isValidDataTokens(
+    const std::vector<std::string_view>& tokens)
+{
+    return tokens.size() > static_cast<size_t>(ATE::regName) &&
+           !tokens[0].starts_with('*');
+}
+
+MPSData MPSImageParser::extractData(const std::vector<std::string_view>& tokens)
+{
+    MPSData data;
+    data.page = getVal<uint8_t>(tokens, ATE::pageNum);
+    data.addr = getVal<uint8_t>(tokens, ATE::regAddrHex);
+
+    std::string regData = getVal<std::string>(tokens, ATE::regDataHex);
+    size_t byteCount = std::min(regData.length() / 2, size_t(4));
+    for (size_t i = 0; i < byteCount; ++i)
+    {
+        data.data[byteCount - 1 - i] = static_cast<uint8_t>(
+            std::stoul(regData.substr(i * 2, 2), nullptr, 16));
+    }
+
+    data.length = static_cast<uint8_t>(byteCount);
+    return data;
+}
+
+std::vector<MPSData> MPSImageParser::getRegistersData()
+{
+    std::vector<MPSData> registersData;
+    for (const auto& tokens : lineTokens)
+    {
+        if (tokens[0].starts_with("END"))
+        {
+            break;
+        }
+
+        if (isValidDataTokens(tokens))
+        {
+            registersData.push_back(extractData(tokens));
+        }
+    }
+    return registersData;
+}
+
+sdbusplus::async::task<bool> MPSVoltageRegulator::parseImage(
+    const uint8_t* image, size_t imageSize)
+{
+    parser = std::make_unique<MPSImageParser>(image, imageSize);
+
+    configuration = std::make_unique<MPSConfig>();
+    configuration->registersData = parser->getRegistersData();
+
+    if (!co_await parseDeviceConfiguration())
+    {
+        co_return false;
+    }
+
+    lg2::debug(
+        "Parsed configuration: Data Size={SIZE}, Vendor ID={VID}, "
+        "Product ID={PID}, Config ID={CID}, CRC User={CRCUSR}, "
+        "CRC Multi={CRCMULTI}",
+        "SIZE", configuration->registersData.size(), "VID", lg2::hex,
+        configuration->vendorId, "PID", lg2::hex, configuration->productId,
+        "CID", lg2::hex, configuration->configId, "CRCUSR", lg2::hex,
+        configuration->crcUser, "CRCMULTI", lg2::hex, configuration->crcMulti);
+
+    co_return true;
+}
+
+std::map<uint8_t, std::vector<MPSData>>
+    MPSVoltageRegulator::getGroupedConfigData(uint8_t configMask, uint8_t shift)
+{
+    std::map<uint8_t, std::vector<MPSData>> groupedData;
+
+    if (!configuration)
+    {
+        return groupedData;
+    }
+
+    for (const auto& data : configuration->registersData)
+    {
+        uint8_t config = (data.page & configMask) >> shift;
+        groupedData[config].push_back(data);
+    }
+
+    return groupedData;
+}
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/mps/mps.hpp b/i2c-vr/mps/mps.hpp
new file mode 100644
index 0000000..da6f854
--- /dev/null
+++ b/i2c-vr/mps/mps.hpp
@@ -0,0 +1,290 @@
+#pragma once
+
+#include "common/include/i2c/i2c.hpp"
+#include "i2c-vr/vr.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <cstdint>
+#include <iterator>
+#include <memory>
+#include <string_view>
+#include <vector>
+
+namespace phosphor::software::VR
+{
+
+/**
+ * @brief
+ * Columns of an Automated Test Equipment (ATE) format configuration file.
+ * Each enumerator corresponds to a tab-separated column index.
+ */
+enum class ATE : uint8_t
+{
+    configId = 0,
+    pageNum,
+    regAddrHex,
+    regAddrDec,
+    regName,
+    regDataHex,
+    regDataDec,
+    writeType,
+    colCount,
+};
+
+enum class MPSPage : uint8_t
+{
+    page0 = 0,
+    page1,
+    page2,
+    page3,
+    page4,
+};
+
+struct MPSData
+{
+    uint8_t page = 0;
+    uint8_t addr = 0;
+    uint8_t length = 0;
+    std::array<uint8_t, 4> data{};
+};
+
+struct MPSConfig
+{
+    uint32_t vendorId = 0;
+    uint32_t productId = 0;
+    uint32_t configId = 0;
+    uint32_t crcUser = 0;
+    uint32_t crcMulti = 0;
+    std::vector<MPSData> registersData;
+};
+
+/**
+ * @brief
+ * Utility class to iterate over lines and tokenize them by tab characters.
+ */
+class TokenizedLines
+{
+  public:
+    TokenizedLines(const uint8_t* d, size_t s) :
+        data(reinterpret_cast<const char*>(d), s)
+    {}
+    /**
+     * @brief Iterator over tokenized lines.
+     */
+    struct Iterator
+    {
+        using iterator_category = std::forward_iterator_tag;
+        using value_type = std::vector<std::string_view>;
+        using difference_type = std::ptrdiff_t;
+        using pointer = const value_type*;
+        using reference = const value_type&;
+
+        Iterator() = default; // End iterator
+        Iterator(std::string_view sv) : remaining(sv)
+        {
+            next();
+        }
+
+        reference operator*() const
+        {
+            return currentTokens;
+        }
+
+        pointer operator->() const
+        {
+            return &currentTokens;
+        }
+
+        Iterator& operator++()
+        {
+            next();
+            return *this;
+        }
+
+        Iterator operator++(int)
+        {
+            auto result = *this;
+            ++(*this);
+            return result;
+        }
+
+        friend bool operator==(const Iterator& a, const Iterator& b)
+        {
+            return a.remaining.empty() && b.remaining.empty();
+        }
+
+        friend bool operator!=(const Iterator& a, const Iterator& b)
+        {
+            return !(a == b);
+        }
+
+      private:
+        std::string_view remaining;
+        std::vector<std::string_view> currentTokens;
+
+        void next()
+        {
+            currentTokens.clear();
+            if (remaining.empty())
+            {
+                return;
+            }
+
+            // Extract current line
+            auto newlinePos = remaining.find('\n');
+            std::string_view line = remaining.substr(0, newlinePos);
+            remaining = (newlinePos == std::string_view::npos)
+                            ? std::string_view{}
+                            : remaining.substr(newlinePos + 1);
+
+            // Tokenize by tab
+            size_t start = 0;
+            while (start < line.size())
+            {
+                start = line.find_first_not_of('\t', start);
+                if (start == std::string_view::npos)
+                {
+                    break;
+                }
+
+                auto end = line.find('\t', start);
+                currentTokens.emplace_back(line.substr(start, end - start));
+                start = (end == std::string_view::npos) ? line.size() : end;
+            }
+        }
+    };
+
+    Iterator begin() const
+    {
+        return Iterator(data);
+    }
+
+    static Iterator end()
+    {
+        return Iterator();
+    }
+
+  private:
+    std::string_view data;
+};
+
+/**
+ * @brief Base parser for MPS configuration images.
+ */
+class MPSImageParser
+{
+  public:
+    MPSImageParser(const uint8_t* image, size_t imageSize) :
+        lineTokens(image, imageSize)
+    {}
+
+    template <typename>
+    inline static constexpr bool always_false = false;
+
+    /**
+     * @brief Extract a typed value from a tokenized line.
+     * @tparam T Return type (string or integral type)
+     * @param tokens Tokenized line
+     * @param index Column index (ATE enum)
+     * @return Parsed value or default if invalid
+     */
+    template <typename T>
+    T getVal(const std::vector<std::string_view>& tokens, ATE index)
+    {
+        size_t idx = static_cast<size_t>(index);
+
+        if (tokens.size() <= idx)
+        {
+            lg2::error("Index out of range for ATE enum: {INDEX}", "INDEX",
+                       static_cast<uint32_t>(idx));
+            return T{};
+        }
+
+        std::string_view token = tokens[idx];
+
+        if constexpr (std::is_same_v<T, std::string>)
+        {
+            return std::string(token);
+        }
+        else if constexpr (std::is_integral_v<T>)
+        {
+            unsigned long val = 0;
+            try
+            {
+                val = std::stoul(std::string(token), nullptr, 16);
+            }
+            catch (...)
+            {
+                lg2::error("Invalid hex value: {INDEX}", "INDEX",
+                           static_cast<uint32_t>(idx));
+                return T{};
+            }
+            return static_cast<T>(val);
+        }
+        else
+        {
+            static_assert(always_false<T>, "Unsupported type in getVal");
+        }
+    }
+
+    /**
+     * @brief Check if a tokenized line contains valid register data.
+     */
+    static bool isValidDataTokens(const std::vector<std::string_view>& tokens);
+
+    /**
+     * @brief Convert tokenized line into MPSData structure.
+     */
+    MPSData extractData(const std::vector<std::string_view>& tokens);
+
+    /**
+     * @brief Collect all register data entries from the parsed image.
+     */
+    std::vector<MPSData> getRegistersData();
+
+    TokenizedLines lineTokens;
+};
+
+/**
+ * @brief Base class for MPS Voltage Regulators.
+ */
+class MPSVoltageRegulator : public VoltageRegulator
+{
+  public:
+    MPSVoltageRegulator(sdbusplus::async::context& ctx, uint16_t bus,
+                        uint16_t address) :
+        VoltageRegulator(ctx), i2cInterface(phosphor::i2c::I2C(bus, address))
+    {}
+
+    /**
+     * @brief Parse device-specific configuration from the loaded image.
+     * @return async task returning true if parsing succeeds
+     */
+    virtual sdbusplus::async::task<bool> parseDeviceConfiguration() = 0;
+
+    /**
+     * @brief Parse an image file into internal MPS configuration.
+     * @param image Pointer to the image data
+     * @param imageSize Size of the image data
+     * @return async task returning true if parsing succeeds
+     */
+    sdbusplus::async::task<bool> parseImage(const uint8_t* image,
+                                            size_t imageSize);
+
+    /**
+     * @brief Group register data by page, optionally masked and shifted.
+     * @param configMask Bitmask to select relevant page bits (default 0xFF)
+     * @param shift Number of bits to shift masked value to obtain group key
+     * @return map of page keys to vector of MPSData
+     */
+    std::map<uint8_t, std::vector<MPSData>> getGroupedConfigData(
+        uint8_t configMask = 0xFF, uint8_t shift = 0);
+
+  protected:
+    phosphor::i2c::I2C i2cInterface;
+    std::unique_ptr<MPSImageParser> parser;
+    std::unique_ptr<MPSConfig> configuration;
+};
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/vr.cpp b/i2c-vr/vr.cpp
index 7280f91..796c9df 100644
--- a/i2c-vr/vr.cpp
+++ b/i2c-vr/vr.cpp
@@ -1,6 +1,7 @@
 #include "vr.hpp"
 
 #include "isl69269/isl69269.hpp"
+#include "mps/mp2x6xx.hpp"
 #include "xdpe1x2xx/xdpe1x2xx.hpp"
 
 #include <map>
@@ -12,19 +13,17 @@
                                          enum VRType vrType, uint16_t bus,
                                          uint16_t address)
 {
-    std::unique_ptr<VoltageRegulator> ret;
     switch (vrType)
     {
         case VRType::XDPE1X2XX:
-            ret = std::make_unique<XDPE1X2XX>(ctx, bus, address);
-            break;
+            return std::make_unique<XDPE1X2XX>(ctx, bus, address);
         case VRType::ISL69269:
-            ret = std::make_unique<ISL69269>(ctx, bus, address);
-            break;
+            return std::make_unique<ISL69269>(ctx, bus, address);
+        case VRType::MP2X6XX:
+            return std::make_unique<MP2X6XX>(ctx, bus, address);
         default:
-            return NULL;
+            return nullptr;
     }
-    return ret;
 }
 
 bool stringToEnum(std::string& vrStr, VRType& vrType)
@@ -32,7 +31,7 @@
     std::map<std::string, enum VRType> VRTypeToString{
         {"XDPE1X2XXFirmware", VRType::XDPE1X2XX},
         {"ISL69269Firmware", VRType::ISL69269},
-    };
+        {"MP2X6XXFirmware", VRType::MP2X6XX}};
 
     if (VRTypeToString.contains(vrStr))
     {
diff --git a/i2c-vr/vr.hpp b/i2c-vr/vr.hpp
index 1212a39..2e224f8 100644
--- a/i2c-vr/vr.hpp
+++ b/i2c-vr/vr.hpp
@@ -13,6 +13,7 @@
 {
     XDPE1X2XX,
     ISL69269,
+    MP2X6XX,
 };
 
 class VoltageRegulator