トップ 新規 編集 差分 一覧 ソース 検索 ヘルプ PDF RSS ログイン

Getting Started Notes - UART

最終更新時間:2010年06月25日 12時19分05秒

UART (Universal Asynchronous Receiver Transmitter)

修正履歴

  • 2010/04/13 - 受信割込によるエコープログラムを追加
  • 2005/08/03 - 受信フロー制御追加
  • 2005/06/07 - リングバッファ:バッファサイズが2^nに限定されないように修正
  • 2005/04/20 - 受信リングバッファ送受信のバッファ変数がvolatile属性でなかったのを修正
  • 2005/04/15 - 受信リングバッファ添え字に&(bufsize-1)をかけるのを忘れていた。修正
  • 2005/03頃 - mega対応で書き直しました。同期通信など一部機能説明は省略。

概要

パソコンとあなたのプロセッサの間でデータを送受信するのに便利な方法の1つに、UARTがあります。この機構はマイクロコントローラに内蔵されており、(非同期)シリアルインタフェースによる通信を可能にします。STK200学習ボードでは、これらのデータ線はD-SUB9pinコネクタに接続されています。

パソコンとの通信を行うためにはシリアルケーブルでパソコンのシリアルポートとこのボードのDSUB9コネクタをつながなければなりません。これに加えて通信を可能にするプログラムを走らせることで通信が成立します。

通信のためのプログラミングでは、まずコントローラとコンピュータ、双方のプログラムの間でデータ交換をするため、両者の速度を合わせなければなりません。さらに、通信の同期も必要です。

 UART関連レジスタ

UBRRL,UBRRH (AT90SではUBRR)

AVRで使用可能なボーレート(Baud Rate,1秒間に送れるビット数。制御用のビットも含む)は、システムクロックに使用されている水晶発振子の周波数及びプロセッサの使用によって決まります。

データシートに記載されている、通信速度とクロック周波数によって決まる値を (UBRRL,UBRRH)レジスタに書き込むことで正しい通信速度が決定できます。たとえば、データシートを見ると、fosc=4MHz、BaudRate=9600bpsの場合、UBRRL=25としなければならないことがわかります。UBRRL値は整数値なので、設定条件によっては若干の誤差が生じる場合があります。データシートには誤差も記載されています。理論上は4%程度まで通信可能ですが、波形の乱れなどの誤差も考えて誤差が2%未満、できれば1%未満となる条件が推奨されます。

もう1つの方法は計算で適切なUBRR値を導き出すことです。

UBRR = fosc(Hz) / (BaudRate*16) - 1     (端数は四捨五入,U2Xbit=0)
UBRR = fosc(Hz) / (BaudRate*8) - 1     (端数は四捨五入,U2Xbit=1)

四捨五入までをマクロで行いたければ以下をつかえばよいでしょう。F_CPUはmakefile内で指定できるシステムクロック周波数です。

#define UBRR_Value(b)       (F_CPU-8*b)/(16*b)
#define UBRR_U2XValue(b)    (F_CPU-4*b)/( 8*b)

ただし、このマクロで求めたUBRR値を使った場合、誤差が推奨範囲内である保証はありません。求めたUBRR値から実現するボーレートを逆算し、これが望むボーレートとの誤差が2%以内であるかどうかを確認する必要があります。

#define BAUD_Value(u)     (F_CPU/(16*(u+1))) 
#define BAUD_U2XValue(u)  (F_CPU/( 8*(u+1))) 
  • ボーレート計算式として BAUD = fOSC /(16 x UBRR + 1) という式が和訳データシートに載っているが、これは BAUD = fOSC /(16 x (UBRR + 1)) という意味で、BAUD = fOSC /((16 x UBRR) + 1) ではない。注意されたい。

UBRRLとUBRRHはアドレスが離れている(*)せいか、残念ながらペアレジスタ変数として1度に16bit変数からセットすることはできません。以下のように8bitずつに分けてセットしてください。上位下位どちらからセットしても良いようですが、習慣として書き込みは上位からということにしておいた方がいいかもしれません。

UBRRH=UBRR_Value(38400) >> 8;
UBRRL=UBRR_Value(38400) &  0xFF;

UBRRHは実際には下位4bitしか使えないので、UBRR指定は12bitで4095(0x0FFF)が上限となります。これ以上の値を入れないでください。デバッグのために数bpsで出力したいなどという場合にはまるかもしれません・・・

(*) mega48/88では連続した2バイト(0xC4,0xC5)になり、ワードアクセスができるように改善されている。つまりこう書けるようになった:

UBRR0 = UBRR_Value;

UDR

実際の送信データはデータレジスタUDRに書き込んで送信され、受信データは同じUDRから読み出すことで得られます。このレジスタは特殊で、UDRに書き込んだ後UDRを読み出してもUDRに書き込んだ値が読み出せるわけではありません。名前は同じですが、UDR読み出しとUDR書き出しでは別のハードウェアをアクセスしていると考えてください。

