File: fsk_lib.m

package info (click to toggle)
codec2 1.2.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 76,376 kB
  • sloc: ansic: 436,819; cpp: 2,091; objc: 1,736; sh: 1,510; python: 1,405; asm: 683; makefile: 605
file content (469 lines) | stat: -rw-r--r-- 15,225 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
% fsk_lib.m
% David Rowe Oct 2015 - present
%
% mFSK modem, started out life as RTTY demodulator for Project
% Horus High Altitude Ballon (HAB) telemetry, also used for:
%
% FreeDV 2400A: 4FSK UHF/UHF digital voice
% Wenet.......: 100 kbit/s HAB High Def image telemetry
%
% Handles frequency offsets, performance right on ideal, C implementation
% in codec2/src

1;

function states = fsk_init(Fs, Rs, M=2, P=8, nsym=50)
  states.M = M;                    
  states.bitspersymbol = log2(M);
  states.Fs = Fs;
  states.Rs = Rs;

  states.nsym = nsym;                             % Number of symbols processed by demodulator in each call, also
                                                  % the timing estimator window
  Ts = states.Ts = Fs/Rs;                         % number of samples per symbol
  assert(Ts == floor(Ts), "Fs/Rs must be an integer");

  N = states.N = Ts*states.nsym;                  % processing buffer size, nice big window for timing est
  bin_width_Hz = 0.1*Rs;                          % we want enough DFT bins to get within 10% of the tones centre
  Ndft = Fs/bin_width_Hz;
  states.Ndft = 2.^ceil(log2(Ndft));              % round to nearest power of 2 for efficient FFT
  states.Sf = zeros(states.Ndft,1);               % current memory of dft mag samples
  states.tc = 0.1;                                % average DFT over longtime window, accurate at low Eb/No, but slow
  
  states.nbit = states.nsym*states.bitspersymbol; % number of bits per processing frame
  Nmem = states.Nmem  = N+2*Ts;                   % two symbol memory in down converted signals to allow for timing adj

  states.f_dc = zeros(M,Nmem);
  states.P = P;                                   % oversample rate out of filter
  assert(Ts/states.P == floor(Ts/states.P), "Ts/P must be an integer");

  states.tx_tone_separation = 2*Rs;
  states.nin = N;                                 % can be N +/- Ts/P samples to adjust for sample clock offsets
  states.verbose = 0;
  states.phi = zeros(1, M);                       % keep down converter osc phase continuous

  % BER stats 

  states.ber_state = 0;
  states.ber_valid_thresh = 0.05;  states.ber_invalid_thresh = 0.1; 
  states.Tbits = 0;
  states.Terrs = 0;
  states.nerr_log = 0;

  % extra simulation parameters

  states.tx_real = 1;
  states.dA(1:M) = 1;
  states.df(1:M) = 0;
  states.f(1:M) = 0;
  states.norm_rx_timing = 0;
  states.ppm = 0;
  states.prev_pkt = [];
 
  % Freq. estimator limits
  states.fest_fmax = Fs;
  states.fest_fmin = 0;
  states.fest_min_spacing = 0.75*Rs;
  states.freq_est_type = 'peak';

  %printf("Octave: M: %d Fs: %d Rs: %d Ts: %d nsym: %d nbit: %d N: %d Ndft: %d fmin: %d fmax: %d\n",
  %       states.M, states.Fs, states.Rs, states.Ts, states.nsym, states.nbit, states.N, states.Ndft, states.fest_fmin, states.fest_fmax);

endfunction


% modulator function

function tx = fsk_mod(states, tx_bits)

    M  = states.M;
    Ts = states.Ts;
    Fs = states.Fs;
    ftx  = states.ftx;
    df = states.df; % tone freq change in Hz/s
    dA = states.dA; % amplitude of each tone

    num_bits = length(tx_bits);
    num_symbols = num_bits/states.bitspersymbol;
    tx = zeros(states.Ts*num_symbols,1);
    tx_phase = 0;
    s = 1;

    for i=1:states.bitspersymbol:num_bits

      % map bits to FSK symbol (tone number)

      K = states.bitspersymbol;
      tone = tx_bits(i:i+(K-1)) * (2.^(K-1:-1:0))' + 1;
      
      tx_phase_vec = tx_phase + (1:Ts)*2*pi*ftx(tone)/Fs;
      tx_phase = tx_phase_vec(Ts) - floor(tx_phase_vec(Ts)/(2*pi))*2*pi;
      if states.tx_real
        tx((s-1)*Ts+1:s*Ts) = dA(tone)*2.0*cos(tx_phase_vec);
      else
        tx((s-1)*Ts+1:s*Ts) = dA(tone)*exp(j*tx_phase_vec);
      end
      s++;

      % freq drift

      ftx += df*Ts/Fs;
    end
    states.ftx = ftx;
