per-point switching: FPGA and LibreVNA MCU changes

FPGA мастера: порт излучает не в одной стадии, а в блоке подряд идущих —
младшие SUBSTAGE_BITS бит счётчика стадий стали положением выходного ключа,
старшие выбирают порт. Вынесено в функцию port_excites в Sweep.vhd, новое поле
приезжает из свободных битов 8:6 регистра SweepSetup через SPIConfig и top.vhd.

Совместимость со стоком строгая: при SUBSTAGE_BITS=0 сдвиг нулевой, а
добавленная проверка "порт-стадия выше последней" ничего не меняет, потому что
счётчик стадий и так пробегает только 0..STAGES. Ручной режим и анализатор
спектра, где STAGES=0, ведут себя ровно как раньше.

MCU: у SetupSweep добавлен параметр substage_bits со значением по умолчанию 0,
поэтому вызовы из Manual.cpp и SpectrumAnalyzer.cpp не тронуты. VNA.cpp берёт
ширину под-стадии из port3Stage — порта 3 у этого железа нет, поле свободно, и
формат пакета не меняется.

Проверено: GHDL анализирует все три VHDL-файла чисто; новый самопроверяющийся
Test_Sweep.vhd прогоняет четыре сценария (сток, два положения, приёмник без
излучения, четыре положения) и проверен мутацией — с возвращённым стоковым
сравнением сценарии с под-стадиями падают. Обе сборки MCU собираются без ошибок.
Битовые позиции сверены сквозь всю цепочку: хост шлёт port3Stage=1 в слове
0x243, MCU пишет регистр 0x7041, FPGA читает SUBSTAGE_BITS=1.

Слейвы перепрошивать не нужно: им шлётся порт-стадия 7, которая со счётчиком
0..3 не совпадает и в стоковом сравнении.

