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