【Arduino工作〜発展編】AVR-RustでAtmega328pのUSARTを使ってみる


2021/11/06
蛸壺の中の工作室|【Arduino工作〜発展編】AVR-RustでAtmega328pのUSARTを使ってみる

RustでAVRマイコン機能を作り込んでいく技術ネタ紹介シリーズの第4段です。

前回はAtmega328pのPWM波形を発生させる方法を詳しく説明しました。

今回はAVRマイコン開発の際に、デバッグ機能としても重宝するUSARTを用いたシリアル通信を使いこなす方法を解説していきます。


USART接続例

まずはAtmega328pを使ってUSARTテスト用環境を以下のように構築します。

合同会社タコスキングダム|蛸壺の中の工作室

ここではシリアル通信用のモニタリングにArduino Unoとそれに接続したPCを使います。

Atmaga328pからのシリアル通信を確認出来るのが目的ですので、Arduino Unoで無くとも、他のRS232C接続可能なUSBコンバータがあればそれで代用してください。


外部発振器の設定

AVRマイコンのUSART機能を使う前に良く理解しておく必要があるのが、システムで利用する実質的なクロック周波数のことです。

USARTでのシリアル通信のボーレート値は、マイコンのクロック周波数ベースで決定されますので、このクロック周波数の考え方と設定方法を復習しておきます。

下位Fuseビットとその書き換え方法

AtmegaシリーズのマイコンにはFuseビットと呼ばれる特別なレジスタが3つあります。

詳しくは
Atmegaデータシートなどで確認してもらうとして、外部発振器の設定に直接関連する下位Fuseビットというレジスタを使い方を理解する必要があります。

            ビットの名称:
    CKDIV8-CKOUT-SUT1-SUT0-CKSEL3-CKSEL2-CKSEL1-CKSEL0

デフォルト: 01100010(0x62)
        

ここではあまり重要ではないので深くは触れませんが、最初の7ビット目のCKDIV8は
0の時に起動時にシステムクロックを8分周に初期化します(デフォルトで8分周)。

6ビット目のCKOUTは、CLKOピン(PB0)からシステムクロック波形を出力すること許可する設定です。デフォルトは
1で、これは出力不許可を意味しています。

5-4ビット目のSUT1-SUT0はリセットからの起動時間を選択します。

            00...14クロック起動。低電圧検出リセット(BOD)許可に使用
01...高速起動(14クロック+4.1ms)。高速上昇電源に使用
10...低速起動(14クロック+65ms)。デフォルト。低速上昇電源に使用
        
どのオプションが適切かはデバイスの電源を考慮して設定します。

デフォルトのSUT1-SUT0の値(
10)となり、とりあえず余裕をもった最大の起動時間に設定されています。

3-0ビット目がクロック周波数を外部から取る上でもっとも重要なビット列になります。

クリスタルやセラミック発振器の設定は詳しく後述しますが、例えばそれ以外の設定ならば、

            0010...校正付き内蔵RC発振器7.3~8.1MHz。デフォルト
0011...128kHz内部発振器128kHz
0000...外部クロック信号を直接利用
        
という感じに利用します。

128kHz内部発振器は非常に低い電力で動作するように利用されるクロック源です。少電力で動作する反面、高精度用途では利用できません。

またここでの外部クロック信号とは、独立した外部デバイス(例えば他のマイコン)からクロック波形を供給する場合に利用します。供給されるクロックが安定的に維持されるように、極力外部からのノイズの影響を受けない設計が必要になります。

また以上のことから、下位Fuseビットを出荷値ままデフォルト使うと、内部RC発振器の約8MHzを8分周するので、おおよそ1MHzがシステムの周波数ということになります。

Avrdudeコマンドによる下位Fuseビットの書き込み

Fuseビットの書き込みは実行ファイルからはできませんが、Avrdudeコマンドから直接マイコンを叩くことで変更できます。

書き込み装置は色々と選択できますが、例えばここではAtmel-ICEを利用しています。

以下のコマンドは下位FuseビットにAtmega328pの工場出荷値
0x62を書き込んでいるコマンドです。

            $ sudo avrdude -p m328p -c atmelice_isp -P usb -U lfuse:w:0x62:m -v
        
見てのように-U lfuse:w:0x**:m0x**の部分に下位Fuseビットの設定値を16進数で与えて書き換えることが可能です。

本記事の下位Fuseビットの話とは直接関係有りませんが、Avrdudeコマンドでの
上位Fuseビットの値を書き換える際の注意点も紹介していました。

書き込み装置によっては、不可逆な書き換えが起こってしまい、困った事態を招くかも知れませんので、Fuseビットの書き換えは慎重に行いましょう。

クリスタル/セラミック発振器のCKSELレジスタ

Arduinoや他のベアメタル製品などの設計で良く目にするように、クリスタル振動子やセラミック振動子などのクロック周波数を安定供給するためのディスクリート部品を使うための設定を説明しましょう。

大体のメーカーデータシートにも構成図が載っていると思いますが、どのマイコンにも発信素子の取り付け用のペアになっているピンが2つ設けられています。

合同会社タコスキングダム|蛸壺の中の工作室

市販の発振素子も様々な種類があり、これらをマイコンへ接続設定するとは言っても、取り付ける振動素子の適性を考えて選択する必要があります。

AVRマイコンでは主に
低電力発振器全振幅発振器低周波数発振器の3つの設定に分かれます。

全振幅発振器

この設定では電気的にノイズが多い環境での利用や他の複数のデバイスへクロック入力を供給する場合に利用します。

対して、消費電力は常時高めになるので、低電圧にならないような電源もそれなりにパワーのあるもので設計を考慮する必要があります。

このため、この全振幅振動モードでは
Vcc = 2.7~5.5[V]の間でなければ動作しないことに気をつけなければいけません。

また推奨される駆動周波数は
0.4~20[MHz]、グラウンド間のコンデンサC1(C2)の推奨容量は12~22[pF]となります。

またこの全振幅振動モードの設定では、CKSELビットだけでなくSUTビットも併せて細かい設定ができます。

下位Fuseビットの設定をまとめると以下の表のようになります。

設定ビット※1

リセットからの遅延時間※2

電圧低下からの復旧遅延時間

推奨条件

000110

14クロック + 4.1 [ms]

258クロック

外部セラミック振動子・高速上昇電源

010110

14クロック + 65 [ms]

258クロック

外部セラミック振動子・低速上昇電源

100110

14クロック

1kクロック

外部セラミック振動子・低電圧検出(BOD)リセットON

110110

14クロック + 4.1 [ms]

1kクロック

外部セラミック振動子・高速上昇電源

000111

14クロック + 65 [ms]

1kクロック

外部セラミック振動子・低速上昇電源

010111

14クロック

16kクロック

外部水晶振動子・低電圧検出(BOD)リセットON

100111

14クロック + 4.1 [ms]

16kクロック

外部水晶振動子・高速上昇電源

110111

14クロック + 65 [ms]

16kクロック

外部水晶振動子・低速上昇電源

※1) SUT1-SUT0-CKSEL3-CKSEL2-CKSEL1-CKSEL0の順序で6ビット分なおこのモードは
CKSEL3-CKSEL2-CKSEL1までのビット部分は011で共通です。

※2)
Vcc = 5[V]の時の目安です。

この表でいうと、上から順に初回の起動時間等が遅くなっていきます。

実装する発信素子や電源のスペックに併せて下位Fuseビットを選択しましょう。

低電力発振器

据え置き型バッテリーなどで電源を供給する場合、出来るだけ少電力に済ませたい設計を求めるならば低電力モードも検討できます。

この低電力モードは先程の全振幅振動モードより更に細かく下位Fuseビットを設定することができます。

先程の表とほぼ内容は一緒ですが、起動に必要なVccの値はケースバイケースです。

設定ビット※1

リセットからの遅延時間※2

電圧低下からの復旧遅延時間

