處理驅動程式庫中的中斷

總覽

本文說明如何編寫及測試 Fuchsia 驅動程式,以便有效率地監聽中斷。中斷是常見的工具,可讓驅動程式庫瞭解何時發生特定硬體 (或虛擬) 事件。在 C++ 中,中斷會以 zx::interrupt 類別表示。「interrupt」和「irq」這兩個字詞可能會交替使用。在此情況下,兩者都代表中斷。

取得中斷

驅動程式庫取得中斷物件的方式取決於情境。常見的做法是從 FIDL 服務執行個體要求中斷。舉例來說,如果驅動程式庫需要代表與特定 GPIO 針腳相關 GPIO 事件的中斷物件,驅動程式庫可以傳送 fuchsia.hardware.gpio.Gpio:GetInterrupt() FIDL 要求至驅動程式庫傳入命名空間內的 fuchsia.hardware.gpio.Service FIDL 服務例項,要求取得該物件。

聆聽中斷

監聽中斷是指在中斷觸發時執行程式碼。在驅動程式中,中斷通常會在生命週期內觸發多次。驅動程式應盡可能快速處理中斷,且不會導致與其他驅動程式庫程式碼發生資料競爭。根據這些需求,建議使用 async::IrqMethod 類別監聽中斷。

IrqMethod 接受類別執行個體方法 (即回呼),每當觸發對應的中斷時,就會執行該方法。這個函式也會接受用於執行回呼的調度器。建議使用驅動程式庫的調度器 DriverBase::dispatcher()。如果驅動程式庫的調度器已同步處理 (驅動程式庫調度器預設會同步處理),回呼的執行作業會等到調度器目前未執行其他程式碼時才會執行。請注意,這表示中斷處理常式的回呼執行作業完成前,分派器會遭到封鎖,無法執行其他程式碼。這與發生中斷觸發程序時,在個別執行緒中執行程式碼不同。在這種情況下,驅動程式庫可能會因為其他原因在第一個執行緒中執行其他程式碼,而兩個執行緒之間可能會發生資料競爭。不建議採用這種做法,因為這類同步方法會增加驅動程式的複雜度、降低可讀性/可維護性,並導致難以偵錯的同步錯誤。

以下範例說明驅動程式庫如何使用 async::IrqMethod 監聽中斷:

#include <lib/async/cpp/irq.h>

class MyDriver : public fdf::DriverBase {
 public:
  zx::result<> Start() override {
    // Get the interrupt for a GPIO FIDL service.
    zx::result<fidl::ClientEnd<fuchsia_hardware_gpio::Gpio>> gpio =
      incoming()->Connect<fuchsia_hardware_gpio::Service::Device>(kIrqGpioParentName);
    if (gpio.is_error()) {
      fdf::error("Failed to connect to irq gpio: {}", gpio);
      return gpio.take_error();
    }
    fidl::WireResult interrupt = fidl::WireCall(gpio.value())->GetInterrupt({});
    if (!interrupt.ok()) {
      fdf::error("Failed to send GetInterrupt request: {}", interrupt.status_string());
      return zx::error(interrupt.status());
    }
    if (interrupt->is_error()) {
      fdf::error("Failed to get interrupt: {}", zx_status_get_string(interrupt->error_value()));
      return interrupt->take_error();
    }
    interrupt_ = std::move(interrupt->value()->interrupt);

    // Start listening to `interrupt_`. `interrupt_handler_` will execute its
    // associated callback on dispatcher `dispatcher()` when `interrupt_` is
    // triggered.
    interrupt_handler_.set_object(interrupt_.get());
    zx_status_t status = interrupt_handler_.Begin(dispatcher());
    if (status != ZX_OK) {
      fdf::error("Failed to listen to interrupt: {}",
        zx_status_get_string(status));
      return zx::error(status);
    }

    return zx::ok();
  }

