ブラシレスDCモータなどを駆動するための三相相補PWMの機能をSTM32で構成しましたので説明します
なお、本記事は以前Qiitaの記事で投稿したものの内容更新版となります
使用するボードはnucleo-G474RE、開発環境は以下記事で紹介したPlatformIO+STM32CubeMXの構成です

また、最終的にはnucleoの拡張ボードX-NUCLEO-IHM16M1を使用したモータ制御を実装したいので、ピン配置などはX-NUCLEO-IHM16M1に合わせた構成としております
STM32CubeMXの設定
TIM1モジュールで三相相補PWMを構成します
PWMの仕様は以下にしました
- キャリア周波数は10kHz、デッドタイムは2us
- アクティブHighのセンターアラインPWM(キャリアの谷で全ローサイドアクティブ+ADC変換トリガ)
- BRK信号がLowになるとすべてのPWMをHi-Zに制御する(過電流保護機能)
以下にSTM32CubeMXの設定画面を載せます
個人的にはBRK周りの設定が難解なように感じました
なお、CH6はAD変換のトリガ用でキャリアの谷でコンペアマッチを発生させ、コンペアマッチでイベントを出力するようにしています








次にADCモジュールの設定です
モータ電流の3シャントによる検出が肝となります
- PWMのキャリアの谷(全ローサイドアクティブ)でAD変換する(TIMのイベント出力を利用)
- 3シャントは同時サンプリングが望ましいので、それぞれチャンネルを分けてインジェクトチャンネルに割り当て
- 3シャント検出後に電流フィードバック制御を構成したいのでインジェクト変換後は割り込みにて処理する
3シャント以外のDCリンク電圧の検出などは適当なチャンネルのレギュラー変換+DMA転送で構成しました
以下にSTM32CubeMXの設定画面を載せます
サンプリング時間などはデフォルトのままにしています(必要に応じて別途調整)








コード作成例
今回はADCのインジェクト変換割り込み処理を別ファイル(motorControl.c)に記述しました
いずれモータ制御用のコードを記述する予定で、それなりの規模になる見込みなので予め分けています
また、標準入出力用の__io_putchar関数と__io_getchar関数も邪魔だったので別ファイル(serial.c)に移動しました

