xref: /aosp_15_r20/external/pigweed/pw_i2c_linux/initiator.cc (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1 // Copyright 2023 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14 #define PW_LOG_MODULE_NAME "I2C"
15 
16 #include "pw_i2c_linux/initiator.h"
17 
18 #include <fcntl.h>
19 #include <linux/i2c-dev.h>
20 #include <linux/i2c.h>
21 #include <sys/ioctl.h>
22 #include <sys/types.h>
23 #include <unistd.h>
24 
25 #include <array>
26 #include <cerrno>
27 #include <cinttypes>
28 #include <mutex>
29 
30 #include "pw_assert/check.h"
31 #include "pw_chrono/system_clock.h"
32 #include "pw_i2c/address.h"
33 #include "pw_log/log.h"
34 #include "pw_status/status.h"
35 #include "pw_status/try.h"
36 
37 namespace pw::i2c {
38 namespace {
39 
40 using ::pw::chrono::SystemClock;
41 
42 // Returns an appropriate status code for the given fault_code (i.e. `errno`).
43 // For unexpected fault codes, logs messages to aid in debugging.
44 // Reference: https://www.kernel.org/doc/html/latest/i2c/fault-codes.html
PwStatusAndLog(int i2c_errno,uint8_t device_address)45 Status PwStatusAndLog(int i2c_errno, uint8_t device_address) {
46   switch (i2c_errno) {
47     case EAGAIN:
48       // Lost arbitration on a multi-controller bus.
49       // This is a normal condition on multi-controller busses.
50       return Status::Aborted();
51     case ENOENT:
52     case ENODEV:
53     case ENXIO:
54     case EREMOTEIO:
55       // Generally indicates the device is unavailable or faulty.
56       // This is a normal condition when incorrect address is specified.
57       //
58       // Return Unavailable instead of NotFound as per the requirements of
59       // pw::i2c::Initiator.
60       PW_LOG_INFO("I2C device unavailable at address 0x%" PRIx8,
61                   device_address);
62       return Status::Unavailable();
63     case ESHUTDOWN:
64       // It's not really clear what would cause a bus to be "suspended".
65       PW_LOG_WARN("I2C bus is suspended");
66       return Status::FailedPrecondition();
67     default:
68       // All other errors are unexpected and don't have a well-defined code.
69       PW_LOG_ERROR("I2C transaction failed for address 0x%" PRIx8 ": errno=%d",
70                    device_address,
71                    i2c_errno);
72       return Status::Unknown();
73   }
74 }
75 
76 }  // namespace
77 
78 // Open the file at the given path and validate that it is a valid bus device.
OpenI2cBus(const char * bus_path)79 Result<int> LinuxInitiator::OpenI2cBus(const char* bus_path) {
80   int fd = open(bus_path, O_RDWR);
81   if (fd < 0) {
82     PW_LOG_ERROR(
83         "Unable to open I2C bus device [%s]: errno=%d", bus_path, errno);
84     return Status::InvalidArgument();
85   }
86 
87   // Verify that the bus supports full I2C functionality.
88   unsigned long functionality = 0;
89   if (ioctl(fd, I2C_FUNCS, &functionality) != 0) {
90     PW_LOG_ERROR("Unable to read I2C functionality for bus [%s]: errno=%d",
91                  bus_path,
92                  errno);
93     close(fd);
94     return Status::InvalidArgument();
95   }
96 
97   if ((functionality & I2C_FUNC_I2C) == 0) {
98     PW_LOG_ERROR("I2C bus [%s] does not support full I2C functionality",
99                  bus_path);
100     close(fd);
101     return Status::InvalidArgument();
102   }
103   return fd;
104 }
105 
LinuxInitiator(int fd)106 LinuxInitiator::LinuxInitiator(int fd) : fd_(fd) { PW_DCHECK(fd_ >= 0); }
107 
~LinuxInitiator()108 LinuxInitiator::~LinuxInitiator() {
109   PW_DCHECK(fd_ >= 0);
110   close(fd_);
111 }
112 
DoWriteReadFor(Address device_address,ConstByteSpan tx_buffer,ByteSpan rx_buffer,SystemClock::duration timeout)113 Status LinuxInitiator::DoWriteReadFor(Address device_address,
114                                       ConstByteSpan tx_buffer,
115                                       ByteSpan rx_buffer,
116                                       SystemClock::duration timeout) {
117   auto start_time = SystemClock::now();
118 
119   // Validate arguments.
120   const auto address = device_address.GetSevenBit();
121   if (tx_buffer.empty() && rx_buffer.empty()) {
122     PW_LOG_ERROR("At least one of tx_buffer or rx_buffer must be not empty");
123     return Status::InvalidArgument();
124   }
125 
126   // Try to acquire access to the bus.
127   if (!mutex_.try_lock_for(timeout)) {
128     return Status::DeadlineExceeded();
129   }
130   std::lock_guard lock(mutex_, std::adopt_lock);
131   const auto elapsed = SystemClock::now() - start_time;
132   return DoWriteReadForLocked(address, tx_buffer, rx_buffer, timeout - elapsed);
133 }
134 
135 // Perform an I2C write, read, or combined write+read transaction.
136 //
137 // Preconditions:
138 //  - `this->mutex_` is acquired
139 //  - `this->fd_` is open for read/write and supports full I2C functionality.
140 //  - `address` is a 7-bit device address
141 //  - At least one of `tx_buffer` or `rx_buffer` is not empty.
142 //
143 // The transaction will be retried if we can't get access to the bus, until
144 // the timeout is reached. There will be no retries if `timeout` is zero or
145 // negative.
DoWriteReadForLocked(uint8_t address,ConstByteSpan tx_buffer,ByteSpan rx_buffer,chrono::SystemClock::duration timeout)146 Status LinuxInitiator::DoWriteReadForLocked(
147     uint8_t address,
148     ConstByteSpan tx_buffer,
149     ByteSpan rx_buffer,
150     chrono::SystemClock::duration timeout) {
151   const auto start_time = SystemClock::now();
152 
153   // Prepare messages for either a read, write, or combined transaction.
154   // Populate `ioctl_data` with either one or two `i2c_msg` operations.
155   // Use the `messages` buffer to store the operations.
156   i2c_rdwr_ioctl_data ioctl_data{};
157   std::array<i2c_msg, 2> messages{};
158   if (!tx_buffer.empty() && rx_buffer.empty()) {
159     messages[0] = i2c_msg{
160         .addr = address,
161         .flags = 0,  // Read transaction
162         .len = static_cast<uint16_t>(tx_buffer.size()),
163         .buf = reinterpret_cast<uint8_t*>(
164             const_cast<std::byte*>(tx_buffer.data())),  // NOLINT: read-only
165     };
166     ioctl_data = {
167         .msgs = messages.data(),
168         .nmsgs = 1,
169     };
170   } else if (!rx_buffer.empty() && tx_buffer.empty()) {
171     messages[0] = i2c_msg{
172         .addr = address,
173         .flags = I2C_M_RD,
174         .len = static_cast<uint16_t>(rx_buffer.size()),
175         .buf = reinterpret_cast<uint8_t*>(rx_buffer.data()),
176     };
177     ioctl_data = {
178         .msgs = messages.data(),
179         .nmsgs = 1,
180     };
181   } else {
182     // DoWriteReadFor already checks that at least one buffer has data.
183     // This is just an internal consistency check.
184     PW_DCHECK(!rx_buffer.empty() && !tx_buffer.empty());
185     messages[0] = i2c_msg{
186         .addr = address,
187         .flags = 0,  // Read transaction
188         .len = static_cast<uint16_t>(tx_buffer.size()),
189         .buf = reinterpret_cast<uint8_t*>(
190             const_cast<std::byte*>(tx_buffer.data())),  // NOLINT: read-only
191     };
192     messages[1] = i2c_msg{
193         .addr = address,
194         .flags = I2C_M_RD,
195         .len = static_cast<uint16_t>(rx_buffer.size()),
196         .buf = reinterpret_cast<uint8_t*>(rx_buffer.data()),
197     };
198     ioctl_data = {
199         .msgs = messages.data(),
200         .nmsgs = 2,
201     };
202   }
203   PW_LOG_DEBUG("Attempting I2C transaction with %" PRIu32 " operations",
204                ioctl_data.nmsgs);
205 
206   // Attempt the transaction. If we can't get exclusive access to the bus,
207   // then keep trying until we run out of time.
208   do {
209     if (ioctl(fd_, I2C_RDWR, &ioctl_data) < 0) {
210       Status status = PwStatusAndLog(errno, address);
211       if (status == Status::Aborted()) {
212         // Lost arbitration and need to try again.
213         PW_LOG_DEBUG("Retrying I2C transaction");
214         continue;
215       }
216       return status;
217     }
218     return OkStatus();
219   } while (SystemClock::now() - start_time < timeout);
220 
221   // Attempt transaction one last time. This thread may have been suspended
222   // after the last attempt, but before the timeout actually expired. The
223   // timeout is meant to be a minimum time period.
224   if (ioctl(fd_, I2C_RDWR, &ioctl_data) < 0) {
225     Status status = PwStatusAndLog(errno, address);
226     if (status == Status::Aborted()) {
227       PW_LOG_INFO("Timeout waiting for I2C bus access");
228       return Status::DeadlineExceeded();
229     }
230     return status;
231   }
232   return OkStatus();
233 }
234 
235 }  // namespace pw::i2c
236