 private:
  // Called by `interrupt_handler_` when `interrupt_` is triggered.
  void HandleInterrupt(
    // Dispatcher that `HandleInterrupt()` was executed on.
    async_dispatcher_t* dispatcher,

    // Object that executed `HandleInterrupt()`.
    async::IrqBase* irq,

    // Status of handling the interrupt.
    zx_status_t status,

    // Information related to the interrupt.
    const zx_packet_interrupt_t* interrupt_packet) {

    if (status != ZX_OK) {
      if (status == ZX_ERR_CANCELED) {
        // Expected behavior as this occurs when `interrupt_handler_` is
        // destructed.
        fdf::debug("Interrupt handler cancelled");
      } else {
        fdf::error("Failed to handle interrupt: {}",
          zx_status_get_string(status));
      }

      // An error status means that the interrupt was not triggered so don't
      // handle it.
      return;
    }

    // Wrap the interrupt ack in a defer to ensure that the interrupt is
    // acknowledged even in the case that an error occurs while trying to
    // handle the interrupt.
    auto ack_interrupt = fit::defer([this] {
      // Acknowledge the interrupt. This "re-arms" the interrupt. If the
      // interrupt is not acknowledged then `interrupt_` cannot be triggered
      // again and `HandleInterrupt()` will not get called again.
      interrupt_.ack();
    });

    // Perform work in response to triggered interrupt.
  }

  // Interrupt to listen to.
  zx::interrupt interrupt_;

  // Calls `this->HandleInterrupt()` every time `interrupt_` is triggered.
  // Destructing `interrupt_handler_` means to no longer listen to `interrupt_`.
  async::IrqMethod<MyDriver, &MyDriver::HandleInterrupt> interrupt_handler_{this};
};

async::IrqMethod 屬於 async-cpp 程式庫,因此請務必將其新增為驅動程式庫的依附元件:

GN

 source_set("my-driver") {
   deps = [
     "//sdk/lib/async:async-cpp",
   ]
 }

Bazel

 cc_library(
     name = "my-driver",
     deps = [
         "@fuchsia_sdk//pkg/async-cpp",
     ],
 )

測試中斷

驅動程式的單元測試應測試驅動程式庫回應中斷的能力。這項測試必須能觸發中斷,不必等待實際的硬體事件,並能驗證驅動程式庫是否會確認中斷。

提供中斷

測試應建立虛擬中斷,提供給驅動程式庫。虛擬中斷是指「虛擬」觸發的中斷 (也就是說,測試的程式碼可以明確觸發中斷,不必等待實際的硬體事件)。虛擬中斷可以這樣建立:

zx::interrupt interrupt;
ASSERT_EQ(
  zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &interrupt),
  ZX_OK);

這項測試應複製中斷,並將副本傳送至驅動程式庫。 測試將重複項目傳送至驅動程式庫的方式取決於環境。建議模擬驅動程式庫實際取得中斷的方式。舉例來說,如果驅動程式庫從 fuchsia.hardware.gpio.Service FIDL 服務執行個體取得 GPIO 中斷,測試應模擬該 FIDL 服務執行個體。以下說明如何複製中斷:

zx::interrupt duplicate;
ASSERT_EQ(interrupt.duplicate(ZX_RIGHT_SAME_RIGHTS, &duplicate), ZX_OK);

觸發中斷

測試可以觸發虛擬中斷,如下所示: ```cpp ASSERT_EQ( interrupt.trigger( // Options. 0,

// Timestamp of when the interrupt was triggered.
zx::clock::get_boot()),

ZX_OK); ```

這會導致驅動程式庫的中斷處理常式執行中斷觸發的回呼。

確認中斷已獲得確認

下一步是確認驅動程式庫已確認中斷。當驅動程式庫確認中斷觸發程序時,中斷會返回「未觸發」狀態。中斷也會傳送有關此狀態變更的信號。測試會監聽這個信號,瞭解中斷要求何時獲得確認。首次建立虛擬中斷時,也會傳送這個訊號。

建議使用 async::WaitMethod 類別,等待中斷訊號。與 async::IrqMethod 類似,當對應的中斷傳送特定信號時,它會呼叫回呼。其中一項重要差異是,async::WaitMethod 必須在回呼呼叫後「重新啟動」,否則收到多個訊號時,不會呼叫回呼。

