stm32plusを使う

stm32plusを使う

概要

Andy Brown氏によるSTM32向けC++ライブラリである stm32plus(andysworkshop / stm32plus) を使用する際のメモ.
便利なのだが,ザ・C++という感じのコードでC++にあまり馴染みがない人にとっては使いづらいと思うので,コードの解説も加えながらメモを残したい.

なお,最新の情報はstm32plusのGitHubレポジトリに掲載されているので,併せて確認してほしい.

セットアップ

stm32plusはスタティックライブラリとしてプロジェクトに組み込むことを想定しているため,事前にビルドが必要である.

下準備

確かWindowsだとコマンドプロンプトの仕様上8192文字以上のコマンドを受け付けてくれなくてビルドが失敗するので,CygwinやMingW32などを使用したほうが良いかもしれない.

ほかにもSConsと呼ばれるmake系のツールを使用するので,それを予めインストールしておく.
Ubuntuなら,

$ sudo apt-get install scons

でインストールできる.

また,Doxygenでドキュメントを生成できるようになっているため,

$ sudo apt-get install doxygen

しておくと便利.

レポジトリをclone

$ git clone https://github.com/andysworkshop/stm32plus.git
$ cd stm32plus

ビルド

$ scons mode=debug mcu=f4 hse=12000000 -j9

SConsに渡すオプションは以下の通り.

名称 説明
mode 最適化のかけ方を変える debug(デバッグ情報あり,最適化なし), fast(-O3に相当), small(-Osに相当)
mcu シリーズの選択 f1hd(STM32F103), f1cle(STM32F107), f1mdvl(STM32F100), f4(STM32F4XX)
hse 外部クロックの周波数(Hz) 数値(12MHzなら12000000)

上記の -j9 はコンパイルに使用するスレッド数.
どっかで((論理コアの数)+1)を指定すると良いという噂を聞いたので9にしてみた.

ビルド途中に”crt0.oがありません”と怒られる場合は,SConstructを編集

env.Replace(CXXFLAGS=["-Wextra","-Werror","-pedantic-errors","-fno-rtti","-std=gnu++0x","-fno-threadsafe-statics","-pipe"])
env.Append(CCFLAGS="-DHSE_VALUE="+hse)
-  env.Append(LINKFLAGS=["-Xlinker","--gc-sections","-mthumb","-g3","-gdwarf-2"])
+  env.Append(LINKFLAGS=["-Xlinker","--gc-sections","-mthumb","-g3","-gdwarf-2","-nostartfiles"])

# add on the MCU-specific definitions

ドキュメントの生成

$ cd lib
$ doxygen Doxyfile

すると,lib/build/doc/htmlの下にhtmlファイルが大量にできる.
index.htmlをブラウザで開けばドキュメントを見られる.

各機能

GPIO

まず最初にstm32plusを使用するにあたって最もベーシックなGPIOを取り上げる.
といってもベーシックでないように見えるのはC++の機能がふんだんに用いられているからである.(それによるメリットも後述する.)


LED点灯

はじめにLEDを点灯させるだけのコードを見てみよう.
このコードはstm32plusのexamples/blinkディレクトリにあるものに少し手を加えたものである.

#include "config/stm32plus.h"
#include "config/gpio.h"

using namespace stm32plus;

class Led {
 public:
   void on() {
     GpioC<DefaultDigitalOutputFeature<8> > pc;
     pc[8].reset();
   }
};

int main() {
 Led led;
 led.on();

 while(1);
}

PC8のピンをLOWにするという動作を行うコードである.
STM32VLDiscovery上で実行すると,右下にある青色のLEDが点灯する.

main関数の中では,Ledクラスの実体化と,on関数の呼び出しが行われている.

Ledクラスのon関数の内容を見ていく.

GpioC<DefaultDigitalOutputFeature<8> > pc;

この行では,PC8をデジタル出力のピンとして設定し,GPIOCを扱うためのインスタンスpcを生成している.
pc[8].reset();

そして,この行でPC8をLOWに設定している.


<バリエーション>

  • GpioCでなくGpioA
    GpioA<DefaultDigitalOutputFeature<8> > pa;
    pa[8].reset();
  • PA8でなくPA9
    GpioA<DefaultDigitalOutputFeature<9> > pa;
    pa[9].reset();
  • PA8も9も10も
    GpioA<DefaultDigitalOutputFeature<8,9,10> > pa;
    pa[8].reset();
    pa[9].reset();
    pa[10].reset();
    もしくは
    GpioA<DefaultDigitalOutputFeature<8,9,10> > pa;
    pa.reset((1<<8)|(1<<9)|(1<<10));

