JavaScriptでわかる!組込みプログラミングの神髄

第2回並列処理プログラムを見やすく書く方法

前回は、一続きの処理をタイマー割り込みを使って分割して行うサンプルを紹介しました。このような処理は、JavaScriptや組込みプログラミングなど、さまざまな場面で必要になります。

今回は、より複雑な処理を見やすく書く方法について説明します。

オートリピートをどう作るか

さっそくサンプルプログラムを見てみましょう。これはメトロノームで、数字は1分間の拍数(いわゆるBPM)です。

リスト1 metronom.html
<HTMLM><HEAD><TITLE>metronom</TITLE>
<SCRIPT type="text/javascript"><!--

var	tempo = 0;
var	accval = 0;
var	arm = 0;
var	t0_interval = 50;
var	repeat_delay = 0;
var	repeat_val = 0;

function	update_tempo(val)
{
	tempo += val;
	if (tempo < 10)
		tempo = 10;
	else if (tempo > 300)
		tempo = 300;
	document.getElementById("tempo").innerHTML = tempo + "";
}

function	int_t0()
{
	accval += tempo;
	if (accval >= 60000 / t0_interval) {
		accval -= 60000 / t0_interval;
		arm ^= 1;
		document.getElementById("box").style.marginLeft = (arm)? "0px" : "200px";
	}
	if (repeat_delay <= 0)
		;
	else if (repeat_delay < 10)
		repeat_delay++;
	else
		update_tempo(repeat_val);
}

function	press_plus()
{
	update_tempo(repeat_val = 1);
	repeat_delay = 1;
}

function	press_minus()
{
	update_tempo(repeat_val = -1);
	repeat_delay = 1;
}

function	release()
{
	repeat_delay = 0;
}

function	init()
{
	tempo = parseInt(document.getElementById("tempo").innerHTML);
	setInterval(int_t0, t0_interval);
	document.onmouseup = release;
}

// --></SCRIPT>
</HEAD><BODY onload="init()">
<H1>metronom</H1>

<DIV id="box" style="margin-left:0px; width:50px; height:50px; background:#f00;">
</DIV>

<P><BIG><BIG>
<SPAN onmousedown="press_minus()">[-1]</SPAN>
<SPAN id="tempo">120</SPAN>
<SPAN onmousedown="press_plus()">[+1]</SPAN>
</BIG></BIG></P>

</BODY></HTML>

[-1]や[+1]のボタンを押し続けると、連続して数字が変わるようになっています。いわゆるオートリピートです。ソースを見る前に、どうやって実現するかを考えてみてください。ヒントですが、ボタンが離されたときに発生する割り込みを利用しています。

リピート処理をしているあいだ、メトロノームの動作が止まってもいいのであれば、処理を並列的に行う必要はありません。しかし、リピートしているあいだもメトロノームの動作を続けたいのであれば、タイマー割り込みの中で2つの処理を行う必要があります。このような場面は、プログラムを書いていればいくらでもでてきますね。

まずメトロノームの処理ですが、50ミリ秒のタイマー割り込みをそのまま使うと、飛び飛びの間隔しか実現できません。たとえば毎分60拍の場合は、タイマー割り込み20回で1拍になります。その1つ上は、タイマー割り込み19回で1拍ですが、これは毎分に換算すると約63拍です。つまり、その中間の毎分61拍という指定はできないことになります。これではメトロノームとして困りますね。

そこで解決策として、拍数を変数に積算していき、変数がオーバーフローしたときに拍を刻むようにします。拍数が大きければ、頻繁にオーバーフローすることになります。たとえは毎分60拍のとき、50ミリ秒ごとに変数に60を加算すると、1秒で1200増えます。このときに拍を刻んで変数を1200減らせば、余った分がきちんと残るようになります。

