【DRV8320HでBLDCを回したい】第3回:NUCLEO-F411REでBEMFを読み取る
はじめに
第2回はTeensy4.0を使用してBLDCを120度通電で強制転流させました。
第3回ではセンサレス120度通電を実現するのに必要な要素であるBEMFの読み取りについて書こうと思います。
BEMFの読み取り方法
BLDCを駆動させている時は基本PWMを使用しているので、波形がパルス状になっています。
拡大すると以下のような感じ、パルス状になっています。
今回は下記画像のようにPWMパルスのHIGHのタイミングでAD変換することでBEMFを読み取ってみようと思います。
NUCLEO-F411REの設定
Teensy4.0でPWMと同期してADCを行う設定が分からなかったので、今回はNUCLEO-F411REを使用します。
参考にしたのは以下のサイトです。以下のサイトではPWMパルスがすべてLOWのタイミングでADCしていますが、自分の場合は設定をアレンジしてPWMパルスがすべてHIGHのタイミングでADCします。
Cube MXの設定
Clock設定
Clock設定でタイマー関係を100MHzにしました。これは特に必要というわけではないんですがなるべく早いほうが良いかなと思ったので設定を変えました。
TIM設定
Channelの1~4を「PWM Generaton CHx」に設定。
- CH1:U相制御用出力
- CH2:V相制御用出力
- CH3:W相制御用出力
- CH4:ADCトリガ用
「Counter Mode」は「Center Aligned mode 1」に設定。Center Aligned modeにすることでカウンタがノコギリ波ではなく三角波になります。modeは1~3までありますがキャプチャコンペア割り込み発生タイミングが変わるようです。
「Counter Period」はPWM周期が20kHzになるように設定しています。ここはお好みで設定してください。
「Trigger Event Selection」は「Output Compare (OC4REF)」に設定。これでCH4のキャプチャコンペア割り込みが発生したタイミングでトリガーを発生できるようになるんだと思ってます。
また「TIM1 capture compare interrupt」を有効にしておきます。これは主にデバッグ用です。
ADC設定
使用するADCのCHにチェックを付けます。これは自分の使用するピンに合わせて設定してください。また、「ADC1 global interrupt」にチェックを付けます。これでADC完了時に割り込みが入るようになります。
ADC_Injected_ConversionModeの「Number Of Conversions」を「3」に設定します。これはUVWの三相をADCするためです。「External Trigger Source」を「Timer 1 Trigger Out event」に設定します。これによってTIMで設定した「Trigger Event Selection」をADCの開始トリガーに出来ます。あとは各「Injected Rank」の「Channel」を使用するADCのCHに変更します。
GPIO設定
あとはDRV8320Hを制御するのに必要なGPIOの設定を行いました。赤枠の部分を「GPIO_Output」で設定しています。
これでCube MXの設定は終了なのでGenerate codeします。
プログラム
追加したプログラムをファイル名ごとに説明します。
main.h
プログラムで新たに追加した定義です。PWM_DUTY_HIGHはBLDCを駆動する時のDuty比です。低めに設定しています。PWM_AD_TIMINGはAD変換するタイミングです。このあたりは波形を見ながら調整しました。DEBUG_BUFFER_SIZEはRAMの容量が許す限りは大きくしても問題ないと思います。
/* USER CODE BEGIN Private defines */ #define PWM_DUTY_HIGH 300 #define PWM_DUTY_LOW 0 #define PWM_AD_TIMING 100 #define DELAY_MS 1 #define DEBUG_BUFFER_SIZE 3200 /* USER CODE END Private defines */
main.c
主にデバッグ用に使用する変数です。実体はstm32f4xx_it.cに定義します。
/* USER CODE BEGIN PV */ extern int debug_ad_flag; extern int debug_buff[4][DEBUG_BUFFER_SIZE]; extern int debug_send_flag; /* USER CODE END PV */
BLDCの120度通電の各STEPの通電状態を切り替える用の関数です。
/* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ void update_CommutationState (int state) { switch(state) { case 0: // U -> V __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, PWM_DUTY_HIGH); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, PWM_DUTY_LOW); HAL_GPIO_WritePin(INL_U_GPIO_Port, INL_U_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_V_GPIO_Port, INL_V_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(INL_W_GPIO_Port, INL_W_Pin, GPIO_PIN_RESET); break; case 1: // U -> W __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, PWM_DUTY_HIGH); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, PWM_DUTY_LOW); HAL_GPIO_WritePin(INL_U_GPIO_Port, INL_U_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_V_GPIO_Port, INL_V_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_W_GPIO_Port, INL_W_Pin, GPIO_PIN_SET); break; case 2: // V -> W __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, PWM_DUTY_HIGH); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, PWM_DUTY_LOW); HAL_GPIO_WritePin(INL_U_GPIO_Port, INL_U_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_V_GPIO_Port, INL_V_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_W_GPIO_Port, INL_W_Pin, GPIO_PIN_SET); break; case 3: // V -> U __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, PWM_DUTY_HIGH); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, PWM_DUTY_LOW); HAL_GPIO_WritePin(INL_U_GPIO_Port, INL_U_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(INL_V_GPIO_Port, INL_V_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_W_GPIO_Port, INL_W_Pin, GPIO_PIN_RESET); break; case 4: // W -> U __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, PWM_DUTY_HIGH); HAL_GPIO_WritePin(INL_U_GPIO_Port, INL_U_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(INL_V_GPIO_Port, INL_V_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_W_GPIO_Port, INL_W_Pin, GPIO_PIN_RESET); break; case 5: // W -> V __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, PWM_DUTY_HIGH); HAL_GPIO_WritePin(INL_U_GPIO_Port, INL_U_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_V_GPIO_Port, INL_V_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(INL_W_GPIO_Port, INL_W_Pin, GPIO_PIN_RESET); break; default: break; } } /* USER CODE END 0 */
ADCの開始とPWMの開始処理です。CH4のみ「Start_IT」関数になっているのはデバッグ用の割り込みを発生させるためです。CH4はADCトリガ用のPWMなのでピンから出力を出したくない場合はHAL_TIM_PWM_Start_IT関数をコメントアウトします。__HAL_TIM_SET_COMPAREによってコンペアマッチの値が設定された時点で内部的にはADC用のトリガが発生しているようです。HAL_TIM_PWM_Start関数はピン出力を開始するかどうかの設定っぽいです(少し自信ない)。
/* USER CODE BEGIN 2 */ // DRV8320H enable HAL_GPIO_WritePin(ENABLE_GPIO_Port, ENABLE_Pin, GPIO_PIN_SET); // ADC start HAL_ADCEx_InjectedStart_IT(&hadc1); // Initialize duty value __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, PWM_AD_TIMING); // PWM start HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3); // For debug HAL_TIM_PWM_Start_IT(&htim1, TIM_CHANNEL_4); /* USER CODE END 2 */
update_CommutationState関数を一定間隔で呼ぶことで強制転流をしています。その下の処理はデバッグ処理です。今回は20kHzでADCを行っているため、周期が早すぎてリアルタイムでシリアル通信などで外部にログが出力できませんでした。そこでNUCLEOに搭載されている青いタクトスイッチを押すとそこからADC結果をバッファー配列に溜め込んでバッファーが一杯になったらBLDCの駆動を停止してシリアルで出力するようなデバッグ処理を追加しています。
/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { update_CommutationState(0); HAL_Delay(DELAY_MS); update_CommutationState(1); HAL_Delay(DELAY_MS); update_CommutationState(2); HAL_Delay(DELAY_MS); update_CommutationState(3); HAL_Delay(DELAY_MS); update_CommutationState(4); HAL_Delay(DELAY_MS); update_CommutationState(5); HAL_Delay(DELAY_MS); // For debug if(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == 0) { debug_ad_flag = 1; } // For debug if(debug_send_flag == 1) { HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET); HAL_ADCEx_InjectedStop_IT(&hadc1); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, PWM_DUTY_LOW); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, PWM_DUTY_LOW); HAL_GPIO_WritePin(INL_U_GPIO_Port, INL_U_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_V_GPIO_Port, INL_V_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(INL_W_GPIO_Port, INL_W_Pin, GPIO_PIN_RESET); char buf[] = "U,V,W\r\n"; HAL_UART_Transmit(&huart2, (uint8_t*)buf, sizeof(buf), 1000); for(int i = 0; i < DEBUG_BUFFER_SIZE; i++) { char str[100] = {0}; sprintf(str,"%d, %d, %d, %d\r\n",debug_buff[0][i],debug_buff[1][i],debug_buff[2][i],debug_buff[3][i]); HAL_UART_Transmit(&huart2, (uint8_t*)str, sizeof(str), 1000); } while(1); } /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */
stm32f4xx_it.c
デバッグ用に使用する変数の宣言
/* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ int debug_ad_flag = 0; int debug_buff[4][DEBUG_BUFFER_SIZE]; int debug_send_flag = 0; int i = 0; /* USER CODE END PV */
ADC完了時に呼ばれる関数です。ADCした値の取得とデバッグ用の処理が書いてあります。仮想中性点は各相のADC値を足して3で割って算出しています。
void ADC_IRQHandler(void) { /* USER CODE BEGIN ADC_IRQn 0 */ int u_phase_voltage = 0; int v_phase_voltage = 0; int w_phase_voltage = 0; int virtual_neutral_voltage = 0; // For debug HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET); // Get ADC result u_phase_voltage = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);// U v_phase_voltage = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_3);// V w_phase_voltage = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);// W virtual_neutral_voltage = (u_phase_voltage + v_phase_voltage + w_phase_voltage) / 3;// N // For debug if(debug_ad_flag == 1 && i < DEBUG_BUFFER_SIZE) { debug_buff[0][i] = u_phase_voltage;// U debug_buff[1][i] = v_phase_voltage;// V debug_buff[2][i] = w_phase_voltage;// W debug_buff[3][i] = virtual_neutral_voltage;//N i++; } // For debug if(i == DEBUG_BUFFER_SIZE) { debug_send_flag = 1; } /* USER CODE END ADC_IRQn 0 */ HAL_ADC_IRQHandler(&hadc1); /* USER CODE BEGIN ADC_IRQn 1 */ /* USER CODE END ADC_IRQn 1 */ }
デバッグ用にポート出力の処理を追加しています。TIM1のCH4のコンペアマッチタイミングで呼ばれるのでADC開始のタイミングでポート出力がHIGHになります。逆にポート出力をLOWにする設定は上に示したADC_IRQHandlerに記述しています。
void TIM1_CC_IRQHandler(void) { /* USER CODE BEGIN TIM1_CC_IRQn 0 */ // For debug HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET); /* USER CODE END TIM1_CC_IRQn 0 */ HAL_TIM_IRQHandler(&htim1); /* USER CODE BEGIN TIM1_CC_IRQn 1 */ /* USER CODE END TIM1_CC_IRQn 1 */ }
動作確認
プログラムを動作させたときの波形を示します。NUCLEOでも強制転流で回すことが出来ました。相変わらず誘起電圧の位相が変な気がしますが...。
拡大すると以下のような感じです。ADトリガは立ち上がりがADC開始、立ち下がりがADC完了を示しています。PWMのHIGHのタイミングでADC出来ていることが分かります。ADC開始から完了まで3usで今回は3CHをADCしているので1CHにつき1usって感じでしょうか。駆動時のDuty比を下げすぎるとADCが間に合わない可能性もありそうです。
タクトスイッチを押すとシリアルで下記のようにログが出力されます。
このデータをエクセルでグラフ化すると下記のような感じ、それっぽい値が取れています。
おわりに
今回ははセンサレス120度通電を実現するのに必要なBEMFの読み取りについて書きました。
次は各相の誘起電圧と仮想中点電圧を比較してゼロクロス検出の方法について書ければなあと思います。
センサレスで回せました↓↓↓