暇人の技術備忘録

ハサミの技術備忘録

趣味でやっている電子工作の備忘録です。

【DRV8320HでBLDCを回したい】第4回:NUCLEO-F302R8でセンサレス120度通電制御

はじめに

第3回ではBEMFをADCで読み取る方法について書きました。

sbasami-tech.hatenablog.com

第3回では上手く読み取れているように書きましたが、実際にADCの結果を使用してセンサレス制御をやってみても上手くいきませんでした。 かなり行き詰まっていた中で、たまたま下記記事を見つけて参考にした所、センサレスでBLDCを回すことができました。この記事を見つけてなかったら未だに回せていないと思います、本当に助かりました!

homemadegarbage.com

また、今回はゼロクロスをADCではなくF302R8内蔵のコンパレータを使用して行いました(コンパレータはF411REには搭載されていなかったのでF302R8を使いました)。 理由としては、①ADC→②仮想中点と各相電圧を比較→③ゼロクロス判定といった処理の①②をコンパレータを使えばハード側でやってくれるのでソフトの負荷軽減になるかなと思ったからです。

NUCLEO-F302R8の設定

Clockの設定

Clockはとりあえず最大動作周波数に設定

TIM1設定

TIM1はモータへのPWM出力を行うために使用します。 Channelの1~4を「PWM Generation CHx」に設定。

  • CH1:U相制御用出力
  • CH2:V相制御用出力
  • CH3:W相制御用出力
  • CH4:ゼロクロス判定割り込み用出力

「Counter Mode」は「Center Aligned mode 1」に設定。Center Aligned modeにすることでカウンタがノコギリ波ではなく三角波になります。modeは1~3までありますがキャプチャコンペア割り込み発生タイミングが変わるようです。

www.minokasago.org

「Counter Period」はPWM周波数が40kHzになるように設定しています。一応PWM周波数はソフトの方で可変できるように実装しました。

また「TIM1 capture compare interrupt」を有効にしておきます。これはCH4のコンペアマッチタイミングでゼロクロス判定を行うためです。

TIM2設定

TIM2はゼロクロスの間隔を計測するために使用します。 Clock Sourceを「Internal Clock」に変更

PrescalerはTIM2のカウンタが1usで1カウントするように設定しています。

あくまでもゼロクロス間隔を計測するだけなので割り込みはありません。

TIM6設定

TIM6はゼロクロスを検出したタイミングから次の転流(ゼロクロス間隔/2、電気角でいうと30deg後?)タイミングで実際に転流を行うための割り込みを発生させる為に使用します。 Clock Sourceを「Internal Clock」に変更

PrescalerはTIM2と同じ1usで1カウントするように設定しています。

割り込みも忘れずに有効にします。

COMP2, 4, 6設定

COMPはゼロクロス検出のために使用します。

今回はCOMPタイミングで割り込み等は発生させないので、機能を有効化させるだけです。COMP4, 6も同じように設定すればOK。

GPIO設定

あとはデバッグとDRV8320Hを制御するのに必要なGPIOの設定を行いました。赤枠の部分を「GPIO_Output」で設定しています。

これでCube MXの設定は終了なのでGenerate codeします。

1つ注意点なのが、CubeMXのバグかわかりませんがCOMP4, 6のInvertingInputに代入する値が間違って生成されるので直す必要があります。「COMP_INVERTINGINPUT_IO2」が正しい設定です。

プログラム

BLDCのセンサレス駆動で追加したコードに付いて下記に示します。

main.h

プロトタイプ宣言が一つあります。main.cに本体がいてstm32f3xx_it.cで使用したいため.hに宣言しています。

/* USER CODE BEGIN EFP */
void BLDC_CC_interrupt(); // TIM1CH4CC割り込みコールバック
/* USER CODE END EFP */

main.c

シリアル関連で使用するのでインクルードしました。

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

BLDC_PWM_FREQ は初期のPWM周波数、BLDC_PWM_ON は初期のDutyです。

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define BLDC_PWM_FREQ 900     // PWM周波数 900 = 40kHz
#define BLDC_PWM_ON 150       // ON  duty 150 / 900 * 100[%]
#define BLDC_PWM_OFF 0        // OFF duty
#define BLDC_PWM_ZEROCROSS_CHECK_TIMING 10 // ゼロクロス判定タイミングduty

