イベント駆動プログラミング

 応答性の良いプログラムを書こうと思ったら、長時間のforループ(当然、whileも含む)や、delay_ms()の様な待ち時間を作る関数を使ってはいけない事は先に説明したとおりである。

 このような考えで、複数のイベント(シリアル通信とか、スイッチとか)を同時に処理するプログラムを考えてみたい。

 まず、始めにシリアル通信を使って、PCからデータを受け取るプログラムを考えてみる。
 

#define F_CPU 16000000UL  // 16MHz動作 _delay_ms()などに関係

#include < avr/io.h >
#include < util/delay.h >
#include < avr/interrupt.h >

// 一文字受信(PC → AVR)
unsigned char getch(void)
{
 // UCSR1AレジスタのRXC1ビットが1になると、データの受信が完了し、UDR1レジスタにデータが保管される
 //  RXC1ビットが0の場合はデータ無し
    while((UCSR1A & 0b10000000) == 0b00000000) ; // データが着信するのを待つ
    return UDR1;   // データ受信の場合は、UDR1レジスタを読み取る
}

void usart_init()
{
    // 通信速度の設定
    //     通信速度はCPUのクロック周波数によって値が変わる
    //     計算式は、 F_CPU / (16 * 通信速度) - 1
    //         得られた値を、UBRR1に設定する。
    UBRR1 = 25;  // 25: 38400bps @ 16MHz

    // 送信ユニット、受信ユニットを有効にする
    UCSR1B = (1 << RXEN1)|(1 << TXEN1);

    // 通信モードの設定
    //      データ長 8 bit
    //      ストップビット長  1bit
    UCSR1C = (0 << USBS1) | 0b00000110;
}

int main()
{
    CLKPR = 0x80; CLKPR = 0;    // 16MHz動作
    DDRB = 0xFF;

    usart_init();

    while(1) {
        PORTB = getch();  // 着信したデータはPORTBに表示する。
    }
}

                  図1 シリアルから文字の受信

 このプログラムは、PCからシリアル経由で受け取った文字を、PORTBに接続したバーLEDに表示するものである。

 このプログラムに以下の機能を追加する事を考えてみる。

  1. スイッチ(PD0)を押すと、LEDをリセットする。
  2. スイッチ(PD7)を押すと、LEDが1秒周期で点滅する。(受け取ったデータで点滅)

1.の機能は比較的容易で、関数getch()を以下のように修正すれば良い。

unsigned char getch(void)
{
 // UCSR1AレジスタのRXC1ビットが1になると、データの受信が完了し、UDR1レジスタにデータが保管される
 //  RXC1ビットが0の場合はデータ無し
    while((UCSR1A & 0b10000000) == 0b00000000) { // データが着信するのを待つ
        if ((PIND & 0b00000001) == 0b00000000) {
            PORTB = 0;
       }
    
}
    return UDR1;   // データ受信の場合は、UDR1レジスタを読み取る
}

                  図2 PORTBをリセットする処理

 ただし、getch()は、シリアルからデータを受信するための関数であり、この中にスイッチのイベントを処理するコードを書くのには違和感を感じることもあるだろう。

 次に、2の機能を考えてみると、次の様な関数を用意して、周期的に呼び出すようにすれば良い。

#define TRUE 1
#define FALSE 0
#define LED_BLINK_TIME 50000

// PORTBに接続したバーLEDを点滅させる。(PD7でON/OFF)
void blink_led()
{
    static long int time = 0;   // static宣言しておかないと周期的な呼び出しに対応できない
    static int rev = 0;           //       (値がリセットされてしまうので)
    static int pushed = FALSE;

    if ((PIND & 0b10000000) == 0b00000000) {
        pushed = TRUE;
        _delay_ms(3); // チヤタリングが消えるのを待つ
    }
    if (((PIND & 0b10000000) == 0b10000000) && (pushed == TRUE)) {
        pushed = FALSE;
        _delay_ms(3); // チヤタリングが消えるのを待つ
        rev = ~rev; // 点滅するか否か
    }

    if ((time > LED_BLINK_TIME) && rev) {   // 一定時間経過したら...
        PORTB = ~PORTB;  // PORTBを点滅
        time = 0;
    }
    time++;
}


