Skip to content
ブログに戻る
チュートリアル

Q15固定小数点の計算方法:Qフォーマット変換と丸め・オーバーフロー

0.1 を Q15 に格納すると 3277(実際の値は 0.100006103515625)になり、1.0 は飽和させないと -1.0 に反転する。Q 記法の読み方と手計算をオンラインで無料確認。

15 分で読める

Q15固定小数点の計算方法:Qフォーマット変換と丸め・オーバーフロー

Q15固定小数点は、小数を「2^15 = 32768 という暗黙の2進スケールを持つ、ごく普通の16ビット符号付き整数」として格納する方式だ。エンコードは掛け算1回と丸め1回で終わる。

raw = round(value × 32768)

デコードは割り算1回だけ。

value = raw ÷ 32768

演算はこれで全部である。0.5 × 32768 = 16384 だから 0.5 はメモリ上では整数 16384 として存在し、16384 ÷ 32768 は正確に 0.5 を返す。指数部も暗黙の1ビットも存在しない。小数点の位置は、書く側と読む側が合意した「取り決め」でしかなく、ハードウェアが見ているのは終始 int16 だ。

この式から直接2つの帰結が出てくる。そして人が半日を溶かすのは、決まってこの2つのどちらかである。

0.1 × 32768 = 3276.8 は整数ではない。Q15 が保持するのは 3277 であり、読み戻される値は 0.1 ではなく 0.100006103515625 になる。

1.0 × 32768 = 32768 は、16ビット符号付き整数の最大値をちょうど1つ超えている。つまり 1.0 には Q15 表現が存在しない。そのときコードが何をするかは、ほとんどのコードベースが文書に残していない「ポリシー」次第だ。飽和させれば 0.999969482421875、ラップアラウンドさせれば -1.0 になる。

どちらの失敗も、式そのものには現れない。Q 記法の意味はデータシートによって食い違うし、丸めとオーバーフローの規則はツールチェーンが黙って決めてしまう。両方向の変換を手で追えるようにしておく価値は、そこにある。

Q15、Q1.15、Qm.n は同じレイアウトなのか

Q15 の意味を3つの資料に尋ねると、3通りの答えが返ってくる。読み違えているわけではない。この記法は本当に標準化されたことがなく、食い違いの正体はたった1ビットだ。

Qm.n の読み方

2つの数字を並べる形式のほうが正直だ。Qm.n では n が小数ビット数、m が整数ビット数を表す。本記事は符号位置を m の内側に数える流儀を採る。Q16.16 が32ビットワードになるのはこの読み方であり、符号を含む整数16ビット+小数16ビット、スケールは 2^16 = 65536 となる。1.5 を Q16.16 でエンコードすると 1.5 × 65536 = 98304、16進では 0x00018000、丸め誤差はゼロだ。

厄介なのは符号ビットである。m の中に数える流儀と、m とは別に足す流儀がある。前者の流儀では Q1.15 は16ビットワード、つまり符号/整数位置が1つと小数15ビットになる。後者の流儀では同じラベルが17ビットを指すことになるが、そんな幅のマシンは存在しない。

同じ Q15 というラベルが、文書によって違う幅を指す理由

数字1つだけの Q15m を落としているので、ワード幅が暗黙になる。DSP の実務ではほぼ例外なく「小数15ビットを持つ16ビット符号付き2の補数ワード」を意味し、TI と ARM のエコシステムが数十年前に落ち着いたのもこの解釈だ。

ところが、こんな説明が広くコピーされて出回っている。「Q15 は小数15ビットの意味だから、32ビットで定義すれば符号1ビット+整数16ビットになる」。よく読むと、この文は符号ビットを m の内側ではなく m の外側に足して数えている。本記事が採るのとは別の流儀だ。ここで説明されているレイアウト自体は実在し、通常は Q16.15 と書かれる。おかしいのは貼られたラベルのほうである。整数16ビットを持つ32ビットワードは、どちらの流儀で数えても Q15 にはならない。同じ段落が十分な数のブログに複製された結果、いまでは一部のクエリで正しい定義より上位に表示されている。

見慣れない文書に出てくる裸の Q15 は、事実ではなく仮説として扱うこと。メモリマップ上のレジスタ幅、ドライバヘッダの C 型、既知のサンプル値など、実測できるものと突き合わせて確かめればいい。

他人のコードと混ざっても壊れない書き方

