RRC-фильтр Verilog
Реализация фильтра Найквиста для обработки импульсного сигнала
В современных системах цифровой связи качество передачи информации напрямую зависит от эффективности обработки сигналов, где ключевую роль играют формирующие фильтры. Среди них особое значение имеют фильтры с корневой косинусоидальной характеристикой (Root Raised Cosine, RRC), которые обеспечивают оптимальный компромисс между спектральной эффективностью и устойчивостью к межсимвольной интерференции (Intersymbol Interference, ISI). Эти фильтры стали стандартом де-факто в большинстве современных систем связи, включая сотовые сети 5G, спутниковую связь и высокоскоростные модемы.
Однако проектирование RRC фильтров представляет собой сложную инженерную задачу, требующую учёта множества параметров: коэффициента сглаживания (roll-off factor), длины фильтра в символах, количества отсчётов на символ, а также точности представления данных. Традиционный подход к разработке таких фильтров предполагает трудоёмкий итерационный процесс, включающий расчёт коэффициентов, выбор структуры реализации, оптимизацию разрядности и верификацию характеристик. Особую сложность добавляет необходимость учёта эффектов квантования при переходе к аппаратной реализации с фиксированной точкой.
Эта статья демонстрирует практическую реализацию метода на примере RRC фильтра с коэффициентом сглаживания 0.2, длиной 10 символов и 8 отсчётами на символ, включая сравнительный анализ прямой и транспонированной структур, верификацию частотных и временных характеристик.
Перейдём к разработке, код ниже рассчитывает коэффициенты для КИХ-фильтра с корневой косинусоидальной характеристикой (RRC), который является основным формирующим фильтром в современных системах цифровой связи. Функция rrcosfilter вычисляет импульсную характеристику фильтра по заданным параметрам: коэффициенту сглаживания roll-off factor, длине фильтра в символах и количеству отсчётов на символ. Фильтр обеспечивает оптимальное формирование спектра передаваемого сигнала, минимизируя межсимвольную интерференцию и ограничивая полосу частот. Расчёт коэффициентов является первым этапом проектирования фильтра и его основой.
using FFTW, DSP
function rrcosfilter(β::Float64, span::Int, sps::Int)
T = 1.0
t = range(-span/2, stop=span/2, length=span*sps+1)
h = similar(t)
ϵ = 1e-10
for (i, τ) in enumerate(t)
if abs(τ) < ϵ
h[i] = 1.0 - β + 4β/π
elseif abs(abs(τ) - T/(4β)) < ϵ
h[i] = (β/√2) * ((1 + 2/π)*sin(π/(4β)) + (1 - 2/π)*cos(π/(4β)))
else
num = sin(π*τ/T*(1-β)) + 4β*τ/T * cos(π*τ/T*(1+β))
den = π*τ/T * (1 - (4β*τ/T)^2)
h[i] = num / den
end
end
return h / √sum(h.^2)
end
rolloff_factor = 0.2
filter_span = 10
samples_per_symbol = 8
coef = rrcosfilter(rolloff_factor, filter_span, samples_per_symbol)
coef = coef[1:2:end]
println("Rolloff factor: ", rolloff_factor)
println("Filter span in symbols: ", filter_span)
println("Output samples per symbol: ", samples_per_symbol)
println("Number of coefficients: ", length(coef))
plot(coef)
Прямая форма КИХ-фильтра — это классическая структура реализации конечного импульсного фильтра, в которой входной сигнал последовательно проходит через цепочку элементов задержки, каждый выход которых умножается на соответствующий коэффициент фильтра, а результаты суммируются для получения выходного сигнала.
Описание кода:
Данный код автоматически создаёт модель прямой формы КИХ-фильтра на основе рассчитанных ранее коэффициентов RRC. Сначала генерируется уникальное имя модели и определяется формат фиксированной точки с 16 битами и 14 дробными разрядами. Коэффициенты преобразуются в формат фиксированной точки.
Затем в цикле создаётся структура фильтра: для каждого коэффициента добавляются блоки усиления (Gain), задержки (Unit Delay) и сложения (Add). Формируется цепочка из элементов задержки, где каждый задержанный сигнал умножается на соответствующий коэффициент и суммируется с предыдущими результатами. Первый коэффициент обрабатывает текущий входной сигнал, а последующие коэффициенты — задержанные версии сигнала.
Входной сигнал подаётся одновременно на первый элемент усиления и первую задержку, создавая каскадную структуру. Все математические операции выполняются в формате фиксированной точки для обеспечения совместимости с аппаратной реализацией. После построения полной структуры модель сохраняется в файл и загружается для дальнейшего использования, обеспечивая автоматизированное создание архитектуры фильтра с минимальным вмешательством разработчика.
name_model = "fir_$(round(Int, rand() * 10000))"
Path = (@__DIR__) * "/" * name_model * ".engee"
println("Path: $Path")
FIXED_POINT_TYPE = "fixdt(1, 16, 14)"
coef = fi(coef, 1, 16, 15)
engee.create(name_model) # Создать модель
engee.add_block("/Basic/Ports & Subsystems/In1", name_model*"/")
engee.add_block("/Basic/Ports & Subsystems/Out1", name_model*"/")
# Устанавливаем тип данных с фиксированной точкой для входного порта
engee.set_param!(name_model*"/In1",
"OutDataTypeStr" => "Fixed-point",
"OutDataTypeStrFixed" => FIXED_POINT_TYPE)
# Определяем количество коэффициентов
num_coef = length(coef)
@time for n in 1:num_coef-1
name_gain = "Gain-" * string(n)
engee.add_block("/Basic/Math Operations/Gain", name_model*"/"*name_gain)
engee.set_param!(name_model*"/"*name_gain,
"Gain" => coef[n],
"OutDataTypeStr" => "Fixed-point",
"OutDataTypeStrFixed" => FIXED_POINT_TYPE)
name_delay = "Delay-" * string(n)
engee.add_block("/Basic/Discrete/Unit Delay", name_model*"/"*name_delay) # Заменяем Delay на UnitDelay
name_add = "Add-" * string(n)
engee.add_block("/Basic/Math Operations/Add", name_model*"/"*name_add)
engee.set_param!(name_model*"/"*name_add,
"OutDataTypeStr" => "Fixed-point",
"OutDataTypeStrFixed" => FIXED_POINT_TYPE)
if n == 1
engee.add_line(name_gain*"/1", name_add*"/1")
end
if n > 1
name_delay_prev = "Delay-" * string(n-1)
engee.add_line(name_delay_prev*"/1", name_delay*"/1")
engee.add_line(name_delay_prev*"/1", name_gain*"/1")
name_add_prev = "Add-" * string(n-1)
engee.add_line(name_add_prev*"/1", name_add*"/1")
engee.add_line(name_gain*"/1", name_add_prev*"/2")
end
if n == num_coef-1
name_gain_last = "Gain-" * string(n+1)
engee.add_block("/Basic/Math Operations/Gain", name_model*"/"*name_gain_last)
engee.set_param!(name_model*"/"*name_gain_last,
"Gain" => coef[n+1],
"OutDataTypeStr" => "Fixed-point",
"OutDataTypeStrFixed" => FIXED_POINT_TYPE)
engee.add_line(name_delay*"/1", name_gain_last*"/1")
engee.add_line(name_gain_last*"/1", name_add*"/2")
engee.add_line(name_add*"/1", "Out1/1")
end
end
engee.add_line("In1/1", "Gain-1/1")
engee.add_line("In1/1", "Delay-1/1")
engee.save(Path)
model = engee.load(Path, force=true)
Транспонированная форма КИХ-фильтра — это альтернативная структура реализации, в которой все операции умножения на коэффициенты выполняются параллельно с входным сигналом, а результаты последовательно накапливаются через цепочку сумматоров и элементов задержки.
Описание кода:
Данный код создаёт транспонированную структуру КИХ-фильтра, которая принципиально отличается от прямой формы. В этой архитектуре входной сигнал одновременно подаётся на все блоки умножения (Gain), каждый из которых умножает его на соответствующий коэффициент. Полученные произведения затем последовательно суммируются через цепочку сумматоров, между которыми вставлены элементы задержки. Это создаёт конвейерную обработку, где каждый такт данные проходят через всю структуру.
Преимущества транспонированной формы для генерации Verilog кода:
- Минимальная критическая задержка — путь от входа до выхода содержит только один сумматор и один элемент задержки, что позволяет достичь более высокой тактовой частоты
- Естественная конвейеризация — структура идеально подходит для конвейерной обработки, увеличивающей пропускную способность
- Параллельное умножение — все операции умножения выполняются одновременно, что упрощает распараллеливание вычислений в аппаратуре
- Упрощённая маршрутизация — в ПЛИС проще реализовать параллельную структуру с одним источником данных
- Лучшая масштабируемость — добавление новых коэффициентов не увеличивает критический путь
name_model = "firT_$(round(Int, rand() * 10000))"
Path = (@__DIR__) * "/" * name_model * ".engee"
println("Path: $Path")
FIXED_POINT_TYPE = "fixdt(1, 16, 14)"
coef = fi(coef, 1, 16, 15)
# СОЗДАЕМ МОДЕЛЬ
engee.create(name_model)
# Создаем транспонированную структуру
engee.add_block("/Basic/Ports & Subsystems/In1", name_model*"/")
engee.add_block("/Basic/Ports & Subsystems/Out1", name_model*"/")
num_coef = length(coef)
# Первый каскад особый - только сумматор
engee.add_block("/Basic/Math Operations/Add", name_model*"/Add-1")
engee.add_block("/Basic/Math Operations/Gain", name_model*"/Gain-1")
engee.set_param!(name_model*"/Gain-1",
"Gain" => coef[1],
"OutDataTypeStr" => "Fixed-point",
"OutDataTypeStrFixed" => FIXED_POINT_TYPE)
# Добавляем фиксированную точку для первого сумматора
engee.set_param!(name_model*"/Add-1",
"OutDataTypeStr" => "Fixed-point",
"OutDataTypeStrFixed" => FIXED_POINT_TYPE)
engee.add_line("In1/1", "Add-1/1")
engee.add_line("In1/1", "Gain-1/1")
engee.add_line("Gain-1/1", "Add-1/2")
# Промежуточные каскады (от 2 до num_coef)
for n in 2:num_coef
name_delay = "Delay-" * string(n-1)
name_add = "Add-" * string(n)
name_gain = "Gain-" * string(n)
engee.add_block("/Basic/Discrete/Unit Delay", name_model*"/"*name_delay)
engee.add_block("/Basic/Math Operations/Add", name_model*"/"*name_add)
engee.add_block("/Basic/Math Operations/Gain", name_model*"/"*name_gain)
engee.set_param!(name_model*"/"*name_gain,
"Gain" => coef[n],
"OutDataTypeStr" => "Fixed-point",
"OutDataTypeStrFixed" => FIXED_POINT_TYPE)
engee.set_param!(name_model*"/"*name_delay, "SampleTime" => st)
# Добавляем фиксированную точку для сумматора
engee.set_param!(name_model*"/"*name_add,
"OutDataTypeStr" => "Fixed-point",
"OutDataTypeStrFixed" => FIXED_POINT_TYPE)
# Соединения
if n == 2
engee.add_line("Add-1/1", name_delay*"/1")
else
prev_add = "Add-" * string(n-1)
engee.add_line(prev_add*"/1", name_delay*"/1")
end
engee.add_line(name_delay*"/1", name_add*"/1")
engee.add_line("In1/1", name_gain*"/1") # Вход идет на ВСЕ коэффициенты
engee.add_line(name_gain*"/1", name_add*"/2")
end
# Выход берем с последнего сумматора
last_add = "Add-" * string(num_coef)
engee.add_line(last_add*"/1", "Out1/1")
engee.save(Path)
model = engee.load(Path, force=true)
Для генерации Verilog кода транспонированная форма предпочтительнее, поскольку она напрямую соответствует высокочастотной аппаратной реализации с конвейерной обработкой, обеспечивая лучшие временные характеристики и более эффективное использование ресурсов ПЛИС при сохранении функциональной эквивалентности с прямой формой.
В ходе работы был реализован процесс интерполяции и децимации для одиночного импульса. Исходный импульс задавался вектором impulse = [1.0; zeros(100)], который затем свертывался с коэффициентами фильтра coef для формирования передаваемого сигнала tx_signal.
На приёмной стороне выполнялась обратная операция — свёртка принятого сигнала с теми же коэффициентами, что давало восстановленный сигнал rx_signal. Визуализация результатов — построение графиков исходного импульса, интерполированного и децимированного сигналов — показала их высокую схожесть, что подтвердило корректность реализации алгоритма.
impulse = [1.0; zeros(100)]
tx_signal = DSP.conv(impulse, coef)
rx_signal = DSP.conv(tx_signal, coef)
plot(impulse)
plot!(tx_signal)
plot!(rx_signal)
Аналогичная процедура была успешно воспроизведена в модели с использованием реализованных нами алгоритмов под валиды, что дополнительно верифицировало правильность подхода. Полученная модель является готовым решением, пригодным для генерации кода на языке Verilog.

