JavaScript
bluetooth
BLE
IEEE754
IEEE11073

[応用編]JavaScriptでバイナリデータを扱ってみる~IEEE-754とIEEE-11073の浮動小数点~(2/3)

More than 1 year has passed since last update.

BRIGHT VIE Advent Calendar 2017 - Qiita の5日目です!

「JavaScriptでバイナリデータを扱ってみる」シリーズでお届けしておりますが、本日は応用編2回目!

最近では、「Web Bluetooth API」も普及しつつあり、
BLE経由でWebサイトとデバイス間でのデータのやり取りが可能となってきているため、
データを扱うときの参考になれば嬉しいです。

なお、ここまでの記事は下記をご参考下さい。

はじめに

昨日はBluetooth経由で温度データを受信する際の共通フォーマット及び処理フローを理解しました。
その中で、温度のデータフォーマットである浮動小数点数において
「IEEE-754で定義されているfloat」と「IEEE-11073で定義されているFLOAT」があることが分かり、
本日はその疑問を解消するとともに実際の生データを処理してみたいと思います。

浮動小数点数とは

浮動小数点とは、2進数(0と1)しか扱えないコンピュータの世界において、小数を表現するために定められた方式のことを指します。
小数点の位置を固定せずに表現することが出来るため、浮動小数点と言われており、非常に大きな数から小さな数まで表すことができます。

図に表すと下記のように、「仮数部」 × 「基数部」 ^ 「指数部」の形で表現されます。

仮数_基数_指数.png

詳細は他の記事をご参考に

IEEE-754 と IEEE-1107

IEEE-754の浮動小数点数について

浮動小数点数の計算で最も広く採用されている標準規格です。
基本形式としては、基数が二進形式が3種類、十進形式が2種類の計5種類が指定されています。

形式名 一般名 基数 備考
binary32 単精度 2 符号ビット: 1ビット、指数部: 8ビット、仮数部: 23ビット
binary64 倍精度 2 符号ビット: 1ビット、指数部: 11ビット、仮数部: 52 ビット
binary128 四倍精度 2 符号ビット: 1ビット、指数部: 15ビット、仮数部: 112 ビット
decimal64 十進倍精度 10 -
decimal128 十進四倍精度 10 -

※ binary16の半精度 と decimal32の十進単精度は、交換形式であって基本形式ではないようです

十進倍精度や十進四倍精度などについては調べきれていませんが、
binary32(単精度浮動小数点数)の場合で例にすると下記のようになります。

[binary32] IEEE-754 単精度浮動小数点数.png

IEEE-11073の浮動小数点について

IEEE-11073はパブリックドメインではないため、なかなか調べるのに苦戦しましたが
下記のやりとりから自分なりに解釈してみました。

全ては網羅できませんでしたが、Bluethoothなどで温度をやり取りするためのフォーマットとしては、下記の2種類になるのかなと思っております。

SFLOATの場合(IEEE-11073 16-bit SFLOAT)

  • 16ビット
  • 基数部が10
  • 指数部が4ビット
  • 仮数部が12ビット
  • 仮数部、指数部ともに2の補数で表現

SFLOATの場合(IEEE-11073 16-bit SFLOAT).png

FLOATの場合(IEEE-11073 32-bit FLOAT)

  • 32ビット
  • 基数部が10
  • 指数部が8ビット
  • 仮数部が24ビット
  • 仮数部、指数部ともに2の補数で表現

FLOATの場合(IEEE-11073 32-bit FLOAT).png

結局違いって!?

BLEのFormat Typeに記載されている浮動小数点型を比較して
一番単純に違いを考えるならば基数部が2の場合がIEEE-754で定められた浮動小数点、
基数部が10の場合がIEEE-11073で定められた浮動小数点ということになるかと思います。

