
Для тех, кто впервые слышит слово секвенсор в применении к радио, дам небольшое введение. Для работы трансивера нужен приёмо-передатчик и антенна, которые соединены кабелем. Кабель может оказаться довольно длинным, в нём происходят значительные потери полезного сигнала, которые возрастают с частотой сигнала. Поэтому на диапазонах УКВ и выше устанавливают дополнительные усилители вблизи антенны, они позволяют компенсировать потери. Но возникает другая проблема: одна антенна, как правило, используется как для приёма, так и для передачи. Поэтому на момент передачи нужно подключить к ней усилитель мощности (УМ/PA), а на момент приёма подключить малошумящий усилитель (МШУ/LNA). Коммутация производится обычно с помощью мощных реле, время срабатывания которых составляет десятки миллисекунд. Во время коммутации может случиться такая ситуация, когда, например, передатчик трансивера ещё не отключился, а к антенне всё еще подключен МШУ. В этом случае мощный сигнал от передатчика может повредить МШУ.
Выходом из этой ситуации является добавление некоторой задержки между переключением реле приём/передача и включением передатчика. Всю эту работу выполняет устройство под названием секвенсор.
Это довольно простое устройство, которое радиолюбители изготавливают самостоятельно не один десяток лет. Наиболее простые варианты могут быть сделаны просто на паре реле, как показано, например, на этой схеме.

