STMマイコンでPWMを出力するときのメモ（HAL, CubeMX）

# 開発環境
## **初期設定コード自動生成：STM32CubeMX**
STM32マイコンの各種ペリフェラルの初期設定を行えます。
GUIでの操作が基本ですが、Arduino・Mbedと比較するとHALドライバーは低級ライブラリなのであらゆる設定を自分で行う必要があります。
ペリフェラルの初期設定するためにはその周辺知識が絶対必須なので、想像していたよりも扱いが難しかったです。
<img width="600" alt="ScreenShot_20210120215321.png (102.1 kB)" src="https://img.esa.io/uploads/production/attachments/17012/2021/01/20/95787/d9bfda71-50bc-483c-94f4-3f4a5484d577.png">
<img width="600" alt="ScreenShot_20210120215313.png (33.4 kB)" src="https://img.esa.io/uploads/production/attachments/17012/2021/01/20/95787/cbe95972-bbf1-4c4b-b281-4e86b06a5ba2.png">
<img width="600" alt="ScreenShot_20210120215300.png (71.3 kB)" src="https://img.esa.io/uploads/production/attachments/17012/2021/01/20/95787/00a762ba-0a49-4e36-9941-5384c5cd4e11.png">


## **IDE：STM32CubeIDE**
STM32CubeMXで生成した初期コードを読み込み、ユーザープログラムを記述していくための環境です。大体ソフトはあると思いますが、公式が出しているIDEが余計な配慮が少ないと思って使っています。

- ①ファイルの管理ができます。
- ②実際にプログラムを記述していくエディタ部です。HALドライバーのソースを見て使用する関数の説明、パラメータ要件の確認なども行います。（徹底的に構造体で管理されているのでパラメータ設定が複雑になりがち。常にHALのソースとにらめっこして開発していた。）
- ③見ているファイル内の宣言されている関数、構造体、オブジェクトの目次です。ソースが数百行のファイルが山ほどあるので見たいところにクリック一つで飛べるのはすごく便利です。
- ④Nucleoボードにはビルトインデバッガが搭載されています。STlink経由でIDEからデバッグが行えます。（指定したブレイクポイントでプログラムを停止させる＆その瞬間の変数・配列の中身を覗くことができる＆プログラムを1文ずつ実行できる）

<img width="900" alt="ScreenShot_20210120215243.png (264.8 kB)" src="https://img.esa.io/uploads/production/attachments/17012/2021/01/20/95787/83930a93-95ef-45a5-a10f-3640687dbb05.png">