次の3つを書き留めておけば、曖昧さは消える。

  • 符号の有無:符号付き2の補数か、符号なしか
  • W:ワードの総ビット数
  • F:小数ビット数

signed, W=16, F=15 は誤読のしようがない。unsigned, W=32, F=16 も同様だ。残りはすべて導出できる。スケールは 2^F、分解能は 2^-F、範囲はワードの整数範囲を 2^F で割ったものになる。この3つの値をプロトコル仕様書と構造体のコメントに書いておけば、この議論を二度としなくて済む。

オンラインの Qフォーマット固定小数点変換ツール が結果のたびに符号の有無・W・F・スケールを併記しているのも同じ理由だ。データシートには「Q15」と書いてあるのに同僚のパーサは別のことを言っている、という場面では、既知のワードを1つデコードすれば10秒ほどで決着がつく。

変換式を手で計算する

どちらの方向も紙とペンで終わる。オシロの画面に映った16進ダンプとにらめっこしているときに、これが効いてくる。

浮動小数点から固定小数点へ:round(x × 2^F)

0.5 を Q15 に入れてみる。

  1. スケーリング:0.5 × 32768 = 16384
  2. 丸め:すでに整数なので 16384 のまま
  3. 範囲チェック:符号付き16ビットは -32768 から 32767 までを保持でき、16384 は収まる
  4. 格納:16384、16進 0x4000、2進 0100000000000000

情報が落ちるとすればステップ2、破綻するとすればステップ3。固定小数点で面白いことはすべて、このどちらかで起きる。

固定小数点から浮動小数点へ:raw ÷ 2^F

今度は逆方向を、符号付き Q15 フィールドに 0xC000 と読めるレジスタキャプチャから始めてみる。

  1. テキストを符号なし16ビットのコードとして解釈する:0xC000 = 49152
  2. 形式は符号付きで最上位ビットが立っているので、2^16 を引く:49152 - 65536 = -16384
  3. スケールを戻す:-16384 ÷ 32768 = -0.5

みんなが飛ばすのはステップ2だ。2の補数の補正なしでは 0xC000+1.5 と読めてしまうが、これは Q15 の範囲にすら入っていない値であり、格好の異常検知になる。デコード結果が形式の範囲外に落ちたなら、まず間違いなく符号の処理を忘れている。

from fractions import Fraction


def encode_q15(x):
    """Decimal value -> signed Q15 stored integer (ties to even)."""
    return round(Fraction(str(x)) * 32768)


def decode_q15(word):
    """Unsigned 16-bit code -> exact Q15 value."""
    if word & 0x8000:
        word -= 0x10000
    return Fraction(word, 32768)


print(encode_q15(0.5))              # 16384
print(encode_q15(0.1))              # 3277
print(float(decode_q15(0xC000)))    # -0.5
print(float(decode_q15(0x0CCD)))    # 0.100006103515625

ここで Fraction はきちんと仕事をしている。先に float を経由してスケーリングすると、まさに測定しようとしているその瞬間に2進の丸めが再混入してしまう。Fraction(str(x)) は、最も近い double ではなく、実際にタイプした10進リテラルのほうを読む。

16進ワードの読み書き

レジスタビューに実際に現れるのは16進であり、変換は機械的だ。3277 は16進で 0xCCD、W/4 桁にゼロ埋めして 0x0CCD になる。必ずゼロ埋めすること。Q15 ワードは16進4桁、Q31 ワードは8桁だ。先頭のゼロを落とすと、バッチデコーダで値がまるごとずれる。

生のコードは16進では符号なしのまま扱うのが原則だ。0x8000 は Q15 の最小値であって +32768 ではないし、それを -0x8000 と書いても誰も得をしない。バイトオーダーは別問題である。Q フォーマットが規定するのは数値のスケーリングだけで、エンディアンについては何も言っていない。0x0CCD をリトルエンディアンでダンプすれば、バイト列は CD 0C として届く。ダンプを読む途中の単なる基数変換なら、2進数・16進数・10進数・8進数変換 — 無料オンラインツール がスケールも符号付き幅も伴わずに2進・8進・16進を扱ってくれる。

Q7、Q15、Q31、Q16.16 の範囲と分解能

この表の数値はすべて、片端が -2^(W-1)2^F で割った値、もう片端が (2^(W-1) - 1)2^F で割った値である。表示用に丸めたものではなく、正確値だ。

