シリアルコミュニケーション
組み込みマイコンでは、最も手軽に、外部との通信を行えるインターフェースであると言えます。
このフレームワークでは、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 の制御下に置かれています。
組み込みでは、通常、この仕組みは使わないので、自分で用意する必要があります。
に、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 の使い方を大まかに扱ったサンプルを用意してあります。
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」キーを押すと、入力された文字列を表示します。