I2CInterface: Add process call methods

Add C++ methods to perform the following SMBus commands:
* Process Call
* Block Write-Block Read Process Call

For Block Write-Block Read Process Call, implement support for writes up
to 255 bytes. SMBus 2.0 supported a maximum of 32 bytes, and SMBus 3.0
supports a maximum of 255 bytes. The current Linux SMBus function only
supports 32 byte writes. Provide an alternate implementation using the
lower level I2C_RDWR ioctl() to support up to 255 bytes.

Tested:
* Verified Process Call worked correctly
* Verified Block Write-Block Read Process Call worked correctly
  * When using SMBus function
  * When using I2C_RDWR ioctl()
* Tested error cases
* See complete test plan at
  https://gist.github.com/smccarney/96eda4c7c11fe4f89e4491c768f76047

Change-Id: Icc1ba840741b1e26a50fe32bad8b2181a01dbb24
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/tools/i2c/i2c.cpp b/tools/i2c/i2c.cpp
index f361be5..28cedf6 100644
--- a/tools/i2c/i2c.cpp
+++ b/tools/i2c/i2c.cpp
@@ -7,6 +7,8 @@
 
 #include <cassert>
 #include <cerrno>
+#include <cstring>
+#include <format>
 
 extern "C"
 {
@@ -18,6 +20,10 @@
 namespace i2c
 {
 
+// Maximum number of data bytes in a block read/block write/block process call
+// in SMBus 3.0. The maximum was 32 data bytes in SMBus 2.0 and earlier.
+constexpr uint8_t I2C_SMBUS3_BLOCK_MAX = 255;
+
 unsigned long I2CDevice::getFuncs()
 {
     // If functionality has not been cached
@@ -32,6 +38,7 @@
 
         if (ret < 0)
         {
+            cachedFuncs = NO_FUNCS;
             throw I2CException("Failed to get funcs", busStr, devAddr, errno);
         }
     }
@@ -124,12 +131,120 @@
                                    busStr, devAddr);
             }
             break;
+        case I2C_SMBUS_PROC_CALL:
+            if (!(funcs & I2C_FUNC_SMBUS_PROC_CALL))
+            {
+                throw I2CException("Missing I2C_FUNC_SMBUS_PROC_CALL", busStr,
+                                   devAddr);
+            }
+            break;
+        case I2C_SMBUS_BLOCK_PROC_CALL:
+            if (!(funcs & I2C_FUNC_SMBUS_BLOCK_PROC_CALL))
+            {
+                throw I2CException("Missing I2C_FUNC_SMBUS_BLOCK_PROC_CALL",
+                                   busStr, devAddr);
+            }
+            break;
         default:
             fprintf(stderr, "Unexpected write size type: %d\n", type);
             assert(false);
     }
 }
 
+void I2CDevice::processCallSMBus(uint8_t addr, uint8_t writeSize,
+                                 const uint8_t* writeData, uint8_t& readSize,
+                                 uint8_t* readData)
+{
+    int ret = 0, retries = 0;
+    uint8_t buffer[I2C_SMBUS_BLOCK_MAX];
+    do
+    {
+        // Copy write data to buffer. Buffer is also used by SMBus function to
+        // store the data read from the device.
+        std::memcpy(buffer, writeData, writeSize);
+        ret = i2c_smbus_block_process_call(fd, addr, writeSize, buffer);
+    } while ((ret < 0) && (++retries <= maxRetries));
+
+    if (ret < 0)
+    {
+        throw I2CException("Failed to execute block process call", busStr,
+                           devAddr, errno);
+    }
+
+    readSize = static_cast<uint8_t>(ret);
+    std::memcpy(readData, buffer, readSize);
+}
+
+void I2CDevice::processCallI2C(uint8_t addr, uint8_t writeSize,
+                               const uint8_t* writeData, uint8_t& readSize,
+                               uint8_t* readData)
+{
+    // Buffer for block write. Linux supports SMBus 3.0 max size for block
+    // write. Buffer will contain register address, byte count, and data bytes.
+    constexpr uint16_t writeBufferSize = I2C_SMBUS3_BLOCK_MAX + 2;
+    uint8_t writeBuffer[writeBufferSize];
+
+    // Buffer for block read. Linux supports smaller SMBus 2.0 max size for
+    // block read. After ioctl() buffer will contain byte count and data bytes.
+    constexpr uint16_t readBufferSize = I2C_SMBUS_BLOCK_MAX + 1;
+    uint8_t readBuffer[readBufferSize];
+
+    // i2c_msg and i2c_rdwr_ioctl_data structs required for ioctl()
+    constexpr unsigned int numMessages = 2;
+    struct i2c_msg messages[numMessages];
+    struct i2c_rdwr_ioctl_data readWriteData;
+    readWriteData.msgs = messages;
+    readWriteData.nmsgs = numMessages;
+
+    int ret = 0, retries = 0;
+    do
+    {
+        // Initialize write buffer with reg addr, byte count, and data bytes
+        writeBuffer[0] = addr;
+        writeBuffer[1] = writeSize;
+        std::memcpy(&(writeBuffer[2]), writeData, writeSize);
+
+        // Initialize first i2c_msg to perform block write
+        messages[0].addr = devAddr;
+        messages[0].flags = 0;
+        messages[0].len = writeSize + 2; // 2 == reg addr + byte count
+        messages[0].buf = writeBuffer;
+
+        // Initialize read buffer. Set first byte to number of "extra bytes"
+        // that will be read in addition to data bytes. Set to 1 since only
+        // extra byte is the byte count.
+        readBuffer[0] = 1;
+
+        // Initialize second i2c_msg to perform block read. Linux requires the
+        // len field to be set to the buffer size.
+        messages[1].addr = devAddr;
+        messages[1].flags = I2C_M_RD | I2C_M_RECV_LEN;
+        messages[1].len = readBufferSize;
+        messages[1].buf = readBuffer;
+
+        // Call ioctl() to send the I2C messages
+        ret = ioctl(fd, I2C_RDWR, &readWriteData);
+    } while ((ret != numMessages) && (++retries <= maxRetries));
+
+    if (ret < 0)
+    {
+        throw I2CException("Failed to execute I2C block process call", busStr,
+                           devAddr, errno);
+    }
+    else if (ret != numMessages)
+    {
+        throw I2CException(
+            std::format(
+                "Failed to execute I2C block process call: {} messages sent",
+                ret),
+            busStr, devAddr);
+    }
+
+    // Read size is in first byte; copy remaining data bytes to readData param
+    readSize = readBuffer[0];
+    std::memcpy(readData, &(readBuffer[1]), readSize);
+}
+
 void I2CDevice::open()
 {
     if (isOpen())
@@ -358,6 +473,55 @@
     }
 }
 
