グレイコードを使わない非同期 FIFO の実装例 (Syncronizer 編)







# はじめに


FPGA や ASIC 等で異なるクロックで動作する回路同士でデータをやりとりする場合には、非同期 FIFO (Asynchronous First-In First-Out) を使って安全にデータを受け渡すことがあります。

非同期 FIFO を設計する際にはグレイコードを使う方法がよく使われますが、この記事ではグレイコードを使わない非同期 FIFO の実装方法について、２回にわけてご紹介していきます。



1. [ 概要編](https://qiita.com/ikwzm/items/df1a9ca8b6359393b26a)  
2.  __Syncronizer 編 (この記事)__      
2.1  Syncronizer の概要   
2.2 Syncronizer の詳細設計   
2.3 Syncronizer の問題点
3. [ Pending Register 編](https://qiita.com/ikwzm/items/8b714020ddec847f83a6) 




# Syncronizer の概要



## 位置付け


グレイコードを使わない非同期 FIFO では、Syncronizer は入力側と出力側で各種情報を伝達する役割をします。（下の図の赤字部分）


![Fig.1 グレイコードを使わない非同期 FIFO での Syncronizer の位置付け](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F873edae3-1fa0-4ce3-a347-97556548a452.jpeg "Fig.1 グレイコードを使わない非同期 FIFO での Syncronizer の位置付け")

Fig.1 グレイコードを使わない非同期 FIFO での Syncronizer の位置付け

<br />




## 仕組み


この記事で紹介する Syncronizer では、異なるクロックで動作する回路同士で情報を安全にやりとりするために、request/acknowledge によるハンドシェイクをしています。

Syncronizer は次の図のような構成になっています。


![Fig.2 Syncronizerの構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F9bb3ef88-7d85-47a8-a91e-b82e40f5ba34.jpeg "Fig.2 Syncronizerの構成")

Fig.2 Syncronizerの構成

<br />



送信側は、情報(I_DATA と I_VALID)を送信する際に request 信号(SYNC_REQ)を受信側に送ると共に情報をレジスタ(SYNC_DATA と SYNC_VALID)に保持します。そして受信側からの acknowledge 信号(SYNC_ACK)が来るまでレジスタ(SYNC_DATA と SYNC_VALID)の値を変更しません。

受信側は、送信側から届いた request 信号(SYNC_REQ)を受け取り、情報(SYNC_DATA と SYNC_VALID)を自分のレジスタ(O_DATA と O_VALID)に保存してから、送信側に acknowledge 信号(SYNC_ACK)を返します。

この仕組みによって、受信側が自分のレジスタ(O_DATA と O_VALID) への入力が完了するまで情報(SYNC_DATA と SYNC_VALID) の値は変化することがないので、メタステーブルを心配することなく安全に情報(I_DATA の値と I_VALID の値)を自分のレジスタ(O_DATA と O_VALID) にロードすることが出来ます。


![Fig.3 Syncronizer のタイミングチャート](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F00b909cc-bf4c-4254-8a03-8a68c8be3a59.jpeg "Fig.3 Syncronizer のタイミングチャート")

Fig.3 Syncronizer のタイミングチャート

<br />






# Syncronizer の詳細設計 (送信側)



## 送信側の構成


Syncronizer の送信側は次のような構造になっています。


![Fig.4 送信側の構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2Ff8076ae4-a57c-4677-b478-bf4b824d0b5a.jpeg "Fig.4 送信側の構成")

Fig.4 送信側の構成

<br />


## 送信側のステートマシン



## ステートマシンの状態遷移


送信側のステートマシンは次の図の左のように状態遷移します。


![Fig.5 送信側のステートマシン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2Ff5888032-bb32-4150-be01-e2f640794714.jpeg "Fig.5 送信側のステートマシン")

Fig.5 送信側のステートマシン

<br />

ステートマシンは VHDL で次のように記述しています。


```VHDL:syncronizer.vhd
 	 I_BLK: block
		signal   curr_state	 	: std_logic_vector(1 downto 0);
		signal   next_state	 	: std_logic_vector(1 downto 0);
		signal   sync_start		: std_logic;
		signal   sync_ready		: std_logic;
		signal   sync_ack_i		: std_logic;
	begin
		next_state(0) <= '1' when (curr_state = "00" and sync_start = '1') or
 		                                (curr_state = "01") or
		                                (curr_state = "11" and sync_start = '0') else '0';
		next_state(1) <= '1' when (curr_state = "01" and sync_ack_i = '1') or
		                                (curr_state = "11") or
		                                (curr_state = "10" and sync_ack_i = '1') else '0';
		process (I_CLK, RST) begin
			if     (RST   = '1') then  curr_state <= (others => '0');
			elsif  (I_CLK'event and I_CLK = '1') then
				if (I_CLR = '1') then curr_state <= (others => '0');
				else                  curr_state <= next_state;
				end if;
			end if;
		end process;
		(中略)
	end block;
```




ステートマシンの状態を one-hot ではなく、このように定義しているのには理由があります。理由は SYNC_ACK 信号が送信側のクロックとは同期していない所謂非同期信号だからです。このようにステートマシンを定義することにより、非同期信号の遅延時間のズレやメタステーブルによる誤動作が起きないようにしています。




## 遅延時間がズレは考慮しなくてよい理由


まず、 SYNC_ACK 信号 を Flip-Flop で叩かずに直接シーケンサに入力している場合を考えます。この場合、sync_ack_i = SYNC_ACK です。

そして SYNC_ACK 信号の変化が影響する Flip-Flop が curr_state[1] だけであることに注目してください。非同期信号の変化の影響をうける Flip-Flop が一つしかないため、そもそも Flip-Flop 間での遅延時間のズレ自体が生じません。


![Fig.6 SYNC_ACK信号の変化は curr_state[1] だけに影響を与える](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F195c1be6-2621-4179-b7a0-cd7fd7b5dab2.jpeg "Fig.6 SYNC_ACK信号の変化は curr_state[1] だけに影響を与える")

Fig.6 SYNC_ACK信号の変化は curr_state[1] だけに影響を与える

<br />




## メタステーブル状態による誤動作対策



### SYNC_ACK を Flip-Flop で叩かない場合


SYNC_ACK 信号は非同期信号なので、curr_state[1] はメタステーブル状態になる可能性があります。


![Fig.7 メタステーブル状態になっても正しく状態遷移する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F9fae6bcc-420f-49bb-8946-066fa37ce079.jpeg "Fig.7 メタステーブル状態になっても正しく状態遷移する")

Fig.7 メタステーブル状態になっても正しく状態遷移する

<br />



* curr_state=01 の時、SYNC_ACK 信号が 0 から　1に 変化して curr_state[1] がメタステーブル状態になったとします。その結果、curr_state[1] がメタステーブル状態から復帰して出力値が 0 または 1 のどちらかになります。(Fig.7 の1)
* curr_state[1] の値が 0 になった場合は curr_state=01 になり、結果としてステートは遷移せずに引き続き SYNC_ACK が 1 になるのを待ちます。そして次のクロックで curr_state=11 に遷移するはずです。(Fig.7 の 2)
* curr_state[1] の値が 1 になった場合は curr_state=11 になり、結果としてはステートが次の状態に遷移します。(Fig.7 の 3)
* curr_state=10 の時、SYNC_ACK 信号が 1 から 0 に 変化して curr_state[1] がメタステーブル状態になったとします。その結果、curr_state[1] がメタステーブル状態から復帰して出力値が 0 または 1 のどちらかになります。(Fig.7の4)
* curr_state[1] の値が 1 になった場合は curr_state=10 になり、結果としてステートは遷移せずに引き続き SYNC_ACK が 0 になるのを待ちます。そして次のクロックで curr_state=00 に遷移するはずです。(Fig.7 の 5)
* curr_state[1] の値が 0 になった場合は curr_state=00 になり、結果としてはステートが次の状態に遷移します。(Fig.7 の 6)



このようにシーケンサを設計することにより、メタステーブル状態になったとしても正しい状態遷移が行われます。



ただし、メタステーブルからの復帰時間については、十分考慮しておく必要があります。何故なら、curr_state[1] の出力がメタステーブルからの復帰時間の分だけ遅れるからです。

同期式回路の設計では、 Flip-Flop の出力と入力の間の遅延時間が周期時間以下になるように、論理合成や配置配線をしますが、これらには通常、メタステーブルからの復帰時間は考慮されません。

curr_state[1] をデコードして各種制御信号を生成して別の Flip-Flop に入力するような場合は、別途、メタステーブルからの復帰時間を考慮した論理合成や配置配線をしなければなりません。そのためには、動作周波数やデバイス(ASIC や FPGA など) の特性やツールの使い方に精通している必要があります。



この状況をもう少し改善する方法として、SYNC_ACK 信号を Flip-Flop で1回叩いてから生成した場合を次節で説明します。




### SYNC_ACK を1段の Flip-Flop で叩いた場合


SYNC_ACK 信号を次の図のように1段の Flip-Flop で叩いた場合を考えます。


![Fig.8 SYNC_ACK を1段の Flip-Flop で叩いた場合](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F5e5fcabe-c0f7-4332-a581-ce277b5b0abc.jpeg "Fig.8 SYNC_ACK を1段の Flip-Flop で叩いた場合")

Fig.8 SYNC_ACK を1段の Flip-Flop で叩いた場合

<br />



この場合、メタステーブル状態になる可能性があるのは sync_ack_i です。したがってメタステーブル状態からの復帰時間を考慮しなければならないのは、sync_ack_i の Flip-Flop 出力から curr_state[1] の Flip-Flop 入力の間だけで済みます。また、curr_state[1] を生成するためのコンビネーション回路は、３入力１出力の簡単な論理回路です。

このような1対１の簡単な回路であれば、おそらく論理合成は特に問題ないでしょう。配置配線時に指定する遅延時間の設定も簡単に済むか、あるいは通常の同期式回路の設定で行って配置配線後のレポートを確認するくらいで済みます。



それでも気になる場合は、次節で紹介するような SYNC_ACK 信号を Flip-Flop で2回叩いて使用する方法があります。




### SYNC_ACK を複数段の Flip-Flop で叩いた場合


SYNC_ACK 信号を次の図のように2段の Flip-Flop で叩いた場合を考えます。


![Fig.9 SYNC_ACK を2段の Flip-Flop で叩いた場合](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F6ac90b26-579f-490e-95a6-26a6089268de.jpeg "Fig.9 SYNC_ACK を2段の Flip-Flop で叩いた場合")

Fig.9 SYNC_ACK を2段の Flip-Flop で叩いた場合

<br />



このようにすることで、SYNC_ACK 信号を入力している１段目の Flip-Flop がメタステーブル状態になったとしても、２段目の Flip-Flop のセットアップ時間前までに安定するならば、sync_ack_i はメタステーブル状態になりません。

しかし、２段目の Flip-Flop のセットアップ時間前までに安定しない場合も考えられます。その場合は、さらに複数段の Flip-Flop で非同期信号を受けると良いでしょう。詳細は以下の記事を参照してください。



* [『同期式回路で非同期信号を扱う際の注意点』 @Qiita](https://qiita.com/ikwzm/items/83e55d85e4e285aa4485)


# Syncronizer の詳細設計 (受信側)



## 受信側の構造


Syncronizer の受信側は次のような構造になっています。


![Fig.10 受信側の構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2Fccc9f439-01c2-48a7-b7a5-060752de9611.jpeg "Fig.10 受信側の構成")

Fig.10 受信側の構成

<br />


## 受信側のステートマシン



## ステートマシンの状態遷移


受信側のステートマシンは次の図の左のように状態遷移します。


![Fig.11 受信側のステートマシン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F59c0f0ff-ba04-4ebd-8063-eab4063c73fd.jpeg "Fig.11 受信側のステートマシン")

Fig.11 受信側のステートマシン

<br />

ステートマシンは VHDL で次のように記述しています。


```VHDL:syncronizer.vhd
 	 O_BLK: block
		signal   curr_state		: std_logic_vector(1 downto 0);
		signal   next_state		: std_logic_vector(1 downto 0);
		signal   sync_req_i		: std_logic;
	begin
		next_state(0) <= '1' when (curr_state = "00" and sync_req_i = '1') or
		                             (curr_state = "01") or
		                             (curr_state = "11" and sync_req_i = '1') else '0';
		next_state(1) <= '1' when (curr_state = "01") or
		                             (curr_state = "11") else '0';
		process (O_CLK, RST) begin
			if     (RST   = '1') then curr_state <= (others => '0');
			elsif  (O_CLK'event and O_CLK = '1') then
				if (O_CLR = '1') then curr_state <= (others => '0');
				else                  curr_state <= next_state;
				end if;
			end if;
		end process;
(中略)
	end block;
```




ステートマシンの状態を one-hot ではなく、このように定義しているのには理由があります。理由は SYNC_REQ 信号が受信側のクロックとは同期していない所謂非同期信号だからです。このようにステートマシンを定義することにより、非同期信号の遅延時間のズレやメタステーブルによる誤動作が起きないようにしています。


## 遅延時間がズレは考慮しなくてよい理由


まず、 SYNC_REQ 信号を Flip-Flop で叩かずに直接シーケンサに入力している場合を考えます。この場合、sync_req_i = SYNC_REQ です。

そして SYNC_REQ 信号の変化が影響する Flip-Flop が curr_state[0] だけであることに注目してください。非同期信号の変化の影響をうける Flip-Flop が一つしかないため、そもそも Flip-Flop 間での遅延時間のズレ自体が生じません。


![Fig.12 SYNC_REQ信号の変化は curr_state[0] だけに影響を与える](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F06055554-d222-4096-9506-08538964e4d0.jpeg "Fig.12 SYNC_REQ信号の変化は curr_state[0] だけに影響を与える")

Fig.12 SYNC_REQ信号の変化は curr_state[0] だけに影響を与える

<br />




## メタステーブル状態による誤動作対策



### SYNC_REQ を Flip-Flop で叩かない場合


SYNC_REQ 信号は非同期信号なので、curr_state[0] はメタステーブル状態になる可能性があります。



* curr_state=00 の時、SYNC_REQ 信号が 0 から　1に 変化して curr_state[0] がメタステーブル状態になったとします。その結果、curr_state[0] がメタステーブル状態から復帰して出力値が 0 または 1 のどちらかになります。
* curr_state[0] の値が 0 になった場合は curr_state=00 になり、結果としてステートは遷移せずに引き続き SYNC_REQ が 1 になるのを待ちます。そして次のクロックで curr_state=01 に遷移するはずです。
* curr_state[0] の値が 1 になった場合は curr_state=01 になり、結果としてはステートが次の状態に遷移します。
* curr_state=11 の時、SYNC_REQ 信号が 1 から 0 に 変化して curr_state[0] がメタステーブル状態になったとします。その結果、curr_state[0] がメタステーブル状態から復帰して出力値が 0 または 1 のどちらかになります。
* curr_state[0] の値が 1 になった場合は curr_state=11 になり、結果としてステートは遷移せずに引き続き SYNC_REQ が 0 になるのを待ちます。そして次のクロックで curr_state=10 に遷移するはずです。
* curr_state[0] の値が 0 になった場合は curr_state=10 になり、結果としてはステートが次の状態に遷移します。



このようにシーケンサを設計することにより、メタステーブル状態になったとしても正しい状態遷移が行われます。



ただし、メタステーブルからの復帰時間については、十分考慮しておく必要があります。何故なら、curr_state[0] の出力がメタステーブルからの復帰時間の分だけ遅れるからです。

同期式回路の設計では、 Flip-Flop の出力と入力の間の遅延時間が周期時間以下になるように、論理合成や配置配線をしますが、これらには通常、メタステーブルからの復帰時間は考慮されません。

curr_state[0] をデコードして各種制御信号を生成して別の Flip-Flop に入力するような場合は、別途、メタステーブルからの復帰時間を考慮した論理合成や配置配線をしなければなりません。そのためには、動作周波数やデバイス(ASIC や FPGA など) の特性やツールの使い方に精通している必要があります。



この状況をもう少し改善する方法として、SYNC_REQ 信号を Flip-Flop で1回叩いてから生成した場合を次節で説明します。




### SYNC_REQ を1段の Flip-Flop で叩いた場合


SYNC_REQ 信号を次の図のように1段の Flip-Flop で叩いた場合を考えます。


![Fig.13 SYNC_REQ信号1段の Flip-Flop で叩いた場合](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F6b2c7190-a08a-4976-a062-a55329ae030f.jpeg "Fig.13 SYNC_REQ信号1段の Flip-Flop で叩いた場合")

Fig.13 SYNC_REQ信号1段の Flip-Flop で叩いた場合

<br />



この場合、メタステーブル状態になる可能性があるのは sync_req_i です。したがってメタステーブル状態からの復帰時間を考慮しなければならないのは、sync_req_i の Flip-Flop 出力から curr_state[0] の Flip-Flop 入力の間だけで済みます。また、curr_state[0] を生成するためのコンビネーション回路は、３入力１出力の簡単な論理回路です。

このような1対１の簡単な回路であれば、おそらく論理合成は特に問題ないでしょう。配置配線時に指定する遅延時間の設定も簡単に済むか、あるいは通常の同期式回路の設定で行って配置配線後のレポートを確認するくらいで済みます。



それでも気になる場合は、次節で紹介するような SYNC_REQ 信号を Flip-Flop で2回叩いて使用する方法があります。




### SYNC_REQ を2段の Flip-Flop で叩いた場合


SYNC_REQ 信号を次の図のように2段の Flip-Flop で叩いた場合を考えます。


![Fig.14 SYNC_REQ信号2段の Flip-Flop で叩いた場合](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F24981%2F0c5db40e-8c82-4059-86b9-e224021b46a8.jpeg "Fig.14 SYNC_REQ信号2段の Flip-Flop で叩いた場合")

Fig.14 SYNC_REQ信号2段の Flip-Flop で叩いた場合

<br />



このようにすることで、SYNC_REQ 信号 を入力している１段目の Flip-Flop がメタステーブル状態になったとしても、２段目の Flip-Flop のセットアップ時間前までに安定するならば、sync_req_i はメタステーブル状態になりません。

しかし、２段目の Flip-Flop のセットアップ時間前までに安定しない場合も考えられます。その場合は、さらに複数段の Flip-Flop で非同期信号を受けると良いでしょう。詳細は以下の記事を参照してください。



* [『同期式回路で非同期信号を扱う際の注意点』 @Qiita](https://qiita.com/ikwzm/items/83e55d85e4e285aa4485)




# Syncronizer の問題点


この記事で紹介した Syncronizer は、異なるクロックで動作する回路同士で情報を安全にやりとりするために、request/acknowledge によるハンドシェイクをしています。しかし、この方法には一つ問題点があります。それは、ハンドシェイクに時間がかかることです。ハンドシェイクしている間は新たなデータを送受信することができません。

この問題を解決するために、ペンディングレジスタを使います。詳細は次の記事を参照してください。



* [『グレイコードを使わない非同期 FIFO の実装例 (Pending Register 編)』 @Qiita](https://qiita.com/ikwzm/items/8b714020ddec847f83a6)


# 参考


* https://github.com/ikwzm/PipeWork/blob/master/src/components/syncronizer.vhd
* [『同期式回路で非同期信号を扱う際の注意点』 @Qiita](https://qiita.com/ikwzm/items/83e55d85e4e285aa4485)