Последнее время в интернете очень много хайпа по поводу нейросетей. И конечно, у людей работа нейросетей обычно ассоциируется с разными сгенерированными котиками-картинками, нейросетевыми девушками, забавными видео, и ещё появилось много генерированных нейросетевых текстов и статей (которые часто вызывают раздражение).

Сейчас я хочу показать реальный пример, как нейросеть может быть использована в практических целях программистом FPGA. Я буду использовать бесплатные возможности нейросети Github Copilot https://github.com/copilot.

Github Copilot допускает 50 чат запросов в месяц бесплатно.

Для использования Github Copilot потребуется VPN, так как доступ к нему для россиян к сожалению похоже ограничен.

Предположим, есть задача: подключить энкодер к ПЛИС. На самом деле в гугле довольно легко найти готовые исходные тексты для модуля Verilog для подключения энкодера.

Вот есть такой модуль:


module quad(clk, quadA, quadB, count);
input clk, quadA, quadB;
output [7:0] count;

reg [2:0] quadA_delayed, quadB_delayed;
always @(posedge clk) quadA_delayed <= {quadA_delayed[1:0], quadA};
always @(posedge clk) quadB_delayed <= {quadB_delayed[1:0], quadB};

wire count_enable = quadA_delayed[1] ^ quadA_delayed[2] ^ quadB_delayed[1] ^ quadB_delayed[2];
wire count_direction = quadA_delayed[1] ^ quadB_delayed[2];

reg [7:0] count;
always @(posedge clk)
begin
  if(count_enable)
  begin
    if(count_direction) count<=count+1; else count<=count-1;
  end
end

endmodule


Допустим, что я не хочу пока сильно вникать, работает этот Verilog модуль или нет, но мне хочется понять, всё ли тут правильно? Мне нужен тестбенч для симуляции этого модуля. Я попрошу Copilot написать мне модуль Testbench Verilog, чтобы я смог его просимулировать. Важно, что задавать вопросы в чат с Copilot буду на русском языке. Ниже я приведу весь наш диалог с нейросетью. Но сразу напишу, что результат поразительный...

Итак, я написал первый запрос в чат нейросети и еще даже не привел свой код для работы с энкодером, а Copilot уже предложил к нему тестбенч.

Во втором запросе я ввёл код моего варианта кода и Copilot согласился с ним и адаптировал тестбенч для сигналов именно моего модуля.

В третьем запросе я попросил добавить в тестбенч создание VCD файла, чтобы можно было бы просмотреть сигналы проекта в графическом виде и нейросеть с лёгкостью сделала это.

Потом я провел симуляцию модуля Verilog и тестбенча с помощью Icarus Verilog и увидел, что выход счетчика не определен, всегда значение "x":


C:\Users\nick\Documents\Verilog>iverilog -o qqq tb_quad.v quad.v

C:\Users\nick\Documents\Verilog>vvp qqq
VCD info: dumpfile tb_quad.vcd opened for output.
Time quadA quadB count
0 0 0 x
50000 0 1 x
70000 1 1 x
90000 1 0 x
110000 0 0 x
130000 0 1 x
150000 1 1 x
170000 1 0 x
190000 0 0 x
210000 0 1 x
230000 1 1 x
250000 1 0 x
310000 0 0 x
330000 1 0 x
350000 1 1 x
370000 0 1 x
390000 0 0 x
410000 1 0 x
430000 1 1 x
450000 0 1 x
510000 0 0 x
530000 0 1 x
550000 1 1 x
570000 1 0 x
590000 0 0 x
610000 1 0 x
630000 1 1 x
650000 0 1 x
tb_quad.v:71: $finish called at 710000 (1ps)


В четвертом запросе я спросил почему выход counter не определён (я так-то и сам знаю почему), но для чистоты эксперимента спросил это у нейросети. И Copilot ответил мне, что нужно инициализировать счетчик либо сигналом сброса, либо простым присвоением начального значения.

Я исправил модуль quad как посоветовала нейросеть вот так:
reg [7:0] count = 0;

И после этого симуляция с Icarus Verilog прошла успешно:


C:\Users\nick\Documents\Verilog>iverilog -o qqq tb_quad.v quad.v