以下範例說明如何監聽中斷確認:

#include <lib/async/cpp/wait.h>

class MyDriverEnvironment : public fdf_testing::Environment {
 public:
  zx::result<> Serve(fdf::OutgoingDirectory& to_driver_vfs) override {
    // Create a virtual interrupt to be listened to by the driver.
    EXPECT_EQ(
      zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &interrupt_),
      ZX_OK);


    zx::interrupt duplicate;
    EXPECT_EQ(interrupt_.duplicate(ZX_RIGHT_SAME_RIGHTS, &duplicate), ZX_OK);
    // Send duplicate interrupt to driver.

    // Dispatcher used to execute `HandleInterruptAck()`. In a driver unit test,
    // it is recommended to use the environment dispatcher so that
    // `HandleInterruptAck()` doesn't block the driver's code execution.
    async_dispatcher_t* dispatcher = fdf::Dispatcher::GetCurrent()->async_dispatcher();

    // Listen for when `interrupt_` is acknowledged.
    interrupt_ack_handler_.set_object(interrupt_.get());
    EXPECT_EQ(interrupt_ack_handler_.Begin(dispatcher), ZX_OK);

    return zx::ok();
  }

 private:
  // Called when `interrupt_` receives an acknowledgement.
  void HandleInterruptAck(
    // Dispatcher that `HandleInterruptAck()` was called on.
    async_dispatcher_t* dispatcher,

    // Object responsible for calling `HandleInterruptAck()`.
    async::WaitBase* wait,

    // Status of waiting for the acknowledgement.
    zx_status_t status,

    // Information related to the acknowledgement.
    const zx_packet_signal_t* signal) {

    if (status != ZX_OK) {
        FAIL() << "Failed to wait for interrupt ack" << zx_status_get_string(status);
    }

    // Do something in response to the acknowledgement.

    // "Re-arm" the listener. Wait for the next time `interrupt_` is
    // acknowledged.
    status = wait->Begin(dispatcher);
    if (status != ZX_OK) {
        fdf::error("Failed to re-arm interrupt ack handler: {}", zx_status_get_string(status));
    }
  }

  // Virtual interrupt that the driver is listening to.
  zx::interrupt interrupt_;

  // Calls `HandleInterruptAck()` whenever `interrupt_` receives an
  // acknowledgement.
  async::WaitMethod<InterruptController, &InterruptController::HandleInterruptAck>
    interrupt_ack_handler_{
      // Class instance to call `HandleInterruptAck()` on.
      this,

      // The object that the signals belong to. The test will provide the
      // interrupt object to `interrupt_ack_handler_` after the interrupt is
      // constructed.
      ZX_HANDLE_INVALID,

      // Call the callback when `interrupt_` is in the "untriggered" state.
      ZX_VIRTUAL_INTERRUPT_UNTRIGGERED,

      // Only call `HandleInterruptAck()` if the
      // ZX_VIRTUAL_INTERRUPT_UNTRIGGERED signal was received after
      // `interrupt_ack_handler_.Begin()` was called. If `interrupt_` is already
      // in the "untriggered" state before `interrupt_ack_handler_.Begin()` is
      // called then don't call `HandleInterruptAck()`.
      ZX_WAIT_ASYNC_EDGE
  };
};

class MyDriverTestConfiguration final {
 public:
  using DriverType = MyDriver;
  using EnvironmentType = MyDriverEnvironment;
};

class MyDriverTest : public testing::Test {
 public:
  void SetUp() override {
    ASSERT_EQ(driver_test_.StartDriver().status_value(), ZX_OK);
  }

 private:
  fdf_testing::BackgroundDriverTest<MyDriverTestConfiguration> driver_test_;
};

async::WaitMethod 屬於 async-cpp 程式庫,因此別忘了將其新增為驅動程式測試的依附元件:

GN

 test("my-driver-test-bin") {
   deps = [
     "//sdk/lib/async:async-cpp",
   ]
 }

Bazel

 fuchsia_cc_test(
     name = "my-driver-test",
     deps = [
         "@fuchsia_sdk//pkg/async-cpp",
     ],
 )