« Arduino 10bit A/D値をmap関数でスケーリングする例 | トップページ | 基板修理の広告にこの素材はマズイやろ »

2020年5月17日 (日)

Arduino なんとかして誤用を正したい:A/Dの1/1023とmap関数

ちょっとまとめてみました。  (関連記事へのリンクも注目)
コメント歓迎!!  ご意見、お待ちしています。

==========================

◆A/Dコンバータのスケーリング方法

Arduino-UNOの内蔵A/Dコンバータは10bit。
0x0000~0x3FF:0~1023の数値が出てくる。
この値から、
  入力電圧値を求める時
  温度センサーなどのセンサー情報を処理する時
  AnalogWriteなどに値を渡す時
などの処理でのミスが目立つ

●基本的な知識:A/D変換データの意味

10bitのA/D変換値(ADC)と入力電圧(Vin)、そして基準電圧(Vref)
は以下の関係がある。
   ADC = (Vin / Vref) * 1024
   Vin = (ADC * Vref) / 1024
つまり10bit ADCの最大値1023は、Vref電圧より「-1LSB」だけ
小さい入力電圧(以上)の時に発生する。
変換値1023はVrefではない。
「Vref - 1LSB」の入力電圧、つまり「Vref * 1023 / 1024」が
ADC=1023での入力電圧となる。

結論:ADCからVinへのスケーリングは「1/1024」で計算しなけ
   ればならない。
   「1/1023」は間違いである。

※関連
2020年1月8日:ミスが広まる 1/1023 vs 1/1024


●基本的な知識:Vref電圧の実値

A/D変換器のVrefはAVCC電源(5V)、内蔵基準電圧(1.1V)、外部
基準電圧入力(AREF)に切り替えできる。

A/D変換を使って、入力電圧や温度センサーの測定値を求める
ときなどは、Vref電圧の実値が重要である。
「電源電圧 = 5V、内蔵基準電圧 = 1.1V」と決め打ちするのは
危険である。
A/D変換後、電圧や温度といった数値に直した値を評価する
時は、実際の電圧を測定し、その値をVref値とすべきである。

例えば・・・内蔵基準電圧=1.1Vは1.0V(min)~1.2V(max)と
データシートで規定されていて、1.1Vと決め打ちできないのは
あきらかである。
5Vや1.1Vで決め打ちして計算した電圧や温度、これを評価する
ときは、Vref値の誤差を含んでいるとの注意書きが必須である。
これを記していない記事は・・・「ちょっとなぁ~」

※関連
2019年3月22日:Arduinoのアナログ基準電圧入力


●間違った知識:map関数

map関数を使ってスケーリングを行う処理でのミスが目立つ。
mapは2点間の数値を元に線形補間を行う。

例えば、10bitのA/D値を8bit値に変換する時:analogReadして
analogWriteするような時、(最大値1023を255に変換する)この
ような記述を見かける。
   y = map(x, 0, 1023, 0, 255);

もともと、10bit→8bitの変換は、値を1/4するだけである。
  (四捨五入の話は別問題として)
この場合の補間式は 「y = 0.25x + 0」で、傾き1/4の線上
で 「x → y」の変換が行われる。
先ほどの「0, 1023, 0, 255」だと傾きが「255/1023」となり
本来の「256/1024」の補正線から外れる。
  256/1024 → 0.25
  255/1023 → 0.249266…

map関数の定義で、
  map(value, fromLow, fromHigh, toLow, toHigh)
   value:   変換したい数値
   fromLow: 現在の範囲の下限
   fromHigh: 現在の範囲の上限
   toLow:   変換後の範囲の下限
   toHigh:  変換後の範囲の上限
上限、下限という表記があるのでanalogRead、analogWrite
の最小、最大値を記入していると推測できる。
しかし、10bit→8bitの変換の時にその最大値を用いるのは
正しくない。

例えば、
   y = map(x, 0, 1023, 0, 255);
