[qemu_edu] Talk to hardware

Rather than implement FIDL purely in software, this patch enables
qemu_edu to map registers in the pci bar and use them to communicate
with the fake hardware.

Tested: ffx component run \
  "fuchsia-pkg://fuchsiasamples.com/eductl#meta/eductl.cm"
Fixed: 98881
Change-Id: I14e6a30d8856480b298efb0c2c98e623c2b97c5e
Reviewed-on: https://fuchsia-review.googlesource.com/c/sdk-samples/drivers/+/675202
Reviewed-by: Christopher Anderson <cja@google.com>
Fuchsia-Auto-Submit: Suraj Malhotra <surajmalhotra@google.com>
Reviewed-by: Suraj Malhotra <surajmalhotra@google.com>
Reviewed-by: Gurjant Kalsi <gkalsi@google.com>
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
diff --git a/src/qemu_edu/BUILD.bazel b/src/qemu_edu/BUILD.bazel
index e2cebcb..62671cd 100644
--- a/src/qemu_edu/BUILD.bazel
+++ b/src/qemu_edu/BUILD.bazel
@@ -1,10 +1,10 @@
 load(
     "@rules_fuchsia//fuchsia:defs.bzl",
+    "fuchsia_cc_binary",
     "fuchsia_component",
     "fuchsia_component_manifest",
-    "fuchsia_cc_binary",
-    "fuchsia_driver_component",
     "fuchsia_driver_bytecode_bind_rules",
+    "fuchsia_driver_component",
     "fuchsia_fidl_library",
     "fuchsia_fidl_llcpp_library",
     "fuchsia_package",
@@ -33,7 +33,10 @@
     name = "bind_bytecode",
     output = "qemu_edu.bindbc",
     rules = "qemu_edu.bind",
-    deps = ["@fuchsia_sdk//bind/fuchsia.pci", "@fuchsia_sdk//bind/fuchsia.acpi"],
+    deps = [
+        "@fuchsia_sdk//bind/fuchsia.acpi",
+        "@fuchsia_sdk//bind/fuchsia.pci",
+    ],
 )
 
 cc_binary(
@@ -41,16 +44,20 @@
     srcs = [
         "qemu_edu.cc",
         "qemu_edu.h",
+        "registers.h",
     ],
     linkshared = True,
     deps = [
         ":fuchsia.hardware.qemuedu_cc",
         "@fuchsia_sdk//fidl/fuchsia.device.fs:fuchsia.device.fs_llcpp_cc",
         "@fuchsia_sdk//fidl/fuchsia.driver.compat:fuchsia.driver.compat_llcpp_cc",
+        "@fuchsia_sdk//fidl/fuchsia.hardware.pci:fuchsia.hardware.pci_llcpp_cc",
         "@fuchsia_sdk//fidl/zx:zx_cc",
         "@fuchsia_sdk//pkg/driver2-llcpp",
         "@fuchsia_sdk//pkg/driver_runtime_cpp",
         "@fuchsia_sdk//pkg/fidl-llcpp-experimental-driver-only",
+        "@fuchsia_sdk//pkg/hwreg",
+        "@fuchsia_sdk//pkg/mmio",
         "@fuchsia_sdk//pkg/sys_component_llcpp",
         "@fuchsia_sdk//pkg/zx-experimental-driver-only",
     ],
@@ -63,8 +70,8 @@
 
 fuchsia_driver_component(
     name = "component",
-    driver_lib = ":qemu_edu",
     bind_bytecode = ":bind_bytecode",
+    driver_lib = ":qemu_edu",
     manifest = ":manifest",
 )
 
diff --git a/src/qemu_edu/eductl.cc b/src/qemu_edu/eductl.cc
index 8bc7865..031a089 100644
--- a/src/qemu_edu/eductl.cc
+++ b/src/qemu_edu/eductl.cc
@@ -16,7 +16,7 @@
 #include <sys/types.h>
 
 constexpr char kEduDevicePath[] =
-    "/dev/sys/platform/platform-passthrough/PCI0/bus/00:06.0/qemu-edu";
+    "/dev/sys/platform/platform-passthrough/PCI0/bus/00:06.0_/qemu-edu";
 constexpr char kEduDevicePath2[] =
     "/dev/sys/platform/platform-passthrough/acpi/acpi-KBLT/qemu-edu";
 
diff --git a/src/qemu_edu/meta/eductl.cml b/src/qemu_edu/meta/eductl.cml
index 110560f..d1b153c 100644
--- a/src/qemu_edu/meta/eductl.cml
+++ b/src/qemu_edu/meta/eductl.cml
@@ -6,7 +6,7 @@
         forward_stdout_to: "log",
         args: [
           'factorial',
-          '15',
+          '12',
         ]
     },
     use: [
diff --git a/src/qemu_edu/qemu_edu.bind b/src/qemu_edu/qemu_edu.bind
index 14c61d6..f4a2082 100644
--- a/src/qemu_edu/qemu_edu.bind
+++ b/src/qemu_edu/qemu_edu.bind
@@ -4,7 +4,7 @@
 using fuchsia.acpi;
 using fuchsia.pci;
 
-if fuchsia.BIND_PROTOCOL == fuchsia.pci.BIND_PROTOCOL.DEVICE {
+if fuchsia.BIND_FIDL_PROTOCOL == fuchsia.pci.BIND_FIDL_PROTOCOL.DEVICE {
 	fuchsia.BIND_PCI_VID == 0x1234;
 	fuchsia.BIND_PCI_DID == 0x11e8;
 } else {
diff --git a/src/qemu_edu/qemu_edu.cc b/src/qemu_edu/qemu_edu.cc
index c1efd9f..291688f 100644
--- a/src/qemu_edu/qemu_edu.cc
+++ b/src/qemu_edu/qemu_edu.cc
@@ -6,6 +6,8 @@
 
 #include <fidl/fuchsia.driver.compat/cpp/wire.h>
 
+#include "src/qemu_edu/registers.h"
+
 namespace fdf {
 using namespace fuchsia_driver_framework;
 }  // namespace fdf
@@ -13,7 +15,9 @@
 namespace qemu_edu {
 
 namespace {
+namespace regs = qemu_edu_regs;
 namespace fio = fuchsia_io;
+namespace fpci = fuchsia_hardware_pci;
 
 zx::status<fidl::ClientEnd<fuchsia_driver_compat::Device>>
 ConnectToParentDevice(const driver::Namespace* ns, std::string_view name) {
@@ -30,32 +34,118 @@
     fdf::wire::DriverStartArgs& start_args, fdf::UnownedDispatcher dispatcher,
     fidl::WireSharedClient<fdf::Node> node, driver::Namespace ns,
     driver::Logger logger) {
-  auto driver = std::make_unique<QemuEduDriver>(
-      dispatcher->async_dispatcher(), std::move(node), std::move(ns), std::move(logger));
-  auto result = driver->Run(dispatcher->async_dispatcher(), std::move(start_args.outgoing_dir()));
+  auto driver = std::make_unique<QemuEduDriver>(dispatcher->async_dispatcher(),
+                                                std::move(node), std::move(ns),
+                                                std::move(logger));
+  auto result = driver->Run(dispatcher->async_dispatcher(),
+                            std::move(start_args.outgoing_dir()));
   if (result.is_error()) {
     return result.take_error();
   }
   return zx::ok(std::move(driver));
 }
 
+zx::status<> QemuEduDriver::MapInterruptAndMmio(
+    fidl::ClientEnd<fpci::Device> pci_client) {
+  auto pci = fidl::BindSyncClient(std::move(pci_client));
+
+  auto bar = pci->GetBar(0);
+  if (!bar.ok()) {
+    FDF_SLOG(ERROR, "failed to get bar", KV("status", bar.status()));
+    return zx::error(bar.status());
+  }
+  if (bar->result.is_err()) {
+    FDF_SLOG(ERROR, "failed to get bar", KV("status", bar->result.err()));
+    return zx::error(bar->result.err());
+  }
+
+  {
+    auto& bar_result = bar->result.response().result;
+    if (!bar_result.result.is_vmo()) {
+      FDF_SLOG(ERROR, "unexpected bar type");
+      return zx::error(ZX_ERR_NO_RESOURCES);
+    }
+
+    zx::status<fdf::MmioBuffer> mmio = fdf::MmioBuffer::Create(
+        0, bar_result.size, std::move(bar_result.result.vmo()),
+        ZX_CACHE_POLICY_UNCACHED_DEVICE);
+    if (mmio.is_error()) {
+      FDF_SLOG(ERROR, "failed to map mmio", KV("status", mmio.status_value()));
+      return mmio.take_error();
+    }
+    mmio_ = *std::move(mmio);
+  }
+
+  // Read the version information from the edu device.
+  auto version_reg = regs::Identification::Get().ReadFrom(&*mmio_);
+
+  // TODO(surajmalhotra): Report version via inspect.
+  major_version_ = version_reg.major_version();
+  minor_version_ = version_reg.minor_version();
+
+  // Map the interrupt.
+  auto result = pci->SetInterruptMode(fpci::wire::InterruptMode::kLegacy, 1);
+  if (!result.ok()) {
+    FDF_SLOG(ERROR, "failed configure interrupt mode",
+             KV("status", result.status()));
+    return zx::error(result.status());
+  }
+  if (result->result.is_err()) {
+    FDF_SLOG(ERROR, "failed configure interrupt mode",
+             KV("status", result->result.err()));
+    return zx::error(result->result.err());
+  }
+
+  auto interrupt = pci->MapInterrupt(0);
+  if (!interrupt.ok()) {
+    FDF_SLOG(ERROR, "failed to map interrupt",
+             KV("status", interrupt.status()));
+    return zx::error(interrupt.status());
+  }
+  if (interrupt->result.is_err()) {
+    FDF_SLOG(ERROR, "failed to map interrupt",
+             KV("status", interrupt->result.err()));
+    return zx::error(interrupt->result.err());
+  }
+  irq_ = std::move(interrupt->result.response().interrupt);
+
+  return zx::ok();
+}
+
 zx::status<> QemuEduDriver::Run(async_dispatcher* dispatcher,
                                 fidl::ServerEnd<fio::Directory> outgoing_dir) {
   // Connect to DevfsExporter.
   auto exporter = ns_.Connect<fuchsia_device_fs::Exporter>();
-  if (exporter.status_value() != ZX_OK) {
+  if (exporter.is_error()) {
     return exporter.take_error();
   }
   exporter_ = fidl::WireClient(std::move(*exporter), dispatcher);
 
   // Connect to parent.
   auto parent = ConnectToParentDevice(&ns_, "default");
-  if (parent.status_value() != ZX_OK) {
+  if (parent.is_error()) {
     FDF_SLOG(ERROR, "Failed to connect to parent",
              KV("status", parent.status_string()));
     return parent.take_error();
   }
 
+  // Connect to pci protocol.
+  auto pci_endpoints = fidl::CreateEndpoints<fpci::Device>();
+  if (pci_endpoints.is_error()) {
+    return pci_endpoints.take_error();
+  }
+  auto connect_result = fidl::WireCall(*parent)->ConnectFidl(
+      fidl::StringView::FromExternal(
+          fidl::DiscoverableProtocolName<fpci::Device>),
+      pci_endpoints->server.TakeChannel());
+  if (!connect_result.ok()) {
+    return zx::error(connect_result.status());
+  }
+  auto pci_status = MapInterruptAndMmio(std::move(pci_endpoints->client));
+  if (pci_status.is_error()) {
+    return pci_status.take_error();
+  }
+
   auto result = fidl::WireCall(*parent)->GetTopologicalPath();
   if (!result.ok()) {
     FDF_SLOG(ERROR, "Failed to get topological path",
@@ -63,11 +153,11 @@
     return zx::error(result.status());
   }
 
-  std::string path(result->path.data(), result->path.size());
+  std::string path(result->path.get());
 
   auto status =
       outgoing_.AddProtocol<fuchsia_hardware_qemuedu::Device>(this, Name());
-  if (status.status_value() != ZX_OK) {
+  if (status.is_error()) {
     FDF_SLOG(ERROR, "Failed to add protocol",
              KV("status", status.status_string()));
     return status;
@@ -106,17 +196,37 @@
 void QemuEduDriver::ComputeFactorial(
     ComputeFactorialRequestView request,
     ComputeFactorialCompleter::Sync& completer) {
-  uint32_t factorial = 1;
-  for (uint32_t i = 1; i < request->input; i++) {
-    factorial *= i;
+  // Write a value into the factorial register.
+  uint32_t input = request->input;
+
+  mmio_->Write32(input, regs::kFactorialCompoutationOffset);
+
+  // Busy wait on the factorial status bit.
+  while (true) {
+    const auto status = regs::Status::Get().ReadFrom(&*mmio_);
+    if (!status.busy()) break;
   }
 
-  FDF_SLOG(INFO, "Replying with", KV("factorial", std::to_string(factorial).c_str()));
+  // Return the result.
+  uint32_t factorial = mmio_->Read32(regs::kFactorialCompoutationOffset);
+
+  FDF_SLOG(INFO, "Replying with", KV("factorial", factorial));
   completer.Reply(factorial);
 }
+
 void QemuEduDriver::LivenessCheck(LivenessCheckRequestView request,
                                   LivenessCheckCompleter::Sync& completer) {
-  completer.Reply(true);
+  constexpr uint32_t kChallenge = 0xdeadbeef;
+  constexpr uint32_t kExpectedResponse = ~(kChallenge);
+
+  // Write the challenge and observe that the expected response is received.
+  mmio_->Write32(kChallenge, regs::kLivenessCheckOffset);
+  auto value = mmio_->Read32(regs::kLivenessCheckOffset);
+
+  const bool alive = value == kExpectedResponse;
+
+  FDF_SLOG(INFO, "Replying with", KV("result", alive));
+  completer.Reply(alive);
 }
 
 }  // namespace qemu_edu
diff --git a/src/qemu_edu/qemu_edu.h b/src/qemu_edu/qemu_edu.h
index 73b838a..71a61a0 100644
--- a/src/qemu_edu/qemu_edu.h
+++ b/src/qemu_edu/qemu_edu.h
@@ -7,13 +7,16 @@
 
 #include <fidl/fuchsia.device.fs/cpp/wire.h>
 #include <fidl/fuchsia.driver.framework/cpp/wire.h>
+#include <fidl/fuchsia.hardware.pci/cpp/wire.h>
 #include <fidl/fuchsia.hardware.qemuedu/cpp/wire.h>
 #include <lib/async/dispatcher.h>
 #include <lib/driver2/namespace.h>
 #include <lib/driver2/record_cpp.h>
 #include <lib/driver2/structured_logger.h>
 #include <lib/fdf/cpp/dispatcher.h>
+#include <lib/mmio/mmio.h>
 #include <lib/sys/component/llcpp/outgoing_directory.h>
+#include <lib/zx/interrupt.h>
 #include <lib/zx/status.h>
 
 #include <memory>
@@ -48,6 +51,8 @@
                      LivenessCheckCompleter::Sync& completer);
 
  private:
+  zx::status<> MapInterruptAndMmio(
+      fidl::ClientEnd<fuchsia_hardware_pci::Device> pci);
   zx::status<> Run(async_dispatcher* dispatcher,
                    fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir);
 
@@ -56,6 +61,12 @@
   driver::Namespace ns_;
   driver::Logger logger_;
   fidl::WireClient<fuchsia_device_fs::Exporter> exporter_;
+
+  std::optional<fdf::MmioBuffer> mmio_;
+  zx::interrupt irq_;
+
+  uint32_t major_version_ = 0;
+  uint32_t minor_version_ = 0;
 };
 
 }  // namespace qemu_edu
diff --git a/src/qemu_edu/registers.h b/src/qemu_edu/registers.h
new file mode 100644
index 0000000..57d064c
--- /dev/null
+++ b/src/qemu_edu/registers.h
@@ -0,0 +1,47 @@
+// Copyright 2022 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_QEMU_EDU_REGISTERS_H_
+#define SRC_QEMU_EDU_REGISTERS_H_
+
+#include <hwreg/bitfields.h>
+
+namespace qemu_edu_regs {
+
+constexpr uint32_t kIdentificationOffset = 0x00;
+constexpr uint32_t kLivenessCheckOffset = 0x04;
+constexpr uint32_t kFactorialCompoutationOffset = 0x08;
+constexpr uint32_t kStatusRegisterOffset = 0x20;
+constexpr uint32_t kInterruptStatusRegisterOffset = 0x24;
+constexpr uint32_t kInterruptRaiseRegisterOffset = 0x60;
+constexpr uint32_t kInterruptAcknowledgeRegisterOffset = 0x64;
+constexpr uint32_t kDmaSourceAddressOffset = 0x80;
+constexpr uint32_t kDmaDestinationAddressOffset = 0x80;
+constexpr uint32_t kDmaTransferCountOffset = 0x90;
+constexpr uint32_t kDmaCommandRegisterOffset = 0x98;
+
+class Identification : public hwreg::RegisterBase<Identification, uint32_t> {
+ public:
+  DEF_FIELD(31, 24, major_version);
+  DEF_FIELD(23, 16, minor_version);
+  DEF_FIELD(15, 0, edu);
+
+  static auto Get() {
+    return hwreg::RegisterAddr<Identification>(kIdentificationOffset);
+  }
+};
+
+class Status : public hwreg::RegisterBase<Status, uint32_t> {
+ public:
+  DEF_BIT(0, busy);
+  DEF_BIT(7, irq_enable);
+
+  static auto Get() {
+    return hwreg::RegisterAddr<Status>(kStatusRegisterOffset);
+  }
+};
+
+}  // namespace qemu_edu
+
+#endif  // SRC_QEMU_EDU_REGISTERS_H_