# 検証した項目・その過程
## **高分解能・高周波のPWMを出力**
### 経緯
今までSTMマイコンではMbedOSを使用して開発を行ってきました。
しかし、Arduinoライブラリと同様に使いやすさ、異種マイコン間での互換性を重視し、削除・制限されている機能がたくさんあります。
その一つにPWMの出力パルス幅の指定があります。MbedOSにはpwm_obj.period_us()というPWMの周期(周波数の逆数)を設定するメソッドがあります。
このメソッドは最小で1us、つまりMbedのPWMステップを1usオーダーでON/OFFできるようになるということです。つまり、制御できる最短時間が1usということなのでPWMを200段階調節したいと思ったとき、1周期当たり$ 1us * 200(分解能) = 200us$ の時間を要します。
結果として周期が200usのPWMは5KHzとなってしまいます。要求されている30KHzには到底及ばないだけではなく、PWM分解能も200しかありません。
そこで、より低級なライブラリである。HALドライバーを使用してPWM生成に使用されているタイマーの設定を直接書き換えることでこの問題を解決します。
参考：[what is the maximum frequency of the PWM output in LPC1768?](https://os.mbed.com/questions/192/what-is-the-maximum-frequency-of-the-PWM/)

### 初期設定
参考：[Getting PWM to work on STM32F4 using ST's HAL libraries](https://electronics.stackexchange.com/questions/179546/getting-pwm-to-work-on-stm32f4-using-sts-hal-libraries)
参考：[STM32 HALを使ってPWM出力してみる](https://moons.link/post-632/)

GUIを使用して、NucleoL432KCに出ているPWMの出力設定を行います。
(PA8のTIM1_CH1を選択しました)
次に左のTIM1のページに移動してClock SourceをInternal Clockに、Channel1をPWM Generation CH1に設定します。
(PA8がタイマー1でチャンネル1だからです。チャンネルとはタイマーのクロックを設定されたモードによる比較、条件に応じた出力を行うユニットです。)
その下のConfigurationのParameter SettingのタブでPrescaler、Counter Periodなどを設定していきます。
設定項目はこのような感じです。
<img width="380" alt="ScreenShot_20210123204228.png (29.5 kB)" src="https://img.esa.io/uploads/production/attachments/17012/2021/01/23/95787/4335d37b-b3bb-4e90-b84e-653500d87b6e.png">
<img width="384" alt="ScreenShot_20210120224319.png (18.5 kB)" src="https://img.esa.io/uploads/production/attachments/17012/2021/01/20/95787/49b9252c-ab86-4b57-8a9d-7e13ea42efcb.png">


参考：[Stm32 pwm frequency and period calculation](https://www.programmersought.com/article/3269403980/)
 ここで重要なのが、Prescaler、Counter Periodです。
この部分はMbedOSからは触れません。（Mbed内でHALをたたけば行けるかもしれませんが。）
Clock ConfigurationのタブからHCLKを確認します。
<img width="429" alt="ScreenShot_20210120224536.png (16.7 kB)" src="https://img.esa.io/uploads/production/attachments/17012/2021/01/20/95787/f00083fb-9005-42c0-a7d1-43d0a1cfd53d.png">
現在は80MHzに設定されています。
この80MHzをベースクロックとしてPWMの周波数を計算します。
周波数の計算式は以下の通りです。

```math
 f_{PWM} = \frac{f_{HCLK}}{(CounterPeriod+1)\times(Prescaler+1)}
```
30KHzのPWMが欲しい時、各定数を考慮すると以下のようになります。

```math
 30KHz = \frac{80MHz}{(CounterPeriod+1)\times(0+1)}\\
```

```math
 \therefore CounterPeriod = 2665.6
```
となります。

次にプログラム側の設定では、以下の関数を実行します。

```cpp
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, count);
```
各第一、第二引数のオブジェクトの値は使用するタイマー/チャンネルによって変更する必要があります。
`__HAL_TIM_SET_COMPARE`関数の第3引数には`CounterPeriod`を最大値とした値を入力することで、PWM の出力を調節できます。

#### タイマ初期設定コード

```cpp
static void MX_TIM1_Init(void)
{

  /* USER CODE BEGIN TIM1_Init 0 */

  /* USER CODE END TIM1_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {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 = 2665;
  htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim1.Init.RepetitionCounter = 0;
  htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  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();
  }
  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);

}
```


#### PWM出力設定コード
CCRxレジスタに値を書きこむ事でタイマーによってクロックアップされた値を比較し、タイマーがオーバーフローしたときに出力がHIGHに、CCRxに設定された値とタイマーのクロックアップ値が一致したとき出力がLOWに設定されます。タイマーの周期は2665クロックでオーバフローするように設定したので、0~2665の値を`__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, count);
`関数の第3引数に書き込むことでDuty比を調節しています。

```cpp
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_TIM1_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
  int count = 0, max_step = 2665, nagative_flag = 0;
  while (1)
  {

	  if(count > max_step){
		 nagative_flag = 1;
		 count = max_step;
	  }
	  else if(count < 0){
		  nagative_flag = 0;
		  count = 0;
	  }
	  if(nagative_flag) count--;
	  else count++;
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, count);
    HAL_Delay(30);
    /* USER CODE END WHILE */
  }
  /* USER CODE END 3 */
}
```


![PWM_Waveform_30KHz.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/925192/29700e04-fc34-6d10-bd62-530208175be4.gif)