前回の記事で、基本的なタイマーの使い方を学びました。
今回は、これをベースにPWM信号を出力する方法を勉強します。今回は、タイマーモジュールのうち、TIM1を使うことにします。
PWM信号出力の設定方法
細かい点は、おいといて、STM32CubeIDEのコード生成機能をつかって、とりあえず簡単にPWM信号を出してみます。
まず、適当に作成したプロジェクトのIOCファイルからSTM32CubeMXを起動します。
左のウィンドウからTimers -> TIM1を選択します。
今回は、PWM信号を2ch出したいと思います。ChannelをPWM Generation CH1と2に設定します。
次にPWMの出力周波数をきめますが、その際に使用するTIM1のクロック周波数を調べます。TIM1は、下図よりAPB2のクロックを使用しているので、その周波数を確認します。
MXからClock configurationをクリックします。
APB2 timer clocksの箇所を確認します。
自分のプロジェクトでは、64MHzとなってました。下の計算ツールを利用して、PWMの周波数の設定に必要なARR(Auto Reload Register)の値を求めます。
ここでは、目標のPWM周波数を10kHz(0.1msec)にして、PSCを0に設定します。
ARR=6399という値がでたので、下の画面のCounter Period という箇所に6399という値を設定します。Prescalerは0です。Counter Mode はUPとします。
次に下のほうにいき、PWM Generation Channelの項目で、ModeをPWM mode 1としとします。Pulseについては、後で説明しますが、CCRという値を指定する箇所で、とりあえず、ここでは、0に設定します。
MX上での設定はこれだけです。ピン配をみるとTIM1_CH1とCH2は、それぞれPC0とPC1に割り当てられていることがわかります。
Alt+Kでコード生成をおこないます。
main.cの編集
main.cで既にMX_TIM1_Init()で初期設定関数が呼ばれてるので、そのあとに下のコードを追加します。これは、タイマーのカウントを開始して、PWM出力を指示するHAL関数になります。
MX_TIM1_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);//追加1 タイマーをスタート
//PWM
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);//追加2 PWM出力開始 ch1
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);//追加3 PWM出力開始 ch2
ここで、計算途中で、Dutyを変更するため、SetMotorDutyというオリジナル関数を作成します。下のとおりです。これをmain.c内の適当な場所にいれておきます。プロトタイプ宣言は適宜おこなってください(記事の最後にmain.cのコード全部をはりつけておきます)
/* USER CODE BEGIN 4 */
static void SetMotorDuty(uint32_t ch,float rDuty){
uint32_t CCR=0;
switch (ch){
case TIM_CHANNEL_1:
if(rDuty>0){
CCR=(uint32_t)(rDuty/100.0f*(TIM1->ARR+1))-1;
}
TIM1->CCR1 = CCR;
break;
case TIM_CHANNEL_2:
if(rDuty>0){
CCR=(uint32_t)(rDuty/100.0f*(TIM1->ARR+1))-1;
}
TIM1->CCR2 = CCR;
break;
}
}
使い方は、下のような感じで、変更したいチャンネル名とDutyを指示してやります。
SetMotorDuty(TIM_CHANNEL_1,20.0);//CH1をDuty=20%で出力
CCRレジスタの説明
作成した関数のなかで、CCRという値(TIM1 Capture/compare register)を書き換えていますが、これは、TIM1_CNTレジスタのタイマーのカウント値と比較するしきい値です。大小関係によって、High/Lowを出力して、下ののようにPWM信号を生成できます。CCRをプログラムで逐次変更することにより、PWMの出力Dutyを可変させることができます。
下図にTIM1のシステムの概要図をのせておきます。これによれば、TIM1はPWM信号を4つ出力できるようです。
CCRは、この出力4チャンネルに対して、個別に値を設定することができます。また、下流側の設定によっては、それぞれCCR1とCCR2で生成した波形(OC1REFとOC2REF)のANDとかORをとった結果を出力チャンネルにだすとかの複雑な処理もできたりします。
図では、CH1N, CH2Nとか、Nつきのチャンネルがあります。これは、相補PWMになります。CH1, CH2を丁度反転したPWM信号です。モータドライバでMOS-FETを駆動する時につかわれます。貫通電流が流れないようにデッドタイムなども設定できる機能などがあります。
今回作成したDuty比からCCRを設定する関数は、下のSTMのプレゼン資料の計算式を参考にしています。
以上より、関数をつかって、メインループ内でdutyを増やしたり、減らしたりしてみます。
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(rDuty>100.0f){
bUp = false;
rDuty = 100.0f;
}
if(rDuty<0.0f){
bUp =true;
rDuty = 0.0f;
}
if(bUp){
rDuty++;
}else{
rDuty--;
}
SetMotorDuty(TIM_CHANNEL_1,rDuty);
SetMotorDuty(TIM_CHANNEL_2,rDuty);
HAL_Delay(50);
}
動作結果は下の通りです。狙いどおり10kHzでPWM信号を出力できました。
カウンターの種類
カウンターの種類はカウントアップ、ダウン、アップ&ダウン(center aligned mode)の3つにあります。
低ノイズであることや高周波数域での安定性などにメリットがあるため、モータ駆動で一般的につかわれるのはcenter aligned modeになります。ただ、同じARRの設定だと、他のタイマーモードよりもPWM周波数が半分になるので、注意が必要です。
図の下のほうにあるとおり、center aligned mode のmode1, mode2, mode3の違いは、caputure compare interruptという割り込み処理が発生するタイミングになります。
※前回の記事では、update interruptというタイマーがオーバーフロー(アンダーフロー)した時に発生する割り込みを使って、Lチカしてましたが、この割り込みはCRR値がカウンター値をまたいだ時に発生する割り込みです
下に動作例をしめしてみました。青がPWM信号で、緑が割り込み時にコールバック関数内で、デジタル信号をトグル出力したものになります。なんで、緑が変化した時に割り込み処理が発生していることを意味しています。
PWMモード
PMWモードは6パターンあります。それぞれ、動作をみていきます。
CH1 – PWM mode 1 , CH2 – PWM mode 2の設定
PWM mode2 はmode1を反転出力しただけのようです。
CH1 – Combined PWM1 , CH2 – PWM mode 1の設定
ここでは、動作が分かりやすいようにChannel 2 のDutyを50%で固定しています。Channel 1はこれまで通りスイープして、50%以下のDutyを指示しようとしていますが、Combined PWM1では、Channel1の最終的な出力がChannel1とChannel 2 のOR条件で決まるため、Channel1は50%以下のDutyは出力されていません。
例は省きますが、Combined PWM2 の場合は、Channel1とChannel2のOR条件ではなく、AND条件になります。
CH1 – Asymmetric PWM1 , CH2 – PWM mode 1の設定
Asymmetric PWM1は二つのCCRの値を使って、信号を制御できるモードです。下図のようにカウントアップ時は、CCR1を参照し、カウントダウン時はCCR2を参照して、PWM信号を出力します。信号の位相をずらしたい時などに使用します。
ちなみにAsymmetric PWM2は、PWM mode2ベースで上記のことをやります。
相補PWM
相補PWMを出力するためには、MX上のChannelの出力設定で、PWM Generation CH1 CH1Nを選択します。すると従来の信号プラス相補反転した信号の出力が追加されます。
生成したコードから下の二行目のコードを追加します。
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);//追加
下が結果です。これでは、極性反転した設定と何も変わらないので、モータ駆動することを想定して、dead timeを設けたいと思います。
Deadtime設定
デッドタイムは、MX上で下の画面から設定できます。
値を0-255で設定とありますが、リファレンスマニュアルをみると、割とめんどくさい感じで、計算されるようです。
計算ツールをつくってみました。これでDTGの値をちょこちょこ変えながら、狙いのdead timeになるように調整します。今回はクロック64MHzでDTG=64とし、1usecのdead timeがはいるようにしました。
注)DTGは0-255の値をいれること
MHz
deadtime[ns] :
結果は、下のとおりです。
狙ったとおりに動いてそうです。
以上です。PWMの設定に関しては、こんなところでしょうか。今回はHALの中身までは、いけませんでしたが、また別の記事で、まとめたいと思います。
PWM🙌
Sample
最後にmain.cを丸ごと貼っておきます。
/* 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"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdbool.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim1;
UART_HandleTypeDef huart2;
/* USER CODE BEGIN PV */
volatile float rDuty=0.0f;
volatile bool bUp = false;
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
static void MX_TIM1_Init(void);
/* USER CODE BEGIN PFP */
static void SetMotorDuty(uint32_t ch,float rDuty);
/* 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_USART2_UART_Init();
MX_TIM1_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);
//PWM
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
SetMotorDuty(TIM_CHANNEL_1,rDuty);
SetMotorDuty(TIM_CHANNEL_2,rDuty);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(rDuty>100.0f){
bUp = false;
rDuty = 100.0f;
}
if(rDuty<0.0f){
bUp =true;
rDuty = 0.0f;
}
if(bUp){
rDuty++;
}else{
rDuty--;
}
SetMotorDuty(TIM_CHANNEL_1,rDuty);
SetMotorDuty(TIM_CHANNEL_2,rDuty);
HAL_Delay(50);
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
/** 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.PLLMUL = RCC_PLL_MUL16;
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_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_TIM1;
PeriphClkInit.Tim1ClockSelection = RCC_TIM1CLK_HCLK;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief TIM1 Initialization Function
* @param None
* @retval None
*/
static void MX_TIM1_Init(void)
{
/* USER CODE BEGIN TIM1_Init 0 */
/* USER CODE END TIM1_Init 0 */
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};
/* USER CODE BEGIN TIM1_Init 1 */
/* USER CODE END TIM1_Init 1 */
htim1.Instance = TIM1;
htim1.Init.Prescaler = 0;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 6399;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_PWM_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterOutputTrigger2 = TIM_TRGO2_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
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;
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
{
Error_Handler();
}
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 0;
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
sBreakDeadTimeConfig.BreakFilter = 0;
sBreakDeadTimeConfig.Break2State = TIM_BREAK2_DISABLE;
sBreakDeadTimeConfig.Break2Polarity = TIM_BREAK2POLARITY_HIGH;
sBreakDeadTimeConfig.Break2Filter = 0;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM1_Init 2 */
/* USER CODE END TIM1_Init 2 */
HAL_TIM_MspPostInit(&htim1);
}
/**
* @brief USART2 Initialization Function
* @param None
* @retval None
*/
static void MX_USART2_UART_Init(void)
{
/* USER CODE BEGIN USART2_Init 0 */
/* USER CODE END USART2_Init 0 */
/* USER CODE BEGIN USART2_Init 1 */
/* USER CODE END USART2_Init 1 */
huart2.Instance = USART2;
huart2.Init.BaudRate = 38400;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
huart2.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart2.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART2_Init 2 */
/* USER CODE END USART2_Init 2 */
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin : B1_Pin */
GPIO_InitStruct.Pin = B1_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pin : LD2_Pin */
GPIO_InitStruct.Pin = LD2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);
}
/* USER CODE BEGIN 4 */
static void SetMotorDuty(uint32_t ch,float rDuty){
uint32_t CCR=0;
switch (ch){
case TIM_CHANNEL_1:
if(rDuty>0){
CCR=(uint32_t)(rDuty/100.0f*(TIM1->ARR+1))-1;
}
TIM1->CCR1 = CCR;
break;
case TIM_CHANNEL_2:
if(rDuty>0){
CCR=(uint32_t)(rDuty/100.0f*(TIM1->ARR+1))-1;
}
TIM1->CCR2 = CCR;
break;
}
}
/* 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 */