+void I2CDevice::processCall(uint8_t addr, uint16_t writeData,
+                            uint16_t& readData)
+{
+    checkIsOpen();
+    checkWriteFuncs(I2C_SMBUS_PROC_CALL);
+
+    int ret = 0, retries = 0;
+    do
+    {
+        ret = i2c_smbus_process_call(fd, addr, writeData);
+    } while ((ret < 0) && (++retries <= maxRetries));
+
+    if (ret < 0)
+    {
+        throw I2CException("Failed to execute process call", busStr, devAddr,
+                           errno);
+    }
+
+    readData = static_cast<uint16_t>(ret);
+}
+
+void I2CDevice::processCall(uint8_t addr, uint8_t writeSize,
+                            const uint8_t* writeData, uint8_t& readSize,
+                            uint8_t* readData)
+{
+    checkIsOpen();
+    unsigned long funcs = getFuncs();
+
+    if ((funcs & I2C_FUNC_SMBUS_BLOCK_PROC_CALL) &&
+        (writeSize <= I2C_SMBUS_BLOCK_MAX))
+    {
+        // Use standard SMBus function which supports smaller SMBus 2.0 maximum
+        processCallSMBus(addr, writeSize, writeData, readSize, readData);
+    }
+    else if (funcs & I2C_FUNC_I2C)
+    {
+        // Use lower level I2C ioctl which supports larger SMBus 3.0 maximum
+        processCallI2C(addr, writeSize, writeData, readSize, readData);
+    }
+    else
+    {
+        throw I2CException(
+            std::format(
+                "Block process call unsupported: writeSize={:d}, funcs={}",
+                writeSize, funcs),
+            busStr, devAddr);
+    }
+}
+
 std::unique_ptr<I2CInterface> I2CDevice::create(
     uint8_t busId, uint8_t devAddr, InitialState initialState, int maxRetries)
 {
diff --git a/tools/i2c/i2c.hpp b/tools/i2c/i2c.hpp
index 5bd049f..c0c71d1 100644
--- a/tools/i2c/i2c.hpp
+++ b/tools/i2c/i2c.hpp
@@ -112,6 +112,55 @@
      */
     void checkWriteFuncs(int type);
 
+    /** @brief SMBus Block Write-Block Read Process Call using SMBus function
+     *
+     * In SMBus 2.0 the maximum write size + read size is <= 32 bytes.
+     * In SMBus 3.0 the maximum write size + read size is <= 255 bytes.
+     * The Linux SMBus function currently only supports the SMBus 2.0 maximum.
+     *
+     * @param[in] addr - The register address of the i2c device
+     * @param[in] writeSize - The size of data to write. Write size + read size
+     *                        must be <= 32 bytes.
+     * @param[in] writeData - The data to write to the i2c device
+     * @param[out] readSize - The size of data read from i2c device. Write size
+     *                        + read size must be <= 32 bytes.
+     * @param[out] readData - Pointer to buffer to hold the data read from the
+     *                        i2c device. Must be large enough to hold the data
+     *                        returned by the device (max is 32 bytes).
+     *
+     * @throw I2CException on error
+     */
+    void processCallSMBus(uint8_t addr, uint8_t writeSize,
+                          const uint8_t* writeData, uint8_t& readSize,
+                          uint8_t* readData);
+
+    /** @brief SMBus Block Write-Block Read Process Call using I2C messages
+     *
+     * This method supports block writes of more than 32 bytes.  It can also be
+     * used with I2C adapters that do not support the block process call
+     * protocol but do support I2C-level commands.
+     *
+     * This method implements the block process call using the lower level
+     * I2C_RDWR ioctl to send I2C messages.  Using this ioctl allows for writes
+     * up to 255 bytes.  The write size + read size must be <= 255 bytes.
+     *
+     * @param[in] addr - The register address of the i2c device
+     * @param[in] writeSize - The size of data to write. Write size + read size
+     *                        must be <= 255 bytes.
+     * @param[in] writeData - The data to write to the i2c device
+     * @param[out] readSize - The size of data read from i2c device. Max read
+     *                        size is 32 bytes, and write size + read size must
+     *                        be <= 255 bytes.
+     * @param[out] readData - Pointer to buffer to hold the data read from the
+     *                        i2c device. Must be large enough to hold the data
+     *                        returned by the device (max is 32 bytes).
+     *
+     * @throw I2CException on error
+     */
+    void processCallI2C(uint8_t addr, uint8_t writeSize,
+                        const uint8_t* writeData, uint8_t& readSize,
+                        uint8_t* readData);
+
   public:
     /** @copydoc I2CInterface::~I2CInterface() */
     ~I2CDevice()
@@ -161,6 +210,15 @@
     void write(uint8_t addr, uint8_t size, const uint8_t* data,
                Mode mode = Mode::SMBUS) override;
 
+    /** @copydoc I2CInterface::processCall(uint8_t,uint16_t,uint16_t&) */
+    void processCall(uint8_t addr, uint16_t writeData,
+                     uint16_t& readData) override;
+
+    /** @copydoc I2CInterface::processCall(uint8_t,uint8_t,const
+     *                                     uint8_t*,uint8_t&,uint8_t*) */
+    void processCall(uint8_t addr, uint8_t writeSize, const uint8_t* writeData,
+                     uint8_t& readSize, uint8_t* readData) override;
+
     /** @brief Create an I2CInterface instance
      *
      * Automatically opens the I2CInterface if initialState is OPEN.
diff --git a/tools/i2c/i2c_interface.hpp b/tools/i2c/i2c_interface.hpp
index 637794b..7f5b68b 100644
--- a/tools/i2c/i2c_interface.hpp
+++ b/tools/i2c/i2c_interface.hpp
@@ -184,6 +184,44 @@
      */
     virtual void write(uint8_t addr, uint8_t size, const uint8_t* data,
                        Mode mode = Mode::SMBUS) = 0;
+
+    /** @brief SMBus Process Call protocol to write and then read word data
+     *
+     * @param[in] addr - The register address of the i2c device
+     * @param[in] writeData - The data to write to the i2c device
+     * @param[out] readData - The data read from the i2c device
+     *
+     * @throw I2CException on error
+     */
+    virtual void processCall(uint8_t addr, uint16_t writeData,
+                             uint16_t& readData) = 0;
+
+    /** @brief SMBus Block Write-Block Read Process Call protocol
+     *
+     * The maximum write size depends on the SMBus version being used and what
+     * functionality the I2C adapter supports.
+     *
+     * If SMBus version 2.0 is being used, the maximum write size is 32 bytes.
+     * The read size + write size must be <= 32 bytes.
+     *
+     * If SMBus version 3.0 is being used and the I2C adapter supports plain
+     * I2C-level commands, the maximum write size is 255 bytes. The read size +
+     * write size must be <= 255 bytes.
+     *
+     * @param[in] addr - The register address of the i2c device
+     * @param[in] writeSize - The size of data to write
+     * @param[in] writeData - The data to write to the i2c device
+     * @param[out] readSize - The size of data read from i2c device. Max read
+     *                        size is 32 bytes.
+     * @param[out] readData - Pointer to buffer to hold the data read from the
+     *                        i2c device. Must be large enough to hold the data
+     *                        returned by the device (max is 32 bytes).
+     *
+     * @throw I2CException on error
+     */
+    virtual void processCall(uint8_t addr, uint8_t writeSize,
+                             const uint8_t* writeData, uint8_t& readSize,
+                             uint8_t* readData) = 0;
 };
 
 /** @brief Create an I2CInterface instance
diff --git a/tools/i2c/test/mocked_i2c_interface.hpp b/tools/i2c/test/mocked_i2c_interface.hpp
index 02b44f8..3fc08c7 100644
--- a/tools/i2c/test/mocked_i2c_interface.hpp
+++ b/tools/i2c/test/mocked_i2c_interface.hpp
@@ -29,6 +29,14 @@
     MOCK_METHOD(void, write,
                 (uint8_t addr, uint8_t size, const uint8_t* data, Mode mode),
                 (override));
+
+    MOCK_METHOD(void, processCall,
+                (uint8_t addr, uint16_t writeData, uint16_t& readData),
+                (override));
+    MOCK_METHOD(void, processCall,
+                (uint8_t addr, uint8_t writeSize, const uint8_t* writeData,
+                 uint8_t& readSize, uint8_t* readData),
+                (override));
 };
 
 } // namespace i2c