SW4STM32でHALを使わずにPWM出力を行う

SW4STM32でHALを使わずにPWM出力を行う
Nucleo F411RE

はじめに

System Workbench for STM32 (SW4STM32)を使ったSTM32マイコンプログラミングについて調べていると,その情報の95%はHALやLLといった抽象化レイヤーを使ったものになるといっても過言ではないでしょう.

しかし,こういった抽象化レイヤーは何をやっているかわからない!!

例えばここ (About STM32 HAL quality and performance – Stack Overflow)では,以下のような意見が見られます.

I do not like HAL for many reasons:

1. It gives pseudo developers false feeling that they do not have to know how their hardware works.

2. Time spent learning HAL may be longer (and usually is) than needed to understand how the hardware works.

3. Horrible overhead

4. Many errors.

まあ,当たり前ですよね!

HALを習得するのと同じ時間で,レジスタの設定を習得できると指摘されています.
USBやEthernetはともかく,基本的なペリフェラルならレジスタ手打ちで行くのが一流STM32erの流儀なのだそう.

ということで,今回はNucleo F411REを使ったPWM出力をレジスタ手打ちで行います.

環境

  • macOS Mojave
  • SW4STM32 v2.8
  • Standalone版 STM32CubeMX 5.0.1 (STM32Cube 1.0)
  • Nucleo F411RE

資料を準備

とりあえず,データシートとリファレンスマニュアルがあればなんとかなります.

今回は,DS10314 (データシート)とRM0383(リファレンスマニュアル)を以下のページから入手しました.

STM32F411 – ST

初期設定

本当なら,全ての設定をレジスタ手打ちで行いたいところですが,システムクロックの初期化に関してはSTM32CubeMXのものが秀逸なので,こちらで行いたいと思います.

初期化なら一回しか実行されないですし,HALでも良しとしましょう.

STM32CubeMXで新規プロジェクトを作成したら,”Pinout & Configuration”は無視して”Clock Configuration”へ進みます.

以下のように設定します.

cubemx_clockconfig
STM32CubeMXで行うクロック設定の画面

“Project Manager”に進み,プロジェクト名などを設定したら,後は適当に進めて”Generate Code”しましょう.

コーディング

準備

まずは,レジスタ手打ちに必要なヘッダファイルをincludeします.AVRで言うところの”avr/io.h”のようなものでしょう.

今回は”stm32f4xx.h”ですが,F3シリーズなら”stm32f3xx.h”になるはずです.

/* USER CODE BEGIN Includes */
#include "stm32f4xx.h"
/* USER CODE END Includes */

タイマについて

STM32F411には,高機能タイマ (Advanced-control timer)であるTIM1と,汎用タイマ (General-purpose timer)であるTIM2~5,TIM9~11があります.

汎用タイマであるTIM2,5 は32bit,TIM3,4は16bitのカウンタを備えています.TIM9~11は,TIM2~5よりもチャンネルの数が少なかったりしますが,PWM出力やカウンタの値が溢れた(アップデート)時の割り込みなど,基本的な機能は取り揃えています.

また,高機能タイマであるTIM1は,通常のタイマ機能に加えてデッドタイム付きPWMを生成する機能などがついています.

STM32でPWM出力を行うことができるのは高機能タイマと汎用タイマですが(型番によっては基本タイマもある),高機能タイマでは汎用タイマにない操作を必要とするので,注意が必要です.

なお,Nucleo F411REを使う場合,TIM2のCH4,TIM5のCH3,4,TIM9の全てのチャンネル (CH1,2)はST-Linkに接続されたUSART2 (PA2,3)と干渉しているため,出力として使うことができません.

PWM出力を行うコード

それでは,PWM出力を行うコードを示します.

汎用タイマの場合

汎用タイマの場合,以下のようにしてPWM出力を行います.

なお,タイマはTIM3,出力チャンネルはCH3 (ピンはPB0)とし,PWMの周波数は40KHz,よって分周比は5,カウンタの上限は500としました.デフォルトのDuty比は50%,よって比較値は250(249)とします.

 // Start TIM3 (40KHz PWM) channel: CH3 (PB0)

