2
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?

More than 3 years have passed since last update.

100円マイコン、インターバルタイマーの使い方

Last updated at Posted at 2021-11-01

インターバルタイマーの使い方

r8c_rj2_block.png

R8C/M1x には3つのタイマーが内蔵されていますが、今回は、最も一般的なインターバルタイマーとしてRB2を説明します。

マイコンのプログラムは、「割り込み」を使うので、一般的なアプリケーションとは異なる部分があると思います。

「割り込み」は上手く利用すると、少ないコストで、大きな自由度と精妙な動きを創れるので、使い方を覚えれば重宝します。


タイマーは、割り込み動作と関連が大きく、平行動作的な機能を提供する事が出来ます。

RB2 を扱う場合、この C++ フレームワークでは、サポートクラスとして「trb_io.hpp」を用意してあります。

このクラスを使う事で、細かい設定を行う事なく、一般化された使い方が出来るようになります。

trb_io.hpp クラス

一定期間で割り込みを起動して、正確な時間間隔を作成します。

インターバルタイマー定義

	volatile uint16_t trb_count_;

	/// タイマー割り込みで実行する動作の定義
	class trb_intr_task {
	public:
		void operator() () {
			++trb_count_;
		}
	};

	typedef device::trb_io<trb_intr_task, uint8_t> TIMER_B;
	TIMER_B	timer_b_;
  • 「trb_io」はタイマーBを利用するテンプレートクラスです。
  • サンプルでは、割り込みに付随する処理を利用する場合として、外部処理を与える構成です。
  • 本来、単にカウンタを進めるだけであれば、外部処理は不要で、標準でサポートしていますが、外部関数を利用するサンプルとしています。
  • 外部処理を利用しない場合は、「trb_intr_task」の代わりに、「utils::null_task」を与えます。

このテンプレートで重要なのは、割り込み時に起動するタスクを外部から与える仕組みです。

通常、割り込み関数を実装して、その関数ポインターを割り込み関数に設定し、割り込み関数から起動するような実装が一般的です。

C++ では、「ファンクタ」と呼ばれる仕組みを使い、テンプレート関数にそのクラスの「実行型」を定義して行います。

こうすると、最適化が行われた場合に、呼び出しに関するコストが削減され、呼び出しコードが割り込み関数内に埋め込まれます。

もし、割り込み内の手続きを利用しない場合は、「utils::null_task」クラスを使います。

このクラスは中身の無い「枠」だけですが、最適化されると、余分な手続きは除外されて無くなります。

このような仕組みと手法は C++ ならではと思います。


ファンクタは、クラス内定義で、() オペレーターをオーバーロードして、呼び出し時の手順を実装します。

上記ソースコードの例では、「trb_count_」をインクリメントしています。

C++ の最適化では、「trb_count_」は、割り込み内と、メイン部で利用されますが、コンパイラには、その関連性を検出できません。
その為、最適化が行われると、必要なコードが消されてしまう場合があります、そこで「volatile」追加する事で、コンパイラの最適化を抑止します。
この辺りの工夫が、割り込み動作を考える上で重要となります。

インターバルタイマー割り込み設定

extern "C" {

	void TIMER_RB_intr()
	{
		timer_b_.itask();
	}

}

タイマーB の割り込み時、trb_io の割り込みサービスが呼ばれるように、上記の実装を行い、C API から見えるようにしておきます。
※「TIMER_RB_intr」は、タイマーB割り込みベクターに登録されています。(vect.h、vect.c)
タイマー RB の割り込みが発生すると、呼ばれます。

タイマー起動(初期化)

	// タイマーの設定
	{
		uint8_t intr_level = 1;
		uint16_t freq = 60; // 60Hz
		timer_b_.start(freq, intr_level);
	}
  • 割り込みレベルは「1」を使います。
  • 周期は60Hzです。

タイマーの同期

		timer_b_.sync();

タイマー割り込みは、メインの裏で定期的に実行されています。(この場合1秒に60回)

タイマー割り込みと同期を行うには、「sync()」関数を使います。

これで、メインプログラムは60Hzで同期して動く事になります。
※メインプログラムの処理時間が60Hz(16.6667ミリ秒)より長い場合、整数倍のファクターで動きます。
※30Hz(33.3334ミリ秒)、20Hz(50ミり秒)・・・

カウンターの利用

上記カウンター「trb_count_」は、1/60秒毎にカウントされますので、メイン部分で、時間定数として利用出来ます。


