| // 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/digital_io.h" |
| |
| #include <linux/gpio.h> |
| |
| #include <algorithm> |
| #include <functional> |
| #include <memory> |
| #include <vector> |
| |
| #include "mock_vfs.h" |
| #include "pw_digital_io_linux/digital_io.h" |
| #include "pw_log/log.h" |
| #include "pw_result/result.h" |
| #include "pw_unit_test/framework.h" |
| #include "test_utils.h" |
| |
| namespace pw::digital_io { |
| namespace { |
| |
| class DigitalIoTest; |
| |
| // Represents a mocked in-kernel GPIO line object. |
| class Line { |
| public: |
| // |
| // Harness-side interface: Intended for use by DigitalIoTest and the |
| // MockFile subclasses. |
| // |
| |
| explicit Line(uint32_t index) : index_(index) {} |
| |
| // Get the logical value of the line, respecting active_low. |
| Result<bool> GetValue() const { |
| // Linux lets you read the value of an output. |
| if (requested_ == RequestedState::kNone) { |
| PW_LOG_ERROR("Cannot get value of unrequested line"); |
| return Status::FailedPrecondition(); |
| } |
| return physical_state_ ^ active_low_; |
| } |
| |
| // Set the logical value of the line, respecting active_low. |
| // Returns OK on success; FAILED_PRECONDITION if not requested as output. |
| Status SetValue(bool value) { |
| if (requested_ != RequestedState::kOutput) { |
| PW_LOG_ERROR("Cannot set value of line not requested as output"); |
| return Status::FailedPrecondition(); |
| } |
| |
| physical_state_ = value ^ active_low_; |
| |
| PW_LOG_DEBUG("Set line %u to physical %u", index_, physical_state_); |
| return OkStatus(); |
| } |
| |
| Status RequestInput(bool active_low) { |
| return DoRequest(RequestedState::kInput, active_low); |
| } |
| |
| Status RequestOutput(bool active_low) { |
| return DoRequest(RequestedState::kOutput, active_low); |
| } |
| |
| void ClearRequest() { requested_ = RequestedState::kNone; } |
| |
| // |
| // Test-side interface: Intended for use by the tests themselves. |
| // |
| |
| enum class RequestedState { |
| kNone, // Not requested by "userspace" |
| kInput, // Requested by "userspace" as an input |
| kOutput, // Requested by "userspace" as an output |
| }; |
| |
| RequestedState requested() const { return requested_; } |
| |
| void ForcePhysicalState(bool state) { physical_state_ = state; } |
| |
| bool physical_state() const { return physical_state_; } |
| |
| private: |
| const uint32_t index_; |
| bool physical_state_ = false; |
| |
| RequestedState requested_ = RequestedState::kNone; |
| bool active_low_ = false; |
| |
| Status DoRequest(RequestedState request, bool active_low) { |
| if (requested_ != RequestedState::kNone) { |
| PW_LOG_ERROR("Cannot request already-requested line"); |
| return Status::FailedPrecondition(); |
| } |
| requested_ = request; |
| active_low_ = active_low; |
| return OkStatus(); |
| } |
| }; |
| |
| // Represents a GPIO line handle, the result of issuing |
| // GPIO_GET_LINEHANDLE_IOCTL to an open chip file. |
| class LineHandleFile : public MockFile { |
| public: |
| LineHandleFile(MockVfs& vfs, const std::string& name, Line& line) |
| : MockFile(vfs, name), line_(line) {} |
| |
| private: |
| Line& line_; |
| |
| // |
| // MockFile impl. |
| // |
| |
| int DoClose() override { |
| line_.ClearRequest(); |
| return 0; |
| } |
| |
| int DoIoctl(unsigned long request, void* arg) override { |
| switch (request) { |
| case GPIOHANDLE_GET_LINE_VALUES_IOCTL: |
| return DoIoctlGetValues(static_cast<struct gpiohandle_data*>(arg)); |
| case GPIOHANDLE_SET_LINE_VALUES_IOCTL: |
| return DoIoctlSetValues(static_cast<struct gpiohandle_data*>(arg)); |
| default: |
| PW_LOG_ERROR("%s: Unhandled request=0x%lX", __FUNCTION__, request); |
| return -1; |
| } |
| } |
| |
| // Handle GPIOHANDLE_GET_LINE_VALUES_IOCTL |
| int DoIoctlGetValues(struct gpiohandle_data* data) { |
| auto result = line_.GetValue(); |
| if (!result.ok()) { |
| return -1; |
| } |
| |
| data->values[0] = *result; |
| return 0; |
| } |
| |
| // Handle GPIOHANDLE_SET_LINE_VALUES_IOCTL |
| int DoIoctlSetValues(struct gpiohandle_data* data) { |
| auto status = line_.SetValue(data->values[0]); |
| if (!status.ok()) { |
| return -1; |
| } |
| |
| return 0; |
| } |
| }; |
| |
| // Represents an open GPIO chip file, the result of opening /dev/gpiochip*. |
| class ChipFile : public MockFile { |
| public: |
| ChipFile(MockVfs& vfs, const std::string& name, std::vector<Line>& lines) |
| : MockFile(vfs, name), lines_(lines) {} |
| |
| private: |
| std::vector<Line>& lines_; |
| |
| // |
| // MockFile impl. |
| // |
| |
| int DoIoctl(unsigned long request, void* arg) override { |
| switch (request) { |
| case GPIO_GET_LINEHANDLE_IOCTL: |
| return DoLinehandleIoctl(static_cast<struct gpiohandle_request*>(arg)); |
| default: |
| PW_LOG_ERROR("%s: Unhandled request=0x%lX", __FUNCTION__, request); |
| return -1; |
| } |
| } |
| |
| // Handle GPIO_GET_LINEHANDLE_IOCTL |
| int DoLinehandleIoctl(struct gpiohandle_request* req) { |
| uint32_t const direction = |
| req->flags & (GPIOHANDLE_REQUEST_OUTPUT | GPIOHANDLE_REQUEST_INPUT); |
| |
| // Validate flags. |
| if (direction == (GPIOHANDLE_REQUEST_OUTPUT | GPIOHANDLE_REQUEST_INPUT)) { |
| PW_LOG_ERROR("%s: OUTPUT and INPUT are mutually exclusive", __FUNCTION__); |
| return -1; |
| } |
| |
| // Only support requesting one line at at time. |
| if (req->lines != 1) { |
| PW_LOG_ERROR("%s: Unsupported req->lines=%u", __FUNCTION__, req->lines); |
| return -1; |
| } |
| |
| uint32_t const offset = req->lineoffsets[0]; |
| uint8_t const default_value = req->default_values[0]; |
| bool const active_low = req->flags & GPIOHANDLE_REQUEST_ACTIVE_LOW; |
| |
| if (offset >= lines_.size()) { |
| PW_LOG_ERROR("%s: Invalid line offset: %u", __FUNCTION__, offset); |
| return -1; |
| } |
| Line& line = lines_[offset]; |
| |
| Status status = OkStatus(); |
| switch (direction) { |
| case GPIOHANDLE_REQUEST_OUTPUT: |
| status.Update(line.RequestOutput(active_low)); |
| status.Update(line.SetValue(default_value)); |
| break; |
| case GPIOHANDLE_REQUEST_INPUT: |
| status.Update(line.RequestInput(active_low)); |
| break; |
| } |
| if (!status.ok()) { |
| return -1; |
| } |
| |
| req->fd = vfs_.InstallNewFile<LineHandleFile>("line-handle", line); |
| return 0; |
| } |
| }; |
| |
| // Test fixture for all digtal io tests. |
| class DigitalIoTest : public ::testing::Test { |
| protected: |
| void SetUp() override { GetMockVfs().Reset(); } |
| |
| void TearDown() override { EXPECT_TRUE(GetMockVfs().AllFdsClosed()); } |
| |
| LinuxDigitalIoChip OpenChip() { |
| int fd = GetMockVfs().InstallNewFile<ChipFile>("chip", lines_); |
| return LinuxDigitalIoChip(fd); |
| } |
| |
| Line& line0() { return lines_[0]; } |
| Line& line1() { return lines_[1]; } |
| |
| private: |
| std::vector<Line> lines_ = std::vector<Line>{ |
| Line(0), // Input |
| Line(1), // Output |
| }; |
| }; |
| |
| // |
| // Tests |
| // |
| |
| TEST_F(DigitalIoTest, DoInput) { |
| LinuxDigitalIoChip chip = OpenChip(); |
| |
| auto& line = line0(); |
| LinuxInputConfig config( |
| /* index= */ 0, |
| /* polarity= */ Polarity::kActiveHigh); |
| |
| ASSERT_OK_AND_ASSIGN(auto input, chip.GetInputLine(config)); |
| |
| // Enable the input, and ensure it is requested. |
| ASSERT_EQ(line.requested(), Line::RequestedState::kNone); |
| ASSERT_OK(input.Enable()); |
| ASSERT_EQ(line.requested(), Line::RequestedState::kInput); |
| |
| Result<State> state; |
| |
| // Force the line high and assert it is seen as active (active high). |
| line.ForcePhysicalState(true); |
| state = input.GetState(); |
| ASSERT_OK(state.status()); |
| ASSERT_EQ(State::kActive, state.value()); |
| |
| // Force the line low and assert it is seen as inactive (active high). |
| line.ForcePhysicalState(false); |
| state = input.GetState(); |
| ASSERT_OK(state.status()); |
| ASSERT_EQ(State::kInactive, state.value()); |
| |
| // Disable the line and ensure it is no longer requested. |
| ASSERT_OK(input.Disable()); |
| ASSERT_EQ(line.requested(), Line::RequestedState::kNone); |
| } |
| |
| TEST_F(DigitalIoTest, DoInputInvert) { |
| LinuxDigitalIoChip chip = OpenChip(); |
| |
| auto& line = line0(); |
| LinuxInputConfig config( |
| /* index= */ 0, |
| /* polarity= */ Polarity::kActiveLow); |
| |
| ASSERT_OK_AND_ASSIGN(auto input, chip.GetInputLine(config)); |
| |
| // Enable the input, and ensure it is requested. |
| ASSERT_EQ(line.requested(), Line::RequestedState::kNone); |
| ASSERT_OK(input.Enable()); |
| ASSERT_EQ(line.requested(), Line::RequestedState::kInput); |
| |
| Result<State> state; |
| |
| // Force the line high and assert it is seen as inactive (active low). |
| line.ForcePhysicalState(true); |
| state = input.GetState(); |
| ASSERT_OK(state.status()); |
| ASSERT_EQ(State::kInactive, state.value()); |
| |
| // Force the line low and assert it is seen as active (active low). |
| line.ForcePhysicalState(false); |
| state = input.GetState(); |
| ASSERT_OK(state.status()); |
| ASSERT_EQ(State::kActive, state.value()); |
| |
| // Disable the line and ensure it is no longer requested. |
| ASSERT_OK(input.Disable()); |
| ASSERT_EQ(line.requested(), Line::RequestedState::kNone); |
| } |
| |
| TEST_F(DigitalIoTest, DoOutput) { |
| LinuxDigitalIoChip chip = OpenChip(); |
| |
| auto& line = line1(); |
| LinuxOutputConfig config( |
| /* index= */ 1, |
| /* polarity= */ Polarity::kActiveHigh, |
| /* default_state= */ State::kActive); |
| |
| ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config)); |
| |
| // Enable the output, and ensure it is requested. |
| ASSERT_EQ(line.requested(), Line::RequestedState::kNone); |
| ASSERT_OK(output.Enable()); |
| ASSERT_EQ(line.requested(), Line::RequestedState::kOutput); |
| |
| // Expect the line to go high, due to default_state=kActive (active high). |
| ASSERT_TRUE(line.physical_state()); |
| |
| // Set the output's state to inactive, and assert it goes low (active high). |
| ASSERT_OK(output.SetStateInactive()); |
| ASSERT_FALSE(line.physical_state()); |
| |
| // Set the output's state to active, and assert it goes high (active high). |
| ASSERT_OK(output.SetStateActive()); |
| ASSERT_TRUE(line.physical_state()); |
| |
| // Disable the line and ensure it is no longer requested. |
| ASSERT_OK(output.Disable()); |
| ASSERT_EQ(line.requested(), Line::RequestedState::kNone); |
| // NOTE: We do not assert line.physical_state() here. |
| // See the warning on LinuxDigitalOut in docs.rst. |
| } |
| |
| TEST_F(DigitalIoTest, DoOutputInvert) { |
| LinuxDigitalIoChip chip = OpenChip(); |
| |
| auto& line = line1(); |
| LinuxOutputConfig config( |
| /* index= */ 1, |
| /* polarity= */ Polarity::kActiveLow, |
| /* default_state= */ State::kActive); |
| |
| ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config)); |
| |
| // Enable the output, and ensure it is requested. |
| ASSERT_EQ(line.requested(), Line::RequestedState::kNone); |
| ASSERT_OK(output.Enable()); |
| ASSERT_EQ(line.requested(), Line::RequestedState::kOutput); |
| |
| // Expect the line to stay low, due to default_state=kActive (active low). |
| ASSERT_FALSE(line.physical_state()); |
| |
| // Set the output's state to inactive, and assert it goes high (active low). |
| ASSERT_OK(output.SetStateInactive()); |
| ASSERT_TRUE(line.physical_state()); |
| |
| // Set the output's state to active, and assert it goes low (active low). |
| ASSERT_OK(output.SetStateActive()); |
| ASSERT_FALSE(line.physical_state()); |
| |
| // Disable the line and ensure it is no longer requested. |
| ASSERT_OK(output.Disable()); |
| ASSERT_EQ(line.requested(), Line::RequestedState::kNone); |
| // NOTE: We do not assert line.physical_state() here. |
| // See the warning on LinuxDigitalOut in docs.rst. |
| } |
| |
| // Verify we can get the state of an output. |
| TEST_F(DigitalIoTest, OutputGetState) { |
| LinuxDigitalIoChip chip = OpenChip(); |
| |
| auto& line = line1(); |
| LinuxOutputConfig config( |
| /* index= */ 1, |
| /* polarity= */ Polarity::kActiveHigh, |
| /* default_state= */ State::kInactive); |
| |
| ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config)); |
| |
| ASSERT_OK(output.Enable()); |
| |
| // Expect the line to stay low, due to default_state=kInactive (active high). |
| ASSERT_FALSE(line.physical_state()); |
| |
| Result<State> state; |
| |
| // Verify GetState() returns the expected state: inactive (default_state). |
| state = output.GetState(); |
| ASSERT_OK(state.status()); |
| ASSERT_EQ(State::kInactive, state.value()); |
| |
| // Set the output's state to active, then verify GetState() returns the |
| // new expected state. |
| ASSERT_OK(output.SetStateActive()); |
| state = output.GetState(); |
| ASSERT_OK(state.status()); |
| ASSERT_EQ(State::kActive, state.value()); |
| } |
| |
| } // namespace |
| } // namespace pw::digital_io |