From 9c7243d26a5de3d70440295c109a251a37ee7c88 Mon Sep 17 00:00:00 2001 From: josh goldsmith Date: Wed, 29 Oct 2025 21:22:19 +0000 Subject: [PATCH 1/2] adding initial remote gpio support --- pynq/pl_server/remote_device.py | 125 +++++- pynq/remote/gpio_pb2.py | 54 +++ pynq/remote/gpio_pb2.pyi | 93 +++++ pynq/remote/gpio_pb2_grpc.py | 360 ++++++++++++++++++ .../pynq-cpp/files/cpp/CMakeLists.txt | 22 +- .../recipes-apps/pynq-cpp/files/cpp/gpio.cc | 201 ++++++++++ .../recipes-apps/pynq-cpp/files/cpp/gpio.h | 85 +++++ .../pynq-cpp/files/cpp/pynq-remote.cc | 253 +++++++++++- .../pynq-cpp/files/protos/gpio.proto | 71 ++++ 9 files changed, 1235 insertions(+), 29 deletions(-) create mode 100644 pynq/remote/gpio_pb2.py create mode 100644 pynq/remote/gpio_pb2.pyi create mode 100644 pynq/remote/gpio_pb2_grpc.py create mode 100644 sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.cc create mode 100644 sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.h create mode 100644 sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/protos/gpio.proto diff --git a/pynq/pl_server/remote_device.py b/pynq/pl_server/remote_device.py index ff7cf2c63..d71d4fef7 100644 --- a/pynq/pl_server/remote_device.py +++ b/pynq/pl_server/remote_device.py @@ -21,8 +21,10 @@ bitstream_hash ) from pynq.remote import ( - remote_device_pb2_grpc, mmio_pb2_grpc, buffer_pb2_grpc, - remote_device_pb2, mmio_pb2, buffer_pb2, + remote_device_pb2_grpc, remote_device_pb2, + mmio_pb2_grpc, mmio_pb2, + buffer_pb2_grpc, buffer_pb2, + gpio_pb2_grpc, gpio_pb2, ) import grpc @@ -184,6 +186,7 @@ def __init__(self, index=0, ip_addr=None, port=PYNQ_PORT, tag="remote{}"): 'device': remote_device_pb2_grpc.RemoteDeviceStub(self.client.channel), 'mmio': mmio_pb2_grpc.MmioStub(self.client.channel), 'buffer': buffer_pb2_grpc.RemoteBufferStub(self.client.channel), + 'gpio': gpio_pb2_grpc.GpioStub(self.client.channel), } self.arch = self.get_arch() @@ -521,7 +524,7 @@ class RemoteGPIO: GPIO functionality is not yet implemented for remote PYNQ devices. """ - def __init__(self, gpio_index=None, direction=None): + def __init__(self, gpio_index, direction, device=None): """Initialize RemoteGPIO object Parameters @@ -530,33 +533,119 @@ def __init__(self, gpio_index=None, direction=None): GPIO pin index number direction : str, optional GPIO direction ('in' or 'out') + device : Device, optional + RemoteDevice object for GPIO operations """ + if device is None: + raise RuntimeError("RemoteGPIO requires a RemoteDevice instance") + self.device = device self.gpio_index = gpio_index - self.direction = direction - warnings.warn("GPIO operations are not yet implemented for remote devices") + self._direction = direction + self._stub = device._stub['gpio'] + + response = self._stub.get_gpio( + gpio_pb2.GetGpioRequest(index=gpio_index, direction=direction) + ) + self._gpio_id = response.gpio_id def read(self): - raise RuntimeError("GPIO operations are not yet implemented for remote devices") + + if self.direction != 'in': + raise AttributeError("Cannot read from GPIO output.") + + response = self._stub.read( + gpio_pb2.GpioReadRequest(gpio_id=self._gpio_id) + ) + return response.value def write(self, value): - raise RuntimeError("GPIO operations are not yet implemented for remote devices") + if self.direction != 'out': + raise AttributeError("Cannot write to GPIO input.") + + if value not in (0, 1): + raise ValueError("Can only write integer 0 or 1.") + + response = self._stub.write( + gpio_pb2.GpioWriteRequest(gpio_id=self._gpio_id, value=value) + ) + return + + def unexport(self): + response = self._stub.unexport( + gpio_pb2.GpioUnexportRequest(gpio_id=self._gpio_id) + ) def release(self): - """Release GPIO resources + self.unexport() - No-op for remote GPIO placeholder - """ - pass + def is_exported(self): + response = self._stub.is_exported( + gpio_pb2.GpioIsExportedRequest(gpio_id=self._gpio_id) + ) + return bool(response.is_exported) + + @property + def index(self): + return self.gpio_index + + @property + def direction(self): + return self._direction + + @property + def path(self): + warnings.warn("This property is not implemented for remote devices.") + + @staticmethod + def get_gpio_base_path(target_label=None, device=None): + if device is None: + raise RuntimeError("get_gpio_base_path requires a RemoteDevice instance.") + stub = device._stub['gpio'] + + response = stub.get_gpio_base_path( + gpio_pb2.GetGpioBasePathRequest(target_label=target_label) + ) + if response.base_path == "": + return None + return response.base_path - # Add class methods to match the GPIO API @staticmethod - def get_gpio_pin(gpio_user_index, target_label=None): - """Get GPIO pin by user index + def get_gpio_base(target_label=None, device=None): + if device is None: + raise RuntimeError("get_gpio_base requires a RemoteDevice instance.") + + base_path = RemoteGPIO.get_gpio_base_path(target_label=target_label, device=device) + if base_path is not None: + return int(''.join(x for x in base_path if x.isdigit())) + + @staticmethod + def get_gpio_pin(gpio_user_index, target_label=None, device=None): + if device is None: + raise RuntimeError("get_gpio_pin requires a RemoteDevice instance.") + + if target_label is not None: + GPIO_OFFSET = 0 + else: + if device.arch == "aarch64": + GPIO_OFFSET = 78 + else: + GPIO_OFFSET = 54 + return (RemoteGPIO.get_gpio_base(target_label=target_label, device=device) + + GPIO_OFFSET + + gpio_user_index) + + @staticmethod + def get_gpio_npins(target_label=None, device=None): + if device is None: + raise RuntimeError("get_gpio_npins requires a RemoteDevice instance.") + + stub = device._stub['gpio'] + response = stub.get_gpio_npins( + gpio_pb2.GetGpioNPinsRequest( + target_label=target_label) + ) + return response.npins - Placeholder method to prevent attribute errors in remote device context. - """ - warnings.warn("GPIO operations are not yet implemented for remote devices") - return gpio_user_index # Just return the index to prevent errors class RemoteInterrupt: """Remote Interrupt placeholder class diff --git a/pynq/remote/gpio_pb2.py b/pynq/remote/gpio_pb2.py new file mode 100644 index 000000000..6a710dd67 --- /dev/null +++ b/pynq/remote/gpio_pb2.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: gpio.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\ngpio.proto\x12\x04gpio\"2\n\x0eGetGpioRequest\x12\r\n\x05index\x18\x01 \x01(\x05\x12\x11\n\tdirection\x18\x02 \x01(\t\"U\n\x0fGetGpioResponse\x12\x14\n\x07message\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07gpio_id\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\n\n\x08_messageB\n\n\x08_gpio_id\"\"\n\x0fGpioReadRequest\x12\x0f\n\x07gpio_id\x18\x01 \x01(\t\"R\n\x10GpioReadResponse\x12\x14\n\x07message\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x12\n\x05value\x18\x03 \x01(\x05H\x01\x88\x01\x01\x42\n\n\x08_messageB\x08\n\x06_value\"2\n\x10GpioWriteRequest\x12\x0f\n\x07gpio_id\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05\"\x13\n\x11GpioWriteResponse\"&\n\x13GpioUnexportRequest\x12\x0f\n\x07gpio_id\x18\x01 \x01(\t\"\x16\n\x14GpioUnexportResponse\"(\n\x15GpioIsExportedRequest\x12\x0f\n\x07gpio_id\x18\x01 \x01(\t\"B\n\x16GpioIsExportedResponse\x12\x18\n\x0bis_exported\x18\x03 \x01(\x08H\x00\x88\x01\x01\x42\x0e\n\x0c_is_exported\"D\n\x16GetGpioBasePathRequest\x12\x19\n\x0ctarget_label\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x0f\n\r_target_label\"?\n\x17GetGpioBasePathResponse\x12\x16\n\tbase_path\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0c\n\n_base_path\"A\n\x13GetGpioNPinsRequest\x12\x19\n\x0ctarget_label\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x0f\n\r_target_label\"4\n\x14GetGpioNPinsResponse\x12\x12\n\x05npins\x18\x01 \x01(\x05H\x00\x88\x01\x01\x42\x08\n\x06_npins2\xd9\x03\n\x04Gpio\x12\x37\n\x08get_gpio\x12\x14.gpio.GetGpioRequest\x1a\x15.gpio.GetGpioResponse\x12\x35\n\x04read\x12\x15.gpio.GpioReadRequest\x1a\x16.gpio.GpioReadResponse\x12\x38\n\x05write\x12\x16.gpio.GpioWriteRequest\x1a\x17.gpio.GpioWriteResponse\x12\x41\n\x08unexport\x12\x19.gpio.GpioUnexportRequest\x1a\x1a.gpio.GpioUnexportResponse\x12H\n\x0bis_exported\x12\x1b.gpio.GpioIsExportedRequest\x1a\x1c.gpio.GpioIsExportedResponse\x12Q\n\x12get_gpio_base_path\x12\x1c.gpio.GetGpioBasePathRequest\x1a\x1d.gpio.GetGpioBasePathResponse\x12G\n\x0eget_gpio_npins\x12\x19.gpio.GetGpioNPinsRequest\x1a\x1a.gpio.GetGpioNPinsResponseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gpio_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_GETGPIOREQUEST']._serialized_start=20 + _globals['_GETGPIOREQUEST']._serialized_end=70 + _globals['_GETGPIORESPONSE']._serialized_start=72 + _globals['_GETGPIORESPONSE']._serialized_end=157 + _globals['_GPIOREADREQUEST']._serialized_start=159 + _globals['_GPIOREADREQUEST']._serialized_end=193 + _globals['_GPIOREADRESPONSE']._serialized_start=195 + _globals['_GPIOREADRESPONSE']._serialized_end=277 + _globals['_GPIOWRITEREQUEST']._serialized_start=279 + _globals['_GPIOWRITEREQUEST']._serialized_end=329 + _globals['_GPIOWRITERESPONSE']._serialized_start=331 + _globals['_GPIOWRITERESPONSE']._serialized_end=350 + _globals['_GPIOUNEXPORTREQUEST']._serialized_start=352 + _globals['_GPIOUNEXPORTREQUEST']._serialized_end=390 + _globals['_GPIOUNEXPORTRESPONSE']._serialized_start=392 + _globals['_GPIOUNEXPORTRESPONSE']._serialized_end=414 + _globals['_GPIOISEXPORTEDREQUEST']._serialized_start=416 + _globals['_GPIOISEXPORTEDREQUEST']._serialized_end=456 + _globals['_GPIOISEXPORTEDRESPONSE']._serialized_start=458 + _globals['_GPIOISEXPORTEDRESPONSE']._serialized_end=524 + _globals['_GETGPIOBASEPATHREQUEST']._serialized_start=526 + _globals['_GETGPIOBASEPATHREQUEST']._serialized_end=594 + _globals['_GETGPIOBASEPATHRESPONSE']._serialized_start=596 + _globals['_GETGPIOBASEPATHRESPONSE']._serialized_end=659 + _globals['_GETGPIONPINSREQUEST']._serialized_start=661 + _globals['_GETGPIONPINSREQUEST']._serialized_end=726 + _globals['_GETGPIONPINSRESPONSE']._serialized_start=728 + _globals['_GETGPIONPINSRESPONSE']._serialized_end=780 + _globals['_GPIO']._serialized_start=783 + _globals['_GPIO']._serialized_end=1256 +# @@protoc_insertion_point(module_scope) diff --git a/pynq/remote/gpio_pb2.pyi b/pynq/remote/gpio_pb2.pyi new file mode 100644 index 000000000..6dc7937a2 --- /dev/null +++ b/pynq/remote/gpio_pb2.pyi @@ -0,0 +1,93 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class GetGpioRequest(_message.Message): + __slots__ = ("index", "direction") + INDEX_FIELD_NUMBER: _ClassVar[int] + DIRECTION_FIELD_NUMBER: _ClassVar[int] + index: int + direction: str + def __init__(self, index: _Optional[int] = ..., direction: _Optional[str] = ...) -> None: ... + +class GetGpioResponse(_message.Message): + __slots__ = ("message", "gpio_id") + MESSAGE_FIELD_NUMBER: _ClassVar[int] + GPIO_ID_FIELD_NUMBER: _ClassVar[int] + message: str + gpio_id: str + def __init__(self, message: _Optional[str] = ..., gpio_id: _Optional[str] = ...) -> None: ... + +class GpioReadRequest(_message.Message): + __slots__ = ("gpio_id",) + GPIO_ID_FIELD_NUMBER: _ClassVar[int] + gpio_id: str + def __init__(self, gpio_id: _Optional[str] = ...) -> None: ... + +class GpioReadResponse(_message.Message): + __slots__ = ("message", "value") + MESSAGE_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + message: str + value: int + def __init__(self, message: _Optional[str] = ..., value: _Optional[int] = ...) -> None: ... + +class GpioWriteRequest(_message.Message): + __slots__ = ("gpio_id", "value") + GPIO_ID_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + gpio_id: str + value: int + def __init__(self, gpio_id: _Optional[str] = ..., value: _Optional[int] = ...) -> None: ... + +class GpioWriteResponse(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class GpioUnexportRequest(_message.Message): + __slots__ = ("gpio_id",) + GPIO_ID_FIELD_NUMBER: _ClassVar[int] + gpio_id: str + def __init__(self, gpio_id: _Optional[str] = ...) -> None: ... + +class GpioUnexportResponse(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class GpioIsExportedRequest(_message.Message): + __slots__ = ("gpio_id",) + GPIO_ID_FIELD_NUMBER: _ClassVar[int] + gpio_id: str + def __init__(self, gpio_id: _Optional[str] = ...) -> None: ... + +class GpioIsExportedResponse(_message.Message): + __slots__ = ("is_exported",) + IS_EXPORTED_FIELD_NUMBER: _ClassVar[int] + is_exported: bool + def __init__(self, is_exported: bool = ...) -> None: ... + +class GetGpioBasePathRequest(_message.Message): + __slots__ = ("target_label",) + TARGET_LABEL_FIELD_NUMBER: _ClassVar[int] + target_label: str + def __init__(self, target_label: _Optional[str] = ...) -> None: ... + +class GetGpioBasePathResponse(_message.Message): + __slots__ = ("base_path",) + BASE_PATH_FIELD_NUMBER: _ClassVar[int] + base_path: str + def __init__(self, base_path: _Optional[str] = ...) -> None: ... + +class GetGpioNPinsRequest(_message.Message): + __slots__ = ("target_label",) + TARGET_LABEL_FIELD_NUMBER: _ClassVar[int] + target_label: str + def __init__(self, target_label: _Optional[str] = ...) -> None: ... + +class GetGpioNPinsResponse(_message.Message): + __slots__ = ("npins",) + NPINS_FIELD_NUMBER: _ClassVar[int] + npins: int + def __init__(self, npins: _Optional[int] = ...) -> None: ... diff --git a/pynq/remote/gpio_pb2_grpc.py b/pynq/remote/gpio_pb2_grpc.py new file mode 100644 index 000000000..2bf2ae21a --- /dev/null +++ b/pynq/remote/gpio_pb2_grpc.py @@ -0,0 +1,360 @@ +# 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 gpio_pb2 as gpio__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 gpio_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 GpioStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.get_gpio = channel.unary_unary( + '/gpio.Gpio/get_gpio', + request_serializer=gpio__pb2.GetGpioRequest.SerializeToString, + response_deserializer=gpio__pb2.GetGpioResponse.FromString, + _registered_method=True) + self.read = channel.unary_unary( + '/gpio.Gpio/read', + request_serializer=gpio__pb2.GpioReadRequest.SerializeToString, + response_deserializer=gpio__pb2.GpioReadResponse.FromString, + _registered_method=True) + self.write = channel.unary_unary( + '/gpio.Gpio/write', + request_serializer=gpio__pb2.GpioWriteRequest.SerializeToString, + response_deserializer=gpio__pb2.GpioWriteResponse.FromString, + _registered_method=True) + self.unexport = channel.unary_unary( + '/gpio.Gpio/unexport', + request_serializer=gpio__pb2.GpioUnexportRequest.SerializeToString, + response_deserializer=gpio__pb2.GpioUnexportResponse.FromString, + _registered_method=True) + self.is_exported = channel.unary_unary( + '/gpio.Gpio/is_exported', + request_serializer=gpio__pb2.GpioIsExportedRequest.SerializeToString, + response_deserializer=gpio__pb2.GpioIsExportedResponse.FromString, + _registered_method=True) + self.get_gpio_base_path = channel.unary_unary( + '/gpio.Gpio/get_gpio_base_path', + request_serializer=gpio__pb2.GetGpioBasePathRequest.SerializeToString, + response_deserializer=gpio__pb2.GetGpioBasePathResponse.FromString, + _registered_method=True) + self.get_gpio_npins = channel.unary_unary( + '/gpio.Gpio/get_gpio_npins', + request_serializer=gpio__pb2.GetGpioNPinsRequest.SerializeToString, + response_deserializer=gpio__pb2.GetGpioNPinsResponse.FromString, + _registered_method=True) + + +class GpioServicer(object): + """Missing associated documentation comment in .proto file.""" + + def get_gpio(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 read(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 write(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 unexport(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 is_exported(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 get_gpio_base_path(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 get_gpio_npins(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_GpioServicer_to_server(servicer, server): + rpc_method_handlers = { + 'get_gpio': grpc.unary_unary_rpc_method_handler( + servicer.get_gpio, + request_deserializer=gpio__pb2.GetGpioRequest.FromString, + response_serializer=gpio__pb2.GetGpioResponse.SerializeToString, + ), + 'read': grpc.unary_unary_rpc_method_handler( + servicer.read, + request_deserializer=gpio__pb2.GpioReadRequest.FromString, + response_serializer=gpio__pb2.GpioReadResponse.SerializeToString, + ), + 'write': grpc.unary_unary_rpc_method_handler( + servicer.write, + request_deserializer=gpio__pb2.GpioWriteRequest.FromString, + response_serializer=gpio__pb2.GpioWriteResponse.SerializeToString, + ), + 'unexport': grpc.unary_unary_rpc_method_handler( + servicer.unexport, + request_deserializer=gpio__pb2.GpioUnexportRequest.FromString, + response_serializer=gpio__pb2.GpioUnexportResponse.SerializeToString, + ), + 'is_exported': grpc.unary_unary_rpc_method_handler( + servicer.is_exported, + request_deserializer=gpio__pb2.GpioIsExportedRequest.FromString, + response_serializer=gpio__pb2.GpioIsExportedResponse.SerializeToString, + ), + 'get_gpio_base_path': grpc.unary_unary_rpc_method_handler( + servicer.get_gpio_base_path, + request_deserializer=gpio__pb2.GetGpioBasePathRequest.FromString, + response_serializer=gpio__pb2.GetGpioBasePathResponse.SerializeToString, + ), + 'get_gpio_npins': grpc.unary_unary_rpc_method_handler( + servicer.get_gpio_npins, + request_deserializer=gpio__pb2.GetGpioNPinsRequest.FromString, + response_serializer=gpio__pb2.GetGpioNPinsResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'gpio.Gpio', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('gpio.Gpio', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Gpio(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def get_gpio(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, + '/gpio.Gpio/get_gpio', + gpio__pb2.GetGpioRequest.SerializeToString, + gpio__pb2.GetGpioResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def read(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, + '/gpio.Gpio/read', + gpio__pb2.GpioReadRequest.SerializeToString, + gpio__pb2.GpioReadResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def write(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, + '/gpio.Gpio/write', + gpio__pb2.GpioWriteRequest.SerializeToString, + gpio__pb2.GpioWriteResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def unexport(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, + '/gpio.Gpio/unexport', + gpio__pb2.GpioUnexportRequest.SerializeToString, + gpio__pb2.GpioUnexportResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def is_exported(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, + '/gpio.Gpio/is_exported', + gpio__pb2.GpioIsExportedRequest.SerializeToString, + gpio__pb2.GpioIsExportedResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def get_gpio_base_path(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, + '/gpio.Gpio/get_gpio_base_path', + gpio__pb2.GetGpioBasePathRequest.SerializeToString, + gpio__pb2.GetGpioBasePathResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def get_gpio_npins(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, + '/gpio.Gpio/get_gpio_npins', + gpio__pb2.GetGpioNPinsRequest.SerializeToString, + gpio__pb2.GetGpioNPinsResponse.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 f30493212..9094522b2 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 @@ -15,6 +15,9 @@ get_filename_component(mmio_proto_path "${mmio_proto}" PATH) get_filename_component(buffer_proto "../protos/buffer.proto" ABSOLUTE) 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) + # 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") @@ -31,10 +34,16 @@ set(buffer_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/buffer.pb.h") set(buffer_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/buffer.grpc.pb.cc") set(buffer_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/buffer.grpc.pb.h") +set(gpio_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/gpio.pb.cc") +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") + 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}" COMMAND ${_PROTOBUF_PROTOC} ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}" --cpp_out "${CMAKE_CURRENT_BINARY_DIR}" @@ -43,7 +52,8 @@ add_custom_command( "${remote_device_proto}" "${mmio_proto}" "${buffer_proto}" - DEPENDS "${remote_device_proto}" "${mmio_proto}" "${buffer_proto}") + "${gpio_proto}" + DEPENDS "${remote_device_proto}" "${mmio_proto}" "${buffer_proto}" "${gpio_proto}") message(STATUS "CMAKE CURRENT DIR = ${CMAKE_CURRENT_SOURCE_DIR}") # Change to whatever the Petalinux project path - will need to change this at some point @@ -113,15 +123,24 @@ add_library(remote_device_grpc_proto ${remote_device_proto_srcs} ${remote_device_proto_hdrs}) +# gpio_grpc_proto +add_library(gpio_grpc_proto + ${gpio_grpc_srcs} + ${gpio_grpc_hdrs} + ${gpio_proto_srcs} + ${gpio_proto_hdrs}) + add_library(pynq buffer.cc mmio.cc device.cc + gpio.cc ) target_link_libraries(remote_device_grpc_proto mmio_grpc_proto buffer_grpc_proto + gpio_grpc_proto ${_REFLECTION} ${_GRPC_GRPCPP} ${_PROTOBUF_LIBPROTOBUF}) @@ -135,6 +154,7 @@ foreach(_target remote_device_grpc_proto mmio_grpc_proto buffer_grpc_proto + gpio_grpc_proto ${_REFLECTION} ${_GRPC_GRPCPP} ${_PROTOBUF_LIBPROTOBUF} diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.cc b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.cc new file mode 100644 index 000000000..5e038ca66 --- /dev/null +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.cc @@ -0,0 +1,201 @@ +#include "gpio.h" + +#define DEBUG + +GPIO::GPIO(uint32_t gpio_index, std::string direction) +{ + if (direction != "in" && direction != "out") { + std::cerr << "Direction must be 'in' or 'out'" << std::endl; + return; + } + + // Set member variables + this->gpio_index = gpio_index; + this->direction = direction; + this->gpio_base_path = "/sys/class/gpio/gpio" + std::to_string(gpio_index) + "/"; + + // Export GPIO if not already exported + if (!std::filesystem::exists(this->gpio_base_path)) { + std::ofstream export_file("/sys/class/gpio/export"); + if (!export_file.is_open()) { + std::cerr << "Failed to open export file" << std::endl; + return; + } + export_file << gpio_index; + export_file.close(); + } + + // Set the direction + std::ofstream direction_file(this->gpio_base_path + "direction"); + if (!direction_file.is_open()) { + std::cerr << "Failed to open direction file" << std::endl; + return; + } + direction_file << this->direction; + direction_file.close(); +} + +GPIO::~GPIO() +{ +} + +uint32_t GPIO::read() +{ + // Check if direction is configured for input + if (this->direction != "in") { + std::cerr << "Cannot read from GPIO configured as output" << std::endl; + return 0; + } + + // Open the value file for reading + std::ifstream value_file(this->gpio_base_path + "value"); + if (!value_file.is_open()) { + std::cerr << "Failed to open value file for reading" << std::endl; + return 0; + } + + // Read the value and convert to integer + std::string value_str; + value_file >> value_str; + value_file.close(); + + uint32_t value = std::stoi(value_str); + + return value; +} + +void GPIO::write(uint32_t value) +{ + // Check if direction is configured for output + if (this->direction != "out") { + std::cerr << "Cannot write to GPIO configured as input" << std::endl; + return; + } + + // Check if value is valid + if (value != 0 && value != 1) { + std::cerr << "Value must be 0 or 1" << std::endl; + return; + } + + // Open the value file for writing + std::ofstream value_file(this->gpio_base_path + "value"); + if (!value_file.is_open()) { + std::cerr << "Failed to open value file for writing" << std::endl; + return; + } + + // Write the value as string + value_file << value; + value_file.close(); +} + +void GPIO::unexport() +{ + // Check if GPIO path exists before attempting to unexport + if (std::filesystem::exists(this->gpio_base_path)) { + std::ofstream unexport_file("/sys/class/gpio/unexport"); + if (!unexport_file.is_open()) { + std::cerr << "Failed to open unexport file" << std::endl; + return; + } + + unexport_file << this->gpio_index; + unexport_file.close(); + } +} + +bool GPIO::is_exported() +{ + // Check if the GPIO base path exists + return std::filesystem::exists(this->gpio_base_path); +} + +std::string get_gpio_base_path(const std::string& target_label) +{ + std::vector valid_labels; + + if (!target_label.empty()) { + valid_labels.push_back(target_label); + } else { + valid_labels.push_back("zynqmp_gpio"); + valid_labels.push_back("zynq_gpio"); + } + + // Walk through /sys/class/gpio directory + std::string gpio_base_dir = "/sys/class/gpio"; + + try { + for (const auto& entry : std::filesystem::directory_iterator(gpio_base_dir)) { + if (entry.is_directory()) { + std::string dir_name = entry.path().filename().string(); + + // Check if directory name contains 'gpiochip' + if (dir_name.find("gpiochip") != std::string::npos) { + std::string label_file_path = entry.path() / "label"; + + // Read the label file + std::ifstream label_file(label_file_path); + if (label_file.is_open()) { + std::string label; + std::getline(label_file, label); + label_file.close(); + + // Remove trailing whitespace (equivalent to rstrip()) + label.erase(label.find_last_not_of(" \t\r\n") + 1); + + // Check if label is in valid_labels + for (const auto& valid_label : valid_labels) { + if (label == valid_label) { + return entry.path().string(); + } + } + } + } + } + } + } catch (const std::filesystem::filesystem_error& e) { + std::cerr << "Error accessing filesystem: " << e.what() << std::endl; + } + + return ""; // Return empty string if not found +} + +uint32_t get_gpio_npins(const std::string& target_label) +{ + std::string base_path = get_gpio_base_path(target_label); + + if (base_path.empty()) { + #ifdef DEBUG + std::cerr << "GPIO base path not found for label: " << target_label << std::endl; + #endif + return 0; + } + + std::ifstream ngpio_file(base_path + "/ngpio"); + if (!ngpio_file.is_open()) { + return 0; + } + + std::string ngpio_str; + std::getline(ngpio_file, ngpio_str); + ngpio_file.close(); + + // Extract only digits + std::string digits_only; + for (char c : ngpio_str) { + if (std::isdigit(c)) { + digits_only += c; + } + } + + if (digits_only.empty()) { + return 0; + } + + try { + return std::stoi(digits_only); + } catch (...) { + return 0; + } +} \ No newline at end of file diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.h b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.h new file mode 100644 index 000000000..b1e796b85 --- /dev/null +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.h @@ -0,0 +1,85 @@ +#ifndef GPIO_H +#define GPIO_H + +#include +#include +#include +#include +#include +#include +#include +#include + +class GPIO +{ + /** + * @class GPIO + * @brief GPIO class to control GPIO pins + * Each instance of this class represents a single GPIO pin. + */ +private: + int gpio_index; + std::string direction; + std::string gpio_base_path; + +public: + /** + * @brief Constructor for GPIO class + * Initializes the GPIO pin with the given index and direction. + * @param gpio_index Index of the GPIO pin + * @param direction Direction of the GPIO pin ("in" or "out") + */ + GPIO(uint32_t gpio_index, std::string direction); + + /** + * @brief Destructor for GPIO class + * Cleans up resources associated with the GPIO pin. + */ + ~GPIO(); + + /** + * @brief Read the value from the GPIO pin + * @return Value read from the GPIO pin + */ + uint32_t read(); + + /** + * @brief Write a value to the GPIO pin + * @param value Value to write to the GPIO pin + */ + void write(uint32_t value); + + /** + * @brief Unexport the GPIO pin + * Cleans up resources associated with the GPIO pin. + */ + void unexport(); + + /** + * @brief Checks if the GPIO pin is exported + * @return true if the GPIO pin is exported, false otherwise + */ + bool is_exported(); + + // Getters for member variables + int get_index() const { return gpio_index; } + std::string get_direction() const { return direction; } + std::string get_gpio_path() const { return gpio_base_path; } + + +}; + + +/** + * @brief Gets the GPIO base path + * @return Base path of the GPIO pin + */ +std::string get_gpio_base_path(const std::string& target_label = ""); + +/** + * @brief Gets the number of GPIO pins available + * @return Number of GPIO pins available + */ +uint32_t get_gpio_npins(const std::string& target_label = ""); + +#endif // GPIO_H \ No newline at end of file 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 a73659bae..6b1a2d8fb 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 @@ -21,22 +21,21 @@ #include #include + #include #include #include -#include -#include -#include -#include +#include #include "buffer.cc" #include "mmio.h" #include "device.h" -// #include -// #include -// #include -// #include -// #include //include actual xrt folder, not just include folder +#include "gpio.h" + +#include +#include +#include +#include // Namespaces from grpc.pb.h to simplify code using buffer::AddressRequest; @@ -84,6 +83,21 @@ using remote_device::WriteFileRequest; using remote_device::WriteFileResponse; using remote_device::ExistsFileRequest; using remote_device::ExistsFileResponse; +using gpio::GetGpioRequest; +using gpio::GetGpioResponse; +using gpio::GpioReadRequest; +using gpio::GpioReadResponse; +using gpio::GpioWriteRequest; +using gpio::GpioWriteResponse; +using gpio::GpioUnexportRequest; +using gpio::GpioUnexportResponse; +using gpio::GpioIsExportedRequest; +using gpio::GpioIsExportedResponse; +using gpio::GetGpioBasePathRequest; +using gpio::GetGpioBasePathResponse; +using gpio::GetGpioNPinsRequest; +using gpio::GetGpioNPinsResponse; +using gpio::Gpio; #define DEBUG @@ -623,6 +637,223 @@ class MMIOImpl final : public Mmio::Service } }; +class GPIOImpl final : public Gpio::Service +{ + /** + * @class GPIOImpl + * @brief Implements the gRPC service for managing GPIO objects. + */ +private: + std::unordered_map> gpios_; ///< Map to store GPIO objects + uint64_t count = 0; ///< Counter for generating GPIO IDs} + +public: + /** + * @brief Adds a new GPIO object. + * Creates and stores a new GPIO object with the given index, direction and identifier. + * @param index Index of the GPIO. + * @param direction Direction of the GPIO. + * @param gpio_id Identifier for the GPIO object. + */ + void addGPIO(uint32_t index, std::string direction, std::string gpio_id) + { + gpios_[gpio_id] = std::make_unique(index, direction); + } + + /** + * @brief Finds a GPIO object by its ID. + * Searches the internal map for a GPIO object with the given identifier. + * @param gpio_id Identifier for the GPIO object. + * @return Pointer to the GPIO object, or nullptr if not found. + */ + GPIO *findGPIO(const std::string &gpio_id) + { + auto it = gpios_.find(gpio_id); + if (it != gpios_.end()) + { + return it->second.get(); + } + return nullptr; + } + + /** + * @brief Handles the GetGpio gRPC request. + * Creates a new GPIO object based on the request parameters and returns its identifier. + * @param context Server context for the request. + * @param request GetGpioRequest message. + * @param response GetGpioResponse message. + * @return gRPC status. + */ + Status get_gpio(ServerContext *context, const GetGpioRequest *request, GetGpioResponse *response) override + { + #ifdef DEBUG + std::cout << "Function: get_gpio, " + << "index=" << request->index() << ", " + << "direction=" << request->direction() + << std::endl; + #endif + std::string gpio_id = std::to_string(count); + addGPIO(request->index(), request->direction(), gpio_id); + count += 1; + response->set_gpio_id(gpio_id); + return grpc::Status::OK; + } + + /** + * @brief Handles the Read gRPC request. + * Reads data from the specified GPIO object and returns it in the response. + * @param context Server context for the request. + * @param request GpioReadRequest message. + * @param response GpioReadResponse message. + * @return gRPC status. + */ + Status read(ServerContext *context, const GpioReadRequest *request, GpioReadResponse *response) override + { + GPIO *gpio = findGPIO(request->gpio_id()); + if (!gpio) + { + return grpc::Status(grpc::StatusCode::NOT_FOUND, "GPIO Object not found."); + } + uint32_t data = gpio->read(); + #ifdef DEBUG + std::cout << "Function: read, " + << "gpio_id=" << request->gpio_id() << ", " + << "Data=" << data + << std::endl; + #endif + response->set_value(data); + return grpc::Status::OK; + } + + /** + * @brief Handles the Write gRPC request. + * Writes the value (0,1) to the specified GPIO object based on the request parameters. + * @param context Server context for the request. + * @param request GpioWriteRequest message. + * @param response GpioWriteResponse message. + * @return gRPC status. + */ + Status write(ServerContext *context, const GpioWriteRequest *request, GpioWriteResponse *response) override + { + #ifdef DEBUG + std::cout << "Function: write, " + << "gpio_id=" << request->gpio_id() << ", " + << "data=" << request->value() + << std::dec + << std::endl; + #endif + GPIO *gpio = findGPIO(request->gpio_id()); + if (!gpio) + { + return grpc::Status(grpc::StatusCode::NOT_FOUND, "GPIO Object not found."); + } + gpio->write(request->value()); + return grpc::Status::OK; + } + + /** + * @brief Handles the Unexport gRPC request. + * Releases the specified GPIO object based on the request parameters. + * @param context Server context for the request. + * @param request GpioUnexportRequest message. + * @param response GpioUnexportResponse message. + * @return gRPC status. + */ + Status unexport(ServerContext *context, const GpioUnexportRequest *request, GpioUnexportResponse *response) override + { + #ifdef DEBUG + std::cout << "Function: unexport, " + << "gpio_id=" << request->gpio_id() + << std::dec + << std::endl; + #endif + GPIO *gpio = findGPIO(request->gpio_id()); + if (!gpio) + { + return grpc::Status(grpc::StatusCode::NOT_FOUND, "GPIO Object not found."); + } + gpio->unexport(); + gpios_.erase(request->gpio_id()); + return grpc::Status::OK; + } + + /** + * @brief Handles the IsExported gRPC request. + * Checks if the specified GPIO object is exported based on the request parameters. + * @param context Server context for the request. + * @param request GpioIsExportedRequest message. + * @param response GpioIsExportedResponse message. + * @return gRPC status. + */ + Status is_exported(ServerContext *context, const GpioIsExportedRequest *request, GpioIsExportedResponse *response) override + { + #ifdef DEBUG + std::cout << "Function: is_exported, " + << "gpio_id=" << request->gpio_id() + << std::dec + << std::endl; + #endif + GPIO *gpio = findGPIO(request->gpio_id()); + response->set_is_exported(gpio != nullptr && gpio->is_exported()); + return grpc::Status::OK; + } + + /** + * @brief Handles the GetGpioBasePath gRPC request. + * Retrieves the base path of the specified GPIO object based on the request parameters. + * @param context Server context for the request. + * @param request GetGpioBasePathRequest message. + * @param response GetGpioBasePathResponse message. + * @return gRPC status. + */ + Status get_gpio_base_path(ServerContext *context, const GetGpioBasePathRequest *request, GetGpioBasePathResponse *response) override + { + #ifdef DEBUG + std::cout << "Function: get_gpio_base_path, " + << "target_label=" << request->target_label() + << std::dec + << std::endl; + #endif + + std::string base_path; + if (request->has_target_label()) { + base_path = ::get_gpio_base_path(request->target_label()); + } else { + base_path = ::get_gpio_base_path(); + } + response->set_base_path(base_path); + return grpc::Status::OK; + } + + /** + * @brief Handles the GetGpioNPins gRPC request. + * Retrieves the number of pins of the specified GPIO object based on the request parameters. + * @param context Server context for the request. + * @param request GetGpioNPinsRequest message. + * @param response GetGpioNPinsResponse message. + * @return gRPC status. + */ + Status get_gpio_npins(ServerContext *context, const GetGpioNPinsRequest *request, GetGpioNPinsResponse *response) override + { + #ifdef DEBUG + std::cout << "Function: get_gpio_npins, " + << "target_label=" << request->target_label() + << std::dec + << std::endl; + #endif + + uint32_t npins; + if (request->has_target_label()) { + npins = ::get_gpio_npins(request->target_label()); + } else { + npins = ::get_gpio_npins(); + } + response->set_npins(npins); + return grpc::Status::OK; + } +}; + + class RemoteDeviceImpl final : public RemoteDevice::Service { /** @@ -785,7 +1016,8 @@ void RunServer(uint16_t port) std::string server_address = "0.0.0.0:" + std::to_string(port); RemoteDeviceImpl remote_device_service; // Create remote_device rpc handler MMIOImpl mmio_service; // Create MMIO rpc handler - BufferImpl buffer_service; + BufferImpl buffer_service; // Create Buffer rpc handler + GPIOImpl gpio_service; // Create Gpio rpc handler remote_device_service.device_name = buffer_service.device_name; grpc::EnableDefaultHealthCheckService(true); @@ -795,6 +1027,7 @@ void RunServer(uint16_t port) builder.RegisterService(&remote_device_service); // Add to RPC running server. builder.RegisterService(&mmio_service); builder.RegisterService(&buffer_service); + builder.RegisterService(&gpio_service); std::unique_ptr server(builder.BuildAndStart()); std::cout << "Server listening on " << server_address << std::endl; diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/protos/gpio.proto b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/protos/gpio.proto new file mode 100644 index 000000000..97621a260 --- /dev/null +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/protos/gpio.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package gpio; + +service Gpio { + rpc get_gpio (GetGpioRequest) returns (GetGpioResponse); + rpc read (GpioReadRequest) returns (GpioReadResponse); + rpc write (GpioWriteRequest) returns (GpioWriteResponse); + rpc unexport (GpioUnexportRequest) returns (GpioUnexportResponse); + rpc is_exported (GpioIsExportedRequest) returns (GpioIsExportedResponse); + rpc get_gpio_base_path (GetGpioBasePathRequest) returns (GetGpioBasePathResponse); + rpc get_gpio_npins (GetGpioNPinsRequest) returns (GetGpioNPinsResponse); +} + +message GetGpioRequest { + int32 index = 1; + string direction = 2; +} + +message GetGpioResponse { + optional string message = 2; + optional string gpio_id = 3; +} + +message GpioReadRequest { + string gpio_id = 1; +} + +message GpioReadResponse { + optional string message = 2; + optional int32 value = 3; +} + +message GpioWriteRequest { + string gpio_id = 1; + int32 value = 2; +} + +message GpioWriteResponse { +} + +message GpioUnexportRequest { + string gpio_id = 1; +} + +message GpioUnexportResponse { +} + +message GpioIsExportedRequest { + string gpio_id = 1; +} + +message GpioIsExportedResponse { + optional bool is_exported = 3; +} + +message GetGpioBasePathRequest { + optional string target_label = 1; +} + +message GetGpioBasePathResponse { + optional string base_path = 3; +} + +message GetGpioNPinsRequest { + optional string target_label = 1; +} + +message GetGpioNPinsResponse { + optional int32 npins = 1; +} \ No newline at end of file From 9fb8508c0cacaa935395ee32d83e13d537873b4b Mon Sep 17 00:00:00 2001 From: josh goldsmith Date: Wed, 3 Dec 2025 14:38:24 +0000 Subject: [PATCH 2/2] updated code comments --- pynq/pl_server/remote_device.py | 186 ++++++++++++++++-- .../recipes-apps/pynq-cpp/files/cpp/gpio.cc | 5 - .../recipes-apps/pynq-cpp/files/cpp/gpio.h | 1 - 3 files changed, 173 insertions(+), 19 deletions(-) diff --git a/pynq/pl_server/remote_device.py b/pynq/pl_server/remote_device.py index d71d4fef7..468d3c424 100644 --- a/pynq/pl_server/remote_device.py +++ b/pynq/pl_server/remote_device.py @@ -518,23 +518,30 @@ def allocate(self, shape, dtype, cacheable=1, **kwargs): class RemoteGPIO: - """Remote GPIO placeholder class - - Placeholder implementation for GPIO operations on remote devices. - GPIO functionality is not yet implemented for remote PYNQ devices. + """Internal Helper class to wrap Linux's GPIO Sysfs API. + + This GPIO class does not handle PL I/O without the use of + device tree overlays. + + Attributes + ---------- + index : int + The index of the GPIO, starting from the GPIO base. + direction : str + Input/output direction of the GPIO. """ def __init__(self, gpio_index, direction, device=None): - """Initialize RemoteGPIO object - + """Return a new RemoteGPIO object. + Parameters ---------- - gpio_index : int, optional - GPIO pin index number - direction : str, optional - GPIO direction ('in' or 'out') - device : Device, optional - RemoteDevice object for GPIO operations + gpio_index : int + The index of the RemoteGPIO using Linux's GPIO Sysfs API. + direction : 'str' + Input/output direction of the GPIO. + device : RemoteDevice + Device object for RemoteGPIO operations """ if device is None: raise RuntimeError("RemoteGPIO requires a RemoteDevice instance") @@ -549,7 +556,14 @@ def __init__(self, gpio_index, direction, device=None): self._gpio_id = response.gpio_id def read(self): - + """The method to read a value from the GPIO. + + Returns + ------- + int + An integer read from the GPIO + + """ if self.direction != 'in': raise AttributeError("Cannot read from GPIO output.") @@ -559,6 +573,18 @@ def read(self): return response.value def write(self, value): + """The method to write a value into the GPIO. + + Parameters + ---------- + value : int + An integer value, either 0 or 1 + + Returns + ------- + None + + """ if self.direction != 'out': raise AttributeError("Cannot write to GPIO input.") @@ -571,14 +597,37 @@ def write(self, value): return def unexport(self): + """The method to unexport the GPIO using Linux's GPIO Sysfs API. + + Returns + ------- + None + + """ response = self._stub.unexport( gpio_pb2.GpioUnexportRequest(gpio_id=self._gpio_id) ) def release(self): + """The method to release the GPIO. + + Returns + ------- + None + + """ self.unexport() def is_exported(self): + """The method to check if a GPIO is still exported using + Linux's GPIO Sysfs API. + + Returns + ------- + bool + True if the GPIO is still loaded. + + """ response = self._stub.is_exported( gpio_pb2.GpioIsExportedRequest(gpio_id=self._gpio_id) ) @@ -598,6 +647,34 @@ def path(self): @staticmethod def get_gpio_base_path(target_label=None, device=None): + """This method returns the path to the Remote GPIO base using Linux's + GPIO Sysfs API. This path relates to the target device, not the + host machine. + + This is a static method. To use: + + >>> from pynq import GPIO + + >>> from pynq.pl_server import RemoteDevice + + >>> device = RemoteDevice(ip_addr='192.168.2.99') + + >>> gpio = GPIO.get_gpio_base_path(device=device) + + Parameters + ---------- + target_label : str + The label of the GPIO driver to look for, as defined in a + device tree entry. + device : RemoteDevice + Remote device object for RemoteGPIO operations + + Returns + ------- + str + The path to the Remote GPIO base. + + """ if device is None: raise RuntimeError("get_gpio_base_path requires a RemoteDevice instance.") stub = device._stub['gpio'] @@ -611,6 +688,36 @@ def get_gpio_base_path(target_label=None, device=None): @staticmethod def get_gpio_base(target_label=None, device=None): + """This method returns the GPIO base using Linux's GPIO Sysfs API. + + This is a static method. To use: + + >>> from pynq import GPIO + + >>> from pynq.pl_server import RemoteDevice + + >>> device = RemoteDevice(ip_addr='192.168.2.99') + + >>> gpio = GPIO.get_gpio_base(device) + + Note + ---- + For path '/sys/class/gpio/gpiochip138/', this method returns 138. + + Parameters + ---------- + target_label : str + The label of the GPIO driver to look for, as defined in a + device tree entry. + device : RemoteDevice + Remote device object for RemoteGPIO operations + + Returns + ------- + int + The GPIO index of the base. + + """ if device is None: raise RuntimeError("get_gpio_base requires a RemoteDevice instance.") @@ -620,6 +727,32 @@ def get_gpio_base(target_label=None, device=None): @staticmethod def get_gpio_pin(gpio_user_index, target_label=None, device=None): + """This method returns a GPIO instance for PS GPIO pins. + + Users only need to specify an index starting from 0; this static + method will map this index to the correct Linux GPIO pin number. + + Note + ---- + The GPIO pin number can be calculated using: + GPIO pin number = GPIO base + GPIO offset + user index + e.g. The GPIO base is 138, and pin 54 is the base GPIO offset. + Then the Linux GPIO pin would be (138 + 54 + 0) = 192. + + Parameters + ---------- + gpio_user_index : int + The index specified by users, starting from 0. + target_label : str + The label of the GPIO driver to look for, as defined in a + device tree entry. + + Returns + ------- + int + The Linux Sysfs GPIO pin number. + + """ if device is None: raise RuntimeError("get_gpio_pin requires a RemoteDevice instance.") @@ -636,6 +769,33 @@ def get_gpio_pin(gpio_user_index, target_label=None, device=None): @staticmethod def get_gpio_npins(target_label=None, device=None): + """This method returns the number of GPIO pins for the GPIO base + using Linux's GPIO Sysfs API. + + This is a static method. To use: + + >>> from pynq import GPIO + + >>> from pynq.pl_server import RemoteDevice + + >>> device = RemoteDevice(ip_addr='192.168.2.99') + + >>> gpio = GPIO.get_gpio_npins() + + Parameters + ---------- + target_label : str + The label of the GPIO driver to look for, as defined in a + device tree entry. + device : RemoteDevice + Remote device object for RemoteGPIO operations + + Returns + ------- + int + The number of GPIO pins for the GPIO base. + + """ if device is None: raise RuntimeError("get_gpio_npins requires a RemoteDevice instance.") diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.cc b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.cc index 5e038ca66..756935ed6 100644 --- a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.cc +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.cc @@ -9,7 +9,6 @@ GPIO::GPIO(uint32_t gpio_index, std::string direction) return; } - // Set member variables this->gpio_index = gpio_index; this->direction = direction; this->gpio_base_path = "/sys/class/gpio/gpio" + std::to_string(gpio_index) + "/"; @@ -47,14 +46,12 @@ uint32_t GPIO::read() return 0; } - // Open the value file for reading std::ifstream value_file(this->gpio_base_path + "value"); if (!value_file.is_open()) { std::cerr << "Failed to open value file for reading" << std::endl; return 0; } - // Read the value and convert to integer std::string value_str; value_file >> value_str; value_file.close(); @@ -72,13 +69,11 @@ void GPIO::write(uint32_t value) return; } - // Check if value is valid if (value != 0 && value != 1) { std::cerr << "Value must be 0 or 1" << std::endl; return; } - // Open the value file for writing std::ofstream value_file(this->gpio_base_path + "value"); if (!value_file.is_open()) { std::cerr << "Failed to open value file for writing" << std::endl; diff --git a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.h b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.h index b1e796b85..82a568eee 100644 --- a/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.h +++ b/sdbuild/boot/meta-pynq/recipes-apps/pynq-cpp/files/cpp/gpio.h @@ -33,7 +33,6 @@ class GPIO /** * @brief Destructor for GPIO class - * Cleans up resources associated with the GPIO pin. */ ~GPIO();