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