形式ビット幅小数ビット Fスケール 2^F最小値最大値(正確値)分解能
Q787128-1127/128 = 0.99218751/128 = 0.0078125
Q15161532768-132767/32768 = 0.9999694824218751/32768 = 0.000030517578125
Q3132312147483648-1(2^31−1)/2^31 = 0.99999999953433871269226074218751/2^31 ≈ 4.6566128730773926e-10
Q16.16321665536-32768(2^31−1)/65536 = 32767.99998474121093751/65536 = 0.0000152587890625

これらの境界値を Qフォーマット固定小数点変換ツール に貼り付ければ、格納される整数・16進ワード・正確な10進表現がそのまま返ってくる。ファームウェアの定数を出荷前に確認する方法としては、これが一番速い。

Q15 の範囲の上限が 0.999969482421875 である理由

Q15 の範囲は非対称で、その非対称性は固定小数点に固有の何かではなく2の補数から来ている。符号付き16ビットワードは整数 -32768 から 32767 までをカバーする。両端を 32768 で割れば範囲は -32768/32768 から 32767/32768、すなわち -1 から 0.999969482421875 になる。

したがって 1.0 はちょうど1 LSB 分だけ足りない。「最大値はおおよそ 1.0」と書いてある資料は、この1 LSB を表示の都合で消している。1.0 には表現そのものが存在しないのだから、飽和したことを告げずに Q15 で 1.0 と報告する変換器は嘘をついている。

-1 が含まれる理由

Q15 の範囲を -1 < X < 0.9999695 と、両端とも開区間で書いている資料は少なくない。下限が間違っている。-32768 ÷ 32768 = -1 は正確に成り立つので -1 は表現可能であり、[-1, 0.999969482421875] は両端とも実際に取れる値だ。範囲を [-1, 1) と書いている資料もあるが、これはフルスケールを基準に同じことを述べたものである。1.0 そのものには届かない一方、届く最大の値は 0.999969482421875 であって、それに満たない何かではない。

これは記法上の揚げ足取りでは済まない。-1 は掛け算を壊す値そのものだからである。-1 × -1 = 1 であり、1 は範囲外だ。-1 には到達しないと思い込んでいる人は、それを捕まえる飽和処理の分岐を書かない。

同じ資料に出てくる切り詰められた上限 0.9999695 は、単なる表示上の産物だ。この値に循環する部分はない。値は 32767/32768 であり 32768 は2の冪なので、10進展開は15桁の 0.999969482421875 で終わる。

0.1 は Q15 に収まらない

スケーリングすれば問題はすぐ見える。0.1 × 32768 = 3276.8。固定小数点は整数しか保持できないので、どこかで折り合いをつけるしかない。

最近接丸めを使うと、格納されるワードは 3277、16進で 0x0CCD になる。そのワードが表す値は次のとおり。

3277 ÷ 32768 = 0.100006103515625

量子化誤差は、要求した値とグリッドが与えられた値との差である。

0.100006103515625 - 0.1 = +0.000006103515625

およそ100万分の6、1 LSB の5分の1ほどだ。音量調整なら無害である。だが毎秒1000回それを足し込む積分器の中では無害ではなく、累積値において毎秒およそ 0.006 という定常的なドリフトになる。

固定小数点の誤差は一様、浮動小数点の誤差は一様でない

Q15 は [-1, 0.999969482421875] の全域に、間隔がきっかり 1/32768 の65536点のグリッドを敷く。0.9 付近の間隔は 0.0001 付近の間隔と同じなので、最悪絶対誤差はどこでも半 LSB だ。おかげで誤差解析は、いい意味で退屈になる。フィルタのノイズフロアを、新人エンジニアでも検算できる算術で押さえられるからだ。

IEEE 754 は逆をやる。有効ビット数を固定して指数を動かすので、隣り合う double 同士の絶対的な間隔は値の大きさとともに広がり、一方で相対誤差はほぼ一定に保たれる。1.0 の近くではその間隔はおよそ 2.2e-16、1e12 の近くではおよそ 0.0001220703125 になる。

失敗は同じで、形が違うだけだ。double も 0.1 を保持できない。実際に格納されるのは 0.1000000000000000055511151231257827021181583404541015625 であり、0.1 + 0.20.30000000000000004 を返すのはそのためである。このケースは姉妹記事の浮動小数点の精度:なぜ 0.1 + 0.2 ≠ 0.3 なのか(2026年版)で詳しく追っている。固定小数点は2進における10進小数の問題を解決するわけではなく、誤差の大きさを予測可能にするだけだ。