C:\Users\nick\Documents\Verilog>vvp qqq
VCD info: dumpfile tb_quad.vcd opened for output.
Time quadA quadB count
0 0 0 0
50000 0 1 0
70000 1 1 0
75000 1 1 255
90000 1 0 255
95000 1 0 254
110000 0 0 254
115000 0 0 253
130000 0 1 253
135000 0 1 252
150000 1 1 252
155000 1 1 251
170000 1 0 251
175000 1 0 250
190000 0 0 250
195000 0 0 249
210000 0 1 249
215000 0 1 248
230000 1 1 248
235000 1 1 247
250000 1 0 247
255000 1 0 246
275000 1 0 245
310000 0 0 245
330000 1 0 245
335000 1 0 244
350000 1 1 244
355000 1 1 245
370000 0 1 245
375000 0 1 246
390000 0 0 246
395000 0 0 247
410000 1 0 247
415000 1 0 248
430000 1 1 248
435000 1 1 249
450000 0 1 249
455000 0 1 250
475000 0 1 251
510000 0 0 251
530000 0 1 251
535000 0 1 252
550000 1 1 252
555000 1 1 251
570000 1 0 251
575000 1 0 250
590000 0 0 250
595000 0 0 249
610000 1 0 249
615000 1 0 248
630000 1 1 248
635000 1 1 249
650000 0 1 249
655000 0 1 250
675000 0 1 251
tb_quad.v:71: $finish called at 710000 (1ps)


Вот как выглядят сигналы модуля в gtkwave после симуляции:



Я вижу, что в целом этот конкретный модуль quad Verilog работает и может считать повороты энкодера по часовой и против часовой стрелки. Но у меня есть замечание - даже по waveform сигналам видно, что такой модуль считает не именно щелчёк поворота энкодера, а выполняет несколько инкрементов или декрементов за щелчёк. В принципе, возможно это не критично для некоторых приложений. Но если это важно, то всегда можно исправить, если попросить Copilot это сделать. Нужно учитывать, к примеру, только фронт сигнала quadA. Итоговый вариант, который я получил с помощью нейросети вот такой:


module quad(
  input clk,
  input quadA,
  input quadB,
  output reg [7:0] count = 0
);

reg [2:0] quadA_delayed = 0;
reg [2:0] quadB_delayed = 0;

// Сдвигаемые регистры для подавления метастабильности
always @(posedge clk) begin
  quadA_delayed <= {quadA_delayed[1:0], quadA};
  quadB_delayed <= {quadB_delayed[1:0], quadB};
end

// Счет только по фронту quadA (0→1) после синхронизации
always @(posedge clk) begin
  if (!quadA_delayed[2] && quadA_delayed[1]) begin
    if (quadB_delayed[1])
      count <= count - 1;
    else
      count <= count + 1;
  end
end

endmodule


Симуляция в Icarus Verilog выдаёт вот такие временные диаграммы сигналов в GtkWave:

 

На самом деле взаимодействие с нейросетью Github Copilot очень впечатляет.
Видно, что постепенно нейросеть перестаёт быть просто игрушкой, но вполне уже можно пробовать использовать её для решения практических задач.

Ниже приведен мой полный диалог с чатом Github Copilot. Форматирование диалога конечно отличается по стилю, так как на нашем сайте у движка совсем другие CSS параметры. Но в целом я постарался сохранить диалог в первоначальном виде. Мои запросы к нейросети в блоках на бледно желтом фоне. Ответы Copilot и его пояснения между моими желтоватыми блоками.


Есть модуль Verilog для опроса двух линий quadA и quadB от энкодера вращения. Модуль Verilog должен выдавать 8 бит счетчика, значение которого увеличивается или уменьшается в зависимости он направления вращения энкодера. Код модуля приведен ниже. Напиши Verilog Testbench для этого модуля, чтобы проверить его работоспособность.



  Ниже приведён базовый шаблон Verilog testbench для модуля энкодера на основе сигналов quadA и quadB. В комментариях указаны типовые фрагменты для эмуляции вращения вперед и назад. Если потребуется подстроить имена или добавить дополнительные порты в зависимости от реального модуля, внесите правки.

  Verilog
// Пусть ваш модуль называется quadrature_counter
module quadrature_counter(
    input wire clk,
    input wire rst,
    input wire quadA,
    input wire quadB,
    output reg [7:0] count
);
// Модуль -- сюда вставьте вашу реализацию
endmodule

