總覽
本文說明如何編寫及測試 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 程式庫,因此別忘了將其新增為驅動程式測試的依附元件: