STM32でタイマを使ってみる

出典: Wikimura

パフォーマンスラインのMedium densityであるSTM32F103RBT6では、ウォッチドッグを除くタイマモジュールが4基搭載されています。内訳は高機能タイマモジュールTIM1、汎用タイマモジュールTIM2~TIM4となっています。

とりあえずタイマで割り込みをかけることを目指します。このページではその経過を記録します。


目次

資料を読む

ペリフェラルを操作するときは、レジスタを直接いじるか、標準ペリフェラルライブラリの関数を使うかという選択肢があります。 ここで問題なのは、機能の詳細説明はリファレンスマニュアル(RM0008)に載っているが、ペリフェラルライブラリのどの関数に対応しているか分からないということです。

マイコン側のリファレンスを読んでどんな機能があるか、どんな単語が出てくるかを把握していないと、関数側の説明がさっぱり読めない。 どちらも読まないといけないのに量があまりに膨大...最低限どの関数を使えれば動かせるのかさっぱり分かりません。

タイマの基本設定

資料を読んだところ、タイマモジュールはカウントを行う本体と、コンペア外部信号をカウントする「Input Capture」、カウント値とコンペアレジスタ値の比較から出力を生成する「Output Compare」からなるようです。Capture/Compareは4チャネルあり、それぞれ独立しているそうです。

タイマを構成する各パートは、個別に設定できるよう関数名や構造体名が以下のように分けられていることが分かりました。(xはチャネル番号1~4)

  • タイマ本体: TimeBase
  • Input capture: IC
  • Output compare: OC

具体例

  • TIM_TimeBaseInit(): タイマの本体初期化
  • TIM_ICInit(): タイマのInput capture初期化
  • TIM_OCxInit(): タイマのOutput compare チャネルx初期化

ただ、なぜOutput compareはチャネル番号指定があるのか...理由は分かりませんでした。ICだからOCだからという訳ではないようです。もしかすると、レジスタ構成によって異なるのかもしれません。ハードウェアの実装上、速度のロスをなくすために工夫されているのかもしれません。

命名規則

次に命名規則に注目しました。他のペリフェラルと共通なのがいくつかあるようです。

  • PPPはペリフェラルの略称を表し、ADC等になる
  • システムおよびソース・ヘッダファイル名には接頭辞stm32f10x_が付く
  • 単一ファイル内で使われる定数はそのファイル内で定義される。複数のファイルで使われる定数はヘッダファイルで定義される。すべての定数は大文字で書かれる。
  • レジスタは定数として扱われる。名前は大文字で書かれる。多くの場合、STM32F10xリファレンスマニュアルで使われている略称が使われる。
  • ペリフェラル関数名は、ペリフェラルの略称とアンダースコアを接頭辞に持つ。各単語は大文字から始まる(例:USART_SendData)。アンダースコアはペリフェラル名と残りの部分を区切るためだけに使われる。
  • 指定したパラメータPPP_InitTypeDefに従ってペリフェラルPPPを初期化する関数はPPP_Initである。(例:TIM_Init)
  • ペリフェラルPPPのレジスタをデフォルト値に戻す関数はPPP_DeInitである。(例:TIM_DeInit)
  • PPP_InitTypeDef構造体にリセット値を埋める関数はPPP_StructInitである。(例:USART_StructInit)
  • 指定したペリフェラルPPPを動作・停止するための関数はPPP_Cmdである。(例:USART_Cmd)
  • 指定したペリフェラルPPPの割り込みソースを有効化・無効化する関数はPPP_ITConfigである。(例:RCC_ITConfig)
  • ペリフェラルPPPのDMAを有効化・無効化する関数はPPP_DMAConfigである。(例:TIM_DMAConfig)
  • ペリフェラルPPPを設定する関数は常にConfigが付く。(例:GPIO_PinRemapConfig)
  • ペリフェラルPPPの持つフラグがセットされているかを調べる関数はPPP_FlagStatusである。(例:I2C_GetFlagStatus)
  • ペリフェラルPPPのフラグをクリアする関数はPPP_ClearFlagである。(例:I2C_ClearFlag)
  • 指定したペリフェラルPPPの割り込みが発生したかを調べる関数はPPP_GetITStatusである。(例:I2C_GetITStatus)
  • ペリフェラルPPPの割り込み保留(Pending)ビットをクリアする関数はPPP_ClearITPendingBitである。(例:I2C_ClearITPendingBit)

LED点滅をタイマで行う

タイマ割り込みの初歩といえば、LED点滅を割り込みで行うというもの。 全ての機能の網羅は無理でも、やりたいことから必要な機能を探していけば何とかなりそうなので...実際に作ってみることから始めます。一定周期を作るだけなら、オーバーフロー発生をポーリングで監視するというのもありますが、いきなり割り込みに挑戦します。

LED点滅を行うため必要な設定(GPIO以外)

  • タイマの初期化
  • プリスケーラでクロック設定
  • 周期の設定
  • オーバーフローで割り込み発生許可
  • コア側での割り込み許可設定
  • 割り込みフラグクリア


TimeBaseプロジェクトをお手本にする

ペリフェラルライブラリと一緒にサンプルプロジェクトが付いてきます。 TIMディレクトリにあるTimeBaseプロジェクトは、タイマ本体(TimeBase)とOutput Compareを利用して、4種類の波形を出力するサンプルのようです。

タイマと割り込みの基本設定が行われていたのでそれを参考にします。

タイマモジュールクロック有効化

まずはクロックを有効化しないといけないので以下を追加しました。

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);


割り込み設定

Cortexコア本体に搭載されたNVIC( Nested Vector Interrupt Controller)を操作するために、misc.hで宣言されている関数や構造体を使うようです。

misc.hという名前なのは、これがペリフェラルの機能ではなくCortex-M3コアの機能を関数化しているからのようです。

void NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;
    /* Enable the TIM2 gloabal Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

    NVIC_Init(&NVIC_InitStructure);
}

意味:

  • NVIC初期化構造体を用意する
  • 設定対象を TIM2_IRQn にする
    • ORする(パイプ記号でつなぐ)と、複数の割り込み要因について同時に設定可能
  • 割り込みの優先度を設定(2種類)
  • 割り込み要因を有効化
  • 設定を反映

今回はこれをそのまま使わせてもらいました。


タイマ設定

ここでは、タイマ本体、プリスケーラ、Output Compare、割り込み要因の設定がされていました。(Output Compareについては省略します)

TIM_TimeBaseStructure.TIM_Period = 65535;
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

/* Prescaler configuration */
TIM_PrescalerConfig(TIM2, 4, TIM_PSCReloadMode_Immediate);

/* TIM IT enable */
TIM_ITConfig(TIM2, TIM_IT_CC1 | TIM_IT_CC2 | TIM_IT_CC3 | TIM_IT_CC4, ENABLE);

/* TIM2 enable counter */
TIM_Cmd(TIM2, ENABLE);
  • 初期化構造体の設定
    • 周期(カウント値の上限)を65535に設定
    • プリスケーラレジスタのカウント上限を0(分周しない)
      • 後で4に設定している
    • クロック分周を 0 に...TIM_CKD_DIV1のこと
      • TIM_CKD_DIV1 -> 0x0000
      • TIM_CKD_DIV2
      • TIM_CKD_DIV4
    • カウントアップモードで動作
  • 設定を反映
  • プリスケーラの分周比を4に設定
  • TIM2個別割り込みTIM_IT_CC1..4要因を許可
  • TIM2を動作


これを見本に、以下のタイマ設定関数を作りました。

void TIM_Configuration( void)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    // 72MHz -> 18MHz -> 1kHz -> 2Hz ?
    TIM_TimeBaseStructure.TIM_Period = 500;
    TIM_TimeBaseStructure.TIM_Prescaler = 18000;
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV4;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    
    // 設定を反映
    TIM_TimeBaseInit( TIM2, &TIM_TimeBaseStructure);

    // 割り込み許可...割り込み要因のORを引数に入れる
    TIM_ITConfig( TIM2, TIM_IT_Update, ENABLE);

    // タイマ2起動
    TIM_Cmd( TIM2, ENABLE);
}

有効な設定値

