- Butterworth filter order from a template
- Dominant frequencies of a signal
- SNR gain of a low-pass filter
- Response of a Chebyshev low-pass filter
- Pulse compression with a Barker 13 code
- Chebyshev type I low-pass: order and poles
- First-order digital low-pass by the bilinear transform with prewarping
- Quantization noise of an 8-bit ADC
- PRBS7 test pattern from a linear feedback shift register
- GMSK frequency pulse with BT = 0.3
Butterworth filter order from a template
A low-pass filter must keep 1 kHz within 1 dB and attenuate 4 kHz by 40 dB. The script computes the minimum Butterworth order, the -3 dB cutoff and the frequency where 40 dB is reached.
#1. Specs: passband 1 kHz at 1 dB max, stopband 4 kHz at 40 dB min; -> "[fp]" 1000; -> "[fs]" 4000; -> "[Ap]" 1; -> "[As]" 40; #2. Minimum Butterworth order; -> "[n]" (math ceil (math calc "log10((10^([As]/10)-1)/(10^([Ap]/10)-1))/(2*log10([fs]/[fp]))")); #3. -3 dB cutoff that meets the passband exactly; -> "[fc]" (math calc "[fp]/(10^([Ap]/10)-1)^(1/(2*[n]))"); #4. Frequency where the attenuation reaches 40 dB; -> "[f40]" (num solve (concat "10*log10(1+(f/" [fc] ")^(2*" [n] "))-40") "f" [fp] 100000); concat "Order " [n] ", fc = " (math round [fc] 1) " Hz, 40 dB reached at " (math round [f40] 0) " Hz, attenuation at 4 kHz = " (math round (math calc "10*log10(1+([fs]/[fc])^(2*[n]))") 1) " dB"
Order 4, fc = 1184 Hz, 40 dB reached at 3744 Hz, attenuation at 4 kHz = 42.3 dB
- Apply the designed filter to a test signal with
dsp filter - Compare with a Chebyshev template by changing the order formula
- Plot the gain curve with
num tableon 10 log10(1 + (f/fc)^2n)
Dominant frequencies of a signal
A 10 Hz signal sampled at 500 Hz is polluted by a 50 Hz mains hum. The script computes the amplitude spectrum and detects its peaks to give each frequency and amplitude.
#1. 100 samples at 500 Hz: 10 Hz tone + 50 Hz hum; -> "[x]" "["; -> "[sep]" ""; for (-> "[i]" 0) (< [i] 100) (++ "[i]") { -> "[x]" (concat [x] [sep] (math calc "sin(2*pi*10*[i]/500)+0.3*sin(2*pi*50*[i]/500)")); -> "[sep]" ","; }; -> "[x]" (concat [x] "]"); #2. One-sided amplitude spectrum; json load "rx_sig_spec" (dsp spectrum [x] 500); #3. Peaks of the amplitude above 0.1; json load "rx_sig_pk" (dsp peaks (json select "rx_sig_spec" /amplitude) "{\"height\": 0.1}"); -> "[k1]" (json select "rx_sig_pk" "/indices[0]"); -> "[k2]" (json select "rx_sig_pk" "/indices[1]"); concat "Peak 1: " (json select "rx_sig_spec" (concat "/f[" [k1] "]")) " Hz, A = " (math round (json select "rx_sig_pk" "/values[0]") 3) "; peak 2: " (json select "rx_sig_spec" (concat "/f[" [k2] "]")) " Hz, A = " (math round (json select "rx_sig_pk" "/values[1]") 3)
Peak 1: 10 Hz, A = 1; peak 2: 50 Hz, A = 0.3
- Apply a
dsp windowhann before the spectrum when the tone is not on a bin - Use
dsp welchfor a noisy signal - Check the period with
dsp autocorr
SNR gain of a low-pass filter
A 5 Hz measurement is disturbed by 60 Hz and 110 Hz interference. The script filters it with a zero-phase Butterworth low-pass and computes the SNR before and after.
#1. 200 samples at 500 Hz: 5 Hz useful signal, disturbances at 60 and 110 Hz; -> "[s]" "["; -> "[x]" "["; -> "[sep]" ""; for (-> "[i]" 0) (< [i] 200) (++ "[i]") { -> "[s]" (concat [s] [sep] (math calc "sin(2*pi*5*[i]/500)")); -> "[x]" (concat [x] [sep] (math calc "sin(2*pi*5*[i]/500)+0.5*sin(2*pi*60*[i]/500)+0.3*sin(2*pi*110*[i]/500)")); -> "[sep]" ","; }; -> "[s]" (concat [s] "]"); -> "[x]" (concat [x] "]"); #2. Zero-phase Butterworth low-pass, 15 Hz, order 4; -> "[y]" (dsp filter [x] 500 "lowpass" 15 4); #3. SNR = 10 log10 of signal power over noise power, before and after; -> "[ps]" (stat sum_sq [s]); -> "[in]" (math calc (concat "10*log10([ps]/" (stat sum_sq (vector sub [x] [s])) ")")); -> "[out]" (math calc (concat "10*log10([ps]/" (stat sum_sq (vector sub [y] [s])) ")")); concat "SNR in = " (math round [in] 1) " dB, SNR out = " (math round [out] 1) " dB, gain = " (math round (math calc "[out]-[in]") 1) " dB"
SNR in = 4.7 dB, SNR out = 19.7 dB, gain = 15 dB
- The edges of filtfilt limit the gain: ignore the first and last samples
- Try
dsp savgolordsp moving_averageand compare the SNR - Use
dsp filterbandstop for a single known interference
Response of a Chebyshev low-pass filter
A type I Chebyshev filter of order 5 with 0.5 dB ripple: the gain is 1/(1 + eps^2 T5(f/fc)^2). The script computes eps, the attenuation at twice the cutoff, the slope at the band edge (n U4(1)), and draws the gain in dB.
#1. Chebyshev type I low-pass: order 5, ripple 0.5 dB, cutoff 1 kHz; -> "[n]" 5; -> "[eps2]" (math calc "10^(0.5/10)-1"); #2. Attenuation at 2 kHz: 10 log of 1 + eps^2 T5 of 2 ^2; -> "[A2k]" (math calc "10*log10(1+[eps2]*chebyshev_t(5, 2)^2)"); #3. Slope of T5 at the band edge, dT/dx = n U4 of 1; -> "[slope]" (math calc "[n]*chebyshev_u(4, 1)"); #4. Gain in dB from 0 to 3 kHz, x = f / 1 kHz; plot function "-10*log10(1+[eps2]*chebyshev_t(5, x)^2)" "x" 0 3
The chart is shown in the Plot tab of the editor.
- Find the order needed for 40 dB at 2 fc with
num solveon n. - Compare with a Butterworth filter of the same order: 1/(1 + x^10).
Pulse compression with a Barker 13 code
A pulse radar transmits a Barker 13 code with 1 us chips. Compute the peak sidelobe ratio of the code from its autocorrelation, then find the echo in a noisy received signal with a matched filter (cross-correlation), its delay and the target range c tau / 2.
#1. Barker 13 code transmitted by a pulse radar, chip time 1 us; -> "[code]" "[1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1]"; #2. Compression gain: autocorrelation peak against the highest sidelobe; json load "rx_ac" (dsp correlate [code] [code]); -> "[main]" (stat max (json doc "rx_ac")); json load "rx_pk" (dsp peaks (json doc "rx_ac")); -> "[psl]" (math calc (concat "20*log10(" [main] "/" (json select "rx_pk" "/values/[1]") ")")); #3. Received echo: delayed by 9 chips, attenuated, with noise; -> "[rx]" "[0.1, -0.2, 0.05, 0.1, -0.1, 0.2, 0, -0.1, 0.1, 0.5, 0.6, 0.4, 0.5, 0.6, -0.4, -0.5, 0.6, 0.4, -0.6, 0.5, -0.4, 0.6, 0.1, -0.1, 0.05]"; #4. Matched filter by cross-correlation, lag of the maximum; json load "rx_mf" (dsp peaks (dsp correlate [rx] [code]) "{\"distance\": 30}"); -> "[lag]" (- (json select "rx_mf" "/indices/[0]") 12); -> "[range]" (math calc (concat "299792458*" [lag] "e-6/2")); concat "peak " [main] ", peak sidelobe ratio " (math round [psl] 2) " dB | echo delay " [lag] " us, matched filter output " (math round (json select "rx_mf" "/values/[0]") 2) " | target range " (math round (/ [range] 1000) 3) " km"
peak 13, peak sidelobe ratio 22.28 dB | echo delay 9 us, matched filter output 6.6 | target range 1.349 km
- Add Doppler: multiply the echo by a phase rotation and see the loss of the matched filter.
- Smooth the output with
dsp moving_averagebefore a CFAR threshold.
Chebyshev type I low-pass: order and poles
An anti-aliasing filter must keep 1 dB ripple up to 1 kHz and attenuate 40 dB at 2 kHz. The script computes the minimum Chebyshev order with the inverse hyperbolic cosine, then the normalized poles s_k = j sin(pi/2 - theta_k + j alpha) with alpha = asinh(1/eps)/n.
#1. Template: ripple 1 dB up to 1 kHz, 40 dB at 2 kHz; -> "[eps]" (math sqrt (- (math power 10 0.1) 1)); -> "[D]" (math sqrt (/ (- (math power 10 4) 1) (* [eps] [eps]))); #2. Minimum order of the Chebyshev filter; -> "[n]" (math ceil (/ (math acosh [D]) (math acosh 2))); #3. Pole parameter alpha and the poles of the upper half plane, normalized to wp = 1; -> "[alpha]" (/ (math asinh (/ 1 [eps])) [n]); -> "[poles]" ""; for (-> "[k]" 1) (<= [k] (math ceil (/ [n] 2))) (++ "[k]") { -> "[x]" (math round (math calc "pi/2-(2*[k]-1)*pi/(2*[n])") 12); -> "[s]" (complex mul "i" (complex sin (concat [x] "+" [alpha] "i"))); -> "[poles]" (concat [poles] " " (math round (complex re [s]) 4) (if (> (math abs (complex im [s])) 0.000001) {concat " +/- j" (math round (complex im [s]) 4)} {""}) ";"); }; concat "eps = " (math round [eps] 4) " | order n = " [n] " | alpha = " (math round [alpha] 4) " | poles:" [poles]
eps = 0.5088 | order n = 5 | alpha = 0.2856 | poles: -0.0895 +/- j0.9901; -0.2342 +/- j0.6119; -0.2895;
- Scale the poles by 2 pi 1000 and build the Sallen-Key sections, one per conjugate pair
- Compare the order with the Butterworth one for the same template
First-order digital low-pass by the bilinear transform with prewarping
A 1 kHz first-order low-pass is needed at fs = 8 kHz. The script prewarps the cutoff with the tangent, computes the coefficients of H(z) = b0 (1 + z^-1) / (1 + a1 z^-1) and checks on the unit circle that the gain is exactly -3 dB at 1 kHz.
#1. Cutoff 1 kHz, sampling 8 kHz: prewarped analog cutoff; -> "[fc]" 1000; -> "[fs]" 8000; -> "[K]" (math tan (/ (* (math pi) [fc]) [fs])); -> "[fa]" (/ (* 2 (* [fs] [K])) (* 2 (math pi))); #2. Coefficients of the digital filter; -> "[b0]" (/ [K] (+ 1 [K])); -> "[a1]" (/ (- [K] 1) (+ [K] 1)); #3. Frequency response at 1 kHz and 3 kHz; -> "[g1]" (complex abs (complex div (complex mul [b0] (complex add 1 (complex from_polar 1 (math calc "-2*pi*1000/8000")))) (complex add 1 (complex mul [a1] (complex from_polar 1 (math calc "-2*pi*1000/8000")))))); -> "[g3]" (complex abs (complex div (complex mul [b0] (complex add 1 (complex from_polar 1 (math calc "-2*pi*3000/8000")))) (complex add 1 (complex mul [a1] (complex from_polar 1 (math calc "-2*pi*3000/8000")))))); concat "prewarped analog cutoff " (math round [fa] 1) " Hz | b0 = b1 = " (math round [b0] 6) ", a1 = " (math round [a1] 6) " | 1 kHz: " (math round (* 20 (math log10 [g1])) 3) " dB | 3 kHz: " (math round (* 20 (math log10 [g3])) 2) " dB, analog RC: " (math round (* -10 (math log10 (+ 1 9))) 2) " dB"
prewarped analog cutoff 1054.8 Hz | b0 = b1 = 0.292893, a1 = -0.414214 | 1 kHz: -3.01 dB | 3 kHz: -15.44 dB, analog RC: -10 dB
- Filter a test signal with
dsp filterusing these b and a coefficients - Do the same for a second-order Butterworth section with the bilinear transform
Quantization noise of an 8-bit ADC
A 0.95 V sine is sampled by an 8-bit ADC with a +/-1 V range. The script quantizes 200 samples with rint, measures the variance of the quantization error, compares it with the classic LSB^2/12 model and the SQNR with 6.02 N + 1.76 dB, and shows the resolution of a double for comparison.
#1. 8-bit ADC, range +/-1 V, sine 0.95 V at a non-coherent frequency; -> "[lsb]" (/ 2 256); -> "[A]" 0.95; json load "rx_p4f_err" "[]"; #2. Quantize 200 samples and keep the errors; for (-> "[n]" 0) (< [n] 200) (++ "[n]") { -> "[x]" (* [A] (math sin (* 0.4593 [n]))); -> "[q]" (* [lsb] (math rint (/ [x] [lsb]))); json iarray "rx_p4f_err" / (- [q] [x]) NUM; }; #3. Measured noise power vs the LSB^2/12 model; -> "[pn]" (stat pvariance (json doc "rx_p4f_err")); json unload "rx_p4f_err"; -> "[sqnr]" (* 10 (math log10 (/ (/ (* [A] [A]) 2) [pn]))); concat "noise " (math round_sig [pn] 4) " V2 vs LSB2/12 = " (math round_sig (/ (* [lsb] [lsb]) 12) 4) " | SQNR = " (math round [sqnr] 2) " dB vs theory " (math round (+ (+ (* 6.02 8) 1.76) (* 20 (math log10 [A]))) 2) " dB | ENOB " (math round (/ (- [sqnr] 1.76) 6.02) 2) " bits | double ulp at 0.95: " (math ulp 0.95)
noise 0.000005521 V2 vs LSB2/12 = 0.000005086 | SQNR = 49.12 dB vs theory 49.47 dB | ENOB 7.87 bits | double ulp at 0.95: 1.1102230246251565E-16
- Add a dither of 1 LSB and look at the harmonics with
dsp spectrum - Repeat for 10, 12 and 16 bits and fit the SQNR line with
stat fit
PRBS7 test pattern from a linear feedback shift register
Serial links are tested with the PRBS7 pattern x^7 + x^6 + 1. The script runs the 7-bit LFSR with bitwise operations, checks that the period is the maximal 127 bits and that the pattern is balanced (64 ones, 63 zeros), and prints the first 24 bits.
#1. Seed 1111111, polynomial x^7 + x^6 + 1; -> "[seed]" 127; -> "[s]" [seed]; -> "[ones]" 0; -> "[period]" 0; -> "[bits]" ""; #2. 127 shifts: feedback = bit 6 XOR bit 5, then shift left and keep 7 bits; for (-> "[n]" 1) (<= [n] 127) (++ "[n]") { -> "[nb]" (math bit_and (math bit_xor (math trunc (/ [s] 64)) (math trunc (/ [s] 32))) 1); -> "[s]" (math bit_and (math bit_or (* [s] 2) [nb]) 127); -> "[ones]" (+ [ones] [nb]); -> "[bits]" (concat [bits] [nb]); if (and (== [s] [seed]) (== [period] 0)) {-> "[period]" [n];}; }; #3. Period and balance of a maximal length sequence; concat "period " [period] " bits" (if (== [period] 127) {" = 2^7 - 1, maximal"} {" not maximal"}) " | ones " [ones] ", zeros " (- 127 [ones]) " | first bits " (string left [bits] 24)
period 127 bits = 2^7 - 1, maximal | ones 64, zeros 63 | first bits 000000100000110000101000
- Map the bits to +/-1 and check the circular autocorrelation, -1 outside the peak, with
dsp correlate - Count the runs: a PRBS7 has 32 runs of length 1
GMSK frequency pulse with BT = 0.3
GSM and Bluetooth use GMSK, an MSK whose frequency pulse is shaped by a Gaussian filter. For BT = 0.3 the script computes the frequency pulse g(t) with the error function, the inter-symbol interference on the neighbour symbols and checks by summation that the phase change of one symbol is pi/2.
#1. BT = 0.3, symbol time T = 1, constant pi B sqrt of 2 over ln 2; -> "[B]" 0.3; -> "[k]" (* (* (math pi) [B]) (math sqrt (/ 2 (math log 2)))); #2. Frequency pulse at 0, T and 2T; -> "[g0]" (/ (- (math erf (* [k] 0.5)) (math erf (* [k] -0.5))) 4); -> "[g1]" (/ (- (math erf (* [k] 1.5)) (math erf (* [k] 0.5))) 4); -> "[g2]" (/ (- (math erf (* [k] 2.5)) (math erf (* [k] 1.5))) 4); #3. Area of the pulse from -3T to 3T, step T/20: the phase change is pi times the area; -> "[area]" 0; for (-> "[i]" -60) (<= [i] 60) (++ "[i]") { -> "[t]" (/ [i] 20); -> "[area]" (+ [area] (/ (- (math erf (* [k] (+ [t] 0.5))) (math erf (* [k] (- [t] 0.5)))) 80)); }; concat "g(0) = " (math round [g0] 4) " (MSK 0.5) | ISI g(T)/g(0) = " (math round (/ [g1] [g0]) 4) ", g(2T)/g(0) = " (math round (/ [g2] [g0]) 6) " | phase per symbol = " (math round (* 180 [area]) 3) " deg"
g(0) = 0.3712 (MSK 0.5) | ISI g(T)/g(0) = 0.1731, g(2T)/g(0) = 4.6E-4 | phase per symbol = 90 deg
- Plot g(t) for BT = 0.3 and 0.5 with
plot xyto see the spreading - Integrate g(t) to get the phase trajectory of a bit sequence
