見出し画像

Arduino Nano Every でブラシレスモーターを駆動 備忘録

Arduino Nano Every でブラシレスモーターを回すことができた。まだまだやることがあるので、この辺で、これまでにやったことを書いておく。

学んだこと

ブラシレスモーターの仕組みと駆動方法
ポートレジスターのいじり方

Arduino Nano Every 公式

用意したもの

Arduino Nano Every

スイッチサイエンスから購入

送料は200円。うれしい低料金。自分は2個購入したので、送料無料。ネコポスでの配送で郵便受けに投函されるので、受け取りの手間がかからない点もよい。

モーター

Amazonから

https://www.amazon.co.jp/gp/product/B089SYW5W3/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1

不良品だった。スムーズに回らなかった。1回転ごとにどこかでこすっている。確認すると、固定子側の金属製の部品が半田付けの近くで変形していた。こすらない程度に手直しした。もうこの製品を買うことはないだろう。

モーターのスペック

商品の説明
色:2204-260KV
仕様:
状態:100%新品
アイテムタイプ:ドローンブラシレスモーター
素材:金属
色:写真のように
サイズ:30cm / 11.8インチ以下
オプション:2204-260KV、2206-260KV
モーターKV:160 RPM / V
モーター抵抗(Rm):5.6オーム
無負荷電流:0.06A / 7.4V
最大連続電流:1.3A
最大電力:15W
モーター直径:28mm
モーター本体の長さ:15mm
ボルト穴の間隔:12mm
ボルトねじ:M2X4
ステータアーム:12N
ポール数:14P
(Amazon の商品のページから転記)

ドライバーIC 

その他

ブレッドボード、スイッチ、ポテンショメーター、抵抗、電池ケース、他。

Arduino Nano Every のセットアップ

詳細は割愛。

Arduino IDE の ATMEGA328 エミュレーションは無しに設定。

pinout データをゲット↓

ページ中ほどの Documentation タブをクリック。

Download the full pinout diagram as PDF here. から PDF ゲット。

構想を練る

ドライバーIC のコントロール電圧は Arduino からの  0 - 5 V だ。モーターの電源は、とりあえず eneloop 4本でやってみる。約4.8Vと想定。ある程度使用した後に実測したら 5.6 Vくらいだった。

一方、Arduino の 5 V は実測値で約 4.8 V。

うーん。Arduino とモータードライバーとは何かでインターフェフェイスしないとダメか。

でも、MOS-FETってアレだったよな。

スペックを調べたら、電源電圧より2ボルトくらい低くなったときに上のやつ(P Channel 側)がオンするみたいだった。逆に電源電圧より高ければ確実にオフするってこと。

正しくは、P channel の Vth が -1.0 V min の -2.0 V max。

下のやつ(N Channel 側)は、逆に GND に対して 2ボルトを越えれば良い。

ATmega4809 のスペックによると、電源電圧が 5V のときで、I/O pin drive strength Voh が 4 V min。場合によっては正しく動かないかもしれないが、実験だから先に進もう。

で、Arduino の端子と FET のゲートを直結することに。

ブラシレスモーターの動作原理も勉強。とりあえずモーターの3つの端子になんらかの順番で電流を流せばよいのだろう、回転が逆だったら線を入れ替えれば良いのだろう、くらいに考えてプログラミングを開始。

プログラミング

これの前にステッピングモーターの駆動に成功していた。ステッピングモーターは、サンプルスケッチを利用したらうまく動作した。ドライバーは、秋月電子で買ったステッピングモータをPIC を使って動かすキット。PIC を外して Arduino を繋いだ。ステッピングモーター用のスケッチを元に、ブラシレスモーター用に作っていく考えだ。

まだモーターは繋がない。初めのうちは LED を光らせて確認していった。ロジックを間違えて電源側のFETとGND 側のFET が同時にONにしてしまったら大電流が流れて危ないだろうし、いきなりモーターを接続するのも危険だろう。しかし LED の点滅が見えない。点滅が目に見えるまでスピードを落として実験した。

ブラシレスモーターの場合、6つのフェーズが必要。で、6つの端子を出力として割り当てて1フェーズに6パターン、High だとか Low だとか DigitalWrite  していた。1フェーズに6行必要だ。全然スマートじゃないな。2フェーズで状態が同じやつがあるので、そういうやつはなにもしないことにしても、1つをオンにして別の1つをオフにしなければならない。

逆回転は、どうする?最初、勘違いして、逆に回したらいけるだろうと、シーケンスを逆にした。しかし、回転方向が変わらなかった。考えたら出力ポートを入れ替えなければならないのだった。ポートの割り当てを変えたらうまくいった。

スイッチで回転の開始、停止と逆転を制御したかった。最初はループの中に入れていたので、それなりに処理時間が食われていた。そこで、割り込みを使おうと考えた。割り込みのやり方はすぐにわかった。

話が前後するが、回転速度がどれくらい出せるかが気になって調べた。そのとき、1.5 Hz。1分あたりにすると 40 回転くらいのスピードしか出ないという悲しい事実が発覚。

もっと速くしたい。少なくとも一桁は速くしないといけないだろう。

まず、自分のプログラミングに問題があった。無駄に時間がかかっていそうなところを改良した。

次にわかったことは、シリアルモニターに情報を表示すると異様に遅くなるということだった。モニターするのをやめたら 10 Hz を超えるくらいにはなった。600 RPM だ。用意したモーターにはこの程度でも良いかもしれない。

しかし、ドローンやラジコン機のモーターは、定格が数千 RPM とある。そのくらいに対応できるようにしたいものだ。そこで、さらなる高速化を目指すことに。

次にやることは、Arduino 特有の命令をやめることだった。

digitalWrite(9, HIGH);
digitalWrite(9, LOW);

とか。これが処理を遅くしているらしいのだ。

ま、このような命令によって気軽にプログラミングできるのが Arduino の良いところとは思うが、残念ながらやめなければならない。

↑この辺とかから情報を得た。

そしてレジスターをいじる方向に進んでいく。

BV(5) って何?って思ったが、よくわからなかった。Nano の場合はこういう書き方はしないようだ。結果、BV(5) 的な表記を理解する必要はなかった。

どうやらポートレジスタとかいうものを理解する必要があるらしい。

で、こことか

こことか

ここを参考にした。

オフィシャルな情報もあった。

AVR って何?とか思いながら、いろいろ情報を漁る。

Arduino Nano Every のプロセッサーは ATmega4809。

Arduino pinout の図において、内側から2個目のオレンジ色のところに書いてあるのが ATmega4809 の端子。PE2 とか、PD7 とか書いてあるのがそれだ。

っていうか、素人なので、図の見方がわからない。

ATmega4809 のポートは、PA から PF まである。なんで小分けになっている?Arduino UNO と全然違う。UNO なら比較的情報が多いが、Nano Every に関する情報はなかなか見つからず、苦労した。

でも最低限の情報は入手できて、ポートアサインの構想を練る。

ブラシレスモーターに使うのは6ビットだ。できれば、6ビットを一気に書き込みたい。あっちに書いてこっちに書いてとかやっていたら行が増えるし、時間差も生じるだろう。6つ揃っているポートを探したら、あった。PORT D だ。PD00 から PD05 まで揃っている。

これにいっぺんに書き込めば、モーターに流す電流の時間差が気にならなくなるだろう。命令も1行で済むから気分が良い。

アナログ入力に1端子(A4 | D18)、デジタル入力2ビットにポート PB0x、デジタル出力6ビットにポート PD0x を使うことにして、次のように割り当てた。

画像2

表中、D1n OUTとかD2p OUTとかいう表記は、n-channel MOS -FETとかp-channel MOS-FET のゲートへのコントロールという意味でつけてみた。

D1p は 1個目の p-channel MOS-FET、D1n は1個目の n-channel MOS-FET ということ。

PORT D って何ビット

ポートにデータを送る場合、目的のビットだけを変更しないとおかしなことになるといった書き込みを見た。

そこで

PORTB.DIR &= ~0b11;

だとか、やるべきかなと思った。

しかしモーターのコントロールは、0 にしたり 1 にしたりしなければならないので、or だけとか and だけとかではなく、上のように1行で済むようなものではなかったのだ。

ということで、

PORTD.OUT

