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 offsetRegisterR/WDescription
0x00IdentificationROMajor / minor version identifier
0x04Card liveness checkRWChallenge to verify operation
0x08Factorial computationRWCompute factorial of the stored value
0x20StatusRWBitfields 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.hardware.pci.Service capability to the driver's component manifest:

qemu_edu/drivers/meta/qemu_edu.cml:

{include:["syslog/client.shard.cml",],program:{runner:'driver',binary:'driver/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.hardware.pci.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/driver_export.h>namespaceqemu_edu{// ...// Initialize this driver instancezx::result<>QemuEduDriver::Start(){// Connect to the parent device node.zx::resultconnect_result=incoming()->Connect<fuchsia_hardware_pci::Service::Device>("default");if(connect_result.is_error()){FDF_SLOG(ERROR,"Failed to open pci service.",KV("status",connect_result.status_string()));returnconnect_result.take_error();}FDF_SLOG(INFO,"edu driver loaded successfully");returnzx::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/mmio/mmio.h>#include <lib/zx/interrupt.h>namespaceedu_device{// Interacts with the device hardware using a fuchsia.hardware.pci client.classQemuEduDevice{public:explicitQemuEduDevice(async_dispatcher_t*dispatcher,fidl::ClientEnd<fuchsia_hardware_pci::Device>pci):dispatcher_(dispatcher),pci_(std::move(pci)){}zx::result<>MapInterruptAndMmio();private:voidHandleIrq(async_dispatcher_t*dispatcher,async::IrqBase*irq,zx_status_tstatus,constzx_packet_interrupt_t*interrupt);async_dispatcher_t*constdispatcher_;fidl::WireSyncClient<fuchsia_hardware_pci::Device>pci_;std::optional<fdf::MmioBuffer>mmio_;zx::interruptirq_;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"#include <lib/driver/logging/cpp/structured_logger.h>namespaceedu_device{// Initialize PCI device hardware resourceszx::result<>QemuEduDevice::MapInterruptAndMmio(){// Retrieve the Base Address Register (BAR) for PCI Region 0autobar=pci_->GetBar(0);if(!bar.ok()){FDF_SLOG(ERROR,"failed to get bar",KV("status",bar.status()));returnzx::error(bar.status());}if(bar->is_error()){FDF_SLOG(ERROR,"failed to get bar",KV("status",bar->error_value()));returnzx::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");returnzx::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()));returnmmio.take_error();}mmio_=*std::move(mmio);}// Configure interrupt handling for the device using INTxautoresult=pci_->SetInterruptMode(fuchsia_hardware_pci::wire::InterruptMode::kLegacy,1);if(!result.ok()){FDF_SLOG(ERROR,"failed configure interrupt mode",KV("status",result.status()));returnzx::error(result.status());}if(result->is_error()){FDF_SLOG(ERROR,"failed configure interrupt mode",KV("status",result->error_value()));returnzx::error(result->error_value());}// Map the device's interrupt to a system IRQautointerrupt=pci_->MapInterrupt(0);if(!interrupt.ok()){FDF_SLOG(ERROR,"failed to map interrupt",KV("status",interrupt.status()));returnzx::error(interrupt.status());}if(interrupt->is_error()){FDF_SLOG(ERROR,"failed to map interrupt",KV("status",interrupt->error_value()));returnzx::error(interrupt->error_value());}irq_=std::move(interrupt->value()->interrupt);// Start listening for interrupts.irq_method_.set_object(irq_.get());irq_method_.Begin(dispatcher_);returnzx::ok();}}// namespace edu_device

Add the new device resources to the driver class:

qemu_edu/drivers/qemu_edu.h:

#include <fidl/examples.qemuedu/cpp/wire.h>#include <lib/driver/component/cpp/driver_base.h>#include <lib/driver/devfs/cpp/connector.h>#include"edu_device.h"namespaceqemu_edu{classQemuEduDriver:publicfdf::DriverBase{public:QemuEduDriver(fdf::DriverStartArgsstart_args,fdf::UnownedSynchronizedDispatcherdriver_dispatcher):fdf::DriverBase("qemu-edu",std::move(start_args),std::move(driver_dispatcher)),devfs_connector_(fit::bind_member<&QemuEduDriver::Serve>(this)){}virtual~QemuEduDriver()=default;// Start hook called by the driver factory.zx::result<>Start()override;private:zx::result<>ExportToDevfs();voidServe(fidl::ServerEnd<examples_qemuedu::Device>request);fidl::WireSyncClient<fuchsia_driver_framework::Node>node_;fidl::WireSyncClient<fuchsia_driver_framework::NodeController>controller_;driver_devfs::Connector<examples_qemuedu::Device>devfs_connector_;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 instancezx::result<>QemuEduDriver::Start(){// Connect to the parent device node.zx::resultconnect_result=incoming()->Connect<fuchsia_hardware_pci::Service::Device>("default");if(connect_result.is_error()){FDF_SLOG(ERROR,"Failed to open pci service.",KV("status",connect_result.status_string()));returnconnect_result.take_error();}// Map hardware resources from the PCI devicedevice_=std::make_shared<edu_device::QemuEduDevice>(dispatcher(),std::move(connect_result.value()));autopci_status=device_->MapInterruptAndMmio();if(pci_status.is_error()){returnpci_status.take_error();}FDF_SLOG(INFO,"edu driver loaded successfully");returnzx::ok();}

Update the driver build configuration to include the new source files and depend on the FIDL binding libraries for fuchsia.hardware.pci:

qemu_edu/drivers/BUILD.bazel:

fuchsia_cc_driver(name="qemu_edu",srcs=["edu_device.cc","edu_device.h","qemu_edu.cc","qemu_edu.h",],deps=["@fuchsia_sdk//fidl/fuchsia.hardware.pci:fuchsia.hardware.pci_llcpp_cc","@fuchsia_sdk//pkg/driver_component_cpp","@fuchsia_sdk//pkg/driver_devfs_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/mmio/mmio.h>#include <lib/zx/interrupt.h>namespaceedu_device{// Register offset addresses for edu device MMIO areaconstexpruint32_tkIdentificationOffset=0x00;constexpruint32_tkLivenessCheckOffset=0x04;constexpruint32_tkFactorialComputationOffset=0x08;constexpruint32_tkStatusRegisterOffset=0x20;constexpruint32_tkInterruptStatusRegisterOffset=0x24;constexpruint32_tkInterruptRaiseRegisterOffset=0x60;constexpruint32_tkInterruptAcknowledgeRegisterOffset=0x64;constexpruint32_tkDmaSourceAddressOffset=0x80;constexpruint32_tkDmaDestinationAddressOffset=0x80;constexpruint32_tkDmaTransferCountOffset=0x90;constexpruint32_tkDmaCommandRegisterOffset=0x98;classIdentification:publichwreg::RegisterBase<Identification,uint32_t>{public:DEF_FIELD(31,24,major_version);DEF_FIELD(23,16,minor_version);DEF_FIELD(15,0,edu);staticautoGet(){returnhwreg::RegisterAddr<Identification>(kIdentificationOffset);}};classStatus:publichwreg::RegisterBase<Status,uint32_t>{public:DEF_BIT(0,busy);DEF_BIT(7,irq_enable);staticautoGet(){returnhwreg::RegisterAddr<Status>(kStatusRegisterOffset);}};// Interacts with the device hardware using a fuchsia.hardware.pci client.classQemuEduDevice{public:explicitQemuEduDevice(async_dispatcher_t*dispatcher,fidl::ClientEnd<fuchsia_hardware_pci::Device>pci):dispatcher_(dispatcher),pci_(std::move(pci)){}zx::result<>MapInterruptAndMmio();voidComputeFactorial(uint32_tinput,fit::callback<void(zx::result<uint32_t>)>callback);zx::result<uint32_t>LivenessCheck(uint32_tchallenge);IdentificationIdentificationRegister(){returnIdentification::Get().ReadFrom(&*mmio_);}StatusStatusRegister(){returnStatus::Get().ReadFrom(&*mmio_);}private:voidHandleIrq(async_dispatcher_t*dispatcher,async::IrqBase*irq,zx_status_tstatus,constzx_packet_interrupt_t*interrupt);async_dispatcher_t*constdispatcher_;fidl::WireSyncClient<fuchsia_hardware_pci::Device>pci_;std::optional<fdf::MmioBuffer>mmio_;zx::interruptirq_;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"#include <lib/driver/logging/cpp/structured_logger.h>namespaceedu_device{// ...// Write data into the factorial register wait for an interrupt.voidQemuEduDevice::ComputeFactorial(uint32_tinput,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.autostatus=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.voidQemuEduDevice::HandleIrq(async_dispatcher_t*dispatcher,async::IrqBase*irq,zx_status_tstatus,constzx_packet_interrupt_t*interrupt){irq_.ack();if(!pending_callback_.has_value()){FDF_LOG(ERROR,"Received unexpected interrupt!");return;}autocallback=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.autoint_status=mmio_->Read32(kInterruptStatusRegisterOffset);mmio_->Write32(int_status,kInterruptAcknowledgeRegisterOffset);// Deassert the legacy INTx interrupt on the PCI bus.autoirq_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_tfactorial=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_tchallenge){// Write the challenge value to the liveness check register.mmio_->Write32(challenge,kLivenessCheckOffset);// Return the result.autovalue=mmio_->Read32(kLivenessCheckOffset);returnzx::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 instancezx::result<>QemuEduDriver::Start(){// ...// Map hardware resources from the PCI devicedevice_=std::make_shared<edu_device::QemuEduDevice>(dispatcher(),std::move(connect_result.value()));autopci_status=device_->MapInterruptAndMmio();if(pci_status.is_error()){returnpci_status.take_error();}// Report the version information from the edu device.autoversion_reg=device_->IdentificationRegister();FDF_SLOG(INFO,"edu device version",KV("major",version_reg.major_version()),KV("minor",version_reg.minor_version()));returnzx::ok();}

Restart the emulator

Shut down any existing emulator instances:

ffxemustop--all

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

ffxemustartcore.x64--headless

Reload the driver

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

bazelrun//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:

ffxlog--filterqemu_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_ [full-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.