TIM_ClockDivisionに入れられる値は以下の3種類だけです。誤って 10(0x000A) を入れたところ、余計なビットを操作してしまったためか、動作しませんでした。

  • TIM_CKD_DIV1: 0x0000
  • TIM_CKD_DIV2: 0x0100
  • TIM_CKD_DIV4: 0x0200

初めのうちは実行時チェックを有効化した方が良いかもしれません。実行時チェックとは、ペリフェラル関数の引数が不正な値をとった時、assert_fail関数へジャンプするチェック機能を付けてくれるというものです。assert_failでは、エラーの起きた行と関数を返すようになっているので、シリアル通信などでミスした個所を追跡できるはずです。


使用するクロック

リファレンスマニュアル(RM0008)のRCC(Reset Clock Control)の説明の個所で、タイマモジュールへ入力されるクロックがどうなっているかが分かります。TIM1~4を搭載しているMedium-densityデバイスの場合、TIM1専用のクロックTIM1CLKと、TIM2~4用のクロックTIMXCLKがあります。

これらについては「STM32のクロック」を参照してください。


割り込みハンドラ

割り込みハンドラをstm32f10x_it.hで宣言し、stm32f10x_it.cに記述しました。割り込みがあるたびにLEDをON/OFFするだけの簡単な処理です。

stm32f10x_it.hにて:
    void TIM2_IRQHandler(void);

stm32f10x_it.cにて:
volatile static unsigned char led= 0;
void TIM2_IRQHandler(void)
{
    // グローバル割り込みから個別要因へジャンプするためにフラグチェックする
    // 今回は要因が1つしかないけど、通常はこういうスタイルで分岐する
    if( TIM_GetITStatus( TIM2, TIM_IT_Update) != RESET)
    {
        // 割り込み保留ビット(=割り込み要因フラグ)をクリア
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);

        // 今回は簡単なので直に書く。ここから関数へジャンプするのもあり。
        if( led)
        {
            GPIO_ResetBits(GPIOC, GPIO_Pin_12);
            led = 0;
        }else{
            GPIO_SetBits(GPIOC, GPIO_Pin_12);
            led = 1;
        }
    }
}


分周比を決定する

STM32のクロック」に書いたように、8MHz外部発振子(器)を接続し、 system_stm32f10x.cでマクロSYSCLK_FREQ_72MHzを定義するとSYSCLKが72MHzになります。 このときの各部のクロックは下図のようになります。

各部のクロック

Olimex社のSTM32-P103、STM32-H103ボードでは、変更を加えない場合この設定になります。 従って、TIM2に入力されるクロックは72MHzとなることが分かります。 これを分周することで所望の周波数を作り出します。 タイマへ入力されるクロックはTIMXCLKと書かれていますが、なぜかCK_INTとも書かれています。

分周比の計算式

プリスケーラ周波数fPSCは、プリスケーラへ入力されるクロック周波数を表します。 ここでは内部クロックfINTをプリスケーラへ入力するので、fPSC = fINT = 72[MHz]です。

カウント周波数周波数fCNTは、プリスケーラ周波数をfPSCとし、 プリスケーラレジスタPSC[15:0]の値をNPSCとすると、次式のようになります。


f_{CNT} = \frac{f_{PSC}}{N_{PSC} + 1}

カウンタの上限値(自動再ロードレジスタARR[15:0]の値)をNARRとすると、割り込み周波数fIRQは次式で表わされます。


f_{IRQ} = \frac{f_{CNT}}{N_{ARR} + 1} = \frac{f_{PSC}}{ (N_{ARR} + 1)(N_{PSC} + 1)}

例えば、LEDを1Hzで点滅させたい場合は、割り込み周波数が2Hzである必要があります。そして割り込みのたびに点灯/消灯を行います。ここでは、内部クロック72MHzからカウント周波数1kHzを作り、そこから2Hzを作ることにします。この場合、以下のように設定します。

    // 72MHz -> 1kHz -> 2Hz
    TIM_TimeBaseStructure.TIM_Period = 4999;
    TIM_TimeBaseStructure.TIM_Prescaler = 7199;
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

    TIM_TimeBaseInit( TIM2, &TIM_TimeBaseStructure);

