【C言語】MOS-FETで構成されるモータードライバでブラシレスモータを駆動させてみた

# あらすじ

ひょんなことから、三相ブラシレスモータを駆動させることとなりました。

ブラシ付きDCモータとは違い、単純に電圧を入力すれば即動いてくれるわけではないので、まともに動かすにもひと手間かかりました。その過程で色々と学びがあったので、まとめます。

# 環境

駆動させるモータ及び、駆動に使用するモータドライバ・マイコンは以下とします。

|使用機器|型番|
|---|---|
|ブラシレスモータ|X2212 950KV|
|モータードライバ|SLA5064|
|マイコン|ESP32|
|IDE|Arduino 1.8.11|


# ブラシレスモータとは？

普通のブラシ付きモータは、シームレスに回転子（ローター）を回転させるために　電流の方向をブラシ（電極）とコミュテータ（整流子）を接触させて機械的に切り替えています。
一方、ブラシレスモータは名の通りブラシが無く、代わりに位置センサ（ホール素子、ホールIC）でロータの位置を把握し、電気の流れる方向を切り替えて駆動させます。
ブラシレスモータは駆動する上で電気の方向を切り替えるための機械的接触が存在しないため、使用する度に回転で摩耗するブラシのメンテナンスが不要であり、また接触によるノイズも発生せず、精緻な制御が可能です。

詳細はモーターメーカーのホームページなどに載っているため、そちらをご覧下さい。


# モータドライバ：SLA5064 について

今回、モータの駆動に使用するモータドライバ「SLA5064」の回路図を、下記に示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/0a5d2a55-7d8f-644e-f190-e50877339d60.png)
仕様：https://www.semicon.sanken-ele.co.jp/sk_content/sla5064_ds_jp.pdf


「3相モータドライブ用」とのことで、６つのMOS-FETから構成されています。これらMOS-FETのゲートへの入力電圧を切り替えることで、UVW相それぞれにタイミング良く入力を印加し、ブラシレスモータを駆動します。

下記は、MOS-FETの回路図です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/15921b6f-0735-0854-e7c3-ce6385229248.png)

G … ゲート
S … ソース
D … ドレイン

上記のゲートに電圧を流すことで、ソース - ドレイン間が通電します。

ゲートに流す電圧の正負は、対象の MOS-FET がNチャネルもしくはPチャネルのどちらであるかに依存します。


Nチャネルではゲートに対してプラスの電圧を入力した際にドレイン - ソース間が通電するのに対し、
Pチャネルではゲートにマイナスの電圧を入力する必要があります。
なお、プラス/マイナスというのは、ソースへの入力電圧に対して、です。

詳しくは下記を参照↓
https://www.marutsu.co.jp/pc/static/large_order/fet_3

注意すべきは、今回使用するモータドライバの回路図をよく見ると、上側３つはPチャネル、下側３つはNチャネルだということです。
そのため、FETがNチャネルかPチャネルなのか、意識してソースを書く必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/6b33b5b0-2c9b-4f25-283d-e326dd17c4f5.png)



# 実際にモータを駆動させてみた

では、モータを駆動させます。

ブラシレスモータを駆動させるためには、前述のモータドライバを構成するそれぞれのFETのゲートに、正しい順序でパルスを入力し、モータ巻線に電力を印加させる必要があります。

https://www.orientalmotor.co.jp/tech/reference/brushless01/

上記ページの表に従って各FETのゲートにパルスを送ることで、モータを駆動させることができました。
注意点として、上記ページで言うところのTr1-Tr3は前述の通りPチャネル型のため、入力パルスのON/OFFを反転させる必要があります。

※ モータ駆動テストのソース及び回路図は後述

# 回転数を調整してみた

前述のモータ駆動方法は、ゲートにそれぞれのFETのゲート入力するパルスの間隔が短いほど、回転数が上昇します。

そこで、モータの回転数検出を試みました。

モータ回転数の検出には、アルプス電気製のロータリーエンコーダ「P-06357」を使用します。
https://akizukidenshi.com/catalog/g/gP-06357/

