aboutsummaryrefslogtreecommitdiff
path: root/octave/fdmdv.m
diff options
context:
space:
mode:
authorMarin Ivanov <[email protected]>2025-07-25 10:17:14 +0300
committerMarin Ivanov <[email protected]>2026-01-18 20:09:26 +0200
commit0168586485e6310c598713c911b1dec5618d61a1 (patch)
tree6aabc2a12ef8fef70683f5389bea00f948015f77 /octave/fdmdv.m
Initial commitHEADmaster
* codec2 cut-down version 1.2.0 * Remove codebook and generation of sources * remove c2dec c2enc binaries * prepare for emscripten
Diffstat (limited to 'octave/fdmdv.m')
-rw-r--r--octave/fdmdv.m971
1 files changed, 971 insertions, 0 deletions
diff --git a/octave/fdmdv.m b/octave/fdmdv.m
new file mode 100644
index 0000000..f13aa26
--- /dev/null
+++ b/octave/fdmdv.m
@@ -0,0 +1,971 @@
+% fdmdv.m
+%
+% Functions that implement a Frequency Division Multiplexed Modem for
+% Digital Voice (FDMDV) over HF channels.
+%
+% Copyright David Rowe 2012
+% This program is distributed under the terms of the GNU General Public License
+% Version 2
+%
+
+fdmdv_common;
+
+% reqd to make sure we get same random bits at mod and demod
+rand('state',1);
+randn('state',1);
+
+% Functions ----------------------------------------------------
+
+
+function f = fdmdv_init(Nc=14)
+ Fs = f.Fs = 8000; % sample rate in Hz
+ T = f.T = 1/Fs; % sample period in seconds
+ Rs = f.Rs = 50; % symbol rate in Hz
+ f.Nc = Nc;
+ Nb = f.Nb = 2; % Bits/symbol for PSK modulation
+ Rb = f.Rb = Nc*Rs*Nb; % bit rate
+ M = f.M = Fs/Rs; % oversampling factor
+ Nsym = f.Nsym = 6; % number of symbols to filter over
+
+ Fsep = f.Fsep = 75; % Separation between carriers (Hz)
+ Fcenter = f.Fcentre = 1500; % Centre frequency, Nc/2 carriers below this,
+ % Nc/2 carriers above (Hz)
+ Nt = f.Nt = 5; % number of symbols we estimate timing over
+ P = f.P = 4; % oversample factor used for rx symbol filtering
+ Nfilter = f.Nfilter = Nsym*M;
+
+ Nfiltertiming = f.Nfiltertiming = M+Nfilter+M;
+
+ Nsync_mem = f.Nsync_mem = 6;
+ f.sync_uw = [1 -1 1 -1 1 -1];
+
+ alpha = 0.5;
+ f.gt_alpha5_root = gen_rn_coeffs(alpha, T, Rs, Nsym, M);
+
+ f.pilot_bit = 0; % current value of pilot bit
+
+ f.tx_filter_memory = zeros(Nc+1, Nfilter);
+ f.rx_filter_memory = zeros(Nc+1, Nfilter);
+ f.Nrx_fdm_mem = Nfilter+M+M/P;
+ f.rx_fdm_mem = zeros(1,f.Nrx_fdm_mem);
+
+ f.snr_coeff = 0.9; % SNR est averaging filter coeff
+
+ % phasors used for up and down converters
+
+ f.freq = zeros(Nc+1,1);
+ f.freq_pol = zeros(Nc+1,1);
+
+ for c=1:Nc/2
+ carrier_freq = (-Nc/2 - 1 + c)*Fsep;
+ f.freq_pol(c) = 2*pi*carrier_freq/Fs;
+ f.freq(c) = exp(j*f.freq_pol(c));
+ end
+
+ for c=floor(Nc/2)+1:Nc
+ carrier_freq = (-Nc/2 + c)*Fsep;
+ f.freq_pol(c) = 2*pi*carrier_freq/Fs;
+ f.freq(c) = exp(j*f.freq_pol(c));
+ end
+
+ f.freq_pol(Nc+1) = 2*pi*0/Fs;
+ f.freq(Nc+1) = exp(j*f.freq_pol(Nc+1));
+
+ f.fbb_rect = exp(j*2*pi*f.Fcentre/Fs);
+ f.fbb_phase_tx = 1;
+ f.fbb_phase_rx = 1;
+
+ % Spread initial FDM carrier phase out as far as possible. This
+ % helped PAPR for a few dB. We don't need to adjust rx phase as DQPSK
+ % takes care of that.
+
+ f.phase_tx = ones(Nc+1,1);
+ f.phase_tx = exp(j*2*pi*(0:Nc)/(Nc+1));
+ f.phase_rx = ones(Nc+1,1);
+
+ % decimation filter
+
+ f.Nrxdec = 31;
+ % fir1() output appears to have changed from when coeffs used in C port were used
+ %f.rxdec_coeff = fir1(f.Nrxdec-1, 0.25);
+ f.rxdec_coeff = [-0.00125472 -0.00204605 -0.0019897 0.000163906 0.00490937 0.00986375 ...
+ 0.0096718 -0.000480351 -0.019311 -0.0361822 -0.0341251 0.000827866 ...
+ 0.0690577 0.152812 0.222115 0.249004 0.222115 0.152812 ...
+ 0.0690577 0.000827866 -0.0341251 -0.0361822 -0.019311 -0.000480351 ...
+ 0.0096718 0.00986375 0.00490937 0.000163906 -0.0019897 -0.00204605 ...
+ -0.00125472];
+
+ % we need room for Nrdec + the max nin, as we may need to filter max_min samples
+
+ f.Nrxdecmem = f.Nrxdec+M+M/P;
+ f.rxdec_lpf_mem = zeros(1,f.Nrxdecmem);
+ f.Q=M/4;
+
+ % freq offset estimation
+
+ f.Mpilotfft = 256;
+ f.Npilotcoeff = 30;
+
+ % here's how to make this filter from scratch, however it appeared to change over different
+ % octave versions so have hard coded to version used for C port
+ %f.pilot_coeff = fir1(f.Npilotcoeff-1, 200/(Fs/2))'; % 200Hz LPF
+ f.pilot_coeff = [0.00223001 0.00301037 0.00471258 0.0075934 0.0118145 0.0174153 ...
+ 0.0242969 0.0322204 0.0408199 0.0496286 0.0581172 0.0657392 ...
+ 0.0719806 0.0764066 0.0787022 0.0787022 0.0764066 0.0719806 ...
+ 0.0657392 0.0581172 0.0496286 0.0408199 0.0322204 0.0242969 ...
+ 0.0174153 0.0118145 0.0075934 0.00471258 0.00301037 0.00223001];
+
+ f.Npilotbaseband = f.Npilotcoeff + M + M/P; % number of pilot baseband samples
+ f.Npilotlpf = 4*M; % reqd for pilot LPF
+ % number of symbols we DFT pilot over
+ % pilot est window
+
+ % pilot LUT, used for copy of pilot at rx
+
+ f.pilot_lut = generate_pilot_lut(f);
+ f.pilot_lut_index = 1;
+ f.prev_pilot_lut_index = 3*M+1;
+
+ % Freq offset estimator states
+
+ f.pilot_baseband1 = zeros(1, f.Npilotbaseband); % pilot baseband samples
+ f.pilot_baseband2 = zeros(1, f.Npilotbaseband); % pilot baseband samples
+ f.pilot_lpf1 = zeros(1, f.Npilotlpf); % LPF pilot samples
+ f.pilot_lpf2 = zeros(1, f.Npilotlpf); % LPF pilot samples
+
+ % Timing estimator states
+
+ f.rx_filter_mem_timing = zeros(Nc+1, Nt*P);
+ f.rx_baseband_mem_timing = zeros(Nc+1, f.Nfiltertiming);
+
+ % Test bit stream state variables
+
+ f = init_test_bits(f);
+endfunction
+
+
+% generate Nc+1 PSK symbols from vector of (1,Nc*Nb) input bits. The
+% Nc+1 symbol is the +1 -1 +1 .... BPSK sync carrier
+
+function [tx_symbols f] = bits_to_psk(f, prev_tx_symbols, tx_bits)
+ Nc = f.Nc; Nb = f.Nb;
+ m4_gray_to_binary = [
+ bin2dec("00")
+ bin2dec("01")
+ bin2dec("11")
+ bin2dec("10")
+ ];
+ m8_gray_to_binary = [
+ bin2dec("000")
+ bin2dec("001")
+ bin2dec("011")
+ bin2dec("010")
+ bin2dec("111")
+ bin2dec("110")
+ bin2dec("100")
+ bin2dec("101")
+ ];
+
+ assert(length(tx_bits) == Nc*Nb, "Incorrect number of bits");
+
+ m = 2 .^ Nb;
+ assert((m == 4) || (m == 8));
+
+ for c=1:Nc
+
+ % extract bits for this symbol
+
+ bits_binary = tx_bits((c-1)*Nb+1:c*Nb);
+ bits_decimal = sum(bits_binary .* 2.^(Nb-1:-1:0));
+
+ % determine phase shift using gray code mapping
+
+ if m == 4
+ phase_shift = (2*pi/m)*m4_gray_to_binary(bits_decimal+1);
+ else
+ phase_shift = (2*pi/m)*m8_gray_to_binary(bits_decimal+1);
+ end
+
+ % apply phase shift from previous symbol
+
+ tx_symbols(c) = exp(j*phase_shift) * prev_tx_symbols(c);
+ end
+
+ % +1 -1 +1 -1 BPSK sync carrier, once filtered becomes two spectral
+ % lines at +/- Rs/2
+
+ if f.pilot_bit
+ tx_symbols(Nc+1) = -prev_tx_symbols(Nc+1);
+ else
+ tx_symbols(Nc+1) = prev_tx_symbols(Nc+1);
+ end
+ if f.pilot_bit
+ f.pilot_bit = 0;
+ else
+ f.pilot_bit = 1;
+ end
+
+endfunction
+
+% LP filter +/- 1000 Hz, allows us to perform rx filtering at a lower rate saving CPU
+
+function [rx_fdm_filter fdmdv] = rxdec_filter(fdmdv, rx_fdm, nin)
+ M = fdmdv.M;
+ Nrxdecmem = fdmdv.Nrxdecmem;
+ Nrxdec = fdmdv.Nrxdec;
+ rxdec_coeff = fdmdv.rxdec_coeff;
+ rxdec_lpf_mem = fdmdv.rxdec_lpf_mem;
+
+ % place latest nin samples at end of buffer
+
+ rxdec_lpf_mem(1:Nrxdecmem-nin) = rxdec_lpf_mem(nin+1:Nrxdecmem);
+ rxdec_lpf_mem(Nrxdecmem-nin+1:Nrxdecmem) = rx_fdm(1:nin);
+
+ % init room for nin output samples
+
+ rx_fdm_filter = zeros(1,nin);
+
+ % Move starting point for filter dot product to filter newest samples
+ % in buffer. This stuff makes my head hurt.
+
+ st = Nrxdecmem - nin - Nrxdec + 1;
+ for i=1:nin
+ a = st+1; b = st+i+Nrxdec-1;
+ %printf("nin: %d i: %d a: %d b: %d\n", nin, i, a, b);
+ rx_fdm_filter(i) = rxdec_lpf_mem(st+i:st+i+Nrxdec-1) * rxdec_coeff';
+ end
+
+ fdmdv.rxdec_lpf_mem = rxdec_lpf_mem;
+end
+
+
+% Combined down convert and rx filter, more memory efficient but less intuitive design
+% TODO: Decimate mem update and downconversion, this will save some more CPU and memory
+% note phase would have to advance 4 times as fast
+
+function [rx_filt fdmdv] = down_convert_and_rx_filter(fdmdv, rx_fdm, nin, dec_rate)
+ Nc = fdmdv.Nc;
+ M = fdmdv.M;
+ P = fdmdv.P;
+ rx_fdm_mem = fdmdv.rx_fdm_mem;
+ Nrx_fdm_mem = fdmdv.Nrx_fdm_mem;
+ phase_rx = fdmdv.phase_rx;
+ freq = fdmdv.freq;
+ freq_pol = fdmdv.freq_pol;
+ Nfilter = fdmdv.Nfilter;
+ gt_alpha5_root = fdmdv.gt_alpha5_root;
+ Q = fdmdv.Q;
+
+ % update memory of rx_fdm_mem, newest nin sample ast end of buffer
+
+ rx_fdm_mem(1:Nrx_fdm_mem-nin) = rx_fdm_mem(nin+1:Nrx_fdm_mem);
+ rx_fdm_mem(Nrx_fdm_mem-nin+1:Nrx_fdm_mem) = rx_fdm(1:nin);
+
+ for c=1:Nc+1
+
+ #{
+ So we have rx_fdm_mem, a baseband array of samples at
+ rate Fs Hz, including the last nin samples at the end. To
+ filter each symbol we require the baseband samples for all Nsym
+ symbols that we filter over. So we need to downconvert the
+ entire rx_fdm_mem array. To downconvert these we need the LO
+ phase referenced to the start of the rx_fdm_mem array.
+
+
+ <--------------- Nrx_filt_mem ------->
+ nin
+ |--------------------------|---------|
+ 1 |
+ phase_rx(c)
+
+ This means winding phase(c) back from this point
+ to ensure phase continuity.
+ #}
+
+ wind_back_phase = -freq_pol(c)*(Nfilter);
+ phase_rx(c) = phase_rx(c)*exp(j*wind_back_phase);
+
+ % down convert all samples in buffer
+
+ rx_baseband = zeros(1,Nrx_fdm_mem);
+
+ st = Nrx_fdm_mem; % end of buffer
+ st -= nin-1; % first new sample
+ st -= Nfilter; % first sample used in filtering
+
+ %printf("Nfilter: %d Nrx_fdm_mem: %d dec_rate: %d nin: %d st: %d\n",
+ % Nfilter, Nrx_fdm_mem, dec_rate, nin, st);
+
+ f_rect = freq(c) .^ dec_rate;
+
+ for i=st:dec_rate:Nrx_fdm_mem
+ phase_rx(c) = phase_rx(c) * f_rect;
+ rx_baseband(i) = rx_fdm_mem(i)*phase_rx(c)';
+ end
+
+ % now we can filter this carrier's P (+/-1) symbols. Due to
+ % filtering of rx_fdm we can filter at rate at rate M/Q
+
+ N=M/P; k = 1;
+ for i=1:N:nin
+ rx_filt(c,k) = (M/Q)*rx_baseband(st+i-1:dec_rate:st+i-1+Nfilter-1) * gt_alpha5_root(1:dec_rate:length(gt_alpha5_root))';
+ k+=1;
+ end
+ end
+
+ fdmdv.phase_rx = phase_rx;
+ fdmdv.rx_fdm_mem = rx_fdm_mem;
+endfunction
+
+
+% LPF and peak pick part of freq est, put in a function as we call it twice
+
+function [foff imax pilot_lpf_out S] = lpf_peak_pick(f, pilot_baseband, pilot_lpf, nin, do_fft)
+ M = f.M;
+ Npilotlpf = f.Npilotlpf;
+ Npilotbaseband = f.Npilotbaseband;
+ Npilotcoeff = f.Npilotcoeff;
+ Fs = f.Fs;
+ Mpilotfft = f.Mpilotfft;
+ pilot_coeff = f.pilot_coeff;
+
+ % LPF cutoff 200Hz, so we can handle max +/- 200 Hz freq offset
+
+ pilot_lpf(1:Npilotlpf-nin) = pilot_lpf(nin+1:Npilotlpf);
+ k = Npilotbaseband-nin+1;;
+ for i = Npilotlpf-nin+1:Npilotlpf
+ pilot_lpf(i) = pilot_baseband(k-Npilotcoeff+1:k) * pilot_coeff';
+ k++;
+ end
+
+ imax = 0;
+ foff = 0;
+ S = zeros(1, Mpilotfft);
+
+ if do_fft
+ % decimate to improve DFT resolution, window and DFT
+
+ Mpilot = Fs/(2*200); % calc decimation rate given new sample rate is twice LPF freq
+ h = hanning(Npilotlpf);
+ s = pilot_lpf(1:Mpilot:Npilotlpf) .* h(1:Mpilot:Npilotlpf)';
+ s = [s zeros(1,Mpilotfft-Npilotlpf/Mpilot)];
+ S = fft(s, Mpilotfft);
+
+ % peak pick and convert to Hz
+
+ [imax ix] = max(abs(S));
+ r = 2*200/Mpilotfft; % maps FFT bin to frequency in Hz
+
+ if ix > Mpilotfft/2
+ foff = (ix - Mpilotfft - 1)*r;
+ else
+ foff = (ix - 1)*r;
+ endif
+ end
+
+ pilot_lpf_out = pilot_lpf;
+
+endfunction
+
+
+% Estimate frequency offset of FDM signal using BPSK pilot. This is quite
+% sensitive to pilot tone level wrt other carriers
+
+function [foff S1 S2 f] = rx_est_freq_offset(f, rx_fdm, pilot, pilot_prev, nin, do_fft)
+ M = f.M;
+ Npilotbaseband = f.Npilotbaseband;
+ pilot_baseband1 = f.pilot_baseband1;
+ pilot_baseband2 = f.pilot_baseband2;
+ pilot_lpf1 = f.pilot_lpf1;
+ pilot_lpf2 = f.pilot_lpf2;
+
+ % down convert latest nin samples of pilot by multiplying by ideal
+ % BPSK pilot signal we have generated locally. The peak of the DFT
+ % of the resulting signal is sensitive to the time shift between the
+ % received and local version of the pilot, so we do it twice at
+ % different time shifts and choose the maximum.
+
+ pilot_baseband1(1:Npilotbaseband-nin) = pilot_baseband1(nin+1:Npilotbaseband);
+ pilot_baseband2(1:Npilotbaseband-nin) = pilot_baseband2(nin+1:Npilotbaseband);
+ for i=1:nin
+ pilot_baseband1(Npilotbaseband-nin+i) = rx_fdm(i) * conj(pilot(i));
+ pilot_baseband2(Npilotbaseband-nin+i) = rx_fdm(i) * conj(pilot_prev(i));
+ end
+
+ [foff1 max1 pilot_lpf1 S1] = lpf_peak_pick(f, pilot_baseband1, pilot_lpf1, nin, do_fft);
+ [foff2 max2 pilot_lpf2 S2] = lpf_peak_pick(f, pilot_baseband2, pilot_lpf2, nin, do_fft);
+
+ if max1 > max2
+ foff = foff1;
+ else
+ foff = foff2;
+ end
+
+ f.pilot_baseband1 = pilot_baseband1;
+ f.pilot_baseband2 = pilot_baseband2;
+ f.pilot_lpf1 = pilot_lpf1;
+ f.pilot_lpf2 = pilot_lpf2;
+endfunction
+
+% Experimental "feed forward" phase estimation function - estimates
+% phase over a windows of Nph (e.g. Nph = 9) symbols. May not work
+% well on HF channels but lets see. Has a phase ambiguity of m(pi/4)
+% where m=0,1,2 which needs to be corrected outside of this function
+
+function [phase_offsets ferr f] = rx_est_phase(f, rx_symbols)
+ global rx_symbols_mem;
+ global prev_phase_offsets;
+ global phase_amb;
+ Nph = f.Nph;
+ Nc = f.Nc;
+
+ % keep record of Nph symbols
+
+ rx_symbols_mem(:,1:Nph-1) = rx_symbols_mem(:,2:Nph);
+ rx_symbols_mem(:,Nph) = rx_symbols;
+
+ % estimate and correct phase offset based of modulation stripped samples
+
+ phase_offsets = zeros(Nc+1,1);
+ for c=1:Nc+1
+
+ % rotate QPSK constellation to a single point
+ mod_stripped = abs(rx_symbols_mem(c,:)) .* exp(j*4*angle(rx_symbols_mem(c,:)));
+
+ % find average phase offset, which will be on -pi/4 .. pi/4
+ sum_real = sum(real(mod_stripped));
+ sum_imag = sum(imag(mod_stripped));
+ phase_offsets(c) = atan2(sum_imag, sum_real)/4;
+
+ % determine if phase has jumped from - -> +
+ if (prev_phase_offsets(c) < -pi/8) && (phase_offsets(c) > pi/8)
+ phase_amb(c) -= pi/2;
+ if (phase_amb(c) < -pi)
+ phase_amb(c) += 2*pi;
+ end
+ end
+
+ % determine if phase has jumped from + -> -
+ if (prev_phase_offsets(c) > pi/8) && (phase_offsets(c) < -pi/8)
+ phase_amb(c) += pi/2;
+ if (phase_amb(c) > pi)
+ phase_amb(c) -= 2*pi;
+ end
+ end
+ end
+
+ ferr = mean(phase_offsets - prev_phase_offsets);
+ prev_phase_offsets = phase_offsets;
+
+endfunction
+
+
+% convert symbols back to an array of bits
+
+function [rx_bits sync_bit f_err phase_difference] = psk_to_bits(f, prev_rx_symbols, rx_symbols, modulation)
+ Nc = f.Nc;
+ Nb = f.Nb;
+
+ m4_binary_to_gray = [
+ bin2dec("00")
+ bin2dec("01")
+ bin2dec("11")
+ bin2dec("10")
+ ];
+
+ m8_binary_to_gray = [
+ bin2dec("000")
+ bin2dec("001")
+ bin2dec("011")
+ bin2dec("010")
+ bin2dec("110")
+ bin2dec("111")
+ bin2dec("101")
+ bin2dec("100")
+ ];
+
+ m = 2 .^ Nb;
+ assert((m == 4) || (m == 8));
+
+ phase_difference = zeros(Nc+1,1);
+ for c=1:Nc
+ norm = 1/(1E-6+abs(prev_rx_symbols(c)));
+ phase_difference(c) = rx_symbols(c) .* conj(prev_rx_symbols(c)) * norm;
+ end
+
+ for c=1:Nc
+ phase_difference(c) *= exp(j*pi/4);
+
+ if m == 4
+
+ % to get a good match between C and Octave during start up use same as C code
+
+ d = phase_difference(c);
+ if (real(d) >= 0) && (imag(d) >= 0)
+ msb = 0; lsb = 0;
+ end
+ if (real(d) < 0) && (imag(d) >= 0)
+ msb = 0; lsb = 1;
+ end
+ if (real(d) < 0) && (imag(d) < 0)
+ msb = 1; lsb = 1;
+ end
+ if (real(d) >= 0) && (imag(d) < 0)
+ msb = 1; lsb = 0;
+ end
+
+ rx_bits(2*(c-1)+1) = msb;
+ rx_bits(2*c) = lsb;
+ else
+ % determine index of constellation point received 0,1,...,m-1
+
+ index = floor(angle(phase_difference(c))*m/(2*pi) + 0.5);
+
+ if index < 0
+ index += m;
+ end
+
+ % map to decimal version of bits encoded in symbol
+
+ if m == 4
+ bits_decimal = m4_binary_to_gray(index+1);
+ else
+ bits_decimal = m8_binary_to_gray(index+1);
+ end
+
+ % convert back to an array of received bits
+
+ for i=1:Nb
+ if bitand(bits_decimal, 2.^(Nb-i))
+ rx_bits((c-1)*Nb+i) = 1;
+ else
+ rx_bits((c-1)*Nb+i) = 0;
+ end
+ end
+ end
+ end
+
+ assert(length(rx_bits) == Nc*Nb);
+
+ % Extract DBPSK encoded Sync bit
+
+ norm = 1/(1E-6+abs(prev_rx_symbols(Nc+1)));
+ phase_difference(Nc+1) = rx_symbols(Nc+1) * conj(prev_rx_symbols(Nc+1)) * norm;
+ if (real(phase_difference(Nc+1)) < 0)
+ sync_bit = 1;
+ f_err = imag(phase_difference(Nc+1))*norm; % make f_err magnitude insensitive
+ else
+ sync_bit = 0;
+ f_err = -imag(phase_difference(Nc+1))*norm;
+ end
+
+ % extra pi/4 rotation as we need for snr_update and scatter diagram
+
+ phase_difference(Nc+1) *= exp(j*pi/4);
+
+endfunction
+
+
+% given phase differences update estimates of signal and noise levels
+
+function [sig_est noise_est] = snr_update(f, sig_est, noise_est, phase_difference)
+ snr_coeff = f.snr_coeff;
+ Nc = f.Nc;
+
+ % mag of each symbol is distance from origin, this gives us a
+ % vector of mags, one for each carrier.
+
+ s = abs(phase_difference);
+
+ % signal mag estimate for each carrier is a smoothed version
+ % of instantaneous magntitude, this gives us a vector of smoothed
+ % mag estimates, one for each carrier.
+
+ sig_est = snr_coeff*sig_est + (1 - snr_coeff)*s;
+
+ %printf("s: %f sig_est: %f snr_coeff: %f\n", s(1), sig_est(1), snr_coeff);
+
+ % noise mag estimate is distance of current symbol from average
+ % location of that symbol. We reflect all symbols into the first
+ % quadrant for convenience.
+
+ refl_symbols = abs(real(phase_difference)) + j*abs(imag(phase_difference));
+ n = abs(exp(j*pi/4)*sig_est - refl_symbols);
+
+ % noise mag estimate for each carrier is a smoothed version of
+ % instantaneous noise mag, this gives us a vector of smoothed
+ % noise power estimates, one for each carrier.
+
+ noise_est = snr_coeff*noise_est + (1 - snr_coeff)*n;
+
+endfunction
+
+
+% calculate current sig estimate for eeach carrier
+
+function snr_dB = calc_snr(f, sig_est, noise_est)
+ Rs = f.Rs;
+
+ % find total signal power by summing power in all carriers
+
+ S = sum(sig_est .^2);
+ SdB = 10*log10(S);
+
+ % Average noise mag across all carriers and square to get an average
+ % noise power. This is an estimate of the noise power in Rs = 50Hz of
+ % BW (note for raised root cosine filters Rs is the noise BW of the
+ % filter)
+
+ N50 = mean(noise_est).^2;
+ N50dB = 10*log10(N50);
+
+ % Now multiply by (3000 Hz)/(50 Hz) to find the total noise power in
+ % 3000 Hz
+
+ N3000dB = N50dB + 10*log10(3000/Rs);
+
+ snr_dB = SdB - N3000dB;
+
+endfunction
+
+
+% sets up test bits system. make sure rand('state', 1) has just been called
+% so we generate the right test_bits pattern!
+
+function f = init_test_bits(f)
+ f.Ntest_bits = f.Nc*f.Nb*4; % length of test sequence
+ f.test_bits = rand(1,f.Ntest_bits) > 0.5; % test pattern of bits
+ f.current_test_bit = 1;
+ f.rx_test_bits_mem = zeros(1,f.Ntest_bits);
+endfunction
+
+
+% returns nbits from a repeating sequence of random data
+
+function [bits f] = get_test_bits(f, nbits)
+
+ for i=1:nbits
+ bits(i) = f.test_bits(f.current_test_bit++);
+
+ if (f.current_test_bit > f.Ntest_bits)
+ f.current_test_bit = 1;
+ endif
+ end
+
+endfunction
+
+
+% Accepts nbits from rx and attempts to sync with test_bits sequence.
+% if sync OK measures bit errors
+
+function [sync bit_errors error_pattern f] = put_test_bits(f, rx_bits)
+ Ntest_bits = f.Ntest_bits;
+
+ % Append to our memory
+
+ [m n] = size(rx_bits);
+ f.rx_test_bits_mem(1:f.Ntest_bits-n) = f.rx_test_bits_mem(n+1:f.Ntest_bits);
+ f.rx_test_bits_mem(f.Ntest_bits-n+1:f.Ntest_bits) = rx_bits;
+
+ % see how many bit errors we get when checked against test sequence
+
+ error_pattern = xor(f.test_bits, f.rx_test_bits_mem);
+ bit_errors = sum(error_pattern);
+
+ % if less than a thresh we are aligned and in sync with test sequence
+
+ ber = bit_errors/f.Ntest_bits;
+
+ sync = 0;
+ if (ber < 0.2)
+ sync = 1;
+ endif
+endfunction
+
+% Generate M samples of DBPSK pilot signal for Freq offset estimation
+
+function [pilot_fdm bit symbol filter_mem phase] = generate_pilot_fdm(f, bit, symbol, filter_mem, phase, freq)
+ M = f.M;
+ Nfilter = f.Nfilter;
+ gt_alpha5_root = f.gt_alpha5_root;
+
+ % +1 -1 +1 -1 DBPSK sync carrier, once filtered becomes two spectral
+ % lines at +/- Rs/2
+
+ if bit
+ symbol = -symbol;
+ else
+ symbol = symbol;
+ end
+ if bit
+ bit = 0;
+ else
+ bit = 1;
+ end
+
+ % filter DPSK symbol to create M baseband samples
+
+ filter_mem(Nfilter) = (sqrt(2)/2)*symbol;
+ for i=1:M
+ tx_baseband(i) = M*filter_mem(M:M:Nfilter) * gt_alpha5_root(M-i+1:M:Nfilter)';
+ end
+ filter_mem(1:Nfilter-M) = filter_mem(M+1:Nfilter);
+ filter_mem(Nfilter-M+1:Nfilter) = zeros(1,M);
+
+ % upconvert
+
+ for i=1:M
+ phase = phase * freq;
+ pilot_fdm(i) = sqrt(2)*2*tx_baseband(i)*phase;
+ end
+
+endfunction
+
+
+% Generate a 4M sample vector of DBPSK pilot signal. As the pilot signal
+% is periodic in 4M samples we can then use this vector as a look up table
+% for pilot signal generation in the demod.
+
+function pilot_lut = generate_pilot_lut(f)
+ Nc = f.Nc;
+ Nfilter = f.Nfilter;
+ M = f.M;
+ freq = f.freq;
+
+ % pilot states
+
+ pilot_rx_bit = 0;
+ pilot_symbol = sqrt(2);
+ pilot_freq = freq(Nc+1);
+ pilot_phase = 1;
+ pilot_filter_mem = zeros(1, Nfilter);
+ %prev_pilot = zeros(M,1);
+
+ pilot_lut = [];
+
+ F=8;
+
+ for fr=1:F
+ [pilot pilot_rx_bit pilot_symbol pilot_filter_mem pilot_phase] = generate_pilot_fdm(f, pilot_rx_bit, pilot_symbol, pilot_filter_mem, pilot_phase, pilot_freq);
+ %prev_pilot = pilot;
+ pilot_lut = [pilot_lut pilot];
+ end
+
+ % discard first 4 symbols as filter memory is filling, just keep last
+ % four symbols
+
+ pilot_lut = pilot_lut(4*M+1:M*F);
+
+endfunction
+
+
+% grab next pilot samples for freq offset estimation at demod
+
+function [pilot prev_pilot pilot_lut_index prev_pilot_lut_index] = get_pilot(f, pilot_lut_index, prev_pilot_lut_index, nin)
+ M = f.M;
+ pilot_lut = f.pilot_lut;
+
+ for i=1:nin
+ pilot(i) = pilot_lut(pilot_lut_index);
+ pilot_lut_index++;
+ if pilot_lut_index > 4*M
+ pilot_lut_index = 1;
+ end
+ prev_pilot(i) = pilot_lut(prev_pilot_lut_index);
+ prev_pilot_lut_index++;
+ if prev_pilot_lut_index > 4*M
+ prev_pilot_lut_index = 1;
+ end
+ end
+endfunction
+
+
+% freq offset state machine. Moves between acquire and track states based
+% on BPSK pilot sequence. Freq offset estimator occasionally makes mistakes
+% when used continuously. So we use it until we have acquired the BPSK pilot,
+% then switch to a more robust tracking algorithm. If we lose sync we switch
+% back to acquire mode for fast-requisition.
+
+function [sync reliable_sync_bit state timer sync_mem] = freq_state(f, sync_bit, state, timer, sync_mem)
+ Nsync_mem = f.Nsync_mem;
+ sync_uw = f.sync_uw;
+
+ % look for 6 symbol (120ms) 010101 of sync sequence
+
+ unique_word = 0;
+ for i=1:Nsync_mem-1
+ sync_mem(i) = sync_mem(i+1);
+ end
+ sync_mem(Nsync_mem) = 1 - 2*sync_bit;
+ corr = 0;
+ for i=1:Nsync_mem
+ corr += sync_mem(i)*sync_uw(i);
+ end
+ if abs(corr) == Nsync_mem
+ unique_word = 1;
+ end
+ reliable_sync_bit = (corr == Nsync_mem);
+
+ % iterate state machine
+
+ next_state = state;
+ if state == 0
+ if unique_word
+ next_state = 1;
+ timer = 0;
+ end
+ end
+ if state == 1
+ if unique_word
+ timer++;
+ if timer == 25 % sync has been good for 500ms
+ next_state = 2;
+ end
+ else
+ next_state = 0;
+ end
+ end
+ if state == 2 % good sync state
+ if unique_word == 0
+ timer = 0;
+ next_state = 3;
+ end
+ end
+ if state == 3 % tentative bad state, but could be a fade
+ if unique_word
+ next_state = 2;
+ else
+ timer++;
+ if timer == 50 % wait for 1000ms in case sync comes back
+ next_state = 0;
+ end
+ end
+ end
+
+ %printf("corr: % -d state: %d next_state: %d uw: %d timer: %d\n", corr, state, next_state, unique_word, timer);
+ state = next_state;
+
+ if state
+ sync = 1;
+ else
+ sync = 0;
+ end
+endfunction
+
+% Save test bits to a text file in the form of a C array
+
+function test_bits_file(filename)
+ global test_bits;
+ global Ntest_bits;
+
+ f=fopen(filename,"wt");
+ fprintf(f,"/* Generated by test_bits_file() Octave function */\n\n");
+ fprintf(f,"const int test_bits[]={\n");
+ for m=1:Ntest_bits-1
+ fprintf(f," %d,\n",test_bits(m));
+ endfor
+ fprintf(f," %d\n};\n",test_bits(Ntest_bits));
+ fclose(f);
+endfunction
+
+
+% Saves RN filter coeffs to a text file in the form of a C array
+
+function rn_file(gt_alpha5_root, filename)
+ Nfilter = length(gt_alpha5_root);
+
+ f=fopen(filename,"wt");
+ fprintf(f,"/* Generated by rn_file() Octave function */\n\n");
+ fprintf(f,"const float gt_alpha5_root[]={\n");
+ for m=1:Nfilter-1
+ fprintf(f," %g,\n",gt_alpha5_root(m));
+ endfor
+ fprintf(f," %g\n};\n",gt_alpha5_root(Nfilter));
+ fclose(f);
+endfunction
+
+
+% Saves rx decimation filter coeffs to a text file in the form of a C array
+
+function rxdec_file(fdmdv, filename)
+ rxdec_coeff = fdmdv.rxdec_coeff;
+ Nrxdec = fdmdv.Nrxdec;
+
+ f=fopen(filename,"wt");
+ fprintf(f,"/* Generated by rxdec_file() Octave function */\n\n");
+ fprintf(f,"const float rxdec_coeff[]={\n");
+ for m=1:Nrxdec-1
+ fprintf(f," %g,\n",rxdec_coeff(m));
+ endfor
+ fprintf(f," %g\n};\n",rxdec_coeff(Nrxdec));
+ fclose(f);
+endfunction
+
+
+function pilot_coeff_file(fdmdv, filename)
+ pilot_coeff = fdmdv.pilot_coeff;
+ Npilotcoeff = fdmdv.Npilotcoeff;
+
+ f=fopen(filename,"wt");
+ fprintf(f,"/* Generated by pilot_coeff_file() Octave function */\n\n");
+ fprintf(f,"const float pilot_coeff[]={\n");
+ for m=1:Npilotcoeff-1
+ fprintf(f," %g,\n",pilot_coeff(m));
+ endfor
+ fprintf(f," %g\n};\n",pilot_coeff(Npilotcoeff));
+ fclose(f);
+endfunction
+
+
+% Saves hanning window coeffs to a text file in the form of a C array
+
+function hanning_file(fdmdv, filename)
+ Npilotlpf = fdmdv.Npilotlpf;
+
+ h = hanning(Npilotlpf);
+
+ f=fopen(filename,"wt");
+ fprintf(f,"/* Generated by hanning_file() Octave function */\n\n");
+ fprintf(f,"const float hanning[]={\n");
+ for m=1:Npilotlpf-1
+ fprintf(f," %g,\n", h(m));
+ endfor
+ fprintf(f," %g\n};\n", h(Npilotlpf));
+ fclose(f);
+endfunction
+
+
+function png_file(fig, pngfilename)
+ figure(fig);
+
+ pngname = sprintf("%s.png",pngfilename);
+ print(pngname, '-dpng', "-S500,500")
+ pngname = sprintf("%s_large.png",pngfilename);
+ print(pngname, '-dpng', "-S800,600")
+endfunction
+
+
+% dump rx_bits in hex
+
+function dump_bits(rx_bits)
+
+ % pack into bytes, MSB first
+
+ packed = zeros(1,floor(length(rx_bits)+7)/8);
+ bit = 7; byte = 1;
+ for i=1:length(rx_bits)
+ packed(byte) = bitor(packed(byte), bitshift(rx_bits(i),bit));
+ bit--;
+ if (bit < 0)
+ bit = 7;
+ byte++;
+ end
+ end
+
+ for i=1:length(packed)
+ printf("0x%02x ", packed(i));
+ end
+ printf("\n");
+
+endfunction
+