1 LSB だけ違う3つの丸めモード

丸めの規則は実装の細部ではなく、データ契約の一部である。丸めについて意見が食い違う2つの正しい実装は、最下位ビットが永遠に一致しないテストベクタを生み出す。そしてその原因追跡は、みじめな作業だ。

モード規則10911.744-3276.8
最近接丸め、中間値は偶数へ最も近いグリッド点へ。ちょうど半分は偶数側の整数へ10912-3277
ゼロ方向への切り捨て小数部を捨てる。絶対値は縮むだけ10911-3276
負の無限大方向への切り下げ常に数直線の下方向へ10911-3277

例が1つでは足りない理由が、2つの列に表れている。正の値では切り捨てと切り下げは一致する。負の値では、切り捨てが -3276.8 をゼロ方向へ引き上げ、切り下げが押し下げるので、まるまる1 LSB 分だけ結果が割れる。

みんなが間違える計算例

0.333 は境界の近くに位置するので、Q15 のテストケースとして具合がいい。0.333 × 32768 = 10911.744 である。

  • 切り捨て:10911、読み戻すと 0.332977294921875
  • 最近接丸め:10912、読み戻すと 0.3330078125

古めのチュートリアルは 10911 と出力したきり、どの規則で出したのかを言わずに先へ進む。その数字を「エンコーダが丸めを行う」コードベースに持ち込めば、ゴールデンベクタは初回の実行で落ちる。しかも差はわずか1カウントなので、ポリシーの不一致ではなくタイプミスのように見えてしまう。

負の値では、3つの規則がはっきり分かれる。-0.1 × 32768 = -3276.8 は、切り捨てなら -3276(値は -0.0999755859375)、切り下げまたは最近接丸めなら -3277(値は -0.100006103515625)になる。切り捨ても最近接丸めも2つの符号を同じように扱い、それぞれ ±3276±3277 になるので、対称な係数ペアは対称なままだ。対称性を崩すのは切り下げのほうで、+0.13276 へ、-0.1-3277 へ送るため、ペアは1カウント分だけ偏る。

使っている言語のデフォルトはどうなっているか

これらのデフォルトに間違いはない。ただ互いに違っていて、しかもどれも自己申告してくれないというだけだ。

  • C/C++:浮動小数点から整数型へのキャストはゼロ方向へ切り捨てる。(int16_t)(0.333f * 32768)10911 になる。
  • Python:組み込みの round() は中間値を偶数へ丸めるので、round(3276.8)3277round(2.5)2 だ。
  • JavaScriptMath.round は中間値を正の無限大方向へ丸めるので対称ではない。Math.round(2.5)3 だが Math.round(-2.5)-2 になる。
  • ハードウェア:DSP の積和演算パスの多くはシフト時に丸めを行い、最近接偶数丸めをモードビットとして提供している。参照 C モデルとシリコンが食い違ったまま、誰かがモードレジスタを読むまで気づかないのはこのためだ。

規則を1つ決め、フォーマット仕様の W と F の隣にその名前を書き、テストベクタにもそれを持たせること。

オーバーフロー:飽和とラップアラウンド

Q15 のオーバーフローが起こすのは次の3つのいずれかだ。値を拒否する、0.999969482421875 にクランプする、-1.0 へラップアラウンドする。 どれになるかはツールチェーンが選んだポリシー次第であり、3番目は符号を黙って反転させる。

1.0 を例に取ろう。スケーリングすると 1.0 × 32768 = 32768 になるが、16ビット符号付き整数の最大値は 32767 である。値はちょうど1つ分だけ範囲を超えている。

ポリシー格納されるワード読み戻した値下流での見え方
エラーなし変換を拒否騒がしく捕捉可能。ツール類にはたいてい正解
飽和0x7FFF = 327670.999969482421875耳でも目でも 1.0 と区別できない
ラップ0x8000 = -32768-1.0フルスケールの符号反転

飽和の行は 0.000030517578125 を失うが、誰も気づかない。ラップの行はフルスケールの正のサンプルをフルスケールの負のサンプルに変えてしまい、オーディオ経路では部屋の反対側まで聞こえるプチッというノイズになる。制御ループなら、フルスケールの指令が逆方向に飛ぶということだ。

