blob: 986addf08aedda306d9395fff20b4f4caef9f7b1 [file]
// 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 <fcntl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>
#include <cinttypes>
#include <mutex>
#include <vector>
#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, uint16_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%" PRIx16,
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%" PRIx16 ": errno=%d",
device_address,
i2c_errno);
return Status::Unknown();
}
}
// Requests the feature set from linux driver and returns the feature set.
Initiator::Feature GetFeaturesFromFd(int fd) {
Initiator::Feature features = Initiator::Feature::kStandard;
unsigned long functionality = 0;
int ioctl_ret = ioctl(fd, I2C_FUNCS, &functionality);
PW_DCHECK_INT_EQ(ioctl_ret, 0);
if (ioctl_ret != 0) {
PW_LOG_WARN("Unable to check i2c features");
return features;
}
if ((functionality & I2C_FUNC_10BIT_ADDR) != 0) {
features = features | Initiator::Feature::kTenBit;
}
if ((functionality & I2C_FUNC_NOSTART) != 0) {
features = features | Initiator::Feature::kNoStart;
}
return features;
}
// Convert flags from internal `Message::Flags` to linux `i2c_msg` flags.
uint16_t LinuxFlagsFromMessage(const Message& msg) {
uint16_t flags = 0;
if (msg.IsRead()) {
flags |= I2C_M_RD;
}
if (msg.IsTenBit()) {
flags |= I2C_M_TEN;
}
if (msg.IsWriteContinuation()) {
flags |= I2C_M_NOSTART;
}
return flags;
}
} // namespace
// Open the file at the given path and validate that it is a valid bus device.
Result<int> 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)
: Initiator(GetFeaturesFromFd(fd)), fd_(fd) {
PW_DCHECK(fd_ >= 0);
}
LinuxInitiator::~LinuxInitiator() {
PW_DCHECK(fd_ >= 0);
close(fd_);
}
// 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
// - `messages` 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::DoTransferFor(span<const Message> messages,
chrono::SystemClock::duration timeout) {
chrono::SystemClock::time_point deadline =
chrono::SystemClock::TimePointAfterAtLeast(timeout);
// Acquire lock for the bus.
if (!mutex_.try_lock_until(deadline)) {
return Status::DeadlineExceeded();
}
std::lock_guard lock(mutex_, std::adopt_lock);
// Populate `ioctl_data` with one `i2c_msg` for each input message.
// Use the `i2c_messages` buffer to store the operations.
std::vector<i2c_msg> i2c_messages;
i2c_messages.reserve(messages.size());
for (const Message& msg : messages) {
if (msg.GetData().size() > std::numeric_limits<uint16_t>::max()) {
return Status::OutOfRange();
}
i2c_messages.push_back({.addr = msg.GetAddress().GetAddress(),
.flags = LinuxFlagsFromMessage(msg),
.len = static_cast<uint16_t>(msg.GetData().size()),
.buf = reinterpret_cast<uint8_t*>(
const_cast<std::byte*>(msg.GetData().data()))});
}
i2c_rdwr_ioctl_data ioctl_data = {
.msgs = i2c_messages.data(),
.nmsgs = static_cast<uint32_t>(i2c_messages.size()),
};
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, i2c_messages.front().addr);
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() < deadline);
// 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, i2c_messages.front().addr);
if (status == Status::Aborted()) {
PW_LOG_INFO("Timeout waiting for I2C bus access");
return Status::DeadlineExceeded();
}
return status;
}
return OkStatus();
}
} // namespace pw::i2c