推奨条件

00---0

14クロック + 4.1 [ms]

258クロック

外部セラミック振動子・高速上昇電源

01---0

14クロック + 65 [ms]

258クロック

外部セラミック振動子・低速上昇電源

10---0

14クロック

1kクロック

外部セラミック振動子・低電圧検出(BOD)リセットON

11---0

14クロック + 4.1 [ms]

1kクロック

外部セラミック振動子・高速上昇電源

00---1

14クロック + 65 [ms]

1kクロック

外部セラミック振動子・低速上昇電源

01---1

14クロック

16kクロック

外部水晶振動子・低電圧検出(BOD)リセットON

10---1

14クロック + 4.1 [ms]

16kクロック

外部水晶振動子・高速上昇電源

11---1

14クロック + 65 [ms]

16kクロック

外部水晶振動子・低速上昇電源

※1) SUT1-SUT0-CKSEL3-CKSEL2-CKSEL1-CKSEL0の順序で6ビット分

このモードでは
CKSEL3-CKSEL2-CKSEL1までのビット部分は以下の4つから選択できます。

            100 ... 0.4~0.9MHz(セラミック振動子向け) / コンデンサ無し
101 ... 0.9~3.0MHz / 12~22pF
110 ... 3.0~8.0MHz / 12~22pF
111 ... 8.0~16MHz / 12~22pF
        
※2) Vcc = 5[V]の時の目安です。

低周波数発振器

このモードは時計用32.768kHz水晶振動子に最適化するような特別なモードです。

特にマイコンによって正確な時計機能を実装する必要がある場合に利用されますが、ここでは関係ないので省略します。

CLKPRレジスタの設定

CLKPR(クロック前置分周)レジスタは、システムの基本クロック周波数を分周させるためのバイトを記述します。

            CLKPCE-無し-無し-無し-CLKPS3-CLKPS2-CLKPS1-CLKPS0
        
まず7ビット目はCLKPCEと呼ばれ、クロック分周の値を変更することを許可・不許可を設定できます。

分周数の変更を許可する場合、CLKPCEに
1を書き込みます。

ただし注意が必要なのが、CLKPCEビットへの書き込みはCLKPS3~0の全ビットが
0である時だけ更新することができます。

さらに、CLKPCEに
1を書き込んでからクロックの変更を許可したら、4クロック後以内にCLKPS3~0ビットに書き込まないといけません。

なぜかというと、CLKPCEは
1を書き込んで4クロックでハードウェア側から許可解除され、再び0に戻る仕様だからです。

3~0ビット目のCLKPSレジスタでクロック分周値を定義します。

分周値が変更されると、直ちに分周器がMCUへのメインクロック周波数を分周し、それに伴って全ての周辺機能のクロック速度がそれに従います。

CLKPS3-CLKPS2-CLKPS1-CLKPS0の順番で以下のようなビット列で分周数を設定することが可能です。

            0000...分周数1
0001...分周数2
0010...分周数4
0011...分周数8(デフォルト)
0100...分周数16
0101...分周数32
0110...分周数64
0111...分周数128
1000...分周数256
        

ということでここでの要点をまとめると、CLKPRの書き換えにおいて、

            1. CLKPS3~0を全て0にする
2. CLKPCEビットを1にして、書き換えを許可する
3. 4クロック以内に新しいCLKPS3~0で書き換える
        
ということを覚えておきましょう。


USARTについて

ここからはようやく首題のUSARTに触れていきます。

UBRRレジスタとボーレート

前置きで、システムのクロック周波数の話を詳しく取り上げたのも、USARTシリアル通信の速度であるボーレート(単位;bps)を正しく換算するためでした。

この値を設定するレジスタが、
UBRRです。

このボーレート設定値
RBAUDR_\mathrm{BAUD}とすると、実行クロック周波数をfoscf_\mathrm{osc}、ターゲットボーレートkBAUDk_\mathrm{BAUD}とおく時、標準速非同期モードの場合に以下の式で与えられます。