// Clock configs depend on channel No. (CH3)
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; // Enable GPIOB clock
GPIOB->MODER |= GPIO_MODER_MODER0_1; // Set PB0 Alternate mode
GPIOB->AFR[0] |= GPIO_AFRL_AFSEL0_1; // Set GPIOB_AFRL AFRL0 to AF2(TIM3)

// Initialize TIM3 before timer configs
TIM3->CR1 &= ~(TIM_CR1_CEN); // Disable TIM3 counter (temporary)
RCC->APB1RSTR |= (RCC_APB1RSTR_TIM3RST); // Reset TIM3 (Software reset)
RCC->APB1RSTR &= ~(RCC_APB1RSTR_TIM3RST); // Reset TIM3 (Software reset)

// Timer configs
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // Enable TIM3 clock
TIM3->EGR |= TIM_EGR_UG; // Update Generation
TIM3->CR1 |= TIM_CR1_ARPE; // Enable TIM3 ARR auto-reload

TIM3->PSC = 5 - 1; // Set TIM3 prescaler
TIM3->ARR = 500 - 1; // Set TIM3 auto-reload value

// Timer configs depend on Channel No. (CH3)
TIM3->CCR3 = 249; // Set TIM3 CH3 pulse width
TIM3->CCMR2 |= TIM_CCMR2_OC3M_1 + TIM_CCMR2_OC3M_2; // PWM mode 1
TIM3->CCMR2 |= TIM_CCMR2_OC3PE; // Enable output compare 3 preload
TIM3->CCER |= TIM_CCER_CC3E; // Enable TIM3 CH3 positive output
TIM3->CCER &= ~(TIM_CCER_CC3NE); // Disable TIM3 CH3 negative Output

// Update interrupt configs ( void TIM3_IRQHandler(void) )
// TIM3->DIER |= TIM_DIER_UIE; // Enable Update Interrupt
// NVIC_EnableIRQ(TIM3_IRQn); // Enable TIM3 Update interrupt

// Enable TIM3 Counter
TIM3->CR1 |= TIM_CR1_CEN;
// Finished to start TIM3 (40KHz PWM)

それでは,順に見ていきます.

まずはこの部分から.

 // Clock configs depend on channel No. (CH3)
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; // Enable GPIOB clock
GPIOB->MODER |= GPIO_MODER_MODER0_1; // Set PB0 Alternate mode
GPIOB->AFR[0] |= GPIO_AFRL_AFSEL0_1; // Set GPIOB_AFRL AFRL0 to AF2(TIM3)

この部分は,CH3を選んだことに依存するクロック関連の処理です.

初めの行は,GPIOBにクロックを供給し,PB0を使えるようにしています.なお,右辺のマクロは “RCC_AHB1ENR_GPIOBEN” ですが,これは “種類_レジスタ_設定する項目” になっています.ほかのレジスタでも,基本的にはこのようにしてリファレンスマニュアルの通りに書けば良いです.

2番目の行は,GPIOBのポート10 (PB10) をAlternate Modeにしています.GPIOのモードにはInput, Output, Alternate, Analogの4つがあり,Alternate Modeは0x10となります.

このように,設定する項目が2bit以上の場合,“種類_レジスタ_設定する項目_0”“種類_レジスタ_設定する項目_1”“種類_レジスタ_設定する項目_2” といったマクロが存在します.それぞれ(0x001 << “種類_レジスタ_設定する項目_Pos”), (0x010 << “種類_レジスタ_設定する項目_Pos”), (0x100 << “種類_レジスタ_設定する項目_Pos”)となっているので,これらを足し合わせることで値を表現することができます.この場合は,0x10を指定したいので,”GPIO_MODER_MODER0_1″だけで良いことになります.

 

最後の行は,GPIOBのポート10 (PB10) が,数あるAlternate Functionの中で,どれになるのかを指定しています.データシートの以下の表によると,TIM3_CH3はAF2となるので,2=0x10より”GPIO_AFRL_AFSEL0_1″を設定すれば良いことがわかります.なお,ポートが0~15まであるのでAFRLとAFRHに分かれていますが,これはAFR[0] および AFR[1]でアクセス可能です.

STM32F411 データシートより抜粋

