RX72N Envision Kit での開発(その2)SCI 編

シリアルコミュニケーション

組み込みマイコンでは、最も手軽に、外部との通信を行えるインターフェースであると言えます。

このフレームワークでは、common/sci_io.hpp (device::sci_io テンプレートクラス)として、「良く使うだろう」場面を想定して実装されています。
C++ テンプレートを使って最初に作り始めたクラスでもあり、感銘深いものがあります。
現在は、その時実装した時から既に数年は経過していて、細かい部分を色々改善して現在に至っています。
なので、この実装は、組み込みマイコンと C++ の親和性が良く発揮されるものだと思います。

まず、「sci_io.hpp」をインクルードします。

#include "common/sci_io.hpp"

C++ では、ヘッダーに全ての実装を書く事が出来、ソースファイルの指定や、ライブラリをリンクする必要は無く、メインのソースにヘッダーをインクルードするだけです。
※当然ですが、使わなければ(実態になる物を定義しなければ)、余分なメモリも消費しません。

SCI を使う為に必要な定義は、以下のようなものです。

    typedef device::SCI2 SCI_CH;  // SCI チャネルの定義
    typedef utils::fixed_fifo<char, 512> RXB;  // RX (RECV) バッファの定義
    typedef utils::fixed_fifo<char, 256> TXB;  // TX (SEND) バッファの定義
    typedef device::sci_io<SCI_CH, RXB, TXB> SCI;  // sci_io の定義

    SCI     sci_;
  • 利用する SCI チャネルを指定します。
  • 受信、送信バッファのサイズを指定します。(16バイト以上が必要)
  • 何も指定しないと、「第一候補」のポートが選択されます。
  • sci_io クラスの定義を typedef して、実態を記述します。

この場合、SCI2 を使い、受信バッファに 512 バイト、送信バッファに 256 バイトを割り当てています。
「バッファ」は、FIFO(First In First Out)で、リングバッファになっていて、固定長です。
※組み込みマイコンの制御では非常に多く利用する頻度があり、組み込み用に実装した専用クラスですが、別にこのクラスを使わなくても、自分でカスタムしたクラスを使う事も出来ます。

RX マイコンでは、SCI2 で利用できるポートが複数あります。
何も指定しないと、port_map クラスで指定されている、第一候補が選択されます。
※これは、sci_io テンプレートのプロトタイプが以下のようになっている為です。

template <class SCI, class RBF, class SBF, port_map::option PSEL = port_map::option::FIRST, class HCTL = NULL_PORT>
class sci_io {

また、SCI のポートモード設定を自分で行いたい場合は、「port_map::option::BYPASS」を選択する事もできます。
この指定を行うと、ポートのモード設定を「バイパス」して何も行われません。

port_map クラス内では、以下のようになっていて、P13(TXD)、P12(RXD) が使われます。

           uint8_t sel = enable ? 0b001010 : 0;
           PORT1::PMR.B3 = 0;
           MPC::P13PFS.PSEL = sel;  // TXD2/SMISO2/SSCL2 (P13 LQFP176: 52)
           PORT1::PMR.B3 = enable;
           PORT1::PMR.B2 = 0;
           MPC::P12PFS.PSEL = sel;  // RXD2/SMOSI2/SSDA2 (P12 LQFP176: 53)
           PORT1::PMR.B2 = enable;

第二候補を選択する場合、SCI の typedef を以下のようにします。

     typedef device::sci_io<SCI_CH, RXB, TXB, device::port_map::option::SECOND> SCI;

この場合、P50(TXD)、P52(RXD) となります。(詳しくは port_map.hpp 参照の事)

定義が出来たら、SCI を使える状態にします。
※ SCI の省電力切り替え等も内部で自動的に行われます。