インターバルタイマの応用

タイマーの周期は正確なので、このタイマーに依存したプログラムにする事で、正確な時間間隔を利用した動作が行えます。

応用として、音楽のテンポ、アニメーションフレーム、など、色々な応用が考えられますが、機械式スイッチのチャタリング除去に応用した例を紹介します。

また、システム全体を、インターバルタイマー周期で駆動する「同期式」の様式も含まれています。

  • この方法は、ほぼ全てのサンプルで利用しています。
  • 「同期式」に対して、「非同期式」による方法もありますが、経験的に、細かくて複雑な機能を提供する場合は「同期式」の方がプログラム全体がシンプルになり簡単だと思えます。
  • ゲームなどのアプリケーションでは、ほぼ「同期式」が使われています。
  • 「同期式」では、「周期」より速く応答する事が出来なくなりますが、通常、それがデメリットにならないアプリケーションが多く、問題になりません。
  • 「周期」より速い応答が必要なら、別の方法で解決出来る場合が殆どです。(割り込みを使った、UART による文字の入出力など)

チャタリングの除去(機械式スイッチ編)

チャタリング.png

機械式スイッチをマイコンに接続した場合、スイッチで発生するチャタリングを除去する必要があります。

もっとも簡単な方法は、サンプリングを利用した方法です。

チャタリングは数ミリ秒程度なので、60Hzでスイッチの状態をサンプリング(16.67ミリ秒)する事で、それより周期の短い信号を除去する事が出来ます。
チャタリングは、数ミリ秒の期間に「1」、「0」が出現する(数キロHz程度)ので、実質、除去する事と同等になります。
また、人間の操作は、60Hzより、十分遅いので、除去されずに残ります。

チャタリングフィルタ.png

※PCに接続されるキーボードも、内蔵されたマイコンがこの方法を使って、チャタリングを除去しています。


ポートの定義

通常、ポート入力は、プルアップして使います。
ですが、その場合、スイッチをONした場合、ロジックレベルは「0」となります。
C++ フレームワークに含まれる、ポートテンプレートを使うとスマートに反転入力を定義出来ます。

	// スイッチの定義
	// 解放で(1)、ON したら、(0) なので、論理を反転している。
	typedef device::PORT<device::PORT1, device::bitpos::B0, false> SW0;
	typedef device::PORT<device::PORT1, device::bitpos::B1, false> SW1;

スイッチの状態をサンプリング

サンプリングで得た値を保存して論理演算する事で、「押した瞬間」、「離した瞬間」を生成しています。
※このテクニックは非常に応用範囲が広く、様々な場面で活用出来ますので良く理解しておく事をお勧めします。

	uint8_t inp_lvl_ = 0;
	uint8_t inp_pos_ = 0;
	uint8_t inp_neg_ = 0;

	void switch_service_()
	{
		uint8_t lvl = SW0::P() | (SW1::P() << 1);  ///< 状態の取得
		inp_pos_ = ~inp_lvl_ &  lvl;  ///< 立ち上がりエッジ検出(押した瞬間)
		inp_neg_ =  inp_lvl_ & ~lvl;  ///< 立ち下がりエッジ検出(離した瞬間)
		inp_lvl_ = lvl;  ///< 状態のセーブ
	}

メインループ

インターバルタイマーに同期して、スイッチの状態をサンプリングし、その結果により処理を行います。

	uint8_t cnt = 0;
	while(1) {
		timer_b_.sync();

		switch_service_();

		if((inp_pos_ & 0b01) != 0) {  // SW0 の押した瞬間
			sci_puts("SW0 - positive\n");
		}
		if((inp_pos_ & 0b10) != 0) {  // SW1 の押した瞬間
			sci_puts("SW1 - positive\n");
		}

		if((inp_neg_ & 0b01) != 0) {  // SW0 の離した瞬間
			sci_puts("SW0 - negative\n");
		}
		if((inp_neg_ & 0b10) != 0) {  // SW1 の離した瞬間
			sci_puts("SW1 - negative\n");
		}

		if((inp_lvl_ & 0b01) != 0) {  // SW0 の押している状態
			if((cnt % 30) == 0) {
				sci_puts("SW0 - ON\n");
			}
		}
		if((inp_lvl_ & 0b10) != 0) {  // SW1 の押している状態
			if((cnt % 30) == 0) {
				sci_puts("SW1 - ON\n");
			}
		}

		++cnt;
		if(cnt >= 60) cnt = 0;
	}

