SNSへはこちら

超簡単!ArduinoでLチカをする方法いろいろ

Arduino といえば、不便便利な IDE とその取り巻きですが、とにかく 開発が簡単なマイコンとして知られています。

本記事では、マイコン開発の基礎であるLチカを、Arduino だからこそできる(?)いろいろな方法で実装してみました。とりあえず今回はボード上の LED を点灯させるだけ(それじゃあチカチカしてないじゃんか、という意見はあると思いますが)。
その方法たちは、独断と偏見によってレベル別に並べてあります。皆さんどうぞご参考に。今回は、Arduino Uno と Arduino Nano で動作を確認しています。

Level 1: 素直にLチカ

よく Web にあるような書き方です。とりあえず Arduino 言語(笑)を使います。至って普通ですね。

#define LED 13
void setup() {
  pinMode(LED, OUTPUT);
}

void loop() {
  digitalWrite(LED, true);
}

この超絶シンプルな書き方にはデメリットがあって、

  • 内部で何をやっているかわからない
  • 動作が遅い(Lチカごときで気にするような内容ではないと思いますが)
  • 楽しくない

という問題があります。そこでこれ以降の書き方たちです。

Level 1.5: C++として書く

いいか、Arduino言語なんかないんだぞ、と言わんばかりに、C++ で書いてみます。ただ #define という過去の遺産を constexpr に変えただけです。

constexpr auto LED = 13;
void setup() {
  pinMode(LED, OUTPUT);
}

void loop() {
  digitalWrite(LED, true);
}

Arduino IDE では constexpr がシンタックスハイライトされないんですね。なんということだ。。。

Level 2: レジスタを叩く

上の方法だと楽しくないですよね。ということで 実際に IO レジスタを叩いてみましょう
まずは LED の接続先を確認します(え?13番ピンじゃないかって??いやいや、そんなわけ無いですよ)。arduino schematic と検索して回路図を見てみます。

ふむふむ、PB5 ですね。わかりました。なので、このポート出力設定と、出力データをいじればいいのです。なので、コードは以下のようになります。

void setup() {
  DDRB = bit(5);
}

void loop() {
  PORTB = bit(5);
}

まあ至って普通ですね。簡単簡単。

Level 2.5: 制御を乗っ取る

よくよく考えると、void setup() とか void loop() とか、どこでどうやって動いているのか不可解ですね。多分以下のような感じなんでしょうけど。

int main() {
  init(); // 何らかのライブラリ用初期化関数

  setup();
  while(true) {
    loop();
  }
}

でも実際にライブラリ内部のソースコードを見たわけじゃないので納得いきません。そこでこれらの関数の挙動を乗っ取ってしまえばいいと考えました。
参考は手前味噌ですが、↓の記事です。

C言語および C++ における属性の話です。 属性とは簡単に言うとコンパイラに「これはこれこれこういう機能を持った特別な関数ですよ〜」って教えるものになります。GCC の拡張ですので ANSI じゃな...

__attribute__((constructor)) をつければ main 関数が実行される前に実行されます。

void setup() {
  // put your setup code here, to run once:
}

void loop() {
  // put your main code here, to run repeatedly:
}

__attribute__((constructor))
void f() {
  asm volatile(
      "sbi 0x04, 5\n\t"
      "sbi 0x05, 5\n\t"
    );
}

Level 4: 機械語を手書き

アセンブラなんて甘えだ! という人はいると思います。あと、インラインアセンブラが気持ち悪いと思う人もいるでしょう。分かります。
この2つの要求に答えるためには、必然的に機械語を手書きすればいいというところに落ち着きますよね。

これはプログラミングマニュアルにあるビット対応図を利用すれば実現できます。どうするかと言うと...

  1. const uint16_t[] として機械語を手打ちした配列を作る
  2. 配列の属性として __attribute__((section(".text"))) とする
  3. その配列ポインタを void (*)() (関数ポインタ)にキャストする
  4. 関数を実行する

この手順を踏みます。1. で const を付けたのに、この変数は RAM 上に配置されるようです。変なの。
あと、AVR は RAM 上のデータは実行できないみたいなので、属性を指定することが必要です。若干ハマりかけました。

それでは、以下が実現したコードです。

void setup() {
  // put your setup code here, to run once:
}

void loop() {
  // put your main code here, to run repeatedly:
}

__attribute__((section(".text")))
const uint16_t code[] = {
  0x9A25, // sbi 0x04, 5
  0x9A2D, // sbi 0x05, 5
  0x9508  // ret
};

__attribute__((constructor))
void f() {
  auto execute = reinterpret_cast<void (*)()>(code);
  execute();
}

至ってシンプルですね。でもいちいちキャストした関数ポインタを execute として束縛するのは...
ということで、f() を省略して書いてみます。

void f() {
  reinterpret_cast<void (*)()>(code)();
}

スッキリしていいですね!!

以上、ただのネタ記事でした。