Документация Engee
Notebook

RC-фильтр для выделения медленного сигнала

В этом примере рассчитывается и проверяется пассивный RC-фильтр нижних частот. Фильтр должен сохранить полезную составляющую около 1 кГц и ослабить пульсацию около 16 кГц. В скрипте используются расчет параметров, аналитическая оценка амплитудно-частотной характеристики, загрузка готовых моделей Engee, программный запуск симуляции, получение скриншотов модели и построение графиков результатов. Пример полезен инженеру, которому необходимо быстро оценить влияние нагрузки на простой аналоговый фильтр.

Введение

Этот пример описывает ход решения задачи

2W1.1 - Фильтр для ослабления сигнала и ослабления помехи

из книги

Томас К. Хейс, Пол Хоровиц
Искусство схемотехники: Теория и практика

путем компьютерного моделирования в Engee.

Пусть входной сигнал состоит из медленной полезной части и быстрой пульсации. Такая ситуация возникает после выпрямителей, импульсных преобразователей и датчиков с высокочастотной наводкой.

Используйте фильтр нижних частот, если полезная составляющая расположена ниже помехи по частоте. В этом примере вход содержит компоненту 1 кГц и пульсацию 16 кГц. Поэтому выберите частоту среза между этими значениями.

Метод фильтрации и расчетные соотношения

Ненагруженный RC-фильтр описывает передаточная функция

Частота среза по уровню дБ равна

Если к выходу подключена нагрузка , она шунтирует конденсатор. Тогда постоянная составляющая уменьшается, а полюс сдвигается вверх:

Расчет параметров

В этой ячейке задайте исходные частоты, выберите резистор и рассчитайте емкость для целевой частоты среза. Затем сравните точное значение с ближайшим удобным стандартным номиналом.

In [ ]:
R = 10_000.0
C_standard = 10e-9
f_signal = 1_000.0
f_ripple = 16_000.0
f_target = 2_000.0

C_exact = 1 / (2pi * R * f_target)
f3db_standard = 1 / (2pi * R * C_standard)

calculation_summary = (;
    R_ohm = R,
    C_exact_F = C_exact,
    C_standard_F = C_standard,
    f3db_standard_Hz = f3db_standard,
)

calculation_summary
Out[0]:
(R_ohm = 10000.0, C_exact_F = 7.957747154594767e-9, C_standard_F = 1.0e-8, f3db_standard_Hz = 1591.5494309189535)

Расчет дает емкость около 8 нФ для частоты 2 кГц. Номинал 0.01 мкФ удобен как стандартное значение. Он снижает частоту среза примерно до 1.59 кГц. Такое смещение усиливает подавление пульсации, но немного сильнее ослабляет полезную составляющую.

Оценка влияния нагрузки

В этой ячейке рассчитайте коэффициенты передачи для трех случаев: без нагрузки, с нагрузкой 100 кОм и с нагрузкой 10 кОм. Так вы увидите, насколько нагрузка меняет уровень сигнала и положение полюса.

In [ ]:
function rc_case(load_ohm; R = R, C = C_standard)
    if isinf(load_ohm)
        dc_gain = 1.0
        tau = R * C
    else
        dc_gain = load_ohm / (R + load_ohm)
        tau = R * C / (1 + R / load_ohm)
    end

    f3db = 1 / (2pi * tau)
    gain(f) = dc_gain / sqrt(1 + (f / f3db)^2)

    return (;
        load_ohm,
        dc_gain,
        tau_s = tau,
        f3db_Hz = f3db,
        gain_1k = gain(f_signal),
        gain_16k = gain(f_ripple),
        ripple_to_signal = gain(f_ripple) / gain(f_signal),
    )
end

load_cases = [
    :no_load => rc_case(Inf),
    :load_100k => rc_case(100_000.0),
    :load_10k => rc_case(10_000.0),
]

