シリアルの設定


概要

LEDの点灯ができたので,今回はシリアルでデータを転送し,シリアルモニタで確認する.組み込み開発ではディスプレイが存在しないことがほとんどなので,基本的にシリアル通信を利用してPCにデータを転送するなどして確認することが必要となる.
シリアル通信については割愛するが,一般的にはシリアルコントローラというハードウェアがシリアルの規格に従ってデータを送受信してくれる.そのため,一般的にはシリアルコントローラの仕様を理解し,正しくハードウェアの設定をすることが重要となる

USART

AVRのマイコンには,USART(Universal Synchronous and Asynchronous serial Receiver and Transmitter)というデバイスがついているらしく,シリアル通信をするにはこのデバイスを使う必要がある.
ここでやらなければいけないのは,

  1. ボーレートの設定
  2. ReceiverとTransmitterの有効化
  3. フレームフォーマットの指定

である.

ボーレートの設定

ボーレートとは,ビット/秒のことで,9600, 14400,115200などが一般的.どうやらAtmega328pでは9600を指定するようです.ただし,ここで設定しなければならいのは以下の計算式で求める内部クロック(UBRRn).ここで,BANDがボーレートで,f_oscはクロックを表している.Atmega328pのクロックは16MHzなので,UBRR = 16000000L / (16 x 9600) -1となる.

ここで求めたUBRRを,UBRR0HUBRR0Lにセットすれば良い(仕様書に掲載のサンプルコードを参照).

さらに,このUBRR0H, UBRR0Lとは,

メモリの0xc5, 0xc4にマッピングされていることがわかる.

ReceiverとTransmitterの有効化

USARTを制御するために,AVR系マイコンは3つのレジスタを用意している(USCRnA, USCRnB, USCRnC,ここでnは具体的な数値が入る).そして,ReceiverとTransmitterの有効/無効を切り替えるのは,USCRnBのBit4, Bit3である.

というわけで,このレジスタのRXENn, TXENnを1にすれば良いことがわかる.

最後に,これらのレジスタは,0xC0 - 0xC2にアサインされていることが仕様からわかる.

仕様を見ると,おそらくAtmega328pにはUSARTを1つしか搭載していないのだと思われる(2つ以上ある場合,UCSR1Bなどがあるはず).

フレームフォーマットの指定

シリアル通信では,1つのポートで0/1を送ることで通信するため,予めどのようなビットの並びでデータを送るか指定する必要がある.

名称 ビット数 意味
スタートビット 1 フレームの先頭であることを示すビット
データビット 5 ~ 9 送るデータ。5~9bitの範囲で自由に設定できる。
パリティビット 0 or 1 ビット誤りしていないかをチェックするビット。無くてもよい。
ストップビット 1 or 2 フレームの終わりであることを示すビット

このうち,スタートビットは常に1なので,データビット,パリティビット,ストップビットを何ビットにするか設定する必要がある.この設定を行うのがUSCRnCレジスタである.

まず,パリティビットの設定

仕様にあるように,UPMN1, UPMN0の組み合わせでパリティあり/なし,偶数パリティ,奇数パリティを決められる.デフォルト値が0なので,何もしなければパリティなし,となる.

次にストップビット

ストップビットの振る舞いを決めるのはUSBSnビットで,デフォルトが0であるため,何も設定しなければストップビットは1ビット,ということになる.

最後にデータビット

データビットは,USCRnCのUCSZn1, UCSZn0ビットの他に,UCSRnBのUCSZn2ビットとの組み合わせで決定される.
デフォルト値は(UCSZn2, UCSZn1, UCSZn0) = (0, 1, 1)なので,8ビットということになる.
先程のC言語のサンプルは,

USCR0C = (1 << USBS0) | (3 << UCSZ00)

となっているので,USCR0Cは0b00001110となり,ストップビットが2,データビットが8ビット,パリティなしということになる(この中でUSBS0とか,特に断りもなく出てくるが,当該ビット分の数字だと思っている(e.g., 3とか)).

データの送受信

シリアルでデータを送信るには,送信可能状態になったときにバッファにデータを書き込めばよい.また,データを受信するには,データを受け取ったかどうかをチェックし,バッファから読み込めば良い.少なくともAtmega328pでは,このバッファ(1バイト)をデータレジスタとして共有しているらしい.

そして,送信できるかどうかを判断するには,USCRnAレジスタの特定のビットを見る必要がある.

送信

送信できるかどうか確認するには,5ビット目のUDREnが1かどうかを見る必要がある.

受信

受信できるかどうか確認するには,7ビット目のRXCnが1かどうかを確認する必要がある.

プログラムの拡張

これまでの内容を踏まえ,LED点灯のプログラムを拡張し,シリアルでのデータ送受信をやってみる.
まずはヘッダファイル.

