548 RC Filter Designer
Design and Analyze RC Filters for Signal Conditioning
548.1 RC Filter Designer Tool
This interactive tool helps you design RC filters for IoT signal conditioning applications. Calculate cutoff frequencies, visualize Bode plots, and get component recommendations with standard E24 value snapping.
This filter designer provides:
- Filter Type Selection: Low-pass, high-pass, band-pass, and band-stop configurations
- Component Calculator: Enter R and C values to compute cutoff frequency
- Reverse Calculator: Specify target frequency to get R/C suggestions
- Bode Plot Visualization: Magnitude (dB) and phase response curves
- Standard Value Snapping: E24 resistor and capacitor series
- Transfer Function Display: Mathematical representation
- Select Filter Type: Choose LP, HP, BP, or BS from the dropdown
- Enter Components: Input resistance and capacitance values
- View Results: See cutoff frequency, Bode plots, and transfer function
- Use Suggester: Enter target frequency to get component recommendations
- Snap to E24: Click to use nearest standard component values
Show code
// ============================================
// RC Filter Designer - Interactive Tool
// Self-contained OJS implementation
// ============================================
{
const d3 = await require("d3@7");
// IEEE Color palette
const colors = {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
yellow: "#F1C40F",
darkBlue: "#1a252f"
};
// E24 standard values (multipliers)
const E24 = [1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0,
3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1];
// Configuration
const width = 950, height = 800;
const margin = { top: 20, right: 30, bottom: 40, left: 70 };
const plotWidth = 400;
const plotHeight = 200;
// State
let state = {
filterType: "lowpass",
resistance: 10000, // 10k ohms
capacitance: 0.0000001, // 100nF
resistance2: 10000, // For band filters
capacitance2: 0.0000001, // For band filters
filterOrder: 1,
targetFrequency: 1000,
queryFrequency: 100
};
// Create container
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1000px")
.style("margin", "0 auto");
// =========================================================================
// CONTROL PANEL
// =========================================================================
const controlPanel = container.append("div")
.style("background", `linear-gradient(135deg, ${colors.navy} 0%, ${colors.darkBlue} 100%)`)
.style("border-radius", "12px 12px 0 0")
.style("padding", "20px");
// Title row
controlPanel.append("div")
.style("text-align", "center")
.style("color", colors.white)
.style("font-size", "22px")
.style("font-weight", "bold")
.style("margin-bottom", "20px")
.text("RC Filter Designer");
// Controls grid
const controlsGrid = controlPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "20px");
// Filter type selector
const typeControl = controlsGrid.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("padding", "15px")
.style("border-radius", "8px");
typeControl.append("label")
.style("color", colors.white)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Filter Type");
const typeSelect = typeControl.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", "none")
.style("font-size", "14px")
.style("cursor", "pointer")
.on("change", function() {
state.filterType = this.value;
updateBandControls();
updateAll();
});
[
{ value: "lowpass", label: "Low-Pass Filter" },
{ value: "highpass", label: "High-Pass Filter" },
{ value: "bandpass", label: "Band-Pass Filter" },
{ value: "bandstop", label: "Band-Stop (Notch) Filter" }
].forEach(opt => {
typeSelect.append("option")
.attr("value", opt.value)
.attr("selected", opt.value === state.filterType ? true : null)
.text(opt.label);
});
// Filter order selector
const orderControl = controlsGrid.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("padding", "15px")
.style("border-radius", "8px");
orderControl.append("label")
.style("color", colors.white)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Filter Order");
const orderSelect = orderControl.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", "none")
.style("font-size", "14px")
.style("cursor", "pointer")
.on("change", function() {
state.filterOrder = +this.value;
updateAll();
});
[
{ value: 1, label: "1st Order (-20 dB/dec)" },
{ value: 2, label: "2nd Order (-40 dB/dec)" }
].forEach(opt => {
orderSelect.append("option")
.attr("value", opt.value)
.attr("selected", opt.value === state.filterOrder ? true : null)
.text(opt.label);
});
// Resistance input
const rControl = controlsGrid.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("padding", "15px")
.style("border-radius", "8px");
rControl.append("label")
.style("color", colors.white)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Resistance (R1)");
const rInputRow = rControl.append("div")
.style("display", "flex")
.style("gap", "8px");
const rInput = rInputRow.append("input")
.attr("type", "number")
.attr("value", state.resistance)
.attr("min", 1)
.attr("step", 100)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", "none")
.style("font-size", "14px")
.on("input", function() {
state.resistance = +this.value || 1;
updateAll();
});
rInputRow.append("span")
.style("color", colors.lightGray)
.style("align-self", "center")
.text("Ohms");
const rSnapBtn = rControl.append("button")
.style("margin-top", "8px")
.style("padding", "6px 12px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-size", "12px")
.text("Snap to E24")
.on("click", function() {
state.resistance = snapToE24(state.resistance);
rInput.property("value", state.resistance);
updateAll();
});
// Capacitance input
const cControl = controlsGrid.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("padding", "15px")
.style("border-radius", "8px");
cControl.append("label")
.style("color", colors.white)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Capacitance (C1)");
const cInputRow = cControl.append("div")
.style("display", "flex")
.style("gap", "8px");
const cInput = cInputRow.append("input")
.attr("type", "number")
.attr("value", state.capacitance * 1e9)
.attr("min", 0.001)
.attr("step", 1)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", "none")
.style("font-size", "14px")
.on("input", function() {
state.capacitance = (+this.value || 0.001) * 1e-9;
updateAll();
});
cInputRow.append("span")
.style("color", colors.lightGray)
.style("align-self", "center")
.text("nF");
const cSnapBtn = cControl.append("button")
.style("margin-top", "8px")
.style("padding", "6px 12px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-size", "12px")
.text("Snap to E24")
.on("click", function() {
const nF = state.capacitance * 1e9;
state.capacitance = snapToE24(nF) * 1e-9;
cInput.property("value", (state.capacitance * 1e9).toFixed(3));
updateAll();
});
// Band filter second stage controls (hidden by default)
const bandControlsDiv = controlsGrid.append("div")
.attr("id", "band-controls")
.style("display", "none")
.style("grid-column", "span 2")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "20px");
// R2 input
const r2Control = bandControlsDiv.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("padding", "15px")
.style("border-radius", "8px");
r2Control.append("label")
.style("color", colors.white)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Resistance (R2) - Second Stage");
const r2InputRow = r2Control.append("div")
.style("display", "flex")
.style("gap", "8px");
const r2Input = r2InputRow.append("input")
.attr("type", "number")
.attr("value", state.resistance2)
.attr("min", 1)
.attr("step", 100)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", "none")
.style("font-size", "14px")
.on("input", function() {
state.resistance2 = +this.value || 1;
updateAll();
});
r2InputRow.append("span")
.style("color", colors.lightGray)
.style("align-self", "center")
.text("Ohms");
// C2 input
const c2Control = bandControlsDiv.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("padding", "15px")
.style("border-radius", "8px");
c2Control.append("label")
.style("color", colors.white)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Capacitance (C2) - Second Stage");
const c2InputRow = c2Control.append("div")
.style("display", "flex")
.style("gap", "8px");
const c2Input = c2InputRow.append("input")
.attr("type", "number")
.attr("value", state.capacitance2 * 1e9)
.attr("min", 0.001)
.attr("step", 1)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", "none")
.style("font-size", "14px")
.on("input", function() {
state.capacitance2 = (+this.value || 0.001) * 1e-9;
updateAll();
});
c2InputRow.append("span")
.style("color", colors.lightGray)
.style("align-self", "center")
.text("nF");
function updateBandControls() {
const isBand = state.filterType === "bandpass" || state.filterType === "bandstop";
bandControlsDiv.style("display", isBand ? "grid" : "none");
}
// =========================================================================
// RESULTS PANEL
// =========================================================================
const resultsPanel = container.append("div")
.style("background", colors.lightGray)
.style("padding", "20px")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(280px, 1fr))")
.style("gap", "20px");
// Cutoff frequency display
const cutoffCard = resultsPanel.append("div")
.style("background", colors.white)
.style("padding", "20px")
.style("border-radius", "8px")
.style("border-left", `4px solid ${colors.teal}`);
cutoffCard.append("div")
.style("color", colors.gray)
.style("font-size", "12px")
.style("text-transform", "uppercase")
.style("margin-bottom", "8px")
.text("Cutoff Frequency");
const cutoffDisplay = cutoffCard.append("div")
.style("color", colors.navy)
.style("font-size", "28px")
.style("font-weight", "bold");
const cutoffFormula = cutoffCard.append("div")
.style("color", colors.gray)
.style("font-size", "12px")
.style("margin-top", "8px")
.style("font-family", "monospace");
// Transfer function display
const tfCard = resultsPanel.append("div")
.style("background", colors.white)
.style("padding", "20px")
.style("border-radius", "8px")
.style("border-left", `4px solid ${colors.orange}`);
tfCard.append("div")
.style("color", colors.gray)
.style("font-size", "12px")
.style("text-transform", "uppercase")
.style("margin-bottom", "8px")
.text("Transfer Function H(s)");
const tfDisplay = tfCard.append("div")
.style("color", colors.navy)
.style("font-size", "16px")
.style("font-family", "monospace")
.style("white-space", "pre-wrap");
// Time constant display
const tauCard = resultsPanel.append("div")
.style("background", colors.white)
.style("padding", "20px")
.style("border-radius", "8px")
.style("border-left", `4px solid ${colors.purple}`);
tauCard.append("div")
.style("color", colors.gray)
.style("font-size", "12px")
.style("text-transform", "uppercase")
.style("margin-bottom", "8px")
.text("Time Constant");
const tauDisplay = tauCard.append("div")
.style("color", colors.navy)
.style("font-size", "24px")
.style("font-weight", "bold");
// =========================================================================
// COMPONENT SUGGESTER
// =========================================================================
const suggesterPanel = container.append("div")
.style("background", colors.white)
.style("padding", "20px")
.style("border-bottom", `1px solid ${colors.lightGray}`);
const suggesterTitle = suggesterPanel.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px")
.style("margin-bottom", "15px");
suggesterTitle.append("span")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", colors.navy)
.text("Component Value Suggester");
const suggesterRow = suggesterPanel.append("div")
.style("display", "flex")
.style("flex-wrap", "wrap")
.style("gap", "15px")
.style("align-items", "center");
suggesterRow.append("label")
.style("color", colors.gray)
.text("Target Cutoff:");
const targetInput = suggesterRow.append("input")
.attr("type", "number")
.attr("value", state.targetFrequency)
.attr("min", 1)
.attr("step", 10)
.style("width", "100px")
.style("padding", "8px")
.style("border", `1px solid ${colors.gray}`)
.style("border-radius", "4px")
.on("input", function() {
state.targetFrequency = +this.value || 1;
updateSuggestions();
});
suggesterRow.append("span")
.style("color", colors.gray)
.text("Hz");
const applyBtn = suggesterRow.append("button")
.style("padding", "8px 16px")
.style("background", colors.orange)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("cursor", "pointer")
.style("font-weight", "bold")
.text("Apply Suggestions")
.on("click", applySuggestions);
const suggestionsDisplay = suggesterPanel.append("div")
.style("margin-top", "15px")
.style("padding", "15px")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("font-family", "monospace")
.style("font-size", "13px");
// =========================================================================
// FREQUENCY QUERY SECTION
// =========================================================================
const queryPanel = container.append("div")
.style("background", "#f0f7f4")
.style("padding", "20px")
.style("border-bottom", `1px solid ${colors.lightGray}`);
const queryTitle = queryPanel.append("div")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "15px")
.text("Query Specific Frequency");
const queryRow = queryPanel.append("div")
.style("display", "flex")
.style("flex-wrap", "wrap")
.style("gap", "15px")
.style("align-items", "center");
queryRow.append("label")
.style("color", colors.gray)
.text("Frequency:");
const queryInput = queryRow.append("input")
.attr("type", "number")
.attr("value", state.queryFrequency)
.attr("min", 0.1)
.attr("step", 10)
.style("width", "100px")
.style("padding", "8px")
.style("border", `1px solid ${colors.gray}`)
.style("border-radius", "4px")
.on("input", function() {
state.queryFrequency = +this.value || 0.1;
updateAll();
});
queryRow.append("span")
.style("color", colors.gray)
.text("Hz");
const queryResults = queryPanel.append("div")
.style("margin-top", "15px")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(150px, 1fr))")
.style("gap", "15px");
const gainResult = queryResults.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("text-align", "center");
gainResult.append("div")
.style("color", colors.gray)
.style("font-size", "12px")
.text("Magnitude");
const gainValue = gainResult.append("div")
.style("color", colors.teal)
.style("font-size", "24px")
.style("font-weight", "bold");
const phaseResult = queryResults.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("text-align", "center");
phaseResult.append("div")
.style("color", colors.gray)
.style("font-size", "12px")
.text("Phase");
const phaseValue = phaseResult.append("div")
.style("color", colors.orange)
.style("font-size", "24px")
.style("font-weight", "bold");
// =========================================================================
// BODE PLOT SVG
// =========================================================================
const svg = container.append("svg")
.attr("viewBox", `0 0 ${width} 500`)
.attr("width", "100%")
.style("background", colors.white)
.style("display", "block");
// Magnitude plot
const magGroup = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top + 20})`);
magGroup.append("text")
.attr("x", plotWidth)
.attr("y", -5)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Magnitude Response (Bode Plot)");
// Magnitude plot background
magGroup.append("rect")
.attr("width", plotWidth * 2)
.attr("height", plotHeight)
.attr("fill", "#fafafa")
.attr("stroke", colors.gray)
.attr("stroke-width", 1);
// Phase plot
const phaseGroup = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top + plotHeight + 80})`);
phaseGroup.append("text")
.attr("x", plotWidth)
.attr("y", -5)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Phase Response");
phaseGroup.append("rect")
.attr("width", plotWidth * 2)
.attr("height", plotHeight)
.attr("fill", "#fafafa")
.attr("stroke", colors.gray)
.attr("stroke-width", 1);
// Scales (logarithmic x-axis)
const freqMin = 1, freqMax = 100000;
const xScale = d3.scaleLog().domain([freqMin, freqMax]).range([0, plotWidth * 2]);
const yScaleMag = d3.scaleLinear().domain([20, -80]).range([0, plotHeight]);
const yScalePhase = d3.scaleLinear().domain([90, -270]).range([0, plotHeight]);
// X-axis labels for magnitude
const xTicks = [1, 10, 100, 1000, 10000, 100000];
xTicks.forEach(f => {
magGroup.append("line")
.attr("x1", xScale(f)).attr("y1", 0)
.attr("x2", xScale(f)).attr("y2", plotHeight)
.attr("stroke", colors.lightGray)
.attr("stroke-width", 1);
magGroup.append("text")
.attr("x", xScale(f))
.attr("y", plotHeight + 15)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(formatFreq(f));
phaseGroup.append("line")
.attr("x1", xScale(f)).attr("y1", 0)
.attr("x2", xScale(f)).attr("y2", plotHeight)
.attr("stroke", colors.lightGray)
.attr("stroke-width", 1);
phaseGroup.append("text")
.attr("x", xScale(f))
.attr("y", plotHeight + 15)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(formatFreq(f));
});
// Y-axis labels for magnitude
[-60, -40, -20, 0, 20].forEach(db => {
magGroup.append("line")
.attr("x1", 0).attr("y1", yScaleMag(db))
.attr("x2", plotWidth * 2).attr("y2", yScaleMag(db))
.attr("stroke", colors.lightGray)
.attr("stroke-width", db === 0 ? 2 : 1);
magGroup.append("text")
.attr("x", -5)
.attr("y", yScaleMag(db) + 4)
.attr("text-anchor", "end")
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(`${db} dB`);
});
// Y-axis labels for phase
[90, 0, -90, -180, -270].forEach(deg => {
phaseGroup.append("line")
.attr("x1", 0).attr("y1", yScalePhase(deg))
.attr("x2", plotWidth * 2).attr("y2", yScalePhase(deg))
.attr("stroke", colors.lightGray)
.attr("stroke-width", deg === 0 ? 2 : 1);
phaseGroup.append("text")
.attr("x", -5)
.attr("y", yScalePhase(deg) + 4)
.attr("text-anchor", "end")
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(`${deg}deg`);
});
// Axis labels
magGroup.append("text")
.attr("x", plotWidth)
.attr("y", plotHeight + 35)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", colors.navy)
.text("Frequency (Hz)");
phaseGroup.append("text")
.attr("x", plotWidth)
.attr("y", plotHeight + 35)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", colors.navy)
.text("Frequency (Hz)");
// -3dB reference line
magGroup.append("line")
.attr("x1", 0).attr("y1", yScaleMag(-3))
.attr("x2", plotWidth * 2).attr("y2", yScaleMag(-3))
.attr("stroke", colors.orange)
.attr("stroke-width", 1)
.attr("stroke-dasharray", "5,5");
magGroup.append("text")
.attr("x", plotWidth * 2 + 5)
.attr("y", yScaleMag(-3) + 4)
.attr("font-size", "10px")
.attr("fill", colors.orange)
.text("-3dB");
// Magnitude response path
const magPath = magGroup.append("path")
.attr("fill", "none")
.attr("stroke", colors.teal)
.attr("stroke-width", 3);
// Phase response path
const phasePath = phaseGroup.append("path")
.attr("fill", "none")
.attr("stroke", colors.orange)
.attr("stroke-width", 3);
// Cutoff frequency markers
const cutoffMarkerMag = magGroup.append("g");
cutoffMarkerMag.append("line")
.attr("y1", 0)
.attr("y2", plotHeight)
.attr("stroke", colors.purple)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "8,4");
cutoffMarkerMag.append("circle")
.attr("r", 6)
.attr("fill", colors.purple)
.attr("stroke", colors.white)
.attr("stroke-width", 2);
const cutoffMarkerPhase = phaseGroup.append("g");
cutoffMarkerPhase.append("line")
.attr("y1", 0)
.attr("y2", plotHeight)
.attr("stroke", colors.purple)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "8,4");
cutoffMarkerPhase.append("circle")
.attr("r", 6)
.attr("fill", colors.purple)
.attr("stroke", colors.white)
.attr("stroke-width", 2);
// Query frequency marker
const queryMarkerMag = magGroup.append("g");
queryMarkerMag.append("line")
.attr("y1", 0)
.attr("y2", plotHeight)
.attr("stroke", colors.red)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "4,4");
queryMarkerMag.append("circle")
.attr("r", 5)
.attr("fill", colors.red);
const queryMarkerPhase = phaseGroup.append("g");
queryMarkerPhase.append("line")
.attr("y1", 0)
.attr("y2", plotHeight)
.attr("stroke", colors.red)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "4,4");
queryMarkerPhase.append("circle")
.attr("r", 5)
.attr("fill", colors.red);
// Second cutoff markers for band filters
const cutoff2MarkerMag = magGroup.append("g").style("display", "none");
cutoff2MarkerMag.append("line")
.attr("y1", 0)
.attr("y2", plotHeight)
.attr("stroke", colors.green)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "8,4");
const cutoff2MarkerPhase = phaseGroup.append("g").style("display", "none");
cutoff2MarkerPhase.append("line")
.attr("y1", 0)
.attr("y2", plotHeight)
.attr("stroke", colors.green)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "8,4");
// Legend
const legend = svg.append("g")
.attr("transform", `translate(${margin.left + 20}, ${margin.top + plotHeight * 2 + 120})`);
const legendItems = [
{ color: colors.teal, label: "Magnitude Response" },
{ color: colors.orange, label: "Phase Response" },
{ color: colors.purple, label: "Cutoff Frequency (fc)" },
{ color: colors.red, label: "Query Frequency" }
];
legendItems.forEach((item, i) => {
const g = legend.append("g")
.attr("transform", `translate(${i * 200}, 0)`);
g.append("rect")
.attr("width", 15)
.attr("height", 15)
.attr("fill", item.color)
.attr("rx", 2);
g.append("text")
.attr("x", 20)
.attr("y", 12)
.attr("font-size", "11px")
.attr("fill", colors.navy)
.text(item.label);
});
// =========================================================================
// CALCULATION FUNCTIONS
// =========================================================================
function calculateCutoffFrequency(R, C) {
return 1 / (2 * Math.PI * R * C);
}
function snapToE24(value) {
// Find the decade
const decade = Math.floor(Math.log10(value));
const normalized = value / Math.pow(10, decade);
// Find closest E24 value
let closest = E24[0];
let minDiff = Math.abs(normalized - E24[0]);
for (const e24val of E24) {
const diff = Math.abs(normalized - e24val);
if (diff < minDiff) {
minDiff = diff;
closest = e24val;
}
}
return closest * Math.pow(10, decade);
}
function formatFreq(f) {
if (f >= 1000000) return (f / 1000000).toFixed(1) + " MHz";
if (f >= 1000) return (f / 1000).toFixed(1) + " kHz";
return f.toFixed(1) + " Hz";
}
function formatResistance(r) {
if (r >= 1000000) return (r / 1000000).toFixed(2) + " M";
if (r >= 1000) return (r / 1000).toFixed(2) + " k";
return r.toFixed(1) + " ";
}
function formatCapacitance(c) {
if (c >= 1e-6) return (c * 1e6).toFixed(2) + " uF";
if (c >= 1e-9) return (c * 1e9).toFixed(2) + " nF";
return (c * 1e12).toFixed(1) + " pF";
}
// Get filter response at frequency f
function getFilterResponse(f) {
const fc1 = calculateCutoffFrequency(state.resistance, state.capacitance);
const fc2 = calculateCutoffFrequency(state.resistance2, state.capacitance2);
const n = state.filterOrder;
let magnitude, phase;
switch (state.filterType) {
case "lowpass":
magnitude = 1 / Math.pow(Math.sqrt(1 + Math.pow(f / fc1, 2)), n);
phase = -n * Math.atan(f / fc1) * (180 / Math.PI);
break;
case "highpass":
magnitude = 1 / Math.pow(Math.sqrt(1 + Math.pow(fc1 / f, 2)), n);
phase = n * (90 - Math.atan(f / fc1) * (180 / Math.PI));
break;
case "bandpass":
const f0_bp = Math.sqrt(fc1 * fc2);
const Q_bp = f0_bp / Math.abs(fc2 - fc1);
const s_bp = f / f0_bp;
magnitude = (s_bp / Q_bp) / Math.sqrt(Math.pow(1 - s_bp * s_bp, 2) + Math.pow(s_bp / Q_bp, 2));
phase = Math.atan2(1 - s_bp * s_bp, s_bp / Q_bp) * (180 / Math.PI);
break;
case "bandstop":
const f0_bs = Math.sqrt(fc1 * fc2);
const Q_bs = f0_bs / Math.abs(fc2 - fc1);
const s_bs = f / f0_bs;
magnitude = Math.abs(1 - s_bs * s_bs) / Math.sqrt(Math.pow(1 - s_bs * s_bs, 2) + Math.pow(s_bs / Q_bs, 2));
phase = -Math.atan2(s_bs / Q_bs, 1 - s_bs * s_bs) * (180 / Math.PI);
break;
}
return { magnitude, phase, db: 20 * Math.log10(Math.max(magnitude, 1e-10)) };
}
function updateSuggestions() {
const targetFc = state.targetFrequency;
// Common capacitor values to try
const commonCaps = [10e-12, 22e-12, 47e-12, 100e-12, 220e-12, 470e-12,
1e-9, 2.2e-9, 4.7e-9, 10e-9, 22e-9, 47e-9,
100e-9, 220e-9, 470e-9, 1e-6];
let suggestions = [];
for (const C of commonCaps) {
const R = 1 / (2 * Math.PI * targetFc * C);
if (R >= 100 && R <= 10000000) {
const R_e24 = snapToE24(R);
const actualFc = calculateCutoffFrequency(R_e24, C);
const error = Math.abs(actualFc - targetFc) / targetFc * 100;
suggestions.push({
R: R_e24,
C: C,
actualFc: actualFc,
error: error
});
}
}
// Sort by error and take top 3
suggestions.sort((a, b) => a.error - b.error);
suggestions = suggestions.slice(0, 3);
let html = `<strong>Suggested Components for ${formatFreq(targetFc)}:</strong><br><br>`;
if (suggestions.length === 0) {
html += "No suitable combinations found. Try a different target frequency.";
} else {
suggestions.forEach((s, i) => {
html += `<strong>Option ${i + 1}:</strong> R = ${formatResistance(s.R)}Ohm, C = ${formatCapacitance(s.C)}<br>`;
html += ` Actual fc = ${formatFreq(s.actualFc)} (error: ${s.error.toFixed(2)}%)<br><br>`;
});
}
suggestionsDisplay.html(html);
// Store best suggestion for apply button
if (suggestions.length > 0) {
state.suggestedR = suggestions[0].R;
state.suggestedC = suggestions[0].C;
}
}
function applySuggestions() {
if (state.suggestedR && state.suggestedC) {
state.resistance = state.suggestedR;
state.capacitance = state.suggestedC;
rInput.property("value", state.resistance);
cInput.property("value", (state.capacitance * 1e9).toFixed(3));
updateAll();
}
}
function updateAll() {
const fc1 = calculateCutoffFrequency(state.resistance, state.capacitance);
const fc2 = calculateCutoffFrequency(state.resistance2, state.capacitance2);
const tau = state.resistance * state.capacitance;
// Update cutoff display
if (state.filterType === "bandpass" || state.filterType === "bandstop") {
const fLow = Math.min(fc1, fc2);
const fHigh = Math.max(fc1, fc2);
cutoffDisplay.text(`${formatFreq(fLow)} - ${formatFreq(fHigh)}`);
cutoffFormula.text(`fc1 = 1/(2piR1C1), fc2 = 1/(2piR2C2)`);
} else {
cutoffDisplay.text(formatFreq(fc1));
cutoffFormula.text(`fc = 1/(2piRC) = 1/(2pi x ${formatResistance(state.resistance)}Ohm x ${formatCapacitance(state.capacitance)})`);
}
// Update time constant
tauDisplay.text(`${(tau * 1000).toFixed(3)} ms`);
// Update transfer function
let tfText = "";
const n = state.filterOrder;
switch (state.filterType) {
case "lowpass":
tfText = n === 1 ?
`H(s) = wc / (s + wc)\n\nwc = ${(2 * Math.PI * fc1).toFixed(1)} rad/s` :
`H(s) = wc^2 / (s^2 + sqrt(2)*wc*s + wc^2)\n\nwc = ${(2 * Math.PI * fc1).toFixed(1)} rad/s`;
break;
case "highpass":
tfText = n === 1 ?
`H(s) = s / (s + wc)\n\nwc = ${(2 * Math.PI * fc1).toFixed(1)} rad/s` :
`H(s) = s^2 / (s^2 + sqrt(2)*wc*s + wc^2)\n\nwc = ${(2 * Math.PI * fc1).toFixed(1)} rad/s`;
break;
case "bandpass":
const f0_bp = Math.sqrt(fc1 * fc2);
tfText = `H(s) = (BW*s) / (s^2 + BW*s + w0^2)\n\nw0 = ${(2 * Math.PI * f0_bp).toFixed(1)} rad/s\nBW = ${(2 * Math.PI * Math.abs(fc2 - fc1)).toFixed(1)} rad/s`;
break;
case "bandstop":
const f0_bs = Math.sqrt(fc1 * fc2);
tfText = `H(s) = (s^2 + w0^2) / (s^2 + BW*s + w0^2)\n\nw0 = ${(2 * Math.PI * f0_bs).toFixed(1)} rad/s\nBW = ${(2 * Math.PI * Math.abs(fc2 - fc1)).toFixed(1)} rad/s`;
break;
}
tfDisplay.text(tfText);
// Generate Bode plot data
const magPoints = [];
const phasePoints = [];
for (let i = 0; i <= 500; i++) {
const f = freqMin * Math.pow(freqMax / freqMin, i / 500);
const response = getFilterResponse(f);
if (isFinite(response.db) && response.db > -80) {
magPoints.push([xScale(f), yScaleMag(response.db)]);
}
if (isFinite(response.phase)) {
phasePoints.push([xScale(f), yScalePhase(response.phase)]);
}
}
magPath.attr("d", d3.line()(magPoints));
phasePath.attr("d", d3.line()(phasePoints));
// Update cutoff markers
const isBand = state.filterType === "bandpass" || state.filterType === "bandstop";
if (fc1 >= freqMin && fc1 <= freqMax) {
const response1 = getFilterResponse(fc1);
cutoffMarkerMag
.attr("transform", `translate(${xScale(fc1)}, 0)`)
.select("circle").attr("cy", yScaleMag(response1.db));
cutoffMarkerPhase
.attr("transform", `translate(${xScale(fc1)}, 0)`)
.select("circle").attr("cy", yScalePhase(response1.phase));
}
if (isBand && fc2 >= freqMin && fc2 <= freqMax) {
cutoff2MarkerMag.style("display", null)
.attr("transform", `translate(${xScale(fc2)}, 0)`);
cutoff2MarkerPhase.style("display", null)
.attr("transform", `translate(${xScale(fc2)}, 0)`);
} else {
cutoff2MarkerMag.style("display", "none");
cutoff2MarkerPhase.style("display", "none");
}
// Update query markers
const queryF = Math.max(freqMin, Math.min(freqMax, state.queryFrequency));
const queryResponse = getFilterResponse(queryF);
queryMarkerMag
.attr("transform", `translate(${xScale(queryF)}, 0)`)
.select("circle").attr("cy", yScaleMag(Math.max(-80, queryResponse.db)));
queryMarkerPhase
.attr("transform", `translate(${xScale(queryF)}, 0)`)
.select("circle").attr("cy", yScalePhase(queryResponse.phase));
// Update query results
gainValue.text(`${queryResponse.db.toFixed(2)} dB`);
phaseValue.text(`${queryResponse.phase.toFixed(1)} deg`);
// Update suggestions
updateSuggestions();
}
// Initial update
updateBandControls();
updateAll();
return container.node();
}548.2 Understanding RC Filters
RC filters are fundamental building blocks in analog signal conditioning for IoT applications. They use passive components (resistors and capacitors) to shape the frequency response of signals.
548.2.1 Filter Types
| Filter Type | Passes | Blocks | Common Applications |
|---|---|---|---|
| Low-Pass | Low frequencies | High frequencies | Noise removal, anti-aliasing |
| High-Pass | High frequencies | Low frequencies | DC blocking, bass removal |
| Band-Pass | Specific range | Outside range | Signal selection, tuning |
| Band-Stop | Outside range | Specific range | Notch filters, 60Hz rejection |
548.2.2 Cutoff Frequency Formula
The cutoff frequency (fc) is where the filter attenuates the signal by 3 dB (approximately 70.7% of the input):
\[f_c = \frac{1}{2\pi RC}\]
This is also called the -3dB point or half-power point.
548.2.3 Transfer Functions
Low-Pass: \[H(s) = \frac{\omega_c}{s + \omega_c}\]
High-Pass: \[H(s) = \frac{s}{s + \omega_c}\]
Where \(\omega_c = 2\pi f_c\) is the angular cutoff frequency.
548.3 Bode Plot Interpretation
Bode plots show how a filter responds across all frequencies:
548.3.1 Magnitude Response
- Passband: Region where signals pass with minimal attenuation (near 0 dB)
- Stopband: Region where signals are significantly attenuated
- Roll-off: Rate of attenuation in the transition region
- 1st order: -20 dB/decade
- 2nd order: -40 dB/decade
548.3.2 Phase Response
- First-order low-pass: 0 deg at DC, -45 deg at fc, approaches -90 deg at high frequencies
- First-order high-pass: +90 deg at DC, +45 deg at fc, approaches 0 deg at high frequencies
548.4 E24 Standard Values
The E24 series provides 24 standard values per decade. Using standard values:
- Reduces component cost
- Improves availability
- Maintains reasonable accuracy (5% tolerance)
- Always snap both R and C to E24 values
- Check the actual cutoff frequency after snapping
- Adjust the less critical component if needed
- Consider E96 (1%) values for precision applications
548.5 Practical Design Guidelines
548.5.1 Choosing Components
| Application | Typical fc | R Range | C Range |
|---|---|---|---|
| Temperature sensors | 1-10 Hz | 10k-100k | 1-10 uF |
| Accelerometers | 50-500 Hz | 1k-10k | 100nF-1uF |
| Audio signals | 20 Hz-20 kHz | 1k-47k | 1nF-1uF |
| Anti-aliasing | fs/2 | 1k-10k | 10nF-100nF |
548.5.2 Design Constraints
- Resistance: Keep between 1k and 1M for most applications
- Too low: Excessive current draw, loading effects
- Too high: Noise pickup, bias current errors
- Capacitance: Consider size and availability
- Electrolytic (1uF+): Polarized, larger, cheaper
- Ceramic (< 1uF): Non-polarized, smaller, temperature dependent
- Film: Stable, accurate, more expensive
- Source impedance: Filter R should be >> source impedance
- Load impedance: Load should be >> filter R
548.6 What’s Next
- Circuit Analysis Solver - Analyze complete circuits
- Low-Pass Filter Animation - See filtering in action
- ADC Sampling - Anti-aliasing filter design
- Sensor Interfacing - Complete signal chains
- Simulations Hub - More interactive tools
This interactive tool is implemented in approximately 800 lines of Observable JavaScript:
Key Features:
- Filter calculations: Accurate Butterworth response modeling for 1st and 2nd order filters
- Bode plot visualization: Logarithmic frequency axis, dB magnitude scale
- E24 value snapping: Automatic component value optimization
- Component suggester: Reverse calculator for target frequencies
- Interactive query: Real-time gain and phase at any frequency
IEEE Color Palette: - Navy (#2C3E50): Primary text, borders - Teal (#16A085): Magnitude response, positive values - Orange (#E67E22): Phase response, highlights - Purple (#9B59B6): Cutoff frequency markers - Red (#E74C3C): Query frequency marker - Gray (#7F8C8D): Axes, labels, secondary text
Mathematical Models: - Cutoff: fc = 1 / (2 * pi * R * C) - Low-pass magnitude: |H| = 1 / sqrt(1 + (f/fc)^(2n)) - Low-pass phase: phi = -n * atan(f/fc) - High-pass magnitude: |H| = 1 / sqrt(1 + (fc/f)^(2n))