diff --git a/python_bladerf/pylibbladerf/cbladerf.pxd b/python_bladerf/pylibbladerf/cbladerf.pxd index bf8cc0b..f37cb9d 100644 --- a/python_bladerf/pylibbladerf/cbladerf.pxd +++ b/python_bladerf/pylibbladerf/cbladerf.pxd @@ -345,6 +345,7 @@ cdef extern from 'libbladeRF.h' nogil: int bladerf_enable_module(bladerf *dev, int ch, c_bool enable) int bladerf_get_timestamp(bladerf *dev, bladerf_direction dir, uint64_t *timestamp) + int bladerf_get_loss_event_count(bladerf *dev, bladerf_direction dir, uint64_t *count) int bladerf_sync_config(bladerf *dev, bladerf_channel_layout layout, bladerf_format format, unsigned int num_buffers, unsigned int buffer_size, unsigned int num_transfers, unsigned int stream_timeout) @@ -552,6 +553,7 @@ cdef extern from 'bladeRF2.h' nogil: int bladerf_get_rfic_register(bladerf *dev, uint16_t address, uint8_t *val) int bladerf_set_rfic_register(bladerf *dev, uint16_t address, uint8_t val) + int bladerf_get_rffe_control(bladerf *dev, uint32_t *value) int bladerf_get_rfic_temperature(bladerf *dev, float *val) diff --git a/python_bladerf/pylibbladerf/pybladerf.pxd b/python_bladerf/pylibbladerf/pybladerf.pxd index 2dd00b0..fcad4e3 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pxd +++ b/python_bladerf/pylibbladerf/pybladerf.pxd @@ -104,6 +104,11 @@ cdef class pybladerf_stream: cdef class PyBladerfDevice: cdef cbladerf.bladerf *__bladerf_device cdef public str serialno + # Last sync_config() arguments per direction, so the stream can be + # restored after libbladeRF tears it down on enable_module(False). + cdef dict __sync_config + cdef set __sync_torn_down + cdef bint __auto_reconfig cdef cbladerf.bladerf *get_ptr(self) diff --git a/python_bladerf/pylibbladerf/pybladerf.pyx b/python_bladerf/pylibbladerf/pybladerf.pyx index e815966..98b0a3d 100644 --- a/python_bladerf/pylibbladerf/pybladerf.pyx +++ b/python_bladerf/pylibbladerf/pybladerf.pyx @@ -1147,6 +1147,12 @@ cdef void *__rx_callback_SC16_Q11(cbladerf.bladerf *dev, cbladerf.bladerf_stream num_samples * async_data.bytes_per_sample, ) + # Keep streaming when no callback is installed. Without this default + # `result` is never assigned, the read below raises UnboundLocalError + # inside the callback, and the stream stops on its very first buffer + # with state STREAM_DONE and no error reported to the caller. + result = 0 + if global_callbacks[ dev]['__rx_callback'] is not None: result = global_callbacks[ dev]['__rx_callback'](global_callbacks[ dev]['device'], pystream, np_buffer, num_samples) @@ -1176,6 +1182,12 @@ cdef void *__rx_callback_SC8_Q7(cbladerf.bladerf *dev, cbladerf.bladerf_stream * num_samples * async_data.bytes_per_sample, ) + # Keep streaming when no callback is installed. Without this default + # `result` is never assigned, the read below raises UnboundLocalError + # inside the callback, and the stream stops on its very first buffer + # with state STREAM_DONE and no error reported to the caller. + result = 0 + if global_callbacks[ dev]['__rx_callback'] is not None: result = global_callbacks[ dev]['__rx_callback'](global_callbacks[ dev]['device'], pystream, np_buffer, num_samples) @@ -1318,6 +1330,11 @@ cdef class PyBladerfDevice: def __cinit__(self): self.__bladerf_device = NULL + # Last sync_config() per direction (0 = RX, 1 = TX) and which + # directions libbladeRF has torn down via enable_module(False). + self.__sync_config = {} + self.__sync_torn_down = set() + self.__auto_reconfig = True def __dealloc__(self): global global_callbacks @@ -1443,6 +1460,29 @@ cdef class PyBladerfDevice: raise_error('pybladerf_get_gain()', result) return gain + def pybladerf_get_rfic_register(self, address: int) -> int: + """Read-only читання RFIC-регістра (`bladerf_get_rfic_register`). + + RFIC-HEALTH-001B: BBPLL lock (0x05E), RX/TX RF PLL lock + (0x247/0x287), RX/TX RF PLL cal done (0x244/0x284). Це САМЕ + read-only виклик — не змінює стан RFIC (на відміну від + `pybladerf_set_rfic_register`, який тут навмисно НЕ обгорнутий: + write-шлях лишається окремим рішенням, не випадковим побічним + ефектом наявності read-обгортки). + """ + cdef uint8_t val + result = cbladerf.bladerf_get_rfic_register( + self.__bladerf_device, address, &val) + raise_error('pybladerf_get_rfic_register()', result) + return val + + def pybladerf_get_rfic_temperature(self) -> float: + cdef float val + result = cbladerf.bladerf_get_rfic_temperature( + self.__bladerf_device, &val) + raise_error('pybladerf_get_rfic_temperature()', result) + return val + def pybladerf_set_gain_mode(self, channel: int, mode: pybladerf_gain_mode) -> None: result = cbladerf.bladerf_set_gain_mode(self.__bladerf_device, channel, mode) raise_error('pybladerf_set_gain_mode()', result) @@ -1656,19 +1696,82 @@ cdef class PyBladerfDevice: raise_error('pybladerf_deinterleave_stream_buffer()', result) def pybladerf_enable_module(self, channel: int, enable: bool) -> None: + # libbladeRF tears the synchronous stream down when a direction is + # disabled (rfic_host.c calls sync_deinit() on !dir_enable, and + # bladerf1.c does the same). This is documented behaviour, but + # re-enabling the module does NOT bring the stream back: every + # later bladerf_sync_tx()/sync_rx() then fails with + # "sync tx invalid: not initialized" + # which gives no hint that sync_config() has to be repeated. + # + # Remember the last configuration per direction and restore it on + # re-enable, so a disable/enable cycle keeps working. result = cbladerf.bladerf_enable_module(self.__bladerf_device, channel, enable) raise_error('pybladerf_enable_module()', result) + direction = 1 if (channel & 1) else 0 # TX channels are odd + if not enable: + self.__sync_torn_down.add(direction) + return + if direction in self.__sync_torn_down: + self.__sync_torn_down.discard(direction) + if not self.__auto_reconfig: + return + cfg = self.__sync_config.get(direction) + if cfg is not None: + self.pybladerf_sync_config(*cfg) + + @property + def auto_sync_reconfig(self) -> bool: + """Whether re-enabling a direction repeats its last sync_config. + + On by default: without it every sync_tx/sync_rx after a + disable/enable cycle fails with "not initialized". Turn it off to + measure the library's own behaviour, or to drive sync_config by + hand. + """ + return self.__auto_reconfig + + @auto_sync_reconfig.setter + def auto_sync_reconfig(self, value: bool) -> None: + self.__auto_reconfig = bool(value) + def pybladerf_get_timestamp(self, direction: pybladerf_direction) -> int: cdef uint64_t timestamp result = cbladerf.bladerf_get_timestamp(self.__bladerf_device, direction, ×tamp) raise_error('pybladerf_get_timestamp()', result) return timestamp + def pybladerf_get_loss_event_count(self, direction: pybladerf_direction) -> int: + '''Loss EPISODES the FPGA counted, per direction. + + Episodes, not samples: an unbroken run of overflow increments this + once however long it lasts. Do not scale it into a sample count. + + Not the same as the OVERRUN metadata flag: that one is computed on + the host from USB queue state and never reads the fabric, so a loss + the FPGA absorbed on its own leaves it clear. + + Cleared by enable_module(True) -- a difference across an enable + boundary is meaningless. Monotonic within one enabled session. + ''' + cdef uint64_t count + result = cbladerf.bladerf_get_loss_event_count(self.__bladerf_device, direction, &count) + raise_error('pybladerf_get_loss_event_count()', result) + return count + def pybladerf_sync_config(self, layout: pybladerf_channel_layout, data_format: pybladerf_format, num_buffers: int, buffer_size: int, num_transfers: int, stream_timeout: int) -> None: result = cbladerf.bladerf_sync_config(self.__bladerf_device, layout, data_format, num_buffers, buffer_size, num_transfers, stream_timeout) raise_error('pybladerf_sync_config()', result) + # Keep the settings so pybladerf_enable_module() can restore the + # stream after libbladeRF tears it down on disable. + direction = 1 if (int(layout) & 1) else 0 # TX layouts are odd + self.__sync_config[direction] = (layout, data_format, num_buffers, + buffer_size, num_transfers, + stream_timeout) + self.__sync_torn_down.discard(direction) + def pybladerf_sync_tx(self, samples: np.ndarray[Any, Any], num_samples: int, metadata: pybladerf_metadata | None = None, timeout_ms: int = 0) -> None: cdef cbladerf.bladerf_metadata *c_metadata_ptr = NULL cdef pybladerf_metadata metadata_link @@ -1676,6 +1779,8 @@ cdef class PyBladerfDevice: if isinstance(metadata, pybladerf_metadata): metadata_link = metadata c_metadata_ptr = metadata_link.get_ptr() + else: + self.__check_metadata_required(1, 'pybladerf_sync_tx') cdef unsigned int c_num_samples = num_samples cdef unsigned int c_timeout_ms = timeout_ms @@ -1693,6 +1798,8 @@ cdef class PyBladerfDevice: if isinstance(metadata, pybladerf_metadata): metadata_link = metadata c_metadata_ptr = metadata_link.get_ptr() + else: + self.__check_metadata_required(0, 'pybladerf_sync_rx') cdef unsigned int c_num_samples = num_samples cdef unsigned int c_timeout_ms = timeout_ms @@ -1703,6 +1810,33 @@ cdef class PyBladerfDevice: result = cbladerf.bladerf_sync_rx(self.__bladerf_device, c_samples_ptr, c_num_samples, c_metadata_ptr, c_timeout_ms) raise_error('pybladerf_sync_rx()', result) + def __check_metadata_required(self, direction: int, caller: str) -> None: + """Refuse a metadata-format transfer that was given no metadata. + + A stream configured with a *_META format carries per-buffer + timestamps and flags. Passing metadata=None leaves libbladeRF with + nowhere to report them, so the caller silently loses the timestamp + it needs and bladerf_get_timestamp() keeps returning 0. Nothing in + the error path points at the real cause, so this reads as dead + hardware rather than a mismatched call. + + Measured on a TX1 -> 50 dB pad -> RX1 loopback: with SC16_Q11 the + frame timestamp stayed at 762229041 across 8 consecutive reads and + get_timestamp() returned 0, so consecutive gain steps analysed the + same buffer and the gain ladder came out non-monotonic. + """ + cfg = self.__sync_config.get(direction) + if cfg is None: + return + fmt = int(cfg[1]) + if fmt in (int(pybladerf_format.PYBLADERF_FORMAT_SC16_Q11_META), + int(pybladerf_format.PYBLADERF_FORMAT_SC8_Q7_META)): + raise RuntimeError( + f'{caller}(): stream is configured with a metadata format ' + f'({pybladerf_format(fmt)}) but metadata=None was passed. ' + 'Timestamps and flags would be lost silently; pass a ' + 'pybladerf_metadata instance.') + def pybladerf_init_rx_stream(self, num_buffers: int, data_format: pybladerf_format, samples_per_buffer: int, num_transfers: int) -> pybladerf_stream: cdef pybladerf_stream pystream = pybladerf_stream() cdef pybladerf_async_data* async_data = malloc(sizeof(pybladerf_async_data)) @@ -1810,6 +1944,13 @@ cdef class PyBladerfDevice: result = cbladerf.bladerf_get_fw_log(self.__bladerf_device, c_filename) raise_error('pybladerf_get_fw_log()', result) + def pybladerf_read_flash_bytes(self, address: int, count: int) -> bytes: + cdef bytearray buf = bytearray(count) + cdef unsigned char[::1] view = buf + result = cbladerf.bladerf_read_flash_bytes(self.__bladerf_device, &view[0], address, count) + raise_error('pybladerf_read_flash_bytes()', result) + return bytes(buf) + def pybladerf_set_vctcxo_tamer_mode(self, mode: pybladerf_vctcxo_tamer_mode) -> None: result = cbladerf.bladerf_set_vctcxo_tamer_mode(self.__bladerf_device, mode) raise_error('pybladerf_set_vctcxo_tamer_mode()', result) @@ -1905,6 +2046,53 @@ cdef class PyBladerfDevice: result = cbladerf.bladerf_set_rfic_register(self.__bladerf_device, address, value) raise_error('pybladerf_set_rfic_register()', result) + def pybladerf_config_gpio_read(self) -> int: + """Read the FPGA configuration GPIO register. + + Carries the per-format mode bits (TIMESTAMP, PACKET, 8BIT_MODE, + HIGHLY_PACKED) that both directions share. perform_format_config() + writes the whole word from one direction's format, so this is where + a stream configuration on one direction can clobber the mode the + other direction needs. + """ + cdef uint32_t value + result = cbladerf.bladerf_config_gpio_read(self.__bladerf_device, &value) + raise_error('pybladerf_config_gpio_read()', result) + return value + + def pybladerf_get_rffe_control(self) -> int: + """Read the RFFE control register (FPGA, not RFIC). + + Carries the RF front-end state that no RFIC register reflects: + SPDT switch positions, per-channel enables, and the direction + ENABLE/TXNRX bits. Needed to tell a switch left in its shutdown + position from an RFIC problem -- every RFIC register can read + back correct while the signal is routed nowhere. + """ + cdef uint32_t value + result = cbladerf.bladerf_get_rffe_control(self.__bladerf_device, &value) + raise_error('pybladerf_get_rffe_control()', result) + return value + + def pybladerf_get_rfic_register(self, address: int) -> int: + """Read one RFIC (AD9361) register over SPI. + + Needed because no FPGA-side register reflects the RFIC's internal + state: the ENSM state, the digital datapath status and the filter + enables live only here. Without this an RFIC stuck with a gated + TX digital clock is indistinguishable from a healthy one, since + the FPGA enable pins read correct in both cases. + """ + cdef uint8_t value + result = cbladerf.bladerf_get_rfic_register(self.__bladerf_device, address, &value) + raise_error('pybladerf_get_rfic_register()', result) + return value + + def pybladerf_set_rfic_register(self, address: int, value: int) -> None: + """Write one RFIC (AD9361) register over SPI.""" + result = cbladerf.bladerf_set_rfic_register(self.__bladerf_device, address, value) + raise_error('pybladerf_set_rfic_register()', result) + def pybladerf_get_rfic_temperature(self) -> float: cdef float value result = cbladerf.bladerf_get_rfic_temperature(self.__bladerf_device, &value) diff --git a/setup.py b/setup.py index 812dbed..7f2545f 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,16 @@ SETUP_REQUIRES = ['Cython>=3.1.0,<3.2.1', 'numpy'] libbladerf_h_paths = [] +# The .pxd files carry the layout of PyBladerfDevice and the libbladeRF +# declarations. Without them in depends, editing a .pxd leaves the modules that +# cimport it compiled against the old struct layout, which shows up at import +# time as "PyBladerfDevice size changed, may indicate binary incompatibility" +# and then as a segfault. +PXD_DEPENDS = [ + 'python_bladerf/pylibbladerf/pybladerf.pxd', + 'python_bladerf/pylibbladerf/cbladerf.pxd', +] + PLATFORM = sys.platform if getenv('LIBLINK'): @@ -104,6 +114,7 @@ def run(self) -> None: # type: ignore sources=['python_bladerf/pylibbladerf/pybladerf.pyx'], include_dirs=['python_bladerf/pylibbladerf', *libbladerf_h_paths, numpy.get_include()], extra_compile_args=['-w'], + depends=PXD_DEPENDS, language='c++', ), Extension( # type: ignore @@ -111,6 +122,7 @@ def run(self) -> None: # type: ignore sources=['python_bladerf/pybladerf_tools/pybladerf_sweep.pyx'], include_dirs=['python_bladerf/pylibbladerf', 'python_bladerf/pybladerf_tools', *libbladerf_h_paths, numpy.get_include()], extra_compile_args=['-w'], + depends=PXD_DEPENDS, language='c++', ), Extension( # type: ignore @@ -118,6 +130,7 @@ def run(self) -> None: # type: ignore sources=['python_bladerf/pybladerf_tools/pybladerf_scan.pyx'], include_dirs=['python_bladerf/pylibbladerf', 'python_bladerf/pybladerf_tools', *libbladerf_h_paths, numpy.get_include()], extra_compile_args=['-w'], + depends=PXD_DEPENDS, language='c++', ), Extension( # type: ignore @@ -125,6 +138,7 @@ def run(self) -> None: # type: ignore sources=['python_bladerf/pybladerf_tools/pybladerf_transfer.pyx'], include_dirs=['python_bladerf/pylibbladerf', 'python_bladerf/pybladerf_tools', *libbladerf_h_paths, numpy.get_include()], extra_compile_args=['-w'], + depends=PXD_DEPENDS, language='c++', ), ],