Hi,

I am using an Ettus B210 in two-channel receive and require accurate time stamps. The B210 is connected to an external 10 MHz clock and PPS source.

Setting the time on the B210 works fine, and reading back the time reports the expected time.

When streaming a single channel, everything works as expected, and if I read back the time after ending the stream, the time reported by the USRP is correct. The time in the hdr metadata files is correct as well.

/2022-04-29 13:39:02.117 | INFO     | __main__:start:317 - Before stream start: system time 1651203542.1179564 usrp time 1651203542.0 (2022-04-29 13:39:02.000000) 2022-04-29 13:39:02.118 | WARNING  | __main__:start:329 - Starting recording immediately
2022-04-29 13:39:02.118 | INFO     | __main__:start:331 - Calling start()
2022-04-29 13:39:02.119 | INFO     | __main__:start:335 - Time after issuing start stream command: 1651203542.1197705 usrp time 1651203542.0 (2022-04-29 13:39:02.000000)

/

When streaming two channels, the setup works fine, but after the top_block.start() command is issued, the time read back is somehow the current time stamp multiplied by 2. The time in the hdr metadata files has the first time stamp being the time stamp around calling top_block.start() multiplied by 2. It however increments properly (from the corrupted origin) when counting the samples.

/2022-04-29 13:39:20.120 | INFO     | __main__:start:317 - Before stream start: system time 1651203560.1199565 usrp time 1651203560.0 (2022-04-29 13:39:20.000000) 2022-04-29 13:39:20.120 | WARNING  | __main__:start:329 - Starting recording immediately
2022-04-29 13:39:20.120 | INFO     | __main__:start:331 - Calling start()
2022-04-29 13:39:22.012 | INFO     | __main__:start:335 - Time after issuing start stream command: 1651203562.012672 usrp time 3302407122.0 (2074-08-25 17:18:42.000000)
/Where /3302407122//= //1651203560///*2 + 2//

I have tested this with both UHD 4.1 and UHD 4.2. GNU radio version: 3.8.0.0 (Python 3.8.10)
////

Is anyone on this mailing list familiar with the issue and have a solution/workaround for this?

I have attached log output and a not so small MWE


Thanks,