// Тестбенч
module tb_quadrature_counter;
    reg clk = 0;
    reg rst = 1;
    reg quadA = 0;
    reg quadB = 0;
    wire [7:0] count;

    // Instantiate the DUT
    quadrature_counter uut (
        .clk(clk),
        .rst(rst),
        .quadA(quadA),
        .quadB(quadB),
        .count(count)
    );

    // Clock generation: 10ns period
    always #5 clk = ~clk;

    // Процедура сброса
    initial begin
        rst = 1;
        #20;
        rst = 0;
    end

    // Стимуляция сигналов quadA/quadB для вращения вперед и назад
    initial begin
        // Ждем сброса
        @(negedge rst);

        // Немного подождем после сброса
        #10;

        // Квадратурная последовательность (вперед)
        // Например, 4 шага вперед
        repeat (4) begin
            {quadA, quadB} = 2'b00; #20;
            {quadA, quadB} = 2'b01; #20;
            {quadA, quadB} = 2'b11; #20;
            {quadA, quadB} = 2'b10; #20;
        end

        // Немного подождем
        #40;

        // Квадратурная последовательность (назад)
        // Например, 3 шага назад
        repeat (3) begin
            {quadA, quadB} = 2'b10; #20;
            {quadA, quadB} = 2'b11; #20;
            {quadA, quadB} = 2'b01; #20;
            {quadA, quadB} = 2'b00; #20;
        end

        // Завершение симуляции
        #50;
        $finish;
    end

    // Мониторинг
    initial begin
        $display("Time  quadA quadB  count");
        $monitor("%4t     %b     %b     %3d", $time, quadA, quadB, count);
    end
endmodule

Как это работает:

  • Вращение вперед:
    00→01→11→10 (clockwise)
  • Вращение назад:
    10→11→01→00 (counter-clockwise)
  • После каждого шага добавлено небольшое ожидание для имитации шагов.

Как использовать:

  • Скомпилируйте (vlog) оба файла (quadrature_counter.v и тестбенч).
  • Запустите симуляцию (e.g., ModelSim, Vivado, etc.).
  • Убедитесь, что при вращении вперед счетчик увеличивается, а при вращении назад уменьшается.

Если есть вопросы по интеграции – вставьте сюда ваш модуль, я адаптирую testbench.


Мой модуль

module quad(clk, quadA, quadB, count);
input clk, quadA, quadB;
output [7:0] count;

reg [2:0] quadA_delayed, quadB_delayed;
always @(posedge clk)
  quadA_delayed <= {quadA_delayed[1:0], quadA};

always @(posedge clk)
  quadB_delayed <= {quadB_delayed[1:0], quadB};

wire count_enable = quadA_delayed[1] ^ quadA_delayed[2] ^ quadB_delayed[1] ^ quadB_delayed[2];
wire count_direction = quadA_delayed[1] ^ quadB_delayed[2];

reg [7:0] count;

always @(posedge clk) begin
  if(count_enable) begin
    if(count_direction) count<=count+1;
  else count<=count-1;
end
end

endmodule.

 


  Отлично, теперь вы предоставили свой модуль, вот полный рабочий Verilog testbench для проверки вашего quad-модуля:

  Verilog
`timescale 1ns/1ps

module tb_quad;

    reg clk = 0;
    reg quadA = 0;
    reg quadB = 0;

    wire [7:0] count;

    // Instantiate Device Under Test
    quad dut (
        .clk(clk),
        .quadA(quadA),
        .quadB(quadB),
        .count(count)
    );

    // Clock generator (10ns period)
    always #5 clk = ~clk;

    // Quadrature forward rotation (CW): 00 -> 01 -> 11 -> 10 -> 00 ...
    task step_forward;
        begin
            {quadA, quadB} = 2'b00; #20;
            {quadA, quadB} = 2'b01; #20;
            {quadA, quadB} = 2'b11; #20;
            {quadA, quadB} = 2'b10; #20;
        end
    endtask

    // Quadrature backward rotation (CCW): 00 -> 10 -> 11 -> 01 -> 00 ...
    task step_backward;
        begin
            {quadA, quadB} = 2'b00; #20;
            {quadA, quadB} = 2'b10; #20;
            {quadA, quadB} = 2'b11; #20;
            {quadA, quadB} = 2'b01; #20;
        end
    endtask

    initial begin
        // Monitor signals
        $display("  Time   quadA quadB   count");
        $monitor("%6t     %b     %b    %3d", $time, quadA, quadB, count);

        // Start from known state
        {quadA, quadB} = 2'b00;
        #30;
        
        // 3 steps forward
        step_forward;
        step_forward;
        step_forward;
        #40;

        // 2 steps backward
        step_backward;
        step_backward;
        #40;

        // Alternate forward & backward
        step_forward;
        step_backward;
        #40;

        $finish;
    end
endmodule

 

Пояснения:

  • Используются задачи step_forward (по часовой стрелке) и step_backward (против часовой стрелки), чтобы симулировать вращение энкодера.
  • Для каждого шага происходит переход через все 4 состояния (00→01→11→10 и наоборот).
  • Мониторинг значений сигнала count после каждого шага.

Ожидаемый результат:

  • После каждого полного шага вперед счётчик увеличивается на 1.
  • После каждого полного шага назад счётчик уменьшается на 1.
  • Тестбенч выводит значения на каждой итерации.

Готово для запуска без изменений!


Добавь в тестбенч создание VCD файла



  Конечно! Вот ваш testbench tb_quad с добавлением генерации VCD-файла для просмотра сигналов в waveform viewer (например, GTKWave):

  Verilog
`timescale 1ns/1ps

