Add i2c voltage regulator updater

- /i2c-vr: Classes following the design in [1]
- /i2c-vr/vr.cpp, vr.hpp: General representation of a voltage regulators
  communication interface
- /vr-i2c/xdpe1x2xx: Support for Infineon XDPE1x2xx class of voltage
  regulators
- /common/i2c/: Basic I2C communication interface

The configuration of a voltage regulator relies on the EM-Schema in [2]

1: https://github.com/openbmc/docs/blob/master/designs/code-update.md
2: https://gerrit.openbmc.org/c/openbmc/entity-manager/+/77463

Tested on QEMU/Yosemite4:

1. Display the firmware inventory
```
curl --insecure --user root:0penBmc \
https://127.0.0.1:2443/redfish/v1/UpdateService/FirmwareInventory
```

Output:
```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/16bae6fd"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/DummyDeviceFirmwareName_6637"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Management_Board_cpld"
    },
    {...}
```

2. Query voltage regulator version

The dummy device sets the default version to 0xBEEF (Decimal: 48879)
```
curl --insecure --user root:0penBmc \
https://127.0.0.1:2443/redfish/v1/UpdateService/FirmwareInventory/
DummyDeviceFirmwareName_4390
```

Output:
```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/DummyDeviceFirmwareName_4390",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Other image",
  "Id": "DummyDeviceFirmwareName_4390",
  "Name": "Software Inventory",
  "Status": {
    "Health": "Warning",
    "HealthRollup": "OK",
    "State": "Disabled"
  },
  "Updateable": false,
  "Version": "48879"
}
```

3. Trigger the update
```
curl -k --insecure --user root:0penBmc \
-H "Content-Type:multipart/form-data" \
-X POST \
-F UpdateParameters="{\"Targets\":[\"/redfish/v1/UpdateService/FirmwareInventory/DummyDeviceFirmwareName_4390\"],\"@Redfish.OperationApplyTime\":\"Immediate\"};type=application/json" \
-F "UpdateFile=@fw_vr_update.bin;type=application/octet-stream" \
https://127.0.0.1:2443/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 the task
```
curl --insecure --user root:0penBmc \
https://127.0.0.1:2443/redfish/v1/TaskService/Tasks/0
```

Output:
```
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task",
  "EndTime": "2025-03-10T13:47:34+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 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": 0,
  "StartTime": "2025-03-10T13:47:34+00:00",
  "TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/0",
  "TaskState": "Completed",
  "TaskStatus": "OK"
}
```

6. Display fw inventory after update
```
curl --insecure --user root:0penBmc \
https://127.0.0.1:2443/redfish/v1/UpdateService/FirmwareInventory
```

Output:
```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/16bae6fd"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/DummyDeviceFirmwareName_282"
    },
    {...}
}
```

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 --insecure --user root:0penBmc \
https://127.0.0.1:2443/redfish/v1/UpdateService/FirmwareInventory/
DummyDeviceFirmwareName_282
```

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

8. Update in progress error

```
curl -k --insecure --user root:0penBmc \
-H "Content-Type:multipart/form-data" \
-X POST \
-F UpdateParameters="{\"Targets\":[\"/redfish/v1/UpdateService/FirmwareInventory/${TARGET}\"],\"@Redfish.OperationApplyTime\":\"Immediate\"};type=application/json" \
-F "UpdateFile=@${FWPATH};type=application/octet-stream" \
https://127.0.0.1:2443/redfish/v1/UpdateService/update
```

Output:
```
{
  "error": {
    "@Message.ExtendedInfo": [
      {
        "@odata.type": "#Message.v1_1_1.Message",
        "Message": "The request failed due to an internal service error.  The service is still operational.",
        "MessageArgs": [],
        "MessageId": "Base.1.19.InternalError",
        "MessageSeverity": "Critical",
        "Resolution": "Resubmit the request.  If the problem persists, consider resetting the service."
      }
    ],
    "code": "Base.1.19.InternalError",
    "message": "The request failed due to an internal service error.  The service is still operational."
  }
```

