こんにちは、ナナです。
マイコンで時間を作り出すというのは、かなり多くの知識が必要となります。慌てずにじっくりと理解しながら進めましょう。
本記事では次の疑問点を解消する内容となっています。
では、タイマの仕組みを学んでいきましょう。
タイマ機能の概要
ソフトウェアシステムを構築する際に時間という要素を管理したいニーズはよく発生します。例えば「LEDを1秒間隔で点滅したい」といった場合は「1秒」という時間を管理できなければなりません。
このようなニーズはもちろんマイコン製造メーカーもわかっていますので、マイコンには時間を作り出す仕組みが用意されています。その機能をタイマ機能と呼びます。
タイマ機能の役割と特徴
マイコンに搭載されたタイマ機能は複数用意されることも一般的であり、H8/36064マイコンでは次の3つのタイマ機能が用意されています。
- タイマB1
- タイマV
- タイマZ
本章においては、この中で最も簡易的なタイマであるタイマB1を利用した時間を管理するタイマモジュールを作成しましょう。
タイマB1機能の紹介
H8/36064に搭載されたタイマB1機能がどのようなものなのかを知るためにはデータシートを参照する必要があります。
データシートの「10章 タイマB1」を見てみましょう。全4ページとなっており、非常に小さなタイマ機能であることがページ数からもわかります。これくらいであれば皆さんも読んでみようと思うのではないでしょうか。
特徴には次のことが記載されています。これらは本章で順に説明していきます。
- クロック選択:8種類
- カウンタのオーバフローで割り込みを発生
タイマB1のレジスタ構成
データシートを見るとタイマB1には3つのレジスタが搭載されていることがわかります。
- タイマモードレジスタB1(TMB1)
- タイマカウンタB1(TCB1)
- タイマロードレジスタB1(TLB1)
この時点では詳細な説明は控えますが、どのようなレジスタなのかを一旦データシートを読んでおいてください。データシートを読むという行為は組み込み開発者に必要な能力です。
タイマ機能で時間を作り出すとは
タイマ機能を利用して時間を作り出すわけですが、実は簡単には時間を作り出すことはできません。時間を作り出す手順を知る必要があります。
どのマイコンでも時間を作り出す流れは基本的に同じ。このノウハウを習得すれば様々なマイコンで同様に時間を作り出すことが可能!
タイマ機能が時間を数える方法
皆さんに「今から1分の時間を数えてください」とお願いしたら、皆さんは「1、2、3,4・・・60」と時間を数えて「1分です」と答えてくれますよね。
マイコンの中にあるタイマ機能も基本的にはこの考え方と同じです。タイマ機能が「1、2、3・・・」と数える間隔はクロックと呼ばれるものを基準にして数えます。
では、具体的にタイマ機能がどのように数を数えるのかを説明していきます。
システムクロック(CPUクロック)を知ろう
皆さんの体は心臓が生み出す鼓動で血液を対流させることで動いています。マイコンも同じようにクロックと呼ばれる鼓動により動いています。クロックとは波形の信号であり、水晶発振器というハードウェアからマイコンに供給されます。
マイコンに供給されるクロックのことをシステムクロックと呼び、このクロックの鼓動が速いほど性能が高く、より多くのプログラムを動かすことができます。クロックの単位はHz(ヘルツ)で表現され、クロック周波数とも呼ばれます。
ビュートローバーに搭載されている水晶発振器は12MHz(メガヘルツ)のクロックを出力します。12MHzとは12,000,000Hz(12×1000×1000)のことになります。12MHzがどこからわかるのかというと、回路図を見ると一目瞭然ですね。
X1というが水晶発振器を示しています。マイコンのOSC1というピンに接続されていますが、OSCとはオシレーター(Oscillator)という意味で発振器を接続するためのピンのことです。
データシートでシステムクロックは記号 φ(ファイ)として記載される!
内部クロック(タイマクロック)を知ろう
タイマ機能の説明においてシステムクロックの話をしたのには理由があります。
タイマ機能は内部クロックというクロックで動くのですが、内部クロックはシステムクロックを元に生成されるからです。内部クロックは次のように生成されます。
プリスケーラの役割とは
タイマB1の中に存在するプリスケーラというハードウェアについて解説しておきましょう。
プリスケーラとは入力されたシステムクロックを間引くためのハードウェアです。タイマB1のレジスタへ設定された値によりクロックを間引く量を調整することが可能です。
タイマB1においては内部クロックの選択肢として次のものがあるとデータシート「10.1章 特徴」に記載されているのがわかるでしょう。
7種類の中でどれを選択するかは皆さんが検討して決定する必要があります。
プリスケーラでクロックを間引く作業のことを分周と呼ぶ。速すぎるシステムクロックをタイマ機能で必要とするレベルの速さに調整するために存在する!
周波数と周期の関係
周波数や周期は学校教育の中でも学んだと思いますが、忘れている人もいるかもしれませんので説明しておきましょう。
具体的にイメージできるように周波数5Hzの場合の図を用意しました。青色の波形になります。
この図において周期が0.2秒とされていますが、理由は明らかですね。ここから言えることは周波数が決まるということは周期が決まるということです。周波数と周期の関係は次の計算式で表現されます。
周期(秒) = 1 / 周波数(Hz)
この計算式はタイマ機能で時間を作るうえで非常に大事なものなので覚えておきましょう。
内部クロックをカウントする:タイマカウンタB1レジスタ(TCB1)
ここまででようやく数を数える知識の下準備が整いました。タイマB1機能にはタイマカウンタB1レジスタ(TCB1)が存在し、入力される内部クロックの数を数える機能が付いています。
ここからわかることはマイコンのタイマ機能は時間を数えるのではなく、内部クロックの数を数えているということなのです。皆さんが時間を作り出す際にはカウントと時間とを適宜変換して考える必要があります。
分周により変化するTCB1のカウントアップ時間
内部クロックはプリスケーラの分周値により変化しますので、それは最終的にTCB1のカウントアップ間隔が変化することになります。この時間は計算できますので下記のようにExcel等の表計算ソフトでまとめておくと便利です。
周期の時間としてミリ秒の時間を出してあります。組み込み開発の世界で扱う時間は秒よりもミリ秒の方が一般的です。それはマイコンにとって秒という単位はあまりにも長い時間であるため、このような計算を行うと小数点以下の桁数が大きくなりわかりづらいのです。
組み込み開発の基本時間単位はミリ秒(ms)を使え!
タイマ機能における割り込みの使い方と割り込みの概念
任意の時間を作り出すためにはタイマ機能+割り込み機能が必要となります。
割り込みとはマイコンに搭載されるイベント処理の機構です。割り込み機能を使いこなすことは組み込み開発者としては避けて通れません。最初は戸惑うこともある概念ですが、しっかりと身につけてください。
割り込みイベントの種類
マイコンには様々な割り込みイベントが定義されています。データシートの「3章 例外処理」が割り込みに関する内容が記述された章となります。
割り込みイベントのことをデータシートでは「例外処理要因」という言葉で表しています。「3.1章 例外処理要因とベクタアドレス」に一覧としてまとまっています。
この一覧をよく見るとタイマB1に関する割り込みイベントも載っているのがわかります。
ベクタ番号とはマイコンの中で割り込みイベントを一意に識別するための番号のことです。この番号は割り込みを利用するプログラムで必要となる数値になるので意味は覚えておきましょう。
割り込みイベント発生時の動作
マイコンにおいて割り込みイベントが発生すると実行中のプログラムを一時中断し、割り込みハンドラと呼ばれる特別な関数を呼び出させることが可能です。
このようにmain関数の処理が動いている最中に特定の割り込みイベントが発生すると、イベントに対応した割り込みハンドラ関数にプログラムが移動します。割り込みハンドラ関数の処理からreturnされると元のプログラムに自動的に復帰します。これが割り込みです。
割り込みはハードウェアに発生するイベントに対してプログラムを即時実行するための仕組みである。割り込みにより応答性の高いソフトウェアを作り出すことができる!
タイマB1のオーバーフロー割り込み
タイマB1機能に対する割り込みイベントとしては「オーバーフロー割り込み」が用意されています。データシートにも次のように書かれています。
内部クロックをカウントするTCB1レジスタは1Byteのレジスタであり、0xFFの状態でカウントアップするとオーバーフローになりますね。タイマB1機能ではこのオーバーフロー時に割り込みイベントを発行することができます。
割り込み制御用レジスタ
割り込みイベントが発生すると割り込みハンドラが自動で呼ばれると説明しましたが、実は問答無用で呼ばれるというわけではありません。
皆さんが作るプログラムの中で「今から割り込みイベントが発生したら呼んでほしい」「今は呼ばないでほしい」といった切り替える機能がレジスタに用意されています。
タイマB1のオーバーフロー割り込みは、例外処理が管理している次のレジスタで制御します。
- 割り込みイネーブルレジスタ2(IENR2)
- 割り込みフラグレジスタ2(IRR2)
割り込みイネーブルレジスタ2(IENR2)とは
イネーブルとは「許可」という意味です。このレジスタは割り込みを許可するために使用します。
皆さんがこのビットに1(許可)を設定した後で、タイマB1のTCB1がオーバーフローすると割り込みハンドラが呼ばれるようになります。
割り込み許可のことを「イネーブル(enable)」
割り込み禁止のことを「ディスエーブル(disable)」と呼ぶ!
割り込みフラグレジスタ2(IRR2)とは
IRR2レジスタは、割り込み許可がされている状態で割り込みイベントが発生したときに、ハードウェアからソフトウェアに対して「割り込みイベントが発生してますよ」という状態を示すフラグが管理されています。
IRR2の説明にはセット条件とクリア条件が書かれていますね。
「セット」というのは1がセットされるという意味であり、オーバーフローしたときにハードウェアによって自動的にセットされると読み取れます。
「クリア」というのは0がセットされるという意味であり、皆さんのプログラムから0を書き込むことで0にクリアできると読み取れます。
割り込みフラグレジスタは次のように使用します。
レジスタはソフトウェアとハードウェアの境界線上に存在します。レジスタを利用してソフトウェアとハードウェアが会話しているかのように連携しながらシステムを動かすのです。
タイマ機能と割り込み機能を使った時間を作り出す方法
では今回のタイマ機能では1ミリ秒(1000分の1秒)を作り出すことを目標に具体的な検討に入ります。
1ミリ秒を作り出すための計算結果一覧
前回計算結果に割り込み向けの計算を追加してみました。緑色の部分です。
「TCB1の数」の欄は計算上は浮動小数点ですが、整数型のレジスタのため小数点以下は切り捨てられます。つまりそれが1ミリ秒に対する誤差ということになります。
分周値が小さいほど時間精度を高くすることができるのですが、 TCB1のレジスタは1Byteのため最大でも256カウントしかできません。そのため、1ミリ秒をカウントするための数が187回の64分周を採用することにします。
64分周では1ミリ秒きっかりの計算結果にはならないため、1ミリ秒あたり0.0027ミリ秒の誤差が発生しますが、今回のシステムではこの誤差は許容範囲であると判断しました。
デジタル時計といった高精度の時間制御が求められるシステムでは、誤差が発生しないように検討しなければならない。対象システムによって誤差の許容度は変化する!
オートリロードタイマ機能について
64分周の設定ではTCB1が187回のカウントアップをした時に1ミリ秒が経過したと言えます。この瞬間を割り込みイベントで捉えたいわけです。
タイマB1機能にはオートリロード機能がついており、任意のカウント値からカウントアップができます。この機能を活用しましょう。
TMB1とTLB1レジスタに値を設定することで約1ミリ秒間隔で割り込みを発生させることができます。これでようやく1msという時間を作り出す設計が完了しました。
割り込みハンドラの登録方法
タイマB1機能におけるオーバーフロー割り込みを利用するためには割り込みハンドラを登録するプログラムを追加しなければなりません。
HEW開発環境では割り込みを登録する下準備がすでにできているため比較的簡単に登録が可能です。HEWプロジェクトの中にintprg.cというファイルがすでに存在していますね。このファイルが割り込みハンドラを管理しているファイルです。
intprg.c(変更前)
// vector 29 Timer B1
__interrupt(vect=29) void INT_TimerB1(void) {/* sleep(); */}
上記のINT_TimerB1関数こそが割り込みハンドラです。vect=29というのがベクタ番号の指定になっています。すでに割り込みハンドラの登録自体はこれで完了していますが、関数の中身が空っぽですね。ここに皆さんが動かしたいプログラムを追加します。
ただし、ここにダラダラと書いていくとメンテナンス性が悪いため、今回追加するTimerモジュールに処理を引き込みましょう。下記①・②の処理を追加してください。
intprg.c(変更後)
#include <machine.h>
// ①Timerモジュールのヘッダインクルード追加
#include "timer.h"
#pragma section IntPRG
・・・省略・・・
// vector 29 Timer B1
__interrupt(vect=29) void INT_TimerB1(void)
{
// ②タイマモジュールの割り込み処理を呼び出す
Timer_interruptHandler();
}
今回皆さんが作成するタイマモジュールにTimer_interruptHandler関数を追加する予定です。その関数をここで呼んであげましょう。
また、関数プロトタイプ宣言を読み込むためにtimer.hをインクルードする処理も追加します。
割り込みハンドラの登録というのは本来非常に泥臭い作業であり、マイコンや開発環境により方法が変化する。本登録例は今回のHEW開発環境での登録方法であることに注意!
タイマ制御を行うTimerモジュールの作成
タイマの制御方法がわかりましたのでプログラムの作成を行います。いつもの手順でtimer.cとtimer.hをHEWプロジェクトに追加してください。
Timerモジュールの役割検討
タイマ制御モジュールでは次の機能を実装したいと思います。
- 時間生成は1ミリ秒単位とする
- システムが起動したら起動からの経過時間を1ミリ秒単位で管理する
- 他のモジュールから起動後経過時間を取得できるインターフェースを用意する
- 指定した時間を待つことができるインターフェースを用意する
では、これらの要求を満たすインターフェースを検討します。
Timerモジュールのインターフェース仕様
それではタイマモジュールのインターフェース仕様を決定します。皆さんはこの仕様を元にプログラムを作成してください。
1.提供ヘッダファイル名
#include “timer.h”
2.定数定義
なし
3.インターフェース定義
3.1 初期化
3.2 システム時刻の取得
3.3 時間経過待ち
3.4 割り込み処理
課題:Timerモジュールを使って時間を作り出してみよう
課題1
課題内容
インターフェース仕様に従いタイマモジュールを作成せよ。timer.cとtimer.hは次のプログラムをベースにして修正を加えよ。
timer.h
#ifndef TIMER_H
#define TIMER_H
//------------------------------------------------
//------------------------------------------------
// マクロ定義(Macro definition)
//------------------------------------------------
//------------------------------------------------
// 型定義(Type definition)
//------------------------------------------------
//------------------------------------------------
// プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
//------------------------------------------------
#endif // TIMER_H
timer.hに追記すべきこと
- インターフェースのプロトタイプ宣言を追加せよ
timer.c
#include "iodefine.h"
#include "timer.h"
//------------------------------------------------
// 概 要:初期化。起動後時間の管理を開始する。
// 引 数:なし
// 戻り値:なし
//------------------------------------------------
void Timer_init(void)
{
}
//------------------------------------------------
// 概 要:起動後時間の取得
// 引 数:なし
// 戻り値:起動後時間(単位:ms)
//------------------------------------------------
unsigned long Timer_getTime(void)
{
}
//------------------------------------------------
// 概 要:指定時間待ち
// 引 数:待ちたい時間(単位:ms)
// 戻り値:なし
//------------------------------------------------
void Timer_waitTime(unsigned long msec)
{
}
//------------------------------------------------
// 概 要:オーバーフロー割り込み(1ms間隔で割り込みハンドラからコールされる)
// 引 数:なし
// 戻り値:なし
//------------------------------------------------
void Timer_interruptHandler(void)
{
}
timer.cに追記すべきこと
- システム時間を管理するグローバル変数を定義せよ。定義の際にはvolatile修飾子を付与し最適化を防止せよ。
- Timer_init関数ではタイマB1のレジスタでオートリロード機能有効、分周、リロード値設定を行い、オーバーフロー割り込みを許可せよ。
- Timer_getTime関数では管理しているシステム時刻を戻り値で提供せよ。
- Timer_waitTime関数では引数指定された時間が経過するまでループ処理で待機せよ。
- Timer_interruptHandler関数ではシステム時間を更新せよ。また、割り込み要求フラグをクリアせよ(クリアを忘れると暴走するため注意)。
timer.h
#ifndef TIMER_H
#define TIMER_H
//------------------------------------------------
//------------------------------------------------
// マクロ定義(Macro definition)
//------------------------------------------------
//------------------------------------------------
// 型定義(Type definition)
//------------------------------------------------
//------------------------------------------------
// プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
void Timer_init(void);
unsigned long Timer_getTime(void);
void Timer_waitTime(unsigned long msec);
void Timer_interruptHandler(void);
//------------------------------------------------
#endif // TIMER_H
- インターフェース仕様書に従いプロトタイプ宣言を追加する。
timer.c
#include "iodefine.h"
#include "timer.h"
// システム時間(ms)
volatile static unsigned long gTimer_systemTime = 0;
//------------------------------------------------
// 概 要:初期化。起動後時間の管理を開始する。
// 引 数:なし
// 戻り値:なし
//------------------------------------------------
void Timer_init(void)
{
// 1msのオーバーフロー割り込み用の設定
TB1.TMB1.BIT.RLD = 1; // オートリロード機能有効
TB1.TMB1.BIT.CKS = 4; // 分周φ/64を選択
TB1.TCB1 = 256 - 187; // リロードカウント値
// タイマB1オーバーフロー割り込み許可
IENR2.BIT.IENTB1 = 1;
}
//------------------------------------------------
// 概 要:起動後時間の取得
// 引 数:なし
// 戻り値:起動後時間(単位:ms)
//------------------------------------------------
unsigned long Timer_getTime(void)
{
return gTimer_systemTime;
}
//------------------------------------------------
// 概 要:指定時間待ち
// 引 数:待ちたい時間(単位:ms)
// 戻り値:なし
//------------------------------------------------
void Timer_waitTime(unsigned long msec)
{
// 呼び出し開始時間の保持
unsigned long nowTime = gTimer_systemTime;
// 指定時間待ちループ
while (1)
{
// 呼び出しから指定時間経過したら待ち終了
if (gTimer_systemTime - nowTime >= msec)
{
break;
}
}
}
//------------------------------------------------
// 概 要:オーバーフロー割り込み(1ms間隔で割り込みハンドラからコールされる)
// 引 数:なし
// 戻り値:なし
//------------------------------------------------
void Timer_interruptHandler(void)
{
// システム時刻を1ms経過
gTimer_systemTime++;
// 割り込み要求フラグクリア
IRR2.BIT.IRRTB1 = 0;
}
- システム時間の管理用にグローバル変数gTimer_systemTimeを定義。ファイル内アクセスかつ最適化防止のためstaticとvolatileを付与して定義している。
- Timer_init関数ではオートリロード機能の有効化、分周64、リロード値69を設定し割り込みを許可している。
- Timer_getTime関数ではシステム時間を戻り値で返却するのみ
- Timer_waitTime関数では呼び出し時のシステム時刻を保存しておき、ループをしながら指定時間の経過を待っている。
- Timer_interruptHandler関数では呼ばれたタイミングでシステム時刻をインクリメントしている。割り込み要求フラグもクリア必要。
課題2
課題内容
MAINモジュールでタイマモジュールを利用できるようにせよ。
また、main関数を変更し、次の1~4を実施したら再度1から繰り返すようにせよ。
- オレンジLEDを点灯し、Timer_waitTime関数で1000ミリ秒待つ
- グリーンLEDを点灯し、Timer_waitTime関数で1000ミリ秒待つ
- オレンジLEDを消灯し、Timer_waitTime関数で1000ミリ秒待つ
- グリーンLEDを消灯し、Timer_waitTime関数で1000ミリ秒待つ
課題が完成したらビルドを行いビュートローバー上で動作させ、期待動作通りに動くことを確認せよ。
main.c
//------------------------------------------------
// 概 要:エントリーポイント
//------------------------------------------------
void main(void)
{
// システム初期化処理
main_init();
while(1)
{
}
}
期待動作
- オレンジLEDが点灯する。
- 1秒後にグリーンLEDが点灯する。
- 1秒後にオレンジLEDが消灯する。
- 1秒後にグリーンLEDが消灯する。
- 1秒後に再度オレンジLEDが点灯し、以降2から繰り返す。
main.c
#include "iodefine.h"
#include "led.h"
#include "sw.h"
// タイマモジュールを使用するためインクルード
#include "timer.h"
//------------------------------------------------
// 内部プロトタイプ宣言
//------------------------------------------------
void main_init(void);
//------------------------------------------------
// 概 要:エントリーポイント
//------------------------------------------------
void main(void)
{
// システム初期化処理
main_init();
while(1)
{
// オレンジLED点灯
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_ON);
Timer_waitTime(1000);
// グリーンLED点灯
Led_setLight(D_LED_KIND_GREEN, D_LED_LIGHT_ON);
Timer_waitTime(1000);
// オレンジLED消灯
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_OFF);
Timer_waitTime(1000);
// グリーンLED消灯
Led_setLight(D_LED_KIND_GREEN, D_LED_LIGHT_OFF);
Timer_waitTime(1000);
}
}
//------------------------------------------------
// 概 要:システム初期化
//------------------------------------------------
void main_init(void)
{
//---------------------------------------------------
// ウォッチドッグタイマの停止(消さないこと)
//---------------------------------------------------
WDT.TCSRWD.BYTE = 0x92;
WDT.TCSRWD.BYTE = 0x92;
//---------------------------------------------------
// PCRの設定(PCRは書き込み専用レジスタ)
//---------------------------------------------------
IO.PCR6 = 0x11; // P60,P64を出力
IO.PCR7 = 0x00; // P74を入力
//---------------------------------------------------
// モジュールの初期化(各モジュールの初期化を実施)
//---------------------------------------------------
Led_init();
Sw_init();
Timer_init();
}
- timer.hをインクルードする
- main関数ではLED点灯・消灯処理の間にTime_waitTime(1000);を入れることで1秒の待ち処理を行う。
- main_init関数ではTimer_init関数の呼び出しを追加する。
課題3
課題内容
課題2の完成状態からmain関数の内容を次のように修正せよ。
- システム起動直後にまずオレンジとグリーンLEDを両方点灯せよ。
- Timer_getTime関数を利用し起動直後のシステム時間を保持せよ。
- 起動時間から5000ミリ秒経過したことをTimer_getTime関数を利用しながら監視を行い、経過したら両方のLEDを消灯せよ。
main.c(次の内容をもとに作成せよ)
//------------------------------------------------
// 概 要:エントリーポイント
//------------------------------------------------
void main(void)
{
// システム初期化処理
main_init();
while(1)
{
}
}
期待動作
起動直後オレンジとグリーンLEDが点灯し、5秒後に両LEDが消灯すること。
main.c(main関数以外は課題2ど同様)
//------------------------------------------------
// 概 要:エントリーポイント
//------------------------------------------------
void main(void)
{
unsigned long time;
// システム初期化処理
main_init();
// 起動時間の保持
time = Timer_getTime();
// 起動直後はLED点灯
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_ON);
Led_setLight(D_LED_KIND_GREEN, D_LED_LIGHT_ON);
while(1)
{
// 起動後5秒経過したらLEDを消灯
if (time + 5000 <= Timer_getTime())
{
// オレンジLED消灯
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_OFF);
// グリーンLED消灯
Led_setLight(D_LED_KIND_GREEN, D_LED_LIGHT_OFF);
}
}
}
- 起動時間の保持処理は必ずmain_init関数の後で実施する必要がある。理由はTimer_init関数が呼ばれた後でなければならないためである。
- Timer_getTimeをループの中で監視しながら起動時間より5秒経過したかを判定している。
- この時間経過判定方法はTimer_waitTime関数を呼ぶのとは違い、main関数で時間経過までの間は他の処理を行うことが可能である。
Q&A:タイマ制御に関するよくある質問
はい、これは意識的に名前にモジュール名を入れています。これには、大きく分けて2つの目的があります。
- 関数名から所属しているモジュールを即座に判断できるようにしている
- システム全体で関数名のバッティングを防止している
関数名でも変数名でも名前というものは開発者にとって非常に大事な情報です。この名前から様々な判断が可能であり、システム全体でルールを統一することで開発者間の意思疎通も図れるようになります。