VMC PL Math Sequencer デザイン

Model Composer を使用した PID コントローラー デザイン - Versal ACAP 向け (XAPP1376)

Document ID
XAPP1376
Release Date
2022-03-09
Revision
1.0 日本語

C または C++ を使用して制御アルゴリズムを記述し、オンザフライで同時にアルゴリズムを変更できます。たとえば、PID、PI、またはリードラグ タイプのコントローラーを C および C++ で記述できます。制御アルゴリズムは、乗算、加算、減算、飽和、除算など一連の算術演算で構成されます。C および C++ を使用すると、メモリ (中間データおよび命令格納用)、入力 (PID コントローラーのサンプルに必要な W および Y 入力など)、算術演算子、および出力 (サーボ モーターを制御する DAC 駆動用の PWM デューティ サイクル) を備えたステート マシンを容易に記述できます。PID 制御ループの W および Y 入力など、入力データが到達すると Math Sequencer の動作が開始し、その動作が完了すると PWM が更新されます。次の図に、PID アルゴリズムに最適化された Math Sequencer の例を示します。

図 1. Math Sequencer のブロック図

LUTRAM に保存された命令アレイがマルチプレクサー A および B の選択を制御します。選択された算術演算は RMW (Read-Modify-Write) レジスタに保存する必要があります。この命令アレイは、どの SPFP 算術演算 (飽和、バイパス、乗算、または加算) が必要かも制御します。次の図に、この Math Sequencer の命令ビットの構成を示します。

図 2. Math Sequencer 命令

クロック サイクルは増加しますが、必要な演算ハードウェア量を削減する方法があります。たとえば、 y = (a – b) の場合、 b × –1 -> b (b に –1 を掛けた値をレジスタ b に格納) の後、 a + b -> bという 2 段階の演算が必要となります。算術演算の結果はレジスタ アレイ A または B に格納されますが、これは算術演算の後に必要となる次のオペランド入力には望ましくありません。この問題を回避するには、データをレジスタ アレイ A から B へ、またはレジスタ アレイ B から A へ移動するバイパス命令が必要です。追加のアルゴリズムの演算性能を拡張し、クロック サイクルを削減するために、シフト、除算、平方根、またはその他の演算時にカスタム演算子を命令パイプラインに追加できます。この C++ ベースの Math Sequencer を使用した SPFP PID の例で必要な演算子は、飽和、バイパス、乗算、加算のみです。

Math Sequencer の C++ は非常に単純で、C で記述すると簡単に理解できます。

#include "ms.h"