Change-Id: I2e11a6c10ae40ed7719ceb86dfd6a38dd5b27017
Signed-off-by: Christopher Meis <christopher.meis@9elements.com>
diff --git a/common/i2c/i2c.cpp b/common/i2c/i2c.cpp
new file mode 100644
index 0000000..99de04b
--- /dev/null
+++ b/common/i2c/i2c.cpp
@@ -0,0 +1,82 @@
+#include "i2c.hpp"
+
+#include <unistd.h>
+
+extern "C"
+{
+#include <i2c/smbus.h>
+#include <linux/i2c-dev.h>
+#include <linux/i2c.h>
+}
+
+namespace phosphor::i2c
+{
+
+int I2C::open()
+{
+    int ret = 0;
+    fd = ::open(busStr.c_str(), O_RDWR);
+    if (fd < 0)
+    {
+        return fd;
+    }
+
+    ret = ioctl(fd, I2C_SLAVE_FORCE, deviceNode);
+    if (ret < 0)
+    {
+        close();
+        return ret;
+    }
+
+    return 0;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> I2C::sendReceive(
+    uint8_t* writeData, uint8_t writeSize, uint8_t* readData,
+    uint8_t readSize) const
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    if (fd <= 0)
+    {
+        co_return false;
+    }
+
+    struct i2c_msg msg[2];
+    struct i2c_rdwr_ioctl_data readWriteData;
+    int msgIndex = 0;
+
+    if (writeSize)
+    {
+        msg[msgIndex].addr = deviceNode;
+        msg[msgIndex].flags = 0;
+        msg[msgIndex].len = writeSize;
+        msg[msgIndex].buf = writeData;
+        msgIndex++;
+    }
+
+    if (readSize)
+    {
+        msg[msgIndex].addr = deviceNode;
+        msg[msgIndex].flags = I2C_M_RD;
+        msg[msgIndex].len = readSize;
+        msg[msgIndex].buf = readData;
+        msgIndex++;
+    }
+
+    readWriteData.msgs = msg;
+    readWriteData.nmsgs = msgIndex;
+
+    if (ioctl(fd, I2C_RDWR, &readWriteData) < 0)
+    {
+        co_return false;
+    }
+    co_return true;
+}
+
+int I2C::close() const
+{
+    return ::close(fd);
+}
+
+} // namespace phosphor::i2c
diff --git a/common/i2c/meson.build b/common/i2c/meson.build
new file mode 100644
index 0000000..fd8a702
--- /dev/null
+++ b/common/i2c/meson.build
@@ -0,0 +1,8 @@
+libi2c_dev = static_library('i2c_dev', 'i2c.cpp', link_args: '-li2c')
+libi2c_inc = include_directories('.')
+libi2c_dep = declare_dependency(
+    common_include,
+    link_with: libi2c_dev,
+    include_directories: libi2c_inc,
+    link_args: '-li2c',
+)
diff --git a/common/include/i2c/i2c.hpp b/common/include/i2c/i2c.hpp
new file mode 100644
index 0000000..e86d574
--- /dev/null
+++ b/common/include/i2c/i2c.hpp
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+
+#include <sdbusplus/async.hpp>
+
+#include <cstdint>
+#include <cstring>
+#include <string>
+
+extern "C"
+{
+#include <i2c/smbus.h>
+#include <linux/i2c-dev.h>
+#include <linux/i2c.h>
+}
+
+namespace phosphor::i2c
+{
+
+class I2C
+{
+  public:
+    explicit I2C(uint16_t bus, uint16_t node) :
+        busStr("/dev/i2c-" + std::to_string(bus)), deviceNode(node)
+    {
+        open();
+    }
+
+    I2C(I2C& i2c) = delete;
+    I2C& operator=(I2C other) = delete;
+    I2C(I2C&& other) = delete;
+    I2C& operator=(I2C&& other) = delete;
+
+    ~I2C()
+    {
+        this->close();
+    }
+
+    sdbusplus::async::task<bool> sendReceive(
+        uint8_t* writeData, uint8_t writeSize, uint8_t* readData,
+        uint8_t readSize) const;
+
+    bool isOpen() const
+    {
+        return (fd != invalidFd);
+    }
+
+    int close() const;
+
+  private:
+    static constexpr int invalidFd = -1;
+    int fd = invalidFd;
+    std::string busStr;
+    uint16_t deviceNode;
+    int open();
+}; // end class I2C
+
+} // namespace phosphor::i2c
diff --git a/common/meson.build b/common/meson.build
index 1e29ae8..6887c38 100644
--- a/common/meson.build
+++ b/common/meson.build
@@ -9,6 +9,7 @@
     'src/software.cpp',
     'src/software_update.cpp',
     'src/host_power.cpp',
-    include_directories: ['.', 'include/', common_include],
+    'i2c/i2c.cpp',
+    include_directories: ['.', 'include/', 'include/i2c/', common_include],
     dependencies: [pdi_dep, phosphor_logging_dep, sdbusplus_dep, libpldm_dep],
 )
