BRIGHT VIE Advent Calendar 2017 - Qiita の5日目です!
「JavaScriptでバイナリデータを扱ってみる」シリーズでお届けしておりますが、本日は応用編2回目!
最近では、「Web Bluetooth API」も普及しつつあり、
BLE経由でWebサイトとデバイス間でのデータのやり取りが可能となってきているため、
データを扱うときの参考になれば嬉しいです。
なお、ここまでの記事は下記をご参考下さい。
はじめに
昨日はBluetooth経由で温度データを受信する際の共通フォーマット及び処理フローを理解しました。
その中で、温度のデータフォーマットである浮動小数点数において
「IEEE-754で定義されているfloat」と「IEEE-11073で定義されているFLOAT」があることが分かり、
本日はその疑問を解消するとともに実際の生データを処理してみたいと思います。
浮動小数点数とは
浮動小数点とは、2進数(0と1)しか扱えないコンピュータの世界において、小数を表現するために定められた方式のことを指します。
小数点の位置を固定せずに表現することが出来るため、浮動小数点と言われており、非常に大きな数から小さな数まで表すことができます。
図に表すと下記のように、「仮数部」 × 「基数部」 ^ 「指数部」の形で表現されます。
詳細は他の記事をご参考に
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(単精度浮動小数点数)の場合で例にすると下記のようになります。
- 参考
IEEE-11073の浮動小数点について
IEEE-11073はパブリックドメインではないため、なかなか調べるのに苦戦しましたが
下記のやりとりから自分なりに解釈してみました。
- 参考
全ては網羅できませんでしたが、Bluethoothなどで温度をやり取りするためのフォーマットとしては、下記の2種類になるのかなと思っております。
SFLOATの場合(IEEE-11073 16-bit SFLOAT)
- 16ビット
- 基数部が10
- 指数部が4ビット
- 仮数部が12ビット
- 仮数部、指数部ともに2の補数で表現
FLOATの場合(IEEE-11073 32-bit FLOAT)
- 32ビット
- 基数部が10
- 指数部が8ビット
- 仮数部が24ビット
- 仮数部、指数部ともに2の補数で表現
結局違いって!?
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進数の表記を確認
なおJavaScriptで算出する場合、下記のようにすれば16進数を2進数で表現することが可能です。
// 16進数[0x69 0x01 0x00 0xFF]を2進数に変換する
> (0x690100FF).toString(2);
1101001000000010000000011111111
手順2. バイトオーダーがリトルエンディアンの場合はメモリ上の状態を反転する
これはプロセッサによって変わる部分だとは思いますが、(IEEE-11073で定められているのかまでは分かっていないです...)
今回はリトルエンディアン方式のためメモリ上の並び順を反転します。
手順3. 指数部を求める
今回は「32ビット」のFLOATであるため、「指数部が8ビット」「仮数部が24ビット」です。
そのため、上位8ビットが指数部となります。
上記を元に指数部の値を求めてみます。
2の補数で表されているため、変換を行った結果、「指数部 = -1」ということがわかりました。
※ 10進数のマイナス値を2の補数に変換するときは、2進数に直す -> ビット反転する -> 1を加える という手順で行うため、2進数から10進数を求める場合はその逆の手順で求める。
- 参考:2の補数について
手順4. 仮数部を求める
指数部を求めた時と同様ですが、32ビットFLOATの場合「指数部が8ビット」「仮数部が24ビット」なので、
下位24ビットが仮数部となります。
上記計算式より、仮数部は「361」ということがわかりました。
手順5. 数式に当てはめて実数を求める
上記の計算結果より
- 基数部: 10
- 指数部: -1
- 仮数部: 361
となり、「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から受信した生データを利用して、温度を解析してみたいと思います。