cpld: add update support for Lattice xo5

Enable firmware update capability for Lattice xo5 devices, allowing
the update flow to recognize and handle this device type.

Test on Santabarbara:
```
1. Check firmware
curl -k -u root:0penBmc -X GET
https://10.10.15.214/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_MB_CPLD_6213
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_MB_CPLD_6213",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "Santabarbara_MB_CPLD_6213",
  "Name": "Software Inventory",
  "Status": {
    "Health": "OK",
    "HealthRollup": "OK",
    "State": "Enabled"
  },
  "Updateable": true,
  "Version": "70000003"
}

2. Trigger Update
curl -k -u root:0penBmc \
  -H "Content-Type:multipart/form-data" \
  -X POST \
  -F UpdateParameters="{\"Targets\":[\"${targetpath}\"], \
  \"@Redfish.OperationApplyTime\":\"Immediate\"};type=application/json" \
  -F "UpdateFile=@${fwpath};type=application/octet-stream" \
  https://${bmc}/redfish/v1/UpdateService/update-multipart
{
  "@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-14T02:26:00+00:00",
  "TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/0",
  "TaskState": "Running",
  "TaskStatus": "OK"
}

3. Check task
curl -u root:0penBmc -k -X GET https://${bmc}/redfish/v1/TaskService/Tasks/0
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task",
  "EndTime": "2025-08-14T02:28:32+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 1 percent complete.",
      "MessageArgs": [
        "0",
        "1"
      ],
      "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 90 percent complete.",
      "MessageArgs": [
        "0",
        "90"
      ],
      "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-14T02:26:00+00:00",
  "TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/0",
  "TaskState": "Completed",
  "TaskStatus": "OK"
}

4. Check firmware again
curl -k -u root:0penBmc -X GET
https://10.10.15.214/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_MB_CPLD_9204
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Santabarbara_MB_CPLD_9204",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "Santabarbara_MB_CPLD_9204",
  "Name": "Software Inventory",
  "Status": {
    "Health": "OK",
    "HealthRollup": "OK",
    "State": "Enabled"
  },
  "Updateable": true,
  "Version": "00000004"
}
```

Change-Id: Id0aef0d105138538851f4c6d3a5496ec8b724eea
Signed-off-by: Daniel Hsu <Daniel-Hsu@quantatw.com>
diff --git a/cpld/lattice/lattice_base_cpld.hpp b/cpld/lattice/lattice_base_cpld.hpp
index cb45c39..b51770f 100644
--- a/cpld/lattice/lattice_base_cpld.hpp
+++ b/cpld/lattice/lattice_base_cpld.hpp
@@ -19,6 +19,7 @@
     LCMXO3LF_4300C,
     LCMXO3D_4300,
     LCMXO3D_9400,
+    LFMXO5_25,
     UNSUPPORTED = -1,
 };
 
@@ -37,6 +38,7 @@
         {latticeChip::LCMXO3LF_4300C, "LCMXO3LF_4300C"},
         {latticeChip::LCMXO3D_4300, "LCMXO3D_4300"},
         {latticeChip::LCMXO3D_9400, "LCMXO3D_9400"},
+        {latticeChip::LFMXO5_25, "LFMXO5_25"},
     };
     auto chipString = chipStringMap.at(chip);
     if (chipStringMap.find(chip) == chipStringMap.end())
@@ -63,6 +65,7 @@
 {
     XO2,
     XO3,
+    XO5,
 };
 
 struct cpldInfo
@@ -82,6 +85,7 @@
      {latticeChipFamily::XO3, {0x01, 0x2e, 0x20, 0x43}}},
     {latticeChip::LCMXO3D_9400,
      {latticeChipFamily::XO3, {0x21, 0x2e, 0x30, 0x43}}},
+    {latticeChip::LFMXO5_25, {latticeChipFamily::XO5, {}}},
 };
 
 struct cpldI2cInfo