    {  // SCI の開始
        uint8_t intr = 2;        // 割り込みレベル
        uint32_t baud = 115200;  // ボーレート
        sci_.start(baud, intr);
    }

これで、115200 bps で受信も送信も出来る状態になります。
ボーレートは整数で指定します、もし、内部分周器の能力を超えた場合(設定出来ない場合)「false」を返して失敗します。
RX マイコン内蔵の SCI は、チャネルによってベースとなるクロックが異なる場合があります。
※PCLKA、PCLKB
SCI の各チャネルの定義内には、どのクロックを使うかなどの情報が内包されていて、ボーレートを計算する時にそのクロック値を使う為、どのチャネルでも、全く同じように使えます。
割り込みレベルは、「2」を使っていますが大きな理由はありません。
※割り込みレベルのポリシーは、システム全体で考える必要のある問題なので、ここでは詳しい方法は延べません。

sci_io クラス内部は、色々な計算、条件分岐などあり、内部レジスタに値を直接入れる事に執着する C プログラマーがいます。
しかし、sci_io はテンプレートなので、コンパイル時に行う最適化により、実行時に必要無い計算や分岐は極限まで排除されます。
※アセンブラコードを見ると良く判ります。
※最適化は、「人」がするものでは無く、「マシン」が行うべき問題です。(もちろん例外はあります)


通信フォーマットは、何も指定しないと、8ビット、1ストップビットになります。
変更したい場合は、以下のように指定します。(8ビット、Even、2ストップビットの場合)

    sci_.start(baud, intr, SCI::PROTOCOL::B8_E_2S);

※PROTOCOL は、sci_io.hpp 内で定義されているものが使えます。

後は、データを送信したり、受信したりするだけです。

    auto ch = sci_.get_ch();  // 1 文字を受信

    sci_.put_ch(ch);  // 1 文字送信

    auto len = sci_.recv_length();  // 受信バッファに格納されている文字数

このように、C++ テンプレートクラスを使ったフレームワークでは、複雑な設定を隠蔽して、アプリケーションを実装する事が出来ます。
別プログラムで、設定を生成する事も無く簡単に扱えると思います。

文字を出力する仕組み

通常、printf などを使った場合、データはどのように処理されているのでしょうか?
※C++ では iostream を使います。
POSIX では「標準出力」と呼ばれる物が設定されており、そこに出力するようになっています。
これは、ファイルディスクリプタで、ファイルに書く事と同じ扱いです。

通常、アプリケーションをコンパイルすると、「libc.a」と呼ばれるライブラリがリンクされます。
この中に、ファイルとのやりとりを行う関数が内包されており、OS の制御下に置かれています。
組み込みでは、通常、この仕組みは使わないので、自分で用意する必要があります。

common/syscalls.c

に、libc.a に代わる組み込み専用の仕組みを用意しています。(syscalls.c のコンパイルと、リンクが必要)

実際にする事は、SCI の出力と、繋ぐ実装をするだけです。

main.cpp 内に、以下のように実装します。

extern "C" {

    // syscalls.c から呼ばれる、標準出力(stdout, stderr)
    void sci_putch(char ch)
    {
        sci_.putch(ch);
    }

    void sci_puts(const char* str)
    {
        sci_.puts(str);
    }

    // syscalls.c から呼ばれる、標準入力(stdin)
    char sci_getch(void)
    {
        return sci_.getch();
    }

    uint16_t sci_length()
    {
        return sci_.recv_length();
    }
}

これで、printf で文字を出力すると、SCI に出力される事になります。

C++ では printf を使わない

printf は便利ですが、重大な欠点があります。
それは可変引数を使ってパラメータを受け渡す為、スタックを経由する点です。
この問題については、ここでは詳しく述べませんので、興味があればご自分で調べてみて下さい。

C++ では、それに代わって、文字を扱う方法として、iostram クラスを使う方法が推奨されています。
iostream クラスは、便利で強力なクラスなのですが、組み込みマイコンのような環境では別の問題が発生します。

それは、メモリ消費が大きい事。

    std::cout << "Hello!" << std::endl;

   text    data     bss     dec     hex filename
 508864   47644    8812  565320   8a048 hello.elf

---
    printf("Hello!\n");

   text    data     bss     dec     hex filename
  13864      48    1924   15836    3ddc hello.elf

上記は、RX マイコンで、iostream と、レガシーな printf を使った場合のメモリ消費の違いです。

これでは、流石に、「常用」するには、問題があります。

そこで、printf に近い使い方が出来て、メモリサイズが小さくなるクラスを実装してあります。
元々は、「boost/format.hpp」のアイデアを真似て作り始めた物ですが、現在は色々な機能を入れてあります。
サイズもそこそこ小さくなります、通常の使い方では、ほぼ printf と遜色なく使えると思います。

