shino's bar

お家で使うLinux tux

PICマイコン開発環境(SDCCでのプログラムの書き方)

26 Apr 2007 (初出: 16 Apr 2007)

フリーのCコンパイラSDCCを使ったサンプルプログラムを解説します。

--> SDCCのインストール
PIC12F683によるファンの速度制御

サンプルの概要

動作概要

LEDを点滅させるものです。入力にプッシュボタンを1つ、出力はLED1つです。 それだけのことに16ピンものマイコンは要らないじゃないかというところですが、入門には定番のPIC16F84Aを用いました。 動作は次のようなものです。

回路図

回路の概略はこのようなものです。 LEDは+5Vから抵抗を介してRB1に接続します。 PIC内部のウィーク・プルアップを使い、プッシュボタンは単にRB7と接地との間に接続するだけです。 発振は20MHzのクリスタルかセラミック振動子を用います。 ここでは使っていませんが、リセット回路は付けておいたほうが良いかもしれません。


プログラムの解説

Cのソースプログラムはこちらです。 動作の割りに、プログラムは少し大層かもしれませんが、 プログラムの要素をたくさん盛り込んでみました。 以下、順不同でかいつまみ解説します。

ヘッダの取り込み

#include <sdcc-lib.h>
#include <pic16f84a.h>

プログラム冒頭にはこのようにヘッダを指定します。 2行目は使用するPICの型名に合わせます。 どのようなヘッダが標準で用意されているかは、 /usr/share/sdcc/include/pic あるいは PIC18Fxxx用は /usr/share/sdcc/include/pic16 ディレクトリの中を見ると分かります。

入出力の定義

#define SW1	(RB7 == 0)	// negative logic
#define LED1	RB1
#define LON	0		// sink on
#define LOFF	1

ボタンを押すとRB7ビットがlowすなわち0となるので、SW1はtrueすなわち-1となります。 LEDはRB1ビットが0のとき点灯、1のとき消灯なので、上記に定義しておきます。

コンフィギュレーションビット

/* configuration bits */
int at 0x2007 __config = _HS_OSC & _WDT_ON & _PWRTE_ON;

コンフィギュレーションビットの設定はこのような書式です。

特殊レジスタの設定

	PORTA = 0xff;		// 全ビットhigh
	PORTB = 0xff;		// LED全消灯
	TRISA = 0xff;		// RAは全入力
	TRISB = 0xf0;		// RB下位4bit出力
	OPTION_REG = 0x07;	// WDT分周比128 (約2秒), week pull up
	PSA = 1;		// TM0はプリスケーラ使わない(WDTで使う)
	T0IF = 0;		// 割り込みフラグクリア
	T0IE = 1;		// TMR0オーバーフロー割り込み発生許可
	GIE= 1;			// 割り込み許可

前半では各レジスタに16進数を与えて設定しています。 後半では各々ビット毎に設定しています。 このようにレジスタ毎でもビット毎でも操作できます。 どのようなレジスタ名、ビット名が使えるかは、 インクルードしたヘッダ /usr/share/sdcc/include/pic/pic16f84a.h を読むと分かります。

main関数

int main() {
	/* ローカル変数定義など */
	initialize();			// 電源リセット後に一度実行される処理
	while (1) {
		ClrWdt();			// WDTクリア
		/*** ここにくり返し処理 ***/
	}
}

Cのプログラムで必須のmain関数はおおむねこのような形です。 電源リセット後にこのmain関数が呼び出されます。 この例のように while (1) { } で無限ループとするのが普通です。 したがってこのmain関数から帰って来ることはないので、ここでは関数の返り値をintとしましたが、 実質は voidです。 ウォッチドッグタイマを使う場合は、このループの冒頭で一度タイマをクリアします。

割り込み処理関数

/* interrupt process */
static void timer() interrupt 0 {
	T0IF = 0;		// 割り込みフラグクリア
	/*** 割り込み時の処理 ***/
}

関数名は任意ですが、static void ...() interrupt 0 は割り込み処理関数の決まった書式です。 基本は冒頭で割り込み原因を調べて分岐するのですが、この例ではタイマー0のオーバーフロー割り込みしか使っていないので、 それは省略しています。分岐の後、割り込み原因となったフラグをクリアし、対応の処理をします。 その他のことはコンパイラがやってくれるので、たとえばレジスタの退避や復旧、割り込み許可(GIE)ビットのセットなど Cプログラム上では不要です。

割り込みを使う上で注意すること

wait_msec関数はウェイト時間(単位:msec)をセットし、割り込み処理関数timerで1msecおきにdown1をカウントダウンします。 ここでdown1はunsigned intで2バイト使っています。 2つのバイトに値をセットしている途中で割り込みが入るとやっかいなことになります。 それを避けるために、いったん割り込みを禁止しておいて、2バイトに値をセットした後に割り込みを許可するという、以下のような手順もありです。

	T0IE = 0;			// カウント停止
	down1 = msec;			// カウント値をロード
	up1 = 0;			// msec以下をクリア
	T0IE = 1;			// カウント再開

しかし、ここでは別の方法を採っています。 関数 wait_msecではcountvalueにカウント値をセットした後、1バイトのフラグ loadflagを立て、 割り込み処理関数 timer側では loadflagを見てカウント値を取り込みます。

wait_msec関数ではdown1が0になるのを待ちます。 ここでもdown1の2つのバイトをチェックしている間に割り込みが入ると判定を誤ります。 それを避けるためにwait_msec関数で1バイトのフラグ waitflagをセットし、 割り込み処理関数timerでdown1が0になるとフラグ waitflagをクリアします。