で PORT D の状態を読んで、それを 0b111111 でマスクしてから、モーター制御用のデータと合体して、PORT D に書き込まなければならない。実際やってみたが、マスクするのが無駄じゃないかと思った。6ビットを1行でうまく書き換えられないかと思って調べたが、そんなうまい話は見つからなかった。よくわからないままやっているが、実際8ビットのハンドリングが必要なのだろうか。必要なら必要で良いが、もしも必要ないのなら、無駄でしかないだろう。

シリアルモニターで上位2ビットを観察した。すると常時 0 だった。見ている限りにおいて、1 になることはなかった。それらのビットのことは、仕様書にも書かれていないし。よって、PORT D レジスターに 6ビットだけ書くことは問題ないと判断した。6ビットデータを書くとしたら、上位2ビットには 0 を書き込むことになる。

しかし、この辺りの疑問の答えはどこに?6ビットなのか8ビットなのかわからないが、詳細はどこで手に入る?まあ、それがわからないなら、ポートレジスターを触らなければよいだけの話なのかも。

波形観測

話は飛ぶが、波形を見てみた。

並べ替えinv

High レベルになるタイミングが、右から左の順になっている。少し気持ち悪いが、オリエンタルモーター社の情報を元に作ったらこうなった。回転が逆と思ったら、モーターの配線を2本、入れ替えれば良いだけのこと。

PinXX はモータードライバーICのピン番号。なお、Pin2、Pin4、Pin11 は実際の信号とは極性を反転して記載した。p-channel と n-channel では、極性が逆なので、そのままでは正しいシーケンスかどうかがわかりにくいからだ。

また、Tr1 〜 Tr6  はオリエンタルモーター社の資料にあった回路のトランジスターの番号。この資料ではバイポーラートランジスターを使っているので、オンオフの極性は6個とも同じ。

周波数の限界

最終的に、周波数の可変範囲は、1 Hz 以下から 145 Hz までとなった。

モーターは、42ステップ、7 サイクルで 1 回転していたので、

145 Hz ➗ 7 ✖️ 60 = 1243 RPM Max

計算、合ってる?自分の技術ではこのあたりが限界かと。あとは、20 MHz で動作させるとか。

ちなみに、使用したモーターは比較的低速用のものだ。低速側は 1 Hz 以下でも動作した。時計の秒針がカチカチ進むくらいに感じたから。42ステップで1周するので、1分間1回転するくらいだろう。高速側は 60 Hz 付近が安定動作の限界と感じた。周波数を徐々に上げていくと145 Hz でも動作したが、起動できなかった。

電圧を上げたらもっと速く回ると思うが、その場合、回路を変えなければならない。エネループが電源というのも心もとないので、次のステップでは電圧が高くて容量の大きな電源を検討したい。

回転速度の調節にミリ秒単位の delay を使っているため、速度のコントロールがかなり不連続になっていた。そこでマイクロ秒による delay も使うようにした。

出来上がっていじっていたら、速度ムラが気になった。原因は調べていない。

ブレッドボードの状況

名称未設定1

これまでに書いたものの他に、スピードコントロールのボリューム(ポテンショメーター)、スタートストップ用のスイッチ(ボタン1)、反転用のスイッチ(ボタン2)が見える。

回路図

回路図

回路図を書くのに LibreOffice Draw を使った。LibreOffice Draw は一部に使えないところがあった。時間を無駄にした。

スイッチのチャタリング防止は、ハードウエア的にもソフトエウア的にも未実装。

スケッチ

// 3相ブラシレスモーターを動かしてみる
// スイッチを押すと回転始め,もう一度押すと停止
// 別のスイッチを押すと逆転
// Arduino とモータードライバー直結、P相:L + N相:H の組合わせは禁止

const int button1 = 9;     // 起動・停止スイッチ
const int button2 = 10;     // 逆転スイッチ

const unsigned long analogRange = 945 - 48; //ポテンショメーターの有効範囲

unsigned long delay_time;       //次のフェーズまでの時間(total)
unsigned long delay_time_ms;   //次のフェーズまでの時間(millisecond)
unsigned long delay_time_us;   //次のフェーズまでの時間(microsecond)
int state = 0;    //button1、0: 停止、1: 運転
int dir = 1;      //button2、回転方向、モーターの配線で変わる
int phase = 0;    //モーターの動作フェーズ
// モーターをドライブする論理値を2次元配列に入れる
//  [phase][dir]
int phase_set[7][2] = {
                       {0b010101,0b010101},  //stop
                       {0b110100,0b110001},  //1
                       {0b110001,0b110100},  //2
                       {0b010011,0b011100},  //3
                       {0b000111,0b001101},  //4
                       {0b001101,0b000111},  //5
                       {0b011100,0b010011}   //6
                     };
