RS-485
RS-485 は、非同期シリアルに RS-485 用ドライバを付け、若干のソフトを追加するだけで、利用出来る。
ドライバーは、差動信号を使い、ノイズに強く、長い通信路でも、信号の品質を維持する事が出来る。
ネットワークのトポロジーとして、CAN のような構成にできて、複数の機器を相互に接続出来る。
詳しくは、RS-485 の解説を参照してもらいたい。
但し、半二重通信なので、送信を行うには、送信ゲートを適切にコントロールする必要がある。
また、プロトコルを実装して、クライアントと通信するので、全体の構成を考える必要がある。
受信だけなら、流れてくるパケットをデコードするだけなので簡単だが、送信するには、受信データを監視して、タイミングを守る必要がある。
プロトコルは規定されていないが、PLC、インバーターなどで良く使われるものがあるようだ。
今回は、三相モーターのインバーターと通信するのがとりあえずのゴール。
sci_io クラスに機能を追加
sci_io クラスは、SCI を使った非同期シリアル通信を扱うクラスとなっている。
今回、RS-485 の送信ゲート制御を追加した。
一つのクラスに、あまり多くの機能を盛り込むのは、設計上別の問題もあるが、色々検討した結果、少しの機能追加で行える事が判ったので、機能追加とした。
RS-485 のゲート制御は、テンプレートパラメータで決定する為、通常の SCI 通信では、余分なコードが残らないように配慮してある。
機能を切り替える仕組み
定義は以下のようなもので、通常の SCI 定義を拡張したものとなっている。
ハードフロー制御(RTS)と共用となっているので、enum class の FLOW_CTRL 型に、RS485 を含めてあり、テンプレートパラメータで指示する。
typedef utils::fixed_fifo<char, 1024> RS485_RXB; // RS-485/RX (受信) バッファの定義
typedef utils::fixed_fifo<char, 512> RS485_TXB; // RS-485/TX (送信) バッファの定義
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 ドライバーの DE(送信ゲート)を制御する為、ポート定義を同時に行う。
- RS-485 ドライバーの受信ゲートは、常に有効にしてある。
- RS-485 ドライバは、ループバックしているので、送信した文字列をそのまま受信する。
- 送信した文字列と受信した文字列が等しければ、衝突は発生せず、正しく送信出来た事を保障する事が出来る。
- RS-485 ドライバーは、MAX3485 を使っている。(3.3V 動作品)
DE ポートの制御
- RS-485 の送信ゲート制御は、送信データ列が出ている間だけ、送信ゲートを有効にする必要がある。
- RX マイコンの SCI では、シリアル送信中のデータ列が出ている期間を知る手法はいくつかあるのだが、送信データがバッファリングされている為、単純な方法では、うまくいかない。
- 送信割り込みは、送信が可能になったタイミングで発生する。
- ストップビットを含めたシリアルデータが全て送られたタイミングで「送信終了割り込み」が発生する。
- 色々検討して実験した結果、送信開始前に、ゲートを有効にして、送信 FIFO が「空」になったら、送信終了割り込みを発生させ、そのタイミングでゲートを閉じるようにした。
送信終了割り込み(TEI)のハンドリング
送信終了割り込み(TEI)は多少厄介で、一工夫が必要となっている。
それは、マイコンによって、通常割り込みの場合と、グループ割り込みの場合がある為で、それを正しく管理する必要がある。
- RX621/RX62N/RX63T/RX24T では通常割り込み。
- RX64M/RX71M/RX65N/RX72N/RX66T/RX72T ではグループ割り込み。
static inline void tei_task_()
{
if(send_.length() == 0) {
RTS::P = 0;
}
SCI::SCR.TEIE = 0;
}
static INTERRUPT_FUNC void tei_itask_()
{
tei_task_();
}
void set_intr_(uint8_t level) noexcept
{
if(level > 0) {
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);
}
}
} else {
icu_mgr::set_interrupt(SCI::RXI, nullptr, level);
icu_mgr::set_interrupt(SCI::TXI, nullptr, level);
if(FLCT == FLOW_CTRL::RS485) {
icu_mgr::set_interrupt(SCI::TEI, nullptr, level);
}
}
}
現在、RX マイコン C++ フレームワークでは、グループ割り込みは、標準のディスパッチルーチンから呼ばれるので、割り込みタスクの属性を適切に設定する必要がある。
SCI::TEI(送信終了割り込み)がグループベクターの場合は、通常のタスクを登録し、グループベクターでは無い場合には、「INTERRUPT_FUNC」属性を付与した関数を登録する。
「INTERRUPT_FUNC」属性では、「RTE」オペコードで終了する。(通常関数は「RTS」オペコード)
RX24T の場合、「TEI」は通常ベクターで、グループでは無いので、「VECTOR::NONE」が返る。
static ICU::VECTOR get_group_vector(ICU::VECTOR vec) noexcept {
return ICU::VECTOR::NONE;
}
RX72T の場合、「TEI」は「VECTOR_AL0」なので、「VECTOR::GROUPAL0」が返る。
static ICU::VECTOR get_group_vector(ICU::VECTOR_AL0 vec) noexcept {
return ICU::VECTOR::GROUPAL0;
}
「icu_mgr::get_group_vector(SCI::TEI);」で、「SCI::TEI」がグループベクターなのか、そうでないのか判定出来る。
C++ では「型」の違いで、コンパイラが自動で関数を呼び分ける事ができ、処理を切り替える事が可能なので、柔軟性が大きい。
この判定は、定数が返るだけなので、最適化がされると、分岐や判定が綺麗に消えて、無駄なコードが残らない為、余分なメモリとマシンサイクルを節約する。
このような精妙な仕組みがスマートに実装出来るのは C++ の特徴となっている。
このような仕組みはC言語では難しい。
グループベクターのハンドリング
自分のフレームワークでは、割り込みなどは登録制にしてあり、グループベクターも登録制となっている。
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
/*!
@brief GROUPAL0・ベクター型
*/
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
enum class VECTOR_AL0 : uint8_t {
TEI11 = 12, ///< SCI11 / TEI11
ERI11, ///< SCI11 / ERI11
SPII0 = 16, ///< RSPI0 / SPII0
SPEI0, ///< RSPI0 / SPEI0
NUM_ = 4
};
SCI11 の送信終了割り込み「TEI11」は「グループベクター AL0」のグループで、上記番号は、レジスターのビット位置となっている。
グループ割り込みが発生した場合に該当する処理を呼び分けるディスパッチルーチンはなるべく高速に実行したいので、割り込みタスク登録時に必要なデータを登録してある。
typedef icu_utils::dispatch<ICU::VECTOR_AL0> GROUPAL0_dispatch_t;
static GROUPAL0_dispatch_t GROUPAL0_dispatch_;
template<typename GRPV>
class dispatch {
...
void set_task(GRPV grpv, GTASK task) noexcept
{
uint32_t bits = 1 << static_cast<uint32_t>(grpv);
for(uint32_t i = 0; i < NUM; ++i) {
if(bits_[i] == bits) {
bits_[i] = 0;
task_[i] = nullptr;
break;
}
}
for(uint32_t i = 0; i < NUM; ++i) {
if(bits_[i] == 0) {
bits_[i] = bits;
task_[i] = task;
break;
}
}
}
void run(uint32_t togo) const noexcept
{
for(uint32_t i = 0; i < NUM; ++i) {
if(bits_[i] == 0) break;
if((bits_[i] & togo) != 0 && task_[i] != nullptr) {
(*task_[i])();
}
}
}
};
static INTERRUPT_FUNC void group_al0_handler_() noexcept
{
GROUPAL0_dispatch_.run(ICU::GRPAL0());
}
static void install_group_task(ICU::VECTOR_AL0 grpv, icu_utils::GTASK task) noexcept
{
ICU::GENAL0.set(grpv, false);
set_task(get_group_vector(grpv), group_al0_handler_);
GROUPAL0_dispatch_.set_task(grpv, task);
if(task != nullptr) {
ICU::GENAL0.set(grpv);
}
}
- dispatch::run(togo) は割り込み要因 GRPAL0 レジスタを引数に呼ばれる。
- 登録されたデータに従い、タスクを呼び出す。
- この辺り、部分的な実装を抜き出しているので雰囲気だけなので、実際は、github のコードを観て下さい。
まとめ
- 現在サポートしているRXマイコンは10種程度に及ぶので、良く考えて実装しないと、修正が大掛かりになる場合もある。
- 同じような機能を提供するクラスや関数をなるべく共有できるようにしないと、全ての品種で恩恵が薄くなる。
C++ だと、テンプレートや、C++11、C++14、C++17 で備わった機能など色々使って、かなりシンプルに実装が出来る、速度も速い!
テンプレートの場合、コンパイルがエラー無く通ると、大体ちゃんと動く場合が多い。
そして、ほとんどが、ヘッダーのみに全て実装出来る、なんて便利なんだろうといつも思う、利用する人が増えれば良いのにと思う。