Configure hardware resources

Peripheral Component Interconnect (PCI) devices expose resources to the system using a variety of interfaces including Interrupts, Memory-Mapped I/O (MMIO) registers, and Direct Memory Access (DMA) buffers. Fuchsia drivers access these resources through capabilities from the parent device node. For PCI devices, the parent offers an instance of the fuchsia.hardware.pci/Device FIDL protocol to enable the driver to configure the device.

In this section, you'll be adding functionality to access the following MMIO registers on the edu device:

Address offset Register R/W Description
0x00 Identification RO Major / minor version identifier
0x04 Card liveness check RW Challenge to verify operation
0x08 Factorial computation RW Compute factorial of the stored value
0x20 Status RW Bitfields to signal the operation is complete

After you complete this section, the project should have the following directory structure:

//fuchsia-codelab/qemu_edu/drivers
                  |- BUILD.bazel
                  |- meta
                  |   |- qemu_edu.cml
                  |- driver_compat.h 
                  |- qemu_edu.bind
                  |- qemu_edu.cc
                  |- qemu_edu.h
                  |- registers.h 

Connect to the parent device

To access the fuchsia.hardware.pci/Device interface from the parent device node, add the fuchsia.driver.compat.Service capability to the driver's component manifest:

qemu_edu/drivers/meta/qemu_edu.cml:

{
    include: [
        "syslog/client.shard.cml",
    ],
    program: {
        runner: 'driver',
        binary: 'lib/libqemu_edu.so',
        bind: 'meta/bind/qemu_edu.bindbc'
    },
    use: [
        { service: 'fuchsia.driver.compat.Service' },
    ],
}

This enables the driver to open a connection to the parent device and access the hardware-specific protocols it offers.

Create the qemu_edu/drivers/driver_compat.h file and add the following code to use the fuchsia.driver.compat.Service capability to open the device connection:

qemu_edu/drivers/driver_compat.h:

#ifndef FUCHSIA_CODELAB_QEMU_EDU_DRIVER_COMPAT_H_
#define FUCHSIA_CODELAB_QEMU_EDU_DRIVER_COMPAT_H_

#include <fidl/fuchsia.driver.compat/cpp/wire.h>

namespace edu_driver_compat {

// Connect to parent device node using fuchsia.driver.compat.Service
zx::status<fidl::ClientEnd<fuchsia_driver_compat::Device>> ConnectToParentDevice(
    const driver::Namespace* ns, std::string_view name) {
  auto result = ns->OpenService<fuchsia_driver_compat::Service>(name);
  if (result.is_error()) {
    return result.take_error();
  }
  return result.value().connect_device();
}

}  // namespace driver_compat

#endif  // FUCHSIA_CODELAB_QEMU_EDU_DRIVER_COMPAT_H_

Update the driver's Run() method to access the fuchsia.hardware.pci/Device offered by the parent device during driver initialization:

qemu_edu/drivers/qemu_edu.cc:

#include "qemu_edu.h"

#include "driver_compat.h"

// ...

// Initialize this driver instance
zx::status<> QemuEduDriver::Run(async_dispatcher* dispatcher,
                                fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir) {

  // Connect to the parent device node.
  auto parent = edu_driver_compat::ConnectToParentDevice(&ns_, "default");
  if (parent.is_error()) {
    FDF_SLOG(ERROR, "Failed to connect to parent", KV("status", parent.status_string()));
    return parent.take_error();
  }

  // Connect to fuchsia.hardware.pci FIDL protocol from the parent device
  auto pci_endpoints = fidl::CreateEndpoints<fuchsia_hardware_pci::Device>();
  if (pci_endpoints.is_error()) {
    return pci_endpoints.take_error();
  }
  auto connect_result = fidl::WireCall(*parent)->ConnectFidl(
      fidl::StringView::FromExternal(fidl::DiscoverableProtocolName<fuchsia_hardware_pci::Device>),
      pci_endpoints->server.TakeChannel());
  if (!connect_result.ok()) {
    return zx::error(connect_result.status());
  }

  FDF_SLOG(INFO, "edu driver loaded successfully");

  return zx::ok();
}

Set up interrupts and MMIO

With a connection open to the fuchsia.hardware.pci/Device, you can begin to map the necessary device resources into the driver. Add the following code to your driver class to declare a new MapInterruptAndMmio() method:

qemu_edu/drivers/qemu_edu.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/sys/component/llcpp/outgoing_directory.h>
#include <lib/zx/status.h>