// UVWのコンパレータ状態
#define COMPARATOR_STATE_0 0b101   // U:1  V:0  W:1
#define COMPARATOR_STATE_1 0b100   // U:1  V:0  W:0
#define COMPARATOR_STATE_2 0b110   // U:1  V:1  W:0
#define COMPARATOR_STATE_3 0b010   // U:0  V:1  W:0
#define COMPARATOR_STATE_4 0b011   // U:0  V:1  W:1
#define COMPARATOR_STATE_5 0b001   // U:0  V:0  W:1

// 転流状態
typedef enum
{
    COMMUTATION_STATE_0 = 0,
    COMMUTATION_STATE_1,
    COMMUTATION_STATE_2,
    COMMUTATION_STATE_3,
    COMMUTATION_STATE_4,
    COMMUTATION_STATE_5,
    COMMUTATION_STATE_ERROR,

}BLDC_CommutationState_enum;
/* USER CODE END PD */

BLDCを回す上で必要な情報は構造体でまとめています。

/* USER CODE BEGIN PV */
typedef struct
{
    int duty;
    int comm_state;                    // 現在の転流状態
    int comm_flag;                 // 転流したフラグ
    int zero_cross_interval_us;        // ゼロクロス間隔[us]
    int comp_UVW;                  // 現在のコンパレータの状態(UVWまとめたやつ)
    int comp_UVW_old;              // 1つ前のコンパレータの状態
}BLDC_Data_struct;
BLDC_Data_struct BLDC;
/* USER CODE END PV */

センサレス駆動用に追加した関数のプロトタイプ宣言。

/* USER CODE BEGIN PFP */
void BLDC_Initialization();                         // 初期化
void BLDC_Update_CommutationState(int state);      // 実際に転流させる
void BLDC_Set_Duty(int duty);                      // Duty設定用
int BLDC_Get_Comparator();                          // コンパレータの状態を取得
int BLDC_Get_NextCommutationState(int comp_uvw);   // コンパレータの状態を渡して次の転流状態を取得
int BLDC_Get_ZeroCrossInterval();                   // ゼロクロス間隔を取得
void BLDC_Set_NextCommutationTiming(int zero_cross_interval_us); // 次の転流タイミングを設定
void BLDC_Check_ZeroCross();                        // ゼロクロス判定
void BLDC_TIM6_interrupt();                         // TIM6割り込みコールバック
/* USER CODE END PFP */

BLDCセンサレス駆動用に追加した関数

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void BLDC_Initialization()
{
    // PWM duty
    BLDC_Set_Duty(BLDC_PWM_ON);
    // DRV8320H enable
    HAL_GPIO_WritePin(ENABLE_GPIO_Port, ENABLE_Pin, GPIO_PIN_SET);
    // Initialize duty value
    __HAL_TIM_SET_AUTORELOAD(&htim1, BLDC_PWM_FREQ-1);
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, BLDC_PWM_OFF);
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, BLDC_PWM_OFF);
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, BLDC_PWM_OFF);
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, BLDC_PWM_ZEROCROSS_CHECK_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);
    HAL_TIM_PWM_Start_IT(&htim1, TIM_CHANNEL_4);
    // Comparator start
    HAL_COMP_Start(&hcomp2);
    HAL_COMP_Start(&hcomp4);
    HAL_COMP_Start(&hcomp6);
    //Zero cross interval counter start
    HAL_TIM_Base_Start(&htim2);
}

void BLDC_Update_CommutationState(int state)
{
    switch(state)
    {
        case COMMUTATION_STATE_0:
            // U -> V
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, BLDC.duty);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, BLDC_PWM_OFF);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, BLDC_PWM_OFF);
            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 COMMUTATION_STATE_1:
            // U -> W
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, BLDC.duty);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, BLDC_PWM_OFF);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, BLDC_PWM_OFF);
            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 COMMUTATION_STATE_2:
            // V -> W
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, BLDC_PWM_OFF);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, BLDC.duty);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, BLDC_PWM_OFF);
            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 COMMUTATION_STATE_3:
            // V -> U
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, BLDC_PWM_OFF);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, BLDC.duty);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, BLDC_PWM_OFF);
            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 COMMUTATION_STATE_4:
            // W -> U
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, BLDC_PWM_OFF);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, BLDC_PWM_OFF);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, BLDC.duty);
            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 COMMUTATION_STATE_5:
            // W -> V
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, BLDC_PWM_OFF);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, BLDC_PWM_OFF);
            __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, BLDC.duty);
            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;
    }
}

