Arduinoと戯れてみる!

Arduino 入門 番外編 17 【map関数】

arduino-extra-edition-17-00Arduino番外編
スポンサーリンク

Arduino 入門
番外編 17
【map関数 まとめ】

こんにちは管理人のomoroyaです。

arduino 入門 番外編はarduinoの基本的なことを解説している記事です。

修正
map関数の解説を修正しました。

 

本記事は、Arduinoで使用するmap関数についてです。

管理人の中では、Arduinoで遊んでいくためには必ず理解しておく関数と考えています。

入門編では、サーボモーターの制御などで使用した関数です。

 

 

map関数は数値をある範囲から別の範囲に変換する関数です。

Arduinoは10bitのADコンバータ(アナログーデジタル変換)を搭載しています。

これにより、0V~5Vが入力されると0~1023の数値に変換できるようになっています。

その時に使用する関数はanalogRead(pin番号)でしたね。

ちなみに、「10bit = 2^10」で計算できます。

2^10 = 1024種類(0~1023)

 

どういったときに使うのか?

「入力された電圧値をデジタル信号に変換して割付し直す」

といったことに使います。

アナログの電圧値をデジタル値に変えて都合の良いように割付し直すということです。

 

0~1023の数値をそのまま使えるかどうかは受け側の処理で異なります。

受け側が8bitの分解能しかない場合もありますし、任意の値を割り付けたいといったこともあります。

そんなときに便利なのがmap関数です。

入門編では、スケッチに組み込まれてしまっていて本当に変換されてるのかどうか体感できませんよね?

 

そこで、本記事は以下をやってみます。

  1. シリアルモニタに数値を入力。
  2. map関数で変換された数値を表示させる。

といったスケッチを描くことで数値が変換されることを認識します。

 

管理人はプログラマーの専門家というわけではありません。

気が向いたときにテキストを開き、ネットを調べ自己学習。

管理人にとってはブログ記事が自己学習のノートです。

書くことによって頭の中が整理できる。

何よりも、忘れたときに見返せるってのが良い。

 

arduino自身のこと、スケッチ(コード、プログラム)を少しづつ理解して行きましょう。

いやいや、arduinoを早速始めたいんだ!というかたは下記の入門編からお読みください。

 

Arduino入門編の解説にて使用しているArduinoは互換品です。

互換品とは言え、Arduinoはオープンソースであり複製して販売するのもライセンス的に問題なし。

そのため互換品の品質も悪くなく、それでいて値段は安いです。

正規品本体の値段程度で豊富な部品が多数ついています。

 

正規品の本体単品がほしい方はこちらとなります。

 

スポンサーリンク

map関数 基本解説

まずは基本的なことを復習。

繰り返しになりますが、「数値をある範囲から別の範囲に変換する」ための関数です。

関数説明
map(value,fromLow,fromHigh
,toLow,toHgih)
value:変換したい変数
fromLow: 現在の範囲の下限
fromHigh:現在の範囲の上限
toLow:変換後の範囲の下限
toHigh:変換後の範囲の上限
例 アナログ入力の0~1023をサーボの0~180に変換。
val = map(val,0,1023,0,180);

 

解説を追記しておきます。

valueがfromLowと同じ値の場合:戻り値はtoLow
valueがfromHighと同じ値の場合:戻り値はtoHigh
valueがfromLowとfromHighの間の値の場合:2つの範囲の大きさの比に基づいて計算

この関数は範囲外の値でも機能します。
範囲に必ず収めたい場合はconstrain関数と併用します。
map関数で使用できるのは整数のみ。
計算結果が少数となる場合は数部分は切り捨てられます。

 

応用編1:数値の反転に使用することも可能

val = map(x, 1, 100, 100, 1);

とすると「1が100、2が99、・・・100が1」という戻り値になります。

 

応用編2:指定範囲にマイナスを指定することも可能

val = map(x, 1, 100, 50, -50);

とすると「1が50、2が49、・・・100が-50」という戻り値になります。

 

map関数 サンプルスケッチ

シリアルモニタに値を入力、map関数で変換した値をシリアルモニタに出力するスケッチです。

簡単にできるかと思いつつ、実は簡単ではありません・・・。

map関数の処理は簡単です。

しかし、シリアルモニタの処理がとても大変!

 

なぜか?

