この記事では、Arduino Uno R3でシリアル通信を使い、プログラムの中の値をパソコンの画面へ表示する練習をします。

Lチカで出力を動かせるようになっても、プログラムの中で「いま変数がいくつか」「センサの値がどう変化したか」は目で見えません。シリアル通信は、変数の値をパソコンへ送って確認するためのデバッグ機能に便利です。組込みソフトの開発では、動かないときにまずログを出して原因を切り分けます。

まずはSerial.begin()Serial.println()で値を表示し、そのあとで同じ出力をUBRR0・UCSR0・UDR0のUARTレジスタで書き直します。そのとき、通信速度(ボーレート)がクロックを分周して作られていることも確認します。これは次のタイマ記事につながる考え方です。

<自己紹介>
筆者は現役の車載組込みソフトエンジニアです。実務でも、動かないコードの原因はログ出力で追うことがほとんどです。この記事では、Arduinoのシリアルを道具として使いながら、その中で何が起きているかまで確認していきます。

シリアル通信で学ぶこと

シリアル通信は、データを1ビットずつ順番に送る通信です。Arduinoでは、USBケーブル越しにパソコンのシリアルモニタへ文字を送れます。

このテーマの要点

  • Serial.begin()で、通信速度(ボーレート)を決めて通信を始める
  • Serial.print()Serial.println()で、値を画面へ送る
  • ATmega328Pでは、UBRR0でボーレート、UCSR0Bで送受信の有効化、UDR0で1バイト送受信する
  • ボーレートは、16MHzのクロックを分周して作られている

ボーレートとは、1秒間に送るビット数のことです。9600なら1秒間に9600ビットを送る速さを表します。送る側と受け取る側で同じ速さに合わせないと、文字が正しく表示されません。

本記事のAPI版・レジスタ版は、Arduino Uno(ATmega328P)向けにPlatformIOでコンパイル確認しました。シリアルモニタの実表示はまだ画像にしていないため、手元のUno R3で出力を確認しながら進めてください。

準備するもの

使うもの 用途
Arduino Uno R3またはR3互換ボード ATmega328PのUARTレジスタを使うため。本記事はUno R4対象外
USBケーブル スケッチの書き込みと、シリアル通信の両方に使う
シリアルモニタ Arduino IDEまたはPlatformIOに付属。送られた文字を表示する

シリアルモニタの開き方が分からない場合は、Arduino IDEのダウンロード〜インストール完全ガイドまたはVS Code+PlatformIOで作るArduino開発環境を先に確認してください。出力がまだのときは、Lチカのドリルで書き込みまでの流れを確かめておくとスムーズです。

まずArduino APIでシリアル出力する

最初はArduinoの書き方で、1秒ごとにカウンタの値を送ります。Serial.begin()で通信速度を決め、Serial.println()で値と改行を送ります。

serial_api.ino

void setup() {
    Serial.begin(9600);
}

void loop() {
    static int count = 0;

    Serial.print("count=");
    Serial.println(count);
    count++;

    delay(1000);
}

Serial.begin(9600)で、ボーレートを9600に設定します。シリアルモニタ側も同じ9600に合わせてください。速度が違うと文字化けします。Serial.print()は改行なし、Serial.println()は末尾に改行を付けて送ります。

書き込んだあとにシリアルモニタを開くと、count=0count=1…と1秒ごとに増えていく表示を確認できます。Serial.begin()の詳しい役割はArduino公式リファレンスでも確認できます。

次にレジスタで同じ出力をする

Arduino API版が動いたら、同じ送信をレジスタで書きます。ATmega328Pでシリアル通信を担うのはUSART0という機能で、次のレジスタを使います。

レジスタ このドリルで行うこと
UBRR0 ボーレートを決める値を入れる。クロックの分周比にあたる
UCSR0B 送信(TXEN0)と受信(RXEN0)を有効にする
UCSR0C データ長などの形式を決める。8ビット・パリティなし・ストップ1にする
UCSR0A 状態を読む。UDRE0で送信バッファの空き、RXC0で受信の完了を確認する
UDR0 1バイトの送受信をする窓口。書けば送信、読めば受信
serial_register.ino

