関数電卓を作ってみました
技術的な面では:
- gmp、mpfr ライブラリを使った多倍長演算(とりあえず250桁)
- C++17 (Renesas GNU RX gcc-8.3.0)
- PC で動作するAPIレベルシュミレータによる開発
シュミレータを作る
GUI を伴ったアプリを作ると、どうしても、容量が大きくなりがちです。
これは、漢字のビットマップデータを同梱しているなどの理由があります。
大きな容量のバイナリーをマイコンに書くと、どうしても時間がかかり、細かい挙動の調整が必要な GUI 系アプリでは、どうしても開発効率が悪いです。
そこで、API レベルで同等な動作をする環境を PC 上で作成して、その環境で機能実装を行い十分試す事で、開発効率を上げる工夫をしました。
自分が作成した GUI のフレームワークでは、描画は全てソフトウェアーで行っており、フレームバッファの表示と、タッチスクリーンをマウス入力に置き換える事で実現出来ます。
また、RX マイコンで使っているフレームワークや便利クラスは、PC 環境(今回は clang でコンパイル)でも、ほとんど何の改造をしなくてもそのまま動くように実装されています。
API レベルシュミレータは、OpenGL、GLFW3 などを利用した独自環境のフレームワークで、マルチプラットホームとなっています。
内部は、480x272 のフレームバッファを作成して、それをリアルタイムに描画しているのみです。
RX マイコンの描画部分は、RX マイコンと、シュミレータで切り替えが出来るようにしてあります。
インクルード部
- シュミレータでは、RX マイコン固有の設定をスポイルしています。
- graphics に含まれるコードの殆どは、ハードウェアーに依存しないので、そのまま利用出来ます。
- 一部、UTF-8 のコードを OEM(SJIS) コードに変換する部分があり、FatFS の API を利用しています。
- その部分だけ、PC 関係の API を呼んでいます。
#ifndef EMU
#include "common/renesas.hpp"
#include "common/fixed_fifo.hpp"
#include "common/sci_i2c_io.hpp"
#include "chip/FT5206.hpp"
#endif
#include "graphics/font8x16.hpp"
#include "graphics/kfont.hpp"
#include "graphics/graphics.hpp"
#include "graphics/simple_dialog.hpp"
#include "graphics/widget_director.hpp"
#include "graphics/scaling.hpp"
フレームバッファ定義
- このクラスは、RX マイコンの GLCDC 制御クラスの機能を模倣しています。
- と言っても、中身は、RGB565 のフレームバッファがあるだけです。
- メインアプリが描画した物は、フレームバッファに書かれます。
- シュミレータは、それをWindowフレームにコピーするだけです。
- その際、RGB565 を RGBA8 に変換します。
#ifdef EMU
template <uint32_t LCDX, uint32_t LCDY>
class glcdc_emu {
public:
static const uint32_t width = LCDX;
static const uint32_t height = LCDY;
private:
uint16_t fb_[LCDX * LCDY];
public:
void sync_vpos() { }
void* get_fbp() { return fb_; }
};
typedef glcdc_emu<LCD_X, LCD_Y> GLCDC;
#endif
タッチパネルインターフェース定義
- PC 上のマウス入力は、RX マイコンのタッチパネル入力に変換して与えています。
- 現状ではマルチタッチが出来ませんが、今回のアプリでは問題ありません。
#ifndef EMU
FT5206_I2C ft5206_i2c_;
typedef chip::FT5206<FT5206_I2C> TOUCH;
#else
class touch_emu {
public:
struct touch_t {
vtx::spos pos;
};
private:
touch_t touch_[4];
uint32_t num_;
public:
touch_emu() : num_(0) { }
uint32_t get_touch_num() const { return num_; }
const auto& get_touch_pos(uint32_t idx) const {
if(idx >= 4) idx = 0;
return touch_[idx];
}
void update() { }
void set_pos(const vtx::spos& pos)
{
touch_[0].pos = pos;
num_ = 1;
}
void reset() { num_ = 0; }
};
typedef touch_emu TOUCH;
#endif
シュミレータ部のソースコードは Git にあります。
MSYS2、mingw64 環境でコンパイル可能です。
https://github.com/hirakuni45/glfw3_app
https://github.com/hirakuni45/glfw3_app/tree/master/glfw3_app/rx_gui_emu
gmp、mpfr をRXマイコン用にコンパイル
gmp、mpfr は古くからある、多倍長をサポートした演算ライブラリです。
gmp は、多倍長の四則演算をサポートするライブラリです。
mpfr は、gmp を利用して、初等関数などをサポートするライブラリです。
最初、boost の多倍長ライブラリ「Boost Multiprecision Library」を使って実装していましたが、RX マイコンで、それらのコードをコンパイルした時に、環境依存の部分が色々あり、コンパイル出来ませんでした。
主に、スレッドセーフにする為の機構などです。
内部的には、pthread などを使っているようですが、RX マイコンでそれを通すとなると、FreeRTOS なども入れて、修正する必要があります。
現在、RX マイコンでも、boost を利用していますが、RX マイコン用にカスタマイズしていません。
カスタマイズが必要無いコードに限って利用しています。
以上の理由から、boost を RX マイコン用にカスタマイズするのは、かなりの労力と思われ、スルーしました。
- gmp、mpfr は、C のライブラリで、RX マイコンでコンパイルできれば、利用可能だと思いました。
- 関数電卓で利用する機能くらいなら、簡単なラッパーを作れば何とかなると思っていました。
- 実際は、既に、色々な人が実装したラッパーを試してみたものの、iostream に依存していて、容量が巨大になるので、断念しました。
gmp、mpfr のコンパイルは比較的簡単で、ソースを展開して、「configure」を実行、コンパイルするだけです。
ただ、最近使っている「Renesas GNU RX gcc-8.3.0」が標準でインストールされている場所のパスに「スペース」が含まれており、「configure」が正しく動きませんでした。
そこで、MSYS2 上の「/usr/local/」以下に、gcc コンパイラコレクションをコピーして、利用しています。
gmp のコンパイル:
% ./configure --host=rx-elf --prefix=/usr/local/rxlib --disable-shared
...
Version: GNU MP 6.2.1
Host type: rx-unknown-elf
ABI: standard
Install prefix: /usr/local/rxlib
Compiler: rx-elf-gcc
Static libraries: yes
Shared libraries: no
% make
% make install
mpfr のコンパイル:
% ./configure --host=rx-elf --prefix=/usr/local/rxlib --with-gmp=/usr/local/rxlib
% make
% make install
gmp、mpfr ライブラリは、MSYS2、mingw64 環境でも普通に使えます。
※pacman でインストールする必要がある。
mpfr ラッパーの実装
mpfr は C 言語のインターフェースなので、オブジェクトをクラス化して、ラッパークラスを作りました。
名前空間として、mpfr を定義しました。
これで、double と同じ感覚で利用出来るようになります。
要点は、オペレータを使い、演算記号をオーバーロードする事と、利用している初等関数のサポートです。
mpfr には、数値を扱う構造体として、「mpfr_t」が定義されており、それを C++ のクラスで包み、扱います。(value クラス)
C++ では、良くある構造です。
namespace mpfr {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
/*!
@brief mpfr オブジェクト
@param[in] num 有効桁数
*/
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
template <uint32_t num>
class value {
mpfr_t t_;
mpfr_rnd_t rnd_;
public:
...
bool operator == (int v) const noexcept { return mpfr_cmp_si(t_, v) == 0; }
bool operator != (int v) const noexcept { return mpfr_cmp_si(t_, v) != 0; }
bool operator == (long v) const noexcept { return mpfr_cmp_si(t_, v) == 0; }
bool operator != (long v) const noexcept { return mpfr_cmp_si(t_, v) != 0; }
bool operator == (double v) const noexcept { return mpfr_cmp_d(t_, v) == 0; }
bool operator != (double v) const noexcept { return mpfr_cmp_d(t_, v) != 0; }
value& operator = (const value& th) noexcept {
mpfr_set(t_, th.t_, rnd_);
return *this;
}
value& operator = (long v) noexcept {
mpfr_set_si(t_, v, rnd_);
return *this;
}
value& operator = (double v) noexcept {
mpfr_set_d(t_, v, rnd_);
return *this;
}
const value operator - () noexcept
{
value tmp(*this);
mpfr_neg(tmp.t_, tmp.t_, rnd_);
return tmp;
}
value& operator += (const value& th) noexcept
{
mpfr_add(t_, t_, th.t_, rnd_);
return *this;
}
value& operator -= (const value& th) noexcept
{
mpfr_sub(t_, t_, th.t_, rnd_);
return *this;
}
value& operator *= (const value& th) noexcept
{
mpfr_mul(t_, t_, th.t_, rnd_);
return *this;
}
value& operator /= (const value& th) noexcept
{
mpfr_div(t_, t_, th.t_, rnd_);
return *this;
}
value operator + (const value& th) const noexcept { return value(*this) += th; }
value operator - (const value& th) const noexcept { return value(*this) -= th; }
value operator * (const value& th) const noexcept { return value(*this) *= th; }
value operator / (const value& th) const noexcept { return value(*this) /= th; }
static value sin(const value& in)
{
value out;
mpfr_sin(out.t_, in.t_, out.get_rnd());
return out;
}
static value asin(const value& in)
{
value out;
mpfr_asin(out.t_, in.t_, out.get_rnd());
return out;
}
...
};
}
電卓機能の実装
計算を行う場合に、+-*/、() などを評価して、計算順番などを評価する必要があり、これはテキストベースで行っています。
※「common/basic_arith.hpp」クラス
また、このクラスでは、特定のシンボル名を変数としたり、関数名と実際の計算をバインドする為の仕組みを入れてあります。
詳しくは、ソースコードを参照して下さい。
- X10 exp10()
- FC キーで、sin, cos, tan -> asin, acos, atan が切り替わる。
- Rad, Grad, Deg 角度ベースの切り替え。
- V0, v10 変数切り替え。
- Min メモリーイン。
- Rcl リコール。
- Ans 答えの呼び出し。
※適当に触っていれば理解出来ると思います。
GUI フレームワーク
Renesas は、RX マイコン内蔵 GLCDC、DRW2D 向けにフリーの GUI フレームワーク「emWin」をリリースしています。
ですが、実装は C 言語ベースで、GUI リソースの作成など専用のアプリで行います。
対象は、RX マイコン内蔵 DRW2D エンジン向けなので、それらを持たないマイコンでは利用出来ません。
また、GUI のような複雑な構造を C 言語ベースで実装するのはかなり抵抗があります・・・
自分は、以前から独自の GUI フレームワークを実装しています。
これは、フレームバッファに対する、ソフトウェアーレンダリングでも、DRW2D エンジンを利用した場合でも使えるようにしてあります。
// ソフトウェアーレンダラー
// typedef graphics::render<GLCDC, FONT> RENDER;
// RX65N/RX72N DRW2D Engine
typedef device::drw2d_mgr<GLCDC, FONT> RENDER;
今回、俺俺 GUI フレームワークを利用しています。
俺俺 GUI では、リソースの配置など、専用アプリを使わず、ソフトで位置指定などを行う必要がありますが、C++17 を積極利用しています。
※ボタンの配置はグリッド状で、逆に簡単です。
ただ、ボタンに文字ではなく、グラフィックスを貼るように拡張したり、ベースの色を変更出来るようにしています。
constexpr int16_t LOC_X(int16_t x)
{
return ORG_X + SPC_X * x;
}
constexpr int16_t LOC_Y(int16_t y)
{
return ORG_Y + SPC_Y * y;
}
typedef gui::button BUTTON;
BUTTON no0_;
BUTTON no1_;
BUTTON no2_;
BUTTON no3_;
BUTTON no4_;
BUTTON no5_;
BUTTON no6_;
BUTTON no7_;
BUTTON no8_;
BUTTON no9_;
BUTTON del_;
BUTTON ac_;
BUTTON mul_;
BUTTON div_;
BUTTON add_;
BUTTON sub_;
BUTTON poi_; // .
BUTTON x10_;
BUTTON ans_;
BUTTON equ_; // =
BUTTON sin_;
BUTTON cos_;
BUTTON tan_;
BUTTON pai_;
BUTTON sqr_; // x^2
BUTTON sqrt_; // √
BUTTON pow_; // x^y
ボタンのコンテキストは、上記にように定義を行い
no0_(vtx::srect(LOC_X(5), LOC_Y(3), BTN_W, BTN_H), "0"),
no1_(vtx::srect(LOC_X(5), LOC_Y(2), BTN_W, BTN_H), "1"),
no2_(vtx::srect(LOC_X(6), LOC_Y(2), BTN_W, BTN_H), "2"),
no3_(vtx::srect(LOC_X(7), LOC_Y(2), BTN_W, BTN_H), "3"),
no4_(vtx::srect(LOC_X(5), LOC_Y(1), BTN_W, BTN_H), "4"),
no5_(vtx::srect(LOC_X(6), LOC_Y(1), BTN_W, BTN_H), "5"),
no6_(vtx::srect(LOC_X(7), LOC_Y(1), BTN_W, BTN_H), "6"),
no7_(vtx::srect(LOC_X(5), LOC_Y(0), BTN_W, BTN_H), "7"),
no8_(vtx::srect(LOC_X(6), LOC_Y(0), BTN_W, BTN_H), "8"),
no9_(vtx::srect(LOC_X(7), LOC_Y(0), BTN_W, BTN_H), "9"),
del_(vtx::srect(LOC_X(8), LOC_Y(0), BTN_W, BTN_H), "DEL"),
通常の文字列設定では、コンストラクターで、ボタンの位置、文字列を設定します。
ボタンの位置は、計算により設定しています。
※「constexpr」を使い、コンパイル時に計算されて埋め込まれます。
sqr_.enable();
sqr_.set_mobj(resource::bitmap::x_2_);
sqr_.at_select_func() = [=](uint32_t id) {
cbuff_ += '^';
cbuff_ += '2';
};
sqrt_.enable();
sqrt_.at_select_func() = [=](uint32_t id) {
cbuff_ += static_cast<char>(FUNC::NAME::SQRT);
cbuff_ += '('; nest_++;
};
pow_.enable();
pow_.set_mobj(resource::bitmap::x_y_);
pow_.at_select_func() = [=](uint32_t id) {
cbuff_ += '^';
};
ビットマップの設定「set_mobj()」は、初期化プロセスで、上記のように行います。
この時、ラムダ式で、ボタンが押された場合の挙動を実装出来るので、非常にシンプルな構造になっています。
GUI 関係のソースコードなど:
https://github.com/hirakuni45/RX/blob/master/graphics/READMEja.md
今後の予定
現状では、三角関数など利用する事が出来るので最低限の「関数電卓」です。
今後、グラフ表示や、簡単なスクリプトで、一連の計算を行うなど、夢が膨らみます。
グラフ付き関数電卓は、買うと意外と高価なので、RX72N Envision kit で実現出来ると、かなり便利に使えると思います。
又、ネットワーク機能もあり、ブラウザ経由からアクセスするとか、色々面白い使い方が出来そうです。
- 画面外の桁を表示する事が出来ない。
- 16進、10進 などの変換が無い。
上記二つの機能が実装されれば、そこそこ実用的かと思えます。
まとめ
今回、API レベルシュミレータを利用する事で、効率良く、電卓アプリを RX マイコンで実現出来ました。
PC 上で動いたコードが、RX マイコン側で何の修正も無しにそのまま動くのは気持ち良いものです。
これから、GUI 系アプリの実装では、便利に使えそうです。
ソースコード:
https://github.com/hirakuni45/RX/tree/master/CALC_sample
ライセンス
電卓アプリ本体、RXマイコンフレームワーク: MIT open source license
libgmp: GNU LGPL v3 and GNU GPL v2
libmpfr: GNU LGPL v3