module tb_quad;

    reg clk = 0;
    reg quadA = 0;
    reg quadB = 0;

    wire [7:0] count;

    // Instantiate Device Under Test
    quad dut (
        .clk(clk),
        .quadA(quadA),
        .quadB(quadB),
        .count(count)
    );

    // Clock generator (10ns period)
    always #5 clk = ~clk;

    // Quadrature forward rotation (CW): 00 -> 01 -> 11 -> 10 -> 00 ...
    task step_forward;
        begin
            {quadA, quadB} = 2'b00; #20;
            {quadA, quadB} = 2'b01; #20;
            {quadA, quadB} = 2'b11; #20;
            {quadA, quadB} = 2'b10; #20;
        end
    endtask

    // Quadrature backward rotation (CCW): 00 -> 10 -> 11 -> 01 -> 00 ...
    task step_backward;
        begin
            {quadA, quadB} = 2'b00; #20;
            {quadA, quadB} = 2'b10; #20;
            {quadA, quadB} = 2'b11; #20;
            {quadA, quadB} = 2'b01; #20;
        end
    endtask

    initial begin
        // VCD DUMP
        $dumpfile("tb_quad.vcd");  // имя файла
        $dumpvars(0, tb_quad);     // лог всё, что связано с этим модулем

        // Monitor signals
        $display("  Time   quadA quadB   count");
        $monitor("%6t     %b     %b    %3d", $time, quadA, quadB, count);

        // Start from known state
        {quadA, quadB} = 2'b00;
        #30;
        
        // 3 steps forward
        step_forward;
        step_forward;
        step_forward;
        #40;

        // 2 steps backward
        step_backward;
        step_backward;
        #40;

        // Alternate forward & backward
        step_forward;
        step_backward;
        #40;

        $finish;
    end
endmodule


Как посмотреть результат:

  1. Прогоните симуляцию (vvp, iverilog либо другой симулятор Verilog).
  2. Откройте файл tb_quad.vcd в GTKWave или другом VCD viewer:
    >gtkwave tb_quad.vcd
    

Вы сможете проследить изменения clk, quadA, quadB и count во времени.


Почему при симуляции сигнал count всегда равен x?



 Причина появления значения x на выводе count в Verilog — это отсутствие инициализации регистра count.
В языке Verilog все регистры (reg), если не заданы явно, по умолчанию инициализируются в неопределённое значение (x).
Ваш модуль содержит такую строку:

reg [7:0] count;

а в блоке always @(posedge clk) отсутствует сброс (reset) или явная инициализация.

Как исправить

Самый надёжный способ — добавить сигнал сброса (reset), либо проинициализировать регистр в начальное значение при запуске симуляции:

Вариант 1: Добавьте асинхронный или синхронный reset

 

  Verilog
module quad(clk, quadA, quadB, count, rst);
input clk, quadA, quadB, rst;
output [7:0] count;

reg [2:0] quadA_delayed, quadB_delayed;
always @(posedge clk) quadA_delayed <= {quadA_delayed[1:0], quadA};
always @(posedge clk) quadB_delayed <= {quadB_delayed[1:0], quadB};

wire count_enable = quadA_delayed[1] ^ quadA_delayed[2] ^ quadB_delayed[1] ^ quadB_delayed[2];
wire count_direction = quadA_delayed[1] ^ quadB_delayed[2];

