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