これでマイコンに書き込んで試したところ、1Hz...ぽくなりました。たぶん1Hzです。

失敗した原因

実は、1Hzで点滅させようとしてかなり速い点滅になるという問題にあってしまいました。

    // 72MHz -> 18MHz -> 1kHz -> 2Hz にならない...
    TIM_TimeBaseStructure.TIM_Period = 500;
    TIM_TimeBaseStructure.TIM_Prescaler = 18000;
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV4;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;

その原因を調べたところ、タイマ初期化構造体のメンバで、クロック分周を設定するTIM_ClockDivisionの意味を間違えていたようです。

このメンバは内部クロックがプリスケーラに入る前の分周比を設定するわけではありませんでした。 正しくは、インプットキャプチャのクロック検出部についているプリスケーラでした。

TIM_ClockDivisionの意味は、インプットキャプチャでクロックを検出するためのサンプリング周波数fDTSとタイマの内部クロックCK_INTの比とのことです。以下はTIM_TimeBaseInit関数でTIM_ClockDivisionに関する記述です。

TIMx->CR1 |= (uint32_t)TIM_TimeBaseInitStruct->TIM_ClockDivision | TIM_TimeBaseInitStruct->TIM_CounterMode;

これは、TIMx_CR1の8-9ビットを設定しています。

  • TIM_CKD_DIV1: CK_INTでサンプリング
  • TIM_CKD_DIV2: 2 * CK_INTでサンプリング
  • TIM_CKD_DIV4: 4 * CK_INTでサンプリング


Eclipseプロジェクト

Olimex社のSTM32-P103およびSTM32-H103ボード上で動作するLED点滅プログラムです。 とりあえず動いたのでEclipseプロジェクトごと置いておきます。 ELF形式とS-Record形式のバイナリが入っています。


ちなみに、ARM-JTAGケーブルがなくても、STMicroelectronics社の提供する書き込みプログラムを使って、 シリアルで書き込む事ができるそうです。 しかし、書き込みできるのはS-Record形式か、ROMイメージ(Intel HEX)形式だけだそうです。

ビルドし直す度にELF形式からの変換をコマンドプロンプトからやるのは面倒なので、以下のように変換コマンドをPost build stepに設定し、自動化すると楽です。

arm-none-eabi-objcopy -O srec ${BuildArtifactFileName} ${BuildArtifactFileBaseName}.mot

コメント

  • いつの間にかSTM32のペリフェラルライブラリに付属するスタートアップファイルがC言語からアセンブリ言語になっていました。[.s]のままだと自動でアセンブルされないらしく、起動しなくなってしまいました。アセンブラの入力ファイルの拡張子は何がデフォルトなのでしょうか。
    • Eclipse全体のプレファレンスから、C/C++のファイルタイプ一覧を見ると、[*.S]がアセンブリ言語として認識されるという設定がありました。試しにスタートアップファイルの拡張子を[.s]から[.S]にしたところ、自動で認識されるようになりました。
    • [*.s]もアセンブリソースファイルとして認識させようとしたのですが、「すでに登録されている」と言われてだめでした。
  • タイマ初期化構造体のプリスケーラ設定メンバはとり得る値が決まっています。これ以外の値を入れると動かなくなることもあるようです。これで悩まされました。
    • 実行時チェックを有効にすれば、変な値を書き込んだ際にassert_faultへ飛ぶようになります。USARTなどを使ってエラー報告するようにすれば良いかもしれません。
  • ビルド変数(環境変数みたいなもの)を使うと、柔軟にファイルやディレクトリの指定ができます。ビルド変数はプロジェクトプロパティの[C/C++ Build]->[Build Variables]から確認できます。[Show system variable]にチェックを入れれば、選択したコンフィギュレーションでの変数を確認できます。

参考文献

  1. STM32ファミリ紹介ページ
  2. STM32関係ファイル
  3. STM32シリーズリファレンスマニュアルRM0008
    • RCC(Reset Clock Controller)
    • TIMx(General Timer)
  4. STM32F10x Standard Peripheral Library(3.1.0)
  5. STM32のクロック
個人用ツール