Liquid
Liquid
Liquid
Joseph D. Gaeddert
March 24, 2014
Blacksburg, Virginia
joseph@liquidsdr.org
This entire project was coded using Vim and gcc, primarily on a Macintosh.
Contents
I Introduction to liquid 2
II Tutorials 10
6 Tutorial: Framing 24
6.1 Problem Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
6.2 Setting up the Environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
6.3 Creating the Frame Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
6.4 Creating the Frame Synchronizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
6.5 Putting it All Together . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6.6 Final Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
ii
7.4 Creating the Frame Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
7.5 Creating the Frame Synchronizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
7.6 Putting it All Together . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
7.7 Final Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
III Modules 49
9 audio 56
9.1 ‘cvsd‘ (continuously variable slope delta) . . . . . . . . . . . . . . . . . . . . . . . . 56
9.1.1 Theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
9.1.2 Pre-/Post-Filtering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
9.1.3 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
9.1.4 Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
10 buffer 60
10.1 window buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
10.2 wdelay buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
12 equalization 65
12.1 System Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
12.2 ‘eqlms‘ (least mean-squares equalizer) . . . . . . . . . . . . . . . . . . . . . . . . . 65
12.3 ‘eqrls‘ (recursive least-squares equalizer) . . . . . . . . . . . . . . . . . . . . . . . . 65
12.4 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
12.5 Blind Equalization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
12.6 Comparison of ‘eqlms‘ and ‘eqrls‘ Object Families . . . . . . . . . . . . . . . . . . . 70
iii
13.5.2 SEC-DED(39,32) block code . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
13.5.3 SEC-DEC(72,64) block code . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
13.6 Convolutional and Reed-Solomon codes . . . . . . . . . . . . . . . . . . . . . . . . . 75
13.7 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
13.7.1 Soft Decoding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
13.8 Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
15 filter 90
15.1 autocorr (auto-correlator) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
15.2 decim (decimator) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
15.3 firfarrow (finite impulse response Farrow filter) . . . . . . . . . . . . . . . . . . . . 93
15.4 firfilt (finite impulse response filter) . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
15.5 firdes (finite impulse response filter design) . . . . . . . . . . . . . . . . . . . . . . . 96
15.5.1 Window prototype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
15.5.2 liquid firdes nyquist() (Nyquist filter design) . . . . . . . . . . . . . . . . . . 98
15.5.3 liquid firdes rnyquist() (square-root Nyquist filter design) . . . . . . . . . . 100
15.5.4 GMSK Filter Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
15.5.5 firdespm (Parks-McClellan algorithm) . . . . . . . . . . . . . . . . . . . . . 102
15.5.6 Miscellaneous functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
15.6 firhilbf (finite impulse response Hilbert transform) . . . . . . . . . . . . . . . . . . 106
15.7 iirfilt (infinite impulse response filter) . . . . . . . . . . . . . . . . . . . . . . . . . . 109
15.8 iirdes (infinite impulse response filter design) . . . . . . . . . . . . . . . . . . . . . 111
15.8.1 liquid iirdes(), the simplified method . . . . . . . . . . . . . . . . . . . . . . 112
15.8.2 internal description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
15.8.3 Available Filter Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
15.8.4 bilinear zpkf (Bilinear z-transform) . . . . . . . . . . . . . . . . . . . . . . . 116
15.8.5 Filter transformations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
15.8.6 Filter Coefficient Computation . . . . . . . . . . . . . . . . . . . . . . . . . 117
15.9 firinterp (interpolator) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
15.10 msresamp (multi-stage arbitrary resampler) . . . . . . . . . . . . . . . . . . . . . . 124
15.11 resamp2 (half-band filter/resampler) . . . . . . . . . . . . . . . . . . . . . . . . . . 126
iv
15.12 resamp (arbitrary resampler) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
15.13 symsync (symbol synchronizer) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
16 framing 136
16.1 interleaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
16.1.1 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
16.2 packetizer (multi-level error-correction) . . . . . . . . . . . . . . . . . . . . . . . . . 138
16.3 bpacket (binary packet generator/synchronizer) . . . . . . . . . . . . . . . . . . . . 141
16.3.1 bpacketgen interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
16.3.2 bpacketsync interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
16.3.3 Code example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
16.4 frame64, flexframe (basic framing structures) . . . . . . . . . . . . . . . . . . . . . 144
16.4.1 frame64 description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
16.4.2 flexframe description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
16.4.3 Framing Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
16.4.4 The Decoding Process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
16.5 framesyncprops s (frame synchronizer properties) . . . . . . . . . . . . . . . . . . . 146
16.6 framesyncstats s (frame synchronizer statistics) . . . . . . . . . . . . . . . . . . . . 147
16.7 ofdmflexframe (OFDM framing structures) . . . . . . . . . . . . . . . . . . . . . . . 148
16.7.1 Operational description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
16.7.2 Subcarrier Allocation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
16.7.3 Pilot Subcarriers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
16.7.4 Window Tapering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
16.7.5 ofdmflexframegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
16.7.6 ofdmflexframesync . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
16.7.7 Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
17 math 157
17.1 Transcendental Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
17.1.1 liquid gammaf(z), liquid lngammaf(z) . . . . . . . . . . . . . . . . . . . . . 157
17.1.2 liquid lowergammaf(z,a), liquid lnlowergammaf(z,a) (lower incomplete Gamma)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
17.1.3 liquid uppergammaf(z,a), liquid lnuppergammaf(z,a) (upper incomplete Gamma)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
17.1.4 liquid factorialf(n) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
17.1.5 liquid nchoosek() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
17.1.6 liquid nextpow2() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
17.1.7 liquid sinc(z) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
17.1.8 liquid lnbesselif(), liquid besselif(), liquid besseli0f() . . . . . . . . . . . . . 159
17.1.9 liquid lnbesseljf(), liquid besselj0f() . . . . . . . . . . . . . . . . . . . . . . . 160
17.1.10 liquid Qf(), liquid MarcumQf(), liquid MarcumQ1f() . . . . . . . . . . . . . 160
17.2 Complex Trigonometry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
17.2.1 liquid csqrtf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
17.2.2 liquid cexpf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
17.2.3 liquid clogf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
v
17.2.4 liquid cacosf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
17.2.5 liquid casinf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
17.2.6 liquid catanf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
17.3 Windowing functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
17.3.1 hamming(), (Hamming window) . . . . . . . . . . . . . . . . . . . . . . . . 162
17.3.2 hann(), (Hann window) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
17.3.3 blackmanharris(), (Blackman-harris window) . . . . . . . . . . . . . . . . . 162
17.3.4 kaiser(), (Kaiser-Bessel window) . . . . . . . . . . . . . . . . . . . . . . . . . 162
17.3.5 liquid kbd window(), (Kaiser-Bessel derived window) . . . . . . . . . . . . . 162
17.4 Polynomials . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
17.4.1 polyf val() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
17.4.2 polyf fit() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
17.4.3 polyf fit lagrange() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
17.4.4 polyf interp lagrange() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
17.4.5 polyf fit lagrange barycentric() . . . . . . . . . . . . . . . . . . . . . . . . . 170
17.4.6 polyf val lagrange barycentric() . . . . . . . . . . . . . . . . . . . . . . . . . 171
17.4.7 polyf expandbinomial() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
17.4.8 polyf expandbinomial pm() . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
17.4.9 polyf expandroots() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
17.4.10 polyf expandroots2() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
17.4.11 polyf findroots() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
17.4.12 polyf mul() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
17.5 Modular Arithmetic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
17.5.1 liquid is prime(n) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
17.5.2 liquid factor(n,*factors,*num factors) . . . . . . . . . . . . . . . . . . . . . . 173
17.5.3 liquid unique factor(n,*factors,*num factors) . . . . . . . . . . . . . . . . . 173
17.5.4 liquid modpow(base,exp,n) . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
17.5.5 liquid primitive root(n) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
17.5.6 liquid primitive root prime(n) . . . . . . . . . . . . . . . . . . . . . . . . . . 174
17.5.7 liquid totient(n) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
18 matrix 175
18.1 Basic math operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
18.1.1 matrix access (access element) . . . . . . . . . . . . . . . . . . . . . . . . . . 175
18.1.2 matrixf add, matrixf sub, matrixf pmul, and matrixf pdiv (scalar math op-
erations) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
18.1.3 matrixf trans(), matrixf hermitian() (transpose matrix) . . . . . . . . . . . 176
18.1.4 matrixf eye() (identity matrix) . . . . . . . . . . . . . . . . . . . . . . . . . 177
18.2 Elementary math operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
18.2.1 matrixf swaprows() (swap rows) . . . . . . . . . . . . . . . . . . . . . . . . . 177
18.2.2 matrixf pivot() (pivoting) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
18.2.3 matrixf mul() (multiplication) . . . . . . . . . . . . . . . . . . . . . . . . . . 178
18.2.4 Transpose multiplication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
18.3 Complex math operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
18.3.1 matrixf inv() (inverse) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
vi
18.3.2 matrixf div() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
18.3.3 matrixf linsolve() (solve linear system of equations) . . . . . . . . . . . . . . 179
18.3.4 matrixf cgsolve() (solve linear system of equations) . . . . . . . . . . . . . . 179
18.3.5 matrixf det() (determinant) . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
18.3.6 matrixf ludecomp crout() (LU Decomposition, Crout’s Method) . . . . . . . 180
18.3.7 matrixf ludecomp doolittle() (LU Decomposition, Doolittle’s Method) . . . 181
18.3.8 matrixf qrdecomp gramschmidt() (QR Decomposition, Gram-Schmidt algo-
rithm) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
18.3.9 matrixf chol() (Cholesky Decomposition) . . . . . . . . . . . . . . . . . . . . 182
18.3.10 matrixf gjelim() (Gauss-Jordan Elimination) . . . . . . . . . . . . . . . . . 182
19 modem 184
19.1 Analog modulation schemes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
19.1.1 freqmodem (analog FM) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
19.1.2 ampmodem (analog AM) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
19.2 Linear digital modulation schemes . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
19.2.1 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
19.2.2 Gray coding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
19.2.3 LIQUID MODEM PSK (phase-shift keying) . . . . . . . . . . . . . . . . . . 193
19.2.4 LIQUID MODEM DPSK (differential phase-shift keying) . . . . . . . . . . 193
19.2.5 LIQUID MODEM APSK (amplitude/phase-shift keying . . . . . . . . . . . 193
19.2.6 LIQUID MODEM ASK (amplitude-shift keying) . . . . . . . . . . . . . . . 196
19.2.7 LIQUID MODEM QAM (quadrature amplitude modulation) . . . . . . . . 196
19.2.8 LIQUID MODEM ARB (arbitrary modem) . . . . . . . . . . . . . . . . . . 198
19.2.9 Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
19.2.10 Soft Demodulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
19.2.11 Error Vector Magnitude . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
19.3 Continuous phase digital modulation schemes . . . . . . . . . . . . . . . . . . . . . 214
19.3.1 gmskmod, gmskdem (Gauss minimum-shift keying) . . . . . . . . . . . . . . 214
20 multichannel 216
20.1 FIR polyphase filterbank channelizer (firpfbch) . . . . . . . . . . . . . . . . . . . . 216
20.2 FIR polyphase filterbank channelizer at double the output rate (firpfbch2) . . . . . 216
20.3 ofdmframe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
vii
22 optim (optimization) 224
22.1 gradsearch (gradient search) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
22.1.1 Theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
22.1.2 Momentum constant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
22.1.3 Step size adjustment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
22.1.4 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
22.2 gasearch genetic algorithm search . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
22.2.1 chromosome, solution representation . . . . . . . . . . . . . . . . . . . . . . 228
22.2.2 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
22.2.3 Example Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
23 random 231
23.1 Uniform . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
23.2 Normal (Gaussian) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
23.3 Exponential . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
23.4 Weibull . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
23.5 Gamma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
23.6 Nakagami-m . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
23.7 Rice-K . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
23.8 Data scrambler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
23.8.1 interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
24 sequence 239
24.1 bsequence, generic binary sequence . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
24.2 msequence, m-sequence (linear feedback shift register) . . . . . . . . . . . . . . . . 240
24.3 complementary codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
25 utility 244
25.1 liquid pack bytes(), liquid unpack bytes(), and liquid repack bytes() . . . . . . . . 244
25.2 liquid pack array(), liquid unpack array() . . . . . . . . . . . . . . . . . . . . . . . 244
25.3 liquid lbshift(), liquid rbshift() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
25.4 liquid lbcircshift(), liquid rbcircshift() . . . . . . . . . . . . . . . . . . . . . . . . . 245
25.5 liquid lshift(), liquid rshift() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
25.6 liquid lcircshift(), liquid rcircshift() . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
25.7 miscellany . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
26 experimental 248
26.1 fbasc (filterbank audio synthesizer codec) . . . . . . . . . . . . . . . . . . . . . . . 248
26.1.1 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
26.1.2 Useful properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
26.2 gport . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
26.2.1 Direct Memory Access . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
26.2.2 Indirect/Copied Memory Access . . . . . . . . . . . . . . . . . . . . . . . . 250
26.2.3 Key differences between memory access modes . . . . . . . . . . . . . . . . 250
26.2.4 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
26.2.5 Problem areas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
viii
26.3 dds (direct digital synthesizer) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
IV Installation 254
28 Examples 257
29 Autotests 267
30 Benchmarks 269
31 Documentation 272
ix
1
liquid-dsp documentation software-defined radio digital signal processing library Users Manual
for Version 1.2.0+ Joseph D. Gaeddert / Blacksburg, Virginia <joseph@liquidsdr.org>
$ git clone git://github.com/jgaeddert/liquid-dsp.git liquid-dsp
$ cd liquid-dsp
$ ./bootstrap.sh
$ ./configure
$ make
# make install
This entire project was coded using Vim and GCC, primarily on a Macintosh.
2
Part I
Introduction to liquid
The next few sections are designed to give you an understanding of liquids intended purpose and
where it might fit within your project. Included is a quick start guide, example source code, and a
brief historical outline.
3
• The object is only used to maintain the state of the signal processing algorithm. For example,
a finite impulse response filter (§ 15.4 ) needs to retain the filter coefficients and a buffer of
input samples. Certain algorithms which do not retain information (those which are mem-
oryless) do not use objects. For example, design rnyquist filter() (§ 15.5.3 ) calculates
the coefficients of a square-root raised-cosine filter, a processing algorithm which does not
need to maintain a state after its completion.
• While the objects do retain internal memory, they typically operate on external user-defined
arrays. As such, it is strictly up to the user to manage his/her own memory. Shared pointers
are a great way to cause memory leaks, double-free bugs, and severe headaches. The bottom
line is to remember that if you created a mess, it is your responsibility to clean it up.
• Certain objects will allocate memory internally, and consequently will use more memory than
others. This memory will only be freed when the appropriate delete() method is invoked.
Don’t forget to clean up your mess!
1 // file: doc/listings/lifecycle.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int order=4; // filter order
7 float fc=0.1f; // cutoff frequency
8 float f0=0.25f; // center frequency (bandpass|bandstop)
9 float Ap=1.0f; // pass-band ripple [dB]
10 float As=40.0f; // stop-band attenuation [dB]
11 liquid_iirdes_filtertype ftype = LIQUID_IIRDES_ELLIP;
12 liquid_iirdes_bandtype btype = LIQUID_IIRDES_BANDPASS;
13 liquid_iirdes_format format = LIQUID_IIRDES_SOS;
14
15 // CREATE filter object (and print to stdout)
16 iirfilt_crcf myfilter;
17 myfilter = iirfilt_crcf_create_prototype(ftype,
18 btype,
19 format,
20 order,
21 fc, f0,
22 Ap, As);
23 iirfilt_crcf_print(myfilter);
24
25 // allocate memory for data arrays
26 unsigned int n=128; // number of samples
27 float complex x[n]; // input samples array
28 float complex y[n]; // output samples array
29
30 // run filter
31 unsigned int i;
32 for (i=0; i<n; i++) {
33 // initialize input
34 x[i] = randnf() + _Complex_I*randnf();
35
36 // EXECUTE filter (repeat as many times as desired)
37 iirfilt_crcf_execute(myfilter, x[i], &y[i]);
38 }
39
40 // DESTROY filter object
41 iirfilt_crcf_destroy(myfilter);
42 }
A more comprehensive example is given in the example file examples/iirfilt crcf example.c,
located under the main [liquid] project directory.
3.3 Why C?
A commonly asked question is ”why C and not C++?” The answer is simple: portability. The
project’s aim is to provide a lightweight DSP library for software-defined radio that does not rely
on a myriad of dependencies. While C++ is a fine language for many projects (and theoretically
3.4 Data Types 7
runs just as fast as C), it is not as portable to embedded platforms as C and typically has a larger
memory footprint. Furthermore, the majority of functions simply perform complex operations
on a data sequence and do not require a high-level object-oriented programming interface. The
significance of object-oriented programming is the techniques used, not the languages describing
it. While a number of signal processing elements in [liquid] use structures, these are simply to save
the internal state of the object. For instance, a firfilt crcf (finite impulse response filter) object
is just a structure which contains—among other things—the filter taps (coefficients) and an input
buffer. This simplifies the interface to the user; one only needs to ”push” elements into the filter’s
internal buffer and ”execute” the dot product when desired. This could also be accomplished with
classes, a construct specific to C++ and other high-level object-oriented programming languages;
however, for the most part, C++ polymorphic data types and abstract base classes are unnecessary
for basic signal processing, and primarily just serve to reduce the code base of a project at the
expense of increased compile time and memory overhead. Furthermore, while C++ templates can
certainly be useful for library development their benefits are of limited use to signal processing
and can be circumvented through the use of pre-processor macros at the gain of increasing the
portability of the code. Under the hood, the C++ compiler’s pre-processor expands templates and
classes before actually compiling the source anyway, so in this sense they are equivalent to the
second-order macros used in [liquid]. The C programming language has a rich history in system
programming—specifically targeting embedded applications—and is the basis behind many well-
known projects including the Linux Kerneland the python programming language. Having said
this, high-level frameworks and graphical interfaces are much more suited to be written in C++
and will beat an implementation in C any day but lie far outside the scope of this project.
1 // file: doc/listings/nco.c
2 // build: gcc -c -o nco.c.o nco.c
3 // link: gcc nco.c.o -o nco -lm -lc -lliquid
4
5 # include <stdio.h>
1
T
8 3 DATA STRUCTURES IN [LIQUID]
6 # include <math.h>
7 # include <liquid / liquid.h>
8 # include <complex.h>
9
10 int main() {
11 // create nco object and initialize
12 nco_crcf n = nco_crcf_create(LIQUID_NCO);
13 nco_crcf_set_phase(n,0.3f);
14
15 // Test native C complex data type
16 float complex x;
17 nco_crcf_cexpf(n, &x);
18 printf("C native complex: %12.8f + j%12.8f\n", crealf(x), cimagf(x));
19
20 // destroy nco object
21 nco_crcf_destroy(n);
22
23 printf("done.\n");
24 return 0;
25 }
1 // file: doc/listings/nco.cc
2 // build: g++ -c -o nco.cc.o nco.cc
3 // link: g++ nco.cc.o -o nco -lm -lc -lliquid
4
5 # include <iostream>
6 # include <math.h>
7
8 # include <liquid / liquid.h>
9
10 // NOTE: the definition for liquid_float_complex will change
11 // depending upon whether the standard C++ <complex>
12 // header file is included before or after including
13 // <liquid/liquid.h>
14 # include <complex>
15
16 int main() {
17 // create nco object and initialize
18 nco_crcf n = nco_crcf_create(LIQUID_NCO);
19 nco_crcf_set_phase(n,0.3f);
20
21 // Test liquid complex data type
22 liquid_float_complex x;
23 nco_crcf_cexpf(n, &x);
24 std::cout << "liquid complex: "
25 << x.real << " + j" << x.imag << std::endl;
3.6 Learning by example 9
26
27 // Test native c++ complex data type
28 std::complex<float> y;
29 nco_crcf_cexpf(n, reinterpret_cast<liquid_float_complex*>(&y));
30 std::cout << "c++ native complex: "
31 << y.real() << " + j" << y.imag() << std::endl;
32
33 // destroy nco object
34 nco_crcf_destroy(n);
35
36 std::cout << "done." << std::endl;
37 return 0;
38 }
It is important, however, to link the code with a C++ linker rather than a C linker. For example,
if the above program (nco.cc) is compiled with g++ it must also be linked with g++, viz
$ g++ -c -o nco.cc.o nco.cc
$ g++ nco.cc.o -o nco -lm -lc -lliquid
Part II
Tutorials
To get you started with using liquid signal processing library this manual begins with tutorials
rather than diving into the details of each signal processing module. The tutorials begin with
simple building blocks for signal processing by introducing a simple analog phase-locked loop which
tracks to the phase of an input complex sinusoid. The forward error-correction tutorial introduces
you to simple error correction and detection capabilities. The framing tutorial puts together data
encapsulation with digital modulation; with just a few lines of code you can convert a block of
raw, unencoded data bytes to frame samples ready to transmit over the air. Last but not least
is the OFDM framing tutorial which builds on the previous tutorial to use an orthogonal parallel
multiplexing communications scheme. Detailed examples of nearly every module can be found in
the examples directory.
11
• a terminal
The problem statement and a brief theoretical description of phase-locked loops is given in the next
section. A walk-through of the source code follows.
where (∗ ) denotes complex conjugation. 2 hose who are savvy with communications techniques
will appreciate that we are dealing in complex baseband and can easily compute the phase error
estimate simply as the argument of the product of xk and yk . Conventional PLLs which have
operated strictly in the real domain multiply only the real components of xk and yk for a phase
2
T
12 4 TUTORIAL: PHASE-LOCKED LOOP
error estimate, assume that the loop filter rejects the high-frequency component, and make the
approximation ∆φ ≈ sin(∆φ) = sin(φ − φ̂)for small phase errors. The goal of the receiver is to
control φk (the phase of the output signal y at time k) to lock onto the input phase of x, hence
the name ”phase-locked loop.” If the phase of the output sample yk is behind that of the input
(∆φ > 0) then φ needs to be advanced appropriately for the next time step. Conversely, if the
phase of yk is ahead of the phase of xk (∆φ < 0) then the receiver need to retard φ. Without going
into a great amount of detail, this control is accomplished using a special filter within the loop.
This filter, known as a ”loop filter,” is designed to reject high-frequency noise and is described with
the transfer function H(z). Specifically H(z) is a 2nd -order integrating low-pass recursive filter
with a natural frequency ωn , a damping factor ζ, and a loop gain K. The natural frequency is
the resonant frequency of H(z) and for all practical purposes is the filter’s bandwidth. Increasing
ωn permits the loop to track to the input signal faster (reduces lock time), but also increases the
amount of noise passed through the loop. Decreasing ωn reduces this noise but also increases the
loop’s acquisition √
time. The damping factor ζ controls the stability of the filter and is typically set
to a value near 1/ 2 ≈ 0.707. The loop gain K is typically very large (on the order of 1000 or so).
For more detailed information on loop filter design the interested reader is referred to § 21.2 . The
estimated phase error ∆φk is filtered using H(z)resulting in an output phase estimate φk+1 which
is used for the subsequent output sample yk+1 as
yk+1 = exp jφk+1 (4)
A summary of the algorithm is given in Algorithm 1 . In the next section we will create a simple
C program to simulate a phase-locked loop with [liquid].
The flag ”-Wall” tells the compiler to print all warnings (unused and uninitialized variables, etc.),
”-o pll” specifies the name of the output program is ”pll”, and ”-lm -lc -lliquid” tells the
linker to link the binary against the math, standard C, and [liquid] DSP libraries, respectively.
Notice that the above command invokes both the compiler and the linker collectively. If the
compiler did not give any errors, the output executable pll is created which can be run as
$ ./pll
and should simply print ”done.” to the screen. You are now ready to add functionality to your
program. We will now edit the file to set up the basic simulation but without controlling the
phase of the output sinusoid. As such the output won’t track to the input resulting in a significant
amount of phase error. This simulation will operate one sample at a time and is organized into
three sections. First, set up the simulation parameters: the initial phase and frequency offsets
(float), and number of samples to run (unsigned int). Next, initialize the complex input and
output variables (x and y) to zero, as well as the state of the phase error (phase error) and output
phase (phi hat) estimates. Finally, set up the computational loop which generates the input and
output samples, computes the phase error between them, and then prints the results to the screen.
Edit pll.c to set up the basic simulation:
1 // file: doc/tutorials/pll_basic_tutorial.c
2 # include <stdio.h>
3 # include <complex.h>
4 # include <math.h>
5 # include <liquid / liquid.h>
6
7 int main() {
8 // simulation parameters
9 float phase_offset = 0.8f; // initial phase offset
10 float frequency_offset = 0.01f; // initial frequency offset
11 unsigned int n = 40; // number of iterations
12
13 float complex x = 0; // input sample
14 float phase_error = 0; // phase error estimate
15 float phi_hat = 0; // output sample phase
16 float complex y = 0; // output sample
17
18 unsigned int i;
19 for (i=0; i<n; i++) {
20 // generate input sample
21 x = cexpf(_Complex_I*(phase_offset + i*frequency_offset));
14 4 TUTORIAL: PHASE-LOCKED LOOP
22
23 // generate output sample
24 y = cexpf(_Complex_I*phi_hat);
25
26 // compute phase error
27 phase_error = cargf(x*conjf(y));
28
29 // print results to screen
30 printf("%3u : phase = %12.8f, error = %12.8f\n", i, phi_hat, phase_error);
31 }
32
33 printf("done.\n");
34 return 0;
35 }
The variables x and y are of type float complex which contains both real and imaginary com-
ponents of type float. The function cexpf() computes the complex exponential of its argument
which for a purely imaginary input jα is simply ejα = cos α + j sin α. Compile and run the program
as before. The program should now output something like this:
Notice that because we aren’t controlling the output phase yet the error increases with the input
phase. In the next section we will design the loop filter to adjust the output phase to lock onto the
input signal given the phase error.
Typically the number of feedback and feedforward coefficients are equal (M = N ), and the co-
efficients themselves are normalized so that a0 = 1. [liquid] implements IIR filters with the
iirfilt xxxt family of objects where ”xxxt” denotes the type definition (see § 3 for details).
In our example we will be using the iirfilt rrrf object which indicates that this is an IIR filter
with real inputs, outputs, and coefficients with precision of type float. The IIR filter objects in [liq-
uid] maintain their state internally, storing the previous inputs and outputs in its internal buffers.
Nearly every object in [liquid] (filter or otherwise) has at least four basic methods: create(),
print(), execute(), and destroy(). For our program we will need to create the filter object by
passing to it a vector of each the feedback and feedforward coefficients. The infinite impulse re-
sponse (IIR) filter we are designing is of order two which means that a and b have three coefficients
each. Generating the loop filter coefficients is fairly straightforward. As stated before, the loop
filter has parameters for natural frequency ωn , damping factor ζ, and loop gain K. Furthermore
the filter is 2nd -order which means that it has three coefficients each for a and b. [liquid] provides
a method for computing such a filter with the iirdes pll active lag() function which accepts
ωn , ζ, and K as inputs and generates the coefficients in two output arrays. The coefficients can be
computed as follows:
float wn = 0.1f; // pll bandwidth
float zeta = 0.707f; // pll damping factor
float K = 1000.0f; // pll loop gain
float b[3]; // feedforward coefficients array
float a[3]; // feedback coefficients array
iirdes_pll_active_lag(wn, zeta, K, b, a);
The life cycle of the IIR filter can be summarized as follows
iirfilt_rrrf loopfilter = iirfilt_rrrf_create(b,3,a,3);
float sample_in = 0.0f;
float sample_out;
{
// repeat as necessary
iirfilt_rrrf_execute(loopfilter, sample_in, &sample_out);
}
iirfilt_rrrf_destroy(loopfilter);
noting that the execute() method can be repeated as many times as necessary before the object is
destroyed. Using the code snippets above, modify your program to include the loop filter to adjust
the output signal’s phase. The input to the filter will be the phase error variable, and its output
will be phi hat. Don’t forget to destroy your filter object once the loop has finished running.
16 4 TUTORIAL: PHASE-LOCKED LOOP
Compile the program as before, creating the executable ”pll.” Running the program should produce
an output similar to this:
iir filter [normal]:
b : 0.32277358 0.07999840 -0.24277516
a : 1.00000000 -1.99995995 0.99996001
0 : phase = 0.25821885, error = 0.80000001
1 : phase = 0.75852644, error = 0.55178112
2 : phase = 1.12857747, error = 0.06147351
3 : phase = 1.27319980, error = -0.29857749
4 : phase = 1.23918116, error = -0.43319979
...
35 : phase = 1.15999877, error = 0.00000751
36 : phase = 1.17000139, error = 0.00000122
37 : phase = 1.18000150, error = -0.00000131
38 : phase = 1.19000030, error = -0.00000140
39 : phase = 1.19999886, error = -0.00000024
done.
Notice that the phase error at the end of the output is very small. The initial error (at k = 0)
is 0.8 which is the value of the phase offset parameter at the beginning of the program. Notice
also that the difference in phase of the last several samples (i.e. the difference between the phase
at steps 38 and 39) is approximately 0.1 which is the initial frequency offset that was given in the
beginning. Play around with the input parameters, particularly the frequency offset and the phase-
locked loop bandwidth. Increasing the PLL bandwidth (wn) should reduce the resulting phase error
more quickly. The downside of having a PLL with a large bandwidth is that when the input signal
has been corrupted by noise then the phase error estimate is also noisy. In this tutorial no noise
term was introduced.
18 5 TUTORIAL: FORWARD ERROR CORRECTION
The flag ”-Wall” tells the compiler to print all warnings (unused and uninitialized variables, etc.),
”-o fec” specifies the name of the output program is ”fec”, and ”-lm -lc -lliquid” tells the
linker to link the binary against the math, standard C, and [liquid] DSP libraries, respectively.
5.2 Setting up the Environment 19
Notice that the above command invokes both the compiler and the linker collectively. If the
compiler did not give any errors, the output executable fec is created which can be run as
$ ./fec
and should simply print ”done.” to the screen. You are now ready to add functionality to your
program. We will now edit the file to set up the basic simulation by generating a message signal
and counting errors as a result of channel effects. The error-correction capabilities will be added in
the next section. First set up the simulation parameters: for now the only parameter will be the
length of the input message, denoted by the variable n (unsigned int) representing the number
of bytes. Initialize n to 8 to reflect an original message of 8 bytes. Create another unsigned
int variable k which will represent the length of the encoded message. This length is equal to
the original (n) with the additional redundancy. For now set k equal to n as we are not adding
FEC coding until the next section. This implies that without any redundancy, the receiver cannot
correct for any errors. Message data in [liquid] are represented as arrays of type unsigned char.
Allocate space for the original, encoded, and decoded messages as msg org[n], msg enc[k], and
msg dec[n], respectively. Initialize the original data message as desired. For example, the elements
in msg org can contain 0,1,2,...,n-1. Copy the contents of msg org to msg enc. This effectively
is a placeholder for forward error-correction which will be discussed in the next section. Corrupt
one of the bits in msg enc (e.g. msg enc[0]
verb|^ |= 0x01; will flip the least-significant bit in the first byte of the msg enc array), and copy
the results to msg dec. Print the encoded and decoded messages to the screen to verify that they
are not equal. Without any error-correction capabilities, the receiver should see a message different
than the original because of the corrupted bit. Count the number of bit differences between the
original and decoded messages. [liquid] provides a convenient interface for doing this and can be
invoked as
unsigned int num_bit_errors = count_bit_errors_array(msg_org,
msg_dec,
n);
Print this number to the screen. Your program should look similar to this:
1 // file: doc/tutorials/fec_basic_tutorial.c
2 # include <stdio.h>
3 # include <liquid / liquid.h>
4
5 int main() {
6 // simulation parameters
7 unsigned int n = 8; // original data length (bytes)
8
9 // compute size of encoded message
10 unsigned int k = n; // (no encoding yet)
11
12 // create arrays
13 unsigned char msg_org[n]; // original data message
14 unsigned char msg_enc[k]; // encoded/received data message
15 unsigned char msg_dec[n]; // decoded data message
16
17 unsigned int i;
20 5 TUTORIAL: FORWARD ERROR CORRECTION
18 // create message
19 for (i=0; i<n; i++) msg_org[i] = i & 0xff;
20
21 // "encode" message (copy to msg_enc)
22 for (i=0; i<n; i++) msg_enc[i] = msg_org[i];
23
24 // corrupt encoded message (flip bit)
25 msg_enc[0] ^= 0x01;
26
27 // "decode" message (copy to msg_dec)
28 for (i=0; i<n; i++) msg_dec[i] = msg_enc[i];
29
30 printf("original message: [%3u] ",n);
31 for (i=0; i<n; i++)
32 printf(" %.2X", msg_org[i]);
33 printf("\n");
34
Compile the program as before, creating the executable ”fec.” Running the program should produce
an output similar to this:
original message: [ 8] 00 01 02 03 04 05 06 07
decoded message: [ 8] 01 01 02 03 04 05 06 07
number of bit errors received: 1 / 64
Notice that the decoded message differs from the original and that the number of received errors
is nonzero.
has these methods; however the fec object replaces execute() with encode() and decode() as
the same object instance can be used for both encoding and decoding. The fec create() method
accepts two arguments, although the second one is basically ignored. The first argument is an
enumeration of the type of codec that you wish to use. To begin, create a new fec object of type
LIQUID FEC HAMMING74 (the second argument can simply be NULL) which creates a Hamming(7,4)
code:
fec q = fec_create(LIQUID_FEC_HAMMING74, NULL);
Details of the available codes in [liquid] can be found in § 13 . This codec nominally accepts
4 bits, appends 3 parity bits, and can detect and correct up to one of these seven transmitted
bits. The Hamming(7,4) code is not particularly strong for its rate; however it is computationally
efficient and has been studied extensively in coding theory. The interface provided by [liquid]
conveniently abstracts from the process of managing 8-bit data symbols (bytes), converting to 4-bit
input symbols, encoding to 7-bit output symbols, and then re-packing into 8-bit output bytes. This
is consistent with any forward error-correction code in [liquid]; as the user, you simply see data
bytes in and data bytes out. The length of the output sequence can be computed using the method
unsigned int k = fec_get_enc_msg_length(LIQUID_FEC_HAMMING74, n);
where n represents the number of uncoded input bytes and k represents the number of encoded
output bytes. This value should be used to appropriately allocate enough memory for the encoded
message. Encoding the data message is as simple as invoking
fec_encode(q, n, msg_org, msg_enc);
which uses our newly-created fec object q to encode n input bytes in the array msg org and store
the result in the output array msg enc. The interface for decoding is nearly identical:
fec_decode(q, n, msg_enc, msg_dec);
Notice that the second argument again represents the number of uncoded data bytes (n). Don’t
forget to destroy the object once you are finished:
fec_destroy(q);
22 unsigned int i;
23 // generate message
24 for (i=0; i<n; i++)
25 msg_org[i] = i & 0xff;
26
27 // encode message
28 fec_encode(q, n, msg_org, msg_enc);
29
30 // corrupt encoded message (flip bit)
31 msg_enc[0] ^= 0x01;
32
33 // decode message
34 fec_decode(q, n, msg_enc, msg_dec);
35
36 // DESTROY the fec object
37 fec_destroy(q);
38
Notice that the decoded message matches that of the original message, even though an error was
introduced at the receiver. As discussed above, the Hamming(7,4) code is not particularly strong;
if too many bits in the encoded message are corrupted then the decoder will be unable to correct
them. Play around with changing the length of the original data message, the encoding scheme,
and the number of errors introduced. For a more detailed program, see examples/fec example.c
in the main [liquid] directory. § 13 describes [liquid]’s FEC module in detail. Additionally, the
packetizer object extends the simplicity of the fec object by adding a cyclic redundancy check
and two layers of forward error-correction and interleaving, all of which can be reconfigured as
desired. See § 16.2 and examples/packetizer example.c for a detailed example program on how
to use the packetizer object.
24 6 TUTORIAL: FRAMING
signal level
g
in
ce
as
wn
en
up
d
ph
er
do
oa
qu
p
ad
e
yl
m
se
p
bl
he
pa
m
ra
ra
p/
ea
pr
time
6 Tutorial: Framing
In the previous tutorials we have created only the basic building blocks for wireless communication.
This tutorial puts them all together by introducing a very simple framing structure for sending and
receiving data over a wireless link. In this context ”framing” refers to the encapsulation of data into
a modulated time series at complex baseband to be transmitted over a wireless link. Conversely,
”packets” refer to packing raw message data bytes with forward error-correction and data validity
check redundancy.
special phasing sequences before any information-bearing symbols which aids in correcting for
carrier and timing offsets. Without going into great detail, these sequences significantly increase
the probability of frame detection and decoding while adding a minimal amount of overhead to the
frame; a small price to pay for increased data reliability!
The flag ”-Wall” tells the compiler to print all warnings (unused and uninitialized variables, etc.),
”-o framing” specifies the name of the output program is ”framing”, and ”-lm -lc -lliquid”
tells the linker to link the binary against the math, standard C, and [liquid] DSP libraries, respec-
tively. Notice that the above command invokes both the compiler and the linker collectively. If the
compiler did not give any errors, the output executable framing is created which can be run as
$ ./framing
and should simply print ”done.” to the screen. You are now ready to add functionality to your
program.
where in the file it should be written. Another application of the header is to include a destination
node identifier for use in packet routing for ad hoc networks. Both the header and payload are
assembled with a 16-bit cyclic redundancy check (CRC) to validate the integrity of the received data
and encoded using the Hamming(12,8) code for error correction. (see § 13 for more information
on error detection and correction capabilities in [liquid]). The encoded header and payload are
modulated with QPSK and encapsulated with a BPSK preamble. Finally, the resulting symbols
are interpolated using a square-root Nyquist matched filter at a rate of 2 samples per symbol. This
entire process is handled internally so that as a user the only thing you will need to do is call one
function. The framegen64 object can be generated with the framegen64 create() method which
accepts two arguments: an unsigned int and a float representing the matched filter’s length (in
symbols) and excess bandwidth factor, respectively. To begin, create a frame generator having a
square-root Nyquist filter with a delay of 3 and an excess bandwidth factor of 0.7 as
framegen64 fg = framegen64_create(3, 0.7);
As with all structures in [liquid] you will need to invoke the corresponding destroy() method when
you are finished with the object. Now allocate memory for the header and payload data arrays,
remembering that they have lengths 12 and 64, respectively. Raw ”message” data are stored as
arrays of type unsigned char in [liquid].
unsigned char header[12];
unsigned char payload[64];
Finally you will need to create a buffer for storing the frame samples. For this framing structure
you will need to allocate 1280 samples of type float complex, viz
float complex y[1280];
Initialize the header and payload arrays with whatever values you wish. All that is needed to
generate a frame is to invoke the frame generator’s execute() method:
framegen64_execute(fg, header, payload, y);
That’s it! This completely assembles the frame complete with interpolation and is ready for up-
conversion and transmission. To generate another frame simply write whatever data you wish
to the header and payload buffers, and invoke the framegen64 execute() method again as done
above. If you wish, print the first few samples of the generated frame to the screen (you will need
to separate the real and imaginary components of each sample).
for (i=0; i<30; i++)
printf("%3u : %12.8f + j*%12.8f\n", i, crealf(y[i]), cimagf(y[i]));
8 int main() {
9 // allocate memory for arrays
10 unsigned char header[8]; // data header
11 unsigned char payload[64]; // data payload
12 float complex y[1340]; // frame samples
13
14 // CREATE frame generator
15 framegen64 fg = framegen64_create();
16 framegen64_print(fg);
17
18 // initialize header, payload
19 unsigned int i;
20 for (i=0; i<8; i++)
21 header[i] = i;
22 for (i=0; i<64; i++)
23 payload[i] = rand() & 0xff;
24
28 : -1.09209442 + j* 0.00000000
29 : 0.03534408 + j* 0.00000000
done.
You might notice that the imaginary component of the samples in the beginning of the frame are
zero. This is because the preamble of the frame is BPSK which has no imaginary component at
complex baseband.
• props is a construct that defines the specific properties of the frame synchronizer. This in-
cludes loop bandwidths for carrier, timing, and gain recovery, as well as squelch and equalizer
control. You may pass the value NULL to use the default parameters (recommended for now).
• callback is a pointer to your callback function which will be invoked each time a frame is
found and decoded.
• userdata is a void pointer that is passed to the callback function each time it is invoked.
This allows you to easily pass data from the callback function. Set to NULL if you don’t wish
to use this.
The framesync64 object has a callback function which has six arguments and looks like this:
int framesync64_callback(unsigned char * _header,
int _header_valid,
unsigned char * _payload,
int _payload_valid,
6.5 Putting it All Together 29
framesyncstats_s _stats,
void * _userdata);
The callback is typically defined to be static and is passed to the instance of framesync64 object
when it is created.
• header is a pointer to the 12 bytes of decoded header data. This pointer is not static and
cannot be used after returning from the callback function. This means that it needs to be
copied locally for you to retain the data.
• header valid is simply a flag to indicate if the header passed its cyclic redundancy check
(”0” means invalid, ”1” means valid). If the check fails then the header data most likely has
been corrupted beyond the point that the internal error-correction code can recover; proceed
with caution!
• payload is a pointer to the 64 bytes of decoded payload data. Like the header, this pointer
is not static and cannot be used after returning from the callback function. Again, this means
that it needs to be copied locally for you to retain the data.
• payload valid is simply a flag to indicate if the payload passed its cyclic redundancy check
(”0” means invalid, ”1” means valid). As with the header, if this flag is zero then the payload
most likely has errors in it. Some applications are error tolerant and so it is possible that
the payload data are still useful. Typically, though, the payload should be discarded and a
re-transmission request should be issued.
• stats is a synchronizer statistics construct that indicates some useful PHY information to
the user. We will ignore this information in our program, but it can be quite useful for certain
applications. For more information on the framesyncstats s structure, see § 16.6 .
• userdata Remember that void pointer you passed to the create() method? That pointer
is passed to the callback and can represent just about anything. Typically it points to another
structure and is the method by which the decoded header and payload data are returned to
the program outside of the callback.
This can seem a bit overwhelming at first, but relax! The next version of our program will only
add about 20 lines of code.
framesync64 fs = framesync64_create(NULL,
mycallback,
NULL);
framesync64_print(fs);
After your line that generates the frame samples (”framegen64 execute(fg, header, payload,
y);”) invoke the synchronizer’s execute() method, passing to it the frame synchronizer object you
just created (fs), the pointer to the array of frame symbols (y), and the length of the array (1280):
framesync64_execute(fs, y, 1280);
Finally, destroy the frame synchronizer object along with the frame generator at the end of the file.
That’s it! Your program should look something like this:
1 // file: doc/tutorials/framing_intermediate_tutorial.c
2 # include <stdio.h>
3 # include <stdlib.h>
4 # include <math.h>
5 # include <complex.h>
6 # include <liquid / liquid.h>
7
8 // user-defined static callback function
9 static int mycallback(unsigned char * _header,
10 int _header_valid,
11 unsigned char * _payload,
12 unsigned int _payload_len,
13 int _payload_valid,
14 framesyncstats_s _stats,
15 void * _userdata)
16 {
17 printf("***** callback invoked!\n");
18 return 0;
19 }
20
21 int main() {
22 // allocate memory for arrays
23 unsigned char header[8]; // data header
24 unsigned char payload[64]; // data payload
25 float complex y[1340]; // frame samples
26
27 // create frame generator
28 framegen64 fg = framegen64_create();
29 framegen64_print(fg);
30
31 // create frame synchronizer using default properties
32 framesync64 fs = framesync64_create(mycallback, NULL);
33 framesync64_print(fs);
34
35 // initialize header, payload
36 unsigned int i;
37 for (i=0; i<8; i++)
38 header[i] = i;
6.5 Putting it All Together 31
Compile and run your program as before and verify that your callback function was indeed invoked.
Your output should look something like this:
As you can see, the framesync64 object has a long list of modifiable properties pertaining to
synchronization; the default values provide a good initial set for a wide range of channel conditions.
Duplicate the line of your code that executes the frame synchronizer. Recompile and run your
code again. You should see the ”***** callback invoked!” printed twice. Your program has
only demonstrated the basic functionality of the frame generator and synchronizer under ideal
conditions: no noise, carrier offsets, etc. The next section will add some channel impairments to
stress the synchronizer’s ability to decode the frame.
32 6 TUTORIAL: FRAMING
The frame counter variable is simply a number we will pass to the callback function to demonstrate
the functionality of the userdata construct. Make sure to initialize frame counter to zero. If you
completed the tutorial on phase-locked loop design you might recognize the phase offset and
frequency offset variables; these will be used in the same way to represent a carrier mismatch
between the transmitter and receiver. The channel gain and noise parameters are a bit trickier
and are set up by the next two lines. Typically the noise power is a fixed value in a receiver;
what changes is the received power based on the transmitter’s power and the gain of the channel;
however because theory dictates that the performance of a link is governed by the ratio of signal
power to noise power, SNR is a more useful than defining signal amplitude and noise variance
independently. The SNRdB and noise floor parameters fully describe the channel in this regard.
The noise standard deviation and channel gain may be derived from these values as follows:
float nstd = powf(10.0f, noise_floor/20.0f);
float gamma = powf(10.0f, (SNRdB+noise_floor)/20.0f);
Add to your program (after the framegen64 execute() line) a loop that modifies each sample of
the generated frame by introducing the channel impairments.
where yi is the frame sample at index i (y[i]), γ is the channel gain defined above (gamma), θ is the
carrier phase offset (phase offset), ω is the carrier frequency offset (frequency offset), σ is the
noise standard deviation defined above (nstd), and n is a circular Gauss random variable. [liquid]
provides the randnf() methods to generate real random numbers with a Gauss distribution; a
circular Gauss random√variable can be generated from two regular Gauss random variables ni and
nq as n = (ni + jnq )/ 2.
y[i] *= gamma;
y[i] *= cexpf(_Complex_I*(phase_offset + i*frequency_offset));
y[i] += nstd * (randnf() + _Complex_I*randnf())*0.7071;
Check the program listed below if you need help. Now modify the program to incorporate the frame
counter. First modify the piece of code where the frame synchronizer is created: replace the last
argument (initially set to NULL) with the address of our frame counter variable. For posterity’s
6.6 Final Program 33
sake, this address will need to be type cast to void* (a void pointer) to prevent the compiler from
complaining. In your callback function you will reverse this process: create a new variable of type
unsigned int* (a pointer to an unsigned integer) and assign it the userdata argument type cast
to unsigned int*. Now de-reference this variable and increment its value. Finally print its value
near the end of the main() definition to ensure it is being properly incremented. Again, check the
program below for assistance. The last task we will do is push one sample at a time to the frame
synchronizer rather than the entire frame block to emulate non-contiguous sample streaming. To
do this, simply remove the line that calls framesync64 execute() on the entire frame and replace
it with a loop that calls the same function but with one sample at a time. The final program is
listed below, and a copy of the source is located in the doc/tutorials/ subdirectory.
1 // file: doc/tutorials/framing_tutorial.c
2 # include <stdio.h>
3 # include <stdlib.h>
4 # include <math.h>
5 # include <complex.h>
6 # include <liquid / liquid.h>
7
25 return 0;
26 }
27
28 int main() {
29 // options
30 unsigned int frame_counter = 0; // userdata passed to callback
31 float phase_offset=0.3f; // carrier phase offset
32 float frequency_offset=0.02f; // carrier frequency offset
33 float SNRdB = 10.0f; // signal-to-noise ratio [dB]
34 float noise_floor = -40.0f; // noise floor [dB]
35
36 // allocate memory for arrays
37 unsigned char header[8]; // data header
38 unsigned char payload[64]; // data payload
39 float complex y[1340]; // frame samples
34 6 TUTORIAL: FRAMING
40
41 // create frame generator
42 framegen64 fg = framegen64_create();
43 framegen64_print(fg);
44
...
***** callback invoked!
header (valid)
payload (valid)
received 1 frames
done.
Play around with the initial options, particularly those pertaining to the channel impairments. Un-
der what circumstances does the synchronizer miss the frame? For example, what is the minimum
SNR level that is required to reliably receive a frame? the maximum carrier frequency offset? The
”random” noise generated by the program will be seeded to the same value every time the program
is run. A new seed can be initialized on the system’s time (e.g. time of day) to help generate
new instances of random numbers each time the program is run. To do so, include the <time.h>
header to the top of your file and add the following line to the beginning of your program’s main()
definition:
srand(time(NULL));
This will ensure a unique simulation is run each time the program is executed. For a more detailed
program, see examples/framesync64 example.c in the main [liquid] directory. § 16 describes
[liquid]’s framing module in detail. While the framing structure described in this section pro-
vides a simple interface for transmitting and receiving data over a channel, its functionality is
limited and isn’t particularly spectrally efficient. [liquid] provides a more robust framing struc-
ture which allows the use of any linear modulation scheme, two layers of forward error-correction
coding, and a variable preamble and payload length. These properties can be reconfigured for
each frame to allow fast adaptation to quickly varying channel conditions. Furthermore, the
frame synchronizer on the receiver automatically reconfigures itself for each frame it detects to
allow as simple an interface possible. The frame generator and synchronizer objects are denoted
flexframegen and flexframesync, respectively, and are described in § 16 . A detailed example
program examples/flexframesync example.c is available in the main [liquid] directory.
36 7 TUTORIAL: OFDM FRAMING
dependent upon the choice of the number, type, and allocation of subcarriers. Furthermore,
the OFDM frame generator permits many different parameters (e.g. modulation/coding schemes,
payload length) which are detected automatically at the receiver without any work on your part.
stdlib.h, math.h, complex.h, and liquid/liquid.h. Add the int main() definition so that
your program looks like this:
1 // file: doc/tutorials/ofdmflexframe_init_tutorial.c
2 # include <stdio.h>
3 # include <stdlib.h>
4 # include <math.h>
5 # include <complex.h>
6 # include <liquid / liquid.h>
7
8 int main() {
9 printf("done.\n");
10 return 0;
11 }
The flag ”-Wall” tells the compiler to print all warnings (unused and uninitialized variables, etc.),
”-o ofdmflexframe” specifies the name of the output program is ”ofdmflexframe”, and ”-lm
-lc -lliquid” tells the linker to link the binary against the math, standard C, and [liquid] DSP
libraries, respectively. Notice that the above command invokes both the compiler and the linker
collectively. If the compiler did not give any errors, the output executable ofdmflexframe is created
which can be run as
$ ./ofdmflexframe
and should simply print ”done.” to the screen. You are now ready to add functionality to your
program.
• t is an unsigned int representing the length of the overlapping tapered window between
OFDM symbols (0≤t≤c).
• p is an M -element array of unsigned char which gives the subcarrier allocation (e.g. which
subcarriers are nulled/disabled, which are pilots, and which carry data). Setting to NULL
tells the ofdmflexframegen object to use the default subcarrier allocation (see § 16.7.2 for
details);
• props is a special structure called ofdmflexframegenprops s which gives some basic prop-
erties including the inner/outer forward error-correction scheme(s) to use (fec0, fec1), and
the modulation scheme (mod scheme) and depth (mod depth). The properties object can be
initialized to its default by using ofdmflexframegenprops init default().
To begin, create a frame generator having 64 subcarriers with cyclic prefix of 16 samples, a tapering
window of 4 samples, the default subcarrier allocation, and default properties as
// create frame generator with default parameters
ofdmflexframegen fg = ofdmflexframegen_create(64, 16, 4, NULL, NULL);
As with all structures in [liquid] you will need to invoke the corresponding destroy() method when
you are finished with the object. Now allocate memory for the header (8 bytes) and payload (120
bytes) data arrays. Raw ”message” data are stored as arrays of type unsigned char in [liquid].
unsigned char header[8];
unsigned char payload[120];
Initialize the header and payload arrays with whatever values you wish. Finally you will need
to create a buffer for storing the frame samples. Unlike the framegen64 object in the previous
tutorial which generates the entire frame at once, the ofdmflexframegen object generates each
symbol independently. For this framing structure you will need to allocate M + c samples of type
float complex (for this example M + c = 64 + 16 = 80), viz.
float complex buffer[80];
Generating the frame consists of two steps: assemble and write. Assembling the frame simply
involves invoking the ofdmflexframegen assemble(fg,header,payload,payload len) method
which accepts the frame generator object as well as the header and payload arrays we initialized
earlier. Internally, the object encodes and modulates the frame, but does not write the OFDM sym-
bols yet. To write the OFDM time-series symbols, invoke the ofdmflexframegen writesymbol()
method. This method accepts three arguments: the frame generator object, the output buffer we
created earlier, and the pointer to an integer to indicate the number of samples that have been
written to the buffer. The last argument is necessary because not all of the symbols in the frame
7.4 Creating the Frame Generator 39
are the same size (the first several symbols in the preamble do not have a cyclic prefix). Invoking
the ofdmflexframegen writesymbol() method repeatedly generates each symbol of the frame and
returns a flag indicating if the last symbol in the frame has been written. Add the instructions to
assemble and write a frame one symbol at a time to your source code:
// assemble the frame and print
ofdmflexframegen_assemble(fg, header, payload, payload_len);
ofdmflexframegen_print(fg);
// print status
printf("ofdmflexframegen wrote
num_written,
last_symbol ? "(last symbol)" : "");
}
That’s it! This completely assembles the frame complete with error-correction coding, pilot sub-
carriers, and the preamble necessary for synchronization. You may generate another frame simply
by initializing the data in your header and payload arrays, assembling the frame, and then writing
the symbols to the buffer. Keep in mind, however, that the buffer is overwritten each time you
invoke ofdmflexframegen writesymbol(), so you will need to do something with the data with
each iteration of the loop. Your program should now look similar to this:
1 // file: doc/tutorials/ofdmflexframe_basic_tutorial.c
2 # include <stdio.h>
3 # include <stdlib.h>
4 # include <math.h>
5 # include <complex.h>
6 # include <liquid / liquid.h>
7
8 int main() {
9 // options
10 unsigned int M = 64; // number of subcarriers
11 unsigned int cp_len = 16; // cyclic prefix length
12 unsigned int taper_len = 4; // taper length
13 unsigned int payload_len = 120; // length of payload (bytes)
14
15 // allocate memory for header, payload, sample buffer
16 unsigned int symbol_len = M + cp_len; // samples per OFDM symbol
17 float complex buffer[symbol_len]; // time-domain buffer
18 unsigned char header[8]; // header
19 unsigned char payload[payload_len]; // payload
20
21 // create frame generator object with default properties
22 ofdmflexframegen fg = ofdmflexframegen_create(M, cp_len, taper_len, NULL, NULL);
40 7 TUTORIAL: OFDM FRAMING
23
24 unsigned int i;
25
26 // initialize header/payload and assemble frame
27 for (i=0; i<8; i++) header[i] = i & 0xff;
28 for (i=0; i<payload_len; i++) payload[i] = rand() & 0xff;
29 ofdmflexframegen_assemble(fg, header, payload, payload_len);
30 ofdmflexframegen_print(fg);
31
32 // generate frame one OFDM symbol at a time
33 int last_symbol=0;
34 while (!last_symbol) {
35 // generate symbol (write samples to buffer)
36 last_symbol = ofdmflexframegen_writesymbol(fg, buffer);
37
38 // print status
39 printf("ofdmflexframegen wrote %3u samples %s\n",
40 symbol_len,
41 last_symbol ? "(last symbol)" : "");
42 }
43
44 // destroy objects and return
45 ofdmflexframegen_destroy(fg);
46 printf("done.\n");
47 return 0;
48 }
Notice that the ofdmflexframegen print() method gives a lot of information, including the num-
ber of null, pilot, and data subcarriers, the number of modulated symbols, the number of OFDM
symbols, and the resulting spectral efficiency. Furthermore, notice that the first three symbols
have only 64 samples while the remaining have 80; these first three symbols are actually part of
the preamble to help the synchronizer detect the presence of a frame and estimate symbol timing
and carrier frequency offsets.
• t is an unsigned int representing the length of the overlapping tapered window between
OFDM symbols (0≤t≤c).
• p is an M -element array of unsigned char which gives the subcarrier allocation (see § 7.4 )
• callback is a pointer to your callback function which will be invoked each time a frame is
found and decoded.
• userdata is a void pointer that is passed to the callback function each time it is invoked.
This allows you to easily pass data from the callback function. Set to NULL if you don’t wish
to use this.
Notice that the first three arguments are the same as in the ofdmflexframegen create() method;
the values of these parameters at the synchronizer need to match those at the transmitter in order
for the synchronizer to operate properly. When the synchronizer does find a frame, it attempts to
decode the header and payload and invoke a user-defined callback function. 6 basic description of
how callback functions work is given in the basic framing tutorial in § 6.4 . The callback function
for the ofdmflexframesync object has seven arguments and looks like this:
6
a
42 7 TUTORIAL: OFDM FRAMING
The callback is typically defined to be static and is passed to the instance of ofdmflexframesync
object when it is created. Here is a brief description of the callback function’s arguments:
• header is a pointer to the 8 bytes of decoded header data (remember that header[8] array
you created with the ofdmflexframegen object?). This pointer is not static and cannot be
used after returning from the callback function. This means that it needs to be copied locally
before returning in order for you to retain the data.
• header valid is simply a flag to indicate if the header passed its cyclic redundancy check
(”0” means invalid, ”1” means valid). If the check fails then the header data have been
corrupted beyond the point that internal error correction can recover; in this situation the
payload cannot be recovered.
• payload is a pointer to the decoded payload data. Like the header, this pointer is not static
and cannot be used after returning from the callback function. Again, this means that it
needs to be copied locally for you to retain the data. When the header cannot be decoded
( header valid == 0) this value is set to NULL.
• payload len is the length (number of bytes) of the payload array. When the header cannot
be decoded ( header valid == 0) this value is set to 0.
• payload valid is simply a flag to indicate if the payload passed its cyclic redundancy check
(”0” means invalid, ”1” means valid). As with the header, if this flag is zero then the payload
almost certainly contains errors.
• stats is a synchronizer statistics construct that indicates some useful PHY information to
the user (such as RSSI and EVM). We will ignore this information in our program, but it
can be quite useful for certain applications. For more information on the framesyncstats s
structure, see § 16.6 .
and just print a message to the screen that indicates that the callback has been invoked, and return
the integer zero (0). This return value for the callback function should always be zero and is reserved
for future development. Within your main() definition, create an instance of ofdmflexframesync
using the ofdmflexframesync create() method, passing it 64 for the number of subcarriers, 16
for the cyclic prefix length, NULL for the subcarrier allocation (default), mycallback, and NULL for
the userdata. Print the newly created synchronizer object to the screen if you like:
ofdmflexframesync fs = ofdmflexframesync_create(64, 16, 4, NULL, mycallback, NULL);
Within the while loop that writes the frame symbols to the buffer, invoke the synchronizer’s
execute() method, passing to it the frame synchronizer object you just created (fs), the buffer of
frame symbols, and the number of samples written to the buffer (num written):
ofdmflexframesync_execute(fs, buffer, num_written);
Finally, destroy the frame synchronizer object along with the frame generator at the end of the file.
That’s it! Your program should look something like this:
1 // file: doc/tutorials/ofdmflexframe_intermediate_tutorial.c
2 # include <stdio.h>
3 # include <stdlib.h>
4 # include <math.h>
5 # include <complex.h>
6 # include <liquid / liquid.h>
7
8 // callback function
9 int mycallback(unsigned char * _header,
10 int _header_valid,
11 unsigned char * _payload,
12 unsigned int _payload_len,
13 int _payload_valid,
14 framesyncstats_s _stats,
15 void * _userdata)
16 {
17 printf("***** callback invoked!\n");
18 printf(" header (%s)\n", _header_valid ? "valid" : "INVALID");
19 printf(" payload (%s)\n", _payload_valid ? "valid" : "INVALID");
20 return 0;
21 }
22
23 int main() {
24 // options
25 unsigned int M = 64; // number of subcarriers
26 unsigned int cp_len = 16; // cyclic prefix length
27 unsigned int taper_len = 4; // taper length
28 unsigned int payload_len = 120; // length of payload (bytes)
29
30 // allocate memory for header, payload, sample buffer
31 unsigned int symbol_len = M + cp_len; // samples per OFDM symbol
32 float complex buffer[symbol_len]; // time-domain buffer
33 unsigned char header[8]; // header
44 7 TUTORIAL: OFDM FRAMING
51 ofdmflexframegen_print(fg);
52 ofdmflexframesync_print(fs);
53
54 // generate frame and synchronize
55 int last_symbol=0;
56 while (!last_symbol) {
57 // generate symbol (write samples to buffer)
58 last_symbol = ofdmflexframegen_writesymbol(fg, buffer);
59
60 // receive symbol (read samples from buffer)
61 ofdmflexframesync_execute(fs, buffer, symbol_len);
62 }
63
64 // destroy objects and return
65 ofdmflexframegen_destroy(fg);
66 ofdmflexframesync_destroy(fs);
67 printf("done.\n");
68 return 0;
69 }
Compile and run your program as before and verify that your callback function was indeed invoked.
Your output should look something like this:
ofdmflexframegen:
...
ofdmflexframesync:
num subcarriers : 64
* NULL : 14
* pilot : 6
* data : 44
cyclic prefix len : 16
taper len : 4
***** callback invoked!
header (valid)
7.7 Final Program 45
payload (valid)
done.
Your program has demonstrated the basic functionality of the OFDM frame generator and syn-
chronizer. The previous tutorial on framing (§ 6 ) added a carrier offset and noise to the signal
before synchronizing; these channel impairments are addressed in the next section.
The first five options define which modulation, coding, and error-checking schemes should be
used in the framing structure. The dphi and SNRdB are the carrier frequency offset (∆φ) and
signal-to-noise ratio (in decibels), respectively. To change the framing generator properties, cre-
ate an instance of the ofdmflexframegenprops s structure, query the current properties list with
ofdmflexframegen getprops(), override with the properties of your choice, and then reconfigure
the frame generator with ofdmflexframegen setprops(), viz.
// re-configure frame generator with different properties
ofdmflexframegenprops_s fgprops;
ofdmflexframegen_getprops(fg, &fgprops);
fgprops.check = check;
fgprops.fec0 = fec0;
fgprops.fec1 = fec1;
fgprops.mod_scheme = ms;
ofdmflexframegen_setprops(fg, &fgprops);
Add this code somewhere after you create the frame generator, but before you assemble the frame.
Adding channel impairments can be a little tricky. We have specified the signal-to-noise ratio in
decibels (dB) but need to compute the equivalent noise standard deviation. Assuming that the
signal power is unity, the noise standard deviation is just σn = 10−SNRdB/20 . The carrier frequency
offset can by synthesized with a phase variable that increases by a constant for each sample, k.
That is, φk = φk−1 + ∆φ. Each sample in the buffer can be multiplied by the resulting complex
sinusoid generated by this phase, with noise added to the result:
Initialize the variables for noise standard deviation and carrier phase before the while loop as
46 7 TUTORIAL: OFDM FRAMING
Create an inner loop (inside the while loop) that modifies the contents of the buffer after the frame
generator, but before the frame synchronizer:
// channel impairments
for (i=0; i < num_written; i++) {
buffer[i] *= cexpf(_Complex_I*phi); // apply carrier offset
phi += dphi; // update carrier phase
cawgn(&buffer[i], nstd); // add noise
}
23 int main() {
24 // options
25 unsigned int M = 64; // number of subcarriers
26 unsigned int cp_len = 16; // cyclic prefix length
27 unsigned int taper_len = 4; // taper length
28 unsigned int payload_len = 120; // length of payload (bytes)
29 modulation_scheme ms = LIQUID_MODEM_PSK8; // payload modulation scheme
30 fec_scheme fec0 = LIQUID_FEC_NONE; // inner FEC scheme
31 fec_scheme fec1 = LIQUID_FEC_HAMMING128; // outer FEC scheme
32 crc_scheme check = LIQUID_CRC_32; // data validity check
33 float dphi = 0.001f; // carrier frequency offset
34 float SNRdB = 20.0f; // signal-to-noise ratio [dB]
35
36 // allocate memory for header, payload, sample buffer
37 unsigned int symbol_len = M + cp_len; // samples per OFDM symbol
7.7 Final Program 47
89 ofdmflexframegen_destroy(fg);
90 ofdmflexframesync_destroy(fs);
91 printf("done.\n");
92 return 0;
93 }
Run this program to verify that the frame is indeed detected and the payload is received free of
errors. For a more detailed program, see examples/ofdmflexframesync example.c in the main
[liquid] directory; this example also demonstrates setting different properties of the frame, but
permits options to be passed to the program from the command line, rather than requiring the
program to be re-compiled. Play around with various combinations of options in the program,
such as increasing the number of subcarriers, modifying the modulation scheme, decreasing the
signal-to-noise ratio, and applying different forward error-correction schemes. What happens to
the spectral efficiency of the frame when you increase the payload from 120 bytes to 400? 7
the spectral efficiency increases from 0.5455 to 0.8511 because the preamble accounts for less of
frame (less overhead) when you decrease the cyclic prefix from 16 samples to 4? 8 the spectral
efficiency increases from 0.5455 to 0.6417 because fewer samples are used for each OFDM symbol
(less overhead) when you increase the number of subcarriers from 64 to 256? 9 the spectral
efficiency decreases from 0.5455 to 0.4412 because the preamble accounts for more of the frame
(increased overhead). What happens when the frame generator is created with 64 subcarriers and
the synchronizer is created with only 62? 10 the synchronizer cannot detect frame because the
subcarriers don’t match (different pilot locations, etc. when the cyclic prefix lengths don’t match?
11 the synchronizer cannot decode header because of symbol timing mis-alignment. What happens
when you decrease the SNR from 20 dB to 10 dB? 12 the payload will probably be invalid because of
too many errors. when you decrease the SNR to 0 dB? 13 the frame header will probably be invalid
because of too many errors. when you decrease the SNR to -10 dB? 14 the frame synchronizer will
probably miss the frame entirely because of too much noise. What happens when you increase the
carrier frequency offset from 0.001 to 0.1? 15 the frame isn’t detected because the carrier offset is
too large for the synchronizer to correct. Try decreasing the number of subcarriers from 64 to 32
and see what happens.
7
A:
8
A:
9
A:
10
A:
11
A:
12
A:
13
A:
14
A:
15
A:
49
Part III
Modules
Source code for [liquid] is organized into modules which are, for the most part, self-contained
elements. The following sections describe these modules in detail with some basic theory behind
their operation, functional interface description, and example code.
50 8 AGC (AUTOMATIC GAIN CONTROL)
output energy
target energy, ē
input energy
e0 e1
For received communications signals, however, the goal is to adjust to the gain of the receiver
relative to the slowly-varying amplitude of the incoming receiver due to shadowing, path loss, etc.
Furthermore, it is impractical to estimate the signal energy of an entire vector for each sample of
input. Therefore it is necessary to make an estimate of the signal energy over a short period of
time. This is accomplished by computing the average of only the previous M samples of |x|2 ; liquid
uses an internal buffer size of M = 16samples. Now that the short-time signal energy has been
estimated, all that remains is to adjust the gain of the receiver accordingly. liquid implements an
open-loop gain control by adjusting the instantaneous gain value to match the estimated signal
energy to drive the output level to unity. The loop filter for the gain is a first-order recursive
low-pass filter with the transfer function defined as
α
Hg (z) = (11)
1 − (1 − α)z −1
√
where α , ω. In order to achieve a unity target energy, the instantaneous ideal gain is therefore
the inverse of the estimated signal level,
p
ĝk = 1/êk (12)
Rather than applying the gain directly to the input signal it is first filtered as
8.2 Locking
The agc object permits the gain to be locked when, for example, the header of a frame has been
received. This is useful for effectively switching the AGC on and off during short, burst-mode
frame transmissions, particularly when the signal has a high-order digital amplitude-modulation
(e.g. 64-QAM) and fluctuations in the AGC could potentially result in symbol errors. When the
agc object is locked, the internal gain control is not updated, and the internal gain at the time
of locking is applied directly to the output signal, forcing gk = gk−1 . Locking and unlocking is
accomplished with the agc crcf lock() and agc crcf unlock() methods, respectively.
8.3 Squelch
The agc object contains internal squelch control to allow the receiver the ability to disable signal
processing when the signal level is too low. In traditional radio design, the squelch circuit suppressed
the output of a receiver when the signal strength would fall below a certain level, primarily used
to prevent audio static due to noise when no other operators were transmitting. Having said that,
the squelch control in liquid is actually somewhat of a misnomer as it does not actually control
the AGC, but rather just monitors the dynamics of the signal level and returns its status to the
controlling unit. The squelch control follows six states—enabled, rising edge trigger, signal high,
falling edge trigger, signal low, and timeout—as depicted in Figure 2 and Table 1 . These states
give the user flexibility in programming networks where frames are transmitted in short bursts
52 8 AGC (AUTOMATIC GAIN CONTROL)
signal strength
timeout
squelch threshold
noise floor
time
and the receiver needs to synchronize quickly. The status of the squelch control is retrieved via
the agc crcf squelch get status() method. The typical control cycle for the AGC squelch is
depicted in Figure 2 . Initially, squelch is enabled (code 0) as the signal has been low for quite some
time. When the beginning of a frame is received, the RSSI increases beyond the squelch threshold
(code 1). All subsequent samples above this threshold return a signal high” status (code 2). Once
the signal level falls below the threshold, the squelch returns a falling edge trigger” status (code
3). All subsequent samples below the threshold until timing out return a signal low” status (code
4). When the signal has been low for a sufficient period of time (defined by the user), the squelch
will return a timeout” status (code 5). All subsequent samples below the threshold will return a
squelch enabled” status.
8.4 Methodology
The reason for all six states (as opposed to just squelch on” and squelch off”) are to allow for the
AGC to adjust to complex signal dynamics. The default operation for the AGC is to disable the
8.5 auto-squelch 53
squelch. For example if the AGC squelch control is in signal low” mode (state 4) and the signal
increases above the threshold before timeout, the AGC will move back to the signal high” mode
(state 2). This is particularly useful for weak signals whose received signal strength is hovering
around the squelch threshold; it would be undesirable for the AGC to enable the squelch in the
middle of receiving a frame!
8.5 auto-squelch
The AGC module also allows for an auto-squelch mechanism which attempts to track the sig-
nal threshold to the noise floor of the receiver. This is accomplished by monitoring the signal
level when squelch is enabled. The auto-squelch mechanism has a 4dB headroom; if the signal
level drops below 4dB beneath the squelch threshold, the threshold will be decremented. This
is useful for receiving weak signals slightly above the noise floor, particularly when the exact
noise floor is not known or varies slightly over time. Auto-squelch is enabled/disabled using the
agc crcf squelch enable auto() and agc crcf squelch disable auto() methods respectively.
8.6 Interface
Listed below is the full interface to the agc family of objects. While each method is listed for the
agc crcf object, the same functionality applies to the agc rrrf object.
• agc crcf create() creates an agc object with default parameters. By default the minimum
gain is 10−6 , the maximum gain is 106 , the initial gain is 1, and the estimate of the input
signal level is 0. Also the AGC type is set to LIQUID AGC DEFAULT.
• agc crcf destroy(q) destroys the object, freeing all internally-allocated memory.
• agc crcf reset(q) resets the state of the agc object. This unlocks the AGC and clears the
estimate of the input signal level.
• agc crcf set gain limits(q,gmin,gmax) sets the minimum and maximum gain values, re-
spectively. This effectively specifies e0 and e1 as in Figure 1 .
• agc crcf lock(q) prevents the AGC from updating its gain estimate. The internal gain is
stored at the time of lock and used for all subsequent occurrences of agc crcf execute().
This is primarily used when the beginning of a frame has been detected, and perhaps the
payload contains amplitude-modulated data which can be corrupted with the AGC aggres-
sively attacking the signal’s high dynamics. Also, locking the AGC conserves clock cycles as
the gain update is not computed. Typically, the locked AGC consumes about 5× fewer clock
cycles than its unlocked state.
• agc crcf unlock(q) unlocks the AGC from a locked state and resumes estimating the input
signal level and internal gain.
• agc crcf execute(q,x,y) applies the gain to the input x, storing in the output sample yand
updates the AGC’s internal tracking loops (of not locked).
54 8 AGC (AUTOMATIC GAIN CONTROL)
• agc crcf get signal level(q) returns a linear estimate of the input signal’s energy level.
• agc crcf get rssi(q) returns an estimate of the input signal’s energy level in dB.
• agc crcf get gain(q) returns the agc object’s internal gain.
• agc crcf squelch enable auto(q) activates the AGC’s automatic squelch module.
• agc crcf squelch disable auto(q) deactivates the AGC’s automatic squelch module.
• agc crcf squelch set threshold(q,t) sets the threshold of the squelch.
• agc crcf squelch set timeout(q,t) sets the timeout (number of samples) after the signal
level has dropped before enabling the squelch again.
• agc crcf squelch get status(q) returns the squelch status code (see Table 1 ).
4 int main() {
5 agc_crcf q = agc_crcf_create(); // create object
6 agc_crcf_set_bandwidth(q,1e-3f); // set loop filter bandwidth
7
8 float complex x; // input sample
9 float complex y; // output sample
10
11 // ... initialize input ...
12
13 // repeat as necessary
14 {
15 // scale input sample, storing the result in ’y’
16 agc_crcf_execute(q, x, &y);
17 }
18
19 // clean it up
20 agc_crcf_destroy(q);
21 }
A demonstration of the transient response of the agc crcf type can be found in Figure 3 in which
an input complex sinusoidal pulse is fed into the AGC. Notice the initial overshoot at the output
signal. A few more detailed examples can be found in the examples subdirectory.
8.6 Interface 55
real
1 imag
input signal
-1
real
1 imag
output signal
-1
9 audio
The audio module in [liquid] provides several objects and functions for compressing, digitizing,
and manipulating audio signals. This is particularly useful for encoding audio data for wireless
communications.
9.1.1 Theory
The algorithm attempts to dynamically adjust the value of ∆to track to the input signal. As with
regular delta modulation algorithms, if the decoded reference signal exceeds the input (the error
signal is negative), a binary 0 is sent and ∆ is subtracted from the reference, otherwise a binary 1
is sent and ∆ is added. However CVSD observes the previous N transmitted bits are stored in a
buffer b̂; ∆ is increased by ζ if they are equal and decreased otherwise. This improves the dynamic
range of the encoder over fixed-delta modulation encoders. A summary of the encoding procedure
can be found in Algorithm 2 .
The decoder reverses this process; by retaining the past N bit inputs in a buffer b̂, the value of ∆
can be adjusted appropriately. A summary of the decoding procedure can be found in Algorithm 3
.
9.1 ‘cvsd‘ (continuously variable slope delta) 57
9.1.2 Pre-/Post-Filtering
To preserve the signal’s integrity the encoder applies a pre-filter to emphasize the high-frequency
information of the signal before the encoding process. The pre-filter is a simple 2-tap FIR filter
defined as
Hpre (z) = 1 − αz −1 (14)
where α controls the amount of emphasis applied. Typical values fore pre-emphasis are 0.92 < α <
0.98; setting α = 0 completely disables this emphasis. This process is reversed on the decoder by
applying the inverse of Hpre (z) as a low-pass de-emphasis filter:
−1 1
Hpre (z) = (15)
1 − αz −1
Additionally, the decoder adds a DC-blocking filter to reject any residual offset caused by the
decoding process. By itself the DC-blocking filter has a transfer function
1 − z −1
H0 (z) = (16)
1 − βz −1
where β controls the cut-off frequency of the filter and is typically set very close to 1. The default
value for β in [liquid] is 0.99. The full post-emphasis filter is therefore
−1 1 − z −1
Hpost (z) = Hpre (z)H0 (z) = (17)
1 − (α + β)z −1 + αβz −2
9.1.3 Interface
The cvsd object in [liquid] allows the user to select both ζas well as N , the number of repeated
bits observed before ∆ is updated. The combination of these values with the sampling rate yields a
speech compression algorithm with moderate quality. Listed below is the full interface to the cvsd
object:
• cvsd create(N,zeta,alpha) creates an agc object with parameters N , ζ, and α.
58 9 AUDIO
• cvsd destroy(q) destroys a cvsd object, freeing all internally-allocated memory and objects.
• cvsd print(q) prints the cvsd object’s internal parameters to the standard output.
• cvsd encode(q,sample) encodes a single audio sample, returning the encoded bit.
• cvsd decode(q,bit) decodes and returns a single audio sample from an input bit.
9.1.4 Example
Here is a basic example of the cvsd object in [liquid]:
1 // file: doc/listings/cvsd.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int nbits=3; // number of adjacent bits to observe
7 float zeta=1.5f; // slope adjustment multiplier
8 float alpha = 0.95; // pre-/post-filter coefficient
9
10 // create cvsd encoder/decoder
11 cvsd q = cvsd_create(nbits, zeta, alpha);
12
13 float x; // input sample
14 unsigned char b; // encoded bit
15 float y; // output sample
16
17 // ...
18
19 // repeat as necessary
20 {
21 b = cvsd_encode(q, x); // encode sample
22
23 y = cvsd_decode(q, b); // decode sample
24 }
25
A demonstration of the algorithm can be seen in Figure 4 where the encoder attempts to track to an
input sinusoid. Notice that the encoder sometimes overshoots the reference signal. This distortion
results in degradations, particularly in the upper frequency bands. A more detailed example is
given in examples/cvsd example.c under the main [liquid] project directory.
9.1 ‘cvsd‘ (continuously variable slope delta) 59
audio input
1 cvsd output
time series
-1
audio input
cvsd output
-20
-40
-60
-80
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
Figure 4: cvsd example encoding a windowed sum of sine functions with ζ = 1.5, N = 2, and
α = 0.95.
60 10 BUFFER
0 1 2 3 4 5 6 7 8
← 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ←
10 buffer
The buffer module includes objects for storing, retrieving, and interfacing with buffered data sam-
ples.
• windowcf recreate(q,n) extends an existing window’s size, similar to the standard C li-
brary’s realloc() to n samples. If the size of the new window is larger than the old one, the
newest values are retained at the beginning of the buffer and the oldest values are truncated.
If the size of the new window is smaller than the old one, the oldest values are truncated.
• windowcf clear(q) clears the contents of the buffer by setting all internal values to zero.
• windowcf index(q,i,*v) retrieves the ith sample in the window, storing the output value
in v. This is equivalent to first invoking read() and then indexing on the resulting pointer;
however the result is obtained much faster. Therefore invoking windowcf index(q,0,*v)
returns the oldest value in the window.
• windowcf read(q,**r) reads the contents of the window by returning a pointer to the
aligned internal memory array. This method guarantees that the elements are linearized.
This method should only be used for reading; writing values to the buffer has unspecified
results.
10.1 window buffer 61
• windowcf push(q,v) shifts a single sample v into the right side of the window, pushing the
oldest (left-most) sample out of the end. Unlike stacks, the windowcf object has no equivalent
”pop” method, as values are retained in memory until they are overwritten.
• windowcf write(q,*v,n) writes a block of n samples in the array v to the window. Effec-
tively, it is equivalent to pushing each sample one at a time, but executes much faster.
Here is an example demonstrating the basic functionality of the window object. The comments
show the internal state of the window after each function call as if the window were a simple C
array.
1 // file: doc/listings/window.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // initialize array for writing
6 float v[] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
7
Hd (z) = z −k (18)
where k is the number of samples of delay. The interface for the wdelay family of objects is listed
below. While the interface is given for wdelayf for floating-point precision, equivalent interfaces
exist for float complex with wdelaycf.
• wdelayf recreate(q,k) adjusts the delay size, preserving the internal state of the object.
• wdelayf print(q) prints the object’s properties internal state to the standard output.
• wdelayf clear(q) clears the contents of the internal buffer by setting all values to zero.
• wdelayf read(q,y) reads the sample at the head of the buffer and stores it to the output
pointer.
N
X −1
T
x·v =x v = x(k)v(k) (19)
k=0
A number of other [liquid] modules rely on dotprod, such as filtering and equalization.
11.2 Interface
There are effectively two ways to use the dotprod module. In the first and most general case, a
vector dot product is computed on two input vectors x and v whose values are not known a priori.
In the second case, a dotprod object is created around vector vwhich does not change (or rarely
changes) throughout its life cycle. This is the more convenient method for filtering objects which
don’t usually have time-dependent coefficients. Listed below is a simple interface example to the
dotprod module object:
1 // file: doc/listings/dotprod_rrrf.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // create input arrays
6 float x[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
7 float v[] = { 0.1f, -0.2f, 1.0f, -0.2f, 0.1f};
8 float y;
9
In both cases the dotprod can be easily integrated with the window object (§ 10.1 ) for managing
input data and alignment. There are three types of dot product objects and are listed in Table 2 .
Listed below is a brief description of the dotprod object interfaces. While the types are described
using the dotprod rrrf object, the same holds true for all other types.
• dotprod rrrf run(h,x,n,y) executes a vector dot product between two vectors h and x,
each of length n and stores the result in the output y. This is not a structured method
and does not require creating a dotprod object, however does not take advantage of SIMD
instructions if available. Rather than speed, its intent is to provide a simple interface to
demonstrate functional correctness.
• dotprod rrrf recreate(q,v,n) recreates a dotprod object with a new set of coefficients v
with a (possibly) different length n.
• dotprod rrrf destroy(q) destroys a dotprod object, freeing all internally-allocated mem-
ory.
• dotprod rrrf execute(q,x,y) executes a dot product with an input vector x and stores
the result in y.
65
12 equalization
This section describes the equalizer module and the functionality of two digital linear adaptive
equalizers implemented in [liquid], LMS and RLS. Their interfaces are nearly identical; however
their internal functionality is quite different. Specifically the LMS algorithm is less computationally
complex but is slower to converge than the RLS algorithm.
where ϕ(n) represents white Gauss noise. The adaptive linear equalizer attempts to use a finite
impulse response (FIR) filter w of length p to estimate the transmitted symbol, using only the
received signal vector y and the known data sequence d, viz
ˆ = wT y
d(n) (21)
n n
where y n = [y(n), y(n − 1), . . . , y(n − p + 1)]T . Several methods for estimating w are known in
the literature, and typically rely on iteratively adjusting w with each input though a recursion
algorithm. This section provides a very brief overview of two prevalent adaptation algorithms; for
a more in-depth discussion the interested reader is referred to [Proakis:2001,Haykin:2002].
where the forgetting factor 0 < λ ≤ 1 which introduces exponential weighting into past data,
appropriate for time-varying channels. The solution to minimizing the cost function c(wn ) is
achieved by setting its partial derivatives with respect to wn equal to zero. The solution at time
n involves inverting the weighted cross correlation matrix for y n , a computationally complex task.
66 12 EQUALIZATION
This step can be circumvented through the use of a recursive algorithm which attempts to estimate
the inverse using the a priori error from the output of the filter. The update equation is
wn+1 = wn + ∆n (24)
where the correction factor ∆n depends on y n and wn , and involves several p × p matrix multiplica-
tions. The RLS algorithm provides a solution which converges much faster than the LMS algorithm,
however with a significant increase in computational complexity and memory requirements.
12.4 Interface
The eqlms and eqrls have nearly identical interfaces so we will leave the discussion to the eqlms ob-
ject here. Like most objects in [liquid], eqlms follows the typical create(), execute(), destroy()
life cycle. Training is accomplished either one sample at a time, or in a batch cycle. If trained
one sample at a time, the symbols must be trained in the proper order, otherwise the algorithm
won’t converge. One can think of the equalizer object in [liquid] as simply a firfilt object (finite
impulse response filter) which has the additional ability to modify its own internal coefficients based
on some error criteria. Listed below is the full interface to the eqlms family of objects. While each
method is listed for eqlms cccf, the same functionality applies to eqlms rrrf as well as the RLS
equalizer objects (eqrls cccf and eqrls rrrf).
• eqlms cccf create(*h,n) creates and returns an equalizer object with n taps, initialized
with the input array h. If the array value is set to the NULL pointer then the internal
coefficients are initialized to {1, 0, 0, . . . , 0}.
• eqlms cccf destroy(q) destroys the equalizer object, freeing all internally-allocated mem-
ory.
• eqlms cccf print(q) prints the internal state of the eqlms object.
• eqlms cccf set bw(q,w) sets the bandwidth of the equalizer to w. For the LMS equalizer
this is the learning parameter µ which has a default value of 0.5. For the RLS equalizer the
”bandwidth” is the forgetting factor λ which defaults to 0.99.
• eqlms cccf reset(q) clears the internal equalizer buffers and sets the internal coefficients
to the default (those specified when create() was invoked).
• eqlms cccf push(q,x) pushes a sample x into the internal buffer of the equalizer object.
• eqlms cccf execute(q,*y) generates the output sample y by computing the vector dot
product (see § 11 ) between the internal filter coefficients and the internal buffer.
• eqlms cccf step(q,d,d hat) performs a single iteration of equalization with an estimated
output dˆ for an expected output d. The weights are updated internally defined by ((22) ) for
the LMS equalizer and ((24) ) for the RLS equalizer.
• eqlms cccf get weights(q,*w) returns the internal filter coefficients (weights) at the cur-
rent state of the equalizer.
Here is a simple example:
12.5 Blind Equalization 67
1 // file: doc/listings/eqlms_cccf.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int n=32; // number of training symbols
7 unsigned int p=10; // equalizer order
8 float mu=0.500f; // LMS learning rate
9
10 // allocate memory for arrays
11 float complex x[n]; // received samples
12 float complex d_hat[n]; // output symbols
13 float complex d[n]; // traning symbols
14
15 // ...initialize x, d_hat, d...
16
17 // create LMS equalizer and set learning rate
18 eqlms_cccf q = eqlms_cccf_create(NULL,p);
19 eqlms_cccf_set_bw(q, mu);
20
21 // iterate through equalizer learning
22 unsigned int i;
23 {
24 // push input sample
25 eqlms_cccf_push(q, x[i]);
26
27 // compute output sample
28 eqlms_cccf_execute(q, &d_hat[i]);
29
30 // update internal weights
31 eqlms_cccf_step(q, d[i], d_hat[i]);
32 }
33
34 // clean up allocated memory
35 eqlms_cccf_destroy(q);
36 }
For more detailed examples, see examples/eqlms cccf example.c and examples/eqrls cccf example.c.
4 int main() {
5 // options
68 12 EQUALIZATION
The equalizer filter is initialized with square-root raised-cosine coefficients (see § 15.5.3 for details of
square-root Nyquist filter designs). After computing each output symbol, the transmitted symbol
is estimated and the equalizer adjusts its coefficients internally using the step() method. This can
be easily combined with the linear modem object’s modem get demodulator sample() interface to
return the estimated symbol after demodulation (see § 19.2 ). An example of the decision-directed
equalizer for a QPSK signal with unknown data is depicted in Figure 5 . A QPSK signal filtered
with a square-root raised-cosine filter is transmitted through a noisy channel with several multi-path
components, contributing to inter-symbol interference. The receiver uses an equalizer initialized
with a matched filter. The output time series in Figure ?? shows that the first 200 symbols are
particularly noisy with a significant amount of inter-symbol interference due to the effects of the
channel. The equalizer, however, quickly adapts and removes most of the interference as can be
seen in Figure ?? (the composite spectrum is nearly flat in the pass-band). For a more detailed
example, see examples/eqlms cccf blind example.c located under the main project directory.
12.5 Blind Equalization 69
1.5
0.5
Real
-0.5
-1
-1.5
0 200 400 600 800 1000
Symbol Index
1.5
0.5
Imag
-0.5
-1
-1.5
0 200 400 600 800 1000
Symbol Index
6
transmit
channel
4 equalizer
composite
2
Power Spectral Density [dB]
-2
-4
-6
-8
-10
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
• the equalizing filter which would give this response is not necessarily realizable; that is, not
all channels can be perfectly inverted,
• forcing the frequency response to zero increases the noise terms of frequencies where the
spectra of the channel response is low. In this regard, the zero-forcing equalizer only reduces
inter-symbol interference and does not maximize the ratio of signal power to both interference
and noise power as the LMS and RLS algorithms do.
It is interesting to note that both the LMS and RLS equalizers converge to nearly the same solution.
The RLS equalizer, however, has a slightly lower error after training while converging to its error
minimum much faster. The RLS equalizer, however, has a much higher computational complexity.
12.6 Comparison of ‘eqlms‘ and ‘eqrls‘ Object Families 71
10
received
LMS
RLS
5
Power Spectral Density [dB]
-5
-10
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
(a) PSD
2
received
LMS EQ
RLS EQ
1.5
0.5
Quadrature phase
-0.5
-1
-1.5
-2
-2 -1.5 -1 -0.5 0 0.5 1 1.5 2
In-phase
(b) constellation
1 channel
LMS
RLS
0.5
Real
-0.5
-1
72 13 FEC (FORWARD ERROR CORRECTION)
Also available for error detection in [liquid] is a checksum. A checksum is a simple way to validate
data received through un-reliable means (e.g. a noisy channel). A checksum is, in essence, a weak
error detection code that simply counts the number of ones in a block of data (modulo 256). The
limitation, however, is that multiple bit errors might result in a false positive validation of the
corrupted data. The checksum is not a strong an error detection scheme as the cyclic redundancy
check. Table 3 lists the available codecs and gives a brief description for each. For a detailed
example program, see examples/crc example.c in the main [liquid] directory.
the user so the Hamming(8,4) code is effectively the same as Hamming(7,4) but with a lower rate.
Additionally, [liquid] implements the Hamming(12,8) code which accepts an 8-bit symbol and adds
four parity bits, extending it to a 12-bit symbol. This yields a theoretical rate of 2/3, and actually
has a performance very similar to that of the Hamming(7,4) code, even with a higher rate.
This roughly doubles the number of clock cycles to decode over rep3. It is well-known that re-
peat codes do not have strong error-correction capabilities for their rate, are are located far from
the Shannon capacity bound [Proakis:2001]. They are exceptionally weak relative to convolutional
Viterbi and Reed-Solomon codes. However, their simplicity in implementation and low compu-
tational complexity gains them a place in digital communications, particularly in software radios
where spectral efficiency goals might be secondary to processing constraints.
74 13 FEC (FORWARD ERROR CORRECTION)
The generator matrix is simply G = P T I 12 and the parity check matrix is H = [I 12 P ]. Notice
matrix is H = [P I n−k ]. Decoding can be achieved by computing the syndrome vector and then
using a look-up table to determine the location of the error. If the computed syndrome cannot be
associated with any particular error location then multiple errors must have occurred for which the
code cannot correct. There is currently no soft decoding implemented in [liquid] for the SEC-DED
codes.
13.7 Interface
In designing the fec interface, I have tried to keep simplicity and reconfigurability in mind. The
various forward error-correction schemes accept bits or symbols formatted in different lengths and
have vastly different interfaces. This potentially makes switching from one scheme to another
difficult as one needs to restructure the data accordingly. [liquid] takes care of all this formatting
under the hood; regardless of the scheme used, the fec object accepts a block of uncoded data
bytes and encodes them into an output block of coded data bytes.
• fec create(scheme,*opts) creates a fec object of a specific scheme (see Table 4 for avail-
able codecs). Notice that the length of the input message does not need to be specified until
encode() or decode() is invoked. The opts argument is intended for future development
and should be ignored by passing the NULL pointer (see example below).
76 13 FEC (FORWARD ERROR CORRECTION)
• fec destroy(q) destroys a fec object, freeing all internally-allocated memory arrays.
• fec decode(q,n,*msg enc,*msg dec) runs the error-correction decoder on an input array
msg enc of k encoded bytes. The resulting best-effort decoded message is written to the n-
byte output array msg dec, allocated by the user. Notice that like the fec encode() method,
the input length n refers to the decoded message length. Depending upon the error-correction
capabilities of the scheme, the resulting data might have been corrupted, and therefore it is
recommended to use either a checksum or a cyclic redundancy check (§ 13.1 ) to validate data
integrity.
• fec get enc msg length(scheme,n) returns the length k of the encoded message in bytes
for an uncoded input of n bytes using the specified encoding scheme. This method can be
called before the fec object is created and is useful for allocating initial memory arrays.
Listed below is a simple example demonstrating the basic interface to the fec encoder/decoder
object:
1 // file: doc/listings/fec.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 unsigned int n = 64; // decoded message length (bytes)
6 fec_scheme fs = LIQUID_FEC_HAMMING74; // error-correcting scheme
7
8 // compute encoded message length
9 unsigned int k = fec_get_enc_msg_length(fs, n);
10
11 // allocate memory for data arrays
12 unsigned char msg_org[n]; // original message
13 unsigned char msg_enc[k]; // encoded message
14 unsigned char msg_rec[k]; // received message
15 unsigned char msg_dec[n]; // decoded message
16
17 // create fec objects
18 fec encoder = fec_create(fs,NULL);
19 fec decoder = fec_create(fs,NULL);
20
21 // repeat as necessary
22 {
23 // ... initialize message ...
24
25 // encode message
26 fec_encode(encoder, n, msg_org, msg_enc);
27
28 // ... push through channel ...
13.8 Performance 77
29
30 // decode message
31 fec_decode(decoder, n, msg_rec, msg_dec);
32 }
33
34 // clean up objects
35 fec_destroy(encoder);
36 fec_destroy(decoder);
37
38 return 0;
39 }
For a more detailed example demonstrating the full capabilities of the fec object, see examples/fec example.c.
13.8 Performance
The performance of an error-correction scheme is typically measured in the bit error rate (BER) for
an antipodally modulated signal in the presence of additive white Gauss noise (AWGN). Certain
applications prefer measuring performance in terms of the signal energy while others require bit
energy, all relative to the noise variance. The two are related by
Eb Es
= (32)
N0 rN0
where Es is the signal energy, Eb is the bit energy, N0 is the noise energy, and r is the rate of
the modulation and coding scheme pair, measured in bits/s/Hz. Table 4 lists the available codecs
and gives a brief description for each. All convolutional and Reed-Solomon codes are available only
if libfec is installed. Figure 8 , Figure 9 , and Figure 10 plot the bit error-rate performance of
the forward error-correction schemes available in [liquid] for a BPSK signal in an AWGN channel.
Each figure depicts the BER versus Eb /N0 (Es /N0 compensated for coding rate). The error rates
were computed by generating packets of 1024 bits, encoding using the appropriate FEC scheme,
modulating the resulting bits with BPSK (see § 19.2.3 ), adding noise, demodulating, and decod-
ing. Each point was simulated with a minimum of 4,000,000 trials and a minimum of 1,000 bit
errors. The raw data can be found in the doc/data/fec-ber/ subdirectory. Figure 8 depicts
the performance of the available built-in [liquid] FEC codecs, including the Hamming, SEC-DED,
and Golay codes. Notice that in terms of Eb /N0 none of these schemes performs extremely well,
perhaps with the exception of the Golay(24,12) code which achieves a BER of 10−5 with an Eb /N0
value of 7.46 dB. Figure 9 depicts the performance of the convolutional codecs available in [liquid]
78 13 FEC (FORWARD ERROR CORRECTION)
100
hard
soft
10-1
10-2
BER
10-3
10-4
10-5
-8 -6 -4 -2 0 2 4 6 8 10
Eb/N0 [dB]
Figure 7: Bit error-rate performance for the Hamming(8,4) codec comparing hard-decision to
soft-decision decoding.
13.8 Performance 79
Table 4: Forward error-correction codecs available in [liquid] with Eb /N0 required for a BER of
10−5
100
10-1
10-2
BER
10-3
Uncoded
10-4 Hamming(7,4)
SEC-DED(22,16)
Hamming(12,8)
SEC-DED(39,32)
SEC-DED(72,64)
Golay(24,12)
10-5
-8 -6 -4 -2 0 2 4 6 8 10
Eb/N0 [dB]
Figure 8: Forward error-correction codec bit error rates (simulated) for built-in [liquid] block
codecs using BPSK modulation and hard-decision decoding.
13.8 Performance 81
100
10-1
10-2
BER
10-3
10-4
Uncoded
conv. r1/2, K=7
conv. r1/2, K=9
conv. r1/3, K=9
conv. r1/6, K=15
10-5
-8 -6 -4 -2 0 2 4 6 8 10
Eb/N0 [dB]
Figure 9: Forward error-correction codec bit error rates (simulated) for convolutional codes using
BPSK modulation and hard-decision decoding.
82 13 FEC (FORWARD ERROR CORRECTION)
100
10-1
-2
10
BER
10-3
Uncoded
conv. r1/2, K=7
10-4 conv. r2/3, K=7
conv. r3/4, K=7
conv. r4/5, K=7
conv. r5/6, K=7
conv. r6/7, K=7
conv. r7/8, K=7
10-5
-8 -6 -4 -2 0 2 4 6 8 10
Eb/N0 [dB]
Figure 10: Forward error-correction codec bit error rates (simulated) for punctured convolutional
codes using BPSK modulation and hard-decision decoding.
when the libfec library is installed. These include LIQUID FEC CONV V27, LIQUID FEC CONV V29,
LIQUID FEC CONV V39, and LIQUID FEC CONV V615. Notice that these codecs provide significant
error-correction capabilities over the Hamming codes; this is a result of the fact that convolu-
tional encoding effectively spreads the redundancy over a broader range of the original message,
correlating the output samples more than the short-length Hamming codes. Figure 10 depicts the
performance of the punctured convolutional codecs (K = 7) available in [liquid], also available when
the libfec library is installed. These include LIQUID FEC CONV V27P23, LIQUID FEC CONV V27P34,
LIQUID FEC CONV V27P45, LIQUID FEC CONV V27P56, LIQUID FEC CONV V27P67, and LIQUID FEC CONV V27P78.
Also included is the unpunctured LIQUID FEC CONV V27 codec, plotted as a reference point. [liquid]
also includes punctured convolutional codes for the K = 9encoder; however because the perfor-
mance is similar to the K = 7 codec its performance is omitted for the sake of brevity.
83
Internally, [liquid] uses several algorithms for computing FFTs including the standard decimation-
in-time (DIT) for power-of-two transforms [Ziemer:1998(Section 10-4)], the Cooley-Tukey mixed-
radix method for composite transforms [CooleyTukey:1965], Rader’s algorithm for prime-length
transforms [Rader:1968], and the DFT given by (33) for very small values of N . The DFT requires
O N 2 operations and can be slow for even moderate sizes of N which is why it is typically reserved
for small transforms. [liquid]’s strategy for computing FFTs is to recursively break the transform
into manageable pieces and perform the best method for each step. For example, a transform
of length N = 128 = 27 can be easily computed using the standard DIT FFT algorithm which
is computationally fast. The Cooley-Tukey algorithm permits any factorable transform of size
N = P Q to be computed with P transforms of size Q and Q transforms of size P . For example,
a transform of length N = 126 can be computed using the Cooley-Tukey algorithm with radices
P = 9 and Q = 14. Furthermore, each of these transforms can be further split using the Cooley-
Tukey algorithm (e.g. 9 = 3 · 3 and 14 = 2 · 7). The smallest resulting transforms can finally be
computed using the DFT algorithm without much penalty. For large transforms of prime length,
[liquid] uses Rader’s algorithm [Rader:1968]which permits any transform of prime length N to be
computed using an FFT and an IFFT each of length N − 1. For example, Rader’s algorithm
can compute a 127-point transform using the 126-point Cooley-Tukey transform (and its inverse)
described above. 16 ader actually gives an alternate algorithm by which any transform of prime
length N can be computed with an FFT and an IFFT of any length greater than 2N − 4. For
example, the 127-point FFT could also be computed using computationally efficient 256-point DIT
transforms. [liquid] includes both algorithms and chooses the most appropriate one for the task.
Through recursion, a tranform of any size can be decomposed into either computationally efficient
DIT FFTs, or combinations of small DFTs. Consequently, [liquid] can compute any transform in
O n log(n) operations. Even still, [liquid] will use the fftw3 library library [fftw:web] for internal
methods if it is available. The presence of fftw3.h and libfftw3 are detected by the configure
script at build time. If found, [liquid] will link against fftw for better performance (it is, however,
16
R
84 14 FFT (FAST FOURIER TRANSFORM)
the fastest FFT in the west, you know). If fftw is unavailable, however, [liquid] will use its own,
slower FFT methods for internal processing. This eliminates libfftw as an external dependency,
but takes advantage of it when available. An example of the interface for computing complex
discrete Fourier transforms is listed below. Notice the stark similarity to libfftw3’s interface.
1 // file: doc/listings/fft.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int n=16; // input data size
7 int flags=0; // FFT flags (typically ignored)
8
9 // allocated memory arrays
10 float complex * x = (float complex*) malloc(n * sizeof(float complex));
11 float complex * y = (float complex*) malloc(n * sizeof(float complex));
12
13 // create FFT plan
14 fftplan q = fft_create_plan(n, x, y, LIQUID_FFT_FORWARD, flags);
15
16 // ... initialize input ...
17
18 // execute FFT (repeat as necessary)
19 fft_execute(q);
20
21 // destroy FFT plan and free memory arrays
22 fft_destroy_plan(q);
23 free(x);
24 free(y);
25 }
14.2 Example
14.3 Real even/odd DFTs
[liquid] also implement real even/odd discrete Fourier transforms; however these are not guaranteed
to be efficient. A list of the transforms and their descriptions is given below.
1
real
imag
0.5
Time Series
-0.5
-1
0 50 100 150 200
Sample Index
30
real
imag
25
20
15
Frequency Response
10
-5
-10
-15
-20
0 50 100 150 200
Sample Index
An example of the interface for computing a discrete cosine transform of type-III (FFT REDFT01) is
listed below.
1 // file: doc/listings/fft_dct.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int n=16; // input data size
7 int type = LIQUID_FFT_REDFT01; // DCT-III
8 int flags=0; // FFT flags (typically ignored)
9
10 // allocated memory arrays
11 float * x = (float*) malloc(n * sizeof(float));
12 float * y = (float*) malloc(n * sizeof(float));
13
14 // create FFT plan
14.4 spgram (spectral periodogram) 87
• spgram push(q,*x,n) pushes n samples of the array x into the internal buffer of an spgram
object.
17
F
88 14 FFT (FAST FOURIER TRANSFORM)
signal level
time
window length
FFT length
delay
• spgram execute(q,*X) computes the spectral periodogram output storing the result in the
nfft-point output array X. The output array is of type float complex and contain output
of the FFT.
An example of the spgram object can be found in Figure 13 in which a frequency-modulated sinusoid
is generated and analyzed. The frequency of the sinusoid changes over time and is clearly visible
in both plots.
14.4 spgram (spectral periodogram) 89
real
imag
1
0.5
Time Series
-0.5
-1
60 20
0
50
-20
40 -40
-60
Time
30
-80
20 -100
-120
10
-140
0 -160
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
15 filter
The filter module is at the core of [liquid]’s digital signal processing functionality. Filter design and
implementation is a significant portion of radio engineering, and consumes a considerable portion
of the baseband receiver’s energy. This section includes interface descriptions for all of the signal
processing elements in [liquid] regarding filter design and implementation. This includes both
infinite and finite (recursive and non-recursive) filters, decimators, interpolators, and performance
characterization.
where N is the window length, and d is the overlap delay. An example of the autocorr interface
is listed below.
1 // file: doc/listings/autocorr.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int n = 60; // autocorr window length
7 unsigned int delay = 20; // autocorr overlap delay
8
9 // create autocorrelator object
10 autocorr_cccf q = autocorr_cccf_create(n,delay);
11
A more detailed example is given in examples/autocorr cccf example.c in the main [liquid]
project directory. Listed below is the full interface to the autocorr family of objects. While each
method is listed for autocorr cccf, the same functionality applies to autocorr rrrf.
• autocorr cccf create(N,d) creates and returns an autocorr object with a window size of
N samples and a delay of d samples.
15.2 decim (decimator) 91
input
1 decimated
Real
-1
input
1 decimated
Imag
-1
Figure 14: decim crcf (decimator) example with D = 4, compensating for filter delay.
• autocorr cccf destroy(q) destroys an autocorr object, freeing all internally-allocated mem-
ory.
• autocorr cccf print(q) prints the internal state of the autocorr object.
• autocorr cccf push(q,x) pushes a sample x into the internal buffer of an autocorr object.
• autocorr cccf execute(q,*rxx) executes the delay, conjugate multiply, and accumulate
operation, storing the result in the output variable rxx .
P −1
• autocorr cccf get energy(q) returns (1/N ) N ∗
k=0 |x(n − k)x (n − k − d)|
• decim crcf create(D,*h,N) creates a decim object with a decimation factor D using N filter
coefficients h.
• decim crcf create prototype(D,m,As) creates a decim object from a prototype filter with
a decimation factor D, a delay of Dm samples, and a stop-band attenuation As dB.
• decim crcf destroy(q) destroys a decim object, freeing all internally-allocated memory.
• decim crcf print(q) prints the parameters of a decim object to the standard output.
• decim crcf execute(q,*x,*y,k) computes the output decimation of the input sequence
x(which is D samples in size) at the index k and stores the result in y.
1 // file: doc/listings/firdecim.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int M = 4; // decimation factor
7 unsigned int h_len = 21; // filter length
8
A more detailed example is given in examples/decim crcf example.c in the main [liquid] project
directory.
15.3 firfarrow (finite impulse response Farrow filter) 93
• firfarrow crcf clear(q) clear filter internal memory buffer. This does not reset the delay.
• firfarrow crcf push(q,x) push a single sample x into the filter’s internal buffer.
• firfarrow crcf execute(q,*y) computes the output sample, storing the result in y.
• firfarrow crcf get length(q) returns length of the filter (number of taps)
• firfarrow crcf get coefficients(q,*h) returns the internal filter coefficients, storing the
result in the output vector h.
• firfarrow crcf groupdelay(q,fc) returns the group delay of the filter at the normalized
frequency fc .
10
mu= 0.500
9.5
mu= 0.375
mu= 0.250
Group Delay
mu= 0.125
mu= 0.000
9
mu=-0.125
mu=-0.250
mu=-0.375
mu=-0.500
8.5
8
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
Figure 15: firfarrow crcf (Farrow filter) group delay example with N = 19, Q = 5, fc = 0.45,
and As = 60 dB.
16 firfarrow_crcf_set_delay(q, 0.3f);
17
18 float complex x; // input sample
19 float complex y; // output sample
20
21 // execute filter (repeat as necessary)
22 {
23 firfarrow_crcf_push(q, x); // push input sample
24 firfarrow_crcf_execute(q,&y); // compute output
25 }
26
27 // destroy object
28 firfarrow_crcf_destroy(q);
29 }
An example of the Farrow filter’s group delay can be found in Figure 15 .
input
1 filtered
real
-1
0 20 40 60 80 100 120
Sample Index
input
1 filtered
imag
-1
0 20 40 60 80 100 120
Sample Index
where h = [h(0), h(1), . . . , h(N − 1)]is the filter impulse response. Notice that the output sample in
(46) is simply the vector dot product (see § 11 ) of the filter coefficients h with the time-reversed
sequence x. An example of the firfilt can be seen in Figure 16 in which a low-pass filter is
applied to a signal to remove a high-frequency component. An example of the firfilt interface is
listed below.
1 // file: doc/listings/firfilt.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int h_len=21; // filter order
7 float h[h_len]; // filter coefficients
8
Listed below is the full interface to the firfilt family of objects. While each method is listed for
firfilt crcf, the same functionality applies to firfilt rrrf and firfilt cccf.
• firfilt crcf destroy(q) destroys a firfilt object, freeing all internally-allocated mem-
ory.
• firfilt crcf print(q) prints the parameters of a firfilt object to the standard output.
• firfilt crcf push(q,x) pushes an input sample x into the internal buffer of the filter ob-
ject.
• firfilt crcf execute(q,*y) generates the output sample y by computing the vector dot
product (see § 11 ) between the internal filter coefficients and the internal buffer.
• firfilt crcf freqresponse(q,fc,*H) returns the response of the filter at the frequency
fc , stored in the pointer H.
• firfilt crcf groupdelay(q,fc) returns the group delay of the filter at the frequency fc .
where w(n) is a time-limited symmetric window and hi (n) is the impulse response of the ideal filter
with a cutoff frequency ωc , viz.
ωc sin ωc n
hi (n) = , ∀n (48)
π ωc n
A number of possible windows could be used; the Kaiser window is particularly common due to
its systematic ability to trade transition bandwidth for stop-band rejection. The Kaiser window is
defined as " r #
2
n
I0 πα 1 − N/2
w(n) = − N/2 ≤ n ≤ N/2, α ≥ 0 (49)
I0 (πα)
where Iν (z) is the modified Bessel function of the first kind of order ν and α is a shape parameter
controlling the window decay. Iν (z) can be expanded as
∞ 1 2 k
4z
z ν X
Iν (z) = (50)
2 k!Γ(k + ν + 1)
k=0
The sum in (50) converges quickly due to the denominator increasing rapidly, (and in particular
for ν = 0 the denominator reduces to (k!)2 ) and thus only a few terms are necessary for sufficient
approximation. The sum (50) converges quickly due to the denominator increasing rapidly, thus
only a few terms are necessary for sufficient approximation. For more approximations to I0 (z) and
Iν (z), see § 17 in the math module. Kaiser gives an approximation for the value of α to give a
particular sidelobe level for the window as [Vaidyanathan:1993((3.2.7))]
0.1102(As − 8.7)
As > 50
α = 0.5842(As − 21)0.4 21 < As ≤ 50 (51)
0 else
where As > 0 is the stop-band attenuation in decibels. This approximation is provided in [liq-
uid] by the kaiser beta As() method, and the length of the filter can be approximated with
estimate req filter len() (see § 15.5.6 for more detail on these methods). The entire design
process is provided in [liquid] with the firdes kaiser window() method which can be invoked as
follows:
liquid_firdes_kaiser(_n, _fc, _As, _mu, *_h);
98 15 FILTER
where n is the length of the filter (number of samples), fc is the normalized cutoff frequency
(0 ≤ fc ≤ 0.5), As is the stop-band attenuation in dB (As > 0), mu is the fractional sample offset
(−0.5 ≤ µ ≤ 0.5), and * h is the n-sample output coefficient array. Listed below is an example of
the firdes kaiser window interface.
1 // file: doc/listings/firdes_kaiser.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 float fc=0.15f; // filter cutoff frequency
7 float ft=0.05f; // filter transition
8 float As=60.0f; // stop-band attenuation [dB]
9 float mu=0.0f; // fractional timing offset
10
11 // estimate required filter length and generate filter
12 unsigned int h_len = estimate_req_filter_len(ft,As);
13 float h[h_len];
14 liquid_firdes_kaiser(h_len,fc,As,mu,h);
15 }
An example of a low-pass filter design using the Kaiser window can be found in Figure 17 .
Nyquist’s criteria for designing a band-limited filter without inter-symbol interference is for the
spectral response of a linear phase filter to be symmetric about its symbol rate. [liquid] provides
several Nyquist filters as design prototypes and are listed in Table 5 . The interface for designing
Nyquist filters is simply
where ftype is one of the filter types in Table 5 , k is the number of samples per symbol, m is the
filter delay in symbols, β is the excess bandwidth (rolloff) factor, ∆t is the fractional sample delay
(usually set to zero for typical filter designs and is ignored in the LIQUID NYQUIST PM design), and
h is the output coefficients array of length 2km + 1.
15.5 firdes (finite impulse response filter design) 99
1 sinc
Kaiser window
sinc, window
0 10 20 30 40 50 60 70
Sample Index
1 composite
filter
0 10 20 30 40 50 60 70
Sample Index
(a) time
20
0
Power Spectral Density [dB]
-20
-40
-60
-80
-100
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
(b) PSD
where ftype is one of the filter types in Table 6 , k is the number of samples per symbol, m is
the filter delay in symbols, β is the excess bandwidth (rolloff) factor, ∆t is the fractional sample
delay (usually set to zero for typical filter designs), and h is the output coefficients array of length
2km + 1. All square-root Nyquist filters in [liquid] have these four basic properties (k, m, β, ∆t)
and produce a filter with N = 2km + 1 coefficients. The most common square-root Nyquist filter
design in digital communications is the square-root raised-cosine (RRC) filter, likely due to the
fact that an expression for its time series can be expressed in closed form. The filter coefficients
themselves are derived from the following equation:
cos [(1 + β)πz] + sin [(1 − β)πz] /(4βz)
h [z] = 4β √ (52)
π T [1 − 16β 2 z 2 ]
where z = n/k − m, and T = 1 for most cases. [liquid] compensates for the two cases where h[n]
might be undefined in the above equation, i.e.
and
β 2 π 2 π
lim h(z) = √ 1+ sin + 1− cos (54)
1
z→± 4β 2 π 4β π 4β
15.5 firdes (finite impulse response filter design) 101
20
ARKAISER
RKAISER
RRC
0 hM3
Power Spectral Density [dB]
-20
-40
-60
-80
-100
-120
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
Figure 18: Contrast of the different square-root Nyquist filters available in [liquid] for k = 2,
m = 9, β = 0.3, and ∆t = 0.
The r-Kaiser and harris-Moerder-3 (hM3) filters cannot be expressed in closed form but rely on
iterations over traditional filter design techniques to search for the design parameters which mini-
mize the resulting filter’s inter-symbol interference (ISI). Similarly the approximate r-Kaiser filter
uses an approximation for the design parameters to eliminate the need for running the search;
this comes at the expense of a slight performance degradation. Figure 18 contrasts the different
square-root Nyquist filters available in [liquid]. The square-root raised-cosine filter is inferior to
the (approximate) r-Kaiser and harris-Moerder-3 filters in both transition bandwidth as well as
side-lobe suppression. In the figure the responses of the r-Kaiser and approximate r-Kaiser filters
are indistinguishable.
The transmit filter for a GMSK modem with a bandwidth-time product BT (equivalent to the
excess bandwidth factor β) is defined as
! !
2πBT 1 2πBT 1
ht (t) = Q p t− −Q p t+ (55)
ln(2) 2 ln(2) 2
102 15 FILTER
√
where Q(z) = 12 1 − erf (z/ 2) (see § 17.1.10 ). The transmit filter imparts inter-symbol interfer-
ence, leaving the receiver to compensate. [liquid] implements a GMSK receive filter by minimizing
the inter-symbol interference of the composite, and as such there is no closed-form solution for the
GMSK receive filter. Figure 19 depicts the transmit, receive, and composite filters in both the
time and frequency domains. Notice that the frequency response of the receive filter has a gain
in the pass-band (around f = 0.13) to compensate for the ISI imparted by the transmit filter.
Consequently the composite filter has nearly zero ISI, as can be seen by its flat pass-band response
and transition through 20 log10 21 .
• bands is a [Nb × 2] matrix of the band edge descriptions. Each row corresponds to an upper
and lower band edge for each region of interest. These regions cannot be overlapping.
• des is an array of size Nb with the desired response (linear) for each band.
• weights is an array of size Nb with the relative error weighting for each band.
• btype gives the filter type for the design. This is typically LIQUID FIRDESPM BANDPASS for
the majority of filters.
• wtype is an array of length Nb which specifies the weighting function for each band (flat,
exponential, or linear).
1.4
Transmit
1.2 Receive
1
Transmit/Receive
0.8
0.6
0.4
0.2
0
-0.2
-0.4
-4 -2 0 2 4
Time [t/T]
1
Composite
0.8
0.6
Composite
0.4
0.2
-0.2
-4 -2 0 2 4
Time [t/T]
(a) time
20
Transmit
Receive
Composite
0
Power Spectral Density [dB]
-20
-40
-60
-80
-100
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
(b) PSD
Impulse Response
0.2
0.1
20
Power Spectral Density [dB]
-20
-40
-60
-80
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
• fir group delay(*h,n,f) computes the group delay for a finite impulse-response filter with
n coefficients h at a frequency f . The group delay τg at frequency f for a finite impulse
response filter of length N is computed as
( P )
N −1 j2πf k · k
k=0 h(k)e
τg = < N −1
(58)
j2πf k
P
k=0 h(k)e
• iir group delay(*b,nb,*a,na,f) computes the group delay for an infinite impulse-response
filter with na feed-back coefficients a, and nb feed-forward coefficients b at a frequency
fP. To compute the group delay, define the flipped convolution of a and b as c(n) =
N −1 ∗
m=0 a (m)b(m − n)for n ∈ {0, 1, . . . , 2(N + 1)}. The group delay τg at frequency f for
an infinite impulse response filter of order N can therefore be computed as
( P2(N +1) )
c(k)e j2πf k · k
k=0
τg = < P2(N +1) −N (59)
k=0 c(k)ej2πf k
• iirdes isstable(*b,*a,n) checks the stability of an infinite impulse-response filter with
nfeed-back and feed-forward coefficients a and brespectively. Stability is tested by computing
the roots of the denominator (poles) and ensuring that they lie within the unit circle. Notice
that the poles in Figure 24 -Figure 28 all have their poles within the unit circle and are
therefore stable (as expected).
between filter length, side-lobe suppression, and transition bandwidth apply. The firhilbf object
uses a half-band filter to implement the transform as efficiently as possible. While any filter length
can be accepted, the firhilbf object internally forces the length to be of the form n = 4m + 1to
reduce the computational load. A half-band filter of this length has 2m zeros and 2m + 1 non-
zero coefficients. Of these non-zero coefficients, the center is exactly 1 while the other 2mare even
symmetric, and therefore only m computations are needed. A graphical example of the Hilbert
decimator can be seen in Figure 21 where a real-valued input sinusoid is converted into a complex
sinusoid with half the number of samples. An example code listing is given below. Although
firhilbf is a placeholder for both decimation (real to complex) and interpolation (complex to
real), separate objects should be used for each task.
1 // file: doc/listings/firhilb.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 unsigned int m=5; // filter semi-length
6 float slsl=60.0f; // filter sidelobe suppression level
7
8 // create Hilbert transform objects
9 firhilbf q0 = firhilbf_create(m,slsl);
10 firhilbf q1 = firhilbf_create(m,slsl);
11
12 float complex x; // interpolator input
13 float y[2]; // interpolator output
14 float complex z; // decimator output
15
16 // ...
17
18 // execute transforms
19 firhilbf_interp_execute(q0, x, y); // interpolator
20 firhilbf_decim_execute(q1, y, &z); // decimator
21
22 // clean up allocated memory
23 firhilbf_destroy(q0);
24 firhilbf_destroy(q1);
25 }
real
real 1
-1
real
1 imag
complex
-1
0 20 40 60 80 100
Sample Index
(a) time
20
original/real
transformed/decimated
0
Power Spectral Density [dB]
-20
-40
-60
-80
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
(b) PSD
Figure 21: firhilbf (Hilbert transform) decimator demonstration. The small signal at f = 0.13
is due to aliasing as a result of imperfect image rejection.
15.7 iirfilt (infinite impulse response filter) 109
For more detailed examples on Hilbert transforms in [liquid], refer to the files examples/firhilb decim example.c
and examples/firhilb interp example.c located within the main [liquid] project directory. See
also: resamp2 (§ 15.11 ), FIR filter design (§ 15.5 ).
where b = [b0 , b1 , . . . , bnb −1 ]T are the feed-forward parameters and a = [a0 , a1 , . . . , ana −1 ]T are the
feed-back parameters of length nb and na , respectively. The z-transform of the transfer function is
therefore
nPb −1
bj z −j
Y (z) j=0 b0 + b1 z −1 + · · · + bnb −1 z nb −1
H(z) = = n −1 = (63)
X(z) a a0 + a1 z −1 + · · · + ana −1 z na −1
ak z −k
P
k=0
Typically the coefficients in H(z) are normalized such that a0 = 1. For larger order filters (even as
small as n ≈ 8) the filter can become unstable due to finite machine precision. It is often therefore
useful to express H(z) in terms of second-order sections. For a filter of order n, these sections are
denoted by the two (L + r) × 3 matrices B and Awhere r = n mod 2 (0 for odd n, 1 for even n)
and L = (n − r)/2.
L
r Y
Br,0 + Br,1 z −1 Bk,0 + Bk,1 z −1 + Bk,2 z −2
Hd (z) = (64)
1 + Ar,1 z −1 1 + Ak,1 z −1 + Ak,2 z −2
k=1
Notice that H(z) is now a series of cascaded second-order IIR filters. The sos’ form is practical
when filters are designed from analog prototypes where the poles and zeros are known.
[liquid] implements second-order sections efficiently with the internal iirfiltsos crcf family of
objects. For a cascaded second-order section IIR filter, use iirfilt crcf create sos(B,A,n).
110 15 FILTER
See also: iirdes (IIR filter design) in § 15.8 . Listed below is the full interface to the iirfilt
family of objects. The interface to the iirfilt object follows the convention of other [liquid] signal
processing objects; while each method is listed for iirfilt crcf, the same functionality applies to
iirfilt rrrf and iirfilt cccf.
• iirfilt crcf create sos(*B,*A,Nsos) creates a new iirfilt object using Nsos second-
order sections. The [Nsos × 3] feed-forward coefficient matrix is specified by B and the
[Nsos × 3] feed-back coefficient matrix is specified by A.
• iirfilt crcf destroy(q) destroys an iirfilt object, freeing all internally-allocated mem-
ory arrays and buffers.
• iirfilt crcf execute(q,x,*y) executes one iteration of the filter with an input x, storing
the result in y, and updating its internal state.
• iirfilt crcf groupdelay(q,fc) returns the group delay of the filter at the normalized
frequency fc .
Listed below is a basic example of the interface. For more detailed and extensive examples, refer
to examples/iirfilt crcf example.c in the main [liquid] project source directory.
1 // file: doc/listings/iirfilt.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int order=4; // filter order
7
8 unsigned int n = order+1;
9 float b[n], a[n];
10
input
1 filtered
real
-1
0 20 40 60 80 100 120
Sample Index
input
1 filtered
imag
-1
0 20 40 60 80 100 120
Sample Index
14 iirfilt_crcf q = iirfilt_crcf_create(b,n,a,n);
15
16 float complex x; // input sample
17 float complex y; // output sample
18
19 // execute filter (repeat as necessary)
20 iirfilt_crcf_execute(q,x,&y);
21
22 // destroy filter object
23 iirfilt_crcf_destroy(q);
24 }
An example of the iirfilt can be seen in Figure 22 in which a low-pass filter is applied to a signal
to remove a high-frequency component.
user may abstract the entire process by using the liquid iirdes() method. Furthermore, if the
end result is to create a filter object as opposed to computing the coefficients themselves, the
iirfilt crcf create prototype() method can be used to generate the object directly (see § 15.7
).
liquid_iirdes(_ftype, _btype, _format, _n, _fc, _f0, _Ap, _As, *_B, *_A);
• format is the output format of the coefficients, e.g. LIQUID IIRDES SOS
• f0 is the normalized center frequency of the analog prototype (only applicable to bandpass
and band-stop filter designs, ignored for low-pass and high-pass filter designs)
• Ap is the pass-band ripple (only applicable to Chebyshev Type-I and elliptic filter designs,
ignored for Butterworth, Chebyshev Type-II, and Bessel designs)
• As is the stop-band ripple (only applicable to Chebyshev Type-II and elliptic filter designs,
ignored for Butterworth, Chebyshev Type-I, and Bessel designs)
As an example, the following example designs a 5th -order elliptic band-pass filter with 1 dB ripple
in the passband, 60 dB ripple in the stop-band, a cut-off frequency of fc /Fs = 0.2and a center
frequency f0 /Fs = 0.25; the frequency response of the resulting filter can be found in Figure 23 .
15.8 iirdes (infinite impulse response filter design) 113
1 // file: doc/listings/iirdes.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int order=5; // filter order
7 float fc = 0.20f; // cutoff frequency (low-pass prototype)
8 float f0 = 0.25f; // center frequency (band-pass, band-stop)
9 float As = 60.0f; // stopband attenuation [dB]
10 float Ap = 1.0f; // passband ripple [dB]
11
12 // derived values
13 unsigned int N = 2*order; // effective order (double because band-pass)
14 unsigned int r = N % 2; // odd/even order
15 unsigned int L = (N-r)/2; // filter semi-length
16
17 // filter coefficients arrays
18 float B[3*(L+r)];
19 float A[3*(L+r)];
20
21 // design filter
22 liquid_iirdes(LIQUID_IIRDES_ELLIP,
23 LIQUID_IIRDES_BANDPASS,
24 LIQUID_IIRDES_SOS,
25 order,
26 fc, f0, Ap, As,
27 B, A);
28
29 // print results
30 unsigned int i;
31 printf("B [%u x 3] :\n", L+r);
32 for (i=0; i<L+r; i++)
33 printf(" %12.8f %12.8f %12.8f\n", B[3*i+0], B[3*i+1], B[3*i+2]);
34 printf("A [%u x 3] :\n", L+r);
35 for (i=0; i<L+r; i++)
36 printf(" %12.8f %12.8f %12.8f\n", A[3*i+0], A[3*i+1], A[3*i+2]);
37 }
-10
Power Spectral Density [dB]
-20
-30
-40
-50
-60
-70
-80
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
Figure 23: Example of the iirdes interface, designing a 5th -order elliptic band-pass filter with
1 dB ripple in the passband, 60 dB ripple in the stop-band, a cut-off frequency of fc /Fs = 0.2 and
a center frequency f0 /Fs = 0.25
15.8 iirdes (infinite impulse response filter design) 115
• Compute frequency pre-warping factor, m, to set cutoff frequency (and center frequency if
designing a band-pass or band-stop filter) using the iirdes freqprewarp() method.
• Convert the low-pass analog prototype Ha (s) to its digital equivalent Hd (z) (also in terms of
zeros, poles, and gain) using the bilinear z-transform using the bilinear zpkf() method.
This maps the analog zeros/poles/gain into digital zeros/poles/gain.
• Transform the low-pass digital prototype to high-pass, band-pass, or band-stop using the
iirdes dzpk lp2bp() method. For the band-pass and band-stop cases, the number of poles
and zeros will need to be doubled.
• Transform the digital z/p/k form of the filter to one of the two forms:
• LIQUID IIRDES BUTTER is a Butterworth filter. This is an all-pole analog design that has
a maximally flat magnitude response in the pass-band. The analog prototype interface is
butter azpkf() which computes the n complex roots pa0 , pa1 , . . . , pan−1 of the nth -order But-
terworth polynomial,
(2k + n + 1) π
pak = ωc exp j (65)
2n
for $k=0,1,\ldots,n-1$.
Note that this results in a set of complex conjugate pairs such that
$(-1)^n s_0 s_1 \cdots s_{n-1} = 1$.
An example of a digital filter response can be found in
{ref:fig:filter:butter};
116 15 FILTER
• LIQUID IIRDES CHEBY1 is a Chebyshev Type-I filter. This design uses Chebyshev polynomi-
als to create a filter with a sharper transition band than the Butterworth design by allowing
ripples in the pass-band. The analog prototype interface is cheby1 azpkf() which computes
the n complex roots pak of the nth -order Chebyshev polynomial. An example of a digital
filter response can be found in Figure 25 ;
• LIQUID IIRDES CHEBY2 is a Chebyshev Type-II filter. This design is similar to that of Cheby-
shev Type-I, except that the Chebyshev polynomial is inverted. This inverts the magnitude
response of the filter and exhibits an equiripple behavior in the stop-band, rather than the
pass-band. The analog prototype interface is cheby2 azpkf(). An example of a digital filter
response can be found in Figure 26
• LIQUID IIRDES ELLIP is an elliptic (Cauer) filter. This design allows ripples in both the pass-
band and stop-bands to create a filter with a very sharp transition band. The design process
is somewhat more involved than the Butterworth and Chebyshev prototypes and requires
solving the elliptic integral of different moduli. For a more detailed description we refer the
interested reader to [Orfanidis:2006]. The analog prototype interface is ellip azpkf(). An
example of a digital filter response can be found in Figure 27 ;
• LIQUID IIRDES BESSEL is a Bessel filter. This is an all-pole analog design that has a maxi-
mally flat group delay response (maximally linear phase response). The solution to the design
happens to be the roots to the Bessel polynomials of equal order. Computing the roots to
the polynomial is, again, somewhat complex. For a more detailed description we refer the
interested reader to [Orchard:1965]. The analog prototype interface is bessel azpkf(). An
example of a digital filter response can be found in Figure 28 .
where m = ωc cot (πωc /ωs ) is the frequency pre-warping factor, computed in [liquid] via the method
iirdes freqprewarp(). Multiplying both the numerator an denominator by (1 + z −1 )np and
applying some algebraic manipulation results in the digital filter
(1 − zd0 z −1 )(1 − zd1 z −1 ) · · · (1 − zdn−1 z −1 )
Hd (s) = kd (70)
(1 − pd0 z −1 )(1 − pd1 z −1 ) · · · (1 − pdn−1 z −1 )
The bilinear zpk() method in [liquid] transforms the the analog zeros (zak ), poles (pak ), and gain
(H0 ) into their digital equivalents (zdk , pdk , G0 ). For a filter with nz analog zeros zak the digital
zeros zdk are computed as (
1+mzak
k < nz
zdk = 1−mzak (71)
−1 otherwise
where m is the pre-warping factor. For a filter with np analog poles pak the digital poles pdk are
computed as
1 + mpak
pdk = (72)
1 − mpak
Keeping in mind that an analog filter’s order is defined by its number of poles, the digital gain can
be computed as
np −1
Y 1 − pdk
G0 = H0 (73)
1 − zdk
k=0
where r = 0 when the filter order is odd, r = 1 when the filter order is even, and L = (n−r)/2. This
can be accomplished using the iirdes dzpk2sosf() method and is preferred over the traditional
transfer function design for stability reasons.
118 15 FILTER
0
Power Spectral Density [dB]
-10
-20
-30
-40
-50
-60
-70
-80
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
(a) spectrum
zeros
poles
0 0.5 1
0
Power Spectral Density [dB]
-10
-20
-30
-40
-50
-60
-70
-80
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
(a) spectrum
zeros
poles
0 0.5 1
0
Power Spectral Density [dB]
-10
-20
-30
-40
-50
-60
-70
-80
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
(a) spectrum
zeros
poles
0 0.5 1
0
Power Spectral Density [dB]
-10
-20
-30
-40
-50
-60
-70
-80
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
(a) spectrum
zeros
poles
0 0.5 1
0
Power Spectral Density [dB]
-10
-20
-30
-40
-50
-60
-70
-80
0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
(a) spectrum
zeros
poles
0 0.5 1
4 int main() {
5 unsigned int M=4; // interpolation factor
6 unsigned int h_len; // interpolation filter length
7
8 // design filter and create interpolator
9 float h[h_len]; // filter coefficients
10 firinterp_crcf q = firinterp_crcf_create(M,h,h_len);
11
12 // generate input signal and interpolate
13 float complex x; // input sample
14 float complex y[M]; // output samples
15
16 // run interpolator (repeat as necessary)
17 {
18 firinterp_crcf_execute(q, x, y);
19 }
20
Listed below is the full interface to the interp family of objects. While each method is listed for
firinterp crcf, the same functionality applies to interp rrrf and interp cccf.
• firinterp crcf create(M,*h,N) creates an interp object with an interpolation factor M using
N filter coefficients h.
• firinterp crcf create prototype(M,m,As) create an interp object using a filter proto-
type designed using the firdes kaiser window() method (see § 15.5 ) with a normalized
cut-off frequency 1/2M , a filter length of 2M m coefficients, and a stop-band attenuation of
As dB.
• firinterp crcf destroy(q) destroys the interpolator, freeing all internally-allocated mem-
ory.
• firinterp crcf print(q) prints the internal properties of the interpolator to the standard
output.
124 15 FILTER
2
interp
symbols
1
Real
-1
-2
-5 0 5 10 15 20 25 30 35 40
Sample Index
2
interp
symbols
1
Imag
-1
-2
-5 0 5 10 15 20 25 30 35 40
Sample Index
Figure 29: firinterp crcf (interpolator) example with M = 4, compensating for filter delay.
• firinterp crcf execute(q,x,*y) executes the interpolator for an input x, storing the re-
sult in the output array y (which has a length of M samples).
A graphical example of the interpolator can be seen in Figure 29 . A detailed example program is
given in examples/firinterp crcf example.c, located under the main [liquid] project directory.
x ↓2 ↓2 ... ↓2 1
≤r≤1 y
2
(a) decimation
x 1≤r≤2 ↑2 ↑2 ... ↑2 y
(b) interpolation
Figure 30: msresamp (multi-stage resampler) block diagram showing both interpolation and
decimation modes
• msresamp crcf destroy(q) destroys the resampler, freeing all internally-allocated memory.
• msresamp crcf print(q) prints the internal properties of the resampler to the standard
output.
• msresamp crcf get delay(q) returns the number of samples of delay in the output (can be
a non-integer value).
Figure 31 gives a graphical depiction in both the time and frequency domains of the multi-stage
resampler acting as an interpolator. The time series has been aligned (shifted by the filter delay
and scaled by the resampling rate) to show equivalence. For a more detailed example, refer to
examples/msresamp crcf example.c located in the main [liquid] project source directory.
which holds for h(n) = w(n) sin(πn/M )/(πn) since sin(πn/M ) = 0 for n = any non-zero multiple
of M. Additionally, M = 2 is the special case of half-band filters. In particular half-band filtering
is computationally efficient because half the coefficients of the filter are zero, and the remaining
half are symmetric (so long as w(n) is also symmetric). In theory, this means that for a filter
length of 4m + 1 taps, only mcomputations are necessary [harris:2004]. The resamp2 object in
[liquid] uses a Kaiser window for w(n) for several reasons, but in particular because it is nearly
optimum, and it is easy to trade side-lobe attenuation for transition bandwidth. Listed below is
the full interface to the resamp2 family of objects. While each method is listed for resamp2 crcf,
the same functionality applies to resamp2 rrrf and resamp2 cccf.
• resamp2 crcf create(m,f0,As) creates a resamp2 object with a resampling rate 2, a filter
semi-length of m samples (equivalent filter length 4m + 1), centered at frequency f0 , and a
stop-band suppression of As dB.
• resamp2 crcf destroy(q) destroys the resampler, freeing all internally-allocated memory.
15.11 resamp2 (half-band filter/resampler) 127
1 original
Real resampled
-1
-20 -10 0 10 20 30 40 50
Input Sample Index
1 original
resampled
Imag
-1
-20 -10 0 10 20 30 40 50
Input Sample Index
(a) time
20
original
resampled
0
Power Spectral Density [dB]
-20
-40
-60
-80
-100
-120
-0.4 -0.2 0 0.2 0.4
Normalized Output Frequency
(b) PSD
Figure 31: msresamp crcf (multi-stage resampler) √ interpolator demonstration with a stop-band
suppression As = 60 dB at the irrational rate r = 19 ≈ 4.359
128 15 FILTER
• resamp2 crcf print(q) prints the internal properties of the resampler to the standard out-
put.
1 // file: doc/listings/resamp2_crcf.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int m = 7; // filter semi-length
7 float As=-60.0f; // resampling filter stop-band attenuation
8
9 // create half-band resampler
10 resamp2_crcf q = resamp2_crcf_create(m,0.0f,As);
11
12 float complex x; // complex input
13 float complex y[2]; // output buffer
14
15 // ... initialize input ...
16 {
17 // execute half-band resampler as interpolator
18 resamp2_crcf_interp_execute(q, x, y);
19 }
20
21 // ... repeat as necessary ...
22
Figure 32 gives a graphical depiction in both the time and frequency domains of the half-band resam-
pler acting as an interpolator. The time series has been aligned (shifted by the filter delay and scaled
by the resampling rate) to show equivalence. For more detailed and extensive examples, refer to
examples/resamp2 crcf decim example.c and examples/resamp2 crcf interp example.c located
in the main [liquid] project source directory.
15.11 resamp2 (half-band filter/resampler) 129
1 original
interpolated
Real
-1
-10 0 10 20 30 40 50
Input Sample Index
1 original
interpolated
Imag
-1
-10 0 10 20 30 40 50
Input Sample Index
(a) time
20
original
interpolated
filter
0
Power Spectral Density [dB]
-20
-40
-60
-80
-100
-120
-0.4 -0.2 0 0.2 0.4
Normalized Output Frequency
(b) PSD
Figure 33 gives a graphical depiction of the arbitrary resampler, in both the time and frequency
domains. The time series has been aligned (shifted by the filter delay and scaled by the resampling
rate) to show equivalence. Additionally, the signal’s power spectrum has been scaled by r to reflect
the change in sampling rate. In the example the input array size is 187 samples; the resampler
15.13 symsync (symbol synchronizer) 131
produced 133 output samples which yields √ a true resampling rate of ṙ = 133/187 ≈ 0.71123
which is close to the target rate of r = 1/ 2 ≈ 0.70711. It is important to understand how
filter design impacts the performance of the resampler. The resamp object interpolates between
available sample points to minimize aliasing effects on the output signal. This is apparent in the
power spectral density plot in Figure 33 which shows very little aliasing on the output signal.
Aliasing can be reduced by increasing the filter length at the cost of additional computational
complexity; additionally the number of filters in the bank can be increased to improve timing
resolution between samples. For synchronization of digital receivers, it is always good practice to
precede the resampler with an anti-aliasing filter to remove out-of-band interference. Listed below
is the full interface to the resamp family of objects. While each method is listed for resamp crcf,
the same functionality applies to resamp rrrf and resamp cccf.
• resamp crcf destroy(q) destroys the resampler, freeing all internally-allocated memory.
• resamp crcf print(q) prints the internal properties of the resampler to the standard output.
• resamp crcf execute(q,x,*y,*nw) executes the resampler for an input sample x, storing
the resulting samples in the output array yspecifying the number of samples written as nw .
The output buffer y needs to be at least dre.
This timing error estimate has significant improvements over heuristic-based estimates such as the
popular Mueller and Muller timing recovery scheme [Mueller:1976]. Applying a simple first-order
recursive loop filter yields the averaged timing estimate
3
original
2 resampled
1
Real
-1
-2
-3
-20 0 20 40 60 80 100 120 140 160 180 200
Input Sample Index
3
original
2 resampled
1
Imag
-1
-2
-3
-20 0 20 40 60 80 100 120 140 160 180 200
Input Sample Index
(a) time
20
original
resampled
0
Power Spectral Density [dB]
-20
-40
-60
-80
-100
-120
-0.4 -0.2 0 0.2 0.4
Normalized Input Frequency
(b) PSD
√
Figure 33: resamp crcf (arbitrary resampler) demonstration, r = 1/ 2 ≈ 0.7071
15.13 symsync (symbol synchronizer) 133
where α = 1 − ωτ and β = 0.22ωτ are the loop filter coefficients for a given filter bandwidth ωτ .
While these coefficients are certainly not optimized, it is important to understand the difficulty
in computing loop filter coefficients when a delay is introduced into a control loop. This delay is
the result of the matched filter itself and can cause instability with traditional phase-locked loop
filter designs. Internally the symsync object uses the principles of the resamp object (arbitrary
resampler, see § 15.12 ) for resampling the signal—actually decimating to one sample per symbol.
Its internal control loop dynamically adjusts the rate r such that the timing phase of the receiver
is aligned with the incoming signal’s symbol timing. Below is a code example demonstrating the
symsync interface. Notice that the symsync crcf execute() method also returns the number of
symbols written to the output buffer.
1 // file: doc/listings/symsync_crcf.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int k=2; // samples/symbol
7 unsigned int m=3; // filter delay (symbols)
8 float beta=0.3f; // filter excess bandwidth factor
9 unsigned int Npfb=32; // number of polyphase filters in bank
10 liquid_rnyquist_type ftype = LIQUID_RNYQUIST_RRC;
11
12 // create symbol synchronizer
13 symsync_crcf q = symsync_crcf_create_rnyquist(ftype,k,m,beta,Npfb);
14
Listed below is the full interface to the symsync family of objects. While each method is listed for
symsync crcf, the same functionality applies to symsync rrrf and symsync cccf.
• symsync crcf create(k,N,*h,h len) creates a symsync object from a prototype filter h of
length hlen and having k samples per symbol. The internal object restructures the input filter
into a polyphase prototype having N filters.
Nyquist filters in [liquid]). The generated filter has k samples per symbol, a nominal delay of
m symbols, and an excess bandwidth factor of β. The internal polyphase filter bank has N
filters.
• symsync crcf destroy(q) destroys the symbol synchronizer, freeing all internally-allocated
memory.
• symsync crcf print(q) prints the internal properties of the symbol synchronizer object to
the standard output.
• symsync crcf clear(q) resets the symbol synchronizer, clearing the internal buffers and
filter state.
• symsync crcf set lf bw(q,w) sets the internal bandwidth of the loop filter to ω.
• symsync crcf lock(q) locks the symbol synchronizer such that it will still decimate the
incoming signal but will not update its internal state.
• symsync crcf unlock(q) unlocks the symbol synchronizer, resuming its ability to track to
the input signal.
• symsync crcf execute(q,*x,nx,*y,*ny) executes the resampler for an input array x with
nx samples, storing the resulting samples in the output array yspecifying the number of sam-
ples written as ny .
• symsync crcf get tau(q) returns the current timing estimate (fractional sampling interval)
of the object.
Figure 34 demonstrates the symsync crcf object recovering the sample timing phase for a QPSK
signal. For a more detailed example, refer to examples/symsync crcf example.c located under
the main [liquid] project source directory.
15.13 symsync (symbol synchronizer) 135
1.5
0.5
Real
-0.5
-1
-1.5
0 100 200 300 400 500
Symbol Index
1.5
0.5
Imag
-0.5
-1
-1.5
0 100 200 300 400 500
Symbol Index
1.5
first 250 symbols
last 250 symbols
0.5
Quadrature phase
-0.5
-1
-1.5
-1.5 -1 -0.5 0 0.5 1 1.5
In-phase
(b) Constellation
Figure 34: symsync (symbol synchronizer) demonstration for a QPSK signal with a square-root
raised-cosine pulse with k = 2 samples/symbol, a delay of m = 4 symbols, and an excess bandwidth
factor β = 0.3
136 16 FRAMING
16 framing
The framing module contains objects and methods for packaging data into manageable frames
and packets. For convention, [liquid] refers to a ”packet” as a group of binary data bytes (often
with forward error-correction applied) that need to be communicated over a wireless link. Objects
that operate on packets in [liquid] are the bpacketgen, bpacketsync and packetizer structures.
By contrast, a ”frame” is a representation of the data once it has been properly partitioned,
encapsulated, and modulated before transmitting over the air. Framing objects included in [liquid]
are the frame64, flexframe, gmskframe, and ofdmflexframe structures which greatly simplify
over-the-air digital communication of raw data.
16.1 interleaver
This section describes the functionality of the [liquid] interleaver object. In wireless communi-
cations systems, bit errors are often grouped together as a result of multi-path fading, demodulator
symbol errors, and synchronizer instability. Interleavers serve to distribute grouped bit errors
evenly throughout a block of data which aids certain forward error-correction (FEC) codes in their
decoding process (see § 13 on error-correcting codes). On the transmit side of the wireless link,
the interleaver re-orders the bits after FEC encoding and before modulation. On the receiving
side, the de-interleaver re-shuffles the bits to their original position before attempting to run the
FEC decoder. The bit-shuffling order must be known at both the transmitter and receiver. The
interleaver object operates by permuting indices on the input data sequence. The indices are
computed during the interleaver create() method and stored internally. At each iteration data
bytes are re-shuffled using the permutation array. Depending upon the properties of the array,
multiple iterations should not result in observing the original data sequence. Shown below is a
simple example where 8 symbols (0, . . . , 7) are re-ordered using a random permutation. The data
at iteration 0 are the original data which are permuted twice.
forward
permutation iter[0] iter[1] iter[2]
0 -> 6 0 6 1
1 -> 4 1 4 3
2 -> 7 2 7 5
3 -> 0 3 0 6
4 -> 3 4 3 0
5 -> 2 5 2 7
6 -> 1 6 1 4
7 -> 5 7 5 2
Reversing the process is as simple as computing the reverse permutation from the input; this is
equivalent to reversing the arrows in the forward permutation (e.g. the 2 → 7 forward permutation
becomes the 7 → 2reverse permutation).
reverse
permutation iter[2] iter[1] iter[0]
0 -> 3 1 6 0
1 -> 6 3 4 1
2 -> 5 5 7 2
3 -> 4 6 0 3
16.1 interleaver 137
4 -> 1 0 3 4
5 -> 7 7 2 5
6 -> 0 4 1 6
7 -> 2 2 5 7
Notice that permuting indices only re-orders the bytes of data and does nothing to shuffle the bits
within the byte. It is beneficial to FEC decoders to separate the bit errors as much as possible.
Therefore, in addition to index permutation, [liquid] also applies masks to the data while permuting.
16.1.1 Interface
The interleaver object operates like most objects in [liquid] with typical create(), destroy(),
and execute() methods.
• interleaver destroy(q) destroys the interleaver object, freeing all internally-allocated mem-
ory arrays.
• interleaver set num iterations(q,k) sets the number of iterations of the interleaver. In-
creasing the number of iterations helps improve bit dispersion, but can also increase execution
time. The default number of iterations at the time of creation is 2 (see Figure 35 ).
• interleaver encode(q,*msg dec,*msg enc) runs the forward interleaver, reading data from
the first array argument and writing the result to the second array argument. The array
pointers can reference the same block of memory, if necessary.
• interleaver decode(q,*msg enc,*msg dec) runs the reverse interleaver, reading data from
the first array argument and writing the result to the second array argument. Like the
encode() method, the array pointers can reference the same block of memory.
This listing gives a basic demonstration to the interface to the interleaver object:
1 // file: doc/listings/interleaver.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int n=9; // message length (bytes)
7
8 // create the interleaver
9 interleaver q = interleaver_create(n);
10 interleaver_set_depth(q,3);
11
12 // create arrays
13 unsigned char msg_org[n]; // original message data
14 unsigned char msg_int[n]; // interleaved data
15 unsigned char msg_rec[n]; // de-interleaved, recovered data
16
17 // ...initialize msg_org...
138 16 FRAMING
18
19 // interleave/de-interleave the data
20 interleaver_encode(q, msg_org, msg_int);
21 interleaver_decode(q, msg_int, msg_rec);
22
A visualization of the interleaver can be seen in Figure 35 where the input index is plotted against
the output index for varying number of iterations. Notice that with zero iterations, the output
and input are identical (no interleaving). With one iteration only the bytes are interleaved, and so
the output is grouped into 8-bit blocks. Further iterations, however, result in sufficiently dispersed
bits, and patterns between input and output indices become less evident. The packetizer object
(§ 16.2 ) uses the interleaver object in conjunction to forward error-correction coding (§ 13 ) to
provide a simple interface for generating protected data packets. A full example can be found in
examples/interleaver example.c.
512
384
Output bit index
256
128
0
0 128 256 384 512
Input bit index
(a) i = 0
512
384
Output bit index
256
128
0
0 128 256 384 512
Input bit index
(b) i = 1
512
384
ndex
140 16 FRAMING
• packetizer decode(q,*pkt,*msg) decodes the k-byte encoded input message storing the
result in the n-byte output. The function returns a 1 if the internal CRC passed and a 0 if
it failed. If no CRC was specified (e.g. LIQUID CRC NONE) then a 1 is always returned.
• packetizer decode soft(q,*pkt,*msg) decodes the encoded input message just like packetizer decode()
but with soft bits instead of hard bytes. The input is an array of type unsigned char with
8 × kelements representing soft bits. As before, the function returns a 1 if the internal CRC
passed and a 0 if it failed. See § 13.7.1 for more information on soft-decision decoding.
1 // file: doc/listings/packetizer.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // set up the options
6 unsigned int n=16; // uncoded data length
7 crc_scheme crc = LIQUID_CRC_32; // validity check
8 fec_scheme fec0 = LIQUID_FEC_HAMMING74; // inner code
9 fec_scheme fec1 = LIQUID_FEC_REP3; // outer code
10
11 // compute resulting packet length
12 unsigned int k = packetizer_compute_enc_msg_len(n,crc,fec0,fec1);
13
14 // set up the arrays
15 unsigned char msg[n]; // original message
16 unsigned char packet[k]; // encoded message
17 unsigned char msg_dec[n]; // decoded message
18 int crc_pass; // decoder validity check
19
20 // create the packetizer object
21 packetizer p = packetizer_create(n,crc,fec0,fec1);
22
1010110110101100101...0110001111010011001...0110010110001100011100011001000110...
Figure 36: Structure used for the bpacketgen and bpacketsync objects.
• bpacketgen print(q) prints the internal state of the bpacketgen object to the standard
output.
142 16 FRAMING
• bpacketgen get packet len(q) returns the length in bytes of the fully-encoded packet.
• bpacketgen encode(q,*msg,*pkt) encodes the n-byte input message msg, storing the result
in the encoded output packet pkt.
• bpacketsync print(q) prints the internal state of the bpacketsync object to the standard
output.
• bpacketsync execute byte(q,byte) runs the synchronizer on a single byte of received data.
• bpacketsync execute sym(q,sym,bps) runs the synchronizer on a symbol with bps bits of
information.
The callback is typically defined to be static and is passed to the instance of bpacketsync object
when it is created.
• payload is a pointer to the decoded bytes of payload data. This pointer is not static and
cannot be used after returning from the callback function. This means that it needs to be
copied locally for you to retain the data.
• payload valid is simply a flag to indicate if the payload passed its cyclic redundancy check
(”0” means invalid, ”1” means valid). If this flag is zero then the payload most likely has
errors in it. Some applications are error tolerant and so it is possible that the payload data
are still useful. Typically, though, the payload should be discarded and a re-transmission
request should be issued.
• userdata is a pointer that given to the bpacketsync object when it was created. This
pointer is passed to the callback and can represent just about anything. Typically it points
to another structure and is the method by which the decoded header and payload data are
returned to the program outside of the callback.
41
42 // push packet through synchronizer
43 bpacketsync_execute(ps, msg_enc, k);
44
45 // clean up allocated objects
46 bpacketgen_destroy(pg);
47 bpacketsync_destroy(ps);
48 }
The framegen64 and framesync64 objects implement a basic framing structure for communicating
packetized data over the air. The framegen64 object accepts a 12-byte header and 64-byte payload
and assemble a 1280sample frame. Internally, the frame generator encodes the header and payload
each with a Hamming(12,8) block code, 16-bit cyclic redundancy check, and modulates the result
with a QPSK modem. The header and payload are encapsulated with special phasing sequences,
and finally the resulting symbols are interpolated using a half-rate root-raised cosine filter (see
§ 15.5.3 ). The true spectral efficiency of the frame is exactly 4/5; 64 bytes of data (512 bits)
encoded into 640 symbols. The frame64 structure has the advantage of simplicity but lacks the
ability for true flexibility.
The flexframegen and flexframesync objects are similar to their frame[gen|sync]64 counter-
parts, however extend functionality to include a number of options in structuring the frame.
While the specifics of the frame64 and flexframe structures are different, both frames consist of
six basic parts:
• ramp/up gracefully increases the output signal level to avoid ”key clicking” and reduce spectral
side-lobes in the transmitted signal. Furthermore, it allows the receiver’s automatic gain
control unit to lock on to the incoming signal, preventing sharp transitions in its output.
16.4 frame64, flexframe (basic framing structures) 145
signal level
g
in
ce
as
wn
en
up
d
ph
er
do
oa
qu
p
ad
e
yl
m
se
p
bl
he
pa
m
ra
ra
p/
ea
pr
time
Figure 37: Framing structure used for the frame64 and flexframe objects.
• preamble phasing is a BPSK pattern which flips phase for each transmitted symbol (+1,-1,+1,-1,...).
This sequence serves several purposes but primarily to help the receiver’s symbol synchro-
nization circuit lock onto the proper timing phase. [This works] because the phasing pattern
maximizes the number of symbol transitions [reword].
• p/n sequence is an m-sequence (see § 24 ) exhibiting good auto- and cross-correlation proper-
ties. This sequence aligns the frame synchronizers to the remainder of the frame, telling them
when to start receiving and decoding the frame header, as well as if the phase of the received
signal needs to be reversed. At this point, the receiver’s AGC, carrier PLL, and timing PLL
should all have locked. The p/n sequence is of length 64 for both the frame64 and flexframe
structures (63-bit m-sequence with additional padded bit).
• header is a fixed-length data sequence which contains a small amount of information about
the rest of the frame. The headers for the frame64 and flexframe structures are vastly
different and are described independently.
• payload is the meat of the frame, containing the raw data to be transferred across the link. For
the frame64 structure, the payload is fixed at 64 bytes (hence its moniker), encoded using
the Hamming(12,8) code (§ 13 ), and modulated using QPSK. The flexframe structure
has a variable length payload and can be modulated using whatever schemes the user desires,
however forward error-correction is executed externally. In both cases the synchronizer object
invokes the callback upon receiving the payload.
A graphical depiction of the framing signal level can be seen in Figure 37 . The relative lengths
of each section are not necessarily to scale, particularly as the flexframe structure allows many
of these sections to be variable in length. NOTE: while the flexframegen and flexframesync
objects are intended to be used in conjunction with one another, the output of flexframegen
requires matched-filtering interpolation before the flexframesync object can recover the data.
such as noise, sample timing offset, and carrier frequency and phase offsets. The receiver corrects
for these impairments as best it can using various other signal processing elements in [liquid] and
attempts to decode the frame. If at any time a frame is decoded (even if improperly), its appropriate
user-defined callback function is invoked. When seeking a frame the synchronizer initially sets its
internal loop bandwidths high for acquisition, including those for the automatic gain control, symbol
timing recovery, and carrier frequency/phase recovery. This is known as acquisition mode, and is
typical for packet-based communications systems. Once the p/n sequence has been found, the
receiver assumes it has a sufficient lock on the channel impairments and reduces its control loop
bandwidths significantly, moving to tracking mode.
• agc gmin/agc gmax are the respective maximum/minimum automatic gain control gain val-
ues. The default values are 10−3 and 104 , respectively.
• sym bw0/sym bw1 are the respective open/closed symbol synchronizer bandwidths. The de-
fault values are 0.08 and 0.05, respectively.
• pll bw0/pll bw1 are the respective open/closed carrier phase-locked loop bandwidths. The
default values are 0.02 and 0.005, respectively.
• k represents the matched filter’s samples per symbol; however this parameter is reserved for
future development. At present this number should be equal to 2 and should not be changed.
• npfb represents the number of filters in the internal symbol timing recovery object’s polyphase
filter bank (see § 15.13 ); however this parameter is reserved for future development and should
not be changed. The default value is 32.
• m represents the matched filter’s symbol delay; however this parameter is reserved for future
development and should not be changed. the default value is 3.
• beta represents the matched filter’s excess bandwidth factor; however this parameter is re-
served for future development and should not be changed. the default value is 0.7.
• squelch enabled is a flag that specifies if the automatic gain control’s squelch is enabled
(see § 8.3 ). Enabling the squelch (setting squelch enabled equal to 1) will ignore received
signals below the squelch threshold value (see below) to help prevent the receiver’s control
loops from drifting. Enabling the squelch is usually desirable; however care must be taken
to properly set the threshold—ideally about 4 dB above the noise floor—so as not to miss
frames with a weak signal. By default the squelch is disabled.
16.6 framesyncstats s (frame synchronizer statistics) 147
• autosquelch enabled is a flag that specifies if the automatic gain control’s auto-squelch
is enabled (see § 8.5 ). In brief, the auto-squelch attempts to track the signal’s power to
automatically squelch signals 4 dB above the noise floor. By default the auto-squelch is
disabled.
• squelch threshold is the squelch threshold value in dB (see § 8.3 ). The default value is
-35.0, but the ideal value is about 4 dB above the noise floor.
• eq len specifies the length of the internal equalizer (see § 12 ). By default the length is set
to zero which disables equalization of the receiver.
• eqrls lambda is the recursive least-squares equalizer forgetting factor λ(see § 12.3 ). The
default value is λ = 0.999.
• evm is an estimate of the received error vector magnitude in decibels of the demodulated
header (see § 19.2 ).
• rssi is an estimate of the received signal strength in dB. This is derived from the synchro-
nizer’s internal automatic gain control object (see § 8 ).
• framesyms a pointer to an array of the frame symbols (e.g. QPSK) at complex baseband
before demodulation. This is useful for plotting purposes. This pointer is not static and
cannot be used after returning from the callback function. This means that it needs to be
copied locally for you to retain the data.
• mod bps the modulation depth (bits per symbol) of the modulation scheme used in the frame.
• check the error-detection scheme (e.g. cyclic redundancy check) used in the payload of the
frame (see § 13 ).
• fec0 the inner forward error-correction code used in the payload (see § 13 ).
• fec1 the outer forward error-correction code used in the payload (see § 13 ).
A simple way to display the information in an instance of framesyncstats s is to use the framesyncstats print()
method.
148 16 FRAMING
• Preamble The preamble consists of two types of phasing symbols: the Section s
and Section l
sequences. The Section s
symbols are necessary for coarse carrier frequency and timing offsets while the Section l
sequence is used for fine timing acquisition and equalizer gain estimation. The transmitter
generates two Section s
symbols and just a single Section l
symbol. This aligns the receiver’s timing to that of the transmitter, signaling the start of the
header.
• Header The header consists of one or more OFDM symbols; the exact number of OFDM
symbols depends on the number of subcarriers allocated and the assignment of these sub-
carriers (null/pilot/data). The header carries exactly 14 bytes of information, 6 of which
are used internally and the remaining 8 are user-defined. The internal header data provide
framing information to the receiver including the modulation, forward error-correction, and
data validation schemes of the payload as well as its length in bytes. These data are encoded
with a forward error-correction scheme and modulated onto the first several OFDM symbols.
• Payload The payload consists of zero or more OFDM symbols. Like the header, the exact
number of OFDM symbols depends on the number of subcarriers allocated and the assignment
of these subcarriers.
16.7 ofdmflexframe (OFDM framing structures) 149
header payload
S0 S0 S1 H0 H1 P0 P1 P(n-1)
... ...
time
Figure 38: Timing structure used for the ofdmflexframegen and ofdmflexframesync objects.
The cyclic prefix is highlighted.
The full frame is assembled according to Figure 38 . Notice that the Section s
symbols do not contain a cyclic prefix; this is to ensure continuity between contiguous Section s
symbols and is necessary to eliminate inter-symbol interference. The single Section l
symbol at the end of the preamble is necessary for timing alignment and an initial equalizer estimate.
Once the frame has been detected, the header is received and decoded. The number of symbols
in the header depends on the number of data subcarriers allocated to each OFDM symbol. The
header includes the modulation, coding schemes, and length of the remainder of the frame. If the
synchronizer successfully decodes the header, it will automatically reconfigure itself to decode the
payload.
• OFDMFRAME SCTYPE NULL: The null option disables this subcarrier from transmission. This is
useful for spectral notching and guard bands (see Figure 39 ). Guard bands are necessary for
interpolation of the signal before transmission;
• OFDMFRAME SCTYPE PILOT: Pilot subcarriers are used to estimate channel impairments includ-
ing carrier phase/frequency offsets as well as timing offsets. Pilot subcarriers are necessary
for coherent demodulation in OFDM systems. The ofdmflexframe structure requires that at
least two subcarriers be designated as pilots. Performance improves if the pilots are evenly
spaced and separated as much as possible (see Figure 39 ), but the exact location of pilots is
not restricted;
• OFDMFRAME SCTYPE DATA: Data subcarriers are reserved for carrying the payload of the frame,
modulated with the desired scheme. The spectral efficiency of the transmission improves with
more data subcarriers. The ofdmflexframe structure requires that at least one subcarrier be
designated for data.
Typically the subcarriers at the band edges are disabled to avoid aliasing during up-conversion/interpolation.
The elements of p are given in the same order as the FFT input (that is, p0 holds the DC subcarrier
and pM/2 holds the subcarrier at half the sampling frequency). The ofdmframe init default sctype(M,*p)
interface initializes the subcarrier allocation array pfor a system with M channels that is expected
to perform relatively well under a variety of channel conditions. Figure 39 depicts an example spec-
tral response of the ofdmflexframe structure with evenly-spaced pilot subcarriers, guard bands,
and a spectral notch in the lower band.
150 16 FRAMING
frequency
−Fs 0 Fs
Figure 39: Example spectral response for the ofdmflexframegen and ofdmflexframesync ob-
jects.
Figure 40 demonstrates how the window is applied to each OFDM symbol; the cyclic postfix of
the previous symbol overlaps the current symbol’s cyclic prefix with the applied window providing
a graceful transition region. Notice that the tapering window does not extend the length of the
symbol beyond its cyclic prefix.
16.7.5 ofdmflexframegen
The ofdmflexframegen object is responsible for assembling raw data bytes into contiguous OFDM
time-domain symbols which the ofdmflexframesync object can receive. The life cycle of the
generator is as follows:
• create the frame generator, passing the number of subcarriers, cyclic prefix length, subcar-
rier allocation, and framing properties (modulation scheme, forward error-correction coding,
payload length, etc);
16.7 ofdmflexframe (OFDM framing structures) 151
time
• write the OFDM symbols (time series) to a buffer until the entire frame has been generated;
• repeat the ”assemble” and ”write symbol” steps for as many frames as is desired;
This listing gives a basic demonstration to the interface to the ofdmflexframegen object:
1 // file: doc/listings/ofdmflexframegen.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int M = 64; // number of subcarriers
7 unsigned int cp_len = 16; // cyclic prefix length
8 unsigned int taper_len = 4; // taper length
9 unsigned int payload_len = 120; // length of payload (bytes)
10
11 // buffers
12 float complex buffer[M + cp_len]; // time-domain buffer
13 unsigned char header[8]; // header data
14 unsigned char payload[payload_len]; // payload data
15 unsigned char p[M]; // subcarrier allocation (null/pilot/data)
16
26 ofdmframe_init_default_sctype(M, p);
27
28 // create frame generator
29 ofdmflexframegen fg = ofdmflexframegen_create(M,cp_len,taper_len,p,&fgprops);
30
16.7.6 ofdmflexframesync
The ofdmflexframesync object is responsible for detecting frames generated by the ofdmflexframesync
object, decoding the header and payloads, and passing the results back to the user by way of a call-
back function. This listing gives a basic demonstration to the interface to the ofdmflexframegen
object:
1 // file: doc/listings/ofdmflexframesync.example.c
2 # include <liquid / liquid.h>
3
4 // callback function
5 int mycallback(unsigned char * _header,
6 int _header_valid,
7 unsigned char * _payload,
8 unsigned int _payload_len,
9 int _payload_valid,
10 framesyncstats_s _stats,
11 void * _userdata)
12 {
13 printf("***** callback invoked!\n");
14 return 0;
15 }
16
17 int main() {
18 // options
19 unsigned int M = 64; // number of subcarriers
20 unsigned int cp_len = 16; // cyclic prefix length
21 unsigned int taper_len = 4; // taper length
22 unsigned char p[M]; // subcarrier allocation (null/pilot/data)
23 void * userdata; // user-defined data
24
40 ofdmflexframesync_destroy(fs);
41 }
Notice that the input buffer can be any length, regardless of the synchronizer object’s properties.
Listed below is the full interface to the ofdmflexframesync object.
• ofdmflexframesync get cfo(q) queries the frame synchronizer for the received carrier fre-
quency offset (relative to sampling rate).
• ofdmflexframesync get rssi(q) queries the frame synchronizer for the received signal strength
of the input (given in decibels).
The callback function for the ofdmflexframesync object has seven arguments and looks like this:
int ofdmflexframesync_callback(unsigned char * _header,
int _header_valid,
unsigned char * _payload,
unsigned int _payload_len,
int _payload_valid,
framesyncstats_s _stats,
void * _userdata);
The callback is typically defined to be static and is passed to the instance of ofdmflexframesync
object when it is created. The return value can be ignored for now and is reserved for future
development. Here is a brief description of the callback function’s arguments:
• header is a volatile pointer to the 8 bytes of decoded user-defined header data from the
frame generator.
• header valid is simply a flag to indicate if the header passed its cyclic redundancy check.
If the check fails then the header data have been corrupted beyond the point that internal
error correction can recover; in this situation the payload cannot be recovered.
16.7 ofdmflexframe (OFDM framing structures) 155
100
Frame Detection
Header Decoding
Probability of Missed Detection/Decoding
-1
10
10-2
-3
10
-6 -4 -2 0 2 4 6 8
SNR [dB]
• payload is a volatile pointer to the decoded payload data. When the header cannot be
decoded ( header valid == 0) this value is set to NULL.
• payload len is the length (number of bytes) of the payload array. When the header cannot
be decoded ( header valid == 0) this value is set to 0.
• payload valid is simply a flag to indicate if the payload passed its cyclic redundancy check
(”0” means invalid, ”1” means valid). As with the header, if this flag is zero then the payload
almost certainly contains errors.
• userdata is the void pointer given to the ofdmflexframesync create() method. Typically
this pointer is a vehicle for getting the header and payload data (as well as any other pertinent
information) back to your main program.
156 16 FRAMING
16.7.7 Performance
Figure 41 shows the performance characteristics of the ofdmflexframe structure for M = 64
subcarriers with default subcarrier allocation. The frame can be detected with a 90% probability
with an SNR level of just -0.4 dB. Furthermore, the probability of detecting the frame and decoding
the header reaches 90% at just 2.6 dB SNR. Decoding the remainder of the frame depends on many
factors such as the modulation scheme and forward error-correction schemes applied. Here are a
few general guidelines for good performance:
• Equalization improves with more subcarriers, but the carrier frequency offset requirements
have tighter restrictions;
• While the interface supports nearly any number of subcarriers desired, synchronization greatly
improves with at least M = 32active (pilot/data) subcarriers;
• More pilot subcarriers can improve performance in low SNR environments at the penalty of
reduced throughput (fewer subcarriers are allocated for data);
• Increasing the cyclic prefix is really only necessary for high multi-path environment with a
large delay spread. Capacity can be increased for most short-range applications by reducing
the cyclic prefix to 4 samples, regardless of the number of subcarriers;
• Most hardware have highly non-linear RF front ends (mixers, amplifiers, etc.) which require
a transmit power back-off by a few dB to ensure linearity, particularly when many subcarriers
are used.
157
17 math
The math module implements several useful functions for digital signal processing including tran-
scendental function not necessarily in the standard C library, windowing functions, and polynomial
manipulation methods.
[liquid] computes Γ(z) from ln Γ(z)(see below) due to its steep, exponential response to z. The
complete Gamma function is defined as
Z ∞
Γ(z) , tz−1 e−t dt (79)
0
The upper an lower incomplete Gamma functions are described in Sections § 17.1.3 and § 17.1.2
, respectively. The natural log of the complete Gamma function is computed by splitting into
158 17 MATH
By definition the sum of the lower and upper incomplete gamma functions is the complete Gamma
function: Γ(z) = γ(z, α) + Γ(z, α). As such, [liquid] computes the upper incomplete Gamma
function as
Γ(z, α) = Γ(z) − γ(z, α) (84)
∞ 1 2 k
4z
z ν X
Iν (z) = (89)
2 k!Γ(k + ν + 1)
k=0
Due to its steep response to z it is often useful to compute Iν (z) by first computing ln Iν (z) as
1 2 k
"∞ #
X
4 z
ln Iν (z) = ν ln(z/2) + ln (90)
k!Γ(ν + k + 1)
k=0
"∞ #
X n o
= ν ln(z/2) + ln exp 2k ln(z/2) − ln Γ(k + 1) − ln Γ(ν + k + 1) (91)
k=0
(92)
This is a particularly useful approximation for the Kaiser window in fixed-point math where w[n]
is computed as the ratio of two large numbers.
160 17 MATH
where Iν is the modified Bessel function of the first kind (see § 17.1.8 ). [liquid] implements QM (α, β)
with the function liquid MarcumQf(M,a,b) using the approximation [Helstrom:1992((25))]
β−α−M
QM (α, β) ≈ erfc(u), u = , σ = M + 2α (100)
σ2
which works over a reasonable range of M , α, and β. The special case for M = 1 is implemented
in [liquid] using the function liquid MarcumQ1f(M,a,b) using the expansion [Helstrom:1960],
∞
α2 + β 2 X α k
Q1 (α, β) = exp − Ik (αβ) (101)
2 β
k=0
The function hamming(n,N) computes the nth of N indices of the Hamming window:
The function hann(n,N) computes the nth of N indices of the Hann window:
The function blackmanharris(n,N) computes the nth of N indices of the Blackman-harris window:
3
X
w(n) = ak cos (2πkn/(N − 1)) (110)
k=0
The function kaiser(n,N,dt,beta) computes the nth of N indices of the Kaiser-β window with a
shape parameter β:
r !
2
n
I0 πβ 1 − N/2
w(n, β) = (111)
I0 (πβ)
where Iν (z) is the modified Bessel function of the first kind of order ν, and β is a parameter
controlling the width of the window and its stop-band attenuation. In [liquid], I0 (z) is computed
using liquid besseli0f() (see § 17.1 ). A fractional sample offset ∆t can be introduced by
n
substituting N/2 with n+∆t
N/2 in (111) .
The function liquid kbd window(n,beta,*w) computes the n-point Kaiser-Bessel derived window
with a shape parameter β storing the result in the n-point array w. The length of the window must
be even.
17.3 Windowing functions 163
0.8
temporal window
0.6
0.4
0.2
0
-20 -10 0 10 20
Sample Index
-20
Power Spectral Density [dB]
-40
-60
-80
-100
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
164 17 MATH
0.8
temporal window
0.6
0.4
0.2
0
-20 -10 0 10 20
Sample Index
-20
Power Spectral Density [dB]
-40
-60
-80
-100
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
17.3 Windowing functions 165
0.8
temporal window
0.6
0.4
0.2
0
-20 -10 0 10 20
Sample Index
-20
Power Spectral Density [dB]
-40
-60
-80
-100
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
166 17 MATH
0.8
temporal window
0.6
0.4
0.2
0
-20 -10 0 10 20
Sample Index
-20
Power Spectral Density [dB]
-40
-60
-80
-100
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
17.3 Windowing functions 167
0.8
temporal window
0.6
0.4
0.2
0
-20 -15 -10 -5 0 5 10 15 20
Sample Index
-20
Power Spectral Density [dB]
-40
-60
-80
-100
-0.4 -0.2 0 0.2 0.4
Normalized Frequency
168 17 MATH
17.4 Polynomials
A number of [liquid] modules require polynomial manipulations, particularly those involving filter
design where transfer functions are represented as the explicit ratio of polynomials in z −1 . This
sub-module is not intended to be complete, but rather is required for the proper functionality of
other modules. Like matrices, polynomials in [liquid] do not use a particular data type, but are
stored as memory arrays.
n
X
Pn (x) = ck xk = c0 + c1 x + c2 x2 + · · · + cn xn (112)
k=0
An nth -order polynomial has n + 1 coefficients ordered in memory in increasing degree. 18 ote
that this convention is reversed from that used in Octave. For example, a 2nd -order polynomial
0.1 − 2.4x + 1.3x2 stored in an array float c[] has c[0]=0.1, c[1]=-2.4, and c[2]=1.3. Notice
that all routines for the type float are prefaced with polyf. This follows the naming convention of
the standard C library routines which append an f to the end of methods operating on floating-
point precision types. Similar matrix interfaces exist in [liquid] for double (poly), double complex
(polyc), and float complex (polycf).
where
x20 · · · xk0
1 x0
1 x1 x21 · · · xk1
X=
(114)
2 k
1 xn−1 xn−1 · · · xn−1
For example this script fits the 4 data samples to a linear (first-order, two coefficients) polynomial:
18
N
17.4 Polynomials 169
The polyf fit lagrange(*x,*y,n,*p) method fit a dataset of n sample points to exact polynomial
of order n − 1 using Lagrange interpolation. Given input vectors x = [x0 , x1 , · · · , xn−1 ]T and
y = [y0 , y1 , · · · , yn−1 ]T , the interpolating polynomial is
n−1 n−1
X Y x − xk
Pn−1 (x) = yj (115)
xj − xk
j=0 k=0
k6=j
For example this script fits the 4 data samples to a cubic (third-order, four coefficients) polynomial:
The polyf interp lagrange(*x,*y,n,x0) method uses Lagrange polynomials to find the inter-
polant (ẋ, ẏ) from a set of n pairs x = [x0 , x1 , · · · , xn−1 ]T and y = [y0 , y1 , · · · , yn−1 ]T .
n−1 n−1
X Y ẋ − xk
ẏ = yj (116)
xj − xk
j=0 k=0
k6=j
0
y
-1
-1 0 1
x
The polyf fit lagrange barycentric(*x,n,*w) method computes the barycentric weights w of
x via
1
wj = Q (117)
k6=j (xj − xk )
which can be used to compute the interpolant (ẋ, ẏ)with fewer computations.
{input:doc/latex.gen/math_polyfit_lagrange.tex}
17.4 Polynomials 171
This is the preferred method for computing Lagrange interpolating polynomials, particularly if x
is unchanging. The function returns ẏ if ẋ is equal to any xj .
float x[4] = {0.0f, 1.0f, 2.0f, 3.0f};
float y[4] = {0.85f, 3.07f, 5.07f, 7.16f};
float w[4];
polyf_fit_lagrange_barycentric(x,4,w);
float x0 = 0.5f;
float y0 = polyf_val_lagrange_barycentric(x,y,w,x0,4);
>>> y0 = 2.00687504
Lagrange polynomials of the barycentric form are used heavily in [liquid]’s implementation of the
Parks-McClellan algorithm (firdespm) for filter design (see § 15.5.5 ).
For example the following script will compute P3 (x) = (1 + x)2 (1 − x):
float p[4];
polyf_expandbinomial_pm(2,1,p);
>>> p = { 1.00000000, 1.00000000, -1.00000000, -1.00000000}
172 17 MATH
where rk are the roots of Pn (x). For example, this script will expand the polynomial P3 (x) =
(x − 1)(x + 2)(x − 3) which has roots {1, −2, 3}:
float roots[3] = {1.0f, -2.0f, 3.0f};
float p[4];
polyf_expandroots(roots,3,p);
>>> p = { 6.00000000, -5.00000000, -2.00000000, 1.00000000}
by first factoring out the bk terms, invoking polyf expandroots(), and multiplying the result by
Q
k bk . For example, this script will expand the polynomial P3 (x) = (2x − 1)(−3x + 2)(−x − 3):
there exists at least one quadratic polynomial p2 (x) = u + vx + x2 which exactly divides Pn (x) and
has two roots (possibly complex)
1 p 1 p
r0 = −v − v 2 − 4u , r1 = −v + v 2 − 4u (124)
2 2
If indeed the roots r0 and r1 are complex, they are also complex conjugates. Bairstow’s method
uses Newtonian iterations to find a pair u and v which are both finite and real-valued. This method
has several advantages over other methods
• iterations operate on real-valued math, even if the roots are complex
17.5 Modular Arithmetic 173
• the algorithm is capable of handling multiple roots (unlike the Durand-Kerner method), i.e.
Pn (x) = (x − 2)(x − 2)(x − 2) · · ·
• the algorithm does not rely on expanding the full polynomial and is therefore resilient to
machine precision
Each iteration of Bairstow’s algorithm reduces the original polynomial order by two, eventually
collapsing the polynomial. The initial choice of u and v determine both algorithm convergence
and speed. [liquid] implements Bairstow’s method with the polyf findroots() function which
accepts an nth -order polynomial in standard expanded form and computes its n roots. The last
term of the polynomial (highest order) cannot be zero, otherwise the algorithm will not converge.
where the product is computed over the unique factors of n. For example, if n = 24 = 23 · 3, then
ϕ(24) = 24 1 − 21 1 − 13 = 8.
175
18 matrix
Matrices are used for solving linear systems of equations and are used extensively in polynomial
fitting, adaptive equalization, and filter design. In [liquid], matrices are represented as just arrays
of a single dimension, and do not rely on special objects for their manipulation. This is to help
portability of the code and ease of integration into other libraries. Here is a simple example of the
matrix interface:
1 // file: doc/listings/matrix.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // designate X as a 4 x 4 matrix
6 float X[16] = {
7 0.84382, -2.38304, 1.43061, -1.66604,
8 3.99475, 0.88066, 4.69373, 0.44563,
9 7.28072, -2.06608, 0.67074, 9.80657,
10 6.07741, -3.93099, 1.22826, -0.42142};
11 matrixf_print(X,4,4);
12
13 // L/U decomp (Doolittle’s method)
14 float L[16], U[16], P[16];
15 matrixf_ludecomp_doolittle(X,4,4,L,U,P);
16 }
Notice that all routines for the type float are prefaced with matrixf. This follows the naming
convention of the standard C library routines which append an f to the end of methods operating
on floating-point precision types. Similar matrix interfaces exist in [liquid] for double (matrix),
double complex (matrixc), and float complex (matrixcf).
and can be used for both setting and retrieving values of a matrix. For example,
X =
0.406911 0.118444 0.923281 0.827254 0.463265
0.038897 0.132381 0.061137 0.880045 0.570341
176 18 MATRIX
float v = matrix_access(X,4,5,0,1);
v =
0.118444
matrix_access(X,4,5,2,3) = 0;
X =
0.406911 0.118444 0.923281 0.827254 0.463265
0.038897 0.132381 0.061137 0.880045 0.570341
0.151206 0.439508 0.695207 0.0 0.999683
0.808384 0.601597 0.149171 0.975722 0.205819
Because this method is really just a macro, there is no error-checking to ensure that one is accessing
the matrix within its memory bounds. Therefore, special care must be taken when programming.
Furthermore, matrix access() can be used for all matrix types (matrixf, matrixcf, etc.).
18.1.2 matrixf add, matrixf sub, matrixf pmul, and matrixf pdiv (scalar math op-
erations)
The matrixf add(*x,*y,*z,m,n), matrixf sub(*x,*y,*z,m,n), matrixf pmul(*x,*y,*z,m,n),
and matrixf pdiv(*x,*y,*z,m,n) methods perform point-wise (scalar) addition, subtraction,
multiplication, and division of the elements of two n × m matrices, X and Y . That is, Z i,k =
X i,k + Y i,k for all i, k. The same holds true for subtraction, multiplication, and division. It is very
important to understand the difference between the methods matrixf pmul() and matrixf mul(),
as well as matrixf pdiv() and matrixf div(). In each case the latter performs a vastly different
operation from matrixf mul() and matrixf div() (see Sections § 18.2.3 and § 18.3.2 , respec-
tively).
X = Y =
0.59027 0.83429 0.764108 0.741641
0.67779 0.19793 0.660932 0.041723
0.95075 0.33980 0.972282 0.347090
matrixf_pmul(X,Y,Z,2,3);
Z =
0.4510300 0.6187437
0.4479731 0.0082582
0.9243971 0.1179412
Similarly, the matrixf hermitian(X,m,n,XH) computes the Hermitian transpose which is identical
to the regular transpose but without the conjugation operation, viz AH
i,j = Aj,i .
matrixf_swaprows(x,4,4,0,2);
7.28072023 -2.06608009 0.67074001 9.80657005
3.99475002 0.88066000 4.69372988 0.44563001
0.84381998 -2.38303995 1.43060994 -1.66603994
6.07741022 -3.93098998 1.22826004 -0.42142001
The pivot element must not be zero. Row a is left unchanged in B. All elements of B in column b
are zero except for row a. This is accomplished in [liquid] with the matrixf pivot(*A,m,n,i,j)
method. For our example 4 × 4 matrix x, pivoting around x1,2 gives:
178 18 MATRIX
matrixf_pivot(x,4,4,1,2);
0.37374675 2.65145779 0.00000000 1.80186427
3.99475002 0.88066000 4.69372988 0.44563001
-6.70986557 2.19192743 0.00000000 -9.74288940
-5.03205967 4.16144180 0.00000000 0.53803295
Multiplication of two input matrices A and B is accomplished with the matrixf mul(*A,ma,na,*B,mb,nb,*C,mc,n
method, and is not to be confused with matrixf pmul() in § 18.1.2 . If A is m × n and B is n × p,
then their product is computed as
n−1
X
AB i,j
= Ai,r B r,j (130)
r=0
Note that the number of columns of A must be equal to the number of rows of B, and that the
resulting matrix is of size m × p(the number of rows in A and columns in B).
A = B =
1 2 3 1 2 3
4 5 6 4 5 6
7 8 9
matrixf_mul(A,2,3, B,3,3, C,2,3);
C =
30 36 42
66 81 96
Next [liquid] performs elementary operations to convert to its row-reduced echelon form. The
resulting matrix has the identity matrix on the left and A−1 on its right, viz
The matrixf inv() method uses Gauss-Jordan elimination (see matrixf gjelim()) for row re-
duction and back-substitution. Pivot elements in A with the largest magnitude are chosen to help
stability in floating-point arithmetic.
matrixf_inv(x,4,4);
-0.33453920 0.04643385 -0.04868321 0.23879384
-0.42204019 0.12152659 -0.07431178 0.06774280
0.35104612 0.15256262 0.04403552 -0.20177667
0.13544561 -0.01930523 0.11944833 -0.14921521
A =
2.9002075 0.1722705 1.3046706 1.8082311
0.1722705 1.0730995 0.2497573 0.1470398
1.3046706 0.2497573 0.8930279 1.1471686
1.8082311 0.1470398 1.1471686 1.5155975
b =
11.7622252
-1.0541668
5.7372437
8.1291904
matrixf_cgsolve(A,4,4, x_hat, NULL)
x_hat =
2.8664699
-1.8786657
1.1224079
1.2764599
For a more complete example, see examples/cgsolve example.c located under the main project
directory.
n−1
Y
det (A) = |A| = U k,k (133)
k=0
This is equivalent to performing L/U decomposition using Crout’s method and then computing the
product of the diagonal elements of L.
matrixf_det(X,4,4) = 585.40289307
k−1
X
Li,k = Ai,k − Li,t U t,k ∀k ∈ {0, n − 1}, i ∈ {k, n − 1} (134)
t=0
k−1
" #
X
U k,j = Ak,j − Lk,t U t,j /Lk,k ∀k ∈ {0, n − 1}, j ∈ {k + 1, n − 1} (135)
t=0
18.3 Complex math operations 181
matrixf_ludecomp_crout(X,4,4,L,U,P)
L =
0.84381998 0.00000000 0.00000000 0.00000000
3.99475002 12.16227055 0.00000000 0.00000000
7.28072023 18.49547005 -8.51144791 0.00000000
6.07741022 13.23228073 -6.81350422 -6.70173073
U =
1.00000000 -2.82410932 1.69539714 -1.97440207
0.00000000 1.00000000 -0.17093502 0.68514121
0.00000000 0.00000000 1.00000000 -1.35225296
0.00000000 0.00000000 0.00000000 1.00000000
k−1
" #
X
Li,k = Ai,k − Li,t U t,k /U k,k ∀k ∈ {0, n − 1}, i ∈ {k + 1, n − 1} (137)
t=0
A =
1.0201000 -1.4341999 0.3232000 -1.0302000
-1.4341999 2.2663999 0.5506001 1.2883999
0.3232000 0.5506001 4.2325001 -1.4646000
-1.0302000 1.2883999 -1.4646000 5.0101995
matrixf_chol(A,4,Lp)
1.0100000 0.0000000 0.0000000 0.0000000
-1.4200000 0.5000000 0.0000000 0.0000000
0.3200000 2.0100000 0.3000003 0.0000000
-1.0200000 -0.3199999 -1.6499993 1.0700010
x = A−1 b (139)
This is also equivalent to augmenting A with b and converting it to its row-reduced echelon form.
If A is non-singular the resulting n × n + 1 matrix will hold x in its last column. The row-reduced
echelon form of a matrix is computed in [liquid] using the Gauss-Jordan elimination algorithm, and
can be invoked as such:
18.3 Complex math operations 183
Ab =
0.84381998 -2.38303995 1.43060994 -1.66603994 0.91488999
3.99475002 0.88066000 4.69372988 0.44563001 0.71789002
7.28072023 -2.06608009 0.67074001 9.80657005 1.06552994
6.07741022 -3.93098998 1.22826004 -0.42142001 -0.81707001
matrixf_gjelim(Ab,4,5)
1.00000000 -0.00000000 0.00000000 -0.00000000 -0.51971692
-0.00000000 1.00000000 0.00000000 0.00000000 -0.43340963
-0.00000000 -0.00000000 1.00000000 -0.00000000 0.64247853
0.00000000 -0.00000000 -0.00000000 0.99999994 0.35925382
Notice that the result contains I n in its first n rows and ncolumns (to within machine precision).
20 ow permutations (swapping) might have occurred.
20
r
184 19 MODEM
19 modem
The modem module implements a set of (mod)ulation/(dem)odulation schemes for encoding infor-
mation into signals. For the analog modems, samples are encoded according to frequency or analog
modulation. For the digital modems, data bits are encoded into symbols representing carrier fre-
quency, phase, amplitude, etc. This section gives a brief overview of modulation schemes available
in [liquid], and provides a brief description of the interfaces.
where fc is the carrier frequency, and k is the modulation index. The modulation index governs the
relative bandwidth of the signal. Two options for demodulation are possible: observing the instan-
taneous frequency on the output of a phase-locked loop, or computing the instantaneous frequency
using the delay-conjugate method. An example of the freqmodem can be found in Figure 43 An
example of the freqmodem interface is listed below.
1 // file: doc/listings/freqmodem.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 float kf = 0.1f; // modulation factor
6 liquid_freqdem_type type = LIQUID_FREQDEM_DELAYCONJ;
7
8 // create modulator/demodulator objects
9 freqmod fmod = freqmod_create(kf);
10 freqdem fdem = freqdem_create(kf, type);
11
12 float m; // input message
13 float complex s; // modulated signal
14 float y; // output/demodulated message
15
16 // repeat as necessary
17 {
18 // modulate signal
19 freqmod_modulate(fmod, m, &s);
20
21 // demodulate signal
22 freqdem_demodulate(fdem, s, &y);
19.1 Analog modulation schemes 185
1.5
input
input/output signal 1 demodulated
0.5
-0.5
-1
-1.5
0 50 100 150 200
Sample Index
1.5
1
modulated signal
0.5
-0.5
-1 real
imag
-1.5
0 50 100 150 200
Sample Index
-20
Power Spectral Density [dB]
-40
-60
-80
-100
-0.5 -0.4 -0.3 -0.2 -0.1 0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
Figure 43: freqmodem demonstration modulating an audio signal with a modulation index of
k = 0.1 and a relative carrier frequency fc /Fs = 0.1.
186 19 MODEM
23 }
24
25 // clean up objects
26 freqmod_destroy(fmod);
27 freqdem_destroy(fdem);
28 }
A more detailed example can be found in examples/freqmodem example.c located under the main
[liquid] project source directory. Listed below is the full interface to the freqmodem object for analog
frequency modulation/demodulation.
where fc is the carrier frequency, and k is the modulation index. For single side-band, only the
upper (USB) or lower half (LSB) of the spectrum is transmitted. The opposing half of the spectrum
is rejected using a Hilbert transform (see § 15.6 ). Let ṡ(t) represent the Hilbert transform of
the message signal s(t) such that its Fourier transform is non-zero only for positive frequency
components, viz (
S(ω) = F {s(t)} ω > 0
Ṡ(ω) = F {ṡ(t)} = (142)
0 ω≤0
Consequently the transmitted upper side-band signal is
(
ṡ(t)ej2πfc t suppressed carrier
xU SB (t) = 1 j2πf t (143)
2 1 + k ṡ(t) e unsuppressed carrier
c
19.1 Analog modulation schemes 187
For lower single side-band, ṡ(t) is simply conjugated. For suppressed carrier modulation the re-
ceiver uses a phase-locked loop for carrier frequency and phase tracking. When the carrier is not
suppressed the receiver demodulates using a simple peak detector and IIR bias removal filter. An
example of the freqmodem interface is listed below.
1 // file: doc/listings/ampmodem.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 float mod_index = 0.1f; // modulation index (bandwidth)
6 float fc = 0.0f; // AM carrier
7 liquid_ampmodem_type type = LIQUID_AMPMODEM_USB;
8 int suppressed_carrier = 0; // suppress the carrier?
9
10 // create mod/demod objects
11 ampmodem mod = ampmodem_create(mod_index, fc, type, suppressed_carrier);
12 ampmodem demod = ampmodem_create(mod_index, fc, type, suppressed_carrier);
13
14 float s; // input message
15 float complex x; // modulated
16 float y; // output/demodulated message
17
18 // repeat as necessary
19 {
20 // modulate signal
21 ampmodem_modulate(mod, s, &x);
22
23 // demodulate signal
24 ampmodem_demodulate(demod, x, &y);
25 }
26
27 // clean up objects
28 ampmodem_destroy(mod);
29 ampmodem_destroy(demod);
30 }
A more detailed example can be found in examples/ampmodem example.c located under the main
[liquid] project source directory. Listed below is the full interface to the ampmodem object for analog
frequency modulation/demodulation.
For arbitrary modulation schemes a linear search over all symbols in SM is required which has a
complexity of O(M 2 ), however one may take advantage of symmetries in certain constellations to
reduce this.
19.2.1 Interface
• modem create(scheme) creates a linear modulator/demodulator modem object with one of
the schemes defined in Table 9 .
• modem create arbitrary(*table,M) creates a generic arbitrary modem (LIQUID MODEM ARB)
with the M -point constellation map defined by the float complex array table. The resulting
constellation is normalized such that it is centered at zero and has unity energy. Note that
M must be equal to 2m where m is an integer greater than zero.
• modem reset(q) resets the internal state of the object. This method is really only relevant to
LIQUID MODEM DPSK (differential phase-shift keying) which retains the phase of the previous
symbol in memory. All other modulation schemes are memoryless.
• modem gen rand sym(q) generates a random integer symbol in {0, M − 1}.
• modem modulate(q,symbol,*x) modulates the integer symbol storing the result in the out-
put value of x. The input symbol value must be less than the constellation size M .
19.2 Linear digital modulation schemes 189
1 input
input/output signal demodulated
0.5
-0.5
-1
1 real
imag
modulated signal
0.5
-0.5
-1
-20
Power Spectral Density [dB]
-40
-60
-80
-100
-0.5 -0.4 -0.3 -0.2 -0.1 0 0.1 0.2 0.3 0.4 0.5
Normalized Frequency
Figure 44: ampmodem demonstration modulating an audio signal using double side-band, non-
suppressed carrier AM with a modulation index of k = 0.1 and a relative carrier frequency fc /Fs =
0.1.
190 19 MODEM
• modem demodulate(q,x,*symbol) finds the closest integer symbol which matches the input
sample x. The exact method by which [liquid] performs this computation is dependent upon
the modulation scheme. For example, while LIQUID MODEM QAM4, and LIQUID MODEM PSK4
are effectively equivalent (four points on the unit circle) they are demodulated differently.
• modem get demodulator sample(q,*xhat) returns the estimated transmitted symbol, x̂, af-
ter demodulation.
• modem get demodulator phase error(q) returns an angle proportional to the phase error
after demodulation. This value can be used in a phase-locked loop (see § 21.2 ) to correct for
carrier phase recovery.
• modem get demodulator evm(q) returns a value equal to the error vector magnitude after
demodulation. The error vector is the difference between the received symbol and the esti-
mated transmitted symbol, e = r − ŝ. The magnitude of the error vector is an indication to
the signal-to-noise/distortion ratio at receiver.
While the same modem structure may be used for both modulation and demodulation for most
schemes, it is important to use separate objects for differential-mode modems (e.g. LIQUID MODEM DPSK)
as the internal state will change after each symbol. It is usually good practice to keep separate
instances of modulators and demodulators. This holds true for most any encoder/decoder pair in
[liquid]. An example of the modem interface is listed below.
1 // file: doc/listings/modem.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // create mod/demod objects
6 modulation_scheme ms = LIQUID_MODEM_QPSK;
7
8 // create the modem objects
9 modem mod = modem_create(ms); // modulator
10 modem demod = modem_create(ms); // demodulator
11 modem_print(mod);
12
13 unsigned int sym_in; // input symbol
14 float complex x; // modulated sample
15 unsigned int sym_out; // demodulated symbol
16
17 // ...repeat as necessary...
18 {
19 // modulate symbol
20 modem_modulate(mod, sym_in, &x);
21
22 // demodulate symbol
23 modem_demodulate(demod, x, &sym_out);
192 19 MODEM
24 }
25
26 // destroy modem objects
27 modem_destroy(mod);
28 modem_destroy(demod);
29 }
Notice that the two encoded symbols 236 (11101100) and 228 (11100100) differ by only one bit.
Now if noise caused the receiver were to demodulate a symbol error, it would result in only a single
bit error instead of 4 without Gray coding. Reversing the process (decoding) is similar to encoding
but slightly more involved. Gray decoding is computed on an encoded input symbol by adding to
it (modulo 2) as many shifted versions of itself as it has bits. In our previous example the receiver
needs to map the received encoded symbol back to the original symbol before encoding:
11101100 gray_in (236) 11100100 gray_in (228)
.1110110 gray_in >> 1 .1110010 gray_in >> 1
..111011 gray_in >> 2 ..111001 gray_in >> 2
...11101 gray_in >> 3 ...11100 gray_in >> 3
....1110 gray_in >> 4 ....1110 gray_in >> 4
.....111 gray_in >> 5 .....111 gray_in >> 5
......11 gray_in >> 6 ......11 gray_in >> 6
.......1 gray_in >> 7 .......1 gray_in >> 7
xor : -------- --------
10110111 gray_out (183) 10111000 gray_out (184)
It is also interesting to note that in linear modems (e.g. PSK), the decoder is actually applied to
the symbol at the transmitter while the encoder is applied to the received symbol at the receiver.
In [liquid], Gray encoding and decoding are computed with the gray encode() gray decode()
methods, respectively.
19.2 Linear digital modulation schemes 193
With phase-shift keying the information is stored in the absolute phase of the modulated signal.
This means that each of M = 2m symbols in the constellation are equally spaced around the unit
circle. Figure 45 depicts the constellation of PSK up to M = 16with the bits gray encoded. While
[liquid] supports up to M = 256, values greater than M = 32are typically avoided due to error
rates for practical signal-to-noise ratios. For an M -symbol constellation, the k th symbol is
sk = ej2πk/M (145)
and QPSK (M = 4)
sk = ej (πk/4+ 4 )
π
(147)
Differential PSK (DPSK) encodes information in the phase change of the carrier. Like regular
PSK demodulation is performed independent of the signal amplitude; however because the data
are encoded using phase transitions rather than absolute phase, the receiver does not have to know
the absolute phase of the transmitter. This allows the receiver to demodulate incoherently, but at
a quality degradation of 3dB. As such the nth transmitted symbol k(n) depends on the previous
symbol, viz
j2π k(n) − k(n − 1)
sk (n) = exp (148)
M
0.5
1 0
0
Q
-0.5
-1
-1 -0.5 0 0.5 1
I
01
1
0.5
11 00
0
Q
-0.5
10
-1
-1 -0.5 0 0.5 1
I
011
1
010 001
0.5
19.2 Linear digital modulation schemes 195
1.5
01
1
0.5
11 10
0
Q
-0.5
00
-1
-1.5
-1.5 -1 -0.5 0 0.5 1 1.5
I
1.5
011
1
001
0.5 010
000 100
0
Q
110
-0.5
101
-1 111
-1.5
-1.5 -1 -0.5 0 0.5 1 1.5
I
1.5
0110
0010 1110
1
0011 1010
0.5 0111
196 19 MODEM
Amplitude-shift keying (ASK) is a simple form of amplitude modulation by which the information
is encoded entirely in the in-phase component of the baseband signal. The encoded symbol is
simply
sk = α 2k − M − 1 (149)
1 M =2
√
1/ 5 M =4
1/√21
M =8
α= √ (150)
1/ 85 M = 16
√
1/ 341
M = 32
√3/M
M > 32
Figure 47 depicts the ASK constellation map for M up to 16. Due to the poor error rate perfor-
mance of ASK values of M greater than 16 are not recommended.
Also known as quadrature amplitude-shift keying, QAM modems encode data using both the in-
phase and quadrature components of a signal amplitude. In fact, the symbol is split into indepen-
dent in-phase and quadrature symbols which are encoded separately as LIQUID MODEM ASK symbols.
Gray encoding is applied to both the I and Q symbols separately to help ensure minimal bit changes
between adjacent samples across both in-phase and quadrature-phase dimensions. This is made
evident in Figure ?? where one can see that the first three bits of the symbol encode the in-phase
component of the sample, and the last three bits encode the quadrature component of the sample.
We may formally describe the encoded sample is
n o
sk = α (2ki − Mi − 1) + j(2kq − Mq − 1) (151)
where ki is the in-phase symbol, kq is the quadrature symbol, Mi = 2mi and Mq = 2mq , are the
number of respective in-phase and quadrature symbols, mi = dlog2 (M )e and mq = blog2 (M )c
are the number of respective in-phase and quadrature bits, and α is a scaling factor to ensure
19.2 Linear digital modulation schemes 197
0.5
0 1
0
Q
-0.5
-1
-1 -0.5 0 0.5 1
I
(a) 2-ASK
1.5
0.5
00 01 11 10
0
Q
-0.5
-1
-1.5
-1.5 -1 -0.5 0 0.5 1 1.5
I
(b) 4-ASK
1.5
0.5
198 19 MODEM
E{s2k } = 1,
√
1/ 2
M =4
√
1/ 6 M =8
√
1/ 10 M = 16
√
1/ 26 M = 32
√
1/ 42 M = 64
1/√106
M = 128
α= √ (152)
1/ 170 M = 256
√
1/ 426 M = 512
√
1/ 682
M = 1024
√
1/ 1706
M = 2048
√
1/ 2730 M = 4096
p2/M
else
Figure 48 depicts the arbitrary rectangular QAM modem constellation maps for M up to 256.
Notice that all the symbol points are gray encoded to minimize bit errors between adjacent symbols.
[liquid] also allows the user to create their own modulation schemes by designating the full signal
constellation. The penalty for defining a constellation as an arbitrary set of points is that it cannot
be decoded systematically. All of the previous modulation schemes have the benefit of being very
fast to decode, and do not necessitate searching over the entire constellation space to find the
nearest symbol. An example interface for generating a pair of arbitrary modems is listed below.
1 // file: doc/listings/modem_arb.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // set modulation depth (bits/symbol)
6 unsigned int bps=4;
7 float complex constellation[1<<bps];
8
9 // ... (initialize constellation) ...
10
11 // create the arbitrary modem objects
12 modem mod = modem_create_arbitrary(constellation, 1<<bps);
13 modem demod = modem_create_arbitrary(constellation, 1<<bps);
14
15 // ... (modulate and demodulate as before) ...
16
17 // destroy modem objects
18 modem_destroy(mod);
19 modem_destroy(demod);
20 }
19.2 Linear digital modulation schemes 199
1.5
0
Q
-0.5
-1
-1.5
(a) 8-QAM
1.5
0.5
0011 0111 1111 1011
0
Q
-0.5
-1.5
(b) 16-QAM
1.5
0.5
Several pre-defined arbitrary signal constellations are available, including optimal QAM constella-
tions, and some other fun (but perhaps not so useful) modulation schemes. Figure 49 shows the
constellation maps for the optimal QAM schemes. Notice that the constellations approximate a
circle with each point falling on the lattice of equilateral triangles. Furthermore, adjacent con-
stellation points differ by typically only a single bit to reduce the resulting bit error rate at the
output of the demodulator. These constellations marginally out-perform regular square QAM (see
Figures Figure 55 and Figure 57 ) at the expense of a significantly increased computational com-
plexity. Figure 50 depicts several available arbitrary constellation maps; however the user can
create any arbitrary constellation map so long as no two points overlap (see modem arb init() and
modem arb init file() in § 19.2.1 ).
19.2.9 Performance
As discussed in § 13.8 , the performance of an error-correction scheme is typically measured in
the bit error rate (BER)—the average error probability for a bit to be in error in the presence
of additive white Gauss noise (AWGN). 21 ssuming the modulated symbols are uncorrelated and
identically distributed. The bit error rate (BER) performance of the different available modulation
schemes can be seen in Figures Figure 51 —Figure 58 , relative to the ratio of energy per bit to
noise power (Eb /N0 ). The raw data can be found in the doc/data/modem-ber/ subdirectory.
1.5
1 0110 1110
0010 1010
0.5
0011 0111 1111
1011
0
Q
0001
0101 1101 1001
-0.5
0000 1000
0100 1100
-1
-1.5
-1.5 -1 -0.5 0 0.5 1 1.5
I
1.5
11010
01110
10110
1 00010 11110
01010
10010
00110
10111
0.5 00011
11011
01111
10011
00111
11111
00001 01011
0
Q
10001
01101
00101 10101
11001
01001
-0.5 01100 10000
11101
00000
11000
01000
-1 00100
10100
11100
-1.5
-1.5 -1 -0.5 0 0.5 1 1.5
I
1.5
28 52
4 12 44
20 60 36
1
5 13 37
29 21 61 45
7 15 31 53
0.5 23 47 39 18
6 14 30 22 55 63 46 38
202 19 MODEM
1.5
0.5
0
Q
-0.5
11111 11101 11100 01100 01101 01111
-1
11010 11110 01110 01010
-1.5
-1.5 -1 -0.5 0 0.5 1 1.5
I
1.5
74 75 91 90 26 27 11 10
72 73 89 88 24 25 9 8
1
93 92 84 85 81 80 16 17 21 20 28 29
95 94 86 87 83 82 18 19 23 22 30 31
0.5
79 78 70 71 67 66 2 3 7 6 14 15
77 76 68 69 65 64 0 1 5 4 12 13
0
Q
-1.5
-1.5 -1 -0.5 0 0.5 1 1.5
I
1.5
1010
1
1011 0010 1000
0.5
0011 0000
19.2 Linear digital modulation schemes 203
0
10
BPSK/2-ASK
DPSK-2
OOK
10-1
10-2
BER
10-3
10-4
10-5
0 5 10 15 20 25 30
Eb/N0 [dB]
0
10
QPSK/4-QAM
4-APSK
4-DPSK
10-1 4-ASK
10-2
BER
10-3
10-4
10-5
0 5 10 15 20 25 30
Eb/N0 [dB]
0
10
8-APSK
8-QAM
8-PSK
10-1 8-DPSK
8-ASK
10-2
BER
10-3
10-4
10-5
0 5 10 15 20 25 30
Eb/N0 [dB]
0
10
16-QAM (opt)
16-QAM
16-APSK
10-1 V.29
16-PSK
16-DPSK
16-ASK
10-2
BER
10-3
10-4
10-5
0 5 10 15 20 25 30
Eb/N0 [dB]
0
10
32-QAM (opt)
32-SQAM
32-APSK
10-1 32-QAM
32-PSK
32-DPSK
10-2
BER
10-3
10-4
10-5
0 5 10 15 20 25 30
Eb/N0 [dB]
100
64-QAM (opt)
64-QAM
64-APSK
10-1 64-VT
64-PSK
64-DPSK
10-2
BER
10-3
10-4
10-5
0 5 10 15 20 25 30
Eb/N0 [dB]
100
128-QAM (opt)
128-SQAM
128-QAM
10-1 128-APSK
10-2
BER
10-3
10-4
10-5
0 5 10 15 20 25 30
Eb/N0 [dB]
100
256-QAM (opt)
256-QAM
256-APSK
10-1
10-2
BER
10-3
10-4
10-5
0 5 10 15 20 25 30
Eb/N0 [dB]
decoders, usually by about 1.5 dB. This soft bit information provides a clue to the decoder as to
the confidence that each bit was received correctly. For turbo-product codes [Berrou:1993] and low-
density parity check (LDPC) codes [Gallager:1962], this soft bit information is nearly a requirement.
Before we continue, let us define some nomenclature:
• M = 2m are the number of points in the constellation (constellation size).
• m = log2 (M ) are the number of bits per symbol in the constellation (modulation depth).
• {b0 , b1 , . . . , bm−1 } is the encoded bit string of sk and is simply the value of k in binary-coded
decimal.
ksk k22 = 1.
P
• SM = {s0 , s1 , . . . , sM −1 } is the set of all symbols in the constellation where 1/M k
• Sbj =t is the subset of SM where the bit at index j is equal to t ∈ {0, 1}.
For example, let the modulation scheme be the generic 4-PSK with the constellation map defined
in Figure ?? which has m = 2, M = 4, and SM = {s0 = 1, s1 = j, s2 = −j, s3 = −1}. Subsets:
• Sb0 =0 = {s0 = 1, s2 = −j} (right-most bit is 0)
• Sbj =0 ∪ Sbj =1 = SM , ∀j .
Let us represent the received signal at a sampling instant n as
where s is the transmitted symbol and w is a zero-mean complex Gauss random variable with
a variance σn2 = E{ww∗ }. Let the transmitted symbols be i.i.d. and drawn from a M -point
constellation, each with m bits of information such that the symbols belong to a set of constellation
points sk ∈ SM and E{sk s∗k } = 1. Assuming perfect channel knowledge, timing, and carrier offset
recovery, the log-likelihood ratio (LLR) of each bit bj is shown to be [LeGoff:1994(Eq. (8))]the
ratio of the two conditional a posteriori probabilities of each bit having been transmitted, viz.
P (bj = 1|observation)
Λ(bj ) = ln (154)
P (bj = 0|observation)
Assuming that the channel is memoryless the ”observation” is simply the received sample r in ((153)
) and does not depend on previous symbols; therefore P (bj = t|observation) = P (bj = t|r(n))and
212 19 MODEM
t ∈ {0, 1}. Furthermore, by assuming that the transmitted symbols are equally probable and that
the noise follows a Gauss distribution [Qiang:2003]the LLR reduces to
! !
X n o X n o
Λ(bj ) = ln exp kr − s+ k22 /2σn2 − ln exp kr − s− k22 /2σn2 (155)
s+ ∈Sbj =1 s− ∈Sbj =0
Conveniently, both the exponential and logarithm operations disappear; furthermore, the noise
variance becomes a scaling factor and is only used to influence the reliability of the obtained
LLR. Figure 59 depicts the soft bit demodulation algorithm for a received 16-QAM signal point,
corrupted by noise. The received sample is r = −0.65 − j0.47 which results in a hard demodulation
of 0001. The subfigures depict each of the four bits in the symbol {b0 , b1 , b2 , b3 }for which the soft
bit output is given, and show the nearest symbol for which a 0 and a 1 at that particular bit index
occurs. For example, Figure ?? shows that the nearest √ symbol containing a 0 at bit index 2 is
s1 =0001 (the hard decision demodulation) at√(−3 − j)/ 10while the nearest symbol containing
a 1 at bit index 2 is s3 =0011 at (−3 + j)/ 10. Plugging s− = s1 and s+ = s3 into ((156)
) and evaluating for σn = 0.2 gives Λ̃(b2 ) = −7.43. Because this number is largely negative, it
is very likely that the transmitted bit b2 was 0. This can be verified by Figure ?? which shows
that the distance from r to s− is much shorter than that of s+ . Conversely, Figure ?? shows
that b1 cannot be demodulated with such certainty; the distances from r to each of s+ and s−
are about the same. This is reflected in the relatively small LLR value of Λ̃(b1 ) = −0.28 which
suggests a high uncertainty in the demodulation of b1 . One major drawback of computing ((156) )
is that finding the maximum requires searching over all constellation points to find the one which
minimizes kr − sk k(where sk ∈ Sbj =t ) is particularly time-consuming. To circumvent this, [liquid]
only searches over a subset Sk ⊂ SM nearest to the hard-demodulated symbol (Sk will typically
only have about four values). This can be done quickly because the hard-demodulated symbol can
be found systematically for most modulation schemes (e.g. for LIQUID MODEM QAM only O(log2 M )
comparisons are needed to make a hard decision). If no symbols are found within Sk for a given bit
value such that Sk ∩ Sbj =t = ∅then the magnitude of Λ(bj ) is sufficiently large and contains little
soft bit information; that is Λ̃(bj ) 0 when Sk∩ Sbj =0 = ∅and Λ̃(bj ) 0 when Sk ∩ Sbj =1 = ∅.
It is guaranteed that Sk ∩ Sbj =0 ∪ Sk ∩ Sbj =1 6= ∅because sk must be in either Sbj =0 or Sbj =1 .
[liquid] performs soft demodulation with the modem demodulate soft(q,x,*symbol,*soft bits)
method. This is the same as the regular demodulate method, but also returns the ”soft” bits in
addition to an estimate of the original symbol. Soft bit information is stored in [liquid] as type
unsigned char with a value of 255 representing a very likely ‘1‘, and a value of 0 representing a
very likely ‘0‘. The erasure condition is 127. soft bit value: [0 1 2 3 ... 64 65 ... 127 ... 192 193
... 253 254 255] interpretation: very likely ’0’ likely ’0’ erasure likely ’1’ very likely ’1’ The fec and
packetizer objects can make use of this soft information to improve the probability of decoding a
packet (see § 13.7.1 and § 16.2 for details).
19.2 Linear digital modulation schemes 213
0
In Phase
0
In Phase
• gmskmod modulate(q,s,*y) modulates a symbol s ∈ {0, 1}, storing the output in k-element
array y.
• gmskdem demodulate(q,*y,*s) demodulates the k-element array y, storing the output sym-
bol (0 or 1) in the de-referenced pointer s.
• gmskdem set eq bw(q,w) sets the bandwidth (learning rate) of the internal LMS equalizer
to w where w ∈ [0, 0.5].
Listed below is an example to interfacing with the gmskmod and gmskdem modulator/demodulator
objects.
1 // file: doc/listings/gmskmodem.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // options
6 unsigned int k=4; // filter samples/symbol
7 unsigned int m=3; // filter delay (symbols)
8 float BT=0.3f; // bandwidth-time product
9
10 // create modulator/demodulator objects
11 gmskmod mod = gmskmod_create(k, m, BT);
12 gmskdem demod = gmskdem_create(k, m, BT);
13
14 unsigned int i;
15 unsigned int sym_in; // input data symbol
16 float complex x[k]; // modulated samples
17 unsigned int sym_out; // demodulated data symbol
18
19 {
20 // generate random symbol {0,1}
21 sym_in = rand() % 2;
22
23 // modulate
24 gmskmod_modulate(mod, sym_in, x);
25
26 // demodulate
27 gmskdem_demodulate(demod, x, &sym_out);
28 }
29
20 multichannel
The multichannel module contains objects and methods relating to digital signal processing tech-
niques to handle signals operating in multiple bands. This includes communications techniques such
as orthogonal frequency-divisional multiplexing (OFDM) as well as channelizers to efficiently up-
convert and downconvert a signal while simultaneously resampling each band. NOTE: this section
is a work in progress
20.2 FIR polyphase filterbank channelizer at double the output rate (firpfbch2)
Finite impulse response polyphase filterbank channelizer with an output rate 2Fs /M (firpfbch2)
20.3 ofdmframe
Orthogonal frequency-divisional multiplexing (OFDM) framing structure.
11 float complex * x;
12 unsigned int i;
13 // loop as necessary
14 {
15 // tx : generate complex sinusoid
16 nco_crcf_cexpf(nco_tx, &x[i]);
17
18 // compute phase error
19 float dphi = nco_crcf_get_phase(nco_tx) -
20 nco_crcf_get_phase(nco_rx);
21
22 // update pll
23 nco_crcf_pll_step(nco_rx, dphi);
24
25 // update nco objects
26 nco_crcf_step(nco_tx);
27 nco_crcf_step(nco_rx);
28 }
29
30 // destry nco object
218 21 NCO (NUMERICALLY-CONTROLLED OSCILLATOR)
31 nco_crcf_destroy(nco_tx);
32 nco_crcf_destroy(nco_rx);
33 }
21.1.2 Interface
Listed below is the full interface to the nco family of objects.
• nco crcf create(type) creates an nco object of type LIQUID NCO or LIQUID VCO.
• nco crcf destroy(q) destroys an nco object, freeing all internally-allocated memory.
• nco crcf print(q) prints the internal state of the nco object to the standard output.
• nco crcf set frequency(q,f) sets the frequency f (equal to the phase step size ∆θ).
• nco crcf adjust phase(q,dtheta) increments the internal nco phase by ∆θ.
• nco crcf step(q) increments the internal nco phase by its internal frequency, θ ← θ + ∆θ
• nco crcf get phase(q) returns the internal phase of the nco object, −π ≤ θ < π.
• nco crcf get frequency(q) returns the internal frequency (phase step size)
• nco crcf mix up(q,x,*y) rotates an input sample x by ejθ , storing the result in the output
sample y.
• nco crcf mix down(q,x,*y) rotates an input sample x by e−jθ , storing the result in the
output sample y.
• nco crcf mix block up(q,*x,*y,n) rotates an n-element input array x by ejθk for k ∈
{0, 1, . . . , n − 1}, storing the result in the output vector y.
• nco crcf mix block down(q,*x,*y,n) rotates an n-element input array x by e−jθk for k ∈
{0, 1, . . . , n − 1}, storing the result in the output vector y.
21.2 PLL (phase-locked loop) 219
∆φ
φ + F (s)
−
K
φ̂ s
The phase-locked loop object provides a method for synchronizing oscillators on different plat-
forms. It uses a second-order integrating loop filter to adjust the frequency of its nco based on an
instantaneous phase error input. As its name implies, a PLL locks the phase of the nco object to
a reference signal. The PLL accepts a phase error and updates the frequency (phase step size) of
the nco to track to the phase of the reference. The reference signal can be another nco object, or
a signal whose carrier is modulated with data. The PLL consists of three components: the phase
detector, the loop filter, and the integrator. A block diagram of the PLL can be seen in Figure 60
in which the phase detector is represented by the summing node, the loop filter is F (s), and the
integrator has a transfer function G(s) = K/s. For a given loop filter F (s), the closed-loop transfer
function becomes
G(s)F (s) KF (s)
H(s) = = (159)
1 + G(s)F (s) s + KF (s)
where the loop gain K absorbs all the gains in the loop. There are several well-known options for
designing the loop filter F (s), which is, in general, a first-order low-pass filter. In particular we
are interested in getting the denominator of H(s) to the standard form s2 + 2ζωn s + ωn2 where ωn
is the natural frequency of the filter and ζ is the damping factor. This simplifies analysis of the
overall transfer function and allows the parameters of F (s) to ensure stability.
The active lag PLL [Best:1997] has a loop filter with a transfer function F (s) = (1 + τ2 s)/(1 +
τ1 s)where τ1 and τ2 are parameters relating to the damping factor and natural frequency. This
220 21 NCO (NUMERICALLY-CONTROLLED OSCILLATOR)
A simple 2nd -order active lag IIR filter can be designed using the following method:
void iirdes_pll_active_lag(float _w, // filter bandwidth
float _zeta, // damping factor
float _K, // loop gain (1,000 suggested)
float * _b, // output feed-forward coefficients [size: 3 x 1]
float * _a); // output feed-back coefficients [size: 3 x 1]
Converting the denominator of ((164) ) into standard form yields the following equations for τ1 and
τ2 : r
K ωn τ2 K 2ζ
ωn = ζ= → τ1 = 2 τ2 = (165)
τ1 2 ωn ωn
The open-loop transfer function is therefore
1 + τ2 s
H 0 (s) = F (s)G(s) = K (166)
τ1 s2
Taking the bilinear z-transform of H 0 (s) gives the digital filter
(1 + τ2 /2) + 2z −1 + (1 − τ2 /2)z −2
H 0 (z) = H 0 (s) 1 1−z−1 = 2K (167)
s= 2
1+z −1
τ1 /2 − τ1 z −1 + (τ1 /2)z −2
A simple 2nd -order active PI IIR filter can be designed using the following method:
21.2 PLL (phase-locked loop) 221
• nco crcf pll step(q,dphi) advances the nco object’s internal phase with a phase error ∆φ
to the loop filter. This method only changes the frequency of the nco object and does not
update the phase until nco crcf step() is invoked. This is useful if one wants to only run
the PLL periodically and ignore several samples. See the example code below for help.
Here is a minimal example demonstrating the interface to the nco object and the internal phase-
locked loop:
1 // file: doc/listings/nco_pll.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 // create nco objects
6 nco_crcf nco_tx = nco_crcf_create(LIQUID_VCO); // transmit NCO
7 nco_crcf nco_rx = nco_crcf_create(LIQUID_VCO); // receive NCO
8
9 // ... initialize objects ...
10
11 float complex * x;
12 unsigned int i;
13 // loop as necessary
14 {
15 // tx : generate complex sinusoid
16 nco_crcf_cexpf(nco_tx, &x[i]);
17
18 // compute phase error
19 float dphi = nco_crcf_get_phase(nco_tx) -
20 nco_crcf_get_phase(nco_rx);
21
22 // update pll
23 nco_crcf_pll_step(nco_rx, dphi);
24
28 }
29
30 // destry nco object
31 nco_crcf_destroy(nco_tx);
32 nco_crcf_destroy(nco_rx);
33 }
See also examples/nco pll example.c and examples/nco pll modem example.c located in the
main [liquid] project directory. An example of the PLL can be seen in Figure 61 . Notice that during
the first 150 samples the NCO’s output signal is misaligned to the input; eventually, however, the
PLL acquires the phase of the input sinusoid and the phase error of the NCO’s output approaches
zero.
21.2 PLL (phase-locked loop) 223
1 input
nco
real
-1
1 input
nco
imag
-1
1
phase error [radians]
-1
-2
-3
0 50 100 150 200 250 300 350 400
Sample Index
22 optim (optimization)
The optim module in [liquid] implements several non-linear optimization algorithms including a
gradient descent search, a quasi-Newton search (experimental: see § 26.5 ) and an evolutionary
algorithm.
22.1.1 Theory
The gradient search is an iterative method and adjusts x proportional to the negative of the gradient
of f evaluated at the current location. The vector x is adjusted by
where γ[n] is the step size and ∇f (x[n]) is the gradient of f at x, at the nth iteration. The gradient
is a vector field which points to the greatest rate of increase, and is computed at x as
∂f ∂f ∂f
∇f (x) = , ,..., (169)
∂x0 ∂x1 ∂xN −1
In most non-linear optimization problems, ∇f (x) is not known, and must be approximated for
each value of x[n] using the finite element method. The partial derivative of the k th component is
estimated by computing the slope of f when xk is increased by a small amount ∆ while holding
all other elements of x constant. This process is repeated for all elements in x to compute the
gradient vector. Mathematically, the k th component of the gradient is approximated by
∂f (x) f (x0 , . . . , xk + ∆, . . . , xN −1 ) − f (x)
≈ (170)
∂xk ∆
Once ∇f (x[n]) is known, ∆x[n + 1] is computed and the optimizing vector is updated via
which is stable only for 0 ≤ α < 1. For flat regions, the gradient vector ∇f (x) is approximately
a constant ∆x, and x[n] therefore becomes a geometric series converging to ∆x/(1 − α). This
accelerates the algorithm across relatively flat regions of f . The momentum constant additionally
adds some stability for regions where the gradient method tends to oscillate, such as steep valleys
in f .
This helps improve stability and convergence over regions where the algorithm might oscillate due
to steep values of f .
22.1.4 Interface
Here is a summary of the parameters used in the gradient search algorithm and their default values:
• gradsearch print(q) prints the internal state of the gradient search object.
• gradsearch reset(q) resets the internal state of the gradient search object.
• gradsearch step(q) steps through a single iteration of the gradient search. The result is
stored in the original input vector vspecified during the create() method.
1 // file: doc/listings/gradsearch.example.c
2 # include <liquid / liquid.h>
3
4 // user-defined utility callback function
5 float myutility(void * _userdata, float * _v, unsigned int _n)
6 {
7 float u = 0.0f;
8 unsigned int i;
9 for (i=0; i<_n; i++)
10 u += _v[i] * _v[i];
11 return u;
12 }
13
14 int main() {
15 unsigned int num_parameters = 8; // search dimensionality
16 unsigned int num_iterations = 100; // number of iterations to run
17 float target_utility = 0.01f; // target utility
18
19 float v[num_parameters]; // optimum vector
20
21 // ... intialize v ...
22
Notice that the utility function is a callback that is completely defined by the user. Figure 62
depicts the performance of the gradient search for the Rosenbrock function, defined as f (x, y) =
(1 − x)2 + 100(y − x2 )2 for input parameters x and y. The Rosenbrock function has a minimum at
(x, y) = (1, 1); however the minimum lies in a deep valley which can be difficult to navigate. From
the figure it is apparent that finding the valley is trivial, but convergence to the minimum is slow.
3
10
2
10
101
100
-1
10
10-2
-3
101.5
0.5
y
1.5
0 1
0.5
0
-0.5
-0.5 -1
-1.5 x
104
102
Rosenbrock Utility
100
10-2
10-4
10-6
10-8
0 100 200 300 400 500
Iteration
Figure 62: gradsearch performance for 2-parameter Rosenbrock function f (x, y) = (1 − x)2 +
100(y − x2 )2 with a starting point of (x0 , y0 ) = (−0.1, 1.4). The minimum is located at (1, 1).
228 22 OPTIM (OPTIMIZATION)
• chromosome create(*b,n) creates a chromosome with n traits. The number of bits per trait
are specified in the array b.
• chromosome create basic(n,b) creates a chromosome with n traits and a constant b bits
for each trait.
• chromosome create clone(p) clones a chromosome from another one, including its repre-
sentation of traits, the number of bits per trait, as well as the values of the traits themselves.
Furthermore, the value of all the chromosome’s traits may be set with the appropriate init()
method:
• chromosome copy(q) copies an existing chromosomes’ internal traits; all other internal pa-
rameters must be equal.
• chromosome init(q,*v) initializes a chromosome’s discrete trait values to the input array
of unsigned int values v. The trait values are in the range [0, 2nk − 1] where nrepresents
the number of bits in the k th trait.
The values of specific traits can be retrieved using the value() methods. They are useful for
evaluating the fitness of the chromosome in the search algorithm’s callback function.
Finally the methods for use in the gasearch algorithm are described:
22.2 gasearch genetic algorithm search 229
• chromosome crossover(p1,p2,c,k) copies the first k bits of the first parent p1 and the re-
maining bits of the second parent p2 to the child chromosome c2 .
22.2.2 Interface
Listed below is a description for the gasearch object in [liquid].
• gasearch run(q,n,target utility) runs multiple iterations of the search algorithm, stop-
ping after either n iterations or if the target utility is met.
• gasearch getopt(q,*chromosome,*u) produces the best chromosome over the coarse of the
search evolution, as well as its utility.
15 int main() {
16 unsigned int num_parameters = 8; // dimensionality of search (minimum 1)
230 22 OPTIM (OPTIMIZATION)
Evolutionary algorithms are well-suited for discrete optimization problems, particularly where a
large number of parameters only hold a few values. The classic example is the knapsack problem
(constrained, non-linear) in which the selection of items with different weights and values must be
chosen to maximize the total value without exceeding a prescribed weight capacity. An example
of using the gasearch object in [liquid] to search over the solution space of the knapsack problem
can be found in the examples directory as examples/gasearch knapsack example.c.
231
1.2
histogram
true PDF
0.8
Probability Density
0.6
0.4
0.2
0
0 0.2 0.4 0.6 0.8 1
x
23 random
The random module in [liquid] includes a comprehensive set of random number generators useful for
simulation of wireless communications channels, particularly for generating noise as well as fading
channels. This includes the uniform, normal, circular (complex) Gaussian, Rice-K, and Weibull
distributions.
23.1 Uniform
The uniform random variable generator in [liquid] simply generates a number evenly distributed
in [0, 1). Internally [liquid] uses the standard rand() method for generating random integers and
then divides by RAND MAX, the maximum number that can be generated. The probability density
function is defined as (
1 if 0 ≤ x < 1
fX (x) = (175)
0 else.
The uniform random number generator is the basis for generating most other distributions in
[liquid].
histogram
0.45 true PDF
0.4
0.35
Probability Density
0.3
0.25
0.2
0.15
0.1
0.05
0
-4 -3 -2 -1 0 1 2 3 4
x
23.3 Exponential
The exponential distribution has a probability density function defined as
fX (x; λ) = λe−λx (177)
23.4 Weibull 233
3.5 histogram
true PDF
2.5
Probability Density
1.5
0.5
0
0 0.5 1 1.5 2 2.5
x
[liquid] generates exponential random variables by inverting the cumulative distribution function,
viz
FX (x; λ) = 1 − e−λx (178)
Specifically if U is uniform random variable with a distribution defined by (175) then X =
− ln U/λhas an exponential distribution defined by (178) . Exponential random number gener-
ator interface:
float randexpf(float _lambda);
float randexpf_pdf(float _x, float _lambda);
float randexpf_cdf(float _x, float _lambda);
23.4 Weibull
The Weibull distribution has a probability density function defined by
α x−γ α−1 exp − x−γ α
n o
x≥γ
fX (x; α, β, γ) = β β β (179)
0 else.
where α is the shape parameter, β is the scale parameter, and γ is the threshold parameter. [liquid]
generates Weibull random variables by inverting the cumulative distribution function, viz
( n α o
1 − exp − x−γ β x≥γ
FX (x; α, β, γ) = (180)
0 else.
234 23 RANDOM
histogram
true PDF
0.8
0.7
0.6
Probability Density
0.5
0.4
0.3
0.2
0.1
0
1 1.5 2 2.5 3 3.5 4 4.5
x
23.5 Gamma
The gamma distribution has a probability density function defined by
( α−1
x −x/β x ≥ 0
αe
fX (x; α, β) = Γ(α)β (181)
0 else.
0.25
histogram
true PDF
0.2
Probability Density
0.15
0.1
0.05
0
0 2 4 6 8 10 12 14 16
x
23.6 Nakagami-m
The Nakagami-m distribution is a versatile stochastic model for modeling radio links [Braun:1991]
and has often been regarded as the best distribution to model land mobile propagation due to
its ability to describe fading situations worse than Rayleigh, including one-sided Gaussian [Si-
mon:1998]. Empirical evidence regarding the efficacy the Nakagami-m distribution has on fading
profiles been presented in [Turin:1980, Suzuki:1977]. Thus statistical inference of the Nakagami-
m fading parameters are of interest in the design of adaptive radios such as optimized transmit
diversity modes [Cavers:1999, Ko:2003] and adaptive modulation schemes [Catreux:2002]. The
Nakagami-m probability density function is given by [Papoulis:2002]
m m 2m−1 −(m/Ω)x2
(
2
Γ(m) Ω x e x≥0
fX (x; m, Ω) = (182)
0 else.
236 23 RANDOM
2 histogram
true PDF
1.5
Probability Density
0.5
0
0 0.5 1 1.5 2
x
where m ≥ 1/2 is the shape parameter and Ω > 0 is the spread parameter. Nakagami-m random
numbers are generated from the gamma distribution. Specifically
√ if R follows a gamma distribution
defined by (181) with parameters α and β, then X = R has a Nakagami-m distribution with
m = α and Ω = β/α. Nakagami random number generator interface:
23.7 Rice-K
The Rice-K multi-path channel models a fading envelope by assuming a line of sight (LoS) com-
ponent to the multi-path elements summed at the receiver. The complex path gain at a partic-
ular frequency consists of a fixed (LoS) and fluctuating (diffuse) components. When assuming a
narrowband complex Gaussian stochastic process, the time-varying envelope will exhibit a Rice
distribution where the Kfactor is the power ratio of the LoS and diffuse components (often referred
to in dB) and thus is commonly used to describe fading environments. The Rice-K distribution
23.8 Data scrambler 237
histogram
true PDF
1.4
1.2
Probability Density
0.8
0.6
0.4
0.2
0
0 0.5 1 1.5 2 2.5
x
where Ω = E R2 is the average signal power and Kis the fading factor (shape parameter). [liquid]
’00000....’ or ’11001100....’) and the modulation scheme is BPSK, the synchronizer probably
won’t be able to recover the symbol timing because adjacent symbols are too similar. This is a result
of spectral correlation introduced which can prevent physical layer synchronization techniques from
tracking or even acquisition. Having said that, certain patterns are beneficial to synchronization
and actually help the receiver track to the incoming signal, however these are usually only intro-
duced as a preamble to a frame or packet where the receiver knows what to expect. It is therefore
imperative to increase the short-term entropy of the underlying data to prevent the receiver from
losing its lock on the signal. The data scrambler routine attempts to ”whiten” the data sequence
with a bit mask in order to achieve maximum entropy.
23.8.1 interface
The data scrambler has two methods, described here:
• scramble data() takes an input sequence of data and scrambles the bits by applying a
periodic mask. The first argument is a pointer to the input data array; the second argument
is its length (number of bytes).
• unscramble data() takes an input sequence of data and unscrambles the bits by applying
the reverse mask applied by scramble data(). Just like scramble data(), the first argument
is a pointer to the input data array; the second argument is its length (number of bytes).
24 sequence
The sequence module implements a number of binary sequencing objects useful for communications,
including generic binary shift registers, linear feedback shift registers, maximal length codes (m-
sequences), and complementary codes.
• bsequence create(n) creates a bsequence object with n bits, filled initially with zeros.
• bsequence push(q,bit) pushes a bit into the back (right side) of a binary sequence, and
in turn drops the left-most bit. Only the right-most (least-significant) bit of the input is
regarded. For example, pushing a 1 into the sequence 0010011 results in 0100111.
• bsequence circshift(q) circularly shifts a binary sequence left, pushing the left-most bit
back into the right-most position. For example, invoking a circular shift on the sequence
1001110 results in 0011101.
• bsequence add(q0,q1,q2) computes the binary addition of two sequences q0 and q1 storing
the result in a third sequence q2. Binary addition of two bits is equivalent to their logical
exclusive or, ⊕. For example, the binary addition of 01100011 and 11011001 is 10111010.
• bsequence get length(q) returns the length of the sequence (number of bits).
240 24 SEQUENCE
$m$ & $n$ & $g$ (hex) & $g$ (octal) & $g$ (binary)
2 & 3 & ‘0x0007‘ & ‘000007‘ & ‘ 111‘
3 & 7 & ‘0x000b‘ & ‘000013‘ & ‘ 1011‘
4 & 15 & ‘0x0013‘ & ‘000023‘ & ‘ 10011‘
5 & 31 & ‘0x0025‘ & ‘000045‘ & ‘ 100101‘
6 & 63 & ‘0x0043‘ & ‘000103‘ & ‘ 1000011‘
7 & 127 & ‘0x0089‘ & ‘000211‘ & ‘ 10001001‘
8 & 255 & ‘0x012d‘ & ‘000455‘ & ‘ 100101101‘
9 & 511 & ‘0x0211‘ & ‘001021‘ & ‘ 1000010001‘
10 & 1023 & ‘0x0409‘ & ‘002011‘ & ‘ 10000001001‘
11 & 2047 & ‘0x0805‘ & ‘004005‘ & ‘ 100000000101‘
12 & 4095 & ‘0x1053‘ & ‘010123‘ & ‘ 1000001010011‘
13 & 8191 & ‘0x201b‘ & ‘020033‘ & ‘ 10000000011011‘
14 & 16383 & ‘0x402b‘ & ‘040053‘ & ‘ 100000000101011‘
15 & 32767 & ‘0x8003‘ & ‘100003‘ & ‘1000000000000011‘
• bsequence index(q,i) returns the bit at a particular index of the sequence, starting from
the right-most bit. For example, indexing the sequence 00000001 at index 0 gives the value
1.
• msequence create default(m) creates an msequence object with m bits in the shift register
using the default generator polynomial (e.g. LIQUID MSEQUENCE GENPOLY M6). The initial
state is set to 000...001.
• msequence destroy(ms) destroys the object ms, freeing all internal memory.
• msequence advance(ms) advances the msequence object’s shift register by computing the
binary dot product of the register with the generator polynomial. The resulting bit is sum of
1s modulo 2 of the dot product and is fed back into the end of the shift register, as well as
returned to the user.
• msequence reset(ms) resets the msequence object’s internal shift register to the original
state (typically 000...001).
The auto-correlation of the m-sequence with generator polynomial g =1000011 can be seen in
Figure 63 . The shift register has six bits (m = 6) and therefore the output sequence is of length
n = 2m − 1 = 63. Notice that the auto-correlation is equal to unity with no delay, and nearly zero
(−1/63) for all other delays.
0.8
sequence
0.6
0.4
0.2
0 10 20 30 40 50 60
delay (number of samples)
0.8
auto-correlation
0.6
0.4
0.2
0
0 10 20 30 40 50 60
delay (number of samples)
1
0.8
0.6
a
0.4
0.2
0
0 10 20 30 40 50 60
delay (number of samples)
1
0.8
0.6
b
0.4
0.2
0
0 10 20 30 40 50 60
delay (number of samples)
1
auto-correlation
0.8 raa
0.6 rbb
0.4 (raa+rbb)/2
0.2
0
-0.2
-0.4
0 10 20 30 40 50 60
delay (number of samples)
25 utility
The utility module contains useful functions, primarily for bit fast bit manipulation. This includes
packing/unpacking byte arrays, counting ones in an integer, computing a binary dot-product, and
others.
25.1 liquid pack bytes(), liquid unpack bytes(), and liquid repack bytes()
Byte packing is used extensively in the fec (§ 13 ) and framing (§ 16 ) modules. These methods
resize symbols represented by various numbers of bits. This is necessary to move between raw data
arrays which use full bytes (eight bits per symbol) to methods expecting symbols of different sizes. In
particular, the liquid repack bytes() method is useful when one wants to transmit a block of 64
data bytes using an 8-PSK modem which requires a 3-bit input symbol. For example repacking two
8-bit symbols 00000000,11111111 into six 3-bit symbols gives 000,000,001,111,111,100. Because
16 bits cannot be divided evenly among 3-bit symbols, the last symbol is padded with zeros.
Listed below is a simple example of packing symbols of varying lengths into a fixed array of bytes;
1 // file: doc/listings/pack_array.example.c
2 # include <liquid / liquid.h>
3
4 int main() {
5 unsigned int sym_size[9] = {8, 2, 3, 6, 1, 3, 3, 4, 3};
6 unsigned char input[9] = {
7 0x81, // 1000 0001
8 0x03, // 11
9 0x05, // 101
25.3 liquid lbshift(), liquid rbshift() 245
10 0x3a, // 11 1010
11 0x01, // 1
12 0x07, // 111
13 0x06, // 110
14 0x0a, // 1010
15 0x04 // 10[0] <- last bit is stripped
16 };
17
18 unsigned char output[4];
19
20 unsigned int k=0;
21 unsigned int i;
22 for (i=0; i<9; i++) {
23 liquid_pack_array(output, 4, k, sym_size[i], input[i]);
24 k += sym_size[i];
25 }
26 // output : 1000 0001 1110 1111 0101 1111 1010 1010
27 // symbol : 0000 0000 1122 2333 3334 5556 6677 7788
28 // output is now {0x81, 0xEF, 0x5F, 0xAA};
29 }
liquid rbshift()
liquid rbcircshift()
// input : 1001 0001 1110 1111 0101 1111 1010 1010
// output [0] : 1001 0001 1110 1111 0101 1111 1010 1010
// output [1] : 0100 1000 1111 0111 1010 1111 1101 0101
// output [2] : 1010 0100 0111 1011 1101 0111 1110 1010
// output [3] : 0101 0010 0011 1101 1110 1011 1111 0101
// output [4] : 1010 1001 0001 1110 1111 0101 1111 1010
// output [5] : 0101 0100 1000 1111 0111 1010 1111 1101
// output [6] : 1010 1010 0100 0111 1011 1101 0111 1110
// output [7] : 0101 0101 0010 0011 1101 1110 1011 1111
liquid rshift()
// input : 1000 0001 1110 1111 0101 1111 1010 1010
// output [0] : 1000 0001 1110 1111 0101 1111 1010 1010
// output [1] : 0000 0000 1000 0001 1110 1111 0101 1111
// output [2] : 0000 0000 0000 0000 1000 0001 1110 1111
// output [3] : 0000 0000 0000 0000 0000 0000 1000 0001
// output [4] : 0000 0000 0000 0000 0000 0000 0000 0000
liquid rcircshift()
// input : 1000 0001 1110 1111 0101 1111 1010 1010
// output [0] : 1000 0001 1110 1111 0101 1111 1010 1010
// output [1] : 1010 1010 1000 0001 1110 1111 0101 1111
// output [2] : 0101 1111 1010 1010 1000 0001 1110 1111
// output [3] : 1110 1111 0101 1111 1010 1010 1000 0001
// output [4] : 1000 0001 1110 1111 0101 1111 1010 1010
25.7 miscellany
This section describes the bit-counting methods which are used extensively throughout [liquid],
particularly the fec (§ 13 ) and sequence (§ 24 ) modules. Integer sizes vary for different machines;
when [liquid] is initially configured (see § IV ), the size of the integer is computed such that the
fastest method can be computed without performing unnecessary loop iterations or comparisons.
• liquid count ones(x) counts the number of 1s that exist in the integer x. For example,
the number 237 is represented in binary as 11101101, therefore liquid count ones(237)
returns 6.
• liquid count ones mod2(x) counts the number of 1s that exist in the integer x, modulo 2;
in other words, it returns 1 if the number of ones in x is odd, 0 if the number is even. For
example, liquid count ones mod2(237) return 0.
• liquid bdotprod(x,y) computes the binary dot-product between two integers x and y as
the sum of ones in x ∧ y, modulo 2 (where ∧ is the logical and operation). This is useful in
linear feedback shift registers (see § 24.2 on m-sequences) as well as certain forward error-
correction codes (see § 13.2 on Hamming codes). For example, the binary dot product between
10110011 and 11101110 is 1 because 10110011 ∧ 11101110 = 10100010 which has an odd
number of 1s.
• liquid count leading zeros(x) counts the number of leading zeros in the integer x. This
is dependent upon the size of the integer for the target machine which is usually either two
or four bytes.
• liquid msb index(x) computes the index of the most-significant bit in the integer x. The
function will return 0 for x = 0. For example if x = 129 (10000001), the function will return
8.
248 26 EXPERIMENTAL
26 experimental
The experimental module is a placeholder for modules which haven’t yet been approved for release,
but might eventually be incorporated into the library. By default the experimental module is
disabled and none of its modules are compiled or installed. It is enabled using the configure flag
--enable-experimental and includes the internal header file include/liquid.experimental.h.
• The majority of audio signals (including music and voice) have a strong time-frequency local-
ization; that is, they only occupy a small fraction of audible frequencies for a short duration.
This is particularly true for voiced signals (e.g. vowel sounds).
• The human ear (and brain) tends to be quite forgiving of spectral compression and often
cannot easily distinguish between neighboring frequency components.
There are several benefits to using fbasc over other compression algorithms such as CVSD and
auto-regressive models, the main being that the algorithm is theoretically lossless (i.e. perfect
reconstruction) as the bit rate increases. As a result, the codec is limited only by the quantization
noise on each channel. Here are some useful definitions, as used in the fbasc code:
• MDCT the modified discrete cosine transform is a lapped discrete cosine transform which uses
a special windowing function to ensure perfect reconstruction on its inverse. The transform
operates on 2M time-domain samples (overlapped by M ) to produce M frequency-domain
samples. Conversely, the inverse MDCT accepts M frequency-domain samples and produces
2M time-domain samples which are windowed and then overlapped to reconstruct the original
signal. For convenience, we may refer to M time-domain samples as a ’symbol.’
• symbol one block of M time-domain samples upon which the MDCT operates.
• channel one of the M frequency-domain components as a result of applying the MDCT. This
is somewhat equivalent to a discrete Fourier transform ’bin.’ Note than M is equal to the
number of channels in analysis.
• frame a set of MDCT symbols upon which the fbasc codec runs its analysis. Because the
codec uses time-frequency localization for its encoding, it is necessary for the codec to gain
enough statistical information about the original signal without losing temporal stationarity.
The codec typically operates on several symbols, however, the exact number depends on the
application.
26.2 gport 249
26.1.1 Interface
• fbasc create() creates an fbasc encoder/decoder object, allocating memory as necessary,
and computing internal parameters appropriately.
• fbasc encode() encode a frame of data, storing the header and frame data separately. This
separation allows the user to use different forward error-correction codes (if desired) to protect
the header differently than the rest of the frame. It is important to keep the two together,
however, as the header is a description of how to decode the frame.
• fbasc decode() decodes a frame of data, generating the reconstructed time series.
26.2 gport
The gport object implements a generic port to share data between asynchronous threads. The
port itself is really just a circular (ring) buffer containing a mutually-exclusive locking mechanism
to allow processes running on independent threads to access its data. Because no other modules
rely on the gport object and because it requires the pthread library, it is likely to be removed from
[liquid] in the near future and likely put into another library, e.g. liquid-threads. There are two
ways to access the data in the gport object: direct memory access and indirect (copied) memory
access, each with distinct advantages and disadvantages. Regardless of which interface you use, the
model is equivalent: a buffer of data (initially empty) is created. The producer is the method in
charge of writing to the buffer (or ”producing” the data). The consumer is the method in charge
of reading the data from the buffer (or ”consuming” it). The producer and consumer methods can
exist on completely separate threads, and do not need to be externally synchronized. The gport
object synchronizes the data between the ports.
• once the request is serviced, the port returns a pointer to an array of data allocated internally
by the port itself.
• the producer writes its data at this location, not exceeding the original number of samples
requested.
250 26 EXPERIMENTAL
• the producer then unlocks the port, indicating how many samples were actually written to
the buffer. This allows the consumer thread to read data from the buffer.
Listed below is a minimal example demonstrating the direct memory access method for the gport
object.
26.2.4 Interface
• gport create() creates a new gport object with an internal buffer of a certain length.
• gport destroy() destroys a gport object, signaling end of message to any connected ports.
26.2 gport 251
• gport producer lock() locks a requested number of samples for producing, returning a void
pointer to the locked data array directly. Invoking this method can be thought of as asking
the port to allocate a certain number of samples for writing. Special care must be taken by
the user not to write more elements to the buffer than were requested. This function is a
blocking call and waits for the data to become available or an end of message signal to be
received. The data are locked until gport producer unlock() is invoked. The number of
unlocked samples does not have to match but cannot exceed those which are locked.
• gport producer unlock() unlocks a requested number of samples from the port. Use in
conjunction with gport producer lock(). Invoking this method can be thought of as telling
the port ”I have written n samples to the buffer you gave me earlier; release them to the
consumer for reading.” The number of samples written to the port cannot exceed the initial
request (e.g. if you request a lock for 100 samples, you should never try to unlock more than
100). There is no internal error-checking to ensure this. Failure to comply could result in
over-writing data internally, and corrupt the consumer side.
• gport produce() produces n samples to the port from an external buffer. This method is
a blocking call and waits for the requested data to become available or an end of message
signal to be received.
• gport produce available() operates just like gport produce() except will write as many
samples as are available when the function is called. Invoking this method is like telling the
buffer ”I have n samples, so write as many as you can right now.” It will always wait for at
least one sample to become available and blocks until this condition is met.
• gport consumer lock() locks a requested number of samples for consuming, returning a
void pointer to the locked data array directly. Invoking this method can be thought of as
asking the port to wait for a certain number of samples to be read. Special care must be
taken by the user not to read more elements to the buffer than were requested. This function
is a blocking call and waits for enough samples to become available or an end of message
signal to be received. The data will be locked until gport consumer unlock() is invoked.
The number of unlocked samples does not have to match but cannot exceed those which are
locked.
• gport consumer unlock() unlocks a requested number of samples from the port. Use in
conjunction with gport consumer lock(). Invoking this method can be though of as telling
the port ”I have read n samples from the buffer you gave me earlier; release them to the
producer for writing.” The number of samples read from the port cannot exceed the initial
request (e.g. if you request a lock for 100 samples, you should never try to unlock more than
100).
• gport consume() consumes n samples from the port and writes to an external buffer. This
method is a blocking call and waits for the requested data to become available or an end of
message signal to be received.
• gport consume available() operates just like gport consume() except will read as many
samples as are available when the function is called. Invoking this method is like telling the
252 26 EXPERIMENTAL
buffer ”I have a buffer of nsamples, so write to it as many as you can right now.” It will
always wait for at least one sample to become available and blocks until this condition is met.
• gport signal eom() signals end of message to any connected gport. This tells the port to
stop waiting for data (on both the producer and consumer side) and return. This method
prevents lock conditions where, e.g., the producer is waiting for several samples to become
available, but the consumer has finished its process. This method is normally invoked only
during gport destroy().
9 float optimum_vect[num_parameters];
10
11 // ...
12
13 // create qnsearch object
14 qnsearch q = qnsearch_create(NULL,
15 optimum_vect,
16 num_parameters,
26.5 qnsearch (Quasi-Newton Search) 253
17 &liquid_rosenbrock,
18 LIQUID_OPTIM_MINIMIZE);
19
20 // execute batch search
21 qnsearch_execute(q, num_iterations, target_utility);
22
23 // execute search one iteration at a time
24 unsigned int i;
25 for (i=0; i<num_iterations; i++)
26 qnsearch_step(q);
27
28 // clean it up
29 qnsearch_destroy(q);
30 }
254
Part IV
Installation
How to build from source, run benchmarks, compile examples, etc.
255
27 Installation Guide
The [liquid] DSP library can be easily built from source and is available from several places. The
two most typical means of distribution are a compressed archive (a tarball) and cloning the source
repository. Tarballs are generated with each stable release and are recommended for users not
requiring bleeding edge development. Users wanting the very latest version (in addition to every
other version) should clone the [liquid] Git repository.
• libm, the standard math library (eventually will be phased out to optional)
While [liquid] was designed to be portable, requiring a minimal amount of dependencies, the project
will take advantage of other libraries if they are installed on the target machine. These optional
packages are:
The build system checks to see if they are installed during the configure process and will generate
an appropriate config.h if they are.
Move into the directory and run the configure script and make the library.
$ cd liquid-dsp-v.v.v
$ ./configure
$ make
# make install
256 27 INSTALLATION GUIDE
Move into the directory and check out a particular tag using the git checkout command. 23 o
list available tags run git tag -l. Build as before with the archive, but with the additional
bootstrapping step.
$ cd liquid-dsp
$ git checkout v1.0.0
$ ./reconf
$ ./configure
$ make
# make install
27.4 Targets
This section lists the specific targets in the main [liquid] project. A basic list can be printed by
invoking ”make help” on the command line. This prints the following to the standard output:
all - build shared library (default)
help - print list of targets (see documentation for more)
install - installs the libraries and header files in the host system
uninstall - uninstalls the libraries and header files in the host system
check - build and run autotest scripts
bench - build and run all benchmarks
examples - build all examples binaries
sandbox - build all sandbox binaries
doc - build documentation (doc/liquid.pdf)
world - build absolutely everything
clean - clean build (objects, dependencies, libraries, etc.)
distclean - removes everything except the originally distributed files
• examples (§ 28 )
• autotests (§ 29 )
• benchmarks (§ 30 )
• documentation (§ 31 )
23
T
257
28 Examples
All examples are built as stand-alone programs not build by the target all by default. You may
build all of the example binaries at one time by running
$ make examples
from the top-level directory. Sometimes, however, it is useful to build one example individually.
This can be accomplished by directly targeting its binary. The example then can be run at the
command line. For example to build and run just the modem example, run
$ make examples/modem_example
$ ./examples/modem_example
linear modem:
scheme: qpsk
bits/symbol: 2
0 : 0.70710677 + j* 0.70710677
1 : -0.70710677 + j* 0.70710677
2 : 0.70710677 + j* -0.70710677
3 : -0.70710677 + j* -0.70710677
num sym errors: 0 / 4
num bit errors: 0 / 8
results written to modem_example.m.
The examples are probably the best way to understand how each signal processing element works.
Each example targets a specific functionality of [liquid], such as FIR filtering, forward error cor-
rection, digital demodulation, etc. A number of the example programs when run will generate an
output .m file which can be run directly in Octave. This is particularly useful for visualizing filtering
operations. Most of the examples have a brief description at the top of the file; these descriptions
are listed below (and are also available in the examples/README.md file) for convenience. Some
of the examples are experimental and will not be built by default (see § 26 on the experimental
module); these examples are described below in § 28.2 .
• agc crcf qpsk example.c: Automatic gain control test for data signals with fluctuating
signal levels. QPSK modulation introduces periodic random zero-crossings which gives in-
stantaneous amplitude levels near zero. This example tests the response of the AGC to these
types of signals.
• agc crcf squelch example.c: Automatic gain control with squelch example. This exam-
ple demonstrates the squelch control functionality of the AGC module. Squelch is used to
suppress the output of the AGC when the signal level drops below a certain threshold.
• asgram example.c: ASCII spectrogram example. This example demonstrates the function-
ality of the ASCII spectrogram. A sweeping complex sinusoid is generated and the resulting
frequency response is printed to the screen.
• autocorr cccf example.c: This example demonstrates the autocorr (auto-correlation) ob-
ject functionality. A random time-domain sequence is generated which exhibits time-domain
repetitions (auto-correlation properties), for example: abcdabcdabcd....abcd. The sequence
is pushed through the autocorr object, and the results are written to an output file. The
command-line arguments allow the user to experiment with the sequence length, number of
sequence repetitions, and properties of the auto-correlator, as well as signal-to- noise ratio.
• bpacketsync example.c:
• bsequence example.c: This example demonstrates the interface to the bsequence (binary
sequence) object. The bsequence object acts like a buffer of bits which are stored and ma-
nipulated efficiently in memory.
• bufferf example.c:
• cgsolve example.c: Solve linear system of equations Ax = b using the conjugate- gradient
method where A is a symmetric positive-definite matrix. Compare speed to matrixf linsolve()
for same system.
• chromosome example.c:
• compand cf example.c:
• compand example.c: This example demonstrates the interface to the compand function (com-
pression, expansion). The compander is typically used with the quantizer to increase the
dynamic range of the converter, particularly for low-level signals. The transfer function is
computed (emperically) and printed to the screen.
• crc example.c: Cyclic redundancy check (CRC) example. This example demonstrates how
a CRC can be used to validate data received through un-reliable means (e.g. a noisy channel).
A CRC is, in essence, a strong algebraic error detection code that computes a key on a block
of data using base-2 polynomials. Also available is a checksum for data validation. See also:
fec example.c
28.1 List of Example Programs 259
• cvsd example.c: Continuously-variable slope delta example, sinusoidal input. This example
demonstrates the CVSD audio encoder interface, and its response to a sinusoidal input. The
output distortion ratio is computed, and the time-domain results are written to a file.
• dotprod cccf example.c: This example demonstrates the interface to the complex floating-
point dot product object (dotprod cccf).
• dotprod rrrf example.c: This example demonstrates the interface to the floating-point dot
product object (dotprod rrrf).
• fct example.c:
• fec example.c: This example demonstrates the interface for forward error-correction (FEC)
codes. A buffer of data bytes is encoded and corrupted with several errors. The decoder
then attempts to recover the original data set. The user may select the FEC scheme from the
command-line interface. See also: crc example.c, checksum example.c, packetizer example.c
• fec soft example.c: This example demonstrates the interface for forward error-correction
(FEC) codes with soft-decision decoding. A buffer of data bytes is encoded before the data
are corrupted with at least one error and noise. The decoder then attempts to recover the
original data set from the soft input bits. The user may select the FEC scheme from the
command-line interface. See also: fec example.c, packetizer soft example.c
• fft example.c: This example demonstrates the interface to the fast discrete Fourier trans-
form (FFT). See also: mdct example.c, fct example.c
• firdecim crcf example.c: This example demonstrates the interface to the firdecim (finite
impulse response decimator) family of objects. Data symbols are generated and then in-
terpolated according to a finite impulse response square-root Nyquist filter. The resulting
sequence is then decimated with the same filter, matched to the interpolator. See also:
firinterp crcf example.c,
• firdes kaiser example.c: This example demonstrates finite impulse response filter design
using a Kaiser window. See also: firdespm example.c
• firdespm example.c: This example demonstrates finite impulse response filter design using
the Parks-McClellan algorithm. See also: firdes kaiser example.c
• firhilb decim example.c: Hilbert transform: 2:1 real-to-complex decimator. This example
demonstrates the functionality of firhilb (finite impulse response Hilbert transform) decimator
which converts a real time series into a complex one with half the number of samples. The
input is a real-valued sinusoid of N samples. The output is a complex-valued sinusoid of N/2
samples. See also: firhilb interp example.c
• firhilb example.c:
• firhilb interp example.c: Hilbert transform: 1:2 complex-to-real interpolator. This ex-
ample demonstrates the functionality of firhilb (finite impulse response Hilbert transform)
interpolator which converts a complex time series into a real one with twice the number of
samples. The input is a complex-valued sinusoid of N samples. The output is a real-valued
sinusoid of 2N samples. See also: firhilb decim example.c
• firpfbch2 crcf example.c: Example of the finite impulse response (FIR) polyphase filter-
bank (PFB) channelizer with an output rate of 2Fs /M as an (almost) perfect reconstructive
system.
• firinterp crcf example.c: This example demonstrates the interp object (interpolator) in-
terface. Data symbols are generated and then interpolated according to a finite impulse
response Nyquist filter.
• firpfbch crcf analysis example.c: Example of the analysis channelizer filterbank. The
input signal is comprised of several signals spanning different frequency bands. The channel-
izer downconverts each to baseband (maximally decimated), and the resulting spectrum of
each is plotted.
• firpfbch crcf example.c: Finite impulse response (FIR) polyphase filter bank (PFB) chan-
nelizer example. This example demonstrates the functionality of the polyphase filter bank
channelizer and how its output is mathematically equivalent to a series of parallel down-
converters (mixers/decimators). Both the synthesis and analysis filter banks are presented.
• firpfbch crcf synthesis example.c: Example of the synthesis channelizer filterbank. Ran-
dom symbols are generated and loaded into the bins of the channelizer and the time-domain
signal is synthesized. Subcarriers around the band edges are disabled as well as those near
0.25 to demonstrate the synthesizer’s ability to efficiently notch the spectrum. The results
are printed to a file for plotting.
• flexframesync example.c: This example demonstrates the basic interface to the flexframegen
and flexframesync objects used to completely encapsulate raw data bytes into frame samples
(nearly) ready for over-the-air transmission. A 14-byte header and variable length payload
are encoded into baseband symbols using the flexframegen object. The resulting symbols
are interpolated using a root-Nyquist filter and the resulting samples are then fed into the
flexframesync object which attempts to decode the frame. Whenever frame is found and
properly decoded, its callback function is invoked.
• framesync64 example.c: This example demonstrates the interfaces to the framegen64 and
framesync64 objects used to completely encapsulate data for over-the-air transmission. A
24-byte header and 64-byte payload are encoded, modulated, and interpolated using the
framegen64 object. The resulting complex baseband samples are corrupted with noise and
moderate carrier frequency and phase offsets before the framesync64 object attempts to de-
code the frame. The resulting data are compared to the original to validate correctness. See
also: flexframesync example.c
• freqmodem example.c:
• gasearch example.c:
• gmskmodem example.c:
• gradsearch example.c:
• gradsearch datafit example.c: Fit 3-parameter curve to sampled data set in the minimum
mean-squared error sense.
• iirdes analog example.c: Tests infinite impulse reponse (IIR) analog filter design. While
this example seems purely academic as IIR filters used in liquid are all digital, it is important
to realize that they are all derived from their analog counterparts. This example serves to
check the response of the analog filters to ensure they are correct. The results of design are
written to a file. See also: iirdes example.c, iirfilt crcf example.c
• iirdes example.c: Tests infinite impulse reponse (IIR) digital filter design. See also: iirdes analog exampl
iirfilt crcf example.c
• iirdes pll example.c: This example demonstrates 2nd-order IIR phase-locked loop filter
design with a practical simulation. See also: nco pll example.c nco pll modem example.c
• iirfilt cccf example.c: Complex infinite impulse response filter example. Demonstrates
the functionality of iirfilt with complex coefficients by designing a filter with specified param-
eters and then filters noise.
• iirfilt crcf example.c: Complex infinite impulse response filter example. Demonstrates
the functionality of iirfilt by designing a low-order prototype (e.g. Butterworth) and using it
to filter a noisy signal. The filter coefficients are real, but the input and output arrays are
complex. The filter order and cutoff frequency are specified at the beginning.
• iirinterp crcf example.c: This example demonstrates the iirinterp object (IIR interpola-
tor) interface.
• interleaver example.c: This example demonstrates the functionality of the liquid inter-
leaver object. Interleavers serve to distribute grouped bit errors evenly throughout a block
of data. This aids certain forward error-correction codes in correcting bit errors. In this
example, data bits are interleaved and de-interleaved; the resulting sequence is validated to
match the original. See also: packetizer example.c
262 28 EXAMPLES
• lpc example.c:
• mdct example.c:
• modem arb example.c: This example demonstrates the functionality of the arbitrary modem,
a digital modulator/demodulator object with signal constellation points chosen arbitrarily.
A simple bit-error rate simulation is then run to test the performance of the modem. The
results are written to a file. See also: modem example.c
• modem soft example.c: This example demonstates soft demodulation of linear modulation
schemes.
• modular arithmetic example.c: This example demonstates some modular arithmetic func-
tions.
• nco example.c: This example demonstrates the most basic functionality of the numerically-
controlled oscillator (NCO) object. See also: nco pll example.c, nco pll modem example.c
• nco pll example.c: This example demonstrates how the use the nco/pll object (numerically-
controlled oscillator with phase-locked loop) interface for tracking to a complex sinusoid. The
loop bandwidth, phase offset, and other parameter can be specified via the command-line
interface. See also: nco example.c, nco pll modem example.c
• nco pll modem example.c: This example demonstrates how the nco/pll object (numerically-
controlled oscillator with phase-locked loop) can be used for carrier frequency recovery in
digital modems. The modem type, SNR, and other parameters are specified via the command-
line interface. See also: nco example.c, nco pll example.c
• ofdmflexframesync example.c:
28.1 List of Example Programs 263
• ofdmframegen example.c:
• ofdmframesync example.c:
• packetizer example.c: Demonstrates the functionality of the packetizer object. Data are
encoded using two forward error-correction schemes (an inner and outer code) before data
errors are introduced. The decoder then tries to recover the original data message. See also:
fec example.c, crc example.c
• packetizer soft example.c: This example demonstrates the functionality of the packe-
tizer object for soft-decision decoding. Data are encoded using two forward error- correction
schemes (an inner and outer code) before noise and data errors are added. The decoder then
tries to recover the original data message. Only the outer code uses soft-decision decoding.
See also: fec soft example.c, packetizer example.c
• pll example.c: Demonstrates a basic phase-locked loop to track the phase of a complex
sinusoid.
• poly findroots example.c:
• polyfit example.c: Test polynomial fit to sample data. See also: polyfit lagrange example.c
• polyfit lagrange example.c: Test exact polynomial fit to sample data using Lagrange
interpolating polynomials. See also: polyfit example.c
• qnsearch example.c:
• quantize example.c:
• random histogram example.c: This example tests the random number generators for differ-
ent distributions.
• repack bytes example.c: This example demonstrates the repack bytes() interface by pack-
ing a sequence of three 3-bit symbols into five 2-bit symbols. The results are printed to the
screen. Because the total number of bits in the input is 9 and not evenly divisible by 2, the
last of the 5 output symbols has a zero explicitly padded to the end.
• resamp2 crcf decim example.c: Halfband decimator. This example demonstrates the in-
terface to the decimating halfband resampler. A low-frequency input sinusoid is generated
and fed into the decimator two samples at a time, producing one output at each itera-
tion. The results are written to an output file. See also: resamp2 crcf interp example.c
decim rrrf example.c
• resamp2 crcf example.c: This example demonstrates the halfband resampler running as
both an interpolator and a decimator. A narrow-band signal is first interpolated by a factor
of 2, and then decimated. The resulting RMS error between the final signal and original is
computed and printed to the screen.
• resamp2 crcf filter example.c: Halfband (two-channel) filterbank example. This exam-
ple demonstrates the analyzer/synthesizer execute() methods for the resamp2xxxt family of
objects.
264 28 EXAMPLES
• resamp2 crcf interp example.c: Halfband interpolator. This example demonstrates the in-
terface to the interpolating halfband resampler. A low-frequency input sinusoid is generated
and fed into the interpolator one sample at a time, producing two outputs at each itera-
tion. The results are written to an output file. See also: resamp2 crcf decim example.c,
interp crcf example.c
• ricek channel example.c: This example generates correlated circular complex Gauss ran-
dom variables using an approximation to the ideal Doppler filter. The resulting Gauss random
variables are converted to Rice-K random variables using a simple transformation. The re-
sulting output file plots the filter’s power spectral density, the fading power envelope as a
function of time, and the distribution of Rice-K random variables alongside the theoretical
PDF to demonstrate that the technique is valid.
• smatrix example.c:
• wdelayf example.c:
• windowf example.c: This example demonstrates the functionality of a window buffer (also
known as a circular or ring buffer) of floating-point values. Values are written to and read from
the buffer using several different methods. See also: bufferf example.c wdelayf example.c
This builds the experimental module (§ 26 ) and allows the corresponding example programs
to be compiled as well. Below is a list of all experimental example programs along with a brief
description of each:
28.2 Experimental Example Programs 265
• ann bitpattern example.c: Artificial neural network (ann) bit pattern example. This ex-
ample demonstrates the functionality of the ann module by training a simple network to learn
prime numbers:
n : b0 b1 b2 | y
--------------------+------
0 : 0 0 0 | 1
1 : 0 0 1 | 1
2 : 0 1 0 | 1
3 : 0 1 1 | 1
4 : 1 0 0 | 0
5 : 1 0 1 | 1
6 : 1 1 0 | 0
7 : 1 1 1 | 1
• ann example.c: Artificial neural network (ann) example. This example demonstrates the
functionality of the ann module by training a simple network to learn the output of a contin-
uous function.
• ann maxnet example.c: Artificial neural network (ann) maxnet example. This example
demonstrates the functionality of the ann maxnet by training a network to recognize and
separate two input patterns in a 2-dimensional plane.
• ann xor example.c: Artificial neural network (ann) eXclusive OR example. This example
demonstrates the functionality of the ann module by training a simple network to learn the
output of an exclusive or (xor) circuit:
x y | z
--------+-----
0 0 | 0
0 1 | 1
1 0 | 1
1 1 | 0
• dds cccf example.c: Direct digital synthesizer example. This example demonstrates the
interface to the direct digital synthesizer. A baseband pulse is generated and then effi-
ciently up-converted (interpolated and mixed up) using the DDS object. The resulting
signal is then down-converted (mixed down and decimated) using the same DDS object.
Results are written to a file. See also: interp crcf example.c, decim crcf example.c,
resamp2 crcf example.c, nco example.c
• fbasc example.c:
• kmeans example.c:
• ofdmoqam example.c:
• ofdmoqamframe64gen example.c:
• ofdmoqamframe64sync example.c:
• ofdmoqamframesync example.c:
• patternset example.c:
• qmfb crcf synthesis example.c: This example demonstrates the functionality of the pat-
ternset structure for easily managing pattern sets for optimization.
29 Autotests
Source code validation is a critical step in any software library, particularly for verifying the portabil-
ity of code to different processors and platforms. Packaged with [liquid] are a number of automatic
test scripts to validate the correctness of the source code. The test scripts are located under each
module’s tests directory and take the form of a C source file. The testing framework operates simi-
larly to CppUnit and cxxtest, however it is written in C. The generator script scripts/autoscript
parses these header files looking for the key ”void autotest ” which corresponds to a specific test.
The script generates the header file autotest include.h which includes all the modules’ test head-
ers as well as several organizing structures for keeping track of which tests have passed or failed.
The result is an executable file, xautotest, which can be run to validate the functional correctness
of [liquid] on your target platform.
29.1 Macros
Each module contains a number of autotest scripts which use pre-processor macros for asserting
the functional correctness of the source code.
• CONTEND SAME DATA(ptrA, ptrB, n) asserts that each of nbyte values in the arrays referenced
by ptrA and ptrB are equal.
The autotest program will keep track of which tests elicit warnings and add them to the list of
unstable tests. Here are some examples:
Simply running the program without any arguments executes all the tests and displays the results
to the screen. This is the default response of the target make check.
==================================
PASSED ALL 600 CHECKS
==================================
30 Benchmarks
Packaged with [liquid] are benchmarks to determine the speed each signal processing element can
run on your machine.
By default, running ”make bench” is equivalent to simply executing the ./benchmark program
which runs all of the benchmarks sequentially. Initially the tool provides an estimate of the proces-
sor’s clock frequency; while not necessarily accurate, this is necessary to gauge the relative speed
by which the benchmarks will run. The tool will then estimate the number of trials so that each
benchmark will take between 50 and 500 ms to run. Listed below is the output of the first several
benchmarks:
estimating cpu clock frequency...
performed 67108864 trials in 650.0 ms
estimated clock speed: 2.468 GHz
setting number of trials to 246754
0: null
0 : null : 23.59 M trials in 220.00 ms (107.212 M t/s, 22.00 cycles/t)
1: agc
1 : agc_crcf : 1.92 M trials in 270.00 ms ( 7.093 M t/s, 337.50 cycles/t)
2 : agc_crcf_squelch : 1.92 M trials in 280.00 ms ( 6.840 M t/s, 350.00 cycles/t)
3 : agc_crcf_locked : 15.32 M trials in 700.00 ms ( 21.887 M t/s, 109.38 cycles/t)
2: window
4 : windowcf_n16 : 7.55 M trials in 260.00 ms ( 29.029 M t/s, 81.25 cycles/t)
5 : windowcf_n32 : 7.55 M trials in 260.00 ms ( 29.029 M t/s, 81.25 cycles/t)
6 : windowcf_n64 : 7.55 M trials in 270.00 ms ( 27.954 M t/s, 84.38 cycles/t)
7 : windowcf_n128 : 7.55 M trials in 260.00 ms ( 29.029 M t/s, 81.25 cycles/t)
8 : windowcf_n256 : 7.55 M trials in 260.00 ms ( 29.029 M t/s, 81.25 cycles/t)
3: dotprod_cccf
270 30 BENCHMARKS
For this run the clock speed was estimated to be 2.468 GHz. Benchmarks are sub-divided into
packages which group similar signal processing algorithms together. For example, package 3 above
refers to benchmarking the dotprod cccf object which computes the vector dot product between
two n-point arrays of complex floats. Specifically, benchmark 11 refers to the speed of an n = 64-
point dot product. In this run the benchmarking tool computed approximately 117,930 64-point
complex dot products in 300 ms (about 393,107 trials per second). For the estimated clock rate this
means that the algorithm requires approximately 6,000 clock cycles to compute a single 64-point
complex vector dot product.
30.2 Examples
Run all benchmarks that include the string ”dotprod”:
$ ./benchmark -s dotprod
estimating cpu clock frequency...
performed 134217728 trials in 912.3 ms
estimated clock speed: 3.516 GHz
setting number of base trials to 351599
running all packages and benchmarks matching ’dotprod’...
5: dotprod_cccf
16 : dotprod_cccf_4 : 70.32 M trials / 407.52 ms (172.55 M t/s, 20.38 c/t)
17 : dotprod_cccf_16 : 17.58 M trials / 197.83 ms ( 88.86 M t/s, 39.57 c/t)
18 : dotprod_cccf_64 : 4.39 M trials / 137.22 ms ( 32.03 M t/s, 109.78 c/t)
19 : dotprod_cccf_256 : 1.10 M trials / 125.15 ms ( 8.78 M t/s, 400.48 c/t)
6: dotprod_crcf
20 : dotprod_crcf_4 : 70.32 M trials / 376.18 ms (186.93 M t/s, 18.81 c/t)
21 : dotprod_crcf_16 : 17.58 M trials / 178.55 ms ( 98.46 M t/s, 35.71 c/t)
22 : dotprod_crcf_64 : 8.79 M trials / 149.02 ms ( 58.99 M t/s, 59.61 c/t)
23 : dotprod_crcf_256 : 2.20 M trials / 109.61 ms ( 20.05 M t/s, 175.38 c/t)
7: dotprod_rrrf
24 : dotprod_rrrf_4 : 45.00 M trials / 156.26 ms (288.00 M t/s, 12.21 c/t)
25 : dotprod_rrrf_16 : 22.50 M trials / 123.14 ms (182.74 M t/s, 19.24 c/t)
26 : dotprod_rrrf_64 : 11.25 M trials / 112.13 ms (100.34 M t/s, 35.04 c/t)
27 : dotprod_rrrf_256 : 5.63 M trials / 147.95 ms ( 38.02 M t/s, 92.47 c/t)
running all remaining scripts matching ’dotprod’...
Run the benchmarks in package 1 (agc crcf) while specifying a clock rate of 3.1 GHz:
$ ./benchmark -p1 -c3.1e9
setting number of base trials to 310000
1: agc_crcf
30.2 Examples 271
31 Documentation
Specifically, ”make doc” builds this .pdf or .html file you’re reading right now.
31.1 Dependencies
The documentation requires a few additional packages to build from scratch:
• pdflatex, the LaTeX engine responsible for making this document with all those pretty
equations
• epstopdf, conversion from .eps to .pdf, required for the figures created with gnuplot
• pygments, the syntax highlighting engine responsible for generating all the fancy code listings
given throughout this document. The command-line equivalent is called pygmentize.
31.2 meltdown
This documentation is generated from a custom parser and generator program called meltdown.
While similar to Markdown, meltdown includes hooks for equations, tables, figures, and cross-
referencing sections.