RBAUD=fosc16kBAUD1\displaystyle{ \begin{aligned} R_\mathrm{BAUD} &= \frac{f_\mathrm{osc}}{16 k_\mathrm{BAUD}} - 1 \end{aligned} }Eq. (1)

なお、ボーレートの設定は標準速非同期モードの他に、より高速な通信を行うための
倍速モードがあります。この記事では倍速モードは利用しませんが、利用する場合には後述のUCSRAレジスタのU2Xビットに1を書き込んで設定を許可します。

例えばクロック周波数が
8[MHz]である場合、9600[bps]狙いとしてボーレートの設定値を計算してみましょう。

RBAUD=fosc16kBAUD1=8 [MHz]16×9600 [bps]151.083\displaystyle{ \begin{aligned} R_\mathrm{BAUD} &= \frac{f_\mathrm{osc}}{16 k_\mathrm{BAUD}} - 1 \\ &= \frac{8\ \mathrm{[MHz]}}{16 \times 9600\ \mathrm{[bps]}} - 1 \\ &\simeq 51.083 \end{aligned} }Eq. (2)

小数点以下切り捨てで、UBRRレジスタには
51を2進法で書き入れることになります。

なお、UBRRは10ビットの非負の整数をとりますので、2バイト分の大きさのレジスタとなります。上位のレジスタは
UBRRH、下位のレジスタはUBRRLで区別します。

この2バイト分の書き込みには少し工夫が必要になるかも知れません。Rustでの利用方法は後ほど実装で解説しましょう。

UCSRレジスタとUDRレジスタ

USARTの制御方法や状態を保持する一般設定を行うレジスタは、UCSRAUCSRBUCSRCの3つのレジスタ内のビットに書き込みます。

AVRマイコンのUSARTで送受信されたデータはUDRレジスタに一時的に緩衝され、ユーザーはこのUDRレジスタをアクセスすることでデータをやり取りすることできます。

よってUSARTからデータ送信したい場合にはUDRレジスタに書き込むことを意味します。

まずはUCSRAから説明すると、例えばUCSR0Aレジスタが以下にようなビット列になります。

            RXC0-TXC0-UDRE0-FE0-DOR0-UPE0-U2X0-MPCM0

デフォルト値:
    0-0-1-0-0-0-0-0
        
重要なビットだけ掻い摘むと、ここでは5ビット目のUDRE0(USART Data Register Empty)が特に重要です。

UDRE0はUSARTから送信待ちになっているデータレジスタが空いているかどうかを教えてくれるフラグ用のビットです。よって読み取り専用のビットで書き込みは不可です。

UDRE0フラグを読み取ることで、送信緩衝レジスタのUDR0が新規のデータを受け取ることが出来るかどうかを判別することができます。

シリアル通信で送信されるデータを配置するキュー配列のようなレジスタ
UDRに書き込むことで、1バイトずつ送信していきます。

また
UDRにデータを書き込んだ時点で、UDREが0の状態になります。

送信が完了してUDRレジスタが空になると、UDREが
1の状態に戻り、再びUDRにデータを書き込むことが可能です。

つまり、
UDRE0が1ならばUDR0は空で新規の送信用データが書き込みできます。

逆に
UDRE0が0ならば未送信のデータがレジスタ内にまだ残っているので、新しい送信データを書き込む準備ができていません。

次にUCSRBレジスタですが、UCSR0Bを例に取ると、以下のようなレジスタ構成になってます。

            RXCIE0-TXCIE0-UDRIE0-RXEN0-TXEN0-UCSZ02-RXB80-TXB80

デフォルト値:
    0-0-0-0-0-0-0-0
        
このレジスタで重要なのは、4−3ビット目のRXEN(Receiver Enable)とTXEN(Transmitter Enable)です。

その名の通り、ビットに
1を書き込んでRxピンとTxピンを起動します。

2ビット目の
UCSZ2は飛び地的にUCSRBレジスタに入ってますが、後述するUCSRCレジスタの設定値の一部ですのでそちらで説明します。

