STM32マイコンのベアメタルRUST(その2)【Lチカ点滅】


 前の記事で、最低限のLED点灯をしてみました。
 流石に、Lチカと書いたら、点滅まではさせておきたいので、続きとします。
 今回の目標は、

  • タイマの使用(割り込みなし)
  • システムクロックの変更

 の2点に絞ります。

タイマの使用

STM32F401のタイマ

 STM32F401には、結構盛り沢山のタイマがついています。全部で8本。仕様の異なる3タイプのタイマとなっており、TIM1に至っては、機能の概略を見ただけで目が眩みそう(笑)
 今回使いたいのは、一定の期間ごとにイベントを出してくれればそれだけでOKです。一番単純な、TIM11を使用します。それでも、結構な機能がついているのですが・・・
 TIM11の構成図をリファレンスマニュアルから抜粋します。
 
 今回は、図の一番下の機能は使いません。
 トリガコントローラーは、単純にシステムクロックを持ってきてもらうだけに使います。これなら、設定はデフォルトです。
 さて、持ってきたクロックですが、今回のCPUのデフォルトでは、16MHzです。このあたりは後半でもう一度触れます。今は、天下りで。
 このクロックは、PSCプリスケーラで一旦分周します。そのうえで、1クロックに一回、CNTカウンタをインクリメントします。インクリメントされた値を、CNTカウンタの上にある自動リロードレジスタの値と比較し、値が同じになれば、イベントを発行すると同時にCNTカウンタがクリアされる。
 という挙動になります。
 設定としては、ほしい時間に1回イベントが発生するように、自動リロードレジスタの値を設定すればよいわけです。
 今回は、LEDを周期1秒。つまり、500ms毎にON・OFFを繰り返すように設定してみます。
 さて、CNTカウンタと、自動リロードレジスタのビット数は、16ビットです。つまり設定できる最大値は、65535です。一方、CNTカウンタは、一秒間に16M回、インクリメントされます。500msで、8M回です。最大値超えてます。
 そこで、プリスケーラでこのクロックを分周して、少し遅くしてあげることで、目標の値が設定できるようにします。このプリスケーラも16bitの値が設定できます。
 16MHzを16000分の1にすると、カウンタへの入力は、1KHz。つまり、1msに一回ととてもわかり易い値になります。
 したがって、プリスケーラの値を16000とし、自動リロードレジスタの値は、500とします。これで、500msになります。

タイマーの使い方

 タイマーの使い方、最低限の手順は、次のようになります。

  • TIM11へのクロックを有効化 (RCC_APB2ENRのtim11enビットをオン)
  • プリスケーラの分周比を設定 (TIM11_PSCの値を16000-1に設定)
  • 自動リロードレジスタの設定 (TIM11_ARRの値を500に設定)
  • タイマーを有効化しカウントスタート (TIM11_CR1のcenビットをオン)
  • カウントアップしたかを監視 (TIM11_SRのuifビットが1になったらカウントアップ)
  • カウントアップのフラグをクリア(TIM11_SRのuifビットをクリア)

プログラム

 プログラムの構成は、最小限にとどめます。uifビットをループ内で常時監視し、成立したら、LEDを反転します。
 LEDの反転は、現在のLEDの出力値を読み出し、これを反転して再度設定します。

 プログラムの完成形は、次の通り。

main.rs
#![no_std]
#![no_main]

// pick a panicking behavior
extern crate panic_halt; // you can put a breakpoint on `rust_begin_unwind` to catch panics
// extern crate panic_abort; // requires nightly
// extern crate panic_itm; // logs messages over ITM; requires ITM support
// extern crate panic_semihosting; // logs messages to the host stderr; requires a debugger

//use cortex_m::asm;
use cortex_m_rt::entry;
use stm32f4::stm32f401;
//use cortex_m::peripheral::{Peripherals, syst};

#[entry]
fn main() -> ! {

    // 初期化
    let peri = stm32f401::Peripherals::take().unwrap();

    // GPIO 電源ON
    peri.RCC.ahb1enr.modify(|_,w| w.gpiocen().enabled());
    // TIM11 電源ON
    peri.RCC.apb2enr.modify(|_,w| w.tim11en().enabled());

    // GPIOC セットアップ
    let gpioc = &peri.GPIOC;
    gpioc.moder.modify(|_,w| w.moder8().output());
    // TIM11 セットアップ
    let tim11 = &peri.TIM11;
    tim11.psc.modify(|_,w| w.psc().bits(16_000u16 - 1));   // 1ms
    tim11.arr.modify(|_,w| unsafe {w.arr().bits(500u16)}); // 500ms
    tim11.cr1.modify(|_,w| w.cen().enabled());

    //main loop
    gpioc.bsrr.write(|w| w.bs8().set());
    loop {
            if tim11.sr.read().uif().is_update_pending() { // カウントアップの確認
                tim11.sr.modify(|_,w| w.uif().clear()); // フラグクリア
                gpioc.odr.modify(|r,w| w.odr8().bit(r.odr8().is_low())); // LEDの反転
            }
    }
}

 先の手順をそのまま、愚直に設定していっているだけの内容です。
 レジスタへの値の書き込みは、ビット数をあえて明示しています。普通に書いても、bits関数の引数定義があるので自動で合わせられると思いますが、この方がわかりやすいでしょう。

 レジスタの読み出しに関する手順が新出でしょうか。カウントアップの確認のところです。
 tim11.sr.read()の関数は、リーダーオブジェクトをそのまま返してきます。そのため、素直に読み出し関数を連鎖で呼び出せます。読み出しのほうが、記述が素直ですね。
 modifyの効果は、LEDの反転をしているところあたりにでます。modify関数のクロージャには、リーダとライタの両方を渡してくれるので、LEDの現在値を読んで、そのまま反転した値を設定という手順が一気に出来ます。

 この状態で、コンパイル・実行をすると、LEDが1Hzで点滅します。

クロックの設定

STM32F401のクロック回路

 さて、先に、天下りでクロック周波数をポンと出しましたが、実は、このCPU、結構複雑なクロックネットワークを実装しています。
 リファレンスの「リセット及びクロック制御」の「クロック」の項を読めばよいわけですが、クロックネットワークのブロック図を引用します。

 と、こんな感じです。
 このうち、上1/3くらいは、リアルタイムクロックとウォッチドグタイマのために有ります。
 中程にある、「16MHz HSI RC」のところが、CPU内蔵のクロック発振器です。
 その下、「4-26MHz HSE OSC」は、外付けの水晶発振器でオプション指定となっています。今回私の使っているNucleo-64では、ST-LINKの回路に装備された8MHzのクロック回路を、ここに引っ張ってくることが可能なようになっています。
 内蔵といえど、まぁ、Lチカ程度の周期であれば、充分な精度ですから、今回はそのまま内蔵クロックを使用します。
 目標とするゴール地点、つまり、システムクロックは、右側の中ほど、黄色く塗られたところに有ります。
 最大クロック周波数は、84MHzと明記されています。
 ちなみに、タイマやGPIOへ供給されるクロックは、その下にあるブロックです。APBxペリフェラルクロックと、APBxタイマクロックと書かれています。図を見ればわかるように、このブロックへ送るクロックにも別途分周器がついています。つまり、システムクロックより遅い速度でペリフェラルの動作が可能になっています。デフォルトでは、システムクロックと同じに設定されています。

PLL回路

 さて、入力周波数は、84MHzには程遠いので、内蔵のPLLが装備されています。平たく言えば、PLL回路は、入力された周波数を増減するための回路です。
 PLLが左下に2つありますが、上のPLLがシステム用、下のPLLは、I2Sを使用するためのものです。
 個々の設定を行い、信号の選択を正しく行ってあげれば、PLLによって増加させたクロックをシステムクロックに供給することが出来ます。
 PLLは、入力したクロックを設定した倍数に増加させ、その後、分周器で割り算した周波数のクロックを出力するようになっています。
 この数字を自由に触れれば、どんな周波数にでもできるわけですが・・・いくつかの制約が有り、制約を守った上で逓倍数と分周比を決めてあげる必要が有ります。

今回の設定

 今回は、目標とするシステムクロックを48MHzとしてみます。
 16MHzのRCから、順番に、設定数値と、選択項目を決定します。
 PLLの前に、MUXがあります。ここは、内蔵クロックか外付けクロックの選択です。内蔵クロックとします。
 次の/Mのところですが、ここは、2以上の値が必要とされ、さらに、マニュアルに、VCOの入力周波数は、1MHz〜2MHz、できれば2MHzとせよと記載が有ります。スタートの周波数が16MHzですから、ここは、1/8とすることにします。Mは8です。
 次は、逓倍数です。ブロック図では、*Nと記載が有ります。ここは、VCOの出力周波数が192〜432MHzの間になるように決定せよとありますので、ここでは、*192とします。これで、VCOの出力周波数は、2*192=384MHzです。
 最後に、PLL内の/Mを決定します。384/8=48ですから、ここは、/8となります。
 そして、次に、ブロック図の真ん中にあるMUXの設定が必要です。個々の設定をPLLCLKに切り替えれば、システムクロックに、今作った48MHzクロックが供給されることになります。

落とし穴

 さて、以上の設定を、レジスタを調べて、割り当てていけば、システムクロックが変更できます。でも、これだけだと、ハードフォルトで落ちます。
 意外なところに、隠し玉が有ります。実は、これは、結構悩みました。
 リファレンスマニュアルでは、内蔵フラッシュインターフェースのところに答えが有ります。このマイコンのプログラムは、内蔵のフラッシュメモリーに格納されます。そして、この内蔵フラッシュから直接プログラムを読み、実行を行っています。フラッシュメモリーの読み出し速度は、遅いです。CPUの動作速度を上げると、読み出しが間に合わなくなります。先読みなどを駆使して、実質CPUと同じ速度で読み出せるようにアクセラレータ回路が実装されていますが、最悪の場合、CPUは、フラッシュメモリーの読み出し待ちでウェートする必要が有ります。このウェートの数値が、「CPU クロック周波数とフラッシュメモリ読出し時間との関係」に明記されていて、30〜60MHzのシステムクロックにするには、2CPUサイクル(1WS)のウェートを設定することとあります。ちなみに、64〜84MHzにするときには、3CPUサイクル(2WS)の設定が必要です。
 システムクロックを変更するには、システムクロック切り替え前に、この設定が必要となります。
 これを忘れると、システムクロックを変更した直後からとってもシステムが不安定になり、近いうちにシステムが必ず落ちるという極めていやらしいバグとなります。デバッガでステップ実行した日には、実行する度に違う挙動が見えるわ。(そりゃそうです。手動でウェートかけてるようなものです。)発見までずいぶん悩ませてもらいました。

クロック切り替え手順とレジスタ

 これで、全部の設定が整いました。手順を整理します。

  • PLLの入力をHSIに設定 (RCC_PLLCFGRのPLLSRCをHSI(0)に設定)
  • PLLのプリスケーラを/8に設定 (RCC_PLLCFGRのPLLMを8に設定)
  • PLLの逓倍器を*192に設定 (RCC_PLLCFGRのPLLNを192に設定)
  • PLLの出力分周器を/8に設定 (RCC_PLLCFGRのPLLPをdiv8(11)に設定)
  • PLLの出力を開始する (RCC_CRのPLLONをONに設定)
  • PLL出力の安定を待つ (RCC_CRのPLLRDYが、onになるまでループ)
  • フラッシュメモリーの遅延を1WSに設定 (FLASH_ACRのLATENCYを1に設定)
  • システムクロックの入力切替をPLLに設定 (RCC_CFGRのSWをpll(10)に設定)
  • システムクロックの切り替え完了を待つ (RCC_CFGRのSWSがpll(10)になるまでループ)

 微妙なフィールドが有るようで、いくつかの設定項目において、残念なunsafeが残ります。おそらく、ここでは引数の値のチェックはされていないと思われます。十分注意して値を設定しましょう。
 また、PLLの起動やシステムクロックの切り替えは、安定するまで少し時間がかかります。起動を確認できるまで、ループを回して待つことにします。