次に,以下の部分です.

 // Initialize TIM3 before timer configs
TIM3->CR1 &= ~(TIM_CR1_CEN); // Disable TIM3 counter (temporary)
RCC->APB1RSTR |= (RCC_APB1RSTR_TIM3RST); // Reset TIM3 (Software reset)
RCC->APB1RSTR &= ~(RCC_APB1RSTR_TIM3RST); // Reset TIM3 (Software reset)

この部分では,TIM3の設定を行う前に一度TIM3のカウンタを無効化し,TIM3へのクロックをリセットしています.

次は,この部分です.

 // Timer configs
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // Enable TIM3 clock
TIM3->EGR |= TIM_EGR_UG; // Update Generation
TIM3->CR1 |= TIM_CR1_ARPE; // Enable TIM3 ARR auto-reload

TIM3->PSC = 5 - 1; // Set TIM3 prescaler
TIM3->ARR = 500 - 1; // Set TIM3 auto-reload value

この部分では,TIM3全体の設定を行なっています.

最初の行ではTIM3へのクロック供給を有効化しています.なお,TIM1の場合は左辺がAPB2ENRになるなど,タイマによってAPB1/APB2が変化するため注意が必要です.

2番目の行では,カウンタを初期化しています.

3番目の行では,TIM3のARRレジスタの値によるカウンタ上限の設定を有効化しています.

最後から2番目の行では,分周比を設定しています.なお,この値+1 が分周比となるため,”5 – 1″という表記になっています.

最後の行では,カウンタの上限値を設定しています.

次に,この部分です.

 // Timer configs depend on Channel No. (CH3)
TIM3->CCR3 = 249; // Set TIM3 CH3 pulse width
TIM3->CCMR2 |= TIM_CCMR2_OC3M_1 + TIM_CCMR2_OC3M_2; // PWM mode 1
TIM3->CCMR2 |= TIM_CCMR2_OC3PE; // Enable output compare 3 preload
TIM3->CCER |= TIM_CCER_CC3E; // Enable TIM3 CH3 positive output
TIM3->CCER &= ~(TIM_CCER_CC3NE); // Disable TIM3 CH3 negative Output

この部分では,TIM3のCH3の設定を行なっています.

最初の行では,CH3の比較値を設定しています.初期設定完了後も,この値を変更することでDuty比を変更することができます.今回は50%なので,500/2 = 250 としました(Compare Matchなので実際の値は249).

2番目の行では,PWMのモードをモード1に設定しています.モード1は0x110なので,先述のマクロと同様に表現されています.なお,チャンネルが変わるとCCMR1/2も変わるため,注意が必要です.

3番目の行では,CCRレジスタのプリロードの有効化を行なっています.すなわち,初期設定完了後,カウンタの動作中に比較値を変更しても,次にアップデートされるまで以前の比較値を維持します.

4番目と5番目の行では,正の出力を有効に,負の出力を無効にしています.

そして,この部分です.uncommentすると,以下のようになります.

 // Update interrupt configs ( void TIM3_IRQHandler(void) )
TIM3->DIER |= TIM_DIER_UIE; // Enable Update Interrupt
NVIC_EnableIRQ(TIM3_IRQn); // Enable TIM3 Update interrupt

この部分では,アップデート時の割り込みを有効化しています.以下のようにして,“TIM3_IRQHandler”という名前の関数を定義すると,その関数が割り込みハンドラとなります.

void TIM3_IRQHandler(void)
{
if( TIM3->SR & TIM_SR_UIF ){
// Some processes
TIM3->SR &= ~(TIM_SR_UIF); // Clear interrupt flag
}
}

割り込みハンドラの中では,TIM3のステータスレジスタにあるフラグをチェックして,それをクリアする必要があることに注意します.

最後の部分では,TIM1を有効化しています.PWMを止める時は,この逆を行うことでとりあえずカウンタがストップします.

 // Enable TIM1 Counter
TIM1->CR1 |= TIM_CR1_CEN;

高機能タイマの場合

高機能タイマの場合,以下のようにしてPWM出力を行います.