ラップアラウンドの危険な性質は、完全に正当に見えるワードを生み出す点にある。0x8000 は -1.0 の合法な Q15 表現だ。下流のどこも、それを本物の -1.0 サンプルと区別できない。事後に grep できるような痕跡は残らず、オーバーフローが起きた地点そのものに計測を仕込む以外に見つける方法がない。

DSP のシリコンが飽和命令を積んでいる理由

飽和は信号処理が求める振る舞いなので、プロセッサは分岐に任せず自前で実装している。ARM には QADD/QSUBSSAT/USAT の飽和シフト命令があり、NEON には VQADD などがあり、x86 SSE には paddsw のようなパック飽和加算がある。TI の C6000 と C55x のファミリは、アキュムレータ経路のモードビットとして飽和を公開している。

フィルタやミキサでは、たまにクリップするサンプルは小さな局所的歪みで済む。だがラップしたサンプルは、スペクトル全域にエネルギーを撒き散らす不連続だ。ハードウェアは「壊滅的に間違う」より「わずかに間違う」をデフォルトに選んでいる。ただし、それが効くのは有効にした場合だけであり、同じチップ上でも素の C の整数演算は依然としてラップする。

Q15 の掛け算に15ビット右シフトが必要な理由

2つの Q15 ワードを整数として掛けると、結果自体は正しいが、もう Q15 ではない。掛け算では小数ビットが加算されるからだ。Q15 × Q15 は Q30 になる。

答えが明らかに 0.25 である 0.5 × 0.5 で追ってみよう。

16384 × 16384 = 268435456          ← this is Q30, not Q15
268435456 ÷ 2^30 = 0.25            ← read as Q30, correct
268435456 >> 15 = 8192             ← realign to Q15
8192 ÷ 32768 = 0.25                ← same answer, back in Q15

268435456 を Q15 として解釈すると 8192.0 と読めてしまい、32768 倍ずれる。この係数こそがバグの正体であり、「ほぼ動く」固定小数点フィルタの誤差が2の冪であることが多い理由でもある。

積には置き場所も要る。16ビットの値2つを掛ければ最大32ビットになるので、中間結果は int32_t でなければならない。積を大量に累算するにはさらに余裕が必要で、C55x のような部品で DSP のアキュムレータが40ビットあるのはそのためだ。

シフトでは、ビットを捨てずに丸める

素の >> 15 は下位15ビットを捨てるが、これは符号付きの値にとって負の無限大方向への切り捨てである。出ていく側の精度で半 LSB を先に足せば、それが最近接丸めに変わる。

#include <stdint.h>
#include <stdio.h>

static int16_t sat_q15(int32_t v) {
    if (v >  32767) return  32767;
    if (v < -32768) return -32768;
    return (int16_t)v;
}

static int16_t mul_q15(int16_t a, int16_t b) {
    int32_t prod = (int32_t)a * (int32_t)b;   /* Q30 */
    int32_t back = (prod + (1 << 14)) >> 15;  /* round, then Q30 -> Q15 */
    return sat_q15(back);
}

int main(void) {
    printf("0.5*0.5   -> %d\n", mul_q15(16384, 16384));
    printf("0.1*0.1   -> %d\n", mul_q15(3277, 3277));
    printf("-1*-1     -> %d\n", mul_q15(-32768, -32768));
    printf("no-round  -> %d\n", (int)(((int32_t)3277 * 3277) >> 15));
    return 0;
}

cc -std=c11 -Wall -o q15 q15.c && ./q15 でコンパイルして実行すると、出力はこうなる。

0.5*0.5   -> 8192
0.1*0.1   -> 328
-1*-1     -> 32767
no-round  -> 327

3行目が先ほどの -1 のケースだ。-32768 × -32768 = 1073741824 は Q30 における 1.0 であり、Q15 では範囲外なので sat_q1532767 にクランプしている。クランプを外せば int16_t へのキャストが -32768 にラップさせ、-1 × -1-1 になってしまう。

最後の2行が丸めの差だ。3277 × 3277 = 10738729 で、素のシフトは 3270.009979248046875)を、丸め付きのシフトは 3280.010009765625)を与える。真の積は 0.01 なので、丸めたほうが2倍以上近い。加算1回でこれが買える。