load_cases
Out[0]:
3-element Vector{Pair{Symbol, @NamedTuple{load_ohm::Float64, dc_gain::Float64, tau_s::Float64, f3db_Hz::Float64, gain_1k::Float64, gain_16k::Float64, ripple_to_signal::Float64}}}:
   :no_load => (load_ohm = Inf, dc_gain = 1.0, tau_s = 0.0001, f3db_Hz = 1591.5494309189535, gain_1k = 0.8467330159648304, gain_16k = 0.09898334222765102, ripple_to_signal = 0.11690029839554805)
 :load_100k => (load_ohm = 100000.0, dc_gain = 0.9090909090909091, tau_s = 9.09090909090909e-5, f3db_Hz = 1750.704374010849, gain_1k = 0.7893901142957572, gain_16k = 0.0988816691205574, ripple_to_signal = 0.12526337400205884)
  :load_10k => (load_ohm = 10000.0, dc_gain = 0.5, tau_s = 5.0e-5, f3db_Hz = 3183.098861837907, gain_1k = 0.4770141081892325, gain_16k = 0.09755993138530368, ripple_to_signal = 0.2045221089071048)

Нагрузка 100 кОм почти не меняет подавление высокочастотной пульсации. Нагрузка 10 кОм заметно уменьшает полезный уровень и повышает частоту среза. Поэтому такой фильтр стоит подключать ко входу с существенно большим сопротивлением, чем последовательный резистор.

Описание моделей

Для примера используются готовые модели Engee.

В набор входят три модели:

  • RCFilter_2W_1_1_signal.engee — сигнальная модель с тремя ветвями Transfer Fcn;
  • RCFilter_2W_1_1_phys_RL100k.engee — физическая RC-цепь с нагрузкой 100 кОм;
  • RCFilter_2W_1_1_phys_RL10k.engee — физическая RC-сеть с нагрузкой 10 кОм.

В физических моделях используются блоки Controlled Voltage Source, Resistor, Capacitor, Voltage Sensor, Electrical Reference и один Solver Configuration на связную физическую сеть.

Подготовка окружения

В этой ячейке задайте пути к готовым моделям. Если вы запускаете пример в другой папке Engee, измените переменную dir.

In [ ]:
using Plots

dir = @__DIR__
model_paths = Dict(
    :signal => joinpath(dir, "RCFilter_2W_1_1_signal.engee"),
    :phys_100k => joinpath(dir, "RCFilter_2W_1_1_phys_RL100k.engee"),
    :phys_10k => joinpath(dir, "RCFilter_2W_1_1_phys_RL10k.engee"),
)
string("prepared; dir=", dir)
Out[0]:
"prepared; dir=/user/alexevs/horoviz_2W_1_1"

Окружение готово к запуску. Переменная model_paths хранит пути к моделям, а dir для файлов с результатами.

Аналитическая проверка

Перед запуском модели постройте расчетную амплитудно-частотную характеристику. Этот график показывает ожидаемое поведение фильтра и помогает проверить результаты симуляции.

In [ ]:
freqs = exp10.(range(log10(100.0), log10(100_000.0), length = 600))

function rc_gain(load_ohm, f)
    c = rc_case(load_ohm)
    return c.dc_gain / sqrt(1 + (f / c.f3db_Hz)^2)
end

p_response = plot(
    freqs,
    20 .* log10.(rc_gain.(Inf, freqs));
    xscale = :log10,
    label = "без нагрузки",
    xlabel = "Частота, Гц",
    ylabel = "Амплитуда, дБ",
    title = "Расчетная АЧХ RC-фильтра",
)
plot!(p_response, freqs, 20 .* log10.(rc_gain.(100_000.0, freqs)); label = "Rн = 100 кОм")
plot!(p_response, freqs, 20 .* log10.(rc_gain.(10_000.0, freqs)); label = "Rн = 10 кОм")

response_path = joinpath(output_dir, "rc_filter_response.png")
savefig(p_response, response_path)
p_response
Out[0]:
No description has been provided for this image

На графике видно, что нагрузка 10 кОм поднимает частоту среза и одновременно снижает низкочастотный уровень. Это худший из рассматриваемых случаев для сохранения полезного сигнала.

Симуляция модели

Теперь загрузите сигнальную модель и запустите симуляцию. Код использует программное управление Engee: engee.load, engee.open, engee.run и engee.close.

image.png
In [ ]:
missing_models = [path for path in values(model_paths) if !isfile(path)]

