AVR のファームウェアを C++ で書くのはどう?

TR-909 クローンのファームウェア、あまり深く考えずに C で書き始めましたけど、どうもちょいちょい「これは、C++ のほうがよくない?」という場面にあたります。

例えばこんなところ

void TriggerRimShot(int8_t velocity) {
  SET_BIT(PORT_TRIG_RIM_SHOT, BIT_TRIG_RIM_SHOT);
  SET_BIT(PORT_LED_RIM_SHOT, BIT_LED_RIM_SHOT);
  g_rim_shot.status = 255;
}

この関数にはこんなマクロが参照されています

#define SET_BIT(port, bit) (port) |= _BV(bit)
#define PORT_TRIG_RIM_SHOT PORTD
#define BIT_TRIG_RIM_SHOT  PD0

なんというか。マクロだらけです。けっこう危ないコードだしメンテも大変そうです。他にもこんなところ

void CheckSwitches(uint8_t prev_switches, uint8_t new_switches) {
  if ((prev_switches ^ new_switches) == 0) {
    return;
  }
  if (IS_RIM_SHOT_ON(new_switches)) {
    TriggerRimShort(127);
  }
  if (IS_OPEN_HI_HAT_ON(new_switches)) {
    TriggerOpenHiHat(127);
  }
  if (IS_CLOSED_HI_HAT_ON(new_switches)) {
    TriggerClosedHiHat(127);
  }
}

スイッチが押されるのを検知する関数ですが、マクロを使うわ微妙に違う似たようなパタンの繰り返しになるわ。実行速度を考えるとループを回したり関数ポインタを使った汎用ルーチンを使ったりは避けたいですけど、すぐにメンテが大変なことになりそうです。これ、テンプレートが使えないかな?

てな風に、C++ で書いた方がいいんじゃね?感がどんどん増してきました。8bit プロセッサのコードを C++ で書くのはあまりやったことがありませんがどんな感じなりそうか見てみました。

まずは最初に出てきたマクロ

#define SET_BIT(port, bit) (port) |= _BV(bit)

これをテンプレートでこんな風に書けたら?

SetBit<PORTB, PB2>();

と思って、PB2 は何かの整数でおそらく間違いないでしょうけど PORTB てそもそも型がわかりません。インクルードファイルをたどっていったところ定義がわかりました。ちなみにこれは ATMega64 の定義でデバイスが違うとアドレスも違うはずです

// in iom64.h
/* Data Register, Port E */
#define PORTE     _SFR_IO8(0x03)

// in sft_defs.h
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

というわけで、PORTx は volatile uint8_t 型のポインタがさす値、ということで定数ではなかったです。テンプレートにはできませんけど、関数で呼べそうです。調べているうちにそのものずばりのドキュメントが見つかりました

https://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_port_pass

C ですけれども、こんな関数を書いて

void SetBits(volatile uintu_t* port, uintu_t mask) {
  *port |= mask;
}

SetBits(&PORTB, 0x55);

みたいに呼べば良いようです。C++ で参照を使うとどうかな?と思ったので、非常に簡単なテストプログラムを書いてシミュレータで実行してみました

// macro
#define SET_BIT(PORT, BIT) (PORT) |= _BV(BIT)

// C style
static void SetBitC(volatile uint8_t *port, const uint8_t bit) {
  *port |= _BV(bit);
}

// C++ style
inline static void SetBitCpp(volatile uint8_t& port, const uint8_t bit) {
  port |= _BV(bit);
}

int main(void)
{
  SET_BIT(PORTB, PB2);
  SetBitC(&PORTB, PB3);
  SetBitCpp(PORTB, PB4);
}

実行してコンパイルされたコードを確認してみましたが、これぐらい簡単な関数だとどの方法でも全く変わらないようです。

C++ に切り替えてさっそく嫌だった繰り返し部分をテンプレートに書き換え。こうだったのが

void CheckSwitches(uint8_t prev_switches, uint8_t new_switches) {
  if ((prev_switches ^ new_switches) == 0) {
    return;
  }
  if (IS_RIM_SHOT_ON(new_switches)) {
    TriggerRimShort(127);
  }
  if (IS_OPEN_HI_HAT_ON(new_switches)) {
    TriggerOpenHiHat(127);
  }
  if (IS_CLOSED_HI_HAT_ON(new_switches)) {
    TriggerClosedHiHat(127);
  }
}

こうなりました。読みやすくなったしスイッチはこれからまだ増えるのでトータルではコードが短くなりそうです

inline bool IsSwitchOn(uint8_t switch_bit) {
  return (PORT_SWITCHES & _BV(switch_bit)) == 0;
}

template <void (*TriggerFunc)(int8_t)>
void CheckSwitch(uint8_t switch_bit, int8_t velocity) {
  if (IsSwitchOn(switch_bit)) {
    TriggerFunc(velocity);
  }
}

void CheckSwitches(uint8_t prev_switches, uint8_t new_switches) {
  if ((prev_switches ^ new_switches) == 0) {
    return;
  }
  CheckSwitch<TriggerRimShot>(BIT_SW_RIM_SHOT, 127);
  CheckSwitch<TriggerOpenHiHat>(BIT_SW_OPEN_HI_HAT, 127);
  CheckSwitch<TriggerClosedHiHat>(BIT_SW_CLOSED_HI_HAT, 127);
}

Comments

No comments yet. Why don’t you start the discussion?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください