0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HALの使い方虎の巻(PWM編)

0
Last updated at Posted at 2026-02-10

ほたるしよう!

どもども、むーちょspやで

前回はLEDをぴかぴかさせました。
LEDがぴかぴかすればこれからも特に問題なく進んでいけると思うので、安心して虎の巻を読み進めていきましょう。

ちなみにこれからはプロジェクトの作成や書き込みについては詳しくは触れないので、わからなくなったらLチカ編に戻りましょう。

ほかの記事のまとめはこちら。

今回の作戦

今回は電子蛍を作ります。
電子蛍とは、電子の蛍です。
LEDをだんだん光らせたりだんだん消したりするものです。

前回作ったものはパッと光ったりパッと消えたりするものでしたが、それをゆっくりやろうというのが今回の趣旨です。

マイコン

マイコンは前回のと同じものを使います。

事前知識

まず、マイコンは中途半端な明るさに光らせることはできません。
マイコンのON/OFFはボタンのON/OFFと同じです。つまみみたいにゆっくり調節はできません。
(DACを使えばできますがここでは触れません)

じゃあどうするのかということですが、高速で点滅させることであいまいな明るさを表現します。

前回LEDを500ms(0.5秒)間隔で点滅させました。
VID_20260211_205234.gif
それじゃあ、これを50ms間隔の速さで点滅させたらどうでしょう。
VID_20260211_205256.gif
まだ点滅してるのがわかりますね。
じゃあ5msだと?
VID_20260211_205321.gif
このくらい速く点滅すると、もはや点滅しているのかがわからなくなります。

そしてこの明るさ、普通に光らせた明るい時と比べると、ちょっと暗く光っているように見えるでしょう。

では、点灯時間が消灯時間よりも長い場合はどうでしょう。
先ほどの場合よりも少し明るくなるかと思います。

このようにして微妙な明るさを表現します。
これをPWM(Pulse width modulation/パルス幅変調)と呼びます。
高速でON/OFFを繰り返し、ONの時間を調節することで微妙な電圧を表現します。

ONの時間とOFFの時間の比率のことをDuty比と呼びます。
計算方法はON / (ON + OFF)です。ずっとONなら100%です。

ちなみに、LED以外にもDCモーターの速度調節にも使います。
ロボコンだとそのくらいか。

実際にやってみよう

さて、実際にプログラムを見てやっていきましょう。

Delayを使う方法

まずは簡単に、前回のプログラムを流用して作ってみましょう。

ちなみに今後のコードの示し方は、BEGINからENDの間を示すことにします。
USER CODE 3ならば/* USER CODE BEGIN 3 *//* USER CODE END 3 */の間に書き込めばOKということです。(ちなこの場合は}を書いてないですが、足りない分は適宜自分で付け足してください。)

話がそれましたが、以下が5msで点滅するコードです。

USER CODE BEGIN 3
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET);
HAL_Delay(5);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_RESET);
HAL_Delay(5);

おそらく、点滅は感知できないと思います。

そしたら、この点滅の比率を変えていってみましょう。
サンプルはこちら

USER CODE BEGIN 3
// だんだん明るく
for (int i = 0;i < 10;i++) {
  for (int j = 0;j < 10;j++) {
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET);
    HAL_Delay(i);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_RESET);
    HAL_Delay(10 - i);
  }
}
// だんだん暗く
for (int i = 0;i < 10;i++) {
  for (int j = 0;j < 10;j++) {
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET);
    HAL_Delay(10 - i);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_RESET);
    HAL_Delay(i);
  }
}

まず、外側の(変数がiの)for文で0~9まで数えます。そして、内側の(変数がjの)for文で複数回パルスを繰り返し、時間をかけて明るくなったり暗くなったりするようにします。

肝はDelayの中身です。
HAL_Delay(i)はそのまま、iミリ秒待ちます。そして、HAL_Delay(10-i)は、10ミリ秒からiミリ秒経った後の残りの時間待つということをします。

こうすることで、10ミリ秒の内、i秒は点灯して残りは消灯するというような動きをします。