void BLDC_Set_Duty(int duty)
{
    BLDC.duty = duty;
}

int BLDC_Get_Comparator()
{
    int comp_u;
    int comp_v;
    int comp_w;
    int comp_uvw;

    comp_u = (READ_BIT(hcomp2.Instance->CSR, COMP_CSR_COMPxOUT)) >> COMP_CSR_COMPxOUT_Pos;
    comp_v = (READ_BIT(hcomp4.Instance->CSR, COMP_CSR_COMPxOUT)) >> COMP_CSR_COMPxOUT_Pos;
    comp_w = (READ_BIT(hcomp6.Instance->CSR, COMP_CSR_COMPxOUT)) >> COMP_CSR_COMPxOUT_Pos;
    comp_uvw = (comp_u << 2) | (comp_v << 1) | comp_w;

    return comp_uvw;
}

int BLDC_Get_NextCommutationState(int comp_uvw)
{
    int comm_next_state;

    switch(comp_uvw)
    {
        case COMPARATOR_STATE_0:
            comm_next_state = COMMUTATION_STATE_0;
            break;
        case COMPARATOR_STATE_1:
            comm_next_state = COMMUTATION_STATE_1;
            break;
        case COMPARATOR_STATE_2:
            comm_next_state = COMMUTATION_STATE_2;
            break;
        case COMPARATOR_STATE_3:
            comm_next_state = COMMUTATION_STATE_3;
            break;
        case COMPARATOR_STATE_4:
            comm_next_state = COMMUTATION_STATE_4;
            break;
        case COMPARATOR_STATE_5:
            comm_next_state = COMMUTATION_STATE_5;
            break;
        default:
            comm_next_state = COMMUTATION_STATE_ERROR;
            break;
    }

    return comm_next_state;
}

int BLDC_Get_ZeroCrossInterval()
{
    int zero_cross_interval_us;

    zero_cross_interval_us = __HAL_TIM_GET_COUNTER(&htim2);
    __HAL_TIM_SET_COUNTER(&htim2, 0);

    return zero_cross_interval_us;
}

void BLDC_Set_NextCommutationTiming(int zero_cross_interval_us)
{
    __HAL_TIM_SET_AUTORELOAD(&htim6, (int)(zero_cross_interval_us / 2));
    __HAL_TIM_SET_COUNTER(&htim6, 0);
    HAL_TIM_Base_Start_IT(&htim6);
}

void BLDC_Check_ZeroCross()
{
    if(BLDC.comp_UVW != BLDC.comp_UVW_old)
    {
        BLDC.zero_cross_interval_us = BLDC_Get_ZeroCrossInterval();
        BLDC_Set_NextCommutationTiming(BLDC.zero_cross_interval_us);

        BLDC.comp_UVW_old = BLDC.comp_UVW;

        HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
    }
}

void BLDC_CC_interrupt()
{
    if(BLDC.comm_flag == 0)
    {
        BLDC.comp_UVW = BLDC_Get_Comparator();
        BLDC_Check_ZeroCross();
    }
    else
    {
        BLDC.comm_flag = 0;
    }
}

void BLDC_TIM6_interrupt()
{
    BLDC.comm_state = BLDC_Get_NextCommutationState(BLDC.comp_UVW);
    BLDC_Update_CommutationState(BLDC.comm_state);
    BLDC.comm_flag = 1;

    HAL_TIM_Base_Stop_IT(&htim6);

    HAL_GPIO_TogglePin(DEBUG_A_GPIO_Port, DEBUG_A_Pin);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim == &htim6){
        BLDC_TIM6_interrupt();
    }
}
/* USER CODE END 0 */

main関数の中身

