Стиль описания конечного автомата как образ мышления


Когда нужно преодолеть врожденную параллельность FPGA, и появляется желание заставить схему работать последовательно, по алгоритму, на помощь приходят конечные автоматы, про которые написано не мало академических и практических трудов.

Например, очень популярной является работа: Clifford E. Cummings, The Fundamentals of Efficient Synthesizable Finite State Machine Design using NC-Verilog and BuildGates. Всякий раз, когда специалисты решают обсудить, как правильно писать конечные автоматы, кто-то обязательно достает эту публикацию.

Статья стала настолько авторитетной, что многие даже не стараются анализировать аргументы автора. В частности, бытует мнение, что профессионалы всегда используют двухчастный способ описания конечных автоматов, имеется ввиду, описание конечных автоматов в 2 always блока. Это утверждение продолжает вызывать жаркие споры, и я хочу пояснить различия в описаниях конечного автомата с разным количество always блоков.

В беседах с коллегами я понял, что споры о том, как надо писать конечные автоматы в 1 или 2, 3 always блока, связаны с разным представлением (осознанием) реализуемого алгоритма, разным типом мышления. Попробую показать это на примере.

Я полагаю, что эта статья не первая статья о FSM и Verilog в вашей жизни, поэтому я не буду объяснять ни что такое конечный автомат, ни как он описывается на Verilog, а перейду сразу к делу.

Представьте, вы сидите дома, вам нужно сходить в магазин, купить хлеба, и вернуться обратно. Реализовать такое поведение просто — это алгоритмическая последовательность действий, ее можно описать так:

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

Для многих такое описание покажется естественным. Четкая последовательность действий, похожа на строки программы. Однако, тот же алгоритм можно описать и по другому:

  • Из дома направиться на улицу
  • С улицы направиться в магазин (хлебный отдел)
  • Из хлебного одела направиться в кассу
  • Из кассы направиться домой

при этом

  • когда мы дома — мы раздеты
  • когда мы на улице — мы одеты
  • когда мы в магазине (хлебном отделе) — мы берем хлеб
  • когда мы у кассы — мы платим деньги

Интересно, хоть кто-то сейчас подумал, что последний вариант логичнее, чем первый?

Так или иначе, оба этих описания имеют представление в виде конечного автомата. Первое в виде описания с одним always блоком, а второе — с двумя или тремя. Оговорюсь, что описание в 2 и 3 always блока — это близнецы-братья, отличаются только техническими нюансами, которые нам сейчас не важны.

Покажем, как описания представляются в виде автомата:
У нас есть состояния автомата: HOME_STATE, STREET_STATE, MARKET_STATE, CASHIER_STATE, есть выходы автомата (наши действия): GET_DRESSED_ACT, UNDRESS_ACT, TAKE_BREAD_ACT, PAY_MONEY_ACT

Описание с 1 always блоком, выглядит так:

always @(posedge clk)
  begin
    if(reset)
      begin
        State <= HOME_STATE;
        Action <= UNDRESS_ACT;
      end
    //---------------------------------  
    else
      begin
        case(State)
          //-----------------------------
          HOME_STATE:
            begin
              Action <= GET_DRESSED_ACT; //одеться
              State <= STREET_STATE; //и пойти на улицу
            end
          
          //-----------------------------
          STREET_STATE:
            begin
              State <= MARKET_STATE; //пойти в магазин
              Action <= TAKE_BREAD_ACT; //взять хлеб
            end
          
          //-----------------------------
          MARKET_STATE:
            begin
              State <= CASHIER_STATE; //пойти в кассу
              Action <= PAY_MONEY_ACT; //оплатить хлеб
            end
          
          //-----------------------------
          CASHIER_STATE:
            begin
              State <= HOME_STATE; //пойти домой 
              Action <= UNDRESS_ACT; //и раздеться
            end
            
          //-----------------------------
          default: //если не знаем где мы (чего быть не может, но вдруг)
            begin
              State <= HOME_STATE; //идём домой
              Action <= UNDRESS_ACT; //и раздеваемся 
            end
        endcase
      end
  end

Теперь описание с 2 alwaysблоками:

//технический блок для организации переходов 
always @(posedge clk)
  begin
    if(reset)
      State <= HOME_STATE;
    else
      State <= NextState;
  end
  
  
