Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/pynq_remote.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions docs/source/pynq_remote/interrupts.rst
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion docs/source/pynq_remote/status.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
135 changes: 111 additions & 24 deletions pynq/pl_server/remote_device.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import asyncio
from pathlib import Path
import pickle
import datetime
Expand All @@ -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
Expand Down Expand Up @@ -187,13 +189,15 @@ 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()
self.name = self.get_board_name()

self.capabilities = {
"REMOTE": True,
"INTERRUPT": True,
}

def get_board_name(self):
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions pynq/remote/interrupt_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions pynq/remote/interrupt_pb2.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
Loading