モーターを鳴らして演奏することが流行っているので自分でもやってみました。ついでにステッピングモーターについて理解を深めました。
はじめに
プログラムにC言語を使いました。
この記事はすでにステッピングモーターを回すことができる人向けです。
今回は2相ユニポーラのハイブリッドステッピングモーターを使いました。
2020.5.31追記 図がところどころ間違っていたので直しました。
ステッピングモーターとは[1]「パルス信号によって回転角度・回転速度を正確に制御できるモーター」です。このパルス信号の周波数を変えると回転速度を変えられます。速く回転させると高い音が鳴り、遅く回転させると低い音が鳴ります。ということは、このモーターは周波数を調整すればドレミファソラシドの音を鳴らせて音楽を演奏できるモーターと言い換えることができます。
※[1], [2], ・・・は参考文献番号。以下同様。
駆動方式
2相ステッピングモーターには2つコイルが使われています。巻き方はモノファイラ巻きとバイファイラ巻きの2種類あります。[2]一つの極に一つのコイルを一方向に巻くのが「ユニファイラ巻き」で、一つの極に2本の線を重ねて巻くのが「バイファイラ巻き」です。
ユニファイラ巻きのステッピングモーターはバイポーラ駆動で動かします。一方でバイファイラ巻きのステッピングモーターはユニポーラ駆動で動かします。
今回使うステッピングモーターはユニポーラ駆動で動かすステッピングモーターです。
励磁方式
[3],[4]各相のコイルへ決まった順番で電流を流す方式を励磁方式といいます。
1相励磁
主にユニポーラモータで使用します。 1相ずつ励磁位相を進める方式で、フルステップ駆動ともいいます。 消費電力は少なくなりますが、振動が発生し易くなります。
2相励磁
バイポーラモータでのフルステップ駆動方式です。 2相ずつ励磁位相を進める方式です。 ユニポーラモータの1相励磁よりも、 出力トルクが大きくダンピング性に優れており、より滑らかに動作します。
1-2相励磁
1相励磁と2相励磁を交互に繰り返す方式で、ハーフステップ駆動ともいいます。 モータのステップ角分解能は、1相や2相励磁方式の半分になります。 2相励磁よりも更に滑らかに動作します。
マイクロステップ駆動
フルステップ、ハーフステップ駆動が、2つのコイル電流をON/OFFし一定角ずつ回転させるのに対し、 マイクロステップ駆動は、2つのコイル電流比率を階段状に変化させることで、 更に細かいステップ角で回転させることができます。
今回はSLA7073MPRTというモタドラを使いました。マイコンでパルス波を出力し、回転方向(CW/CCW)と励磁方法を決めてモタドラに入力すると、モタドラからユニポーラ駆動のステッピングモーターを回すために必要な電気信号4つ( A相, Aバー相, B相, Bバー相)が出力されます。
音が鳴る原理
モタドラからの周期的なパルス電気信号が磁界の中にあるコイルを流れると、ステッピングモーターのローター(回転子。これには磁石が付いています。)とステータ(固定子。これにはコイルが付いています。)に周期的に力が働くので振動します。これがステッピングモーター音の原因と推測されます。
2019.9.3追記 推測しただけであって未検証です。
参考文献[5]によるとステッピングモーターをマイクロステップ駆動すると静音化できるようです。マイクロステップ駆動では単純なON/OFFのパルス波ではなく比較的滑らかな電気信号でモーターを駆動するから振動が抑えられたのでしょう。
2019.9.1追記 マイクロステップ駆動による静音化については自分で試しておりません。各自試してみてください。
2020.5.16追記 マイクロステップ駆動を試してみました。そして1-2相励磁のときとマイクロステップ駆動のときとでステッピングモーターが出す音を耳で確かめました。そのときモタドラへ入力するパルス信号の周波数は両方とも同じにしました。その結果、マイクロステップ駆動のときのほうが音が小さかったです。音の高さは両方とも同じでした。しかし、少し音が小さいだけでした。マイクロステップ駆動でも無音化できないようです。
ドレミファソラシド
ネットで調べればドレミファソラシドの周波数が調べられます。今回は参考文献[6]を使って必要な音階の周波数を求めました。
曲をプログラミングするためには
ステッピングモーターで演奏するために必要なのは音の高さと長さの2つです。だからプログラムにおける楽譜は音の高さとその長さを格納した配列で表せます。ステッピングモーターを駆動する電圧を音ごとに変更すれば音の大きさを調整できるかもしれませんが今回はやりませんでした。あと、音色は変更できません。
C言語に使える文字に「#」と「♭」は含まれていません。そこでドイツ音階を代わりに使いました。ドイツ音階なら全てアルファベットで書き表せるからです。
対応表 |
ド
|
レ
|
ミ
|
ファ
|
ソ
|
ラ
|
シ
|
臨時記号
なし
|
C
ツェー
|
D
デー
|
E
エー
|
F
エフ
|
G
ゲー
|
A
アー
|
H
ハー
|
#
|
Cis
ツィス
|
Dis
ディス
|
Eis
エイス
|
Fis
フィス
|
Gis
ギス
|
Ais
アイス
|
His
ヒス
|
♭
|
Ces
ツェス
|
Des
デス
|
Es
エス
|
Fes
フェス
|
Ges
ゲス
|
As
アス
|
B
ベー
|
ドはCではなくDO、ド#はCisではなくDO_SHARP、レはDではなくREとすれば少し読みやすいプログラムになったかもしれませんが、楽譜を全部手打ちしたため一音が短いほうが楽だったのでドイツ音階を採用しました。
パルス波(としてPWMを使います)の周期を変えることで色々な高さの音を鳴らします。
PWM周期レジスタに入れる値
鳴らしたい音階の周波数(これをf[Hz]とおきます)を元にして実際にPWM周期レジスタに入れる値を計算します。
式(1)の「タイマのクロック周波数」の単位は[Hz]です。どのマイコンを使うかとクロックをどう設定するかとタイマをどう設定するかによってこの値は異なるので自分で調べましょう。
今回はSTM32F303K8T6マイコンをシステムクロック周波数64[MHz]に設定し、プリスケーラでタイマのクロック周波数を1[MHz]にして使いました。このマイコンにはFPU(浮動小数点演算装置)がついているので、遠慮なく小数を含む計算をマイコンで行います。
この計算は次のようにマクロを使って実現しました。2行目のマクロのXには音階の周波数(実数)が入ります。
#define CLK_FREQ 1000000
#define FREQ2PWMPERIOD(X) (int)(CLK_FREQ / (X))
マクロ
音の高さと長さをマクロで表します。音の高さには鳴らしたい音の周波数を、音の長さには実際に伸ばす時間[ms]を使います。
音階
音階マクロを例示します。ドレミファソラシとド♯レ♯ファ♯ソ♯ラ♯です。例えばレ♭はド♯と同じ音なのでマクロにする必要はありません。
5はオクターブの番号です。音が高いオクターブほど大きい数字を使います。どの数字を使うかは実際に鳴らして決めてください。
休符はRESTとしました。
ここでは1オクターブ分だけしか示していないので、必要があれば自分で補ってください。
音が低すぎても高すぎてもきれいにモーターが鳴りません。実際に鳴らしてどのオクターブならきれいに鳴るかを確かめてみてください。
#define C5 523.251 #define Cis5 554.365 #define D5 587.330 #define Dis5 622.254 #define E5 659.255 #define F5 698.456 #define Fis5 739.989 #define G5 783.991 #define Gis5 830.609 #define A5 880.000 #define Ais5 932.328 #define H5 987.767 #define REST 0
2020.4.2追記:Shift_JISの文字コードで表現された「ソ」の2バイト目は0x5Cであり、プログラムをコンパイルするときに「\」と認識されてしまいます。このように2バイト目が0x5Cである文字を駄目文字と呼ぶそうです。意図通りにコンパイルするためにはコメント「//ソ」を「//そ」に書き換えるなどの工夫が必要です。
長さ
長さマクロを示します。全音符と全休符の長さを表すlen1が1536[ms]である理由は3で割り切れる数字のほうがいいかと思ったからです。ちょうどいい長さであればいくつでもいいと思います。
#define LEN1 1536
#define LEN2 LEN1 / 2
#define LEN2d LEN1 / 4 * 3
#define LEN4 LEN1 / 4
#define LEN4d LEN1 / 8 * 3
#define LEN3_4 LEN1 /2 / 3
#define LEN8 LEN1 / 8
#define LEN3_8 LEN1 / 4 / 3
#define LEN8d LEN1 / 16 * 3
#define LEN16 LEN1 / 16
楽譜
例として「かえるの合唱」の楽譜について説明します。
音の高さと長さを別々の配列に格納しました。二元配列を使って一つの配列に音の高さと長さの両方を格納してもいいと思います。この2つを合わせて楽譜配列と呼びます。
frog_notesには音階マクロを格納します。この配列を音階配列と呼びます。
frog_lenには長さマクロを格納します。この配列を長さ配列と呼びます。
音階配列frog_notes[i]に格納した音を長さ配列frog_len[i]に格納した長さ分伸ばして演奏します。
ヘッダファイルfrog_notes.hに音階をまとめて記述し、ヘッダファイルfrog_len.hに音の長さをまとめて記述します。そして配列宣言時に配列の中に音と長さを格納します。
const float frog_notes[] = {
#include "frog_notes.h"
};
const uint16_t frog_len[] = {
#include "frog_len.h"
};
音階配列の中身であるヘッダファイルfrog_notes.hには次のように書いて下さい。コメントの数字は小節番号を表しています。
C5 ,
D5 ,
E5 ,
F5 ,
E5 ,
D5 ,
C5 ,
REST ,
E5 ,
F5 ,
G5 ,
A5 ,
G5 ,
F5 ,
E5 ,
REST ,
C5 ,
C5 ,
C5 ,
C5 ,
C5 ,
C5 ,
D5 ,
D5 ,
E5 ,
E5 ,
F5 ,
F5 ,
E5 ,
D5 ,
C5 ,
長さ配列の中身であるヘッダファイルfrog_len.hには次のように書いて下さい。
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN4 ,
LEN2 ,
LEN2 ,
LEN2 ,
LEN2 ,
LEN8 ,
LEN8 ,
LEN8 ,
LEN8 ,
LEN8 ,
LEN8 ,
LEN8 ,
LEN8 ,
LEN4 ,
LEN4 ,
LEN2 ,
実はステッピングモーター演奏で一番大変なのが楽譜を書く作業です。まず曲を聴きながら楽譜に書き起こし、プログラムに書き込みます。めちゃくちゃ面倒なので途中で何度も止めようかと思いましたが馬鹿力でやりきりました。だれか楽に演奏できるようにするための方法を知っていたら教えてほしいです。ちなみに私は「コンバットマーチ」と「ようこそジャパリパークへ」と「アンダー・ザ・シー」と「ゲゲゲの鬼太郎」と「エレクトリカルパレード」と「君をのせて(天空の城ラピュタ)」と「カエルの歌(二重奏)」をプログラムに書き込みました。しんどかったです。
耳コピした内容が間違ってないか確認するためにDOMINOというフリー作曲ソフトで音を確認しました。このようなソフトを使ってうまくやる方法がないか考え中です。
楽譜読み込み
楽譜配列をfor()文で読み込みます。ms_wait()はミリ秒待機する自作関数です。レジスタ構造体にHALライブラリを使っているので注意してください。今回は2個のステッピングモーターを同時に同じように鳴らします。TIM16によるPWM波で1個目のステッピングモーターを制御し、TIM17によるPWM波で2個目のステッピングモーターを制御します。
まず、音階配列に格納した周波数[Hz]からPWM波の周期を計算し、TIM16->ARRレジスタに代入します。タイマカウンタの比較一致値をPWM波の周期の半分とします。(必ずしもタイマカウンタの比較一致値をPWM波の周期の半分にする必要はありません。)そのあとに長さ配列に格納した伸ばす長さ[ms]分だけ待機します。最後にTIM16の2つのレジスタに0を代入して音を消し、少しだけ待機します。(今回は10[ms]待機しました。)こうすることで音と音が区切れて音楽が聴きとりやすくなります。TIM17についても同様です。以上の流れにより音を1音鳴らせます。次の音を鳴らしたいときは配列の添え字iをインクリメントして楽譜配列の次の音を読みに行きます。
ここで休符マクロRESTを0にした効果が現れます。場合分けしなくても休符のときはARRレジスタに0が代入されるので音を消せます。
for(i = 0; i < 31; i++) {
TIM16->ARR = FREQ2PWMPERIOD(frog_notes[i]);
TIM16->CCR1 = TIM16->ARR / 2;
TIM17->ARR = TIM16->ARR;
TIM17->CCR1 = TIM16->CCR1;
ms_wait(frog_len[i]);
TIM16->ARR = 0;
TIM16->CCR1 = 0;
TIM17->ARR = 0;
TIM17->CCR1 = 0;
ms_wait(10);
}
main関数
main関数はこんな感じになります。init()関数は各種初期化を行う自作関数です。
int main(void)
{
init();
GPIOB->ODR &= ~GPIO_ODR_7;
uint8_t i;
for(i = 0; i < 31; i++) {
TIM16->ARR = FREQ2PWMPERIOD(frog_notes[i]);
TIM16->CCR1 = TIM16->ARR / 2;
TIM17->ARR = TIM16->ARR;
TIM17->CCR1 = TIM16->CCR1;
ms_wait(frog_len[i]);
TIM16->ARR = 0;
TIM16->CCR1 = 0;
TIM17->ARR = 0;
TIM17->CCR1 = 0;
ms_wait(10);
}
GPIOB->ODR |= GPIO_ODR_7;
while(1);
return 0;
}
プログラム全景
マイコンの初期設定についてはここでは説明しませんが、ソースコードを載せておきます。色々とコメントをつけておいたので参考にしてください。
2019.8.15追記:SW4STM32(開発環境)に最初からついてくるスタートアップファイル(?)を使うと、このコードをコンパイルできない可能性があります。参考にするだけにしてください。
2020.4.2追記:Shift_JISの文字コードで表現された「ソ」の2バイト目は0x5Cであり、プログラムをコンパイルするときに「\」と認識されてしまいます。このように2バイト目が0x5Cである文字を駄目文字と呼ぶそうです。意図通りにコンパイルするためにはコメント「//ソ」を「//そ」に書き換えるなどの工夫が必要です。
#include "stm32f3xx.h"
#define C5 523.251
#define Cis5 554.365
#define D5 587.330
#define Dis5 622.254
#define E5 659.255
#define F5 698.456
#define Fis5 739.989
#define G5 783.991
#define Gis5 830.609
#define A5 880.000
#define Ais5 932.328
#define H5 987.767
#define REST 0
#define LEN1 1536
#define LEN2 LEN1 / 2
#define LEN2d LEN1 / 4 * 3
#define LEN4 LEN1 / 4
#define LEN4d LEN1 / 8 * 3
#define LEN3_4 LEN1 /2 / 3
#define LEN8 LEN1 / 8
#define LEN3_8 LEN1 / 4 / 3
#define LEN8d LEN1 / 16 * 3
#define LEN16 LEN1 / 16
#define CLK_FREQ 1000000
#define FREQ2PWMPERIOD(X) (int)(CLK_FREQ / (X))
const float frog_notes[] = {
#include "frog_notes.h"
};
const uint16_t frog_len[] = {
#include "frog_len.h"
};
void init(void);
void sysclk_init(uint8_t multiple);
void drive_init(void);
void ms_wait(uint32_t ms);
int main(void) {
init();
GPIOB->ODR &= ~GPIO_ODR_7;
uint8_t i;
for(i = 0; i < 31; i++) {
TIM16->ARR = FREQ2PWMPERIOD(frog_notes[i]);
TIM16->CCR1 = TIM16->ARR / 2;
TIM17->ARR = TIM16->ARR;
TIM17->CCR1 = TIM16->CCR1;
ms_wait(frog_len[i]);
TIM16->ARR = 0;
TIM16->CCR1 = 0;
TIM17->ARR = 0;
TIM17->CCR1 = 0;
ms_wait(10);
}
GPIOB->ODR |= GPIO_ODR_7;
while(1);
return 0;
}
void ms_wait(uint32_t ms) {
SysTick->LOAD = 8000 - 1;
SysTick->VAL = 0;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
uint32_t i;
for(i = 0; i < ms; i++){
while( !(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) );
}
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}
void init(void) {
sysclk_init(16);
drive_init();
}
void sysclk_init(uint8_t multiple) {
if( multiple <= 0 || multiple > 16 ) {
return;
}
RCC->CFGR |= RCC_CFGR_PLLSRC_HSI_DIV2;
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;
RCC->CFGR |= ( (multiple - 1) << RCC_CFGR_PLLMUL_Pos);
FLASH->ACR |= FLASH_ACR_LATENCY_1;
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
RCC->CFGR |= RCC_CFGR_SW_PLL;
while( (RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL );
}
void drive_init(void) {
RCC->AHBENR |= RCC_AHBENR_GPIOBEN;
GPIOB->MODER &= ~GPIO_MODER_MODER7_Msk;
GPIOB->MODER &= ~GPIO_MODER_MODER6_Msk;
GPIOB->MODER &= ~GPIO_MODER_MODER3_Msk;
GPIOB->MODER |= (GPIO_MODE_OUTPUT_PP << GPIO_MODER_MODER7_Pos);
GPIOB->MODER |= (GPIO_MODE_OUTPUT_PP << GPIO_MODER_MODER6_Pos);
GPIOB->MODER |= (GPIO_MODE_OUTPUT_PP << GPIO_MODER_MODER3_Pos);
GPIOB->ODR |= GPIO_ODR_7;
GPIOB->ODR |= GPIO_ODR_3;
GPIOB->ODR |= GPIO_ODR_6;
RCC->APB2ENR |= RCC_APB2ENR_TIM16EN;
TIM16->CR1 = TIM_CR1_CEN;
TIM16->CR2 = 0;
TIM16->DIER = TIM_DIER_UIE;
TIM16->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
TIM16->CCER = TIM_CCER_CC1E;
TIM16->BDTR = TIM_BDTR_MOE;
TIM16->CNT = 0;
TIM16->PSC = 63;
TIM16->EGR = TIM_EGR_UG;
RCC->AHBENR |= RCC_AHBENR_GPIOBEN;
GPIOB->MODER &= ~(1 << 8);
GPIOB->MODER |= 1 << 9;
GPIOB->AFR[0] |= (GPIO_AF1_TIM16 << GPIO_AFRL_AFRL4_Pos);
RCC->APB2ENR |= RCC_APB2ENR_TIM17EN;
TIM17->CR1 = TIM_CR1_CEN;
TIM17->CR2 = 0;
TIM17->DIER = TIM_DIER_UIE;
TIM17->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
TIM17->CCER = TIM_CCER_CC1E;
TIM17->BDTR = TIM_BDTR_MOE;
TIM17->CNT = 0;
TIM17->PSC = 63;
TIM17->EGR = TIM_EGR_UG;
RCC->AHBENR |= RCC_AHBENR_GPIOBEN;
GPIOB->MODER &= ~(1 << 10);
GPIOB->MODER |= 1 << 11;
GPIOB->AFR[0] |= (GPIO_AF10_TIM17 << GPIO_AFRL_AFRL5_Pos);
}
発展
ステッピングモーターを2個別々に鳴らして二重奏しましょう。2個のモーターで同じ音を鳴らしていた先ほどよりも複雑になります。楽譜配列がモーター1用音階配列、モーター2用音階配列、待機時間配列、モーター1用処理配列、モーター2用処理配列の5つ必要だからです。
モーター1, 2が鳴っている(鳴)または鳴ってない(静)状態の組み合わせは(1, 2)=(静, 静), (鳴, 静), (静, 鳴), (鳴, 鳴)の4通りあります。ある状態がどれくらいの長さ続くかを格納した配列が待機時間配列です。この待機時間とはモーター1と2のどちらかで状態(鳴/静)が変化してから次に変化が起こるまでの時間です。ある状態から次の状態に移るときどういう命令をしてやればよいかを格納してある配列がモーター1, 2用処理配列です。ここで言っている処理とはある状態の始まりに行われる命令と終わりに行われる命令の組のことです。(始, 終)=(鳴らす, 消す), (鳴らす, そのまま), (そのまま, そのまま), (そのまま, 消す)の4通りあります。
これらを扱う関数を作ってください。この関数の名前を二重奏関数と呼ぶことにします。二重奏関数が呼び出されるとまずモーター1, 2用処理配列を見てモーター1, 2に別々の命令をします。待機時間分待機したらモーター1, 2に別々の命令をします。そして二重奏関数を抜けます。二重奏関数を実行している間は上記の4通りの状態のうちどれか一つにいることになります。そして再び二重奏関数を呼び出します。この繰り返しによって二重奏を実現します。
大変ですね。だからこの項目は発展にしたのです。一応状態遷移を示しておきます。この図は左から始まって時間が進み、右で終わる図です。各列が各状態を表し、時間が経過して次の列に移るたびに二重奏関数が呼び出されます。
豆知識
ステッピングモーターを励磁させなくても小さな音が鳴ります。マイコンから出力されるパルス波の音だと思います。
2相励磁で音を鳴らしてみると1-2相励磁のときと比べて1オクターブ高い音が鳴りました。前のほうで示した励磁波形の図を見てください。1-2相励磁のパルス波の周期は2相励磁のパルス波の周期の2倍です。つまり2相励磁のパルス波の周波数は1-2相励磁のパルス波の周波数の2倍です。周波数が2倍になると、音が1オクターブ高くなるので2相励磁のときは1-2相励磁のときより1オクターブ高い音が鳴ります。
参考文献
[1]ステッピングモーターの概要 - ステッピングモーターとは|製品情報 |オリエンタルモーター株式会社、2019年2月7日閲覧
[2]ユニポーラ駆動とバイポーラ駆動、2019年2月7日閲覧
[3]ステッピングモータの駆動運転 - indexPro、2019年2月7日閲覧
[4]モータを廻す、2019年2月7日閲覧
[5]気圧計のバグつぶしと、ステッピングモーターの静音化: がた老AVR研究所、2019年2月7日閲覧
[6]音階と周波数の対応表、2019年2月7日閲覧