Исходники теперь опережают лежащие рядом бинарники — предупреждение добавлено в
начало reproduce/README.md, хеш README в SHA256SUMS обновлён. Порядок ввода в
строй расписан в per_point_switching.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ayzen
2026-09-09 22:06:23 +03:00
co-authored by Claude Opus 5
parent 0db3dbe295
commit 7d1879ad79
10 changed files with 504 additions and 51 deletions
@@ -56,6 +56,9 @@ entity SPICommands is
SYNC_MASTER : out STD_LOGIC;
PORT1_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
-- How many LOW bits of the stage counter select the stimulus switch
-- position instead of the excited port. Zero is the stock layout.
SUBSTAGE_BITS : out STD_LOGIC_VECTOR (2 downto 0);
PORT1_EN : out STD_LOGIC;
PORT2_EN : out STD_LOGIC;
REF_EN : out STD_LOGIC;
@@ -288,6 +291,7 @@ begin
when 5 => ADC_PHASEINC <= spi_buf_out(11 downto 0);
when 6 => STAGES <= spi_buf_out(15 downto 13);
SYNC_ENABLED <= spi_buf_out(12);
SUBSTAGE_BITS <= spi_buf_out(8 downto 6);
PORT1_STAGE <= spi_buf_out(5 downto 3);
PORT2_STAGE <= spi_buf_out(2 downto 0);
when 7 => SPI_OVERWRITE_ENABLED <= spi_buf_out(15);
@@ -77,7 +77,11 @@ entity Sweep is
STAGES : in STD_LOGIC_VECTOR (2 downto 0);
PORT1_STAGE : in STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : in STD_LOGIC_VECTOR (2 downto 0);
-- Number of LOW bits of the stage counter that select the stimulus
-- switch position rather than the excited port. Zero restores the
-- stock behaviour of one stage per port.
SUBSTAGE_BITS : in STD_LOGIC_VECTOR (2 downto 0);
PORT1_ACTIVE : out STD_LOGIC;
PORT2_ACTIVE : out STD_LOGIC;
@@ -97,6 +101,28 @@ architecture Behavioral of Sweep is
signal stage_cnt : unsigned (2 downto 0);
signal config_reg : std_logic_vector(95 downto 0);
signal source_active : std_logic;
-- A port excites during a BLOCK of consecutive stages instead of a single one:
-- the low SUBSTAGE_BITS bits of the stage counter are the stimulus switch
-- position and the bits above them select the port. With SUBSTAGE_BITS = 0 this
-- reduces to the stock exact comparison, one stage per port.
--
-- A port number beyond the last port stage can never match, which is how the
-- host silences a receive-only device (it sends 7). The stock "one past the last
-- stage" sentinel cannot be used once the port comes from the upper bits: with
-- four stages those bits only ever hold 0 or 1, leaving no free value.
function port_excites(
stage : unsigned(2 downto 0);
port_stage : std_logic_vector(2 downto 0);
substage_width : std_logic_vector(2 downto 0);
last_stage : std_logic_vector(2 downto 0)
) return boolean is
begin
if unsigned(port_stage) > shift_right(unsigned(last_stage), to_integer(unsigned(substage_width))) then
return false;
end if;
return shift_right(stage, to_integer(unsigned(substage_width))) = unsigned(port_stage);
end function;
begin
CONFIG_ADDRESS <= std_logic_vector(point_cnt);
@@ -195,13 +221,13 @@ begin
when Settling =>
NEW_DATA <= '0';
source_active <= '0';
if std_logic_vector(stage_cnt) = PORT1_STAGE then
if port_excites(stage_cnt, PORT1_STAGE, SUBSTAGE_BITS, STAGES) then
PORT1_ACTIVE <= '1';
source_active <= '1';
else
PORT1_ACTIVE <= '0';
end if;
if std_logic_vector(stage_cnt) = PORT2_STAGE then
if port_excites(stage_cnt, PORT2_STAGE, SUBSTAGE_BITS, STAGES) then
PORT2_ACTIVE <= '1';
source_active <= '1';
else
@@ -0,0 +1,278 @@
----------------------------------------------------------------------------------
-- Self-checking testbench for the stage/port gating in Sweep.
--
-- Covers the per-point stimulus switching change (see
-- device_firmware/per_point_switching.md): with SUBSTAGE_BITS > 0 a port must
-- excite across a BLOCK of consecutive stages instead of a single one, while
-- SUBSTAGE_BITS = 0 must behave exactly like the stock design.
--
-- Runs without a trigger loop (SYNC_ENABLED = '0'), so the sweep advances on its
-- own and every stage is observed at the moment sampling starts.
--
-- ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd
-- ghdl -r --std=93 Test_Sweep --assert-level=error
----------------------------------------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity Test_Sweep is
end Test_Sweep;
architecture Behavioral of Test_Sweep is
constant CLK_PERIOD : time := 10 ns;
signal clk : std_logic := '0';
signal reset : std_logic := '1';
signal simulation_done : boolean := false;
-- Sweep configuration driven per scenario.
signal npoints : std_logic_vector(12 downto 0) := (others => '0');
signal stages : std_logic_vector(2 downto 0) := "001";
signal port1_stage : std_logic_vector(2 downto 0) := "000";
signal port2_stage : std_logic_vector(2 downto 0) := "001";
signal substage_bits : std_logic_vector(2 downto 0) := "000";
-- DUT interface.
signal config_data : std_logic_vector(95 downto 0);
signal sampling_busy : std_logic := '0';
signal start_sampling : std_logic;
signal reload_pll_regs : std_logic;
signal pll_reload_done : std_logic := '1';
signal port1_active : std_logic;
signal port2_active : std_logic;
signal source_ce : std_logic;
-- Expected (port1, port2) excitation per stage, indexed by stage number.
type excitation is record
port1 : std_logic;
port2 : std_logic;
end record;
type excitation_list is array (natural range <>) of excitation;
-- Observation bookkeeping, shared between the checker and the stimulus.
signal observed_stages : natural := 0;
signal failures : natural := 0;
signal expected_port1 : std_logic_vector(7 downto 0) := (others => '0');
signal expected_port2 : std_logic_vector(7 downto 0) := (others => '0');
signal stages_per_point : positive := 2;
signal scenario_name : string(1 to 24) := (others => ' ');
-- config_reg(95) = 0 keeps the sweep from halting; bits 92..90 = "001" picks the
-- shortest sample count so the simulation stays fast. Everything else is unused
-- by the state machine under test.
function make_config return std_logic_vector is
variable value : std_logic_vector(95 downto 0) := (others => '0');
begin
value(90) := '1';
return value;
end function;
begin
config_data <= make_config;
clk <= not clk after CLK_PERIOD / 2 when not simulation_done else '0';
dut : entity work.Sweep
port map (
CLK => clk,
RESET => reset,
NPOINTS => npoints,
CONFIG_ADDRESS => open,
CONFIG_DATA => config_data,
USER_NSAMPLES => std_logic_vector(to_unsigned(6, 13)),
NSAMPLES => open,
SETTLING_TIME => std_logic_vector(to_unsigned(3, 20)),
SAMPLING_BUSY => sampling_busy,
SAMPLING_DONE => '0',
START_SAMPLING => start_sampling,
BAND_SELECT => open,
MAX2871_DEF_4 => (others => '0'),
MAX2871_DEF_3 => (others => '0'),
MAX2871_DEF_1 => (others => '0'),
MAX2871_DEF_0 => (others => '0'),
SOURCE_REG_4 => open,
SOURCE_REG_3 => open,
SOURCE_REG_1 => open,
SOURCE_REG_0 => open,
LO_REG_4 => open,
LO_REG_3 => open,
LO_REG_1 => open,
LO_REG_0 => open,
RELOAD_PLL_REGS => reload_pll_regs,
PLL_RELOAD_DONE => pll_reload_done,
PLL_LOCKED => '1',
SWEEP_HALTED => open,
SWEEP_RESUME => '0',
SYNC_ENABLED => '0',
SYNC_MASTER => '0',
TRIGGER_IN => '0',
TRIGGER_OUT => open,
NEW_DATA => open,
ATTENUATOR => open,
SOURCE_FILTER => open,
STAGES => stages,
PORT1_STAGE => port1_stage,
PORT2_STAGE => port2_stage,
SUBSTAGE_BITS => substage_bits,
PORT1_ACTIVE => port1_active,
PORT2_ACTIVE => port2_active,
SOURCE_CE => source_ce,
DEBUG_STATUS => open,
RESULT_INDEX => open
);
-- Minimal ADC model: busy for a few cycles once sampling is requested.
sampling_model : process(clk)
variable countdown : natural := 0;
begin
if rising_edge(clk) then
if start_sampling = '1' and countdown = 0 then
countdown := 4;
sampling_busy <= '1';
elsif countdown > 0 then
countdown := countdown - 1;
if countdown = 0 then
sampling_busy <= '0';
end if;
end if;
end if;
end process;
-- Minimal PLL model: the reload takes a few cycles, then reports done.
pll_model : process(clk)
variable countdown : natural := 0;
begin
if rising_edge(clk) then
if reload_pll_regs = '1' then
pll_reload_done <= '0';
countdown := 3;
elsif countdown > 0 then
countdown := countdown - 1;
if countdown = 0 then
pll_reload_done <= '1';
end if;
end if;
end if;
end process;
-- Checks the excitation pattern at the instant each stage starts sampling.
-- The stage number is not read back from the DUT on purpose: counting sampling
-- starts independently is what makes a wrong stage order visible.
checker : process(clk)
variable stage_index : natural;
variable was_sampling : std_logic := '0';
begin
if rising_edge(clk) then
if reset = '1' then
was_sampling := '0';
-- Owned solely by this process: a second driver from the stimulus
-- would make the signal unresolved.
observed_stages <= 0;
else
if start_sampling = '1' and was_sampling = '0' then
stage_index := observed_stages mod stages_per_point;
assert port1_active = expected_port1(stage_index)
report scenario_name & ": stage " & integer'image(stage_index)
& " port1_active=" & std_logic'image(port1_active)
& ", expected " & std_logic'image(expected_port1(stage_index))
severity error;
assert port2_active = expected_port2(stage_index)
report scenario_name & ": stage " & integer'image(stage_index)
& " port2_active=" & std_logic'image(port2_active)
& ", expected " & std_logic'image(expected_port2(stage_index))
severity error;
-- The source must be powered exactly when some port is excited.
assert source_ce = (port1_active or port2_active)
report scenario_name & ": stage " & integer'image(stage_index)
& " source_ce disagrees with the active ports"
severity error;
if port1_active /= expected_port1(stage_index)
or port2_active /= expected_port2(stage_index)
or source_ce /= (port1_active or port2_active) then
failures <= failures + 1;
end if;
observed_stages <= observed_stages + 1;
end if;
was_sampling := start_sampling;
end if;
end if;
end process;
stimulus : process
-- Applies one scenario and waits until the requested number of stages has
-- been observed, so the test fails loudly on a stalled state machine.
procedure run_scenario(
name : string;
cfg_npoints : natural;
cfg_stages : natural;
cfg_port1 : natural;
cfg_port2 : natural;
cfg_substage_bits : natural;
pattern : excitation_list;
stages_to_observe : positive
) is
variable deadline : natural := 0;
begin
reset <= '1';
scenario_name <= (others => ' ');
scenario_name(1 to name'length) <= name;
npoints <= std_logic_vector(to_unsigned(cfg_npoints, 13));
stages <= std_logic_vector(to_unsigned(cfg_stages, 3));
port1_stage <= std_logic_vector(to_unsigned(cfg_port1, 3));
port2_stage <= std_logic_vector(to_unsigned(cfg_port2, 3));
substage_bits <= std_logic_vector(to_unsigned(cfg_substage_bits, 3));
stages_per_point <= pattern'length;
expected_port1 <= (others => '0');
expected_port2 <= (others => '0');
for index in pattern'range loop
expected_port1(index) <= pattern(index).port1;
expected_port2(index) <= pattern(index).port2;
end loop;
-- Held long enough for the checker to zero its own stage counter.
wait for CLK_PERIOD * 4;
reset <= '0';
while observed_stages < stages_to_observe loop
wait for CLK_PERIOD;
deadline := deadline + 1;
assert deadline < 20000
report name & ": sweep stalled, only " & integer'image(observed_stages)
& " stage(s) observed"
severity failure;
end loop;
report name & ": " & integer'image(observed_stages) & " stage(s) checked";
end procedure;
begin
-- Stock layout: one stage per port, must be untouched by the change.
run_scenario(
"stock, no substages", 1, 1, 0, 1, 0,
(0 => ('1', '0'), 1 => ('0', '1')),
4); -- two points, so the stage counter is seen wrapping
-- Per-point switching: port 1 excites in stages 0..1, port 2 in stages 2..3.
run_scenario(
"two switch positions", 0, 3, 0, 1, 1,
(0 => ('1', '0'), 1 => ('1', '0'), 2 => ('0', '1'), 3 => ('0', '1')),
4);
-- Receive-only device: the host parks both ports at 7, nothing may excite.
run_scenario(
"receiver only", 0, 3, 7, 7, 1,
(0 => ('0', '0'), 1 => ('0', '0'), 2 => ('0', '0'), 3 => ('0', '0')),
4);
-- Four switch positions on one port: stages 0..3 all excite port 1.
run_scenario(
"four switch positions", 0, 7, 0, 1, 2,
(0 => ('1', '0'), 1 => ('1', '0'), 2 => ('1', '0'), 3 => ('1', '0'),
4 => ('0', '1'), 5 => ('0', '1'), 6 => ('0', '1'), 7 => ('0', '1')),
8);
assert failures = 0
report integer'image(failures) & " check(s) failed"
severity failure;
report "Test_Sweep: all scenarios passed" severity note;
simulation_done <= true;
wait;
end process;
end Behavioral;
@@ -150,6 +150,7 @@ architecture Behavioral of top is
STAGES : in STD_LOGIC_VECTOR (2 downto 0);
PORT1_STAGE : in STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : in STD_LOGIC_VECTOR (2 downto 0);
SUBSTAGE_BITS : in STD_LOGIC_VECTOR (2 downto 0);
PORT1_ACTIVE : out STD_LOGIC;
PORT2_ACTIVE : out STD_LOGIC;
@@ -262,6 +263,7 @@ architecture Behavioral of top is
SYNC_MASTER : out STD_LOGIC;
PORT1_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
SUBSTAGE_BITS : out STD_LOGIC_VECTOR (2 downto 0);
PORT1_EN : out STD_LOGIC;
PORT2_EN : out STD_LOGIC;
REF_EN : out STD_LOGIC;
@@ -386,6 +388,7 @@ architecture Behavioral of top is
signal sweep_sync_master : STD_LOGIC;
signal sweep_port1_stage : STD_LOGIC_VECTOR (2 downto 0);
signal sweep_port2_stage : STD_LOGIC_VECTOR (2 downto 0);
signal sweep_substage_bits : STD_LOGIC_VECTOR (2 downto 0);
signal sweep_config_data : std_logic_vector(95 downto 0);
signal sweep_config_address : std_logic_vector(12 downto 0);
signal sweep_source_filter : std_logic_vector(1 downto 0);
@@ -731,6 +734,7 @@ begin
STAGES => sweep_stages,
PORT1_STAGE => sweep_port1_stage,
PORT2_STAGE => sweep_port2_stage,
SUBSTAGE_BITS => sweep_substage_bits,
PORT1_ACTIVE => sweep_excite_port1,
PORT2_ACTIVE => sweep_excite_port2,
@@ -815,6 +819,7 @@ begin
SYNC_MASTER => sweep_sync_master,
PORT1_STAGE => sweep_port1_stage,
PORT2_STAGE => sweep_port2_stage,
SUBSTAGE_BITS => sweep_substage_bits,
SPI_OVERWRITE_ENABLED => HW_overwrite_enabled,
SPI_OVERWRITE_DATA => HW_overwrite_data,
DFT_BIN1_PHASEINC => dft_bin1_phaseinc,
@@ -143,12 +143,14 @@ void FPGA::SetSettlingTime(uint16_t us) {
WriteRegister(Reg::SettlingTimeHigh, value >> 16);
}
void FPGA::SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize, bool syncMaster) {
void FPGA::SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize, bool syncMaster, uint8_t substage_bits) {
uint16_t value = 0x0000;
value |= (uint16_t) (stages & 0x07) << 13;
if(synchronize) {
value |= 0x1000;
}
// Bits 11..9 of this register are still unused.
value |= (uint16_t) (substage_bits & 0x07) << 6;
value |= (port1_stage & 0x07) << 3;
value |= (port2_stage & 0x07) << 0;
WriteRegister(Reg::SweepSetup, value);
@@ -110,7 +110,11 @@ void WriteRegister(FPGA::Reg reg, uint16_t value);
void SetNumberOfPoints(uint16_t npoints);
void SetSamplesPerPoint(uint32_t nsamples);
void SetSettlingTime(uint16_t us);
void SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize = false, bool syncMaster = false);
// substage_bits is how many LOW bits of the stage counter select the stimulus
// switch position instead of the excited port, letting a port excite across a
// block of consecutive stages (per-point stimulus switching). Zero, the default,
// is the stock layout of exactly one stage per port.
void SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize = false, bool syncMaster = false, uint8_t substage_bits = 0);
void Enable(Periphery p, bool enable = true);
void Disable(Periphery p);
bool IsEnabled(Periphery p);
@@ -338,7 +338,11 @@ bool VNA::Setup(Protocol::SweepSettings s) {
FPGA::Enable(FPGA::Periphery::LO1Chip);
FPGA::Enable(FPGA::Periphery::LO1RF);
#endif
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.syncMode != 0, s.syncMaster);
// port3Stage carries the stimulus switch substage width: this hardware has no
// port 3, so the field is free, and reusing it keeps the packet layout intact.
// A host that does not know about per-point switching leaves it at zero, which
// is the stock one-stage-per-port layout.
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.syncMode != 0, s.syncMaster, s.port3Stage);
Trigger::SetMode((Trigger::Mode) s.syncMode);
FPGA::Enable(FPGA::Periphery::PortSwitch);
pointCnt = 0;
@@ -3,6 +3,21 @@
Здесь всё, что нужно, чтобы пересобрать прошивки из [`device_firmware/`](../) с нуля.
Проект лежит целиком — не патч, а полное дерево с уже внесёнными доработками.
> **Исходники опережают бинарники.** В дерево внесено переключение стимула
> внутри частотной точки (см. [`../../per_point_switching.md`](../../per_point_switching.md)),
> а лежащие рядом `.vnafw`, `extracted/` и `build*/` — это ПРЕДЫДУЩАЯ прошивка,
> собранная до этой доработки и сейчас залитая в приборы. Поэтому:
>
> * `./build.sh` без аргументов по-прежнему даёт побайтовое совпадение — он
> только переупаковывает готовые `.bin`, не трогая исходники;
> * `./build.sh --stm32` и любая сборка FPGA дадут **другие** бинарники, уже с
> новой логикой — это ожидаемо, а не поломка;
> * исходники, из которых собраны текущие `.vnafw`, лежат в git-коммите
> `d2338ff`.
>
> После сборки и проверки на железе обновите `.vnafw`, `extracted/`, `build*/`
> и `SHA256SUMS`, и этот блок можно будет убрать.
## Что где
```
@@ -70,7 +85,11 @@ tail -c +341737 combined.vnafw | head -c 85004 > VNA_embedded.bin # STM32
## Отличия от апстрима v1.6.5
Всего 12 файлов. Всё остальное дерево совпадает с апстримом байт в байт.
Аппаратный триггер и external-LO slave — 13 файлов (не 12: в списке не значился
`PC_Application/LibreVNA-GUI/Traces/trace.cpp`, правка в GUI, к прошивкам
отношения не имеющая). Переключение стимула внутри точки добавило ещё 5
изменённых файлов и один новый. Всё остальное дерево совпадает с апстримом байт
в байт.
### STM32 — поддержка аппаратного триггера (это и есть съём без USB)
@@ -117,6 +136,39 @@ tail -c +341737 combined.vnafw | head -c 85004 > VNA_embedded.bin # STM32
Новые файлы: `BuildConfig.vhd`, `BuildConfig_external_lo_slave.vhd`,
`VNA_external_lo_slave.xise`; в `VNA.xise` добавлена ссылка на `BuildConfig.vhd`.
### Переключение стимула внутри частотной точки
Внесено позже остального, бинарников рядом ещё нет (см. блок в начале файла).
Полное описание — [`../../per_point_switching.md`](../../per_point_switching.md).
**`FPGA/VNA/Sweep.vhd`** — порт излучает не в одной стадии, а в БЛОКЕ подряд
идущих: младшие `SUBSTAGE_BITS` бит счётчика стадий стали положением выходного
ключа, старшие выбирают порт. Вынесено в функцию `port_excites`. Значение
порт-стадии выше последней никогда не совпадает — так хост глушит излучение
слейвов, посылая им 7.
**`FPGA/VNA/SPIConfig.vhd`**, **`FPGA/VNA/top.vhd`** — новый вход `SUBSTAGE_BITS`
из свободных битов 8:6 регистра `SweepSetup` (адрес 6) и его проброс в `Sweep`.
**`Application/Drivers/FPGA/FPGA.{hpp,cpp}`** — у `SetupSweep` добавлен параметр
`substage_bits` со значением по умолчанию 0, чтобы вызовы из ручного режима и
анализатора спектра не менялись.
**`Application/VNA.cpp`** — ширина под-стадии берётся из `port3Stage`: у этого
железа нет порта 3, поле свободно, и его переиспользование не меняет формат
пакета.
**При `SUBSTAGE_BITS = 0` логика бит в бит совпадает с апстримом** — это
проверено симуляцией.
Новый файл `FPGA/VNA/Test_Sweep.vhd` — самопроверяющийся тестбенч на четыре
сценария (сток, два положения ключа, приёмник без излучения, четыре положения).
В проект ISE не добавлен, на сборку не влияет. Запуск любым VHDL-симулятором:
```bash
ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd && ghdl -r --std=93 Test_Sweep
```
### Прочее
**`AssembleFirmware.py`** — переписан: добавлены `--bitstream`, `--mcu`, `--output`
@@ -15,5 +15,5 @@ c0e69bbcadd9fac20de10acbe1875ceba39cc7ce35a7c64ed54f44150560ef75 *LibreVNA/FPGA/
ba729999be7849187704cf4ccbc4ae4a8877b0860eee1b44225c0585537bb091 *LibreVNA/Software/VNA_embedded/build/VNA_embedded.bin
56654a0344556d24ecca10f119e0226b8297a2bb89d1aa4217280854e9ffa7bd *LibreVNA/Software/VNA_embedded/build_external_lo_slave/VNA_embedded.bin
# комплект
993e3809f3935b8ca8100d1b3910eea0382c298bd085a656b64d641ee0971108 *README.md
a3c88be8abbff988650d7fe20f4a93745d46c347c9d0936aa5b43a1ff29dc70b *README.md
be686c2000094acb7703727a3eb9249f7ae8a3517dc7d8630fe97f394aadb849 *build.sh
+121 -43
View File
@@ -56,49 +56,39 @@
## FPGA мастера
Сделано в [`multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/`](multi_libreVNA_firmware/reproduce/LibreVNA/FPGA/VNA/),
осталось собрать битстрим в ISE.
### `Sweep.vhd`
Сейчас порт включается по точному совпадению, поэтому излучает ровно в одной
стадии, и во второй под-стадии зондирующего сигнала просто не будет:
Порт включался по точному совпадению, поэтому излучал ровно в одной стадии, и во
второй под-стадии зондирующего сигнала просто не было. Сравнение вынесено в
функцию `port_excites`: младшие `SUBSTAGE_BITS` бит счётчика стадий — положение
ключа, старшие — номер порта. Сдвиг на переменное число бит синтезируется в
трёхбитный barrel shifter.
```vhdl
if std_logic_vector(stage_cnt) = PORT1_STAGE then
```
Меняется на сравнение по старшим битам плюс явное «порт выключен». Сдвиг на
переменное число бит синтезируется в трёхбитный barrel shifter:
```vhdl
-- в entity
SUBSTAGE_BITS : in STD_LOGIC_VECTOR (2 downto 0);
-- в architecture
signal substage_shift : natural range 0 to 7;
substage_shift <= to_integer(unsigned(SUBSTAGE_BITS));
-- в состоянии Settling, вместо каждой из двух проверок:
if unsigned(PORT1_STAGE) <= unsigned(STAGES)
and shift_right(stage_cnt, substage_shift) = unsigned(PORT1_STAGE) then
PORT1_ACTIVE <= '1';
source_active <= '1';
else
PORT1_ACTIVE <= '0';
if unsigned(port_stage) > shift_right(unsigned(last_stage), to_integer(unsigned(substage_width))) then
return false;
end if;
return shift_right(stage, to_integer(unsigned(substage_width))) = unsigned(port_stage);
```
Проверка совместимости:
Совместимость со стоком **строгая, а не приблизительная**. При `SUBSTAGE_BITS = 0`
сдвиг нулевой и условие вырождается в `stage_cnt = port_stage` плюс проверка
`port_stage <= STAGES`. Эта проверка не может ничего изменить: счётчик стадий
пробегает только `0..STAGES`, поэтому при `port_stage > STAGES` совпадения не было
бы и так. То есть для всех существующих вызовов поведение идентично — включая
ручной режим и анализатор спектра, где `STAGES = 0`.
* `SUBSTAGE_BITS = 0`, `STAGES = 1`, порты 0 и 1 — сдвиг нулевой, сравнение
вырождается в исходное. Стоковое поведение сохраняется бит в бит.
* Слейв с `port_stage = 7`: `7 <= 1` ложно → порт выключен. Старый хост слал 2 —
`2 <= 1` тоже ложно, так что новый битстрим работает и со старым хостом.
* `SUBSTAGE_BITS = 1`, `STAGES = 3`: порт 0 активен в стадиях 0 и 1, порт 1 — в
2 и 3.
Значение выше последней порт-стадии никогда не совпадает — так хост глушит
излучение слейвов, посылая им 7. Старый хост слал «одну за последней», это тоже
глушит.
### `SPIConfig.vhd`
### `SPIConfig.vhd` и `top.vhd`
В регистре `SweepSetup` (адрес 6) заняты биты 15:13, 12, 5:3 и 2:0 — **биты 11:6
свободны**. Берём три из них:
свободны**. Заняты три из них:
```vhdl
when 6 => STAGES <= spi_buf_out(15 downto 13);
@@ -108,27 +98,41 @@ when 6 => STAGES <= spi_buf_out(15 downto 13);
PORT2_STAGE <= spi_buf_out(2 downto 0);
```
### `top.vhd`
В `top.vhd` добавлен сигнал `sweep_substage_bits` и проброшен от `SPIConfig` к
`Sweep`.
Пробросить `SUBSTAGE_BITS` от `SPIConfig` к `Sweep` — новый сигнал рядом с
`sweep_stages` / `sweep_port1_stage`.
### Проверка
Рядом лежит самопроверяющийся тестбенч `Test_Sweep.vhd` на четыре сценария: сток,
два положения ключа, приёмник без излучения, четыре положения. Он гоняет автомат
без триггерной петли (`SYNC_ENABLED = '0'`) и сверяет `PORT1_ACTIVE`/`PORT2_ACTIVE`
в момент старта каждой оцифровки.
```bash
ghdl -a --std=93 Sweep.vhd Test_Sweep.vhd && ghdl -r --std=93 Test_Sweep
```
Тест проверен мутацией: если вернуть стоковое точное сравнение, сценарии с
под-стадиями падают, а стоковый продолжает проходить.
## Прошивка STM32 LibreVNA
Для самих стадий менять нечего: `stageCnt` уже считается до `settings.stages`,
значения тегируются номером стадии, `VNADatapoint<32>` вмещает 4 стадии × 3
значения. Нужно только протащить новое поле:
Сделано, собирается. Для самих стадий менять было нечего: `stageCnt` уже считается
до `settings.stages`, значения тегируются номером стадии, `VNADatapoint<32>`
вмещает 4 стадии × 3 значения. Протащено только новое поле:
```c
// FPGA.cpp
// FPGA.cpp — параметр со значением по умолчанию, чтобы не трогать вызовы из
// ручного режима и анализатора спектра
void FPGA::SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage,
uint8_t substage_bits, bool synchronize, bool syncMaster) {
bool synchronize, bool syncMaster, uint8_t substage_bits) {
...
value |= (uint16_t) (substage_bits & 0x07) << 6;
// VNA.cpp
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.port3Stage,
s.syncMode != 0, s.syncMaster);
// VNA.cpp — ширина под-стадии приезжает в port3Stage: порта 3 у этого железа нет,
// поле свободно, формат пакета не меняется
FPGA::SetupSweep(s.stages, s.port1Stage, s.port2Stage, s.syncMode != 0,
s.syncMaster, s.port3Stage);
```
Собирать так же, как описано в [`reproduce/README.md`](multi_libreVNA_firmware/reproduce/README.md).
@@ -168,6 +172,80 @@ STM32 прибора в Hi-Z на обеих линиях (`GPIO_MODE_INPUT`, `N
паузы по скользящему окну с запасом, то есть подстраивается сам под любые
настройки развёртки; отдельный провод для маркера начала развёртки не нужен.
## Порядок ввода в строй
### Кого перепрошивать
**Только мастер.** Слейвы остаются на текущей прошивке, и вот почему: хост
выставляет им обе порт-стадии в 7, а стоковое сравнение `stage_cnt = PORTn_STAGE`
при счётчике 0..3 с семёркой не совпадает никогда — то есть слейв не излучает и
без доработки. Число стадий он берёт из того же поля `STAGES`, что и раньше, так
что четыре стадии на точку отработает как есть, а тегирование значений номером
стадии в стоковой прошивке уже общее.
Это заметно экономит силы: slave-битстрим пересобирать не надо, а он, напомню,
побайтово невоспроизводим.
### 1. Битстрим FPGA мастера (Xilinx ISE 14.7)
1. Скопировать [`multi_libreVNA_firmware/reproduce/LibreVNA/FPGA`](multi_libreVNA_firmware/reproduce/LibreVNA/FPGA)
в общую папку виртуалки (`C:\shared\LibreVNA\FPGA`).
2. Открыть **`VNA.xise`** — проект мастера. Не `VNA_external_lo_slave.xise`.
3. Process → **Generate Programming File**.
4. Забрать `top.bin` **сразу** и переименовать в `top_master.bin`: оба проекта
пишут в один и тот же файл, и следующая сборка его затрёт. Именно так был
потерян исходный мастер-битстрим.
Битстрим невоспроизводим побайтово (place & route недетерминирован плюс
timestamp внутри), так что новый файл будет отличаться от старого при любом
исходе — это нормально.
### 2. Прошивка MCU мастера
```bash
cd multi_libreVNA_firmware/reproduce/LibreVNA/Software/VNA_embedded
make BINPATH="<путь к arm-none-eabi из CubeIDE>/tools/bin" -j4
```
Без `EXTERNAL_MASTER_LO_SLAVE` — это и есть сборка мастера. Проверено: с этими
правками собирается без ошибок.
### 3. Упаковка .vnafw
```bash
cd multi_libreVNA_firmware/reproduce/LibreVNA
python3 AssembleFirmware.py \
--bitstream FPGA/VNA/top_master.bin \
--mcu Software/VNA_embedded/build/VNA_embedded.bin \
--output ../combined_per_point_switching.vnafw
```
Имя намеренно новое: **не перезаписывайте `combined.vnafw`, пока не проверите
всё на железе** — это ваш путь отката. После успешной проверки замените им
`combined.vnafw`, обновите `extracted/`, `build/`, `SHA256SUMS` и уберите
предупреждение в начале [`reproduce/README.md`](multi_libreVNA_firmware/reproduce/README.md).
Учтите: `./build.sh` сверяет результат со старым эталоном и теперь сообщит о
расхождении. Это ожидаемо, а не поломка.
### 4. Заливка
Штатным LibreVNA-GUI: **Device → Firmware Update**, скормить `.vnafw` целиком —
GUI сам разложит битстрим и прошивку MCU.
### 5. Включение режима и проверка
Только после того, как мастер поднялся на новой прошивке и обычный режим
(`stimulus_switch_positions: 1`) работает как раньше — включать переключение.
Порядок проверки на стенде:
1. Осциллографом на PB2 и PB5 контроллера: на одной частоте четыре стадии и два
периода `A B A B`, PB5 молчит.
2. По USB CDC контроллера (развёртку остановить): `parity corrections : 0` и
`stages per point : 4`.
3. Сверить трейсы: выходы 0 и 1 должны совпасть с тем, что даёт старый режим —
это те же порт 1 и порт 2 в положении A.
## Хост
Уже сделано. Включается одним полем в `run_config.json`: