STM32のPWMでduty比を自在に動かす——サーボモーターを例に、つまずきどころを順番に
STM32でPWMを出すのは、慣れてしまえば数行で済みます。でも自分が最初につまずいたのは、コードの書き方ではありませんでした。
Prescaler・ARR・CCR という3つの値が、周波数とduty比にどう効いてくるのか。その前提の仕組みが分からないまま、CubeMXの数字をなんとなく書き換えていたんです。調べてもピンポイントの解説がなかなか出てこなくて、3時間ほど迷子になりました。最後は人に教えてもらって、ようやく腑に落ちた感じです。
この記事は、そのとき自分が一番ほしかった「前提の地図」を、最初にまとめたものです。後半では、実際にラジコン用のサーボモーターを動かすところまでやります。コードに入る前に、まず仕組みの話からゆっくり始めます。
環境:STM32CubeIDE 1.19.0 / HALドライバ / Nucleo-L432KC(STM32L432KCUx)。タイマは TIM1、PWMはチャンネル1(ピンは PA8)。最終的にサーボを 50Hz で動かします。
1. duty比は、どうやって決まるのか
ここを先に押さえておくと、あとがずっと楽になります。急がず仕組みだけ見ていきましょう。
PWMはタイマのカウンタを使って作ります。関わるレジスタは、まず2つだけ覚えれば大丈夫です。
- ARR(Auto-Reload Register) … カウンタがここまで数えたら0に戻ります。周期を決める値です。CubeMXでは "Counter Period" という名前で出てきます。
- CCR(Capture/Compare Register) … カウンタがこの値を超えたかどうかで、出力のON/OFFが切り替わります。duty比を決める値です。
duty比は、この2つの比で決まります。
duty比 = CCR / (ARR + 1)
たとえば ARR = 999 なら、カウンタは 0〜999 の1000カウントで1周期です。ここで CCR = 500 にすると、duty比は 500 / 1000 = 50% になります。
周期の幅(=PWM周波数)のほうは、PSC(Prescaler)とARRの組み合わせで決まります。
PWM周波数 = タイマクロック / ((PSC + 1) × (ARR + 1))
自分のボードの実例で確かめてみる
抽象的な式だけだと分かりにくいので、実際の設定で計算してみます。今回はサーボを動かすため、クロックを最大の 80MHz まで上げました。TIM1 がぶら下がっている APB2 のタイマクロックも 80MHz になっています。
サーボのPWMは「周期20ms(=50Hz)」と決まっています。この50Hzを、上の式から逆算して作ります。
ここでひと工夫します。PSC = 79(=80分周)にすると、
80,000,000 ÷ (79 + 1) = 1,000,000 Hz
となり、カウンタが 1µs ごとに1進む状態になります。この「1µs刻み」を作っておくのが、サーボ制御を一気に楽にするコツです(理由は後半で分かります)。
あとは周期を 20ms にするため、20000カウント分を ARR に入れます。
ARR + 1 = 20000 → ARR = 19999
PWM周波数 = 1,000,000 ÷ 20000 = 50 Hz(ぴったり)
ここで大事なのは、周波数(ARR・PSC)と duty比(CCR)は、それぞれ独立して決められるということです。周波数を 50Hz に固定したまま、CCRだけ動かせば、サーボの角度(パルス幅)だけが変わります。
自分が3時間つまずいたのは、まさにこの「どのレジスタが何を決めているのか」の対応が頭になかったからでした。逆に、ここさえ掴めれば、このあとのコードはぐっと気楽になります。
細かい補足:
ARR = 20000と入れると周期は(20000+1)×1µs = 20.001ms(約49.9975Hz)になります。サーボにとっては誤差の範囲で問題なく動きますが、ぴったり50HzにしたいならARR = 19999です。
2. CubeMX側の設定(つまずきポイント①)
コードを書く前に、CubeMX側でPWMを有効にしておく必要があります。ここを飛ばすと「コードは合っているはずなのに何も出ない」という状態になりがちです。
まずピンの割り当てです。今回は PA8 を TIM1_CH1 として使っています。
次に、TIM1 の設定です。手順はこうなります。
- Timers から TIM1 を選ぶ
- Channel1 を「PWM Generation CH1」に設定する(ここを忘れやすいです)
- Parameter Settings の Counter Settings で値を入れる
-
Prescaler (PSC) …
80-1(=79)と式のまま入力できます -
Counter Period (ARR) …
19999(ぴったり50Hzにする場合)
-
Prescaler (PSC) …
ここで入れた PSC と ARR が、そのまま周波数を決めます。クロックの大元が何MHzなのかは、さっきの Clock Configuration タブで確認できます。ここを一度ちゃんと見ておくと、周波数の計算で迷わなくなります。
補足:TIM1 は「アドバンスドタイマ」と呼ばれる高機能なタイマですが、HALの
HAL_TIM_PWM_Start()を使えば内部で必要な処理(メイン出力の有効化など)をやってくれるので、特別な追加コードなしでPWMを出せます。
3. コードでPWMをスタートする(つまずきポイント②)
CubeMXがコードを生成したら、main() の初期化のあと、ループに入る前にPWMを開始します。
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
実は、この1行を呼び忘れて「波形が出ない」と悩むのが、いちばん多いつまずき方かもしれません。CubeMXは設定はしてくれますが、スタートのコードまでは書いてくれないので、ここは自分で1行足してあげる必要があります。
4. duty比(パルス幅)を動かしながら変える(つまずきポイント③)
ここが本題です。動作中にduty比を変えたいとき、CCRに直接代入する書き方は環境によってうまくいかないことがあるので、専用のマクロを使うのが安心です。
// △ 直接代入(環境によっては動かないことがある)
htim1.Instance->CCR1 = 1500;
// ◎ こちらを使うのがおすすめ
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 1500);
ここで、セクション1で作った「1µs刻み」が効いてきます。カウンタが1µsで1進むので、CCRに入れた値が、そのままパルス幅(µs)になります。サーボなら、この数字がほぼそのまま角度に対応します。
5. 「duty比100%にできない」問題(地味だけど引っかかる)
duty比を100%(出力をずっとON)にしたいのに、CCR = ARR だと一瞬だけOFFが混じってしまう、という現象があります。
理由は、出力が切り替わるのが「カウンタがCCRを超えたとき」だからです。CCR = ARR だと、カウンタが最大値に達する瞬間だけ、比較がOFF側に振れてしまいます。完全にONにしたいときは、ARR より大きい値を入れます。逆に完全OFF(0%)は CCR = 0 です。
サーボ制御ではこの端っこまで使うことはほぼありませんが、LEDの調光やモーター制御で「最大まで振り切らない」という地味なバグの原因になるので、頭の片隅に置いておくと安心です。
6. 実践:サーボモーターを動かす
ここまでの知識を全部使って、実際にサーボを動かします。設定はこうでした。
- クロック 80MHz、
PSC = 79→ カウンタ 1µs 刻み -
ARR = 19999→ 周期 20ms(50Hz) - → CCRに入れた値 = パルス幅(µs)
サーボは、このパルス幅で角度が決まります。標準的な値はこうです。
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 1500); // 1.5ms → 中央
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 1000); // 1.0ms → 片側いっぱい
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 2000); // 2.0ms → 反対側いっぱい
1500 を入れたら中央、と覚えるだけでいいので、とても直感的です。
角度(0〜180度)を指定したいなら、こんな小さな変換関数を作っておくと便利です。
// 0〜180度 を 1000〜2000µs に変換
uint16_t angle_to_us(uint8_t deg) {
return 1000 + (uint16_t)((uint32_t)deg * 1000 / 180);
}
// 使用例:90度(中央)に動かす
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, angle_to_us(90));
端から端へゆっくり往復させる(スイープ)なら、こうです。
for (uint16_t us = 1000; us <= 2000; us += 10) {
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, us);
HAL_Delay(20);
}
注意:パルス幅 1.0〜2.0ms はあくまで標準値です。サーボによっては 0.5〜2.5ms まで動くもの(可動範囲が広いタイプ)もあります。範囲を超えると無理に動こうとして唸ったり、最悪壊れたりするので、まずは狭めの 1.0〜2.0ms から試して、ご自分のサーボのデータシートで対応範囲を確認してください。
ちなみにduty比で見ると、中央(1.5ms)は 1500 / 20000 = 7.5% と、とても狭い範囲しか使いません。LEDの調光(0〜100%を広く使う)とは数字の感覚がまったく違うのが、サーボPWMの面白いところです。
まとめ
- 周期は ARR、duty比は CCR、
duty比 = CCR / (ARR+1) - 周波数は
タイマクロック / ((PSC+1) × (ARR+1)) - サーボなら、80MHz・
PSC=79で 1µs刻みを作り、ARR=19999で 50Hz。CCR = パルス幅(µs) になる - CubeMXで Channel を PWM Generation にするのを忘れずに
-
HAL_TIM_PWM_Start()を呼ばないと波形は出ない - duty比(パルス幅)を動かすときは
__HAL_TIM_SET_COMPARE()を使う
仕組みさえ掴めれば、PWMはぐっと扱いやすくなります。最初の自分のように「どの値が何を決めているのか分からない」状態の人に、この記事が地図になればうれしいです。
※この記事のコード・計算式は STM32L432KC + 標準的なHALドライバを前提にしています。HALのバージョンやボード、サーボの個体によって細部が異なる場合があるので、最終的にはご自分の環境で動作を確認してから使ってください。