プログラム

 さて、これを適用した結果は、次のようになります。

src/main.rs
#![no_std]
#![no_main]

// pick a panicking behavior
extern crate panic_halt; // you can put a breakpoint on `rust_begin_unwind` to catch panics
// extern crate panic_abort; // requires nightly
// extern crate panic_itm; // logs messages over ITM; requires ITM support
// extern crate panic_semihosting; // logs messages to the host stderr; requires a debugger

//use cortex_m::asm;
use cortex_m_rt::entry;
use stm32f4::stm32f401;
//use cortex_m::peripheral::{Peripherals, syst};

#[entry]
fn main() -> ! {

    // 初期化
    let peri = stm32f401::Peripherals::take().unwrap();


    // システムクロック 48MHz
    // PLLCFGR設定
    // bit22: PLLSRC=hsi
    // bit17-16: PLLP=8
    // bit14-06: PLLN=192
    // bit05-00: PLLM=8
    {
        let pllcfgr = &peri.RCC.pllcfgr;
        pllcfgr.modify(|_,w| w.pllsrc().hsi());
        pllcfgr.modify(|_,w| w.pllp().div8());
        pllcfgr.modify(|_,w| unsafe { w.plln().bits(192u16) });
        pllcfgr.modify(|_,w| unsafe { w.pllm().bits(8u8) });
    }

    // PLL起動
    peri.RCC.cr.modify(|_,w| w.pllon().on());
    while peri.RCC.cr.read().pllrdy().is_not_ready() {
        // PLLの安定を待つ
    }

    // フラッシュ読み出し遅延の変更
    peri.FLASH.acr.modify(|_,w| unsafe {w.latency().bits(1u8)});
    // システムクロックをPLLに切り替え
    peri.RCC.cfgr.modify(|_,w| w.sw().pll());
    while !peri.RCC.cfgr.read().sws().is_pll() { 
        // システムクロックの切り替え完了を待つ
    }

    // GPIO 電源ON
    peri.RCC.ahb1enr.modify(|_,w| w.gpiocen().enabled());
    // TIM11 電源ON
    peri.RCC.apb2enr.modify(|_,w| w.tim11en().enabled());

    // GPIOC セットアップ
    let gpioc = &peri.GPIOC;
    gpioc.moder.modify(|_,w| w.moder8().output());
    // TIM11 セットアップ
    let tim11 = &peri.TIM11;
    tim11.psc.modify(|_,w| w.psc().bits(48_000u16 - 1));   // 1ms
    tim11.arr.modify(|_,w| unsafe {w.arr().bits(500u16)}); // 500ms
    tim11.cr1.modify(|_,w| w.cen().enabled());

    //main loop
    gpioc.bsrr.write(|w| w.bs8().set());
    loop {
            if tim11.sr.read().uif().is_update_pending() {
                tim11.sr.modify(|_,w| w.uif().clear());
                gpioc.odr.modify(|r,w| w.odr8().bit(r.odr8().is_low()));
            }
    }
}

 プログラム中、TIM11のプリスケーラの設定を、システムクロックの変更に伴い、48000に修正しています。これで、点滅速度は、やはり1Hzとなります。
 ここをもとの16000のままで実行すると、点滅速度が3倍になっているのが観察できます。つまり、クロック周波数が上がっていることが確認できるということです。

次にやりたいこと

 タイマーのカウントアップをループで監視するのは、やっぱり気に入りませんね。CPU全力でぶん回して電力使い放題ですし。次は、割り込みを調べてみることにします。
 ここまでは、完成した後で、このメモ書きましたが、この続きは、まだ遊んでないので、続編が出るかどうかは未定です。

参考文献

参考文献で、前回より増えているものは、なにもありません。
 

シリーズ目次