xref: /aosp_15_r20/external/pigweed/pw_digital_io_linux/digital_io.cc (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1 // Copyright 2024 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 "pw_digital_io_linux"
15 #define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
16 
17 #include "pw_digital_io_linux/digital_io.h"
18 
19 #include <errno.h>
20 #include <fcntl.h>
21 #include <linux/gpio.h>
22 
23 #include <memory>
24 
25 #include "log_errno.h"
26 #include "pw_digital_io/digital_io.h"
27 #include "pw_log/log.h"
28 #include "pw_result/result.h"
29 #include "pw_status/status.h"
30 
31 // NOTE: Currently only the v1 userspace ABI is supported.
32 // TODO: https://pwbug.dev/328268007 - Add support for v2.
33 
34 namespace pw::digital_io {
35 namespace {
36 
37 using internal::OwnedFd;
38 
FdGetState(OwnedFd & fd)39 Result<State> FdGetState(OwnedFd& fd) {
40   struct gpiohandle_data req = {};
41 
42   if (fd.ioctl(GPIOHANDLE_GET_LINE_VALUES_IOCTL, &req) < 0) {
43     LOG_ERROR_WITH_ERRNO("GPIOHANDLE_GET_LINE_VALUES_IOCTL failed:", errno);
44     return Status::Internal();
45   }
46 
47   return req.values[0] ? State::kActive : State::kInactive;
48 }
49 
50 }  // namespace
51 
52 // TODO(jrreinhart): Support other flags, e.g.:
53 // GPIOHANDLE_REQUEST_OPEN_DRAIN,
54 // GPIOHANDLE_REQUEST_OPEN_SOURCE,
55 // GPIOHANDLE_REQUEST_BIAS_PULL_UP,
56 // GPIOHANDLE_REQUEST_BIAS_PULL_DOWN,
57 // GPIOHANDLE_REQUEST_BIAS_DISABLE,
58 
GetFlags() const59 uint32_t LinuxConfig::GetFlags() const {
60   uint32_t flags = 0;
61 
62   switch (polarity) {
63     case Polarity::kActiveHigh:
64       break;
65     case Polarity::kActiveLow:
66       flags |= GPIOHANDLE_REQUEST_ACTIVE_LOW;
67       break;
68   }
69 
70   return flags;
71 }
72 
GetFlags() const73 uint32_t LinuxInputConfig::GetFlags() const {
74   uint32_t flags = LinuxConfig::GetFlags();
75   flags |= GPIOHANDLE_REQUEST_INPUT;
76   return flags;
77 }
78 
GetFlags() const79 uint32_t LinuxOutputConfig::GetFlags() const {
80   uint32_t flags = LinuxConfig::GetFlags();
81   flags |= GPIOHANDLE_REQUEST_OUTPUT;
82   return flags;
83 }
84 
85 //
86 // LinuxDigitalIoChip
87 //
88 
Open(const char * path)89 Result<LinuxDigitalIoChip> LinuxDigitalIoChip::Open(const char* path) {
90   int fd = open(path, O_RDWR);
91   if (fd < 0) {
92     // TODO(jrreinhart): Map errno to Status
93     LOG_ERROR_WITH_ERRNO("Could not open %s:", errno, path);
94     return Status::Internal();
95   }
96   return LinuxDigitalIoChip(fd);
97 }
98 
GetLineHandle(uint32_t offset,uint32_t flags,uint8_t default_value)99 Result<OwnedFd> LinuxDigitalIoChip::Impl::GetLineHandle(uint32_t offset,
100                                                         uint32_t flags,
101                                                         uint8_t default_value) {
102   struct gpiohandle_request req = {
103       .lineoffsets = {offset},
104       .flags = flags,
105       .default_values = {default_value},
106       .consumer_label = "pw_digital_io_linux",
107       .lines = 1,
108       .fd = -1,
109   };
110   if (fd_.ioctl(GPIO_GET_LINEHANDLE_IOCTL, &req) < 0) {
111     LOG_ERROR_WITH_ERRNO("GPIO_GET_LINEHANDLE_IOCTL failed:", errno);
112     return Status::Internal();
113   }
114   if (req.fd < 0) {
115     return Status::Internal();
116   }
117   return OwnedFd(req.fd);
118 }
119 
GetLineEventHandle(uint32_t offset,uint32_t handle_flags,uint32_t event_flags)120 Result<OwnedFd> LinuxDigitalIoChip::Impl::GetLineEventHandle(
121     uint32_t offset, uint32_t handle_flags, uint32_t event_flags) {
122   struct gpioevent_request req = {
123       .lineoffset = offset,
124       .handleflags = handle_flags,
125       .eventflags = event_flags,
126       .consumer_label = "pw_digital_io_linux",
127       .fd = -1,
128   };
129   if (fd_.ioctl(GPIO_GET_LINEEVENT_IOCTL, &req) < 0) {
130     LOG_ERROR_WITH_ERRNO("GPIO_GET_LINEEVENT_IOCTL failed:", errno);
131     return Status::Internal();
132   }
133   if (req.fd < 0) {
134     return Status::Internal();
135   }
136   return OwnedFd(req.fd);
137 }
138 
GetInterruptLine(const LinuxInputConfig & config,std::shared_ptr<LinuxGpioNotifier> notifier)139 Result<LinuxDigitalInInterrupt> LinuxDigitalIoChip::GetInterruptLine(
140     const LinuxInputConfig& config,
141     std::shared_ptr<LinuxGpioNotifier> notifier) {
142   if (!impl_) {
143     return Status::FailedPrecondition();
144   }
145   return LinuxDigitalInInterrupt(impl_, config, notifier);
146 }
147 
GetInputLine(const LinuxInputConfig & config)148 Result<LinuxDigitalIn> LinuxDigitalIoChip::GetInputLine(
149     const LinuxInputConfig& config) {
150   if (!impl_) {
151     return Status::FailedPrecondition();
152   }
153   return LinuxDigitalIn(impl_, config);
154 }
155 
GetOutputLine(const LinuxOutputConfig & config)156 Result<LinuxDigitalOut> LinuxDigitalIoChip::GetOutputLine(
157     const LinuxOutputConfig& config) {
158   if (!impl_) {
159     return Status::FailedPrecondition();
160   }
161   return LinuxDigitalOut(impl_, config);
162 }
163 
164 //
165 // LinuxDigitalInInterrupt
166 //
167 
~Impl()168 LinuxDigitalInInterrupt::Impl::~Impl() {
169   std::lock_guard lock(mutex_);
170 
171   // Explicitly close in order to unregister.
172   CloseHandle();
173 }
174 
GetEventFlags() const175 uint32_t LinuxDigitalInInterrupt::Impl::GetEventFlags() const {
176   if (handler_ == nullptr) {
177     return 0;
178   }
179   switch (trigger_) {
180     case InterruptTrigger::kActivatingEdge:
181       return (config_.polarity == Polarity::kActiveHigh)
182                  ? GPIOEVENT_REQUEST_RISING_EDGE
183                  : GPIOEVENT_REQUEST_FALLING_EDGE;
184     case InterruptTrigger::kDeactivatingEdge:
185       return (config_.polarity == Polarity::kActiveHigh)
186                  ? GPIOEVENT_REQUEST_FALLING_EDGE
187                  : GPIOEVENT_REQUEST_RISING_EDGE;
188     case InterruptTrigger::kBothEdges:
189       return GPIOEVENT_REQUEST_BOTH_EDGES;
190   }
191 }
192 
SubscribeEvents()193 Status LinuxDigitalInInterrupt::Impl::SubscribeEvents() {
194   PW_CHECK(fd_is_event_handle_);
195 
196   // NOTE: Passing a normal reference is a little risky, especially since the
197   // notifier doesn't even save it; it puts it in the kernel epoll object. To
198   // make this safe, we unsubscribe from events in the destructor.
199   return notifier_->RegisterLine(fd_.fd(), *this);
200 }
201 
UnsubscribeEvents()202 Status LinuxDigitalInInterrupt::Impl::UnsubscribeEvents() {
203   PW_CHECK(fd_is_event_handle_);
204   return notifier_->UnregisterLine(fd_.fd());
205 }
206 
CloseHandle()207 void LinuxDigitalInInterrupt::Impl::CloseHandle() {
208   if (!enabled()) {
209     return;
210   }
211 
212   if (fd_is_event_handle_) {
213     if (auto status = UnsubscribeEvents(); !status.ok()) {
214       PW_LOG_WARN("Failed to unsubscribe events: %s", status.str());
215     }
216   }
217 
218   // Close the open file handle and release the line request.
219   fd_.Close();
220 }
221 
OpenHandle()222 Status LinuxDigitalInInterrupt::Impl::OpenHandle() {
223   if (enabled() && interrupts_desired_ == fd_is_event_handle_) {
224     // Already enabled with the right file type. Nothing to do.
225     return OkStatus();
226   }
227 
228   // Close the file if it is already open, so it can be re-requested.
229   CloseHandle();
230 
231   if (interrupts_desired_) {
232     // Open a lineevent handle; lineevent_create enables IRQs.
233     PW_LOG_INFO("Interrupts desired; Opening a line event handle");
234     PW_TRY_ASSIGN(fd_,
235                   chip_->GetLineEventHandle(
236                       config_.index, config_.GetFlags(), GetEventFlags()));
237     fd_is_event_handle_ = true;
238 
239     if (auto status = SubscribeEvents(); !status.ok()) {
240       // Don't use CloseHandle since that will attempt to unsubscribe.
241       fd_.Close();
242       return status;
243     }
244   } else {
245     // Open a regular linehandle
246     PW_LOG_INFO("Interrupts not desired; Opening a normal line handle");
247     PW_TRY_ASSIGN(fd_, chip_->GetLineHandle(config_.index, config_.GetFlags()));
248     fd_is_event_handle_ = false;
249   }
250 
251   return OkStatus();
252 }
253 
DoEnable(bool enable)254 Status LinuxDigitalInInterrupt::Impl::DoEnable(bool enable) {
255   std::lock_guard lock(mutex_);
256   if (enable) {
257     return OpenHandle();
258   } else {
259     CloseHandle();
260   }
261   return OkStatus();
262 }
263 
264 // Backend-specific note:
265 // Unlike baremetal implementations, this operation is expensive.
DoEnableInterruptHandler(bool enable)266 Status LinuxDigitalInInterrupt::Impl::DoEnableInterruptHandler(bool enable) {
267   std::lock_guard lock(mutex_);
268 
269   if (enable && !handler_) {
270     // PRE: When enabling, a handler must have been set using
271     // DoSetInterruptHandler().
272     return Status::FailedPrecondition();
273   }
274 
275   // Because this is expensive, we explicitly support enabling the interrupt
276   // handler before enabling the line.
277   interrupts_desired_ = enable;
278   if (enabled()) {
279     // Line is currently enabled (handle open). Re-open if necessary.
280     return OpenHandle();
281   } else {
282     // Proper handle will be opened on next DoEnable().
283     return OkStatus();
284   }
285 }
286 
DoSetInterruptHandler(InterruptTrigger trigger,InterruptHandler && handler)287 Status LinuxDigitalInInterrupt::Impl::DoSetInterruptHandler(
288     InterruptTrigger trigger, InterruptHandler&& handler) {
289   std::lock_guard lock(mutex_);
290 
291   // Enforce interface preconditions.
292   if (handler && handler_) {
293     // PRE: If setting a handler, no handler is permitted to be currently set.
294     return Status::FailedPrecondition();
295   }
296   if (!handler && interrupts_enabled()) {
297     // PRE: When cleaing a handler, the interrupt handler must be disabled.
298     return Status::FailedPrecondition();
299   }
300 
301   ++handler_generation_;
302   handler_ = std::move(handler);
303   trigger_ = trigger;
304   return OkStatus();
305 }
306 
HandleEvents()307 void LinuxDigitalInInterrupt::Impl::HandleEvents() {
308   InterruptHandler saved_handler{};
309   uint32_t current_handler_generation{};
310   State state{};
311 
312   {
313     std::lock_guard lock(mutex_);
314 
315     // Possible race condition: We could receive a latent event after events
316     // were disabled.
317     if (!interrupts_enabled()) {
318       return;
319     }
320 
321     // Consume the event from the event handle.
322     errno = 0;
323     struct gpioevent_data event;
324     ssize_t nread = fd_.read(&event, sizeof(event));
325     if (nread < static_cast<ssize_t>(sizeof(event))) {
326       LOG_ERROR_WITH_ERRNO("Failed to read from line event handle:", errno);
327       return;
328     }
329 
330     PW_LOG_DEBUG("Got GPIO event: timestamp=%llu, id=%s",
331                  static_cast<unsigned long long>(event.timestamp),
332                  event.id == GPIOEVENT_EVENT_RISING_EDGE    ? "RISING_EDGE"
333                  : event.id == GPIOEVENT_EVENT_FALLING_EDGE ? "FALLING_EDGE"
334                                                             : "<unknown>");
335 
336     // Note that polarity (ACTIVE_LOW) is already accounted for
337     // by the kernel; see gpiod_get_value_cansleep().
338     switch (event.id) {
339       case GPIOEVENT_EVENT_RISING_EDGE:
340         // "RISING_EDGE" always means inactive -> active.
341         state = State::kActive;
342         break;
343       case GPIOEVENT_EVENT_FALLING_EDGE:
344         // "FALLING_EDGE" always means active -> inactive.
345         state = State::kInactive;
346         break;
347       default:
348         PW_LOG_ERROR("Unexpected event.id = %u", event.id);
349         return;
350     }
351 
352     // Borrow the handler while we handle the interrupt, so we can invoked it
353     // without holding the mutex. Do this only after all calls that could fail.
354     std::swap(saved_handler, handler_);
355     current_handler_generation = handler_generation_;
356   }
357 
358   // Invoke the handler without holding the mutex.
359   if (saved_handler) {
360     saved_handler(state);
361   }
362 
363   // Restore the saved handler.
364   {
365     std::lock_guard lock(mutex_);
366 
367     // While we had the mutex released, a consumer could have called
368     // DoEnable, DoEnableInterruptHandler, or DoSetInterruptHandler.
369     // Only restore the saved handler if the consumer didn't call
370     // DoSetInterruptHandler.
371     if (handler_generation_ == current_handler_generation) {
372       std::swap(handler_, saved_handler);
373     }
374   }
375 }
376 
DoGetState()377 Result<State> LinuxDigitalInInterrupt::Impl::DoGetState() {
378   return FdGetState(fd_);
379 }
380 
381 //
382 // LinuxDigitalIn
383 //
384 
DoEnable(bool enable)385 Status LinuxDigitalIn::DoEnable(bool enable) {
386   if (enable) {
387     if (enabled()) {
388       return OkStatus();
389     }
390     PW_TRY_ASSIGN(fd_, chip_->GetLineHandle(config_.index, config_.GetFlags()));
391   } else {
392     // Close the open file handle and release the line request.
393     fd_.Close();
394   }
395   return OkStatus();
396 }
397 
DoGetState()398 Result<State> LinuxDigitalIn::DoGetState() { return FdGetState(fd_); }
399 
400 //
401 // LinuxDigitalOut
402 //
403 
DoEnable(bool enable)404 Status LinuxDigitalOut::DoEnable(bool enable) {
405   if (enable) {
406     if (enabled()) {
407       return OkStatus();
408     }
409     uint8_t default_value = config_.default_state == State::kActive;
410     PW_TRY_ASSIGN(
411         fd_,
412         chip_->GetLineHandle(config_.index, config_.GetFlags(), default_value));
413   } else {
414     // Close the open file handle and release the line request.
415     fd_.Close();
416   }
417   return OkStatus();
418 }
419 
DoGetState()420 Result<State> LinuxDigitalOut::DoGetState() { return FdGetState(fd_); }
421 
DoSetState(State level)422 Status LinuxDigitalOut::DoSetState(State level) {
423   struct gpiohandle_data req = {
424       .values = {(level == State::kActive) ? uint8_t(1) : uint8_t(0)},
425   };
426 
427   if (fd_.ioctl(GPIOHANDLE_SET_LINE_VALUES_IOCTL, &req) < 0) {
428     LOG_ERROR_WITH_ERRNO("GPIOHANDLE_SET_LINE_VALUES_IOCTL failed:", errno);
429     return Status::Internal();
430   }
431 
432   return OkStatus();
433 }
434 
435 }  // namespace pw::digital_io
436