處理驅動程式庫中的中斷

總覽

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

取得中斷

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

聆聽中斷

監聽中斷要求是指在中斷要求觸發時執行程式碼。 在驅動程式中,中斷通常會在生命週期內觸發多次。驅動程式應盡可能快速處理中斷,且不會與其他驅動程式庫程式碼造成資料競爭。

C++

根據這些需求,建議使用 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",
     ],
 )

荒漠油廠

在 Rust 中,中斷通常是透過從中斷控制代碼建立 fuchsia_async::OnInterrupt 結構體,並在產生的工作中處理該結構體來處理。OnInterrupt implements futures::Stream ,可供非同步監聽。

以下範例說明 Rust 驅動程式如何監聽中斷:

use fdf_component::{Driver, DriverContext};
use fuchsia_async::{OnInterrupt, Task};
use futures::StreamExt;
use zx::{Interrupt, RealInterruptKind};

pub struct MyDriver {
  /// Task that handles interrupts. Hold onto it so that when the driver is
  /// dropped the task is cancelled.
  interrupt_handler: Task<()>,
}

impl Driver for MyDriver {
    async fn start(context: DriverContext) -> Result<Self, Status> {
        let interrupt: Interrupt<RealInterruptKind> = todo!();
        let interrupt_stream = OnInterrupt::new(interrupt);
        let interrupt_handler = Task::spawn(async move {
          while let Some(Ok(_time)) = interrupt_stream.next().await {
            // Perform work in response to triggered interrupt.
            if let Err(e) = Self::handle_interrupt() {
                log::error!("Failed to handle interrupt: {e:?}");
                // Don't return early as we still want to ack the interrupt.
            }

            // Acknowledge the interrupt. This "re-arms" the interrupt. If the
            // interrupt is not acknowledged then it cannot be triggered again.
            if let Err(e) = interrupt_stream.ack() {
                log::error!("Failed to ack interrupt: {e:?}");
            }
          }
        });

        Ok(Self { interrupt_handler })
    }

    async fn handle_interrupt() -> Result<(), Status> {
        todo!();
    }
}

OnInterrupt 屬於 fuchsia_async 程式庫,因此別忘了將其新增為驅動程式庫的依附元件:

GN

 fuchsia_component("my-driver") {
   deps = [
     "//src/lib/fuchsia-async",
     "//third_party/rust_crates:futures",
   ]
 }

測試中斷

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

提供中斷

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

C++

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

荒漠油廠

let interrupt = zx::VirtualInterrupt::create_virtual()?;

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

C++

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

荒漠油廠

// Duplicate the virtual interrupt.
let interrupt: zx::VirtualInterrupt =
  self.interrupt.duplicate_handle(Rights::SAME_RIGHTS)?;

// Convert the duplicated virtual interrupt into a real interrupt as the
// driver expects a real interrupt.
let interrupt = Interrupt::from(interrupt.into_handle());

觸發中斷

測試可以觸發虛擬中斷,如下所示:

C++

ASSERT_EQ(
  interrupt.trigger(
    // Options.
    0,

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

荒漠油廠

interrupt.trigger(
  // Timestamp of when the interrupt was triggered.
  zx::Instant::from_nanos(0)
)?;

這會向驅動程式庫發出中斷觸發訊號。

確認中斷已獲得確認

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

C++

建議使用 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",
     ],
 )

荒漠油廠

在 Rust 中,您可以使用 fuchsia_async::OnSignals VIRTUAL_INTERRUPT_UNTRIGGERED 訊號非同步等待:OnSignals 實作 Future,因此可以等待。

use fuchsia_async::OnSignals;
use zx::Signals;

// Wait for the interrupt to be acknowledged.
OnSignals::new(&interrupt, Signals::VIRTUAL_INTERRUPT_UNTRIGGERED).await?;

如要在 Rust 驅動程式庫測試中使用這項功能,您必須將 fuchsia-async 依附元件新增至 BUILD.gnBUILD.bazel 檔案:

GN

 fuchsia_unittest("my-driver-test") {
   deps = [
     "//src/lib/fuchsia-async",
   ]
 }

Bazel

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