毎分61拍の場合、先ほどのやり方では正確に刻むことはできませんでした。しかし積算方式であれば、まず20回目の割り込みで1220になります。ここで拍を刻んで1200を引くと、20残ります。次は、通算40回目の割り込みで1240になり、拍を刻んで40残ります。次は60回目で1260になり、60残ります。そして、79回目で1219になり、19残ります。このように、自動的に20回と19回が組み合わさって、平均すると毎分61拍が刻めることになります。

次に、これと平行して、オートリピートの処理を行います。オートリピートは、スイッチを押すとまず1回カウントされ、押し続けると連続でカウントされるものです。キーを押し続けると連続で文字が入力されますが、これをマウスでもおこなおうというものです。

マウスの場合、押したときに一度割り込みが発生しますが、そのあとは何も起きません。従って、押したときにグローバル変数をセットしておいて、タイマー割り込みでリピート処理をおこないます。タイマー割り込みをそのまま使うと、押した瞬間から毎秒20カウントのスピードで増えてしまいますので、最初の10カウントは無視するようにします。また、マウスを離したら、この処理を止めるようにします。

グローバル変数に保存するのは、マウスを押してからどれくらい経ったかと、押されたのがどのボタンかの2つです。前者は、最初の10カウントを無視するのに使います。後者は、リピートのときに数を増やすのか減らすのかを判断するのに使います。

こうして、2つのタイマー処理が並列に行えるようになりました。

グローバル変数を減らす

さて、たった3つの処理を平行して行うだけなのに、プログラムを見るとずいぶん複雑になってしまいました。もちろん、ここでマルチタスクとかスレッドを導入するのもエレガントですが、たかがメトロノームに16bitのプロセッサとか4KBのメモリを使えるわけがありませんし、大きな電池がないと動かないとしたらマイナスです。

このまま処理の数が増えていったら、どんな問題があるでしょう。個人的には、グローバル変数が気になります。電源スイッチの長押しとか、いろいろ追加していったら、どの変数がどの処理に使われているかがわかりにくくなり、ミスの原因になりそうです。

では、グローバル変数はなぜ必要になるのでしょうか。それは、2つ以上の関数が1つの変数を共有したいからです。であれば、2つの関数でだけ共有できる変数を使えば、グローバル変数を減らすことができます。その具体的な方法と、それによってプログラムがどう変わるかを、見ていきましょう。

C言語の場合

組込みシステムではC言語をよく使います。C言語の場合、スコープをうまく使うと、グローバル変数を減らすことができます。今回は、関数内のstatic変数を使用します。

関数(またはさらに内側のスコープ)でstatic宣言された変数は、関数からリターンしても値が保存されます。これをグローバル変数のかわりに使います。そして、2つの関数を1つの関数にまとめ、引数で動作を変えるようにします。

 /* mode=0:repeat 1:+=val -1:release */
 static void button_proc(int mode, int val)
 {
   static int dir = 0;
   static int delay = 0;
   
   if (mode < 0) {
     delay = 0;
     return;
   }
   if (mode > 0) {
     dir = val;
     delay = 1;
   } else if (delay <= 0)
     return;
   else if (delay < 10) {
     delay++;
     return;
   }
   update_tempo(dir);
 }

前掲のサンプルとの整合性を優先したため、ちょっと不自然なところもありますが、やっていることは単純です。タイマー割り込みでは、50ミリ秒ごとにbutton_proc(0, 0)を呼び出します。ボタンが押されたらbutton_proc(1, 1)、離されたらbutton_proc(-1, 0)です。こうすることで、リピート関係の変数を、関数の中に閉じ込めることができました。

この方法はいつでも利用できるというわけではありませんが、変数のスコープを小さくするのに役立ちます。また、似たような処理がいくつもあったときに、グローバル変数の名前を衝突しないように割り当てる必要もなくなります。

JavaScriptの場合

一方、JavaScriptではクロージャという仕組みを使うと、イベント処理をきれいに書くことができます。クロージャは動的な関数の定義で、関係のあるコードをまとめて書くことができます。