シフトそのものについて注意が1つ。C では C23 以前、負の符号付き整数の右シフトは処理系定義である。とはいえ実際に出会うコンパイラはどれも算術シフトを行う。それでも落ち着かないなら、32768 で割ってコンパイラにシフトを吐かせるか、バイアスを掛けたうえで符号なし型に対してシフトすればいい。シフトとマスクのより広い仕組みはビット演算完全ガイド:AND・OR・XOR・シフトとビットマスクの実践で扱っている。

加算は先に Q 値を揃える必要がある

掛け算は Q 値を予測可能な形で変える。加算は不一致をまったく許容しない。Q7 のワードを Q15 のワードに足せば無意味な結果になる。オペランドがスケールを共有していないからだ。

まずシフトで揃えること。Q7 における 0.564 であり、64 << 816384、これが Q15 の 0.5 だ。シフト量は小数ビット数の差、15 - 7 = 8 である。

上へのシフトは正確だが余裕を食う。Q7 の値を Q15 へ昇格させるには、より広い容器が要るからだ。下へのシフトは損失を伴い、掛け算と同じ丸めの判断を必要とする。いずれにせよ、あらゆる中間結果の Q 値をコメントに書いておくこと。スケールが作者の頭の中にしか存在しない固定小数点コードは、1か月と経たずに保守不能になる。

Q15固定小数点と IEEE 754 浮動小数点のどちらを選ぶか

どちらも2進の位取り記数法なので、原理的にはどちらにも精度上の優位はない。判断は、ターゲットのハードウェアが何を代償として要求するか、そしてどんな保証が必要かに帰着する。

判断材料固定小数点に傾く場合浮動小数点に傾く場合
ハードウェア FPU はあるかFPU なし、またはソフトウェア浮動小数点ライブラリ単一サイクル演算のハードウェア FPU あり
ダイナミックレンジはどれくらいか正規化オーディオのように既知かつ有界何桁にもまたがる
ワイヤフォーマットがスケールを規定しているかプロトコルやレジスタが2進スケールを固定しているフィールドが本物の float
ビルドをまたいでビット単位で一致する必要があるか必要。整数はどこでも再現するFMA や最適化で揺れても許容できる
メモリや帯域は逼迫しているか16ビットサンプルなら32ビット float の半分に収まる制約ではない
コードを保守するのは誰かQ 記法に慣れたチーム混成チーム。スケールのバグのほうがリスクが大きい

最後の行は冗談ではない。固定小数点は誤差を実行時から設計フェーズへ移す。それが得な取引になるのは、誰かがその設計作業をきちんとやる場合だけだ。ハードウェア FPU を積んだ Cortex-M4F では、単精度 float のほうが速く、しかも安全であることが多い。反射的に Q15 へ手を伸ばす習慣は、もはや主流ではない部品から受け継いだ癖である。

IEEE 754 に軍配が上がる場面

ワード自体が固定スケールではなく符号・指数・仮数を持つときは、浮動小数点を選ぶこと。それが Q フォーマットの適用範囲が尽きる瞬間だ。指数が値ごとに変わるため、割るべき単一の 2^F が存在しない。

実務ではこの2つの表現が絶えず出会う。センサのデータは Q15 のレジスタワードとして届き、長い計算のために float へ昇格し、DAC へ渡すときにまた Q15 に戻る。その経路の浮動小数点側をビット単位で覗くなら、IEEE 754 変換ツール — 浮動小数点数を16進数へ、16進数を浮動小数点数へ(FP16/32/64)が値を符号・指数・仮数に分解し、格納されている正確な10進表現を出力してくれる。Q フォーマット変換ツールが固定スケールに対してやっているのと同じ仕事だ。

どちらも土台は同じ

Q フォーマットも IEEE 754 も素の整数も、同じビット列を「小数点がどこにあるか」「それが動けるか」という規則の違いだけで読み分けている。位取りの部分が心もとない、あるいは電卓に手を伸ばさずに 0x0CCD0000 1100 1100 1101 と読めるようになりたいなら、基数変換ガイド:2進数・16進数・8進数・10進数の相互変換と実践が両方の形式が乗っている土台を押さえている。

Q15固定小数点のFAQ

Q15とは何か

