ルックアップテーブルと補間の実装
三角関数・対数・指数や、センサの非線形補正などの処理において、FPUのないマイコンで毎回そのまま計算すると、処理時間もコードサイズも膨らみます。定番の解決策が、結果をあらかじめ計算して配列に持っておくルックアップテーブル(LUT)です。本記事では、LUTの設計手順と、テーブルを小さく保ちながら精度を確保する線形補間の実装、刻み幅の決め方までを解説します。
LUTとは:計算を「参照」に変える
LUTは、入力に対する計算結果をあらかじめ求めて配列にしておき、実行時は主に配列参照(必要に応じて補間)で答えを得る手法です。三角関数のような計算量の多い関数の高速化が代表例で、テーブルをconst配列として定義すれば、多くのツールチェーンでは読み出し専用セクション(.rodata等)に配置され、リンカ設定によってフラッシュメモリに置けます。なお、実際の配置(フラッシュメモリ常駐/起動時コピーの有無)は、リンカスクリプトやメモリ構成(MCU/SoC)に依存します。実行時間が入力値によらずほぼ一定になるため、リアルタイム処理との相性も抜群です。
テーブルの粗さと精度:補間で両立する
全入力値分のテーブルを持てば誤差はゼロですが、メモリを大量に使います。たとえば12ビット入力なら4096要素です。そこで、テーブルの刻みを粗くしてメモリを減らし、表にない中間の入力に対しては隣り合う2点を直線でつないで近似します。これが線形補間で、メモリ使用量と精度のトレードオフを実用的なバランスに落とし込む定石です。
線形補間の実装(等間隔テーブル)
テーブルの刻みを等間隔にすると、「どの区間か」の検索が割り算1回(またはシフト1回)で済み、実装も実行も速くなります。整数演算だけで書いた例を示します。
#include <stdint.h>
#define STEP 64u /* テーブルの刻み(2のべき乗) */
#define TBL_LEN (4096u / STEP + 1u) /* 12ビット入力 → 65要素 */
_Static_assert(STEP != 0u, "STEPは0にできません");
_Static_assert((STEP & (STEP - 1u)) == 0u, "STEPは2のべき乗にしてください");
static const int16_t table[TBL_LEN] = { /* スクリプトで生成した値 */ };
int16_t lut_interp(uint16_t x) /* x: 想定は 0〜4095(12ビット)。範囲外はクランプ */
{
if (x >= 4096u) {
return table[TBL_LEN - 1u]; /* 範囲外は端の値にクランプ */
}
uint16_t idx = x / STEP; /* 区間番号(シフトに最適化される) */
uint16_t frac = x % STEP; /* 区間内の位置 */
int16_t y0 = table[idx];
int16_t y1 = table[idx + 1u];
/* y0 と y1 のあいだを frac/STEP の割合で按分する */
int32_t dy = (int32_t)y1 - (int32_t)y0;
return (int16_t)((int32_t)y0 + (dy * (int32_t)frac) / (int32_t)STEP);
}
ポイントは3つです。①刻みを2のべき乗(かつ定数)にすると、除算・剰余が多くのコンパイラでシフトやビットマスクに最適化されやすく、固定小数点実装で有利です。②差分の乗算は必ず広い型(ここではint32_t)でおこない、オーバーフローを防ぎます(「整数オーバーフローと未定義動作」参照)。③入力は0〜4095(12ビット)を想定し、xはuint16_tのため負値は発生しません。範囲外の入力は上限側の端の値にクランプします。また、idx+1を参照するため、テーブルは区間数+1要素を用意します。
テーブルの作り方:手計算ではなくスクリプトで
テーブルの中身は、PC上のスクリプト(Pythonや表計算ソフト)で生成してconst配列のソースコードとして出力するのが鉄則です。生成条件(入力範囲・刻み・スケーリング・元の数式)をスクリプトに残しておけば、仕様変更時の再生成も、レビューでの検算も容易になります。手作業でテーブルを編集し始めると、生成条件と実体がずれて、誰も検証できないテーブルが出来上がります。
刻み幅の決め方と誤差の確認
- 必要精度から逆算する:線形補間の誤差は、関数の曲がり(曲率)が大きい区間ほど大きくなります。スクリプト側で全入力に対する真値との最大誤差を計算し、要求精度に収まる刻みを選びます。補間対象が十分に滑らかな場合、線形補間の最大誤差は刻み幅の2乗に比例するため、刻み幅を半分にすると誤差はほぼ4分の1になります。ただし、固定小数点の丸め誤差が支配的な場合はこの限りではありません。
- 曲率が偏っているなら区間を分ける:サーミスタ特性のように一部だけ急カーブな関数では、急な区間だけ細かいテーブルにする2段構成も有効です。
- 単位とスケーリングを明記する:「テーブルの値は0.1℃単位」のような暗黙の約束はバグの温床です。生成スクリプトとヘッダのコメントに必ず残します。
応用例
LUT+補間は、サーミスタのA/D変換値→温度変換(「サーミスタによる温度測定」のlog計算の置き換え)、正弦波テーブルによる波形生成、表示のガンマ補正、非線形センサの直線化、固定小数点演算と組み合わせた数学関数の近似など、信号処理のあらゆる場面で使えます。なお、高性能CPUでは計算側(SIMD化された近似多項式など)が速い一方、メモリアクセスの遅延やキャッシュミスの影響で、LUTが必ずしも最速とは限りません。これに対して、内蔵フラッシュメモリからほぼ一定時間で読めるマイコンでは、安定した高速化手段として今も第一選択です。
まとめ
LUTの設計は「等間隔・2のべき乗の刻み」「補間の乗算は広い型で」「テーブルはスクリプト生成」の3点が骨子です。重い計算を見つけたら、まず表引き(LUT参照)+線形補間で置き換えられないかを検討します。この引き出しがあるだけで、FPUのないマイコンの設計自由度は大きく広がります。