最後のUCSRCレジスタは、USARTの動作状態を設定するレジスタです。

例えばUCSR0Cは以下のようなレジスタ構成です。

            UMSEL01-UMSEL00-UPM01-UPM00-USBS0-UCSZ01-UCSZ00-UCPOL0

デフォルト値:
    0-0-0-0-0-1-1-0
        
始めの7−6ビット目のUMSEL1とUMSEL0が00の場合、USARTは非同期処理で動作します。

5−4ビット目のUPM1とUPM0でシリアル通信のパリティモードを選択できます。

パリティビットはエラー検出などに利用できますが、今回は使わないので、
00で無しの設定にしておきます。

3ビット目のUSBSは停止ビットの数を選択します。必要な停止ビット1つなら
0、2つなら1を書き込みます。

2−1ビット目のUCSZ1とUCSZ0は、先程のUCSRBの2ビット目のUCSZ2を加味して、送信するデータのビット長を設定します。

シリアル通信は大体8ビットで1フレームで送信する形式が多いと思います。

デフォルトもUCSZ2-0は
011であり、8ビットデータ長の通信ベースになっています。


プログラムの実装

えらくUSART関連のレジスタの話が長くなってしまいましたが、ここからようやく具体的なプログラミングの話に移ります。

なおAVRマイコンのデバッカーからプログラムを書き込むまでの手順は以前のブログ記事で紹介していますので、Rustプログラミングによる実行ファイルのビルドやプログラム書き込み手順を知りたい場合にはそちらを参照してください。

RustでAtmega328pへプログラミング

以下が今回利用するRustのソースコードです。

            #![feature(llvm_asm, lang_items, unwind_attributes)]
#![feature(start)]
#![no_std]
#![no_main]

extern crate avr_delay;
extern crate avr_std_stub;
extern crate avrd;

use avrd::atmega328p::{
    DDRB, PORTB
};

use avr_delay::{delay};
use core::ptr::{write_volatile, read_volatile};

//👇主クロック周波数の分周用
use avrd::atmega328p::{CLKPR};

//👇USART関連の設定レジスタ
use avrd::atmega328p::{
    UCSR0A,
    UCSR0B,
    UCSR0C,
    UDR0,
    UBRR0
};


#[no_mangle]
#[start]
pub extern "C" fn main() {
    clock_init(); //クロックの初期化
    usart_init(); //USARTの初期化

    unsafe { write_volatile(DDRB, 0b00000010); }
    let mut out = 0b00000000;

    loop {
        //👇実行クロック周波数8MHzの場合の1秒
        delay(2000000);

        //👇動作確認用のLED点滅
        unsafe { write_volatile(PORTB, out); }
        out ^= 1 << 1;

        //👇1ループに付き1文字を送信
        transmit('d' as u8);
    }
}

#[no_mangle]
fn clock_init() {
    unsafe {
        //👇CLKPRレジスタを一旦クリア
        write_volatile(CLKPR, 0b10000000);
        //👇4クロック以内に分周数2を書き込む
        write_volatile(CLKPR, 0b00000001);
    }
}

#[no_mangle]
fn usart_init() {
    unsafe {
        //👇ボーレート9600bps設定
        write_volatile(UBRR0, 51);

        //👇RxとTxを起動
        write_volatile(UCSR0B, 0b00011000);

        //👇非同期・パリティなし・データ長8ビット
        write_volatile(UCSR0C, 0b00000110);
    }
}

#[no_mangle]
fn transmit(byte: u8) {
    //👇データ送信完了まで待つ
    while !ready_to_transmit() {}

    //👇UDRに新しいデータを書き込む
    unsafe { write_volatile(UDR0, byte); }
}

#[no_mangle]
fn ready_to_transmit() -> bool {
    unsafe {
        //👇UDRE0(UCSR0Aの5ビット目)を読み込んで
        //  1(送信完了)かどうか調べる
        (read_volatile(UCSR0A) & (1 << 5)) > 0
    }
}
        