//блок работы автомата: выбор перехода и действия
always @(*)
  begin
    case(Sate)
      //-----------------------------
      HOME_STATE: //когда мы дома 
        begin
          Action = UNDRESS_ACT; //мы раздеты 
          NextState = STREET_STATE; //собираемся идти на улицу
        end
      
      //-----------------------------
      STREET_STATE: //когда мы на улице
        begin
          Action = GET_DRESSED_ACT; //мы одеты
          NextState = MARKET_STATE; //собираемся в магазин
        end
      
      //-----------------------------
      MARKET_STATE: //когда мы в магазине
        begin
          Action = TAKE_BREAD_ACT; //берем хлеб 
          NextState = CASHIER_STATE; //собираемся в кассу
        end
      
      //-----------------------------
      CASHIER_STATE: //когда мы у кассы 
        begin
          Action = PAY_MONEY_ACT; //мы платим деньги 
          NextState = HOME_STATE; //собираемся пойти домой           
        end
        
      //-----------------------------
      default://если не знаем где мы (чего быть не может, но вдруг)
        begin
          NextState = HOME_STATE; //мы хотим домой
          Action = GET_DRESSED_ACT; //мы одеты (на всякий случай)
        end
    endcase
  end


Хочу обратить внимание на особенности этих 2 описаний, которые и являются причиной священной войны за число блоков always.
1 always блок
  HOME_STATE:
    begin
      Action <= GET_DRESSED_ACT; //одеться
      State <= STREET_STATE; //и пойти на улицу
    end

2 always блока

  HOME_STATE: //когда мы дома 
    begin
      Action = UNDRESS_ACT; //мы раздеты 
      NextState = STREET_STATE; //собираемся идти на улицу
    end

Автомат с 1 always блоком в текущем состоянии определяет, какие действия он собирается делать дальше, не заботясь о том, что он делает сейчас. А автомат с 2 always блоками, в текущем состояние определяет, что он делает сейчас, и его не заботит, что он будет делать дальше или делал до этого.

Нельзя однозначно сказать какое поведение лучше или правильнее. Все сильно зависит от задачи, от реализуемого автоматом алгоритма. Для демонстрации изменим ситуацию. Теперь у вас есть дом, работа, бар. Вы ходите на работу — работать, в бар — пить пиво. В бар вы ходите как из дома по выходным, так и с работы в пятницу.

В первой реализации, с 1 always, вам надо внимательно следить куда вы собираетесь идти чтобы случайно не начать пить пиво на работе или работать в баре. Во второй реализации, с 2 always блоками, вы защищены от этого. Тут все четко определено: состояние на работе — работаем, состояние в баре — пьем пиво.

С другой стороны, в описании с 2 always блоками, придя в бар с работы, вы не сможете не пить пиво. Состояние в этой реализации жестко фиксирует действие бар — пьем пиво. А в описании с 1 always блоком, ваши действия в баре определяются в момент выхода из прошлого состояния. С работы вы можете пойти в бар и выпить виски. Каждый второй поход из дома в бар может заканчиваться вечеринкой. Текущее состояние в баре никак вас не ограничивает.

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

Если у вас сложная сеть переходов и в разные состояния вы попадаете многими путями, имеет смысл использовать схему с 2 always блоками. У вас не будет шанса забыть задать какой то из выходов автомата при очередном переходе.

С другой стороны, если вы пишите простой автомат с практически линейной структурой, можно использовать описание с 1 always блоком. Фактически вы задаете последовательность на выходе, а состояния используете просто для организации последовательного выполнения. Так как выходы автомата не зависят от текущего состояния, не надо будет прописывать их значение в каждом состоянии, описание будет короче и логичнее.

В статье, с которой мы начали, хорошо показано почему надо использовать описания с 2 и 3 always блоками, а описание с 1 always блоком отмечено как самое плохое. Автор рекомендует избегать такого описания. Поэтому хочется привести пример реального интерфейса удобного для описания в 1 always блок и защитить данный вид описания. Для сравнения я приведу тот же автомат описанный в 3 always блока. Необходимо 3, а не 2 блока, потому, что мы будем использовать регистровые выходные сигналы.