の時、10bitの半値である512を代入すると127が返ってくる。
正しい値は8bitの半値=128である。
また1LSB多い1024を代入しても256とならず255となってしまう。

※参考
  ・2019年4月3日:線形補間って「LERP」って言うんだ!

以下のように考えてもこれがミスであることがわかる。
この考えで8bit→10bitの変換をすると、
   y = map(x, 0, 255, 0, 1023);
と書くことになる。
これで「x=255」を変換すると「y=1023」が得られる。
一見正しいようだが(式どおりの答え)、当たり前に考えて255の
4倍である「1020」が正しい値である。

正しくは、
 10bit→8bit変換  y = map(x, 0, 1024, 0, 256);
 8bit→10bit変換  y = map(x, 0, 256, 0, 1024);
である。

10bit→8bit変換では以下の書式でも正しい値が得られる。
  y = map(x, 40, 1000, 10, 250);
「0,1024,0, 256」の直線の傾き=0.25と、オフセット=0は同じ
である。

※余談
本来のスケーリング処理は下限上限という意味ではなく、
2点の表示値(A/D値の読み)と、実際の測定値(外部電圧計など
での読み)というふうに、キャリブレーションをおこなった
結果の補正に用いる。
A/D変換回路に付随する回路、例えば基準電圧値の誤差や前置
アンプのゲイン誤差やオフセット誤差、これらを補正するた
めに線形補間を行う。

※以下の例題がおかしいのが根本原因か
http://www.musashinodenpa.com/arduino/ref/index.php?f=0&pos=2743
https://www.arduino.cc/reference/en/language/functions/math/map/

※関連
2020年5月16日:Arduino 10bit A/D値をmap関数でスケーリングする例


※追記 2020-05-19
10bit→8bitの変換では数値が大きくて誤差が見えにくい。
そこで、極端な例として、3bit→2bitへの変換を考えてみる。
 map(x, 0, 7, 0, 3) …<a>
 map(x, 0, 8, 0, 4) …<b>

<a>がよく出ている例題「0,1023, 0,255」での方法。
変換前と変換後の下限値・上限値を指定している。

<b>が補間直線の傾きを考えて記したもの。
3bit→2bitなんで単純に0.5。 2で割れば良い。

結果は、
入力値 <a> <b>
 0   0  0
 1   0  0
 2   0  1
 3   1  1
 4   1  2
 5   2  2
 6   2  3
 7   3  3
----------------
 8   3  4
 9   3  4
 10   4  5
 11   4  5
 :    :
 16   6  8
 24   10  12
 32   13  16
 40   17  20

mapでは最大値を超えても変換結果が出てくる(線形補間だから)
ので、8以上の値を入れるとミスしているのが良く分かる。
また、四捨五入の問題でもないことが見える。

線形補間の式であるmap関数、「map(x, 0,1023, 0,255)」は
10bit→8bit変換におて根本的に間違った使い方をしていること
がわかっていただけたであろうか。


※さらに追記

英文のArduino Reference
https://www.arduino.cc/reference/en/language/functions/math/map/
を見てみると、
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Notes & Warnings

As previously mentioned, the map() function uses integer math.
So fractions might get suppressed due to this. For example, fractions
like 3/2, 4/3, 5/4 will all be returned as 1 from the map() function,
despite their different actual values.
So if your project requires precise calculations (e.g. voltage accurate
to 3 decimal places), please consider avoiding map() and implementing
the calculations manually in your code yourself.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
なんて書かれている。
google翻訳にかけると・・・
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
前述のとおり、map()関数は整数演算を使用します。
そのため、これにより分数が抑制される可能性があります。
たとえば、3 / 2、4 / 3、5 / 4などの分数は、実際の値が異なっ
ていても、すべてmap()関数から1として返されます。
したがって、プロジェクトで正確な計算が必要な場合(小数点以下3桁
まで正確な電圧など)は、map()を避け、手動でコードに計算を実装
することを検討してください。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
文末の「・・・yourself.」:自分でなんとかしろ!
っと言っているよう・・・

