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
                  |- edu_device.cc 
                  |- edu_device.h 
                  |- qemu_edu.bind
                  |- qemu_edu.cc
                  |- qemu_edu.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',
        // Identifies the device categories, for compatibility tests. This
        // example driver uses the 'misc' category; real drivers should
        // select a more specific category.
        device_categories: [
          { category: 'misc', subcategory: '' },
        ],
    },
    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.

Update the driver's Start() 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 <lib/driver/component/cpp/service_client.h>

namespace qemu_edu {
// ...

// Initialize this driver instance
zx::result<> QemuEduDriver::Start() {

  // Connect to the parent device node.
  auto compat_parent =
      driver::Connect<fuchsia_driver_compat::Service::Device>(*context().incoming());
  if (compat_parent.is_error()) {
    FDF_SLOG(ERROR, "Failed to connect to compat", KV("status", compat_parent.status_string()));
    return compat_parent.take_error();
  }

  auto parent = fidl::WireSyncClient(std::move(compat_parent.value()));

  // 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 = 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();
}

}  // namespace qemu_edu

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.

Create the new qemu_edu/drivers/edu_device.h file in your project directory with the following contents:

qemu_edu/drivers/edu_device.h:

#ifndef FUCHSIA_CODELAB_QEMU_EDU_DEVICE_H_
#define FUCHSIA_CODELAB_QEMU_EDU_DEVICE_H_

#include <fidl/fuchsia.hardware.pci/cpp/wire.h>
#include <lib/async/cpp/irq.h>
#include <lib/driver/component/cpp/structured_logger.h>
#include <lib/mmio/mmio.h>
#include <lib/zx/interrupt.h>

namespace edu_device {

// Interacts with the device hardware using a fuchsia.hardware.pci client.
class QemuEduDevice {
 public:
  explicit QemuEduDevice(driver::Logger* logger, async_dispatcher_t* dispatcher,
                         fidl::ClientEnd<fuchsia_hardware_pci::Device> pci)
      : dispatcher_(dispatcher), logger_(logger), pci_(std::move(pci)) {}

  zx::result<> MapInterruptAndMmio();

 private:
  void HandleIrq(async_dispatcher_t* dispatcher, async::IrqBase* irq, zx_status_t status,
                 const zx_packet_interrupt_t* interrupt);

  async_dispatcher_t* const dispatcher_;

  driver::Logger* logger_;
  fidl::WireSyncClient<fuchsia_hardware_pci::Device> pci_;
  std::optional<fdf::MmioBuffer> mmio_;
  zx::interrupt irq_;
  async::IrqMethod<QemuEduDevice, &QemuEduDevice::HandleIrq> irq_method_{this};
  std::optional<fit::callback<void(zx::result<uint32_t>)>> pending_callback_;
};

}  // namespace edu_device

#endif  // FUCHSIA_CODELAB_QEMU_EDU_DEVICE_H_

Create the new qemu_edu/drivers/edu_device.cc file and add the following code to implement the MapInterruptAndMmio() method. 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.

qemu_edu/drivers/edu_device.cc:

#include "edu_device.h"

namespace edu_device {

// Initialize PCI device hardware resources
zx::result<> QemuEduDevice::MapInterruptAndMmio() {
  // 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::result<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 using INTx
  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);
  // Start listening for interrupts.
  irq_method_.set_object(irq_.get());
  irq_method_.Begin(dispatcher_);

  return zx::ok();
}

}  // namespace edu_device

Add the new device resources to the driver class:

qemu_edu/drivers/qemu_edu.h:

#include <lib/driver/component/cpp/driver_cpp.h>
#include <lib/driver/compat/cpp/context.h>

#include "edu_device.h"

namespace qemu_edu {

class QemuEduDriver : public driver::DriverBase {

 public:
  QemuEduDriver(driver::DriverStartArgs start_args, fdf::UnownedDispatcher driver_dispatcher)
      : driver::DriverBase("qemu-edu", std::move(start_args), std::move(driver_dispatcher)) {}

  virtual ~QemuEduDriver() = default;

  // Start hook called by the driver factory.
  zx::result<> Start() override;

