Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lチカで終わらせないArduino シリアル通信 Part2

Lチカで終わらせないArduino シリアル通信 Part2

ArduinoとPCの間で通信を行う方法についてまとめた資料です。LチカからPCとArduinoで通信を行う簡単な方法を説明しています。
Part1はコチラ(https://speakerdeck.com/tomit3/ltikadezhong-warasenaiarduino-siriarutong-xin-part1)

tomit3

May 06, 2023
Tweet

More Decks by tomit3

Other Decks in Technology

Transcript

  1. 自己紹介とこの資料について • 自己紹介 • ソフトウェア開発を仕事にしている • ソフトウェア、ハードウェアに限らずモノ作りが好き • GitHubに公開 •

    関連する資料もslideshareで公開(tomitomi3で検索) • この資料について • とある勉強会向けに作成した資料。ArduinoをLチカだけで使用するので はなく、便利なハードとして使うために「通信」させて使う方法につい てまとめた。全2回の2回目。
  2. 前回 • 前回のArduinoコード • 受信データがあったら、読みとり処理を行う 1バイトの解釈(ʼaʼかʼbʼなど)なら問題無し。 • コード上のシリアル通信API(関数) • Serial.available()

    • 受信バッファにデータがあるかどうかを返す。 • Serial.read() • 受信バッファの先頭1バイトを返す。無ければ-1を返す。 void loop() { while (Serial.available() > 0) { byte InputData = Serial.read(); //receve switch (InputData) { case 'a’ : //略 break; case 'b’ : //略 break; default : //略 break; } } }
  3. 前回 • 前回のArduinoコード • 受信データがあったら、読みとり処理を行う 1バイトの解釈(ʼaʼかʼbʼなど)なら問題無し。 • コード上のシリアル通信API(関数) • Serial.available()

    • 受信バッファにデータがあるかどうかを返す。 • Serial.read() • 受信バッファの先頭1バイトを返す。無ければ-1を返す。 • 気になる事 • 受信バッファとは? • 先頭1バイトしか読まない?複数バイトは? void loop() { while (Serial.available() > 0) { byte InputData = Serial.read(); //receve switch (InputData) { case 'a’ : //略 break; case 'b’ : //略 break; default : //略 break; } } }
  4. 受信バッファとは • 送信されたデータは受信しないと消える。 • イメージとしては水とバケツ。一定量ためるバケツ=バッファ • 送信されたデータは受信バッファにデータを貯める • Arduino UNOは送信用バッファ、送信用バッファに64バイト確保

    • バッファがあふれた場合上書きされる • バッファをあふれないようにデータを読み込む必要がある • バッファが無いと常に監視・取得する必要があることになる • 受信バッファは「FIFO」というキュー構造で保存 • 先に入れたデータは最初に取り出す。受信の順番が維持される • Searil.read()はバッファから1バイト毎に取り出す(=1バイト毎にバッファから削除)
  5. シリアル通信 … 0番目 1番目 63番目 シリアル通信 受信バッファ FIFO(First In First

    Out 先入れ先出し) 読み込み (メモリに転送) Arduino シリアル通信HWとSW 受信バッファとは
  6. //serial control #define BAUDRATE 9600 //9600 115200 //-------------------------------------------------------------------- //Setup() //--------------------------------------------------------------------

    void setup() { //init serial Serial.begin(BAUDRATE); } //-------------------------------------------------------------------- //loop() //-------------------------------------------------------------------- void loop() { //1バイト読み込む unsigned char rcv = Serial.read(); //符号無しunsigned charは-1は255となる if(rcv==255) { return;//何もしない。再度loop()へ。 } //2進数と16進数で表示する Serial.println(rcv,BIN); Serial.println(rcv,HEX); delay(1000); } 受信バッファとFIFO • 送信文字を2進数・16進数で返信 • 右のコードを書き込む • シリアルモニタで文字列を送信 • 改行 LFのみ • Baudrate 9600 bps • 「1」を入れて送信 1
  7. 受信バッファとFIFO • 「1」を入力すると2つの出力 • 1つめ目 • 1行目 110001(2進数) • 2行目

    31 (16進数) • 2つめ目 • 1010 • A ※LF:LineFeed 改行文字を意味する //-------------------------------------------------------------------- //loop() //-------------------------------------------------------------------- void loop() { //1バイト読み込む unsigned char rcv = Serial.read(); //符号無しunsigned charは-1は255となる if(rcv==255) { return;//何もしない。再度loop()へ。 } //2進数と16進数で表示する Serial.println(rcv,BIN); Serial.println(rcv,HEX); delay(1000); }
  8. 補足:文字1と数字1の違い • 文字の1と数字の1は異なるモノ • 文字の1はASCIIコードに基づき変換 され数字で表現(0x31、10進数で49) • 数字の1は1として表現 • 先ほどの例で「A」は制御文字で改行

    • シリアルモニタで入力した文字は、 全てASCIIコードで変換され送信 • 次のデモで重要なので覚えておく https://ja.wikipedia.org/wiki/ASCII (accessed 2021/9/20)
  9. LF 0x31 1[送信] 受信バッファ FIFO(First In First Out) 先入れ先出し ↑

    “1”、改行文字の 2バイトが受信バッファにたまる 順序が維持 1バイトずつ読み込み表示 改行 “1” 1番目 63番目 0番目 受信バッファとFIFO 「1」を送信すると下記が送信 ・文字1 ・改行文字LF
  10. 受信バッファとFIFO • 「12」を送信すると LF 0x32 0x31 0番目 1番目 63番目 12[送信]

    受信バッファ FIFO(First In First Out) 先入れ先出し ↑ “1”、”2”、改行文字の 3バイトが受信バッファにたまる 1バイトずつ読み込み表示 改行 ”2” “1”
  11. LF 0x32 0x31 0番目 1番目 63番目 12[送信] 受信バッファ FIFO(First In

    First Out) 先入れ先出し ↑ “1”、”2”、改行文字の 3バイトが受信バッファにたまる 順序が維持 1バイトずつ読み込み表示 改行 ”2” “1”
  12. 受信バッファがあふれないように読み込む • 受信バッファがあふれないように読み込むには? ①1バイトずつ読み込む • Serial.read()で「1バイトを読み込み、処理」を繰り返す。処理時間が間に合えばよい ②複数バイトをまとめて読み込む • 「1バイト読み込み⇒処理⇒・・・」というのは時間がかかる。 •

    Serial.avarable()で受信データを確認し、バッファが半分になったらSerial.read()でま とめて読み込む。効率が良い。 ⇒いずれも定期的にSerialのバッファサイズを確認(ポーリング) • システムの仕様(要求次第)に合わせて決めるべきところ • リアルタイムに結果が欲しい場合、プールしてよいかなど
  13. 効率よく送る(データ構造) • 例:「15」を送る。受信側は数字として扱う。 • 文字で送る(シリアルモニタはこちら) • 文字を送ることになる「1」と「5」の2バイト • [0x31][0x35]を送る=1バイトを2回送信 •

    文字⇒数字の変換を行う。 • 数字(バイト)で送る(ソフトを作る場合はこちら) • 「15」(数字)をバイト変換。1バイト • [0x0F]を送る=1バイトを1回送信 • 数字をそのまま使用可能 • 1回に複数送る • 数字0〜15に限定可能ならば、1回でまとめておる事が可能 • 1回目を上位4ビット、2回目を4ビットに割り当てて送信 =1バイトを1回送信で2回分送れる 0 0 0 0 0 0 0 0 1byte = 8 bit 0 0 0 1 1 1 1 1 0 0 1 0 0 0 1 1 0x01 0x05 0 0 0 0 1 1 1 1 0x15 1 1 1 1 0 0 0 1 0x79 ↑ 1bit目 ↑ 8bit目 1回目と2回目を1バイトに格納
  14. まとめてデータを送受信する • 前回のArduinoコード • 受信データがあったら、読みとるというコード 1バイトの解釈(ʼaʼかʼbʼなど)なら問題無し。 • コード上のシリアル通信API(関数) • Serial.available()

    • 受信バッファにデータがあるかどうかを返す。 • Serial.read() • 受信バッファの先頭1バイトを返す。無ければ-1を返す。 • 気になる事 • 受信バッファとは? • 先頭1バイトしか読まない?複数バイトは? void loop() { while (Serial.available() > 0) { byte InputData = Serial.read(); //receve switch (InputData) { case 'a’ : //略 break; case 'b’ : //略 break; default : //略 break; } } }
  15. 複数バイトを送る • 複数バイトで意味を成すデータ構造の場合 • 例:intは2バイト(Arduinoでは)、自作の構造体 • 1バイトを超える場合(255を超える数字)エンディアンに注意 • 送受信時に決めておく。複数バイトで構成されている場合に注意。 •

    一般的にはアーキテクチャでは気にするが、送受信で事前にルールとして決めておく • 例:256 • 1バイト表現できないため2バイトで表現する • 256=0x100=0b100000000 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 リトルエンディアン ビッグエンディアン
  16. 複数バイトを送る • 複数バイト受信と複数バイトの解釈 • 右のコードをloop()に書き込む • 2バイト送り、1バイトずつ読み込む。 2バイト受信時、解釈して表示する。 • 1回目の1バイトを上位、2回目の1バイトを下位とする

    • シリアルモニタで「12」を送信 • 改行文字は無し(余計なデータのため) int rcvCount = 0; byte byte1 =0; byte byte2 =0; void loop() { //read 1byte from FIFO buffer unsigned char rcvData = Serial.read(); //符号無しcharは-1は255となる if (rcvData == 255) { delay(10); return;//何もしないので終わる } //受信毎に分解して解釈する if (rcvCount == 0) { //1回目の1バイト受信 byte1 = rcvData; //increment receve count rcvCount++; } else if (rcvCount == 1) { //2回目の1バイト受信 byte2 = rcvData; //上位下位をどうするか? unsigned int value = 0; //int型にデータを当てはめる value += byte1; value = value << 8; //8ビット 左シフト value += byte2; //2進数と16進数で表示する Serial.println(value, BIN); Serial.println(value, HEX); Serial.println(value); //receve count reset rcvCount = 0; } delay(10); }
  17. 0x32 0x31 0番目 1番目 63番目 12[送信] 受信バッファ FIFO(First In First

    Out) 先入れ先出し 2バイト読み込んだら下記の処理 0x31を代入 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 8bit左シフト 0x32を代入 0 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0 0 1 0 2バイト分確保 //上位下位をどうするか? unsigned int value = 0; //int型にデータを当てはめる value += byte1; value = value << 8; value += byte2; 0x3132、12594(10進数)
  18. • 通信するときに必要な事の一部 • 任意のデータを送る • 数字、文字列、データ構造 • 送ったデータが「正しく」送れたか? • データを正しく送れたか/受け取れたか

    • データを取りこぼしなく送れたか/受け取れたか ⇒受信側が判定 • 例:ネットワークではTCP/IPが担保 • 相手先と通信できることを事前確認(ハンドシェイク) • データを分割して送信(パケット)し、パケット毎にデータの正しさ(チェックサム、 CRCなど)を確認し、間違いがあれば再送信 USB 「通信」するということ
  19. 受信データが正しいか判定する • 受信したデータが正しいかどうか? • 受信データだけでは正しさは分からない。ヒントが欲しい。 • 送信側と受信側で同じことをして再現できる技法が良い • マイコン(MCU)は一般的に計算力が高くないため計算負荷が低いものが良い •

    データがおかしいと分かれば、再送依頼、破棄などの判断が行える • データの「正しさのヒント」をデータに入れ込む • チェックディジット クレジットカードの番号判定 数字の入れ替え検出 • チェックサム データが正しいか判定 計算処理低 • ハッシュ関数 データが正しい(本物か)か判定 計算処理高
  20. 受信データが正しいか判定する • チェックサム • 送信データを1バイト毎に加算した数字のこと • 1、2バイトetc 送るデータサイズによる。受信バッファから2バイトでもよい • 1の補数、2の補数を使うのが一般的。受信側で足せば0になるようにする。計算量低

    • 送信側が行う事 • 送信データのチェックサムを計算 • チェックサムを別途送信 • 受信側で行う事 • 受信データのチェックサムを計算 • 受信データで計算したチェックサムと送信で計算したチェックサムを比較
  21. ・・・ ・・・ 0x32 0x31 0番目 1番目 63番目 受信バッファ FIFO(First In

    First Out) 先入れ先出し 10進数 送信データ 49 1 1byte 50 2 2byte 51 3 3byte 52 4 4byte 53 5 5byte 54 6 6byte 55 7 7byte 97 a 8byte チェックサム int 461 (チェックサム byte 205) 10進数 受信データ 49 1 1byte 50 2 2byte 51 3 3byte 52 4 4byte 53 5 5byte 54 6 6byte 55 7 7byte 97 a 8byte PC(送信) Arduino(受信) 送信側で計算したチェックサム 受信側で計算したチェックサム を比較する チェックサム int 461 (チェックサム byte 205)
  22. unsigned char rcvCount = 0; unsigned int checkSum = 0;

    //-------------------------------------------------------------------- //loop() //-------------------------------------------------------------------- void loop() { //read 1byte from FIFO buffer unsigned char rcvData = Serial.read(); //符号無しcharは-1は255となる if (rcvData == 255) { delay(10); return;//何もしないので終わる } //チェックサム計算 checkSum += rcvData; //受信毎に分解して解釈する if (rcvCount == 5) { if (checkSum == 309) { //チェックサム一致=データが正しい Serial.println("data correct!"); } else { //チェックサム不一致=データが正しい Serial.println("data incorrect!"); } //receve count reset rcvCount = 0; checkSum = 0; //バッファクリア Serial.flush(); } else { //increment receve count rcvCount++; } delay(10); } • チェックサムの確認 • 左コードを書き込む。 • 6文字受信した時のチェックサムを計算して結果を出力 • チェックサム「309」をハードコーディング チェックサムが309は正常、それ以外は異常と出力 • 実験 下記を入力 • シリアルモニタ(改行無し)で6文字送信 • 123456 • 正常 ⇒ OK • 112345 • 異常 ⇒ 異なるデータが入ったことを検出 • 654321 • 正常 ⇒ チェックサムは順番違いは検出できない
  23. 10進数 文字列 49 1 1byte 50 2 2byte 51 3

    3byte 52 4 4byte 53 5 5byte 54 6 6byte 1度に送るデータを加算 チェックサム 309 10進数 文字列 49 1 1byte 50 2 2byte 51 3 3byte 52 4 4byte 53 5 5byte 54 6 6byte 受信データを加算 チェックサム 309 受信データの確認=チェックサムの比較 同一であればデータが送られたときと同じ可能性が高い 注意:例では値を固定したが、送信データが変わるとチェックサムも変わる 送信側は送信データとチェックサムをまとめて送るようにする
  24. 通信ルール=プロトコルを作る • 受信データの「境界」が不明 • どこがチェックサム、どこが実データか?送信・受信側で事前に決める • どんなデータ構造を? • どう送るのか? •

    作成したプロトコル 1. 送るデータをまとまり(パケット)で送る。 • パケットの構造は下記で構成 • ヘッダー部 • チェックサム、(ここに送信サイズもある) • データ部(ボディ・ペイロード と呼ばれる) • 1回に送る実データ(任意データ構造) 2. 1度に送るデータはヘッダー+データで10バイト固定 ヘッダー チェックサム データ 2バイト 8バイト 1回に下記パケットを送る ⇒プロトコル
  25. ペイロード (送るデータ) ペイロード (送るデータ) ヘッダー 送るサイズ ペイロード (送るデータ) プロトコル •

    1度に送るデータはNバイト固定 問題 1. バッファがオーバーして1バイト 欠損したとき受信側は待機し続 ける必要がある。 ⇒タイムアウトの実装など 2. 受信中にノイズがあった受信 データが正しいかどうか? ⇒データの正当性確認 プロトコル • 1度に送るデータは可変⻑ • ヘッダーに送るサイズを付与 問題 1. バッファがオーバーして1バイト 欠損したとき受信側は待機し続 ける必要がある。 ⇒タイムアウトの実装など 2. 受信中にノイズがあった受信 データが正しいかどうか? ⇒データの正当性確認 プロトコル • 1度に送るデータはNバイト固定 • チェックサムを付加 (誤り訂正符号) 問題 1. バッファがオーバーして1バイト 欠損したとき受信側は待機し続 ける必要がある。 ⇒タイムアウトの実装など ヘッダー 誤り訂正符号 いろんなプロトコル
  26. 通信ルール=プロトコルを作る • シリアルモニタでデータを送るのは難しいので解説 • 前のプロトコルに従いデータを受信して、整合性確認 1 1byte 205 2byte 49

    3byte 50 4byte 51 5byte 52 6byte 53 7byte 54 8byte 55 9byte 97 10byte 10進数 文字列 49 1 1byte 50 2 2byte 51 3 3byte 52 4 4byte 53 5 5byte 54 6 6byte 55 7 7byte 97 a 8byte チェックサム 461 パケットに変換 1 1byte 205 2byte 49 3byte 50 4byte 51 5byte 52 6byte 53 7byte 54 8byte 55 9byte 97 10byte チェックサム データ部
  27. #define RCV_SIZE 10 #define RCV_LOOP 10 byte rcvData[RCV_SIZE]; //-------------------------------------------------------------------- //loop()

    //-------------------------------------------------------------------- void loop() { //シリアル通信で1パケット受信 //データがあった=欲しいデータが送信中/されたとする if (Serial.available() > 0) { //受信データ変数クリア memset(rcvData, 0, RCV_SIZE); //RCV_LOOP分がデータ受信待ちなどの待機≒タイムアウト unsigned int chkSumRcv = 0; int readCount = 0; for (int i = 0; i < RCV_LOOP; i++) { //wait 9600 bit/s=1200 byte/s->10 byteは8.3ms相当で貯まるはず delay(5); //read unsigned int canReadSize = Serial.available(); if (canReadSize <= 0) { continue; } else if (canReadSize > 0) { //バッファにあるデータを1バイトずつ読み込む for (int i = 0; i < canReadSize; i++) { byte temp = Serial.read(); rcvData[readCount] = temp; //先頭2バイトはチェックサム計算しない if (readCount >= 2) { chkSumRcv += temp; } readCount++; } //指定サイズを読み込んだ時ループを脱する if (readCount == RCV_SIZE) { break; } } } //受信データのチェックサム確認 unsigned int chkSumFromRcvData = (rcvData[0]<<8) | rcvData[1]; if ( chkSumRcv == chkSumFromRcvData) { Serial.println("OK"); } else { Serial.println("Fail"); } } delay(1); }
  28. まとめ • ArduinoとPCの通信を中心に • Arduinoの基本的な使い方とシリアル通信の基本的な概要 • 複数バイト、バッファの考え、チェックサム • 互いのHWの通信のためのプロトコル作り •

    パケット、データ正当性 • 省略したこと • PCの送受信(UIスレッドではなく、別スレッドでポーリングすべきetc) • 再送信 • HWのハンドシェイク(複数のCOMのHWでは機器区別が可能) • 送受信が必要。「HELLO ⇒ HELLO」と返すなどでもOK。
  29. アプリケーション層 プレゼンテーション層 セッション層 トランスポート層 ネットワーク層 データリンク層 物理層 デバイス同士で通信をするために定義(作る)する必要がある 232Cの物理的な規格 RX/TX、UART

    差動etc 232Cの通信プロトコル(データリンク層かは議論があるかも) データビットetc USB PC<-Serial通信->ArduinoにおけるOSI参照モデル ・パケットにして送る ・1度に送るデータサイズ ・データの整合性:チェックサム
  30. 補足:バッファサイズを拡張したい方向け 下記コード変更して、ATMEGA328Pに書き込む ¥arduino-1.x.x¥hardware¥arduino¥avr¥cores¥Arduino¥HardwareSerial_private.h #if !defined(SERIAL_TX_BUFFER_SIZE) #if ((RAMEND - RAMSTART) <

    1023) #define SERIAL_TX_BUFFER_SIZE 16 #else #define SERIAL_TX_BUFFER_SIZE 64 #endif #endif #if !defined(SERIAL_RX_BUFFER_SIZE) #if ((RAMEND - RAMSTART) < 1023) #define SERIAL_RX_BUFFER_SIZE 16 #else #define SERIAL_RX_BUFFER_SIZE 64 #endif #endif #if (SERIAL_TX_BUFFER_SIZE>256) typedef uint16_t tx_buffer_index_t; #else typedef uint8_t tx_buffer_index_t; #endif #if (SERIAL_RX_BUFFER_SIZE>256) typedef uint16_t rx_buffer_index_t; #else typedef uint8_t rx_buffer_index_t; #endif ・サイズ SERIAL_TX_BUFFER_SIZE SERIAL_RX_BUFFER_SIZE がそれぞれ送受信バッファサイズ。デフォルトは64となる (MCUのメモリサイズが1023バイト以下は16) ・ポイント 1.リングバッファのため、2^nのサイズにするのが望ましい。 2.「256」より大きくするとアトミック性ガード(Atomicity?)を 実装しないため不安定になるとのこと。 ATMEGA 328P用にコンパイルして書き込む必要がある。
  31. 補足:available()とread() • 1関数呼び出し時のコスト • 下記は受信バッファ0の時の時間を計測 • Searial.available()1.72 us • Searil.read()

    1.52 us ※Arduino クロック周波数 16Mhz。AVR 1命令1クロック処理。1命令=0.0625 us。 25クロック • 0.2usほどread()の方が早い。read()を多用しようではなく、メモリ転送や 処理も考慮するとまとめてデータを呼び出したほうがよい。
  32. 補足 • 中間HWとしてのArduino • HWの中間・仲介役として優秀なArduino • M5シリーズとArduinoの実装例が豊富(ユーザー数が多いからと推測) • PC⇒HW •

    電気的な仕組みはクリアしたとして、電圧値値読み込むためのA/D変換IDとの通信、 SPI、I2Cプロトコルでの通信・・・があるが • Arduinoは前述の実装コードが多数公開されているのでここを簡略化できる