UCSRA (AT90SではUSR)

UCSRA(USR) はUDRに読み書きしたデータの送受信状況の情報を示します。
位置 bit名 機能 bitクリア条件
7 RXC UDRに新しいデータが到着、読み出し待ち中 UDR読み出し
6 TXC データ送信完了 UDRへの書き込み
5 UDRE UDR空き、UDRへ書き込み可 UDRへの書き込み
4 FE ストップビットが検出できない 新しいデータ到着
3 OR UDRを読み出さないうちに次のデータが到着・上書きされた 新しいデータ到着
2 PE(megaのみ) パリティエラー検出(UCSRCのUPM1 UPM0が設定されているとき)
1 U2X(megaのみ) 倍速許可。
0 MPCM(megaのみ) マルチプロセサ通信動作

UCSRB (AT90SではUCR)

UCSRB(UCR)はUARTの設定を行うレジスタです。

UCSRB(UCR) - register:
位置 bit名 機能
7 RXCIE 受信割り込み許可。UCSRA-RXC=1でSIG_UART_RECV割り込み
6 TXCIE UART送信完了割り込み許可。UCSRA-TXC=1でSIG_UART_TRANS割り込み
5 UDRIE UDR空き割り込み許可。UCSRA-UDREがセットでSIG_UART_DATA割り込み
4 RXEN 受信許可
3 TXEN 送信許可
2 UCSZ2 9bit/characterで送受信を行う.AT90SではCHR9
1 RXB8 9bit設定時、受信データのbit8はここから読み出す
0 TXB8 9bit設定時、送信データのbit8はここに書き込む

UCSRC (megaのみ)

通常のUART通信をする場合には設定の必要はないレジスタです。説明省略〜

UCSRC - register:
位置 bit名 機能
7 URSEL UCSRCに書き込むときは必ず1 (*)(**)
6 UMSEL 同期通信を行う
5-4 UPM1-0 パリティ選択
3 USBS stopビットを2bitにする
2-1 UCSZ1-0 UCSRBのUCSZ2とあわせて、5〜9bit幅のデータ送受信設定を可能にする(***)
0 UCPOL 同期通信時のクロック極性選択

(*)UCSRCがUBRRHとアドレスを共用しているための仕掛け。UBRRHは4bitしか使われていないので、このビットは必ず0になる。

(**)mega48/88ではUCSRCとUBRRHが分離され、URSELはUMSEL1になり、原則として0を書くようになった。詳細はデータシート参照。

(***)他のビットを操作する為にバイト単位で書き込むときは、これらのビットの初期値が1であることに注意してください。

 送信

簡単な送信プログラム

  • UDRに最初のデータが書かれると、すぐに送信機構にデータが転送され送信される。転送後UDRは空く。
  • UDRが空いたら、データを書き込むことが出来る。前のデータの送信が終わるまで送信機構が使用中なのでUDRに書き込んだデータは送信機構には送られず待機させられる。
  • この間は「UDRはビジー」となり、UDRに次のデータを書き込むことは出来ない。
  • 送信中データの送信が終わると、UDR内データが送信機構に送られ、UDRが空く。
  • UDRが空いたことを知るには、UCSRAレジスタのUDREビットを見ればいい。これがセット(1)されていればUDRは空いている。
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>

const prog_char a[]  = "This is a test message";

void uart_putchar(char c){
    loop_until_bit_is_set(UCSRA, UDRE); //UDREビットが1になるまで待つ
    UDR = c;
}
void uart_putstr_pgm(char *s){
    char c;
    while(1){
        c = pgm_read_byte(s++);
        if (c==0) break;
        uart_putchar(c);
    }
}
int main(void){
    UCSRB = _BV(TXEN); /* 送信許可 */
    UBRRH = 0;
    UBRRL = 25;    /* 4MHzにて9600bps設定 */
    for(;;){
        uart_putstr_pgm(a);
        /*他の処理*/
    }
}

1文字をUARTで送信する関数uart_putchar()を作成しています。UCSRAのビットUDREをチェックし、UDRが空くまで待ち、空いたらUDRに文字を渡します。メインプログラムではプログラムメモリ上の文字列から1文字ずつ取り出し、uart_putchar()により送信しています。1文字ずつ送信する毎にUDRが空くのを待つので、このケースでは、uart_putstr_pgm()の実行時間は22.88msecかかります。同時に他の処理を行うと、全体の処理時間は22.88msec+「他の処理」時間となります。

割り込みを使った送信

  • 最初の送信データをUDRにセット後、UDRIEをセットするとUDR空き割り込みが有効になる
  • 以後、UDRが空くと、UCSRAレジスタのUDREビットがセットされる。このときUDR空き割り込みがかかり、割り込みルーチンが呼ばれる
  • 割り込みルーチンが呼ばれた時点でUDRは空いているので、割り込みルーチンで次のデータをUDRに送る処理をする。送信するデータがなくなった場合は割り込みルーチン内で判定し、UDRIEをクリアしてそれ以上割り込みが掛かるのを止める
  • UART以外にも言えることですが、割り込み内で書き替える変数にはvolatile属性をつけて、変更が確実に変数に反映されるようにして下さい。
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <string.h>

