aboutsummaryrefslogtreecommitdiff
path: root/octave/ofdm_lib.m
diff options
context:
space:
mode:
Diffstat (limited to 'octave/ofdm_lib.m')
-rw-r--r--octave/ofdm_lib.m1284
1 files changed, 1284 insertions, 0 deletions
diff --git a/octave/ofdm_lib.m b/octave/ofdm_lib.m
new file mode 100644
index 0000000..e4ae35a
--- /dev/null
+++ b/octave/ofdm_lib.m
@@ -0,0 +1,1284 @@
+% ofdm_lib.m
+% David Rowe Mar 2017
+
+#{
+ Library of functions that implement a PSK OFDM modem.
+#}
+
+1;
+qam16;
+esno_est;
+ofdm_mode;
+ofdm_state;
+ofdm_helper;
+
+%-------------------------------------------------------------
+% ofdm_init
+%-------------------------------------------------------------
+
+#{
+
+ Modem frame has a pilot every Ns symbols. There are Ns-1 data
+ symbols between every pilot.
+
+ e.g. for Ns=4, Nc=6:
+
+ |-Nc-| Time
+ DDDDDD |
+ PPPPPPPP --- |
+ DDDDDD | |
+ DDDDDD Ns |
+ DDDDDD | |
+ PPPPPPPP --- \|/
+ DDDDDD | |
+
+ Freq------------------>
+
+ In this figure, time flows down, freq across.
+#}
+
+function states = ofdm_init(config)
+ Rs = config.Rs;
+ Tcp = config.Tcp;
+ Ns = config.Ns;
+ Nc = config.Nc;
+ bps = config.bps;
+ Np = config.Np;
+ Ntxtbits = config.Ntxtbits;
+ Nuwbits = config.Nuwbits;
+ ftwindow_width = config.ftwindow_width;
+ timing_mx_thresh = config.timing_mx_thresh;
+ tx_uw = config.tx_uw;
+ bad_uw_errors = config.bad_uw_errors;
+ amp_scale = config.amp_scale;
+ amp_est_mode = config.amp_est_mode;
+ EsNo_est_all_symbols = config.EsNo_est_all_symbols;
+ EsNodB = config.EsNodB;
+ state_machine = config.state_machine;
+ edge_pilots = config.edge_pilots;
+ clip_gain1 = config.clip_gain1;
+ clip_gain2 = config.clip_gain2;
+ foff_limiter = config.foff_limiter;
+ txbpf_width_Hz = config.txbpf_width_Hz;
+ data_mode = config.data_mode;
+
+ states.Fs = 8000;
+ states.bps = bps;
+ states.Rs = Rs;
+ states.Tcp = Tcp;
+ states.Ns = Ns; % one pilot every Ns symbols, e.g. Ns=4, ...PDDDPDDDP...
+ states.Nc = Nc; % Number of carriers
+ states.M = states.Fs/Rs; % oversampling rate
+ states.Ncp = Tcp*states.Fs;
+ states.Nbitsperframe = (Ns-1)*Nc*bps; % total bits in all data symbols in modem frame
+ states.Nsampersymbol = states.M+states.Ncp; % number of samples in a single symbol
+ states.Nsamperframe = Ns*states.Nsampersymbol; % number of samples in a modem frame
+ states.qam16 = [
+ 1 + j, 1 + j*3, 3 + j, 3 + j*3;
+ 1 - j, 1 - j*3, 3 - j, 3 - j*3;
+ -1 + j, -1 + j*3, -3 + j, -3 + j*3;
+ -1 - j, -1 - j*3, -3 - j, -3 - j*3]/3;
+ rms = sqrt(states.qam16(:)'*states.qam16(:)/16);% set average Es to 1
+ states.qam16 /= rms;
+ states.qam16 *= exp(-j*pi/4); % same rotation as QPSK constellation
+ states.Np = Np; % number of modem frames per packet. In some modes we want
+ % the total packet of data to span multiple modem frames, e.g. HF data
+ % and/or when the FEC codeword is larger than the one
+ % modem frame. In other modes (e.g. 700D/2020) Np=1, ie the modem frame
+ % is the same length as the packet/FEC codeword.
+ states.Nbitsperpacket = Np*states.Nbitsperframe;
+ states.Tpacket = Np*Ns*(Tcp+1/Rs); % time for one packet in ms
+
+ states.Ntxtbits = Ntxtbits; % reserved bits/frame for auxiliary text information. Uncoded/unprotected so may
+ % be of limited use going forward, consider setting to 0
+ states.Nuwbits = Nuwbits;
+
+ % some basic sanity checks
+ assert(floor(states.M) == states.M);
+
+ % UW symbol placement.
+ % Note we need to fill each UW symbols with bits. The LDPC decoder
+ % works on symbols so we can't break up any symbols into UW/FEC
+ % encoded bits.
+
+ states.uw_ind = states.uw_ind_sym = [];
+
+ % lets see if all UW syms will fit in frame
+ Nuwsyms = states.Nuwbits/bps;
+ Ndatasymsperframe = (Ns-1)*Nc;
+ states.spread_uw = 0;
+ if states.spread_uw
+ uw_step = 1.8*floor(states.Nbitsperpacket/states.Nuwbits);
+ else
+ uw_step = Nc+1; % default step for UW sym placement
+ end
+ last_sym = floor(Nuwsyms*uw_step/bps+1);
+ if last_sym > states.Np*Ndatasymsperframe
+ uw_step = Nc-1; % try a different step
+ end
+ last_sym = floor(Nuwsyms*uw_step/bps+1);
+ assert(last_sym <= states.Np*Ndatasymsperframe); % we still can't fit them all
+
+ % Place UW symbols in frame
+ for i=1:Nuwsyms
+ ind_sym = floor(i*uw_step/bps+1);
+ % printf("%d sym: %d\n",i, ind_sym);
+ states.uw_ind_sym = [states.uw_ind_sym ind_sym]; % symbol index
+ for b=bps-1:-1:0
+ states.uw_ind = [states.uw_ind bps*ind_sym-b]; % bit index
+ end
+ end
+
+ % how many of the first few frames have UW symbols in them
+ Nsymsperframe = states.Nbitsperframe/states.bps;
+ states.Nuwframes = ceil(states.uw_ind_sym(end)/Nsymsperframe);
+
+ states.tx_uw = tx_uw;
+ assert(length(states.tx_uw) == states.Nuwbits);
+ tx_uw_syms = [];
+ for b=1:bps:states.Nuwbits
+ if bps == 2 tx_uw_syms = [tx_uw_syms qpsk_mod(states.tx_uw(b:b+1))]; end
+ if bps == 4 tx_uw_syms = [tx_uw_syms qam16_mod(states.qam16, states.tx_uw(b:b+bps-1))]; end
+ end
+ states.tx_uw_syms = tx_uw_syms;
+
+ % if the UW has this many errors it is "bad", the binomal cdf can be used to
+ % set this with the ofdm_determine_bad_uw_errors() function below
+ %
+ % Nuw=12; plot(0:Nuw, binocdf(0:Nuw,Nuw,0.05)); hold on; plot(binocdf(0:Nuw,Nuw,0.5)); hold off;
+ states.bad_uw_errors = bad_uw_errors;
+
+ states.ofdm_peak = 16384;
+ % use this to scale tx output to 16 bit short to a peak value of 16384. Adjusted by experiment
+ states.amp_scale = amp_scale;
+ % when using the clipping, this is the manual gain value. Adjusted by experiment, trade off between
+ % increased average power and BER
+ states.clip_gain1 = clip_gain1;
+ states.clip_gain2 = clip_gain2;
+ states.txbpf_width_Hz = txbpf_width_Hz;
+
+ % this is used to scale inputs to LDPC decoder to make it amplitude indep
+ states.mean_amp = 0;
+
+ % use a fixed EsNo for LDPC decoder, this seems to work OK and avoid another estimator
+ states.EsNodB = EsNodB;
+
+ % generate same BPSK pilots each time
+ rand('seed',1);
+ states.pilots = 1 - 2*(rand(1,Nc+2) > 0.5);
+ %printf("number of pilots total: %d\n", length(states.pilots));
+
+ % If set, place pilots at carrier 1 and Nc+2 to support low bandwidth phase est over grid
+ % of 12 pilot_samples. Used for 700D and 2020
+ states.edge_pilots = edge_pilots;
+ if states.edge_pilots == 0
+ states.pilots(1) = 0;
+ states.pilots(Nc+2) = 0;
+ end
+
+ % carrier tables for up and down conversion
+ states.fcentre = fcentre = 1500;
+ alower = fcentre - Rs * (Nc/2); % approx frequency of lowest carrier
+ Nlower = round(alower / Rs) - 1; % round this to nearest integer multiple from 0Hz to keep DFT happy
+ %printf(" fcentre: %f alower: %f alower/Rs: %f Nlower: %d\n", fcentre, alower, alower/Rs, Nlower);
+ w = (Nlower:Nlower+Nc+1)*2*pi/(states.Fs/Rs);
+ W = zeros(Nc+2,states.M);
+ for c=1:Nc+2
+ W(c,:) = exp(j*w(c)*(0:states.M-1));
+ end
+ states.w = w;
+ states.W = W;
+
+ % fine timing search +/- window_width/2 from current timing instant,
+ % set this to roughly twice the maximum delay spread
+ states.ftwindow_width = ftwindow_width;
+
+ % magic number we adjust by experiment (see ofdm_dev.m acquisition tests, blog post on 700D sync)
+ states.timing_mx_thresh = timing_mx_thresh;
+
+ % Receive buffer: rxbufst + D P DDD P DDD P DDD P D
+ % ^
+ % nominal start of current modem frame
+
+ if length(data_mode)
+ Nrxbufhistory = (states.Np+2)*states.Nsamperframe; % extra storage at start of rxbuf to allow us to step back in time
+ else
+ Nrxbufhistory = 0;
+ end
+ states.rxbufst = Nrxbufhistory; % start of rxbuf window used for demod of current rx frame
+ states.Nrxbufhistory = Nrxbufhistory;
+
+ % D P DDD P DDD P DDD P D
+ states.Nrxbufmin = states.Nsampersymbol + 3*states.Nsamperframe + states.Nsampersymbol + states.Nsampersymbol;
+ states.Nrxbuf = Nrxbufhistory + states.Nrxbufmin;
+ states.rxbuf = zeros(1, states.Nrxbuf);
+
+ % default settings on a bunch of options and states
+
+ states.verbose = 0;
+ states.timing_en = 1;
+ states.foff_est_en = 1;
+ states.phase_est_en = 1;
+ states.phase_est_bandwidth = "high";
+ states.dpsk = 0;
+ states.amp_est_mode = amp_est_mode;
+
+ states.foff_est_gain = 0.1;
+ states.foff_limiter = foff_limiter;
+ states.foff_est_hz = 0;
+ states.sample_point = states.timing_est = 1;
+ states.nin = states.Nsamperframe;
+ states.timing_valid = 0;
+ states.timing_mx = 0;
+ states.coarse_foff_est_hz = 0;
+
+ states.foff_metric = 0;
+
+ % generate OFDM pilot symbol, used for timing and freq offset est
+
+ rate_fs_pilot_samples = states.pilots * W/states.M;
+
+ % During tuning it was found that not including the cyc prefix in
+ % rate_fs_pilot_samples produced better fest results
+
+ %states.rate_fs_pilot_samples = [rate_fs_pilot_samples(states.M-states.Ncp+1:states.M) rate_fs_pilot_samples];
+ states.rate_fs_pilot_samples = [zeros(1,states.Ncp) rate_fs_pilot_samples];
+
+ % pre-compute a constant used to detect valid modem frames
+
+ Npsam = length(states.rate_fs_pilot_samples);
+ states.timing_norm = Npsam*(states.rate_fs_pilot_samples * states.rate_fs_pilot_samples');
+ % printf("timing_norm: %f\n", states.timing_norm)
+
+ % sync state machine
+
+ states.sync_state = states.last_sync_state = 'search';
+ states.uw_errors = 0;
+ states.sync_counter = 0;
+ states.frame_count = 0; % number of frames we have been in sync
+ states.sync_start = 0;
+ states.sync_end = 0;
+ states.modem_frame = 0; % keep track of how many frames received in packet
+ states.state_machine = state_machine; % mode specific state machine
+ states.packetsperburst = 0; % for OFDM data modes, how many packets before we reset state machine
+ states.postambledetectoren = strcmp(data_mode,"burst");
+ states.npre = states.npost = 0; % counters for logging
+
+ % LDPC code is optionally enabled
+
+ states.rate = 1.0;
+ states.ldpc_en = 0;
+
+ % init some output states for logging
+
+ states.rx_sym = zeros(1+Ns+1+1, Nc+2);
+
+ % Es/No (SNR) est states
+
+ states.EsNo_est_all_symbols = EsNo_est_all_symbols;
+ states.clock_offset_est = 0;
+
+ % pre-amble for data modes
+ states.data_mode = data_mode;
+ if length(states.data_mode)
+ states.tx_preamble = ofdm_generate_preamble(states, 2);
+ states.tx_postamble = ofdm_generate_preamble(states, 3);
+ end
+
+ % automated tests
+ test_qam16_mod_demod(states.qam16);
+ test_assemble_disassemble(states);
+endfunction
+
+% Gray coded QPSK modulation function
+function symbol = qpsk_mod(two_bits)
+ two_bits_decimal = sum(two_bits .* [2 1]);
+ switch(two_bits_decimal)
+ case (0) symbol = 1;
+ case (1) symbol = j;
+ case (2) symbol = -j;
+ case (3) symbol = -1;
+ endswitch
+endfunction
+
+% Gray coded QPSK demodulation function
+function two_bits = qpsk_demod(symbol)
+ bit0 = real(symbol*exp(j*pi/4)) < 0;
+ bit1 = imag(symbol*exp(j*pi/4)) < 0;
+ two_bits = [bit1 bit0];
+endfunction
+
+
+function out = freq_shift(in, foff, Fs)
+ foff_rect = exp(j*2*pi*foff/Fs);
+ foff_phase_rect = exp(j*0);
+
+ for r=1:length(in)
+ foff_phase_rect *= foff_rect;
+ out(r) = in(r)*foff_phase_rect;
+ end
+endfunction
+
+
+% -----------------------------------------------------------------
+% ofdm_mod - modulates a complete packet (one or more modem frames)
+% ----------------------------------------------------------------
+
+function tx = ofdm_mod(states, tx_bits)
+ ofdm_load_const;
+ assert(length(tx_bits) == Nbitsperpacket);
+
+ % map to symbols in linear array
+
+ if bps == 1
+ tx_sym_lin = 2*tx_bits - 1;
+ end
+ if bps == 2
+ for s=1:Nbitsperpacket/bps
+ tx_sym_lin(s) = qpsk_mod(tx_bits(2*(s-1)+1:2*s));
+ end
+ end
+ if bps == 4
+ for s=1:Nbitsperpacket/bps
+ tx_sym_lin(s) = qam16_mod(states.qam16,tx_bits(4*(s-1)+1:4*s));
+ end
+ end
+
+ tx = ofdm_txframe(states, tx_sym_lin);
+endfunction
+
+
+% ----------------------------------------------
+% ofdm_txframe - modulates one packet of symbols
+% ----------------------------------------------
+
+function tx = ofdm_txframe(states, tx_sym_lin)
+ ofdm_load_const;
+ assert(length(tx_sym_lin) == Nbitsperpacket/bps);
+
+ % place data symbols in multi-carrier frame with pilots and boundary carriers
+
+ s = 1; tx_frame = zeros(Np*Ns,Nc+2);
+ for r=1:Np*Ns
+ if mod(r-1,Ns) == 0
+ % row of pilots
+ tx_frame(r,:) = pilots;
+ else
+ % row of data symbols
+ arowofsymbols = tx_sym_lin(s:s+Nc-1);
+ tx_frame(r,2:Nc+1) = arowofsymbols;
+ s += Nc;
+ if states.dpsk
+ tx_frame(r,2:Nc+1) = tx_frame(r,2:Nc+1) .* tx_frame(r-1,2:Nc+1);
+ end
+ end
+ end
+ % make sure we use all the symbols
+ assert((s-1) == length(tx_sym_lin));
+
+ % OFDM upconvert symbol by symbol so we can add CP
+
+ tx = [];
+ for r=1:Ns*Np
+ asymbol = tx_frame(r,:) * W/M;
+ asymbol_cp = [asymbol(M-Ncp+1:M) asymbol];
+ tx = [tx asymbol_cp];
+ end
+endfunction
+
+
+% -----------------------------------------------------------
+% est_timing
+% -----------------------------------------------------------
+
+#{
+ Correlates known samples (for example pilots or a preamble) with a window of received
+ samples to determine the most likely timing offset. Optionally combines
+ known samples from two frames (e.g. pilots at start of this and next frame)
+ so we need at least Nsamperframe+M+Ncp samples in rx.
+
+ Can be used for acquisition (coarse timing), and fine timing. Tends
+ to break down when freq offset approaches +/- symbol rate (e.g +/-
+ 25 Hz for 700D).
+#}
+
+function [t_est timing_valid timing_mx av_level] = est_timing(states, rx, known_samples, step, dual=1)
+ ofdm_load_const;
+ Npsam = length(known_samples);
+
+ Ncorr = length(rx) - (Nsamperframe+Npsam);
+ corr = zeros(1,Ncorr);
+ %printf("Npsam: %d M+Ncp: %d Ncorr: %d Nsamperframe: %d step: %d\n", Npsam, M+Ncp, Ncorr, Nsamperframe, step);
+
+ % normalise correlation so we can compare to a threshold across varying input levels
+
+ av_level = 2*sqrt(states.timing_norm*(rx*rx')/length(rx)) + 1E-12;
+
+ % correlate with pilots at start and (optionally) end of frame to determine timing offset
+
+ for i=1:step:Ncorr
+ rx1 = rx(i:i+Npsam-1);
+ corr_st = rx1 * known_samples';
+ corr_en = 0;
+ if dual
+ % for the streaming voice modes we also correlate with pilot samples at start of next frame
+ rx2 = rx(i+Nsamperframe:i+Nsamperframe+Npsam-1);
+ corr_en = rx2 * known_samples';
+ end
+ corr(i) = (abs(corr_st) + abs(corr_en))/av_level;
+ end
+
+ [timing_mx t_est] = max(abs(corr));
+ % only declare timing valid if there are enough samples in rxbuf to demodulate a frame
+ timing_valid = (abs(rx(t_est)) > 0) && (timing_mx > timing_mx_thresh);
+
+ if verbose > 1
+ printf(" av_level: %5.4f mx: %4.3f timing_est: %4d timing_valid: %d\n", av_level, timing_mx, t_est, timing_valid);
+ end
+ if verbose > 2
+ figure(10); clf;
+ subplot(211); plot(rx)
+ subplot(212); plot(corr)
+ figure(11); clf; plot(real(known_samples));
+ end
+
+endfunction
+
+
+% -----------------------------------------------------------
+% est_freq_offset_known_corr
+% -----------------------------------------------------------
+
+#{
+ Determines frequency offset at current timing estimate, used for
+ coarse freq offset estimation during streaming mode acquisition.
+#}
+
+function foff_est = est_freq_offset_known_corr(states, rx, known_samples, t_est, dual=1)
+ ofdm_load_const;
+ Npsam = length(known_samples);
+
+ % extract pilot samples from either end of frame
+ rx1 = rx(t_est:t_est+Npsam-1); rx2 = rx(t_est+Nsamperframe:t_est+Nsamperframe+Npsam-1);
+
+ % "mix" these down (correlate) with 0 Hz offset pilot samples
+ corr_st = rx1 .* conj(known_samples);
+ if dual
+ corr_en = rx2 .* conj(known_samples);
+ end
+
+ % sample sum of DFT magnitude of correlated signals at each freq offset and look for peak
+ st = -20; en = 20; foff_est = 0; Cabs_max = 0;
+
+ for f=st:en
+ w = 2*pi*f/Fs;
+ C_st = corr_st * exp(j*w*(0:Npsam-1))';
+ C_en = 0;
+ if dual
+ C_en = corr_en * exp(j*w*(0:Npsam-1))';
+ end
+ Cabs = abs(C_st) + abs(C_en);
+ %printf("f: %4.1f Cabs: %f Cmax: %f\n", f, Cabs, Cabs_max);
+ if Cabs > Cabs_max
+ Cabs_max = Cabs;
+ foff_est = f;
+ end
+ end
+
+ if states.verbose > 1
+ printf(" foff_est: %f\n", foff_est);
+ end
+
+endfunction
+
+
+% Joint estimation used for data mode burst acquistion
+
+function [t_est foff_est timing_mx] = est_timing_and_freq(states, rx, known_samples, tstep, fmin, fmax, fstep)
+ ofdm_load_const;
+ Npsam = length(known_samples);
+
+ Ncorr = length(rx) - Npsam + 1;
+ corr = zeros(1,Ncorr);
+
+ % set up matrix of freq shifted known samples for correlation with received signal. Each row
+ % is the known samples shifted by a different freq offset
+
+ M = [];
+ for afcoarse=fmin:fstep:fmax
+ w = 2*pi*afcoarse/Fs;
+ wvec = exp(j*w*(0:Npsam-1));
+ M = [M; known_samples .* wvec];
+ end
+
+ % At each timing position, correlate with known samples at all possible freq offsets. Result
+ % is a column vector for each timing offset. Each matrix cell is a freq,timing coordinate
+
+ corr = [];
+ for t=1:tstep:Ncorr
+ rx1 = rx(t:t+Npsam-1);
+ col = M * rx1';
+ corr = [corr, col];
+ end
+
+ % best timing offset is the col with the global max of the corr matrix
+ max_col = max(abs(corr));
+ [mx mx_col] = max(max_col);
+ t_est = (mx_col-1)*tstep;
+
+ % obtain normalised real number for timing mx
+ mag1 = known_samples*known_samples';
+ mag2 = rx(t_est+1:t_est+Npsam)*rx(t_est+1:t_est+Npsam)';
+ timing_mx = mx*mx'/(mag1*mag2+1E-12);
+
+ % determine frequency offset for row where max is located
+ [tmp freq_row] = max(corr(:,mx_col));
+ foff_est = fmin + fstep*(freq_row-1);
+
+ if verbose > 1
+ printf(" t_est: %d timing:mx: %f foff_est: %f\n", t_est, timing_mx, foff_est);
+ end
+ if verbose > 2
+ figure(10); clf;
+ subplot(211); plot(rx)
+ subplot(212); plot(corr)
+ figure(11); clf; plot(real(known_samples));
+ end
+
+endfunction
+
+
+% streaming mode acquistion, used mainly for voice modes
+
+function [timing_valid states] = ofdm_sync_search_stream(states)
+ ofdm_load_const;
+
+ st = rxbufst + M+Ncp + Nsamperframe + 1; en = st + 2*Nsamperframe + M+Ncp - 1;
+
+ % Attempt coarse timing estimate (i.e. detect start of frame) at a range of frequency offsets
+
+ timing_mx = 0; fcoarse = 0; timing_valid = 0; ct_est = 1;
+ for afcoarse=-40:40:40
+ % vector of local oscillator samples to shift input vector
+ % these could be computed on the fly to save memory, or pre-computed in flash at tables as they are static
+
+ if afcoarse != 0
+ w = 2*pi*afcoarse/Fs;
+ wvec = exp(-j*w*(0:2*Nsamperframe+M+Ncp-1));
+
+ % choose best timing offset metric at this freq offset
+ [act_est atiming_valid atiming_mx] = est_timing(states, wvec .* states.rxbuf(st:en), states.rate_fs_pilot_samples, 2);
+ else
+ % exp(-j*0) is just 1 when afcoarse is 0
+ [act_est atiming_valid atiming_mx] = est_timing(states, states.rxbuf(st:en), states.rate_fs_pilot_samples, 2);
+ end
+
+ %printf("afcoarse: %f atiming_mx: %f\n", afcoarse, atiming_mx);
+
+ if atiming_mx > timing_mx
+ ct_est = act_est;
+ timing_valid = atiming_valid;
+ timing_mx = atiming_mx;
+ fcoarse = afcoarse;
+ end
+ end
+
+ % refine freq est within -/+ 20 Hz window
+
+ if fcoarse != 0
+ w = 2*pi*fcoarse/Fs;
+ wvec = exp(-j*w*(0:2*Nsamperframe+M+Ncp-1));
+ foff_est = est_freq_offset_known_corr(states, wvec .* states.rxbuf(st:en), states.rate_fs_pilot_samples, ct_est);
+ foff_est += fcoarse;
+ else
+ % exp(-j*0) is just 1 when fcoarse is 0
+ foff_est = est_freq_offset_known_corr(states, states.rxbuf(st:en), states.rate_fs_pilot_samples, ct_est);
+ end
+
+ if verbose
+ printf(" ct_est: %4d mx: %3.2f coarse_foff: %5.1f timing_valid: %d", ct_est, timing_mx, foff_est, timing_valid);
+ end
+
+ if timing_valid
+ states.nin = ct_est - 1;
+ else
+ states.nin = Nsamperframe;
+ end
+
+ states.timing_valid = timing_valid;
+ states.timing_mx = timing_mx;
+ states.coarse_foff_est_hz = foff_est;
+ states.sample_point = states.timing_est = 1;
+endfunction
+
+
+% two stage acquisition detector for burst mode
+
+function results = burst_acquisition_detector(states, rx, n, known_sequence)
+ ofdm_load_const;
+
+ % initial search over coarse grid
+ tstep = 4; fstep = 5;
+ [ct_est foff_est timing_mx] = est_timing_and_freq(states, rx(n:n+2*Nsamperframe-1), known_sequence,
+ tstep, fmin = -50, fmax = 50, fstep);
+ % refine estimate over finer grid
+ fmin = foff_est - ceil(fstep/2); fmax = foff_est + ceil(fstep/2);
+ fine_st = max(1, n + ct_est - tstep/2); fine_en = fine_st + Nsamperframe + tstep - 1;
+ [ct_est foff_est timing_mx] = est_timing_and_freq(states, rx(fine_st:fine_en), known_sequence, 1, fmin, fmax, 1);
+ % refer ct_est to nominal start of frame rx_buf(n)
+ ct_est += fine_st - n;
+ results.ct_est = ct_est; results.foff_est = foff_est; results.timing_mx = timing_mx;
+end
+
+
+% Burst mode acquisition ------------------------------------------
+
+function [timing_valid states] = ofdm_sync_search_burst(states)
+ ofdm_load_const;
+
+ pre_post = "";
+ st = rxbufst + M+Ncp + Nsamperframe + 1; en = st + 2*Nsamperframe - 1;
+ pre = burst_acquisition_detector(states, states.rxbuf, st, states.tx_preamble);
+ if states.postambledetectoren
+ post = burst_acquisition_detector(states, states.rxbuf, st, states.tx_postamble);
+ end
+
+ if isfield(states,"postambletest") pre.timing_mx = 0; end % force ignore preamble to test postamble
+
+ if (states.postambledetectoren == 0) || (pre.timing_mx > post.timing_mx)
+ timing_mx = pre.timing_mx; ct_est = pre.ct_est; foff_est = pre.foff_est;
+ pre_post = "pre";
+ else
+ timing_mx = post.timing_mx; ct_est = post.ct_est; foff_est = post.foff_est;
+ pre_post = "post";
+ end
+ timing_valid = timing_mx > timing_mx_thresh;
+
+ if timing_valid
+ % potential candidate found ....
+
+ % calculate number of samples we need on next buffer to get into sync
+ if strcmp(pre_post, "post")
+ states.nin = 0;
+ % printf("\n rxbufst: %d ", states.rxbufst);
+ states.rxbufst -= states.Np*states.Nsamperframe; % backup to first modem frame in packet
+ states.rxbufst += ct_est - 1;
+ states.npost++;
+ % printf("%d\n", states.rxbufst);
+ else
+ % ct_est is start of preamble, so advance past that to start of first modem frame
+ states.nin = Nsamperframe + ct_est - 1;
+ states.npre++;
+ end
+ else
+ states.nin = Nsamperframe;
+ end
+
+ states.ct_est = ct_est;
+ states.timing_valid = timing_valid;
+ states.timing_mx = timing_mx;
+ states.sample_point = states.timing_est = 1;
+ states.foff_est_hz = foff_est;
+
+ if verbose
+ printf(" ct_est: %4d nin: %4d mx: %3.2f foff_est: %5.1f timing_valid: %d %4s",
+ ct_est, states.nin, timing_mx, foff_est, timing_valid, pre_post);
+ end
+endfunction
+
+
+% ----------------------------------------------------------------------------------
+% ofdm_sync_search - attempts to find coarse sync parameters for modem initial sync
+% ----------------------------------------------------------------------------------
+
+function [timing_valid states] = ofdm_sync_search(states, rxbuf_in)
+ ofdm_load_const;
+
+ % update rxbuf so it is primed for when we have to call ofdm_demod()
+
+ states.rxbuf(1:Nrxbuf-states.nin) = states.rxbuf(states.nin+1:Nrxbuf);
+ states.rxbuf(Nrxbuf-states.nin+1:Nrxbuf) = rxbuf_in;
+
+ if strcmp(states.data_mode, "burst")
+ [timing_valid states] = ofdm_sync_search_burst(states);
+ else
+ [timing_valid states] = ofdm_sync_search_stream(states);
+ end
+endfunction
+
+
+% ------------------------------------------
+% ofdm_demod - Demodulates one frame of bits
+% ------------------------------------------
+
+#{
+
+ For phase estimation we need to maintain buffer of 3 frames plus
+ one pilot, so we have 4 pilots total. '^' is the start of current
+ frame that we are demodulating.
+
+ P DDD P DDD P DDD P
+ ^
+
+ Then add one symbol either side to account for movement in
+ sampling instant due to sample clock differences:
+
+ D P DDD P DDD P DDD P D
+ ^
+
+ Returns:
+ rx_bits - (hard decoded/raw/uncoded) demodulated data bits from packet
+ aphase_est - phase est for each data symbol
+ rx_np - output data symbols after phase correction
+ rx_amp - amplitude estimates for each symbol
+#}
+
+function [states rx_bits achannel_est_rect_log rx_np rx_amp] = ofdm_demod(states, rxbuf_in)
+ ofdm_load_const;
+
+ % insert latest input samples into rxbuf
+
+ rxbuf(1:Nrxbuf-states.nin) = rxbuf(states.nin+1:Nrxbuf);
+ rxbuf(Nrxbuf-states.nin+1:Nrxbuf) = rxbuf_in;
+
+ % get latest freq offset estimate
+
+ woff_est = 2*pi*foff_est_hz/Fs;
+
+ % update timing estimate --------------------------------------------------
+
+ delta_t = coarse_foff_est_hz = timing_valid = timing_mx = 0;
+ if timing_en
+ % update timing at start of every frame
+
+ % search for timing in a window centered on timing_est, the window will typically be around 2Ncp wide as we could
+ % get a shift of +Ncp or -Ncp if we swing from one delay extreme to another
+ st = rxbufst + M+Ncp + Nsamperframe + 1 - floor(ftwindow_width/2) + (timing_est-1);
+ en = st + Nsamperframe-1 + M+Ncp + ftwindow_width-1;
+
+ [ft_est timing_valid timing_mx] = est_timing(states, rxbuf(st:en) .* exp(-j*woff_est*(st:en)), rate_fs_pilot_samples, 1);
+ % printf(" timing_est: %d ft_est: %d timing_valid: %d timing_mx: %d\n", timing_est, ft_est, timing_valid, timing_mx);
+
+ % if we are in a deep fade timing_valid will not be asserted as ft_est will be garbage, so we don't
+ % adjust timing est, just freewheel for now
+ if timing_valid
+
+ % adjust timing_est based on ft_est
+ timing_est = timing_est + ft_est - ceil(ftwindow_width/2);
+
+ % Track the ideal sampling point, which is Ncp for a multipath signal whose delay varies between 0 and Ncp. The
+ % timing est will be bouncing back and forth due to multipath so we may need to use the upper or lower limit of
+ % the timing est to track the ideal sample_point. A good way to explore this algorithm is to disable the feedback
+ % loop for nin adjustment below, and look at the plots from ofdm_rx with +ve and -ve sample clock offsets
+ % (sox can be used to resample). The "4" constants are small guard bands so we don't stumble outside of the CP
+ % due to noise.
+
+ delta_t = ft_est - ceil(ftwindow_width/2); % just used for plotting
+ sample_point = max(timing_est+4, sample_point); % we are at max timing est, so sample point just above
+ sample_point = min(timing_est+Ncp-4, sample_point); % we are at min timing_est, so sample point Ncp above
+ end
+
+ if verbose > 1
+ printf(" ft_est: %2d mx: %3.2f coarse_foff: %4.1f foff: %4.1f\n", ft_est, timing_mx, coarse_foff_est_hz, foff_est_hz);
+ end
+
+ end
+
+ % down convert at current timing instant----------------------------------
+
+ rx_sym = zeros(1+Ns+1+1, Nc+2);
+
+ % previous pilot
+
+ st = rxbufst + M+Ncp + Nsamperframe + (-Ns)*(M+Ncp) + 1 + sample_point; en = st + M - 1;
+
+ for c=1:Nc+2
+ acarrier = rxbuf(st:en) .* exp(-j*woff_est*(st:en)) .* conj(W(c,:));
+ rx_sym(1,c) = sum(acarrier);
+ end
+
+ % pilot - this frame - pilot
+
+ for rr=1:Ns+1
+ st = rxbufst + M+Ncp + Nsamperframe + (rr-1)*(M+Ncp) + 1 + sample_point; en = st + M - 1;
+ for c=1:Nc+2
+ acarrier = rxbuf(st:en) .* exp(-j*woff_est*(st:en)) .* conj(W(c,:));
+ rx_sym(rr+1,c) = sum(acarrier);
+ end
+ end
+
+ % next pilot
+
+ st = rxbufst + M+Ncp + Nsamperframe + (2*Ns)*(M+Ncp) + 1 + sample_point; en = st + M - 1;
+ for c=1:Nc+2
+ acarrier = rxbuf(st:en) .* exp(-j*woff_est*(st:en)) .* conj(W(c,:));
+ rx_sym(Ns+3,c) = sum(acarrier);
+ end
+
+ % est freq err based on all carriers ------------------------------------
+
+ if foff_est_en
+ freq_err_rect = sum(rx_sym(2,:))' * sum(rx_sym(2+Ns,:));
+
+ % prevent instability in atan(im/re) when real part near 0
+
+ freq_err_rect += 1E-6;
+
+ %printf("freq_err_rect: %f %f angle: %f\n", real(freq_err_rect), imag(freq_err_rect), angle(freq_err_rect));
+ freq_err_hz = angle(freq_err_rect)*Rs/(2*pi*Ns);
+ if states.foff_limiter
+ freq_err_hz = max(freq_err_hz,-1);
+ freq_err_hz = min(freq_err_hz, 1);
+ end
+ foff_est_hz = foff_est_hz + foff_est_gain*freq_err_hz;
+ end
+
+ % OK - now channel for each carrier and correct phase ----------------------------------
+
+ achannel_est_rect = zeros(1,Nc+2);
+ aamp_est_pilot = zeros(1,Nc+2);
+ for c=2:Nc+1
+
+ % estimate channel for this carrier using an average of 12 pilots
+ % in a rect 2D window centred on this carrier
+
+ % PPP <-- frame-1
+ % ---
+ % PPP <-- you are here
+ % DDD
+ % DDD
+ % PPP <-- frame+1
+ % ---
+ % PPP <-- frame+2
+
+ if isfield(states, "phase_est_bandwidth")
+ phase_est_bandwidth = states.phase_est_bandwidth;
+ else
+ phase_est_bandwidth = "low";
+ end
+
+ if strcmp(phase_est_bandwidth, "high")
+ % Only use pilots at start and end of this frame to track quickly changes in phase
+ % present. Useful for initial sync where freq offset est may be a bit off, and
+ % for high Doppler channels. As less pilots are averaged, low SNR performance
+ % will be poorer.
+ achannel_est_rect(c) = rx_sym(2,c)*pilots(c)'; % frame
+ achannel_est_rect(c) += rx_sym(2+Ns,c)*pilots(c)'; % frame+1
+ aamp_est_pilot(c) = abs(rx_sym(2,c)) + abs(rx_sym(2+Ns,c));
+ elseif strcmp(phase_est_bandwidth, "low")
+ % Average over a bunch of pilots in adjacent carriers, and past and future frames, good
+ % low SNR performance, but will fall over with high Doppler or freq offset.
+ cr = c-1:c+1;
+ achannel_est_rect(c) = rx_sym(2,cr)*pilots(cr)'; % frame
+ achannel_est_rect(c) += rx_sym(2+Ns,cr)*pilots(cr)'; % frame+1
+ aamp_est_pilot(c) = sum(abs(rx_sym(2,cr)));
+ aamp_est_pilot(c) += sum(abs(rx_sym(2+Ns,cr)));
+
+ % use next step of pilots in past and future
+
+ achannel_est_rect(c) += rx_sym(1,cr)*pilots(cr)'; % frame-1
+ achannel_est_rect(c) += rx_sym(2+Ns+1,cr)*pilots(cr)'; % frame+2
+ aamp_est_pilot(c) += sum(abs(rx_sym(1,cr)));
+ aamp_est_pilot(c) += sum(abs(rx_sym(2+Ns+1,cr)));
+ end
+ end
+
+ % pilots are estimated over 12 pilot symbols, so find average
+
+ if strcmp(phase_est_bandwidth, "high")
+ achannel_est_rect /= 2;
+ aamp_est_pilot /= 2;
+ elseif strcmp(phase_est_bandwidth, "low")
+ achannel_est_rect /= 12;
+ aamp_est_pilot /= 12;
+ end
+
+ aphase_est_pilot = angle(achannel_est_rect);
+ if states.amp_est_mode == 0
+ % legacy 700D/2020 ampl estimator for compatibility with current C code
+ aamp_est_pilot = abs(achannel_est_rect);
+ end
+ achannel_est_rect = aamp_est_pilot.*exp(j*aphase_est_pilot);
+
+ % correct phase offset using phase estimate, and demodulate
+ % bits, separate loop as it runs across cols (carriers) to get
+ % frame bit ordering correct
+
+ rx_bits = []; rx_np = []; rx_amp = []; achannel_est_rect_log = [];
+ for rr=1:Ns-1
+ for c=2:Nc+1
+ if phase_est_en
+ if states.dpsk
+ rx_corr = rx_sym(rr+2,c) * rx_sym(rr+1,c)';
+ else
+ rx_corr = rx_sym(rr+2,c) * exp(-j*aphase_est_pilot(c));
+ end
+ else
+ rx_corr = rx_sym(rr+2,c);
+ end
+
+ rx_np = [rx_np rx_corr];
+ rx_amp = [rx_amp aamp_est_pilot(c)];
+
+ % hard decision demod
+ if bps == 1 abit = real(rx_corr) > 0; end
+ if bps == 2 abit = qpsk_demod(rx_corr); end
+ if bps == 4 abit = qam16_demod(states.qam16, rx_corr, max(1E-12,aamp_est_pilot(c))); end
+ rx_bits = [rx_bits abit];
+ end % c=2:Nc+1
+ achannel_est_rect_log = [achannel_est_rect_log; achannel_est_rect(2:Nc+1)];
+ end
+
+ % Adjust nin to take care of sample clock offset. When debugong or exploring how timing loop works
+ % it's a good idea to comment out this code to "open the loop".
+
+ nin = Nsamperframe;
+ if timing_en && timing_valid
+ states.clock_offset_est = 0.9*states.clock_offset_est + 0.1*abs(states.timing_est - timing_est)/Nsamperframe;
+ thresh = (M+Ncp)/8;
+ tshift = (M+Ncp)/4;
+ if timing_est > thresh
+ nin = Nsamperframe+tshift;
+ timing_est -= tshift;
+ sample_point -= tshift;
+ end
+ if timing_est < -thresh
+ nin = Nsamperframe-tshift;
+ timing_est += tshift;
+ sample_point += tshift;
+ end
+ end
+
+ % use internal rxbuf samples if they are available
+ rxbufst_next = rxbufst + nin;
+ %printf("\nrxbufst: %d rxbufst_next: %d nin: %d Nrxbufmin: %d rqd: %d Nrxbuf: %d\n",
+ % rxbufst, rxbufst_next, nin, Nrxbufmin, rxbufst_next + Nrxbufmin, Nrxbuf);
+ if rxbufst_next + Nrxbufmin <= Nrxbuf
+ % printf("Can maybe use rxbufst!\n");
+ rxbufst = rxbufst_next;
+ nin = 0;
+ end
+
+ % maintain mean amp estimate for LDPC decoder
+ states.mean_amp = 0.9*states.mean_amp + 0.1*mean(rx_amp);
+
+ states.rx_sym = rx_sym;
+ states.rxbuf = rxbuf;
+ states.nin = nin;
+ states.rxbufst = rxbufst;
+ states.timing_valid = timing_valid;
+ states.timing_mx = timing_mx;
+ states.timing_est = timing_est;
+ states.sample_point = sample_point;
+ states.delta_t = delta_t;
+ states.foff_est_hz = foff_est_hz;
+ states.coarse_foff_est_hz = coarse_foff_est_hz; % just used for tofdm
+endfunction
+
+
+function SNR3kdB = snr_from_esno(states, EsNodB)
+ ofdm_load_const;
+
+ % We integrate over M samples to get the received symbols. Additional signal power
+ % is used for the cyclic prefix samples.
+ cyclic_power = 10*log10((Ncp+M)/M);
+ % Es is the energy for each symbol. To get signal power lets
+ % multiply by symbols/second, and calculate noise power in 3000 Hz.
+ SNR3kdB = EsNodB + 10*log10(Nc*Rs/3000) + cyclic_power;
+endfunction
+
+% ----------------------------------------------------------------------------------
+% assemble_modem_packet - assemble modem packet from UW, payload, and txt bits
+% ----------------------------------------------------------------------------------
+
+function modem_frame = assemble_modem_packet(states, payload_bits, txt_bits)
+ ofdm_load_const;
+
+ # Due to the operation of the FEC encoder or interleaver, Tx data
+ # usually comes in "packet size" chunks, so assembly operates on an
+ # entire packet (multiple modem frames if Np>1)
+
+ p = 1; u = 1;
+ modem_frame = zeros(1,Nbitsperpacket);
+
+ for b=1:Nbitsperpacket-Ntxtbits;
+ if (u <= Nuwbits) && (b == uw_ind(u))
+ modem_frame(b) = tx_uw(u++);
+ else
+ modem_frame(b) = payload_bits(p++);
+ end
+ end
+ t = 1;
+ for b=Nbitsperpacket-Ntxtbits+1:Nbitsperpacket
+ modem_frame(b) = txt_bits(t++);
+ end
+ assert(u == (Nuwbits+1));
+ assert(p = (length(payload_bits)+1));
+endfunction
+
+
+% ----------------------------------------------------------------------------------
+% assemble_modem_packet_symbols - assemble modem packet from UW, payload, and txt bits
+% ----------------------------------------------------------------------------------
+
+function modem_frame = assemble_modem_packet_symbols(states, payload_syms, txt_syms)
+ ofdm_load_const;
+
+ Nsymsperpacket = Nbitsperpacket/bps;
+ Nuwsyms = Nuwbits/bps;
+ Ntxtsyms = Ntxtbits/bps;
+ modem_frame = zeros(1,Nsymsperpacket);
+ p = 1; u = 1;
+
+ for s=1:Nsymsperpacket-Ntxtsyms;
+ if (u <= Nuwsyms) && (s == uw_ind_sym(u))
+ modem_frame(s) = states.tx_uw_syms(u++);
+ else
+ modem_frame(s) = payload_syms(p++);
+ end
+ end
+ t = 1;
+ for s=Nsymsperpacket-Ntxtsyms+1:Nsymsperpacket
+ modem_frame(s) = txt_syms(t++);
+ end
+ assert(u == (Nuwsyms+1));
+ assert(p = (length(payload_syms)+1));
+endfunction
+
+
+% ------------------------------------------------------------------------------------------------
+% extract_uw - extract just the UW from the first few frames of a packet, to check UW
+% during acquisition
+% -------------------------------------------------------------------------------------------------
+
+function rx_uw = extract_uw(states, rx_syms, rx_amps)
+ ofdm_load_const;
+
+ Nsymsperframe = Nbitsperframe/bps;
+ assert(length(rx_syms) == Nuwframes*Nsymsperframe);
+ Nuwsyms = Nuwbits/bps;
+ rx_uw_syms = zeros(1,Nuwsyms);
+ rx_uw_amps = zeros(1,Nuwsyms);
+ u = 1;
+
+ for s=1:Nuwframes*Nsymsperframe
+ if (u <= Nuwsyms) && (s == uw_ind_sym(u))
+ rx_uw_syms(u) = rx_syms(s);
+ rx_uw_amps(u) = rx_amps(s);
+ u++;
+ end
+ end
+ assert(u == (Nuwsyms+1));
+
+ % now demodulate UW bits
+ rx_uw = zeros(1,Nuwbits);
+
+ for s=1:Nuwsyms
+ if bps == 2
+ rx_uw(bps*(s-1)+1:bps*s) = qpsk_demod(rx_uw_syms(s));
+ elseif bps == 4
+ rx_uw(bps*(s-1)+1:bps*s) = qam16_demod(states.qam16,rx_uw_syms(s), max(1E-12,rx_amps(s)));
+ end
+ end
+endfunction
+
+
+% ------------------------------------------------------------------------------------------------
+% disassemble_modem_packet - extract UW, txt bits, and payload symbols from a packet of symbols
+% -------------------------------------------------------------------------------------------------
+
+function [rx_uw payload_syms payload_amps txt_bits] = disassemble_modem_packet(states, modem_frame_syms, modem_frame_amps)
+ ofdm_load_const;
+
+ Nsymsperpacket = Nbitsperpacket/bps;
+ Nuwsyms = Nuwbits/bps;
+ Ntxtsyms = Ntxtbits/bps;
+ payload_syms = zeros(1,Nsymsperpacket-Nuwsyms-Ntxtsyms);
+ payload_amps = zeros(1,Nsymsperpacket-Nuwsyms-Ntxtsyms);
+ rx_uw_syms = zeros(1,Nuwsyms);
+ rx_uw_amps = zeros(1,Nuwsyms);
+ txt_syms = zeros(1,Ntxtsyms);
+ p = 1; u = 1;
+
+ for s=1:Nsymsperpacket-Ntxtsyms;
+ if (u <= Nuwsyms) && (s == uw_ind_sym(u))
+ rx_uw_syms(u) = modem_frame_syms(s);
+ rx_uw_amps(u) = modem_frame_amps(s);
+ u++;
+ else
+ payload_syms(p) = modem_frame_syms(s);
+ payload_amps(p++) = modem_frame_amps(s);
+ end
+ end
+ t = 1;
+ for s=Nsymsperpacket-Ntxtsyms+1:Nsymsperpacket
+ txt_syms(t++) = modem_frame_syms(s);
+ end
+ assert(u == (Nuwsyms+1));
+ assert(p = (Nsymsperpacket+1));
+
+ % now demodulate UW and txt bits
+
+ rx_uw = zeros(1,Nuwbits);
+ txt_bits = zeros(1,Ntxtbits);
+
+ for s=1:Nuwsyms
+ if bps == 2
+ rx_uw(bps*(s-1)+1:bps*s) = qpsk_demod(rx_uw_syms(s));
+ elseif bps == 4
+ rx_uw(bps*(s-1)+1:bps*s) = qam16_demod(states.qam16,rx_uw_syms(s),rx_uw_amps(s));
+ end
+ end
+ for s=1:Ntxtsyms
+ txt_bits(2*s-1:2*s) = qpsk_demod(txt_syms(s));
+ end
+
+endfunction
+
+
+%-----------------------------------------------------------------------
+% ofdm_rand - a psuedo-random number generator that we can implement
+% in C with identical results to Octave. Returns an unsigned
+% int between 0 and 32767
+%-----------------------------------------------------------------------
+
+function r = ofdm_rand(n, seed=1)
+ r = zeros(1,n);
+ for i=1:n
+ seed = mod(1103515245 * seed + 12345, 32768);
+ r(i) = seed;
+ end
+endfunction
+
+
+% build a single modem frame preamble vector for reliable single frame acquisition
+% on data modes
+function tx_preamble = ofdm_generate_preamble(states, seed=2)
+ tmp_states = states;
+ % tweak local copy of states so we can generate a 1 modem-frame packet
+ tmp_states.Np = 1; tmp_states.Nbitsperpacket = tmp_states.Nbitsperframe;
+ preamble_bits = ofdm_rand(tmp_states.Nbitsperframe, seed) > 16384;
+ tx_preamble = ofdm_mod(tmp_states, preamble_bits);
+endfunction
+
+
+% ------------------------------------------------------------------------------
+% Handle FEC encoding/decoding
+% ------------------------------------------------------------------------------
+
+function [frame_bits bits_per_frame] = fec_encode(states, code_param, mode, payload_bits)
+ ofdm_load_const;
+ if code_param.data_bits_per_frame != code_param.ldpc_data_bits_per_frame
+ % optionally lower the code rate by "one stuffing" - setting Nunused data bits to 1
+ Nunused = code_param.ldpc_data_bits_per_frame - code_param.data_bits_per_frame;
+ frame_bits = LdpcEncode([payload_bits ones(1,Nunused)], code_param.H_rows, code_param.P_matrix);
+ % remove unused data bits from codeword, as they are known to the receiver and don't need to be transmitted
+ frame_bits = [ frame_bits(1:code_param.data_bits_per_frame) frame_bits(code_param.ldpc_data_bits_per_frame+1:end) ];
+ else
+ frame_bits = LdpcEncode(payload_bits, code_param.H_rows, code_param.P_matrix);
+ end
+ bits_per_frame = length(frame_bits);
+
+endfunction
+
+function [rx_bits paritychecks] = fec_decode(states, code_param, ...
+ payload_syms_de, payload_amps_de, ...
+ mean_amp, EsNo)
+ ofdm_load_const;
+ % note ldpc_dec() handles optional lower code rate zero-stuffing
+ [rx_codeword paritychecks] = ldpc_dec(code_param, mx_iter=100, demod=0, dec=0, ...
+ payload_syms_de/mean_amp, EsNo,
+ payload_amps_de/mean_amp);
+ rx_bits = rx_codeword(1:code_param.data_bits_per_frame);
+endfunction
+
+
+function [tx nclipped] = ofdm_clip(states, tx, threshold_level, plot_en=0)
+ ofdm_load_const;
+ tx_ = tx;
+ ind = find(abs(tx) > threshold_level);
+ nclipped = length(ind);
+ tx(ind) = threshold_level*exp(j*angle(tx(ind)));
+ if plot_en
+ figure(2); clf; plot(abs(tx_(1:5*M))); hold on; plot(abs(tx(1:5*M))); hold off;
+ endif
+end
+
+% two stage Hilbert clipper to improve PAPR
+function tx = ofdm_hilbert_clipper(states, tx, tx_clip_en)
+ tx *= states.amp_scale;
+
+ % optional compressor to improve PAPR
+
+ nclipped = 0;
+ if tx_clip_en
+ if states.verbose
+ printf("%f %f\n", states.clip_gain1, states.clip_gain2);
+ end
+ [tx nclipped] = ofdm_clip(states, tx*states.clip_gain1, states.ofdm_peak);
+
+ cutoff_norm = states.txbpf_width_Hz/states.Fs;
+ w_centre = mean(states.w); centre_norm = w_centre/(2*pi);
+ tx = ofdm_complex_bandpass_filter(cutoff_norm, centre_norm,100,tx);
+
+ % filter messes up peak levels use this to get us back to approx 16384
+ tx *= states.clip_gain2;
+ end
+
+ % Hilbert Clipper 2 - remove any really low probability outliers after clipping/filtering
+ % even on vanilla Tx
+ [tx tmp] = ofdm_clip(states, tx, states.ofdm_peak);
+
+ % note this is PAPR of complex signal, PAPR of real signal will be 3dB-ish larger
+ peak = max(abs(tx)); RMS = sqrt(mean(abs(tx).^2));
+ cpapr = 10*log10((peak.^2)/(RMS.^2));
+
+ if states.verbose
+ printf("Peak: %4.2f RMS: %5.2f CPAPR: %4.2f clipped: %5.2f%%\n",
+ peak, RMS, cpapr, nclipped*100/length(tx));
+ end
+endfunction
+
+
+% Complex bandpass filter built from low pass prototype as per src/filter.c,
+% cutoff_freq and center_freq are normalised such that cutoff_freq = 0.5 is Fs/2
+function out = ofdm_complex_bandpass_filter(cutoff_freq,center_freq,n_coeffs,in)
+ lowpass_coeff = fir1(n_coeffs-1, cutoff_freq);
+ k = (0:n_coeffs-1);
+ bandpass_coeff = lowpass_coeff .* exp(j*2*pi*center_freq*k);
+ out = filter(bandpass_coeff,1,in);
+endfunction
+
+
+% Complex bandpass filter for Rx - just used on the very low SNR modes to help
+% with acquisition
+function [rx delay_samples] = ofdm_rx_filter(states, mode, rx)
+ delay_samples = 0;
+ if strcmp(mode,"datac4") || strcmp(mode,"datac13") || strcmp(mode,"datac14")
+ w_centre = mean(states.w); centre_norm = w_centre/(2*pi);
+ n_coeffs = 100;
+ cutoff_Hz = 400; cutoff_norm = cutoff_Hz/states.Fs;
+ rx = ofdm_complex_bandpass_filter(cutoff_norm,centre_norm,n_coeffs,rx);
+ delay_samples = n_coeffs/2;
+ end
+endfunction
+
+
+% returns an unpacked CRC16 (array of 16 bits) calculated from an array of unpacked bits
+function unpacked_crc16 = crc16_unpacked(unpacked_bits)
+ % pack into bytes
+ mod(length(unpacked_bits),8);
+ assert(mod(length(unpacked_bits),8) == 0);
+ nbytes = length(unpacked_bits)/8;
+ mask = 2 .^ (7:-1:0);
+ for i=1:nbytes
+ st = (i-1)*8 + 1; en = st+7;
+ bytes(i) = sum(mask .* unpacked_bits(st:en));
+ end
+ crc16_hex = crc16(bytes);
+ crc16_dec = [hex2dec(crc16_hex(1:2)) hex2dec(crc16_hex(3:4)) ];
+ unpacked_crc16 = [];
+ for b=1:length(crc16_dec)
+ unpacked_crc16 = [unpacked_crc16 bitand(crc16_dec(b), mask) > 0];
+ end
+endfunction