По мере развития технологий радиолюбители делали секвенсоры используя множество методов: используя транзисторы, компараторы, таймеры. Последние разработки всё чаще используют микроконтроллеры. Я же решил пойти дальше и сделать секвенсор на ПЛИС.
Вот типичная схема, которую рекомендует использовать US4ICI для своих МШУ и секвенсоров (https://vhfdesign.com/ru/other/sequencer-pcb.html):

На картинке видна временная диаграмма работы секвенсора, обеспечивающая последовательное включение и выключение устройств при переходе в режим передачи и обратно.
Может возникнуть вопрос: для чего делать свой секвенсор, да еще на ПЛИС, когда можно сделать просто на реле или купить готовую плату?
Во-первых, просто интересно использовать новые технологии для решения повседневных задач, да еще не за дорого, благодаря текущей цене на б/у плату Antminer S9 (300-600 руб.).
Во-вторых, попробую объяснить рационально:
Мне нужен секвенсор для работы через радиолюбительские спутники. Здесь используется дуплексный режим работы на разнесённых частотах. Так, если для приёма используется антенна и МШУ на диапазон 70 см (диапазон U), то на передачу нужна антенна и УМ на диапазон 2м (диапазон V). И всё это работает одновременно, используются так же два фидера. При переключении со спутника на спутник может меняться комбинация используемых спутником диапазонов V/U, U/V, U/U, V/V. Видно, что коммутация в таком случае гораздо сложнее, чем в случае работы в одном диапазоне в полудуплексном режиме.

Из прошлой статьи вы могли узнать, что у меня есть трудности с прокладкой кабелей на крышу. И здесь я проявляю экономию. Так, для каждого диапазона я использую лишь один провод для управления включением МШУ или УМ. Сделано это просто:
Если не подавать по проводу никакого напряжения, то антенна к фидеру подключается напрямую
Если подать по проводу напряжение 12 вольт, то к антенне на крыше подключится МШУ
Если подать по проводу напряжение 36 вольт, то к антенне вместо МШУ подключится УМ. Напряжение 36 вольт относительно безопасно и позволяет уменьшить ток через провода при работе УМ. Сами УМ сделаны на модулях MITSUBISHI и запитываются от DC-DC преобразователей 36в - 13,8в
Я хочу, чтоб секвенсор сам подстраивался под выбранные диапазоны приёма и передачи. Именно для этой цели я модифицировал прошивку LibreSDR, как написал в этой статье (https://habr.com/ru/articles/880926/). Если посмотрите, то увидите, что через UART выводятся из LibreSDR band data для передачи данных о текущих частотах передачи и приёма. Эти данные должны попасть в секвенсор и автоматически определить логику коммутации при смене режимов приёма и передачи.
В моей конструкции трансивера есть несколько схем защиты УМ: от превышения тока, КСВ, мощности. Я сделал схему аппаратной защиты от превышения уставок по этим параметрам. Эта схема формирует логический сигнал FAULT, сообщающий секвенсору об аварийной ситуации. В свою очередь, секвенсор как можно быстрее должен среагировать и отключить всё в попытке предотвратить развитие аварии. Реализовывать эту логику лучше аппаратно, чем программно. Работать будет быстрее и надёжнее. Поэтому тут самое место для использования ПЛИС.
Сам по себе аппаратный секвенсор должен, как мне кажется, обладать большей надёжностью, чем программный. По идее он не должен быть чувствителен к возможным сбоям и ошибкам в управляющей программе.
Как видите, это достаточно оригинальное решение со специфическими требованиями, для него нет готовой схемы коммутации, нужно придумывать что-то своё. Самое то, чтобы попробовать использовать Antminer S9.
Вооружившись учебниками по Verilog начинаем писать код секвенсора. Сначала я написал код для работы секвенсора только под диапазон 2м. Выглядит он как обычный Конечный автомат (FSM). FSM (Finite State Machine) — это математическая модель, используемая для описания поведения систем, которые могут находиться в одном из конечного числа состояний и переходить между ними в ответ на события. Конечные автоматы очень удобны для написания кода на Verilog. Получается ясный код с понятной логикой работы.
Вначале кода определяются входные и выходные сигналы секвенсора:
module seq144 #
(
parameter integer SEQ_DELAY = 1500000
)
(
input clk, // Входной сигнал тактирования
input reset, // Входной сигнал для асинхронный сброс
input ptt, // Входной сигнал включения передачи. 0 активный
output reg lna144, // Выходной сигнал включения LNA
output reg pa144, // Выходной сигнал включения нижнего PA
output reg a144 // Выходной сигнал включения предусилителя, реле, верхнего PA
);
// Возможные состояния FSM
localparam READY_STATE = 4'b0001; // 1
localparam TRANSMIT_START_STATE = 4'b0010; // 2
localparam TRANSMIT_START2_STATE = 4'b0100; // 4
localparam TRANSMIT_STATE = 4'b1000; // 8
reg [3:0] State;
reg [3:0] NextState;
Видно, что нужно предусмотреть сигналы тактирования и сброса автомата в начальное состояние. Тут же определяется перечень возможных состояний автомата.
Дальше описывается то, как будет организована задержка по времени между переключением разных состояний. Задержки организуются путем создания регистра для счетчика тактового сигнала. Ведётся обратный отсчёт. В моем случае тактовая частота 50 МГц и счетчик должен посчитать от 1500000 до 0 перед тем, как перейти в новое состояние. По сигналу reset счётчик сбрасывается.
parameter DELAY_CNT_SIZE = 21;
reg [DELAY_CNT_SIZE - 1 : 0] DCounter;
//обработка счетчика паузы
always @(posedge clk)
begin
if(reset)
begin
DCounter <= 0;
end
else
begin
//счетчик все время идет до 0, кроме
//моментов смены состояния, когда задается величина
//паузы для следующего состояния
//DELAY_SETUP
if(ptt == 1'b0) //включена передача
begin
if ((State == READY_STATE) && (NextState == TRANSMIT_START_STATE))
DCounter <= SEQ_DELAY;
else if((State == TRANSMIT_START_STATE) && (NextState == TRANSMIT_START2_STATE))
DCounter <= SEQ_DELAY;
end
else //выключена передача
begin
if ((State == TRANSMIT_STATE) && (NextState == TRANSMIT_START2_STATE))
DCounter <= SEQ_DELAY;
else if((State == TRANSMIT_START2_STATE) && (NextState == TRANSMIT_START_STATE))
DCounter <= SEQ_DELAY;
end
//----------------------------------------------------------
if(DCounter != 0)
begin
DCounter <= DCounter - 1'b1;
end
end
end
Машина состояний разделена на две части. Первая часть отвечает за формирование выходных сигналов на основании состояния FSM:
always @(posedge clk)
begin
if(reset)
begin
lna144 <= 1'b1;
a144 <= 1'b0;
pa144 <= 1'b0;
end
else
begin
//чтобы значение выхода изменялось вместе с изменением
//состояния, а не на следующем такте, анализируем NextState
case(NextState)
READY_STATE:
begin
lna144 <= 1'b1;
a144 <= 1'b0;
pa144 <= 1'b0;
end
TRANSMIT_START_STATE:
begin
lna144 <= 1'b0;
a144 <= 1'b0;
pa144 <= 1'b0;
end
TRANSMIT_START2_STATE:
begin
lna144 <= 1'b0;
a144 <= 1'b1;
pa144 <= 1'b0;
end
TRANSMIT_STATE:
begin
lna144 <= 1'b0;
a144 <= 1'b1;
pa144 <= 1'b1;
end
endcase
end
end
Вторая часть отвечает за выполнение переходов между состояниями:
always @(*)
begin
//по умолчанию сохраняем текущее состояние
NextState = State;
case(State)
//--------------------------------------
READY_STATE:
begin
if(ptt == 1'b0) //включена передача
begin
NextState = TRANSMIT_START_STATE;
end
end
//--------------------------------------
TRANSMIT_START_STATE:
begin
if ((DCounter == 0) && (ptt == 1'b0))
begin
NextState = TRANSMIT_START2_STATE;
end
else if ((DCounter == 0) && (ptt == 1'b1))
begin
NextState = READY_STATE;
end
end
//--------------------------------------
TRANSMIT_START2_STATE:
begin
if ((DCounter == 0) && (ptt == 1'b0))
begin
NextState = TRANSMIT_STATE;
end
else if ((DCounter == 0) && (ptt == 1'b1))
begin
NextState = TRANSMIT_START_STATE;
end
end
//--------------------------------------
TRANSMIT_STATE:
begin
if(ptt == 1'b1) //выключена передача
begin
NextState = TRANSMIT_START2_STATE;
end
end
//--------------------------------------
default: NextState = READY_STATE;
endcase
end
Всё это хорошо выглядит в готовом виде, но заработало оно далеко не с первого раза. Поэтому считаю необходимым написать здесь об обязательной необходимости прогона работы схемы в симуляторе. Не стоит пренебрегать написанием тестов для кода схемотехники.
Для симуляции в Vivado есть отдельный раздел, как очевидно, с названием SIMULATION. В разделе «источники» есть папка с названием Simulation sources. Для выполнения симуляции необходимо написать файл так называемого test bench. Он пишется на том же Verilog, но с дополнительными синтаксическими конструкциями для целей тестирования. Для моего секвенсора я написал такой тест:
module seq144_tb;
//Inputs in the module seq144. Need to use register type
reg clk = 0;
reg rst = 0;
reg ptt = 1;
//Outputs in the module seq144. Need to use net type
wire lna144;
wire pa144;
wire a144;
// Instantiate the Unit Under Test (UUT) for module
seq144 uut (
.clk(clk),
.reset(rst),
.ptt(ptt),
.lna144(lna144),
.pa144(pa144),
.a144(a144)
);
// Generate the continuous clock signal. Wait for 10ns. Period is 20ns
always #10 clk = ~clk;
initial
begin
rst = 1;
#40
rst = 0;
#40
ptt = 0;
#200
ptt = 1;
#180
ptt = 0;
#500
$finish();
end
endmodule
Можно видеть, что в тесте инициализируется модуль секвенсора, тактовый сигнал, а в ходе теста меняются значения входных сигналов через промежутки времени в наносекундах (#40 это ожидание 40 нс). Завершается тест по инструкции $finish().
Это только код теста. Для его запуска нужно сделать модуль верхним в списке тестов, т.е. выполнить в меню Vivado команду Set as top. В этом случае при запуске симуляции выполнение теста начнется с этого модуля.
Далее запускаем симуляцию и получаем некоторый результат, например:

Видно, как со временем меняются логические уровни сигналов при работе модуля секвенсора. Внимательно смотрим и меняем свой код в случае нарушения логики.
Можно отлаживаться не в симуляторе, а в уже готовой системе. Тогда нужно добавить в дизайн модуль ILA. Его описание выходит за рамки статьи.
Модуль секвенсора написан, но я хотел, чтоб он автоматически настраивался в зависимости от выбранных диапазонов работы трансивера. Это можно сделать разными способами, но я выбрал настройку через запись управляющей программой в оперативную память конфигурационных настроек секвенсора.
Для организации такого режима работы в Vivado есть довольно сложный путь. Необходимо создать свой IP блок с интерфейсом к шине AXI4. Vivado в этом случае обеспечит автоматическую настройку подключения к шине, предоставит для работы блока адрес в общем адресном пространстве системы. По этому адресу из прикладной программы нужно будет записывать конфигурацию секвенсора.

В меню Vivado есть пункт Tools->Create and package new IP. При вызове появляется визард, где нужно выбрать Create new AXI4 peripherial. Далее визард сгенерирует весь необходимый код обертки, необходимый для соединения блока с шиной AXI. В коде будут указаны места, где пользователь должен добавить свою функциональность. У меня получилось так:

Пришлось пройти этот путь, добавить в код нового IP модуля вызов секвенсора и связать регистры настройки секвенсора с регистрами шины AXI.
Подсказки в шаблоне кода понятны. Ясно как пользоваться регистрами, которые записывает управляющая программа. А вот как записать регистр на шине из своего Verilog кода не очень понятно. По крайней мере шаблон не предусматривает такого использования, но я сделал так чтоб во второй регистр писалось состояние секвенсора:
assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
always @(*)
begin
// Address decoding for reading registers
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
2'h0 : reg_data_out <= slv_reg0;
2'h1 : reg_data_out <= { 21'b0, lna144, a144, pa144, lna430, a430, pa430, slv_reg0[2:0], hard_fault, ptt};
//2'h1 : reg_data_out <= slv_reg1;
2'h2 : reg_data_out <= slv_reg2;
2'h3 : reg_data_out <= slv_reg3;
default : reg_data_out <= 0;
endcase
end
Готовый блок встраиваем в общую схему секвенсора. Целиком схема выглядит так:

Видно, как блок секвенсора встроен в схему. Так же видна жесткая логика обработки сигнала аварии FAULT.
Несколько слов о доступе к памяти и шине AXI из управляющей программы. Вообще ОС linux блокирует пользователям прямой доступ к памяти. Правильное проектирование подразумевает написание модуля ядра, из которого такой доступ возможен. Но я не хотел писать модуль ядра. Для таких ленивых как я, есть обходная возможность. Можно так сконфигурировать linux, что появится устройство /dev/mem. В конфиге сборки ядра нужно написать:
CONFIG_DEVMEM=y
При наличии прав root с помощью этого устройства можно напрямую читать память и писать в неё. Утилиты devmem, devmem2 позволяют обращаться к памяти из командной строки linux, что удобно для отладки.
Для работы с такой памятью в Rust есть крейт ddevmem. Пример его использования для чтения и записи в память:
use ddevmem::{register_map, DevMem};
register_map! {
pub unsafe map MyRegisterMap {
0x00 => rw reg0: u32,
0x04 => ro reg1: u32,
0x08 => wo reg2: u32
}
}
let devmem = unsafe { DevMem::new(0xD0DE_0000, None).unwrap() };
let mut reg_map = unsafe { MyRegisterMap::new(std::sync::Arc::new(devmem)).unwrap() };
let (reg0_address, reg0_offset) = (reg_map.reg0_address(), reg_map.reg0_offset());
let reg1_value = *reg_map.reg1();
*reg_map.reg2_mut() = reg1_value;
Управляющая программа моего секвенсора написана на Rust, она получает от LibreSDR по UART данные об используемых в данный момент времени диапазонах приёма и передачи, затем записывает конфигурацию с помощью devmem в регистры секвенсора.
Отступление о багах. Для сборки проекта как обычно нужно сгенерировать bitstream и создать FSBL. На этом этапе после создания своего IP блока я столкнулся с проблемой. Vivado генерирует файл описания XSA, на основании которого создаётся FSBL. Файл этот просто ZIP архив, если заглянуть внутрь, то там будут, например, makefile для сборки загрузочного кода Zynq. Так вот, автоматически созданный проект FSBL отказался компилировать автоматически же созданный Vivado код. Проблема оказалась в сгенерированном для сборке IP makefile. Пришлось его заменить на аналогичный файл из IP блока uartlite, после чего FSBL собрался без проблем. Возможно это просто баг в Vitis 2022 и в новых версиях таких проблем нет.
Дополнительно всю информацию по состоянию системы я решил выводить на SPI LCD экранчик. Когда появился экран, захотелось выводить на него еще информацию о мощности передачи и КСВ.

Добавил в систему АЦП модуль ADS1115. Это превратило секвенсор из утилитарного функционального блока в центр управления трансивером, мониторинга его работы.
В ходе использования Antminer S9 выяснилась одна неприятная особенность. Пока система не загрузилась на выходах ПЛИС могут быть неопределенные уровни сигнала. По этой причине подключенные к ПЛИС реле при включении системы могли случайным образом включаться, что было недопустимым поведением. Пришлось добавить в конструкцию блок, который бы подавал питание на блок коммутации реле только после полной загрузки системы и запуска управляющей программы.
С этой целью я использовал модуль ЦАП mcp4725.

При включении питания у него на выходе гарантируется 0 вольт. Я подключил выход ЦАП к ключевому транзистору подачи питания на блок коммутации. При старте управляющей программы, она по шине I2C устанавливает выход ЦАП в 5 В и это включает питание системы. Таким образом неопределенное поведение при запуске платы было устранено.
На этом заканчиваю описание секвенсора на Antminer S9. Сейчас я его использую в составе спутникового трансивера. Его работу проиллюстрирую вот такой фотографией.

HDL, скрипты сборки linux и исходники управляющей программы на Rust можно посмотреть здесь https://github.com/lesha108/sequencer.git
Надеюсь, что этими статьями я заинтересовал кого-то из радиолюбителей попробовать поэкспериментировать с платами Antminer S9. Делитесь своими идеями в комментариях, а лучше пишите статьи с описанием проектов на хабре!