これをビルドして出来た実行ファイルをAVRマイコンに書き込んでおきます。

下位Fuseビットの書き換え

前述したように、外部振動素子からクロック周波数を供給する場合には、下位Fuseビットも併せて書き換えする必要があります。

書き換える下位Fuseビット値はお手元の環境状態によっても異なるので、ここではあくまで参考としてください。

現在の著者の手持ちの部品と5V電源だと、低電力モードと低速起動を狙って、

            CKDIV8-CKOUT-SUT1-SUT0-CKSEL3-CKSEL2-CKSEL1-CKSEL0
0-1-11(低速起動)-111(低電力モード)-1(低速起動)
        
と設定したい場合、01111111は16進法で0x7Fですので、Avrdudeコマンドで以下のように打ち込みます。

            $ sudo avrdude -p m328p -c atmelice_isp -P usb -U lfuse:w:0x7F:m -v
        
これはAvrdudeコマンドで単独で下位Fuseビットを書き込んでもいいですし、先程の実行ファイルと同時に書き込んでも結構です。

信号受信用スケッチをArduinoへ書き込む

今回はArduinoを挟んでPCとAVRマイコン間でシリアル通信をやり取りさせてみます。

シリアル通信は基本的にモジュール1対1型の技術ですので、一つのシリアル通信で複数台のデバイスは扱えません。

しかしArduinoとPCとのシリアル通信のやり取りに1つ、ArduinoとAtmega328pとのシリアル通信で1つの2つが必要になります。

ArudinoをPCで繋ぐ場合、既にハードウェアシリアル(物理ピンはRX:0ピン/TX:1ピン)が自動で使用されるため、Atmega328p側にはソフトウェアシリアルで通信する必要があります。

ということで、以下のようにAtmega328p側から信号を受信して、それをPC側に転送するプログラムスケッチをArduinoに書き込む必要があります。

            #include <SoftwareSerial.h>
//Rx:2ピン, Tx:3ピン
SoftwareSerial mySerial = SoftwareSerial(2, 3);

char data = 'a';

void setup() {
    Serial.begin(9600);
    mySerial.begin(9600);
    pinMode(2, INPUT);
    pinMode(3, OUTPUT);
    delay(200);
}

void loop() {
    if (mySerial.available() > 0) {
        //ソフトウェアシリアルでAtmegaから受け取ったデータを読込
        data = mySerial.read();

        //ハードウェアシリアルでPCモニタに表示
        Serial.println(data);
    }
}
        

一度Arduino書き込んでおけば、以降はArudinoIDEのシリアルモニタを開くだけで通信状況を確認できるようになります。


通信テスト

さて、書き込みを終えたAtmega328pを以下の写真のようにブレッドボード上でテスト回路を組んで、シリアル通信出来ているかを試してみましょう。

合同会社タコスキングダム|蛸壺の中の工作室

これをPC側のAduinoIDEのシリアルモニターで確認すると、

合同会社タコスキングダム|蛸壺の中の工作室

となり、確かにAtmega328pのSUARTから送信された文字がArduino側で受信されていることが確認できます。

しかし、なんだか読み取り精度が甘いのか、同期するタイミングが悪いのか、
dの文字を送っているのに、結構な頻度で間違った文字が表示されています。

もともとソフトウェアシリアルは精度面での用途ではあまり宜しくないので、ボーレートも9600bpsで利用するのがせいぜいなのかも知れません。

またブレッドボード上でしかもコテコテのジャンパー配線しているのがかなり致命的で、あちらこちらでノイズが発生している気がします。

...とりあえず今回はUSARTの信号送信ができていることが確認できた、ということでお茶を濁しておきます。


まとめ

今回はAVRマイコンのUSART機能をRustで使う方法を紹介していきました。

とはいえAtmaga328pからの信号送信でしか実装していませんでしたが、内容が長くなりそうなので次回以降でADC入力と併せてAtmega328p側で信号受信の方もやってみたいと思います。


参考

ATmega48A/PA/88A/PA/168A/PA/328/P データシート