上記エンコーダでモータの回転数をモニタしたところ、パルスの送信間隔を早めることで、モータの回転数も上昇することが分かります。

※ エンコーダの機能確認、エンコーダによるモータ回転数検出に使用したソースと回路図は後述


# ハードウェア外観

モータの駆動とエンコーダによる回転数検出のために作成したハードを、以下に示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/8c6877b3-e143-d394-4df3-84dd8694b5c7.png)

汚い。。


モータはユニバーサルプレートに、エンコーダはユニバーサル基盤にそれぞれ無理矢理固定し、両者の回転軸をユニバーサルジョイントで接続しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/b240446d-2baa-a130-390f-594ccb995642.png)


エンコーダの回転抵抗（とハードのしょぼさ）のせいで、特に低い回転数ではぎくしゃくするのはご愛嬌！

※ 回路図は後述


# 備考1：ボディダイオードとは？

回路図を見てみると、FETの中にはダイオードが存在します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/7b6e6766-7782-342f-eab7-2fa6463b9463.png)

これはあくまでMOSFETの構造上、ソース-ドレイン間のpn接合により形成されるもので、ボディダイオード（又は寄生ダイオード、内臓ダイオード）と呼ばれています。


# 備考2：ツェナーダイオード/ゲート保護ダイオードとは？

回路図を更に見ると、ゲートもダイオードを介して他の線とつながっているのが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/786edc82-f922-2d8e-8828-df4748835009.png)

このダイオードの回路記号をよく見ると、他のダイオードのそれと微妙に異なります。
（線が真っすぐではなく折れ曲がっている）

これは「ツェナーダイオード」であり、一般のダイオードとは逆にカソード（-）からアノード（+）に向けて電圧を印加することで、一定電圧を取得したり過電圧から回路を保護することを用途としています。

このモータドライバにおいては、ゲートからの高電圧が電源/GNDに印加されるのを防ぐ、また逆にゲートそのものに高電圧が印加されるのを防いでいるのでしょう。

このようなFETにおいてこのように使用されるダイオードを「ゲート保護ダイオード」といいます。

※ 因みにツェナーダイオードとよく似たものでシャントレギュレータ（基準電圧IC）があり、高い出力精度を要求される場面ではシャントレギュレータが使用されるようです。


# ソース

以下、使用したソースと回路を記します。

回路図の作図には「fritzing」（フリッツィングと読むらしい）を使いました。
有償ですが、非常に便利です。これを今まで使っていなかったのが悔やまれる。

## 1. モータ駆動

モータの回転数は、「DFLT_DLY」の数値で調整可能です。