И так, у нас есть модуль, который реализует запись в асинхронную память. На вход модуль принимает строб чтения либо записи, данные и адрес для записи, адрес для чтения. На выход модуль выдает сигнал завершения чтения или записи и считанные данные. Модуль управляет простой асинхронной памятью, имеющей следующие временные диаграмы работы.



Основной смысл модуля по стробу чтения либо записи «развернуть времянку», выдержать заданные интервалы, а по завершению выдать 1 тактовый сигнал готовности. Нам необходимо соблюдать следующие интервалы

  • Rs — время установки адреса и сигнала выбора памяти перед чтением
  • Rp — время выдержки сигнала разрешения выхода памяти до появления корректных данных
  • Rh — время выдержки сигнала выбора памяти после снятия разрешения выхода
  • Ws — время установки адреса, данных и сигнала выбора памяти перед записью
  • Wp — длительность выставления сигнала разрешения записи
  • Wh — время выдержки данных и адреса после снятия сигнала разрешения записи

Интервалы мы будем измерять в количестве тактов входной частоты и зададим интервалы константами READ_SETUP, READ_PULSE, READ_HOLD и WRITE_SETUP, WRITE_PULSE, WRITE_HOLD.

Описание с 1 always блоком:

module mem_ctrl_1
(
  //system side
  input clk, 
  input reset,
  input w_strb,
  input r_strb,
  input [7:0] s_waddress,
  input [7:0] s_raddress,
  input [7:0] s_data_to,
  output reg [7:0] s_data_from,
  output reg done,
    
  //memory side
  output reg [7:0] m_address,
  output reg [7:0] m_data_to,
  input [7:0] m_data_from,
  output reg cs_n,
  output reg oe_n,
  output reg we
);

//------------------------------------------------------
parameter PAUSE_CNT_SIZE = 16;

parameter READ_SETUP   = 5;
parameter READ_PULSE   = 3;
parameter READ_HOLD    = 1;

parameter WRITE_SETUP  = 5;
parameter WRITE_PULSE  = 3;
parameter WRITE_HOLD   = 1;

//------------------------------------------------------
reg [PAUSE_CNT_SIZE - 1 : 0] PCounter;

//------------------------------------------------------
reg        [3:0] State;

localparam [3:0] IDLE          = 0;

localparam [3:0] PREPARE_READ  = 1;
localparam [3:0] READ          = 2;
localparam [3:0] END_READ      = 3;

localparam [3:0] PREPARE_WRITE = 4;
localparam [3:0] WRITE         = 5;
localparam [3:0] END_WRITE     = 6;