#include <fidl/fuchsia.hardware.pci/cpp/wire.h>
#include <lib/mmio/mmio.h>
#include <lib/zx/interrupt.h>

namespace qemu_edu {

class QemuEduDriver {
 public:
  QemuEduDriver(async_dispatcher_t* dispatcher,
                fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
                driver::Logger logger)
      : outgoing_(component::OutgoingDirectory::Create(dispatcher)),
        node_(std::move(node)),
        ns_(std::move(ns)),
        logger_(std::move(logger)) {}

  virtual ~QemuEduDriver() = default;

  // Report driver name to driver framework
  static constexpr const char* Name() { return "qemu-edu"; }
  // Start hook called by driver framework
  static zx::status<std::unique_ptr<QemuEduDriver>> Start(
      fuchsia_driver_framework::wire::DriverStartArgs& start_args,
      fdf::UnownedDispatcher dispatcher,
      fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
      driver::Logger logger);

 private:
  zx::status<> Run(async_dispatcher* dispatcher,
                   fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir);
  zx::status<> MapInterruptAndMmio(fidl::ClientEnd<fuchsia_hardware_pci::Device> pci);
  
  component::OutgoingDirectory outgoing_;
  fidl::WireSharedClient<fuchsia_driver_framework::Node> node_;
  driver::Namespace ns_;
  driver::Logger logger_;

  std::optional<fdf::MmioBuffer> mmio_;
  zx::interrupt irq_;
};

}  // namespace qemu_edu

This method performs the following tasks:

  1. Access the Base Address Register (BAR) of the appropriate PCI region.
  2. Extract Fuchsia's VMO (Virtual Memory Object) for the region.
  3. Create an MMIO buffer around the region to access individual registers.
  4. Configure an Interrupt Request (IRQ) mapped to the device's interrupt.

Add the following code to implement the MapInterruptAndMmio() method:

qemu_edu/drivers/qemu_edu.cc:

namespace qemu_edu {
// ...

// Initialize PCI device hardware resources
zx::status<> QemuEduDriver::MapInterruptAndMmio(
    fidl::ClientEnd<fuchsia_hardware_pci::Device> pci_client) {
  auto pci = fidl::BindSyncClient(std::move(pci_client));

  // Retrieve the Base Address Register (BAR) for PCI Region 0
  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->is_error()) {
    FDF_SLOG(ERROR, "failed to get bar", KV("status", bar->error_value()));
    return zx::error(bar->error_value());
  }

  // Create a Memory-Mapped I/O (MMIO) region over BAR0
  {
    auto& bar_result = bar->value()->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);
  }

  // Configure interrupt handling for the device
  auto result = pci->SetInterruptMode(fuchsia_hardware_pci::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->is_error()) {
    FDF_SLOG(ERROR, "failed configure interrupt mode", KV("status", result->error_value()));
    return zx::error(result->error_value());
  }

  // Map the device's interrupt to a system IRQ
  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->is_error()) {
    FDF_SLOG(ERROR, "failed to map interrupt", KV("status", interrupt->error_value()));
    return zx::error(interrupt->error_value());
  }
  irq_ = std::move(interrupt->value()->interrupt);

  return zx::ok();
}

}  // namespace qemu_edu

Update the driver's Run() method to call the new method during driver initialization:

qemu_edu/drivers/qemu_edu.cc:

// Initialize this driver instance
zx::status<> QemuEduDriver::Run(async_dispatcher* dispatcher,
                                fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir) {

  // Connect to the parent device node.
  auto parent = edu_driver_compat::ConnectToParentDevice(&ns_, "default");
  if (parent.is_error()) {
    FDF_SLOG(ERROR, "Failed to connect to parent", KV("status", parent.status_string()));
    return parent.take_error();
  }

  // Connect to fuchsia.hardware.pci FIDL protocol from the parent device
  auto pci_endpoints = fidl::CreateEndpoints<fuchsia_hardware_pci::Device>();
  if (pci_endpoints.is_error()) {
    return pci_endpoints.take_error();
  }
  auto connect_result = fidl::WireCall(*parent)->ConnectFidl(
      fidl::StringView::FromExternal(fidl::DiscoverableProtocolName<fuchsia_hardware_pci::Device>),
      pci_endpoints->server.TakeChannel());
  if (!connect_result.ok()) {
    return zx::error(connect_result.status());
  }

  // Map hardware resources from the PCI device
  auto pci_status = MapInterruptAndMmio(std::move(pci_endpoints->client));
  if (pci_status.is_error()) {
    return pci_status.take_error();
  }

  FDF_SLOG(INFO, "edu driver loaded successfully");

  return zx::ok();
}