void ms(float w_in, float y_in, float &pwm) {

	// for register & instruction details see math_sequencer_rv2.xls (MS Excel Spreadsheet)

	// A mux  data
	static float a_mux[] = {0, Gi, Gd, c, Gp, 0, 0, 0, 0, 0, 0, minus1, plus1, zeroc};
	// B mux data
	static float b_mux[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, minus1, plus1, zeroc};
	// constant definitions
#pragma HLS ARRAY_PARTITION variable=b_mux complete dim=1

	// load data from interface
	a_mux[5] = (float) w_in; // cast variable to float
	a_mux[6] = (float) y_in;

	// setup instructions
	unsigned short mnemonics[23] = {0x6CC2,0x5B31,0x633,0x2652,0x1662,0x3442,0xB272,0x82A1,0x7C42,0x7A11,0x93B1,0xA5A1,0xA03,0x4642,0xD8B5,0xA791,0x79A1,0xA23,0x8074,0x9084,0x7D5,0x8E5};
	const short num_instr = 22;

	ap_uint<16> instruction;
	ap_uint<4> instr_sel; // instruction mux controls
	ap_uint<4> store;
	ap_uint<4> dsel_b, dsel_a;

	float a_sel_data, b_sel_data; // variables for data management & results storage
	float op_results; // 32 bit results
	float fsat_o; // float data types needed to support saturate


		instruction_loop : for (short inst_loop = 0; inst_loop < num_instr; inst_loop++) {
			// split up instruction into tasks
#pragma HLS PIPELINE
			instruction = (ap_uint<16>) mnemonics[inst_loop]; 

			// split out instructions into sub blocks
			instr_sel = instruction & 0x000F; // instruction select
			store = (instruction & 0x00F0) >> 4; // store results where?
			dsel_b = (instruction & 0x0F00) >> 8; // source B side mux from what register?
			dsel_a = (instruction & 0xF000) >> 12; // source A side mux from what register?

			// determine A select
			a_sel_data = a_mux[dsel_a];

			// determine B select
			b_sel_data = b_mux[dsel_b];

			switch(instr_sel) {
				case 0: break; // invalid instruction
				case 1: op_results = a_sel_data + b_sel_data; break; // a+b
				case 2: op_results = a_sel_data * b_sel_data; break; // a*b
				case 3: if((b_sel_data >= min_limit) && (b_sel_data <= max_limit)) // saturate
			        		fsat_o = b_sel_data;
			    		else if (b_sel_data < min_limit)
			    			fsat_o = min_limit;
			    		else if (b_sel_data > max_limit)
			    			fsat_o = max_limit;
						op_results = fsat_o; break;
				case 4: op_results = a_sel_data; break; // bypass a
				case 5: op_results = b_sel_data; break; // bypass b
				// case 6: op_results = a_sel_data / b_sel_data; break; // a/b
				break;
			} // end instr_sel

			switch(store) {
				case 0: b_mux[8] = op_results; break; // yi
				case 1: b_mux[7] = op_results; break; // yd
				case 2: b_mux[1] = op_results; break; // pwm
				case 3: b_mux[6] = op_results; break; // error
				case 4: a_mux[7] = op_results; break; // pid_mult
				case 5: a_mux[8] = op_results; break; // x1; a_mux
				case 6: a_mux[9] = op_results; break; // x2; a_mux
				case 7: b_mux[2] = op_results; break; // prev_x1
				case 8: b_mux[3] = op_results; break; //prev_x2
				case 9: b_mux[9] = op_results; break; // pid_addsub
				case 10: b_mux[10] = op_results; break; // pid_addsub2
				case 11: a_mux[10] = op_results; break; // tmp_a; a_mux
				case 12: b_mux[11] = op_results; break; // tmp_b
				case 13: b_mux[4] = op_results; break; // prev_yd
				case 14: b_mux[5] = op_results; break; // prev_yi
				case 15: break; // invalid
			} // end store results

		} // end instruction_loop
	pwm = b_mux[1]; // PWM is a pass by reference variable

} // end math sequencer

VMC では、ユーザーが作成した C および C++ デザインの機能開発とデバッグが可能です。MATLAB Simulink 環境での DSP 開発に一般的に使用される Simulink スコープ、ディスプレイ、データ ログ、MATLAB スクリプト、およびその他の手法に加え、従来の C および C++ デバッグ手法 (GNU デバッガーまたは Microsoft Visual C) と printf オプションも利用できます。

マイクロコード命令の作成を簡単にするため、PID シーケンスに必要な 16 進数値の生成を支援する次のような Microsoft Excel スプレッドシートを使用しました。

図 3. Math Sequencer 命令シーケンスの生成に使用する Excel スプレッドシート

ここでも、基準となる Simulink リファレンス モデルと Math Sequencer の間には機能やダイナミック レンジに相違がないことが論理シミュレーションで確認できます。

図 4. Simulink と Math Sequencer のシミュレーション結果の比較

次の図に示すように、2 つの HLS #pragma 命令を C++ コードに適用することにより、次の PL 実装はクロック レート = 1∕1.567e–9 = 638.2 MHz、サンプル レート = 1∕(83 × 1.567e–9) = 7.7 MSPS となります。

図 5. PL への実装結果
図 6. Math Sequencer 実装の結果

Math Sequencer の長所は、アルゴリズム要件に基づいて演算子を容易に変更できることにあります。たとえば、除算演算子や平方根関数を追加してマグニチュードを計算できます。また、Math Sequencer なら後で命令シーケンスを変更する場合でもハードウェア実装を変更する必要がありません。さらに、Math Sequencer はレイテンシとのトレードオフでリソース使用量を削減できます。次の表に、HLS ツールボックスを使用した実装と C++ Math Sequencer で実装した PID 制御ループを直接比較した結果を示します。

表 1. リソース、レイテンシ、クロック周波数、およびサンプル レートの比較
  DSP LUT FF ブロック RAM レイテンシ (クロック) クロック (MHz) サンプル レート (MSPS)
VMC HLS ツールボックス 5 565 505 0 69 472 6.8
C++ Math Sequencer 4 513 962 0 83

638.2

7.7