ポインタでやってしまうミス③
ポインタというよりかは、演算子の優先順位に関する物ですが、ミスしがちかなと思いましたので切り取っておきます。
Pattern 4
#include <stdio.h> void mesurement(unsigned int *iChk, unsigned char cNum); int _tmain(int argc, _TCHAR* argv[ ]) { unsigned int iChk[ ]={0}; mesurement(iChk, 5); for(int i=0; i<10; i++){ printf("iChk[%d]=%d\n", i,iChk[i] ); } return 0; } void mesurement(unsigned int *iChk, unsigned char cNum) { for(int i=0; i<10; i++, iChk++) { if(i==cNum){ *iChk ++; } } }
あまり良い例ではないのですが、関数mesurementで番号cNum目の配列iChkのカウントをインクリメントするプログラムです。このプログラムは正常にコンパイルが通るのですが、今言った意図通りには動きません。問題は*iCk++;にあります。二項+演算子などの演算子より間接参照演算子*の方が通常は優先順位が高いのですが、後置増分演算子++や後置減分演算子--は間接参照演算子*より優先順位が高く、該当の書き方ではポインタをインクリメントして中身を参照するだけ(隣のアドレスを参照するだけ)でRAMには何も処理しません。中身に対してインクリメントを働かしたいなら括弧をつける必要があります。(*iChk)++; なんとなく間接参照演算子の方が++より優先順位が高そうなのですが、ポインタの取り扱いには括弧を意図的につけるのがよさそうです。
試験プログラム
#include <stdio.h> int _tmain(int argc, _TCHAR* argv[]) { unsigned short time[10] = {10,20,30,40,50,60,70,80,90,100}; unsigned short *wdTimer1 = time; unsigned short *wdTimer2 = time; printf("time addr=%d \n", time); printf("wdTimer1 addr=%d, entity=%d\n", wdTimer1, *wdTimer1); printf("wdTimer2 addr=%d, entity=%d\n", wdTimer2, *wdTimer2); *wdTimer1 = *wdTimer1 + 1; printf("wdTimer1 addr=%d, entity=%d\n", wdTimer1, *wdTimer1); *wdTimer2 ++; printf("wdTimer2 addr=%d, entity=%d\n", wdTimer2, *wdTimer2); return 0; }
上のプログラムを見る事で *wdTimer1 = *wdTimer1 + 1;という書き方であれば、*より+の方が優先順位が低く*wdTimer2 ++; という書き方であれば*より+の方が優先順位が高いという事が確認出来るかと思います。
可読性やセマンティックスという観点では今回のPattern4はiChk[i]++; という書き方が良いかと思います。括弧()は可読性を下げますし、かといってa = a+1; では冗長に感じてしまいます。演算子の優先順位は意外に把握できていないものですから、優先順位を知っている事を前提としたプログラムは他の人に意図が伝わりにくいかと思います。その場に合わせたシンタックスシュガーを使うなどして、人にやさしいプログラムを心掛けるのが良いかと思います。
volatileの使い方②
volatileの使い方としてvolatileの使い方①で説明しました。ここではその続きとして他のパターンを見てみたいと思います。
Pattern 5
unsigned int iTimer;
#pragma interrupt
void Interrupt1msecTimer(void){
if(iTimer){
iTimer--;
}
}
void PortWait(void){
iTimer = 10; /*10msecの待ち処理*/
while(1){
if(!iTimer) break;
}
}
上記はソフトウェアウェイトのタイマ割り込みを使用したパターンです。割り込み関数の表現の仕方はマイコンによってさまざまありますが、ここではInterrup1msecTimerという関数が1msec毎に呼び出される事を想定しています。このPortWait関数内にてiTimerにレジスタが割りあたってしまった場合、割り込み関数でディクリメントされずにiTimer = 10で固定され、PortWait関数内で無限ループにはまってしまいます。こういった場合はvolatile unsigned iTimer; にしてください。
Pattern 6
unsigned int iTimer;
unsigned int iTask=1;
#pragma interrupt
void Interrupt1msecTimer(void){
if(iTimer){
iTimer--;
}
}
bool DeviceControlSequence(void){
bool bRtn = false;
switch(iTask){
case 1:
…
iTimer = 10;
iTask++;
break;
case 2:
if(!iTimer){
…
iTask++;
}
break;
case 3:
…
break;
default:
ASSERT();
break;
}
return(bRtn);
}
上記は他デバイスを制御するようなプログラムに良く見られる、シーケンス処理もしくはタスク処理などと呼ばれるものです。(もっとも一般的な呼び名はなんなのか良くわかりません) 他デバイスがあると、通信に次のコマンドを発行するためのWaitを持たせる事が良くあります。しかしDeviceControlSequence内のiTimerにレジスタが割りあたってしまった場合はどうなるでしょうか?ここでもiTimer=10固定となりcase 2以降の処理に進まなくなってしまいます。結局Pattern5もPattern6も同じ事なのですが、タイマ割り込みの関数で使用するグローバル変数はvolatileをつけるのを常習化させておくと良いかと思います。
さて、意図と違うRAMの使い方をされてしまう(レジスタが割りあたってしまう)プログラムを見てきましたが、こちらの意図と違うからと言って常にvolatileが有効なわけではありません。次のプログラムを見てください。
Pattern 7
struct StBuffer{
unsigned char byTxdBuffer[256];
unsigned char byRxdBuffer[256];
unsigned char byTransBuffer[512];
};
StBuffer stBuffer;
void initialize(void){
unsinged char *pbTemp = (unsigned char)&stBuffer;
unsigned short i;
for(i=0; i < sizeof(StBuffer); i++ ){
*pbTemp = 0;
}
}
巨大な構造体(組み込みの世界ではそこそこ巨大です。)で宣言された変数stBufferをinitialize関数内で0初期化しています。ここでpbTempポインタの指し示す空間にスタックが割り当てられてしまう場合があります(通常であればそれはグローバル変数stBufferの先頭アドレスのはずにも関わらず)。そうなった場合この関数に入った途端1kのスタックを消費する事になります、スタックの資源もカツカツですからこの事によりスタックオーバーフローが起こってしまう場合があるので望ましいコンパイル結果とは思えません。通常であればstBufferがグローバル変数なのでRAMのアドレスをpbTempに格納し4byteしかスタックを使いそうにないのですが、コンパイラによってはご丁寧にスタックを一回確保してから丸写しという処理をする場合があるようなのです。この場合最適化の影響ではなかった為、volatileをつけても意図した処理に変更する事ができませんでした。コンパイラの仕様によるコンパイル動作はvolatileで回避できません、もはや処理を変えるかスタックを増やすしかありません。
まとめ
・タイマ割り込み処理に使用する、時間計測のグローバル変数には無条件にvolatileをつける。
・コンパイラ仕様によるコンパイル結果にはvolatileは無効となる。
バイナリとテキスト②
テキストがバイナリのサブセットな為、基本的にはバイナリと言っていれば間違いではないのですが、それでもテキストとバイナリは意識して使い分けています。(以下は私の解釈なので一般的ではないかもしれません)
テキストファイルとバイナリファイルというと、
テキストファイル、それ以外のデータが入っているものをバイナリファイル
と言っている気がしますが、C言語的には終端が0か否かで考えます。
char foo [ ] = {'A', 'B', 'C'};
は0終端でないのでバイナリ
この場合char型でバイナリデータを扱っているので望ましくない。
(このデータをテキストだと思って、文字列を扱う標準関数に引き渡すと0終端でないため予想しないバグが発生する。)
unsinged char bar [ ] = "ABC";
は0終端なのでテキスト
この場合はunsigned char型でテキストデータを扱っているので許容できるが、char型を使う方が寛容。
(標準関数に渡す時キャストする必要が出てくる。)
char foo [ ] = "abc";
は0終端なのでテキスト
この場合char型でテキストデータを扱っているので良い。
unsinged char bar [ ] = {'A', 'B', 'C'};
は0終端でないのでバイナリ
この場合はunsigned char型でバイナリデータを扱っているので良い。0終端でない事を意識するにもふさわしい型。
まとめ
テキストで扱いたいなら必ず0終端にする。
バイナリとテキスト①
バイナリとテキストが同列、もしくは反対語のような関係にあるものと誤解する場合が見られますが、テキストはバイナリのサブセットであり、バイナリはテキストのスーパーセットです。
テキスト ⊂ バイナリ(テキストはバイナリの部分集合)
この辺の曖昧さがchar型の使い方に現れがちです。
char foo = 130;
といったようなchar型の使い方をする人がいますがこれは間違いです、バグを引き起こしますので直しましょう。char型をunsignedと勘違いしがちなんですが、char型がsigned か unsignedかは処理系依存です。という事は 処理系によってはchar型の範囲が -128 ~ +127 、0 ~ + 255 となり、たとえば例のfoo が -126(130を2進で表して2の補数にマイナスをつける) となる処理系と130となる処理系が現れます。char型のままバイナリデータを扱ってCheckSum計算などすると処理系によって答えが変わってきてしまいます。バイナリデータとして扱う場合はちゃんとunsigned char とする必要があります。逆に
unsigned char bar[] = "abc";
とする人も多くあります。これはバグになることはありません。(バグになるような事例は見たことありません。) ただしテキストを扱う場合はchar型を使うのを推奨します。(標準関数ではテキストをchar型で扱っており、unsignedでテキストを扱う場合余計なキャストをつける必要が出てきます。)
まとめると
unsigned char 型 はバイナリを扱う テキストも扱える(非推奨)
char 型 はテキストを扱う
となり最初の
テキスト ⊂ バイナリ(テキストはバイナリの部分集合)
の関係が見えてくると思います。
条件付きコンパイル②
今回も条件付きコンパイルの詳細を見ていきましょう。
例2.否定
#include <stdio.h> #define YELLOW 0 #define BLUE 1 #define LED //#define GREEN void func(int num){ printf("%d\n", num); } int main(){ /* 否定 */ #if !YELLOW //真 func(11); #endif //#if !LED // fatal error C1017: 整数定数式が無効です。 // func(12); //#endif #if !(YELLOW) //真 func(13); #endif #ifndef YELLOW //偽 func(14); #endif //#ifndef (YELLOW) //fatal error C1016: #if[n]def は、識別子を必要とします。 // func(15); //#endif #if !defined YELLOW //偽 func(16); #endif #if !defined (YELLOW) //偽 func(17); #endif #if !defined (LED) //偽 func(18); #endif #if !defined (GREEN) //真 func(19); #endif }
ここでのポイントは#ifndef と 否定演算子! の使い方になります。 #if !(YELLOW) と書いた場合はYELLOWの値の否定なので真となります。対して #ifndef YELLOWと書いた場合はYELLOWという文字列が定義されているので、その否定となり偽となります。同様に#if !defined (YELLOW)も文字列は定義されているので、その否定となり偽となります。なお#if defined !(YELLOW) とはかけません。defined で修飾されたシンボルはただのシンボルであり定義された数値は評価されません。 否定演算子!はオペランド(被演算子)に真偽値 しかとれませんので、シンボルを修飾することはできません。この事からわかるようにdefined (シンボル) の戻り値は真偽値に他なりません。なお否定演算子!は見ずらいので忌避されます。僕の経験で言うと基本的には条件付きコンパイルでは否定を使わず#elseを使います。インクルードガードの時だけ汚くなるので#ifndefを使います。
まとめ
①#if は続く値が真偽値なので否定演算子!で否定できる。
②#ifdef, defined は続く値がシンボルなので否定演算子!で否定できない。
③否定は見ずらく真偽を勘違いしやすいのであまり使わない。
④#ifndefはインクルードガードで使う。
条件付きコンパイル①
条件付きコンパイル、プリプロセッサは、コンパイラの違いを吸収したり、テストコードを残したり、同じソースをプロジェクト毎に使い分けたり、コメントアウト(//や/**/、/**/は入れ子が出来ないので)の代わりに使ったりと、どのプロジェクトでも大活躍だと思いますが、正確に使い分けられていなかったり、またソースの可読性を著しく低下させたりする事からバグの温床となる事が良くあります。ここでは#if, #ifdef, #if defined, この三種類のプリプロセッサ命令の違いを詳細に見ていきたいと思います。
例1.プリプロセッサ条件付きコンパイル
#include <stdio.h> #define YELLOW 0 #define BLUE 1 #define LED //#define GREEN void func(int num){ printf("%d\n", num); } int main(){ #if YELLOW //偽 func(1); #endif #if (YELLOW) //偽 func(2); #endif //#if LED // fatal error C1017: 整数定数式が無効です。 // func(3); //#endif #if GREEN //偽 func(4); #endif #ifdef YELLOW //真 func(5); #endif //#ifdef (YELLOW) //fatal error C1016: #if[n]def は、識別子を必要とします。 // func(6); //#endif #if defined YELLOW //真 func(7); #endif #if defined (YELLOW) //真 func(8); #endif #if defined (LED) //真 func(9); #endif #if defined (GREEN) //偽 func(10); #endif }
#defineの違いに注目してください。YELLOWはシンボルが定義され、その値が0と定義されています。BLUEはシンボルが定義され、その値が1と定義されています。LEDはシンボルだけ定義されており、GREENはシンボルも値も未定義です。
#ifは続く値、もしくはシンボルの値が真か偽を評価しますが、#ifdefと#if definedは続くシンボルそれ自体が定義されているかいないかを評価するという違いがあります。なお0が偽、0以外が真となります。
例ではYELLOWの値が0と定義されている為、#if YELLOWの評価は偽となります、一方、#ifdef YELLOW, #if defined YELLOWではYELLOWというシンボルの定義はありますので、その値が0だろうが1だろうが評価が真となります。また#if LEDはLEDに値が定義されていない為、LEDを評価できずプリコンパイル時にエラーとなります。一方、#ifdef LED, #if defined LEDではシンボル自体は定義されているので真になります。#if GREENはシンボルすら定義されていないのでプリコンパイルでエラーとはならず偽となります、もちろん#ifdef GREEN, #if defined GREENでもGREENが未定義なので偽となります。
ここまで見ると#ifdefと#if definedは同じ仕様のようですが、#ifdefで評価するシンボルは()括弧をつけるとエラーとなりますので注意が必要です。一方、#ifと#if definedは()括弧をつけても評価結果は変わりません、むしろセキュアコーディングにおいてはマクロに()括弧をつける事を推奨していますので(関数マクロなどでは括弧をつけないと意図しない結果を返すことがあるため)#ifと#if definedにつづく値、シンボルには積極的に()括弧をつけていくのが良いかと思います。
まとめ
①#ifは値もしくはシンボルの値の真偽を評価し、#ifdef, #if definedはシンボルそれ自体の定義有無を評価する。
②#ifは値を持たないシンボルを評価できないが、#ifdef, #if definedはシンボルの値があってもなくても値は無視して定義有無だけを評価する。
③#ifdef は評価対象に括弧をつけてはならないが、 #if, #if definedは評価対象に積極的に括弧をつける事が推奨される。
volatileの使い方①
型修飾子にはconstとvolatile(人によっては型修飾子はlongとかshortのサイズ修飾のキーワードを指している場合があるようですが、ここではK&Rに従います。)がありますが、volatileを使う事はそうそうないかと思います。しかしマイコンの周辺機能を使用するエンジニアはよく知っておく必要があるのがvolatileです。volatile の役割は以下となります。
①処理系(コンパイラ)で行われる最適化を抑制する。
②暗黙の型変換を許可しない。
です。①は一般的ですが、②はあまり知られていない気がします。C++のexplicitの役割を兼ね備えています。
さてコンパイラで行われる最適化を抑制するとありますが、そんなプログラムの内容に影響を与えるような最適化が行われる事があるのか?それダメじゃないですか?と思ってしまいます。だいたい最適化ってどういったものがあるのでしょうか?最適化は主に二つの視点から行われます。
①リソース(ROM/RAM)の使用量を減らす。
②実行速度を早くする。
です。では、いくつか最適化対象となるようなコードとvolatileの効用を以下に記します。
Pattern 1
size_t iAddr = 0x00EFFFFF;
unsigned char *piAddr = (unsigned char*)iAddr;
*piAddr = 0x00;
*piAddr = 0x01;
*piAddr = 0x02;
...
同じ命令が並んでいますが、実際に意味があるのは最後の*piAddr = 0x02;だけのように見えます。そのためコンパイラは*piAddr = 0x00; *piAddr = 0x01;を消して(dead code elimination)アセンブリにします。(ただし、最適化はコンパイラが独自に行いますのでこれが確実に行われるかは判りません。) こういった処理は周辺機能のレジスタなら十分にありえます。この時volaltile unsigned char *piAddrとして最適化を抑制します。
Pattern 2
unsigned char *piTmpData = (unsigned char *)0x00EFFFFF;
unsigned char iData = 0x01;
*piTmpData = iData;
...
//以降piTmpDataを使用しない
これもPattern1とほぼ同じです。コンパイラはローカル変数*piTmpDataが右辺で使用されない事から*piTmpData = iData; という代入式を無駄な処理と判断し、この式を消してアセンブリにします。これも周辺機能のレジスタならありえます。これもvolatileの出番です。
Pattern 3
int iData;
int iCnt;
for( iCnt = 0 ; iCnt < 100 ; iCnt++ ){
iData = iCnt;
}
...
//以降iDataを使用しない
waitの処理でこういった処理は見られますがこれも、コンパイラは無駄な処理として全部消してアセンブリにします。(もう一度書きますが、最適化はコンパイラが独自に行うのでこれが確実に行われるかは判りません。)こういったwait処理はレジスタを設定してから次の命令まで何マイクロ空けろとデータシートに書かれている場合に行いますがこの場合もvolatileの出番です。
Pattern 4
unsigned char *piAddr;
piAddr = (*unsigned char)0x00EFFFFF;
while(*piAddr == 0){
...
}
...
piAddrがポートレジスタだった場合、状態を変えるのは外部要因になりますが内部処理としてpiAddrの指す先に汎用レジスタが使われてしまう最適化が行われたらどうなるでしょうか?外部要因で変わるはずの0x00EFFFFF上の値が汎用レジスタに置き換わっている為、外部要因の値の変更が行われず、無限Loopとなってしまいます。(ローカル変数はスタックになるくらいだと思っていたのが、汎用レジスタの妙技で次から次へと変数の格納先が変わることがあります。コンパイラがどういったレジスタの使い方をするか正直未知数です。)こういった場合にvolatile unsigned char *piAddr;をして、この最適化を抑制します。
なお補足しておきますが、unsigned char * volatile piAddr; とするとアドレスが最適化されないという意味になります。基本的にはアドレスの指す先が最適化されないというのが一般的ではないかと思います。
まとめ
・周辺機能のアドレスやレジスタを格納する時はvolatileをつける。
・wait処理はvolatileをつける。
です。他にもコンパイラによって特殊な最適化があるかと思いますのでそんな時はvolatileをつけてみてください。最適化はコンパイラが変わった時の落とし穴となります。この手のバグは本当に見つけにくいので必要なところをよく見極めましょう。コンパイラのリリースノートにはよく注目するべきです。