mapは悪くない。
線形補間を32bitの整数で実行している。
mapに与える数値をミスっているのが問題。

日本語解説で、下限・上限と記されているが
英文ではthe lower boundとthe upper bound 。
下限・上限としか言いようがないか・・・
数学的な言い回しだとどうなるんだろうか。

================================
それと、もうちょい。
  map(x, 0,1023, 0,255)  ・・・これが正しい例を紹介

これが、例えばA/D変換器のキャリブレーション結果で、
A/Dの値が「1023」の時にその入力電圧をテスターで計っ
てみたら「255.0mV」だった。  (精度表現のためあえて.0を追記)
この場合の変換式はこれで合っている。

このA/Dコンバータが実は10bitではなくもっと桁数が大きくって、
例えば12bitの分解能だったとする。
電圧を4倍の「1020.0mV」に上げてみると、A/D値も4倍になり
「4092」という測定結果になるはずである。(4095ではなく)
この場合のスケーリング値は「255/1023」で正しい

10bit→8bitの変換、つまり数値を単純に1/4するだけの計算で
この値を使っているからおかしいわ
けだ。

================================
  以下はコメントに対するお答え
     2020年5月17日 (日) 16時40分 タナカ さん

※map関数の中で二つの上限値を+1なんて処理をしたら、本来の
線形補間ができなくなっていまう。
mapのコードそのものは変更しなくてよい。
「下限・上限」というから間違うのか。
重要なのは変換直線の傾きとオフセット。
それを決める2点のX,Y位置。
この一次関数って、中学生の問題?
しっかりしろエンジニア!

================================
※追記 コメントに書いたが本文にも載せておく。

mapが浮動小数点を許すのなら・・・
map(x, 0, 1023.0, 0, 255.75)で解決だ。

この補正値は256.0/1024.0と同じ0.25。

================================

■関連記事ピックアップ:居酒屋ガレージ日記から

2020年5月2日:Arduino 放置したポートが及ぼす電源電流変化
2020年4月25日:Arduino やっぱり気になる放置ポート
2020年2月10日:ArduinoのanalogWrite 1/255なの?
2020年2月4日:Arduinoのタイマー OCRレジスタは「n」じゃなく「n - 1」の値を設定せよ
2013年04月25日:AVRマイコンのAREFピン
2013年05月02日:AVRマイコンのAREFピン #2

 

|

« Arduino 10bit A/D値をmap関数でスケーリングする例 | トップページ | 基板修理の広告にこの素材はマズイやろ »

Arduino」カテゴリの記事

コメント

いつも拝見しています。
これ、本家でも2014年に問題として提起されて、たまに議論されつつも今もそのままになってますね。

https://github.com/arduino/ArduinoCore-API/issues/51

ここの真ん中くらいのコメントにあるmapPlus1()の実装が、ご提案の方法に似てますでしょうか。あちらは関数の中で+1してるようです。

投稿: タナカ | 2020年5月17日 (日) 16時40分

タナカさん、情報どうも。

map関数内の問題じゃなく、使い方の問題です。
もともとは、10bit値を1/4すれば良い話。
スケーリング処理は、傾き0.25の数直線でおわり。
四捨五入や入出力数値制限を絡ませる問題じゃありません。

一つ前の書き込みでは「浮動小数点ではどや?!」を確かめました。

ただ・・・analogWriteにはこういう問題があります。
http://igarage.cocolog-nifty.com/blog/2020/02/post-d4c821.html
これとmap関数の使い方とをごっちゃにするとややこしい。

この問題提起、よく見つけてくださりました。
「スケッチ例が悪い」が結論かと。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月17日 (日) 17時24分