if isempty(missing_models)
    signal_payload = Core.eval(Main, quote
        model = engee.load($(model_paths[:signal]); force = true)
        engee.open(model)
        results = engee.run(model)
        (; results, result_text = repr(results))
    end)

    signal_results = signal_payload.results
    string("signal model simulated; results=", signal_payload.result_text)
else
    string("missing_models=", join(missing_models, ";"))
end
Out[0]:
"signal model simulated; results=SimulationResult(\n    run_id => 148,\n    \"input_sum.1\" => WorkspaceArray{Float64}(\"RCFilter_2W_1_1_signal/input_sum.1\")\n,\n    \"lp_load_10k.1\" => WorkspaceArray{Float64}(\"RCFilter_2W_1_1_signal/lp_load_10k.1\")\n,\n    \"lp_load_100k.1\" => WorkspaceArray{Float64}(\"RCFilter_2W_1_1_signal/lp_load_100k.1\")\n,\n    \"lp_no_load.1\" => WorkspaceArray{Float64}(\"RCFilter_2W_1_1_signal/lp_no_load.1\")\n\n)"

Сигнальная модель запущена. Результат симуляции содержит входной сигнал и три выходных сигнала для разных нагрузок.

Проверка физических моделей

В этой ячейке запустите две физические модели. Они проверяют ту же RC-схему как acausal-электрическую цепь с реальными электрическими узлами и датчиком напряжения. По изображениям ниже видно их структуру: источник, последовательный резистор, конденсатор и нагрузка должны образовывать одну связную электрическую цепь с общей землей.

image.png
In [ ]:
physical_payload = Core.eval(Main, quote
    model_paths_main = $(model_paths)
    output_dir_main = $(output_dir)
    results = Dict{Symbol, Any}()

    for key in (:phys_100k, :phys_10k)
        model = engee.load(model_paths_main[key]; force = true)
        engee.open(model)
        results[key] = engee.run(model)
        engee.close(model; force = true)
    end

    (; results, result_text = repr(results))
end)

physical_results = physical_payload.results
string("physical models simulated; results=", physical_payload.result_text)
Warning: Assignment to `model` in soft scope is ambiguous because a global variable by the same name exists: `model` will be treated as a new local. Disambiguate by using `local model` to suppress this warning or `global model` to assign to the existing global variable.
@ In[48]:7
Out[0]:
"physical models simulated; results=Dict{Symbol, Any}(:phys_10k => SimulationResult(\n    run_id => 150,\n    \"input_sum.1\" => WorkspaceArray{Float64}(\"RCFilter_2W_1_1_phys_RL10k/input_sum.1\")\n,\n    \"vout_sensor.V\" => WorkspaceArray{Float64}(\"RCFilter_2W_1_1_phys_RL10k/vout_sensor.V\")\n\n), :phys_100k => SimulationResult(\n    run_id => 149,\n    \"input_sum.1\" => WorkspaceArray{Float64}(\"RCFilter_2W_1_1_phys_RL100k/input_sum.1\")\n,\n    \"vout_sensor.V\" => WorkspaceArray{Float64}(\"RCFilter_2W_1_1_phys_RL100k/vout_sensor.V\")\n\n))"

Обе физические модели запускаются тем же программным способом.

Анализ результатов симуляции

Постройте временные графики по результатам сигнальной модели. Сравните вход с выходами для разных нагрузок. Так легче увидеть подавление пульсации и падение амплитуды при тяжелой нагрузке.

In [ ]:
function series_from_result(results, key)
    df = results[key]
    return df.time, df.value
end

time, input_signal = series_from_result(signal_results, "input_sum.1")
_, y_no_load = series_from_result(signal_results, "lp_no_load.1")
_, y_100k = series_from_result(signal_results, "lp_load_100k.1")
_, y_10k = series_from_result(signal_results, "lp_load_10k.1")

p_time = plot(
    time .* 1000,
    input_signal;
    label = "вход",
    xlabel = "Время, мс",
    ylabel = "Напряжение, отн. ед.",
    title = "Фильтрация сигнала с пульсацией",
)
plot!(p_time, time .* 1000, y_no_load; label = "без нагрузки")
plot!(p_time, time .* 1000, y_100k; label = "Rн = 100 кОм")
plot!(p_time, time .* 1000, y_10k; label = "Rн = 10 кОм")