```c:brushless_motor_drive.c
#define DFLT_DLY 10 // 通常のパルス間遅延 [ms]
#define GATE_02  14 // SLA5064のゲート番号とESP32のDI/Oポート番号の対応
#define GATE_04  25
#define GATE_06  26
#define GATE_08  27
#define GATE_09  32
#define GATE_11  33


void setup() 
{  
  pinMode(GATE_02, OUTPUT); //モータドライバ入力用１
  pinMode(GATE_04, OUTPUT); //モータドライバ入力用２
  pinMode(GATE_06, OUTPUT); //モータドライバ入力用３
  pinMode(GATE_08, OUTPUT); //モータドライバ入力用４
  pinMode(GATE_09, OUTPUT); //モータドライバ入力用５
  pinMode(GATE_11, OUTPUT); //モータドライバ入力用６
}

void loop() {

  digitalWrite(GATE_02, LOW);
  digitalWrite(GATE_04, LOW);
  digitalWrite(GATE_06, HIGH);
  digitalWrite(GATE_08, LOW);
  digitalWrite(GATE_09, HIGH);
  digitalWrite(GATE_11, HIGH);
  delay(DFLT_DLY);

  digitalWrite(GATE_02, LOW);
  digitalWrite(GATE_04, HIGH);
  digitalWrite(GATE_06, LOW);
  digitalWrite(GATE_08, LOW);
  digitalWrite(GATE_09, HIGH);
  digitalWrite(GATE_11, HIGH);
  delay(DFLT_DLY);

  digitalWrite(GATE_02, HIGH);
  digitalWrite(GATE_04, HIGH);
  digitalWrite(GATE_06, LOW);
  digitalWrite(GATE_08, LOW);
  digitalWrite(GATE_09, LOW);
  digitalWrite(GATE_11, HIGH);
  delay(DFLT_DLY);

  digitalWrite(GATE_02, HIGH);
  digitalWrite(GATE_04, HIGH);
  digitalWrite(GATE_06, HIGH);
  digitalWrite(GATE_08, LOW);
  digitalWrite(GATE_09, LOW);
  digitalWrite(GATE_11, LOW);
  delay(DFLT_DLY);

  digitalWrite(GATE_02, LOW);
  digitalWrite(GATE_04, HIGH);
  digitalWrite(GATE_06, HIGH);
  digitalWrite(GATE_08, HIGH);
  digitalWrite(GATE_09, LOW);
  digitalWrite(GATE_11, LOW);
  delay(DFLT_DLY);
  
  digitalWrite(GATE_02, LOW);
  digitalWrite(GATE_04, LOW);
  digitalWrite(GATE_06, HIGH);
  digitalWrite(GATE_08, HIGH);
  digitalWrite(GATE_09, LOW);
  digitalWrite(GATE_11, HIGH);
  delay(DFLT_DLY);
}

```



以下、回路図です。

モータドライバは適当なものがなかったので、ICを組み合わせてなんとなく作りました。
また、モータも配線が3つのモノがなかったので、適当です。
（実際に使用したのはサーボモータでは勿論ないので注意）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/4f7ec200-300f-1b48-2e6a-c47b3f2c883c.png)




## 2. エンコーダで回転数検出の確認

ハードウェア割込みを使用して、エンコーダのパルスを検出しています。

```c:encoder_varification.c
#define PA  18 // エンコーダA相の入力ポート番号
#define PB  19 // エンコーダB相の入力ポート番号
#define RES 24 // エンコーダの分解能
#define CW   1 // モータの回転方向（時計回り）
#define CCW -1 // モータの回転方向（逆時計回り）
#define CTRL_PERIOD 10 // 制御周期 [msec]

volatile bool apls, bpls;      // A・B相のパルス状態を保持
volatile int curnum;           // A・B相のパルス状態を表現するための数値
volatile int dly1, dly2, dly3; // 3つ前までのパルス状態を保持
volatile int cnt, dir;         // 1ループ辺りのエンコーダのクリック数と回転方向


void setup() {

  Serial.begin( 115200 );
  pinMode(PA, INPUT);
  pinMode(PB, INPUT);
  attachInterrupt(PA, get_encoder_pulse, CHANGE);
  attachInterrupt(PB, get_encoder_pulse, CHANGE);
}

void loop() {

  double sttime = micros() / 1000; // loop処理の開始時間[ms]
  static double pt; // mainループの処理に費やした時間[ms]
  
  static double oldtime;
  double timediff = (sttime - oldtime) / 1000;  // [msec]で表示
  double rpm = (dir * cnt * 60 * 1000) / (RES * timediff * 1000);

  Serial.println(rpm); // 回転数[rpm]を表示

  cnt = 0; // 1制御周期間のエンコーダクリック回数をリセット
  oldtime = sttime;
  pt = micros() / 1000 - sttime;

  if (pt > CTRL_PERIOD) // 処理にかかる時間が制御周期よりも長い場合
  { 
    // returnしても再度loopが呼び出されるため、以下警告のみ表示 
    Serial.println("制御周期をもっと長くして下さい。");
  }
  else
  {
    delay(CTRL_PERIOD - pt); // 制御周期[ms]実現のための待ち時間
  }
}

// A・B相のパルス変化時に割込み処理を実施
void get_encoder_pulse(){

  // A・B相のパルス状態を保持
  apls = digitalRead(PA);
  bpls = digitalRead(PB);

  // A・B相のパルス状態を1-4までの数値で表現
  if (!apls && bpls)
  {
    curnum = 1;
  }
  else if(!apls && !bpls)
  {
    curnum = 2;
  }
  else if (apls && !bpls)
  {
    curnum = 3;
  }
  else if (apls && bpls)
  {
    curnum = 4;
  }

  // パルス状態の遷移から、時計回り/逆時計回りを判断
  if (dly3 == 1 && dly2 == 2 && dly1 == 3 && curnum == 4)
  {
    // 時計回りの回転を検知
    dir = CW;
    cnt ++; // エンコードクリック回数を加算
  }
  else if(dly3 == 3 && dly2 == 2 && dly1 == 1 && curnum == 4)
  {
    // 逆時計回りの回転を検知
    dir = CCW;
    cnt ++; // エンコードクリック回数を加算
  }

  // 3つ前までのパルス状態を保持
  if (curnum != dly1)
  {
    dly3 = dly2;
    dly2 = dly1;
    dly1 = curnum;
  }
}

```