const prog_char a[]  = "This is a test message";
volatile char *tx_s;  /*a[]をコピーして使うポインタ*/
ISR(UART_UDRE_vect){
    char c;
    c = pgm_read_byte(tx_s++);
    if (c==0)
        UCSRB = UCSRB & ~_BV(UDRIE); /* UDR空き割り込み禁止 */
    else
        UDR = c;
}
void uart_putstr_pgm(char *ss){
    while (bit_is_set(UCSRB,UDRIE));  /*他の文字列を表示中なら、終わるまで待つ*/
    tx_s=ss;
    UCSRB = UCSRB | _BV(UDRIE); /* UDR空き割り込みを有効にする。 */
}

int main(void){
    UCSRB = _BV(TXEN); /* 送信許可 */
    UBRRH = 0;
    UBRRL = 25;       /* 4MHzにて9600bps設定 */
    sei();
    for(;;){
        uart_putstr_pgm(a);
        /*その他の処理*/
    }
}

今回のサンプルでは、送信ルーチン uart_putstrで文字列のアドレスだけをグローバル変数tx_sにセットし、UDRIEをセットします。UDRIEがセットされたことで、UDRがあくたびにUDR空き割り込みルーチンが呼ばれます。このルーチンでtx_sを1つずつ進めながらプログラムメモリから1文字とりだして送信する動作を行います。割り込みルーチン内で文字列末尾の'\0'を検出したら、UDRIEをクリアしてこの先の送信を止めるようにしています。

初回のuart_putstrの待ち時間はほとんどゼロになります。しかし2回目以降の実行は、先に送信指定した文字列が送信しきるまで待たなければならないので、前記プログラムと同様待たされます。前記プログラムとの違いは、uart_putstr_pgm(a)間に別の処理がある場合に現われます。

送信時間が23msecかかる処理と、20msecかかる他の処理を交互に行う場合、

非割り込みでは、23+20=43msecかかる
 0msec  uart_putstr_pgm(a)をコール。
23msec  uart_putstr_pgm(a)送信終了、他の処理を開始
43msec  他の処理が終了、uart_putstr_pgm()をコール。以下繰り返し
割り込みでは、以下のように23msecで完了する
 0msec  uart_putstr_pgm(a)をコール。関数はすぐに終了、割り込みルーチンは送信を開始
 1msec  他の処理を開始
21msec  他の処理が終了、uart_putstr_pgm()がコールされるが前の送信が終了するのを待つ
23msec  割り込みルーチンが送信終了、uart_putstr_pgm()が動き出す。以下繰り返し

リングバッファによる送信

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <string.h>
#include <stdlib.h>

#define rx_bufsize (16)
#define tx_bufsize (16)

typedef struct{
    char buf[tx_bufsize];  /*送信バッファサイズ*/
    uint8_t buf_in;     /* バッファに文字を押し込むときの位置*/
    uint8_t buf_size;   /*バッファに貯められている文字数 */
} TxBuf_t;

volatile TxBuf_t TxBuf;

const prog_char a[]  = "\nTCNT1 = ";

ISR(UART_UDRE_vect){
    /* TxBuf.buf_size==0の状態でDATA割り込みを許可すると変なことになります。注意。 */
    uint8_t buf_out;
    buf_out=TxBuf.buf_in-TxBuf.buf_size;
    if (TxBuf.buf_in<TxBuf.buf_size) buf_out += tx_bufsize;
    UDR = TxBuf.buf[buf_out];
    TxBuf.buf_size--;
    if (TxBuf.buf_size==0) UCSRB = UCSRB & ~_BV(UDRIE); /* UDR空き割り込み禁止 */
}

void uart_putchar(char c){
    while (TxBuf.buf_size > (tx_bufsize-1)) ;  /* バッファが空くまで待つ */
    cli(); //バッファを書き換える(読み出しも含む)操作は必ず割り込み禁止で!
    TxBuf.buf[TxBuf.buf_in++] = c;
    TxBuf.buf_size++;
    if (TxBuf.buf_in==tx_bufsize) TxBuf.buf_in=0;
    UCSRB = UCSRB | _BV(UDRIE); /* UDR空き割り込みを有効にする。 */
    sei();
}

void uart_putstr_pgm(char *s){
    char c;
       for(;;){
        c = pgm_read_byte(s++);
        if (c==0) break;
        uart_putchar(c);
    }
}
void uart_putstr(char *s){
    char c;
      for(;;){
        c = *s++;
        if (c==0) break;
        uart_putchar(c);
    }
}
int main(void){
    char buf[8];
    UCSRB = _BV(TXEN); /* 送信許可 */
    UBRRH = 0;
    UBRRL = 25;       /* 4MHzにて9600bps設定 */
    TCCR1B = 1;      /* TCNT1を動かす */
    TxBuf.buf_size = 0;    /*バッファを空にする */
    for(;;){
        uart_putstr_pgm(a);
        itoa(TCNT1,buf,10);
        uart_putstr(buf);
    }
}

