2001/05/27 【ソフトウエア編TOPに戻る】
109.パソコンとシリアル通信するのページでは、シリアルポートを使った簡単なデータ通信の方法についてご紹介しました。シリアル通信では、基本的には1文字分(8ビット、1バイト)のデータしかやり取りすることが出来ず、複数の連続した文字データをやり取りするには、ソフトウエア上の工夫が必要でしたが、その方法についても簡単にご紹介しました。
しかし、109.のページでご紹介した受信関数には、ある弱点がありました。それは、受信を待っている状態の時、相手からデータが送られてこない間は何もできないということです。一旦受信の待機状態に入ったら、ひたすらデータが送信されてくるまでただボーっと時間を潰しているしかなかったのです。CPUボードには沢山の仕事をしてもらわなければならないのに、これでは効率が悪すぎますね。
相手先からの受信は、SSR(シリアルステータスレジスタ)のRDRF(レシーブデータレジスタフル)のビットが1であることを確認すれば検知できるのでした。なので、定期的にこのビットを見に行って、受信があるかどうかを確認する方法(ポーリング)も考えられますが、いつ来るデータかも分からないのに、いつも気にしていなければならないというのも大変です。
その解決策として、今回はデータを受信した時に発生する割り込みを利用してみます。これが出来ると、普段はロボットに何か仕事をさせておいて、パソコンから命令を与えた時だけ特別な仕事をさせる、といった制御が効率よくできるようになります。
ここでは、以下の内容についてご紹介しています。
110−3.スタートアップオブジェクトstartup3.marの説明
以下に割込み受信テスト用プログラムsertest2.cをご紹介します。このプログラムは、メイン関数、割込みによる1文字受信関数、文字列読み込み関数が主なところで、それ以外の関数類は、以前にご紹介してあるものばかりです。
今回は割込みによるシリアルデータ受信という機能が主役になりますが、割込み処理については105.タイマー割込みで効率的な制御のページなどで既に試しているため、全然大した事はありません。むしろ、今回の要点は以下の点にあります。
■1という文字と、1という数字の違い → ASCIIコードと数値の変換
■割込み処理での文字列の受信 → C言語のポインタの使い方
■グローバル変数のメモリ上への領域の確保 → startup.marでの処理
以下のプログラムでは、再びパソコンのハイパーターミナルと通信を行います。ハイパーターミナルから1,2,3の各キーを押すと、それに応じてAKI-H8開発ボード側のLEDの点滅速度が変わります。また、S のキーを押すと、今度は文字列入力モードに入ります。パソコン側から文字列を入力して行きリターンキーを押すと、AKI-H8側から入力された文字列を一括でパソコンに返送します。
今回作った割込みによる1文字受信関数、文字列読み込み関数は、今までと違って汎用性のある関数ではありません。なので、プログラムの動作を理解する上での参考例としてお考え下さい。
ソースプログラムは、ここをクリックするとダウンロードできます。(sertest2.c) 以下に掲載しますソースリストには、時間稼ぎ関数、シリアル通信の基本関数、液晶表示関数の各関数類を省いています。ダウンロードできるファイルには全て記入されているので、確実に動作します。
また、ハードウエアについては、電子回路編 3.AKI-H8開発キットの回路を参照して下さい。
なお、スタートアップオブジェクトは、今回割込みを利用しているので、このあとご紹介するstartup3.marでないと動作しません。startup3.marはstartup.marに名前を変更してから、ソフトウエア編の3.コンパイル手順 3−1.スタートアップオブジェクトを作るを参照して、startup.objファイルを作成し、sertest2.objとリンクして下さい。
|
/***************************************************/
|
先ずはじめに、以下の宣言を行っています。
#include <3048f.h>
これはおなじみ。3048fヘッダファイルの読み込みです。
#pragma interrupt(intrxi1) /*割込み関数指定 */
これはintrxi1という関数を割込みで起動するという宣言です。startup.mar(startup3.mar)の中でも宣言しています。
extern char RXD1;
/*SCI1 受信データ グローバル変数 */
extern char STR;
/*文字列の最初の文字 グローバル変数*/
これは、プログラム内の全てで使用できる変数の宣言です。これもstartup.marの中で宣言しています。
メイン関数は以外とソースリストが長いのですが、主な処理部はほんの一部です。あとは全て初期化やパソコンへのメッセージ送信です。
int i,input;
/*変数の宣言
*/
P3.DDR = 0xff;
/*port3出力に設定 液晶表示制御用 */
P5.DDR = 0xff;
/*port5出力に設定 LED表示用 */
timer_init();
/*タイマー0の初期化 */
timer1_init();
/*タイマー1の初期化 */
lcd_init();
/*液晶表示器の初期化 */
sci1_init();
/*シリアルポート1の初期化 */
RXD1 = '1';
/*受信データグローバル変数初期化 */
input = 1;
/*RXD1に合わせて1にしておく */
この部分では、変数の宣言や、各種ポートの初期化、変数の初期化を行っています。RXD1とinputの各変数を1に初期化していますが、その理由はあとで説明します。
なお、注意して見ると、timer_init();とtimer1_init();という2個の時間稼ぎ用タイマーの初期化をしています。timer1_init();の方は初めてのお目見えです。今回のプログラムでは、通常のお仕事として開発ボードのLEDを点滅させますが、その周期を決定するための時間稼ぎと、それに平行して液晶表示器などのハードウエア制御用に使う時間稼ぎの2種類が必要なため、別々のITUを使って時間稼ぎをさせています。ダウンロードできるソースファイルの一番最後に関数を載せてありますので、参照して下さい。
/*パソコン画面表示*/
sci1_strtx("[COMMAND MANUAL]");
/*パソコンのメッセージ送信 */
sci1_tx('\r'); /*パソコンカーソル位置先頭 */
sci1_tx('\n');
/*パソコン改行 */
sci1_strtx("INPUT 1 key : LED1/2 blink FAST");
sci1_tx('\r');
sci1_tx('\n');
sci1_strtx("INPUT 2 key : LED1/2 blink MIDDLE");
sci1_tx('\r');
sci1_tx('\n');
sci1_strtx("INPUT 3 key : LED1/2 blink SLOW");
sci1_tx('\r');
sci1_tx('\n');
sci1_strtx("INPUT s key : input strings : Enter finish");
sci1_tx('\r');
sci1_tx('\n');
sci1_tx('\n');
/*液晶表示 */
lcd_locate(0,1);
/*液晶表示位置指定 */
lcd_print("READY GO!");
/*入力文字の表示 */
この辺は、やたらと長ったらしいのですが、要はパソコンと液晶表示器にメッセージを表示させるための処理です。AKI-H8の電源を入れた時に、パソコンとの通信がうまく動いているよ、という確認の意味と、ソフトの取扱い説明を兼ねてユーザーインターフェイスを向上させています。
/* シリアルポート1割込み許可 */
SCI1.SCR.BIT.RIE = 1; /* 受信割込み許可 */
メイン関数での一番のポイントはここです。ここでシリアルポートからの割込みを許可しています。
SCR(シリアルコントロールレジスタ)には、以下の意味があるのでした。
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| TIE | RIE | TE | RE | MPIE | TEIE | CKE1 | CKE0 |
ここで、ビット6のRIEを1にすると、受信割込み発生が許可されます。ここでは、3048f.hのヘッダファイル上に宣言されているSCI1.SCR.BIT.RIE を使ったビット処理で、直接RIEビットを1にしています。SCI1.SCR.BYTE |= 0x40; としても、同様の結果が得られると思いますが、SCI1.SCR.BIT.RIE = 1;とした方が意味がわかり易いですね。
/* メイン関数での主なお仕事はLEDの点滅だけ */
while(1){
input = RXD1 & 0x0f; /*アスキーコード→数字変換 */
P5.DR.BYTE = 0x01;
/*LED5 ○●表示 */
wait1(100*input);
/*LED点灯周期設定 */
P5.DR.BYTE = 0x02;
/*LED5 ●○表示 */
wait1(100*input);
/*LED点灯周期設定 */
}
さて、ようやくメイン関数での主なお仕事の部分です。ここでは、ポート5に接続されているLED1と2の点滅を繰り返しているだけです。それ以外は何もしていません! ただし、100*inputという時間周期で点滅させています。先ほどinputは 1 に初期化したので、100*inputは100となり、100ミリ秒(0.1秒)の間隔で点滅を繰り返すことになります。ところで、inputはinput = RXD1 & 0x0f;であると書かれています。これは一体何をしているのでしょうか?
RXD1も、先ほどinputと一緒に '1' に初期化しました。ここで先ほどの初期化を良く見て頂きたいのが、RXD1 = 1; ではなくRXD1 = '1'; としている点です。何が違うか分かりますか?1を ' (シングルクォーテーション)で囲っているのです。
ただの1だと数字の1として扱われ、シングルクォーテーションで囲うと、文字(1文字分)として扱われます。「数字と文字と何が違うんだ!1は1じゃないか!」と怒られそうですが、違います。
例えばパソコンのハイパーターミナルに「1」と表示させるために、sci1_tx(1); としても、画面には「1」と表示してくれません。ハイパーターミナルには、表示させたい文字コード(アスキーコード ASCII)を送ってあげないといけないのです。sci1_tx('1'); とすれば、C言語のコンパイラは1という文字だと判断して、1という文字に相当するASCIIコードを送ってくれるようになります。これは、液晶表示器に対しても同じことが言えます。つまり、シリアル通信経由で、キーボードから1のキーが叩かれた時に送られてくるデータは、1という数字ではなく、1というASCIIコード(文字コード)なのです。
ASCIIコードにはアルファベットや半角カタカナ、簡単な記号などが含まれますが、数字については以下のようなコードになっています。
| 文字 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| ASCII | 30h | 31h | 32h | 33h | 34h | 35h | 36h | 37h | 38h | 39h |
なので、31h (0010 0001)というコードが、1という文字を表します。
前置きが長くなりました。メイン関数上のinput = RXD1 & 0x0f; の意味ですが、RXD1というデータの上位4ビットをマスクして0(ゼロ)にしなさいという意味です。後で説明しますが、RXD1はシリアル通信によってパソコンから送られてくるデータで、中身はASCIIコードです。ここで、送られてきたデータが0〜9などの数字を示す文字だった場合、上位4ビットを0(ゼロ)にすることで、数字に変換されるのです。
しつこいようですが、ASCIIコードは普通16進数で表記するので31hと書きますが、これは10進数で言うと49と同じことです。つまり、1という文字は、ASCIIコード上では49という数字で表現されるのです。なので、1を示すASCIIコードから48という数字を引くと、1という数字(01h)となります。これは文字ではなく数字なので、計算に使用することができます。input = RXD1 & 0x0f; ではこれと同じ原理で文字と数字を変換しているのです。 大分しつこく書きましたが、文字と数字の違いが分かりましたか? これをごっちゃにして考えると、後のプログラムがうまく動作しないので、とても基本的で重要な考え方です。
この関数は、シリアルポート1からデータの受信があった場合に、自動的に起動する割込み関数です。
char data;
/* 受信データ変数の宣言 */
switch(SCI1.SSR.BIT.RDRF)
/* 受信状態判定 */
{
case 1:
/* RDRF=1 正常受信 */
data = SCI1.RDR; /* data 取り出し */
SCI1.SSR.BIT.RDRF = 0; /* 受信フラグクリア */
break;
default:
/* エラー発生時 */
SCI1.SSR.BYTE &= 0xc7; /* エラーフラグクリア */
return;
}
まず始めに、dataというchar型変数を宣言し、switch文でSSR(シリアルステータスレジスタ)のRDRF(レシーブデータレジスタフル)ビットの状態を見て、次に行なう処理を決めます。RDRFが1の場合、正常にデータ受信が行なわれていることを示しているため、RDR(レシーブデータレジスタ)の値を、先ほど宣言した変数dataで格納します。その後、次の受信に備えてRDRFビットをクリアしておきます。
switch文でRDRFが1ではなかった場合、受信エラーが発生したことを示しているため、エラーフラグを全てクリアして、return;で処理を終了します。
ここの部分の詳細な動作説明は、109.パソコンとシリアル通信するの109−3−2.sci1からの1文字受信関数を参照して下さい。データが正常に受信されている場合、次の処理に入ります。
/* 入力コマンド判定処理 */
switch(data)
/*入力文字判定 */
{
case '1':
case '2':
case '3':RXD1 =
data; /*グローバル変数への代入*/
sci1_tx(data); /*入力データをPCに返送
*/
sci1_tx('\r'); /*リターン行頭へ移動 */
lcd_locate(0,0); /*液晶表示位置指定 */
lcd_write4(data,1); /*入力文字の表示 */
break;
case 's':
case 'S':str_read();
/*文字列入力処理*/
break;
}
こでは、入力された(キーボードで押された)データによって、次に行なう処理を決めています。1か2か3が入力された場合、入力されたデータでグローバル変数RXD1を書き換えます。その後、入力されたデータをパソコンに返送し、液晶表示器にも送っています。
s(小文字)かS(大文字)が入力された場合、str_read( );関数を実行します。この関数については、この後すぐご説明します。
switch文で、caseの後に記入されているパラメータは、全てシングルクォーテーションで囲われていることをお忘れなく。
ところで、ここで書き換えられたRXD1のデータは、割込み処理が終わってメイン関数に処理が戻ると、ハードウエアの動きに反映します。つまりここでは、LEDの点滅速度が変わります。しかしメイン関数にとって、RXD1の値がどこで変わったかということなど分からないのです。お仕事中に頭を殴られて気絶しているうちに、目の前の仕事道具を入れ替えられて、目を覚ましたら何事も無かったかのように、再び目の前の仕事道具を使ってお仕事を再開しているようなものです。メイン関数はRXD1の値を使うだけで、変更することなど普段から何も考えていないのです。なので、メイン関数にとって非常に処理が楽だということになるのです。
先ほどの割込み受信関数intrxi1();では、RXD1というグローバル変数を書き換えて、メイン関数の動作に影響を与えました。割込み関数は、メイン関数から引数をもらって呼び出される訳ではないため、メイン関数との間でデータの受け渡しをする場合は、グローバル変数を使うしかありません。グローバル変数とは、プログラム内の全ての関数から共通に参照、書き換えができる変数のことで、ハード的に言えば同じメモリーアドレスのデータをみんなで読み書きしているということです。なので、データひとつだけを扱うのは比較的簡単にできます。しかし、文字列のように複数のデータが集まって初めて意味を持つデータ郡を扱うにはどうしたら良いのでしょうか?
割込みでグローバル変数の操作を行なうためには、startup.marで変数の宣言(メモリー領域の確保)を行なわなければなりません。しかしアセンブラ言語であるstartup.marでは配列変数の宣言などといった高級なことはできません。できるのは、変数名(領域名)と、確保する領域の大きさの指定だけです。
今回この関数では、グローバル変数に文字列を格納しています。そのためにポインタという手法を使います。
文字列読み込み関数str_read();では、パソコンから送信されてきたデータを、1文字ずつ外部変数STRに保存して行き、リターン入力で保存処理を終了します。ただし、変数STRは配列変数ではないため、char型の変数1文字分のデータしか扱えません。そこでポインタの登場です。
char rx_data;
/*受信データ */
char *p;
/*ポインタ変数 */
SCI1.SCR.BIT.RIE = 0;
/* 受信割込み禁止 */
p = &STR;
/*STR変数のアドレスにする */
先ず始めに、受信データを一時的に格納するための変数rx_dataをchar型で宣言します。次に、ポインタ変数 *pを、char型で宣言します。
SCI1.SCR.BIT.RIE = 0;
これで受信割込みを禁止します。ここでは、パソコンから複数の文字データを受け取り、リターンが押されるまでの間、割り込み発生を禁止しています。
p = &STR;
これが非常に重要です。これは、先ほど宣言したポインタ変数*pを使って、グローバル変数STR用に確保してあるメモリー領域を自由に操作するためのものです。*p自体はchar型の文字データ1文字分を扱える変数ですが、pはそのデータが格納されているメモリー上のアドレスを示します。また、STRはchar型のグローバル変数で、やはり文字データ1文字分を扱える変数ですが、&STRとするとSTR変数のメモリー上のアドレスを示します。つまり、p = &STR; とすることにより、ポインタ変数*pは、グローバル変数STRと同じメモリー領域を直接操作することが出来るようになるのです。
sci1_strtx("Strings Input Mode");
/*メッセージをPCに送信 */
sci1_tx('\r');
/*カーソル行頭 */
sci1_tx('\n');
/*改行 */
lcd_locate(0,1);
/*液晶表示位置指定 */
lcd_print(" "); /*液晶2行目クリア */
lcd_locate(0,1);
/*液晶表示位置指定 */
ここでは、パソコンにメッセージを表示させ、液晶表示器の2行目を16文字分のスペースで埋めて見かけ上クリアさせています。
do{
rx_data = sci1_rx(); /*
1文字受信 */
if(rx_data != '\r'){
lcd_write4(rx_data,1); /* 1文字液晶表示 */
*p = rx_data;
/* 1文字を文字列に格納 */
sci1_tx(*p);
/* 1文字送信 */
p++;
/* 次の文字アドレスへ */
}
}while(rx_data != '\r');
/* リターン入力を検出 */
*p = '\0';
/* 文字列最後はNULL */
実際のポインタを使った処理は、ここで行なっています。do{ }while(rx_data != '\r')は、受信データがリターン以外の間、{ }の中の処理を繰り返します。
rx_data = sci1_rx(); で割込みではない通常の文字受信を行なって、データをrx_dataに格納します。lcd_write4(rx_data,1); で受信した文字を液晶表示器に表示させ、*p = rx_data;で受信したデータを*pに格納します。ここで、*pはグローバル変数STRと同じアドレスの変数に設定してあるため、STRの値を書き換えたと同じことになります。sci1_tx(*p); で今格納したデータをパソコンに返送します。そして、p++; ですが、これは p = p + 1 と同じ意味です。p とは変数*p(STR)のアドレスを示すので、アドレスをひとつ増やす、つまり次の文字を格納する場所に移動する、ということになります。この処理の繰り返しによって、グローバル変数STRのアドレスを先頭にして文字列データが順々に格納されて行きます。
リターンが押されると、if(rx_data != '\r'){ }のif文よって、データの格納は行なわれず、pのアドレスは最後の文字が格納された次の場所を示しているため、そのまま*p = '\0'; を実行して、文字列データを締めくくります。
ここでひとつの疑問。わざわざp++などとしないで、&STR++とすれば、直接STR変数の次のアドレスが指定できるじゃん、と思いませんか?そうすれば、わざわざchar *pのポインタ変数なんか使わなくて良いと。実際にそういうプログラムを書いてもエラーになります。それは、&STRというのは変数ではなく、決まったアドレスの値を示す定数だからです。なので、直接定数を足したり引いたりできないのです。やっぱりポインタ変数*pは必要でした。
sci1_tx('\r');
/*カーソル行頭 */
sci1_tx('\n');
/*改行 */
sci1_strtx(&STR);
/*文字列をPCに送信 */
sci1_tx('\r');
/*カーソル行頭 */
sci1_tx('\n');
/*改行
*/
SCI1.SCR.BIT.RIE = 1;
/* 受信割込み許可 */
return;
文字列が入力し終わると、sci1_strtx(&STR); で、今保存した文字列データを一気にパソコンに返送します。文字列送信関数sci1_strtx( )の引数として、&STR(STR変数のアドレス)を渡している点にご注意下さい。
最後にSCI1.SCR.BIT.RIE = 1; で再び割込み許可の設定に戻します。
110−3.スタートアップオブジェクトstartup3.marの説明
今までご紹介したsertest2.cのプログラムは、このスタートアップオブジェクトソースstartup3.marとリンクさせないと動作しません。このスタートアップでは、sci1からの受信割込みが有効となるように割込みベクタテーブルの設定を行い、sertest2.c上で使用しているグローバル変数の宣言や領域確保などを行なっています。
startup3.marはここをクリックするとダウンロードできます。ダウンロード後はstartup.marに名前を変更してから、ソフトウエア編の3.コンパイル手順 3−1.スタートアップオブジェクトを作るを参照して、startup.objファイルを作成し、sertest2.objとリンクして下さい。
| ;STARTUP ROUTINE H8/300H ; .cpu 300ha:20 ;H8/300H advanced 1Mbyte MODE .import _main ;外部関数mainを定義 .import _intrxi1 ;外部割込み関すを定義 .export _RXD1 ;グローバル変数を宣言 .export _STR ;グローバル変数を宣言 ; .section vect,data,locate=h'00000; ; ;割り込みベクトルテーブル .data.l init ;1 reset vectのジャンプ先ラベル指定 ; .org h'00E4 ;rxi1 sci1の受信割込み時のアドレス .data.l _intrxi1 ;受信割込み時のジャンプ先ラベル指定 ; .org h'00100 ; init: mov.l #h'fff10,er7 ;SPの設定 ldc #0,ccr ;CLEAR INTERRUPT MASK,NOT USE UI BIT jmp @_main ;関数mainへのジャンプ .section D,DATA,locate=h'fef10; _RXD1: .res.w 1 _STR: .res.b 16 ; .end |
以下に主要部分だけご説明します。
.import _main
;外部関数mainを定義
.import _intrxi1
;外部割込み関すを定義
.export _RXD1
;グローバル変数を宣言
.export _STR
;グローバル変数を宣言
ここで、C言語プログラムで使用する関数(外部関数)とグローバル変数の宣言を行ないます。
.org h'00E4
;rxi1 sci1の受信割込み時のアドレス
.data.l _intrxi1
;受信割込み時のジャンプ先ラベル指定
sci1で受信割込みが発生すると、00e4番地のアドレスに書き込まれているアドレスにジャンプすることが決められています。ここでは、00e4番地にintrxi1というラベルを記入し、アドレス指定の代わりを行なっています。このラベルは割込み関数の名称と同じです。つまり、割込み時に、この名称と同じC言語の関数が実行されることになります。
.section
D,DATA,locate=h'fef10;
_RXD1: .res.w 1
_STR: .res.b
16
.section D,DATA,locate=h'fef10;では、ここからはデータ領域だよ、というセクションの宣言を行なっています。そして、そのアドレスはFEF10hからだよ、と言っています。AKI-H8では、ここからRAM領域になります。
_RXD1: .res.w 1
ここでは、FEF10hのアドレスから、RXD1という領域名で、2バイト分のメモリ領域を確保しています。.resとはメモリ領域確保のための命令で、.wは2バイト単位、1が領域数を示します。C言語で言えば、int型の変数の領域ということになります。
_STR: .res.b 16
ここでは、RXD1の領域の次に、STRという領域名で、16バイト分のメモリ領域を確保しています。.bは1バイト単位、16が領域数を示します。C言語で言えば、char型の変数の領域を16個分ということになります。つまり、16文字分の文字列データ領域を、ここで確保しているのです。
さて、ここからプログラムsertest2の動作についてご説明します。先ず準備としてパソコンとAKI-H8を接続しておき、パソコンにはハイパーターミナルを立ち上げておいて下さい。ハイパーターミナルの設定については、109.パソコンとシリアル通信するの109−4.ソフトsertest1の動作説明を参照して下さい。
![]() |
AKI-H8の電源を入れると、パソコンと液晶画面に、このようなメッセージが表示されます。液晶の右上のLEDが早めに点滅しています。
|
![]() |
このとき、キーボードから1か2か3のキーを好きに押してみます。LEDの点滅速度が変わりましたか?割込み処理なので、LEDを点滅させながら受信できるのです。
|
![]() |
次に s か S を押してみましょう。パソコンにメッセージが表示され、液晶の2行目がクリアされます。LEDの点滅が止まり、文字列入力モードに入りました。
|
![]() |
ここでは tekurobo と入れてみました。一文字ずつ入力された状態が、パソコン画面と液晶画面に表示されて行きます。
|
![]() |
文字列を入力し終わったら、エンターキーを押します。すると、パソコン画面に文字列が一気に表示され、AKI-H8はLEDの点滅が再開されます。
|
109.パソコンとシリアル通信するで行なった実験と良く似たものですが、決定的な違いはLEDを点滅させたまま(メインの仕事をさせたまま)、受信を受け付けることが出来るようになったということです。今回のプログラムでは、文字列の受信を行なう時にLEDの点滅を止めてしまっていますが、これもプログラムの作りようでは、点滅を続けたまま文字列入力も可能になります。ただし、ちょっと煩雑になります。
パソコンからのキー操作で、AKI-H8を色々なモードで動作させることが出来ると言う意味では、結構有効な手法だと思いませんか?