# Определяем текущую директорию
current_dir = @__DIR__
# Выводим структуру папок
println("Структура папок в директории: $current_dir")
println()
for (root, dirs, files) in walkdir(current_dir)
for dir in dirs
if startswith(dir, "one_impulse_Nyquist_filter")
println("$dir/")
dir_path = joinpath(root, dir)
for file in readdir(dir_path)
println(" └── $file")
end
println()
end
end
end
Сгенерированный Verilog-код представляет собой аппаратную реализацию передающего (TX) и приёмного (RX) фильтров Найквиста для обработки импульсного сигнала. Основные компоненты:
-
Модули верхнего уровня (
one_impulse_Nyquist_filter_TX/RX.v):- Управляют синхронизацией и валидацией данных
- Содержат конечный автомат для контроля потока данных
- Интегрируют FIR-фильтр и блок задержки
- Оба фильтра используют одинаковые ядра обработки (FIR и Delay), но различаются в управляющей логике и условиях активации.
-
FIR-фильтр (
fir_liner_1.v):- Реализует линейный КИХ-фильтр (41 коэффициент)
- Использует фиксированные коэффициенты для формирования импульсной характеристики
- Обрабатывает 16-битные данные со знаком
-
Блок задержки (
Delay_40.v):- Реализует цепочку из 40 регистров задержки
- Синхронизирует валидный сигнал с задержкой фильтрации
- Обеспечивает временное согласование данных
Вывод
Проведённое моделирование подтвердило корректность реализации обработки импульса. Сходство результатов, полученных в математической среде и аппаратной модели, доказывает правильность работы алгоритма и точность выбранных коэффициентов фильтра.
Успешно сгенерированный аппаратный код представляет собой полноценные передающий и приёмный модули. Они построены на общем ядре фильтра и различаются лишь логикой управления, что обеспечивает энергоэффективность передачи и непрерывность приёма. Код обладает модульностью и готов к синтезу для реализации на целевой цифровой платформе.