このプログラムは、送りたい文字列をとりあえず別の場所(バッファ)にため込んでおき、UDR空き割り込みの方でバッファ内の文字列を1文字ずつ送信しています。バッファ内に文字がなくなってもUDRが空いている限りこの割り込みは掛かり続けますので、バッファ内に送る文字がなくなったときはUDRIEをクリアしていったん割り込みを禁止します。バッファに文字列を送り込んだ後に再度UDRIEをセットして送信を再開させます。

FIFOバッファの構造
        buf_in-buf_size      buf_in
             ↑                ↓
buf[]  □□□■■■■■■■■■□□□□
        0 1 2 3 4 5 6 7 8 9101112131415
             |←−buf_size−→|

buf_in は、これからバッファに追加する文字を保存する場所を表します。バッファに文字を追加するときはbuf_in位置に書き込み、次の保存のためにbuf_inは+1され、現在覚えている文字数を記憶するための変数 buf_sizeも+1されます。最初に覚え込ませた変数は(buf_in-buf_size)の位置にあります。UDRバッファが空いたら、UDR空き割り込みを利用して、ここから取り出した文字をUDRに送り、buf_sizeを-1することで次の取り出しに備えます。これにより先着者から処理を行う、Fast-In-Fast-Out(FIFO)バッファが実現できます。

このままではbuf_inが15を越えてしまうのですが、バッファをリング状に使うことで解決します。buf_inが16(=buf[]のサイズ)になったらbuf_inを0に戻してやります。バッファにデータを追加する時にこのチェックを行うことでうまく行きます。

バッファが満杯なのにさらに押し込もうとしたり、空なのにさらに取り出そうとしたりしないように、バッファから出し入れするときはbuf_sizeを事前チェックします。

割り込みルーチンもバッファを操作する場合、バッファの操作は必ず割り込み禁止状態で行ってください

            buf_in-buf_size        buf_in=16
                   ↑6                 ↓
buf[]  ■□□□□□■■■■■■■■■■■□□□□
        0 1 2 3 4 5 6 7 8 9101112131415 0 1 2 3 4
                   |←−buf_size−−−→|

TXCを使う

TXC が 0 だからといって送信中とは限らず(初期状態は 0)、1 だからといって送信が完了しているとは限りません(空いたあと、UDRにデータが送りこまれたかもしれない)。UDR空き割り込みと同時に管理する必要があります。

  • というか、けっこう難しい。どっかに実例ないかな。
  • というか、TXCって出番少なそうですね。どんな場合に使うのでしょう?データを送り終わったらスリープするなんて場面かな?
  • そうですね。半二重以外では、TX/RX の先に繋がっている RS232C ドライバを寝かせる場合とか。
  • これはRS485の半二重で利用するのが一般的です。送信完了で外部のドライバとレシーバをハード的に切り替えるのに使用します。

送信部はこんな感じ(未確認; ATmega48 を想定)?

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

// tx_bufsize は 2 のベキであること。
#define tx_bufsize (16)

struct {
    char buf[tx_bufsize];
    uint8_t buf_in, buf_size;
} TxBuf;

typedef enum { UARTTX_OFF,        // UDRバッファもリングバッファも空、送信回線もオフ。
               UARTTX_EMPTY,      // リングバッファは空、UDRバッファ内にはデータがあり、送信中。
	       UARTTX_RUN,        // リングバッファにもUDRバッファ内にもデータがあり、送信中。
               UARTTX_DISCONT     // リングバッファにはデータがあるかもしれないが、送信はしていない。
                              } UARTTX_STATE;
static volatile UARTTX_STATE uart_tx_state = UARTTX_OFF;

ISR(USART_TX_vect){
    UCSR0B &= ~_BV(TXCIE0);
    sei();                         // 送信完了割り込み以外を許可。
    uart_tx_state = UARTTX_DISCONT;
    // 送信完了割り込み発生後に送信完了割り込みが必要になることはない。
}

ISR(USART_UDRE_vect){
    UCSR0B &= ~_BV(UDRIE0);
    if(UCSR0A & _BV(TXC0)) UCSR0A = _BV(TXC0); // uart_tx_putchar内で禁止されてしまった完了割り込みをクリア。
    sei();                            // UDR空き割り込み以外を許可。

    TxBuf.buf_size --;
    UDR0 = TxBuf.buf[(TxBuf.buf_in - TxBuf.buf_size) & (tx_bufsize - 1)];

    cli();
    if (TxBuf.buf_size != 0) {
	UCSR0B |=  _BV(UDRIE0);   // データがある。割り込みから復帰と同時にUDR空き割り込みを再許可。
	uart_tx_state = UARTTX_RUN;
}
       else{
	UCSR0B |=  _BV(TXCIE0);   // バッファは空。次の割り込みが必要ならそれは送信完了割り込みのはず。
	uart_tx_state = UARTTX_EMPTY;
    }
}