前半はだんだん明るく、後半はだんだん暗くしています。

これを書き込めば、10[ms] x 10[回] x 10[段階] x 2[前後半] = 2000ms で点滅を繰り返すことになります。

どうでしょう。いけましたかね。

Delayを使う方法の欠点

さて、Delayを使って電子蛍を作ることができました。
しかし、このコードには一つ重大な欠点があります。
それは、電子蛍を光らせている間、ほかのことが一切できないということです。
もし別のことをすると、時間比率が狂って正しく動かなくなります。

PWMはモーターの制御にも使います。
モーターを一つしか動かすことのできないプログラムは欠陥です。まともにロボットを動かすことはできないでしょう。

そこで別の方法をとることになります。

タイマーを使う方法

さて、Delayを使わない方法として、今回はタイマーを使ったやり方を紹介します。

CubeMX

まずCubeMXに移動し、PB3TIM2_CH2に変更します。
image.png

できたら、左のところからTimers > TIM2を選び、Channel2PWM Generation CH2に変更します。

image.png

そうすると、右のPB3ピンが緑になると思います。

次にタイマーの設定をします。
ConfigurationPrescaler39に、Counter Periodところを9にしてください。
image.png

設定出来たらGENERATE CODEしましょう!

Visual studio code

コードの生成が終わったらVSCodeに移動します。

Core/Src/main.cを開いて以下のコードを追記します。

USER CODE 2
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
USER CODE 3
for (int i = 0;i < 10;i++) {
  __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, i);
  HAL_Delay(100);
}
for (int i = 8;i > 0;i--) {
  __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, i);
  HAL_Delay(100);
}

それぞれ書く場所が違うので間違えないように注意してください。
また、USER CODE 3は最後に無限ループの}を忘れないように。

これを書き込むと、先ほどと同じように点滅を始めると思います。

何が変わったのか

一見、Delayを使った場合と変わらないように見えるかもしれません。
しかし、このコードは「Delayの値を変えてもPWMの明るさに影響がない」という特徴があります。

DelayでPWMを作っていたコードではDelayが必ず必要で、その値を変えると明るさにも影響があります。
一方タイマーを使った方では__HAL_TIM_SET_COMPAREという関数で明るさを設定した後は何もしなくてもその明るさで光り続けてくれます。

つまり、Duty比50%でずっと出力し続けてその間ほかのことしたいみたいな場合はタイマーを使った方が優れているわけです。

タイマーの解説

さて、このタイマーは何をしていたのでしょう。
ここからはタイマーについて少し解説しようと思います。

クロック

まず、マイコンは時間を扱うために基準となる一つのクロックをマイコン内部で生成しています。
タイマーはこの基準のクロックが何回来たかを数える仕事をしています。

では、クロックはどこからきているのでしょう。

Nucleo-F303K8が載せている石であるSTM32-F303K8T6のデータシートを見てみましょう。

このデータシートの13ページ目にクロックダイアグラムがあります。
image.png
TIM2はAPB1につながっていることがわかるでしょう。

続いて、CubeMXのClock Configurationタブを開くとこのようなものがあります。
image.png

これは、クロックがどのように流れているかを見ることができるものです。
左にクロックの源のHSI RCがあり、右に先ほどTIM2がつながっていたAPB1であるABP1 Timer clocksがあります。

これを見ると、HSI RCからなんやかんやあってABP1 Timer clocksに 8MHz が来ていることがわかると思います。
(図のやつだとInput frequencyに赤線が引いてありますけど、誤植です。矢印を辿っていくとHSI RCに辿り着くと思います。)

つまり、APB1につながっているTIM2には8MHzのクロックが来ているということです。

カウンター

さて、ただそこに 8MHzのクロック源があってもどうしようもありません。

そこで、タイマーではカウンターを使ってクロックをカウントしています。
クロックが何回来たかを数えているということです。
そして、何回クロックが来たら何かをするみたいなことをすることでいろいろなことをします。

ここでは、Duty比25%のPWMを作る場合を考えます。

image.png

