[tpm] Initial TPM driver implementation.

All this does is send a shutdown command to the TPM when its suspend
hook is called.

Bug: 76095
Bug: 76072
Change-Id: I033ee762e58b9e425ee7b42810ff5425f6f39cf2
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/554386
Commit-Queue: Simon Shields <simonshields@google.com>
Reviewed-by: Abdulla Kamar <abdulla@google.com>
Reviewed-by: Alex Legg <alexlegg@google.com>
diff --git a/boards/common/x64-common.gni b/boards/common/x64-common.gni
index cec152b..7452b45 100644
--- a/boards/common/x64-common.gni
+++ b/boards/common/x64-common.gni
@@ -24,6 +24,7 @@
       "//src/devices/rtc/drivers/intel-rtc",
       "//src/devices/serial/drivers/uart16550",
       "//src/devices/spi/drivers/intel-gspi",
+      "//src/devices/tpm/drivers/tpm",
       "//src/devices/usb/drivers/xhci-rewrite:xhci",
       "//src/graphics/display/drivers/intel-i915",
       "//src/graphics/display/drivers/simple:simple.amd-kaveri",
diff --git a/src/devices/bind/fuchsia.tpm/BUILD.gn b/src/devices/bind/fuchsia.tpm/BUILD.gn
new file mode 100644
index 0000000..844bba4
--- /dev/null
+++ b/src/devices/bind/fuchsia.tpm/BUILD.gn
@@ -0,0 +1,9 @@
+# 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/bind/bind.gni")
+
+bind_library("fuchsia.tpm") {
+  source = "fuchsia.tpm.bind"
+}
diff --git a/src/devices/bind/fuchsia.tpm/fuchsia.tpm.bind b/src/devices/bind/fuchsia.tpm/fuchsia.tpm.bind
new file mode 100644
index 0000000..394b87dc
--- /dev/null
+++ b/src/devices/bind/fuchsia.tpm/fuchsia.tpm.bind
@@ -0,0 +1,9 @@
+// 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.
+
+library fuchsia.tpm;
+
+extend uint fuchsia.BIND_PROTOCOL {
+  IMPL = 159,
+};
diff --git a/src/devices/tpm/BUILD.gn b/src/devices/tpm/BUILD.gn
index 6419049..f6db470 100644
--- a/src/devices/tpm/BUILD.gn
+++ b/src/devices/tpm/BUILD.gn
@@ -4,10 +4,16 @@
 
 group("drivers") {
   testonly = true
-  deps = [ "drivers/cr50-spi" ]
+  deps = [
+    "drivers/cr50-spi",
+    "drivers/tpm",
+  ]
 }
 
 group("tests") {
   testonly = true
-  deps = [ "drivers/cr50-spi:tests" ]
+  deps = [
+    "drivers/cr50-spi:tests",
+    "drivers/tpm:tests",
+  ]
 }