以下、回路図です。

コンデンサは、エンコーダのパルスのノイズ除去対策です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/30a9ac95-5376-0aae-eef4-e9925a354c63.png)


## 3. モータ回転数をエンコーダで確認

ESP32のデュアルコアを使用して、メインタスクで回転検出、サブタスクにてモータ駆動を実施しています。
モータのパルス間遅延（DFLT_DLY）が10[ms]で約120[rpm]、7[ms]で約180[rmp]、4[ms]で約320[rmp]です。
3[ms]以下にすると … 高速過ぎて残念ながら回転してくれません。



```c:get_motor_rpm_with_encoder.c
#define DFLT_DLY  7 // 通常のパルス間遅延 [ms]
#define GATE_02  14 // SLA5064のゲート番号とESP32のDI/Oポート番号の対応
#define GATE_04  25
#define GATE_06  26
#define GATE_08  27
#define GATE_09  32
#define GATE_11  33

#define PA  18 // エンコーダA相の入力ポート番号
#define PB  19 // エンコーダB相の入力ポート番号
#define RES 24 // エンコーダの分解能
#define CW   1 // モータの回転方向（時計回り）
#define CCW -1 // モータの回転方向（逆時計回り）
#define CTRL_PERIOD 100 // 制御周期 [msec]

volatile bool apls, bpls;      // A・B相のパルス状態を保持
volatile int curnum;           // A・B相のパルス状態を表現するための数値
volatile int dly1, dly2, dly3; // 3つ前までのパルス状態を保持
volatile int cnt, dir;         // 1ループ辺りのエンコーダのクリック数と回転方向


void setup()
{
  Serial.begin(115200);
  
  // デュアルコアの使用設定
  xTaskCreatePinnedToCore(subProcess, "subProcess", 4096, NULL, 1, NULL, 0);

  pinMode(PA, INPUT);
  pinMode(PB, INPUT);
  attachInterrupt(PA, get_encoder_pulse, CHANGE);
  attachInterrupt(PB, get_encoder_pulse, CHANGE);

  pinMode(GATE_02, OUTPUT); //モータドライバ入力用１
  pinMode(GATE_04, OUTPUT); //モータドライバ入力用２
  pinMode(GATE_06, OUTPUT); //モータドライバ入力用３
  pinMode(GATE_08, OUTPUT); //モータドライバ入力用４
  pinMode(GATE_09, OUTPUT); //モータドライバ入力用５
  pinMode(GATE_11, OUTPUT); //モータドライバ入力用６
}

// 回転数の検出
void loop()
{
  double sttime = micros() / 1000; // loop処理の開始時間[ms]
  static double pt; // mainループの処理に費やした時間[ms]

  static double oldtime;
  double timediff = (sttime - oldtime) / 1000;  // [msec]で表示
  double rpm = (dir * cnt * 60 * 1000) / (RES * timediff * 1000);

  Serial.println(rpm); // 回転数[rpm]を表示

  cnt = 0; // 1制御周期間のエンコーダクリック回数をリセット
  oldtime = sttime;
  pt = micros() / 1000 - sttime;

  if (pt > CTRL_PERIOD) // 処理にかかる時間が制御周期よりも長い場合
  { 
    // returnしても再度loopが呼び出されるため、以下警告のみ表示 
    Serial.println("制御周期をもっと長くして下さい。");
  }
  else
  {
    delay(CTRL_PERIOD - pt); // 制御周期[ms]実現のための待ち時間
  }
}

// A・B相のパルス変化時に割込み処理を実施
void get_encoder_pulse(){

  // A・B相のパルス状態を保持
  apls = digitalRead(PA);
  bpls = digitalRead(PB);

  // A・B相のパルス状態を1-4までの数値で表現
  if (!apls && bpls)
  {
    curnum = 1;
  }
  else if(!apls && !bpls)
  {
    curnum = 2;
  }
  else if (apls && !bpls)
  {
    curnum = 3;
  }
  else if (apls && bpls)
  {
    curnum = 4;
  }

  // パルス状態の遷移から、時計回り/逆時計回りを判断
  if (dly3 == 1 && dly2 == 2 && dly1 == 3 && curnum == 4)
  {
    // 時計回りの回転を検知
    dir = CW;
    cnt ++; // エンコードクリック回数を加算
  }
  else if(dly3 == 3 && dly2 == 2 && dly1 == 1 && curnum == 4)
  {
    // 逆時計回りの回転を検知
    dir = CCW;
    cnt ++; // エンコードクリック回数を加算
  }

  // 3つ前までのパルス状態を保持
  if (curnum != dly1)
  {
    dly3 = dly2;
    dly2 = dly1;
    dly1 = curnum;
  }
}

// モータ駆動
void subProcess(void * pvParameters)
{
  for(;;)
  {
    digitalWrite(GATE_02, LOW);
    digitalWrite(GATE_04, LOW);
    digitalWrite(GATE_06, HIGH);
    digitalWrite(GATE_08, LOW);
    digitalWrite(GATE_09, HIGH);
    digitalWrite(GATE_11, HIGH);
    delay(DFLT_DLY);
  
    digitalWrite(GATE_02, LOW);
    digitalWrite(GATE_04, HIGH);
    digitalWrite(GATE_06, LOW);
    digitalWrite(GATE_08, LOW);
    digitalWrite(GATE_09, HIGH);
    digitalWrite(GATE_11, HIGH);
    delay(DFLT_DLY);
  
    digitalWrite(GATE_02, HIGH);
    digitalWrite(GATE_04, HIGH);
    digitalWrite(GATE_06, LOW);
    digitalWrite(GATE_08, LOW);
    digitalWrite(GATE_09, LOW);
    digitalWrite(GATE_11, HIGH);
    delay(DFLT_DLY);
  
    digitalWrite(GATE_02, HIGH);
    digitalWrite(GATE_04, HIGH);
    digitalWrite(GATE_06, HIGH);
    digitalWrite(GATE_08, LOW);
    digitalWrite(GATE_09, LOW);
    digitalWrite(GATE_11, LOW);
    delay(DFLT_DLY);
  
    digitalWrite(GATE_02, LOW);
    digitalWrite(GATE_04, HIGH);
    digitalWrite(GATE_06, HIGH);
    digitalWrite(GATE_08, HIGH);
    digitalWrite(GATE_09, LOW);
    digitalWrite(GATE_11, LOW);
    delay(DFLT_DLY);
  
    digitalWrite(GATE_02, LOW);
    digitalWrite(GATE_04, LOW);
    digitalWrite(GATE_06, HIGH);
    digitalWrite(GATE_08, HIGH);
    digitalWrite(GATE_09, LOW);
    digitalWrite(GATE_11, HIGH);
    delay(DFLT_DLY);
  }
}
```

