C言語でマイコン制御 その3
6.装置制御の定番技
「マイコンを使ってLEDをON/OFFしてみた。C言語もかじってみた。」 しかし、その先に進めない初心者も少なくないと思います。簡単な装置とはいえ、実際にマイコンとC言語で装置を制御しようと思っても、どこから手を着けたらいいか分からないからです。
ここでは装置制御のマイコン・プログラムに関し、定石とも言える方法を考えてみます。
通常のPC上のプログラムの場合、ソフト開発の途中のデバッグ作業はPC自体を使い、画面に内部の変数を表示させたり、ステップ操作で確認作業を進めますが、ハード回路やメカ装置がつながった装置の場合はかなり様子が変わってしまいます。だいたい、プログラムをステップ毎に確認すること自体が困難なケースがほとんどです。ハード回路やメカが追従しないからです。特に割り込みが主体になる制御系では全く不可能とさえいえます。そんな、装置制御に適した開発手順が以下の方法です。
6-1 空の main() を用意する。
int main(void)
{
}
中身のないmain()関数を用意します。
6-2 まずは初期化関数に手をつける
デバイスを初期化する関数です。最初は簡単に
void init(void)
{
DDRB = PBIO;
}
のように最低限度必要なポートのI/Oを設定します。
ついでに、最上位のプロトタイプ宣言部に
void init(void);
を追加してください。プロトタイプ宣言は、そのファイルで使う関数の目次です。
【注】 ソース・ファイルが複数に分割されていて、他のファイルで実際の関数が宣言されている場合は、
extern 返値 関数名(引数, ,);
のように外部ファイル使用を宣言します。
DDRBはポートBの各ビットが入力ポートなのか、出力ポートなのかを設定するレジスタです。対応ビットに0を書くとINになり、1を書き込むとOUTポートになります。PBIOをdefineしなければいけませんから、定数を書く記述に
#define PBIO 0b11111101
を書き、LED(b0)を出力1に指定します。b1は入力にするので0に設定します。IO定義は直接プログラム中に書いても構いませんが、#define文にまとめておいたほうが後から確認したりする時に便利です。
これでメイン関数を
int main(void)
{
init();
while(1);
}
のようにすれば、コンパイルしてプログラムを走らせることもできる筈です。最後にプログラムを止めるために while(1); とします。
上記「DDRB」のようなレジスタは「I/Oレジスタ」と呼ばれます。ATtiny2313に限らず、どのプロセッサにも固有のI/Oレジスタが用意されていて、ポートの機能や、各種タイマー、通信機能、割り込み機能など、プロセッサのほとんどの機能を設定することができるようになっています。マニュアルの末尾近くにはレジスタ一覧がある筈です。
6-3 出力関数
どんな制御装置でも最低1個のLEDはつけると思いますが、この出力はプログラム開発のベースキャンプです。
ポート定義として
#define LED 0b00000001
とします。LEDはビット0に接続するからです。
そして、サンプルプログラムの「出力関数」部分を見てください。
void off_led(void)
{ PORTB |= LED;}
これはLEDをoffする関数です。回路上、ポートBのb0をHにすればこのポートからGNDには電流が流れなくなり、LEDはOFFします。ですから
off_led()関数はPORTBのb0を定数LED(0b00000001)とビットORして1を書きます。
void on_led(void)
{ PORTB &= ~LED;
}
は、LEDをONする関数です。ビットANDして、PORTBのb0を0にしています。
~LED は0b00000001のビットを反転ですから、0b11111110です。これをビットANDすると、PORTBのb0は0になります。
void off_led(void); と void on_led(void); をプロトタイプ宣言部に書けば、以降はmain() に
on_led(); あるいは off_led();を入れてみて実際にLEDが点灯、消灯するか確かめることが出来ます。
コンパイルエラーもでないのにLEDが正常に動作しないとしたら、ハード接続か、IO定義を疑います。
最悪テスターを当てれば何処にミスがあったか判る筈です。
【注】1ビットの出力は上例のように簡単にできますが、では、同一ポートの複数ビットへの同時出力はどうするのでしょうか?
この操作は残念ながら1命令ではできません。操作対象の複数ビットの全ビットに一旦0を書き、次に必要なビットを1にする、あるいは、複数ビットに一旦1を書き、次に必要なビットを0にする、というような2行程
が必要となるからです。
ヒゲ状のパルスとはいえ望まないデータが出力されることが許されない場合は、ポートにデータを直接出力するのではなく、別に出力バッファを用意して、2行程によるバッファ操作を完了した後、このバッファをポートに出力するような手段を使います。無論対象となるビットだけでなく、同じポートの他のビットも同じように取り扱われる必要があります。
なお、複数のポートに渡るビット・データを完全に同期させて出力する方法はプログラム上では不可能で、ラッチ信号を追加するなどのハード対策が必要となります。
なお、出力と割込みの関係は、特別に注意する必要がありますので、「割込みとメインの干渉」の項を参照してください。
6-4 次は「割り込み」
割り込み機能は、いわばコンピューターの緊急呼び出し機能です。人間もそうですが、自分の作業にだけ没頭できれば良いのですが、編み物をしている最中に、かかって来た電話に出なければなりません。その電話に出ている最中に12分前にセットしておいたタイマーが鳴り出したので、電話を一旦置いたまま、スパゲッティーの火を止めて、ザルにあけたら電話に戻り、すみませんと、続きの話をして、それが終わったら、スパゲッティが冷えないうちに食べてから、ようやく元の編み物の作業に戻れます。
プロセッサの割り込み機能は、割り込み条件が成立した場合、各種割り込み専用のアドレスに自動的にプログラムの実行が移されて、その割り込みプログラムが終了したら、また中断していたプログラムアドレスに実行が戻される機能です。ただしAVRは各割込み専用アドレスが呼び出されるのですが、PICの場合は割込み要因が異なっていても同じ4番地が呼び出されるので、割込みレジスタフラグでどんな割込みが発生したかを識別する必要があります。
割り込み処理中に別の割り込み処理をすることも不可能ではありません。
割込み処理(インタラプト)のプログラムは、独立したプログラム単位として記述します。予め決められた割込み関数名でプログラムを書くと、割込み条件が成立した時、実行中のメインのアドレスは、スタッカに保留され、該当する割込みベクトルテーブルが参照されて、割込み関数が呼び出されることになります。割込みの関数の終了で、スタッカに保留されていたメインの継続アドレスがプログラムカウンタに再ロードされます。
主な割込みは
@外部割込み(特定ポートの信号のレベルやエッジで割込み発生)
Aタイマー/カウンタ割込み(外部信号またはクロックでカウントして所定値で割込み発生)
B通信割込み(受信割込み、送信完了割込みなど)
などですが、割り込みは可能な限り軽く、そして少なくすべきです。特に、システムの中に高速ステッピングモーターや遅れてはならないデータ転送デバイスが含まれる場合は、割込みに過度な負担を掛けることは厳禁です。
割り込みに頼り過ぎると、問題が発生した際の分析を困難にします。ごく稀に、おかしな動作が起こるが再現困難、などというのは大抵割り込みが絡んでいます。ノイズによる暴走と紛らわしいため、多くのエンジニアが落ち込む底なしの沼でもあります。
しかし、プログラマーは割込みから逃れることはできません。機構が要求する応答速度と、ポートの振り分けを予め理解した上で、割込みに託すべき機能を決定しなければならないのです。
タイマー割り込みで信号の入力監視
ポートへの入力を調べたい場合、
if (PINB & SW1)
のようにしたいところですが、実際の装置には、各種ノイズが入ってくるばかりでなく、スイッチにはチャタリングと呼ばれる接点のバウンドがあったりして、簡単には入力を判断することができません。
まず、ノイズですが、電源ラインに乗って来るようなノイズは一般的に極めて短時間で、1回のパルスは数μs以下しか続きません (これを越えるノイズが入って来るとしたらフィルターを強化するなどの回路上の対策をすべきです)。 従って、1〜2ms間隔で入力してみて、連続で同じデータが入力して来たら初めて有効と判断すれば、電源ラインから混入してくるノイズはほとんど除去できます。
一方、スイッチの接点がバウンドして発生するチャタリングは、長くても20〜25ms程度です。チャタリング防止の簡易的な方法は、30ms間隔で入力をする方法です。操作スイッチ等はこの方法で十分です。
ただし、装置動作を検出するマイクロスイッチなどは、動作の認識が遅れないようにするために、スイッチの入力データが変化したら、これ以降の30ms間、入力判断をマスクする方法を取ります。
さて
ISR(TIMER0_OVF_vect)
{
}
は、AVRのタイマー0 オーバーフロー割込み処理に当てられた専用の割込み関数です。
内蔵された8ビットのタイマー0は init() に書かれたように設定すると、クロックの1/64周期でダウンカウントし、カウンタが0から0xffへオーバーフローする際に ISR(TIMER0_OVF_vect) の割り込みを発生します。つまり、mainプログラム実行を中断して割り込みプログラムに飛んでくる訳です。そして割り込み処理が終了すればまた元のプログラムに戻って行きます。
クロック8MHzを64分周すると8÷64=0.125MHz → 8μs/カウント です。1000μsでは125回のダウンカウントですから
#define TC (0xff -
125)
のようにしすれば、1ms毎に割り込みが発生することになります。
割り込み処理の冒頭の
TCNT0 = TC;
はタイマー0をリスタートしている記述です。
そして、以下の方法で、2回一致した信号を始めて入力バッファに取り込んでいます。
PBinbo = PBinbn; // 前回入力データを旧データにセーブ
PBinbn = PINB; //
ポートを新バッファに入力
PBinbn = PBinbn ^ PBLG; // 論理整合
if (PBinbn == PBinbo) // 旧データと新データが同じなら
{ PBinb = PBinbn; } // 確定バッファを更新
次にチャタリングですが、1ms×30回に1回、入力バッファの内容を転送することでチャタリングを回避しています。
センサー入力関数が正常に働いているかは、main() で
int main(void)
{
while(1)
{
if ( PBinb0 & SW1)
on_led();
else
off_led();
}
}
のように確認することができます。
タイマー割り込みで時間管理
一般的に、どんな装置であっても、動作の結果が帰って来ないと無限待ちのループに入ってしまう危険性が発生します。これを避ける最も本的な手段が割り込みタイマーです。
下はモーターが起動してから一定時間内にセンサーにHP信号が来ない場合、エラーを判断するプログラム例です。
#define I_TC (0xff - 16) // タイマー0割り込み時定数 0.128ms×16 = 2.05 ≒ 2m
#define T_OUT 1800 // タイムアウト判定時間 *2ms
volatile unsigned int i_tm; // 割り込みタイマー変数
volatile char time_out_f; // タイムアウト通知フラグ
ISR(TIMER0_OVF_vect) // 8ビットタイマー0:オーバーフロー割込み
{
TCNT0 = I_TC; // タイマー再スタート
if(i_tm) // タイマー動作中なら
{
i_tm--;
if (!i_tm)
{
time_out_f = 1; // タイムアウトフラグでメインへ通知
}
}
}
int main(void)
{
;;;
TIMSK = 0b00000010; // 8bit timer over flow interrupt enavle
TCCR0B = 0x05; // 8ビットタイマー 8MHz/1024 =7.8125kHz --> 0.128ms
TCNT0 = I_TC; // 時定数2ms
i_tm = 0;
sei(); //割り込み有効
;;;
m_on(); // モーター起動
i_tm = T_OUT; // 監視タイマー起動
while (!(PBinb & HP) && !(time_out_f)); // HP信号が無くタイムアウトで無い間は待ち
if(time_out_f) //タイムアウトであった場合はエラー処理へ
{err(ERR2); } // エラー処理へ
i_tm = 0;
;;; //監視を停止
タイマー割り込み機能は、時間が巡ってくるごとに、「ちょっと何かをする」くらいのことしかできません。上の例では「もし i_tm というカウンターが0でなかったら、その値を-1する」という作業をタイマー割り込みでさせています。メインプログラムは、タイマーを使いたい時にそれを起動させ、自分の仕事をしながら、タイムアウトの旗が立ったかどうかを監視しています。
タイマー割り込みでステッピングモーター駆動
ステッピングモーターは等加速度制御される場合が多いため、一般的には、センサー入力のような一定周期タイマーとは別な割り込みタイマーを当てます。例えば、100pps程度のステッピングモーターと1200bps以下のシリアル受信なら、一つのCPUで制御できるかも知れませんが、これを超える処理が要求される場合は、ステッピングモーター駆動には専用のCPUを当てるか、シリアル通信にも専用のCPUを当てて、メインとサブCPU間は別チャンネルのシリアル通信を使うか、クロック+1ビット〜数ビットのポート間データ転送を利用する等の方法を使います。
外部割込み
ATtiny2313のINT0割込みは、 PD2 (pin6)の信号で割込みを発生させることができます。
int main()
{
DDRD = 0b00000000; // PORT設定 PD2 = in
MCUCR = 0b00000010; //INT0↓で割込み発生
GIMSK = 0b01000000; //INT0マスク外し
sei(); //割込み許可
,,,,,
}
ISR(INT0_vect) //外部割込み INT0
{
処理;
}
【注意】 外部割込みは、信号のレベルで発生するタイプと、↑または↓エッジで発生するタイプがありますが、少なくとも信号が乱れないような専用の回路対策が必要です。使うにしても不測の電源落ちの際の緊急避難対策等に限定すべきべきでしょう。
割込みとメインの干渉
あるポートへの出力をメインで制御するか、割り込み先で制御するかという選択は、システム設計上でも極めて重要な選択です。何故なら、同一ポートの全ビットは、メインから、あるいは、割込み先の何れか一方からのみ制御されることが望ましいからです。
その理由は、
PORTB &= ~TM;
というような一行命令であっても、複数ビット操作の場合、Cコンパイラは、
in r24, 0x18
andi r24, 0xF0
out 0x18, r24
のように、ポートの状況を一旦in命令で入力してから、これを論理演算してからout命令で再出力するコードを生成します。これらの3命令が連続して実行される限りは問題はないのですが、もし途中で割込みが発生し、同じポートに対する異なったデータの書き込みが発生したらどうでしょうか?後から発生した新しい割り込みの結果が無効となる事態が発生することになります。
オフィスのLANシステムの中では、何れかのユーザーが共有ファイルをOPENしたら、後から同じファイルを開く他のユーザーは読み込みモードでしか開けなくようにOSが管理してくれますが、マイコンのC言語環境では、プログラマー自らが管理しなければなりません。
メインで出力を実行するか、あるいは、割り込み先でのみ実行するか、明確に切り分けられていれば、問題は生じてこないのですが、これを混同すると、稀に誤動作し、しかも再現性が無い、というような厄介さに見舞われることになります。
割り込みで使われる出力ポートとメインで使われる出力ポートを分離することができない場合(それが普通ですが、)は、
メイン用出力ルーチンと割り込み用出力ルーチンを別に設けます。
割り込み内で使う出力ルーチンには割り込み禁止と割り込み許可命令を記述しませんが、メインで使用する出力ルーチンは、割り込みを禁止してから出力し、再び割り込みを許可してから出力ルーチンを出るようにします。
例
//メインで使用する出力ルーチン
void off_kl(void)
{
cli(); //割り込み禁止
PORTB &= ~KL;
sei(); //割り込み許可
}
//割り込み内で使用する出力ルーチン
void ioff_kl(void)
{ PORTB &= ~KL; }
ただし、割り込みのネスティング(多重割り込みの許可)を使う場合は厄介です。割り込みが一重なら、割り込みが終わる出口でRETI (スタックの回帰と割り込みデージーチェーンのリセット)命令が1回処理されるだけですが、多重割り込みを許可してしまうと、深いルーチンから出た、浅い割り込みルーチン内で、意図しない割り込みが発生してしまう危険性があるからです。管理しきれない割り込みは乱用すべきでというのは鉄則なのです。
更に、割り込みとメインの干渉は同一ポートの共有から発生する問題だけではありません。割り込みが管理している変数をメイン側がこの
値を参照するだけでも、深刻な干渉が発生します。逆にメインが管理している値を割り込み側が参照する場合も同じです。例えば、割込みが掛かっている間、メインは禁止されているので、干渉はしない筈、しかも書き換えるのではなく、見るだけなのだから問題は無い、と思い勝ちですが、実際はその「見方」にも問題が絡んできます。例えば、変数が16ビットであり、処理が8ビット単位で実行されているとしたら、処理の途中に割込みが入り、変数の残り8ビットがガラリと入れ替わってしまうことがあるのです。これはシステムを完全に錯乱させてしまう原因にもなります。
割り込みが管理する変数を、メイン側からこれを参照する際は、一時的に割り込みを禁止する必要があります。割り込みを禁止しないで参照するとしたら、
@ 割り込み側:
メインに渡したいデータを、専用の外部変数に格納して、メインへの通知フラグを立てる
A メイン側:
通知フラグを監視して、通知フラグが立ったら専用変数からデータを取り込み、通知フラグを倒す
B 割り込み側:
通知フラグが倒れるのを待って@へ
というような、少々手の込んだ手法が必要になります。
なお、AVRとH8の割り込み制御関数は
AVR
sei(); // 割り込み許可
cli(); // 割り込み禁止
H8
#define ei() set_imask_ccr(0) // 割り込み許可 ei() の定義
#define di() set_imask_ccr(1) // 割り込み禁止 di() の定義
です。
AVRの割り込み処理の例
以下の表はAVRの主な割込み関数です(新関数名が推奨されています)
割り込み名称 割込み関数名(旧) → 割込み関数名(新)
外部割込0(INT0 pin): SIGNAL(SIG_INTERRUPT0) → ISR(INT0_vect)
外部割込1(INT1 pin)SIGNAL(SIG_INTERRUPT1) → ISR(INT1_vect)
タイマ0 比較出力:SIGNAL(SIG_OUTPUT_COMPARE0) → ISR(TIM0_COMP_vect)
タイマ0 オーバーフロー:SIGNAL(SIG_OVERFLOW0) → ISR(TIMER0_OVF_vect)
タイマ1 入力獲得
:SIGNAL(SIG_INPUT_CAPTURE1) →ISR(TIMER1_CAPT_vect)
タイマ1 比較出力A:SIGNAL(SIG_OUTPUT_COMPARE1A) → ISR(TIMER1_COMPA_vect)
タイマ1 比較出力B:SIGNAL(SIG_OUTPUT_COMPARE1B) → ISR(TIMER1_COMPB_vect)
タイマ1 オーバーフローSIGNAL(SIG_OVERFLOW1) → ISR(TIMER1_OVF_vect)
タイマ2 比較出力:SIGNAL(SIG_OUTPUT_COMPARE2) →ISR(TIMER2_COMP_vect)
タイマ2 オーバーフロー:SIGNAL(SIG_OVERFLOW2) → ISR(TIMER2_OVF_vect)
UART 受信完了:SIGNAL(SIG_USART_RECV) →ISR(USART0_RX_vect)
UART 送信バッファ空:SIGNAL(SIG_UART_DATA) → ISR(USART_UDRE_vect)
UART 送信完了 SIGNAL(SIG_UART_TRANS) →ISR(USART_TXC_vect)
ADC 変換終了:SIGNAL(SIG_ADC) → ISR(ADC_vect)
EEPROM操作可能:SIGNAL(SIG_EEPROM) →ISR(EE_RDY_vect)
例えば、ATtiny2313の8-bit Timer/Counter0 オーバーフロー割込みと、16-bit Timer/Counter1 オーバーフロー割込みを発生させる場合は次のようにします。
int main(void)
{
,,, // ポート初期化
TIMSK = 0b10000010; // 16ビットtimer1オーバーフロー割り込み+8ビットオーバーフロー
TCCR1A = 0;
TCCR1B = 0x02; //16ビットタイマー分周 8MHz/8 = 1μsec/count
TCCR0B = 0x05; //8ビットタイマー分周 8MHz/1024 = 0.128m/count
TCNT0 = 0xff - 78; // 8ビットタイマー時定数
TCNT1 = 0xffff - 800; //16ビットタイマー時定数
sei();
,,, // メイン処理記述
}
ISR(TIMER0_OVF_vect) // 8ビットタイマー0 オーバーフロー割込み関数
{
TCNT0 = 0xff - 78; //再割込み起動
,,,, // 割込み処理記述
}
SIGNAL(SIG_OVERFLOW1) // 16ビットタイマー1 オーバーフロー割込み関数
{
TCNT1 = 0xffff - 800; //再割込み起動
,,,, // 割込み処理記述
}
6-5 いつも使う機能関数をコピー
メイン関数でつかうタイマーのlptm()などは、定数さえ変えれば他のプログラムでも使えます。コピーして使いましょう。
実は、コンパイル作業するソースファイルは1個に限定されてはいません。統合開発環境下では、複数のヘッダファイル、Cソースファイル、アセンブラ・ソースファイルを各々自動的にコンパイルし、リンカが繋げて、一つのターゲット・ファイルが出来上がります。常用する関数などは、別ファイルにライブラリ化しておけば、いちいち関数をmainソースファイルにコピーしなくてすますことができます。ただし、必要以上にソースファイルを細分化するのは考え物です。
6-6 main()関数
C言語のプログラムは、必ず main(void) { という関数を実行するところから始まります。それがC言語の決まりです。この main関数の中で、一つのLEDを周期的に点滅させたいだけなら、プログラムはそれ程難しくはありません。しかし、それとは別な周期でもう一つのLEDを点滅させたいとなると割り込みで処理するしかないのでしょうか。点滅だけならそれも可能でしょうがもっと複雑な作業ともなると厄介です。
ところで、コンピュータも以前は一つのプログラムが動いている間は別のプログラムを実行することはできませんでしたが、現在はWindowsをはじめ多くのマルチタスクOSは、複数のプログラムを切り替えながら、あたかも並行処理するかのように走らせることができます。無論実際は同時並行処理しているのではなく、途中で棚上げしては隣の仕事をちょっとして、という具合に時間分割で処理しているという訳です。
マイコンも同じようにマルチタスしようと思えばできますが実際はしません。それはひどく効率の悪いシステムになってしまうからです。人を相手にするPCは主画面さえサクサク動けば背景の作業は手間取っても余り文句を言われることはないのですが、主にマイコンが相手をする周辺機器には待ったなしの動作が求められる場合が多いのです。そこで考えられたのが以下の幾つかの方法です、
ステージ方式
サンプルのプログラムは簡単なものですが、一般にはもっと長いものになります。一般的にmain()関数は
int main(void)
{
init(); // I/O初期化
sei(); // 割込み開始
s_test(); // 装置の自己診断 6-9項参照
while(1) // メイン処理
{
func1(); // ステージ関数1
func2(); // ステージ関数2
func3(); // ステージ関数3
s_func1(); // メンテナンス関数
}
}
のように構成します。while(1) 内の複数の関数は、装置の動作や操作上、独立性があって、個別に考えたほうがよいプログラム単位です。デバッグの容易性を考慮することも重要です。
例えば、センサーによって目標を捕捉、しこれを追跡する装置のシステムを考えてみます。
先ず主要関数名を決め、コメントを記述します。コメントは関数の仕様書です。仕様書が無いとその関数の中身を構築することはできません。
int main(void)
{
ini(); //初期化
sei(); //割り込み開始
s_test(); //電源投入時のテスト機能
while(1)
{
get_target(); //目標捕捉
psuit_drv(); //目標追跡
maint(); //メンテナンス
}
}
void ini(void) //デバイス初期化と装置の初期動作
{
}
void s_test(void) //入出力テスト、動作定数設定、エラー履歴、版数管理
{
}
void get_tatget(void) //センサー・サーボで目標を追跡捕捉するステージ関数
{
}
void psuit_drv() //捕らえた目標に向け装置を駆動するステージ関数
{
}
void maint(void) //目標を見失った、または目標に達したら停止するメンテナンス関数
{
}
こうして、システムの骨格を作ったあと、個々のモジュール関数に必要な下層の機能モジュールを、同じ手法、つまり関数名の命名と仕様コメントを構築していきます。
さて、聞きなれない「ステージ関数」ですが、ステージ関数は呼ばれたら内部に止まってしまってはいけません。(ただし、装置を待機状態にしてから実行するメンテナンス専用の関数は、別ですが) 全てのfuncが回り続けることで、見かけ上のマルチタスクが成立するからです。フロー図で書けば菱形
◇ で書かれる全ての判断待ちのステージにおいて、
case ステージ:
if (判断条件)
{条件が成立した時に実行する処理をして、ステージのswitchを次に進める;} // 条件が成立
break; //switch文から脱出
else //条件が不成立で待つ間の実行処理
{待ち間の処理;}
break; //switch文から脱出
の構造にします。こうすることで、待ちループは解消され、複数のステージ関数がマルチタスクのように動作することが可能となります。
個別のファンクションは次のようにします。これは、始動ボタンが押されたら、モーターを一定時間だけ正回転させ、次に逆転して、ホームポジションセンサー位置で停止させるタスクです。
volatile unsigned char st; //ステージ変数の宣言 (外部変数)
void mot_cont(void) //モーター制御
{
switch (st) //ステージ変数 st によって分岐
{
case 0:
if( in_bf & st_sw) //入力バッファのスタートスイッチに信号が入ったら
{
on_mot_fd(); //モーター正回転ON
i_tm = TC1; // 割り込みタイマーに動作時間セット
st++; //次のステージへ
}
break;
case 1:
if( ! i_tm ) //割り込みタイマー待ち
{
off_mot(); //モーターOFF
i_tm = TC2; // 割り込みタイマーに休止時間セット
st++; //次のステージへ
}
break;
case 2:
if( ! i_tm ) //割り込みタイマー待ち
{
on_mot_rv(); //モーター逆回転ON
i_tm = TC3; // 割り込みタイマーにエラー・タイマー セット
st++; //次のステージへ
}
break;
case 3:
if( s_hp() ) //ホームポジション検出なら
{
off_mot(); //モーターOFF
i_tm = CT4; //割り込みタイマーにモーター静止時間セット
st++; //次のステージへ
}
else
{
if(! i_tm ) //ホームポジションが規定時間内に検出できなかったら
err(); //エラー処理へ
}
break;
case 4:
if( ! i_tm ) //割り込みタイマー待ち
{
st = 0; //最初のステージへ戻り
}
break;
}
}
というようにし、ステージ変数stによって、必要な判断ルーチンに直接入ってきて、もし、まだ次の処理ができる状態でなかったら、breakして帰ってしまうようにします。こうすることで、複数の動作が平行して進むシステムが実現します。
メインによる割込み処理の分担
特定の条件が成立した時に、長い処理が必要となる場合は、割込み先では、イベントの発生だけを検知し、これをフラグでメインに通知し、メインで必要な処理をする方法を採ります。
ISR(TIMER0_OVF_vect) //SIGNAL(SIG_OVERFLOW0) 8ビットタイマー0オーバーフロー割込み
{
TCNT0 = TC0; //タイマー再スタート
,,,
if( SIG & PDinb) //信号検出なら
{
int_f = 1; // メインへの通知フラグ立て
}
,,,,
}
int main(void)
{
init(); // I/O初期化
sei(); // 割込み開始
s_test(); // 装置の自己診断 6-9項参照
while(1) // メイン処理
{
func1(); // ステージ関数1
func2(); // ステージ関数2
func3(); // ステージ関数3
int_fnc1(); // 割込みフラグ監視処理1
s_func1(); // メンテナンス関数
}
}
void int_fnc1(void) // 割込み先で検知した実行ルーチン
{
if(int_f1) // もし通知フラグが立っていたら
{
処理;
int_f1 = 0; //通知フラグクリヤ
}
}
ただし、処理時間が割込み間隔を越えるような処理をすると、割込み要求が跳んでしまう危険が伴うため、基本的には使うべきではありません。
コマンド・ピン方式
ほとんどの場合、mainの処理は上のようにステージ処理、またはmainによる割込み処理の分担で済ますことができますが、監視すべき対象が複雑になってくるとステージ処理方式を使うことは困難となります。例えば、コンベアのラインで配送先ごとに各ゲートへ搬送物を分類するような物流配送システムの場合は、監視すべき数量が多く、かつ変動するため、上のような方法で処理することは現実的ではありません。
コマンドピン方式は、タイマーまたはエンコーダーによるシステムカウント値と実行命令を組み合わせたコマンドピンを使う方式で、概念的には、写真下の電源を設定した時間にON-OFFできる差込ピン型のタイマーに似た方式です。
きっかけとなるイベントが発生したら、これ以降必要となる制御を、「どのカウント値で、どういう条件であったら、何を実行する」というコマンド・ピンの形で、コマンド・バッファの中にカウント値でソートしながら植え込むことにより、先々のシステム動作を決定してゆきます。なお、「実行する」という概念は「処理する」だけでなく「判断する」も含まれますから、フローチャートで表現できるあらゆる処理をこのコマンドピンで実現できることになります。
メインは、システムカウンターを監視し、次のピンの指定するカウント値がシステムカウンターと一致、もしくは超えた場合に、ピンが指示するコマンドを実行することで、コマンド・バッファに植え込まれたピンを順次処理していきます。そして、処理済み、または不要となったピンは引き抜きます。
一つのコマンド・ピンは、
実行すべきシステムカウント値、[実行条件、] コマンド、[コマンド、、] 区切り記号、
で構成します。最終ピンの後部には終端記号のピンを植え込みます。
実行条件はセンサーやフラグの状態で、コマンドの処理内容(ピン関数)はインデックステーブルで記述するか、関数へのポインタで記述します。
システムカンタ自体は割込みでカウントアップされるようにしますが、コマンド・ピンの処理はメインで実行するようにします。
コマンド・バッファ内のンピンを操作する基本機能は、上に述べた「ソート&インサート」の他、「指定するコマンド・ピンの引き抜きと詰め合い」「全コマンドピン消去」、「コマンドピンの再植え込み機能」、 などが必要とされるでしょう。
このコマンド・ピン方式は、ピン関数の種類を増したりその機能を変更することも自由にできます。固定化されたシーケンスではなく、プログラムの流れ自体をフレキシブルに変えるような仕組みも可能です。
ただし、ランダムに発生するイベントなどを常時監視する方法としては不適であること、構造が複雑化する可能性があること、ピン処理時に過剰な作業が集中してしまうなどの弱点もあるため、基本設計に十分注意する必要があります。
このように、プログラムの主要な働きは、ごく簡単なシステムの場合は、割込みは不要で、単純な main だけで済ますこともできます。また、テッピングモーターを回したり、受信したデータを再送信するような、高速繰り返し処理なら割込みだけで十分です。少々複雑でも順序通りの動作で済ますことができるのであれば、ステージ方式、複雑な動きが要求される場合はコマンド・ピン方式、更に複雑な動作の場合は、以上の手法を組み合わせて使用するか、後述のマルチ・プロセッサを考えることになります。
6-7 エラー処理
装置エラーが発生した時の処理および、エラーからの回復はとても重要です。
エラーは、
@受信データにエラーが発生した
A規定時間内、あるいは規定ステップ内に、機構動作の完了信号が上がってこない。
Bメモリが正常に機能しない
Cプロセッサが暴走した(ウォッチドックが働いた、無効アドレスがアクセスされた)
などがあります。
エラーが発生した際の手続きは、@の場合は表示してデータ再送信を促す等の軽い処理で済みますが、A以降のエラーに関しては以下のような処理が必要になります。
1. 割り込みを総て停止し、動力駆動系を全部OFFする。
2. エラー内容(エラー番号)を表示する。(必要あればエラー履歴に記録する)
3. 回復操作を待つ
4. 回復処理する。
エラーは、いろいろな状況の中で検出され、また、連鎖して発生することもありますから、フラグ類の処理ばかりでなく、スタックレベルの処理も厄介なものとなります。
これを一気に解決するのがシステム・リセットの実行です。つまり、電源が投入された直後に実行されるべく用意された組込み関数(リセット関数)を使う方法です。
以下は、AVRとH8のリセット関数です。
AVR
__ctors_end();
注)WinAVRでなくAtmelStudioの場合は
__ctors_end は使えないので次のように直接ベクトルテーブルの次の
番地を呼出す必要があるようです。
((void(*)(void)) 0xアドレス)();
H8
PowerON_Reset();
AVRやH8以外のプロセッサの場合のリセット関数名は、.map ファイルと共に生成されるアセンブラ・ソース(拡張子はコンパイラによって別々)のベクトル・テーブルの0番地を見ればわかると思います。頭の
’_’を外したり、追加したりするコンパイラもあります。
なお、リセット関数では、プロセッサ以外のデバイスは初期化されませんので注意してください。
【例】下は、AVRで、カバーなどのインターロックが開けられたけらたことを、タイマー割込みで監視し、装置を緊急停止し、インターロックが閉じられたら、装置を電源投入と同じ状態から再スタートするプログラム例です。
ISR(TIMER0_OVF_vect) //SIGNAL(SIG_OVERFLOW0) 8ビットタイマー0オーバーフロー割込み
{
TCNT0 = TC0; //間隔 0.5ms
// INTインタロック 監視
if(!test_f && PIND & INT) // テストモードでない時にインタロック開きなら
{
cli(); // 割込み禁止
all_off(); // 全出力off
dspmsg(L1,CV_OPEN); // カバー開放表示
lptm(200);
while(PIND & INT); // カバー閉じ待ち
dspmsg(L1,"Close ");
lptm(3000);
__ctors_end(); // システムリセット
}
,,,,,
}
インターロックは何時開けられるか予測できず、装置のあらゆる行程で、監視し続けることはmain()関数では極めて厄介な作業となります。一方、割込み中で監視した場合、メインへの復帰はこれも厄介です。上の方法はこれを両立させるシンプルな方法です。
6-8 自己診断プログラム
装置を自己診断する機能は開発者ばかりでなく、保守作業者、ときにはユーザーにとっても重要な機能です。通常の電源投入ではなく、特定の操作スイッチを押しながら(あるいは一定時間押しながら)電源を投入する、などの操作で開始されるプログラム構造にします。
機能としては、
@ センサー入力テスト(センサーの状況を表示)
A 出力テスト(各出力を順次ON)
B デモ動作テスト
C エラー履歴表示
D エラー履歴クリヤ
E デフォルト実行(EEPROM初期化)
F エージング・テスト
G バージョン表示
などです。
なお、下は操作部の4つのスイッチ LSW,RSW,SSW,MSW を使った電源投入の仕方でテスト、設定、トータルカウンタクリヤに分岐させる例です。
// PB
#define LSW 0x80 //(in) L sw
#define RSW 0x40 //(in) R sw
#define SSW 0x20 //(in) S sw
#define MSW 0x10 //(in) M sw
//======== macro定義 =========
#define in_lsw() (PBinb & LSW) // < / -
#define in_rsw() (PBinb & RSW) // > / +
#define in_ssw() (PBinb & SSW) // S sw
#define in_msw() (PBinb & MSW) // M sw
int main(void)
{
lptm(200); // 電源安定まで待機
ini(); // デバイス初期化
sei(); // 割込有効
lptm(500); // 入力バッファ有効まで待機
// 電源投入時のモードチェック
if( !in_lsw() && !in_rsw() && in_ssw() && in_msw() ) //SSW+MSW ONで テスト実行
{ test(); }
if( in_lsw() && !in_rsw() && !in_ssw() && !in_msw()
) //LSW ONで 設定実行
{ set(); }
if( in_lsw() && in_rsw() && in_ssw() && in_msw()
) //LSW+RSW+SSW+MSW ONで トータルクリヤ実行
{ clr_total(); }
7. AVRコントローラ その他の機能
7-1 EEPROMの読み書き
AVRは内蔵している不揮発性メモリ(EEPROM)にデータを書き込んだり、読み取ることができます。書き込んだデータは電源切断によっても消去されることはありません。
#include <avr/io.h>
#include <avr/eeprom.h>
void EEPROM_write(unsigned int Address, unsigned char Data);
unsigned char EEPROM_read(unsigned int Address);
void lptm(unsigned ms);
// EEPROM 1バイト書込みファンクション
void EEPROM_write(unsigned int Address, unsigned char Data)
{
while(EECR & (1<<EEPE)); /* 前に行った書込み完了をチェック */
EEAR = Address; /* アドレスおよびデータレジスタのセット */
EEDR = Data;
EECR |= (1<<EEMPE); /* EEMPE(EEPマスターライトイネーブル)に1をセット*/
EECR |= (1<<EEPE); /* EEPEを1にセットして書込みスタート*/
lptm(5);
}
// EEPROM 1バイト読み出しファンクション
unsigned char EEPROM_read(unsigned int Address)
{
while(EECR & (1<<EEPE)); /* 前に行った書込み完了をチェック */
EEAR = Address; /* アドレスレジスターセット*/
EECR |= (1<<EERE); /* EEREに1をセットして読み込みスタート*/
lptm(5);
return EEDR; /* データを保持してリターン*/
}
void lptm(unsigned ms)
{
volatile int i;
while(ms)
{
for (i = 440; i > 0; i--);
ms--;
}
return;
}
【注】 本来ループタイマーは必要ない筈ですが、入れないと書き込みが不確実な場合があるようです。
使い方
EEPROM_write( dat , 0); // アドレス0に変数datの値をを書き込み
dat = EEPROM_read(0) // アドレス0のデータを変数datに読み出し
【注意】AVRに内蔵されているEEPROMは、電源が切れてもメモリ内容は消えないが、AVRISP MkUによって、プログラムを書き込むと、それだけで、EEPROM内のデータが変化してしまうことに注意する必要がある。つまり、設定データなどが入っているAVRのプログラムを更新書換えすると、ユーザーが設定していたデータが消滅してしまうため、プログラム書換え前に、データをメモしておき、後に再設定するような作業が必要である。
7-2 PWM
PWM(パルス幅変調)機能は、デジタルとアナログの一種の架け橋で、高速で切り替わるONとOFFの割合を変化させることで、出力の平均値をなだらかに変えていく方法です。
例えば、モーターに加える電圧を頻繁に断続すれば、100%の回転力ではなく、もっと小さい出力で回すことができる筈です。そして、この比率を滑らかに変化させたり、更には、モーターの回転数を何らかの方法で検出して、この結果をパルス幅制御にフィードバックしてやれば、モーター回転数自体を安定化することもできます。
AVRコントローラの高速PWM機能は、8ビットタイマカウンタ(TCNT0)が0から255までカウントアップする時、OCR0Aにセットした値より低い値の間、OCOA端子はHを出力します。この機能は、専用の出力ポートを使うため、回路設計が限定されてしまう弱点はありますが、割り込みを使わずに、高速動作が可能というメリットがあります。
下記はPB3に接続したLEDを ホタルのように滑らかに明滅するプログラムです。
なお、回路は、 PB3端子→330〜470Ω→(K)LED(A)→Vcc に接続してください。また、AVR StudioのFusesタグはCKDIV8のチェックを外し8MHzにします。
下の設定ではTCNT0は1MHzの8Bitカウンターが動作するので、周期 1000kHz÷256≒3.9KHz の変調パルスが出力されます。
#include <avr/io.h>
void lptm(unsigned int ms) ; //EEPROMの項参照
int main(void)
{
unsigned char d;
DDRB = 0xff; // B=out
TCCR0A = 0b10000011; // (比較一致=L,底=H) -> OC0A(PB3:pin14) /8bit高速PWM動作
TCCR0B = 0b00000010; // クロック分周比 1/8=1MHz
while(1)
{
for ( d=0xfd; d>=1 ; --d ) //暗→明
{ OCR0A = d; lptm(2); }
for ( d=1; d<=0xfd ; ++d ) //明→暗
{ OCR0A = d; lptm(5); }
}
}
7-3 アナログ-デジタル変換
ATtiny2313には、アナログコンパレーター機能があり、外部のアナログ信号のレベルと、指定レベル値とを比較することはできますが、AD変換そのものではありません。
ATmegaシリーズには複数チャンネルのAD変換が備わっています。基準電圧で内蔵コンデンサを充電する時間をカウントする簡易AD変換です。マイクロコントローラーは多数のデジタル回路の集まりですから、アナログ信号に対するノイズは厳しいものがありますが、温度計のような、速度を求めない用途なら、データ処理次第で十分使える可能性はあります。
7-4 USART シリアル通信
AVRやPICなど、多くのマイコンに備わっているUSARTは、RS-232C系の非同期シリアル通信機能で、RS-232Cドライバーを介して、PCとのデータ通信、およびマイコン同士の通信が可能となります。短距離のマイコン同士ならドライバーなしで、RXポートとTXポートをクロス接続することができます。また、3つ以上のマイコン間通信であれば、74LS07などのオープンコレクタをプルアップ接続すれば、全ての送受信線をパラに接続することができます。また、例えば
ESP-WROOM-02 を介することで、WiFi 経由でネット接続も可能となります。
USARTの受信割込みを使うと、メインで常に監視する必要がなくなります。また、複数データを落とさず受信する必要がある場合は、受信はリングバッファ構造にして、残バッファ数が一定以下になったら、別途RTS-CTSなどの制御線を使って受信制限するか、データ線を使って、X-ON.X-OFFコードで制御するなどの方法を使います。
ATtiny2313の例
自己ループテストの場合はTXとRXポートを接続しておきます。
ISR(USART_RX_vect) //1バイト受信完了割込み (スペルに誤りがあってもエラーとならないため注意)
{
unsigned char d;
d = UDR; // 1バイト受信
,,,
}
int main(void)
{
DDRD = 0b00000010; // PD0(RXD)=in PD1(TXD)=out
PORTD = 0xff;
UBRRH = 0; // ボーレート H
UBRRL = 25; // ボーレート L 19.2k bps / 8M
UCSRB = (1<<RXEN)|(1<<TXEN)|(1<<RXCIE); // 送受信許可 受信割込許可
UCSRC = (0<<UMSEL)|(0<<USBS)|(3<<UCSZ0); // フレーム形式設定(8ビット,1ストップビット)
sei(); // 割込有効
UDR = 0x55; // テストデータ送信(非割込み)
while ( !(UCSRA & (1<<UDRE)) ); //送信バッファ空きをチェック
,,,,
}
ATmega164Pの例
自己ループテストの場合はTXとRXポートを接続しておきます。
SIGNAL(SIG_USART_RECV) // 1バイト受信完了割込み(スペルに誤りがあってもエラーとならないため注意)
{
unsigned char d;
d = UDR0; // バイト受信
,,,
}
int main(void)
{
DDRD = 0b00000010; // IN=0 OUT=1
PORTD = 0xff;
UBRR0H = 0; // ボーレート H
UBRR0L = 25; // ボーレート L 19.2k bps / 8M
UCSR0B = (1<<RXEN0)|(1<<TXEN0)|(1<<RXCIE0); // 送受信許可
受信割込許可
UCSR0C = (3<<UCSZ00); // フレーム形式設定(8ビット,1ストップビット)
sei(); // 割込有効
UDR0 = 0x55; // テストデータ送信(非割込み)
while ( !(UCSR0A & (1 << UDRE0)) ) ; //送信バッファ空きをチェックする場合は
,,,
}
【注意】 "1<<RXEN0)" などの操作は ヘッダー( winavr-20110\avr\include\avr\iomxxx.h)
に規定されているビット位置を設定値の中に取り出す操作です。bit0が1に規定されている値は 0b00000001、bit1なら 0b00000010
というように、規定の値を1ビット左シフトすると規定は値として使えることになります。
同様に"3<< "は隣り合う2ビット取り出す操作になります。
この方法を使えば、いちいちマニュアルを参照する必要性が軽減されます。
7-5 信号変化による外部割込み
プログラムで信号をチェックするのではなく、ハード的に信号を取り込む方法が外部割込みです。INT0割込みは6-4で説明しましたが、ここでは信号の変化による割込みを説明します。
シグナル・チェンジ割込みは、信号が変化することによって割込みが発生します。
出力指定したビットやマスクしたビットからも割込みがかかるため、目的のビット変化であることをプログラムで振り分ける必要があります。ただし、発生した要因を正確に突き止めるのは結構厄介ですから、複雑なシステムの中でこの割込みに頼るのは避けた方が無難と思われます。
下の例は、ATmegaによる 信号変化割込み(PCINT)の例です。
ATmega164
volatile unsigned char pcint_bf,tcl_cnt;
SIGNAL(SIG_PIN_CHANGE3) // PCINT3(PD2ポート)割込み発生
{
unsigned char d;
cli(); // 割込み禁止
d = PIND & TCL; // 入力
if(d != pcint_bf) // 旧データと変化していたら 有効変化割込み
{
pcint_bf = d; //旧データ差し替え
tcl_cnt++; // 必要な処理
}
sei(); // 割込み有効
}
int main(void)
{
PCICR = 0b00001000; // PCINT3 ピン変化割込み設定(注意:ポートを出力に設定しても割込み発生)
PCMSK3 = 0b00000100; // PD2ピンにマスク設定
}
8.AVR資料の翻訳
AVRマイコンのオリジナルはアトメル社から英文で出されているものですが、下記日本語サイトに翻訳文が掲載されています。
http://www.avr.jp/user/ds.htm
ATMEL翻訳
9.WinAVRのバグ
Cでのシステム開発は、とても助かるのですが、Cコンパイラにバグが無いかというと微妙な問題で、正直なところWinAVRにも時折問題が出てくることは確かです。特に、構造が少し入り組んだfor文のループ中などでは、変数が正常に機能しなかったりする場合があるようです。一方的にCコンパイラを信じ切っていると深みにハマル場合もありますので、様子がどうも変な時はループ変数を条件式の外に出してみるとか、などの工夫をしてみる必要があります。
10. マルチ・プロセッサによる「分散タスク」の提案
マイコンも高速になったといわれますが、その速度には限界があります。実際のシステムにおいて、プロセッサが着いていけなくなるのは、待ったなしの処理、例えば高速エンコーダーや外部からのデータ受信、ステッピングモーター駆動、プリンタードライブなどの複数の処理がたまたま衝突した時です。しかし、全体からすれば空き時間が多いのが実情ではないでしょうか。この、
たまたまのために高価で複雑なCPUにするのでは芸がありません。安い汎用のプロセッサで済ませたい処です。現に、車1台の中には数えきれない程のCPUが搭載されています。一般の電子機器も複数のCPUを組み込むのは当然の流れかも知れません。
複数のCPU間で情報を交換する方法には各種ありますが、USARTによる調歩同期シリアル通信が主に用いられ、ドライバーとしてRS-422またはRS-485が使われ、各社から専用のドライバーが発売されています。
RS-422 接続
RS-485 半二重接続
RS-485 全二重接続
シリアル通信で伝達できる情報の最高速度と、それに要する最短時間は以下のようになります。
1バイト伝送に
bps 要する最短時間(μs)
9600 1042
19200 521
38400 260
57600 174
115200 86.8
230400 43.4
複数ユニット間で情報をやりとりする方法にも多種多様な方法がありますが、シンプルな方法としては、半二重接続を使い、ホストCPUを決めておいて、このホストを中心にポーリングという手法で各端末を順次指定し、データを授受する方法が一般的です。システムがそれ程複雑ではない場合は以下のような方法が使われます。
ホスト 端末
→ @ホストが端末アドレスを指定
← A指定された端末は自身が持っている情報をホストに返す。
→ Bホストは端末から返された情報をバッファメモリの該当部分に上書きし、バッファメモリの全情報を共通アドレス指定で全端末に送信する。各端末は自身に必要な情報を受信してメモリを更新する。
例えば、38400bpsを使い、端末数が15で、@Aに1バイト、Bに2バイトを使うシステムでは、一周するのに16ms程度必要になることになります。これ以上の応答速度が要求される情報は、通信を使うのではなく、自身のCPUシステムで処理しなければなりません。
Top page