From d80eb6af2d30a1c0fbdcc43b55e0b32eaf6b2f34 Mon Sep 17 00:00:00 2001 From: Austin Foxley Date: Fri, 14 Jul 2023 18:59:20 +0000 Subject: pw_i2c: Add i2c rpc service Service to read/write from arbitrary i2c devices. Useful for bringup and debugging. Change-Id: Ife50e6126a0e718aed1cb68822ac074e3b5f5874 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155250 Reviewed-by: Jonathon Reinhart Commit-Queue: Austin Foxley Reviewed-by: Carlos Chinchilla --- pw_i2c/BUILD.bazel | 49 ++++++++ pw_i2c/BUILD.gn | 35 ++++++ pw_i2c/docs.rst | 39 ++++++ pw_i2c/i2c.options | 19 +++ pw_i2c/i2c.proto | 52 ++++++++ pw_i2c/i2c_service.cc | 90 ++++++++++++++ pw_i2c/i2c_service_test.cc | 245 +++++++++++++++++++++++++++++++++++++ pw_i2c/public/pw_i2c/i2c_service.h | 50 ++++++++ 8 files changed, 579 insertions(+) create mode 100644 pw_i2c/i2c.options create mode 100644 pw_i2c/i2c.proto create mode 100644 pw_i2c/i2c_service.cc create mode 100644 pw_i2c/i2c_service_test.cc create mode 100644 pw_i2c/public/pw_i2c/i2c_service.h diff --git a/pw_i2c/BUILD.bazel b/pw_i2c/BUILD.bazel index 98afc2230..d17d9ea56 100644 --- a/pw_i2c/BUILD.bazel +++ b/pw_i2c/BUILD.bazel @@ -17,6 +17,12 @@ load( "pw_cc_library", "pw_cc_test", ) +load( + "//pw_protobuf_compiler:pw_proto_library.bzl", + "pw_proto_filegroup", + "pw_proto_library", +) +load("@rules_proto//proto:defs.bzl", "proto_library") package(default_visibility = ["//visibility:public"]) @@ -157,3 +163,46 @@ pw_cc_test( "//pw_unit_test", ], ) + +pw_proto_filegroup( + name = "i2c_proto_and_options", + srcs = ["i2c.proto"], + options_files = ["i2c.options"], +) + +proto_library( + name = "i2c_proto", + srcs = [":i2c_proto_and_options"], +) + +pw_proto_library( + name = "i2c_cc", + deps = [":i2c_proto"], +) + +pw_cc_library( + name = "i2c_service", + srcs = ["i2c_service.cc"], + hdrs = ["public/pw_i2c/i2c_service.h"], + includes = ["public"], + deps = [ + ":address", + ":i2c_cc.pwpb_rpc", + ":initiator", + "//pw_chrono:system_clock", + "//pw_containers:vector", + "//pw_status", + ], +) + +pw_cc_test( + name = "i2c_service_test", + srcs = ["i2c_service_test.cc"], + deps = [ + ":i2c_service", + ":initiator_mock", + "//pw_containers:vector", + "//pw_rpc/pwpb:test_method_context", + "//pw_status", + ], +) diff --git a/pw_i2c/BUILD.gn b/pw_i2c/BUILD.gn index 700f8edad..4eed45af1 100644 --- a/pw_i2c/BUILD.gn +++ b/pw_i2c/BUILD.gn @@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni") import("$dir_pw_build/target_types.gni") import("$dir_pw_chrono/backend.gni") import("$dir_pw_docgen/docs.gni") +import("$dir_pw_protobuf_compiler/proto.gni") import("$dir_pw_unit_test/test.gni") config("public_include_path") { @@ -70,6 +71,27 @@ pw_source_set("register_device") { deps = [ "$dir_pw_assert" ] } +pw_proto_library("protos") { + sources = [ "i2c.proto" ] + inputs = [ "i2c.options" ] + prefix = "pw_i2c" +} + +pw_source_set("i2c_service") { + public = [ "public/pw_i2c/i2c_service.h" ] + sources = [ "i2c_service.cc" ] + public_deps = [ + ":protos.pwpb_rpc", + "$dir_pw_chrono:system_clock", + "$dir_pw_i2c:initiator", + ] + deps = [ + "$dir_pw_containers:vector", + "$dir_pw_i2c:address", + "$dir_pw_status", + ] +} + pw_source_set("mock") { public_configs = [ ":public_include_path" ] public = [ "public/pw_i2c/initiator_mock.h" ] @@ -102,6 +124,7 @@ pw_test_group("tests") { ":device_test", ":initiator_mock_test", ":register_device_test", + ":i2c_service_test", ] } @@ -138,6 +161,18 @@ pw_test("initiator_mock_test") { ] } +pw_test("i2c_service_test") { + enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != "" + sources = [ "i2c_service_test.cc" ] + deps = [ + ":i2c_service", + "$dir_pw_containers:vector", + "$dir_pw_i2c:mock", + "$dir_pw_rpc/pwpb:test_method_context", + "$dir_pw_status", + ] +} + pw_doc_group("docs") { sources = [ "docs.rst" ] } diff --git a/pw_i2c/docs.rst b/pw_i2c/docs.rst index 4a96584f8..3b0b1605d 100644 --- a/pw_i2c/docs.rst +++ b/pw_i2c/docs.rst @@ -92,3 +92,42 @@ list. An example of this is shown below: pw::i2c::GmockInitiator ----------------------- gMock of Initiator used for testing and mocking out the Initiator. + +I2c Debug Service +================= +This module implements an I2C register access service for debugging and bringup. +To use, provide it with a callback function that returns an ``Initiator`` for +the specified ``bus_index``. + +Example invocations +------------------- +Using the pigweed console, you can invoke the service to perform an I2C read: + +.. code:: python + + device.rpcs.pw.i2c.I2c.I2cRead(bus_index=0, target_address=0x22, register_address=b'\x0e', read_size=1) + +The above shows reading register 0x0e on a device located at +I2C address 0x22. + +For peripherals that support 4 byte register width, you can specify as: + +.. code:: python + + device.rpcs.pw.i2c.I2c.I2cRead(bus_index=0, target_address=
, register_address=b'\x00\x00\x00\x00', read_size=4) + + +And similarly, for performing I2C write: + +.. code:: python + + device.rpcs.pw.i2c.I2c.I2cWrite(bus_index=0, target_address=0x22,register_address=b'\x0e', value=b'\xbc') + + +Similarly, multi-byte writes can also be specified with the bytes fields for +`register_address` and `value`. + +I2C peripherals that require multi-byte access may expect a specific endianness. +The order of bytes specified in the bytes field will match the order of bytes +sent/received on the bus. Maximum supported value for multi-byte access is +4 bytes. diff --git a/pw_i2c/i2c.options b/pw_i2c/i2c.options new file mode 100644 index 000000000..09e71ff60 --- /dev/null +++ b/pw_i2c/i2c.options @@ -0,0 +1,19 @@ +// Copyright 2023 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. + +pw.i2c.I2cWriteRequest.register_address max_size:4 +pw.i2c.I2cWriteRequest.value max_size:32 +pw.i2c.I2cReadRequest.register_address max_size:4 +pw.i2c.I2cReadResponse.value max_size:32 + diff --git a/pw_i2c/i2c.proto b/pw_i2c/i2c.proto new file mode 100644 index 000000000..40804108b --- /dev/null +++ b/pw_i2c/i2c.proto @@ -0,0 +1,52 @@ +// Copyright 2023 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. +syntax = "proto3"; + +package pw.i2c; + +message I2cWriteRequest { + // Which I2C initiator bus to communicate on. + uint32 bus_index = 1; + // 7-bit I2C target address to write to. + uint32 target_address = 2; + // Register address to write. Follow the endianness required by the + // peripheral for multi-byte address. + bytes register_address = 3; + // Value to write. Follow the endianness required by the peripheral. + bytes value = 4; +} + +message I2cWriteResponse {} + +message I2cReadRequest { + // Which I2C initiator bus to communicate on. + uint32 bus_index = 1; + // 7-bit I2C target address to read from. + uint32 target_address = 2; + // Register address to write. Follow the endianness required by the + // peripheral for multi-byte address. + bytes register_address = 3; + // Expected number of bytes from the peripheral. + uint32 read_size = 4; +} + +message I2cReadResponse { + bytes value = 1; +} + +service I2c { + // Enable access to I2C devices implementing register read/writes. + rpc I2cWrite(I2cWriteRequest) returns (I2cWriteResponse) {} + rpc I2cRead(I2cReadRequest) returns (I2cReadResponse) {} +} diff --git a/pw_i2c/i2c_service.cc b/pw_i2c/i2c_service.cc new file mode 100644 index 000000000..5d6da0559 --- /dev/null +++ b/pw_i2c/i2c_service.cc @@ -0,0 +1,90 @@ +// Copyright 2023 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_i2c/i2c_service.h" + +#include +#include + +#include "pw_assert/check.h" +#include "pw_chrono/system_clock.h" +#include "pw_containers/vector.h" +#include "pw_i2c/address.h" +#include "pw_rpc/pwpb/server_reader_writer.h" +#include "pw_status/status.h" + +namespace pw::i2c { +namespace { + +constexpr auto kI2cTimeout = + chrono::SystemClock::for_at_least(std::chrono::milliseconds(100)); + +} // namespace + +void I2cService::I2cWrite( + const pwpb::I2cWriteRequest::Message& request, + rpc::PwpbUnaryResponder& responder) { + Initiator* initiator = initiator_selector_(request.bus_index); + if (initiator == nullptr) { + responder.Finish({}, Status::InvalidArgument()).IgnoreError(); + return; + } + + // Get the underlying buffer size of the register_address and value fields. + pwpb::I2cWriteRequest::Message size_message; + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + constexpr auto kMaxWriteSize = + size_message.register_address.max_size() + size_message.value.max_size(); + + Vector write_buffer{}; + write_buffer.assign(std::begin(request.register_address), + std::end(request.register_address)); + std::copy(std::begin(request.value), + std::end(request.value), + std::back_inserter(write_buffer)); + auto result = initiator->WriteFor( + Address{static_cast(request.target_address)}, + write_buffer, + kI2cTimeout); + responder.Finish({}, result).IgnoreError(); +} + +void I2cService::I2cRead( + const pwpb::I2cReadRequest::Message& request, + rpc::PwpbUnaryResponder& responder) { + // Get the underlying buffer size of the ReadResponse message. + pwpb::I2cReadResponse::Message size_message; + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + constexpr auto kMaxReadSize = size_message.value.max_size(); + + Initiator* initiator = initiator_selector_(request.bus_index); + if (initiator == nullptr || request.read_size > kMaxReadSize) { + responder.Finish({}, Status::InvalidArgument()).IgnoreError(); + return; + } + Vector value{}; + value.resize(request.read_size); + auto result = initiator->WriteReadFor( + Address{static_cast(request.target_address)}, + request.register_address, + {value.data(), value.size()}, + kI2cTimeout); + + if (result.ok()) { + responder.Finish({value}, OkStatus()).IgnoreError(); + } else { + responder.Finish({}, result).IgnoreError(); + } +} + +} // namespace pw::i2c diff --git a/pw_i2c/i2c_service_test.cc b/pw_i2c/i2c_service_test.cc new file mode 100644 index 000000000..7f70dda35 --- /dev/null +++ b/pw_i2c/i2c_service_test.cc @@ -0,0 +1,245 @@ +// Copyright 2023 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_i2c/i2c_service.h" + +#include +#include + +#include "gtest/gtest.h" +#include "pw_i2c/address.h" +#include "pw_i2c/initiator.h" +#include "pw_i2c/initiator_mock.h" +#include "pw_rpc/pwpb/test_method_context.h" +#include "pw_status/status.h" + +namespace pw::i2c { +namespace { + +auto MakeSingletonSelector(Initiator* initiator) { + return [initiator](size_t pos) { return pos == 0 ? initiator : nullptr; }; +} + +TEST(I2cServiceTest, I2cWriteSingleByteOk) { + Vector register_addr{}; + Vector register_value{}; + constexpr auto kExpectWrite = bytes::Array<0x02, 0x03>(); + register_addr.push_back(kExpectWrite[0]); + register_value.push_back(kExpectWrite[1]); + auto transactions = MakeExpectedTransactionArray( + {Transaction(OkStatus(), + Address{0x01}, + kExpectWrite, + {}, + std::chrono::milliseconds(100))}); + MockInitiator i2c_initiator(transactions); + + PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite) + context{MakeSingletonSelector(&i2c_initiator)}; + + context.call({.bus_index = 0, + .target_address = 0x01, + .register_address = register_addr, + .value = register_value}); + EXPECT_TRUE(context.done()); + EXPECT_EQ(context.status(), OkStatus()); + EXPECT_EQ(i2c_initiator.Finalize(), OkStatus()); +} + +TEST(I2cServiceTest, I2cWriteMultiByteOk) { + constexpr int kWriteSize = 4; + Vector register_addr{}; + Vector register_value{}; + constexpr auto kExpectWrite = + bytes::Array<0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09>(); + std::copy(kExpectWrite.begin(), + kExpectWrite.begin() + kWriteSize, + std::back_inserter(register_addr)); + std::copy(kExpectWrite.begin() + kWriteSize, + kExpectWrite.end(), + std::back_inserter(register_value)); + auto transactions = MakeExpectedTransactionArray( + {Transaction(OkStatus(), + Address{0x01}, + kExpectWrite, + {}, + std::chrono::milliseconds(100))}); + MockInitiator i2c_initiator(transactions); + + PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite) + context{MakeSingletonSelector(&i2c_initiator)}; + + context.call({.bus_index = 0, + .target_address = 0x01, + .register_address = register_addr, + .value = register_value}); + EXPECT_TRUE(context.done()); + EXPECT_EQ(context.status(), OkStatus()); + EXPECT_EQ(i2c_initiator.Finalize(), OkStatus()); +} + +TEST(I2cServiceTest, I2cWriteInvalidBusIndex) { + Vector register_addr{}; + Vector register_value{}; + + MockInitiator i2c_initiator({}); + + PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite) + context{MakeSingletonSelector(&i2c_initiator)}; + + context.call({.bus_index = 1, + .target_address = 0x01, + .register_address = register_addr, + .value = register_value}); + EXPECT_TRUE(context.done()); + EXPECT_EQ(context.status(), Status::InvalidArgument()); + EXPECT_EQ(i2c_initiator.Finalize(), OkStatus()); +} + +TEST(I2cServiceTest, I2cReadSingleByteOk) { + constexpr auto kExpectWrite = bytes::Array<0x02>(); + constexpr auto kExpectRead = bytes::Array<0x03>(); + Vector register_addr{}; + register_addr.push_back(kExpectWrite[0]); + + auto transactions = MakeExpectedTransactionArray( + {Transaction(OkStatus(), + Address{0x01}, + kExpectWrite, + kExpectRead, + std::chrono::milliseconds(100))}); + MockInitiator i2c_initiator(transactions); + + PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead) + context{MakeSingletonSelector(&i2c_initiator)}; + + context.call({.bus_index = 0, + .target_address = 0x01, + .register_address = register_addr, + .read_size = static_cast(kExpectRead.size())}); + + EXPECT_TRUE(context.done()); + EXPECT_EQ(context.status(), OkStatus()); + for (size_t i = 0; i < kExpectRead.size(); ++i) { + EXPECT_EQ(kExpectRead[i], context.response().value[i]); + } + EXPECT_EQ(i2c_initiator.Finalize(), OkStatus()); +} + +TEST(I2cServiceTest, I2cReadMultiByteOk) { + constexpr auto kExpectWrite = bytes::Array<0x02, 0x04, 0x06, 0x08>(); + constexpr auto kExpectRead = bytes::Array<0x03, 0x05, 0x07, 0x09>(); + Vector register_addr{}; + std::copy(kExpectWrite.begin(), + kExpectWrite.end(), + std::back_inserter(register_addr)); + auto transactions = MakeExpectedTransactionArray( + {Transaction(OkStatus(), + Address{0x01}, + kExpectWrite, + kExpectRead, + std::chrono::milliseconds(100))}); + MockInitiator i2c_initiator(transactions); + + PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead) + context{MakeSingletonSelector(&i2c_initiator)}; + + context.call({.bus_index = 0, + .target_address = 0x01, + .register_address = register_addr, + .read_size = static_cast(kExpectRead.size())}); + + EXPECT_TRUE(context.done()); + EXPECT_EQ(context.status(), OkStatus()); + for (size_t i = 0; i < kExpectRead.size(); ++i) { + EXPECT_EQ(kExpectRead[i], context.response().value[i]); + } + EXPECT_EQ(i2c_initiator.Finalize(), OkStatus()); +} + +TEST(I2cServiceTest, I2cReadMaxByteOk) { + constexpr auto kExpectWrite = bytes::Array<0x02, 0x04, 0x06, 0x08>(); + constexpr auto kExpectRead = bytes::Array<0x03, 0x05, 0x07, 0x09>(); + pwpb::I2cReadResponse::Message size_message; + static_assert(sizeof(kExpectRead) <= size_message.value.max_size()); + + Vector register_addr{}; + std::copy(kExpectWrite.begin(), + kExpectWrite.end(), + std::back_inserter(register_addr)); + auto transactions = MakeExpectedTransactionArray( + {Transaction(OkStatus(), + Address{0x01}, + kExpectWrite, + kExpectRead, + std::chrono::milliseconds(100))}); + MockInitiator i2c_initiator(transactions); + + PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead) + context{MakeSingletonSelector(&i2c_initiator)}; + + context.call({ + .bus_index = 0, + .target_address = 0x01, + .register_address = register_addr, + .read_size = sizeof(kExpectRead), + }); + + EXPECT_TRUE(context.done()); + EXPECT_EQ(context.status(), OkStatus()); + // EXPECT_EQ(kExpectRead, context.response().value); + EXPECT_EQ(i2c_initiator.Finalize(), OkStatus()); +} + +TEST(I2cServiceTest, I2cReadMultiByteOutOfBounds) { + pwpb::I2cReadResponse::Message response_message; + constexpr auto kMaxReadSize = response_message.value.max_size(); + constexpr auto kRegisterAddr = bytes::Array<0x02, 0x04, 0x06, 0x08>(); + Vector register_addr{}; + std::copy(kRegisterAddr.begin(), + kRegisterAddr.end(), + std::back_inserter(register_addr)); + MockInitiator i2c_initiator({}); + + PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead) + context{MakeSingletonSelector(&i2c_initiator)}; + + context.call({.bus_index = 0, + .target_address = 0x01, + .register_address = register_addr, + .read_size = kMaxReadSize + 1}); + + EXPECT_TRUE(context.done()); + EXPECT_EQ(context.status(), Status::InvalidArgument()); + EXPECT_EQ(i2c_initiator.Finalize(), OkStatus()); +} + +TEST(I2cServiceTest, I2cReadInvalidBusIndex) { + Vector register_addr{}; + MockInitiator i2c_initiator({}); + + PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead) + context{MakeSingletonSelector(&i2c_initiator)}; + + context.call({.bus_index = 1, + .target_address = 0x01, + .register_address = register_addr, + .read_size = 1}); + + EXPECT_TRUE(context.done()); + EXPECT_EQ(context.status(), Status::InvalidArgument()); + EXPECT_EQ(i2c_initiator.Finalize(), OkStatus()); +} + +} // namespace +} // namespace pw::i2c diff --git a/pw_i2c/public/pw_i2c/i2c_service.h b/pw_i2c/public/pw_i2c/i2c_service.h new file mode 100644 index 000000000..7b029b1fd --- /dev/null +++ b/pw_i2c/public/pw_i2c/i2c_service.h @@ -0,0 +1,50 @@ +// Copyright 2023 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. +#pragma once + +#include +#include +#include +#include + +#include "pw_function/function.h" +#include "pw_i2c/i2c.pwpb.h" +#include "pw_i2c/i2c.rpc.pwpb.h" +#include "pw_i2c/initiator.h" +#include "pw_rpc/pwpb/server_reader_writer.h" + +namespace pw::i2c { + +// RPC service to perform I2C transactions. +class I2cService final : public pw_rpc::pwpb::I2c::Service { + public: + // Callback which returns an initiator for the given position or nullptr if + // the position not valid on this device. + using InitiatorSelector = pw::Function; + + explicit I2cService(InitiatorSelector&& initiator_selector) + : initiator_selector_(std::move(initiator_selector)) {} + + void I2cWrite( + const pwpb::I2cWriteRequest::Message& request, + pw::rpc::PwpbUnaryResponder& responder); + void I2cRead( + const pwpb::I2cReadRequest::Message& request, + pw::rpc::PwpbUnaryResponder& responder); + + private: + InitiatorSelector initiator_selector_; +}; + +} // namespace pw::i2c -- cgit v1.2.3