//------------------------------------------------------
always @(posedge clk)
  begin
    if(reset) 
      begin
        done        <= 1'b0;
        m_address   <= 8'd0;
        m_data_to   <= 8'd0;
        s_data_from <= 8'd0;
        cs_n        <= 1'b1;
        oe_n        <= 1'b1;
        we          <= 1'b0;  
        State       <= IDLE;
        PCounter    <=    0;
      end 
    else
      begin
        //счётчик всегда считает до 0
        if(PCounter != 0)
          PCounter <= PCounter - 1'b1;

        //всегда снимаем сигнал готовности, 
        //он не больше 1 такта
        done  <= 1'b0;
          
        case(State)
          //--------------------------
          IDLE:
            begin
              if(w_strb == 1'b1) //запрос записи
                begin
                  cs_n <= 1'b0;
                  m_data_to <= s_data_to;
                  State <= PREPARE_WRITE;
                  m_address <= s_waddress;
                  PCounter <= WRITE_SETUP;
                end
              else if(r_strb == 1'b1) //запрос чтения
                begin
                  cs_n <= 1'b0;
                  m_address <= s_raddress;
                  State <= PREPARE_READ;
                  PCounter <= READ_SETUP;
                end
            end

          //--------------------------
          PREPARE_READ:
            begin
              if(PCounter == 0)
                begin
                  State <= READ;
                  oe_n <= 1'b0;
                  PCounter <= READ_PULSE;
                end
            end
            
          //--------------------------
          READ:
            begin
              if(PCounter == 0)
                begin
                  State <= END_READ;
                  oe_n <= 1'b1;
                  PCounter <= READ_HOLD;
                  s_data_from <= m_data_from;
                end            
            end

          //--------------------------
          END_READ:
            begin
              if(PCounter == 0)
                begin
                  State <= IDLE;
                  cs_n  <= 1'b1;
                  done  <= 1'b1;
                end                        
            end
            
          //--------------------------
          PREPARE_WRITE:
            begin
              if(PCounter == 0)
                begin
                  State <= WRITE;
                  we <= 1'b1;
                  PCounter <= WRITE_PULSE;
                end            
            end

          //--------------------------
          WRITE:
            begin
              if(PCounter == 0)
                begin
                  State <= END_WRITE;
                  we <= 1'b0;
                  PCounter <= WRITE_HOLD;
                end
            end
            
          //--------------------------
          END_WRITE:
            begin
              if(PCounter == 0)
                begin
                  State <= IDLE;
                  cs_n  <= 1'b1;
                  done  <= 1'b1;
                end            
            end            
            
          //--------------------------
          default: //невозможная ситуация
            begin
              done        <= 1'b0;
              m_address   <= 8'd0;
              m_data_to   <= 8'd0;
              s_data_from <= 8'd0;
              cs_n        <= 1'b1;
              oe_n        <= 1'b1;
              we          <= 1'b0;  
              State       <= IDLE;            
            end
        endcase
      end
  end

endmodule

Описание с 3 always блоками:

module mem_ctrl_3
(
  //system side
  input clk, 
  input reset,
  input w_strb,
  input r_strb,
  input [7:0] s_waddress,
  input [7:0] s_raddress,
  input [7:0] s_data_to,
  output reg [7:0] s_data_from,
  output reg done,
    
  //memory side
  output reg [7:0] m_address,
  output reg [7:0] m_data_to,
  input [7:0] m_data_from,
  output reg cs_n,
  output reg oe_n,
  output reg we
);

//------------------------------------------------------
parameter PAUSE_CNT_SIZE = 16;

parameter READ_SETUP   = 5;
parameter READ_PULSE   = 3;
parameter READ_HOLD    = 1;

parameter WRITE_SETUP  = 5;
parameter WRITE_PULSE  = 3;
parameter WRITE_HOLD   = 1;

//------------------------------------------------------
reg [PAUSE_CNT_SIZE - 1 : 0] PCounter;

//------------------------------------------------------
reg        [3:0] State;
reg        [3:0] NextState;

localparam [3:0] IDLE          = 0;

localparam [3:0] PREPARE_READ  = 1;
localparam [3:0] READ          = 2;
localparam [3:0] END_READ      = 3;

localparam [3:0] PREPARE_WRITE = 4;
localparam [3:0] WRITE         = 5;
localparam [3:0] END_WRITE     = 6;

//------------------------------------------------------
//технический блок
always @(posedge clk)
  begin
    if(reset)
      State <= IDLE;
    else
      State <= NextState;
  end

//------------------------------------------------------
//выбор перехода 
always @(*)
  begin
    //по умолчанию сохраняем текущее состояние 
    NextState = State;
    
    case(State)
      //--------------------------------------
      IDLE: 
        begin
          if(w_strb == 1'b1) //запрос записи
            begin
              NextState = PREPARE_WRITE;
            end
          else if(r_strb == 1'b1) //запрос чтения
            begin
              NextState = PREPARE_READ;
            end
        end
    
    
      //--------------------------------------
      PREPARE_READ:
        begin
          if(PCounter == 0)
            begin
              NextState = READ;
            end
        end
        
      //--------------------------------------
      READ:
        begin
          if(PCounter == 0)
            begin
              NextState = END_READ;
            end            
        end

      //--------------------------------------
      END_READ:
        begin
          if(PCounter == 0)
            begin
              NextState = IDLE;
            end                        
        end
        
      //--------------------------------------
      PREPARE_WRITE:
        begin
          if(PCounter == 0)
            begin
              NextState = WRITE;
            end            
        end

      //--------------------------------------
      WRITE:
        begin
          if(PCounter == 0)
            begin
              NextState = END_WRITE;
            end
        end
        
      //--------------------------------------
      END_WRITE:
        begin
          if(PCounter == 0)
            begin
              NextState = IDLE;
            end            
        end     
      
      //--------------------------------------
      default: NextState = IDLE;
    endcase
  end
 
//-------------------------------------------
//задание выхода
always @(posedge clk)
  begin
    if(reset)
      begin
        cs_n        <= 1'b1;
        oe_n        <= 1'b1;
        we          <= 1'b0;  
      end
    else
      begin
        //чтобы значение выхода изменялось вместе с изменением
        //состояния, а не на следующем такте, анализируем NextState
        case(NextState)
          //--------------------------------------
          IDLE: 
            begin
              cs_n        <= 1'b1;
              oe_n        <= 1'b1;
              we          <= 1'b0;               
            end
        
          //--------------------------------------
          PREPARE_READ:
            begin
              cs_n        <= 1'b0;
              oe_n        <= 1'b1;
              we          <= 1'b0;
            end
            
          //--------------------------------------
          READ:
            begin
              cs_n        <= 1'b0;
              oe_n        <= 1'b0;
              we          <= 1'b0;           
            end

          //--------------------------------------
          END_READ:
            begin
              cs_n        <= 1'b0;
              oe_n        <= 1'b1;
              we          <= 1'b0;                     
            end
            
          //--------------------------------------
          PREPARE_WRITE:
            begin
              cs_n        <= 1'b0;
              oe_n        <= 1'b1;
              we          <= 1'b0;          
            end

          //--------------------------------------
          WRITE:
            begin
              cs_n        <= 1'b0;
              oe_n        <= 1'b1;
              we          <= 1'b1;
            end
            
          //--------------------------------------
          END_WRITE:
            begin
              cs_n        <= 1'b0;
              oe_n        <= 1'b1;
              we          <= 1'b0;          
            end          
        endcase
      end
  end
 
 
//-------------------------------------------
//обработка задания адреса и данных
//эти сигналы задаются только на границе переходов 
always @(posedge clk)
  begin
    if(reset)
      begin
        m_address   <= 8'd0;
        m_data_to   <= 8'd0;
        s_data_from <= 8'd0;
        done        <= 1'b0;
      end
    else
      begin
        if      ((State == IDLE) && (NextState == PREPARE_WRITE))
          begin
            m_address <= s_waddress;
            m_data_to <= s_data_to;
          end
        else if ((State == IDLE) && (NextState == PREPARE_READ))
          m_address <= s_raddress;
        
        //----------------------------------------------------------------
        if      ((State == READ) && (NextState == END_READ))
          s_data_from <= m_data_from;
          
        //----------------------------------------------------------------          
        if      ((State == END_READ)  && (NextState == IDLE))
          done <= 1'b1;
        else if ((State == END_WRITE) && (NextState == IDLE))
          done <= 1'b1;          
        else
          done <= 1'b0;
      end
  end
 
//-------------------------------------------
//обработка счетчика паузы 
always @(posedge clk)
  begin
    if(reset)
      begin
        PCounter <= 0;
      end
    else
      begin
        //счетчик все время идет до 0, кроме
        //моментов смены состояния, когда задается величина
        //паузы для следующего состояния 
        if     ((State == IDLE) && (NextState == PREPARE_WRITE))
          PCounter <= WRITE_SETUP;
        else if((State == PREPARE_WRITE) && (NextState == WRITE))
          PCounter <= WRITE_PULSE;
        else if((State == WRITE) && (NextState == END_WRITE))
          PCounter <= WRITE_HOLD;       
        
        //----------------------------------------------------------
        else if((State == IDLE) && (NextState == PREPARE_READ))
          PCounter <= READ_SETUP;
        else if((State == PREPARE_READ) && (NextState == READ))
          PCounter <= READ_PULSE;
        else if((State == READ) && (NextState == END_READ))
          PCounter <= READ_HOLD;
        
        //----------------------------------------------------------          
        else if(PCounter != 0)
          PCounter <= PCounter - 1'b1;
      end
  end

endmodule

Вот результат моделирования работы описаний



Как мы видим, ведут они себя одинаково и соответствую желаемым временным диаграммам памяти. Описание в 3 always блока получилось в большее число блоков, иначе блок задания выходов сильно бы усложнился. Заряжать счетчик, сохранять данные для записи в память и обратно нам надо в одном конкретном такте. Для этого нужно либо добавлять однотактные состояния в автомат, либо создавать конструкции выделения этих тактов. Я предпочел второй вариант и вынес конструкции в отдельные блоки, чтобы не усложнять блок задания выходов.

Так или иначе, в этом примере мы видим насколько больше получается описание в 3 always блока (брутто 287 строк против 181). В нем больше мест, где можно совершить ошибку. Отлаживается оно тоже сложнее. Если вы просматривали эти два описания, то могли заметить, что в первом описании вся картина работы видна сразу, а во втором мы всегда видим какую то часть. Полная картина разнесена по всему файлу.

Разбирать так описанный чужой автомат отдельное «удовольствие». Особенно, если условия переходов зависит от выходов автомата (в нашем случает состояние задает счетчик, а счетчик задает условие перехода). Сначала смотришь значение выходов в текущем состоянии, потом летишь в блок переходов и смотришь, куда мы переходим в данном состоянии и при таком значении выходов. Потом опять мотаешь в блок задания выходов, смотришь их изменения от нового состояния.

Автомат описанный в 2 always блока анализируется чуть проще, у него выходы и переходы часто лежат рядом в одном блоке, но ровно до момента, пока мы не захотим защелкнуть состояние выхода в регистр. Тут начинают появляться регистры с парными комбинаторными значениями их следующего состояния, потом начинают добавляться условия защелкивания и мы возвращаемся к исходному упражнению.

Надеюсь теперь описание с 1 always блоком, потеряет титул «самое плохое описание, старайтесь его избегать». Конечно не стоит всегда использовать только этот вид описания. При ветвистых сетях переходов 1 блоковое описание правда неудобно. Быстро разрастается по объему кода и перестает управляться. Однако выкидывать его из арсенала разработки однозначно не стоит.

Комментарии (12)


  1. nerudo
    07.02.2018 12:13

    Разбиение на 1/2/3 процесса/блока, как у вас показано в начале текста — вещь чисто техническая и ни на что не влияющая. Более глубокое различие в концепции описания — это выбор между формированием внутри КА только набора управляющих сигналов (как у вас в примере с 3, а на самом деле 5 блоками), либо интеграция более сложной логики вовнутрь описания (как в примере с 1 блоком). И вот здесь выбор может быть более сложным в зависимости от сложности и архитектуры создаваемого решения. Хотя мне тоже, признаюсь, более удобным кажется слепить все вместе ;)


  1. GolikovAndrey Автор
    07.02.2018 12:38

    Я пытаюсь говорить про разницу между 2(3) и 1 частным описаниями автоматов. Технически, конечно, можно комбинаторный always блок написать через

    assign r = (c1) ? a  : 
               (с2) ? b  : 
               ....  
    

    но это жестоко).
    Поэтому 2 частному автомату я определяю 2 always блока. Я не пытаюсь числом always блоков определить тип описания, скорее тип описания определяет минимальный набор блоков.


  1. Kopart
    07.02.2018 14:01

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

    В существенном количестве случаев можно обойтись без организации FSM, там где подойдет реализация в 1 always block. Тк FSM будет нас ограничивать в подходе с единой синхронизации состояний (чем-то сходным с софтверным подходом, где все последовательно), и не позволит использовать «распределенную синхронизацию» только где надо.

    Те может глубинная идея в книге была в том, что там где можно описать в 1 always block FSM сам по себе и не нужен, тк ограничивает.


    1. GolikovAndrey Автор
      07.02.2018 21:44

      Все может быть, я прочел ту идею, которую прочел. И поделился тем о чем подумал. Я не настаиваю ни на какой интерпритации:)


  1. Tausinov
    07.02.2018 21:45

    Есть FSM Милли и FSM Мура, а сколько брать процессов для их описания — уже формальности.


    1. GolikovAndrey Автор
      07.02.2018 21:48

      Я не затрагивал виды автоматов, кстати, их больше двух. Я сосредоточился именно на способе их описания. Так что может и формальность, но именно на формальности я и сосредоточился)


  1. Khort
    08.02.2018 09:16

    По типу автоматов — в статье описан классический алгоритм Мура (автомат Мили можно свести к Мура), поскольку язык верилог под конструкцией always понимает элемент памяти типа мастер-слейв (вообще и защелку тоже, но в контексте поста — триггер), и другие типы автоматов (которые нельзя свести к Мура) таким кодом не опишешь. Можно было бы для расширения кругозора описать еще автомат Хаффмана, но тогда без always, поскольку он асинхронен (использует элемент памяти на линии задержки).

    Было бы полезно не просто запостить некий верилог-код автомата, а показать как сделать так, чтобы синтезаторы synopsys/cadence понимали, что это за автомат. Зачем? Да потому что в тексте нигде нет ни слова про минимизацию автоматов, а так хоть тулам на откуп отдать можно было бы. Поэтому статья Sunburst design (которую автор критикует в начале) реально рулит, а этот пост — нет.


    1. GolikovAndrey Автор
      08.02.2018 09:46

      Если сделать все что вы написали, то получиться еще одна статья с которой мы начали. Мне жаль, что мне не удалось донести до Вас основную идею моего поста :)


      1. Khort
        08.02.2018 10:05

        А в чем идея? В стиле описания? Если бы Вы показали, что синтезатор два олвейз-блока минимизирует лучше чем один или три олвейз-блока — был бы аргумент. Вместо этого ведутся рассуждения о числе строк кода. Вы знаете, что индусам платят за число строк кода? Они сделают из Вашей публикации диаметрально противоположный вывод. О вкусах, как говорят, не спорят.


        1. GolikovAndrey Автор
          08.02.2018 10:18

          Я думаю не стоит сокрушаться, что я не написал в статье то, что вы хотели бы видеть. Это же дает вам огромные возможности для своих статей :)

          Если говорить про оптимизацию (по разным параметрам) то сильнее влияют способы кодирования состояний и типы автоматов, чем число always блоков при их описании.

          И да, я пытаюсь говорить о стилях и строках, потому что считаю немаловажными параметрами качества кода: его читаемость, управляемость, поддерживаемость.


  1. eloiman
    09.02.2018 17:39

    Спасибо автору за статью, очень интересно! Во-первых, как я понимаю код автора содержит генерацию латчей, что не очень хорошо, и даже плохо ;) Код, по хорошему, надо исправить. Например, для первого примера с 2мя блоками в начале второго блока должна быть строка «NextState <= State;» Иначе будет latch. По крайней мере, на VHDL это так. Во-вторых, множественны параллельные процессы необходимы для более сложных алгоритмов. Например, в примере с памятью, события от модуля памяти могут приходить в другом clock domain, что требует дополнительных процессов для синхронизации доменов. В-третьих, можно заметить что описанная FSM всегда синхронизированна по clk, что не оптимально по скорости. Для оптимизации нужно применять комбинаторную логику с синхронизацией не по клоку а по событиям, что конечно, порождает большое количество параллельных процессов. Рекомендую обратиться к коду примеров Xilinx что бы «ужаснуться» от количеству процессов даже в простом коде ;)


    1. GolikovAndrey Автор
      09.02.2018 17:55

      Пожалуйста, рад что вам понравилось.

      Во-первых, как я понимаю код автора содержит генерацию латчей, что не очень хорошо, и даже плохо

      Ну во-первых не содержит, потому что есть состояние дефолт. Латч появляется если существуют сочетания условий в которых комбинаторная переменная не имеет описания выхода и следовательно вынуждена сохранять свое значение. Добавление в начале значения по умолчанию — это защита на случай если что-то забудем описать в вариантах.
      Во-вторых у ксалинкса есть примитивы латчей, у альтеры их симуляция, а в асиках их применяют довольно таки часто.
      По крайней мере, на VHDL это так

      И на ВХДЛ тоже.

      Во-вторых, множественны параллельные процессы необходимы для более сложных алгоритмов. Например, в примере с памятью, события от модуля памяти могут приходить в другом clock domain, что требует дополнительных процессов для синхронизации доменов.

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

      В-третьих, можно заметить что описанная FSM всегда синхронизированна по clk, что не оптимально по скорости.

      Неужели вы предлагаете на клоковые входы триггеров заводить комбинаторные сигналы. И это в FPGA? И это для увеличения скорости?
      Рекомендую обратиться к коду примеров Xilinx что бы «ужаснуться» от количеству процессов даже в простом коде ;)

      Вы либо только начинаете, либо уже так продвинуты в описании железа, что на моем уровне мне трудно понять что вы имеете в виду %). Но все же я больше склоняюсь к первому, чем ко второму).
      В любом случае, спасибо за мнение :)