66
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++17 が組み込み開発にもたらす恩恵

Last updated at Posted at 2024-01-19

IMG_0280s.jpg

組み込み開発に有益な C++

自分が制作しているRXマイコン(R8C、RL78)C++ フレームワークは、年月が経ち、少しづつ成長を遂げ進歩してきています。
今回、主に C++17 に備わっている機能で、組み込み開発にもたらす恩恵が大きいと感じる機能に絞って紹介しようと思います。

C++17 としていますが、これは、現在メインで利用しているコンパイラ「GNU-RX gcc-8.3.0」が C++17 に対応したコンパイラである事が関係しています。
又、C++17 に備わっている機能は多義に上りますが、その一部でも便利に使えれば大きな恩恵があります。

C++17 は、2017 年にオンラインになっている仕様なので、2024 年の現在では、多くの環境で使う事が出来ると思います。


C++ ではヘッダーに全てを実装出来る!

これは C++17 の機能ではありませんが、より簡潔に書けるようになっています。

そもそも、ヘッダー(定義)とソースコード(実装)を別ける文化は、リソースが少なかった昔の開発環境に起因していると思います。
C++ では、ヘッダーに全てを実装して、「別ける」必要性は無くなっており、より簡単になっています。

ヘッダーに全てを実装する事で、コードの管理がより簡単になりますし、殆どの場合、コンパイル時間を節約し、より深い最適化を後押しする事が出来ます。

になり、ソースコードをプロジェクトに登録する必要性がありません。
ヘッダーに実装すれば、どんな大きなプロジェクトでも、ソースコードは一つだけになります。


static 変数をどうするか?

ヘッダーに全てを実装する場合に、問題になる事案があります。

  class abcd {

    static int count_;
  
  };

この「count_」は、「static」であり、「abcd」クラスとは別に管理されます。
「abcd」クラスは複数あっても、「count_」は、一つだけで、「abcd」クラスで共有されます。
特に、組み込み開発の実装では、変数を「static」宣言する必要性があり、このような場面が多くあります。

以前の方法では、「count_」の実態をソースに記述して、別に管理しリンクしていました。

int abcd::count_;

ですが、テンプレートを使って少し工夫すると、ヘッダーに「count_」の実態を記述する事が出来ます。
@shibainuudon さんからの指摘で、C++17 から、インライン変数が導入された為、実態を定義する必要がなく、もっと簡単に書ける事が判りました。(勉強不足ですね・・・)

//  template<class _>
//  class abcd_ {
    class abcd {

//    static int count_;
    static inline int count_;

  };
//  template<class _> int abcd_<_>::count_ = 0;  // この宣言が実態となる、初期値を代入出来る。
//  typedef abcd_<void> abcd;

static にする変数が複数ある場合、構造体に入れて、一括管理すると便利です。

//  template<class_>
//  class abcd_ {
    class abcd {

    struct holder_t {
      int count;
      int value1;
      int value2;
      holder_t() : count(0), value1(0), value2(0) { }  // 初期化リストで初期値を代入
    };
//  static holder_t holder_;
    static inline holder_t holder_;

  };
//  template<class _> typename abcd_<_>::holder_t abcd_<_>::holder_;  // holder_ の実態
//  typedef abcd_<void> abcd;

当然ながら、「holder_.count」のようにアクセスする必要があります。

ですが、単にヘッダーだけインクルードして、使えるようになる恩恵はかなり大きく、利便性が高いです。
コードを再利用する場合も、ヘッダーのみコピーすれば済みます。


constexpr

constexpr は C++11 から入った機能ですが、C++17 になって、より使いやすくなりました。

単純な読出し専用データ集合

組み込み開発では、フォントのデータなどを含める事が多いです。

  class font8x16 {
    static constexpr uint8_t bitmap_[] = {
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,

    };
  };

コンパイル時定数

constexpr では、コンパイル時に定数を計算して、固定した値として埋め込む事が出来ます。
この「計算」の過程では、複雑な計算、条件分岐も行う事が出来る為、かなり柔軟な仕組みを実装する事が可能です。
以前は、別ツールで、事前にテーブルを作成して、ソースコードに配置して、管理していたような事が、一つのプロジェクト内で一括して行えます。

たとえば、RGB565 の 16 ビットにパックされたカラーテーブルを持つ場合、RGB888 で定義しておき、コンパイル時に、シフト、論理演算を行い 16 ビットにパックする事が出来ます。
以下の例では。RGB888 の元データも保持しています。

  struct share_color {

    static constexpr uint16_t to_565(uint8_t r, uint8_t g, uint8_t b)
    {
      return
          (static_cast<uint16_t>(r & 0xf8) << 8)
        | (static_cast<uint16_t>(g & 0xfc) << 3)
        | (static_cast<uint16_t>(b) >> 3);
    }

    uint16_t	rgb565;
    color_t		rgba8;

    explicit share_color() noexcept :
      rgb565(to_565(0, 0, 0)), rgba8(0, 0, 0, 255) { }

    constexpr share_color(uint8_t r, uint8_t g, uint8_t b) noexcept :
      rgb565(to_565(r, g, b)), rgba8(r, g, b, 255) { }
  };

  struct def_color {
    static constexpr share_color Black   = { 0, 0, 0 };
    static constexpr share_color White   = { 255, 255, 255 };

    static constexpr share_color Orange       = { 255, 165,   0 };  // オレンジ
    static constexpr share_color SafeColor    = {  51, 204, 255 };  // セーフカラー(水色系)
    static constexpr share_color EmeraldGreen = {   0, 164, 116 };
    static constexpr share_color LightPink    = { 255, 182, 193 };

    static constexpr share_color DarkSafeColor  = { 23, 54, 64 };
    static constexpr share_color LightSafeColor = { 23+23/2, 54+54/2, 64+64/2 };
  };

static_assert

「static_assert」は、計算結果を評価して、コンパイルを止める事が出来ます。
「計算」は、constexpr を使った関数で、複雑な条件を導けます。

システムクロックの設定

RXマイコンでは、内部のクロック設定はかなり複雑で、色々な条件があり、デバイスによって構成が異なっています。
RXマイコン C++ フレームワークでは、クロック設定を「clock_profile.hpp」に集約して、これを元に、内部設定を行います。
この過程で、設定出来ない条件の場合に、static_assert により、コンパイルが停止するので、間違った設定を回避する事がスマートに出来ます。

	class clock_profile {
	public:
		//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
		/*!
			@brief  発信器タイプ @n
					USB を利用する場合、BASE は 4、6、8、12MHz にする。 @n
					LOCO は、起動時のモードなので、設定する事はない。
		*/
		//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
		enum class OSC_TYPE : uint8_t {
			XTAL,	///< クリスタル接続(1~20MHz)
			CERA,	///< セラミック発振子リード品(16MHz ~ 20MHz)
			EXT,	///< 外部クロック入力(最大 20MHz)
			HOCO,	///< 内蔵高速オンチップオシレーター(BASE: 32MHz, 54MHz)
			LOCO,	///< 内蔵低速オンチップオシレーター (125KHz)
		};
		static constexpr auto       OSCT        = OSC_TYPE::XTAL;	///< オシレーターの選択
		static constexpr uint32_t   BASE		= 12'000'000;		///< 外部接続クリスタル(1MHz ~ 20MHz)

		static constexpr bool		TURN_SBC	= false;			///< サブクロックを利用する場合「true」
		static constexpr bool       TURN_USB    = true;				///< USB を使う場合「true」

		static constexpr uint32_t   PLL_BASE	= 54'000'000;		///< PLL ベースクロック(最大 54MHz)

		static constexpr uint32_t   ICLK		= 54'000'000;		///< ICLK 周波数(最大 54MHz)
		static constexpr uint32_t   PCLKA		= 54'000'000;		///< PCLKB 周波数(最大 54MHz)
		static constexpr uint32_t   PCLKB		= 27'000'000;		///< PCLKB 周波数(最大 32MHz)
		static constexpr uint32_t   PCLKD		= 54'000'000;		///< PCLKD 周波数(最大 54MHz)
		static constexpr uint32_t   FCLK		= 27'000'000;		///< FCLK 周波数(最大 1 ~ 32MHz)
		static constexpr uint32_t	BCLK		= 27'000'000;		///< BCLK 周波数(最大 32MHz)

		static constexpr uint32_t	DELAY_MS	= ICLK / 1'000'000 / 4;	///< ソフトウェアー遅延における定数(1マイクロ秒)
	};
  • 上記は RX231 のクロックプロファイルです。
  • OSCT に XTAL を指定した場合、BASE は 1MHz ~ 20MHz の範囲でなければなりません。
  • PLL_BASE は、BASE の 4 倍から、0.5 倍刻みで、13.5 倍までです。
  • ICLK、PCLKA、PCLKB、PCLKD、FCLK、BCLK は、PLL_BASE の 1、1/2、1/4、1/8 のいずれかにする必要があります。
  • ICLK などの最大周波数制限は、オーバークロックを可能とする為、あえて評価していません。
	static constexpr bool check_base_clock_() noexcept
	{
		bool ret = false;
		switch(OSCT) {
		case clock_profile::OSC_TYPE::XTAL:
			if(clock_profile::BASE >= 1'000'000 && clock_profile::BASE <= 20'000'000) {
				ret = true;
			}
			break;
		case clock_profile::OSC_TYPE::CERA:  // 16MHz to 20MHz lead parts
			if(clock_profile::BASE >= 16'000'000 && clock_profile::BASE <= 20'000'000) {
				ret = true;
			}
			break;
		case clock_profile::OSC_TYPE::EXT:
			if(clock_profile::BASE > 0 && clock_profile::BASE <= 20'000'000) {
				ret = true;
			}
			break;
		case clock_profile::OSC_TYPE::HOCO:
			// 32MHz, `54MHz
			if(clock_profile::BASE == 32'000'000) {
				ret = true;
			} else if(clock_profile::BASE == 54'000'000) {
				ret = true;
			}
			break;
		default:
			ret = true;
			break;
		}
		return ret;
	}

	static constexpr bool check_clock_div_(uint32_t clk) noexcept
	{
		auto div = clock_div_(clk);
		if(div > 0b0110) {
			return false;  // overflow
		}
		if((clk << div) != ((clock_profile::PLL_BASE) & (0xffffffff << div))) {
			return false;  // 割り切れない周期
		}
		return true;
	}

	static bool boost_master_clock()
	{
		static_assert(check_base_clock_(), "BASE to overflow.");

		static_assert(check_clock_div_(clock_profile::ICLK),  "ICLK can't divided.");
		static_assert(check_clock_div_(clock_profile::PCLKA), "PCLKA can't divided.");
		static_assert(check_clock_div_(clock_profile::PCLKB), "PCLKB can't divided.");
		static_assert(check_clock_div_(clock_profile::PCLKD), "PCLKD can't divided.");
		static_assert(check_clock_div_(clock_profile::FCLK),  "FCLK can't divided.");
		static_assert(check_clock_div_(clock_profile::BCLK),  "BCLK can't divided.");


		static_assert((clock_profile::PLL_BASE * 2 / clock_profile::BASE) >= 8, "PLL_BASE clock divider underflow. (x10)");
		static_assert((clock_profile::PLL_BASE * 2 / clock_profile::BASE) <= 27, "PLL_BASE clock divider overflow. (x30)");
		static_assert((clock_profile::PLL_BASE * 2 % clock_profile::BASE) == 0, "PLL_BASE clock can't divided.");
...

    }