以下、回路図です。

前述の「1. モータ駆動」「2. エンコーダで回転数検出の確認」を組み合わせたものです。
（モータやエンコーダの配置が変わっているので分かり辛いですが）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/8063661a-b4a5-5007-20ef-135821e1fd4d.png)



# 課題

## 1.「GPIO使い過ぎ」問題

モータを１個動かす上で、GPIOを6チャンネルも消費するのが気になりました。
モータが２個になれば、GPIOも2倍の12個が必要です。
実際にブラシレスモータをアクチュエータとして何か作ろうとした場合、
同じ方法でモータを制御すると苦労するかもなあ。。

## 2.「簡単に脱調」問題

今回の駆動方法ではフィードバック制御を一切いれていないため、例えば回転中に回転軸に手動で無理矢理負荷をかけたりすると、容易に脱調してスムーズに回らなくなります。また、そもそも電源ON時にスムーズに回らない時があります。実際にロボットなどのアクチュエータとして使用する際には、フィードバックなしというのはほぼ有り得ないため、今後はベクトル制御などの手法も試してみたいなと思っています。






# 参考にさせていただいた情報

本記事執筆のために、前述のもの以外にも下記情報を参考にさせていただきました。

|内容|リンク先|
|---|---|
|ESP32のピン配置など|http://kit.socinno.com/1_3_b/|
|ゲート保護ダイオード|https://toragi.cqpub.co.jp/Portals/0/backnumber/2004/08/p139-140.pdf|
|pn接合ダイオード|http://fhirose.yz.yamagata-u.ac.jp/img/pn5.pdf|
|ブラシレスモータの駆動原理|https://www.rohm.co.jp/electronics-basics/motors/motor_what3 <br>https://www.nidec.com/jp/technology/motor/basic/00020/|
|各トランジスタの特徴|https://www.shindengen.co.jp/products/semi/column/basic/mosfet/mosfet.html <br>https://contents.zaikostore.com/semiconductor/4779/|
|ダイオードの基本原理|https://www.buildinsider.net/small/electronics/04 <br>https://www.matsusada.co.jp/column/diode.html|
|FETの説明|https://qiita.com/niQSun/items/251e5824c70724a7d2c0|
|寄生ダイオードの詳細|https://techweb.rohm.co.jp/knowledge/sic/s-sic/04-s-sic/6147#:~:text=%E3%83%9C%E3%83%87%E3%82%A3%E3%83%80%E3%82%A4%E3%82%AA%E3%83%BC%E3%83%89%E3%81%AF%E3%80%81MOSFET%E3%81%AE,%E3%81%A8%E3%82%82%E5%91%BC%E3%81%B0%E3%82%8C%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82 <br>https://www.shindengen.co.jp/products/semi/column/basic/mosfet/mosfet_1.html <br>https://amasawahakusyo.com/electronics/mosfet-body-diode/|
|ツェナーダイオードの詳細|https://contents.zaikostore.com/semiconductor/4616/|
|シャントレギュレータとツェナーダイオードの違い|https://detail-infomation.com/zener-diode-and-shunt-regulator-ic/|
|ロータリーエンコーダの使用方法|https://jumbleat.com/2016/12/17/encoder_1/  <br>https://dotstud.io/blog/mongoose-os-rotary-encoder/<br>https://wave.hatenablog.com/entry/2017/03/14/191000|
|外部割込みとvolatileの関係|http://tool-support.renesas.com/autoupdate/support/onlinehelp/ja-JP/csp/V4.01.00/CS+.chm/Compiler-CCRH.chm/Output/ccrhaac0104y.html|
|エンコーダのノイズ対策|http://sheep-me.me/2019/12/05/geidai_21/<br>http://funcs.org/1789<br>http://sheep-me.me/2019/12/05/geidai_21/|
|Fritzing に ESP32 を追加|https://tom2rd.sakura.ne.jp/wp/2019/07/14/post-9533/|
|ESP32でデュアルコア活用|https://www.mgo-tec.com/blog-entry-arduino-esp32-multi-task-dual-core-01.html<br>https://programresource.net/2020/02/25/2994.html|


# 最後

ご指摘・改善案・間違い指摘、大歓迎です。是非是非是非是非遠慮せずに下さい。