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