クロージャの典型的な使い方は、以下のようなものです。sx、syの変数に注目してください。

 function 図形上でプレス()
 {
   var sx = x座標;
   var sy = y座標;
   document.onmousemove = function() {
     var x移動量 = x座標 - sx;
     var y移動量 = y座標 - sy;
     /* 移動処理 */
   };
   ducument.onmouseup = function() {
     document.onmousemove();
     /* リリース処理 */
     document.onmousemove = null;
     document.onmouseup = null;
   };
   document.onmousemove();
 }

「図形上でプレス」の関数が呼ばれると、上記のプログラムが1行ずつ実行されていきます。function() {....}という部分は関数の動的な宣言で、C言語でいうところの関数ポインタが得られます。このとき、⁠図形上でプレス」関数内の変数であるsxとsyは、function()で宣言されつつある関数の中でも使うことができます。呼び出し元ではなく、宣言文が実行される時のスコープであることに注意してください。

では、これをメトロノームに応用してみましょう。

リスト2 metronom_j.html
<HTML><HEAD><TITLE>metronom</TITLE>
<SCRIPT type="text/javascript"><!--

var	tempo = 0;
var	onrepeat = null;

function	update_tempo(val)
{
	tempo += val;
	if (tempo < 10)
		tempo = 10;
	else if (tempo > 300)
		tempo = 300;
	document.getElementById("tempo").innerHTML = tempo + "";

}

function	press_plus()
{
	var	delay = 0;
	onrepeat = function(force) {
		if ((force)||(delay >= 9))
			update_tempo(1);
		else
			delay++;
	};
	document.onmouseup = function() {
		onrepeat = null;
	};
	onrepeat(1);
}

function	press_minus()
{
	var	delay = 0;
	onrepeat = function(force) {
		if ((force)||(delay >= 9))
			update_tempo(-1);
		else
			delay++;
	};
	document.onmouseup = function() {
		onrepeat = null;
	};
	onrepeat(1);
}

function	init()
{
	var	t0_interval = 50;
	var	accval = 0;
	var	arm = 0;
	
        tempo = parseInt(document.getElementById("tempo").innerHTML);
	setInterval(function() {
		accval += tempo;
		if (accval >= 60000 / t0_interval) {
			accval -= 60000 / t0_interval;
			arm ^= 1;
			document.getElementById("box").style.marginLeft = (arm)? "0px" : "200px";
		}
		if ((onrepeat))
			onrepeat(0);
	}, t0_interval);
}

// --></SCRIPT>
</HEAD><BODY onload="init()">
<H1>metronom</H1>

<DIV id="box" style="margin-left:0px; width:50px; height:50px; background:#f00;">
</DIV>

<P><BIG><BIG>
<SPAN onmousedown="press_minus()">[-1]</SPAN>
<SPAN id="tempo">120</SPAN>
<SPAN onmousedown="press_plus()">[+1]</SPAN>
</BIG></BIG></P>

</BODY></HTML>

onrepeatというグローバル変数は、リピート処理のための関数を定義しておくと、タイマー割り込みから呼び出されるようにします。処理の数が増えた場合でも、onrepeat変数は1つだけで構いません。

ボタンが押されたら、リピート処理の関数を宣言してonrepeatに登録します。そして、最初の1回を直接呼び出します。これは、+1か-1かの設定を2ヵ所に書きたくなかったというのが理由なので、onrepeatを呼び出すかわりにupdate_tempo()を呼んでも構いません。delayという変数に、押されてから何回目の割り込みかが保持されており、クロージャと共有しています。

また、マウスが離されたときに、document.onmouseupを直接クリアすることで、関数を減らしています。これで、リピートの間隔がちょっと違うボタンなどを、簡単に追加できるようになりました。

次回は処理の分割について、もっと違う種類のものを見ていくことにしましょう。

おすすめ記事

記事・ニュース一覧