Initial RGB Status, IMU, fuel guage, and IO expander tests

Change-Id: I583f019d942eac36420763d866c5a68dd9cdb337
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/kudzu/+/175352
Reviewed-by: Armando Montanez <amontanez@google.com>
diff --git a/applications/app_common/public/app_common/common.h b/applications/app_common/public/app_common/common.h
index f55d970..0ef81e4 100644
--- a/applications/app_common/public/app_common/common.h
+++ b/applications/app_common/public/app_common/common.h
@@ -28,6 +28,8 @@
   // any other methods in this class.
   static pw::Status Init();
 
+  static pw::Status FrameCallback();
+
   // Return an initialized display.
   static pw::display::Display& GetDisplay();
 
diff --git a/applications/app_common_impl/BUILD.gn b/applications/app_common_impl/BUILD.gn
index 062f9a4..59fb25c 100644
--- a/applications/app_common_impl/BUILD.gn
+++ b/applications/app_common_impl/BUILD.gn
@@ -146,10 +146,13 @@
   "$dir_pw_framebuffer_pool",
   "$dir_pw_ft6236",
   "$dir_pw_i2c_rp2040",
+  "$dir_pw_icm42670p",
   "$dir_pw_log",
+  "$dir_pw_max17948",
   "$dir_pw_spi_rp2040",
   "$dir_pw_sync:borrow",
   "$dir_pw_sync:mutex",
+  "$dir_pw_tca9535",
   "$dir_pw_thread:thread",
   "$dir_pw_thread_freertos:thread",
   "//applications/app_common:app_common.facade",
diff --git a/applications/app_common_impl/app_common_vars.gni b/applications/app_common_impl/app_common_vars.gni
index a5f6a44..8e39b2e 100644
--- a/applications/app_common_impl/app_common_vars.gni
+++ b/applications/app_common_impl/app_common_vars.gni
@@ -63,8 +63,8 @@
   pw_app_common_SPI_CLOCK_GPIO = ""
 
   # RP2040 I2C pins.
-  pw_app_common_I2C_BUS0_SCL = "8"
-  pw_app_common_I2C_BUS0_SDA = "9"
-  pw_app_common_I2C_BUS1_SCL = "2"
-  pw_app_common_I2C_BUS1_SDA = "3"
+  pw_app_common_I2C_BUS0_SCL = "9"
+  pw_app_common_I2C_BUS0_SDA = "8"
+  pw_app_common_I2C_BUS1_SCL = "3"
+  pw_app_common_I2C_BUS1_SDA = "2"
 }
diff --git a/applications/app_common_impl/common_host_imgui.cc b/applications/app_common_impl/common_host_imgui.cc
index 49a561f..1015ed9 100644
--- a/applications/app_common_impl/common_host_imgui.cc
+++ b/applications/app_common_impl/common_host_imgui.cc
@@ -57,6 +57,8 @@
 }  // namespace
 
 // static
+Status Common::FrameCallback() { return pw::OkStatus(); }
+
 Status Common::Init() { return s_display_driver.Init(); }
 
 // static
diff --git a/applications/app_common_impl/common_pico.cc b/applications/app_common_impl/common_pico.cc
index 0ef1785..7aada42 100644
--- a/applications/app_common_impl/common_pico.cc
+++ b/applications/app_common_impl/common_pico.cc
@@ -26,13 +26,16 @@
 #include "pw_digital_io_rp2040/digital_io.h"
 #include "pw_ft6236/device.h"
 #include "pw_i2c_rp2040/initiator.h"
+#include "pw_icm42670p/device.h"
 #include "pw_log/log.h"
+#include "pw_max17948/device.h"
 #include "pw_pixel_pusher_rp2040_pio/pixel_pusher.h"
 #include "pw_spi_rp2040/chip_selector.h"
 #include "pw_spi_rp2040/initiator.h"
 #include "pw_status/status.h"
 #include "pw_sync/borrow.h"
 #include "pw_sync/mutex.h"
+#include "pw_tca9535/device.h"
 #include "pw_thread/detached_thread.h"
 #include "pw_thread/thread.h"
 #include "pw_thread_freertos/context.h"