time_plot_path = joinpath(output_dir, "rc_filter_time_signals.png")
savefig(p_time, time_plot_path)
p_time
Out[0]:
No description has been provided for this image

Выходные сигналы стали более гладкими, чем входной сигнал. Вариант с 100 кОм близок к ненагруженному фильтру. Вариант с 10 кОм дает меньший уровень полезной составляющей, потому что нагрузка образует делитель с последовательным резистором.

Сводная таблица

Соберите расчетные показатели в компактную таблицу. Она фиксирует частоту среза, коэффициент передачи полезной составляющей и относительное прохождение пульсации.

In [ ]:
summary_rows = [
    (case = string(name), values...)
    for (name, values) in load_cases
]

summary_csv = joinpath(output_dir, "rc_filter_summary.csv")
open(summary_csv, "w") do io
    println(io, "case,load_ohm,dc_gain,tau_s,f3db_Hz,gain_1k,gain_16k,ripple_to_signal")
    for row in summary_rows
        println(io, join((
            row.case,
            row.load_ohm,
            row.dc_gain,
            row.tau_s,
            row.f3db_Hz,
            row.gain_1k,
            row.gain_16k,
            row.ripple_to_signal,
        ), ","))
    end
end

summary_rows
Out[0]:
3-element Vector{@NamedTuple{case::String, load_ohm::Float64, dc_gain::Float64, tau_s::Float64, f3db_Hz::Float64, gain_1k::Float64, gain_16k::Float64, ripple_to_signal::Float64}}:
 (case = "no_load", load_ohm = Inf, dc_gain = 1.0, tau_s = 0.0001, f3db_Hz = 1591.5494309189535, gain_1k = 0.8467330159648304, gain_16k = 0.09898334222765102, ripple_to_signal = 0.11690029839554805)
 (case = "load_100k", load_ohm = 100000.0, dc_gain = 0.9090909090909091, tau_s = 9.09090909090909e-5, f3db_Hz = 1750.704374010849, gain_1k = 0.7893901142957572, gain_16k = 0.0988816691205574, ripple_to_signal = 0.12526337400205884)
 (case = "load_10k", load_ohm = 10000.0, dc_gain = 0.5, tau_s = 5.0e-5, f3db_Hz = 3183.098861837907, gain_1k = 0.4770141081892325, gain_16k = 0.09755993138530368, ripple_to_signal = 0.2045221089071048)

Таблица демонстрирует инженерный компромисс. Большая нагрузка почти не влияет на фильтр. Нагрузка, сравнимая с R, снижает уровень сигнала и ухудшает разделение полезной составляющей и пульсации.

Заключение

Мы рассчитали и проверили RC-фильтр нижних частот для отделения медленного сигнала от быстрой пульсации. Номиналы R = 10 кОм и C = 0.01 мкФ дают частоту среза около 1.59 кГц. Этого достаточно, чтобы заметно ослабить компоненту 16 кГц и сохранить большую часть сигнала 1 кГц.

Модель также показывает ограничение пассивного фильтра. Если нагрузка мала, она меняет и коэффициент передачи, и частоту среза. Поэтому подключайте такой фильтр к входу с высоким сопротивлением или добавляйте буферный каскад.

Список литературы

  1. Хейс Т.К., Хоровиц П. Искусство схемотехники. Теория и практика. БХВ-Петербург, 2022. с.128-130
  2. Хоровиц П., Хилл У. Искусство схемотехники. Том 1. Мир, 1993.
  3. Бессонов Л. А. Теоретические основы электротехники. Электрические цепи. Гардарики, 2002.
  4. Седра А., Смит К. Микроэлектронные схемы. Вильямс.
  5. Engee Help Center. Документация блоков Transfer Fcn, Sine Wave, Resistor, Capacitor, Controlled Voltage Source, Voltage Sensor, Solver Configuration.
  6. Engee Help Center. Программное управление моделями: engee.load, engee.open, engee.run, engee.get_results, engee.close.