void wait_msec( unsigned int msec) {
	if ( msec == 0 ) { return; }	// 0 msec なら何もしない
	countvalue = msec;		// カウント値をセット
	loadflag = 1;			// ロード指令
	waitflag = 1;			// ウエイト中を示すフラグ
	while ( waitflag ) { ; }	// アンダーフローまで待つ
}

できるだけ1バイトで済むように構成するというのが良い方法ですが、 2バイト以上のデータを割り込み処理関数との間でやりとりしなければならないときは、 ここで示したように1バイトのフラグを用いて通信するとよいでしょう。

static unsigned char volatile loadflag, waitflag;
static unsigned int volatile brink, countvalue;
static unsigned int up1, down1;

この例のように割り込み処理関数とその他の関数とで共通に使う変数は static volatile として宣言しておかなければなりません。 volatileはコンパイラに最適化をさせないための宣言です。 そうしておかないと、変数waitflagに1をセットし、それが 0になるのを待つというようなプログラムはエラーになります。

以上を踏まえた割り込み処理関数timerは次のようになりました。

static unsigned char volatile loadflag, waitflag;
static unsigned int volatile brink, countvalue;
static unsigned int up1, down1;

/* interrupt process */
static void timer() interrupt 0 {
	T0IF = 0;		// 割り込みフラグクリア
	/*** load down counter ***/
	if ( loadflag ) {
		down1 = countvalue;	// カウント値(msec)をロード
		up1 = 0;		// msec未満をクリア
		loadflag = 0;
	}
	/*** msec timer ***/
	if ( ++up1 > MSEC ) {
		up1 = 0;
		if (down1) { --down1; }
	}
	/*** アンダーフロー時の処理 ***/
	if ( down1 == 0 ) {
		if ( waitflag ) {		// ウェイト中(点滅しない)
			waitflag = 0;		// アンダーフローを伝えるフラグ
		} else if ( brink > 0) {
			/*** LED 点滅の処理 (省略) ***/
		}
	}
}

ところで、このmsecタイマーは正確ではありません。20MHzのクロックを20*4*256で割っているためです。 20.48MHzのクロックを用いると正確な1msecが得られます。

wait_msec関数が採る引数(msec単位)は unsigned int なので最大は 65535すなわち1分ちょっととなりますが、 ウォッチドッグタイマーを使っていると、そのオーバーフローが制限になります。 このサンプルでは128:1のポストスケーラーを付けており、およそ500msecが上限となります。

インライン・アセンブラ

NOP命令など、どうしてもアセンブラでしか書けない命令もあります。 アセンブラ命令を __asm__endasm;で囲むことで これをCのプログラム内に書くことができます。 次はよく使うアセンブラ命令をプリプロセッサ命令 #define を使って定義したものです。 これらをプログラム冒頭に書いておくとよいでしょう。 ヘッダにしておくというのも良いですが。

/*** usefull assembler codes ***/
#define Nop()           do { _asm nop _endasm; } while(0)
#define ClrWdt()        do { _asm clrwdt _endasm; } while(0)
#define Sleep()         do { _asm sleep _endasm; } while(0)

#define EEPROM_WRITE()	do {				\
	EECON2=0x00;	/* Get in right bank */		\
	__asm	MOVLW	0x55		__endasm; 	\
	__asm	MOVWF	EECON2		__endasm; 	\
	__asm	MOVLW	0xaa		__endasm; 	\
	__asm	MOVWF	EECON2		__endasm; 	\
	__asm	BSF	EECON1,1;	__endasm;	\
	} while(0)

/***  word <--> byte  ******************/
struct hl {
	unsigned char low;
	unsigned char high;
};
union uword {
	struct hl bytes;
	unsigned short word;
};
static union uword work;
*****************************************/

2バイトを繋げて16ビットに、または16ビットを分解して2バイトにしたいとき、Cでは
value = higherbyte;
value = value<<8 | lowerbyte;
などとも書けますが、構造体と共用体を用いた上記の word <--> byte以下の宣言をしておくと、たとえば次のようにできます。 (今回のサンプルプログラムでは使っていない。)

    unsigned char lowerbyte, higerbyte;
    unsigned int value;
    work.bytes.low = lowerbyte;
    work.bytes.high = higherbyte;
    value = work.word;

入力信号の積分

今回のものは、押しボタンスイッチを押すごとに点滅周期を変えるというトグル動作に使っています。 このような使い方では、スイッチのチャタリング(バウンスによる断続)やノイズなどで誤動作が起きやすくなります。 昔の回路ですとここにコンデンサと抵抗で構成する積分回路と称するものを挿入するのですが、 マイコンを使うならソフトウェアで対応することができます。 チャタリング防止には、入力を受け付けた後に100msec程度のウェイトを入れるというのが一般的です。 ノイズによる誤入力はこれでは防げません。 次のように入力を積分するとうまくいきます。

#define SW1	(RB7 == 0)	// negative logic
#define SMOOTH	100		// SW1入力の平滑度 (1-126)

static signed char swcount;
static unsigned char swflag;

unsigned char sw() {
	/*** SW1の状態をカウントすることで積分の効果 ***/
	if ( SW1 ) {
		if ( ++swcount >= SMOOTH ) {
			swcount = SMOOTH;
			swflag = 1;
		}
	} else {
		if ( --swcount <= 0 ) {
			swcount = 0;
			swflag = 0;
		}
	}
	return swflag;
}

積算値を保持する swcountは signed char (-128から+127) なので、SMOOTHは126までの値で指定します。 これによって生じる遅れは、sw()を参照するメインループの周期のSMOOTH倍ということになります。

関連ページ

ネット上の参考情報

ご意見、お問い合わせは Linux掲示板


shino's bar goto [シノバー店内案内] [お家で使うLinux]