aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAustin Foxley <afoxley@google.com>2023-07-14 18:59:20 +0000
committerCQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-07-14 18:59:20 +0000
commitd80eb6af2d30a1c0fbdcc43b55e0b32eaf6b2f34 (patch)
tree01b59239a61800539a2ad39d8a050a0e54352c4b
parent629081583deb17187a1be40c2b61c863c5ad6b3c (diff)
downloadpigweed-d80eb6af2d30a1c0fbdcc43b55e0b32eaf6b2f34.tar.gz
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 <jrreinhart@google.com> Commit-Queue: Austin Foxley <afoxley@google.com> Reviewed-by: Carlos Chinchilla <cachinchilla@google.com>
-rw-r--r--pw_i2c/BUILD.bazel49
-rw-r--r--pw_i2c/BUILD.gn35
-rw-r--r--pw_i2c/docs.rst39
-rw-r--r--pw_i2c/i2c.options19
-rw-r--r--pw_i2c/i2c.proto52
-rw-r--r--pw_i2c/i2c_service.cc90
-rw-r--r--pw_i2c/i2c_service_test.cc245
-rw-r--r--pw_i2c/public/pw_i2c/i2c_service.h50
8 files changed, 579 insertions, 0 deletions
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=<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 <algorithm>
+#include <chrono>
+
+#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<pwpb::I2cWriteResponse::Message>& 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<std::byte, kMaxWriteSize> 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<uint16_t>(request.target_address)},
+ write_buffer,
+ kI2cTimeout);
+ responder.Finish({}, result).IgnoreError();
+}
+
+void I2cService::I2cRead(
+ const pwpb::I2cReadRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::I2cReadResponse::Message>& 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<std::byte, kMaxReadSize> value{};
+ value.resize(request.read_size);
+ auto result = initiator->WriteReadFor(
+ Address{static_cast<uint16_t>(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 <algorithm>
+#include <chrono>
+
+#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<std::byte, 4> register_addr{};
+ Vector<std::byte, 4> 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<std::byte, kWriteSize> register_addr{};
+ Vector<std::byte, kWriteSize> 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<std::byte, 4> register_addr{};
+ Vector<std::byte, 4> 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<std::byte, 4> 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<uint32_t>(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<std::byte, 4> 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<uint32_t>(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<std::byte, 4> 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<std::byte, 4> 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<std::byte, 4> 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 <array>
+#include <cstddef>
+#include <memory>
+#include <utility>
+
+#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<I2cService> {
+ public:
+ // Callback which returns an initiator for the given position or nullptr if
+ // the position not valid on this device.
+ using InitiatorSelector = pw::Function<Initiator*(size_t pos)>;
+
+ explicit I2cService(InitiatorSelector&& initiator_selector)
+ : initiator_selector_(std::move(initiator_selector)) {}
+
+ void I2cWrite(
+ const pwpb::I2cWriteRequest::Message& request,
+ pw::rpc::PwpbUnaryResponder<pwpb::I2cWriteResponse::Message>& responder);
+ void I2cRead(
+ const pwpb::I2cReadRequest::Message& request,
+ pw::rpc::PwpbUnaryResponder<pwpb::I2cReadResponse::Message>& responder);
+
+ private:
+ InitiatorSelector initiator_selector_;
+};
+
+} // namespace pw::i2c