int uart_tx_waitfor_discont(void){   /* 今送信中のデータを送り終わるまで待つ   */

    if(uart_tx_state == UARTTX_RUN) {
	cli();
	UCSR0B = (UCSR0B | _BV(TXCIE0))  & ~_BV(UDRIE0);  // UDR空き割り込みのかわりに完了割り込みを許可。
	sei();
	while(uart_tx_state != UARTTX_DISCONT);
	return 0;
    }
    while(uart_tx_state == UARTTX_EMPTY);   // 最後の 1 文字の送信に入っているので、ただ待つ。
    return 0;
}    
 
int uart_tx_waitfor_end(void){       /* すべてのデータが送り終わるまで待つ   */

    if(uart_tx_state == UARTTX_OFF) return 0;
    while(uart_tx_state != UARTTX_DISCONT);
    return 0;
}

int uart_tx_cont(void){  /* 送信再開 */
    uint8_t sreg;

    if (TxBuf.buf_size == 0) {
	uart_tx_state = UARTTX_DISCONT; // データがないので再開不能。
	return -1;
    }
    uart_tx_state = UARTTX_RUN;
    sreg = SREG;
    cli();
    UCSR0B |=  _BV(UDRIE0);             // UDR空き割り込みを許可して正常再開。
    SREG = sreg;
    return 0;
}

void uart_tx_putchar(char c) {  /* 1 文字送信 */
    if(TxBuf.buf_size > (tx_bufsize - 1) ){               // バッファフルならばまず送信許可から。
	cli();
	UCSR0B = (UCSR0B | _BV(UDRIE0))  & ~ _BV(TXCIE0); // UDR空き割り込みを許可。完了割り込みを禁止。
	sei();
	while(TxBuf.buf_size > (tx_bufsize - 1) );        // バッファが空くまで待つ。
    }
    cli();
    TxBuf.buf[(TxBuf.buf_in++) & (tx_bufsize - 1)] = c;
    TxBuf.buf_size++;
    UCSR0B = (UCSR0B | _BV(UDRIE0))  & ~ _BV(TXCIE0);
    uart_tx_state = UARTTX_RUN;
    sei();
}
  • 送信データ数をカウントし、送信割り込みで同じカウンタをデクリメントして、未送信データ数を把握するというのはどうだろう?途中割り込みなし状態で送信したりしたらむちゃくちゃになるので注意が必要だけど
uint8_t TX_CNT=0;
ISR(UART_UDRE_vect) {
    UDR0 = hogehoge:
    TX_CNT++; //UDRにデータを書き込むすべての場所に記述
}
ISR(USART_TX_vect) {
    TX_CNT--;
    if (!TX_CNT){
       //データ送出し切った時の処理
    }
}

 受信

割り込みを使わない場合

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

int main(void){
    DDRB = 0xFF;
    UCSRB = _BV(RXEN); /* 受信有効 */
    UBRRH = 0;
    UBRRL = 25;    /* 通信速度設定 */
    sei(); /* enable interrupts */
    for(;;){
        if (bit_is_set(UCSRA,RXC)) PORTB = UDR;
        /*他の処理*/
    }
}

データが受信されるとUCSRAのビットRXCがセットされます。これを感知したらUDRを読み出しPORTBに出力してLED表示しています。

このケースはこれでも問題ないのですが、forループ内で他の処理も行う場合、UDRからデータを読み出す前に次のデータが到着してしまいデータが壊れてしまうことがあります。受信チェック間隔(受信処理時間+他の処理時間+ループ処理時間)が1バイト受信時間(*1)より充分短い必要があります。それが実現できない場合は割り込みを使います。

 *1:8bit,stopbit=1ならおおむね(1,000,000/ボーレート/10)μsec。

割り込みによる受信

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

ISR(UART_RX_vect) {
    PORTB = UDR;
}
int main(void){
    DDRB = 0xFF;
    UCSRB = _BV(RXCIE)|_BV(RXEN); /* 受信・受信割り込み有効 */
    UBRRH = 0;
    UBRRL = 25;    /* 通信速度設定 */
    sei(); /* enable interrupts */
for(;;){}
}

このプログラムは9600bpsでPCから送られたデータをデータレジスタUDRでうけます。受信時に受信割り込みルーチンがコールされ、割り込み処理によりすぐにUDRが読み出され、適切に処理されます。文字データはPORTBに出力され、LEDで表示されます。

もう少し複雑なケース

受信データが1バイトではなく、文字列や多バイト整数の場合は受信割り込み側で工夫すると便利です。たとえば文字列+改行をうけ、文字列が決められたパスワードと合致していればLEDを光らせたい場合は以下のような感じで実現します。