シリアルモニタでのやり取りは、文字でのやり取りになります。

いわゆるASCIIコードです。

しかもですよ、基本的にキャラクター型の1バイトずつでしかやりとができない。

そう、1文字単位でのやり取りなんです。

例えば、シリアルモニタに123といれても、1と2と3が1文字ずつ送られるってことです。

数桁の数字を送るためにはスケッチに工夫が必要です。

 

大きく処理は2つ。

1.ASCIIコードを数値に変換
ASCIIコードの「0」は10進数で48、16進数で0x0030、char型で’0′
※1文字をシングルクオーテーションで囲うとchar型の表記です。
上記から10進、16進、char型のいずれかで計算してあげます。
10進数で計算する場合は、48を引く。
16進数で計算する場合は、0x0030を引く。
char型として計算する場合は、’0’を引く。
いずれかの計算で「0」になります。
2.桁数を認識させて変換
桁数を認識させるのも手間です。
例えばですが、123と入力してシリアルモニタで送信する。
最初の1は100倍、2は10倍、3はそのままで全ての和を取る。
といった処理が必要になります。

map関数のお勉強が違うお勉強に・・・。

map関数よりASCIIコードの変換の方が大変でした。

 

スケッチに通信用の関数を使いますので念のため説明をのせておきます。

関数説明
Serial.begin(speed)シリアル通信のデータ転送レートをbpsを指定します。
bpsはビット/秒。
speed:転送レート
転送レートは下記が指定可能です。
300, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200
戻り値:なし
Serial.available()シリアルポートにデータがあるかどうかの確認をする。
戻り値:シリアルバッファにあるデータのバイト数
Serial.read()受信データの読み込み。
戻り値:
読み込み可能なデータの最初の1バイトを返す。
-1の場合はデータなし。
Serial.print(data,format)ASCIIテキストでシリアルポートにデータを出力する。
data:出力する値
format:基数または有効桁数(BIN,DEC・・・など)
戻り値:送信したバイト数
Serial.println(data,format)データの末尾に改行を入れて送信。
Serial.print()と同じフォーマットが使えます。

 

下記が、map関数をシリアルモニタで確認するためのサンプルスケッチです。

スケッチ内のdelay(100);が肝です。

9600bpsのシリアル通信で64byteぶんのデータを読み込むための時間を計算。

多少マージンを取って100ms待つことでデータを取得完了できると判断。

これによりASCIIコードの複数文字を桁数のある数字として認識させる処理ができています。

 

サンプルスケッチ修正
val1 = map(sum, 0, 1023, 0, 255); //10bitを8bitに変換 ←間違い
val1 = map(sum, 0, 1024, 0, 256); //10bitを8bitに変換 ←正しい

 

//番外編 17 map関数
//map関数の変換を確認するサンプルスケッチ
//https://omoroya.com/

long sum = 0;
long val1=0, val2=0, val3=0;

void setup() {
  Serial.begin(9600); //データ転送レートを指定
}

void loop() {
  
  bailout:

  //受信データがある場合if内を処理
  if (Serial.available() > 0){
    delay(100);                             //64byteぶんのデータを受信できるように100ms待機
    byte data_size = Serial.available();    //受信したデータ数を取得(桁数取得のため)
    byte num[data_size];                    //桁数分の配列を設定
    Serial.print("Number of digits =");
    Serial.println(data_size);              //桁数の表示(動作確認のため)

    //for文内で1文字ずつのASCIIコードを数値に変換
    for (byte i = 0 ; i < data_size ; i++)
    {
      char val = Serial.read();             //受信データの読み込み 1byteぶん

      //char文字valが数字の場合はif文内処理、そうでない場合else内処理
      if (isdigit(val)){
        num[i] = val - '0';                 //ASCII文字を数値に変換
        sum = sum*10 + num[i];              //桁数を考慮して数値に変換
      } else {
        Serial.println("Please enter a number.");
        Serial.println("");
        goto bailout;                       //valが数字でない場合bailoutへ抜ける
      }
      
    }
    
    val1 = map(sum, 0, 1024, 0, 256);    //10bitを8bitに変換
    val2 = map(sum, 0, 1023, 1023, 0);   //反転変換
    val3 = map(sum, 0, 1023, 0,-1023);   //マイナス変換
    
    Serial.print("input value : ");
    Serial.println(sum, DEC);            //入力した値を表示
    Serial.print("10bit->8bit : ");
    Serial.println(val1, DEC);           //10bitを8bitに変換した値を表示
    Serial.print("reverse     : ");
    Serial.println(val2, DEC);           //反転変換した値を表示
    Serial.print("minus       : ");
    Serial.println(val3, DEC);           //マイナス変換した値を表示
    Serial.println("");
    
    sum = 0;                             //数値をリセット
  }
  
}

 