serial.h
#ifndef _SERIAL_H_INCLUDED_
#define _SERIAL_H_INCLUDED_

int serial_init();                         /* デバイス初期化 */
int serial_is_send_enable();               /* 送信可能か? */
int serial_send_byte(unsigned char b);     /* 1文字送信 */
int serial_is_recv_enable();               /* 受信可能か? */
unsigned char serial_recv_byte();          /* 1文字受信 */
#endif

続いて実装

serial.c
#include "serial.h"

#define IO_OFFSET 0x20

#define MEM8(addr) (*(volatile unsigned char *)(addr))
#define IO8(addr)  MEM8((addr) + IO_OFFSET)

#define USART_UCSR0A MEM8(0xc0)

/* UCSR0Aの各ビットの定義 */
#define USART_UCSR0A_UDRE0 (1<<5) /* 送信バッファが空でセット */
#define USART_UCSR0A_TXC0  (1<<6) /* 送信完了でセット */
#define USART_UCSR0A_RXC0  (1<<7) /* 受信バッファにデータ有りでセット */

/* UCSR0Bの各ビットの定義 */
#define USART_UCSR0B MEM8(0xc1)
#define USART_UCSR0B_UCSZ02 (1<<2)
#define USART_UCSR0B_TXEN0  (1<<3)
#define USART_UCSR0B_RXEN0  (1<<4)
#define USART_UCSR0B_UDRIE0 (1<<5)
#define USART_UCSR0B_TXCIE0 (1<<6)
#define USART_UCSR0B_RXCIE0 (1<<7)

/* UCSR0Cの各ビットの定義 */
#define USART_UCSR0C MEM8(0xc2)
#define USART_UCSR0C_UCSZ00      (1<<1)
#define USART_UCSR0C_UCSZ01      (1<<2)
#define USART_UCSR0C_STOPBIT_1   (0<<3)
#define USART_UCSR0C_STOPBIT_2   (1<<3)
#define USART_UCSR0C_PARITY_NONE (0<<4)
#define USART_UCSR0C_PARITY_EVEN (2<<4)
#define USART_UCSR0C_PARITY_ODD  (3<<4)
#define USART_UCSR0C_MODE_ASYNC  (0<<6)
#define USART_UCSR0C_MODE_SYNC   (1<<6)
#define USART_UCSR0C_MODE_SPI    (3<<6)

/* UBRRを書き込むレジスタのアドレス */
#define USART_UBRR0L MEM8(0xc4)
#define USART_UBRR0H MEM8(0xc5)

/* 送受信用バッファ */
#define USART_UDR0   MEM8(0xc6)

int serial_init() {
  USART_UCSR0B = 0;

#define CPU_CLOCK 16000000L
#define BAUDRATE 9600
  USART_UBRR0L = (CPU_CLOCK / (BAUDRATE * 16L) - 1);      // UBRRの書き込み
  USART_UBRR0H = (CPU_CLOCK / (BAUDRATE * 16L) - 1) >> 8; // UBRRの書き込み

  USART_UCSR0B = USART_UCSR0B_RXEN0 | USART_UCSR0B_TXEN0; // 送受信可能にする
  USART_UCSR0C = USART_UCSR0C_UCSZ00 | USART_UCSR0C_UCSZ01; // フレームフォーマットの設定

  return 0;
}

int serial_is_send_enable() {
  return (USART_UCSR0A & USART_UCSR0A_UDRE0);
}

int serial_send_byte(unsigned char c)
{
  while (!serial_is_send_enable())
    ;
  USART_UDR0 = c;
  return 0;
}

int serial_is_recv_enable() {
  return (USART_UCSR0A & USART_UCSR0A_RXC0);
}

unsigned char serial_recv_byte() {
  unsigned char c;

  while (!serial_is_recv_enable())
    ;
  c = USART_UDR0;
  return c;
}

これらを追加した上で,以下のようにmain.cを書き換える

main.c
#include "serial.h"


int putc(unsigned char c) {
    if (c == '\n') {
        serial_send_byte('\r');
    }
    return serial_send_byte(c);
}

unsigned char getc() {
    return serial_recv_byte();
}

int main(void) {
    serial_init();
    putc('D');
    char c = getc(); // 1文字シリアルから受信
    putc(++c);       // ASCIIの次の文字を出力
    while (1) {
    }
    return 0;
}

これらをコンパイルし,Arduino Unoに書き込み,シリアルモニタを使って実行してみる

ちょっとわかりにくいが,最初に"D"という文字が出たあと,"A"を送信している.その後インクリメントされ,"B"が表示されていることがわかる.

シリアルでの送受信プログラムは以下のコマンドで実行することができる

>git clone https://github.com/hiro4669/iosv.git
>cd iosv
>git branch serial_ver1 origin/serial_ver1
>git checkout serial_ver1
>cd serial
>make