#include <avr/io.h>
#include <avr/interrupt.h>
#include <string.h>

volatile uint8_t uart_i = 0; /*受信した文字数*/
volatile char uart_s[17]; /*受信した文字列*/
ISR(UART_RX_vect) {
     char c;
     c = UDR;
     if (c < ' ') /*改行*/
     {
         if (uart_i==0) return;
         uart_s[uart_i]=0;
         if (strcmp(uart_s,"PassWord")==0) 
             PORTB = 0x00; /* Password ok, OOOOOOOO */
         else
             PORTB = 0x55; /* Password NG, OxOxOxOx */
         uart_i=0;
     }
     else{
         if (uart_i>=16) return;
         uart_s[uart_i++] = c;
     }
}
int main(void)
{
    DDRB = 0xFF;
    UCSRB = _BV(RXCIE)|_BV(RXEN); /* 受信・受信割り込み有効 */
    UBRRH = 0;
    UBRRL = 25;    /* 通信速度設定 */
    sei(); /* enable interrupts */
    
    for(;;){}
}

受信文字列はとりあえずuart_s[]に保存されます。16文字を越えないようにuart_iで管理しています。改行文字(面倒なので0x20未満)を確認したら、末尾に0をつけ文字列の形にした後,strcmp関数でパスワード文字列と比較し、比較結果をPORTBにつないだLEDで表示しています。

リングバッファによる受信

他の処理が長引いた場合、データを取りこぼす可能性があります。そのようなおそれがある場合はバッファ付き受信で行う意味があります。他の処理の時間が短く、受信間隔より充分短いとわかっている場合はバッファ処理する意味はあまりないでしょう。たとえば、9600bps受信で、1msecより十分短い間隔で受信チェックを行えるならばポーリングで充分です。

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <string.h>
#include <stdlib.h>

#define rx_bufsize (64)
typedef struct{
    char buf[rx_bufsize];
    uint8_t buf_in,buf_size;
} RxBuf_t;

volatile RxBuf_t RxBuf;