void main()
{
 ......
  PORTB = 0b10101010;
 ......
    while(1) {
    ....
       blink_led();
 

                  図3 PORTBの点滅

  PORTBに適当な数値を設定しておいて、blink_led()をmain()の中のwile(1)による無限ループで、呼び出せば、スイッチPD7の操作でLEDの点滅を切り替えられる。

 ここまでは、問題無くコーディングできると思う。次に、3つの機能を1つにまとめる作業を進める。

#define F_CPU 16000000UL  // 16MHz動作 _delay_ms()などに関係

#include < avr/io.h >
#include < util/delay.h >
#include < avr/interrupt.h >
 

// PORTBに接続したバーLEDを点滅させる。(PD7でON/OFF)
void blink_led()
{
    static long int time = 0;   // static宣言しておかないと周期的な呼び出しに対応できない
    static int rev = 0;           //       (値がリセットされてしまうので)
    static int pushed = FALSE;

    if ((PIND & 0b10000000) == 0b00000000) {
        pushed = TRUE;
        _delay_ms(3); //
※⑤ チヤタリングが消えるのを待つ
    }
    if (((PIND & 0b10000000) == 0b10000000) && (pushed == TRUE)) {
        pushed = FALSE;
        _delay_ms(3); //
※⑥ チヤタリングが消えるのを待つ
        rev = ~rev; // 点滅するか否か
    }

    if ((time > LED_BLINK_TIME) && rev) {   // 一定時間経過したら...  ※⑦
        PORTB = ~PORTB;  // PORTBを点滅
        time = 0;
    }
    time++; //
※⑦
}

// 一文字受信(PC → AVR)
unsigned char getch(void)
{
 // UCSR1AレジスタのRXC1ビットが1になると、データの受信が完了し、UDR1レジスタにデータが保管される
 //  RXC1ビットが0の場合はデータ無し
   while((UCSR1A & 0b10000000) == 0b00000000) { // データが着信するのを待つ
        if ((PIND & 0b00000001) == 0b00000000) { 
// ※③
            PORTB = 0;
       }
       blink_led(); 
// ※②
    }

    return UDR1;   // データ受信の場合は、UDR1レジスタを読み取る
}

void usart_init()
{
    // 通信速度の設定
    //     通信速度はCPUのクロック周波数によって値が変わる
    //     計算式は、 F_CPU / (16 * 通信速度) - 1
    //         得られた値を、UBRR1に設定する。
    UBRR1 = 25;  // 25: 38400bps @ 16MHz

    // 送信ユニット、受信ユニットを有効にする
    UCSR1B = (1 << RXEN1)|(1 << TXEN1);

    // 通信モードの設定
    //      データ長 8 bit
    //      ストップビット長  1bit
    UCSR1C = (0 << USBS1) | 0b00000110;
}

int main()
{
    CLKPR = 0x80; CLKPR = 0;    // 16MHz動作
    DDRB = 0xFF;

    usart_init();

    while(1) {
        PORTB = getch();  // 着信したデータはPORTBに表示する。
       blink_led(); // ※①
    }
}

                                     図4   実装方法

1.の機能(PORTBをリセット)については、の様に記述すればよい。
2.の機能(LEDの点滅)については、

  • blink_led()をの位置に挿入すると、うまく動かない。これは、getch()が文字を受け取るまでリターンしないため。
  • の位置に挿入すると何となく動いているように見えるが、このままだと問題がある。
      (連続して文字が送られてくると、の待ちでシリアルからのデータを取りこぼす危険がある。_delay_ms(3)の待
       ち時間は3ミリ秒、シリアルの通信速度が38400bpsであれば、1文字の送信にかかる時間は0.26ミリ秒なので)

  取りこぼしの問題については、⑤、⑥の待ちループを、⑦の様に開いたループの形にするか、⑤、⑥の_delay_ms()の中で、シリアルポートのRXC1ビットをチェックする処理を入れるかになる。
 いずれにしろ、getch()の中に処理を記述してゆくことになり、非常に汚いコードになって行くことは避けられない。
 また、_delay_ms()の中でRXC1ビットをチェックする(_delay_ms()の代わりになる関数を定義して)ということになると、イベントのチェックをプログラムの複数個所でチェックするようになり、更に汚いコードになる。

 このような事態を緩和する目的として、イベントループを導入する。イベントループを使うことで、外部からの入力等で発生するイベントをチェックする専用の関数を別に定義し、イベントの検出とそれに対応する処理を切り離すことが出来る。

#define F_CPU 16000000UL  // 16MHz動作 _delay_ms()などに関係

#include < avr/io.h >
#include < util/delay.h >
#include < avr/interrupt.h >

#define TRUE 1
#define FALSE 0
#define LED_BLINK_TIME 50000

int Rev = 0;

// PD7に接続したスイッチが押されたかどうか判定
int PD7_pushed()
{
    static int pushed = FALSE;
    static int c_time = 0;  // チャタリング待ち用のタイマ変数

    if ((PIND & 0b10000000) == 0b00000000) {
        if ((c_time > 500) || (pushed == TRUE)) { // チヤタリングが消えるのを待つ
            pushed = TRUE;
            c_time = 0;
        } else {
            c_time++;
        }
    }
    if (((PIND & 0b10000000) == 0b10000000) && (pushed == TRUE)) {
        if (c_time > 500) {
            pushed = FALSE;
            c_time = 0;
            return TRUE;
        } else {
            c_time++;
        }
    }
    return FALSE;
}

// PD0に接続したスイッチが押されたかどうか判定
int PD0_pushed()
{
    static int pushed = FALSE;
    static int c_time = 0;  // チャタリング待ち用のタイマ変数

    if ((PIND & 0b00000001) == 0b00000000) {
        if ((c_time > 500) || (pushed == TRUE)) { // チヤタリングが消えるのを待つ
            pushed = TRUE;
            c_time = 0;
        } else {
            c_time++;
        }
    }
    if (((PIND & 0b00000001) == 0b00000001) && (pushed == TRUE)) {
        if (c_time > 500) {
            pushed = FALSE;
            c_time = 0;
            return TRUE;
        } else {
            c_time++;
        }
    }
    return FALSE;
}

// PORTBに接続したバーLEDを点滅させる。(PD7でON/OFF)
void blink_led()
{
    static long int time = 0;

    if ((time > LED_BLINK_TIME) && Rev) {
        PORTB = ~PORTB;
        PORTC = 0b10000000;
        time = 0;
    }
    time++;
}

// 一文字受信(PC → AVR)
void  getch(void)
{
    PORTB = UDR1;   // データ受信の場合は、UDR1レジスタを読み取る
}

void blink_change()
{
      Rev = ~Rev;
}

void portb_reset()
{
    PORTB = 0;
}

void EventLoop()
{
    if ((UCSR1A & 0b10000000) == 0b10000000) { // データが着信するのを待つ
        getch();
    }
    if (PD7_pushed()) { // スイッチが押されたら
        blink_change();
    }
    if (PD0_pushed()) { //
        portb_reset();
    }
}


void usart_init()
{
    // 通信速度の設定
    //     通信速度はCPUのクロック周波数によって値が変わる
    //     計算式は、 F_CPU / (16 * 通信速度) - 1
    //         得られた値を、UBRR1に設定する。
    UBRR1 = 25;  // 25: 38400bps @ 16MHz

    // 送信ユニット、受信ユニットを有効にする
    UCSR1B = (1 << RXEN1)|(1 << TXEN1);

    // 通信モードの設定
    //      データ長 8 bit
    //      ストップビット長  1bit
    UCSR1C = (0 << USBS1) | 0b00000110;
}

int main()
{
    CLKPR = 0x80; CLKPR = 0;    // 16MHz動作
    DDRB = 0xFF;

    PORTD = 0b10000001;        // プルアップ設定
    MCUCR &= ~(1 << PUD);

    usart_init();

    while(1) {
        blink_led();
        EventLoop();
    }
}

                    図5 イベントループによる実装例
 

最終更新:2016年03月24日 10:38