| // Copyright 2024 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_digital_io_linux/digital_io.h" |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <linux/gpio.h> |
| |
| #include <memory> |
| |
| #include "log_errno.h" |
| #include "pw_digital_io/digital_io.h" |
| #include "pw_log/log.h" |
| #include "pw_result/result.h" |
| #include "pw_status/status.h" |
| |
| // NOTE: Currently only the v1 userspace ABI is supported. |
| // TODO: https://pwbug.dev/328268007 - Add support for v2. |
| |
| namespace pw::digital_io { |
| namespace { |
| |
| using internal::OwnedFd; |
| |
| Result<State> FdGetState(OwnedFd& fd) { |
| struct gpiohandle_data req = {}; |
| |
| if (fd.ioctl(GPIOHANDLE_GET_LINE_VALUES_IOCTL, &req) < 0) { |
| LOG_ERROR_WITH_ERRNO("GPIOHANDLE_GET_LINE_VALUES_IOCTL failed:", errno); |
| return Status::Internal(); |
| } |
| |
| return req.values[0] ? State::kActive : State::kInactive; |
| } |
| |
| } // namespace |
| |
| // TODO(jrreinhart): Support other flags, e.g.: |
| // GPIOHANDLE_REQUEST_OPEN_DRAIN, |
| // GPIOHANDLE_REQUEST_OPEN_SOURCE, |
| // GPIOHANDLE_REQUEST_BIAS_PULL_UP, |
| // GPIOHANDLE_REQUEST_BIAS_PULL_DOWN, |
| // GPIOHANDLE_REQUEST_BIAS_DISABLE, |
| |
| uint32_t LinuxConfig::GetFlags() const { |
| uint32_t flags = 0; |
| |
| switch (polarity) { |
| case Polarity::kActiveHigh: |
| break; |
| case Polarity::kActiveLow: |
| flags |= GPIOHANDLE_REQUEST_ACTIVE_LOW; |
| break; |
| } |
| |
| return flags; |
| } |
| |
| uint32_t LinuxInputConfig::GetFlags() const { |
| uint32_t flags = LinuxConfig::GetFlags(); |
| flags |= GPIOHANDLE_REQUEST_INPUT; |
| return flags; |
| } |
| |
| uint32_t LinuxOutputConfig::GetFlags() const { |
| uint32_t flags = LinuxConfig::GetFlags(); |
| flags |= GPIOHANDLE_REQUEST_OUTPUT; |
| return flags; |
| } |
| |
| // |
| // LinuxDigitalIoChip |
| // |
| |
| Result<LinuxDigitalIoChip> LinuxDigitalIoChip::Open(const char* path) { |
| int fd = open(path, O_RDWR); |
| if (fd < 0) { |
| // TODO(jrreinhart): Map errno to Status |
| LOG_ERROR_WITH_ERRNO("Could not open %s:", errno, path); |
| return Status::Internal(); |
| } |
| return LinuxDigitalIoChip(fd); |
| } |
| |
| Result<OwnedFd> LinuxDigitalIoChip::Impl::GetLineHandle(uint32_t offset, |
| uint32_t flags, |
| uint8_t default_value) { |
| struct gpiohandle_request req = { |
| .lineoffsets = {offset}, |
| .flags = flags, |
| .default_values = {default_value}, |
| .consumer_label = "pw_digital_io_linux", |
| .lines = 1, |
| .fd = -1, |
| }; |
| if (fd_.ioctl(GPIO_GET_LINEHANDLE_IOCTL, &req) < 0) { |
| LOG_ERROR_WITH_ERRNO("GPIO_GET_LINEHANDLE_IOCTL failed:", errno); |
| return Status::Internal(); |
| } |
| if (req.fd < 0) { |
| return Status::Internal(); |
| } |
| return OwnedFd(req.fd); |
| } |
| |
| Result<OwnedFd> LinuxDigitalIoChip::Impl::GetLineEventHandle( |
| uint32_t offset, uint32_t handle_flags, uint32_t event_flags) { |
| struct gpioevent_request req = { |
| .lineoffset = offset, |
| .handleflags = handle_flags, |
| .eventflags = event_flags, |
| .consumer_label = "pw_digital_io_linux", |
| .fd = -1, |
| }; |
| if (fd_.ioctl(GPIO_GET_LINEEVENT_IOCTL, &req) < 0) { |
| LOG_ERROR_WITH_ERRNO("GPIO_GET_LINEEVENT_IOCTL failed:", errno); |
| return Status::Internal(); |
| } |
| if (req.fd < 0) { |
| return Status::Internal(); |
| } |
| return OwnedFd(req.fd); |
| } |
| |
| Result<LinuxDigitalInInterrupt> LinuxDigitalIoChip::GetInterruptLine( |
| const LinuxInputConfig& config, |
| std::shared_ptr<LinuxGpioNotifier> notifier) { |
| if (!impl_) { |
| return Status::FailedPrecondition(); |
| } |
| return LinuxDigitalInInterrupt(impl_, config, notifier); |
| } |
| |
| Result<LinuxDigitalIn> LinuxDigitalIoChip::GetInputLine( |
| const LinuxInputConfig& config) { |
| if (!impl_) { |
| return Status::FailedPrecondition(); |
| } |
| return LinuxDigitalIn(impl_, config); |
| } |
| |
| Result<LinuxDigitalOut> LinuxDigitalIoChip::GetOutputLine( |
| const LinuxOutputConfig& config) { |
| if (!impl_) { |
| return Status::FailedPrecondition(); |
| } |
| return LinuxDigitalOut(impl_, config); |
| } |
| |
| // |
| // LinuxDigitalInInterrupt |
| // |
| |
| LinuxDigitalInInterrupt::Impl::~Impl() { |
| std::lock_guard lock(mutex_); |
| |
| // Explicitly close in order to unregister. |
| CloseHandle(); |
| } |
| |
| uint32_t LinuxDigitalInInterrupt::Impl::GetEventFlags() const { |
| if (handler_ == nullptr) { |
| return 0; |
| } |
| switch (trigger_) { |
| case InterruptTrigger::kActivatingEdge: |
| return (config_.polarity == Polarity::kActiveHigh) |
| ? GPIOEVENT_REQUEST_RISING_EDGE |
| : GPIOEVENT_REQUEST_FALLING_EDGE; |
| case InterruptTrigger::kDeactivatingEdge: |
| return (config_.polarity == Polarity::kActiveHigh) |
| ? GPIOEVENT_REQUEST_FALLING_EDGE |
| : GPIOEVENT_REQUEST_RISING_EDGE; |
| case InterruptTrigger::kBothEdges: |
| return GPIOEVENT_REQUEST_BOTH_EDGES; |
| } |
| } |
| |
| Status LinuxDigitalInInterrupt::Impl::SubscribeEvents() { |
| PW_CHECK(fd_is_event_handle_); |
| |
| // NOTE: Passing a normal reference is a little risky, especially since the |
| // notifier doesn't even save it; it puts it in the kernel epoll object. To |
| // make this safe, we unsubscribe from events in the destructor. |
| return notifier_->RegisterLine(fd_.fd(), *this); |
| } |
| |
| Status LinuxDigitalInInterrupt::Impl::UnsubscribeEvents() { |
| PW_CHECK(fd_is_event_handle_); |
| return notifier_->UnregisterLine(fd_.fd()); |
| } |
| |
| void LinuxDigitalInInterrupt::Impl::CloseHandle() { |
| if (!enabled()) { |
| return; |
| } |
| |
| if (fd_is_event_handle_) { |
| if (auto status = UnsubscribeEvents(); !status.ok()) { |
| PW_LOG_WARN("Failed to unsubscribe events: %s", status.str()); |
| } |
| } |
| |
| // Close the open file handle and release the line request. |
| fd_.Close(); |
| } |
| |
| Status LinuxDigitalInInterrupt::Impl::OpenHandle() { |
| if (enabled() && interrupts_desired_ == fd_is_event_handle_) { |
| // Already enabled with the right file type. Nothing to do. |
| return OkStatus(); |
| } |
| |
| // Close the file if it is already open, so it can be re-requested. |
| CloseHandle(); |
| |
| if (interrupts_desired_) { |
| // Open a lineevent handle; lineevent_create enables IRQs. |
| PW_LOG_INFO("Interrupts desired; Opening a line event handle"); |
| PW_TRY_ASSIGN(fd_, |
| chip_->GetLineEventHandle( |
| config_.index, config_.GetFlags(), GetEventFlags())); |
| fd_is_event_handle_ = true; |
| |
| if (auto status = SubscribeEvents(); !status.ok()) { |
| // Don't use CloseHandle since that will attempt to unsubscribe. |
| fd_.Close(); |
| return status; |
| } |
| } else { |
| // Open a regular linehandle |
| PW_LOG_INFO("Interrupts not desired; Opening a normal line handle"); |
| PW_TRY_ASSIGN(fd_, chip_->GetLineHandle(config_.index, config_.GetFlags())); |
| fd_is_event_handle_ = false; |
| } |
| |
| return OkStatus(); |
| } |
| |
| Status LinuxDigitalInInterrupt::Impl::DoEnable(bool enable) { |
| std::lock_guard lock(mutex_); |
| if (enable) { |
| return OpenHandle(); |
| } else { |
| CloseHandle(); |
| } |
| return OkStatus(); |
| } |
| |
| // Backend-specific note: |
| // Unlike baremetal implementations, this operation is expensive. |
| Status LinuxDigitalInInterrupt::Impl::DoEnableInterruptHandler(bool enable) { |
| std::lock_guard lock(mutex_); |
| |
| if (enable && !handler_) { |
| // PRE: When enabling, a handler must have been set using |
| // DoSetInterruptHandler(). |
| return Status::FailedPrecondition(); |
| } |
| |
| // Because this is expensive, we explicitly support enabling the interrupt |
| // handler before enabling the line. |
| interrupts_desired_ = enable; |
| if (enabled()) { |
| // Line is currently enabled (handle open). Re-open if necessary. |
| return OpenHandle(); |
| } else { |
| // Proper handle will be opened on next DoEnable(). |
| return OkStatus(); |
| } |
| } |
| |
| Status LinuxDigitalInInterrupt::Impl::DoSetInterruptHandler( |
| InterruptTrigger trigger, InterruptHandler&& handler) { |
| std::lock_guard lock(mutex_); |
| |
| // Enforce interface preconditions. |
| if (handler && handler_) { |
| // PRE: If setting a handler, no handler is permitted to be currently set. |
| return Status::FailedPrecondition(); |
| } |
| if (!handler && interrupts_enabled()) { |
| // PRE: When cleaing a handler, the interrupt handler must be disabled. |
| return Status::FailedPrecondition(); |
| } |
| |
| ++handler_generation_; |
| handler_ = std::move(handler); |
| trigger_ = trigger; |
| return OkStatus(); |
| } |
| |
| void LinuxDigitalInInterrupt::Impl::HandleEvents() { |
| InterruptHandler saved_handler{}; |
| uint32_t current_handler_generation{}; |
| State state{}; |
| |
| { |
| std::lock_guard lock(mutex_); |
| |
| // Possible race condition: We could receive a latent event after events |
| // were disabled. |
| if (!interrupts_enabled()) { |
| return; |
| } |
| |
| // Consume the event from the event handle. |
| errno = 0; |
| struct gpioevent_data event; |
| ssize_t nread = fd_.read(&event, sizeof(event)); |
| if (nread < static_cast<ssize_t>(sizeof(event))) { |
| LOG_ERROR_WITH_ERRNO("Failed to read from line event handle:", errno); |
| return; |
| } |
| |
| PW_LOG_DEBUG("Got GPIO event: timestamp=%llu, id=%s", |
| static_cast<unsigned long long>(event.timestamp), |
| event.id == GPIOEVENT_EVENT_RISING_EDGE ? "RISING_EDGE" |
| : event.id == GPIOEVENT_EVENT_FALLING_EDGE ? "FALLING_EDGE" |
| : "<unknown>"); |
| |
| // Note that polarity (ACTIVE_LOW) is already accounted for |
| // by the kernel; see gpiod_get_value_cansleep(). |
| switch (event.id) { |
| case GPIOEVENT_EVENT_RISING_EDGE: |
| // "RISING_EDGE" always means inactive -> active. |
| state = State::kActive; |
| break; |
| case GPIOEVENT_EVENT_FALLING_EDGE: |
| // "FALLING_EDGE" always means active -> inactive. |
| state = State::kInactive; |
| break; |
| default: |
| PW_LOG_ERROR("Unexpected event.id = %u", event.id); |
| return; |
| } |
| |
| // Borrow the handler while we handle the interrupt, so we can invoked it |
| // without holding the mutex. Do this only after all calls that could fail. |
| std::swap(saved_handler, handler_); |
| current_handler_generation = handler_generation_; |
| } |
| |
| // Invoke the handler without holding the mutex. |
| if (saved_handler) { |
| saved_handler(state); |
| } |
| |
| // Restore the saved handler. |
| { |
| std::lock_guard lock(mutex_); |
| |
| // While we had the mutex released, a consumer could have called |
| // DoEnable, DoEnableInterruptHandler, or DoSetInterruptHandler. |
| // Only restore the saved handler if the consumer didn't call |
| // DoSetInterruptHandler. |
| if (handler_generation_ == current_handler_generation) { |
| std::swap(handler_, saved_handler); |
| } |
| } |
| } |
| |
| Result<State> LinuxDigitalInInterrupt::Impl::DoGetState() { |
| return FdGetState(fd_); |
| } |
| |
| // |
| // LinuxDigitalIn |
| // |
| |
| Status LinuxDigitalIn::DoEnable(bool enable) { |
| if (enable) { |
| if (enabled()) { |
| return OkStatus(); |
| } |
| PW_TRY_ASSIGN(fd_, chip_->GetLineHandle(config_.index, config_.GetFlags())); |
| } else { |
| // Close the open file handle and release the line request. |
| fd_.Close(); |
| } |
| return OkStatus(); |
| } |
| |
| Result<State> LinuxDigitalIn::DoGetState() { return FdGetState(fd_); } |
| |
| // |
| // LinuxDigitalOut |
| // |
| |
| Status LinuxDigitalOut::DoEnable(bool enable) { |
| if (enable) { |
| if (enabled()) { |
| return OkStatus(); |
| } |
| uint8_t default_value = config_.default_state == State::kActive; |
| PW_TRY_ASSIGN( |
| fd_, |
| chip_->GetLineHandle(config_.index, config_.GetFlags(), default_value)); |
| } else { |
| // Close the open file handle and release the line request. |
| fd_.Close(); |
| } |
| return OkStatus(); |
| } |
| |
| Result<State> LinuxDigitalOut::DoGetState() { return FdGetState(fd_); } |
| |
| Status LinuxDigitalOut::DoSetState(State level) { |
| struct gpiohandle_data req = { |
| .values = {(level == State::kActive) ? uint8_t(1) : uint8_t(0)}, |
| }; |
| |
| if (fd_.ioctl(GPIOHANDLE_SET_LINE_VALUES_IOCTL, &req) < 0) { |
| LOG_ERROR_WITH_ERRNO("GPIOHANDLE_SET_LINE_VALUES_IOCTL failed:", errno); |
| return Status::Internal(); |
| } |
| |
| return OkStatus(); |
| } |
| |
| } // namespace pw::digital_io |