Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
BenHut1 opened this issue Nov 9, 2022 · 6 comments
Open

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

BenHut1 opened this issue Nov 9, 2022 · 6 comments
Labels
documentation documentation improvement or addition question question from the community that is not technical support

Comments

@BenHut1
Copy link

BenHut1 commented Nov 9, 2022

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.

@BenHut1 BenHut1 added the question question from the community that is not technical support label Nov 9, 2022
@martinling
Copy link
Member

martinling commented Nov 9, 2022

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:

bool sample_rate_frac_set(uint32_t rate_num, uint32_t rate_denom)
{
const uint64_t VCO_FREQ = 800 * 1000 * 1000; /* 800 MHz */
uint32_t MSx_P1, MSx_P2, MSx_P3;
uint32_t a, b, c;
uint32_t rem;
hackrf_ui()->set_sample_rate(rate_num / 2);
/* Find best config */
a = (VCO_FREQ * rate_denom) / rate_num;
rem = (VCO_FREQ * rate_denom) - (a * rate_num);
if (!rem) {
/* Integer mode */
b = 0;
c = 1;
} else {
/* Fractional */
uint32_t g = gcd(rem, rate_num);
rem /= g;
rate_num /= g;
if (rate_num < (1 << 20)) {
/* Perfect match */
b = rem;
c = rate_num;
} else {
/* Approximate */
c = (1 << 20) - 1;
b = ((uint64_t) c * (uint64_t) rem) / rate_num;
g = gcd(b, c);
b /= g;
c /= g;
}
}

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
Copy link
Member

martinling commented Nov 9, 2022

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
Copy link
Author

BenHut1 commented Nov 9, 2022

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:

bool sample_rate_frac_set(uint32_t rate_num, uint32_t rate_denom)
{
const uint64_t VCO_FREQ = 800 * 1000 * 1000; /* 800 MHz */
uint32_t MSx_P1, MSx_P2, MSx_P3;
uint32_t a, b, c;
uint32_t rem;
hackrf_ui()->set_sample_rate(rate_num / 2);
/* Find best config */
a = (VCO_FREQ * rate_denom) / rate_num;
rem = (VCO_FREQ * rate_denom) - (a * rate_num);
if (!rem) {
/* Integer mode */
b = 0;
c = 1;
} else {
/* Fractional */
uint32_t g = gcd(rem, rate_num);
rem /= g;
rate_num /= g;
if (rate_num < (1 << 20)) {
/* Perfect match */
b = rem;
c = rate_num;
} else {
/* Approximate */
c = (1 << 20) - 1;
b = ((uint64_t) c * (uint64_t) rem) / rate_num;
g = gcd(b, c);
b /= g;
c /= g;
}
}

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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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

  • The sample rate error is always positive.
  • The possible error increases with the absolute sample rate.
  • The worst case is still less than 1Hz error.
  • About 23% of rates can be set exactly.

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()

@martinling martinling added the documentation documentation improvement or addition label Nov 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation documentation improvement or addition question question from the community that is not technical support
Projects
None yet
Development

No branches or pull requests

2 participants