endfunction


% Estimate the frequency of the FSK tones.  In some applications (such
% as balloon telemetry) these may not be well controlled by the
% transmitter, so we have to try to estimate them.

function states = est_freq(states, sf, ntones)
  N = states.N;
  Ndft = states.Ndft;
  Fs = states.Fs;
  
  % This assumption is OK for balloon telemetry but may not be true in
  % general

  min_tone_spacing = states.fest_min_spacing;
  
  % set some limits to search range, which will mean some manual re-tuning

  fmin = states.fest_fmin;
  fmax = states.fest_fmax;
  % note 0 Hz is mapped to Ndft/2+1 via fftshift
  st = floor(fmin*Ndft/Fs) + Ndft/2;  st = max(1,st);
  en = floor(fmax*Ndft/Fs) + Ndft/2;  en = min(Ndft,en);
  
  #printf("Fs: %f Ndft: %d fmin: %f fmax: %f st: %d en: %d\n",Fs, Ndft,  fmin, fmax, st, en)
  
  % Update mag DFT  ---------------------------------------------

  % we break up input buffer to a series of overlapping Ndft sequences
  numffts = floor(length(sf)/(Ndft/2)) - 1;
  h = hanning(Ndft);
  for i=1:numffts
    a = (i-1)*Ndft/2+1; b = a + Ndft - 1;
    Sf = abs(fftshift(fft(sf(a:b) .* h, Ndft)));

    % Smooth DFT mag spectrum, slower to respond to changes but more
    % accurate.  Single order IIR filter is an exponentially weighted
    % moving average.  This means the freq est window is wider than
    % timing est window
    tc = states.tc; states.Sf = (1-tc)*states.Sf + tc*Sf;
  end

  % Search for each tone method 1 - peak pick each tone location ----------------------------------

  f = []; a = [];
  Sf = states.Sf;
  for m=1:ntones
    [tone_amp tone_index] = max(Sf(st:en));
    tone_index += st - 1;
    
    f = [f (tone_index-1-Ndft/2)*Fs/Ndft];
    a = [a tone_amp];

    % zero out region min_tone_spacing either side of max so we can find next highest peak
    % closest spacing for non-coh mFSK is Rs

    stz = tone_index - floor((min_tone_spacing)*Ndft/Fs);
    stz = max(1,stz);
    enz = tone_index + floor((min_tone_spacing)*Ndft/Fs);
    enz = min(Ndft,enz);
    Sf(stz:enz) = 0;
  end

  states.f = sort(f);
  
  % Search for each tone method 2 - correlate with mask with non-zero entries at tone spacings -----

  % Create a mask with non-zero entries at tone spacing.  Might be
  % smarter to use the DFT of a hanning window as mask
  
  mask = zeros(1,Ndft);
  mask(1:3) = 1;
  for m=1:ntones-1
    bin = round(m*states.tx_tone_separation*Ndft/Fs);
    mask(bin:bin+2) = 1;
  end
  mask = mask(1:bin+2);
  states.mask = mask;
  
  % drag mask over Sf, looking for peak in correlation
  b_max = st; corr_max = 0;
  Sf = states.Sf; corr_log = [];
  for b=st:en-length(mask)
    corr = mask * Sf(b:b+length(mask)-1);
    corr_log = [corr_log corr];
    if corr > corr_max
      corr_max = corr;
      b_max = b;
    end
  end
  foff = ((b_max-1)-Ndft/2)*Fs/Ndft;
  
  if bitand(states.verbose, 0x8)
    % enable this to single step through frames
    figure(1); clf; subplot(211); plot(Sf,'b;sf;'); 
    hold on; plot(max(Sf)*[zeros(1,b_max) mask],'g;mask;'); hold off;
    subplot(212); plot(corr_log); ylabel('corr against f');
    printf("foff: %4.0f\n", foff);
    kbhit;
  end
  states.f2 = foff + (0:ntones-1)*states.tx_tone_separation;
end


% ------------------------------------------------------------------------------------
% Given a buffer of nin input Rs baud FSK samples, returns nsym bits.
%
% nin is the number of input samples required by demodulator.  This is
% time varying.  It will nominally be N (8000), and occasionally N +/- 
% Ts/2 (e.g. 8080 or 7920).  This is how we compensate for differences between the
% remote tx sample clock and our sample clock.  This function always returns
% N/Ts (e.g. 50) demodulated bits.  Variable number of input samples, constant number
% of output bits.