一般的な DSP の慣習では、Q15 は小数15ビットを持つ16ビット符号付き2の補数ワード、つまり符号位置1つと小数位置15個からなる。スケールは 2^15 = 32768、分解能は 1/32768 = 0.000030517578125、範囲は -1 から 32767/32768 までだ。Q のラベルは文書によって揺れるので、ラベルだけを信用せず、総ビット幅と符号の有無を確認すること。

Q15とQ1.15は同じものか

たいていは同じ16ビット符号付きレイアウトを指し、Q1.151 は符号位置を数えたものだ。ただしこの記法は普遍的ではなく、符号ビットを m の中に数えず上乗せする流儀の著者もいる。信頼できる記述は「符号の有無+総ビット数 W +小数ビット数 F」であり、このレイアウトなら signed, W=16, F=15 となる。

0.5 をQ15で表すといくつになるか

0.5 を Q15 で表すと、格納される整数は 16384、16進ワードは 0x4000 だ。計算は 0.5 × 32768 = 16384 で、すでに整数なので丸めも量子化誤差も発生しない。デコードでも確認できる。16384 ÷ 32768 = 0.5 は正確に成り立つ。

Q15の最大値と最小値はいくつか

最小値は -1 で、これは範囲に含まれる。-32768 ÷ 32768 がちょうど -1 になるからだ。最大値は 32767/32768 = 0.999969482421875。どちらも実際に取れる値なので、範囲は [-1, 0.999969482421875] となり、その外側に落ちるのは 1.0 のほうだ。下限を開区間として書いている資料は誤りで、その誤りが -1 × -1 のオーバーフローのケースを隠してしまう。

Q15がオーバーフローするとどうなるか

適用されているポリシーによる。エラーポリシーなら変換を拒否する。飽和なら最も近い端点にクランプするので、1.00.999969482421875 になり、1 LSB 分の損失は通常聞き取れない。ラップアラウンドなら 2^16 の剰余が適用されるので、1.0 は 32768 にスケーリングされて -32768 として読み戻され、結果として -1.0 になる。符号がまるごと反転するわけだ。DSP ハードウェアがまさにこの理由で飽和をデフォルトにしている一方、素の C の整数演算はラップする。

Q15同士を掛けたあとに15ビット右シフトが必要なのはなぜか

小数ビットが加算されるからだ。Q15 × Q15 は Q30 の積を生むので、整数の結果は15個ではなく30個の小数ビットを抱えている。15ビット右シフトすればそれが Q15 に再整列する。16384 × 16384 = 268435456 で、268435456 >> 15 = 8192、これをデコードすると 0.25 になる。切り捨てではなく最近接丸めにしたいなら、シフト前に 1 << 14 を足すこと。そして32ビットの積があふれないよう、中間結果は int32_t に置いておくこと。

IEEE 754ではなくQフォーマットを使うべきなのはどんなときか

プロトコル、レジスタマップ、DSP アルゴリズム、コーデックなどが整数ワードに対する2進スケールをすでに固定している場合は、Q フォーマットを使う。そのスケールはインターフェースの一部であって、こちらが選べるものではない。IEEE 754 を使うのは、値に広いダイナミックレンジが必要なとき、ターゲットにハードウェア FPU があるとき、あるいはフィールドが本当に符号・指数・仮数を格納しているときだ。スケールの絡まない単なる進数の読み替えなら、どちらの話でもない。それはただの基数変換だ。

まとめ

Q15固定小数点は、掛け算1回と丸めの判断1つ、そして範囲チェック1回でできている。入るときは raw = round(value × 32768)、出るときは value = raw ÷ 32768。式は自明だ。失敗はすべて、誰も文書化しない部分に潜んでいる。

だから文書化すること。ラベルだけで足りると仮定せず、すべての Q ラベルの隣に符号の有無と W と F を書く。丸めモードも同じ場所に明記する。負の値では切り捨てと切り下げが1 LSB 分だけ割れるからだ。オーバーフローがエラーになるのか、飽和するのか、ラップするのかも明示する。ラップのケースは 1.0-1.0 に変えたうえ、証拠を何ひとつ残さないからだ。

レジスタのワードと表計算が食い違ったら、既知の値を1つ Qフォーマット固定小数点変換ツール でデコードしてみるといい。格納される整数、正確な10進表現、量子化誤差、表現可能な範囲を並べて表示し、しかもすべてブラウザ内で計算する。W と F についての誰の思い込みが間違っていたのかを突き止めるには、たいていこれで十分だ。

タグ: fixed-point dsp embedded q-format number-representation