STM32 + LL TIMのインプットキャプチャを用いてPWMのDutyと周波数を計測する

2021年2月16日

こんにちは。今回は、STM32をいじっていきたいと思います。
プログラムの実行は以下の写真のような感じで行いました。

使用したボードはSTM32F401REの搭載されたNucleoボードです。PWM1つのポートから出力をして、TIMのインプットキャプチャに設定したピンの2つを接続しました。PWMの出力を確認するためにLEDをつけておきました。
今回の実行結果は以下のようになりました。

Dutyが1%のときは読むことができませんでしたが、Dutyが2以上のときには出力に近い周波数とDutyを取得することができていることがわかると思います。




データシートを確認する

データシートを確認していきます。LLAPIは、関数の中でレジスタをたたいているだけなので、それなりに理解をする必要があります。
TIM3のインプットキャプチャの記述は次のようになっていました。

RM0368より引用

図112よりTIMのチャンネル2と別のチャンネル(チャンネル3)を使用して実装をしていきたいと思います。LLAPIで設定されたコードを確認したところ、最低限必要だと思われる初期設定はしてくれていたため、実装に必要なレジスタの確認をしていきます。

RM0368より引用

今回の実装では割り込みを用いて実装を行う予定のため、割り込み時のフラグの確認をしてみるとCC1IFに説明が書いてありました。検出の設定をしたエッジが検出できたときにフラグが立ち上がるようです。ただし、フラグが立ち上がったあとに自動でフラグが0になるというわけではないということがわかりました。
このことをもとに実装をしていきます。

STM32CubeMXの設定

TIM2をPWM出力に設定しました。(PWM出力を別の装置で行う場合は必要ありません)

インプットキャプチャの設定はTIM3で行いました。

TI2FP2をトリガーにするために、Slave ModeはReset Modeに設定しています。Counter Periodは65535に設定しました。Channel3はFalling Edgeに変更を忘れないようにしてください。
続いて、クロックの確認をします。
TIM2,TIM3のクロックソースはAPB1 Timer Clocksなので84MHzであることがわかりました。
このことをもとに、今回の設定で計測できるPWMの周波数の最小値は、
84000000 / 65535 = 1281.75783932[Hz]
以上となります。もし、これよりも小さいPWMの周波数の計測したい場合はプリスケーラで分周すればできます。計算式は以下の通りです。
(APB1 Timer Clocks) / ((Counter Period) * (Prescaler+1) )

また、今回のプログラムでは、printfを使用するため、printfの実装方法については以下の記事を参考にしてください。

続いて、Project Managerで設定をしていきます。Project Nameは適当に決めてください。Toolchain/IDEは各自の環境にあったものを選択してください。他の2つのタブの設定は以下の通りです。

Advanced Settingで使用するAPIはLL一択です。HALから変えましょう。
ここまで設定したらコードをGenerate Codeを押してコードを作ってもらいましょう。

実装編

まずは、tim.cのUSER CODE BEGIN 0の下に変数の定義をしました。

uint32_t ch2_data, ch3_data;
float duty, frequency;
volatile uint32_t cnt_tim;

USER CODE BEGIN 1の下に関数を書いていきたいと思います。
TIMのPWMの実装

void TIM2_PWM_Start(void) {
  LL_TIM_CC_EnableChannel(TIM2, LL_TIM_CHANNEL_CH2);
  LL_TIM_EnableCounter(TIM2);
}
void TIM2_PWM_Out(uint32_t scale, uint32_t pwm) {
  LL_TIM_SetPrescaler(TIM2, scale);
  if (pwm > 99) pwm = 99;
  LL_TIM_OC_SetCompareCH2(TIM2, pwm);
  cnt_tim = 0;
  printf("frequency = %ld, pwm = %ld\r\n", 84000000 / ((scale + 1) * (99 + 1)),pwm);
}

TIMのInputCaptureの実装

void TIM3_InputCaputure_Start(void) {
  LL_TIM_CC_EnableChannel(TIM3, LL_TIM_CHANNEL_CH2);
  LL_TIM_CC_EnableChannel(TIM3, LL_TIM_CHANNEL_CH3);
  LL_TIM_EnableIT_CC2(TIM3);
  LL_TIM_EnableIT_CC3(TIM3);
  LL_TIM_EnableCounter(TIM3);
}
void TIM3_Callback(void) {
  if (LL_TIM_IsActiveFlag_CC2(TIM3) == SET) {
    LL_TIM_ClearFlag_CC2(TIM3);
    ch2_data = LL_TIM_IC_GetCaptureCH2(TIM3);
    frequency = (float)(84000000.0f / ch2_data);
  }
  if (LL_TIM_IsActiveFlag_CC3(TIM3) == SET) {
    LL_TIM_ClearFlag_CC3(TIM3);
    ch3_data = LL_TIM_IC_GetCaptureCH3(TIM3);
    if (ch2_data != 0) {
      duty = (float)(ch3_data * 100.0f / ch2_data);
    }
  }
}

ここで、Dutyの計算と周波数の計算についてですが、Dutyは、Highの時間 * 100/Lowの時間、周波数はTIMの周波数/Lowの時間で計算することが可能です。

プログラムを載せませんが、tim.hにグローバル変数をexternおよび関数のプロトタイプ宣言をしました。続いて、stm32f4xx_it.cにtim.hをインクルードして、SysTick_Handlerにcnt_tim++を追加、TIM3_IRQHandlerにTIM3_Callback()を追加しましょう。

main.cの初期化後のプログラムは以下の通りです。

/* USER CODE BEGIN 2 */
setbuf(stdout, NULL);
printf("pwm input capture sample\r\n");
LL_SYSTICK_EnableIT();
TIM2_PWM_Start();
TIM3_InputCaputure_Start();
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */

for (int i = 0; i <= 100; i++) {
  LL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
  TIM2_PWM_Out(i, i);
  while (cnt_tim < 2000) {
    if (cnt_tim % 250 == 0)
      printf("ch2 = %ld, ch3 = %ld, duty = %f, frequency = %f\r\n",ch2_data, ch3_data, duty, frequency);
    }
  }
}

Systickの1msタイマ割り込みを有効にして、pwmのdutyおよびプリスケーラを2秒ごとに変更するようにプログラムをつくりました。cnt_timはTIM2_PWM_Outで0に設定、SysTick_Handlerで足されているため、無限ループに陥るということはありません。

さいごに

リファレンスマニュアルの説明が短かったため、データシートのレジスタと睨めっこして実装することになりました。STM32のリファレンスマニュアルは説明がしっかりと書いてあるところとほとんど書いてないところで実装のしやすさが天と地ほどかわるような気がします。HALを使えば簡単だよってことなんでしょうか?

参考文献

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