また、計算方法としても符号ビットを利用するのか、2の補数を利用するのかとかでしょうか?
(IEEE-754の「十進倍精度」などがこれに当てはまるのかどうかまでは分かっていないですが...詳しい人教えてください...><

計算方法

では実際のデータを扱いながら、それぞれの定義で処理がどのように変わるのかみてみましょう。
なお、IEEE-754の「binary32」や「binary64」については、DateViewクラスのgetFloat32やgetFloat64で扱い可能なので、ここでの詳細は割愛させていただきます。

サンプルデータで検証

とりあえずサンプルデータを確認します。

bit数 メモリ上での表記(16進数表記) 実数値
FLAOT(IEEE-11073 32-bit FLOAT) 32bit 0x69 0x01 0x00 0xFF 36.1
FLOAT(IEEE-11073 32-bit FLOAT) 32bit 0x1F 0x0E 0x00 0xFE 36.15
SFLOAT(IEEE-11073 16-bit SFLOAT) 16bit 0x72 0x00 114

上記の「メモリ上での表記」を与えた場合に、「実数値」が正しく計算されるようなロジックを組んでいきます。

FLAOT(IEEE-11073 32-bit FLOAT)の計算方法

「36.1」を表す「0x69 0x01 0x00 0xFF」というバイナリデータを扱ってみます。

手順1. 2進数の表記を確認

手順1_16進数の値を2進数に変換しメモリ上での管理状態を可視化.png

なおJavaScriptで算出する場合、下記のようにすれば16進数を2進数で表現することが可能です。

// 16進数[0x69 0x01 0x00 0xFF]を2進数に変換する
> (0x690100FF).toString(2);
1101001000000010000000011111111

手順2. バイトオーダーがリトルエンディアンの場合はメモリ上の状態を反転する

これはプロセッサによって変わる部分だとは思いますが、(IEEE-11073で定められているのかまでは分かっていないです...)
今回はリトルエンディアン方式のためメモリ上の並び順を反転します。

手順2_リトルエンディアン表記の場合、メモリの状態を反転する.png

手順3. 指数部を求める

今回は「32ビット」のFLOATであるため、「指数部が8ビット」「仮数部が24ビット」です。
そのため、上位8ビットが指数部となります。

サンプルデータ_FLAOT(IEEE-11073 32-bit FLOAT)の場合.png

上記を元に指数部の値を求めてみます。

手順3_指数部を求める.png

2の補数で表されているため、変換を行った結果、「指数部 = -1」ということがわかりました。

※ 10進数のマイナス値を2の補数に変換するときは、2進数に直す -> ビット反転する -> 1を加える という手順で行うため、2進数から10進数を求める場合はその逆の手順で求める。

手順4. 仮数部を求める

指数部を求めた時と同様ですが、32ビットFLOATの場合「指数部が8ビット」「仮数部が24ビット」なので、
下位24ビットが仮数部となります。

手順4. 仮数部を求める.png

上記計算式より、仮数部は「361」ということがわかりました。

手順5. 数式に当てはめて実数を求める

上記の計算結果より

  • 基数部: 10
  • 指数部: -1
  • 仮数部: 361

361*10^-1.png

となり、「0x69 0x01 0x00 0xFF」というバイナリデータが「36.1」になることがわかりました。

なお、SFLOATの場合はビットの計算位置が違うのみで、
計算の流れはほぼおなじのため割愛します。

JavaScriptで実装してみる

IEEE-11073 32-bit FLOAT型

それでは、上記の処理を実装してみましょう。

/**
 * IEEE-11073 32-bit FLOAT型をバイナリから取得し実数に変換する
 * 
 * @param dv:DataView   32ビットのバイナリデータ
 * @param littleEndian  バイトオーダーの扱い(デフォルトは、fakse = ビッグエンディアン方式:)
 */
function readFLOAT(dv, littleEndian = false) {

  // 1. バイトオーダーを指定して32ビットデータを取得
  var data = dv.getUint32(0, littleEndian);

  // 2. 仮数部を取得する(32ビットの場合は下位24ビットが仮数部)
  //    - 取得した32ビットデータに下位24ビットの全てのフラグを立てた値を掛け算することで取得
  var mantissa = (data & 0x00FFFFFF);

  // 仮数部が2の補数かどうかを判定する
    // 下位24ビットのうちの上位1ビット目にフラグが立っていれば2の補数
  if ((mantissa & 0x00800000) > 0) {
    // 負の値の場合は、2の補数を用いて数値を求める
    mantissa = -1 * (~(mantissa - 0x01) & 0x00FFFFFF)
  }

  // 3. 指数部を取得する(32ビットの場合は上位8ビットが指数部)
  // 上位8ビットを取得するためには、32ビットの場合右へ24ビットシフトさせると取得可能
  // なお、JavaScriptの場合「>>」を利用すると、符号を維持する右シフトであるため、 
  // ビット演算を用いると2の補数も考慮して10進数に変換してくれる
  var exponential = data >> 24;

  // 「仮数部 × 基数部 ^ 指数部」の公式に当てはめて変換
  return mantissa * Math.pow(10, exponential);
}

正しい値が取得できるかチェック

/**
 *  「0x69 0x01 0x00 0xFF」が「36.1」であることを確認する
 */
// 32ビットのバッファ領域を確保
var bufferA = new ArrayBuffer(4);
// 32ビット データを書き込み
var dvA = new DataView(bufferA);
// 「0x69 0x01 0x00 0xFF」を書き込み
dvA.setUint32(0, 0x690100FF);
var littleEndian = true;

var resultA = readFLOAT(dvA, littleEndian);
console.log(resultA);   // 36.1と表示される

/**
 *  「0x1F 0x0E 0x00 0xFE」が「36.15」であることを確認する
 */
var bufferB = new ArrayBuffer(4);
var dvB = new DataView(bufferB);
// 「0x1F 0x0E 0x00 0xFE」を書き込み
dvB.setUint32(0, 0x1F0E00FE);
var littleEndian = true;

var resultB = readFLOAT(dvB, littleEndian);
console.log(resultB);   // 36.15と表示される

IEEE-11073 32-bit SFLOAT型

ロジックは、FLOAT型と同じですがビット数が違ったりするので念のため

/**
 * IEEE-11073 16-bit SFLOAT型をバイナリから取得し実数に変換する
 * 
 * @param dv:DataView   16ビットのバイナリデータ
 * @param littleEndian  バイトオーダーの扱い(デフォルトは、fakse = ビッグエンディアン方式:)
 */
function readSFLOAT(dv, littleEndian = false) {
  // 1. バイトオーダーを指定して16ビットデータを取得
  var data = dv.getUint16(0, littleEndian);

  // 2. 仮数部を取得する(16ビットの場合は下位12ビットが仮数部)
  //    - 取得した16ビットデータに下位12ビットの全てのフラグを立てた値を掛け算することで取得
  var mantissa = (data & 0x0FFF);

  // 仮数部も2の補数で判定するため16ビットのうちの12ビット中の上位1桁がマイナスを表していないかを確認
  if ((mantissa & 0x0800) > 0) {
      // もしマイナス値の場合は、1を引いた後反転処理と12ビット分の論理和を取得する)
      mantissa = -1 * (~(mantissa - 0x01) & 0x0FFF)
  }

  // 3. 指数部を取得する(16ビットの場合は上位4ビットが指数部)
  // 上位4ビットを取得するためには16ビットの場合右へ12ビットシフトさせると取得可能
  var exponential = data >> 12;

  // 「仮数部 × 基数部 ^ 指数部」の公式に当てはめて変換
  return mantissa * Math.pow(10, exponential);
}

正しい値が取得できるかチェック

/**
 *  「0x72 0x00」が「114」であることを確認する
 */
// 16ビットのバッファ領域を確保
var buffer = new ArrayBuffer(2);
// 16ビットのデータを書き込み
var dv = new DataView(buffer);

// 144である「0x72 0x00」を書き込み(なお符号なしで書き込む)
dv.setUint16(0, 0x7200);
var littleEndian = true;

var result = readSFLOAT(dv, littleEndian);
console.log(result);   // 114と表示される

とりあえず上記処理にてサンプルに記載されていた値を入れてみたところ正常な実数が返ってきたのであっているのかなぁと
(仮数部がマイナスのときの動きが若干怪しい気もしていますが...)

ひとまずこれでBLEから取得したデータを読み解くことはできそうなので安心

まとめ

さて、JavaScriptでBLEで定義されている浮動小数点型のデータを扱う場合をまとめると、下記のようになるのかな...

Format short Name Description 計算方法
0x14 float32 IEEE-754 32-bit floating point DataViewクラスのgetFloat32/setFloat32
0x15 float64 IEEE-754 64-bit floating point DataViewクラスのgetFloat64/setFloat64
0x16 SFLOAT IEEE-11073 16-bit SFLOAT 上記計算方法
0x17 FLOAT IEEE-11073 32-bit FLOAT 上記計算方法

[引用元]Format Types - Bluetooth

(※ 間違っていたら指摘下さい...m(_ _)m)

組み込み系のエンジニアの方なら当たり前のことかもしれませんが、
Web系出身のエンジニアからするとあまり馴染みが無いので、とても新鮮でした。
(昔は資格を取得することを目的にIPAの「基本情報」や「応用情報」を勉強しておりましたが、やはり勉強しておいてよかったと今になって思います。)

さて、明日はいよいよ最終回。
実際にBluetoothから受信した生データを利用して、温度を解析してみたいと思います。

参考