LED消灯

消灯する場合はresetをsetに書き換えれば良い.
だがそれだけでは芸がないのでC++風に書いてみる.

#include "config/stm32plus.h"
#include "config/gpio.h"

using namespace stm32plus;

class Led {
 public:
   void on() {
     pc[8].reset();
   }
   void off() {
     pc[8].set();
   }
 private:
   GpioC<DefaultDigitalOutputFeature<8> > pc;
};

int main() {
 Led led;
 led.on();
 for(volatile int i=0;i<60000;i++); // kuzu
 led.off();

 while(1);
}

Ledクラスのメンバ変数としてpcを宣言した.
これによってLedクラスがインスタンス化される時にGPIOCが初期化される.

ボタンによる入力

EXTIを用いたボタン入力の割り込み処理

Timing


SysTickによるLED点滅

最も単純に時間を得る方法としてSysTickを用いることが多い.
stm32plusではSysTick自体は隠蔽されていて,timingという区分でRTCなどと一緒に実装されている.

LEDを点滅させるいわゆる「Lチカ」を行うために,SysTickを利用したdelay関数を使用してみる.

#include "config/stm32plus.h"
#include "config/gpio.h"
#include "config/timing.h"

using namespace stm32plus;

class Led {
 public:
   void on() {
     pc[8].reset();
   }
   void off() {
     pc[8].set();
   }
   void blinkOnce() {
     on();
     MillisecondTimer::delay(1000);
     off();
     MillisecondTimer::delay(1000);
   }
 private:
   GpioC<DefaultDigitalOutputFeature<8> > pc;
};

int main() {
 MillisecondTimer::initialise();

 Led led;
 while(1){
   led.blinkOnce();
 }

 return 0;
}

まず

MillisecondTimer::initialise();

でミリ秒精度のSysTickタイマを設定し,
MillisecondTimer::delay(1000);

で1000msのディレイを作り出している.


<バリエーション>

  • 500msのディレイ
    MillisecondTimer::delay(500);

MicrosecondDelayを用いた高精度ディレイ

通常のタイマをマイクロ秒オーダーの高精度ディレイを実現するために使用する機能が実装されている.
MicrosecondDelayはデフォルトでTIM6を使用する設定になっているが,MicrosecondDelayTemplateを用いると,その他のタイマにこの機能を割り当てることができる.

#include "config/stm32plus.h"
#include "config/gpio.h"
#include "config/timing.h"

using namespace stm32plus;

class Led {
 public:
   void on() {
     pc[8].reset();
   }
   void off() {
     pc[8].set();
   }
   void blinkOnce() {
     on();
     MicrosecondDelay::delay(65000); // 65.000ミリ秒のディレイ
     off();
     MicrosecondDelay::delay(65000);
   }
 private:
   GpioC<DefaultDigitalOutputFeature<8> > pc;
};

int main() {
 MicrosecondDelay::initialise();

 Led led;
 while(1){
   led.blinkOnce();
 }

 return 0;
}

<バリエーション>

  • TIM7を使う
    MicrosecondDelayTemplate<Timer7<Timer7InternalClockFeature> >::initialise();

RTCを用いた時刻の取得

<デバイスないので未検証>

Timer

TimerはTimingとは異なりSTM32のタイマの機能をそのまま使うためのものである.
STM32のタイマの数と機能は非常に豊富かつ多彩で,それをオブジェクト指向的にかつ簡単に扱うことができるのもstm32plusの魅力である.


Timerを走らせるだけ

PWM出力的なのを手動で書いてみた.

#include "config/stm32plus.h"
#include "config/gpio.h"
#include "config/timer.h"

using namespace stm32plus;

class LedTimer {
 public:
   LedTimer(){
     timer.setTimeBaseByFrequency(5000, 6000,TIM_CounterMode_Up); // カウントの周波数5kHz,6000カウントでリセット,カウントアップ
     timer.enablePeripheral();
     tt=(TIM_TypeDef*)timer;
   }
   void on() {
     pc[8].reset();
   }
   void off() {
     pc[8].set();
   }
   void output() {
     if(tt->CNT<=1000){
       on();
     } else {
       off();
     }
   }
 private:
   GpioC<DefaultDigitalOutputFeature<8> > pc;
   Timer6<Timer6InternalClockFeature> timer;
   TIM_TypeDef *tt;
};

int main() {
 LedTimer ledtimer;
 while(1){
   ledtimer.output();
 }

 return 0;
}

変なことをやっているが,これはTIMx->CNTを取得するための苦肉の策である.

重要なコードは以下の2つである.