@@ -183,6 +186,30 @@
 }
 #endif
 
+const uint8_t kStatusPinRed = 23;
+const uint8_t kStatusPinGreen = 24;
+const uint8_t kStatusPinBlue = 25;
+
+void ConfigStatusRgb() {
+  std::array<uint8_t, 3> pwm_pins = {
+      kStatusPinRed, kStatusPinGreen, kStatusPinBlue};
+  for (auto& pin : pwm_pins) {
+    gpio_set_function(pin, GPIO_FUNC_PWM);
+    auto slice_num = pwm_gpio_to_slice_num(pin);
+    pwm_config cfg = pwm_get_default_config();
+    pwm_set_wrap(slice_num, 65535);
+    pwm_init(slice_num, &cfg, true);
+
+    pwm_set_gpio_level(pin, 65535);
+  }
+}
+
+void SetStatusRgb(uint8_t r, uint8_t g, uint8_t b) {
+  pwm_set_gpio_level(kStatusPinRed, 65535 - r * r);
+  pwm_set_gpio_level(kStatusPinGreen, 65535 - g * g);
+  pwm_set_gpio_level(kStatusPinBlue, 65535 - b * b);
+}
+
 SpiValues::SpiValues(pw::spi::Config config,
                      pw::spi::ChipSelector& selector,
                      pw::sync::VirtualMutex& initiator_mutex)
@@ -205,6 +232,9 @@
 pw::i2c::PicoInitiator i2c0_bus(ki2c0Config);
 pw::i2c::PicoInitiator i2c1_bus(ki2c1Config);
 
+pw::tca9535::Device io_expander(i2c1_bus);
+pw::icm42670p::Device imu(i2c0_bus);
+pw::max17948::Device fuel_guage(i2c0_bus);
 pw::ft6236::Device touch_screen_controller(i2c0_bus);
 Touchscreen s_touchscreen = Touchscreen(&touch_screen_controller);
 
@@ -213,8 +243,29 @@
     kDisplayDrawThreadStackWords>
     display_draw_thread_context;
 
+Rp2040DigitalInOut s_io_reset_n(10);
+Rp2040DigitalInOut s_imu_fsync(13);
+
 }  // namespace
 
+Status Common::FrameCallback() {
+  // touch_screen_controller.LogControllerInfo();
+
+  if (io_expander.Probe() == pw::OkStatus()) {
+    io_expander.LogControllerInfo();
+  }
+
+  if (fuel_guage.Probe() == pw::OkStatus()) {
+    fuel_guage.LogControllerInfo();
+  }
+
+  if (imu.Probe() == pw::OkStatus()) {
+    imu.LogControllerInfo();
+  }
+
+  return pw::OkStatus();
+}
+
 // static
 Status Common::Init() {
 #if OVERCLOCK_250
@@ -224,6 +275,10 @@
   set_sys_clock_khz(250000, false);
 #endif
 
+  ConfigStatusRgb();
+  // Set to a dim pink.
+  SetStatusRgb(32, 12, 32);
+
   s_display_cs_pin.Enable();
   s_display_dc_pin.Enable();
 #if DISPLAY_RESET_GPIO != -1
@@ -237,8 +292,31 @@
   i2c0_bus.Enable();
   i2c1_bus.Enable();
 
+  s_io_reset_n.Enable();
+  s_io_reset_n.SetStateActive();
+
+  s_imu_fsync.Enable();
+  s_imu_fsync.SetStateInactive();
+
   touch_screen_controller.Enable();
-  touch_screen_controller.LogControllerInfo();
+  if (touch_screen_controller.Probe() == pw::OkStatus()) {
+    touch_screen_controller.LogControllerInfo();
+  }
+
+  io_expander.Enable();
+  if (io_expander.Probe() == pw::OkStatus()) {
+    io_expander.LogControllerInfo();
+  }
+
+  fuel_guage.Enable();
+  if (fuel_guage.Probe() == pw::OkStatus()) {
+    fuel_guage.LogControllerInfo();
+  }
+
+  imu.Enable();
+  if (imu.Probe() == pw::OkStatus()) {
+    imu.LogControllerInfo();
+  }
 
 #if BACKLIGHT_GPIO != -1
   SetBacklight(0xffff);  // Full brightness.
diff --git a/applications/badge/main.cc b/applications/badge/main.cc
index f4118a0..eab46c1 100644
--- a/applications/badge/main.cc
+++ b/applications/badge/main.cc
@@ -142,6 +142,7 @@
 
   Touchscreen& touchscreen = Common::GetTouchscreen();
 
+  uint32_t frame_start_millis = pw::spin_delay::Millis();
   // The display loop.
   while (1) {
     frame_counter.StartFrame();
@@ -212,6 +213,11 @@
 
     // Every second make a log message.
     frame_counter.EndFrame();
+
+    if (pw::spin_delay::Millis() > frame_start_millis + 10000) {
+      Common::FrameCallback();
+      frame_start_millis = pw::spin_delay::Millis();
+    }
   }
 }
 