platformIOの場合、ファイルを分けても特に何の設定しなくて勝手に読み込んでくれます
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2023 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "dma.h"
#include "usart.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#define GETCHAR_PROTOTYPE int __io_getchar(void)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#define GETCHAR_PROTOTYPE int f getc(FILE* f)
#endif
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
uint16_t iu, iv, iw;
uint8_t brkErr = 0;
static uint16_t adc1Buf;
static uint16_t adc2Buf[2];
static uint32_t errTick;
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* 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_DMA_Init();
MX_LPUART1_UART_Init();
MX_ADC1_Init();
MX_ADC2_Init();
MX_ADC3_Init();
MX_TIM1_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
setbuf( stdout, NULL );
setbuf( stdin, NULL );
HAL_ADCEx_InjectedStart_IT(&hadc1);
HAL_ADCEx_InjectedStart_IT(&hadc2);
HAL_ADCEx_InjectedStart_IT(&hadc3);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&adc1Buf, 1);
HAL_ADC_Start_DMA(&hadc2, (uint32_t *)adc2Buf, 2);
HAL_TIM_PWM_Start_IT(&htim1, TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Start_IT(&htim1, TIM_CHANNEL_1);
HAL_TIM_PWM_Start_IT(&htim1, TIM_CHANNEL_2);
HAL_TIMEx_PWMN_Start_IT(&htim1, TIM_CHANNEL_2);
HAL_TIM_PWM_Start_IT(&htim1, TIM_CHANNEL_3);
HAL_TIMEx_PWMN_Start_IT(&htim1, TIM_CHANNEL_3);
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
uint32_t startTick = HAL_GetTick();
while (1)
{
uint32_t nowTick = HAL_GetTick();
if( nowTick - startTick > 1000 )
{
startTick = nowTick;
printf("==========\r\n");
printf("Vbus:%.2fV\r\n", adc1Buf*3.3f/4096.0f);
printf("Temp:%.2fV\r\n", adc2Buf[0]*3.3f/4096.0f);
printf("Speed:%.2fV\r\n", adc2Buf[1]*3.3f/4096.0f);
printf("Iu:%.2fV\r\n", iu*3.3f/4096.0f);
printf("Iv:%.2fV\r\n", iv*3.3f/4096.0f);
printf("Iw:%.2fV\r\n", iw*3.3f/4096.0f);
printf("==========\r\n");
printf("\r\n");
}
if( !brkErr )
{
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, 0);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, htim1.Init.Period); // H:0% L:100%
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, htim1.Init.Period>>1); // H:50% L:50%
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 0); // H:100% L:0%
}
else
{
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, 1);
if( nowTick - errTick > 5000 )
{
__HAL_TIM_MOE_ENABLE(&htim1);
}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV4;
RCC_OscInitStruct.PLL.PLLN = 85;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
void HAL_TIMEx_BreakCallback(TIM_HandleTypeDef *htim)
{
errTick = HAL_GetTick();
brkErr = 1;
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
#include "stm32g4xx_hal.h"
extern uint16_t iu, iv, iw;
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
static uint8_t adNum;
if( hadc->Instance == ADC1 )
{
iu = HAL_ADCEx_InjectedGetValue(hadc, 1);
adNum++;
}
else if( hadc->Instance == ADC2 )
{
iv = HAL_ADCEx_InjectedGetValue(hadc, 1);
adNum++;
}
else if( hadc->Instance == ADC3 )
{
iw = HAL_ADCEx_InjectedGetValue(hadc, 1);
adNum++;
}
if( adNum < 3 ) return;
adNum = 0;
// ここからモータ制御のコードを記述する
}
動作確認
PWMはQiitaの記事のように空間ベクトル変調のデモをしてもよかったのですが、今回は固定Dutyで確認しました
まずはわかりやすいCH2の上下50%Dutyの波形です(黄色がH側、緑色がN側)



きちんと10kHz/デッドタイム2usの相補PWMになっていることが確認できました
次に下側100%DutyのCH1と上側100%DutyのCH3の波形です
あまり映えない波形ですが一応載せておきます


Duty100%の信号になっていることが確認できました
ちなみに1でもカウンタが増減するとデッドタイム分だけOFFします
CH3でコンペアカウンタを1に設定した波形も載せておきます


デッドタイムの2usだけH側がOFFすることを確認しました
Duty100%に制御する必要ある場合は丸め誤差等に気を付ける必要がありそうです
次にBRK信号の動作確認をしました
PWM出力中にBRK端子をH→Lにします(紫色: BRK信号)

BRK信号のLow信号でPWMがHi-Z状態(プルダウンしているのでLow状態)に制御されていることが確認できました
Break状態からの解除は、BRK端子がインアクティブ状態に戻っている状態で__HAL_TIM_MOE_ENABLEマクロ関数を実行すると実行されます
今回のコード例はBRK動作後5秒後に自動復帰するように組んでいるので、復帰動作についても確認しました

5秒後に自動復帰して再度PWM出力が再開していることを確認できました
ADCはメインループ内でprintfで1秒毎に変換結果を出力するようにしました
また、ここからは拡張ボードX-NUCLEO-IHM16M1を取り付けた状態(外部よりDC24V印可)で確認しました

Tere TermでAD変換結果を確認します
なお、今回出力しているのはADポートの端子電圧そのものとなります

VbusはDC24V入力時に設計値で1.5VとなるのでOKです
Tempは25℃時に設計値で0.58Vとなるはずですが少し低め(温度としては25℃より高め)に出ているようです
Speedは拡張ボードに搭載のボリュームを回すと値が変化することを確認しました
肝心の3シャントについては今回はモータ未接続で電流が流れていないので0A時のオフセット電圧の確認までです
設計値で1.56Vとなるはずなので概ねOKと判断しました
AD変換タイミング等は次回実際にモータ回す際に確認します