--
Edwin Peters (PhD,MEng)
Research Fellow - UNSW Canberra Space
Single channel recording
2022-04-29 13:38:58.358 | INFO     | __main__:__init__:165 - USRPS: [{'usrp': <gr_block usrp_source (1)>, 'n_channels': 1, 'antenna': 'RX2', 'serial': '3136D4B', 'config': {'serial': '3136D4B', 'subdev': 'A:A', 'args': 'recv_frame_size=1032, num_recv_frames=5120', 'antenna': 'RX2'}}]
2022-04-29 13:38:58.359 | DEBUG    | __main__:__init__:166 - settings: {'Fc_hz': 437000000.0, 'Fs_hz': 500000.0, 'gain_dB': 45, 'NID': 666, 'file_name': '', 'file_path': '/media/rf_data/recordings', 'antenna': 'RX2', 'pps_present': True, 'test_dev_null': False, 'usrps': [{'serial': '3136D4B', 'subdev': 'A:A', 'args': 'recv_frame_size=1032, num_recv_frames=5120', 'antenna': 'RX2'}]}
2022-04-29 13:38:58.359 | DEBUG    | __main__:__init__:174 - USRP {'usrp': <gr_block usrp_source (1)>, 'n_channels': 1, 'antenna': 'RX2', 'serial': '3136D4B', 'config': {'serial': '3136D4B', 'subdev': 'A:A', 'args': 'recv_frame_size=1032, num_recv_frames=5120', 'antenna': 'RX2'}}: Setting clock source to external and time source to external
2022-04-29 13:38:58.775 | INFO     | __main__:__init__:180 - Clock ref locked True
2022-04-29 13:38:58.775 | DEBUG    | __main__:__init__:183 - 0 antenna RX2 gain 45
2022-04-29 13:38:58.777 | DEBUG    | __main__:__init__:206 - 1
2022-04-29 13:38:59.779 | WARNING  | __main__:__init__:222 - syncing time -- If this hangs, please verify that an external pps is configured
2022-04-29 13:38:59.780 | DEBUG    | __main__:sync_clock:244 - USRP0 time 1.316781625 1970-01-01 10:00:01.316782
2022-04-29 13:39:00.083 | INFO     | __main__:sync_clock:258 - USRP 3136D4B -- Successfully synced USRP GPS time in 1 attempts
2022-04-29 13:39:00.084 | INFO     | __main__:__init__:224 - verifying time
2022-04-29 13:39:01.010 | INFO     | __main__:check_clock:275 - USRP0 time 1651203541.0 2022-04-29 13:39:01.000000
2022-04-29 13:39:01.010 | WARNING  | __main__:check_clock:280 - All USRP clocks in sync (to the second)
2022-04-29 13:39:02.011 | DEBUG    | __main__:__init__:229 - tune the USRPs
2022-04-29 13:39:02.117 | DEBUG    | __main__:__init__:231 - done
2022-04-29 13:39:02.117 | INFO     | __main__:<module>:353 - start recording
2022-04-29 13:39:02.117 | INFO     | __main__:start:317 - Before stream start: system time 1651203542.1179564 usrp time 1651203542.0 (2022-04-29 13:39:02.000000)
2022-04-29 13:39:02.118 | WARNING  | __main__:start:329 - Starting recording immediately
2022-04-29 13:39:02.118 | INFO     | __main__:start:331 - Calling start()
2022-04-29 13:39:02.119 | INFO     | __main__:start:335 - Time after issuing start stream command: 1651203542.1197705 usrp time 1651203542.0 (2022-04-29 13:39:02.000000)
2022-04-29 13:39:04.894 | WARNING  | __main__:<module>:363 - verifying clock after recording
2022-04-29 13:39:05.011 | INFO     | __main__:check_clock:275 - USRP0 time 1651203545.0 2022-04-29 13:39:05.000000
2022-04-29 13:39:05.012 | WARNING  | __main__:check_clock:280 - All USRP clocks in sync (to the second)
2022-04-29 13:39:05.012 | INFO     | __main__:<module>:365 - {'Fc_hz': 437000000.0, 'Fs_hz': 500000.0, 'gain_dB': 45, 'NID': 666, 'file_name': '', 'file_path': '/media/rf_data/recordings', 'antenna': 'RX2', 'pps_present': True, 'test_dev_null': False, 'usrps': [{'serial': '3136D4B', 'subdev': 'A:A', 'args': 'recv_frame_size=1032, num_recv_frames=5120', 'antenna': 'RX2'}], 'output_file_names': ['1651203538_666_ch0.dat']}