diff --git a/i2c-vr/README.md b/i2c-vr/README.md
new file mode 100644
index 0000000..174069b
--- /dev/null
+++ b/i2c-vr/README.md
@@ -0,0 +1,25 @@
+# Voltage Regulator Update Daemon
+
+This daemon implements the update process for voltage regulators attached via
+I2C/PMBus bus.
+
+## Configuration Example
+
+This example shows an addition to configuration for the Infineon XDPE1X2XX
+target. It only shows the DBUS related information provided by the
+configuration.
+
+### EntityManager configuration
+
+```json
+{
+  "Address": "0x66",
+  "Bus": 28,
+  "Name": "MB_VR_CPU_VCORE1_PDDIO",
+  "Type": "XDPE1X2XX",
+  "FirmwareInfo": {
+    "VendorIANA": "6653",
+    "CompatibleHardware": "com.tyan.Hardware.S8030.SPI.Host"
+  }
+}
+```
diff --git a/i2c-vr/i2cvr_device.cpp b/i2c-vr/i2cvr_device.cpp
new file mode 100644
index 0000000..b695689
--- /dev/null
+++ b/i2c-vr/i2cvr_device.cpp
@@ -0,0 +1,66 @@
+#include "i2cvr_device.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/context.hpp>
+
+namespace phosphor::software::i2c_vr::device
+{
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> I2CVRDevice::updateDevice(const uint8_t* image,
+                                                       size_t imageSize)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    bool ret = false;
+    setUpdateProgress(20);
+
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    ret = co_await vrInterface->verifyImage(image, imageSize);
+    //  NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    if (!ret)
+    {
+        co_return false;
+    }
+
+    setUpdateProgress(50);
+
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    ret = co_await vrInterface->updateFirmware(false);
+    //  NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    if (!ret)
+    {
+        co_return false;
+    }
+
+    setUpdateProgress(80);
+
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    ret = co_await vrInterface->reset();
+    //  NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    if (!ret)
+    {
+        co_return false;
+    }
+
+    setUpdateProgress(100);
+
+    lg2::info("Successfully updated VR {NAME}", "NAME", config.configName);
+
+    co_return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> I2CVRDevice::getVersion(uint32_t* sum) const
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    if (!(co_await this->vrInterface->getCRC(sum)))
+    //  NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    {
+        co_return false;
+    }
+    co_return true;
+}
+
+} // namespace phosphor::software::i2c_vr::device
diff --git a/i2c-vr/i2cvr_device.hpp b/i2c-vr/i2cvr_device.hpp
new file mode 100644
index 0000000..198d1c6
--- /dev/null
+++ b/i2c-vr/i2cvr_device.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "common/include/device.hpp"
+#include "common/include/software_config.hpp"
+#include "common/include/software_manager.hpp"
+#include "vr.hpp"
+
+namespace SoftwareInf = phosphor::software;
+namespace ManagerInf = SoftwareInf::manager;
+namespace DeviceInf = SoftwareInf::device;
+namespace ConfigInf = SoftwareInf::config;
+
+namespace VRInf = SoftwareInf::VR;
+
+namespace SDBusPlusSoftware = sdbusplus::common::xyz::openbmc_project::software;
+
+namespace phosphor::software::i2c_vr::device
+{
+
+class I2CVRDevice : public DeviceInf::Device
+{
+  public:
+    using DeviceInf::Device::softwareCurrent;
+    I2CVRDevice(sdbusplus::async::context& ctx, enum VRInf::VRType vrType,
+                const uint16_t& bus, const uint8_t& address,
+                ConfigInf::SoftwareConfig& config,
+                ManagerInf::SoftwareManager* parent) :
+        DeviceInf::Device(
+            ctx, config, parent,
+            {SDBusPlusSoftware::ApplyTime::RequestedApplyTimes::Immediate,
+             SDBusPlusSoftware::ApplyTime::RequestedApplyTimes::OnReset}),
+        vrInterface(VRInf::create(ctx, vrType, bus, address))
+    {}
+
+    std::unique_ptr<VRInf::VoltageRegulator> vrInterface;
+
+    sdbusplus::async::task<bool> updateDevice(const uint8_t* image,
+                                              size_t image_size) final;
+
+    sdbusplus::async::task<bool> getVersion(uint32_t* sum) const;
+};
+
+} // namespace phosphor::software::i2c_vr::device
diff --git a/i2c-vr/i2cvr_software_manager.cpp b/i2c-vr/i2cvr_software_manager.cpp
new file mode 100644
index 0000000..4c9d398
--- /dev/null
+++ b/i2c-vr/i2cvr_software_manager.cpp
@@ -0,0 +1,115 @@
+#include "i2cvr_software_manager.hpp"
+
+#include "common/include/dbus_helper.hpp"
+#include "common/include/software_manager.hpp"
+#include "i2cvr_device.hpp"
+#include "vr.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/bus.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+
+#include <cstdint>
+
+PHOSPHOR_LOG2_USING;
+
+namespace VR = phosphor::software::VR;
+namespace I2CDevice = phosphor::software::i2c_vr::device;
+namespace SoftwareInf = phosphor::software;
+namespace ManagerInf = phosphor::software::manager;
+
+const std::string configDBusName = "I2CVR";
+const std::vector<std::string> emConfigTypes = {"XDPE1X2XXFirmware",
+                                                "DummyDeviceFirmware"};
+
+I2CVRSoftwareManager::I2CVRSoftwareManager(sdbusplus::async::context& ctx) :
+    ManagerInf::SoftwareManager(ctx, configDBusName)
+{}
+
+void I2CVRSoftwareManager::start()
+{
+    std::vector<std::string> configIntfs;
+    configIntfs.reserve(emConfigTypes.size());
+    for (auto& name : emConfigTypes)
+    {
+        configIntfs.push_back("xyz.openbmc_project.Configuration." + name);
+    }
+
+    ctx.spawn(initDevices(configIntfs));
+    ctx.run();
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> I2CVRSoftwareManager::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<uint64_t> busNum = co_await dbusGetRequiredProperty<uint64_t>(
+        ctx, service, path, configIface, "Bus");
+    std::optional<uint64_t> address =
+        co_await dbusGetRequiredProperty<uint64_t>(ctx, service, path,
+                                                   configIface, "Address");
+    std::optional<std::string> vrChipType =
+        co_await dbusGetRequiredProperty<std::string>(ctx, service, path,
+                                                      configIface, "Type");
+
+    if (!busNum.has_value() || !address.has_value() || !vrChipType.has_value())
+    {
+        error("missing config property");
+        co_return false;
+    }
+
+    VR::VRType vrType;
+    if (!VR::stringToEnum(vrChipType.value(), vrType))
+    {
+        error("unknown voltage regulator type: {TYPE}", "TYPE",
+              vrChipType.value());
+        co_return false;
+    }
+
+    lg2::debug(
+        "[config] Voltage regulator device type: {TYPE} on Bus: {BUS} at Address: {ADDR}",
+        "TYPE", vrChipType.value(), "BUS", busNum.value(), "ADDR",
+        address.value());
+
+    auto i2cDevice = std::make_unique<I2CDevice::I2CVRDevice>(
+        ctx, vrType, static_cast<uint16_t>(busNum.value()),
+        static_cast<uint16_t>(address.value()), config, this);
+
+    std::unique_ptr<SoftwareInf::Software> software =
+        std::make_unique<SoftwareInf::Software>(ctx, *i2cDevice);
+
+    uint32_t sum;
+    if (!(co_await i2cDevice->getVersion(&sum)))
+    {
+        error("unable to obtain Version/CRC from voltage regulator");
+        co_return false;
+    }
+
+    software->setVersion(std::to_string(sum));
+
+    std::set<RequestedApplyTimes> allowedApplyTime = {
+        RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset};
+
+    software->enableUpdate(allowedApplyTime);
+
+    i2cDevice->softwareCurrent = std::move(software);
+
+    devices.insert({config.objectPath, std::move(i2cDevice)});
+
+    co_return true;
+}
+
+int main()
+{
+    sdbusplus::async::context ctx;
+
+    I2CVRSoftwareManager i2cVRSoftwareManager(ctx);
+
+    i2cVRSoftwareManager.start();
+    return 0;
+}
diff --git a/i2c-vr/i2cvr_software_manager.hpp b/i2c-vr/i2cvr_software_manager.hpp
new file mode 100644
index 0000000..d0cad8c
--- /dev/null
+++ b/i2c-vr/i2cvr_software_manager.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "common/include/software_manager.hpp"
+
+#include <sdbusplus/async/context.hpp>
+
+namespace ManagerInf = phosphor::software::manager;
+namespace SDBusAsync = sdbusplus::async;
+
+class I2CVRSoftwareManager : public ManagerInf::SoftwareManager
+{
+  public:
+    I2CVRSoftwareManager(SDBusAsync::context& ctx);
+
+    SDBusAsync::task<bool> initDevice(const std::string& service,
+                                      const std::string& path,
+                                      SoftwareConfig& config) final;
+
+    void start();
+};
diff --git a/i2c-vr/meson.build b/i2c-vr/meson.build
new file mode 100644
index 0000000..001fcf4
--- /dev/null
+++ b/i2c-vr/meson.build
@@ -0,0 +1,32 @@
+i2cvr_src = files('i2cvr_device.cpp', 'i2cvr_software_manager.cpp', 'vr.cpp')
+
+regulators_src = files('xdpe1x2xx/xdpe1x2xx.cpp')
+
+i2cvr_include = include_directories('.')
+
+executable(
+    'phosphor-i2cvr-software-update',
+    'i2cvr_software_manager.cpp',
+    i2cvr_src,
+    regulators_src,
+    include_directories: [common_include, i2cvr_include],
+    dependencies: [
+        sdbusplus_dep,
+        phosphor_logging_dep,
+        pdi_dep,
+        boost_dep,
+        libpldm_dep,
+    ],
+    link_with: [software_common_lib, libpldmutil],
+    install: true,
+)
+
+systemd_system_unit_dir = dependency('systemd').get_variable(
+    'systemdsystemunitdir',
+    pkgconfig_define: ['prefix', get_option('prefix')],
+)
+
+install_data(
+    'xyz.openbmc_project.Software.I2CVR.service',
+    install_dir: systemd_system_unit_dir,
+)
diff --git a/i2c-vr/vr.cpp b/i2c-vr/vr.cpp
new file mode 100644
index 0000000..f7c7c0f
--- /dev/null
+++ b/i2c-vr/vr.cpp
@@ -0,0 +1,40 @@
+#include "vr.hpp"
+
+#include "xdpe1x2xx/xdpe1x2xx.hpp"
+
+#include <map>
+
+namespace phosphor::software::VR
+{
+
+std::unique_ptr<VoltageRegulator> create(sdbusplus::async::context& ctx,
+                                         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;
+        default:
+            return NULL;
+    }
+    return ret;
+}
+
+bool stringToEnum(std::string& vrStr, VRType& vrType)
+{
+    std::map<std::string, enum VRType> VRTypeToString{
+        {"XDPE1X2XXFirmware", VRType::XDPE1X2XX},
+    };
+
+    if (VRTypeToString.contains(vrStr))
+    {
+        vrType = VRTypeToString[vrStr];
+        return true;
+    }
+    return false;
+}
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/vr.hpp b/i2c-vr/vr.hpp
new file mode 100644
index 0000000..670dc9f
--- /dev/null
+++ b/i2c-vr/vr.hpp
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <sdbusplus/async.hpp>
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+namespace phosphor::software::VR
+{
+
+enum class VRType
+{
+    XDPE1X2XX,
+};
+
+class VoltageRegulator
+{
+  public:
+    explicit VoltageRegulator(sdbusplus::async::context& ctx) : ctx(ctx) {}
+    virtual ~VoltageRegulator() = default;
+
+    VoltageRegulator(VoltageRegulator& vr) = delete;
+    VoltageRegulator& operator=(VoltageRegulator other) = delete;
+    VoltageRegulator(VoltageRegulator&& other) = delete;
+    VoltageRegulator& operator=(VoltageRegulator&& other) = delete;
+
+    // @brief Parses the firmware image into the configuration structure
+    //        and verifies its correctness.
+    // @return sdbusplus::async::task<bool> true indicates success.
+    virtual sdbusplus::async::task<bool> verifyImage(const uint8_t* image,
+                                                     size_t imageSize) = 0;
+
+    // @brief Applies update to the voltage regulator
+    // @return sdbusplus::async::task<bool> true indicates success.
+    virtual sdbusplus::async::task<bool> updateFirmware(bool force) = 0;
+
+    // @brief resets the voltage regulator for the update to take effect.
+    // @return sdbusplus::async::task<bool> true indicates success.
+    virtual sdbusplus::async::task<bool> reset() = 0;
+
+    // @brief Requests the CRC value of the voltage regulator over I2C.
+    // @param pointer to write the result to.
+    // @returns < 0 on error
+    virtual sdbusplus::async::task<bool> getCRC(uint32_t* checksum) = 0;
+
+    // @brief This function returns true if the voltage regulator supports
+    //        force of updates.
+    virtual bool forcedUpdateAllowed() = 0;
+
+  protected:
+    sdbusplus::async::context& ctx;
+};
+
+std::unique_ptr<VoltageRegulator> create(sdbusplus::async::context& ctx,
+                                         enum VRType vrType, uint16_t bus,
+                                         uint16_t address);
+
+bool stringToEnum(std::string& vrStr, VRType& vrType);
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/xdpe1x2xx/xdpe1x2xx.cpp b/i2c-vr/xdpe1x2xx/xdpe1x2xx.cpp
new file mode 100644
index 0000000..28894e0
--- /dev/null
+++ b/i2c-vr/xdpe1x2xx/xdpe1x2xx.cpp
@@ -0,0 +1,733 @@
+#include "xdpe1x2xx.hpp"
+
+#include "common/include/i2c/i2c.hpp"
+
+#include <unistd.h>
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <cstdio>
+
+#define REMAINING_TIMES(x, y) (((((x)[1]) << 8) | ((x)[0])) / (y))
+
+PHOSPHOR_LOG2_USING;
+
+namespace phosphor::software::VR
+{
+
+const uint32_t CRC32Poly = 0xEDB88320;
+const int VRResetDelay = 500000;
+
+enum RevisionCode
+{
+    REV_A = 0x00,
+    REV_B,
+    REV_C,
+    REV_D,
+};
+
+enum ProductID
+{
+    ProductIDXDPE15284 = 0x8A,
+    ProductIDXDPE19283 = 0x95,
+};
+
+const uint32_t PMBusICDeviceID = 0xAD;
+const uint32_t PMBusSTLCml = 0x7E;
+const uint32_t IFXICDeviceIDLen = 2;
+const uint32_t IFXMFRAHBAddr = 0xCE;
+const uint32_t IFXMFRRegWrite = 0xDE;
+const uint32_t IFXMFRFwCmdData = 0xFD;
+const uint32_t IFXMFRFwCmd = 0xFE;
+const uint32_t MFRFwCmdReset = 0x0e;
+const uint32_t MFRFwCmdRmng = 0x10;
+const uint32_t MFRFwCmdOTPConfSTO = 0x11;
+const uint32_t MFRFwCmdOTPFileInvd = 0x12;
+const uint32_t MFRFwCmdGetCRC = 0x2D;
+const int XDPE15284CConfSize = 1344;
+const int XDPE19283BConfSize = 1416;
+const uint32_t VRWarnRemaining = 3;
+const uint32_t SectTrim = 0x02;
+
+const char* const AddressField = "PMBus Address :";
+const char* const ChecksumField = "Checksum :";
+const char* const DataStartTag = "Configuration Data]";
+const char* const DataEndTag = "[End Configuration Data]";
+const char* const DataComment = "//";
+const char* const DataXV = "XV";
+
+XDPE1X2XX::XDPE1X2XX(sdbusplus::async::context& ctx, uint16_t bus,
+                     uint16_t address) :
+    VoltageRegulator(ctx), i2cInterface(phosphor::i2c::I2C(bus, address))
+{}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> XDPE1X2XX::getDeviceId(uint8_t* deviceID)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    bool ret = false;
+    uint8_t tbuf[16] = {0};
+    tbuf[0] = PMBusICDeviceID;
+    tbuf[1] = 2;
+    uint8_t tSize = 1;
+    uint8_t rbuf[16] = {0};
+    uint8_t rSize = IFXICDeviceIDLen + 1;
+
+    ret = co_await this->i2cInterface.sendReceive(tbuf, tSize, rbuf, rSize);
+    if (!ret)
+    {
+        error("Failed to get device ID");
+        co_return false;
+    }
+
+    std::memcpy(deviceID, &rbuf[1], IFXICDeviceIDLen);
+
+    co_return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> XDPE1X2XX::mfrFWcmd(uint8_t cmd, uint8_t* data,
+                                                 uint8_t* resp)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    bool ret = false;
+    uint8_t tBuf[16] = {0};
+    uint8_t rBuf[16] = {0};
+    uint8_t tSize = 0;
+    uint8_t rSize = 0;
+
+    if (data)
+    {
+        tBuf[0] = IFXMFRFwCmdData;
+        tBuf[1] = 4; // Block write 4 bytes
+        tSize = 6;
+        std::memcpy(&tBuf[2], data, 4);
+        ret = co_await this->i2cInterface.sendReceive(tBuf, tSize, rBuf, rSize);
+        if (!ret)
+        {
+            error("Failed to send MFR command: {CMD}", "CMD",
+                  std::string("IFXMFRFwCmdDAta"));
+            co_return false;
+        }
+    }
+
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::microseconds(300));
+
+    tBuf[0] = IFXMFRFwCmd;
+    tBuf[1] = cmd;
+    tSize = 2;
+    rSize = 0;
+    ret = co_await this->i2cInterface.sendReceive(tBuf, tSize, rBuf, rSize);
+    if (!ret)
+    {
+        error("Failed to send MFR command: {CMD}", "CMD",
+              std::string("IFXMFRFwCmd"));
+        co_return false;
+    }
+
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::microseconds(20000));
+
+    if (resp)
+    {
+        tBuf[0] = IFXMFRFwCmdData;
+        tSize = 1;
+        rSize = 6;
+        ret = co_await this->i2cInterface.sendReceive(tBuf, tSize, rBuf, rSize);
+        if (!ret)
+        {
+            error("Failed to send MFR command: {CMD}", "CMD",
+                  std::string("IFXMFRFwCmdData"));
+            co_return false;
+        }
+        if (rBuf[0] != 4)
+        {
+            error(
+                "Failed to receive MFR response with unexpected response size");
+            co_return false;
+        }
+        std::memcpy(resp, rBuf + 1, 4);
+    }
+
+    co_return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> XDPE1X2XX::getRemainingWrites(uint8_t* remain)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    bool ret = false;
+    uint8_t tBuf[16] = {0};
+    uint8_t rBuf[16] = {0};
+    uint8_t devId[2] = {0};
+
+    ret = co_await this->mfrFWcmd(MFRFwCmdRmng, tBuf, rBuf);
+    if (!ret)
+    {
+        error("Failed to request remaining writes");
+        co_return false;
+    }
+
+    ret = co_await this->getDeviceId(devId);
+    if (!ret)
+    {
+        error("Failed to request device ID for remaining writes");
+        co_return false;
+    }
+
+    int configSize = getConfigSize(devId[1], devId[0]);
+    if (configSize < 0)
+    {
+        error("Failed to request valid configuration size");
+        co_return false;
+    }
+
+    *remain = REMAINING_TIMES(rBuf, configSize);
+
+    co_return 0;
+}
+
+int XDPE1X2XX::getConfigSize(uint8_t deviceId, uint8_t revision)
+{
+    int size = -1;
+
+    switch (deviceId)
+    {
+        case ProductIDXDPE19283:
+            if (revision == REV_B)
+            {
+                size = XDPE19283BConfSize;
+            }
+            break;
+        case ProductIDXDPE15284:
+            size = XDPE15284CConfSize;
+            break;
+        default:
+            error(
+                "Failed to get configuration size of {DEVID} with revision {REV}",
+                "DEVID", deviceId, "REV", revision);
+            return -1;
+    }
+
+    return size;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> XDPE1X2XX::getCRC(uint32_t* checksum)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    uint8_t tBuf[16] = {0};
+    uint8_t rBuf[16] = {0};
+
+    bool ret = co_await this->mfrFWcmd(MFRFwCmdGetCRC, tBuf, rBuf);
+    if (!ret)
+    {
+        error("Failed to get CRC value");
+        co_return false;
+    }
+
+    *checksum = (static_cast<uint32_t>(rBuf[3]) << 24) |
+                (static_cast<uint32_t>(rBuf[2]) << 16) |
+                (static_cast<uint32_t>(rBuf[1]) << 8) |
+                (static_cast<uint32_t>(rBuf[0]));
+
+    co_return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> XDPE1X2XX::program(bool force)
+// NOLINTEND(readability-static-accessed-through-instance)
+
+{
+    bool ret = false;
+    uint8_t tBuf[16] = {0};
+    uint8_t rBuf[16] = {0};
+    uint8_t remain = 0;
+    uint32_t sum = 0;
+    int size = 0;
+
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    ret = co_await getCRC(&sum);
+    // NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    if (!ret)
+    {
+        error("Failed to program the VR");
+        co_return -1;
+    }
+
+    if (!force && (sum == configuration.sumExp))
+    {
+        error("Failed to program the VR - CRC value are equal with no force");
+        co_return -1;
+    }
+
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    ret = co_await this->getRemainingWrites(&remain);
+    // NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    if (!ret)
+    {
+        error("Failed to program the VR - unable to obtain remaing writes");
+        co_return -1;
+    }
+
+    if (!remain)
+    {
+        error("Failed to program the VR - no remaining write cycles left");
+        co_return -1;
+    }
+
+    if (!force && (remain <= VRWarnRemaining))
+    {
+        error(
+            "Failed to program the VR - {REMAIN} remaining writes left and not force",
+            "REMAIN", remain);
+        co_return -1;
+    }
+
+    // Added reprogramming of the entire configuration file.
+    // Except for the trim section, all other data will be replaced.
+    // 0xfe 0xfe 0x00 0x00 instructs the command to reprogram all header codes
+    // and XVcode. If the old sections are not invalidated in OTP, they can
+    // affect the CRC calculation.
+
+    tBuf[0] = 0xfe;
+    tBuf[1] = 0xfe;
+    tBuf[2] = 0x00;
+    tBuf[3] = 0x00;
+
+    ret = co_await this->mfrFWcmd(MFRFwCmdOTPFileInvd, tBuf, NULL);
+    if (!ret)
+    {
+        error("Failed to program the VR - Invalidation of currect FW");
+        co_return ret;
+    }
+
+    co_await sdbusplus::async::sleep_for(ctx,
+                                         std::chrono::microseconds(500000));
+
+    for (int i = 0; i < configuration.sectCnt; i++)
+    {
+        struct configSect* sect = &configuration.section[i];
+        if (sect == NULL)
+        {
+            error(
+                "Failed to program the VR - unexpected NULL section in config");
+            ret = -1;
+            break;
+        }
+
+        if ((i <= 0) || (sect->type != configuration.section[i - 1].type))
+        {
+            tBuf[0] = PMBusSTLCml;
+            tBuf[1] = 0x1;
+            uint8_t tSize = 2;
+            uint8_t rSize = 0;
+            ret = co_await this->i2cInterface.sendReceive(tBuf, tSize, rBuf,
+                                                          rSize);
+            if (!ret)
+            {
+                error("Failed to program the VR on sendReceive {CMD}", "CMD",
+                      std::string("PMBusSTLCml"));
+                break;
+            }
+
+            tBuf[0] = sect->type;
+            tBuf[1] = 0x00;
+            tBuf[2] = 0x00;
+            tBuf[3] = 0x00;
+
+            ret = co_await this->mfrFWcmd(MFRFwCmdOTPFileInvd, tBuf, NULL);
+            if (!ret)
+            {
+                error("Failed to program VR on mfrFWCmd on {CMD}", "CMD",
+                      std::string("MFRFwCmdOTPFileInvd"));
+                break;
+            }
+
+            co_await sdbusplus::async::sleep_for(
+                ctx, std::chrono::microseconds(10000)); // Write delay
+
+            tBuf[0] = IFXMFRAHBAddr;
+            tBuf[1] = 4;
+            tBuf[2] = 0x00;
+            tBuf[3] = 0xe0;
+            tBuf[4] = 0x05;
+            tBuf[5] = 0x20;
+            tSize = 6;
+            rSize = 0;
+
+            ret = co_await this->i2cInterface.sendReceive(tBuf, tSize, rBuf,
+                                                          rSize);
+            if (!ret)
+            {
+                error("Failed to program VR on sendReceive on {CMD}", "CMD",
+                      std::string("IFXMFRAHBAddr"));
+                break;
+            }
+
+            co_await sdbusplus::async::sleep_for(
+                ctx, std::chrono::microseconds(10000));
+            size = 0;
+        }
+
+        // programm into scratchpad
+        for (int j = 0; j < sect->dataCnt; j++)
+        {
+            tBuf[0] = IFXMFRRegWrite;
+            tBuf[1] = 4;
+            uint8_t tSize = 6;
+            uint8_t rSize = 0;
+            memcpy(&tBuf[2], &sect->data[j], 4);
+            ret = co_await this->i2cInterface.sendReceive(tBuf, tSize, rBuf,
+                                                          rSize);
+            if (!ret)
+            {
+                error("Failed to program the VR on sendReceive {CMD}", "CMD",
+                      std::string("IFXMFRRegWrite"));
+                break;
+            }
+            co_await sdbusplus::async::sleep_for(
+                ctx, std::chrono::microseconds(10000));
+        }
+        if (ret)
+        {
+            break;
+        }
+
+        size += sect->dataCnt * 4;
+        if ((i + 1 >= configuration.sectCnt) ||
+            (sect->type != configuration.section[i + 1].type))
+        {
+            // Upload to scratchpad
+            std::memcpy(tBuf, &size, 2);
+            tBuf[2] = 0x00;
+            tBuf[3] = 0x00;
+            bool ret = co_await this->mfrFWcmd(MFRFwCmdOTPConfSTO, tBuf, NULL);
+            if (ret)
+            {
+                error("Failed to program the VR on mfrFWcmd {CMD}", "CMD",
+                      std::string("MFRFwCmdOTPConfSTO"));
+                break;
+            }
+
+            // wait for programming soak (2ms/byte, at least 200ms)
+            // ex: Config (604 bytes): (604 / 50) + 2 = 14 (1400 ms)
+            size = (size / 50) + 2;
+            for (int j = 0; j < size; j++)
+            {
+                co_await sdbusplus::async::sleep_for(
+                    ctx, std::chrono::microseconds(100000));
+            }
+
+            tBuf[0] = PMBusSTLCml;
+            uint8_t tSize = 1;
+            uint8_t rSize = 1;
+            ret = co_await this->i2cInterface.sendReceive(rBuf, tSize, tBuf,
+                                                          rSize);
+            if (!ret)
+            {
+                error("Failed to program VR on sendReceive {CMD}", "CMD",
+                      std::string("PMBusSTLCml"));
+                break;
+            }
+            if (rBuf[0] & 0x01)
+            {
+                error("Failed to program VR - response code invalid");
+                break;
+            }
+        }
+    }
+
+    if (!ret)
+    {
+        co_return false;
+    }
+
+    co_return true;
+}
+
+int XDPE1X2XX::lineSplit(char** dest, char* src, char* delim)
+{
+    char* s = strtok(src, delim);
+    int size = 0;
+    int maxSz = 5;
+
+    while (s)
+    {
+        *dest++ = s;
+        if ((++size) >= maxSz)
+        {
+            break;
+        }
+        s = strtok(NULL, delim);
+    }
+
+    return size;
+}
+
+int XDPE1X2XX::parseImage(const uint8_t* image, size_t image_size)
+{
+    size_t lenEndTag = strlen(DataEndTag);
+    size_t lenStartTag = strlen(DataStartTag);
+    size_t lenComment = strlen(DataComment);
+    size_t lenXV = strlen(DataXV);
+    size_t start = 0;
+    const int maxLineLength = 40;
+    char line[maxLineLength];
+    char* token = NULL;
+    bool isData = false;
+    char delim = ' ';
+    uint16_t offset;
+    uint8_t sectType = 0x0;
+    uint32_t dWord;
+    int dataCnt = 0;
+    int sectIndex = -1;
+
+    for (size_t i = 0; i < image_size; i++)
+    {
+        if (image[i] == '\n')
+        {
+            std::memcpy(line, image + start, i - start);
+            if (!strncmp(line, DataComment, lenComment))
+            {
+                token = line + lenComment;
+                if (!strncmp(token, DataXV, lenXV))
+                {
+                    debug("Parsing: {OBJ}", "OBJ",
+                          reinterpret_cast<const char*>(line));
+                }
+                start = i + 1;
+                continue;
+            }
+            if (!strncmp(line, DataEndTag, lenEndTag))
+            {
+                debug("Parsing: {OBJ}", "OBJ",
+                      reinterpret_cast<const char*>(line));
+                break;
+            }
+            else if (isData)
+            {
+                char* tokenList[8] = {0};
+                int tokenSize = lineSplit(tokenList, line, &delim);
+                if (tokenSize < 1)
+                {
+                    start = i + 1;
+                    continue;
+                }
+
+                offset = (uint16_t)strtol(tokenList[0], NULL, 16);
+                if (sectType == SectTrim && offset != 0x0)
+                {
+                    continue;
+                }
+
+                for (int i = 1; i < tokenSize; i++)
+                {
+                    dWord = (uint32_t)strtol(tokenList[i], NULL, 16);
+                    if ((offset == 0x0) && (i == 1))
+                    {
+                        sectType = (uint8_t)dWord;
+                        if (sectType == SectTrim)
+                        {
+                            break;
+                        }
+                        if ((++sectIndex) >= MaxSectCnt)
+                        {
+                            return -1;
+                        }
+
+                        configuration.section[sectIndex].type = sectType;
+                        configuration.sectCnt = sectIndex + 1;
+                        dataCnt = 0;
+                    }
+
+                    if (dataCnt >= MaxSectDataCnt)
+                    {
+                        return -1;
+                    }
+
+                    configuration.section[sectIndex].data[dataCnt++] = dWord;
+                    configuration.section[sectIndex].dataCnt = dataCnt;
+                    configuration.totalCnt++;
+                }
+            }
+            else
+            {
+                if ((token = strstr(line, AddressField)) != NULL)
+                {
+                    if ((token = strstr(token, "0x")) != NULL)
+                    {
+                        configuration.addr =
+                            (uint8_t)(strtoul(token, NULL, 16) << 1);
+                    }
+                }
+                else if ((token = strstr(line, ChecksumField)) != NULL)
+                {
+                    if ((token = strstr(token, "0x")) != NULL)
+                    {
+                        configuration.sumExp =
+                            (uint32_t)strtoul(token, NULL, 16);
+                    }
+                }
+                else if (!strncmp(line, DataStartTag, lenStartTag))
+                {
+                    isData = true;
+                    start = i + 1;
+                    continue;
+                }
+                else
+                {
+                    start = i + 1;
+                    continue;
+                }
+            }
+            start = i + 1;
+        }
+    }
+
+    return 0;
+}
+
+int XDPE1X2XX::checkImage()
+{
+    uint8_t i;
+    uint32_t crc;
+    uint32_t sum = 0;
+
+    for (i = 0; i < configuration.sectCnt; i++)
+    {
+        struct configSect* sect = &configuration.section[i];
+        if (sect == NULL)
+        {
+            error("Failed to check image - unexpected NULL section");
+            return -1;
+        }
+
+        crc = calcCRC32(&sect->data[2], 2);
+        if (crc != sect->data[2])
+        {
+            error("Failed to check image - first CRC value mismatch");
+            return -1;
+        }
+        sum += crc;
+
+        // check CRC of section data
+        crc = calcCRC32(&sect->data[3], sect->dataCnt - 4);
+        if (crc != sect->data[sect->dataCnt - 1])
+        {
+            error("Failed to check image - second CRC value mismatch");
+            return -1;
+        }
+        sum += crc;
+    }
+
+    if (sum != configuration.sumExp)
+    {
+        error("Failed to check image - third CRC value mismatch");
+        return -1;
+    }
+
+    return 0;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> XDPE1X2XX::verifyImage(const uint8_t* image,
+                                                    size_t imageSize)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    if (parseImage(image, imageSize) < 0)
+    {
+        error("Failed to update firmware on parsing Image");
+        co_return false;
+    }
+
+    if (checkImage() < 0)
+    {
+        error("Failed to update firmware on check image");
+        co_return false;
+    }
+
+    co_return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> XDPE1X2XX::updateFirmware(bool force)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    // NOLINTBEGIN(clang-analyzer-core.uninitialized.Branch)
+    bool ret = co_await program(force);
+    // NOLINTEND(clang-analyzer-core.uninitialized.Branch)
+    if (!ret)
+    {
+        error("Failed to update firmware on program");
+        co_return false;
+    }
+
+    // Reset the configuration
+    configuration.addr = 0;
+    configuration.totalCnt = 0;
+    configuration.sumExp = 0;
+    configuration.sectCnt = 0;
+    for (int i = 0; i <= MaxSectCnt - 1; i++)
+    {
+        configuration.section[i].type = 0;
+        configuration.section[i].dataCnt = 0;
+        for (int j = 0; j <= MaxSectDataCnt; j++)
+        {
+            configuration.section[i].data[j] = 0;
+        }
+    }
+
+    co_return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> XDPE1X2XX::reset()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    bool ret = co_await mfrFWcmd(MFRFwCmdReset, NULL, NULL);
+    if (!ret)
+    {
+        error("Failed to reset the VR");
+        co_return false;
+    }
+
+    co_await sdbusplus::async::sleep_for(
+        ctx, std::chrono::microseconds(VRResetDelay));
+
+    co_return true;
+}
+
+uint32_t XDPE1X2XX::calcCRC32(const uint32_t* data, int len)
+{
+    if (data == NULL)
+    {
+        return 0;
+    }
+
+    uint32_t crc = 0xFFFFFFFF;
+    for (int i = 0; i < len; i++)
+    {
+        crc ^= data[i];
+
+        for (int b = 0; b < 32; b++)
+        {
+            if (crc & 0x1)
+            {
+                crc = (crc >> 1) ^ CRC32Poly; // lsb-first
+            }
+            else
+            {
+                crc >>= 1;
+            }
+        }
+    }
+
+    return ~crc;
+}
+
+bool XDPE1X2XX::forcedUpdateAllowed()
+{
+    return true;
+}
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/xdpe1x2xx/xdpe1x2xx.hpp b/i2c-vr/xdpe1x2xx/xdpe1x2xx.hpp
new file mode 100644
index 0000000..53fdcf1
--- /dev/null
+++ b/i2c-vr/xdpe1x2xx/xdpe1x2xx.hpp
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "common/include/i2c/i2c.hpp"
+#include "i2c-vr/vr.hpp"
+
+#include <sdbusplus/async.hpp>
+
+#include <cstdint>
+
+namespace phosphor::software::VR
+{
+
+class XDPE1X2XX : public VoltageRegulator
+{
+  public:
+    XDPE1X2XX(sdbusplus::async::context& ctx, uint16_t bus, uint16_t 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> reset() final;
+
+    sdbusplus::async::task<bool> getCRC(uint32_t* checksum) final;
+    bool forcedUpdateAllowed() final;
+
+  private:
+    static const int MaxSectCnt = 16;
+    static const int MaxSectDataCnt = 200;
+
+    struct configSect
+    {
+        uint8_t type;
+        uint16_t dataCnt;
+        uint32_t data[MaxSectDataCnt];
+    };
+
+    struct xdpe1x2xxConfig
+    {
+        uint8_t addr;
+        uint16_t totalCnt;
+        uint32_t sumExp;
+        uint8_t sectCnt;
+        struct configSect section[MaxSectCnt];
+    };
+
+    sdbusplus::async::task<bool> getDeviceId(uint8_t* deviceId);
+    sdbusplus::async::task<bool> mfrFWcmd(uint8_t cmd, uint8_t* data,
+                                          uint8_t* resp);
+    sdbusplus::async::task<bool> getRemainingWrites(uint8_t* remain);
+    sdbusplus::async::task<bool> program(bool force);
+
+    int parseImage(const uint8_t* image, size_t imageSize);
+    int checkImage();
+
+    static uint32_t calcCRC32(const uint32_t* data, int len);
+    static int getConfigSize(uint8_t deviceId, uint8_t revision);
+    static int lineSplit(char** dst, char* src, char* delim);
+
+    phosphor::i2c::I2C i2cInterface;
+
+    uint8_t Id;
+    struct xdpe1x2xxConfig configuration;
+};
+
+} // namespace phosphor::software::VR
diff --git a/i2c-vr/xyz.openbmc_project.Software.I2CVR.service b/i2c-vr/xyz.openbmc_project.Software.I2CVR.service
new file mode 100644
index 0000000..6d129ad
--- /dev/null
+++ b/i2c-vr/xyz.openbmc_project.Software.I2CVR.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Voltage Regulator Software Update Daemon
+After=xyz.openbmc_project.ObjectMapper
+After=xyz.openbmc_project.EntityManager.service
+
+[Service]
+Restart=always
+Type=exec
+RemainAfterExit=no
+ExecStart=/usr/bin/phosphor-i2cvr-software-update
+
+[Install]
+WantedBy=multi-user.target
diff --git a/meson.build b/meson.build
index 5b8f78c..9c7c919 100644
--- a/meson.build
+++ b/meson.build
@@ -68,7 +68,9 @@
 
 common_include = include_directories('.')
 
-common_build = build_tests.allowed() or get_option('bios-software-update').enabled()
+common_build = build_tests.allowed() or get_option('bios-software-update').enabled() or get_option(
+    'i2cvr-software-update',
+).enabled()
 
 if common_build
     libpldm_dep = dependency('libpldm')
@@ -86,6 +88,10 @@
     subdir('bios')
 endif
 
+if get_option('i2cvr-software-update').enabled()
+    subdir('i2c-vr')
+endif
+
 if build_tests.allowed()
     subdir('test')
 endif
diff --git a/meson.options b/meson.options
index b88eac6..157c762 100644
--- a/meson.options
+++ b/meson.options
@@ -50,6 +50,13 @@
 )
 
 option(
+    'i2cvr-software-update',
+    type: 'feature',
+    value: 'enabled',
+    description: 'Enable update of i2c voltage regulators',
+)
+
+option(
     'side-switch-on-boot',
     type: 'feature',
     value: 'enabled',