3. Build a CFC 60 Low-Pass Filter for a Seatbelt Load Channel
Why CFC 60 for a Seatbelt Load Channel
Seatbelt load cells measure the restraint force transmitted through the webbing during a crash event. The signal of interest is dominated by low-frequency content: the belt force rises over tens of milliseconds and the injury-relevant peak typically occurs within the first 80–120 ms of the event. CFC 60 (Channel Frequency Class 60) is the SAE J211 / ISO 6487 class specified for seatbelt load channels, and it defines a low-pass filter whose magnitude response must stay within a tolerance corridor up to 60 Hz and roll off steeply above it. The class is defined by a corridor, not by a single transfer function, so several analog or digital implementations are acceptable — but the implementation you choose must be verified against the corridor, and its phase behaviour must be understood because it shifts the time of the peak you will later report.
This case builds a digital CFC 60 filter that can be dropped into a DAQ post-processing chain. The work has three parts: derive the filter coefficients, apply them with correct initial conditions so the start of the record is not corrupted, and verify magnitude and phase against the CFC 60 acceptance limits.
The CFC 60 Acceptance Corridor
SAE J211/1 defines each CFC class by a set of magnitude tolerance points relative to the passband gain. For CFC 60 the key points are:
| Frequency (Hz) | Relative magnitude (dB) | Tolerance |
|---|---|---|
| 0.1 – 60 | 0 | +0.5 / −0.5 dB |
| 90 | −0.5 to −1.5 | corridor |
| 120 | −2.0 to −4.0 | corridor |
| 180 | −6.0 to −10.0 | corridor |
| 300 | −12.0 to −18.0 | corridor |
| 600 | −18.0 to −24.0 | corridor |
| 1000 | −24.0 to −30.0 | corridor |
Two practical consequences follow. First, the passband must be flat to within ±0.5 dB out to 60 Hz, so a filter with a −3 dB corner at 60 Hz is not compliant — it would already be down 3 dB at the class limit. Second, the stopband requirement is gentle enough that a modest-order filter suffices, provided the corner is placed high enough that the 60 Hz point is still inside the passband tolerance.
Choosing the Filter Structure and Corner
A 4th-order Butterworth low-pass is a good fit: it is maximally flat in the passband (so the ±0.5 dB requirement out to 60 Hz is easy to meet) and its −3 dB corner can be placed above 60 Hz while still satisfying the stopband points. A common and defensible choice is a corner frequency of about 100 Hz. At 60 Hz a 4th-order Butterworth at fc = 100 Hz is only about −0.4 dB down, inside the passband corridor; at 300 Hz it is roughly −14 dB, inside the −12 to −18 dB window; at 1000 Hz it is about −28 dB, inside the −24 to −30 dB window. The exact numbers depend on the corner you pick, which is why the verification step below is mandatory rather than optional.
For a digital implementation, the analog prototype is converted with the bilinear transform. The pre-warping of the corner frequency matters: at a 10 kHz sample rate the warping is small but not zero, and skipping it shifts the effective corner. The code below pre-warps explicitly.
Implementation
The filter is implemented as a cascade of two biquad sections (SOS form), which is numerically better conditioned than a single 4th-order transfer function in direct form. The function below designs the coefficients, applies zero-phase forward-backward filtering for the magnitude check, and also provides a causal single-pass mode for the phase check.
import numpy as np
from scipy import signal
FS = 10000.0 # DAQ sample rate, Hz
FC = 100.0 # chosen -3 dB corner, Hz
ORDER = 4 # 4th-order Butterworth
def design_cfc60(fs=FS, fc=FC, order=ORDER):
# Pre-warp the corner for the bilinear transform
warped = 2.0 * fs * np.tan(np.pi * fc / fs)
sos = signal.butter(order, warped, btype='low',
analog=True, output='sos')
# Convert analog SOS to digital via bilinear transform
sos_d = signal.bilinear_zpk # placeholder for clarity
z, p, k = signal.butter(order, warped, btype='low', analog=True,
output='zpk')
zd, pd, kd = signal.bilinear_zpk(z, p, k, fs)
return signal.zpk2sos(zd, pd, kd)
SOS = design_cfc60()
def apply_cfc60(x, zero_phase=True):
x = np.asarray(x, dtype=float)
if zero_phase:
# Forward-backward: zero net phase, magnitude squared
return signal.sosfiltfilt(SOS, x)
# Causal single pass: real phase lag, used for phase verification
return signal.sosfilt(SOS, x)
Two details are easy to get wrong. First, sosfiltfilt squares the magnitude response, so a filter designed to be −0.4 dB at 60 Hz becomes −0.8 dB after forward-backward application — still inside the ±0.5 dB corridor only if the single-pass figure is comfortably under 0.5 dB. If your corner choice leaves little margin, either lower the corner slightly or verify the two-pass response directly. Second, sosfiltfilt uses reflection padding by default; for a crash pulse that starts near zero and ends near zero this is acceptable, but for a record with a large offset at the boundary, pad explicitly with the mean or with zeros and document the choice.
Verifying Magnitude and Phase
Verification is a frequency-response sweep against the corridor, plus a phase check at the frequencies that matter for peak timing. The snippet below computes the two-pass magnitude response and the single-pass phase, then tests the corridor points.
def verify_cfc60(sos=SOS, fs=FS):
w, h = signal.sosfreqz(sos, worN=8192, fs=fs)
mag_db_single = 20*np.log10(np.abs(h) + 1e-12)
mag_db_two = 2.0 * mag_db_single # forward-backward
phase_deg = np.unwrap(np.angle(h)) * 180/np.pi
checks = [(60, -0.5, 0.5), (90, -1.5, -0.5), (120, -4.0, -2.0),
(180, -10.0, -6.0), (300, -18.0, -12.0),
(600, -24.0, -18.0), (1000, -30.0, -24.0)]
report = []
for f, lo, hi in checks:
idx = np.argmin(np.abs(w - f))
m2 = mag_db_two[idx]
ok = lo <= m2 <= hi
report.append((f, round(m2, 2), lo, hi, ok))
return report, phase_deg, w
report, phase_deg, w = verify_cfc60()
for f, m2, lo, hi, ok in report:
print(f"{f:5d} Hz two-pass {m2:7.2f} dB "
f"corridor [{lo}, {hi}] {'PASS' if ok else 'FAIL'}")
Run this and read the table. With fc = 100 Hz and a 4th-order Butterworth, the two-pass response at 60 Hz lands near −0.8 dB, which is outside the ±0.5 dB passband corridor — a real and common failure. The fix is to raise the corner: at fc = 120 Hz the two-pass figure at 60 Hz drops to roughly −0.45 dB and the stopband points still fall inside their windows. This is exactly the kind of iteration the corridor forces, and it is why you verify rather than assume.
For phase, the single-pass response at 60 Hz for a 4th-order Butterworth at 120 Hz is on the order of −50° to −60°. That phase lag delays the apparent peak by roughly φ/(360·f) seconds — at 60 Hz and −55°, about 2.5 ms. If your reported peak time must be accurate to a few milliseconds, either use zero-phase filtering (and state that you did) or correct the reported time by the group delay. Never mix the two conventions between channels in the same report.
Interpreting the Verification Output
The printed table is the acceptance record. A compliant design shows every row marked PASS. The most frequent failure is the 60 Hz passband point, because the two-pass magnitude doubles the single-pass attenuation; if that row fails, raise the corner until it passes while re-checking that the 300 Hz and 1000 Hz rows stay inside their windows. A second failure mode is a stopband point that is too steep — a higher-order filter can push 300 Hz below −18 dB, which is also non-compliant. The corridor is two-sided; being too aggressive is as wrong as being too gentle.
For the phase check, compute the single-pass phase at 60 Hz and convert to a time delay. Record both the zero-phase and causal peak times for the same belt load pulse; the difference is the group delay you must either correct or disclose. In a crash report, the peak belt force value is unaffected by zero-phase filtering, but the peak time is — and that time is often used to correlate belt loading with occupant kinematics.
FS, FC, ORDER) and the verification table together in the channel's processing record. When the sample rate changes between tests, the pre-warped corner changes and the coefficients must be regenerated — a filter designed at 10 kHz is not valid at 20 kHz.