考え方としてのステートマシン記述スタイル
FPGAの固有の並列性を克服する必要があり、回路を順番に動作させたいという要望がある場合、アルゴリズムに従って、有限状態マシンが助けになります。
たとえば、仕事は非常に人気があります: Clifford E. Cummings、NC-VerilogおよびBuildGatesを使用した効率的な合成可能な有限状態機械設計の基礎 。 専門家が有限状態マシンを正しく記述する方法について議論することにしたときはいつでも、誰かがこの出版物を確実に手に入れるでしょう 。
この記事は非常に権威あるので、多くの人は著者の議論を分析しようとさえしません。 特に、専門家は常に2つの部分からなる有限状態マシンの記述方法を使用しているという意見があります。つまり、2つのブロックでの有限状態マシンの記述です。 この声明は白熱した議論を引き起こし続けており、 常に異なるブロック数を持つ有限状態マシンの記述の違いを説明したいと思います。
同僚との会話で、1または2、3常にブロックで有限状態マシンを記述する方法についての議論は、実装されたアルゴリズムのさまざまな表現(認識)、さまざまな種類の思考に関連していることに気付きました。 これを例で示してみます。
この記事はあなたの人生におけるFSMとVerilogについての最初の記事ではないと思うので、ステートマシンとは何か、Verilogでどのように記述されているかについては説明しませんが、ポイントにまっすぐ進みます。
あなたが家に座って、店に行ってパンを買って戻ってくる必要があると想像してください。 このような動作を実装するには、アクションのアルゴリズムシーケンスであるため、次のように記述できます。
- 着替えて外に出る
- 店に行ってパンを買って
- チェックアウト時に支払います
- 家に帰って服を脱ぐ
多くの人にとって、そのような説明は自然に思えます。 プログラム行に類似した明確な一連のアクション。 ただし、同じアルゴリズムは別の方法で説明できます。
- 通りから家に帰る
- 街頭から店まで(パン売り場)
- パンを着てレジに向かう
- チェックアウトの頭から家
しながら
- 私たちが家にいるとき-私たちは服を脱いでいる
- 私たちが外にいるとき-私たちは服を着ています
- 私たちが店にいるとき(パン部門)-パンを取ります
- チェックアウト時に-お金を払います
興味深いことに、少なくとも誰かが、最後のオプションが最初のオプションよりも論理的であると考えましたか?
いずれにせよ、これらの記述はどちらも有限状態機械の形で表現されています。 1つ目は、 常にブロックが1つある説明の形式で、2つ目は2つまたは3つの説明があります。 ブロック2とブロック3の記述は常に双子の兄弟であり、現在私たちにとって重要ではない技術的なニュアンスのみが異なることを予約します。
説明がオートマトンの形式でどのように表示されるかを示します。
マシンの状態があります:HOME_STATE、STREET_STATE、MARKET_STATE、CASHIER_STATE、マシンの出力(アクション)があります:GET_DRESSED_ACT、UNDRESS_ACT、TAKE_BREAD_ACT、PAY_MONEY_ACT
1ブロックの説明は常に次のようになります。
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 @(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つの記述の特徴に注意を払いたいと思います。それは常にブロック数の聖戦の原因です。
1 常にブロック
HOME_STATE: begin Action <= GET_DRESSED_ACT; // State <= STREET_STATE; // end
常に 2つのブロック
HOME_STATE: // begin Action = UNDRESS_ACT; // NextState = STREET_STATE; // end
現在の状態で常に 1のブロックを持つオートマトンは、現在実行していることを気にすることなく、次に実行するアクションを決定します。 現在の状態で常に 2つのマシンは、現在何をしているかを判断し、次に何をするか、以前何をしたかを気にしません。
どちらの動作が優れているか、より正しいかを明確に言うことは不可能です。 すべては、タスク、マシンに実装されたアルゴリズムに強く依存します。 実証するために、状況を変更します。 今、あなたは家 、 仕事 、 バーを持っています 。 あなたは仕事に行き、仕事に行き、 ビールに飲みに行きます。 週末は家から、金曜日は仕事からバーに行きます 。
最初の実装では、 常に 1を使用して 、 職場でビールを飲んだり バー で働いたりしないように、行き先を注意深く監視する必要があります 。 2つ目の実装では、 常に 2つのブロックで、これから保護されます。 ここではすべてが明確に定義されています。 仕事中の状態-私たちは働い ています、バーの状態-私たちはビールを飲みます 。
一方、仕事からバーに来て、 常に 2ブロックの説明では、ビールを飲むしか仕方がありません。 この実装の状態は、 バーの動作を厳密に修正します 。 ビールを飲みます 。 また、 常に 1 をブロックする説明では、バーのアクションは 、過去の状態を終了するときに決定されます。 仕事からバーに行って ウィスキーを飲むことができます。 家からバーへの2回目の旅行はパーティーで終わります。 バーの現在の状態は決して制限しません。
どちらのマシンもプロジェクトに適しています。特定の状況でどの機能が役立つかを正しく判断する必要があります。
遷移の複雑なネットワークがあり、さまざまな状態でさまざまな状態になる場合、 常に 2つのブロックを持つスキームを使用することは理にかなっています。 次の移行時にマシンの出力のどれを尋ねるかを忘れることはありません。
一方、ほぼ直線的な構造を持つ単純なオートマトンを作成している場合は、 常に 1ブロックの説明を使用できます。 実際、出力でシーケンスを指定し、状態を使用して、順次実行を整理します。 マシンの出力は現在の状態に依存しないため、各状態で値を登録する必要はありません。説明はより簡潔で論理的です。
私たちが始めた記事は、 常に 2と3のブロックを含む記述を使用する必要がある理由をよく示しており、 常に1のブロックを含む記述は最悪としてマークされます。 著者はそのような記述を避けることを推奨します。 したがって、1での記述に常に便利な実際のインターフェースの例を挙げて、このタイプの記述を常にブロックして保護したいと思います。 比較のために、3つのalwaysブロックで説明した同じマシンを提供します。 レジスタ出力信号を使用するため、2ブロックではなく3ブロックが必要です。
そして、非同期メモリへの書き込みを実装するモジュールがあります。 入力モジュールは、読み取りまたは書き込みストローブ、書き込み用のデータとアドレス、読み取り用のアドレスを受け取ります。 モジュールは、読み取りまたは書き込み完了信号と読み取りデータを出力します。 このモジュールは、次の一時的な操作図を使用して、単純な非同期メモリを管理します。
読み取りまたは書き込みストローブ上のモジュールの主な意味は、「一時的な兵舎を展開」し、指定された間隔を維持し、完了時に1つの準備完了信号を発行することです。 次の間隔を守る必要があります
- Rs-読み取り前にアドレスとメモリ選択信号を設定する時間
- Rpは、正しいデータが得られるまでのメモリ出力イネーブル信号の露出時間です。
- Rh-出力解像度を削除した後のメモリ選択信号の露出時間
- Ws-記録する前にアドレス、データ、メモリ選択信号を設定する時間
- Wp-記録許可信号の期間
- Wh-信号イネーブル記録を削除した後のデータとアドレスの時間遅延
入力周波数のクロック数で間隔を測定し、定数READ_SETUP、READ_PULSE、READ_HOLDおよびWRITE_SETUP、WRITE_PULSE、WRITE_HOLDで間隔を設定します。
常に 1つのブロックの説明:
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つの説明:
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つのブロックの記述は、 常により多くのブロックになります。そうしないと、出力を設定するためのブロックがはるかに複雑になります。 1つの特定のクロックサイクルでカウンターを充電し、メモリに書き込むためにデータを保存する必要があります。 これを行うには、シングルサイクル状態をマシンに追加するか、これらのメジャーを強調表示する構造を作成する必要があります。 2番目のオプションを選択し、出力を設定するためにブロックを複雑にしないように、デザインを別のブロックに配置しました。
いずれにしても、この例では、 常に 3つのブロックの記述がどれだけ多いかがわかります(総計287行対181)。 間違える可能性のある場所は他にもあります。 また、デバッグがより難しくなります。 これらの2つの説明を見ると、最初の説明では作品の全体像がすぐに表示され、2番目の説明では常に一部が表示されることに気づいたかもしれません。 ファイル全体に全体像が投稿されます。
このように記述されたエイリアンマシンを分解することは、別個の「喜び」です。 特に、遷移条件がマシンの出力に依存する場合(この場合、状態はカウンターによって設定され、カウンターは遷移の条件を設定します)。 まず、現在の状態の出力の値を見てから、遷移ブロックに飛んで、この状態で出力の値がどこにあるのかを調べます。 その後、出口のタスクのブロックに再び巻き込み、新しい状態からの変化を観察します。
2つの常にブロックで説明されているオートマトンは少し簡単に分析され、その出力と遷移はしばしば1つのブロックに並んでいますが、出力状態をクリックしてレジスタに入れるまでは正確です。 ここで、次の状態のペアの組み合わせ値を持つレジスタが表示され始め、スナップ条件が追加され始め、元の演習に戻ります。
これで、 常に 1のブロックの説明が「最悪の説明、それを避けようとする」というタイトルを失うことを願っています。 もちろん、この種の説明だけを常に使用する必要はありません。 分岐遷移ネットワーク1では、ブロックの説明は本当に不便です。 コードサイズが急速に増大し、管理されなくなります。 しかし、開発兵器からそれを捨てることは間違いなく価値がありません。