map関数 サンプルスケッチ 実行結果

「ツール」 ⇒ 「シリアルモニタ」 を開きます。

適当な数値を入力してリターンもしくは送信ボタンを押せば結果が表示されます。

数値以外を入力すると「Please enter a number.」と表示されます。

 

下図がシリアルモニタの実行結果。

シリアルモニタは「改行なし」を選択してください。※下図赤枠

改行なしを選択しないと改行も入力値として認識してしまいます。

 

下図は0、1023、512、256、128、64、32を順次入力した結果です。

arduino-extra-edition-17-01

 

map関数が機能しているのがわかりましたでしょうか。

 

map関数 10bit → 8bit 変換問題

ご指摘を受けまして、map関数のbit変換問題について追記します。

居酒屋ガレージ店主さんありがとうござました。

 

10bitを8bitに変換するということは、1024個のデータを256個のデータに変換するということになります。

要するに「256個/1024個 = 1/4」にすればよいだけであり、比を0.25にするということです。

map()関数はただの数値変換(線形補間)の関数のため、使い方に注意が必要になります。

2つの範囲の大きさの比に基づいて計算する関数。←ここが重要!

 

ここで10bit、8bitをを数値で考えてみます。

10bit:0~1023(1024個のデータ)
8bit:0~255(256個のデータ)

上記のように、最大値は1023、255であるためmap()関数でbit変換をする場合、下記と記述したくなるはずです。

val1 = map(sum, 0, 1023, 0, 255); //10bitを8bitに変換 ←間違い

これが、大きな間違いのもとでしてmap関数はあくまでも「2つの範囲の大きさの比に基づいて計算する関数」です。

考えるべきは、比の値であって、最大値の数値に合わせることではありません!

 

ここで、map()関数の計算を確認。

単純にmin、maxの値から比を計算していることがわかります。