function [rx_bits states] = fsk_demod(states, sf)
  M = states.M;
  N = states.N;
  Ndft = states.Ndft;
  Fs = states.Fs;
  Rs = states.Rs;
  Ts = states.Ts;
  nsym = states.nsym;
  P = states.P;
  nin = states.nin;
  verbose = states.verbose;
  Nmem = states.Nmem;
  f = states.f;

  assert(length(sf) == nin);

  % down convert and filter at rate P ------------------------------

  % update filter (integrator) memory by shifting in nin samples
  
  nold = Nmem-nin; % number of old samples we retain

  f_dc = states.f_dc; 
  f_dc(:,1:nold) = f_dc(:,Nmem-nold+1:Nmem);

  % freq shift down to around DC, ensuring continuous phase from last frame, as nin may vary
  for m=1:M
    phi_vec = states.phi(m) + (1:nin)*2*pi*f(m)/Fs;
    f_dc(m,nold+1:Nmem) = sf .* exp(j*phi_vec)';
    states.phi(m)  = phi_vec(nin);
    states.phi(m) -= 2*pi*floor(states.phi(m)/(2*pi));
  end
  % save filter (integrator) memory for next time
  states.f_dc = f_dc;
  
  % integrate over symbol period, which is effectively a LPF, removing
  % the -2Fc frequency image.  Can also be interpreted as an ideal
  % integrate and dump, non-coherent demod.  We run the integrator at
  % rate P*Rs (1/P symbol offsets) to get outputs at a range of
  % different fine timing offsets.  We calculate integrator output
  % over nsym+1 symbols so we have extra samples for the fine timing
  % re-sampler at either end of the array.

  f_int = zeros(M,(nsym+1)*P);
  for i=1:(nsym+1)*P
    st = 1 + (i-1)*Ts/P;
    en = st+Ts-1;
    for m=1:M
      f_int(m,i) = sum(f_dc(m,st:en));
    end
  end
  states.f_int = f_int;
  
  % fine timing estimation -----------------------------------------------

  % Non linearity has a spectral line at Rs, with a phase
  % related to the fine timing offset.  See:
  %   http://www.rowetel.com/blog/?p=3573 
  % We have sampled the integrator output at Fs=P samples/symbol, so
  % lets do a single point DFT at w = 2*pi*f/Fs = 2*pi*Rs/(P*Rs)
  %
  % Note timing non-linearity derived by experiment.  Not quite sure what I'm doing here.....
  % but it gives 0dB impl loss for 2FSK Eb/No=9dB, testmode 1:
  %   Fs: 8000 Rs: 50 Ts: 160 nsym: 50
  %   frames: 200 Tbits: 9700 Terrs: 93 BER 0.010

  Np = length(f_int(1,:));
  w = 2*pi*(Rs)/(P*Rs);
  timing_nl = sum(abs(f_int(:,:)).^2);
  x = timing_nl * exp(-j*w*(0:Np-1))';
  norm_rx_timing = angle(x)/(2*pi);
  rx_timing = norm_rx_timing*P;

  states.x = x;
  states.timing_nl = timing_nl;
  states.rx_timing = rx_timing;
  prev_norm_rx_timing = states.norm_rx_timing;
  states.norm_rx_timing = norm_rx_timing;

  % estimate sample clock offset in ppm
  % d_norm_timing is fraction of symbol period shift over nsym symbols

  d_norm_rx_timing = norm_rx_timing - prev_norm_rx_timing;

  % filter out big jumps due to nin changes

  if abs(d_norm_rx_timing) < 0.2
    appm = 1E6*d_norm_rx_timing/nsym;
    states.ppm = 0.9*states.ppm + 0.1*appm;
  end

  % work out how many input samples we need on the next call. The aim
  % is to keep angle(x) away from the -pi/pi (+/- 0.5 fine timing
  % offset) discontinuity.  The side effect is to track sample clock
  % offsets

  next_nin = N;
  if norm_rx_timing > 0.25
     next_nin += Ts/4;
  end
  if norm_rx_timing < -0.25;
     next_nin -= Ts/4;
  end
  states.nin = next_nin;

  % Now we know the correct fine timing offset, Re-sample integrator
  % outputs using fine timing estimate and linear interpolation, then
  % extract the demodulated bits

  low_sample = floor(rx_timing);
  fract = rx_timing - low_sample;
  high_sample = ceil(rx_timing);

  if bitand(verbose,0x2)
    printf("rx_timing: %3.2f low_sample: %d high_sample: %d fract: %3.3f nin_next: %d\n", rx_timing, low_sample, high_sample, fract, next_nin);
  end

  f_int_resample = zeros(M,nsym);
  rx_bits = zeros(1,nsym*states.bitspersymbol);
  tone_max = zeros(1,nsym);
  rx_nse_pow = 1E-12; rx_sig_pow = 0.0;
  
  for i=1:nsym
    st = i*P+1;
    f_int_resample(:,i) = f_int(:,st+low_sample)*(1-fract) + f_int(:,st+high_sample)*fract;

    % Hard decision decoding, Largest amplitude tone is the winner.
    % Map this FSK "symbol" back to bits, depending on M

    [tone_max(i) tone_index] = max(f_int_resample(:,i));
    st = (i-1)*states.bitspersymbol + 1;
    en = st + states.bitspersymbol-1;
    arx_bits = dec2bin(tone_index - 1, states.bitspersymbol) - '0';
    rx_bits(st:en) = arx_bits;

    % each filter is the DFT of a chunk of spectrum.  If there is no tone in the
    % filter it can be considered an estimate of noise in that bandwidth
    rx_pows = f_int_resample(:,i) .* conj(f_int_resample(:,i));
    rx_sig_pow += rx_pows(tone_index);
    rx_nse_pow += (sum(rx_pows) - rx_pows(tone_index))/(M-1);
  end

  states.f_int_resample = f_int_resample;

  % Eb/No estimation (todo: this needs some work, like calibration, low Eb/No perf, work for all M)
  tone_max = abs(tone_max);
  states.EbNodB = -6 + 20*log10(1E-6+mean(tone_max)/(1E-6+std(tone_max)));

  % Estimators for LDPC decoder, might be a bit rough if nsym is small
  rx_sig_pow = rx_sig_pow/nsym;
  rx_nse_pow = rx_nse_pow/nsym;
  states.v_est = sqrt(rx_sig_pow-rx_nse_pow); 
  states.SNRest = rx_sig_pow/rx_nse_pow;
