Overview
This document covers how to write and test a Fuchsia driver that can listen to
interrupts in an efficient manner.
Interrupts are a common tool for letting a driver know when a certain hardware
(or virtual) event has occurred. In C++, interrupts are represented by the
zx::interrupt class. You
may see the words "interrupt" and "irq" used interchangeably. In this context,
they both represent an interrupt.
Acquiring an interrupt
How the driver acquires an interrupt object is context-dependent. A common
approach is to request an interrupt from a FIDL service instance. For example,
if a driver wanted an interrupt object that represented GPIO events related to a
specific GPIO pin then the driver can request one by sending a
fuchsia.hardware.gpio.Gpio:GetInterrupt() FIDL request
to the fuchsia.hardware.gpio.Service FIDL service
instance within the driver's incoming namespace.
Listening to an interrupt
Listening to an interrupt means executing code when the interrupt is triggered.
In drivers, it is common for interrupts to be triggered more than once over the
course of the interrupt's lifetime. The driver should be able to handle
interrupts as quickly as possible and not cause data races with other driver
code. Based on these requirements, it is recommended to use the
async::IrqMethod class to listen
to an interrupt.
IrqMethod accepts a class instance method (i.e. a callback) that will be
executed every time the corresponding interrupt is triggered. It also accepts a
dispatcher used to execute the callback. It is recommended to use the driver's
dispatcher DriverBase::dispatcher(). If the driver's dispatcher is
synchronized (driver dispatchers are synchronized by default) then the execution
of the callback will wait until the dispatcher is not currently executing other
code. Keep in mind that this means the interrupt handler's callback execution
will block the dispatcher from executing other code until it completes. This is
opposed to executing code in a separate thread when an interrupt trigger occurs.
In that scenario, the driver might be executing other code in the first thread
for other reasons and data races may occur between the two threads. This
approach is not recommended as it requires synchronization methods that increase
the driver's complexity, reduce its readability/maintainability, and introduce
synchronization bugs which are difficult to debug.
Here's an example of how a driver can listen to an interrupt using
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 belongs to the async-cpp library so don't forget to add it
as a dependency to the driver:
GN
source_set("my-driver") {
deps = [
"//sdk/lib/async:async-cpp",
]
}
Bazel
cc_library(
name = "my-driver",
deps = [
"@fuchsia_sdk//pkg/async-cpp",
],
)
Testing an interrupt
The driver's unit tests should test the driver's ability to respond to interrupts. This requires that the test can trigger interrupts without waiting for a real hardware event and can verify that the driver is acknowledging interrupts.
Providing an interrupt
The test should create a virtual interrupt to provide to the driver. Virtual interrupts are interrupts that can be triggered "virtually" (i.e. the test's code can explicitly trigger the interrupt without waiting for an actual hardware event). A virtual interrupt can be created like so:
zx::interrupt interrupt;
ASSERT_EQ(
zx::interrupt::create(zx::resource(), 0, ZX_INTERRUPT_VIRTUAL, &interrupt),
ZX_OK);
The test should duplicate this interrupt and send the duplicate to the driver.
How the test sends the duplicate to the driver is context-dependent. It
is recommended to simulate how the driver really acquires an interrupt. For
example, if a driver acquires a GPIO interrupt from a
fuchsia.hardware.gpio.Service FIDL service instance then the test should fake
that FIDL service instance. Here is how to duplicate an interrupt:
zx::interrupt duplicate;
ASSERT_EQ(interrupt.duplicate(ZX_RIGHT_SAME_RIGHTS, &duplicate), ZX_OK);
Triggering an interrupt
The test can trigger a virtual interrupt like so: ```cpp ASSERT_EQ( interrupt.trigger( // Options. 0,
// Timestamp of when the interrupt was triggered.
zx::clock::get_boot()),
ZX_OK); ```
This will cause the driver's interrupt handler to execute its callback for interrupt triggers.
Verifying an interrupt was acknowledged
The next step is verifying that the driver acknowledged the interrupt. When a driver acknowledges an interrupt trigger, the interrupt returns to an "untriggered" state. The interrupt will also send a signal about this state change. The test will listen for this signal to know when the interrupt has been acknowledged. This signal is also sent when a virtual interrupt is first created.
It is recommended to use
async::WaitMethod class in
order to wait for the interrupt's signals. Similar to async::IrqMethod, it will
call its callback when the corresponding interrupt sends a specific signal. One
important difference is that async::WaitMethod will need to be "re-armed"
after its callback is called, otherwise, it will not call its callback when it
receives multiple signals.
Here's an example of how to listen to an interrupt acknowledgement:
#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 belongs to the async-cpp library so don't forget to add it
as a dependency to the driver's tests: