/* a little hack to transmit FM with a B210 * ---- * 2023-05-18: first release * ---- * you need a 2MHz mono 16b audio file that you can generate like: * ffmpeg -i input.mp3 -ar 2000000 -ac 1 /tmp/out.wav * Why 2MHz? It was working better than others (say 200KHz or 400KHz * with the B210). * File name hardcoded to /tmp/out.wav * ---- * to compile: * g++ -Wall -g -o fm fm.cc -lm -luhd -lboost_system * for me (crazy setup): * g++ -Wall -g -I /home/sed/c/dab/external/usr/include -o fm fm.cc -lm -Wl,-rpath=/home/sed/c/dab/external/usr/lib/x86_64-linux-gnu -L/home/sed/c/dab/external/usr/lib/x86_64-linux-gnu -luhd -lboost_system * ---- * to run: * ./fm * for me: * UHD_IMAGES_DIR=/home/sed/c/dab/external/usr/share/uhd/4.3.0 LD_LIBRARY_PATH=/home/sed/c/dab/external/usr/lib/x86_64-linux-gnu ./fm * and tune your FM receiver to 100.2MHz * ---- * for a 400KHz file, change: * usrp->set_tx_rate(2000e3, 0); * with: * usrp->set_tx_rate(400e3, 0); * and: * d_phase += .6 * x; * with: * d_phase += 3.6 * x; * This value depends on your input audio file, how loud it is. * Just try values and use one that gives loud enough signal * without saturation. * I don't understand it all properly to be honest. * ---- * references: * - https://www.dlineradio.co.uk/articles/building-a-very-low-power-fm-transmitter-with-hackrf-one/ * contains a mono gnuradio FM transmitter and also a stereo one * - source code of gnuradio * - https://electronicscoach.com/frequency-modulation.html * contains those formulas for single tone FM: * the signal to transmit with FM: * m(t) = Vm * cos(2 * pi * fm * t) * the carrier wave: * c(t) = Vc * sin(omega_c * t + phi) * instantaneous frequency: * fi(t) = fc + kf * Vm * cos(2 * pi * fm * t) * fi(t) = fc + delta_f * cos(2 * pi * fm * t) * delta_f = kf * Vm * - https://www.analog.com/media/en/training-seminars/design-handbooks/Software-Defined-Radio-for-Engineers-2018/SDR4Engineers_CH02.pdf#page=34 * contains the formula for the output of B210: r(t) = I(t) * cos(omega_c * t) - Q(t) * sin(omega_c * t) * (if I understand well) * - https://kb.ettus.com/Suggested_Reading * - https://wiki.gnuradio.org/index.php/Frequency_Mod * - https://www.analog.com/en/education/education-library/software-defined-radio-for-engineers.html * - https://pysdr.org * - https://nvhrbiblio.nl/biblio/boek/Faruque%20-%20Radio%20Frequency%20Modulation%20made%20easy.pdf * ---- * some octave code from https://dsp.stackexchange.com/questions/73450/from-iq-signal-to-fm-modulated-carrier-how-its-done: % Keep Octave happy pkg load signal; % Bit rate Rb = 9600; % Modulation index; 1/2 is Minimum Shift Keying. modulation_index = 1/2; % GMSK specific parameters BT = 0.4; L = 3; % duration of pulse in symbols % Optional frequency shift for demonstration of modulation on a % carrier within -Fs/2 to +Fs/2 freq_shift_hz = 12.5e3; % Samples per symbol and sample rate out of the modulation process sps = 10; Fs = Rb * sps; % pulse filter taps % Build Gaussian pulse filter Ls = round(L*sps); % FIXME, code assumes even. alpha = sqrt(2/log(2)) * pi * BT; k = [(-Ls/2+1):1:(Ls/2-1)]; taps_pf = (erf(alpha*(k/sps + 0.5)) - erf(alpha*(k/sps - 0.5)))*0.5/sps; K_pf = length(taps_pf); if (mod(K_pf,2) == 0) delay_pf = K_pf/2; else delay_pf = (K_pf-1)/2; end % Create a random bit string nbits = 50; message_bits = round(rand(nbits,1)); nrz_message_bits = (message_bits - 0.5)*2; % Upsample and pulse shape the packet % N.B. Octave's filter function sets up a history of 0's for us. x = sps*[upsample(nrz_message_bits, sps); zeros(delay_pf+sps*3/2, 1)]; packet_baseband = filter(taps_pf, [1], x); % Frequency modulate the baseband packet fm_gain = pi/(Fs/2) * Rb/2 * modulation_index; x = packet_baseband * fm_gain; phase = cumsum(x); % phase is integral of frequency packet_modulated = exp(1i*mod(phase, 2*pi)); % Perform a frequency shift (using a time varying complex exponential) radian_phase_inc = pi/(Fs/2) .* freq_shift_hz; rotator = exp(1i*mod(radian_phase_inc * [0:(length(packet_modulated)-1)],2*pi)); packet_modulated_if = packet_modulated .* rotator.'; figure(1); stem(message_bits); title('Message Bits'); xlabel('Bit number'); ylabel('Value'); grid on; figure(2); N1 = length(packet_baseband); t1 = [0:N1-1]/Fs; plot(t1, packet_baseband * Rb/2 * modulation_index, 'x-'); title('Upsampled, Pulse Shaped, Scaled, and Level Shifted NRZ Bits'); xlabel('Time (seconds)'); ylabel('Frequency Deviation (Hz)'); grid on; figure(3); N2 = length(phase); t2 = [0:N2-1]/Fs; plot(t2, phase, '.-'); title('Accumulated Phase'); xlabel('Time (seconds)'); ylabel('Accumulated Phase (radians)'); grid on; figure(4); N = length(packet_modulated); t = [0:N-1]/Fs; plot(t, real(packet_modulated), t, imag(packet_modulated)); title('I & Q of GMSK Modulated Signal'); xlabel('Time (seconds)'); ylabel('Amplitude'); grid on; figure(5); N_if = length(packet_modulated_if); t_if = [0:N_if-1]/Fs; plot(t_if, real(packet_modulated_if), t_if, imag(packet_modulated_if)); title('I & Q of GMSK Modulated Signal at IF'); xlabel('Time (seconds)'); ylabel('Amplitude'); grid on; * ---- * TODO: * - FM pre-emphasis (boost high frequencies) * see gr-analog/python/analog/fm_emph.py * - lowpass at 15KHz of audio signal * - resampling of audio (inout 48KHz stereo, output 200KHz (or whatever)) * see gr-filter/include/gnuradio/filter/rational_resampler.h * - resampling of mixed signal from 200KHz to 2MHz * - stereo * (add a tone at 19Khz, and add L+R at center and the L-R at 38KHz * (multiply - signal by 38KHz tone, do pass filter 23KHZ - 53KHz)) * - rds */ #include #include #include #include #include static uhd::usrp::multi_usrp::sptr usrp; static uhd::tx_streamer::sptr tx_stream; void usrp_init(void) { std::string args = ""; //type=b200"; uhd::device_addrs_t device_adds = uhd::device::find(args); if(device_adds.size() == 0) { printf("no b200?\n"); abort(); } args += ",num_recv_frames=256" ; usrp = uhd::usrp::multi_usrp::make(args); usrp->set_master_clock_rate(20e6); printf("%d TX channels, %d RX channels\n", (int)usrp->get_tx_num_channels(), (int)usrp->get_rx_num_channels()); usrp->set_tx_rate(2000e3, 0); usrp->set_tx_freq(100.2e6, 0); usrp->set_tx_bandwidth(200e3, 0); ::uhd::gain_range_t gain_range = usrp->get_tx_gain_range(0); usrp->set_tx_gain(gain_range.stop(), 0); uhd::stream_args_t stream_args_tx("sc16", "sc16"); stream_args_tx.channels.push_back(0); tx_stream = usrp->get_tx_stream(stream_args_tx); printf("tx stream max num samps: %d\n", (int)tx_stream->get_max_num_samps()); usrp->set_time_now(uhd::time_spec_t(0.0)); int i = 0; printf("TX Channel %u\n",i); std::cout << std::endl<get_tx_rate(i)/1e6) << std::endl; std::cout << boost::format("Actual TX frequency: %fGHz...") % (usrp->get_tx_freq(i)/1e9) << std::endl; std::cout << boost::format("Actual TX gain: %f...") % (usrp->get_tx_gain(i)) << std::endl; std::cout << boost::format("Actual TX bandwidth: %fM...") % (usrp->get_tx_bandwidth(i)/1e6) << std::endl; std::cout << boost::format("Actual TX antenna: %s...") % (usrp->get_tx_antenna(i)) << std::endl; std::cout << boost::format("Device timestamp: %f...") % (usrp->get_time_now().get_real_secs()) << std::endl; } void usrp_tx(char *txbuf) { uhd::tx_metadata_t tx_md; tx_md.has_time_spec = false; tx_md.start_of_burst = true; tx_md.end_of_burst = true; tx_stream->send(txbuf, 1024, tx_md); } static char txbuf[1024*2*2]; int main(void) { usrp_init(); int i; short *t = (short *)txbuf; double d_phase = 0; short *metallica = (short *)malloc(4096); if (metallica == NULL) abort(); loop: int fin = open("/tmp/out.wav", O_RDONLY); if (fin == -1) abort(); int pos = 2048; while (1) { if (pos == 2048) { int l = read(fin, metallica, 4096); if (l <= 0) goto end; pos = 0; memset(metallica + l, 0, 4096 - l); } for (i = 0; i < 1024; i++, pos++) { double x = metallica[pos] / 32768.; d_phase += .6 * x; d_phase = fmod(d_phase + M_PI, 2 * M_PI) - M_PI; t[i*2] = 32000 * cos(d_phase); t[i*2+1] = 32000 * sin(d_phase); } usrp_tx(txbuf); } end: close(fin); goto loop; return 0; }