diff --git a/docs/source/pynq_remote.rst b/docs/source/pynq_remote.rst index 54f7ce4e0..d78c5fd1f 100644 --- a/docs/source/pynq_remote.rst +++ b/docs/source/pynq_remote.rst @@ -10,6 +10,7 @@ PYNQ.remote pynq_remote/quickstart pynq_remote/image_build pynq_remote/remote_device + pynq_remote/interrupts pynq_remote/cpp_index pynq_remote/status pynq_remote/env_variables diff --git a/docs/source/pynq_remote/interrupts.rst b/docs/source/pynq_remote/interrupts.rst new file mode 100644 index 000000000..ddf41ae33 --- /dev/null +++ b/docs/source/pynq_remote/interrupts.rst @@ -0,0 +1,32 @@ +.. _remote_interrupts: + +Remote Interrupts +================= + +PYNQ.remote supports hardware interrupts over the network, so interrupt-driven code such as DMA transfer completion and GPIO edge detection works remotely using the same API as a local board. + +Usage +----- + +Interrupts use the same ``Interrupt`` class as classic PYNQ. Set the ``PYNQ_REMOTE_DEVICES`` environment variable before importing ``pynq`` (see :doc:`env_variables`), then create an ``Interrupt`` for a pin and await it: + +.. code-block:: python + + import os + os.environ["PYNQ_REMOTE_DEVICES"] = "192.168.2.99" # before importing pynq + from pynq import Overlay, Interrupt + + ol = Overlay("my_design.bit") + irq = Interrupt("axi_dma_0/mm2s_introut") + await irq.wait() + +No source changes are required to move interrupt-driven code or notebooks between a local board and a remote board. + +Overlay reload +-------------- + +As in classic PYNQ, downloading a new overlay invalidates existing interrupt objects. A subsequent ``wait()`` raises ``RuntimeError("Interrupt invalidated by Overlay change")``. Create a new ``Interrupt`` after loading a new overlay. + +.. note:: + + The ``UioController`` class is not used in remote mode; the target device handles the underlying UIO access. Use ``Interrupt`` for interrupt handling. diff --git a/docs/source/pynq_remote/status.rst b/docs/source/pynq_remote/status.rst index c772c856a..3c897caa9 100644 --- a/docs/source/pynq_remote/status.rst +++ b/docs/source/pynq_remote/status.rst @@ -16,6 +16,6 @@ This document outlines the current status of PYNQ.remote. * :doc:`../pynq_libraries/mmio` * :doc:`../pynq_libraries/dma` * :doc:`../pynq_libraries/allocate` - * :doc:`../pynq_libraries/interrupt` (coming soon) + * :doc:`../pynq_libraries/interrupt` (see :ref:`remote_interrupts`) * :doc:`../pynq_libraries/psgpio` (coming soon) * Multi-board support diff --git a/pynq/pl_server/remote_device.py b/pynq/pl_server/remote_device.py index a6bde9803..afa4ec761 100644 --- a/pynq/pl_server/remote_device.py +++ b/pynq/pl_server/remote_device.py @@ -1,4 +1,5 @@ import os +import asyncio from pathlib import Path import pickle import datetime @@ -25,6 +26,7 @@ mmio_pb2_grpc, mmio_pb2, buffer_pb2_grpc, buffer_pb2, gpio_pb2_grpc, gpio_pb2, + interrupt_pb2_grpc, interrupt_pb2, ) import grpc @@ -187,6 +189,7 @@ def __init__(self, index=0, ip_addr=None, port=PYNQ_PORT, tag="remote{}"): 'mmio': mmio_pb2_grpc.MmioStub(self.client.channel), 'buffer': buffer_pb2_grpc.RemoteBufferStub(self.client.channel), 'gpio': gpio_pb2_grpc.GpioStub(self.client.channel), + 'interrupt': interrupt_pb2_grpc.RemoteInterruptStub(self.client.channel), } self.arch = self.get_arch() @@ -194,6 +197,7 @@ def __init__(self, index=0, ip_addr=None, port=PYNQ_PORT, tag="remote{}"): self.capabilities = { "REMOTE": True, + "INTERRUPT": True, } def get_board_name(self): @@ -808,43 +812,126 @@ def get_gpio_npins(target_label=None, device=None): class RemoteInterrupt: - """Remote Interrupt placeholder class - - Placeholder implementation for interrupt handling on remote devices. - Interrupt functionality is not yet implemented for remote PYNQ devices. - - Parameters - ---------- - fullpath : str, optional - Full path to interrupt device + """Remote interrupt support over gRPC. + + Registers an interrupt on the remote server and waits for hardware + events via a unary gRPC call wrapped with asyncio.wrap_future(). + + If the Overlay is changed or re-downloaded this object is invalidated + and waiting raises a RuntimeError, mirroring the local Interrupt class. """ - - def __init__(self, fullpath=None): + + def __init__(self, fullpath): + """Initialise an Interrupt object attached to the specified pin + + Parameters + ---------- + fullpath : str + Fully qualified interrupt pin name in the block diagram of the + form ${cell}/${pin} (e.g. "axi_dma_0/mm2s_introut"). Raises an + exception if the pin cannot be found in the currently active + Overlay. + """ self.fullpath = fullpath - warnings.warn(f"Interrupts are not yet implemented for remote devices") + self._interrupt_id = None + self._stub = None + self._timestamp = None + self._ensure_registered() + + def _ensure_registered(self): + device = Device.active_device + if not device.has_capability("REMOTE") or not device.has_capability("INTERRUPT"): + raise RuntimeError("Active device does not support remote interrupts") + self._stub = device._stub['interrupt'] + from ..pl import PL + if self.fullpath not in PL.interrupt_pins: + raise ValueError("No Pin of name {} found".format(self.fullpath)) + + pin_info = PL.interrupt_pins[self.fullpath] + controller_name = pin_info.get('controller', pin_info.get('parent', '')) + pin_index = pin_info['index'] + + raw_irq = 0 + controller_phys_addr = 0 + if controller_name: + ctrl_info = PL.interrupt_controllers.get(controller_name, {}) + raw_irq = ctrl_info.get('raw_irq', 0) + controller_phys_addr = PL.ip_dict.get( + ctrl_info.get('name', controller_name), {} + ).get('phys_addr', 0) + + try: + resp = self._stub.register_interrupt( + interrupt_pb2.RegisterRequest( + pin_name=self.fullpath, + pin_index=pin_index, + raw_irq=raw_irq, + controller_phys_addr=controller_phys_addr, + )) + except grpc.RpcError as e: + raise RuntimeError("Interrupt registration RPC failed: {}".format(e)) + if not resp.interrupt_id: + raise RuntimeError( + "Interrupt registration failed: {}".format(resp.msg)) + self._interrupt_id = resp.interrupt_id + self._timestamp = PL.timestamp + + async def wait(self): + """Wait for the interrupt to fire on the remote board. - def wait(self, timeout=None): - raise RuntimeError("Interrupts are not yet implemented for remote devices") + Uses gRPC's .future() variant so that asyncio cancellation + propagates to the server: cancelling the awaiting task calls + call.cancel(), which signals the RPC to terminate and lets + the server's IsCancelled() check release the waiter. + """ + from ..pl import PL + if PL.timestamp != self._timestamp: + raise RuntimeError("Interrupt invalidated by Overlay change") + call = self._stub.wait_for_interrupt.future( + interrupt_pb2.WaitRequest(interrupt_id=self._interrupt_id)) + try: + resp = await asyncio.wrap_future(call) + except asyncio.CancelledError: + call.cancel() + raise + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.CANCELLED: + raise RuntimeError(e.details() or "Interrupt invalidated by Overlay change") + raise + if resp.status == interrupt_pb2.WaitResponse.FIRED: + return + elif resp.status == interrupt_pb2.WaitResponse.TIMEOUT: + raise TimeoutError("Interrupt wait timed out") + elif resp.status == interrupt_pb2.WaitResponse.ERROR: + raise RuntimeError(f"Interrupt error: {resp.msg}") + + def __del__(self): + if self._interrupt_id and self._stub: + try: + self._stub.release_interrupt( + interrupt_pb2.ReleaseRequest( + interrupt_id=self._interrupt_id)) + except Exception: + pass class RemoteUioController: - """Remote UIO Controller placeholder class + """Stub for remote UIO controller. - Placeholder implementation for UIO (Userspace I/O) operations on remote devices. - UIO functionality is not yet implemented for remote PYNQ devices. - - Parameters - ---------- - device : Device, optional - Device object for UIO operations + The server handles UIO internally, so this class is not used + in remote mode. Kept for API compatibility. """ def __init__(self, device=None): + warnings.warn( + "RemoteUioController is a no-op stub. Use pynq.Interrupt " + "for interrupt handling on remote devices.", + stacklevel=2, + ) self.device = device - warnings.warn("UIO operations are not yet implemented for remote devices") def add_event(self, event, number): - raise RuntimeError("UIO operations are not yet implemented for remote devices") + pass def __del__(self): pass diff --git a/pynq/remote/interrupt_pb2.py b/pynq/remote/interrupt_pb2.py new file mode 100644 index 000000000..6671a9490 --- /dev/null +++ b/pynq/remote/interrupt_pb2.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: interrupt.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0finterrupt.proto\x12\tinterrupt\"e\n\x0fRegisterRequest\x12\x10\n\x08pin_name\x18\x01 \x01(\t\x12\x11\n\tpin_index\x18\x02 \x01(\r\x12\x0f\n\x07raw_irq\x18\x03 \x01(\r\x12\x1c\n\x14\x63ontroller_phys_addr\x18\x04 \x01(\x04\"B\n\x10RegisterResponse\x12\x10\n\x03msg\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x0cinterrupt_id\x18\x02 \x01(\tB\x06\n\x04_msg\"7\n\x0bWaitRequest\x12\x14\n\x0cinterrupt_id\x18\x01 \x01(\t\x12\x12\n\ntimeout_ms\x18\x02 \x01(\r\"\x85\x01\n\x0cWaitResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.interrupt.WaitResponse.Status\x12\x10\n\x03msg\x18\x02 \x01(\tH\x00\x88\x01\x01\"+\n\x06Status\x12\t\n\x05\x46IRED\x10\x00\x12\x0b\n\x07TIMEOUT\x10\x01\x12\t\n\x05\x45RROR\x10\x02\x42\x06\n\x04_msg\"&\n\x0eReleaseRequest\x12\x14\n\x0cinterrupt_id\x18\x01 \x01(\t\"+\n\x0fReleaseResponse\x12\x10\n\x03msg\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x06\n\x04_msg2\xf9\x01\n\x0fRemoteInterrupt\x12O\n\x12register_interrupt\x12\x1a.interrupt.RegisterRequest\x1a\x1b.interrupt.RegisterResponse\"\x00\x12G\n\x12wait_for_interrupt\x12\x16.interrupt.WaitRequest\x1a\x17.interrupt.WaitResponse\"\x00\x12L\n\x11release_interrupt\x12\x19.interrupt.ReleaseRequest\x1a\x1a.interrupt.ReleaseResponse\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'interrupt_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_REGISTERREQUEST']._serialized_start=30 + _globals['_REGISTERREQUEST']._serialized_end=131 + _globals['_REGISTERRESPONSE']._serialized_start=133 + _globals['_REGISTERRESPONSE']._serialized_end=199 + _globals['_WAITREQUEST']._serialized_start=201 + _globals['_WAITREQUEST']._serialized_end=256 + _globals['_WAITRESPONSE']._serialized_start=259 + _globals['_WAITRESPONSE']._serialized_end=392 + _globals['_WAITRESPONSE_STATUS']._serialized_start=341 + _globals['_WAITRESPONSE_STATUS']._serialized_end=384 + _globals['_RELEASEREQUEST']._serialized_start=394 + _globals['_RELEASEREQUEST']._serialized_end=432 + _globals['_RELEASERESPONSE']._serialized_start=434 + _globals['_RELEASERESPONSE']._serialized_end=477 + _globals['_REMOTEINTERRUPT']._serialized_start=480 + _globals['_REMOTEINTERRUPT']._serialized_end=729 +# @@protoc_insertion_point(module_scope) diff --git a/pynq/remote/interrupt_pb2.pyi b/pynq/remote/interrupt_pb2.pyi new file mode 100644 index 000000000..d0ed48430 --- /dev/null +++ b/pynq/remote/interrupt_pb2.pyi @@ -0,0 +1,62 @@ +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class RegisterRequest(_message.Message): + __slots__ = ("pin_name", "pin_index", "raw_irq", "controller_phys_addr") + PIN_NAME_FIELD_NUMBER: _ClassVar[int] + PIN_INDEX_FIELD_NUMBER: _ClassVar[int] + RAW_IRQ_FIELD_NUMBER: _ClassVar[int] + CONTROLLER_PHYS_ADDR_FIELD_NUMBER: _ClassVar[int] + pin_name: str + pin_index: int + raw_irq: int + controller_phys_addr: int + def __init__(self, pin_name: _Optional[str] = ..., pin_index: _Optional[int] = ..., raw_irq: _Optional[int] = ..., controller_phys_addr: _Optional[int] = ...) -> None: ... + +class RegisterResponse(_message.Message): + __slots__ = ("msg", "interrupt_id") + MSG_FIELD_NUMBER: _ClassVar[int] + INTERRUPT_ID_FIELD_NUMBER: _ClassVar[int] + msg: str + interrupt_id: str + def __init__(self, msg: _Optional[str] = ..., interrupt_id: _Optional[str] = ...) -> None: ... + +class WaitRequest(_message.Message): + __slots__ = ("interrupt_id", "timeout_ms") + INTERRUPT_ID_FIELD_NUMBER: _ClassVar[int] + TIMEOUT_MS_FIELD_NUMBER: _ClassVar[int] + interrupt_id: str + timeout_ms: int + def __init__(self, interrupt_id: _Optional[str] = ..., timeout_ms: _Optional[int] = ...) -> None: ... + +class WaitResponse(_message.Message): + __slots__ = ("status", "msg") + class Status(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + FIRED: _ClassVar[WaitResponse.Status] + TIMEOUT: _ClassVar[WaitResponse.Status] + ERROR: _ClassVar[WaitResponse.Status] + FIRED: WaitResponse.Status + TIMEOUT: WaitResponse.Status + ERROR: WaitResponse.Status + STATUS_FIELD_NUMBER: _ClassVar[int] + MSG_FIELD_NUMBER: _ClassVar[int] + status: WaitResponse.Status + msg: str + def __init__(self, status: _Optional[_Union[WaitResponse.Status, str]] = ..., msg: _Optional[str] = ...) -> None: ... + +class ReleaseRequest(_message.Message): + __slots__ = ("interrupt_id",) + INTERRUPT_ID_FIELD_NUMBER: _ClassVar[int] + interrupt_id: str + def __init__(self, interrupt_id: _Optional[str] = ...) -> None: ... + +class ReleaseResponse(_message.Message): + __slots__ = ("msg",) + MSG_FIELD_NUMBER: _ClassVar[int] + msg: str + def __init__(self, msg: _Optional[str] = ...) -> None: ... diff --git a/pynq/remote/interrupt_pb2_grpc.py b/pynq/remote/interrupt_pb2_grpc.py new file mode 100644 index 000000000..a40a5d2ed --- /dev/null +++ b/pynq/remote/interrupt_pb2_grpc.py @@ -0,0 +1,188 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from pynq.remote import interrupt_pb2 as interrupt__pb2 + +GRPC_GENERATED_VERSION = '1.64.0' +GRPC_VERSION = grpc.__version__ +EXPECTED_ERROR_RELEASE = '1.65.0' +SCHEDULED_RELEASE_DATE = 'June 25, 2024' +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + warnings.warn( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in interrupt_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' + + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', + RuntimeWarning + ) + + +class RemoteInterruptStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.register_interrupt = channel.unary_unary( + '/interrupt.RemoteInterrupt/register_interrupt', + request_serializer=interrupt__pb2.RegisterRequest.SerializeToString, + response_deserializer=interrupt__pb2.RegisterResponse.FromString, + _registered_method=True) + self.wait_for_interrupt = channel.unary_unary( + '/interrupt.RemoteInterrupt/wait_for_interrupt', + request_serializer=interrupt__pb2.WaitRequest.SerializeToString, + response_deserializer=interrupt__pb2.WaitResponse.FromString, + _registered_method=True) + self.release_interrupt = channel.unary_unary( + '/interrupt.RemoteInterrupt/release_interrupt', + request_serializer=interrupt__pb2.ReleaseRequest.SerializeToString, + response_deserializer=interrupt__pb2.ReleaseResponse.FromString, + _registered_method=True) + + +class RemoteInterruptServicer(object): + """Missing associated documentation comment in .proto file.""" + + def register_interrupt(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def wait_for_interrupt(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def release_interrupt(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_RemoteInterruptServicer_to_server(servicer, server): + rpc_method_handlers = { + 'register_interrupt': grpc.unary_unary_rpc_method_handler( + servicer.register_interrupt, + request_deserializer=interrupt__pb2.RegisterRequest.FromString, + response_serializer=interrupt__pb2.RegisterResponse.SerializeToString, + ), + 'wait_for_interrupt': grpc.unary_unary_rpc_method_handler( + servicer.wait_for_interrupt, + request_deserializer=interrupt__pb2.WaitRequest.FromString, + response_serializer=interrupt__pb2.WaitResponse.SerializeToString, + ), + 'release_interrupt': grpc.unary_unary_rpc_method_handler( + servicer.release_interrupt, + request_deserializer=interrupt__pb2.ReleaseRequest.FromString, + response_serializer=interrupt__pb2.ReleaseResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'interrupt.RemoteInterrupt', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('interrupt.RemoteInterrupt', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class RemoteInterrupt(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def register_interrupt(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/interrupt.RemoteInterrupt/register_interrupt', + interrupt__pb2.RegisterRequest.SerializeToString, + interrupt__pb2.RegisterResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def wait_for_interrupt(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/interrupt.RemoteInterrupt/wait_for_interrupt', + interrupt__pb2.WaitRequest.SerializeToString, + interrupt__pb2.WaitResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def release_interrupt(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/interrupt.RemoteInterrupt/release_interrupt', + interrupt__pb2.ReleaseRequest.SerializeToString, + interrupt__pb2.ReleaseResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/CMakeLists.txt b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/CMakeLists.txt index 259a62b6b..ec02b4f12 100644 --- a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/CMakeLists.txt +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/CMakeLists.txt @@ -24,6 +24,9 @@ get_filename_component(buffer_proto_path "${buffer_proto}" PATH) get_filename_component(gpio_proto "../protos/gpio.proto" ABSOLUTE) get_filename_component(gpio_proto_path "${gpio_proto}" PATH) +get_filename_component(interrupt_proto "../protos/interrupt.proto" ABSOLUTE) +get_filename_component(interrupt_proto_path "${interrupt_proto}" PATH) + # Generated sources set(remote_device_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/remote_device.pb.cc") set(remote_device_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/remote_device.pb.h") @@ -45,11 +48,17 @@ set(gpio_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/gpio.pb.h") set(gpio_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/gpio.grpc.pb.cc") set(gpio_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/gpio.grpc.pb.h") +set(interrupt_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/interrupt.pb.cc") +set(interrupt_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/interrupt.pb.h") +set(interrupt_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/interrupt.grpc.pb.cc") +set(interrupt_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/interrupt.grpc.pb.h") + add_custom_command( OUTPUT "${remote_device_proto_srcs}" "${remote_device_proto_hdrs}" "${remote_device_grpc_srcs}" "${remote_device_grpc_hdrs}" "${mmio_proto_srcs}" "${mmio_proto_hdrs}" "${mmio_grpc_srcs}" "${mmio_grpc_hdrs}" "${buffer_proto_srcs}" "${buffer_proto_hdrs}" "${buffer_grpc_srcs}" "${buffer_grpc_hdrs}" "${gpio_proto_srcs}" "${gpio_proto_hdrs}" "${gpio_grpc_srcs}" "${gpio_grpc_hdrs}" + "${interrupt_proto_srcs}" "${interrupt_proto_hdrs}" "${interrupt_grpc_srcs}" "${interrupt_grpc_hdrs}" COMMAND ${_PROTOBUF_PROTOC} ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}" --cpp_out "${CMAKE_CURRENT_BINARY_DIR}" @@ -59,7 +68,8 @@ add_custom_command( "${mmio_proto}" "${buffer_proto}" "${gpio_proto}" - DEPENDS "${remote_device_proto}" "${mmio_proto}" "${buffer_proto}" "${gpio_proto}") + "${interrupt_proto}" + DEPENDS "${remote_device_proto}" "${mmio_proto}" "${buffer_proto}" "${gpio_proto}" "${interrupt_proto}") # RFSoC-only proto generation if(RFSOC) @@ -193,6 +203,13 @@ add_library(gpio_grpc_proto ${gpio_proto_srcs} ${gpio_proto_hdrs}) +# interrupt_grpc_proto +add_library(interrupt_grpc_proto + ${interrupt_grpc_srcs} + ${interrupt_grpc_hdrs} + ${interrupt_proto_srcs} + ${interrupt_proto_hdrs}) + # RFSoC-only proto libraries, source list, and link/deps lists. # These variables are empty on non-RFSoC boards so the splat below produces # vanilla MMIO/GPIO/Buffer-only build. @@ -229,6 +246,7 @@ add_library(pynq mmio.cc device.cc gpio.cc + interrupt.cc ${RFSOC_SOURCES} ) @@ -238,6 +256,7 @@ add_dependencies(pynq mmio_grpc_proto remote_device_grpc_proto gpio_grpc_proto + interrupt_grpc_proto ${RFSOC_DEPS} ) @@ -253,6 +272,7 @@ target_link_libraries(remote_device_grpc_proto mmio_grpc_proto buffer_grpc_proto gpio_grpc_proto + interrupt_grpc_proto ${RFSOC_LINKS} ${_REFLECTION} ${_GRPC_GRPCPP} @@ -268,6 +288,7 @@ foreach(_target mmio_grpc_proto buffer_grpc_proto gpio_grpc_proto + interrupt_grpc_proto ${RFSOC_LINKS} ${_REFLECTION} ${_GRPC_GRPCPP} diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/interrupt.cc b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/interrupt.cc new file mode 100644 index 000000000..d3e2145d1 --- /dev/null +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/interrupt.cc @@ -0,0 +1,557 @@ +#include "interrupt.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG + +namespace +{ + // AXI Interrupt Controller register offsets (per AMD PG099). + namespace axi_intc + { + constexpr uint32_t IPR = 0x04; // Interrupt Pending Register + constexpr uint32_t IER = 0x08; // Interrupt Enable Register (full) + constexpr uint32_t IAR = 0x0C; // Interrupt Acknowledge Register + constexpr uint32_t SIE = 0x10; // Set Interrupt Enable (write 1 to enable) + constexpr uint32_t CIE = 0x14; // Clear Interrupt Enable (write 1 to disable) + constexpr uint32_t MER = 0x1C; // Master Enable Register + constexpr uint32_t MER_ENABLE = 0x3; // MER value: ME (bit 0) | HIE (bit 1) + constexpr uint32_t MAX_LINES = 32; // AXI INTC supports up to 32 input lines + } + + // Maximum interval between predicate re-evaluations in wait_for_interrupt. + constexpr auto WAIT_HEARTBEAT = std::chrono::milliseconds(1000); + + // Sentinel stored in epoll_event.data.u32 to identify the self-pipe wake + // fd. No real Linux IRQ uses this value. + constexpr uint32_t SELF_PIPE_SENTINEL = 0xFFFFFFFF; +} + +InterruptImpl::InterruptImpl() +{ + epoll_fd_ = epoll_create1(0); + if (epoll_fd_ < 0) + { + std::cerr << "InterruptImpl: failed to create epoll fd: " + << std::strerror(errno) << std::endl; + return; + } + + int pipe_fds[2] = {-1, -1}; + if (pipe(pipe_fds) < 0) + { + std::cerr << "InterruptImpl: failed to create self-pipe: " + << std::strerror(errno) << std::endl; + close(epoll_fd_); + epoll_fd_ = -1; + return; + } + pipe_read_fd_ = pipe_fds[0]; + pipe_write_fd_ = pipe_fds[1]; + + fcntl(pipe_read_fd_, F_SETFL, O_NONBLOCK); + + struct epoll_event ev{}; + ev.events = EPOLLIN; + ev.data.u32 = SELF_PIPE_SENTINEL; + epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, pipe_read_fd_, &ev); + + epoll_thread_ = std::thread(&InterruptImpl::epoll_loop, this); +} + +InterruptImpl::~InterruptImpl() +{ + // Wake any in-flight waiters before tearing down the dispatch thread. + invalidate_all_internal(); + + shutdown_.store(true); + wake_epoll(); + + if (epoll_thread_.joinable()) + { + epoll_thread_.join(); + } + + if (pipe_read_fd_ >= 0) close(pipe_read_fd_); + if (pipe_write_fd_ >= 0) close(pipe_write_fd_); + if (epoll_fd_ >= 0) close(epoll_fd_); +} + +void InterruptImpl::wake_epoll() +{ + if (pipe_write_fd_ < 0) return; + uint8_t val = 1; + ::write(pipe_write_fd_, &val, 1); +} + +std::string InterruptImpl::find_uio_device(uint32_t raw_irq) +{ + // /proc/interrupts row layout (relevant columns only): + // : [ ...] + // We match on the first column (Linux IRQ number, with trailing colon) + // and capture the last column (driver/device name). + std::string dev_name; + { + std::ifstream proc_int("/proc/interrupts"); + if (!proc_int.is_open()) return ""; + + std::string line; + while (std::getline(proc_int, line)) + { + std::istringstream iss(line); + std::vector cols; + for (std::string col; iss >> col; ) cols.push_back(col); + if (cols.empty()) continue; + + std::string &first = cols.front(); + if (first.empty() || first.back() != ':') continue; + first.pop_back(); + + try + { + if (std::stoul(first) == raw_irq) + { + dev_name = cols.back(); + break; + } + } + catch (...) { /* header row or malformed line */ } + } + } + if (dev_name.empty()) return ""; + + try + { + for (const auto &entry : std::filesystem::directory_iterator("/sys/class/uio")) + { + std::ifstream name_file(entry.path() / "name"); + if (!name_file.is_open()) continue; + std::string uio_name; + std::getline(name_file, uio_name); + while (!uio_name.empty() && std::isspace(static_cast(uio_name.back()))) + uio_name.pop_back(); + if (uio_name == dev_name) + return "/dev/" + entry.path().filename().string(); + } + } + catch (const std::filesystem::filesystem_error &e) + { + std::cerr << "find_uio_device: " << e.what() << std::endl; + } + return ""; +} + +void InterruptImpl::epoll_loop() +{ + constexpr int MAX_EVENTS = 16; + struct epoll_event events[MAX_EVENTS]; + + while (!shutdown_.load()) + { + int nfds = epoll_wait(epoll_fd_, events, MAX_EVENTS, 1000); + if (nfds < 0) + { + if (errno == EINTR) continue; + std::cerr << "epoll_wait: " << std::strerror(errno) << std::endl; + break; + } + + for (int i = 0; i < nfds; i++) + { + const uint32_t key = events[i].data.u32; + + // Self-pipe wake: drain and continue + if (key == SELF_PIPE_SENTINEL) + { + uint8_t buf[64]; + while (::read(pipe_read_fd_, buf, sizeof(buf)) > 0) { } + continue; + } + + // UIO event: key holds the raw_irq. Look up the UioDevice and + // read 4 bytes from its fd to acknowledge the kernel-side IRQ. + std::shared_ptr uio; + std::shared_ptr intc; + { + std::lock_guard lock(global_mtx_); + auto uio_it = uio_devices_.find(key); + if (uio_it == uio_devices_.end()) continue; + uio = uio_it->second; + for (auto &[phys_addr, ctrl] : intc_controllers_) + { + if (ctrl->parent_raw_irq == key) + { + intc = ctrl; + break; + } + } + } + + if (uio->fd < 0) continue; + uint32_t val; + ssize_t n = ::read(uio->fd, &val, sizeof(val)); + if (n != sizeof(val)) continue; + + std::lock_guard lock(global_mtx_); + + if (intc) + { + // Cascaded INTC: read IPR, mask all pending lines, ack, then notify + std::lock_guard intc_lock(intc->mtx); + const uint32_t ipr = intc->mmio->read(axi_intc::IPR); + + uint32_t pending = ipr; + while (pending != 0) + { + const uint32_t line = __builtin_ctz(pending); + intc->mmio->write(1u << line, axi_intc::CIE); + pending &= ~(1u << line); + } + intc->mmio->write(ipr, axi_intc::IAR); + + for (auto &[id, reg] : registrations_) + { + if (reg->intc.get() == intc.get() && (ipr & (1u << reg->pin_index))) + { + { + std::lock_guard reg_lock(reg->mtx); + reg->fired.store(true); + } + reg->cv.notify_all(); + } + } + } + else + { + // Direct UIO (no INTC): notify all registrations on this UIO + for (auto &[id, reg] : registrations_) + { + if (reg->uio && reg->uio->raw_irq == key) + { + { + std::lock_guard reg_lock(reg->mtx); + reg->fired.store(true); + } + reg->cv.notify_all(); + } + } + } + } + } +} + +grpc::Status InterruptImpl::register_interrupt( + grpc::ServerContext *context, + const interrupt::RegisterRequest *request, + interrupt::RegisterResponse *response) +{ +#ifdef DEBUG + std::cout << "RegisterInterrupt: pin=" << request->pin_name() + << " raw_irq=" << request->raw_irq() + << " controller_phys_addr=0x" << std::hex << request->controller_phys_addr() << std::dec + << " pin_index=" << request->pin_index() << std::endl; +#endif + + if (request->pin_index() >= axi_intc::MAX_LINES) + { + response->set_msg("pin_index out of range (max " + + std::to_string(axi_intc::MAX_LINES - 1) + ")"); + return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, response->msg()); + } + + std::lock_guard lock(global_mtx_); + + // Step 1: get-or-create UioDevice (cached by raw_irq; shared_ptr ref-counted) + std::shared_ptr uio; + auto uio_it = uio_devices_.find(request->raw_irq()); + if (uio_it != uio_devices_.end()) + { + uio = uio_it->second; + } + else + { + std::string uio_path = find_uio_device(request->raw_irq()); + if (uio_path.empty()) + { + response->set_msg("UIO device not found for raw_irq " + + std::to_string(request->raw_irq())); + return grpc::Status(grpc::StatusCode::NOT_FOUND, response->msg()); + } + + int uio_fd = open(uio_path.c_str(), O_RDWR); + if (uio_fd < 0) + { + response->set_msg("Failed to open " + uio_path + ": " + + std::strerror(errno)); + return grpc::Status(grpc::StatusCode::INTERNAL, response->msg()); + } + + struct epoll_event ev{}; + ev.events = EPOLLIN; + ev.data.u32 = request->raw_irq(); + if (epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, uio_fd, &ev) < 0) + { + close(uio_fd); + response->set_msg("Failed to add UIO fd to epoll: " + + std::string(std::strerror(errno))); + return grpc::Status(grpc::StatusCode::INTERNAL, response->msg()); + } + + uio = std::make_shared(); + uio->fd = uio_fd; + uio->raw_irq = request->raw_irq(); + uio_devices_[request->raw_irq()] = uio; + } + + // Step 2: get-or-create IntcController (singleton by phys_addr) + std::shared_ptr intc; + if (request->controller_phys_addr() > 0) + { + auto intc_it = intc_controllers_.find(request->controller_phys_addr()); + if (intc_it != intc_controllers_.end()) + { + intc = intc_it->second; + } + else + { + intc = std::make_shared(); + intc->mmio = std::make_unique( + static_cast(request->controller_phys_addr()), 32); + intc->phys_addr = request->controller_phys_addr(); + intc->parent_raw_irq = request->raw_irq(); + + // Initialize controller once: disable all lines, then enable + // the master. Per-line enables happen in wait_for_interrupt. + intc->mmio->write(0, axi_intc::IER); + intc->mmio->write(axi_intc::MER_ENABLE, axi_intc::MER); + + intc_controllers_[request->controller_phys_addr()] = intc; + } + } + + // Step 3: create the registration with shared_ptr copies of uio/intc + std::string interrupt_id = "irq_" + std::to_string(id_counter_++); + auto reg = std::make_shared(); + reg->interrupt_id = interrupt_id; + reg->pin_name = request->pin_name(); + reg->pin_index = request->pin_index(); + reg->intc = intc; + reg->uio = uio; + + registrations_[interrupt_id] = std::move(reg); + wake_epoll(); + + response->set_interrupt_id(interrupt_id); + return grpc::Status::OK; +} + +grpc::Status InterruptImpl::wait_for_interrupt( + grpc::ServerContext *context, + const interrupt::WaitRequest *request, + interrupt::WaitResponse *response) +{ +#ifdef DEBUG + std::cout << "WaitForInterrupt: id=" << request->interrupt_id() + << " timeout_ms=" << request->timeout_ms() << std::endl; +#endif + + // Hold a shared_ptr copy for the wait's duration. release_interrupt and + // invalidate_all_internal may erase the map entry while we sleep; the + // local copy keeps the InterruptRegistration alive until we return. + std::shared_ptr reg_ptr; + { + std::lock_guard lock(global_mtx_); + auto it = registrations_.find(request->interrupt_id()); + if (it == registrations_.end()) + { + response->set_status(interrupt::WaitResponse::ERROR); + response->set_msg("Unknown interrupt_id: " + request->interrupt_id()); + return grpc::Status::OK; + } + reg_ptr = it->second; + } + InterruptRegistration *reg = reg_ptr.get(); + + // Take reg->mtx BEFORE the invalidated check AND before arming. This + // serializes with invalidate_all_internal (which also sets invalidated + // under reg->mtx) and prevents lost wakeups: if the IRQ fires after + // arming, the epoll thread must wait for us to enter cv.wait_until(). + std::unique_lock lock(reg->mtx); + + if (reg->invalidated.load()) + { + response->set_status(interrupt::WaitResponse::ERROR); + response->set_msg("Interrupt invalidated by Overlay change"); + return grpc::Status(grpc::StatusCode::CANCELLED, "Interrupt invalidated"); + } + + reg->fired.store(false); + + if (reg->intc) + { + std::lock_guard intc_lock(reg->intc->mtx); + reg->intc->mmio->write(1u << reg->pin_index, axi_intc::SIE); + } + + if (reg->uio && reg->uio->fd >= 0) + { + uint32_t enable = 1; + ::write(reg->uio->fd, &enable, sizeof(enable)); + } + + auto predicate = [&]() + { + return reg->fired.load() || reg->invalidated.load() || + reg->released.load() || context->IsCancelled(); + }; + + // Heartbeat-bounded wait so cancellation (context->IsCancelled()) is + // observed even when no IRQ fires. Without this, a dropped client + // connection would leak this gRPC worker thread. + using clock = std::chrono::steady_clock; + const auto deadline = request->timeout_ms() > 0 + ? clock::now() + std::chrono::milliseconds(request->timeout_ms()) + : clock::time_point::max(); + + bool wait_result = false; + while (true) + { + if (predicate()) { wait_result = true; break; } + const auto now = clock::now(); + if (now >= deadline) { wait_result = false; break; } + const auto wake = (deadline - now < WAIT_HEARTBEAT) ? deadline : now + WAIT_HEARTBEAT; + reg->cv.wait_until(lock, wake); + } + + if (reg->invalidated.load()) + { + response->set_status(interrupt::WaitResponse::ERROR); + response->set_msg("Interrupt invalidated by Overlay change"); + return grpc::Status(grpc::StatusCode::CANCELLED, "Interrupt invalidated"); + } + if (reg->released.load()) + { + response->set_status(interrupt::WaitResponse::ERROR); + response->set_msg("Interrupt released"); + return grpc::Status(grpc::StatusCode::CANCELLED, "Interrupt released"); + } + if (context->IsCancelled()) + { + response->set_status(interrupt::WaitResponse::ERROR); + response->set_msg("Client cancelled"); + return grpc::Status::CANCELLED; + } + if (!wait_result) + { + response->set_status(interrupt::WaitResponse::TIMEOUT); + return grpc::Status::OK; + } + + reg->fired.store(false); + response->set_status(interrupt::WaitResponse::FIRED); + return grpc::Status::OK; +} + +grpc::Status InterruptImpl::release_interrupt( + grpc::ServerContext *context, + const interrupt::ReleaseRequest *request, + interrupt::ReleaseResponse *response) +{ +#ifdef DEBUG + std::cout << "ReleaseInterrupt: id=" << request->interrupt_id() << std::endl; +#endif + + std::lock_guard lock(global_mtx_); + auto it = registrations_.find(request->interrupt_id()); + if (it == registrations_.end()) + { + response->set_msg("Unknown interrupt_id"); + return grpc::Status::OK; + } + + // Local shared_ptr; keeps the Registration alive through this function. + auto reg = it->second; + + // Wake any in-flight waiter. The shared_ptr they hold (via + // wait_for_interrupt) keeps the Registration alive until they return. + { + std::lock_guard reg_lock(reg->mtx); + reg->released.store(true); + } + reg->cv.notify_all(); + + registrations_.erase(it); + + // Drop our local ref so the use_count check below sees the true + // remaining reference count (map + any waiters of OTHER registrations + // sharing this UIO/INTC). + reg.reset(); + + // Reclaim any cached UIO/INTC that no registration references anymore. + // use_count == 1 means only the cache map holds it; erasing drops the + // last ref and runs the destructor (which closes the UIO fd). + for (auto uio_it = uio_devices_.begin(); uio_it != uio_devices_.end(); ) + { + if (uio_it->second.use_count() == 1) + uio_it = uio_devices_.erase(uio_it); + else + ++uio_it; + } + for (auto intc_it = intc_controllers_.begin(); intc_it != intc_controllers_.end(); ) + { + if (intc_it->second.use_count() == 1) + intc_it = intc_controllers_.erase(intc_it); + else + ++intc_it; + } + + wake_epoll(); + return grpc::Status::OK; +} + +void InterruptImpl::invalidate_all_internal() +{ +#ifdef DEBUG + std::cout << "InterruptImpl: invalidate_all_internal()" << std::endl; +#endif + + std::lock_guard lock(global_mtx_); + + for (auto &[id, reg] : registrations_) + { + { + std::lock_guard reg_lock(reg->mtx); + reg->invalidated.store(true); + } + reg->cv.notify_all(); + } + + // Eagerly close UIO fds and mark them -1. Waiter-held UioDevices + // survive past the map clear via shared_ptr, but their fds are now + // inert so no further IRQ activity is possible. The UioDevice + // destructor's check (fd >= 0) prevents a double-close. + for (auto &[irq, uio] : uio_devices_) + { + if (uio->fd >= 0) + { + close(uio->fd); + uio->fd = -1; + } + } + + uio_devices_.clear(); + intc_controllers_.clear(); + registrations_.clear(); + + wake_epoll(); +} diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/interrupt.h b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/interrupt.h new file mode 100644 index 000000000..12a3f0a89 --- /dev/null +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/interrupt.h @@ -0,0 +1,189 @@ +#ifndef INTERRUPT_H +#define INTERRUPT_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "mmio.h" + +class InterruptImpl final : public interrupt::RemoteInterrupt::Service +{ + /** + * @class InterruptImpl + * @brief gRPC service for remote FPGA interrupt handling. + * + * Hosts a single background epoll thread that waits on all open UIO file + * descriptors and dispatches IRQ events to per-registration condition + * variables awaited by gRPC clients. AXI INTC controllers are managed as + * singletons per physical address and demultiplexed in user space. + * + * UioDevice, IntcController, and InterruptRegistration are all shared_ptr + * managed: each Registration holds shared_ptr copies of the UIO and INTC + * it references, so concurrent release or bitstream invalidation cannot + * destroy them out from under a blocked waiter. In-flight waits use a + * 1-second heartbeat to bound cancellation latency. + */ +public: + /** + * @brief Constructor. Creates the epoll instance, self-pipe wake fd, + * and starts the background dispatch thread. + */ + InterruptImpl(); + + /** + * @brief Destructor. Wakes any in-flight waiters with an invalidate, + * joins the epoll thread, and closes all owned file descriptors. + */ + ~InterruptImpl(); + + InterruptImpl(const InterruptImpl &) = delete; + InterruptImpl &operator=(const InterruptImpl &) = delete; + InterruptImpl(InterruptImpl &&) = delete; + InterruptImpl &operator=(InterruptImpl &&) = delete; + + /** + * @brief Register a new interrupt for a client. + * @param context gRPC server context. + * @param request Pin name + Linux raw IRQ + INTC physical address + pin index. + * @param response interrupt_id on success, msg on failure. + * @return OK on success; NOT_FOUND, INVALID_ARGUMENT, or INTERNAL on failure. + */ + grpc::Status register_interrupt( + grpc::ServerContext *context, + const interrupt::RegisterRequest *request, + interrupt::RegisterResponse *response) override; + + /** + * @brief Block until the registered interrupt fires. + * Holds shared_ptr copies of the registration, its UIO, and its INTC + * for the wait's duration so concurrent release/invalidate cannot + * destroy them. + * @param context gRPC server context (polled for client cancellation). + * @param request interrupt_id + optional timeout in milliseconds. + * @param response Status (FIRED, TIMEOUT, ERROR) and optional message. + * @return OK if FIRED or TIMEOUT, CANCELLED on invalidation/release/client cancel. + */ + grpc::Status wait_for_interrupt( + grpc::ServerContext *context, + const interrupt::WaitRequest *request, + interrupt::WaitResponse *response) override; + + /** + * @brief Release a registration. Wakes any in-flight waiter, then + * erases the map entry. UIO and INTC are reclaimed automatically when + * their shared_ptr ref counts drop to one (only the cache map holds). + * @param context gRPC server context. + * @param request interrupt_id to release. + * @param response Optional message. + * @return Always OK (unknown ids are ignored). + */ + grpc::Status release_interrupt( + grpc::ServerContext *context, + const interrupt::ReleaseRequest *request, + interrupt::ReleaseResponse *response) override; + + /** + * @brief Invalidate all registrations after a bitstream reload. + * Sets invalidated on every registration, notifies waiters, closes + * UIO fds eagerly, and clears the maps. Waiter-held UioDevices and + * IntcControllers survive until the waiters return, but their fds are + * already closed so further IRQ activity is impossible. + */ + void invalidate_all_internal(); + +private: + /** + * @struct UioDevice + * @brief RAII handle to one /dev/uioN. The destructor closes the fd. + * Lifetime managed by shared_ptr; the uio_devices_ map and every + * referring InterruptRegistration each hold a copy. + */ + struct UioDevice + { + int fd = -1; + uint32_t raw_irq = 0; + ~UioDevice() { if (fd >= 0) close(fd); } + }; + + /** + * @struct IntcController + * @brief Singleton AXI INTC controller wrapper, shared_ptr managed. + * One instance per controller physical address; cached in the + * intc_controllers_ map and referenced by every InterruptRegistration + * that uses it. parent_raw_irq is the Linux IRQ of the UIO this INTC + * is cascaded behind; used by epoll_loop for dispatch. + */ + struct IntcController + { + std::unique_ptr mmio; + uint64_t phys_addr = 0; + std::mutex mtx; + uint32_t parent_raw_irq = 0; + }; + + /** + * @struct InterruptRegistration + * @brief One client's interest in a specific interrupt pin. + * Owned via shared_ptr in registrations_; each in-flight waiter holds + * its own shared_ptr copy. Holds shared_ptrs to its UIO and INTC so + * concurrent release/invalidate cannot destroy them while a wait is + * in flight. intc is nullptr for direct-UIO (PS GIC) pins. + */ + struct InterruptRegistration + { + std::string interrupt_id; + std::string pin_name; + uint32_t pin_index = 0; + std::shared_ptr intc; + std::shared_ptr uio; + std::mutex mtx; + std::condition_variable cv; + std::atomic fired{false}; + std::atomic invalidated{false}; + std::atomic released{false}; + }; + + /** + * @brief Map a raw Linux IRQ to a UIO device path. + * @param raw_irq The Linux virtual IRQ number to look up. + * @return Path to the UIO device, or empty string if not found. + */ + std::string find_uio_device(uint32_t raw_irq); + + /** + * @brief Background thread entry. Waits on the epoll set and + * dispatches IRQ events to registered waiters via condition variables. + */ + void epoll_loop(); + + /** + * @brief Wake the epoll loop by writing one byte to the self-pipe. + */ + void wake_epoll(); + + // Maps keyed by, respectively: controller phys_addr, raw IRQ, + // and interrupt_id. All three and id_counter_ are protected by + // global_mtx_. + std::unordered_map> intc_controllers_; + std::unordered_map> uio_devices_; + std::unordered_map> registrations_; + std::mutex global_mtx_; + + int epoll_fd_ = -1; + int pipe_read_fd_ = -1; + int pipe_write_fd_ = -1; + std::thread epoll_thread_; + std::atomic shutdown_{false}; + uint64_t id_counter_ = 0; +}; + +#endif // INTERRUPT_H diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/pynq-remote.cc b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/pynq-remote.cc index 1d2e8e1c8..a5ac4ea0c 100644 --- a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/pynq-remote.cc +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/pynq-remote.cc @@ -26,6 +26,7 @@ #include #include #include +#include #ifdef RFSOC #include #include @@ -35,6 +36,7 @@ #include "mmio.h" #include "device.h" #include "gpio.h" +#include "interrupt.h" #ifdef RFSOC #include "xrfclk.h" #include "xrfdc.h" @@ -106,6 +108,12 @@ using gpio::GetGpioBasePathResponse; using gpio::GetGpioNPinsRequest; using gpio::GetGpioNPinsResponse; using gpio::Gpio; +using interrupt::RegisterRequest; +using interrupt::RegisterResponse; +using interrupt::WaitRequest; +using interrupt::WaitResponse; +using interrupt::ReleaseRequest; +using interrupt::ReleaseResponse; #ifdef RFSOC using xrfclk::FindDevicesRequest; using xrfclk::FindDevicesResponse; @@ -904,6 +912,7 @@ class RemoteDeviceImpl final : public RemoteDevice::Service public: std::string device_name = ""; + InterruptImpl *interrupt_service_ = nullptr; /** * @brief Constructor for RemoteDeviceImpl. * Checks if the /lib/firmware/ directory exists and creates it if it does not. @@ -965,6 +974,10 @@ class RemoteDeviceImpl final : public RemoteDevice::Service file.close(); remote_device_.download(remote_device_.get_bitstream_attrs().first); + if (interrupt_service_) + { + interrupt_service_->invalidate_all_internal(); + } #ifdef RFSOC // The PL has been reprogrammed, so any cached RFDC/clock state on the // server now points at the old overlay. Tell the dependent services to @@ -1082,12 +1095,14 @@ void RunServer(uint16_t port) MMIOImpl mmio_service; // Create MMIO rpc handler BufferImpl buffer_service; // Create Buffer rpc handler GPIOImpl gpio_service; // Create Gpio rpc handler + InterruptImpl interrupt_service; // Create Interrupt rpc handler #ifdef RFSOC XrfclkImpl xrfclk_service; // Create Xrfclk rpc handler XrfdcImpl xrfdc_service; // Create Xrfdc rpc handler remote_device_service.set_rfsoc_services(&xrfdc_service, &xrfclk_service); #endif remote_device_service.device_name = buffer_service.device_name; + remote_device_service.interrupt_service_ = &interrupt_service; grpc::EnableDefaultHealthCheckService(true); grpc::reflection::InitProtoReflectionServerBuilderPlugin(); @@ -1097,6 +1112,7 @@ void RunServer(uint16_t port) builder.RegisterService(&mmio_service); builder.RegisterService(&buffer_service); builder.RegisterService(&gpio_service); + builder.RegisterService(&interrupt_service); #ifdef RFSOC builder.RegisterService(&xrfclk_service); builder.RegisterService(&xrfdc_service); diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/protos/interrupt.proto b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/protos/interrupt.proto new file mode 100644 index 000000000..500d0166e --- /dev/null +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/protos/interrupt.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package interrupt; + +service RemoteInterrupt { + rpc register_interrupt(RegisterRequest) returns (RegisterResponse) {} + rpc wait_for_interrupt(WaitRequest) returns (WaitResponse) {} + rpc release_interrupt(ReleaseRequest) returns (ReleaseResponse) {} +} + +message RegisterRequest { + string pin_name = 1; + uint32 pin_index = 2; + uint32 raw_irq = 3; + uint64 controller_phys_addr = 4; +} + +message RegisterResponse { + optional string msg = 1; + string interrupt_id = 2; +} + +message WaitRequest { + string interrupt_id = 1; + uint32 timeout_ms = 2; +} + +message WaitResponse { + enum Status { FIRED = 0; TIMEOUT = 1; ERROR = 2; } + Status status = 1; + optional string msg = 2; +} + +message ReleaseRequest { + string interrupt_id = 1; +} + +message ReleaseResponse { + optional string msg = 1; +} diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/pynq-cpp.bb b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/pynq-cpp.bb index 23bfcb781..481b0b40b 100644 --- a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/pynq-cpp.bb +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/pynq-cpp.bb @@ -21,9 +21,12 @@ SRC_URI = "file://cpp/CMakeLists.txt \ file://cpp/buffer.h \ file://cpp/gpio.cc \ file://cpp/gpio.h \ + file://cpp/interrupt.cc \ + file://cpp/interrupt.h \ file://protos/buffer.proto \ file://protos/gpio.proto \ file://protos/mmio.proto \ + file://protos/interrupt.proto \ file://protos/remote_device.proto \ file://cmake/common.cmake \ file://pynq-remote.service"