まず、8MHzのクロックが入ってきます。
ちょっと早いので、Prescalerを使って分周します。
Prescalerは(Prescaler+1)分の1にクロックを分周してくれます。
これでクロック速度を遅くします。

このPrescalerを通ったクロックを使ってタイマーはクロックを数えます。

カウンターはCounter periodでリセットされます。
カウンターは0から数えるので、(Counter period+1)分の1に分周されます。

そして、出力はCompare以下の時にON、Compare以上の時にOFFになります。

それぞれの使い方としては、Prescalerで周波数を調整、Counter periodで分解能を決めて、CompareでDuty比を決めるような感じです。

実際のプログラム

それでは、実際にやってみようで作ったプログラムの例を見てみましょう。

TIM2に来ているのは8MHzでした。
そして、TIM2のPrescaler39に、Counter Period9に設定しました。

計算方法は以下の通り

PWM周波数 = \frac{クロック源}{(Prescaler+1)(Counter Period+1)}

PrescalerCounter periodを+1しているのは、それぞれ0から数えるからですね。なので、実際に値を決めるときは欲しい値から-1する必要があるわけです。

TIM2の周波数は、

\frac{8,000,000}{(39+1)(9+1)}=20,000 [Hz]

で20kHzとなります。
そして、Counter period9なので、0~9の10段階になるわけです。

実際決めるときは分解能(Counter period)とPWM周波数をあらかじめ決め、そこから逆算することでPrescalerを求めるみたいなことをします。

Prescaler = \frac{クロック源}{(Counter Period+1)\times PWM周波数}-1

この式使えばいい感じになると思います。

なんで20kHzか

なんでPWM周波数を20kHzにしてるかというと、電子蛍には関係ないのですが、DCモーターを回すときにモーターがPWMのパルスで振動して音が鳴るので人の可聴域である20kHzよりも高速で動かした方がいいというサークル内の暗黙のルールがあるからです。

ほんで、それ以外でPWMを使う時はたいてい速ければ周波数はいくらでも大丈夫です。

ただ逆に速すぎると、インピーダンスがどーのこーのとかMOSFETのサージがどーのこーのとかICの立ち上がり立下りがどーのこーのとか別の問題が出てくるので、あんまり速くしすぎるのもよくないです。
なのでとりあえず20kHz使ってるということなんですね。

こういう感じでタイマーの各種変数を決めていきます。

プログラム

さて、最後にプログラムの解説をしようと思います。
先ほどのプログラムを再掲します。

USER CODE 2
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);

まず、このUSER CODE 2は、while(1)でメインループに入る前の、Arduinoで言うsetup()の部分です。
そこでHAL_TIM_PWM_Start()を使うことで、TIM2のChannel2を動かし始めるわけです。
htim2がタイマー2を、TIM_CHANNEL_2がチャンネル2を表します。

続いてメインループの中を見てみましょう。

USER CODE 3
for (int i = 0;i < 10;i++) {
  __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, i);
  HAL_Delay(100);
}
for (int i = 8;i > 0;i--) {
  __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, i);
  HAL_Delay(100);
}

__HAL_TIM_SET_COMPARE()関数を使ってCompareの値を変更します。
これも同じようにhtim2がタイマー2を、TIM_CHANNEL_2がチャンネル2を表します。
そうするとONとOFFの閾値が変わってDuty比を変更することができます。

最小値は0、最大値はCounter periodです。
for文を使って0~9の範囲で回しています。
上のループでiが0から9まで上がり、下のループで8から1まで下がることで点滅しています。

それだけ。

お疲れ様です

ちょっと長くなってしまいました。
PWMの使い方ついでにタイマーについても解説したので、だいぶボリューミーでしたね。
たぶんいつかもうちょっとまとめようかな。

一応、今後タイマーまとめ回みたいなのを作る予定なので、その時に。

次回はサーボモーターの制御について書きたいと思います。
サーボはパルス幅の時間を決めないといけないのでしっかり計算することになりますが、まあ、ここで解説した計算式がわかれば行けると思います。

そんな感じ。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?