endfunction


% BER counter and test frame sync logic -------------------------------------------
% We look for test_frame in rx_bits_buf,  rx_bits_buf must be twice as long as test_frame

function states = ber_counter(states, test_frame, rx_bits_buf)
  nbit = length(test_frame);
  assert (length(rx_bits_buf) == 2*nbit);
  state = states.ber_state;
  next_state = state;

  if state == 0

    % try to sync up with test frame

    nerrs_min = nbit;
    for i=1:nbit
      error_positions = xor(rx_bits_buf(i:nbit+i-1), test_frame);
      nerrs = sum(error_positions);
      if nerrs < nerrs_min
        nerrs_min = nerrs;
        states.coarse_offset = i;
      end
    end
    if nerrs_min/nbit < states.ber_valid_thresh
      next_state = 1;
    end
    if bitand(states.verbose,0x4)
      printf("coarse offset: %d nerrs_min: %d next_state: %d\n", states.coarse_offset, nerrs_min, next_state);
    end
    states.nerr = nerrs_min;
  end

  if state == 1  

    % we're synced up, lets measure bit errors

    error_positions = xor(rx_bits_buf(states.coarse_offset:states.coarse_offset+nbit-1), test_frame);
    nerrs = sum(error_positions);
    if nerrs/nbit > states.ber_invalid_thresh
      next_state = 0;
      if bitand(states.verbose,0x4)
        printf("coarse offset: %d nerrs: %d next_state: %d\n", states.coarse_offset, nerrs, next_state);
      end
    else
      states.Terrs += nerrs;
      states.Tbits += nbit;
      states.nerr_log = [states.nerr_log nerrs];
    end
    states.nerr = nerrs;
  end

  states.ber_state = next_state;
endfunction


% Alternative stateless BER counter that works on packets that may have gaps between them

function states = ber_counter_packet(states, test_frame, rx_bits_buf)
  ntestframebits = states.ntestframebits;
  nbit = states.nbit;

  % look for offset with min errors

  nerrs_min = ntestframebits; coarse_offset = 1;
  for i=1:nbit
    error_positions = xor(rx_bits_buf(i:ntestframebits+i-1), test_frame);
    nerrs = sum(error_positions);
    %printf("i: %d nerrs: %d\n", i, nerrs);
    if nerrs < nerrs_min
      nerrs_min = nerrs;
      coarse_offset = i;
    end
  end

  % if less than threshold count errors

  if nerrs_min/ntestframebits < 0.05 
    states.Terrs += nerrs_min;
    states.Tbits += ntestframebits;
    states.nerr_log = [states.nerr_log nerrs_min];
    if bitand(states.verbose, 0x4)
      printf("coarse_offset: %d nerrs_min: %d\n", coarse_offset, nerrs_min);
    end
  end
endfunction