timer.setTimeBaseByFrequency(5000, 6000,TIM_CounterMode_Up);

これは,タイマをカウントアップモードにし,5kHz(1秒間に5000回)でカウントし,6000回カウントしたら0にリセットするという設定をするコードである.
StdPeriphLibでいうTIM_TimeBaseInit関数にあたる.

もうひとつはこのコードである.

timer.enablePeripheral();

これは,タイマを有効化する関数で,StdPeriphLibでいうTIM_InitとTIM_Cmd関数にあたる.


<バリエーション>

  • TIM7を使う
    Timer7<Timer7InternalClockFeature> timer;
  • 6000Hzでカウントアップする
    timer.setTimeBaseByFrequency(5000, 6000,TIM_CounterMode_Up);

コンペアマッチ出力

タイマの機能であるコンペアマッチ出力を使用して,カウンタがある値に等しくなったときにピンの出力を反転させるプログラムを書く.
STM32VLDiscoveryの青LED(PC8)は,ピン機能のリマップを使用するとTIM3のCH3に割り当てることができる.(データシート参照)

#include "config/stm32plus.h"
#include "config/gpio.h"
#include "config/timer.h"

using namespace stm32plus;

class LedTimer {
 public:
   LedTimer(){
     timer.setTimeBaseByFrequency(5000, 6000,TIM_CounterMode_Up); // カウントの周波数5kHz,6000カウントでリセット,カウントアップ
     timer.initCompare(1000); // (カウンタ==1000)で出力を反転
     timer.enablePeripheral();
   }
 private:
   GpioC<DefaultDigitalOutputFeature<8> > pc;
   Timer3<Timer3InternalClockFeature,TimerChannel3Feature,Timer3GpioFeature<TIMER_REMAP_FULL,TIM3_CH3_OUT> > timer;
};

int main() {
 LedTimer ledtimer;
 while(1){
   ;
 }

 return 0;
}

出力を反転させる処理はハードウェア的に行われるため,設定だけしてタイマを走らせれば勝手にLEDがチカチカする.

timer.initCompare(1000);

このコードによって,コンペアマッチに指定する値(と処理内容:デフォルトでは出力反転)を記述できる.


<バリエーション>

  • PWM出力
    PWM出力にすることも可能.
    timer.initCompare(1000,TIM_OCMode_PWM1);

PWMの出力

前項のバリエーションで記述した指定方法ではDuty比を変えるのが面倒である.
そこで,このような記法が用意されている.

timer.setTimeBaseByFrequency(50000, 100); // 50kHz,100カウント
timer.initCompareForPwmOutput(); // PWMを設定.初期Duty比0%
/* ...中略... */
// Duty比を設定したい場所で
timer.setDutyCycle(50); // Duty比50%


<バリエーション>

  • 初期Duty比を設定したい
    timer.initCompareForPwmOutput(30); // 初期Duty比30%
  • PWM2モードを使いたい
    timer.initCompareForPwmOutput(30,TIM_OCMode_PWM2); // 初期Duty比30%,PWM2モード
  • 百分率ではなくカウント数を直接指定したい
    timer.setCompare(54); // 54カウントでPWM

Timerによる割り込み

TimerInterruptFeatureをテンプレート引数に追加して,割り込みハンドラをバインド(紐付け)する処理を行うだけで,割り込みの設定も記述することができる.

#include "config/stm32plus.h"
#include "config/gpio.h"
#include "config/timer.h"

using namespace stm32plus;

class LedTimer {
 public:
   LedTimer(){
     timer.setTimeBaseByFrequency(5000, 6000);
     // 割り込みハンドラをバインド
     timer.TimerInterruptEventSender.insertSubscriber(
         TimerInterruptEventSourceSlot::bind(this, &LedTimer::onInterrupt)
     );
     timer.enableInterrupts(TIM_IT_Update); // オーバーフロー割り込み有効化
     timer.enablePeripheral();
   }

   // 割り込みハンドラ
   void onInterrupt(TimerEventType tet,uint8_t timerNumber){
     // オーバーフロー割り込みの場合
     if(tet==TimerEventType::EVENT_UPDATE){
       state=!state;
       pc[8].setState(state);
     }
   }
 private:
   GpioC<DefaultDigitalOutputFeature<8> > pc;
   Timer6<Timer6InternalClockFeature,Timer6InterruptFeature> timer;
   bool state=false;
};

int main() {
 Nvic::initialise(); // 割り込みを使用するためにNVICを初期化
 LedTimer ledtimer;
 while(1){
   ;
 }

 return 0;
}

ロータリエンコーダを読む