C++ を活用した SCI ドライバー
組み込みマイコンでは、動作状態を知る方法として、LED などのインジケーターが最も簡単な方法ですが、より複雑なプログラムを実装していくと、もっと細かい状態を知りたくなります。
SCI(非同期シリアル通信)は、マイコン側の詳細な状態を知る方法として、LED の次に簡単な方法と言えると思います。
3本の配線(RXD, TXD, GND)で、PCと通信でき、マイコンの状態を知る事が出来ます。
RXマイコンに限らず、非同期通信を行うには、フレームワークのサポートが無ければ、ハードウェアーマニュアルを読み、多義に渡った多くのレジスターを設定する必要があります。
自分のフレームワークでは、C++ を活用して、なるべく簡単な手続きで、シリアル通信が出来るようにしてあります。
マイコン種別固有の設定や差異は、C++ フレームワークが適切に吸収する事で、ノータッチで、純粋にアプリケーションの実装に集中出来ます。
また、SCI ドライバーのコードは、固有の依存部分がほぼ無く、読みやすい構造にしてあります。(#ifdef などで場合別けをしていない)
特殊な通信制御を実装する際の雛形としても利用出来ます。
今回は、その方法や、使われているテクニック、考え方などを紹介します。
このフレームワークでサポートしているRXマイコンは、以下のようなもので、全て同じように扱う事が出来るように工夫してあります。
RX220, RX62N, RX63[1N], RX24T, RX64M, RX71M, RX65[1N], RX66T, RX72T, RX72N
※今後、全てのRXマイコンをサポートしようと思っている。
プログラムで設定や動作を生成するような事も全く必要なく、柔軟性を重視し、判りやすく、必要最小限のパラメーターだけで動かせるように配慮しています。
コンパイラは gcc-8.3.0 を利用していますが、C++17 に対応していれば、コンパイル可能と思います。
最適化すれば、ほぼトップスピードで動作します。
ジェネレータを使ってソースコードを生成する黒魔術
とあるC言語のフレームワークでは、PC上で動作するアプリケーションを使って、設定を行い、ソースコードを生成して利用する手法が行われています。
しかし、この方法には欠点があり、自分としては、最善の方法とは思えません。
勿論、利点もあるとは思いますが、自分の尺度では、利点より欠点が凌駕していると思っています。
- 設定アプリが、マイコンアプリと密接に関連してしまい、再現性を低くする。
- 設定アプリの操作(GUI による)自体が、マイコンアプリの一部となってしまい、その手順を残す事や再現する事が困難となる。
- 設定アプリには、設定状態をセーブする機能がありますが、特定のバージョンに依存して、後年、同じように動作する保証が無い。
- 設定アプリのソースコードは公開されていないので、ブラックボックスとなっていて内部で何が行われているか明確ではない。
- 設定アプリの使い方や、前提条件など関連する事を学んで理解する必要がある。
- そもそも前提条件が多すぎるし、ハードルが高い。
- 生成されたソースコードに、自身のコードを追加した場合、設定をやり直したら、正しくマージはされずに失ってしまうし、コードの管理を難しくする。
- 何か、変更をする必要が生じたら、生成を一からやり直し、場合により、呼び出し名が変わる為全て修正する必要が発生する。
デメリットは他にも色々ありますが、手法として、この方法には限界があると思います。
C++ の新しい機能を適切に使って実装されたフレームワークなら、このような問題を根本的に改善する事が出来ます。
RX マイコン開発環境、C++ フレームワークのインストール
上記リンクを参照して、開発環境のインストール、Github から C++ フレームワークの取得を行って下さい。
コンパイラは、ルネサス GNU ツールの gcc-8.3.0 の利用を推奨します。(登録する事で無償で利用可能で、容量制限もありません)
プログラムのコーディングは、VSCode が便利です、C++ フレームワークには、VSCode 用の設定ファイルが含まれます。
C++ フレームワークのルートを VSCode で開けば、VSCode が RX マイコン C++ フレームワークを認識するようになります。
基本的に、MSYS2 環境で行うので、幾つかのコマンドを覚える必要がありますが、それらのスキルは、Linux などを扱う際に役にたちますし、沢山あるファイルから何かを探したり、自動化に役立ちますので、覚えていても損はありません。
C++ フレームワークによるSCI通信手順
まず、以下のソースを観て下さい。
#include "common/renesas.hpp"
#include "common/fixed_fifo.hpp"
#include "common/sci_io.hpp"
typedef utils::fixed_fifo<char, 512> RXB; // RX (受信) バッファの定義
typedef utils::fixed_fifo<char, 256> TXB; // TX (送信) バッファの定義
typedef device::sci_io<device::SCI2, RXB, TXB> SCI;
SCI sci_;
...
{ // SCI の開始
constexpr uint32_t baud = 115200; // ボーレート(任意の整数値を指定可能)
auto intr = device::ICU::LEVEL::_2; // 割り込みレベル(NONE を指定すると、ポーリング動作になる)
sci_.start(baud, intr); // 標準では、8ビット、1ストップビットを選択
}
これは、sci_sample における必要最小限のコードを抜き出したものです。
マイコンが変わっても、基本は同じで、ポートの候補が変わる程度です。
通常 SCI 通信では、送信、受信をバッファリングし、割り込み処理を利用して少ないコストで使う事が出来ます。
そうすれば、間欠的な通信でも、CPU リソースを最適化して、広帯域な通信とパフォーマンスを入手出来ます。
- ソースコードは MIT ライセンスで公開しているので、自由に使う事が出来ます。
- ソースコードは、全てヘッダーに実装されているので、ライブラリをリンクする必要がありません。
- ボーレートは、クロック設定などを参照して内部で計算される為、自由に設定が出来ます。
- SCI のチャネルを変更する場合、SCI の typedef 中の、SCI2 を SCIx と変更するだけです。
- 複数のチャネルを使う場合でも、コンテキストが増えるだけで構造的に扱えます。
- バッファを大きくしたり、小さくしたりする場合も、typedef の定数を変更するだけです。
- 関係クラスは、記憶割り当てを行わないので、リンクが成功すれば、動作する事になっています。
- 関数プロトタイプの引数は、「型」に依存するように配慮している為、間違った設定を行う事が困難です。
- これは、コメントを省略しても、動作が明確で、保守を簡単にします。
- C++ の最適化は高性能で、余分なコードは極限まで追い出す事が出来、高速に動作します。
- 定義部分はある程度マイコンに依存していますが、他は、全て共通で、コードの共有、再利用が可能です。
マイコン固有の設定
以下は、RX220、RX72N のプロジェクトファイルにあたる「Makefile」の主要部分です。
TARGET = sci_sample
DEVICE = R5F52206
RX_DEF = SIG_RX220
TARGET = sci_sample
DEVICE = R5F572NN
RX_DEF = SIG_RX72N
Makefile は、make コマンドで、gcc を呼び出し、各ソースを適切にコンパイルします。
- 「TARGET」は、プロジェクト名で、コンパイル、リンクが通れば、「sci_sample.mot」が出来て、このファイルをマイコンに書き込みます。
- 「DEVICE」はマイコンによりROM、RAM のサイズが異なるので、それに合わせて変更します。
- 「RX_DEF」は、マイコンの種別で、これを変えれば、各マイコン用に特化したコンパイル、リンクが行われます。
- マイコンの種別切り替え、個別の設定、共通の機能は、「common/makefile」で行われています。
- 基本的に「make」と打ち込むだけで、従属規則が生成され、全てのビルドが適切に行えます。
- CMake などで前処理する必要もなく、OS-X、Linux 環境でも同じように動作します。
- 何かファイルを編集したら、再度「make」と打てば従属規則に従って、適切にコンパイル、リンクが実行されます。
- VSCode をインストールすれば、VSCode のコンソールから行えます。
- rxprog をインストールすれば、マイコンへの書き込みもシリアル経由で行う事が出来ます。(make run)
- ファイルを追加したい場合は、Makefile の適切な場所にファイル名を追加するだけです。
- Makefile は、IDE におけるプロジェクトファイルと同等の機能を提供します。
ビルドの様子:
/d/Git/RX/SCI_sample/RX72N % make
mkdir -p release/SCI_sample/; \
rx-elf-g++ -MM -DDEPEND_ESCAPE -std=c++17 -Wall -Werror -Wno-unused-variable -Wno-unused-function -fno-exceptions -DNDEBUG -O3 -misa=v3 -DSIG_RX72N -I. -I../ -I../../ -I../../RX600/drw2d/inc/tes ../../SCI_sample/main.cpp \
| sed 's/main\.o:/release\/SCI_sample\/main.o release\/SCI_sample\/main.d:/' > release/SCI_sample/main.d ; \
[ -s release/SCI_sample/main.d ] || rm -f release/SCI_sample/main.d
...
rx-elf-as -c -misa=v3 -I../../rxlib/include -I/c/boost_1_74_0 -I. -I../ -I../../ -I../../RX600/drw2d/inc/tes -o release/common/start.o ../../common/start.s
...
rx-elf-gcc -c -std=gnu99 -Wall -Werror -Wno-unused-variable -fno-exceptions -DNDEBUG -O3 -misa=v3 -DSIG_RX72N -I../../rxlib/include -I/c/boost_1_74_0 -I. -I../
-I../../ -I../../RX600/drw2d/inc/tes -o release/common/syscalls.o ../../common/syscalls.c
mkdir -p release/SCI_sample/; \
rx-elf-g++ -c -std=c++17 -Wall -Werror -Wno-unused-variable -Wno-unused-function -fno-exceptions -DNDEBUG -O3 -misa=v3 -DSIG_RX72N -I../../rxlib/include -I/c/boost_1_74_0 -I. -I../ -I../../ -I../../RX600/drw2d/inc/tes -o release/SCI_sample/main.o ../../SCI_sample/main.cpp
rx-elf-gcc -nostartfiles -Wl,-Map,sci_sample.map -T ../../RX72N/R5F572NN.ld -L../../rxlib/lib -L../../RX600/drw2d -o sci_sample.elf release/common/start.o release/common/init.o release/common/vect.o release/common/syscalls.o release/SCI_sample/main.o -ldrw2d
rx-elf-size sci_sample.elf
text data bss dec hex filename
38972 48 2412 41432 a1d8 sci_sample.elf
rx-elf-objcopy --srec-forceS3 --srec-len 32 -O srec sci_sample.elf sci_sample.mot
rx-elf-objdump -h -S sci_sample.elf > sci_sample.lst
ここまで、簡単に出来て、VSCode 環境があれば、IDE を使う理由がほぼ無いと思います。
通信プロトコルを変更したい場合
「sci_io」テンプレートクラスの「start」関数では、標準で、8ビット、1ストップビット、パリティ無しが選択されます。
他のプロトコルを選択したい場合、通信プロトコルを追加で指定します。
constexpr uint32_t baud = 115200; // ボーレート(任意の整数値を指定可能)
auto intr = device::ICU::LEVEL::_2; // 割り込みレベル(NONE を指定すると、ポーリング動作になる)
auto protocol = SCI::PROTOCOL::B8_E_2S; // 8 ビット、Even(偶数)、2 Stop Bits
sci_.start(baud, intr, protocol);
通信プロトコルは以下のものが利用出来ます。
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
/*!
@brief SCI 通信プロトコル型
*/
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
enum class PROTOCOL : uint8_t {
B7_N_1S, ///< 7 ビット、No-Parity、1 Stop Bit
B7_E_1S, ///< 7 ビット、Even(偶数)、1 Stop Bit
B7_O_1S, ///< 7 ビット、Odd (奇数)、1 Stop Bit
B7_N_2S, ///< 7 ビット、No-Parity、2 Stop Bits
B7_E_2S, ///< 7 ビット、Even(偶数)、2 Stop Bits
B7_O_2S, ///< 7 ビット、Odd (奇数)、2 Stop Bits
B8_N_1S, ///< 8 ビット、No-Parity、1 Stop Bit
B8_E_1S, ///< 8 ビット、Even(偶数)、1 Stop Bit
B8_O_1S, ///< 8 ビット、Odd (奇数)、1 Stop Bit
B8_N_2S, ///< 8 ビット、No-Parity、2 Stop Bits
B8_E_2S, ///< 8 ビット、Even(偶数)、2 Stop Bits
B8_O_2S, ///< 8 ビット、Odd (奇数)、2 Stop Bits
};
static_assert と、constexpr を活用した、ボーレートの精度検査
sci_io テンプレートクラスでは、SCI に備わっている機能を参照して、指定されたボーレートになるように工夫します。
SCI の機能は、RXマイコンにより異なる場合があり、ボーレートの生成精度に違いが出ます。
あまりに遅い速度や速い速度では、物理的に設定の範囲を超えてしまいます。
その場合、「start」関数は、「false」を返して、失敗します。
C++11 から、「static_assert」、「constexpr」が利用可能になり、それらを使った実装を行う事で、コンパイル時に設定ボーレートの精度を検査する事が可能になりました。
これらの機能を活用する事で、動作不良などのケースをコンパイル時に見つけて、エラーで止める事が出来ます。
やり方は簡単で、以下のように、コードを1行追加するだけです。
constexpr uint32_t baud = 115200; // ボーレート(任意の整数値を指定可能)
static_assert(SCI::probe_baud(baud), "Failed baud rate accuracy test");
auto intr = device::ICU::LEVEL::_2;
sci_.start(baud, intr);
「probe_baud」関数は sci_io のメンバ関数で、ボーレートの誤差が 3.2% を超えた場合に「false」を返すので、static_assert によりコンパイルが停止します。
- 8ビット1ストップビット、パリティ無し(スタートビットを入れて10ビット)の場合、波形の中央でサンプリングした限界誤差は 3.9% 程度。
- この関数は、constexpr 属性で実装されているので、コンパイル時に値を計算して評価します。
- 実行バイナリーには、余分なコードは含まれないので実行の際に影響を与えません。
- 精度は、第二引数で与えられるので、より厳密なボーレート検査も可能です。
精度を 2% にする:
static_assert(SCI::probe_baud(baud, 20), "Failed baud rate accuracy test");
精度が閾値 を超えた場合にコンパイルを失敗する:
../../SCI_sample/main.cpp: In function 'int main(int, char**)':
../../SCI_sample/main.cpp:148:32: error: static assertion failed: Failed baud rate accuracy test
static_assert(SCI::probe_baud(baud), "Failed baud rate accuracy test");
~~~~~~~~~~~~~~~^~~~~~
make: *** [../../common/makefile:231: release/SCI_sample/main.o] Error 1
シリアル通信ポートの選択
RXマイコンでは、シリアル通信で利用するポート、RXDx、TXDx は、複数の選択肢があります。
sci_io テンプレートクラスでは、これらを選ぶ仕組みを用意してあります。
たとえば、SCI1 の第二選択肢を選ぶ場合、port_map::ORDER 型を使って、typedef は以下のようにします。
typedef device::sci_io<device::SCI1, RXB, TXB, device::port_map::ORDER::SECOND> SCI;
具体的に、ORDER 型によって、どのポートが有効になるのかは、port_map クラスを観れば判ります。
RX72T/port_map.hpp (SCI1) の場合:
static bool sci1_(ORDER opt, bool enable, bool clock)
{
uint8_t sel = enable ? 0b001010 : 0;
switch(opt) {
case ORDER::FIRST:
// PD5/RXD1 (20/100) (25/144)
// PD4/SCK1 (21/100) (26/144)
// PD3/TXD1 (22/100) (27/144)
if(clock) {
PORTD::PMR.B4 = 0;
MPC::PD4PFS.PSEL = sel;
PORTD::PMR.B4 = enable;
}
PORTD::PMR.B3 = 0;
PORTD::PMR.B5 = 0;
MPC::PD3PFS.PSEL = sel;
MPC::PD5PFS.PSEL = sel;
PORTD::PMR.B3 = enable;
PORTD::PMR.B5 = enable;
break;
case ORDER::SECOND:
// P25/SCK1 (94/144)
// PC4/TXD1 (98/144)
// PC3/RXD1 (99/144)
if(clock) {
PORT2::PMR.B5 = 0;
MPC::P25PFS.PSEL = sel;
PORT2::PMR.B5 = enable;
}
PORTC::PMR.B4 = 0;
PORTC::PMR.B3 = 0;
MPC::PC4PFS.PSEL = sel;
MPC::PC3PFS.PSEL = sel;
PORTC::PMR.B4 = enable;
PORTC::PMR.B3 = enable;
break;
default:
return false;
}
return true;
}
現在、選択肢は、かなり広範囲に用意してあります(MPC の解説に依存)が、それでも、足りない場合もあります。
そのような場合、ORDER::BYPASS を指定して、外部で設定する事が出来ます。
- SCI ポートの関係は、ハードウェアーマニュアル MPC の項目を参照して下さい。
- RXxxx/port_map.hpp を参考にして下さい。
- C++ の場合、選択肢が沢山あっても、使われないコードは削減され、実行バイナリーに含まれないので肥大化しません。
USB シリアルとマイコンの接続、ターミナルソフトの設定
PCとマイコンをシリアル通信で接続するには、USBシリアル変換器が必要です。
私は、USBシリアル変換器を自作していますが、秋月電子などでモジュールを販売しているので、それを利用する事も出来ます。
RXマイコンを 3.3V で動かす事が多い為、自作の基板では、レギュレータを内蔵しています。
USB/RS-232C 変換ケーブルを利用する事も出来ますが、RS-232Cの場合、ロジックレベルへの変換が必要なので、USBシリアル変換モジュールを使う方が簡潔です。
- USB シリアル側 RXD とマイコン側 TXD を接続。
- USB シリアル側 TXD とマイコン側 RXD を接続。
- USB シリアル側 GND とマイコン側 GND を接続。
良くある事故として、RXD と RXD、TXD と TXD を繋ぐ人がいますが、信号の意味を考えて下さい。
又、信号レベルが異なる場合(3.3Vと5Vなど)は、適合するような工夫をして下さい。
Windows の場合、ターミナルソフトとして「TeraTerm」が便利で高機能です。
ターミナル側で、スピード:115200、データ:8 ビット、ストップビット:1 bit、パリティ:none、フロー制御:none を選択します。
ポートは、接続する順番や、機器、色々な条件で変化します。(上記の例では、たまたま COM7 になっています)
接続した USB シリアルのポートが何になっているかは、コントロールパネルのデバイスマネージャーで確認出来ます。
TeraTerm のショートカットを作り、リンク先のコマンドパスの後ろにオプションコマンドを設定する事で個別の設定を行う事が出来て便利です。
オプションコマンドは、TeraTerm のドキュメントに解説があります。
標準文字入出力
- gcc では、実行環境は POSIX 準拠となっています。
- POSIX では、標準文字入出力の仕組みとして、stdout、stdin、stderr ハンドルがあります。
- これらのハンドルは、ファイル操作関数(read、write、printf)などで使われ、文字を入出力します。
- C++ では、C の関数「printf」は使いません、その代わり、std::cout などを使ってやりとりします。
- printf は、引数のパラメータをスタックを経由してやりとりするので、基本的に安全な方法とは言えません。
- C++ なら、std::cout を使うべきなのですが、組み込みマイコンでは、別の事情があります。
- それは、std::cout 関連のコードが巨大で、RAM、ROM を大量に消費してしまう事です。
- そこで、common/format.hpp クラスを実装してあり、printf と同じような感覚で、文字を扱う事が出来ます。
- 詳しい解説は「組み込みでも使える、printf に代わる C++ format クラス」を参照して下さい。
- common/format クラスは、IEEE754 32 ビット浮動小数点表示を独自にサポートしていて、コンパクトです。
- RX220 では、FPU をサポートしない為、浮動小数点を扱うと、実行バイナリーは大きくなりがちです。
マイコン側のプログラムでは、SCI と標準入出力を繋ぐ必要があり、その仕組みを用意してあります。
- syscalls.c を追加する。(SCI_sample プロジェクトでは追加済)
- SCI のコンテキスト関数を、syscalls.c の C 言語コードから利用出来るようにする。
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();
}
}
これで、文字をターミナルに送ったり、ターミナルから文字を受け取ったりする事が出来ます。
stdin からの文字入力は、利用可能ですが、stdin だけでは、文字入力があるか、無いかを判別できず、sidin 関係の API を呼ぶと、文字入力があるまでブロックされてしまいます。
そこで、以下のように、文字入力があるかどうかを検査出来ます。
if(sci_.recv_length() > 0) {
// 文字入力が有る
} else {
// 文字入力は無い
}
SCI_sample では、文字入力を行毎にバッファリングして、ワード単位に表示するようになっています。
Start SCI (UART) sample for 'RX72T DIY' 200[MHz]
SCI PCLK: 50000000 [Hz]
SCI Baud rate (set): 115200 [BPS]
Baud rate (real): 115287 (0.08 [%])
SEMR_BRME: true
SEMR_BGDM: true
CMT Timer (set): 100 [Hz]
Timer (real): 100 [Hz] (0.00 [%])
# abc def ghq 123
Param0: 'abc'
Param1: 'def'
Param2: 'ghq'
Param3: '123'
#
RS-485 ドライバーの制御
RS-485 は、通信ラインを差動信号で入出力し、半二重通信を行います。
詳しくは、ぐぐって下さい。
差動信号でやりとりする事で、ノイズに強く、長い転送路でも、信号が劣化しない為、高品質の通信が行えます。
通常複数のノードを接続して、複数の機器と相互に通信を行います。
ハード的には、シリアル入出力に、RS-485 に準拠したドライバーを接続し、ゲートをコントロールするだけです。
この参考回路は、RX72T ボードに搭載した RS-485 ドライバー MAX3485 によるものです。
- RS-485 では、半二重で通信する為、何等かのプロトコルを用意して、送信を適切に制御する必要があります。
- RS-485 では、ノードは自分以外、複数接続されるので、自分以外が送信中に、他のノードが送信すると、データが衝突して正常に送信出来ない構造です。
- 通常は、常に受信のみ監視して、送信を行う場合に送信ゲートを有効にして送信を行い、送信が終了したら、送信ゲートを無効にします。
sci_io テンプレートクラスでは、送信ゲートを制御する仕組みを入れてあり、以下のように定義する事で、簡単に扱えます。
// PA0: TXD11, PA1: RXD11 for RX72T
// P32: MAX3485 /RE, P33: MAX3485 DE
typedef device::PORT<device::PORT3, device::bitpos::B2> RS485_NRE; // for MAX3485 /RE
typedef device::PORT<device::PORT3, device::bitpos::B3> RS485_DE; // for MAX3485 DE
typedef device::sci_io<RS485_CH, RS485_RXB, RS485_TXB, device::port_map::ORDER::SECOND, device::sci_io_base::FLOW_CTRL::RS485, RS485_DE> RS485;
RS485 rs485_;
RS-485 の送信ゲート(RS485_DE)を制御するポート(PORT33)を定義します。
PORT テンプレートクラスは、1ビットのポートを定義するクラスで、ポートと、ビット位置を指定します。
この例では、受信ポートは、常に有効にしておきます。
そうすると、送信した文字と、同じ文字を受け取る事が出来たら、正しく送信出来た事を保障する事が出来ます。
送信ゲートをオシロスコープで観測:
ピンクは、TXD11 ライン、黄色は、RS485_DE ライン
テンプレートクラスと、関数呼び出しの大きな違い
Arduino などでは、ポートを操作する仕組みとして、「デジタルポート」 API が用意されています。
pinMode(8, OUTPUT);
digitalWrite(8, LOW);
...
digitalWrite(8, HIGH);
- 「8」は、単なる整数で明確ではありません。
- 内部では、ポートに対応する整数により処理を振り分けていて、最適化されても高速に動作させるのが難しいです。
- 「8」は #define や、const などで判りやすい名称に再定義する事も出来ますが、単なる整数で、「型」では無いので間違いが起こりやすいと思います。
- 全く関係ない別の整数を引数に使ったとしても、コンパイラはそれを見つける事が困難です。
C++ テンプレートで表現した PORT クラスでは:
typedef device::PORT<device::PORT3, device::bitpos::B2> RS485_NRE; // for MAX3485 /RE
RS485_NRE::DIR = 1;
RS485_NRE::P = 0; // NRE enable
上記のように明確で、判りやすく、ポートの変更も簡単です。
そして、最適化された場合に、余分な動作は全て削減され、高速に動作します。
fff0558d: fb 7e 03 c0 08 mov.l #0x8c003, r7
fff05592: f0 72 bset #2, [r7].b
fff05594: fb 7e 23 c0 08 mov.l #0x8c023, r7
fff05599: f0 7a bclr #2, [r7].b
上記は、C++ のポートクラスへの操作が、アセンブラ命令に翻訳された部分を抜き出したものですが、余分な命令がほぼありません。
- 0x8c003 は、PORT3 のデータ方向レジスタアドレスです。(DIR = 1 の部分)
- 0x8c023 は、PORT3 の出力レジスタアドレスです。(P = 0 の部分)
- bset 命令は、指定のビット位置に「1」を立てます。
- bclr 命令は、指定のビット位置を「0」にします。
C++ のテンプレートクラスは、テンプレートパラメータが定数なので、最適化を行う深度が深く、非常に巧妙に行われます。
ハードフローとソフトフロー制御
現在実装中で、今後サポート予定です。
- 自分が実験している環境では、文字をロストした事がほぼ無く、必要性を感じていません。
- ですが、ソフトフロー制御は必要かもしれません。
- あまり多くの機能を一つのクラスに入れ込むのは、良くない面もありますので、考え中です。
RX マイコン特有の機種依存を隠蔽する C++ のテクニック
RX マイコンでは、種類により、機能が異なり、割り込みなどかなり異なります。
そのような場合、C 言語ベースのフレームワークでは、「#ifdef」などで分類して、個別の機能を実装している場合が多いと思います。
これが、「独自プログラムによる設定の生成」のような発想を生むのかと思います。
この問題は、SCI の制御と、それとは関係が無い機種依存のコードが混在して複雑化して読みにくくなる事です。
C++ では、これらの方法を使わなくても、コンパイラに自動で選ばせるような仕組みなど、色々な手法を使う事が出来ます。
電力削減機能
RXマイコンのプログラムを実装した人は、一度は経験する事ですが、「電力削減機能」があります。
通常、起動時、RAM、I/O ポートは普通に扱えますが、それ以外の電力は、切ってあり、レジスタにアクセスしても何も起こりません。
それが判らなくて、苦労した話は、「あるある」だと思います。
C++ RXマイコンフレームワークでは、「ペリフェラル型」として、個々の機能を定義してあります。(RXxxx/peripheral.hpp)
これは、「enum class」で定義してあり、異なった型や整数を受け付けません。
※ C 言語の enum とは異なります。
例えば、SCI1 は、device::peripheral::SCI1 型として定義してあり、多くの場面で、この型を使います。
device::power_mgr::turn クラスを使って、個々の電源を制御出来ます。
device::power_mgr::turn(device::peripheral::SCI1); // 電源を入れる
device::power_mgr::turn(device::peripheral::SCI1, false); // 電源を切る
又、これとは別に、SCI のレジスター定義として、device::SCIx クラスが定義されています。
この中に、ペリフェラルの型を置いてあるので、SCIx::PERIPHERAL としてアクセスする事で参照できます。
このようにする事で、SCI2 や、SCI3 であっても、参照しているだけなので、分岐が必要無く直接的に自動でコードが生成されます。
sci_io テンプレートクラスではこの仕組みを利用して、電源を制御しています。
template <class SCI, class RBF, class SBF, port_map::ORDER PSEL = port_map::ORDER::FIRST,
typename sci_io_base::FLOW_CTRL FLCT = sci_io_base::FLOW_CTRL::NONE, class RTS = NULL_PORT>
class sci_io : public sci_io_base {
...
if(!power_mgr::turn(SCI::PERIPHERAL)) {
return false;
}
power_mgr では、電源制御が共有になっている場合に、それをケアしています。
たとえば、CMT0、CMT1 と CMT2、CMT3 のように、電源が共有されている場合があります。
この場合、CMT0、CMT1 を使っていて、CMT1 を OFF にしても、CMT0 が有効なら、電源は OFF にならないようにしてあります。
割り込みの管理
RX マイコンで、厄介なのは、割り込み関係だと思います。
機種により、非常に多くの割り込み要因があり、割り込みが足りなくなっている場合があります。
そこで、「通常割り込み」から「選択型割り込み」、「グループ割り込み」と異なる割り込みが存在します。
RX マイコン C++ フレームワークでは、これをシンプルに扱う仕組みを実装してあります。
SCI では、送信終了割り込みが、通常割り込みと、グループ割り込みの場合があり、異なる実装を行う必要があります。
sci_io クラスでは、RS-485 をサポートする場合、送信終了割り込み(TEI)を使う必要があります。
icu_mgr::set_interrupt(SCI::RXI, rxi_task_, level);
icu_mgr::set_interrupt(SCI::TXI, txi_task_, level);
if(FLCT == FLOW_CTRL::RS485) {
auto gv = icu_mgr::get_group_vector(SCI::TEI);
if(gv == ICU::VECTOR::NONE) {
icu_mgr::set_interrupt(SCI::TEI, tei_itask_, level);
} else {
icu_mgr::set_interrupt(SCI::TEI, tei_task_, level);
}
}
グループ割り込みの場合、割り込み関数は、フレームワークが要因別にディスパッチを行う為、「通常関数」を登録する必要があります。
上記のように、get_group_vector で、通常割り込みなのか、グループ割り込みなのかを判断して、登録する割り込み関数を別けています。
- RXI、TXI については、全てのマイコンで、通常割り込みなので、そのまま設定しています。
- 割り込みの管理は、ICU クラス、icu_mgr クラスでおこなっており、各マイコンで内容が異なりますが、sci_io クラスは、それを意識する必要がありません。
- この分岐は、テンプレートパラメータの定数で行っているので、コンパイル時に決定され、最終的なコードには分岐が無く、埋め込まれます。
まとめ
RX マイコン C++ フレームワークのコードは、かなり多く、複雑ですが、アプリケーションを作る場合は、内部の動作はあまり意識しなくても良いように工夫しています。
また、このフレームワークは、全てのコードを MIT ライセンスで公開してあり、自由に改変も可能です。
SCI 以外にも、CMT、MTU、TPU、CAN、USB、RIIC、RSPI、TMR、GPT、GPTW、GLCDC、DRW2D、SDHC、D/A、A/D、など様々なペリフェラルドライバーがあり、日々改善、改修を行っています。
GUIや、サウンド、FatFs、FreeRTOS なども簡単に扱えるように、ドライバー、汎用化を進めています。
C++ を活用する事で、最初のハードルを下げ、利便性と、速度、柔軟性など、多くのメリットがあるものと思います。
誰もが利用するような機能など、一般化出来る機能は、個々が実装しなくても良いように今後、バリエーションやサンプルを増やしていく予定です。
自分の GitHub でスポンサーに登録すれば(少しの費用がかかる)サポートを受けられますので、興味ある方は参照して下さい。