long map(long x, long in_min, long in_max, long out_min, long out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

 

この式に1023と255を当てはめてみると・・・

= (x – 0) * (255 – 0) / (1023 – 0) + 0
= (x – 0) * 255 / 1023
= (x – 0) * 0.2492・・・

比の値が、255/1023 = 0.2492・・・となり0.25になっていません。

求めるべき比の値が間違っています。

 

では、0.25にするためにはどうしたらよいか?

下記のように記述すればよいわけです。

val1 = map(sum, 0, 1024, 0, 256); //10bitを8bitに変換 ←正しい

これをmap()関数の計算に当てはめると以下となります。

= (x – 0) * (256 – 0) / (1024 – 0) + 0
= (x – 0) * 256 / 1024
= (x – 0) * 0.25

求めたい比の0.25になりました。

 

map()関数のbit変換で大事なことは、データの個数がいくつか?ということです。

数式を見てわかるように、求めるのは比であるということに注意が必要です。

 

■ここからは余談・・・

ここで1つ気づくのです。

10bitから8bitの変換、比を計算しているだけということは下記に示す1と2はどちらも同じということ。

  1.  map(sum, 0, 1024, 0, 256);  //10bitを8bitに変換
  2.  map(sum, 0, 4, 0, 1);      //10bitを8bitに変換

 

さらに、わざわざmap()関数を使わなくても、読み取った値を1/4倍して小数点以下を切り捨てれば同じこと・・・。

例えば、下記。

読み取った値はval変数に入っているとする。

val変数の値を10bitから8bitに変換してval1に入れることを考える。

小数点以下を切り捨てるためval、val1はlong型で定義しておく!

val1 = val * 0.25;

 

単純にbit変換するだけならmap()関数不要です。

 

bit変換実際の例 3bit -> 2bit

簡単のため3bit -> 2bit変換で考えてみます。

3bit、2bitを数値であらわすと下記となります。

3bit:0~7(8個のデータ)
2bit:0~3(4個のデータ)

先ほど解説した通り、求めるのは比であるためデータの個数で考えます。

求める比は
4 / 8 = 0.5
となります。

 

3bit -> 2bitのmap()関数の変換をシリアルモニタに表示するサンプルスケッチで確認していきます。

本Lesson解説のシリアルモニタの使い方とは異なり簡単なスケッチになっています。

このサンプルスケッチのシリアルモニタ部分の解説は以下を確認ください。

 

ではさっそく確認していきましょう。

下記サンプルスケッチのmap()関数について2つの場合を確かめてみます。

試したい方のコメントアウトを消して実行してみてください。

//val1 = map(val, 0, 7, 0, 3); //3bitを2bitに変換 <-間違い
//val1 = map(val, 0, 8, 0, 4); //3bitを2bitに変換 <-正しい

 

//番外編 17-2 map()関数のお勉強
//map()関数の結果をシリアルモニタに表示するサンプルスケッチ
//https://omoroya.com/

long val1=0;

void setup() {
  Serial.begin(9600); //データ転送レートを指定
}

void loop() {

  //受信データがある場合if内を処理
  if (Serial.available() > 0){
    long val = Serial.parseInt();    //文字列データを数値に変換
    Serial.print(val);               //valを表示
    Serial.print(" ");               //スペース
    //val1 = map(val, 0, 7, 0, 3);   //3bitを2bitに変換 <-間違い
    //val1 = map(val, 0, 8, 0, 4);   //3bitを2bitに変換 <-正しい
    
    Serial.print("3bit->2bit : ");
    Serial.println(val1, DEC);       //3bitを2bitに変換した値を表示
  }
  
}

 

■結果がこちら

左が正しい指定である「map(val, 0, 8, 0, 4); 」を指定した場合。

右が間違いとなる「map(val, 0, 7, 0, 3); 」を指定した場合。

8個のデータを4個に割り当てるので比は0.5であり、右側の結果が明らかにおかしいことがわかります。

arduino-extra-edition-17-02a

 

まとめ

Arduino 入門 番外編 17 【map関数 まとめ】

いかがだったでしょうか?

 

map関数の学習のつもりがシリアル通信用の関数をお勉強することに・・・。

本記事で記載したASCIIコードを数値に変換、桁数の変換、色々使えそうな気がします。

せっかくですので、数値変換、桁数変換などを関数化していつでも使えるようにしたいと考えます。

 

番外編 18はこちら。

数値変換、関数化しなくても簡単に文字列を数値に変換できる関数がありましたというお話です。

サンプルスケッチも記載しています。

こちらも併せてお読みください。

 

最後に

疑問点、質問などありましたら気軽にコメントください。

この電子部品の解説をしてほしい!などなどなんでもOKです。

リンク切れ、間違いなどあればコメントいただけると助かります。

 

Arduino入門編、番外編、お役立ち情報などなどサイトマップで記事一覧をぜひご確認ください。

 

Arduino入門編で使用しているUNOはAmazonにて購入可能です。

互換品とは言え、Arduinoはオープンソース。

複製して販売するのもライセンス的に問題なし。

 

そのため互換品の品質も悪くなく、それでいて値段は安いです。

正規品本体の値段程度で豊富な部品が多数ついています。

 

学習用、遊び用、お試し用には安価な互換品がおすすめです。

 

 

上記のものでも十分に多数の部品が入っていますが、最初からもっと多数の部品が入っているこちらもお勧めです。

 

Amazonでお得に買う方法

Amazonでお得に購入するなら、Amazonギフト券がおすすめです。

現金でチャージするたびに、チャージ額に応じたポイントが付与されます。

最大2.5%!!!(Amazonプライム会員ならさらにお得)

チャージ額(一回分)一般プライム会員
5,000円~19,999円0.5%1.0%
2,0000円~39,999円1.0%1.5%
40,000円~89,999円1.5%2.0%
90,000円~2.0%2.5%

さらに、初回チャージで2000ポイントもらえるキャンペーンも実施中!
※いつもは1000ポイント、今なら2000ポイントです!

\Amazonギフト券 2000ポイントキャンペーン/
Amazonチャージ 初回購入で2000ポイントキャンペーン

 

補足情報
コンビニ・ATM・ネットバンクが対象
購入は1円単位で可能

コメント

  1. omoroyaomoroya より:

    map()関数について、記事を更新しました。
    参考にされた方は一度、お読みいただけると幸いです。

    • 居酒屋ガレージ店主(JH3DBO) より:

      記事のリライト、ありがとうございます。
      ご面倒おかけしました。
      10bit→8bitの変換にmap()を使おうとするからややこしい(本来は単純に1/4すればよい)のであって、map()はなにも悪くありません。

      元の例題が悪いとしか言いようがないのです。
      それをみんな真似してしまって、書籍にまでなっている。
        (10bit→8bit変換が目的じゃなかったらこの式でエエんです)

      これは「なんとかせなあかん」とコメントしたのが最初の書き込みです。

    • 居酒屋ガレージ店主(JH3DBO) より:

      用語について・・・「補完と補間」。
      文中、「線形補完」と記されている所があります。
      数学的用語では「線形補間」が正しい漢字です。

    • omoroyaomoroya より:

      ご指摘ありがとうございます。
      誤字ですね(;^_^A
      修正しておきましした。

  2. 居酒屋ガレージ店主(JH3DBO) より:

    『Arduino なんとかして誤用を正したい:A/Dの1/1023とmap関数』
    http://igarage.cocolog-nifty.com/blog/2020/05/post-115911.html
    にあれこれ追記しています。
    「y = map(x, 0, 1023, 0, 255);」が「ありな例」も解説しています。
    コメントも含めて再読していただければと・・・

    • omoroyaomoroya より:

      コメントありがとうございます。
      map関数に関して、追記したいと考えています。
      ADコンバータがどうのこうのということではなくmap()関数の使い方の問題ですね。
      1023の問題もそうですが、単純に0から始まり1023がmaxと考えて当てはめてしまうことが原因でしょうね。
      単純に1023を255に割り当てると考えてしまう・・・。
      であれば、1024を256に割り当てるでも良いはずですが、AD変換されて出てくる数値のmaxが1023であるためない数値の1024を使うのが気持ち悪い。
      で考えた挙句、1023、255で良いかと思ってしまう・・・。というのが間違いの原因な気がします。
      map()関数の例題の説明が良くないですよね。

  3. 居酒屋ガレージ店主(JH3DBO) より:

    Arduino 入門 番外編 05 【アナログ入力 とは】
    https://omoroya.com/arduino-extra-edition-05/
    Arduino 入門 番外編 17 【map関数】
    https://omoroya.com/arduino-extra-edition-17/
    に関してです。
    「アナログ入力値→電圧値への変換」方法と「map関数」の使い方について気になるところがあります。
    詳細は、私のブログ記事にまとめています。
    ご一読いただければと。
    ・Arduino なんとかして誤用を正したい:A/Dの1/1023とmap関数
    http://igarage.cocolog-nifty.com/blog/2020/05/post-115911.html
    ・Arduino 10bit A/D値をmap関数でスケーリングする例
    http://igarage.cocolog-nifty.com/blog/2020/05/post-a19b6e.html
    ・ミスが広まる 1/1023 vs 1/1024
    http://igarage.cocolog-nifty.com/blog/2020/01/post-a02d3f.html

    • omoroyaomoroya より:

      ご指摘ありがとうございます!

      1024種の組み合わせがあると記載しておきながら1023で割っている時点でという話ですね。
      確かに1024で割るのが正しい使い方だと思います。
      Arduinoに搭載されているマイコンのデータシートに記載されている計算式も1024です。
      計算値+1LSBの範囲内の電圧というのが正しいということですね。

      電源電圧、vrefの実測値に関してはその通りかと思います。
      キャリブレーションする必要性がありますが、遊びの段階ではそこまではとも思っています。

      map()関数に関しても、下記ご指摘の通りです。
      ”10bitの半値である512を代入すると127が返ってくる。正しい値は8bitの半値=128である”
      スケッチが正しいか確認するときに、半値の値などいくつか確認しで127が返ってくることがわかり
      、あれ、これおかしいなと・・・。
      ただ、修正しようとすると「Arduinoはじめよう」の書籍、公式サンプル例の記載と違うということが起きてしまい公式のサンプルに従う方向で記載しています。
      また、使い方次第ではありかなとも思っています。
      しばし、考えてみます。
      みなさんに、
      「居酒屋ガレージ店主」さんのホームページ読んでいただいて考えていただくのが良いかもしれません。

タイトルとURLをコピーしました