この Web ページは、Smart Zynq ボードの機能を探求する小規模プロジェクトのグループに属しています。
はじめに
市場には、SG90、MG90S、MG995、MG996R など、シンプルで低コストな DC サーボモーターがいくつかあります。これらのモーターはホビープロジェクト、特に簡単なロボットの製作を目的としています。こうしたモーターは、単一の PWM 信号で制御されます。
一部のモーターは、通常 180 度といった制限された角度範囲内で回転します。このようなモーターでは、PWM 信号がモーターの角度位置を制御します。別のモーター(「360 度モーター」と呼ばれることが多い)は、連続的に回転できます。このようなモーターでは、PWM 信号が回転の速度と方向を制御します。同じモーターモデルでも、角度制限付きのバリエーションと、連続回転可能なバリエーションの 2 種類が存在することがよくあります。この種のモーターを購入するときは、この違いに注意することが重要です。
このチュートリアルでは、最大 8 つのサーボモーターを Smart Zynq ボードに接続し、簡単な Linux コマンドでこれらのモーターを制御する方法を説明します。また、FPGA 内部にレジスタインターフェースを実装するために、Xillybus のシーク可能なストリーム (seekable streams) を使う方法も紹介します。
説明と写真を簡潔にするため、このチュートリアルで示す電気配線はモーター 1 台分にのみ対応しています。モーターを追加するには、ここに示す同じ部品にワイヤを追加するだけで可能です。
モーターの電気インターフェース
関連するタイプのサーボモーターへの接続は、3 本のワイヤで構成されます。
- 茶色のワイヤはグランドに接続します。
- 赤色のワイヤは +5V 電源に接続します。
- オレンジ色のワイヤは制御信号 (PWM) に接続します。
PWM 信号の周期は 20ms (50 Hz) である必要があります。パルスが High になっている時間が、モーターの位置または角速度を制御します。
ほとんどのデータシートによると、パルス幅は 1ms から 2ms の間であるべきとされています。この情報は一部のモーターでは正しくありません。たとえば、Tower Pro の SG90(回転角 180 度の制限付き)の場合、正しい範囲はおよそ 500μs から 2450μs です。この範囲が 180 度の回転に対応します。2560μs までのパルスであれば、モーターをわずかに遠くまで回転させることができます。このモーターは、これより長いパルスを無視します。
したがって、モーターが反応するパルス幅の範囲を調べるには、データシートに頼るよりも、実際にモーターで実験する方が良いでしょう。これらのモーターはホビープロジェクト向けであるため、仕様データが正確でない場合があります。
電気接続
まず、サーボモーターは、機械的なモーターにエネルギーを供給するために、専用の電源が必要です。この電源の電圧は +5V です。Smart Zynq ボードに給電している電源を使うことはお勧めしません。モーターは電圧レベルの急激な変化を引き起こすことが多いためです。電源電圧が不安定だと、Smart Zynq ボードが誤動作する可能性があります。
オレンジ色のワイヤ上の PWM 信号は、この信号が High のとき、電源とほぼ同じ電圧を持つ必要があります。言い換えれば、オレンジ色のワイヤとグランドの間の電圧は、0 または約 +5V でなければなりません。
しかし、Smart Zynq ボードの出力電圧は、High のときでも 3.3V しかありません。このような出力をモーターのオレンジ色のワイヤに直接接続しても、モーターが正しく応答する可能性は十分にあります。5V 電源をベースにしたデジタル回路は、通常 2.5V を超える電圧を論理 '1' とみなすためです。
とはいえ、PWM 信号に 3.3V が加わった状態でモーターが確実に動作するかは定かではありません。推奨される解決策は、Smart Zynq ボードの出力を 0V または 5V に変換する電圧レベルシフタ (voltage level shifter) を使用することです。このチュートリアルでは、TXS0108E チップを搭載した小型ボードを使用します。このボードはチップのピンを露出させているだけで、通常のワイヤをチップに接続できます。
この写真は、1 つのサーボモーターを制御するための電気接続の構成を示しています。写真の下部にある Smart Zynq ボードは、Dupont ワイヤで TXS0108E ボードに接続されています。このボードの反対側は、サーボモーターの PWM 入力と、モーター用の専用電源(標準的な電源プラグ用のアダプタ経由)の両方に接続されています。モーターの赤と茶色のワイヤも、モーターに電源電圧を供給するために、このボードにはんだ付けされています。
この写真は、TXS0108E ボードとその接続をより詳しく示したものです。
OE と VA の間の赤いワイヤに注意してください。これにより、チップの出力イネーブル (output enable) が High になります。
Smart Zynq のピンヘッダへの接続は、次の写真に示します。ここでの接続では、モーターは J6/1 によって制御されます。
次の表は、関連する 4 つの部品間の接続をまとめたものです。
| TXS0108E ボードのピン | Smart Zynq のピン | モーターのワイヤ | +5V 電源 | 備考 |
|---|---|---|---|---|
| VA | J6/37 (3.3V) | -- | -- | OE にも接続 |
| A4 | J6/1 | -- | -- | |
| OE | -- | -- | -- | VA に接続 |
| GND | J6/35 (GND) | 茶色のワイヤ | GND | |
| VB | -- | 赤色のワイヤ | +5V | |
| B4 | -- | オレンジ色のワイヤ | -- |
TXS0108E チップは、A4 入力から B4 出力への電圧レベルシフトを行います。このチップには、さらに 7 組のピンペアがあります。A4/B4 ペアは、このチュートリアルでは任意に選ばれました。8 つのモーターを制御するには、このチップの 8 つすべてのピンペアを使用できます。ここでは示していません。8 つのモーターを制御するために必要なワイヤの数が多いと、写真がわかりにくくなるためです。
J6 ピンヘッダを見つけるには、Smart Zynq ボードの裏面に「Bank 33 VCCIO Vadj」と書かれている場所を探します。この表示の近くにあるピンの列が、このチュートリアルで使うピンヘッダです。つまり、J6/1 は HDMI コネクタに最も近いピンです。
Smart Zynq ボードは、USB ポートの 1 つを通じて別の電源に接続されていることに注意してください。また、J6 ピンヘッダの最後のピンは 5V ですので、このピンにワイヤを接続しないでください。
Vivado プロジェクトの準備
demo バンドル (demo bundle) の zip ファイル(boot partition kit)から新しい Vivado プロジェクトを作成します。verilog/src/xillydemo.v をテキストエディタで開きます。コードの中で「PART 4」というラベルの付いた部分を削除し、その部分の代わりに次を挿入します。
assign user_r_mem_8_empty = 0;
assign user_r_mem_8_eof = 0;
assign user_w_mem_8_full = 0;
reg [7:0] reg0, reg1, reg2, reg3, reg4, reg5, reg6, reg7;
always @(posedge bus_clk)
if (user_w_mem_8_wren)
case (user_mem_8_addr[2:0])
0: reg0 <= user_w_mem_8_data;
1: reg1 <= user_w_mem_8_data;
2: reg2 <= user_w_mem_8_data;
3: reg3 <= user_w_mem_8_data;
4: reg4 <= user_w_mem_8_data;
5: reg5 <= user_w_mem_8_data;
6: reg6 <= user_w_mem_8_data;
7: reg7 <= user_w_mem_8_data;
endcase
reg [7:0] mem_8_data_reg;
assign user_r_mem_8_data = mem_8_data_reg;
always @(posedge bus_clk)
if (user_r_mem_8_rden)
case (user_mem_8_addr[2:0])
0: mem_8_data_reg <= reg0;
1: mem_8_data_reg <= reg1;
2: mem_8_data_reg <= reg2;
3: mem_8_data_reg <= reg3;
4: mem_8_data_reg <= reg4;
5: mem_8_data_reg <= reg5;
6: mem_8_data_reg <= reg6;
7: mem_8_data_reg <= reg7;
endcase
servo_pwm servo_pwm_i [7:0]
(
.clk(bus_clk),
.rst(quiesce),
.pwm_signal(J6[7:0]),
.pwm_width( { reg7, reg6, reg5, reg4, reg3, reg2, reg1, reg0 } )
);
更新された xillydemo.v をこのリンクからダウンロードすることもできます。
プロジェクトの verilog/src/ ディレクトリに、次を含む新しいファイル servo_pwm.v を作成します(コピーペーストするか、このリンクからダウンロードします)。
module servo_pwm
(
input clk,
input rst,
output reg pwm_signal,
input [7:0] pwm_width
);
reg [9:0] enable_count;
reg enable;
reg [10:0] pwm_count;
reg [8:0] threshold;
always @(posedge clk)
begin
if (enable_count == 999)
begin
enable_count <= 0;
enable <= 1;
end
else
begin
enable_count <= enable_count + 1;
enable <= 0;
end
threshold <= pwm_width + 50; // Add 0.5 ms to PWM width
if (enable)
begin
if (pwm_count == 1999)
pwm_count <= 0;
else
pwm_count <= pwm_count + 1;
pwm_signal <= (pwm_count < threshold);
end
if (rst)
begin
enable_count <= 0;
pwm_count <= 0;
pwm_signal <= 0;
end
end
endmodule
次に、このファイルを Vivado プロジェクトに追加します。[File] > [Add Sources…] をクリックし、"Add or create design sources" を選択します。次に [Next] をクリックします。[Add Files] ボタンをクリックし、verilog/src/ ディレクトリにある "servo_pwm.v" というファイルを選択します。次に [Finish] ボタンをクリックします。
更新したプロジェクトから、demo バンドル (demo bundle) 用にビットストリーム (bitstream) ファイルを作成したときと同じ方法で、ビットストリーム (bitstream) ファイルを作成します。また、同じ方法でビットストリーム (bitstream) ファイルを TF カードにコピーします(このプロジェクトで作成したファイルで、古い xillydemo.bit ファイルを上書きします)。
サーボモーターの制御
このセクションで示す手順は、Smart Zynq ボード上のシェルプロンプトで実行してください。
まず、ディレクトリを移動し、Xillybus 用のデモアプリケーションをコンパイルします。
# cd ~/xillybus/demoapps/ # make gcc -g -Wall -O3 memwrite.c -o memwrite gcc -g -Wall -O3 memread.c -o memread gcc -g -Wall -O3 streamread.c -o streamread gcc -g -Wall -O3 streamwrite.c -o streamwrite gcc -g -Wall -O3 -pthread fifo.c -o fifo
FPGA には、サーボモーターを制御するための 8 つのレジスタがあります。初期状態では、その値はすべてゼロです。次に示すのは、これらの値を 10 進形式で表示する方法です。
# hexdump -v -n 8 -e '8/1 "%u " "\n" ' /dev/xillybus_mem_8 0 0 0 0 0 0 0 0
より単純な hexdump コマンドでも、同じ値を 16 進形式で表示できます。
# hexdump -v -n 8 -C /dev/xillybus_mem_8 00000000 00 00 00 00 00 00 00 00 |........| 00000008
memwrite プログラム (xillybus/demoapps/ ディレクトリ内) を使ってモーターを制御できます。たとえば、次のコマンドはレジスタ 0 の値を 120 に変更します。
# ./memwrite /dev/xillybus_mem_8 0 120
これにより、J6/1 に接続されたモーターの回転位置または速度が変わります(上の写真のように接続されている場合)。
PWM パルス幅は、このレジスタの値に応じて次の式で決まります。
t = 500 + (x * 10)
この式で、t はマイクロ秒単位で表され、x は対応するレジスタの値です。したがって、パルス幅のデフォルト値は 500μs です(つまり x=0 の場合)。上記のコマンドはレジスタの値を 120 に変更しました。その結果、パルス幅は 1700μs に変わりました。
各レジスタは 1 バイトで構成されるため、その値は 0 から 255 の範囲です。したがって、各モーターのパルス幅は 500μs から 3050μs の間で変更できます。この値の範囲の一部は、ほとんどのモーターで不正とみなされることに注意してください。各モーターでさまざまな値を試して、どのように反応するかを確認するのが最善です。
別のピンに接続されたモーターを制御するには、別のレジスタに書き込みます。たとえば、モーターが Smart Zynq の J6/4 に接続されている場合、次のようなコマンドを使用できます。
# ./memwrite /dev/xillybus_mem_8 3 50
このコマンドは、パルス幅を 1000μs(つまり 1ms)に変更します。
これら 2 つのコマンドの後、値を読み戻すと変更を確認できます。
# hexdump -v -n 8 -e '8/1 "%u " "\n" ' /dev/xillybus_mem_8 120 0 0 50 0 0 0 0
これは値を 10 進形式で示しています。16 進形式の場合は次のとおりです。
# hexdump -v -n 8 -C /dev/xillybus_mem_8 00000000 78 00 00 32 00 00 00 00 |x..2....| 00000008
memwrite の仕組み
memwrite のソースコードは、~/xillybus/demoapps/ ディレクトリにある memwrite.c というファイルです。このプログラムは、Xillybus のシーク可能なストリーム (seekable streams) を使ってレジスタにアクセスする方法を示しています。このプログラムの仕組みを理解したい場合は、ソースコードを見ることをお勧めします。ここでは、以下で重要な 2 つの部分だけを説明します。
プログラムは、最初の引数で指定された名前のファイルを開きます。このファイルのファイルディスクリプタ (file descriptor) は変数 @fd に保存されます。
次に、プログラムは lseek() を次のように呼び出します。
if (lseek(fd, address, SEEK_SET) < 0) {
perror("Failed to seek");
exit(1);
}
変数 @address にはプログラムの 2 番目の引数が入ります。上記の memwrite の最初の使用例では、これは 0 でした。2 番目の例では 3 でした。一般に、lseek() はファイル内の特定の位置に移動するために使われます。この場合、ファイル内の位置は、アクセスしたいレジスタの番号と同じです。
次に、プログラムは allwrite() を関数呼び出しします。
allwrite(fd, &data, 1);
これにより、1 バイトがファイルに書き込まれます。allwrite() は memwrite.c で定義されており、よく知られた write() 関数に似ています。したがって、allwrite() を使う代わりに、次もほぼ同じです。
write(fd, &data, 1);
違いは、write() はデータが書き込まれることを保証しないことです。一方、allwrite() はデータがファイルに書き込まれることを保証します。
この場合、allwrite() の関数呼び出しは、プログラムの 3 番目の引数の値を書き込みます。つまり、これがレジスタの新しい値です。
ホスト側の Xillybus API の詳細については、この件に関するドキュメント、特にセクション 6.1 を参照してください。
Verilog コードの説明
まず、xillydemo.v 内の Verilog コードが FPGA 内部のレジスタをどのように実装しているかを説明します。その後、PWM パルスの実装について説明します。
ハードウェアレジスタを実装するための Xillybus API は、Xillybus FPGA デザイナーズガイドで詳しく説明されています。
レジスタに関連するワイヤは、元の xillydemo.v ファイルで既に定義されています。
// Wires related to /dev/xillybus_mem_8
wire user_r_mem_8_rden;
wire user_r_mem_8_empty;
wire [7:0] user_r_mem_8_data;
wire user_r_mem_8_eof;
wire user_r_mem_8_open;
wire user_w_mem_8_wren;
wire user_w_mem_8_full;
wire [7:0] user_w_mem_8_data;
wire user_w_mem_8_open;
wire [4:0] user_mem_8_addr;
wire user_mem_8_addr_update;
これらのワイヤは、Xillybus IP コア (IP core) のインスタンシエーション (instantiation) の一部として、この IP コアに接続されています。
xillybus xillybus_ins (
// Ports related to /dev/xillybus_mem_8
// FPGA to CPU signals:
.user_r_mem_8_rden(user_r_mem_8_rden),
.user_r_mem_8_empty(user_r_mem_8_empty),
.user_r_mem_8_data(user_r_mem_8_data),
.user_r_mem_8_eof(user_r_mem_8_eof),
.user_r_mem_8_open(user_r_mem_8_open),
// CPU to FPGA signals:
.user_w_mem_8_wren(user_w_mem_8_wren),
.user_w_mem_8_full(user_w_mem_8_full),
.user_w_mem_8_data(user_w_mem_8_data),
.user_w_mem_8_open(user_w_mem_8_open),
// Address signals:
.user_mem_8_addr(user_mem_8_addr),
.user_mem_8_addr_update(user_mem_8_addr_update),
[ ... ]
.quiesce(quiesce)
);
次に、このプロジェクト専用に追加された Verilog コードを見てみましょう。このコードは、冒頭の次の部分で、ホストとのデータ交換にフロー制御 (flow control) が適用されないようにしています。
assign user_r_mem_8_empty = 0;
assign user_r_mem_8_eof = 0;
assign user_w_mem_8_full = 0;
続いて、8 つのレジスタが宣言されます。
reg [7:0] reg0, reg1, reg2, reg3, reg4, reg5, reg6, reg7;
次の部分は、レジスタへの書き込み操作を実装しています。@user_w_mem_8_wren が High のとき、8 つのレジスタのいずれかに新しい値が書き込まれます。@user_mem_8_addr が、影響を受けるレジスタを選択します。@user_w_mem_8_wren と @user_mem_8_addr はどちらも Xillybus IP コアの出力です。
always @(posedge bus_clk)
if (user_w_mem_8_wren)
case (user_mem_8_addr[2:0])
0: reg0 <= user_w_mem_8_data;
1: reg1 <= user_w_mem_8_data;
2: reg2 <= user_w_mem_8_data;
3: reg3 <= user_w_mem_8_data;
4: reg4 <= user_w_mem_8_data;
5: reg5 <= user_w_mem_8_data;
6: reg6 <= user_w_mem_8_data;
7: reg7 <= user_w_mem_8_data;
endcase
この後、これらのレジスタから値を読み取る機能が実装されます。@user_r_mem_8_rden が High のとき、@user_r_mem_8_data が更新され、いずれかのレジスタの値が格納されます。@user_mem_8_addr が、どのレジスタの値を読み取るかを選択します。
reg [7:0] mem_8_data_reg;
assign user_r_mem_8_data = mem_8_data_reg;
always @(posedge bus_clk)
if (user_r_mem_8_rden)
case (user_mem_8_addr[2:0])
0: mem_8_data_reg <= reg0;
1: mem_8_data_reg <= reg1;
2: mem_8_data_reg <= reg2;
3: mem_8_data_reg <= reg3;
4: mem_8_data_reg <= reg4;
5: mem_8_data_reg <= reg5;
6: mem_8_data_reg <= reg6;
7: mem_8_data_reg <= reg7;
endcase
最後に、servo_pwm モジュールのインスタンシエーション (instantiation) が行われます。
servo_pwm servo_pwm_i [7:0]
(
.clk(bus_clk),
.rst(quiesce),
.pwm_signal(J6[7:0]),
.pwm_width( { reg7, reg6, reg5, reg4, reg3, reg2, reg1, reg0 } )
);
このインスタンシエーション (instantiation) によって、servo_pwm の同一のコピーが 8 つ作成されることに注意してください。最初のコピーは reg0 と J6[0] に接続され、2 番目のコピーは reg1 と J6[1] に接続されます。以下同様です。
それでは、servo_pwm モジュールを見てみましょう。このモジュールは、ポートとレジスタの宣言から始まります。
module servo_pwm
(
input clk,
input rst,
output reg pwm_signal,
input [7:0] pwm_width
);
reg [9:0] enable_count;
reg enable;
reg [10:0] pwm_count;
reg [8:0] threshold;
先ほど述べたように、@pwm_width はレジスタの値を受け取ります。この値は、ホスト上の memwrite コマンドによって更新されます。@pwm_signal は Smart Zynq ボードのピンに接続されているため、この信号がオレンジ色のワイヤ経由でモーターに届きます。
このモジュールの次の部分は、ストローブ信号を実装しています。@enable は 1000 クロックサイクルごとに 1 回 High になります。@clk の周波数は 100 MHz です。したがって、@enable は 10μs ごとに 1 回 High になります。
always @(posedge clk)
begin
if (enable_count == 999)
begin
enable_count <= 0;
enable <= 1;
end
else
begin
enable_count <= enable_count + 1;
enable <= 0;
end
前述のように、0 から 255 の範囲のレジスタ値は、500μs から 3050μs の間の PWM パルス幅に対応します(パルス幅をマイクロ秒で表す式は 500 + x * 10 でした)。
したがって、@threshold の式は次のようになります。
threshold <= pwm_width + 50; // Add 0.5 ms to PWM width
このレジスタには、PWM パルス(つまり PWM 信号が High の間)に @enable が High になる回数が格納されます。この式は、レジスタの値が PWM パルスの長さを 10μs 単位で表すという事実を反映しています。レジスタの値に 50 を加えることは、最小パルス幅の 500μs に対応します。
@enable は、ここでこの部分のクロックイネーブル (clock enable) として使われます。
if (enable)
begin
if (pwm_count == 1999)
pwm_count <= 0;
else
pwm_count <= pwm_count + 1;
pwm_signal <= (pwm_count < threshold);
end
@pwm_count は 0 から 1999 まで数えて、再び 0 から始まります。このカウンタは 10μs ごとに 1 回だけ変化するため、20000μs = 20ms ごとに 1 周します。言い換えれば、PWM パルスの繰り返し周期は、要件どおり 20ms です。
@pwm_signal は、@pwm_count が @threshold より小さい間、High になります。これが、レジスタによってパルス幅が制御される仕組みです。
このモジュールの最後の部分は、いくつかのレジスタをリセットします。
if (rst)
begin
enable_count <= 0;
pwm_count <= 0;
pwm_signal <= 0;
end
end
endmodule
@rst が High のとき、この部分は上で書かれたすべてに優先します。したがって、@rst はリセット信号の機能を持ちます。
Verilog コードと実際のピンの関係
上記の Verilog コードは J6 という inout ポートを使っていますが、このポートの接続はどのようにしてピンヘッダに到達するのでしょうか。答えは xillydemo.xdc にあります。このファイルは、ビットストリーム (bitstream) を作成する Vivado プロジェクトの一部です("vivado-essentials" ディレクトリ内にあります)。
xillydemo.xdc には、FPGA が電子部品として正しく動作するために必要なさまざまな情報が含まれています。このファイルには、とりわけ次の行が含まれています。
[ ... ]
## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22 [get_ports {J6[0]}]; #J6/1 = IO_B33_LN2
set_property PACKAGE_PIN T22 [get_ports {J6[1]}]; #J6/2 = IO_B33_LP2
set_property PACKAGE_PIN W22 [get_ports {J6[2]}]; #J6/3 = IO_B33_LN3
set_property PACKAGE_PIN V22 [get_ports {J6[3]}]; #J6/4 = IO_B33_LP3
set_property PACKAGE_PIN Y21 [get_ports {J6[4]}]; #J6/5 = IO_B33_LN9
set_property PACKAGE_PIN Y20 [get_ports {J6[5]}]; #J6/6 = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}]; #J6/7 = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}]; #J6/8 = IO_B33_LP7
[ ... ]
最初の行は、ポート J6[0] を U22 に接続すべきであることを示しています。U22 は FPGA の物理パッケージ上の位置です。Smart Zynq の回路図によると、この FPGA ピンはピンヘッダの最初のピンに接続されています。他のポートの位置も同じ方法で定義されています。
まとめ
このチュートリアルでは、Smart Zynq ボードと Xillybus のレジスタ API を使ってサーボモーターを制御する方法を説明しました。このアプリケーションのホスト側アプリケーションと Verilog コードの両方を紹介しました。
また、このチュートリアルは、レジスタを使ってハードウェアを制御する必要がある他のアプリケーションの基礎としても役立ちます。


