DO NOT SUBMIT: Add SDMMC hardware test

Change-Id: Ia4c4685a713094ce776b43fd0a3232347656e650
diff --git a/src/devices/block/drivers/sdmmc/BUILD.gn b/src/devices/block/drivers/sdmmc/BUILD.gn
index 844582e..abee53a 100644
--- a/src/devices/block/drivers/sdmmc/BUILD.gn
+++ b/src/devices/block/drivers/sdmmc/BUILD.gn
@@ -141,5 +141,6 @@
   deps = [
     ":sdmmc-bind_test",
     ":sdmmc-test",
+    "hardware-test",
   ]
 }
diff --git a/src/devices/block/drivers/sdmmc/hardware-test/BUILD.gn b/src/devices/block/drivers/sdmmc/hardware-test/BUILD.gn
new file mode 100644
index 0000000..94910f5
--- /dev/null
+++ b/src/devices/block/drivers/sdmmc/hardware-test/BUILD.gn
@@ -0,0 +1,38 @@
+# Copyright 2021 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/components.gni")
+
+executable("hardware-test-bin") {
+  output_name = "sdmmc_hardware_test"
+  sources = [
+    "hardware-test.cc",
+    "main.cc",
+    "sdmmc-test-device-controller.cc",
+  ]
+  deps = [
+    "//sdk/banjo/fuchsia.hardware.sdio:fuchsia.hardware.sdio_banjo_cpp",
+    "//sdk/fidl/fuchsia.device:fuchsia.device_llcpp",
+    "//sdk/fidl/fuchsia.hardware.gpio:fuchsia.hardware.gpio_llcpp",
+    "//sdk/fidl/fuchsia.hardware.i2c:fuchsia.hardware.i2c_llcpp",
+    "//sdk/fidl/fuchsia.hardware.sdio:fuchsia.hardware.sdio_llcpp",
+    "//sdk/fidl/fuchsia.sysinfo:fuchsia.sysinfo_llcpp",
+    "//sdk/lib/fdio",
+    "//zircon/system/ulib/fbl",
+    "//zircon/system/ulib/hwreg",
+    "//zircon/system/ulib/zxc",
+    "//zircon/system/ulib/zxtest",
+  ]
+}
+
+# fuchsia_test_component("hardware-test-component") {
+#   component_name = "sdmmc-hardware-test"
+#   manifest = "hardware-test.cml"
+#   deps = [ ":hardware-test-bin" ]
+# }
+
+fuchsia_shell_package("hardware-test") {
+  package_name = "sdmmc-hardware-test"
+  deps = [ ":hardware-test-bin" ]
+}
diff --git a/src/devices/block/drivers/sdmmc/hardware-test/hardware-test.cc b/src/devices/block/drivers/sdmmc/hardware-test/hardware-test.cc
new file mode 100644
index 0000000..ada5e3d
--- /dev/null
+++ b/src/devices/block/drivers/sdmmc/hardware-test/hardware-test.cc
@@ -0,0 +1,341 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <dirent.h>
+#include <fidl/fuchsia.device/cpp/wire.h>
+#include <fidl/fuchsia.hardware.i2c/cpp/wire.h>
+#include <fidl/fuchsia.hardware.sdio/cpp/wire.h>
+#include <fuchsia/hardware/sdio/c/banjo.h>
+#include <lib/fdio/fdio.h>
+#include <lib/fidl/llcpp/arena.h>
+#include <lib/zx/clock.h>
+#include <lib/zx/time.h>
+#include <stdio.h>
+#include <sys/types.h>
+
+#include <vector>
+
+#include <fbl/unique_fd.h>
+#include <zxtest/zxtest.h>
+
+#include "sdmmc-test-device-controller-regs.h"
+#include "sdmmc-test-device-controller.h"
+
+namespace sdmmc {
+
+class SdmmcHardwareTest : public zxtest::Test {
+ public:
+  void SetUp() override {
+    fidl::WireSyncClient i2c =
+        GetFidlClient<fuchsia_hardware_i2c::Device2>(kControllerI2cDevicePath);
+    controller_ = SdmmcTestDeviceController(std::move(i2c));
+    ASSERT_TRUE(controller_.is_valid(), "Failed to connect to I2C device");
+
+    // Reset the test device controller. This will probably fail because the controller doesn't ack
+    // on I2C before resetting.
+    CoreControl::Get().FromValue(0).set_por_reset(1).WriteTo(controller_);
+
+    const zx::time start = zx::clock::get_monotonic();
+    while (CoreControl::Get().FromValue(0).ReadFrom(controller_) != ZX_OK) {
+    }
+    printf("Took %luus for core to reset\n", (zx::clock::get_monotonic() - start).to_usecs());
+
+    EXPECT_OK(CoreControl::Get().FromValue(0).set_core_enable(1).WriteTo(controller_));
+
+    // 0x0000 is reserved, set RCA to 0x0001 instead.
+    EXPECT_OK(Rca0::Get().FromValue(1).WriteTo(controller_));
+    EXPECT_OK(Rca1::Get().FromValue(0).WriteTo(controller_));
+
+    // Support the entire voltage range.
+    EXPECT_OK(Ocr0::Get().FromValue(0).WriteTo(controller_));
+    EXPECT_OK(Ocr1::Get().FromValue(0b1111'1111).WriteTo(controller_));
+    EXPECT_OK(Ocr2::Get().FromValue(0b1111'1111).WriteTo(controller_));
+
+    SetupCis();
+  }
+
+ protected:
+  // TODO: Extract these into a device-specific config
+  static constexpr char kControllerI2cDevicePath[] =
+      "/dev/sys/platform/05:00:2/aml-i2c/i2c/i2c-1-50";
+  static constexpr char kSdmmcDevicePath[] = "/dev/aml_sd";
+
+  void SetupCis() {
+    // CCCR
+    EXPECT_OK(controller_.WriteFunction(
+        0, 0x0000,
+        {
+            // clang-format disable
+            /* [000] */ 0x43, 0x03, 0x00, 0x06,  // SDIO spec 3.00, functions 1 and 2 ready
+            /* [004] */ 0x00, 0x00, 0x00, 0x00,
+            // ----- BUG ----- Must set 4BLS for core driver to register four-bit bus capability.
+            /* [008] */ 0x83, 0x00, 0x10, 0x00,  // cmd52/cmd53 supported, CIS pointer (0x1000)
+            /* [00c] */ 0x00, 0x00, 0x00, 0x00,
+            /* [010] */ 0x00, 0x00, 0x00, 0x01,  // No function 0 block ops, high speed supported
+            /* [014] */ 0x07, 0x00, 0x00, 0x00,  // SDR50/SDR104/DDR50 supported
+            // clang-format enable
+        }));
+
+    // FBR function 1
+    EXPECT_OK(controller_.WriteFunction(
+        0, 0x0100,
+        {
+            // clang-format disable
+            /* [100] */ 0x00, 0x00, 0x00, 0x00,
+            /* [104] */ 0x00, 0x00, 0x00, 0x00,
+            /* [108] */ 0x00, 0x0d, 0x10, 0x00,  // CIS pointer (0x100d)
+            /* [10c] */ 0x00, 0x00, 0x00, 0x00,
+            /* [110] */ 0x00, 0x00, 0x00, 0x00,  // Block size set to zero initially
+            // clang-format enable
+        }));
+
+    // FBR function 2
+    EXPECT_OK(controller_.WriteFunction(
+        0, 0x0200,
+        {
+            // clang-format disable
+            /* [200] */ 0x00, 0x00, 0x00, 0x00,
+            /* [204] */ 0x00, 0x00, 0x00, 0x00,
+            /* [208] */ 0x00, 0x0d, 0x10, 0x00,  // CIS pointer (0x100d)
+            /* [20c] */ 0x00, 0x00, 0x00, 0x00,
+            /* [210] */ 0x00, 0x00, 0x00, 0x00,  // Block size set to zero initially
+            // clang-format enable
+        }));
+
+    EXPECT_OK(controller_.WriteFunction(
+        0, 0x1000,
+        {
+            // clang-format disable
+            // CISTPL_FUNCE for function 0: 4 bytes, block size 256, max transfer rate 200Mbit/s.
+            0x22, 0x04, 0x00, 0x00, 0x01, 0b101'011,
+            // CISTPL_MANFID for function 0: 4 bytes, manufacturer ID 0x0000, product ID 0x0000.
+            0x20, 0x04, 0x00, 0x00, 0x00, 0x00,
+            // CISTPL_END
+            0xff,
+            // CISTPL_FUNCE for I/O functions: 42 bytes, block size 512.
+            0x22, 0x2a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00,
+            // CISTPL_MANFID for function 0: 4 bytes, manufacturer ID 0x0000, product ID 0x0000.
+            0x20, 0x04, 0x00, 0x00, 0x00, 0x00,
+            // CISTPL_END
+            0xff,
+            // clang-format enable
+        }));
+  }
+
+  static void RestartSdmmcDriver() {
+    fidl::WireSyncClient sdmmc_device = GetFidlClient<fuchsia_device::Controller>(kSdmmcDevicePath);
+    ASSERT_TRUE(sdmmc_device.is_valid());
+
+    const auto response = sdmmc_device->Rebind(fidl::StringView());
+    ASSERT_TRUE(response.ok());
+    EXPECT_TRUE(response->result.is_response());
+  }
+
+  std::vector<fidl::WireSyncClient<fuchsia_hardware_sdio::Device>> GetTestSdioFidlClients(
+      const uint8_t max_function) {
+    if (max_function == 0 || max_function > 7) {
+      return {};
+    }
+
+    std::vector<fidl::WireSyncClient<fuchsia_hardware_sdio::Device>> clients(max_function);
+    for (uint8_t i = 1; i <= max_function; i++) {
+      // Loop indefinitely trying to get the client. We have to wait for SDIO intialization to
+      // complete for the devices to be created, which may take some time. If initialization errors
+      // occur let the test runner time out and fail the test.
+      while (!clients[i - 1].is_valid()) {
+        clients[i - 1] = GetTestSdioFidlClient(i);
+      }
+    }
+
+    return clients;
+  }
+
+  SdmmcTestDeviceController controller_;
+
+ private:
+  // Attempts to get a client of the specified function device for the SDIO test rig. Returns an
+  // invalid client on any errors.
+  static fidl::WireSyncClient<fuchsia_hardware_sdio::Device> GetTestSdioFidlClient(
+      const uint8_t function) {
+#if 0
+    constexpr uint32_t kTestManufacturerId = 0x02d0;
+    constexpr uint32_t kTestProductId = 0x4359;
+#endif
+    constexpr uint32_t kTestManufacturerId = 0x0000;
+    constexpr uint32_t kTestProductId = 0x0000;
+
+    if (function == 0) {
+      return {};
+    }
+
+    DIR* const sdio_dir = opendir("/dev/class/sdio");
+    if (sdio_dir == nullptr) {
+      return {};
+    }
+
+    fidl::WireSyncClient<fuchsia_hardware_sdio::Device> function_client = {};
+    struct dirent* entry = readdir(sdio_dir);
+    for (; entry != nullptr; entry = readdir(sdio_dir)) {
+      fbl::unique_fd device(openat(dirfd(sdio_dir), entry->d_name, O_RDWR));
+      if (!device.is_valid()) {
+        continue;
+      }
+
+      fidl::WireSyncClient client = GetFidlClient<fuchsia_hardware_sdio::Device>(std::move(device));
+      if (!client.is_valid()) {
+        continue;
+      }
+
+      const auto response = client->GetDevHwInfo();
+      if (response->result.is_err()) {
+        continue;
+      }
+      if (response->result.response().function != function) {
+        continue;
+      }
+
+      const auto& hw_info = response->result.response().hw_info;
+      if (hw_info.dev_hw_info.num_funcs <= function) {
+        continue;
+      }
+
+      const auto& function_info = hw_info.funcs_hw_info[function];
+      if (function_info.manufacturer_id == kTestManufacturerId &&
+          function_info.product_id == kTestProductId) {
+        function_client = std::move(client);
+        break;
+      }
+    }
+
+    closedir(sdio_dir);
+    return function_client;
+  }
+};
+
+TEST_F(SdmmcHardwareTest, InitSuccess) {
+  // Re-bind the SDMMC driver to initialize with the new settings.
+  printf("Restarting SDMMC driver\n");
+  RestartSdmmcDriver();
+  printf("Done, waiting for FPGA init success\n");
+
+  EXPECT_OK(CoreStatus::WaitForInitSuccess(controller_));
+  printf("Done, waiting for SDIO clients\n");
+
+  auto clients = GetTestSdioFidlClients(2);
+  ASSERT_EQ(clients.size(), 2);
+  printf("Done\n");
+
+  // ----- BUG ----- Core driver doesn't set SDIO_CARD_DIRECT_COMMAND capability.
+  constexpr uint32_t kExpectedSdioCaps =
+      SDIO_CARD_MULTI_BLOCK | /* SDIO_CARD_DIRECT_COMMAND | */ SDIO_CARD_HIGH_SPEED |
+      SDIO_CARD_FOUR_BIT_BUS | SDIO_CARD_UHS_SDR50 | SDIO_CARD_UHS_SDR104 | SDIO_CARD_UHS_DDR50;
+
+  for (const auto& client : clients) {
+    // TODO: Read CCCR and make sure all registers were set correctly
+    const auto response = client->GetDevHwInfo();
+    ASSERT_TRUE(response.ok());
+    ASSERT_TRUE(response->result.is_response());
+
+    const auto& hw_info = response->result.response().hw_info;
+    EXPECT_EQ(hw_info.dev_hw_info.num_funcs, 3);  // Includes function 0
+    EXPECT_EQ(hw_info.dev_hw_info.sdio_vsn, 4);   // Version 3.00
+    EXPECT_EQ(hw_info.dev_hw_info.cccr_vsn, 3);   // Version 3.00
+    EXPECT_EQ(hw_info.dev_hw_info.caps, kExpectedSdioCaps);
+
+    for (uint32_t i = 0; i < 3; i++) {
+      EXPECT_EQ(hw_info.funcs_hw_info[i].manufacturer_id, 0);
+      EXPECT_EQ(hw_info.funcs_hw_info[i].product_id, 0);
+      if (i == 0) {
+        EXPECT_EQ(hw_info.funcs_hw_info[i].max_blk_size, 256);
+        EXPECT_EQ(hw_info.funcs_hw_info[i].max_tran_speed, 200'000);
+      } else {
+        EXPECT_EQ(hw_info.funcs_hw_info[i].max_blk_size, 512);
+      }
+      EXPECT_EQ(hw_info.funcs_hw_info[i].fn_intf_code, 0);
+    }
+  }
+}
+
+TEST_F(SdmmcHardwareTest, InitSuccessWithCmd52Retries) {
+  auto control = CoreControl::Get().FromValue(0);
+  EXPECT_OK(control.ReadFrom(controller_));
+  EXPECT_OK(control.set_error_injection_enable(1).WriteTo(controller_));
+
+  EXPECT_OK(CrcErrorControl::Get().FromValue(0).set_cmd52_crc_error_enable(1).WriteTo(controller_));
+  EXPECT_OK(
+      Cmd52ErrorControl::Get().FromValue(0).set_transfers_until_crc_error(5).WriteTo(controller_));
+
+  // Re-bind the SDMMC driver to initialize with the new settings.
+  printf("Restarting SDMMC driver\n");
+  RestartSdmmcDriver();
+  printf("Done, waiting for FPGA init success\n");
+
+  EXPECT_OK(CoreStatus::WaitForInitSuccess(controller_));
+  printf("Done, waiting for SDIO clients\n");
+
+  auto clients = GetTestSdioFidlClients(2);
+  ASSERT_EQ(clients.size(), 2);
+  printf("Done\n");
+}
+
+TEST_F(SdmmcHardwareTest, InitFailureCmd52Errors) {
+  auto control = CoreControl::Get().FromValue(0);
+  EXPECT_OK(control.ReadFrom(controller_));
+  EXPECT_OK(control.set_error_injection_enable(1).WriteTo(controller_));
+
+  EXPECT_OK(CrcErrorControl::Get().FromValue(0).set_cmd52_crc_error_enable(1).WriteTo(controller_));
+  EXPECT_OK(Cmd52ErrorControl::Get().FromValue(0).WriteTo(controller_));
+
+  // Re-bind the SDMMC driver to initialize with the new settings.
+  printf("Restarting SDMMC driver\n");
+  RestartSdmmcDriver();
+  printf("Done, waiting for FPGA init failure\n");
+
+  EXPECT_OK(CoreStatus::WaitForInitFailure(controller_));
+  printf("Done\n");
+}
+
+TEST_F(SdmmcHardwareTest, ReadCccr) {
+  // Re-bind the SDMMC driver to initialize with the new settings.
+  printf("Restarting SDMMC driver\n");
+  RestartSdmmcDriver();
+  printf("Done, waiting for FPGA init success\n");
+
+  EXPECT_OK(CoreStatus::WaitForInitSuccess(controller_));
+  printf("Done, waiting for SDIO clients\n");
+
+  auto clients = GetTestSdioFidlClients(2);
+  ASSERT_EQ(clients.size(), 2);
+  printf("Done\n");
+
+  zx::status<uint8_t> io_enable = controller_.ReadFunction(0, 0x2);
+  ASSERT_TRUE(io_enable.is_ok());
+  EXPECT_EQ(io_enable.value(), 0b110);  // Functions 1 and 2 should be enabled.
+}
+
+TEST_F(SdmmcHardwareTest, ReadWriteCccr) {
+  // Write a reserved bus width value, and make sure it changes after init.
+  EXPECT_OK(controller_.WriteFunction(0, 0x7, {0b01}));
+
+  // Re-bind the SDMMC driver to initialize with the new settings.
+  printf("Restarting SDMMC driver\n");
+  RestartSdmmcDriver();
+  printf("Done, waiting for FPGA init success\n");
+
+  EXPECT_OK(CoreStatus::WaitForInitSuccess(controller_));
+  printf("Done, waiting for SDIO clients\n");
+
+  auto clients = GetTestSdioFidlClients(2);
+  ASSERT_EQ(clients.size(), 2);
+  printf("Done\n");
+
+  zx::status<uint8_t> bus_width = controller_.ReadFunction(0, 0x7);
+  ASSERT_TRUE(bus_width.is_ok());
+  EXPECT_EQ(bus_width.value(), 0b10);
+}
+
+}  // namespace sdmmc
diff --git a/src/devices/block/drivers/sdmmc/hardware-test/hardware-test.cml b/src/devices/block/drivers/sdmmc/hardware-test/hardware-test.cml
new file mode 100644
index 0000000..cd9709e
--- /dev/null
+++ b/src/devices/block/drivers/sdmmc/hardware-test/hardware-test.cml
@@ -0,0 +1,27 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+{
+    include: [
+        "//sdk/lib/syslog/client.shard.cml",
+        "//src/sys/test_manager/system-test.shard.cml",
+        "//src/sys/test_runners/gtest/default.shard.cml",
+    ],
+    program: {
+        binary: "bin/sdmmc_hardware_test",
+    },
+    use: [
+        {
+            protocol: [
+                "fuchsia.hardware.i2c.Device2",
+                "fuchsia.sysinfo.SysInfo",
+            ],
+        },
+        {
+            directory: "dev-mediacodec",
+            from: "parent",
+            rights: [ "rw*" ],
+            path: "/dev/class/media-codec",
+        },
+    ],
+}
diff --git a/src/devices/block/drivers/sdmmc/hardware-test/main.cc b/src/devices/block/drivers/sdmmc/hardware-test/main.cc
new file mode 100644
index 0000000..10c1cc7
--- /dev/null
+++ b/src/devices/block/drivers/sdmmc/hardware-test/main.cc
@@ -0,0 +1,110 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <fidl/fuchsia.hardware.gpio/cpp/wire.h>
+#include <fidl/fuchsia.hardware.i2c/cpp/wire.h>
+#include <fidl/fuchsia.sysinfo/cpp/wire.h>
+#include <stdio.h>
+
+#include <zxtest/zxtest.h>
+
+#include "sdmmc-test-device-controller.h"
+
+static zx_status_t CheckControllerIdAndVersion() {
+  // TODO: Extract these into a device-specific config
+  constexpr char kControllerI2cDevicePath[] = "/dev/sys/platform/05:00:2/aml-i2c/i2c/i2c-1-50";
+  constexpr uint8_t kExpectedCoreVersion = 1;
+  constexpr char kExpectedControllerId[] = "SDIO";
+
+  fidl::WireSyncClient i2c =
+      sdmmc::GetFidlClient<fuchsia_hardware_i2c::Device2>(kControllerI2cDevicePath);
+  sdmmc::SdmmcTestDeviceController controller(std::move(i2c));
+  if (!controller.is_valid()) {
+    fprintf(stderr, "Failed to connect to %s\n", kControllerI2cDevicePath);
+    return ZX_ERR_IO;
+  }
+
+  // TODO: Re-enable these checks
+#if 1
+  const auto id = controller.GetId();
+  if (id.is_error()) {
+    fprintf(stderr, "Failed to read controller ID: %s\n", id.status_string());
+    return id.error_value();
+  }
+
+  if (id->size() != strlen(kExpectedControllerId) ||
+      memcmp(id->data(), kExpectedControllerId, id->size()) != 0) {
+    fprintf(stderr, "Invalid controller ID\n");
+    return ZX_ERR_BAD_STATE;
+  }
+
+  const auto version = controller.GetCoreVersion();
+  if (version.is_error()) {
+    fprintf(stderr, "Failed to read controller version: %s\n", id.status_string());
+    return version.error_value();
+  }
+
+  if (version.value() != kExpectedCoreVersion) {
+    fprintf(stderr, "Unexpected core version %u\n", version.value());
+    return ZX_ERR_BAD_STATE;
+  }
+#endif
+
+  return ZX_OK;
+}
+
+int main(int argc, char** argv) {
+  // TODO: Extract these into a device-specific config
+  constexpr char kSysInfoPath[] = "/svc/fuchsia.sysinfo.SysInfo";
+  constexpr char kExpectedBoardName[] = "vim3";
+
+  constexpr char kPowerGpioDevicePath[] = "/dev/gpio-expander/ti-tca6408a/gpio-107";
+
+  setlinebuf(stdout);
+
+  fidl::WireSyncClient sysinfo = sdmmc::GetFidlClient<fuchsia_sysinfo::SysInfo>(kSysInfoPath);
+  if (!sysinfo.is_valid()) {
+    fprintf(stderr, "Failed to connect to %s\n", kSysInfoPath);
+    return 1;
+  }
+
+  const auto board_name = sysinfo->GetBoardName();
+  if (!board_name.ok() || board_name->status != ZX_OK) {
+    fprintf(stderr, "Failed to get board name\n");
+    return 1;
+  }
+
+  if (board_name->name.size() != strlen(kExpectedBoardName) ||
+      memcmp(board_name->name.data(), kExpectedBoardName, board_name->name.size()) != 0) {
+    printf("Detected unsupported board %s, skipping tests\n", kExpectedBoardName);
+    return 0;
+  }
+
+  if (CheckControllerIdAndVersion() != ZX_OK) {
+    return 1;
+  }
+
+  fidl::WireSyncClient voltage_gpio =
+      sdmmc::GetFidlClient<fuchsia_hardware_gpio::Gpio>(kPowerGpioDevicePath);
+  if (!voltage_gpio.is_valid()) {
+    fprintf(stderr, "Failed to connect to %s\n", kPowerGpioDevicePath);
+    return 1;
+  }
+
+  // Set the bus voltage to 1.8V.
+  const auto response = voltage_gpio->ConfigOut(1);
+  if (!response.ok() || response->result.is_err()) {
+    fprintf(stderr, "Failed to set SDMMC bus voltage\n");
+    return 1;
+  }
+
+  // Sleep for some time to allow the voltage to stabilize.
+  zx::nanosleep(zx::deadline_after(zx::sec(1)));
+
+  const int ret = RUN_ALL_TESTS(argc, argv);
+
+  // Set the bus voltage back to 3.3V.
+  voltage_gpio->ConfigOut(0);
+  return ret;
+}
diff --git a/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller-regs.h b/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller-regs.h
new file mode 100644
index 0000000..df8826e
--- /dev/null
+++ b/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller-regs.h
@@ -0,0 +1,153 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_DEVICES_BLOCK_DRIVERS_SDMMC_HARDWARE_TEST_SDMMC_TEST_DEVICE_CONTROLLER_REGS_H_
+#define SRC_DEVICES_BLOCK_DRIVERS_SDMMC_HARDWARE_TEST_SDMMC_TEST_DEVICE_CONTROLLER_REGS_H_
+
+#include <hwreg/bitfields.h>
+
+#include "sdmmc-test-device-controller.h"
+
+namespace sdmmc {
+
+// See //zircon/system/ulib/hwreg-i2c/include/hwreg/i2c.h
+
+template <class DerivedType>
+class SdmmcTestDeviceControllerRegisterBase
+    : public hwreg::RegisterBase<DerivedType, uint8_t, void> {
+ public:
+  template <typename T>
+  DerivedType& ReadFrom(T* reg_io) = delete;
+  template <typename T>
+  DerivedType& WriteTo(T* mmio) = delete;
+
+  using RegisterBaseType = hwreg::RegisterBase<DerivedType, uint8_t, void>;
+
+  zx_status_t ReadFrom(SdmmcTestDeviceController& controller) {
+    const auto addr = static_cast<uint8_t>(RegisterBaseType::reg_addr());
+
+    zx::status<uint8_t> value = controller.ReadReg(addr);
+    if (value.is_error()) {
+      return value.error_value();
+    }
+    RegisterBaseType::set_reg_value(value.value());
+    return ZX_OK;
+  }
+
+  zx_status_t WriteTo(SdmmcTestDeviceController& controller) {
+    const auto addr = static_cast<uint8_t>(RegisterBaseType::reg_addr());
+    return controller.WriteReg(addr, RegisterBaseType::reg_value()).status_value();
+  }
+};
+
+template <class RegType>
+class SdmmcTestDeviceControllerRegisterAddr : public hwreg::RegisterAddr<RegType> {
+ public:
+  static_assert(std::is_base_of<SdmmcTestDeviceControllerRegisterBase<RegType>, RegType>::value,
+                "Parameter of SdmmcTestDeviceControllerRegisterAddr<> should derive from "
+                "SdmmcTestDeviceControllerRegisterBase");
+
+  template <typename T>
+  RegType ReadFrom(T* reg_io) = delete;
+
+  explicit SdmmcTestDeviceControllerRegisterAddr(uint8_t reg_addr)
+      : hwreg::RegisterAddr<RegType>(reg_addr) {}
+};
+
+class CoreControl : public SdmmcTestDeviceControllerRegisterBase<CoreControl> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<CoreControl>(0x5); }
+
+  DEF_BIT(0, core_enable);
+  DEF_BIT(1, error_injection_enable);
+  DEF_BIT(7, por_reset);
+};
+
+class CoreStatus : public SdmmcTestDeviceControllerRegisterBase<CoreStatus> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<CoreStatus>(0x6); }
+
+  static zx_status_t WaitForInitSuccess(SdmmcTestDeviceController& controller) {
+    CoreStatus reg = CoreStatus::Get().FromValue(0);
+    while (!reg.init_finished()) {
+      if (zx_status_t status = reg.ReadFrom(controller); status != ZX_OK) {
+        return status;
+      }
+    }
+
+    return ZX_OK;
+  }
+
+  static zx_status_t WaitForInitFailure(SdmmcTestDeviceController& controller) {
+    CoreStatus reg = CoreStatus::Get().FromValue(0);
+    while (!reg.init_failed()) {
+      if (zx_status_t status = reg.ReadFrom(controller); status != ZX_OK) {
+        return status;
+      }
+    }
+
+    return ZX_OK;
+  }
+
+  DEF_BIT(0, init_finished);
+  DEF_BIT(1, init_failed);
+};
+
+class Ocr2 : public SdmmcTestDeviceControllerRegisterBase<Ocr2> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<Ocr2>(0x7); }
+};
+
+class Ocr1 : public SdmmcTestDeviceControllerRegisterBase<Ocr1> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<Ocr1>(0x8); }
+};
+
+class Ocr0 : public SdmmcTestDeviceControllerRegisterBase<Ocr0> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<Ocr0>(0x9); }
+};
+
+class Rca1 : public SdmmcTestDeviceControllerRegisterBase<Rca1> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<Rca1>(0xa); }
+};
+
+class Rca0 : public SdmmcTestDeviceControllerRegisterBase<Rca0> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<Rca0>(0xb); }
+};
+
+class CardStatusR1 : public SdmmcTestDeviceControllerRegisterBase<CardStatusR1> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<CardStatusR1>(0xc); }
+
+  DEF_BIT(0, error);
+  DEF_BIT(1, illegal_command);
+  DEF_BIT(2, com_crc_error);
+  DEF_BIT(3, out_of_range);
+};
+
+class CardStatusR5 : public SdmmcTestDeviceControllerRegisterBase<CardStatusR5> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<CardStatusR5>(0xd); }
+};
+
+class CrcErrorControl : public SdmmcTestDeviceControllerRegisterBase<CrcErrorControl> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<CrcErrorControl>(0xe); }
+
+  DEF_BIT(6, cmd52_crc_error_enable);
+};
+
+class Cmd52ErrorControl : public SdmmcTestDeviceControllerRegisterBase<Cmd52ErrorControl> {
+ public:
+  static auto Get() { return SdmmcTestDeviceControllerRegisterAddr<Cmd52ErrorControl>(0x17); }
+
+  DEF_FIELD(3, 0, transfers_until_crc_error);
+};
+
+}  // namespace sdmmc
+
+#endif  // SRC_DEVICES_BLOCK_DRIVERS_SDMMC_HARDWARE_TEST_SDMMC_TEST_DEVICE_CONTROLLER_REGS_H_
diff --git a/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller.cc b/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller.cc
new file mode 100644
index 0000000..22a2fab
--- /dev/null
+++ b/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller.cc
@@ -0,0 +1,177 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "sdmmc-test-device-controller.h"
+
+namespace {
+
+constexpr int kI2cRetries = 10;
+
+}  // namespace
+
+namespace sdmmc {
+
+zx::status<std::vector<uint8_t>> SdmmcTestDeviceController::ReadI2c(
+    const std::vector<uint8_t>& address, const uint8_t size) {
+  fidl::Arena allocator;
+
+  fidl::WireRequest<fuchsia_hardware_i2c::Device2::Transfer> request;
+
+  request.segments_is_write = fidl::VectorView<bool>(allocator, 2);
+  request.segments_is_write[0] = true;
+  request.segments_is_write[1] = false;
+
+  request.write_segments_data = fidl::VectorView<fidl::VectorView<uint8_t>>(allocator, 1);
+  request.write_segments_data[0] = fidl::VectorView<uint8_t>(allocator, address.size());
+  memcpy(request.write_segments_data[0].mutable_data(), address.data(),
+         address.size() * sizeof(address[0]));
+
+  request.read_segments_length = fidl::VectorView<uint8_t>(allocator, 1);
+  request.read_segments_length[0] = size;
+
+  return RetryI2cRequest(request);
+}
+
+zx::status<> SdmmcTestDeviceController::WriteI2c(const std::vector<uint8_t>& address,
+                                                 const std::vector<uint8_t>& data) {
+  if (address.size() + data.size() > UINT8_MAX) {
+    return zx::error(ZX_ERR_OUT_OF_RANGE);
+  }
+
+  fidl::Arena allocator;
+
+  fidl::WireRequest<fuchsia_hardware_i2c::Device2::Transfer> request;
+
+  request.segments_is_write = fidl::VectorView<bool>(allocator, 1);
+  request.segments_is_write[0] = true;
+
+  request.write_segments_data = fidl::VectorView<fidl::VectorView<uint8_t>>(allocator, 1);
+  request.write_segments_data[0] =
+      fidl::VectorView<uint8_t>(allocator, address.size() + data.size());
+
+  {
+    uint8_t* const write_data = request.write_segments_data[0].mutable_data();
+    memcpy(write_data, address.data(), address.size() * sizeof(address[0]));
+    memcpy(write_data + address.size(), data.data(), data.size() * sizeof(data[0]));
+  }
+
+  request.read_segments_length = fidl::VectorView<uint8_t>(allocator, 0);
+
+  if (const auto status = RetryI2cRequest(request); status.is_error()) {
+    return zx::error(status.error_value());
+  }
+  return zx::ok();
+}
+
+zx::status<std::vector<uint8_t>> SdmmcTestDeviceController::ReadReg(const uint8_t reg,
+                                                                    const uint8_t size) {
+  return ReadI2c({reg}, size);
+}
+
+zx::status<uint8_t> SdmmcTestDeviceController::ReadReg(const uint8_t reg) {
+  const zx::status<std::vector<uint8_t>> read_data = ReadI2c({reg}, 1);
+  if (read_data.is_error()) {
+    return zx::error(read_data.error_value());
+  }
+  return zx::ok((*read_data)[0]);
+}
+
+zx::status<> SdmmcTestDeviceController::WriteReg(const uint8_t reg, const uint8_t value) {
+  return WriteI2c({reg}, {value});
+}
+
+zx::status<std::vector<uint8_t>> SdmmcTestDeviceController::ReadFunction(const uint8_t function,
+                                                                         const uint32_t address,
+                                                                         const uint8_t size) {
+  if (address > kMaxFunctionAddress || function > 7) {
+    return zx::error(ZX_ERR_OUT_OF_RANGE);
+  }
+  return ReadI2c(FunctionAddressToVector(function, address), size);
+}
+
+zx::status<uint8_t> SdmmcTestDeviceController::ReadFunction(const uint8_t function,
+                                                            const uint32_t address) {
+  const zx::status<std::vector<uint8_t>> read_data = ReadFunction(function, address, 1);
+  if (read_data.is_error()) {
+    return zx::error(read_data.error_value());
+  }
+  return zx::ok((*read_data)[0]);
+}
+
+zx::status<> SdmmcTestDeviceController::WriteFunction(const uint8_t function,
+                                                      const uint32_t address,
+                                                      const std::vector<uint8_t>& data) {
+  if (address > kMaxFunctionAddress || function > 7) {
+    return zx::error(ZX_ERR_OUT_OF_RANGE);
+  }
+  return WriteI2c(FunctionAddressToVector(function, address), data);
+}
+
+zx::status<std::array<uint8_t, 4>> SdmmcTestDeviceController::GetId() {
+  std::array<uint8_t, 4> id;
+  // TODO: Register address constant
+  zx::status<std::vector<uint8_t>> id_vector = ReadReg(1, id.size());
+  if (id_vector.is_error()) {
+    return zx::error(id_vector.status_value());
+  }
+  if (id_vector->size() != id.size()) {
+    fprintf(stderr, "Unexpected read data size %lu\n", id_vector->size());
+    return zx::error(ZX_ERR_INTERNAL);
+  }
+
+  memcpy(id.data(), id_vector->data(), id.size());
+  return zx::ok(id);
+}
+
+std::vector<uint8_t> SdmmcTestDeviceController::FunctionAddressToVector(const uint8_t function,
+                                                                        const uint32_t address) {
+  std::vector<uint8_t> ret(4);
+  ret[0] = 0xf0 | function;
+  ret[1] = (address >> 16) & 0xff;
+  ret[2] = (address >> 8) & 0xff;
+  ret[3] = address & 0xff;
+  return ret;
+}
+
+zx::status<std::vector<uint8_t>> SdmmcTestDeviceController::RetryI2cRequest(
+    const fidl::WireRequest<fuchsia_hardware_i2c::Device2::Transfer>& request) {
+  for (int i = 0; i < kI2cRetries; i++) {
+    fidl::WireRequest<fuchsia_hardware_i2c::Device2::Transfer> req = request;
+    const auto response =
+        fidl::WireResult<fuchsia_hardware_i2c::Device2::Transfer>(i2c_.client_end(), &req);
+    if (!response.ok()) {
+      fprintf(stderr, "FIDL request failed: %s\n", zx_status_get_string(response.status()));
+      return zx::error(response.status());
+    }
+
+    // An error here represents an I2C bus error, continue to retry the transfer.
+    if (response->result.is_err()) {
+      continue;
+    }
+
+    if (response->result.response().read_segments_data.count() !=
+        request.read_segments_length.count()) {
+      fprintf(stderr, "Invalid read segments count %lu\n",
+              response->result.response().read_segments_data.count());
+      return zx::error(ZX_ERR_INTERNAL);
+    }
+
+    // Write request -- no data to return.
+    if (request.read_segments_length.count() == 0) {
+      return zx::ok(std::vector<uint8_t>{});
+    }
+
+    const fidl::VectorView<uint8_t>& read_data = response->result.response().read_segments_data[0];
+    if (read_data.count() != request.read_segments_length[0]) {
+      fprintf(stderr, "Unexpected read data size %lu\n", read_data.count());
+      return zx::error(ZX_ERR_INTERNAL);
+    }
+
+    return zx::ok(std::vector<uint8_t>(read_data.data(), read_data.data() + read_data.count()));
+  }
+
+  return zx::error(ZX_ERR_IO);
+}
+
+}  // namespace sdmmc
diff --git a/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller.h b/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller.h
new file mode 100644
index 0000000..3b4d588
--- /dev/null
+++ b/src/devices/block/drivers/sdmmc/hardware-test/sdmmc-test-device-controller.h
@@ -0,0 +1,86 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_DEVICES_BLOCK_DRIVERS_SDMMC_HARDWARE_TEST_SDMMC_TEST_DEVICE_CONTROLLER_H_
+#define SRC_DEVICES_BLOCK_DRIVERS_SDMMC_HARDWARE_TEST_SDMMC_TEST_DEVICE_CONTROLLER_H_
+
+#include <fcntl.h>
+#include <fidl/fuchsia.hardware.i2c/cpp/wire.h>
+#include <lib/fdio/fdio.h>
+#include <lib/zx/status.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <array>
+
+#include <fbl/unique_fd.h>
+
+namespace sdmmc {
+
+template <typename T>
+fidl::WireSyncClient<T> GetFidlClient(fbl::unique_fd device) {
+  fidl::ClientEnd<T> client;
+  zx_status_t status =
+      fdio_get_service_handle(device.release(), client.channel().reset_and_get_address());
+  if (status != ZX_OK) {
+    return {};
+  }
+
+  return fidl::WireSyncClient<T>(std::move(client));
+}
+
+template <typename T>
+fidl::WireSyncClient<T> GetFidlClient(const char* path) {
+  fbl::unique_fd device(open(path, O_RDWR));
+  if (!device.is_valid()) {
+    return {};
+  }
+
+  return GetFidlClient<T>(std::move(device));
+}
+
+class SdmmcTestDeviceController {
+ public:
+  SdmmcTestDeviceController() = default;
+  explicit SdmmcTestDeviceController(fidl::WireSyncClient<fuchsia_hardware_i2c::Device2> i2c)
+      : i2c_(std::move(i2c)) {}
+
+  SdmmcTestDeviceController(SdmmcTestDeviceController&& other) noexcept
+      : i2c_(std::move(other.i2c_)) {}
+  SdmmcTestDeviceController& operator=(SdmmcTestDeviceController&& other) noexcept {
+    i2c_ = std::move(other.i2c_);
+    return *this;
+  }
+
+  bool is_valid() const { return i2c_.is_valid(); }
+
+  zx::status<std::vector<uint8_t>> ReadI2c(const std::vector<uint8_t>& address, uint8_t size);
+  zx::status<> WriteI2c(const std::vector<uint8_t>& address, const std::vector<uint8_t>& data);
+
+  zx::status<std::vector<uint8_t>> ReadReg(uint8_t reg, uint8_t size);
+  zx::status<uint8_t> ReadReg(uint8_t reg);
+  zx::status<> WriteReg(uint8_t reg, uint8_t value);
+
+  zx::status<std::vector<uint8_t>> ReadFunction(uint8_t function, uint32_t address, uint8_t size);
+  zx::status<uint8_t> ReadFunction(uint8_t function, uint32_t address);
+  zx::status<> WriteFunction(uint8_t function, uint32_t address, const std::vector<uint8_t>& data);
+
+  // TODO: Register address constant
+  zx::status<uint8_t> GetCoreVersion() { return ReadReg(0); }
+  zx::status<std::array<uint8_t, 4>> GetId();
+
+ private:
+  static constexpr uint32_t kMaxFunctionAddress = 0x1'ffff;
+
+  static std::vector<uint8_t> FunctionAddressToVector(uint8_t function, uint32_t address);
+
+  zx::status<std::vector<uint8_t>> RetryI2cRequest(
+      const fidl::WireRequest<fuchsia_hardware_i2c::Device2::Transfer>& request);
+
+  fidl::WireSyncClient<fuchsia_hardware_i2c::Device2> i2c_;
+};
+
+}  // namespace sdmmc
+
+#endif  // SRC_DEVICES_BLOCK_DRIVERS_SDMMC_HARDWARE_TEST_SDMMC_TEST_DEVICE_CONTROLLER_H_