ISR(UART_RX_vect) {
     char c;
     c = UDR;
     if (RxBuf.buf_size>=rx_bufsize) {
         return; 
         //フロー制御なし。バッファあふれの場合到着データは失われる
     RxBuf.buf[RxBuf.buf_in++]=c;
     if (RxBuf.buf_in>=rx_bufsize) RxBuf.buf_in=0;
     RxBuf.buf_size++;
}

uint8_t uart_getch(void){
    char c;
    uint8_t buf_out;
    while (RxBuf.buf_size) ;
    cli();
    buf_out=RxBuf.buf_in-RxBuf.buf_size;
    if (RxBuf.buf_in<RxBuf.buf_size) {
        buf_out += rx_bufsize;
    }
    c = RxBuf.buf[buf_out];
    if (RxBuf.buf_size==0) {
        RxBuf.buf_size += rx_bufsize;
    }
    RxBuf.buf_size--;
    sei();
    return c;
}

int main(void){
    char buf[16];
    uint8_t buf_i;
    int16_t res;
    DDRB = 0xFF;
    UCSRB = _BV(RXCIE)|_BV(RXEN); /* 受信・受信割り込み有効 */
    UBRRH = 0;
    UBRRL = 25;    /* 通信速度設定 */
    RxBuf.buf_in = RxBuf.buf_size = 0; /*バッファクリア*/
    sei(); /* enable interrupts */
    buf[0]=0;
    buf_i=0;
    for(;;){
        if (RxBuf.buf_size) {
            res = uart_getch();
            if (buf_i >= (sizeof(buf)-1) ) res=0;
            if (res) {
                buf[buf_i++] = res;
            } else {
                buf[buf_i]   = 0;
                if (strcmp(buf,"PassWord")==0) 
                    PORTB = 0x00; /* Password ok, OOOOOOOO */
                else
                    PORTB = 0x55; /* Password NG, OxOxOxOx */
                buf_i=0;
            }
        } else {
            /*あれば他の処理*/
        }
    }
}

受信フロー制御(動作未確認)

以下のような場合はフロー制御をする必要があります。

  • 他の処理がバッファで吸収できないほど長くかかる場合
  • 受信速度より処理速度が遅い場合
  • 逆に受信速度を十分速くして受信待ちのロスをなくしたい場合

割り込みルーチン部分とuart_getch部分のみ掲載。出力ピンを1つ用意し(RTS)、メインルーチンで出力設定し、これを操作することで送信元に送信可否を伝える。

(ヘッダなど省略)
#define RTS PD4

ISR(UART_RX_vect) {
     char c;
     c = UDR;
     if (RxBuf.buf_size>=(rx_bufsize-16) ) {
         PORTD |= (1<<RTS);	//RTS=OFF(RS232C側では-12V、AVR側ではHigh)
         //RTS(Request To Send)に相当する信号線を用意しRxBuf.buf_sizeが
         //フルに近くなったらRTSをoffにして送信側に送信を止めさせる
         //参考:http://www.linux.or.jp/JF/JFdocs/Serial-HOWTO-16.html
     }
     RxBuf.buf[RxBuf.buf_in++]=c;
     if (RxBuf.buf_in>=rx_bufsize) RxBuf.buf_in=0;
     RxBuf.buf_size++;
}

uint8_t uart_getch(void) {
    char c;
    uint8_t buf_out;
    while (RxBuf.buf_size) ;
    cli();
    buf_out=RxBuf.buf_in-RxBuf.buf_size;
    if (RxBuf.buf_in<RxBuf.buf_size) {
        buf_out += rx_bufsize;
    }
    c = RxBuf.buf[buf_out];
    if (RxBuf.buf_size==0) {
        RxBuf.buf_size += rx_bufsize;
    }
    RxBuf.buf_size--;
    sei();
    if (RxBuf.buf_size<rx_bufsize-16) {
        PORTD &= ~(1<<RTS);
        //フロー制御を行う場合は、RxBuf.buf_sizeが減ってきたらRTSをonにして
        //(RS232C側で+12V、AVR側でLow)、送信側にデータ送信再開を許可する
    }
    return c;
}

int main(void) {
    :(略)
    DDRD = (1<<RTS);   //RTSを出力に
    PORTD=(1<<RTS);    //準備ができるまでRTS=OFF(High)
    :(略)
    sei(); /* enable interrupts */
    PORTD &= ~(1<<RTS);  //受信準備が整ったらRTS=ON(Low)にする
    :
    (以下略)

受信フロー制御を行っている相手に送信する(作成中)

相手のRTSに接続されている線(CTS)を読み取り、これがOff(AVR側でHigh)である場合は送信を停止します。

バッファつき送信の場合、送信を停止した結果UDR空き割り込みがかかり続けますので、UDR空き割り込みを停止する必要があります。また、CTSを監視して、Onになったら送信再開をする必要があります。

  • タイマ割り込みで定期的にチェック
  • CTSを外部割込みに接続し、外部割込みルーチンでCTSON/OFF処理を行う

など。ややこしいので、送信はバッファつきじゃないほうが楽かも・・・

 送受信

一部を除くAVRのUARTは全二重で、送信と受信は同時に動かすことができます。PCなどの端末に接続し、パスワードを入力し、もしそれが正しければ[OK]を、誤っていれば[NG]を返すものを作ってみます。(動作未確認)

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <string.h>
#include <stdlib.h>

#define rx_bufsize (64)
typedef struct {
    char buf[rx_bufsize];
    uint8_t buf_in,buf_size;
} RxBuf_t;

volatile RxBuf_t RxBuf;

ISR(UART_RX_vect) {
     char c;
     c = UDR;
     if (RxBuf.buf_size>=rx_bufsize) {
         return;
     }
     RxBuf.buf[RxBuf.buf_in++]=c;
     if (RxBuf.buf_in>=rx_bufsize) RxBuf.buf_in=0;
     RxBuf.buf_size++;
}

uint8_t uart_getch(void) {
    char c;
    uint8_t buf_out;
    while (RxBuf.buf_size) ;
    cli();
    buf_out=RxBuf.buf_in-RxBuf.buf_size;
    if (RxBuf.buf_in<RxBuf.buf_size) {
        buf_out += rx_bufsize;
    }
    c = RxBuf.buf[buf_out];
    if (RxBuf.buf_size==0) {
        RxBuf.buf_size += rx_bufsize;
    }
    RxBuf.buf_size--;
    sei();
    return c;
}

 #define tx_bufsize (16)

typedef struct {
    char buf[tx_bufsize];  /*送信バッファサイズ*/
    uint8_t buf_in;     /* バッファに文字を押し込むときの位置*/
    uint8_t buf_size;   /*バッファに貯められている文字数 */
} 
  TxBuf_t;

volatile TxBuf_t TxBuf;

const prog_char a[]  = "\nTCNT1 = ";

ISR(UART_UDRE_vect) {
    /* TxBuf.buf_size==0の状態でDATA割り込みを許可すると変なことになります。注意。 */
    uint8_t buf_out;
    buf_out=TxBuf.buf_in-TxBuf.buf_size;
    if (TxBuf.buf_in<TxBuf.buf_size) buf_out += tx_bufsize;
    UDR = TxBuf.buf[buf_out];
    TxBuf.buf_size--;
    if (TxBuf.buf_size==0) UCSRB = UCSRB & ~_BV(UDRIE); /* UDR空き割り込み禁止 */
}

void uart_putchar(char c) {
    while (TxBuf.buf_size > (tx_bufsize-1)) ;  /* バッファが空くまで待つ */
    cli(); //バッファを書き換える(読み出しも含む)操作は必ず割り込み禁止で!
    TxBuf.buf[TxBuf.buf_in++] = c;
    TxBuf.buf_size++;
    if (TxBuf.buf_in==tx_bufsize) TxBuf.buf_in=0;
    UCSRB = UCSRB | _BV(UDRIE); /* UDR空き割り込みを有効にする。 */
    sei();
}
void uart_putstr_pgm(char *s) {
    char c;
    for(;;) {
        c = pgm_read_byte(s++);
        if (c==0) break;
        uart_putchar(c);
    }
}
void uart_putstr(char *s) {
    char c;
    for(;;) {
        c = *s++;
        if (c==0) break;
        uart_putchar(c);
    }
}

const prog_char ok[]   = "==> OK! :-) ";
const prog_char ng[]   = "==> NG! :-< ";
const prog_char prompt[] = "\nPassword :";

int main(void) {
    char buf[16];
    uint8_t buf_i;
    int16_t res;
    DDRB = 0xFF;
    UCSRB = _BV(TXEN)|_BV(RXCIE)|_BV(RXEN)|_BV(UDRIE); /* 送信・受信・送受信割り込み有効 */
    UBRRH = 0;
    UBRRL= 25;    /* 通信速度設定 */
    RxBuf.buf_in = TxBuf.buf_in = 0; /*バッファクリア*/
    sei(); /* enable interrupts */
    buf_i=0;
    uart_putstr_pgm((char *)prompt);
    for(;;) {
        if (RxBuf.buf_size) {
            res = uart_getch();
            if (buf_i >= (sizeof(buf)-1) ) res=0;
            if (res) {
                buf[buf_i++] = res;
                uart_putchar((char)res);
            } 
               else {
                buf[buf_i]   = 0;
                if (strcmp(buf,"PassWord")==0) {
                    uart_putstr_pgm((char *)ok);
                } else {
                    uart_putstr_pgm((char *)ng);
                }
                buf_i=0;
            }
        } else {
            /*あれば他の処理*/
        }
    }
}

受信割込によりエコープログラム

 USARTでは、通常入力した文字が端末に表示されますが実際は送った文字が随時返答されているのでそのように見えます。これがあたかもやまびこ(エコー)に見えるのでエコープログラムといいます。もし、割込みを使わないと次の点で非同期式の場合は特に問題となります。

  • 高速に入力されている場合は取りこぼしする可能性がある。
  • 低速に入力されている場合はオーバヘッドが大きい。

また、割込みで書くとmain関数がきれいになるなどメリットもあります。単純なエコープログラムなのでputu関数が無駄ですが、実際にはswitch文とかで分岐したりとかいろいろなことができます。いろいろ、改良してみてください。

/* Atmega168p システムクロック1MHz*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>

void putu(char c) {
	UDR0 = c;    /* 引数を送信 */
}

ISR(USART_RX_vect) {
	char c;
	c = UDR0; /* 受信したデータを格納して */
	putu(c); /* 送信し返す */
}

int main(void) {
	DDRB = 0xFF; /* 安全のためI/Oポートを出力モードに */
	DDRC = 0xFF;
	DDRD = 0xFF;

	UCSR0A = _BV(U2X0); /* ボーレート倍モードをセット */
       UBRR0 = 12;    /* 1MHzにて9600bps設定 */
       UCSR0B = _BV(TXEN0)|_BV(TXCIE0)|_BV(RXEN0)|_BV(RXCIE0); /* 送受信許可と割込み発生許可 */
	sei();    /* 割込み発生許可 */
       for(;;)   /* やりたいことをここに書く。今回はないので何もなし。スリープにするのもありでしょう */
       {
       }
	return 0;
}

 AT90S→megaシリーズの変更点

普通に8bitUART(非同期シリアル)として使うなら、以下の置換だけでOKと思われます。

  • USR→UCSRA
  • UCR→UCSRB
  • UBRR=hoge; → UBRRL=hoge; UBRRH=0;

その他の変更点

  • 名称変更。UART→USART (S=Synchronous 同期シリアル通信機能)
  • 基本的には旧AVRで未定義だったビット及びUCSRCのビットを0とすれば互換のようです。
  • 受信UDRの二重化。UCSRAのフラグ類はUDRを読むと次のUDRに対するものに変わってしまうので、UDRを読む前にUCSRAのフラグ(RXB8含む)を読まなければいけない。
  • 受信用シフトレジスタにも改良あり、UDRを読み出さないうちに3バイト到着しても、4バイト目のスタートビットが到着するまではデータは失われない・・らしい
  • UCSRCの新設。同期通信やパリティビットなど新しい機能の選択。UARTだけ使うなら不要。

この章はmegaシリーズ向けに書き換えられていますので、mega用のルーチンをAT90Sシリーズ用に使う場合は、以下の点に気をつけてください。

  • U2Xビットがありません。U2X=1で使われている場合はU2X=0にしてUBRRを1/2にしてください。ボーレート誤差が若干増える場合があります。
  • UBRRが8bitです。UBRRHが0でない場合はそのまま置き換えることはできません。
  • その他、megaで新設された機能を使っていないかどうか要チェック。