// Copyright 2023 The Pigweed Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #define PW_LOG_MODULE_NAME "I2C" #include "pw_i2c_linux/initiator.h" #include #include #include #include #include #include #include #include #include #include #include "pw_assert/check.h" #include "pw_chrono/system_clock.h" #include "pw_i2c/address.h" #include "pw_log/log.h" #include "pw_status/status.h" #include "pw_status/try.h" namespace pw::i2c { namespace { using ::pw::chrono::SystemClock; // Returns an appropriate status code for the given fault_code (i.e. `errno`). // For unexpected fault codes, logs messages to aid in debugging. // Reference: https://www.kernel.org/doc/html/latest/i2c/fault-codes.html Status PwStatusAndLog(int i2c_errno, uint8_t device_address) { switch (i2c_errno) { case EAGAIN: // Lost arbitration on a multi-controller bus. // This is a normal condition on multi-controller busses. return Status::Aborted(); case ENOENT: case ENODEV: case ENXIO: case EREMOTEIO: // Generally indicates the device is unavailable or faulty. // This is a normal condition when incorrect address is specified. // // Return Unavailable instead of NotFound as per the requirements of // pw::i2c::Initiator. PW_LOG_INFO("I2C device unavailable at address 0x%" PRIx8, device_address); return Status::Unavailable(); case ESHUTDOWN: // It's not really clear what would cause a bus to be "suspended". PW_LOG_WARN("I2C bus is suspended"); return Status::FailedPrecondition(); default: // All other errors are unexpected and don't have a well-defined code. PW_LOG_ERROR("I2C transaction failed for address 0x%" PRIx8 ": errno=%d", device_address, i2c_errno); return Status::Unknown(); } } } // namespace // Open the file at the given path and validate that it is a valid bus device. Result LinuxInitiator::OpenI2cBus(const char* bus_path) { int fd = open(bus_path, O_RDWR); if (fd < 0) { PW_LOG_ERROR( "Unable to open I2C bus device [%s]: errno=%d", bus_path, errno); return Status::InvalidArgument(); } // Verify that the bus supports full I2C functionality. unsigned long functionality = 0; if (ioctl(fd, I2C_FUNCS, &functionality) != 0) { PW_LOG_ERROR("Unable to read I2C functionality for bus [%s]: errno=%d", bus_path, errno); close(fd); return Status::InvalidArgument(); } if ((functionality & I2C_FUNC_I2C) == 0) { PW_LOG_ERROR("I2C bus [%s] does not support full I2C functionality", bus_path); close(fd); return Status::InvalidArgument(); } return fd; } LinuxInitiator::LinuxInitiator(int fd) : fd_(fd) { PW_DCHECK(fd_ >= 0); } LinuxInitiator::~LinuxInitiator() { PW_DCHECK(fd_ >= 0); close(fd_); } Status LinuxInitiator::DoWriteReadFor(Address device_address, ConstByteSpan tx_buffer, ByteSpan rx_buffer, SystemClock::duration timeout) { auto start_time = SystemClock::now(); // Validate arguments. const auto address = device_address.GetSevenBit(); if (tx_buffer.empty() && rx_buffer.empty()) { PW_LOG_ERROR("At least one of tx_buffer or rx_buffer must be not empty"); return Status::InvalidArgument(); } // Try to acquire access to the bus. if (!mutex_.try_lock_for(timeout)) { return Status::DeadlineExceeded(); } std::lock_guard lock(mutex_, std::adopt_lock); const auto elapsed = SystemClock::now() - start_time; return DoWriteReadForLocked(address, tx_buffer, rx_buffer, timeout - elapsed); } // Perform an I2C write, read, or combined write+read transaction. // // Preconditions: // - `this->mutex_` is acquired // - `this->fd_` is open for read/write and supports full I2C functionality. // - `address` is a 7-bit device address // - At least one of `tx_buffer` or `rx_buffer` is not empty. // // The transaction will be retried if we can't get access to the bus, until // the timeout is reached. There will be no retries if `timeout` is zero or // negative. Status LinuxInitiator::DoWriteReadForLocked( uint8_t address, ConstByteSpan tx_buffer, ByteSpan rx_buffer, chrono::SystemClock::duration timeout) { const auto start_time = SystemClock::now(); // Prepare messages for either a read, write, or combined transaction. // Populate `ioctl_data` with either one or two `i2c_msg` operations. // Use the `messages` buffer to store the operations. i2c_rdwr_ioctl_data ioctl_data{}; std::array messages{}; if (!tx_buffer.empty() && rx_buffer.empty()) { messages[0] = i2c_msg{ .addr = address, .flags = 0, // Read transaction .len = static_cast(tx_buffer.size()), .buf = reinterpret_cast( const_cast(tx_buffer.data())), // NOLINT: read-only }; ioctl_data = { .msgs = messages.data(), .nmsgs = 1, }; } else if (!rx_buffer.empty() && tx_buffer.empty()) { messages[0] = i2c_msg{ .addr = address, .flags = I2C_M_RD, .len = static_cast(rx_buffer.size()), .buf = reinterpret_cast(rx_buffer.data()), }; ioctl_data = { .msgs = messages.data(), .nmsgs = 1, }; } else { // DoWriteReadFor already checks that at least one buffer has data. // This is just an internal consistency check. PW_DCHECK(!rx_buffer.empty() && !tx_buffer.empty()); messages[0] = i2c_msg{ .addr = address, .flags = 0, // Read transaction .len = static_cast(tx_buffer.size()), .buf = reinterpret_cast( const_cast(tx_buffer.data())), // NOLINT: read-only }; messages[1] = i2c_msg{ .addr = address, .flags = I2C_M_RD, .len = static_cast(rx_buffer.size()), .buf = reinterpret_cast(rx_buffer.data()), }; ioctl_data = { .msgs = messages.data(), .nmsgs = 2, }; } PW_LOG_DEBUG("Attempting I2C transaction with %" PRIu32 " operations", ioctl_data.nmsgs); // Attempt the transaction. If we can't get exclusive access to the bus, // then keep trying until we run out of time. do { if (ioctl(fd_, I2C_RDWR, &ioctl_data) < 0) { Status status = PwStatusAndLog(errno, address); if (status == Status::Aborted()) { // Lost arbitration and need to try again. PW_LOG_DEBUG("Retrying I2C transaction"); continue; } return status; } return OkStatus(); } while (SystemClock::now() - start_time < timeout); // Attempt transaction one last time. This thread may have been suspended // after the last attempt, but before the timeout actually expired. The // timeout is meant to be a minimum time period. if (ioctl(fd_, I2C_RDWR, &ioctl_data) < 0) { Status status = PwStatusAndLog(errno, address); if (status == Status::Aborted()) { PW_LOG_INFO("Timeout waiting for I2C bus access"); return Status::DeadlineExceeded(); } return status; } return OkStatus(); } } // namespace pw::i2c