void setup() {
// initialize serial communication at 9600 bits per second:
 Serial.begin(9600);
// ボタン用のピンを入力として設定する、下位2ビットを LOW に
 PORTB.DIRSET = (0 << 0) | (0 << 1);
 //PORTB.DIR &= ~0b11; でも良い
//スタートストップボタンに割り込みを使う
 attachInterrupt(digitalPinToInterrupt(button1), on_off, FALLING);
//回転方向の反転スイッチに割り込みを使う
 attachInterrupt(digitalPinToInterrupt(button2), reverse, FALLING);
// PORTD の現在値を読んで下位6ビットだけ書き換えようとしたが
// そこまでしなくても6ビットを書き込めば良いみたい
// モーターへの出力ピンのモード設定、下位6ビットを HIGH、出力に設定
 PORTD.DIR |= 0b111111;
// MOS FET 全部オフ
 PORTD.OUT = phase_set[0][0];
}

// ON/OFF SW が押されたとき、state を反転する
void on_off() {
 state = !state;
 if (state == 0){
   PORTD.OUT = phase_set[0][0];
 }
}

// REVERSE SW が押されたとき、dir を反転する
void reverse() {
 dir = !dir;
}
 
void loop() {  
// ポテンショメーターの上端および下端は切り捨てる

int sensorValue = analogRead(A4);
    if (sensorValue < 49){
        delay_time =  analogRange * 1000 / (49 - 48);
    }else if (sensorValue > 945){
        delay_time =  analogRange * 1000 / (945 - 48);
    }else{
        delay_time = analogRange * 1000 / (sensorValue - 48);  // in microsecond
    }
    
    delay_time_ms = delay_time / 1000;
    delay_time_us = delay_time % 1000;

    if (state == 1){
        PORTD.OUT = phase_set[phase][dir];
         
        delayMicroseconds(delay_time_us);
        delay(delay_time_ms);
        
        // phase を1つずつ増やす
        phase += 1;
        // phase が 6 を超えたら1に戻す
        if (phase > 6){
            phase = 1;
        }
    }
}

note に貼り付けたらインデントがおかしい。手直ししたから余計おかしくなったかも。

phase_set[7][2] について補足
検討の途中で、シーケンスに合わせて場合分けして、モーターをコントロールするデータを出力するようにしていた。また、プログラムが長くなったので、関数化してみたりした。しかし、関数の呼び出しは時間を消費すると聞き、関数をやめ、ついでに場合分けもやめた。
フェーズは数値なので、それを配列のインデックスに当てはめれば、必要なデータを1発でゲットできると気づき、配列化した。結果、
    PORTD.OUT = phase_set[phase][dir];
という1行に集約できた。

全部捨てたらどこまで速くなるか(追記)

loop の中のポテンショメーター関連の処理をやめて、delay もほぼゼロにしたところ、10 kHz くらい行くことがわかった。

void loop() {
// Delay なしで
    while(state == 1){
        PORTD.OUT = phase_set[phase][dir];
        delay(0);      
        phase = newPhase[phase];
    }
}

newPhase[] は配列。下のスケッチでは、1ずつ増やして6を超えたら1に戻るなんてやっているやつ。配列化して、現在のフェーズを指定したら次のフェーズが返ってくるようにした。結果、場合分けや足し算がいらなくなった。

delay を完全になくしたら、動かなくなった。

最速では、ジッターというか周波数の揺らぎがすごく気になった。

感想

Arduino の言語は、C言語。語弊があるかもしれないが、C言語と言い切ってしまおう。C言語は、過去に学習しようとして挫折したままになっている。難しいと感じる。ま、勉強していないからわからないのは当たり前だ。とりあえず、必要なところだけ調べてなんとか動くプログラムにした。人がみればおかしなことをやっていると思うだろうけど。

Language reference

t.koba

この記事が気に入ったらサポートをしてみませんか?