電磁部品で音を奏でる、エレマグハーモニー第2弾。STM32のマイコンでソフトウェアから送られてきた情報をどう処理しているかについてご説明していこうと思います。
皆さんこんにちは。Jinです。
春も終わり、すっかり暑くなってきましたが、皆さんいかがお過ごしでしょうか。
一年ももう半分が終わろうとしています。時の流れって早いですね…
前回ご紹介したプレイヤーからの情報を処理するマイコンのプログラムをご紹介します。前回の記事は以下のものになります。あわせてご確認くださいませ。
githubにてソースコードを掲載しております。
また、こちらの企画ではJLCPCBにご支援をいただいております。ぜひご覧ください。
ピンアサイン
STM32のプログラムはCubeIDEで記述しているため、ほとんどのコードは自動で生成してくれます。
CubeIDEの設定項目で、ピンは以下のようになっています。
Stp_Motorのピンはタイマー3のOutput Compare出力に設定されています。具体的な制御方法は後述しますが、ステッピングモータは二相励磁で制御しています。
Solenoidは同じくタイマー3のPWM出力、Relayは全てGPIO標準のHAL出力、Floppy制御に必要なDirectionNとStepNのピンに関してはタイマーで制御していますが出力はGPIO標準のHAL出力になります。
STM32のマイコン内で行っている各制御を詳しくご紹介していきます。
シリアル通信割り込み
まずはシリアル通信(USART)です。UARTは非同期型のシリアル通信で、よくマイコンとPC間の通信に使用されます。
USARTは非同期通信のみ対応のUARTに、同期通信の機能を加えたものになりますが、今回は非同期しか使用しません。
詳しいシリアル通信の話は今後別のところでまとめさせていただくとして、今回はどういう処理を行っているかに焦点を当てます。
STM32側のUSART通信の設定は以下のようになっております。
Baud Rateは限界を責めています。というのも、ソフトウェア側の処理を変更し、PCやマイコンの処理速度に依存するプログラムとしていますので、限界まで通信速度を上げないと曲のテンポが遅れてしまいます。
その他はデフォルトのままですが、以下のように割り込みを設定しています。これにより、マイコンにUART受信があった場合(=midiデータを受信したとき)周波数を即座に変更できるようになっています。
まずUART通信では、送られたデータをバッファに格納し、1バイト溜まった時にこの割り込み処理を発生させています。そのため、1バイトごとに処理を分ける必要があります。
よってまず最初にuartCntというグローバル変数でデータ本体かデータのヘッダーかを判定しています。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
static uint8_t event = 0;
static uint8_t part = 0;
//whether data header or data body
if(uartCnt){
...
}else{
...
}
//next data
HAL_UART_Receive_IT(&huart2, (uint8_t *)data, 1);
}
ソフトウェアから送信されるデータは2バイトあり、1バイト目(ヘッダー)にはイベントとパート、2バイト目(ボディ)には演奏するノーツ番号の情報が格納されています。
データの格納を終えると、HAL_UART_Receive_IT関数で次の1バイト受信待機処理を行っています。
if文は非0の時実行であるため、1バイト目の受信をelse文で記述します。上記コードのelse文の中身は、以下のようになっています。
event = data[0] & 0xf0;
part = data[0] & 0x0f;
if(event){
//note On
uartCnt++;
}else{
//note Off
//reset value
notes[part] = 0;
freqs[part] = 0;
if(part < TimerNum){
if(part < 1){
//reset Timer of melody(solenoid & stp motor)
HAL_TIM_PWM_Stop(& times[part], TIM_CHANNEL_1);
HAL_TIM_OC_Stop(& times[part], TIM_CHANNEL_2);
HAL_TIMEx_OCN_Stop(& times[part], TIM_CHANNEL_2);
HAL_TIM_OC_Stop(& times[part], TIM_CHANNEL_3);
HAL_TIMEx_OCN_Stop(& times[part], TIM_CHANNEL_3);
}else if(part > 4){
//reset timer of floppy
HAL_TIM_Base_Stop_IT(& times[part]);
}
}
}
これは1バイト目の受信処理です。最初にシフト演算でイベントの種類と鳴らすパート情報を格納しています。1バイト8ビットの内、上位4ビットがイベントタイプ、下位4ビットがパート情報になります。
パートは0がメロディ、1~4がドラム、5~7がベースの情報になっています。
もし1バイト目のデータでNoteがOffであった場合、各種変数をリセットした後、タイマーや出力をリセットしています。ドラムの場合ノートオフはソフトウェアで無視されるため、処理を記述する必要がありません。
NoteがOnであった場合は次の1バイトでノート番号を受け取るため、UartCntをインクリメントしておきます。
次に、if文の中に記述された、ノート番号を受け取る処理です。
notes[part] = data[0];
freqs[part] = noteParFreq[notes[part]];
if(part < TimerNum){
if(part >= 5 || part < 1){
//double the frequency if it is floppy or stp motor
freqs[part] *= 2;
setTimer(part, times[part], (timer_clock / (freqs[part] * timerPeriod) - 1),70);
}else{
notes[part] = 0;
HAL_GPIO_TogglePin(relayPort[part - 1], relayPin[part - 1]);
}
}
uartCnt = 0;
パートごとのノートの配列に情報を格納します。その後、ノート番号と周波数の変換処理を行います。ノート番号と周波数の変換処理はデータテーブルによって行っています。
次にpart変数でどのパートかを判定し、メロディorベースであればsetTimer関数を呼び、ドラムであればトグルすることで楽器を演奏します。
タイマーをセット後、uartCntをリセットして次のデータを待機します。
タイマー割り込み
シリアル割込みでノートがOnだと判断されたら、setTimerに周波数情報を入力して反映させています。
ドラムは後述の出力トグルで演奏していますが、メロディを担当するソレノイドやモーター、ベースを担当するフロッピーは周期的に振動を起こして音の波を伝播させる必要があります。
そのため、タイマーにてパルス(矩形波)を出力しています。特に高機能なTIM1ではメロディの出力を行っており、その設定は以下のようになっています。
Channel1にソレノイド用のPWM出力、Channel2と3はそれぞれステッピングモータの二相のモータに信号を送るためのものになっています。
Clock SourceはInternal Clock(内部にて生成される)に設定しており、その周波数は以下のように64MHzとなっております。
setTimer関数は以下のようになっています。
//Set frequency of Timer
void setTimer(uint8_t part, TIM_HandleTypeDef htim, uint32_t prescaler, uint32_t pwm){
if(pwm > 99) pwm = 99;
if(prescaler < 0) prescaler = 799;
htim.Instance = htim.Instance;
htim.Init.Prescaler = prescaler;
htim.Init.CounterMode = TIM_COUNTERMODE_UP;
htim.Init.Period = timerPeriod - 1; //because of start with 0
htim.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim);
if(part < 1){
TIM_OC_InitTypeDef sConfigOC = {0}; //pwm setting only
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = pwm;
//set duty
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
HAL_TIM_PWM_ConfigChannel(&htim, &sConfigOC, TIM_CHANNEL_1);
//start
HAL_TIM_PWM_Start(&htim, TIM_CHANNEL_1);
sConfigOC.OCMode = TIM_OCMODE_TOGGLE;
sConfigOC.Pulse = 0;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
HAL_TIM_OC_ConfigChannel(&htim, &sConfigOC, TIM_CHANNEL_2);
HAL_TIM_OC_Start(&htim, TIM_CHANNEL_2);
HAL_TIMEx_OCN_Start(&htim, TIM_CHANNEL_2);
sConfigOC.Pulse = timerPeriod / 2 - 1;
HAL_TIM_OC_ConfigChannel(&htim, &sConfigOC, TIM_CHANNEL_3);
HAL_TIM_OC_Start(&htim, TIM_CHANNEL_3);
HAL_TIMEx_OCN_Start(&htim, TIM_CHANNEL_3);
}else{
// start
HAL_TIM_Base_Start_IT(&htim);
}
}
最初にhtim(halのタイマー)を初期化します。その後、PWMや出力比較モード(ステッピングモータ用、後述)を初期化し、設定を行っています。
それぞれの設定項目の意味はドキュメントをご参照ください。フロッピーの場合、タイマー割り込みを行うので、HAL_TIM_Base_Start_ITで割り込み用のタイマーを始動させています。
//Timer interrupt (floppy control only)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
uint8_t timerIdx = 0;
for(; timerIdx < TimerNum; timerIdx++){
if(htim->Instance == times[timerIdx].Instance)
break;
}
if(timerIdx < TimerNum){
//Update position
//timer : 5 --> floppy : 0, 1
//timer : 6 --> floppy : 2, 3
//timer : 7 --> floppy : 4, 5
uint8_t floppyIdx = timerIdx - 5;
currentPosition[floppyIdx] = (currentState[floppyIdx] == GPIO_PIN_SET) ? currentPosition[floppyIdx] - 1 : currentPosition[floppyIdx] + 1;
if(currentPosition[floppyIdx] <= 0){
currentState[floppyIdx] = GPIO_PIN_RESET;
HAL_GPIO_WritePin(floppyPort[floppyIdx * 2], floppyPin[floppyIdx * 2], currentState[floppyIdx]);
}else if(currentPosition[floppyIdx] >= 158){
currentState[floppyIdx] = GPIO_PIN_SET;
HAL_GPIO_WritePin(floppyPort[floppyIdx * 2], floppyPin[floppyIdx * 2], currentState[floppyIdx]);
}
HAL_GPIO_TogglePin(floppyPort[floppyIdx * 2 + 1], floppyPin[floppyIdx * 2 + 1]);
}
}
フロッピー用タイマー割り込み処理です。まず、タイマーによってこの割り込み関数が呼ばれた時、そのタイマーからパートの番号(timerIdx)を取得しています。
パート番号から出力するフロッピーのピンを選択し、ステップを1ステップ進めています。この割り込み関数はsetTimerで設定した周波数で周期的に呼ばれるため、この1ステップ進める処理を繰り返し振動を起こします。
GPIO制御
GPIOはポートとピンの全てを以下のように配列に格納して制御しやすくしております。
GPIO_TypeDef* relayPort[RelayNum] = {};
uint32_t relayPin[RelayNum] = {};
GPIO_TypeDef* floppyPort[FloppyNum * 2] = {};
uint32_t floppyPin[FloppyNum * 2] = {};
TIM_HandleTypeDef times[TimerNum] = {};
...
/* USER CODE BEGIN 2 */
//floppy1 --> 0 : direction, 1 : step
//floppy2 --> 2 : direction, 3 : step
//floppy3 --> 4 : direction, 5 : step
floppyPort[0] = Direction1_GPIO_Port;
floppyPort[1] = Step1_GPIO_Port;
floppyPort[2] = Direction2_GPIO_Port;
floppyPort[3] = Step2_GPIO_Port;
floppyPort[4] = Direction3_GPIO_Port;
floppyPort[5] = Step3_GPIO_Port;
floppyPin[0] = Direction1_Pin;
floppyPin[1] = Step1_Pin;
floppyPin[2] = Direction2_Pin;
floppyPin[3] = Step2_Pin;
floppyPin[4] = Direction3_Pin;
floppyPin[5] = Step3_Pin;
relayPort[0] = Relay1_GPIO_Port;
relayPort[1] = Relay2_GPIO_Port;
relayPort[2] = Relay3_GPIO_Port;
relayPort[3] = Relay4_GPIO_Port;
relayPin[0] = Relay1_Pin;
relayPin[1] = Relay2_Pin;
relayPin[2] = Relay3_Pin;
relayPin[3] = Relay4_Pin;
ラベルで直接初期化しようとするとエラーが出たので、上記のように一つ一つ記述しています(汚いのはおいておいて…)
setTimer関数や、シリアル通信の受信処理では、パートから配列のindexを算出し、各GPIOのピンを制御しています。リレーやフロッピーの制御はこのように行っております。
フロッピードライブはステップを進める制御をしていますが、詳しいことは次回の記事にてフロッピーの内部にあるステッピングモータドライブの制御説明時に行いますので、是非ご確認ください!
ステッピングモータ制御
ステッピングモータでは、ニ相励磁の制御を行うにあたり、STM32のタイマーで出力比較機能を用いてその出力を行いました。
STM32のタイマーは複雑になっており、まずTimerClockをプリスケーラ値に設定された値で分周します。
プリスケーラで分周された周波数ごとに内部のカウントレジスタの値をカウントアップされ、その値が自動リロードレジスタ(ARR)に格納されている値(TIM_Period値)を超えたら、予め指定しておいた処理を行います。
今回使用する出力比較機能では、チャネルごとに0とARRの間のキャプチャコンペアレジスタ(CCR)が設定されており、このCCRとカウントレジスタの比較を行います。
このCCRが設定できると何がいいのかと言いますと、チャネルごとに位相が設定できます。後述するステッピングモータの二相励磁では、モータごとに位相を1/4周期ずらす必要があり、この出力比較モードで制御するのがお勧めです。
先ほどより繰り返し出てくるニ相励磁という制御は、ニ相のステッピングモーターのコイルに1/4周期ずつずらしてパルスをかける制御方式です。
詳しいことは後日モーターの詳細記事にまとめようと思っておりますので、是非チェックしてみてください!
今回は先述の出力比較機能を用いて、1/4周期ずつずらしたものをコイルに入力し制御しています。1/2周期のずれは反転出力ですので、チャネル一つに反転出力付きの出力を設定することで、2つのチャネルで制御が可能になっています。
まとめ
いかがだったでしょうか。今回は前回に引き続き、Ele Mag Harmonyの特にマイコン側のプログラムを解説させていただきました。
シリアル通信の受信をトリガーに、様々な設定を行ってハードウェアを制御しております。ハードウェアを作成するにあたって、基板をJLCPCBにご提供いただいておりますので、併せてご覧くださいませ。
今回の制御にて重要になってくるのは、やはりタイマーの設定で、特にSTM32のタイマーは非常に使い勝手がよく機能も豊富なので、是非皆さんも使ってみてください。きっとSTM32に惚れるはずです!!w
それではまた次の記事でお会いしましょう!GoodBye!!