Dual channel recording
2022-04-29 13:39:15.988 | INFO     | __main__:__init__:165 - USRPS: [{'usrp': <gr_block usrp_source (1)>, 'n_channels': 2, 'antenna': 'RX2', 'serial': '3136D4B', 'config': {'serial': '3136D4B', 'subdev': 'A:A A:B', 'args': 'recv_frame_size=1032, num_recv_frames=5120', 'antenna': 'RX2'}}]
2022-04-29 13:39:15.988 | DEBUG    | __main__:__init__:166 - settings: {'Fc_hz': 437000000.0, 'Fs_hz': 500000.0, 'gain_dB': 45, 'NID': 666, 'file_name': '', 'file_path': '/media/rf_data/recordings', 'antenna': 'RX2', 'pps_present': True, 'test_dev_null': False, 'usrps': [{'serial': '3136D4B', 'subdev': 'A:A A:B', 'args': 'recv_frame_size=1032, num_recv_frames=5120', 'antenna': 'RX2'}]}
2022-04-29 13:39:15.989 | DEBUG    | __main__:__init__:174 - USRP {'usrp': <gr_block usrp_source (1)>, 'n_channels': 2, 'antenna': 'RX2', 'serial': '3136D4B', 'config': {'serial': '3136D4B', 'subdev': 'A:A A:B', 'args': 'recv_frame_size=1032, num_recv_frames=5120', 'antenna': 'RX2'}}: Setting clock source to external and time source to external
2022-04-29 13:39:16.412 | INFO     | __main__:__init__:180 - Clock ref locked True
2022-04-29 13:39:16.413 | DEBUG    | __main__:__init__:183 - 0 antenna RX2 gain 45
2022-04-29 13:39:16.414 | DEBUG    | __main__:__init__:183 - 1 antenna RX2 gain 45
2022-04-29 13:39:16.416 | DEBUG    | __main__:__init__:206 - 2
2022-04-29 13:39:17.418 | WARNING  | __main__:__init__:222 - syncing time -- If this hangs, please verify that an external pps is configured
2022-04-29 13:39:17.420 | DEBUG    | __main__:sync_clock:244 - USRP0 time 1.6931099062500001 1970-01-01 10:00:01.693110
2022-04-29 13:39:18.030 | INFO     | __main__:sync_clock:258 - USRP 3136D4B -- Successfully synced USRP GPS time in 1 attempts
2022-04-29 13:39:18.031 | INFO     | __main__:__init__:224 - verifying time
2022-04-29 13:39:19.011 | INFO     | __main__:check_clock:275 - USRP0 time 1651203559.0 2022-04-29 13:39:19.000000
2022-04-29 13:39:19.012 | WARNING  | __main__:check_clock:280 - All USRP clocks in sync (to the second)
2022-04-29 13:39:20.013 | DEBUG    | __main__:__init__:229 - tune the USRPs
2022-04-29 13:39:20.118 | DEBUG    | __main__:__init__:231 - done
2022-04-29 13:39:20.119 | INFO     | __main__:<module>:353 - start recording
2022-04-29 13:39:20.120 | INFO     | __main__:start:317 - Before stream start: system time 1651203560.1199565 usrp time 1651203560.0 (2022-04-29 13:39:20.000000)
2022-04-29 13:39:20.120 | WARNING  | __main__:start:329 - Starting recording immediately
2022-04-29 13:39:20.120 | INFO     | __main__:start:331 - Calling start()
2022-04-29 13:39:22.012 | INFO     | __main__:start:335 - Time after issuing start stream command: 1651203562.012672 usrp time 3302407122.0 (2074-08-25 17:18:42.000000)
2022-04-29 13:39:27.165 | WARNING  | __main__:<module>:363 - verifying clock after recording
2022-04-29 13:39:28.010 | INFO     | __main__:check_clock:275 - USRP0 time 3302407128.450936 2074-08-25 17:18:48.450936
2022-04-29 13:39:28.011 | WARNING  | __main__:check_clock:280 - All USRP clocks in sync (to the second)
2022-04-29 13:39:28.011 | INFO     | __main__:<module>:365 - {'Fc_hz': 437000000.0, 'Fs_hz': 500000.0, 'gain_dB': 45, 'NID': 666, 'file_name': '', 'file_path': '/media/rf_data/recordings', 'antenna': 'RX2', 'pps_present': True, 'test_dev_null': False, 'usrps': [{'serial': '3136D4B', 'subdev': 'A:A A:B', 'args': 'recv_frame_size=1032, num_recv_frames=5120', 'antenna': 'RX2'}], 'output_file_names': ['1651203556_666_ch0.dat', '1651203556_666_ch1.dat']}
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#
# SPDX-License-Identifier: GPL-3.0
#
# GNU Radio Python Flow Graph
# Title: Not titled yet
#

import json5 as json

from gnuradio import blocks
from gnuradio import gr
from gnuradio.filter import firdes
from gnuradio.fft import window
import sys
import os
import signal
from argparse import ArgumentParser
from gnuradio.eng_arg import eng_float, intx
from gnuradio import eng_notation
from gnuradio import gr, blocks
from gnuradio import uhd
import time
import numpy as np
from datetime import datetime
from loguru import logger
logger.add("logs/min_test_example.log", rotation="1 week")

# for printing time stamp
fmt = '%Y-%m-%d %H:%M:%S.%f'


dual_record_settings_json = """
{
    "Fc_hz": 437000000.0,
    "Fs_hz": 500000.0,
    "gain_dB": 45,
    "NID": 666,
    "file_name": "",
    "file_path": "/media/rf_data/recordings",
    "antenna":
    "RX2",
    "pps_present": true,
    "test_dev_null": false,
    "usrps": [
	{
	    "serial": "3136D4B",
	    "subdev": "A:A A:B",
	    "args": "recv_frame_size=1032, num_recv_frames=5120",
	    "antenna": "RX2"
	}
    ]
}
"""
single_record_settings_json = """
{
    "Fc_hz": 437000000.0,
    "Fs_hz": 500000.0,
    "gain_dB": 45,
    "NID": 666,
    "file_name": "",
    "file_path": "/media/rf_data/recordings",
    "antenna":
    "RX2",
    "pps_present": true,
    "test_dev_null": false,
    "usrps": [
	{
	    "serial": "3136D4B",
	    "subdev": "A:A",
	    "args": "recv_frame_size=1032, num_recv_frames=5120",
	    "antenna": "RX2"
	}
    ]
}
"""


class bulk_usrp_record(gr.top_block):

    def __init__(self, cfg):
        gr.top_block.__init__(self, "")

        # get default settings
        self.cfg = cfg
        self.samp_rate = int(cfg.get('Fs_hz'))
        self.Fc_hz = int(cfg.get('Fc_hz'))
        self.gain_dB = cfg.get('gain_dB', 0)
        self.antenna = cfg.get('antenna', 'RX2')  # default if not specified for each USRP
        self.subdev = cfg.get('subdev', 'A:A')  # default if not specified for each USRPS
        self.args = cfg.get('args', '')  # default if not specified for each USRPS
        self.pps_present = cfg.get('pps_present', True)  # default to pps enabled. If no pps is present the script will hang
        self.test_dev_null = cfg.get('test_dev_null', False)  # write output to dev/null just for testing purposes

        self.file_base_name = cfg.get('file_name')
        self.file_path = cfg.get('file_path')

        # check the USRPs
        usrp_configs = cfg.get('usrps', [])

        if len(usrp_configs) == 0:  # empty list, initialize one USRP with one channel

            n_channels = len(self.subdev.split(' '))
            uhd_usrp_source_0 = uhd.usrp_source(
                self.args,
                uhd.stream_args(
                    cpu_format="fc32",
                    args='',
                    channels=list(range(0, n_channels)),
                ),
            )
            uhd_usrp_source_0.set_subdev_spec(self.subdev, 0)
            uhd_usrp_source_0.set_thread_priority(1)  # -1 lower priority, 0 normal, 1 higher priority

            self.uhd_usrp_sources = [
                {'usrp': uhd_usrp_source_0,
                 'n_channels': n_channels,
                 'antenna': self.antenna,
                 'config': {}

                 }
            ]

        else:
            # make multiple USRP devices. All devices must be fully specified
            # Required: Serial and n_channels
            # args

            self.uhd_usrp_sources = []
            for u in usrp_configs:

                subdev = u.get('subdev', self.subdev)
                n_channels = len(subdev.split(' '))
                args = u.get('args', self.args)
                serial = u.get('serial','')
                addr = u.get('addr','')
                if len(serial) > 0:
                    args += f', serial={serial}'
                elif len(addr) > 0:
                    args += f', addr={addr}'

                uhd_usrp_source_0 = uhd.usrp_source(
                    args,
                    uhd.stream_args(
                        cpu_format="fc32",
                        args='',
                        channels=list(range(0, n_channels)),
                    ),
                )
                uhd_usrp_source_0.set_subdev_spec(subdev, 0)
                uhd_usrp_source_0.set_thread_priority(1)  # -1 lower priority, 0 normal, 1 higher priority

                self.uhd_usrp_sources.append(
                    {'usrp': uhd_usrp_source_0,
                     'n_channels': n_channels,
                     'antenna': u.get('antenna', self.antenna),
                     'serial': serial,
                     'config': u
                     }
                )


        logger.info(f'USRPS: {self.uhd_usrp_sources}')
        logger.debug(f'settings: {self.cfg}')

        # finish configuring the USRPS
        for usrp in self.uhd_usrp_sources:
            u = usrp['usrp']
            u_cfg = usrp['config']
            clock_source = u_cfg.get('clock_source','external') 
            time_source = u_cfg.get('time_source','external')
            logger.debug(f'USRP {usrp}: Setting clock source to {clock_source} and time source to {time_source}')
            u.set_clock_source(clock_source, 0)
            u.set_time_source(time_source, 0)
            u.set_samp_rate(self.samp_rate) # needs to be set immediately
            # check USRP lock status
            status_ref_locked = u.get_mboard_sensor('ref_locked').to_bool()
            logger.info(f'Clock ref locked {status_ref_locked}')

            for c in range(usrp['n_channels']):
                logger.debug(f'{c} antenna {usrp["antenna"]} gain {self.gain_dB}')
                u.set_antenna(usrp['antenna'], c)
                try:
                    u.set_rx_agc(False, c)
                except (NotImplementedError, RuntimeError):
                    logger.debug(f'Disabling AGC not supported for usrp {usrp["serial"]}')
                u.set_gain(self.gain_dB, c)

        # Create the file sources and connect these
        cfg['output_file_names'] = []
        if self.test_dev_null is True:
            file_base = 'rec/null'
            file_path = ''
            logger.info(f'Writing to {file_base}')
        else:
            file_date = int(time.time())
            file_path = f'{self.file_path}/'
            file_base = f'{file_date}_{cfg.get("NID","")}'

        file_offset = 0
        for usrp in self.uhd_usrp_sources:
            # create file name as
            
            logger.debug(usrp['n_channels'])
            for c in range(usrp['n_channels']):
                if self.test_dev_null:
                    f_name = f'{file_base}_{c+file_offset}.bin'
                else:
                    f_name = f'{file_base}_ch{c+file_offset}.dat'

                cfg['output_file_names'].append(f_name)
                blocks_file_meta_sink_0 = blocks.file_meta_sink(gr.sizeof_gr_complex*1, file_path + f_name, self.samp_rate, 1, blocks.GR_FILE_FLOAT, True, 1000000, detached_header=True)
                blocks_file_meta_sink_0.set_unbuffered(False)
                self.connect((usrp['usrp'], c), (blocks_file_meta_sink_0, 0))
            
            file_offset = file_offset + usrp['n_channels']

        if self.pps_present:
            time.sleep(1)
            logger.warning('syncing time -- If this hangs, please verify that an external pps is configured')
            self.sync_clock()
            logger.info('verifying time')
            self.check_clock()
        else:
            logger.warning('skipping time sync, since pps is configured not to be present')
        time.sleep(1)
        logger.debug('tune the USRPs')
        self.tune_usrps()
        logger.debug('done')


    def sync_clock(self):
        """
        Set the time on the USRPs
        """
        time_tol = 50e-9
        
        for i,usrp in enumerate(self.uhd_usrp_sources):
            
            usrp_last_pps_time = usrp['usrp'].get_time_last_pps().get_real_secs()
            dtstamp = datetime.fromtimestamp(usrp_last_pps_time)
            logger.debug(f'USRP{i} time {usrp_last_pps_time} {dtstamp.strftime(fmt)}')

            cur_time = time.time() # GPS synced pc clock
            cnt = 0
            t_err = int(cur_time) - usrp_last_pps_time
            while abs(t_err) > time_tol:
                usrp['usrp'].set_time_next_pps(uhd.time_spec_t(int(time.time())+1))
                time.sleep(0.1)

                usrp_last_pps_time = usrp['usrp'].get_time_last_pps().get_real_secs()
                cur_time = time.time()
                t_err = int(cur_time) - usrp_last_pps_time
            cnt += 1

            logger.info(f'USRP {usrp.get("serial","")} -- Successfully synced USRP GPS time in {cnt} attempts')


    def check_clock(self):
        """
        Check whether the USRPs are in sync (same second)
        """
        u = self.uhd_usrp_sources[0]['usrp']
        last_time = u.get_time_last_pps().get_real_secs()
        # wait for clock edge on one USRP
        while u.get_time_last_pps().get_real_secs() - last_time == 0:
            time.sleep(0.001)

        times = []
        for i,usrp in enumerate(self.uhd_usrp_sources):
            times.append(usrp['usrp'].get_time_last_pps().get_real_secs())
            dtstamp = datetime.fromtimestamp(times[-1])
            logger.info(f'USRP{i} time {times[-1]} {dtstamp.strftime(fmt)}')

        if not all([t == times[0] for t in times]):
            logger.warning(f'USRP clocks not synced. times recorded: {times}')
        else:
            logger.warning('All USRP clocks in sync (to the second)')
        
        
        
    def tune_usrps(self):
        """
        Tune all the USRPs coherently to the center frequency.
        """
        CMD_DELAY = 2
        usrp_time = self.uhd_usrp_sources[0]['usrp'].get_time_last_pps().get_real_secs()

        cmd_time = usrp_time + CMD_DELAY
        for usrp in self.uhd_usrp_sources:
            u = usrp['usrp']
            for c in range(usrp['n_channels']):
                u.set_center_freq(self.Fc_hz, c)

                
        
    def get_samp_rate(self):
        return self.samp_rate

    def set_samp_rate(self, samp_rate):
        self.samp_rate = samp_rate
        self.uhd_usrp_source_0.set_samp_rate(self.samp_rate)

    def start(self,delay_sec=0):
        """
        Override the start method to insert a command timer

        TODO: B210 two channel mode throws "USRP Source Block caught rx error code: 2" because the time gets multiplied by 2 when starting the recording. 
              Check whether UHD 4.2 fixes this
        """

        # all USRPs should be in sync so use the time from one
        usrp_last_pps_time = self.uhd_usrp_sources[0]['usrp'].get_time_last_pps().get_real_secs()
        dtstamp = datetime.fromtimestamp(usrp_last_pps_time)
        logger.info(f'Before stream start: system time {time.time()} usrp time {usrp_last_pps_time} ({dtstamp.strftime(fmt)})')

        if delay_sec > 0:
            for u in self.uhd_usrp_sources:
                u['usrp'].set_start_time(uhd.time_spec_t(int(usrp_last_pps_time)+delay_sec))

        
            logger.warning(f'Starting recording in {int(usrp_last_pps_time)+delay_sec - int(time.time())} s at time {int(usrp_last_pps_time)+delay_sec}')
            usrp_last_pps_time = self.uhd_usrp_sources[0]['usrp'].get_time_last_pps().get_real_secs()
            dtstamp = datetime.fromtimestamp(usrp_last_pps_time)
            logger.info(f'Check time again system time {time.time()} usrp time {usrp_last_pps_time} ({dtstamp.strftime(fmt)})')
        else:
            logger.warning('Starting recording immediately')

        logger.info('Calling start()')
        gr.top_block.start(self)
        usrp_last_pps_time = self.uhd_usrp_sources[0]['usrp'].get_time_last_pps().get_real_secs()
        dtstamp = datetime.fromtimestamp(usrp_last_pps_time)
        logger.info(f'Time after issuing start stream command: {time.time()} usrp time {usrp_last_pps_time} ({dtstamp.strftime(fmt)})')


if __name__ == '__main__':

    cfg = json.loads(dual_record_settings_json)
    
    tb = bulk_usrp_record(cfg)
    
    def sig_handler(sig=None, frame=None):
        tb.stop()
        tb.wait()

        sys.exit(0)

    signal.signal(signal.SIGINT, sig_handler)
    signal.signal(signal.SIGTERM, sig_handler)

    logger.info(f'start recording')
    tb.start(0) # TODO: B210 two channel mode multiplies time by 2 upon start, so anything above 0 here will throw rx error code 2 

    try:
        input('Press Enter to quit: ')
    except EOFError:
        pass
    
    tb.stop()
    tb.wait()
    logger.warning(f'verifying clock after recording')
    tb.check_clock()
    logger.info(cfg)

    

Reply via email to