diff --git a/src/devices/tpm/drivers/tpm/BUILD.gn b/src/devices/tpm/drivers/tpm/BUILD.gn
new file mode 100644
index 0000000..ab4a8c7
--- /dev/null
+++ b/src/devices/tpm/drivers/tpm/BUILD.gn
@@ -0,0 +1,66 @@
+# 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/bind/bind.gni")
+import("//build/components.gni")
+import("//build/config/fuchsia/rules.gni")
+import("//build/test.gni")
+
+driver_bind_rules("tpm_bind") {
+  rules = "tpm.bind"
+  header_output = "tpm_bind.h"
+  tests = "bind_tests.json"
+  deps = [ "//src/devices/bind/fuchsia.tpm" ]
+}
+
+common_deps = [
+  ":tpm_bind",
+  "//sdk/banjo/fuchsia.hardware.tpmimpl:fuchsia.hardware.tpmimpl_banjo_cpp",
+  "//sdk/fidl/fuchsia.hardware.tpmimpl:fuchsia.hardware.tpmimpl_llcpp",
+  "//src/devices/lib/driver",
+  "//src/lib/ddktl",
+  "//zircon/system/ulib/async-loop:async-loop-cpp",
+  "//zircon/system/ulib/hwreg",
+  "//zircon/system/ulib/zx",
+]
+
+driver_module("tpm-driver") {
+  sources = [ "tpm.cc" ]
+  deps = common_deps
+}
+
+fuchsia_system_package("tpm-pkg") {
+  package_name = "tpm"
+  deps = [ ":tpm-driver" ]
+
+  allowed_in_extra_deps = true
+}
+
+test("tpm-driver-test-bin") {
+  output_name = "tpm-driver-test"
+  sources = [
+    "tpm-test.cc",
+    "tpm.cc",
+  ]
+  deps = common_deps + [
+           "//src/devices/testing/mock-ddk",
+           "//zircon/system/ulib/zxtest",
+         ]
+}
+
+fuchsia_unittest_package("tpm-driver-test") {
+  deps = [ ":tpm-driver-test-bin" ]
+}
+
+group("tpm") {
+  deps = [ ":tpm-pkg" ]
+}
+
+group("tests") {
+  testonly = true
+  deps = [
+    ":tpm-driver-test",
+    ":tpm_bind_test",
+  ]
+}
diff --git a/src/devices/tpm/drivers/tpm/bind_tests.json b/src/devices/tpm/drivers/tpm/bind_tests.json
new file mode 100644
index 0000000..7077348
--- /dev/null
+++ b/src/devices/tpm/drivers/tpm/bind_tests.json
@@ -0,0 +1,16 @@
+[
+    {
+        "device": {
+            "fuchsia.BIND_PROTOCOL": "fuchsia.spi.BIND_PROTOCOL.IMPL"
+        },
+        "expected": "abort",
+        "name": "Wrong protocol"
+    },
+    {
+        "device": {
+            "fuchsia.BIND_PROTOCOL": "fuchsia.tpm.BIND_PROTOCOL.IMPL"
+        },
+        "expected": "match",
+        "name": "Match"
+    }
+]
diff --git a/src/devices/tpm/drivers/tpm/commands.h b/src/devices/tpm/drivers/tpm/commands.h
new file mode 100644
index 0000000..eae3126
--- /dev/null
+++ b/src/devices/tpm/drivers/tpm/commands.h
@@ -0,0 +1,48 @@
+// 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_TPM_DRIVERS_TPM_COMMANDS_H_
+#define SRC_DEVICES_TPM_DRIVERS_TPM_COMMANDS_H_
+
+#include <endian.h>
+#include <zircon/compiler.h>
+#include <zircon/types.h>
+
+// The definitions here are spread across two parts of the TPM2 spec:
+// - part 2, "structures".
+// - part 3, "commands".
+//
+// https://trustedcomputinggroup.org/wp-content/uploads/TCG_TPM2_r1p59_Part2_Structures_pub.pdf
+// https://trustedcomputinggroup.org/wp-content/uploads/TCG_TPM2_r1p59_Part3_Commands_pub.pdf
+
+#define TPM_ST_NO_SESSIONS (0x8001)
+#define TPM_CC_SHUTDOWN (0x0145)
+
+#define TPM_SU_CLEAR 0x00
+#define TPM_SU_STATE 0x01
+
+struct TpmCmdHeader {
+  uint16_t tag;
+  uint32_t command_size;
+  uint32_t command_code;
+} __PACKED;
+
+struct TpmShutdownCmd {
+  TpmCmdHeader hdr;
+  uint16_t shutdown_type;
+
+  explicit TpmShutdownCmd(uint16_t type) : shutdown_type(htobe16(type)) {
+    hdr = {.tag = htobe16(TPM_ST_NO_SESSIONS),
+           .command_size = htobe32(sizeof(TpmShutdownCmd)),
+           .command_code = htobe32(TPM_CC_SHUTDOWN)};
+  }
+} __PACKED;
+
+struct TpmShutdownResponse {
+  uint16_t tag;
+  uint32_t response_size;
+  uint32_t response_code;
+};
+
+#endif  // SRC_DEVICES_TPM_DRIVERS_TPM_COMMANDS_H_
diff --git a/src/devices/tpm/drivers/tpm/registers.h b/src/devices/tpm/drivers/tpm/registers.h
new file mode 100644
index 0000000..b07733e
--- /dev/null
+++ b/src/devices/tpm/drivers/tpm/registers.h
@@ -0,0 +1,111 @@
+// 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_TPM_DRIVERS_TPM_REGISTERS_H_
+#define SRC_DEVICES_TPM_DRIVERS_TPM_REGISTERS_H_
+
+#include <fuchsia/hardware/tpmimpl/llcpp/fidl.h>
+#include <lib/ddk/debug.h>
+#include <lib/zx/status.h>
+
+#include <hwreg/bitfields.h>
+
+namespace tpm {
+
+using fuchsia_hardware_tpmimpl::wire::RegisterAddress;
+
+enum TpmFamily {
+  // TPM v1.2
+  kTpmFamily12 = 0,
+  // TPM v2.0
+  kTpmFamily20 = 1,
+};
+
+// This base class just implements convenience |ReadFrom| and |WriteTo| methods that use a supplied
+// FIDL client to write/read a TPM register.
+template <class SelfType, uint16_t address>
+class TpmReg : public hwreg::RegisterBase<SelfType, uint32_t, hwreg::EnablePrinter> {
+ public:
+  zx_status_t ReadFrom(fidl::WireSyncClient<fuchsia_hardware_tpmimpl::TpmImpl>& client) {
+    auto result = client.Read(0, RegisterAddress(address), 4);
+    if (!result.ok()) {
+      zxlogf(ERROR, "Failed to send read FIDL request: %s", result.FormatDescription().data());
+      return result.status();
+    }
+    if (result->result.is_err()) {
+      zxlogf(ERROR, "Failed to read: %d", result->result.err());
+      return result->result.err();
+    }
+    auto& data = result->result.response().data;
+    if (data.count() != 4) {
+      zxlogf(ERROR, "Incorrect response size");
+      return ZX_ERR_BAD_STATE;
+    }
+
+    uint32_t val = 0;
+    memcpy(&val, data.data(), sizeof(val));
+    this->set_reg_value(val);
+    return ZX_OK;
+  }
+
+  zx_status_t WriteTo(fidl::WireSyncClient<fuchsia_hardware_tpmimpl::TpmImpl>& client) {
+    uint32_t value = this->reg_value();
+    auto data =
+        fidl::VectorView<uint8_t>::FromExternal(reinterpret_cast<uint8_t*>(&value), sizeof(value));
+    auto result = client.Write(0, RegisterAddress(address), data);
+    if (!result.ok()) {
+      zxlogf(ERROR, "Failed to send write FIDL request: %s", result.FormatDescription().data());
+      return result.status();
+    }
+    if (result->result.is_err()) {
+      zxlogf(ERROR, "Failed to write: %d", result->result.err());
+      return result->result.err();
+    }
+    return ZX_OK;
+  }
+};
+
+// All of these registers are defined in the TPM PC client platform spec.
+// https://www.trustedcomputinggroup.org/wp-content/uploads/PCClientPlatform-TPM-Profile-for-TPM-2-0-v1-03-20-161114_public-review.pdf
+// The PC client platform spec defines separate registers for SPI/LPC and I2C, however, the two are
+// mostly compatible.
+
+// TPM_STS: 5.5.2.5, "Status Register" and 7.3.5.6, "TPM_STS".
+class StsReg : public TpmReg<StsReg, RegisterAddress::kTpmSts> {
+ public:
+  DEF_ENUM_FIELD(TpmFamily, 27, 26, tpm_family);
+  DEF_BIT(25, reset_establishment);
+  DEF_BIT(24, command_cancel);
+  DEF_FIELD(23, 8, burst_count);
+  DEF_BIT(7, sts_valid);
+  DEF_BIT(6, command_ready);
+  DEF_BIT(5, tpm_go);
+  DEF_BIT(4, data_avail);
+  DEF_BIT(3, expect);
+  DEF_BIT(2, self_test_done);
+  DEF_BIT(1, response_retry);
+};
+
+// TPM_INTF_CAPABILITY: 5.5.2.7, "Interface Capability" and 7.3.5.5, "TPM_INT_CAPABILITY".
+//
+// Note that the I2C version of the interface only defines bits 0, 1, 2, and 7.
+// Reads of other fields will always return zero.
+class IntfCapabilityReg : public TpmReg<IntfCapabilityReg, RegisterAddress::kTpmIntCapability> {
+ public:
+  DEF_FIELD(30, 28, interface_version);
+  DEF_FIELD(10, 9, data_transfer_size_support);
+  DEF_BIT(8, burst_count_static);
+  DEF_BIT(7, command_ready_int_support);
+  DEF_BIT(6, interrupt_edge_falling);
+  DEF_BIT(5, interrupt_edge_rising);
+  DEF_BIT(4, interrupt_level_low);
+  DEF_BIT(3, interrupt_level_high);
+  DEF_BIT(2, locality_change_int_supported);
+  DEF_BIT(1, sts_valid_int_support);
+  DEF_BIT(0, data_avail_int_support);
+};
+
+}  // namespace tpm
+
+#endif  // SRC_DEVICES_TPM_DRIVERS_TPM_REGISTERS_H_
diff --git a/src/devices/tpm/drivers/tpm/tpm-test.cc b/src/devices/tpm/drivers/tpm/tpm-test.cc
new file mode 100644
index 0000000..2788363
--- /dev/null
+++ b/src/devices/tpm/drivers/tpm/tpm-test.cc
@@ -0,0 +1,252 @@
+// 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 "src/devices/tpm/drivers/tpm/tpm.h"
+
+#include <fuchsia/hardware/tpmimpl/llcpp/fidl.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/sync/condition.h>
+
+#include <condition_variable>
+#include <mutex>
+
+#include <ddktl/device.h>
+#include <zxtest/zxtest.h>
+
+#include "src/devices/testing/mock-ddk/mock-device.h"
+#include "src/devices/tpm/drivers/tpm/commands.h"
+#include "src/devices/tpm/drivers/tpm/registers.h"
+
+// See Table 22,
+// https://www.trustedcomputinggroup.org/wp-content/uploads/PCClientPlatform-TPM-Profile-for-TPM-2-0-v1-03-20-161114_public-review.pdf
+enum TpmState {
+  kIdle,
+  kReady,
+  kReception,
+  kExecution,
+  kCompletion,
+  // Special state to indicate test is finished and thread should stop.
+  kTeardownTest,
+};
+
+using fuchsia_hardware_tpmimpl::wire::RegisterAddress;
+class TpmTest : public zxtest::Test, public fidl::WireServer<fuchsia_hardware_tpmimpl::TpmImpl> {
+ public:
+  TpmTest()
+      : loop_(&kAsyncLoopConfigNeverAttachToThread), fake_root_(MockDevice::FakeRootParent()) {}
+
+  void SetUp() override {
+    status_.set_tpm_family(tpm::kTpmFamily20).set_sts_valid(1);
+    ASSERT_OK(loop_.StartThread("tpm-test-thread"));
+
+    exec_thread_ = std::thread(&TpmTest::ExecThread, this);
+
+    fidl::WireSyncClient<fuchsia_hardware_tpmimpl::TpmImpl> client;
+    auto server = fidl::CreateEndpoints(&client.client_end());
+    ASSERT_TRUE(server.is_ok());
+    fidl::BindServer(loop_.dispatcher(), std::move(server.value()), this);
+
+    ASSERT_TRUE(client.client_end().is_valid());
+    auto device = std::make_unique<tpm::TpmDevice>(fake_root_.get(), std::move(client));
+    ASSERT_OK(device->DdkAdd(ddk::DeviceAddArgs("tpm")));
+    device.release();
+  }
+
+  void TearDown() override {
+    {
+      std::scoped_lock lock(state_mutex_);
+      state_ = kTeardownTest;
+    }
+    state_change_.notify_all();
+    if (exec_thread_.joinable()) {
+      exec_thread_.join();
+    }
+  }
+
+  void Read(ReadRequestView request, ReadCompleter::Sync& completer) override {
+    switch (request->address) {
+      case RegisterAddress::kTpmSts: {
+        ASSERT_EQ(request->count, 4);
+        uint32_t value = status_.reg_value();
+        completer.ReplySuccess(
+            fidl::VectorView<uint8_t>::FromExternal(reinterpret_cast<uint8_t*>(&value), 4));
+        break;
+      }
+      case RegisterAddress::kTpmDataFifo: {
+        ASSERT_LE(request->count, status_.burst_count());
+        ASSERT_GT(request->count, 0);
+        std::scoped_lock lock(state_mutex_);
+        auto amount = std::min(static_cast<size_t>(request->count), fifo_.size());
+        completer.ReplySuccess(fidl::VectorView<uint8_t>::FromExternal(fifo_.data(), amount));
+        fifo_.erase(fifo_.begin(), fifo_.begin() + amount);
+        if (fifo_.empty()) {
+          status_.set_data_avail(0);
+        }
+        break;
+      }
+      default: {
+        ASSERT_TRUE(false, "unexpected register");
+      }
+    }
+  }
+
+  void HandleStsWrite(uint32_t value) __TA_NO_THREAD_SAFETY_ANALYSIS {
+    tpm::StsReg st;
+    st.set_reg_value(value);
+    if (st.command_ready()) {
+      std::scoped_lock lock(state_mutex_);
+      if (state_ == kIdle) {
+        state_ = kReady;
+        fifo_.clear();
+        status_.set_command_ready(1).set_burst_count(64);
+      } else {
+        state_ = kIdle;
+        status_.set_command_ready(0);
+      }
+      state_change_.notify_all();
+    } else if (st.tpm_go()) {
+      // The spec technically defines setting TPM_GO for all states, but receiving it in any state
+      // except reception probably indicates a bug.
+      std::scoped_lock lock(state_mutex_);
+      ASSERT_EQ(state_, kReception);
+      ASSERT_EQ(status_.expect(), 0);
+      state_ = kExecution;
+      state_change_.notify_all();
+    } else {
+      ASSERT_FALSE(true, "Unknown bit set in TPM STS register!");
+    }
+  }
+
+  void Write(WriteRequestView request, WriteCompleter::Sync& completer) override {
+    switch (request->address) {
+      case RegisterAddress::kTpmSts: {
+        ASSERT_EQ(request->data.count(), 4);
+        uint32_t value = *reinterpret_cast<uint32_t*>(request->data.mutable_data());
+        // There should only be 1 bit set on a TPM write.
+        // Leading zeros + trailing zeros should equal 31.
+        ASSERT_EQ(__builtin_clz(value) + __builtin_ctz(value), 31);
+        HandleStsWrite(value);
+        break;
+      }
+      case RegisterAddress::kTpmDataFifo: {
+        std::scoped_lock lock(state_mutex_);
+        if (state_ == kReady) {
+          state_ = kReception;
+          status_.set_expect(1);
+          state_change_.notify_all();
+        }
+        fifo_.insert(fifo_.end(), request->data.begin(), request->data.end());
+        UpdateExpect();
+        break;
+      }
+      default: {
+        ASSERT_FALSE(true, "Unsupported register");
+        break;
+      }
+    }
+    completer.ReplySuccess();
+  }
+
+  void UpdateExpect() __TA_REQUIRES(state_mutex_) {
+    TpmCmdHeader* hdr = reinterpret_cast<TpmCmdHeader*>(fifo_.data());
+    if (be32toh(hdr->command_size) == fifo_.size()) {
+      status_.set_expect(0);
+    }
+  }
+
+  void ExecThread() {
+    auto cur_state = TpmState::kIdle;
+    std::scoped_lock lock(state_mutex_);
+    while (state_ != kTeardownTest) {
+      state_change_.wait(state_mutex_, [&cur_state, this]() __TA_REQUIRES(state_mutex_) {
+        return state_ != cur_state;
+      });
+      cur_state = state_;
+      switch (state_) {
+        case kExecution: {
+          std::vector<uint8_t> out;
+          TpmCmdHeader* hdr = reinterpret_cast<TpmCmdHeader*>(fifo_.data());
+          handle_command_(hdr, out);
+          fifo_ = std::move(out);
+          state_ = kCompletion;
+          status_.set_data_avail(1);
+          break;
+        }
+        default: {
+          break;
+        }
+      }
+    }
+  }
+
+ protected:
+  async::Loop loop_;
+  tpm::StsReg status_;
+  TpmState state_ __TA_GUARDED(state_mutex_) = TpmState::kIdle;
+  std::vector<uint8_t> fifo_ __TA_GUARDED(state_mutex_);
+  std::mutex state_mutex_;
+  std::condition_variable_any state_change_;
+  std::thread exec_thread_;
+
+  std::shared_ptr<MockDevice> fake_root_;
+  std::function<void(TpmCmdHeader*, std::vector<uint8_t>&)> handle_command_;
+};
+
+TEST_F(TpmTest, TestDdkInit) {
+  auto dev = fake_root_->GetLatestChild();
+  dev->InitOp();
+  ASSERT_OK(dev->WaitUntilInitReplyCalled());
+}
+
+TEST_F(TpmTest, TestDdkSuspendToRam) {
+  auto dev = fake_root_->GetLatestChild();
+  dev->InitOp();
+  ASSERT_OK(dev->WaitUntilInitReplyCalled());
+
+  uint16_t shutdown_type = -1;
+  handle_command_ = [&shutdown_type](TpmCmdHeader* c, std::vector<uint8_t>& out) {
+    ASSERT_EQ(be32toh(c->command_code), TPM_CC_SHUTDOWN);
+    ASSERT_EQ(be32toh(c->command_size), sizeof(TpmShutdownCmd));
+    TpmShutdownCmd* shutdown = reinterpret_cast<TpmShutdownCmd*>(c);
+    shutdown_type = be16toh(shutdown->shutdown_type);
+
+    TpmShutdownResponse r{
+        .tag = htobe16(TPM_ST_NO_SESSIONS),
+        .response_size = htobe32(sizeof(TpmShutdownResponse)),
+        .response_code = 0,
+    };
+    out.resize(sizeof(r), 0);
+    memcpy(out.data(), &r, sizeof(r));
+  };
+
+  dev->SuspendNewOp(DEV_POWER_STATE_D0, false, DEVICE_SUSPEND_REASON_SUSPEND_RAM);
+  dev->WaitUntilSuspendReplyCalled();
+  ASSERT_EQ(shutdown_type, TPM_SU_STATE);
+}
+
+TEST_F(TpmTest, TestDdkSuspendShutdown) {
+  auto dev = fake_root_->GetLatestChild();
+  dev->InitOp();
+  ASSERT_OK(dev->WaitUntilInitReplyCalled());
+
+  uint16_t shutdown_type = -1;
+  handle_command_ = [&shutdown_type](TpmCmdHeader* c, std::vector<uint8_t>& out) {
+    ASSERT_EQ(be32toh(c->command_code), TPM_CC_SHUTDOWN);
+    ASSERT_EQ(be32toh(c->command_size), sizeof(TpmShutdownCmd));
+    TpmShutdownCmd* shutdown = reinterpret_cast<TpmShutdownCmd*>(c);
+    shutdown_type = be16toh(shutdown->shutdown_type);
+
+    TpmShutdownResponse r{
+        .tag = htobe16(TPM_ST_NO_SESSIONS),
+        .response_size = htobe32(sizeof(TpmShutdownResponse)),
+        .response_code = 0,
+    };
+    out.resize(sizeof(r), 0);
+    memcpy(out.data(), &r, sizeof(r));
+  };
+
+  dev->SuspendNewOp(DEV_POWER_STATE_D0, false, DEVICE_SUSPEND_REASON_POWEROFF);
+  dev->WaitUntilSuspendReplyCalled();
+  ASSERT_EQ(shutdown_type, TPM_SU_CLEAR);
+}
diff --git a/src/devices/tpm/drivers/tpm/tpm.bind b/src/devices/tpm/drivers/tpm/tpm.bind
new file mode 100644
index 0000000..1dd00de
--- /dev/null
+++ b/src/devices/tpm/drivers/tpm/tpm.bind
@@ -0,0 +1,7 @@
+// 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.
+
+using fuchsia.tpm;
+
+fuchsia.BIND_PROTOCOL == fuchsia.tpm.BIND_PROTOCOL.IMPL;
diff --git a/src/devices/tpm/drivers/tpm/tpm.cc b/src/devices/tpm/drivers/tpm/tpm.cc
new file mode 100644
index 0000000..13ce7c5
--- /dev/null
+++ b/src/devices/tpm/drivers/tpm/tpm.cc
@@ -0,0 +1,232 @@
+// 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 "src/devices/tpm/drivers/tpm/tpm.h"
+
+#include <fuchsia/hardware/tpmimpl/cpp/banjo.h>
+#include <fuchsia/hardware/tpmimpl/llcpp/fidl.h>
+#include <lib/ddk/debug.h>
+#include <lib/fit/defer.h>
+#include <zircon/errors.h>
+#include <zircon/status.h>
+
+#include "src/devices/tpm/drivers/tpm/commands.h"
+#include "src/devices/tpm/drivers/tpm/registers.h"
+#include "src/devices/tpm/drivers/tpm/tpm_bind.h"
+
+namespace tpm {
+using fuchsia_hardware_tpmimpl::wire::RegisterAddress;
+
+zx_status_t TpmDevice::Create(void *ctx, zx_device_t *parent) {
+  ddk::TpmImplProtocolClient tpm(parent);
+  if (!tpm.is_valid()) {
+    zxlogf(ERROR, "Failed to get TPM impl!");
+    return ZX_ERR_NOT_SUPPORTED;
+  }
+
+  auto endpoints = fidl::CreateEndpoints<fuchsia_hardware_tpmimpl::TpmImpl>();
+  if (endpoints.is_error()) {
+    return endpoints.error_value();
+  }
+
+  tpm.ConnectServer(endpoints->server.TakeChannel());
+
+  fidl::WireSyncClient<fuchsia_hardware_tpmimpl::TpmImpl> client(std::move(endpoints->client));
+
+  auto device = std::make_unique<TpmDevice>(parent, std::move(client));
+  zx_status_t status = device->DdkAdd(ddk::DeviceAddArgs("tpm"));
+  if (status == ZX_OK) {
+    __UNUSED auto unused = device.release();
+  }
+  return status;
+}
+
+void TpmDevice::DdkInit(ddk::InitTxn txn) {
+  zx_status_t status = ZX_OK;
+  auto replier = fit::defer([&status, &txn]() { txn.Reply(status); });
+  StsReg sts;
+  if ((status = sts.ReadFrom(tpm_)) != ZX_OK) {
+    return;
+  }
+  if (sts.tpm_family() != TpmFamily::kTpmFamily20) {
+    zxlogf(ERROR, "unsupported TPM family, expected 2.0");
+
+    status = ZX_ERR_NOT_SUPPORTED;
+    return;
+  }
+}
+
+void TpmDevice::DdkSuspend(ddk::SuspendTxn txn) {
+  std::optional<TpmShutdownCmd> cmd;
+
+  switch (txn.suspend_reason()) {
+    case DEVICE_SUSPEND_REASON_REBOOT:
+    case DEVICE_SUSPEND_REASON_REBOOT_BOOTLOADER:
+    case DEVICE_SUSPEND_REASON_REBOOT_RECOVERY:
+    case DEVICE_SUSPEND_REASON_POWEROFF:
+      cmd = TpmShutdownCmd(TPM_SU_CLEAR);
+      break;
+    case DEVICE_SUSPEND_REASON_SUSPEND_RAM:
+      cmd = TpmShutdownCmd(TPM_SU_STATE);
+      break;
+    default:
+      zxlogf(WARNING, "Unknown suspend state %d", txn.requested_state());
+      txn.Reply(ZX_OK, DEV_POWER_STATE_D0);
+      return;
+  }
+
+  if (cmd.has_value()) {
+    // TODO(fxbug.dev/81433): move this onto a command-processing thread.
+    zx_status_t result = DoCommand(&cmd.value().hdr);
+    if (result != ZX_OK) {
+      zxlogf(ERROR, "Error sending TPM shutdown command: %s", zx_status_get_string(result));
+      txn.Reply(result, DEV_POWER_STATE_D0);
+      return;
+    }
+  }
+  txn.Reply(ZX_OK, txn.requested_state());
+}
+
+zx_status_t TpmDevice::DoCommand(TpmCmdHeader *cmd) {
+  // See section 5.5.2.2 of the client platform spec.
+  zx_status_t status = StsReg().set_command_ready(1).WriteTo(tpm_);
+  if (status != ZX_OK) {
+    return status;
+  }
+
+  StsReg sts;
+  do {
+    status = sts.ReadFrom(tpm_);
+    if (status != ZX_OK) {
+      return status;
+    }
+  } while (!sts.command_ready());
+
+  auto finish_command = fit::defer([this]() {
+    zx_status_t status = StsReg().set_command_ready(1).WriteTo(tpm_);
+    if (status != ZX_OK) {
+      zxlogf(ERROR, "Failed to write to TPM while finishing command.");
+      return;
+    }
+  });
+
+  size_t command_size = betoh32(cmd->command_size);
+  uint8_t *buf = reinterpret_cast<uint8_t *>(cmd);
+  while (command_size > 1) {
+    status = sts.ReadFrom(tpm_);
+    if (status != ZX_OK) {
+      return status;
+    }
+    size_t burst_count = sts.burst_count();
+    size_t burst = std::min(burst_count, command_size - 1);
+    zxlogf(DEBUG, "Writing burst of %zu bytes, burst_count = %zu cmd_size = %zu", burst,
+           burst_count, command_size);
+    if (burst == 0) {
+      zxlogf(WARNING, "TPM burst is zero when it shouldn't be.");
+      continue;
+    }
+
+    auto view = fidl::VectorView<uint8_t>::FromExternal(buf, burst);
+    auto result = tpm_.Write(0, RegisterAddress::kTpmDataFifo, view);
+    if (!result.ok()) {
+      zxlogf(ERROR, "FIDL call failed!");
+      return result.status();
+    }
+    if (result->result.is_err()) {
+      zxlogf(ERROR, "Failed to write: %d", result->result.err());
+      return result->result.err();
+    }
+
+    buf += burst;
+    command_size -= burst;
+  }
+
+  // There should be exactly one byte left.
+  if (command_size != 1) {
+    return ZX_ERR_BAD_STATE;
+  }
+
+  do {
+    status = sts.ReadFrom(tpm_);
+    if (status != ZX_OK) {
+      return status;
+    }
+  } while (sts.sts_valid() == 0);
+  if (sts.expect() != 1) {
+    zxlogf(ERROR, "TPM should expect more data!");
+    return ZX_ERR_BAD_STATE;
+  }
+  auto result =
+      tpm_.Write(0, RegisterAddress::kTpmDataFifo, fidl::VectorView<uint8_t>::FromExternal(buf, 1));
+  if (!result.ok()) {
+    zxlogf(ERROR, "FIDL call failed!");
+    return result.status();
+  }
+  if (result->result.is_err()) {
+    zxlogf(ERROR, "Failed to write: %d", result->result.err());
+    return result->result.err();
+  }
+
+  status = sts.ReadFrom(tpm_);
+  if (status != ZX_OK) {
+    return status;
+  }
+
+  if (sts.expect() == 1) {
+    zxlogf(ERROR, "TPM expected more bytes than we wrote.");
+    return ZX_ERR_INTERNAL;
+  }
+
+  status = StsReg().set_tpm_go(1).WriteTo(tpm_);
+  if (status != ZX_OK) {
+    return status;
+  }
+
+  status = sts.ReadFrom(tpm_);
+  if (status != ZX_OK) {
+    return status;
+  }
+  while (sts.data_avail() == 0) {
+    // Wait for a response.
+    status = sts.ReadFrom(tpm_);
+    if (status != ZX_OK) {
+      return status;
+    }
+    zx::nanosleep(zx::deadline_after(zx::usec(500)));
+  }
+
+  while (sts.data_avail()) {
+    // TODO(fxbug.dev/76095): actually return the result of the command.
+    size_t burst_count = sts.burst_count();
+    if (burst_count != 0) {
+      auto result = tpm_.Read(0, RegisterAddress::kTpmDataFifo, burst_count);
+      if (!result.ok()) {
+        zxlogf(ERROR, "FIDL call failed!");
+        return result.status();
+      }
+      if (result->result.is_err()) {
+        zxlogf(ERROR, "Failed to read: %d", result->result.err());
+        return result->result.err();
+      }
+    }
+
+    status = sts.ReadFrom(tpm_);
+    if (status != ZX_OK) {
+      return status;
+    }
+  }
+
+  return ZX_OK;
+}
+
+static const zx_driver_ops_t driver_ops = []() {
+  zx_driver_ops_t ops = {};
+  ops.version = DRIVER_OPS_VERSION;
+  ops.bind = TpmDevice::Create;
+  return ops;
+}();
+}  // namespace tpm
+
+// clang-format off
+ZIRCON_DRIVER(tpm, tpm::driver_ops, "zircon", "0.1");
diff --git a/src/devices/tpm/drivers/tpm/tpm.h b/src/devices/tpm/drivers/tpm/tpm.h
new file mode 100644
index 0000000..d27d74c
--- /dev/null
+++ b/src/devices/tpm/drivers/tpm/tpm.h
@@ -0,0 +1,40 @@
+// 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_TPM_DRIVERS_TPM_TPM_H_
+#define SRC_DEVICES_TPM_DRIVERS_TPM_TPM_H_
+
+#include <fuchsia/hardware/tpmimpl/llcpp/fidl.h>
+#include <lib/ddk/device.h>
+
+#include <ddktl/device.h>
+
+#include "src/devices/tpm/drivers/tpm/commands.h"
+
+namespace tpm {
+
+class TpmDevice;
+using DeviceType = ddk::Device<TpmDevice, ddk::Initializable, ddk::Suspendable>;
+
+class TpmDevice : public DeviceType {
+ public:
+  TpmDevice(zx_device_t* parent, fidl::WireSyncClient<fuchsia_hardware_tpmimpl::TpmImpl> client)
+      : DeviceType(parent), tpm_(std::move(client)) {}
+
+  static zx_status_t Create(void* ctx, zx_device_t* parent);
+
+  // DDK mixins
+  void DdkInit(ddk::InitTxn txn);
+  void DdkRelease() { delete this; }
+  void DdkSuspend(ddk::SuspendTxn txn);
+
+ private:
+  zx_status_t DoCommand(TpmCmdHeader* cmd);
+
+  fidl::WireSyncClient<fuchsia_hardware_tpmimpl::TpmImpl> tpm_;
+};
+
+}  // namespace tpm
+
+#endif  // SRC_DEVICES_TPM_DRIVERS_TPM_TPM_H_