ターミナル表示:

Start R8C SWITCH sample
SW0 - positive
SW1 - positive
SW0 - negative
SW1 - negative
SW1 - positive
SW1 - ON
SW1 - ON
SW1 - ON
SW1 - negative
SW0 - positive
SW1 - positive
SW0 - ON
SW1 - ON
SW0 - ON
SW1 - ON
SW0 - ON
SW1 - ON
SW0 - ON
SW1 - ON
SW0 - negative
SW1 - negative
SW0 - positive
SW0 - ON

チャタリングの除去(ロータリーエンコーダー編)

家電などに良く使われるロータリーエンコーダーも内部は機械式接点を持っており、チャタリングがあり、除去する必要があります。

ロータリーエンコーダーは、2つの信号があり、位相が90度ずれています。
その為、時計回り(CW)、反時計回り(CCW)を検出出来ます。

ロータリーエンコーダー.png

  • マウスホイールなどにも使われています。
  • 光学式の場合は、通常チャタリングがありません。

ロータリーエンコーダーの場合、ON/OFF間隔が高速なので、先の例で使った60Hzでは、十分ではありません。

  • サンプリングが遅い場合、ノブを速く回した場合に、応答できずに、カウントミスをします。
  • チャタリングの発生時間を超えない程度に速くする必要があります。
  • このサンプルでは360Hzを採用しています。
  • ロータリーエンコーダーのチャタリング期間は、プルアップ抵抗値や、電源電圧により変化します。
  • どのくらいの周期が適切なのかは、ロータリーエンコーダーの仕様によります。

エンコーダーテンプレートを使った回転方向の検出

このフレームワークでは、ロータリーエンコーダー入力を簡単に扱えるようにテンプレートクラス chip/ENCODER.hppを用意してあります。

ポートの定義とエンコーダーテンプレートの型

	// エンコーダー入力の定義
	static const uint8_t TIMER_MULTI_NUM = 6;
	typedef device::PORT<device::PORT1, device::bitpos::B0> PHA;
	typedef device::PORT<device::PORT1, device::bitpos::B1> PHB;
	// パラメーターを指定しない場合、DECODE::PHA_POS: A 相の立ち上がりのみでカウントとなる。
	typedef chip::ENCODER<PHA, PHB, uint16_t> ENCODER;
	ENCODER	encoder_;

インターバルタイマーへ組み込む

trb_io テンプレートクラスは、割り込み時のタスク実行を、ファンクタで行います。
ENCODER テンプレートクラスは、この仕様に合わせて、ファンクタを定義してあります。

ファンクタの型として、「ENCODER」を定義しています。

	class timer_t {
		uint8_t		multi_;
		volatile uint8_t	count_;
	public:
		timer_t() : multi_(0), count_(0) { }

		void sync60()
		{
			auto tmp = count_;
			while(tmp == count_) ;
		}

		void operator () ()
		{
			encoder_();

			++multi_;
			if(multi_ >= TIMER_MULTI_NUM) {
				++count_;
				multi_ = 0;
			}
		}
	};

	typedef device::trb_io<timer_t, uint8_t> TIMER_B;
	TIMER_B timer_b_;
  • エンコーダーのサンプリングは、360Hz なので、メインループ同期用に 60Hz を生成しています。

初期化

	// エンコーダー関係の初期化
	{
		encoder_.start();
	}

	// タイマーB初期化
	{
		uint8_t ir_level = 2;
		timer_b_.start(60 * TIMER_MULTI_NUM, ir_level);
	}

エンコーダー値の取得

	uint16_t value = encoder_.get_count();
	while(1) {
		// メインループは 60Hz で動かす
		timer_b_.task_.sync60();

		auto count = encoder_.get_count();
		if(count != value) {
			value = count;
			utils::format("%05d\n") % value;
		}
	}

ターミナル表示:

Start R8C ENCODER sample
65535
00000
00001
00002
00003
00004
00005
00004
00003
00002
00001
00000
65535
65534
65533
65534
65535
00000
00001
00002
00003
00004
00003

まとめ

インターバルタイマーは、簡単な仕組みですが、非常に応用範囲が広い分野でもあります。

また、同期式のアプリケーションの考え方も、複雑な事を行う基礎になっており、習得する必要があると考えます。

R8C/C++ フレームワークの使いどころも色々と出てきて、段々と楽しくなってきたと思いますw

参考リンク

前の記事

次の記事

開発環境構築など


2
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
2
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?