 private:
  std::unique_ptr<compat::Context> compat_context_;
  std::shared_ptr<edu_device::QemuEduDevice> device_;
};

}  // 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::result<> QemuEduDriver::Start() {

  // Connect to the parent device node.
  auto compat_parent =
      driver::Connect<fuchsia_driver_compat::Service::Device>(*context().incoming());
  if (compat_parent.is_error()) {
    FDF_SLOG(ERROR, "Failed to connect to compat", KV("status", compat_parent.status_string()));
    return compat_parent.take_error();
  }

  auto parent = fidl::WireSyncClient(std::move(compat_parent.value()));

  // 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 = 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
  device_ = std::make_shared<edu_device::QemuEduDevice>(&logger(), dispatcher(),
                                                        std::move(pci_endpoints->client));
  auto pci_status = device_->MapInterruptAndMmio();
  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 fuchsia.hardware.pci:

qemu_edu/drivers/BUILD.bazel:

cc_binary(
    name = "qemu_edu",
    srcs = [
        "qemu_edu.cc",
        "qemu_edu.h",
    ],
    linkshared = True,
    deps = [
        "@fuchsia_sdk//fidl/fuchsia.hardware.pci:fuchsia.hardware.pci_llcpp_cc",
        "@fuchsia_sdk//pkg/driver_compat",
        "@fuchsia_sdk//pkg/driver_component_cpp",
        "@fuchsia_sdk//pkg/hwreg",
        "@fuchsia_sdk//pkg/mmio",
    ],
)

Read device registers

With the base resources mapped into the driver, you can access individual registers. Add the following register definitions to the qemu_edu/drivers/edu_device.h file in your project:

qemu_edu/drivers/edu_device.h:

#include <hwreg/bitfields.h>

#include <fidl/fuchsia.hardware.pci/cpp/wire.h>
#include <lib/async/cpp/irq.h>
#include <lib/driver/component/cpp/structured_logger.h>
#include <lib/mmio/mmio.h>
#include <lib/zx/interrupt.h>

namespace edu_device {

// Register offset addresses for edu device MMIO area
constexpr uint32_t kIdentificationOffset = 0x00;
constexpr uint32_t kLivenessCheckOffset = 0x04;
constexpr uint32_t kFactorialComputationOffset = 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); }
};

// Interacts with the device hardware using a fuchsia.hardware.pci client.
class QemuEduDevice {
 public:
  explicit QemuEduDevice(driver::Logger* logger, async_dispatcher_t* dispatcher,
                         fidl::ClientEnd<fuchsia_hardware_pci::Device> pci)
      : dispatcher_(dispatcher), logger_(logger), pci_(std::move(pci)) {}

  zx::result<> MapInterruptAndMmio();

  void ComputeFactorial(uint32_t input, fit::callback<void(zx::result<uint32_t>)> callback);
  zx::result<uint32_t> LivenessCheck(uint32_t challenge);

  Identification IdentificationRegister() { return Identification::Get().ReadFrom(&*mmio_); }
  Status StatusRegister() { return Status::Get().ReadFrom(&*mmio_); }

 private:
  void HandleIrq(async_dispatcher_t* dispatcher, async::IrqBase* irq, zx_status_t status,
                 const zx_packet_interrupt_t* interrupt);

  async_dispatcher_t* const dispatcher_;

  driver::Logger* logger_;
  fidl::WireSyncClient<fuchsia_hardware_pci::Device> pci_;
  std::optional<fdf::MmioBuffer> mmio_;
  zx::interrupt irq_;
  async::IrqMethod<QemuEduDevice, &QemuEduDevice::HandleIrq> irq_method_{this};
  std::optional<fit::callback<void(zx::result<uint32_t>)>> pending_callback_;
};

}  // namespace edu_device

This 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.

Implement the following additional methods in qemu_edu/drivers/edu_device.cc to interact with the MMIO region to read and write data into the respective edu device registers:

  • ComputeFactorial(): Write an input value to the factorial computation register and wait for the device to asynchronously signal completion using an interrupt.
  • HandleIrq(): Read the computation result from the factorial register and report it to the pending callback.
  • LivenessCheck(): Write a challenge value to the liveness check register and confirm the expected result.