diff --git a/build_overrides/pigweed.gni b/build_overrides/pigweed.gni
index 4bea6b9..60067b8 100644
--- a/build_overrides/pigweed.gni
+++ b/build_overrides/pigweed.gni
@@ -31,4 +31,7 @@
       get_path_info("../lib/pw_touchscreen_null", "abspath")
   dir_pw_touchscreen_ft6236 =
       get_path_info("../lib/pw_touchscreen_ft6236", "abspath")
+  dir_pw_tca9535 = get_path_info("../lib/pw_tca9535", "abspath")
+  dir_pw_max17948 = get_path_info("../lib/pw_max17948", "abspath")
+  dir_pw_icm42670p = get_path_info("../lib/pw_icm42670p", "abspath")
 }
diff --git a/lib/pw_icm42670p/BUILD.gn b/lib/pw_icm42670p/BUILD.gn
new file mode 100644
index 0000000..a24ca1a
--- /dev/null
+++ b/lib/pw_icm42670p/BUILD.gn
@@ -0,0 +1,36 @@
+# 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.
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_icm42670p") {
+  public_configs = [ ":default_config" ]
+  public_deps = [
+    "$dir_pw_i2c:initiator",
+    "$dir_pw_status",
+  ]
+  public = [ "public/pw_icm42670p/device.h" ]
+  deps = [
+    "$dir_pw_digital_io",
+    "$dir_pw_i2c:register_device",
+    "$dir_pw_log",
+  ]
+  sources = [ "device.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/lib/pw_icm42670p/device.cc b/lib/pw_icm42670p/device.cc
new file mode 100644
index 0000000..31faab8
--- /dev/null
+++ b/lib/pw_icm42670p/device.cc
@@ -0,0 +1,103 @@
+// 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_icm42670p/device.h"
+
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+
+#define PW_LOG_MODULE_NAME "pw_icm42670p"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
+
+#include "pw_bytes/bit.h"
+#include "pw_i2c/address.h"
+#include "pw_i2c/register_device.h"
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+
+using ::pw::Status;
+using namespace std::chrono_literals;
+
+namespace pw::icm42670p {
+
+namespace {
+
+constexpr pw::i2c::Address kAddress = pw::i2c::Address::SevenBit<0x68>();
+
+void ImuReadReg(pw::i2c::RegisterDevice& device,
+                uint8_t addr,
+                const char* name) {
+  auto data =
+      device.ReadRegister8(addr, pw::chrono::SystemClock::for_at_least(10ms));
+
+  if (data.ok()) {
+    PW_LOG_INFO("%s: %02x", name, *data);
+  } else {
+    PW_LOG_INFO("failed to read %s", name);
+  }
+}
+
+void ImuReadReg16(pw::i2c::RegisterDevice& device,
+                  uint8_t addr,
+                  const char* name) {
+  auto data =
+      device.ReadRegister16(addr, pw::chrono::SystemClock::for_at_least(10ms));
+
+  if (data.ok()) {
+    PW_LOG_INFO("%s: %04x", name, *data);
+  } else {
+    PW_LOG_INFO("failed to read %s", name);
+  }
+}
+
+}  // namespace
+
+Device::Device(pw::i2c::Initiator& initiator)
+    : initiator_(initiator),
+      device_(initiator,
+              kAddress,
+              endian::little,
+              pw::i2c::RegisterAddressSize::k1Byte) {}
+
+Status Device::Enable() {
+  device_.WriteRegister8(
+      0x1f, 0x0f, pw::chrono::SystemClock::for_at_least(10ms));
+
+  return OkStatus();
+}
+
+Status Device::Probe() {
+  pw::Status probe_result(initiator_.ProbeDeviceFor(
+      kAddress, pw::chrono::SystemClock::for_at_least(50ms)));
+
+  if (probe_result != pw::OkStatus()) {
+    PW_LOG_DEBUG("ICM-42670-P Probe Failed");
+  } else {
+    PW_LOG_DEBUG("ICM-42670-P Probe Ok");
+  }
+  return probe_result;
+}
+
+void Device::LogControllerInfo() {
+  device_.WriteRegister8(
+      0x1f, 0x0f, pw::chrono::SystemClock::for_at_least(10ms));
+
+  ImuReadReg(device_, 0x75, "WHO_AM_I");
+  ImuReadReg(device_, 0x1f, "PWR_MGMT0");
+  ImuReadReg(device_, 0x0c, "X0");
+  ImuReadReg(device_, 0x0b, "X1");
+}
+
+}  // namespace pw::icm42670p
diff --git a/lib/pw_icm42670p/public/pw_icm42670p/device.h b/lib/pw_icm42670p/public/pw_icm42670p/device.h
new file mode 100644
index 0000000..288e7a1
--- /dev/null
+++ b/lib/pw_icm42670p/public/pw_icm42670p/device.h
@@ -0,0 +1,40 @@
+// 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 <cstdint>
+
+#include "pw_i2c/address.h"
+#include "pw_i2c/initiator.h"
+#include "pw_i2c/register_device.h"
+#include "pw_status/status.h"
+
+namespace pw::icm42670p {
+
+class Device {
+ public:
+  Device(pw::i2c::Initiator& initiator);
+  ~Device() = default;
+
+  Status Enable();
+  Status Probe();
+  void LogControllerInfo();
+
+ private:
+  pw::i2c::Initiator& initiator_;
+  pw::i2c::RegisterDevice device_;
+};
+
+}  // namespace pw::icm42670p
diff --git a/lib/pw_max17948/BUILD.gn b/lib/pw_max17948/BUILD.gn
new file mode 100644
index 0000000..b3a6d66
--- /dev/null
+++ b/lib/pw_max17948/BUILD.gn
@@ -0,0 +1,36 @@
+# 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.
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_max17948") {
+  public_configs = [ ":default_config" ]
+  public_deps = [
+    "$dir_pw_i2c:initiator",
+    "$dir_pw_status",
+  ]
+  public = [ "public/pw_max17948/device.h" ]
+  deps = [
+    "$dir_pw_digital_io",
+    "$dir_pw_i2c:register_device",
+    "$dir_pw_log",
+  ]
+  sources = [ "device.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/lib/pw_max17948/device.cc b/lib/pw_max17948/device.cc
new file mode 100644
index 0000000..6b92b14
--- /dev/null
+++ b/lib/pw_max17948/device.cc
@@ -0,0 +1,99 @@
+// 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_max17948/device.h"
+
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+
+#define PW_LOG_MODULE_NAME "pw_max17948"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
+
+#include "pw_bytes/bit.h"
+#include "pw_i2c/address.h"
+#include "pw_i2c/register_device.h"
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+
+using ::pw::Status;
+using namespace std::chrono_literals;
+
+namespace pw::max17948 {
+
+namespace {
+
+constexpr pw::i2c::Address kAddress = pw::i2c::Address::SevenBit<0x36>();
+
+void FuelReadReg(pw::i2c::RegisterDevice& device,
+                 uint8_t addr,
+                 const char* name) {
+  auto data =
+      device.ReadRegister16(addr, pw::chrono::SystemClock::for_at_least(10ms));
+
+  if (data.ok()) {
+    PW_LOG_INFO("%s: %04x", name, *data);
+  } else {
+    PW_LOG_INFO("failed to read %s", name);
+  }
+}
+
+}  // namespace
+
+Device::Device(pw::i2c::Initiator& initiator)
+    : initiator_(initiator),
+      device_(initiator,
+              kAddress,
+              endian::little,
+              pw::i2c::RegisterAddressSize::k1Byte) {}
+
+Status Device::Enable() {
+  device_.WriteRegister8(
+      0x1f, 0x0f, pw::chrono::SystemClock::for_at_least(10ms));
+
+  return OkStatus();
+}
+
+Status Device::Probe() {
+  pw::Status probe_result(initiator_.ProbeDeviceFor(
+      kAddress, pw::chrono::SystemClock::for_at_least(10ms)));
+
+  if (probe_result != pw::OkStatus()) {
+    PW_LOG_DEBUG("MAX17948 Probe Failed");
+  } else {
+    PW_LOG_DEBUG("MAX17948 Probe Ok");
+  }
+  return probe_result;
+}
+
+void Device::LogControllerInfo() {
+  auto data =
+      device_.ReadRegister16(0x2, pw::chrono::SystemClock::for_at_least(10ms));
+  if (data.ok()) {
+    PW_LOG_INFO("VCELL: %d mV", static_cast<uint32_t>(*data) * 78125 / 1000000);
+  } else {
+    PW_LOG_INFO("failed to read VCELL");
+  }
+  FuelReadReg(device_, 0x4, "SOC");
+  FuelReadReg(device_, 0x6, "MODE");
+  FuelReadReg(device_, 0x8, "VERSION");
+  FuelReadReg(device_, 0xa, "HIBRT");
+  FuelReadReg(device_, 0xc, "CONFIG");
+  FuelReadReg(device_, 0x14, "VALRT");
+  FuelReadReg(device_, 0x16, "CRATE");
+  FuelReadReg(device_, 0x18, "VRESET/ID");
+  FuelReadReg(device_, 0x1a, "STATUS");
+}
+
+}  // namespace pw::max17948
diff --git a/lib/pw_max17948/public/pw_max17948/device.h b/lib/pw_max17948/public/pw_max17948/device.h
new file mode 100644
index 0000000..6f21234
--- /dev/null
+++ b/lib/pw_max17948/public/pw_max17948/device.h
@@ -0,0 +1,40 @@
+// 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 <cstdint>
+
+#include "pw_i2c/address.h"
+#include "pw_i2c/initiator.h"
+#include "pw_i2c/register_device.h"
+#include "pw_status/status.h"
+
+namespace pw::max17948 {
+
+class Device {
+ public:
+  Device(pw::i2c::Initiator& initiator);
+  ~Device() = default;
+
+  Status Enable();
+  Status Probe();
+  void LogControllerInfo();
+
+ private:
+  pw::i2c::Initiator& initiator_;
+  pw::i2c::RegisterDevice device_;
+};
+
+}  // namespace pw::max17948
diff --git a/lib/pw_tca9535/BUILD.gn b/lib/pw_tca9535/BUILD.gn
new file mode 100644
index 0000000..14db013
--- /dev/null
+++ b/lib/pw_tca9535/BUILD.gn
@@ -0,0 +1,36 @@
+# 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.
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_tca9535") {
+  public_configs = [ ":default_config" ]
+  public_deps = [
+    "$dir_pw_i2c:initiator",
+    "$dir_pw_status",
+  ]
+  public = [ "public/pw_tca9535/device.h" ]
+  deps = [
+    "$dir_pw_digital_io",
+    "$dir_pw_i2c:register_device",
+    "$dir_pw_log",
+  ]
+  sources = [ "device.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/lib/pw_tca9535/device.cc b/lib/pw_tca9535/device.cc
new file mode 100644
index 0000000..e32c456
--- /dev/null
+++ b/lib/pw_tca9535/device.cc
@@ -0,0 +1,91 @@
+// 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_tca9535/device.h"
+
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+
+#define PW_LOG_MODULE_NAME "pw_tca9535"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
+
+#include "pw_bytes/bit.h"
+#include "pw_i2c/address.h"
+#include "pw_i2c/register_device.h"
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+
+using ::pw::Status;
+using namespace std::chrono_literals;
+
+namespace pw::tca9535 {
+
+namespace {
+
+constexpr pw::i2c::Address kAddress = pw::i2c::Address::SevenBit<0x20>();
+
+}  // namespace
+
+Device::Device(pw::i2c::Initiator& initiator)
+    : initiator_(initiator),
+      device_(initiator,
+              kAddress,
+              endian::little,
+              pw::i2c::RegisterAddressSize::k1Byte) {}
+
+Status Device::Enable() {
+  // Port 0 is all button input.
+  device_.WriteRegister8(
+      0x6, 0xff, pw::chrono::SystemClock::for_at_least(10ms));
+  // Port 1 pins 6 and 7 are DISP_RESET and TOUCH_RESET which should be high.
+  device_.WriteRegister8(
+      0x3, 0xff, pw::chrono::SystemClock::for_at_least(10ms));
+  device_.WriteRegister8(
+      0x7, 0x3f, pw::chrono::SystemClock::for_at_least(10ms));
+
+  return OkStatus();
+}
+
+Status Device::Probe() {
+  pw::Status probe_result(initiator_.ProbeDeviceFor(
+      kAddress, pw::chrono::SystemClock::for_at_least(10ms)));
+
+  if (probe_result != pw::OkStatus()) {
+    PW_LOG_DEBUG("TCA9535 Probe Failed");
+  } else {
+    PW_LOG_DEBUG("TCA9535 Probe Ok");
+  }
+  return probe_result;
+}
+
+void Device::LogControllerInfo() {
+  auto port0 =
+      device_.ReadRegister8(0x0, pw::chrono::SystemClock::for_at_least(10ms));
+  if (port0.ok()) {
+    PW_LOG_INFO("port 0: %02x", *port0);
+  } else {
+    PW_LOG_INFO("port 0 read failed");
+  }
+
+  auto port1 =
+      device_.ReadRegister8(0x1, pw::chrono::SystemClock::for_at_least(10ms));
+  if (port1.ok()) {
+    PW_LOG_INFO("port 1: %02x", *port1);
+  } else {
+    PW_LOG_INFO("port 1 read failed");
+  }
+}
+
+}  // namespace pw::tca9535
diff --git a/lib/pw_tca9535/public/pw_tca9535/device.h b/lib/pw_tca9535/public/pw_tca9535/device.h
new file mode 100644
index 0000000..c571beb
--- /dev/null
+++ b/lib/pw_tca9535/public/pw_tca9535/device.h
@@ -0,0 +1,40 @@
+// 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 <cstdint>
+
+#include "pw_i2c/address.h"
+#include "pw_i2c/initiator.h"
+#include "pw_i2c/register_device.h"
+#include "pw_status/status.h"
+
+namespace pw::tca9535 {
+
+class Device {
+ public:
+  Device(pw::i2c::Initiator& initiator);
+  ~Device() = default;
+
+  Status Enable();
+  Status Probe();
+  void LogControllerInfo();
+
+ private:
+  pw::i2c::Initiator& initiator_;
+  pw::i2c::RegisterDevice device_;
+};
+
+}  // namespace pw::tca9535
diff --git a/targets/rp2040/BUILD.gn b/targets/rp2040/BUILD.gn
index 4b82865..71be1b6 100644
--- a/targets/rp2040/BUILD.gn
+++ b/targets/rp2040/BUILD.gn
@@ -88,7 +88,7 @@
 
     # Kudzu pin assignments.
     pw_app_common_BACKLIGHT_GPIO = "15"
-    pw_app_common_DISPLAY_TE_GPIO = "21"
+    pw_app_common_DISPLAY_TE_GPIO = "16"
     pw_app_common_DISPLAY_CS_GPIO = "17"
     pw_app_common_DISPLAY_DC_GPIO = "20"
     pw_app_common_SPI_MOSI_GPIO = "19"