Update the driver build configuration to depend on the FIDL binding libraries for these two protocols:

qemu_edu/drivers/BUILD.bazel:

cc_binary(
    name = "qemu_edu",
    srcs = [
        "qemu_edu.cc",
        "qemu_edu.h",
    ],
    linkshared = True,
    deps = [
        "@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_cpp_wire",
        "@fuchsia_sdk//pkg/hwreg",
        "@fuchsia_sdk//pkg/mmio",
        "@fuchsia_sdk//pkg/sys_component_llcpp",
        "@fuchsia_sdk//pkg/zx",
    ],
)

Read device registers

With the base resources mapped into the driver, you can access individual registers. Create the new qemu_edu/drivers/registers.h file in your project directory with the following contents:

qemu_edu/drivers/registers.h:

#ifndef FUCHSIA_CODELAB_QEMU_EDU_REGISTERS_H_
#define FUCHSIA_CODELAB_QEMU_EDU_REGISTERS_H_

#include <hwreg/bitfields.h>

namespace edu_device_registers {

// Register offset addresses for edu device MMIO area
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 edu_device_registers

#endif  // FUCHSIA_CODELAB_QEMU_EDU_REGISTERS_H_

This file declares the register offsets provided in the device specification as constants. Fuchsia's hwreg library wraps the registers that represent bitfields, making them easier to access without performing individual bitwise operations.

Add the following to the driver's Run() method to read the major/minor version from the identification register from the MMIO region and print it to the log:

qemu_edu/drivers/qemu_edu.cc:

#include "qemu_edu.h"

#include "driver_compat.h"

#include "registers.h"

// ...

// Initialize this driver instance
zx::status<> QemuEduDriver::Run(async_dispatcher* dispatcher,
                                fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir) {
  // ...

  // Map hardware resources from the PCI device
  auto pci_status = MapInterruptAndMmio(std::move(pci_endpoints->client));
  if (pci_status.is_error()) {
    return pci_status.take_error();
  }

  // Report the version information from the edu device.
  auto version_reg = edu_device_registers::Identification::Get().ReadFrom(&*mmio_);
  FDF_SLOG(INFO, "edu device version", KV("major", version_reg.major_version()),
           KV("minor", version_reg.minor_version()));

  return zx::ok();
}

Update the driver's build configuration to include the new includes as source files:

qemu_edu/drivers/BUILD.bazel:

cc_binary(
    name = "qemu_edu",
    srcs = [
        "qemu_edu.cc",
        "qemu_edu.h",
        "driver_compat.h",
        "registers.h",
    ],
    linkshared = True,
    deps = [
        "@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_cpp_wire",
        "@fuchsia_sdk//pkg/hwreg",
        "@fuchsia_sdk//pkg/mmio",
        "@fuchsia_sdk//pkg/sys_component_llcpp",
        "@fuchsia_sdk//pkg/zx",
    ],
)

Restart the emulator

Shut down any existing emulator instances:

ffx emu stop --all

Start a new instance of the Fuchsia emulator with driver framework enabled:

ffx emu start workstation_eng.qemu-x64 --headless \
 --kernel-args "driver_manager.use_driver_framework_v2=true" \
 --kernel-args "driver_manager.root-driver=fuchsia-boot:///#meta/platform-bus.cm" \
 --kernel-args "devmgr.enable-ephemeral=true"

Reload the driver

Use the bazel run command to build and execute the component target:

bazel run --config=fuchsia_x64 //fuchsia-codelab/qemu_edu/drivers:pkg.component

The bazel run command rebuilds the package and runs ffx driver register to reload the driver component.

Inspect the system log and verify that you can see the updated FDF_SLOG() message containing the version read from the identification register:

ffx log --filter qemu_edu
[driver_manager][driver_manager.cm][I]: [driver_runner.cc:959] Binding fuchsia-pkg://bazel.pkg.component/qemu_edu#meta/qemu_edu.cm to  00_06_0_
[universe-pkg-drivers:root.sys.platform.platform-passthrough.PCI0.bus.00_06_0_][qemu-edu,driver][I]: [fuchsia-codelab/qemu_edu/qemu_edu.cc:75] edu device version major=1 minor=0 

Congratulations! Your driver can now access the PCI hardware resources provided by the bound device node.