qemu_edu/drivers/edu_device.cc:

#include "edu_device.h"

namespace edu_device {
// ...

// Write data into the factorial register wait for an interrupt.
void QemuEduDevice::ComputeFactorial(uint32_t input,
                                     fit::callback<void(zx::result<uint32_t>)> callback) {
  if (pending_callback_.has_value()) {
    callback(zx::error(ZX_ERR_SHOULD_WAIT));
  }

  // Tell the device to raise an interrupt after computation.
  auto status = StatusRegister();
  status.set_irq_enable(true);
  status.WriteTo(&*mmio_);

  // Write the value into the factorial register to start computation.
  mmio_->Write32(input, kFactorialComputationOffset);

  // We will receive an interrupt when the computation completes.
  pending_callback_ = std::move(callback);
}

// Respond to INTx interrupts triggered by the device, and return the compute result.
void QemuEduDevice::HandleIrq(async_dispatcher_t* dispatcher, async::IrqBase* irq,
                              zx_status_t status, const zx_packet_interrupt_t* interrupt) {
  irq_.ack();
  if (!pending_callback_.has_value()) {
    FDF_LOG(ERROR, "Received unexpected interrupt!");
    return;
  }
  auto callback = std::move(*pending_callback_);
  pending_callback_ = std::nullopt;
  if (status != ZX_OK) {
    FDF_SLOG(ERROR, "Failed to wait for interrupt", KV("status", zx_status_get_string(status)));
    callback(zx::error(status));
    return;
  }

  // Acknowledge the interrupt with the edu device.
  auto int_status = mmio_->Read32(kInterruptStatusRegisterOffset);
  mmio_->Write32(int_status, kInterruptAcknowledgeRegisterOffset);

  // Deassert the legacy INTx interrupt on the PCI bus.
  auto irq_result = pci_->AckInterrupt();
  if (!irq_result.ok() || irq_result->is_error()) {
    FDF_SLOG(ERROR, "Failed to ack PCI interrupt",
             KV("status", irq_result.ok() ? irq_result->error_value() : irq_result.status()));
    callback(zx::error(ZX_ERR_IO));
    return;
  }

  // Reply with the result.
  uint32_t factorial = mmio_->Read32(kFactorialComputationOffset);
  FDF_SLOG(INFO, "Replying with", KV("factorial", factorial));
  callback(zx::ok(factorial));
}

// Write a challenge value to the liveness check register and return the result.
zx::result<uint32_t> QemuEduDevice::LivenessCheck(uint32_t challenge) {
  // Write the challenge value to the liveness check register.
  mmio_->Write32(challenge, kLivenessCheckOffset);

  // Return the result.
  auto value = mmio_->Read32(kLivenessCheckOffset);
  return zx::ok(value);
}

}  // namespace edu_device

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

qemu_edu/drivers/qemu_edu.cc:

// Initialize this driver instance
zx::result<> QemuEduDriver::Start() {
  // ...

  // Map hardware resources from the PCI device
  device_ = std::make_shared<edu_device::QemuEduDevice>(&logger(), dispatcher(),
                                                        std::move(pci_endpoints->client));
  auto pci_status = device_->MapInterruptAndMmio();
  if (pci_status.is_error()) {
    return pci_status.take_error();
  }

  // Report the version information from the edu device.
  auto version_reg = device_->IdentificationRegister();
  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 source files:

qemu_edu/drivers/BUILD.bazel:

cc_binary(
    name = "qemu_edu",
    srcs = [
        "edu_device.cc",
        "edu_device.h",
        "qemu_edu.cc",
        "qemu_edu.h",
    ],
    linkshared = True,
    deps = [
        "@fuchsia_sdk//fidl/fuchsia.hardware.pci:fuchsia.hardware.pci_llcpp_cc",
        "@fuchsia_sdk//pkg/driver_compat",
        "@fuchsia_sdk//pkg/driver_component_cpp",
        "@fuchsia_sdk//pkg/hwreg",
        "@fuchsia_sdk//pkg/mmio",
    ],
)

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 //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.pt.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.