diff --git a/cpld/lattice/lattice_cpld_factory.cpp b/cpld/lattice/lattice_cpld_factory.cpp
index ab0cb85..5f5fb58 100644
--- a/cpld/lattice/lattice_cpld_factory.cpp
+++ b/cpld/lattice/lattice_cpld_factory.cpp
@@ -1,6 +1,7 @@
 #include "lattice_cpld_factory.hpp"
 
 #include "lattice_xo3_cpld.hpp"
+#include "lattice_xo5_cpld.hpp"
 
 #include <phosphor-logging/lg2.hpp>
 
@@ -27,6 +28,10 @@
             return std::make_unique<LatticeXO3CPLD>(
                 CPLDInterface::ctx, CPLDInterface::bus, CPLDInterface::address,
                 chipModelStr, "CFG0", false);
+        case latticeChipFamily::XO5:
+            return std::make_unique<LatticeXO5CPLD>(
+                CPLDInterface::ctx, CPLDInterface::bus, CPLDInterface::address,
+                chipModelStr, "CFG0", false);
         default:
             lg2::error("Unsupported Lattice CPLD chip family: {CHIPMODEL}",
                        "CHIPMODEL", chipModelStr);
diff --git a/cpld/lattice/lattice_xo5_cpld.cpp b/cpld/lattice/lattice_xo5_cpld.cpp
new file mode 100644
index 0000000..75e0d48
--- /dev/null
+++ b/cpld/lattice/lattice_xo5_cpld.cpp
@@ -0,0 +1,377 @@
+#include "lattice_xo5_cpld.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+namespace phosphor::software::cpld
+{
+
+constexpr std::chrono::milliseconds ReadyPollInterval(10);
+constexpr std::chrono::milliseconds ReadyTimeout(1000);
+
+enum class xo5Cmd : uint8_t
+{
+    sectorErase = 0xd8,
+    pageProgram = 0x02,
+    pageRead = 0x0b,
+    readUsercode = 0xc0
+};
+
+enum class xo5Status : uint8_t
+{
+    ready = 0x00,
+    notReady = 0xff
+};
+
+struct xo5Cfg
+{
+    static constexpr size_t pageSize = 256;
+    static constexpr size_t pagesPerBlock = 256;
+    static constexpr size_t blocksPerCfg = 11;
+};
+
+static bool getStartBlock(uint8_t cfg, uint8_t& startBlock)
+{
+    static constexpr std::array<uint8_t, 3> cfgStartBlocks = {0x01, 0x10, 0x1F};
+
+    if (cfg >= cfgStartBlocks.size())
+    {
+        return false;
+    }
+
+    startBlock = cfgStartBlocks[cfg];
+    return true;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::waitUntilReady(
+    std::chrono::milliseconds timeout)
+{
+    const auto endTime = std::chrono::steady_clock::now() + timeout;
+
+    auto readDummy = [this]() -> sdbusplus::async::task<bool> {
+        std::vector<uint8_t> request = {};
+        std::vector<uint8_t> response = {0xff};
+        if (!i2cInterface.sendReceive(request, response))
+        {
+            lg2::error("Failed to read.");
+            co_return false;
+        }
+        if (response.at(0) == static_cast<uint8_t>(xo5Status::ready))
+        {
+            co_return true;
+        }
+        co_return false;
+    };
+
+    while (std::chrono::steady_clock::now() < endTime)
+    {
+        if (co_await readDummy())
+        {
+            co_return true;
+        }
+        co_await sdbusplus::async::sleep_for(ctx, ReadyPollInterval);
+    }
+
+    lg2::error("Timeout waiting for device ready");
+    co_return false;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::eraseCfg()
+{
+    auto cfgIndex = (target == "CFG0") ? 0 : 1;
+    uint8_t startBlock;
+    if (!getStartBlock(cfgIndex, startBlock))
+    {
+        lg2::error("Error: invalid cfg index.");
+        co_return false;
+    }
+    const auto endBlock = startBlock + xo5Cfg::blocksPerCfg;
+
+    auto eraseBlock = [this](uint8_t block) -> sdbusplus::async::task<bool> {
+        std::vector<uint8_t> request;
+        std::vector<uint8_t> response = {};
+        request.reserve(4);
+        request.push_back(static_cast<uint8_t>(xo5Cmd::sectorErase));
+        request.push_back(block);
+        request.push_back(0x0);
+        request.push_back(0x0);
+        if (!i2cInterface.sendReceive(request, response))
+        {
+            lg2::error("Failed to erase block");
+            co_return false;
+        }
+        co_return true;
+    };
+
+    for (size_t block = startBlock; block < endBlock; ++block)
+    {
+        if (!(co_await eraseBlock(block)))
+        {
+            lg2::error("Erase failed: Block {BLOCK}", "BLOCK", block);
+            co_return false;
+        }
+        if (!(co_await waitUntilReady(ReadyTimeout)))
+        {
+            lg2::error("Failed to wait until ready");
+            co_return false;
+        }
+    }
+    co_return true;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::programPage(
+    uint8_t block, uint8_t page, const std::vector<uint8_t>& data)
+{
+    std::vector<uint8_t> request;
+    std::vector<uint8_t> response = {};
+    request.reserve(4 + data.size());
+    request.push_back(static_cast<uint8_t>(xo5Cmd::pageProgram));
+    request.push_back(block);
+    request.push_back(page);
+    request.push_back(0x0);
+    request.insert(request.end(), data.begin(), data.end());
+
+    if (!i2cInterface.sendReceive(request, response))
+    {
+        co_return false;
+    }
+    co_return true;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::programCfg()
+{
+    using diff_t = std::vector<uint8_t>::difference_type;
+
+    auto cfgIndex = (target == "CFG0") ? 0 : 1;
+    uint8_t startBlock;
+    if (!getStartBlock(cfgIndex, startBlock))
+    {
+        lg2::error("Error: invalid cfg index.");
+        co_return false;
+    }
+    const auto endBlock = startBlock + xo5Cfg::blocksPerCfg;
+    const auto& cfgData = fwInfo.cfgData;
+    const auto totalBytes = cfgData.size();
+    size_t bytesWritten = 0;
+
+    for (size_t block = startBlock; block < endBlock; ++block)
+    {
+        for (size_t page = 0; page < xo5Cfg::pagesPerBlock; ++page)
+        {
+            if (bytesWritten >= totalBytes)
+            {
+                co_return true;
+            }
+
+            auto offset = static_cast<diff_t>(bytesWritten);
+            auto remaining = static_cast<diff_t>(totalBytes - bytesWritten);
+            const auto chunkSize =
+                std::min(static_cast<diff_t>(xo5Cfg::pageSize), remaining);
+            std::vector<uint8_t> chunk(
+                std::next(cfgData.begin(), offset),
+                std::next(cfgData.begin(), offset + chunkSize));
+
+            auto success = false;
+            success |= co_await programPage(block, page, chunk);
+            co_await sdbusplus::async::sleep_for(ctx, ReadyPollInterval);
+            success |= co_await waitUntilReady(ReadyTimeout);
+            if (!success)
+            {
+                lg2::error("Failed to program block {BLOCK} page {PAGE}",
+                           "BLOCK", block, "PAGE", page);
+                co_return false;
+            }
+            bytesWritten += chunkSize;
+        }
+    }
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::readPage(
+    uint8_t block, uint8_t page, std::vector<uint8_t>& data)
+{
+    if (data.empty())
+    {
+        lg2::error("Error: data vector is empty.");
+        co_return false;
+    }
+    std::vector<uint8_t> request = {};
+    std::vector<uint8_t> response = {};
+    request.reserve(4);
+    request.push_back(static_cast<uint8_t>(xo5Cmd::pageRead));
+    request.push_back(block);
+    request.push_back(page);
+    request.push_back(0x0);
+
+    if (!i2cInterface.sendReceive(request, response))
+    {
+        co_return false;
+    }
+    lg2::debug("Read page {BLOCK} {PAGE} succeeded", "BLOCK", block, "PAGE",
+               page);
+    request.clear();
+
+    std::this_thread::sleep_for(std::chrono::milliseconds(1));
+
+    if (!(co_await waitUntilReady(ReadyTimeout)))
+    {
+        co_return false;
+    }
+
+    if (!i2cInterface.sendReceive(request, data))
+    {
+        co_return false;
+    }
+
+    co_return data[0] == static_cast<uint8_t>(xo5Status::ready);
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::verifyCfg()
+{
+    using diff_t = std::vector<uint8_t>::difference_type;
+
+    auto cfgIndex = (target == "CFG0") ? 0 : 1;
+    uint8_t startBlock;
+    if (!getStartBlock(cfgIndex, startBlock))
+    {
+        lg2::error("Error: invalid cfg index.");
+        co_return false;
+    }
+    const auto endBlock = startBlock + xo5Cfg::blocksPerCfg;
+    const auto& cfgData = fwInfo.cfgData;
+    const auto totalBytes = cfgData.size();
+    uint8_t readBuffer[1 + xo5Cfg::pageSize];
+    size_t bytesVerified = 0;
+
+    for (size_t block = startBlock; block < endBlock; ++block)
+    {
+        for (size_t page = 0; page < xo5Cfg::pagesPerBlock; ++page)
+        {
+            if (bytesVerified >= totalBytes)
+            {
+                co_return true;
+            }
+
+            auto offset = static_cast<diff_t>(bytesVerified);
+            auto remaining = static_cast<diff_t>(totalBytes - bytesVerified);
+            const auto chunkSize =
+                std::min(static_cast<diff_t>(xo5Cfg::pageSize), remaining);
+
+            std::vector<uint8_t> expected(
+                std::next(cfgData.begin(), offset),
+                std::next(cfgData.begin(), offset + chunkSize));
+
+            std::vector<uint8_t> chunk;
+            {
+                std::vector<uint8_t> readVec(readBuffer,
+                                             readBuffer + 1 + chunkSize);
+
+                if (co_await readPage(block, page, readVec))
+                {
+                    chunk.assign(readVec.begin() + 1, readVec.end());
+                }
+                else
+                {
+                    chunk.clear();
+                }
+            }
+
+            if (chunk.empty())
+            {
+                lg2::error("Failed to read Block {BLOCK} Page {PAGE}", "BLOCK",
+                           block, "PAGE", page);
+                co_return false;
+            }
+            if (!std::equal(chunk.begin(), chunk.end(), expected.begin()))
+            {
+                lg2::error("VERIFY FAILED: Block {BLOCK} Page {PAGE}", "BLOCK",
+                           block, "PAGE", page);
+                co_return false;
+            }
+
+            bytesVerified += chunkSize;
+        }
+    }
+    co_return true;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::readUserCode(uint32_t& userCode)
+{
+    constexpr size_t resSize = 5;
+    std::vector<uint8_t> request = {commandReadFwVersion, 0x0, 0x0, 0x0};
+    std::vector<uint8_t> response(resSize, 0);
+
+    if (!i2cInterface.sendReceive(request, response))
+    {
+        lg2::error("Failed to send read user code request.");
+        co_return false;
+    }
+
+    userCode |= response[4] << 24;
+    userCode |= response[3] << 16;
+    userCode |= response[2] << 8;
+    userCode |= response[1];
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::prepareUpdate(const uint8_t* image,
+                                                           size_t imageSize)
+{
+    if (target.empty())
+    {
+        target = "CFG0";
+    }
+    else if (target != "CFG0" && target != "CFG1")
+    {
+        lg2::error("Error: unknown target.");
+        co_return false;
+    }
+
+    if (!jedFileParser(image, imageSize))
+    {
+        lg2::error("JED file parsing failed");
+        co_return false;
+    }
+    lg2::debug("JED file parsing success");
+
+    if (!(co_await waitUntilReady(ReadyTimeout)))
+    {
+        lg2::error("Error: Device not ready.");
+        co_return false;
+    }
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::doUpdate()
+{
+    lg2::debug("Erasing {TARGET}...", "TARGET", target);
+    if (!(co_await eraseCfg()))
+    {
+        lg2::error("Erase cfg data failed.");
+        co_return false;
+    }
+
+    lg2::debug("Programming {TARGET}...", "TARGET", target);
+    if (!(co_await programCfg()))
+    {
+        lg2::error("Program cfg data failed.");
+        co_return false;
+    }
+
+    co_return true;
+}
+
+sdbusplus::async::task<bool> LatticeXO5CPLD::finishUpdate()
+{
+    lg2::debug("Verifying {TARGET}...", "TARGET", target);
+    if (!(co_await verifyCfg()))
+    {
+        lg2::error("Verify cfg data failed.");
+        co_return false;
+    }
+    co_return true;
+}
+
+} // namespace phosphor::software::cpld
diff --git a/cpld/lattice/lattice_xo5_cpld.hpp b/cpld/lattice/lattice_xo5_cpld.hpp
new file mode 100644
index 0000000..efc301f
--- /dev/null
+++ b/cpld/lattice/lattice_xo5_cpld.hpp
@@ -0,0 +1,39 @@
+#include "lattice_base_cpld.hpp"
+
+namespace phosphor::software::cpld
+{
+
+class LatticeXO5CPLD : public LatticeBaseCPLD
+{
+  public:
+    LatticeXO5CPLD(sdbusplus::async::context& ctx, const uint16_t bus,
+                   const uint8_t address, const std::string& chip,
+                   const std::string& target, const bool debugMode) :
+        LatticeBaseCPLD(ctx, bus, address, chip, target, debugMode)
+    {}
+    ~LatticeXO5CPLD() override = default;
+    LatticeXO5CPLD(const LatticeXO5CPLD&) = delete;
+    LatticeXO5CPLD& operator=(const LatticeXO5CPLD&) = delete;
+    LatticeXO5CPLD(LatticeXO5CPLD&&) noexcept = delete;
+    LatticeXO5CPLD& operator=(LatticeXO5CPLD&&) noexcept = delete;
+
+  protected:
+    sdbusplus::async::task<bool> prepareUpdate(const uint8_t* image,
+                                               size_t imageSize) override;
+    sdbusplus::async::task<bool> doUpdate() override;
+    sdbusplus::async::task<bool> finishUpdate() override;
+    sdbusplus::async::task<bool> readUserCode(uint32_t& userCode) override;
+
+  private:
+    sdbusplus::async::task<bool> waitUntilReady(
+        std::chrono::milliseconds timeout);
+    sdbusplus::async::task<bool> eraseCfg();
+    sdbusplus::async::task<bool> programCfg();
+    sdbusplus::async::task<bool> programPage(uint8_t block, uint8_t page,
+                                             const std::vector<uint8_t>& data);
+    sdbusplus::async::task<bool> verifyCfg();
+    sdbusplus::async::task<bool> readPage(uint8_t block, uint8_t page,
+                                          std::vector<uint8_t>& data);
+};
+
+} // namespace phosphor::software::cpld
diff --git a/cpld/meson.build b/cpld/meson.build
index 005bb67..575775b 100644
--- a/cpld/meson.build
+++ b/cpld/meson.build
@@ -4,9 +4,10 @@
     'lattice/lattice_base_cpld.cpp',
     'lattice/lattice_cpld_factory.cpp',
     'lattice/lattice_xo3_cpld.cpp',
+    'lattice/lattice_xo5_cpld.cpp',
 )
 
-executable(
+exe = executable(
     'phosphor-cpld-software-update',
     cpld_src,
     cpld_vendor_src,