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
4 changes: 4 additions & 0 deletions python_bladerf/pylibbladerf/pybladerf.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ 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 cbladerf.bladerf *get_ptr(self)

Expand Down
63 changes: 63 additions & 0 deletions python_bladerf/pylibbladerf/pybladerf.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,10 @@ 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()

def __dealloc__(self):
global global_callbacks
Expand Down Expand Up @@ -1656,9 +1660,29 @@ 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)
cfg = self.__sync_config.get(direction)
if cfg is not None:
self.pybladerf_sync_config(*cfg)

def pybladerf_get_timestamp(self, direction: pybladerf_direction) -> int:
cdef uint64_t timestamp
result = cbladerf.bladerf_get_timestamp(self.__bladerf_device, direction, &timestamp)
Expand All @@ -1669,13 +1693,23 @@ cdef class PyBladerfDevice:
result = cbladerf.bladerf_sync_config(self.__bladerf_device, layout, data_format, <unsigned int> num_buffers, <unsigned int> buffer_size, <unsigned int> num_transfers, <unsigned int> 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

if isinstance(metadata, pybladerf_metadata):
metadata_link = metadata
c_metadata_ptr = <cbladerf.bladerf_metadata*> metadata_link.get_ptr()
else:
self.__check_metadata_required(1, 'pybladerf_sync_tx')

cdef unsigned int c_num_samples = <unsigned int> num_samples
cdef unsigned int c_timeout_ms = <unsigned int> timeout_ms
Expand All @@ -1693,6 +1727,8 @@ cdef class PyBladerfDevice:
if isinstance(metadata, pybladerf_metadata):
metadata_link = metadata
c_metadata_ptr = <cbladerf.bladerf_metadata*> metadata_link.get_ptr()
else:
self.__check_metadata_required(0, 'pybladerf_sync_rx')

cdef unsigned int c_num_samples = <unsigned int> num_samples
cdef unsigned int c_timeout_ms = <unsigned int> timeout_ms
Expand All @@ -1703,6 +1739,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 = <pybladerf_async_data*> malloc(sizeof(pybladerf_async_data))
Expand Down