なお,タイマはTIM1,出力チャンネルはCH3 (ピンはPA10)とし,PWMの周波数は100KHz,よって分周比は1,カウンタの上限は1000としました.デフォルトのDuty比は50%,よって比較値は500(499)とします.

 // Start TIM1 (100KHz PWM) channel: CH3 (PA10)

// Clock configs depend on channel No. (CH3)
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Enable GPIOA clock
GPIOA->MODER |= GPIO_MODER_MODER10_1; // Set PA10 Alternate mode
GPIOA->AFR[1] |= GPIO_AFRH_AFSEL10_0; // Set GPIOA_AFRH AFRH10 to AF1(TIM1)

// Initialize TIM1 before timer configs
TIM1->CR1 &= ~(TIM_CR1_CEN); // Disable TIM1 counter (temporary)
RCC->APB2RSTR |= (RCC_APB2RSTR_TIM1RST); // Reset TIM1 (Software reset)
RCC->APB2RSTR &= ~(RCC_APB2RSTR_TIM1RST); // Reset TIM1 (Software reset)

// Timer configs
RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // Enable TIM1 clock
TIM1->EGR |= TIM_EGR_UG; // Update Generation
TIM1->CR1 |= TIM_CR1_ARPE; // Enable TIM1 ARR auto-reload

TIM1->PSC = 1 - 1; // Set TIM1 prescaler
TIM1->ARR = 1000 - 1; // Set TIM1 auto-reload value

// Timer configs depend on Channnel No. (CH3)
TIM1->CCR3 = 499; // Set TIM1 CH3 pulse width
TIM1->CCMR2 |= TIM_CCMR2_OC3M_1 + TIM_CCMR2_OC3M_2; // PWM mode 1
TIM1->CCMR2 |= TIM_CCMR2_OC3PE; // Enable output compare 3 preload
TIM1->CCER |= TIM_CCER_CC3E; // Enable TIM1 CH3 positive output
TIM1->CCER &= ~(TIM_CCER_CC3NE); // Disable TIM1 CH3 negative Output

// Timer configs depend on advanced-control timer
TIM1->BDTR &= ~(TIM_BDTR_OSSR); // Set TIM1 CHx Negative Output as High-impedance (Set Before MOE bit!)
TIM1->BDTR |= TIM_BDTR_MOE; // Enable TIM1 Output

// Update interrupt configs
// TIM1->DIER |= TIM_DIER_UIE; // Enable Update Interrupt
// NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn); // Enable TIM1 Update interrupt

// Enable TIM1 Counter
TIM1->CR1 |= TIM_CR1_CEN;
// Finished to start TIM1 (100KHz PWM)

汎用タイマとは違って,以下の部分が付け加えられました.

 // Timer configs depend on advanced-control timer
TIM1->BDTR &= ~(TIM_BDTR_OSSR); // Set TIM1 CHx Negative Output as High-impedance (Set Before MOE bit!)
TIM1->BDTR |= TIM_BDTR_MOE; // Enable TIM1 Output

高機能タイマにはBDTRレジスタがあって,この中のMOEビット(萌えではありません.Main Output Enableです)を立てる必要があります.また,OSSRビットを以下の表に則って明示的にセットしてありますが,この設定はMOEビットの設定よりも前に持ってこないとなぜかバグるため注意が必要です.

リファレンスマニュアルより抜粋

また,割り込みハンドラの名前が変わります.汎用タイマでは “NVIC_EnableIRQ( TIMx_IRQn )”でしたが,これが“NVIC_EnableIRQ( TIM1_UP_TIM10_IRQn )” (UPはUpdateの意) になり,割り込みハンドラの名前は “TIMx_IRQHandler” から “TIM1_UP_TIM10_IRQHandler”になります.この辺の組み合わせについては,STM32CubeMXの設定を見るのが早いと思います.

CubeMXでどのタイマと共用しているかを調べる

そのほかの設定に関しては,汎用タイマと同様に行うことができます.

最後に

レジスタ手打ちによるPWMは,なかなか辛い作業になりました.

日本語はもちろん,英語で調べてもBare MetalなPWM出力に関する情報はかなり少ない!!

ただ,やはりCubeMXがあることで答え合わせができるので,そこはとても心強いなあと感じました.