BASE に 24'000'000 を指定した為コンパイルエラー:

../../RX231/system_io.hpp:103:35: error: static assertion failed: BASE to overflow.
    static_assert(check_base_clock_(), "BASE to overflow.");
                  ~~~~~~~~~~~~~~~~~^~

シリアル通信のボーレート評価

シリアル通信では、内部ハードウェアーの分周器を操作して、決められたボーレートを設定します。

この時、分周器の能力を超えた速度を検出する必要があります。
又、システムのクロック周期に起因して、設定ボーレートには誤差が生じますが、誤差は、許容範囲に入っている必要があります。

RXマイコン C++ フレームワークの、シリアル通信ドライバーは、指定したボーレートと実際のボーレートを計算して誤差を求め、許容範囲外の場合を検出して、コンパイルエラーで止める機能を実装してあります。

	{  // SCI の開始
		constexpr uint32_t baud = 115200;  // ボーレート(任意の整数値を指定可能)
		static_assert(SCI::probe_baud(baud), "Failed baud rate accuracy test");  // 許容誤差(3%)を超える場合、コンパイルエラー
		auto intr = device::ICU::LEVEL::_2;		// 割り込みレベル(NONE を指定すると、ポーリング動作になる)
		sci_.start(baud, intr);  // 標準では、8ビット、1ストップビットを選択
	}

実際のコードは、Github にあります。

許容誤差を超えた場合:

../../SCI_sample/main.cpp:183:32: error: static assertion failed: Failed baud rate accuracy test
   static_assert(SCI::probe_baud(baud), "Failed baud rate accuracy test");  // 許容誤差01;31m~~~~~~~~~~~~~~~^~~~~~

起動時、実際のボーレートと誤差を表示した例:

Start SCI (UART) sample for 'RX231 DIY' 54[MHz]
SCI PCLK: 27000000 [Hz]
SCI Baud rate (set): 115200 [BPS]
  Baud rate (real): 115015 (-0.16 [%])
  SEMR_BRME: true
  SEMR_BGDM: true

まとめ

今回は、主に、ヘッダー化、constexpr、static_assert について解説しました。

細かい部分では、色々とありますが、この3つは、応用範囲が広く効果も大きいと思います。

C++ は「組み込み開発」に、まさにピッタリの開発環境ではと思っています。

RXマイコン C++ フレームワークは MIT ライセンスで Github で公開しています。

参考

66
55
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
66
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?