reg [7:0] count;
always @(posedge clk)
begin
  if (rst)
    count <= 0;         // сбросить счётчик на reset
  else if(count_enable)
    if(count_direction) count <= count + 1; else count <= count - 1;
end

endmodule

 

И не забудьте подключить rst в testbench и подать импульс сброса (обычно в начале симуляции).

Вариант 2: Явная инициализация

В SystemVerilog (или если ваш симулятор Verilog поддерживает begin/end-инициализацию для reg):

reg [7:0] count = 0;

Поддержка такой инициализации может зависеть от симулятора (iverilog поддерживает, ModelSim — только в SystemVerilog).

Кратко:

  • Значение x у регистра — нет инициализации (типичная причина).
  • Добавьте сброс (reset) или инициализируйте регистр явно, чтобы избежать этого.

Дополнение

Для промышленных проектов ВСЕГДА реализуйте и используйте сброс (reset), особенно в FPGA и ASIC.

Пример с reset для вашего тестбенча:

  Verilog
reg rst = 1;

// Клок генератор...
// Подключить .rst(rst) в инстанциацию quad

initial begin
  rst = 1;
  #20;
  rst = 0;
  // далее тест как обычно
end

 

Без инициализации регуляр reg — всегда x до первого присваивания!


Исправь модуль quad так, что бы подсчет импульсов поворота энкодера происходил только один раз за счелчёк энкодера, только в момент фронта сигнала quadA

 

CopilotImg  Вот исправленный модуль, учитывающий импульс только по фронту (переходу 0→1) сигнала quadA. В этом случае анализируем только момент появления фронта quadA, и определяем направление через значение quadB (классическая схема для опроса энкодера):

  Verilog
module quad(
    input clk,
    input quadA,
    input quadB,
    output reg [7:0] count
);

reg quadA_delayed;

// Фиксация значения quadA для обнаружения фронта
always @(posedge clk)
    quadA_delayed <= quadA;

// Обновление счетчика только на фронте quadA
always @(posedge clk) begin
    if (!quadA_delayed && quadA) begin  // обнаружен фронт 0 -> 1
        if (quadB)
            count <= count - 1; // если quadB=1 во время fronta, то назад
        else
            count <= count + 1; // если quadB=0, то вперед
    end
end

endmodule

 

Пояснения
  • quadA_delayed — хранит предыдущее значение quadA на каждом такте.
  • !quadA_delayed && quadA — истинно только на фронте 0→1 quadA.
  • При каждом таком фронте, если в этот же момент quadB=0 — вращение вперёд, если quadB=1 — назад.

Этот подход гарантирует увеличение или уменьшение счетчика ровно один раз за реальный «щёлчёк» (импульс фронта) энкодера!


Похоже работает, но нужно добавить регистры reg [2:0] quadA_delayed, quadB_delayed; для защиты от метастабильности.

 


CopilotImg  Конечно, это хороший подход для синхронизации и подавления метастабильности!

Вот исправленный вариант вашего модуля, использующий трехтактовые регистры задержки для quadA и quadB — счет идет только по фронту (0→1) синхронизированного сигнала:

  Verilog88
module quad(
    input clk,
    input quadA,
    input quadB,
    output reg [7:0] count
);

reg [2:0] quadA_delayed = 0;
reg [2:0] quadB_delayed = 0;

// Сдвигаемые регистры для подавления метастабильности
always @(posedge clk) begin
    quadA_delayed <= {quadA_delayed[1:0], quadA};
    quadB_delayed <= {quadB_delayed[1:0], quadB};
end

// Счет только по фронту quadA (0→1) после синхронизации
always @(posedge clk) begin
    if (!quadA_delayed[2] && quadA_delayed[1]) begin
        if (quadB_delayed[1])
            count <= count - 1;
        else
            count <= count + 1;
    end
end

endmodule

 

Пояснения:

  • Метастабильные (async) сигналы quadA и quadB проходят через 3-регистровые цепочки синхронизации.
  • Фронт определяется по срезу: прежнее значение (quadA_delayed[2]) было 0, новое (quadA_delayed[1]) — 1.
  • Направление определяет quadB, синхронизированный этим же выходом регистра.

Такой модуль корректно считает импульсы только по фронту синхронизированного quadA!

 


Добавить комментарий


Защитный код
Обновить