概览
本文档介绍了如何编写和测试能够以高效方式监听中断的 Fuchsia 驱动程序。中断是一种常用工具,用于让驱动程序了解何时发生了特定硬件(或虚拟)事件。在 C++ 中,中断由 zx::interrupt 类表示。您可能会看到“interrupt”和“irq”这两个字词交替使用。在此上下文中,两者都表示中断。
获取中断
驱动程序获取中断对象的方式取决于具体情况。一种常见的方法是从 FIDL 服务实例请求中断。例如,如果驱动程序需要一个表示与特定 GPIO 引脚相关的 GPIO 事件的中断对象,则该驱动程序可以通过向驱动程序传入命名空间中的 fuchsia.hardware.gpio.Service FIDL 服务实例发送 fuchsia.hardware.gpio.Gpio:GetInterrupt() FIDL 请求来请求一个。
聆听中断
监听中断是指在触发中断时执行代码。在驱动程序中,中断在其生命周期内被触发多次的情况很常见。驱动程序应能尽可能快速地处理中断,并且不会与其他驱动程序代码发生数据竞争。根据这些要求,建议使用 async::IrqMethod 类来监听中断。
IrqMethod 接受一个类实例方法(即回调),每当触发相应的中断时,该方法都会执行。它还接受用于执行回调的调度程序。建议使用驱动程序的调度器 DriverBase::dispatcher()。如果驱动程序的调度器是同步的(驱动程序调度器默认情况下是同步的),则回调的执行将等待调度器当前未执行其他代码。请注意,这意味着中断处理程序的回调执行会阻塞调度器,使其无法执行其他代码,直到回调执行完成。这与在发生中断触发时在单独的线程中执行代码相反。在这种情况下,驱动程序可能会因其他原因在第一个线程中执行其他代码,并且这两个线程之间可能会发生数据竞争。不建议采用这种方法,因为它需要使用会增加驱动程序复杂性、降低其可读性/可维护性并引入难以调试的同步 bug 的同步方法。
以下示例展示了驱动程序如何使用 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( // 选项。 0,
// Timestamp of when the interrupt was triggered.
zx::clock::get_boot()),
ZX_OK); ```
这会导致驱动程序的中断处理程序执行其针对中断触发的回调。
验证中断是否已确认
下一步是验证驱动程序是否确认了中断。当驱动程序确认中断触发时,中断会返回到“未触发”状态。中断还会发送有关此状态变化的信号。测试将监听此信号,以了解中断何时得到确认。首次创建虚拟中断时,也会发送此信号。
建议使用 async::WaitMethod 类来等待中断信号。与 async::IrqMethod 类似,当相应的中断发送特定信号时,它会调用其回调。一个重要的区别是,在调用 async::WaitMethod 的回调后,需要重新“启用”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 库,因此请务必将其添加为驱动程序测试的依赖项: