greatscottgadgets / hackrf

low cost software radio platform
https://greatscottgadgets.com/hackrf/
GNU General Public License v2.0
6.33k stars 1.5k forks source link

[Question]: What is the granularity of sample rate? #1223

Open BenHut1 opened 1 year ago

BenHut1 commented 1 year ago

What would you like to know?

How precisely can I set the HackRF's sample rate? To the nearest MHz? Or to the nearest Hz? Or something in between?

I'm asking because I was wondering if I could set the sample rate to 4,800,000 MHz. This would be ideal for audio, as it's an exact power-of-ten multiple of 48kHz (a common sound card sample rate) which would greatly simplify the algorithm used to upscale audio data from my microphone to the HackRF sample rate, so as to be able to transmit live audio coming into my sound card from the microphone.

Likewise if I could set the sample rate of the HackRF to exactly 14,318,180Hz (14.31880MHz) this would be great, as this is an exact multiple (4 times, specifically) of the chroma carrier frequency in NTSC TV. This sample rate would put the chroma carrier exactly half way between the 0Hz and the nyquist limit. If this exact sample rate (accurate to 1Hz precision) could be used with the HackRF, it would greatly simplify software I'm writing to transmit or receive NTSC video. With both transmitting and receiving, it would allow my image frame's full width (including all sync and blanking) to be an integer number (so it can be measured in pixels, and I don't have to worry about fractions of a pixel in the width of the image). Specifically the image width would be 910 pixels in this case. For receiving specifically, it would also make it much easier to separate the luma and chroma signals, using simply a horizontal kernel filter of (1, 0, 1) for extracting luma and a horizontal kernel filter of (-1, 0, 1) for extracting chroma.

martinling commented 1 year ago

How precisely can I set the HackRF's sample rate? To the nearest MHz? Or to the nearest Hz? Or something in between?

Something in between. The HackRF's Si5351 clock generator produces an 800MHz clock internally, which is fed to multiple programmable synthesizers, each of which has a fractional divider. If one of those units can produce twice your desired sample rate, then it can be used exactly.

The relevant calculation can be seen here: https://github.com/greatscottgadgets/hackrf/blob/15636efb4d05948991fd193efcd41e493eb3bb9a/firmware/common/hackrf_core.c#L352-L389

The rate_num input must be twice the sample rate you want, because we need to generate a clock at twice the sample rate for the ADC/DAC, which takes the I and Q data on consecutive clock cycles.

If your values hit the Integer mode or Perfect match cases, the sample rate you want can be set exactly. If not, it will be an approximation. You can apply the values you want by calling hackrf_set_sample_rate_manual.

martinling commented 1 year ago

I wrote a quick script to check which values can be set exactly, and it looks like both 4,800,000 Hz and 14,318,180 Hz can be set exactly:

$ python samplerate.py 4800000
Exact match, fractional mode
hackrf_set_sample_rate_manual(dev, 4800000, 1)

$ python samplerate.py 14318180
Exact match, fractional mode
hackrf_set_sample_rate_manual(dev, 14318180, 1)

Edit: see corrected script below.

BenHut1 commented 1 year ago

How precisely can I set the HackRF's sample rate? To the nearest MHz? Or to the nearest Hz? Or something in between?

Something in between. The HackRF's Si5351 clock generator produces an 800MHz clock internally, which is fed to multiple programmable synthesizers, each of which has a PLL and a fractional divider. If one of those units can produce twice your desired sample rate, then it can be used exactly.

The relevant calculation can be seen here:

https://github.com/greatscottgadgets/hackrf/blob/15636efb4d05948991fd193efcd41e493eb3bb9a/firmware/common/hackrf_core.c#L352-L389

The rate_num input must be twice the sample rate you want, because we need to generate a clock at twice the sample rate for the ADC/DAC, which takes the I and Q data on consecutive clock cycles. The denominator may be from 1 to 32.

If your values hit the Integer mode or Perfect match cases, the sample rate you want can be set exactly. If not, it will be an approximation. You can apply the values you want by calling hackrf_set_sample_rate_manual.

I have a question about the code you linked to here. In the line that says a = (VCO_FREQ * rate_denom) / rate_num; The result of the above equation (because it includes VCO_FREQ, which is a value of type Double) will be a Double. So when the variable "a" (an unsigned integer) is set to this value, rounding will have to occur. What method of rounding is used in your program? Is it round down always (floor), round up always (ceiling), or round to nearest integer? And if it is round to nearest integer, what method is used for tie breaking (when the value is exactly half way between two integers)? Does it always break a tie by rounding up, rounding down, rounding to the nearest even number, or rounding to the nearest odd number?

I know that on a PC (an Intel x86 processor) floating point rounding can be configured to use one of 4 different methods. However, this code that you linked to is not PC-side code. It's firmware for the HackRF itself. What method of rounding does the processor in the HackRF use? Can it be configured to use different methods? And what method is used in the official firmware, who's code you linked to here?

Also, the code above that tests if it can use an exact frequency, or only an approximate frequency, doesn't seem to output what that approximate frequency will be. If I select a sample rate that can only be used approximately, the above algorithm outputs 3 values stored in variables called a, b, and c. But how are those 3 values then converted into the frequency that the HackRF will actually use?

martinling commented 1 year ago

VCO_FREQ is a uint64_t, not a double. There's no floating point involved in any of this, it's all integers.

If I select a sample rate that can only be used approximately, the above algorithm outputs 3 values stored in variables called a, b, and c. But how are those 3 values then converted into the frequency that the HackRF will actually use?

Well, how they're actually converted into the frequency is that the values a, b and c are packed into the MSx_P1, MSx_P2, MSx_P3 register values and written to the Si5351, which physically generates the frequency. I don't have an equation to hand, but you could look at the Si5351 documentation. Or assume that those a, b and c resulted from the Perfect match case, and work backwards to find the rate_num that would have given that result.

martinling commented 1 year ago

The previous script was not quite right, although it was correct about the exact matches for 4.8MHz and 14.318180 MHz.

Below is a corrected script, which displays all the results including the effective divisor, resulting sample rate and offset from target.

The two rates you were interested in:

$ python samplerate.py 4800000
Target sample rate: 4800000 / 1 = 4800000.000000 Hz
Exact match, fractional mode
VCO frequency: 800000000 Hz
Divisor: 83 + 1 / 3 = 83.333333
Double sample rate: 9600000.000000 Hz
Actual sample rate: 4800000.000000 Hz
Sample rate error: 0.000000 Hz

$ python samplerate.py 14318180
Target sample rate: 14318180 / 1 = 14318180.000000 Hz
Exact match, fractional mode
VCO frequency: 800000000 Hz
Divisor: 27 + 670457 / 715909 = 27.936511
Double sample rate: 28636360.000000 Hz
Actual sample rate: 14318180.000000 Hz
Sample rate error: 0.000000 Hz

Example of an approximate match:

$ python samplerate.py 12345678
Target sample rate: 12345678 / 1 = 12345678.000000 Hz
Approximate match only
VCO frequency: 800000000 Hz
Divisor: 32 + 419432 / 1048575 = 32.400002
Double sample rate: 24691356.571140 Hz
Actual sample rate: 12345678.285570 Hz
Sample rate error: 0.285570 Hz

Example of setting an exact samplerate of fractional Hz:

$ python samplerate.py 12345675 4
Target sample rate: 12345675 / 4 = 3086418.750000 Hz
Exact match, fractional mode
VCO frequency: 800000000 Hz
Divisor: 129 + 296317 / 493827 = 129.600042
Double sample rate: 6172837.500000 Hz
Actual sample rate: 3086418.750000 Hz
Sample rate error: 0.000000 Hz

The script:

from math import gcd
import sys

if len(sys.argv) < 2 or len(sys.argv) > 3:
    print("Usage: %s <numerator> [denominator]" % sys.argv[0])
    print("where target rate in Hz is (numerator / denominator)")
    print("The denominator defaults to 1")
    sys.exit(1)

# Get target sample rate from command line.
target_num = int(sys.argv[1])
if len(sys.argv) == 3:
    target_denom = int(sys.argv[2])
else:
    target_denom = 1
target_rate = target_num / target_denom
print("Target sample rate: %d / %d = %f Hz" %
        (target_num, target_denom, target_rate))

# Calculate closest match using same method as firmware.
VCO_FREQ = 800 * 1000 * 1000
rate_num = target_num * 2
rate_denom = target_denom
a = (VCO_FREQ * rate_denom) // rate_num
rem = (VCO_FREQ * rate_denom) - (a * rate_num)
if rem == 0:
    print("Exact match, integer mode")
    b = 0
    c = 1
else:
    g = gcd(rem, rate_num)
    rem //= g
    rate_num //= g
    if rate_num < (1 << 20):
        print("Exact match, fractional mode")
        b = rem
        c = rate_num
    else:
        print("Approximate match only")
        c = (1 << 20) - 1
        b = (c * rem) // rate_num
        g = gcd(b, c)
        b //= g
        c //= g

# Calculate resulting divisor and frequencies.
divisor = a + b / c
double_rate = VCO_FREQ / divisor
actual_rate = double_rate / 2
rate_error = actual_rate - target_rate

print("VCO frequency: %d Hz" % VCO_FREQ)
print("Divisor: %d + %d / %d = %f" % (a, b, c, divisor))
print("Double sample rate: %f Hz" % double_rate)
print("Actual sample rate: %f Hz" % actual_rate)
print("Sample rate error: %f Hz" % rate_error)
martinling commented 1 year ago

Vectorising the error calculation over all integer samplerates from 1 Hz to 20 MHz gives some interesting results:

image

from matplotlib.pyplot import *
import numpy as np

VCO_FREQ = 800 * 1000 * 1000

target_num = np.arange(1, 20000001)
target_denom = 1
target_rate = target_num / target_denom
rate_num = target_num * 2
rate_denom = 1
a = (VCO_FREQ * rate_denom) // rate_num
b = np.empty_like(a)
c = np.empty_like(a)
rem = (VCO_FREQ * rate_denom) - (a * rate_num)
int_mode = rem == 0
b[int_mode] = 0
c[int_mode] = 1
frac = ~int_mode
g = np.gcd(rem[frac], rate_num[frac])
rem[frac] //= g
rate_num[frac] //= g
exact = frac & (rate_num < (1 << 20))
b[exact] = rem[exact]
c[exact] = rate_num[exact]
approx = frac & ~exact
c[approx] = (1 << 20) - 1
b[approx] = (c[approx] * rem[approx]) // rate_num[approx]
g = np.gcd(b[approx], c[approx])
b[approx] //= g
c[approx] //= g
divisor = a + b / c
double_rate = VCO_FREQ / divisor
actual_rate = double_rate / 2
rate_error = actual_rate - target_rate

plot(rate_error)
title("Sample rate error vs target sample rate")
xlabel("Sample rate (Hz)")
ylabel("Sample rate error (Hz)")
show()