int main(void)
{
  /* USER CODE BEGIN 1 */
    int bldc_step = 0;
    int i = 0;
    int duty = BLDC_PWM_ON;
    int freq = BLDC_PWM_FREQ;
    uint8_t buff[1];
    HAL_StatusTypeDef ret;
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_TIM1_Init();
  MX_COMP2_Init();
  MX_COMP4_Init();
  MX_COMP6_Init();
  MX_TIM2_Init();
  MX_TIM6_Init();
  /* USER CODE BEGIN 2 */
  BLDC_Initialization();

  // Forced commutation
  for(i = 0; i < 5; i ++)
  {
    BLDC_Update_CommutationState(bldc_step);
    bldc_step++;
    bldc_step %= 6;
    HAL_Delay(5);
  }
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    ret = HAL_UART_Receive(&huart2, buff, 1, 1000);
    // B1スイッチを押しながらシリアルで「1」「2」を送信すると周波数が変化
    // B1スイッチを押さずにシリアルで「1」「2」を送信するとDutyが変化
    if(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == 1)
    {
        if (ret == HAL_OK)
        {
            if(buff[0] == '1')
            {
                duty += 10;
            }
            else if(buff[0] == '2')
            {
                duty -= 10;
            }
        }
        BLDC_Set_Duty(duty);
    }
    else
    {
        if (ret == HAL_OK)
        {
            if(buff[0] == '1')
            {
                freq += 10;
            }
            else if(buff[0] == '2')
            {
                freq -= 10;
            }
        }
        __HAL_TIM_SET_AUTORELOAD(&htim1, freq-1);
    }
    if (ret == HAL_OK)
    {
        char str[100] = {0};
        sprintf(str,"%d\t%d\r\n",duty,freq);
        HAL_UART_Transmit(&huart2, (uint8_t*)str, sizeof(str), 1000);
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

stm32f4xx_it.c

コールバック関数追加

void TIM1_CC_IRQHandler(void)
{
  /* USER CODE BEGIN TIM1_CC_IRQn 0 */
  BLDC_CC_interrupt();
  /* 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 */
}

BEMF検出回路

今回は下記のような回路でBEMF検出を行っています。フィルターはなく単純に分圧しているだけです。電源電圧7.4Vのときに3.3Vを超えないように手持ちの抵抗で設計しました。

フィルター無しで検出できるのはソフトの方でPWMがONのタイミングでのみBEMF検出を行っているのでPWMパルスのノイズを無視できています。 この方法の欠点としてはPWMのDutyが小さいとPWM ONの区間が短すぎてBEMF検出処理が間に合わなくなって正常に回らなくなったりしました。

動作確認

プログラムを動作させたときの波形を示します。ちゃんとゼロクロスを検出して転流することができています。

  • BEMF検出回路を通った後のU相電圧(黃色)
  • BEMF検出回路を通った後のV相電圧(水色)
  • BEMF検出回路を通った後の仮想中性点(紫色)
  • ゼロクロス検出トグル出力(青色)

ちなみに今回のプログラムは下記のような駆動方法なのですが、

引用元:ブラシレスモータ 120°通電(矩形波駆動) - Toshiba

試しに下記のような相補PWMのような駆動方法を試すと

引用元:ブラシレスモータ 120°通電(矩形波駆動) - Toshiba

下記のような波形になりました。自分は雰囲気でモータ回してるので駆動方法による特性とかは全く分かってませんが、上の波形と比べると同じDutyで回しているのにこちらのほうが回転数が遅くなりました。

  • BEMF検出回路を通った後のU相電圧(黃色)
  • BEMF検出回路を通った後のV相電圧(水色)
  • BEMF検出回路を通った後の仮想中性点(紫色)
  • ゼロクロス検出トグル出力(青色)

おわりに

今回でなんとかBLDCをセンサレス駆動させることができました。これで自作ESCが作れるぞと思ったのも束の間、色々調べてみると課題が出てきました。

今回の手法だと1000KVモータはDuy100%まで回せたのですが、6000KVモータはどうやらゼロクロス判定が40kHz周期では粗すぎるようでDutyを上げていくと回転数が飽和してしまいました。 ゼロクロス判定周期を増やそうとしているのですが中々上手く行ってないです。

今回のプログラムだと100k eRPMで回るかどうかという感じですが、市販のESCは500k eRPMとかで回ると仕様に書いてあるのでこれくらい回せないと6000KVとかは回らないのかなあとか思っています。

このあたりを解決しないとドローン飛ばせないと思うので、自作ESCはまだまだ先になりそうです。