Arduinoアナログ入出力変換 map関数の高速化

未分類

はじめに

汎用性の高い、map関数。(remapとも言う?)
私は特にArduinoのアナログ入力→アナログ出力(PWM)の変換で使用することが多いのですが、
毎回古いコードから探すのが面倒なので自分用にメモ。

結論

コピペ用に結論から書いておきます。
測定した限りでは、これが一番速かったです。

C++
// analog read to analog write
#define ar2aw(x) ((long)(x * 0.249266862170088f)) // 0.249266862170088 = 255/1023

このあと解説、検証を行うので興味があれば見ていってください。

解説

どういった経緯で上のコードになるのか、通常のmap関数から段階的に記載します。

C++
long map(long x, long in_min, long in_max, long out_min, long out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

↓戻り値をyとし、数式に直す

y = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min

↓実際の設定値を当てはめる(入力=0~1023, 出力=0~255)

y = (x - 0) * (255 - 0) / (1023 - 0) + 0

↓±0は不要なので削除

y = (x) * (255) / (1023)

↓固定値部分を事前に計算

y = x * 0.249266862170088

となります。

検証

使用する環境

・Arduino IDE
・Arduino UNO R4 Minima
 (通常のUNOを引っ張り出してくるのが面倒だったので32bitのArduinoで…)

検証用のコード

何パターンか試してみました。
変更内容は各関数のコメントに書いてあります。

sumをprintしている理由は、sumの値を使わない場合だとdtの値が0になるためです。
恐らくコンパイル時の最適化によってforやmap関数ごと必要無い処理とみなされ省略されているのだと思います。

C++
#define ANALOG_READ_MIN (0)
#define ANALOG_READ_MAX (1023)

#define ANALOG_WRITE_MIN (0)
#define ANALOG_WRITE_MAX (255)

// 数式を簡略化
long map2(long x)
{
  return (long)(x * 0.249266862170088f);
};

// inlineを付ける
inline long map3(long x)
{
  return (long)(x * 0.249266862170088f);
};

// マクロで記載
#define map4(x) ((long)(x * 0.249266862170088f))

// long型のまま計算
long map5(long x)
{
  return (ANALOG_WRITE_MAX * x / ANALOG_READ_MAX);
};

void resultPrint(int number, unsigned long ts, unsigned long te, int sum)
{
  unsigned long dt = te - ts;
  String str = "map" + String(number) + "() " + String(dt) + " msec          sum=" + String(sum);
  Serial.println(str);
}

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  Serial.println("");

  unsigned long ts, te;
  int n = 10000; // 繰り返し回数
  int sum = 0;
  int number = 1;

  // ====================================================================================================
  // 1
  // ====================================================================================================

  ts = millis();

  sum = 0;
  for (int i = 0; i < n; ++i)
  {
    for (int j = ANALOG_READ_MIN; j <= ANALOG_READ_MAX; ++j)
    {
      sum += map(j, ANALOG_READ_MIN, ANALOG_READ_MAX, ANALOG_WRITE_MIN, ANALOG_WRITE_MAX);
    }
  }

  te = millis();

  resultPrint(number++, ts, te, sum);

  // ====================================================================================================
  // 2
  // ====================================================================================================

  // ~ ほぼ同じなので省略 ~
}

処理時間の測定結果

上のコードを実行した結果です。
3倍くらい高速化されました。

map1() 7372 msec          sum=1300500000 ←Arduinoライブラリのmap()
map2() 2354 msec sum=1300500000
map3() 2568 msec sum=1300500000
map4() 2354 msec sum=1300500000
map5() 2666 msec sum=1300500000

※環境によって速度差が違う可能性があります。

何度か実行しましたが傾向は変わらなかったです。

(2)(4)は恐らく(2)の時点でコンパイル時の最適化によって関数が展開されて
内部的には同じになっているのだと思います。

(3)と(4)は同じになると思ったのですが
inlineを付けて遅くなることもあるんですね…謎です。

(5)は下記の違いで結果的に少し遅くなっているのかもしれません。
・(2)(4)
 float型の掛け算 1回、long型にキャスト
・(5)
 long型の掛け算 1回、long型の割り算 1回

(2)(4)どちらを使うかは好みで良いのかなと。

もし、より高速化できる方法があればご教授いただけますと幸いです。

コメント

タイトルとURLをコピーしました