#include <Arduino.h>

void uart_init(uint16_t ubrr) {
    UBRR0H = (uint8_t)(ubrr >> 8);
    UBRR0L = (uint8_t)ubrr;
    UCSR0B = _BV(TXEN0);                  // 送信を有効化
    UCSR0C = _BV(UCSZ01) | _BV(UCSZ00);   // 8ビット・パリティなし・ストップ1
}

void uart_tx(uint8_t c) {
    while (!(UCSR0A & _BV(UDRE0))) {       // 送信バッファが空くまで待つ
        // 待機
    }
    UDR0 = c;
}

void uart_print(const char *s) {
    while (*s != '\0') {
        uart_tx((uint8_t)*s);
        s++;
    }
}

void setup() {
    uart_init(F_CPU / 16 / 9600 - 1);     // 9600bps
    uart_print("register uart ready\r\n");
}

void loop() {
    uart_print("tick\r\n");
    delay(1000);
}

F_CPU / 16 / 9600 - 1が、ボーレートを決める計算です。F_CPUはクロック周波数で、Uno R3では16,000,000(16MHz)を指します。9600bpsなら計算結果は103。この値をUBRR0へ入れると、USART0が16MHzのクロックを分周して9600bpsを作ります。

API版とレジスタ版の対応Serial.begin(9600)が、UBRR0へのボーレート設定と、UCSR0BUCSR0Cの有効化・形式設定をまとめて肩代わりします。Serial.print()は、UDR0への書き込みとUDRE0の空き待ちを繰り返す処理です。

データシートからレジスタを読み解く

TXEN0UDRE0も、いきなり覚えるものではありません。ATmega328Pのデータシート(Microchip、文書番号7810D–AVR)の「19. USART0」の章を開き、各レジスタの説明とビット構成を読みながら決めていきます。Lチカと同じで、レジスタは8ビットで、ビットごとに役割が決まっているのがポイント。以下では、データシートの該当箇所を原文引用しながら確認します。

① ボーレートは式で決まる

データシートのボーレートの節(19.3.1、Table 19-1「Equations for Calculating Baud Rate Register Setting」p.146)に、設定値の式が載っています。非同期ノーマルモードの式は UBRR0 = f_OSC / (16 × BAUD) − 1 です。f_OSCはクロック(Uno R3は16MHz)、BAUDは目標のボーレート。9600bpsなら結果は103で、これをUBRR0へ入れます。

② UCSR0Bで送受信を有効にする

送信や受信を有効にするビットは、UCSR0Bというレジスタの中にあります。データシートの「19.10.3 UCSRnB」(p.160)には、送信を有効にするビットTXEN0(Bit 3)について原文でこう書かれています(英文は原文引用です)。

Bit 3 – TXENn: Transmitter Enable n
Writing this bit to one enables the USART transmitter.
(訳:ビット3 – TXENn:送信の有効化。このビットに1を書くと、USART送信機が有効になる。)

出典:ATmega328P Datasheet(Microchip, 7810D–AVR)19.10.3 UCSRnB, p.160 より原文引用(© Microchip Technology)

UCSR0Bの定義図は次のとおりです。今回使うのは送信のTXEN0(3番ビット)と、受信のRXEN0(4番ビット)です。

ATmega328PデータシートのUCSR0Bレジスタ定義。bit4がRXEN0(受信有効)、bit3がTXEN0(送信有効)
図:UCSR0B(USART制御・状態レジスタB)の定義。bit3=TXEN0(送信有効)、bit4=RXEN0(受信有効)。出典:ATmega328P Datasheet(Microchip, 7810D–AVR)19.10.3 UCSRnB, p.160 より原文引用(© Microchip Technology)

下の表は、この定義から今回使うビットを抜き出したものです。

レジスタ bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
UCSR0B RXCIE0 TXCIE0 UDRIE0 RXEN0 TXEN0 UCSZ02 RXB80 TXB80

だから「送信を有効にする」はUCSR0Bの3番ビット(TXEN0)を1にすること、と読み取れます。データ長を8ビットにするUCSZ01UCSZ00UCSR0Cにあり、こちらも同じようにビット構成の表で位置を確認します。

③ UCSR0Aで送受信の状態を読む

送ってよいか、受け取れたかは、UCSR0Aというレジスタの状態ビットで分かります。送信バッファの空きはUDRE0(5番ビット)、受信完了はRXC0(7番ビット)です。データシートの「19.10.2 UCSRnA」(p.159)には、UDRE0について原文でこう書かれています(英文は原文引用です)。

Bit 5 – UDREn: USART Data Register Empty
The UDREn flag indicates if the transmit buffer (UDRn) is ready to receive new data. If UDREn is one, the buffer is empty, and therefore ready to be written.
(訳:ビット5 – UDREn:USARTデータレジスタ空。UDREnフラグは、送信バッファ(UDRn)が新しいデータを受け取れる状態かを示す。UDREnが1なら、バッファは空で、書き込み可能である。)

出典:ATmega328P Datasheet(Microchip, 7810D–AVR)19.10.2 UCSRnA, p.159 より原文引用(© Microchip Technology)

レジスタ bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
UCSR0A RXC0 TXC0 UDRE0 FE0 DOR0 UPE0 U2X0 MPCM0

while (!(UCSR0A & _BV(UDRE0)))は、この5番ビットが1(=送信バッファが空いた)になるまで待つ、という意味だったわけです。名前を暗記していなくても、データシートのビット構成の表さえ引ければ、コードの意味がそのまま読み解けます。

そしてもう一つ大切なのが、ボーレートもクロックを分周して作っているという見方です。UBRR0に入れた値は分周比にあたり、次のタイマ記事で扱うプリスケーラと同じ考え方。通信も時間の制御も、もとは同じ1つのクロックを割って作られています。

練習問題(基礎5+発展2)

基礎5問は、API版とレジスタ版の対応を確認する問題です。発展2問は、レジスタで文字列を送り、受信も扱います。まず自分で書いてから回答を開いてください。

基礎 Q1

ボーレート9600で通信を始め、Helloを1秒ごとに送るArduino API版を書いてください。

解答・解説を見る
q1_api.ino

void setup() {
    Serial.begin(9600);
}

void loop() {
    Serial.println("Hello");
    delay(1000);
}

解説:Serial.begin()setup()で1回だけ呼びます。println()は末尾に改行を付けるので、1行ずつ表示されます。シリアルモニタ側のボーレートも9600に合わせてください。

基礎 Q2

API版で、int countの値をcount=0のように表示し、1秒ごとに1ずつ増やしてください。

解答・解説を見る
q2_api.ino

void loop() {
    static int count = 0;

    Serial.print("count=");
    Serial.println(count);
    count++;

    delay(1000);
}

解説:ラベルはprint()で改行せずに送り、値はprintln()で改行を付けて送ります。staticを付けると、loop()を抜けてもcountの値が残ります。

基礎 Q3

レジスタ版で、9600bpsにするためにUBRR0へ入れる値の計算式を書いてください(Uno R3、16MHz)。

解答・解説を見る
F_CPU / 16 / 9600 - 1

解説:データシートの式UBRR = f_OSC / (16 × BAUD) − 1そのままです。16MHz・9600bpsなら結果は103になります。この値が、クロックを割ってボーレートを作る分周比にあたります。

基礎 Q4

レジスタ版で、送信を有効にする1行を書いてください。

解答・解説を見る
UCSR0B = _BV(TXEN0);

解説:UCSR0BのTXEN0ビットを1にすると、送信が有効になります。受信も使うときはUCSR0B = _BV(TXEN0) | _BV(RXEN0);のようにRXEN0も立てます。

基礎 Q5

レジスタ版で、1バイトを送信するuart_tx()を書いてください。送信バッファが空くまで待ってから送ります。

解答・解説を見る
q5_register.ino

void uart_tx(uint8_t c) {
    while (!(UCSR0A & _BV(UDRE0))) {
        // 送信バッファが空くまで待つ
    }
    UDR0 = c;
}

解説:UDRE0が1になると送信バッファが空いた合図です。空きを待たずにUDR0へ書くと、前のデータが送り終わる前に上書きしてしまいます。ビットの読み方があやふやならC言語 ビット演算のドリルを復習してください。

発展 Q6

uart_tx()を使って、文字列を最後まで1文字ずつ送るuart_print()を書いてください。

解答・解説を見る
q6_register.ino

void uart_print(const char *s) {
    while (*s != '\0') {
        uart_tx((uint8_t)*s);
        s++;
    }
}

解説:文字列の終わりを表す'\0'まで、1文字ずつuart_tx()へ渡します。ポインタで先頭から順に進める書き方は、C言語 ポインタのドリルと同じ考え方です。

発展 Q7

受信を有効にし、1バイトを受け取るuart_rx()を書いてください。受信が終わるまで待ってから読みます。

解答・解説を見る
q7_register.ino

// setup() で RXEN0 も立てておく
// UCSR0B = _BV(TXEN0) | _BV(RXEN0);

uint8_t uart_rx(void) {
    while (!(UCSR0A & _BV(RXC0))) {
        // 受信が終わるまで待つ
    }
    return UDR0;
}

解説:RXC0が1になると1バイト受信できた合図です。そのあとUDR0を読むと、受け取ったデータが取り出せます。送信のUDRE0と受信のRXC0は、同じUCSR0Aの別のビットです。

例:実務で実際に書くコード

実務でも、動かないコードの原因はまずログ出力で追います。処理の要所で変数の値を送っておくと、どこまで想定どおりに動いたかが分かります。いわゆるprintfデバッグですね。実務でも使うところもあるかと思います。
※私の場合は専用ツール(CaNoeやHillsなど)を使用するので実務ではまた違った検証をしますが。

debug_log.c

/* センサ値を読んだ直後にログを出す */
uart_print("sensor read\r\n");

/* 判定に入る前に、しきい値と現在値を出す */
uart_print("check threshold\r\n");

/* 出力を切り替えた記録を残す */
uart_print("output on\r\n");

車載ECUでも、開発中はUARTでログを出したり、デバッガで変数を見たりして原因を切り分けます。「動かないときに、中の状態を見えるようにする」という考え方は、Arduinoでも実務でも同じです。総合課題でも、温湿度の値や判定結果をシリアルへ出して動作を確かめます。

最後につまずきポイントをおさらい

ボーレートが合わないと文字化けするプログラムのSerial.begin()と、シリアルモニタ側の速度設定は必ず同じ値にします。PlatformIOではplatformio.inimonitor_speedが表示側の速度です。文字化けしたら、まずこの2つが一致しているかを確認してください。

もう一つのつまずきは、UBRR0UDR0などのレジスタ名がATmega328P向けだということです。Uno R4など別のマイコンを載せたボードでは、レジスタ名も仕様も変わります。レジスタ版は、ボードに載っているマイコンを確認してから書いてください。改行を送るときは、環境によって\nだけで足りるか、\r\nが要るかが変わる点にも注意します。

よくある質問(FAQ)

ボーレートは何を選べばよいですか?

最初は9600で問題ありません。もっと速く表示したいときは115200もよく使われます。大切なのは、プログラムとシリアルモニタで同じ値にそろえることです。速度を上げるほど、短い時間で多くのデータを送れます。

画面が文字化けします

ほとんどはボーレートの不一致が原因です。Serial.begin()の値と、シリアルモニタ側の速度が同じかを確認してください。レジスタ版では、UBRR0へ入れた計算式のクロックが16MHzになっているかも見直します。

Serial.printとSerial.printlnの違いは何ですか?

print()は改行なし、println()は末尾に改行を付けて送ります。ラベルと値を同じ行に並べたいときはprint()、1行ずつ区切りたいときはprintln()を使い分けます。

次に読む

シリアルで中の値を見られるようになったら、次はdelay()に頼らず、決まった間隔で処理を動かすタイマへ進みます。ボーレートで出てきた「クロックを分周する」考え方を、今度は時間の制御に使います。