STM32 + HALでTIMのPWMを使ってみる

2020年6月11日

STM32マイコンのペリフェラル関連記事を一覧にまとめました。

こんにちは。先日、STM32でROSを対応させるときに、USARTのDMAをLLで書いていたのですが上手く動かすことができず断念するということをしてしまいました。1

言い訳は注釈で書きました。USARTをHALにしたので、他の周辺機能で設定が面倒なものはHALを使用することにしました。SPIやGPIOはレジスタ直接たたくということをしています2

モータードライバを動かすためにTIMのPWM出力をプリスケーラとDutyの両方を変更するものを、HALで実装をしようとしたら少しはまったので忘備録として書いていきたいと思います。LLAPIのときにデータシートを眺めたりすることはしたので、データシートを読んでみたりしてみたいという方はこちらをご覧ください。

使用しているマイコンはSTM32F405RGT,環境はgcc(g++),makefileでブザーを鳴らしていきます。




HAL APIのTIMのPWM周りの実装を確認する

初期化の関数を確認

PWM出力の場合は、プリスケーラ等をSTM32CubeMXで設定をしているため、ほぼ必ずそれらの値を反映させて設定をしているプログラムがあるはずです。すなわち、初期設定を変更する方法は、TIMxの初期化の関数を読めばわかりそうだということが考えられます。3

void MX_TIM1_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};
  TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};

  htim1.Instance = TIM1;
  htim1.Init.Prescaler = 799;
  htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim1.Init.Period = 99;
  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.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();
  }
  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.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
  if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
  {
    Error_Handler();
  }
  HAL_TIM_MspPostInit(&htim1);

}

この関数を確認すると、最初にhtim1の構造体にプリスケーラ等の設定をして、関数を使用して設定を反映していることがわかります。PWM出力の前には、sConfigOCの構造体にいろいろな設定をしていることが読み取れます。TIM1の場合はデッドタイム等の指定もできるようでデッドタイムの指定は sBreakDeadTimeConfig構造体で行うということも読みとれました。今回の実装ではデッドタイムは使用しないため、使用すべきものはhtim1構造体とsConfigOCの構造体でOKということがわかりました。

STM32CubeMXの設定

STM32CubeMXの設定は、LLAPIの記事と同じようにしてHALのまま出力をしました。

CubeMXで自動生成をしてくれたら、該当のディレクトリに移動するなり統合開発環境で開くなりして実装ができる状態にしましょう。

実装をしてみる

PWMを出力する関数を作る

PWMを出力する関数には、プリスケーラの値、PWMのDutyの値を入力できるように作りました。実装は以下の通りです。


void buzzer_pwmout(uint32_t prescaler, uint32_t pwm)
{
  TIM_OC_InitTypeDef sConfigOC = {0};

  if(pwm > 99) pwm = 99;

  htim1.Instance = TIM1;
  htim1.Init.Prescaler = prescaler;
  htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim1.Init.Period = 99;
  htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_PWM_Init(&htim1);

  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = pwm; // set duty
  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_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);

  // start
	HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

}

pwmの最大出力を超える入力は最大値で制限するようにしています。それ以外に関しては、先程確認した通りにしています。

使用例

ブザーを鳴らす関数を以下のように定義しました。

#include "buzzer.h"

#include "tim.h"

static volatile uint32_t beep_time = 0;
static volatile uint32_t buzzer_counter = 0;
static uint8_t buzzer_busy = 0;

void buzzer_output(uint16_t _scale, uint32_t _beep_time)
{
  if(buzzer_busy == 1){
    return;
  }

  buzzer_pwmout(_scale, 99);
  buzzer_busy = 1;
  beep_time = _beep_time;
  buzzer_counter = 0;
}

void buzzer_interrupt(void)
{
  if(buzzer_counter < beep_time){
    buzzer_counter++;
  } else {
    buzzer_pwmout(799, 0);
    buzzer_busy = 0;    
  }
}
#ifndef __BUZZER_H
#define __BUZZER_H

#include <stdint.h>

#ifdef __cplusplus
 extern "C" {
#endif

#define C_SCALE 3058U
#define D_SCALE 2886U
#define E_SCALE 2724U
#define F_SCALE 2571U
#define G_SCALE 1926U
#define A_SCALE 1818U
#define B_SCALE 1620U
#define C_H_SCALE 1529U

void buzzer_output(uint16_t _scale, uint32_t _beep_time);
void buzzer_interrupt(void);

#ifdef __cplusplus
}
#endif

#endif /* __BUZZER_H */

ブザーをある一定時間ならすことは、mainルーチンで呼び出した後にブザーが鳴っている間main関数の処理を止めてまつということをしたくないため、割り込み関数内でブザーがどれくらいの時間をなっているのか確認して、設定した時間よりも長くなったらブザーの値を消すということや、ブザーが鳴っている最中に新しいブザーの命令が来た場合は、ブザーの処理を行わずに関数を終了するようにしています。待つプログラムにしたい場合はフラグが落ちるまでという処理をwhileで行えばできると思います。

buzzer_interrupt関数は1ms割り込みのかかるsystickの割り込みで呼んであげるといいかなと思います。

さいごに

HALを使用すると、HALAPIの関数が乗ったドキュメントとSTM32CubeMXの初期コードを見てからデータシートを軽く読むだけで動いたのでとても楽に感じました。

LLAPIを使用してDMA等の設定をいじって上手く動かず挫折した後の出来事だったのでとても驚きました。マイコンをレジスタでたたいていきたいという謎のプライド等がない場合はHALを使用して安全に周辺機能を叩いたほうがいいのかなと思ったりしている今日です。自分のブログをみていたらHALのプログラム例があんまりなかったので書いていこうかなと思っています。HALのコードをmakefileで出力すると最適化オプションが-O2で設定されているため、痛い目にあうことがあるのでmakefileの環境の方は気を付けた方がいいかもです。

LLAPIでDMA転送受信でROSSerialが安定して動くようになったら記事にする予定です。余裕をもっていじることができるように目の前のタスクをさばいてい行きたいと思います。

参考文献

UM1725 User Manual

RM0090 リファレンスマニュアル

  1. 6時間程度データシート等を読んで、転送、受信はできたがROSSerialではエラーが出たりしてしまったため。もうちょっと頑張ればなんとかなったかもしれないが、STM32でROSSerial対応よりも優先度の高いタスクがあったため後日やることとした。
  2. HALAPIを使用しているといえるのだろうか?
  3. 邪道かもしれません。