みなさんこんにちわ、
SAIです。
※注意 今回はちょっと小難しいことを書くので、
ギブアップしそうな方は、最後のほうの![]()
![]()
![]()
まで呼び飛ばして、
コードをまねていただければよいかと思います。
昨日、突然「関数ポインタ」なんて言い出しました。
関数ポインタ とは、何でしょうか?
![]()
![]()
![]()
![]()
![]()
![]()
その名の通り、関数の配置されているポインタのことです。
Arduinoだと見ることができないのですが、
プログラム環境をビルドすると、
定数、関数、などは、マイコンのROM領域に -----①
グローバル変数はマイコンのRAM領域(固定)に -----②
ローカル変数(一時変数)はマイコンのRAM領域(スタック)に -----③
配置されます。
①と②は、ビルドしている最中に、ROMのどこに配置されるかがきまります。
③は、スタック領域なので関数が呼び出されている間だけスタックに配置されます。
Arduinoだと見ることができないのですが、
一般的なマイコンコンパイラだと、Mapファイルが生成され、
↓こんな感じで,配置されたアドレスが見れますね。
-----------------------------------------------------------
SECTION=P
FILE=C:\Hew2\TClassTestZ_3\TClassTestZ_3\Debug\TClassTestZ_3.obj
00000826 0000086d 48
msecwait(int)
00000826 14 func ,g 1
_main
0000083a 32 func ,g 1
↑アドレス ↑サイズ
-----------------------------------------------------------
SECTION=R
FILE=C:\Hew2\TClassTestZ_3\TClassTestZ_3\Debug\sbrk.obj
0000fb80 0000fb81 2
_brk
0000fb80 2 data ,l 2
-----------------------------------------------------------
main()のアドレスは、0000083aに配置されています。
変数 blkのアドレスは、0000fb80に配置されています。
ROM/RAMのアドレスとしては、↓こんな感じ
さて、
変数のポインタのおさらいをしましょう。
変数のポインタってどんな仕組みだったでしょうか?
変数 blk に”1”を代入すると、
blk = 1;
変数blkは、0000fb80に配置されています。
処理としては、0000fb80の値を”1”に変更したことになりますね。
これをポインタで操作してみましょう。
blkのポインタを示すには、↓こうするとblkPtrに0000fb80が入ることになりますね。
int * blkPtr = & blk;
↑ blkのポインタを取り出し。
print("blk add = %x",blkPtr);
とすれば、0000fb80となっていることが見れるはずです。
このポインタを使って、実体を操作するには、
* blkPtr = 1 ;
↑ blkポインタの 実体
このように、blkPtrの実体に1を入れることになります。
思い出したでしょうか?
& アドレス取り出し。
* 実態操作
ですね。
思い出したでしょうか?
![]()
![]()
![]()
![]()
![]()
![]()
この操作ですが、
& アドレス取り出し。
* 実態操作
既にアドレスがきまっている場合、つまり、①や②の場合、関数にも使うことができます。
先ほどのMAPに戻ります。
-----------------------------------------------------------
_main
0000083a 32 func ,g 1
↑アドレス ↑サイズ
-----------------------------------------------------------
main()のアドレスは、0000083aに配置されています。
これを、アドレスに変えるとどうなるでしょうか?
変数の時と同じように
print("main add = %x", & main );
とすれば、0000083aとなっていることが見れるはずです。
ここから、main()関数がどのように呼び出されているか、考えてみましょう。
main();
main()関数は、0000083aに配置されています。
処理としては、0000083aのアドレスにスタックポインタを移動させ、
0000083aから続くアドレスに書き込まれている処理を実行している
ということになります。
逆に言うと、
0000083aのアドレスを関数として扱ってやれば、数字から関数を呼び出すことができるのです!
イメージとしては、こんな感じ!↓
* mainPtr();
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
これを実現するためには、一つ壁があります。
mainPtr が、関数であることを定義しなければなりません。
どのように定義したらよいでしょうか?
答えは、typedef で定義するのです。
typedef void (*pFunc)(void);
初心者では、なかなか見ないtypedef ですね。
typedef ・・・型定義
void ***** (void ) という関数だよ
そして、型の名前をpFuncとあるということを定義しています。
引数があれば、引数型を入れて定義すればOKです。
つまり、
----------------------------------------
void main (void);
↓ ↓ ↓
typedef void (*pFunc)(void);
----------------------------------------
*pFunc が main()
(pFunc の実体) が main関数 ということになりますね!
--------------------------------------------------------
これを逆にすると・・・
--------------------------------------------------------
pFunc は 、 & main()
(pFunc ) は main関数のポインタ ということになりますね!
--------------------------------------------------------
ちょっと雑な説明ですが、理解できたでしょうか?
ここまで理解出来たら、あとは楽です。
pFunc を配列にすれば、関数のポインタを配列にしたことになります。
昨日のコードを関数配列にすると、こんな感じですね。
// 関数配列 //
typedef void (*pFunc)(void);
pFunc FunctionList[] =
{
&DisplayDraw /* 機能1 7Seg表示関数 */
,&DisplayLedChika /* 機能2 LChika表示関数 */
};
![]()
![]()
![]()
![]()
![]()
![]()
次の問題。
どうやって関数の呼び出しをするのでしょうか?
答えから先に書くと、
--------------------------------------------------------
(void) ( * FunctionList[i] ) ();
↓ ↓ ↓
void 型の、 アドレスFunctionList[]の実態 の 引数がvoid で呼び出す
--------------------------------------------------------
です。
/* 登録している機能分呼び出し */
for( int i = 0; i < aFuncNum;i++)
{
(void) (*FunctionList[i])();
}
これで、配列に登録されている関数を、ぐるぐる呼び出せることになりますね!
というわけで、
↑このブログで書いたコードのうち、
メインのコードを修正してみましょう。
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
雑談
関数ポインタ呼び出しって、理解したら便利なのですが、
理解するのが難しいですよね。
ポインタの理解ができていたとしても、
関数ポインタの理解は結構難しいんじゃないかな?
SAIは意外とすんなり理解できたのですが、
2000年代前半、携帯電話開発をしていた時に何人か若い子に説明したけど
なかなか理解できる子はいませんでした。
最終的に、めったに使わない処理だから、マネしてくれって言った覚えがあります。
一つの壁なんでしょうね。
ちなみに、C++言語だともう一癖あります。
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
それでは、main.ino のみコードを書きます。
残り2ファイルは、4-1と一緒だよ
/*
* Author: SAI
*/
/* *** Include *** */
#include <TimerOne.h>
/* 注意
TimerOne.h を使用するためには、
スケッチ -> ライブラリのインクルード -> ライブラリの管理
から、TimerOne をインストールしておく必要があります。
*/
// 7Seg LED 表示Function //
#include "DisplayFunction.h"
/* *** Define 定義 *** */
/* 1桁の表示時間 */
const long TIMER_DELAY = (5 * 1000);
/* 1秒にかかるカウント数 */
const long TIMER_CNT_SECOUND = ((long)1000 * 1000) /(TIMER_DELAY);
/* 砂時計用内部カウンター */
long gTimerCounter;
long gDispTime;
/* initial setting 初期設定 */
void setup() {
initialize();
/* タイマー用のカウンターを初期化 */
// Timer setting タイマー設定
Timer1.initialize(TIMER_DELAY); //マイクロ秒単位で設定
Timer1.attachInterrupt(timerFire); // コールバック関数設定
gTimerCounter = 0;
gDispTime = 0;
// Lチカ用 //
pinMode(A0, OUTPUT);
}
/* Mail loop 通常処理(繰り返し) */
void loop() {
delay(1000);
}
// 関数配列 //
typedef void (*pFunc)(void);
pFunc FunctionList[] =
{
&DisplayDraw /* 機能1 7Seg表示関数 */
,&DisplayLedChika /* 機能2 LChika表示関数 */
};
/*
* タイマー割り込み処理
* 100ms(=100000us)毎にここが呼び出される */
void timerFire() {
// 時間計測 //
gTimerCounter++;
if(gTimerCounter >= (long)10000*1000)
{
gTimerCounter = 0;
}
/* 100ms単位の時間に変換 */
int lTime = gTimerCounter / (TIMER_CNT_SECOUND /10 ) ;
if(gDispTime != lTime)
{
gDispTime = lTime;
/* 桁上がりした時だけ、1msの処理をする */
delay(1);
}
/* 7Seg表示 */
// DisplayDraw();
/* LChika表示 */
// DisplayLedChika();
int aFuncNum = sizeof(FunctionList)/sizeof(FunctionList[1]);
/* 登録している機能分呼び出し */
for( int i = 0; i < aFuncNum;i++)
{
(void) (*FunctionList[i])();
}
}
void DisplayLedChika()
{
/* 100ms単位の時間に変換 */
int lTime = gTimerCounter / (TIMER_CNT_SECOUND /1 ) ;
if((lTime % 2) != 1)
{
analogWrite(A0, 255);
}
else
{
analogWrite(A0, 0);
}
}


