設定硬體資源

週邊元件互連網路 (PCI) 裝置會使用各種介面向系統提供資源,包括中斷、記憶體對應的 I/O (MMIO) 註冊和直接記憶體存取 (DMA) 緩衝區。Fuchsia 驅動程式會透過父項裝置節點的功能存取這些資源。針對 PCI 裝置,父項提供 fuchsia.hardware.pci/Device FIDL 通訊協定的執行個體,讓驅動程式庫可以設定裝置。

在本節中,您將新增在 edu 裝置上存取下列 MMIO 註冊的功能:

地址偏移 註冊 R/W 說明
0x00 身分識別 RO 主要 / 子版本 ID
0x04 卡片有效性檢查 RW 驗證作業的驗證問題
0 x 8 因式運算 RW 儲存值的計算階乘
0x20 狀態 RW 用來表示作業已完成的 Bitfields

完成這個部分後,專案應具有以下目錄結構:

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

連線到家長裝置

如要從父項裝置節點存取 fuchsia.hardware.pci/Device 介面,請將 fuchsia.hardware.pci.Service 能力新增至驅動程式庫元件資訊清單:

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' },
    ],
}

這麼做可讓驅動程式庫開啟與父項裝置的連線,並存取其提供的硬體專屬通訊協定。

更新驅動程式庫的 Start() 方法,以便存取父項裝置在驅動程式庫初始化期間提供的 fuchsia.hardware.pci/Device

qemu_edu/drivers/qemu_edu.cc:

#include "qemu_edu.h"

#include <lib/driver/component/cpp/driver_export.h>

namespace qemu_edu {
// ...

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

  // Connect to the parent device node.
  zx::result connect_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()));
    return connect_result.take_error();
  }

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

  return zx::ok();
}

}  // namespace qemu_edu

設定中斷和 MMIO

開啟 fuchsia.hardware.pci/Device 連線後,您就可以開始將必要的裝置資源對應至驅動程式庫。

在專案目錄中建立新的 qemu_edu/drivers/edu_device.h 檔案,並在其中加入以下內容:

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>

namespace edu_device {

// Interacts with the device hardware using a fuchsia.hardware.pci client.
class QemuEduDevice {
 public:
  explicit QemuEduDevice(async_dispatcher_t* dispatcher,
                         fidl::ClientEnd<fuchsia_hardware_pci::Device> pci)
      : dispatcher_(dispatcher), 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_;

  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_

建立新的 qemu_edu/drivers/edu_device.cc 檔案並新增以下程式碼,以實作 MapInterruptAndMmio() 方法。這個方法會執行下列工作:

  1. 存取適當 PCI 區域的基準地址註冊 (BAR)。
  2. 擷取該區域的 Fuchsia VMO (虛擬記憶體物件)。
  3. 請在區域周圍建立 MMIO 緩衝區,存取個別暫存器。
  4. 設定對應至裝置中斷情形的中斷要求 (IRQ)。

qemu_edu/drivers/edu_device.cc:

#include "edu_device.h"

#include <lib/driver/logging/cpp/structured_logger.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

將新的裝置資源新增至驅動程式庫類別:

qemu_edu/drivers/qemu_edu.h:

#include <lib/driver/component/cpp/driver_base.h>
#include <lib/driver/devfs/cpp/connector.h>
#include <fidl/examples.qemuedu/cpp/wire.h>

#include "edu_device.h"

namespace qemu_edu {

class QemuEduDriver : public fdf::DriverBase {

 public:
  QemuEduDriver(fdf::DriverStartArgs start_args,
                fdf::UnownedSynchronizedDispatcher driver_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();
  void Serve(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

更新驅動程式庫的 Run() 方法,以便在驅動程式庫初始化期間呼叫新方法:

qemu_edu/drivers/qemu_edu.cc:

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

  // Connect to the parent device node.
  zx::result connect_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()));
    return connect_result.take_error();
  }

  // Map hardware resources from the PCI device
  device_ = std::make_shared<edu_device::QemuEduDevice>(dispatcher(), std::move(connect_result.value()));
  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();
}

更新驅動程式庫建構設定,納入新的來源檔案,並依附於 fuchsia.hardware.pci 的 FIDL 繫結程式庫:

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",
    ],
)

讀取裝置註冊記錄

將基本資源對應至驅動程式庫後,您就可以存取個別註冊作業。將下列註冊定義新增至專案的 qemu_edu/drivers/edu_device.h 檔案:

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>

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(async_dispatcher_t* dispatcher,
                         fidl::ClientEnd<fuchsia_hardware_pci::Device> pci)
      : dispatcher_(dispatcher), 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_;

  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

這會將裝置規格中提供的註冊位移宣告為常數。Fuchsia 的 hwreg 程式庫會封裝代表位元的註冊資料,不需執行個別位元作業即可輕鬆存取。

qemu_edu/drivers/edu_device.cc 中實作下列其他方法,即可與 MMIO 區域互動,將資料讀取及寫入個別的 edu 裝置註冊中:

  • ComputeFactorial():將輸入值寫入階乘運算登錄,然後等待裝置透過中斷以非同步方式傳送完成。
  • HandleIrq():讀取階乘暫存器的運算結果,並回報給待處理的回呼。
  • LivenessCheck():將驗證值寫入有效性檢查登錄器,並確認預期結果。

qemu_edu/drivers/edu_device.cc:

#include "edu_device.h"

#include <lib/driver/logging/cpp/structured_logger.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

將以下內容新增至驅動程式庫的 Start() 方法,以便從 MMIO 區域讀取識別資訊登錄的主要和次要版本,並將其輸出至記錄中:

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>(dispatcher(), std::move(connect_result.value()));
  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();
}

重新啟動模擬器

關閉所有現有的模擬器執行個體:

ffx emu stop --all

啟動已啟用驅動程式庫架構的 Fuchsia 模擬器新執行個體:

ffx emu start core.x64 --headless

重新載入驅動程式庫

使用 bazel run 指令建構並執行元件目標:

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

bazel run 指令會重新建構套件,並執行 ffx driver register 以重新載入驅動程式庫元件。

檢查系統記錄,確認您可以看見更新後的 FDF_SLOG() 訊息,其中包含從識別註冊器讀取的版本:

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_
[full-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 

恭喜!您的驅動程式庫現在可以存取繫結裝置節點提供的 PCI 硬體資源。