いつからArduinoを使い始めたんだと調べてみたら、2012年12月。
http://act-ele.c.ooco.jp/blogroot/igarage/article/3192.html
この記事↑にコメントいただいたラジオペンチさんの予言どおり
「・・・こんなもん使うくらいならスケッチ書き込んだCPUを引っこ抜き、別のユニバーサル基板に挿した方が手っ取り早い・・・」
まさにこれ。
ユニバーサル基板にブートローダーを焼いたATmega328Pを載せて、CPUを引っこ抜いたArduino基板からRESET・RXD・TXD・GNDの4線を引っ張り出して書き込み。
こんな工作ばかり。
Arduino-UNOを買ったのは、ほんとこれ1回だけ。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月18日 (月) 08時45分

map関数に関してあれこれ追記。
広く出回っている「map(x, 0,1023, 0,255)」、
これをなんとかして欲しい。

私よりもっと数学的センスのある人の一言が欲しい!

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月19日 (火) 10時02分

コメント先:Arduino 入門 番外編 17 【map関数 まとめ】 | おもろ家
https://omoroya.com/arduino-extra-edition-17/#comment-551

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月19日 (火) 20時17分

この話、ADコンバーターの量子化誤差を入れて考えると結論が違ってくるような気がします。

ここで前提としている考えているADコンバーターの出力はアナログの真値に対し 0~-1LSB の誤差がある、つまり平均で -1/2LSBの誤差(オフセット)があるものとして考えられていると思います。

そういうADコンバーターもあるでしょうが、多くの場合、というか理想ADコンバーターの量子化誤差は±1/2LSBで平均誤差はゼロではないかと思います。それは置いておいて、

map関数は線形補間なのでそういう事情を無視して計算するので話がややこしくなっている気がします。

別の言い方をすると、10ビットから8ビットに分解能を落とす場合、下位2ビットを捨てると誤差の平均値が増える、つまり-1/2LSBの誤差から-2LSBの誤差に拡大してしまいます。
それはそういうものだと考えてしまえば良いのですが、map関数は両端を通る直線で計算するので誤差の平均値を増やさないように計算するのではないかと思います。

つまり、map(x, 0, 1023, 0, 255) でも間違ってはいないのではないでしょうか。

投稿: ラジオペンチ | 2020年5月20日 (水) 08時09分

10bitの半値の512が8bitで127になってしまうというのは、量子化誤差の積み上げでしょうか?
  ※もともと整数計算が前提の関数ですんで。
また、追記の例で示しました、3bit→2bitではいかがでしょうか。
補正直線の傾きの差がすべてかと。

追記しましたように、A/D値と実値のキャリブレーション操作のように、map(x,0,1023,0,255) でもって正しい値が算出できる例を示しています。
ただ、10bit→8bitで(0,1023,0,255)はおかしいぞということです。。

mapそのものは正しいのです。
その使い方、数字の入れ方が???というお話し。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月20日 (水) 09時36分

mapが浮動小数点を許すのなら・・・
map(x, 0, 1023.0, 0, 255.75)で解決です。

答えも浮動小数点になるでしょうから、それを切り捨てるなり四捨五入するなりご自由にということで。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月20日 (水) 09時50分

この記事では3つのことについてウダウダ言ってます。
その中で、Vrefとmap。
これをうまくまとめれば、ArduinoのA/Dコンバータのキャリブレーション例が解説できます。
・用意するもの
 デジタル電圧計と10kΩくらいのボリューム
・接続
 ボリュームの1端子をGNDに3を5Vに。
 2端子をアナログ入力と電圧計の+に。
 電圧計-はGNDに。
・スケッチ
 アナログ入力してそのA/D値(0~1023)をシリアル出力。
 map関数での変換値もいっしょに。
 例えば、A/D値1000のときの電圧計の読みが4.90Vだったとすると、
   map(0, 1000, 0, 490)と。

これで個別Vrefでのスケーリングが完了。
  ※Vrefは5.0176Vと推定できる。

Vrefを計らなくてもA/D値から入力電圧を実値に直せる。
これこそmapの使い方。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月20日 (水) 10時39分

「map」に恨みを持ってるわけじゃありません。

A/Dの「1/1023」問題を調べていたら、知らなかった関数「map」に出くわしたのです。
mapって関数、普通の「C」にはありませんので。
Arduinoで初体験。