    utils::format("Hello!\n");

   text    data     bss     dec     hex filename
   6700      48    2136    8884    22b4 hello.elf

プロジェクトは、別にありますが、common/format.hpp にコピーしてあります。
詳しくは、format.hpp プロジェクトを参照して下さい。

詳しくは、上記プロジェクトを参照してもらえばと思います。
※このクラスは、他の環境(VC、mingw64、Linux)でもインクルードするだけで普通に使えます。

たとえば、以下のように使えます。

    int a = 1000;
    utils::format("%d\n") % a;

実装がヘッダーに集中する事による大きなメリット

C++ で最も改善されたと思う一例は、ヘッダーと実装を分ける必要性が無くなった事にあります。

しかし、現実には、C++ なのに、ヘッダーとソース(実装)に分けている人が多いようです。
※典型的なのは、Arduino のスケッチなど

ヘッダーに実装を書く事で、コンパイル時に全てのコードが評価される為、インクルードヘッダーが多くなると、コンパイル時間が多くかかると思っている人がいると思います。
ですが、実際には、その逆で、全体のコンパイル時間は、ソースの数が多い程その差は大きく、極端に短くなります。

人間の感覚や常識は当てにならないものです。

C++ のクラスをソースに分けて実装する場合、冗長な書き方も必要になり、非常に面倒です。

また、全てがヘッダーにあるので、管理が非常に簡単になり、何かの機能を持ちだす場合など、ヘッダーが一つあれば済むので、コピー忘れなどが減り、修正した場合なども二重に管理する必要もありません、そして、ソースをプロジェクトに追加する必要が無いので利便性が増します。

良い事ずくめなのですが、どんな実装方法でも可能な訳ではなく、「コツ」のような物は必要です。
自分のフレームワークでは、ほぼ、ヘッダーのみなので参考にしてもらえればと思います。

SCI_sample サンプルプログラムの使い方

SCI の使い方を大まかに扱ったサンプルを用意してあります。

SCI_sample

RX72N Envision Kit では、

W10./d/Git/RX % cd SCI_sample
W10./d/Git/RX/SCI_sample % ls
main.cpp  README.md  READMEja.md  RX24T  RX64M  RX65N  RX66T  RX71M  RX72N
W10./d/Git/RX/SCI_sample % cd RX72N
W10./d/Git/RX/SCI_sample/RX72N % make

...

W10./d/Git/RX/SCI_sample/RX72N % ls
Makefile  release  sci_sample.elf  sci_sample.lst  sci_sample.map  sci_sample.mot
  • RX72N Envision Kit の CN8 USB ポートと、PC をマイクロ USB ケーブルで接続する。
  • TeraTerm などのターミナルソフトを起動する。
  • ルネサスの COM ポート、115200 Baud、8 ビット、1 ストップビットに設定。
  • sci_sample.mot をターゲットに書き込みます。

※ PC に、ルネサス社の USB シリアルドライバーがインストールされている必要がありますが、Flash Programmer v3 をインストールする際にインストールされる。

# Start SCI (UART) sample for 'RX72N' 240[MHz]
Baud rate (set):  115200
Baud rate (real): 115355 (0.13 [%])
CMT rate (set):  100 [Hz]
CMT rate (real): 100 [Hz] (0.00 [%])
#

※設定ボーレートと実際のボーレート表示
RX マイコンの SCI は、ボーレートクロック生成の分周器が粗いので、高いボーレートでは、誤差がそれなりにあります。
MDDR ビットレート補正を使って、設定周期に近い値に調整してはいるけど、115200 では、上記値が限界となっています。
歩調同期式の場合、1バイトの転送で、最後のストップビット(10ビット分)までで、ズレが許容されれば、エラーは出ないので、上記の誤差は問題無いと思います。

  • 115200 = 8.7マイクロ秒
  • 8.7 x 10 x 0.0013 = 0.113 マイクロ秒
  • 許容範囲は、8.7 の 2/5 と考えると、3.5 マイクロ秒なので、十分な誤差範囲と言えると思う。

このサンプルでは、キーボードから入力された文字をエコーバックします。
また、「RETURN」キーを押すと、入力された文字列を表示します。