そしたら「なんか違うぞ」の匂いがしてきたわけです。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月20日 (水) 12時03分

もっと極端に 10bit→1bitなら・・・
例題の書式なら map(x, 0, 1023, 0, 1)
そして     map(x, 0, 1024, 0, 2)
答え0と1の分かれ目は・・・

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月20日 (水) 12時13分

map(x, 0, 1024, 0, 2)とやると、綺麗に512で切り替わって気持ち良いですね。

あと、あれこれ数字を入れて遊んでいたのですが、xの値がゼロをまたぐ、つまりマイナスからプラスに変化する時に、戻り値がゼロに引っかかちゃうんですね。
mapの戻り値は前方と後方で滑らかに値が変化すると思ってたのでちょっと意外でした。
C言語ってこういう定義なんでしたっけ、うーん勉強して出直します。

投稿: ラジオペンチ | 2020年5月20日 (水) 17時57分

mapの使用例で感じた怪しい匂い。
それをまとめておきます。
  ※A/Dの1/1023問題を検索するまでmap関数なんて知らなんだ、
   というのが実情。

「Arduino 日本語リファレンス」のmap関数。
ここで示されている最初の2例。
  範囲の下限を上限より大きな値に設定できます。
  そうすると値の反転に使えます。
  例 y = map(x, 1, 50, 50, 1);

  範囲を指定するパラメータに負の数を使うこともできます。
  例 y = map(x, 1, 50, 50, -100);

これらはこれでok。そういう変換だから。

ところが3例目のこれ。
目的が『アナログ入力の10ビットの値を8ビットに丸めます。』

この処理に「val = map(val, 0, 1023, 0, 255);」
としているもんだから・・・

「ちょっと待て。 単純に1/4するだけやで。」
「A/Dの1/1023問題と同じ匂いやん」
「みんな、なんで1023が好きやねん?!」
っと。

検索したらこの3例目を使った変換処理があちこちに。
補正線の傾き、たいていの場合、分母が1023になっていて、正しい1024とは1違うだけなんで、式としての狂いはわずか。
それでも、半値の512で検証すれば、なんかおかしいっとなるはずなんだけど、わずかな違いなんで整数計算の誤差の範囲かっと見逃されている感じ。

結局のところ、A/Dの1/1023問題と同じじゃないかという匂いがしているのです。

※A/Dの1/1023、ミスが広まる 1/1023 vs 1/1024
http://igarage.cocolog-nifty.com/blog/2020/01/post-a02d3f.html
を見てもらったせいなのでしょうか、訂正されているページがあります。
ラジオペンチさんのページのように、その理由を書いておいてもらえればありがたいです。
http://radiopench.blog96.fc2.com/blog-entry-972.html
「これは恥ずかしいミスだっ」とを広めたい。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年5月22日 (金) 10時03分

こんにちは。
「おもろ家」管理人のomoroyaです。
コメントありがとうございました。
内容一読させていただきました。
それを受けて下記記事を見直し更新しました。

Arduino 入門 番外編 17 【map関数 まとめ】 | おもろ家
https://omoroya.com/arduino-extra-edition-17/

投稿: omoroya | 2020年6月 7日 (日) 14時12分

omoroyaさん、記事のリライトありがとうございます。
3bit→2bit変換を例にしてもらって、ちょっとうれしい・・・っと。

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年6月 8日 (月) 11時13分

omoroyaさん、牛乳のお話しにコメントしました。
長期保存可能 大阿蘇 牛乳 感想
https://omoroya.com/milk-stored-long-time/

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年6月 8日 (月) 12時13分

★書籍でもやってる「1/1023」と「map(x, 0, 1023, 0, 255)」
http://igarage.cocolog-nifty.com/blog/2020/09/post-e67167.html

投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年9月 9日 (水) 09時24分

コメントを書く



(ウェブ上には掲載しません)




« Arduino 10bit A/D値をmap関数でスケーリングする例 | トップページ | 基板修理の広告にこの素材はマズイやろ »