勝手な電子工作・・

勝手なオリジナル電子工作に関する記事を書きます

ソフトウェアシリアルをコンパクトに作るーArduino編-そしてトレースに使う

f:id:a-tomi:20200805173852j:plain

シリアル通信を小さく作る例をArduinoの例で説明したいと思います。少し複雑な工作でも、ESPなどを持ち出さずにUNO(ATmega328P)などありきたりのマイコンで作る「生活の知恵」でもありますし、シリアル通信のアナライザーを作る知恵でもあります。この前後の記事でご紹介する二酸化炭素濃度ロガーなど、通信種類が多い場合などに役立ちます。

上の写真は左がArduino-UNO、右がコンパチ機、間に信号を観察するための小さなブレッドボードを挟んでいます。

そもそもIDE環境がArduinoとPCの接続にハードウェア・シリアル通信を使うため、それが1つしかないUNOの場合には、2つ目が必要なら「ソフトウェアシリアル」を使います。ところがその標準ライブラリーは資源を意外に多く消費します。そこで必要な、小容量で複数つなぐ方法としてここで説明したいと思います。

ご紹介する手作りソフトの受信ロジックは、デバイス間のシリアル通信を外部システムとして勝手に受信(つまりトレースやブロードキャスト通信も)できます。そのスケッチ例は、この記事の最後のほうにつけておきます(2020.8.13追加)。

雑談ですが、シリアル通信はUART通信、非同期通信、スタートストップ通信、調歩式、等々色んな名称がありますかね。物理的なリンクだけ見れば各デバイスの時計に頼り、同期をとらずにビットをやりとりします。現代から見れば乱暴^^;  世界でオンラインシステムが始まった時から使われ、日本では1964年東京オリンピックがその通信システムの始まり。同期式が使われだしたのはだいぶ後です。

それがマイコンで今でも広く使われているのが不思議ですが、デバイスは今やどれも正確なクォーツクを使い問題はほぼないわけです。フィリップスI2C通信の絶妙アーキテクチャには程遠いですが、単純な2線で便利です。次の写真のマイクロSDリーダーライターは、シリアル通信で動かす日本製モジュールで、小さなマイコンでもSDカード処理がらくちんでできてしまいます。

f:id:a-tomi:20200805195805j:plain

2011年に放射能測定器を色々作りましたが、簡単なインターフェイスでロギングするのに随分重宝です。高価でしたが。今も時々使いますが、この製造元である塩尻市の(有)Suntechさんが2020年4月末で営業を終了されたのは残念。あ、雑談はここまで。つまりシリアル接続は単純なのでポピュラーに使われているわけです。

 

まずArduino標準ライブラリーのソフトウェアシリアル通信を使うサンプルスケッチを載せます。最初に送る側(仮にMasterと呼びます)です。ここではハードウェアシリアルでPCのキーボードから受けた文字を、そのままソフトウェアシリアルで送信します。

/* Software serial - exapmle 
 * Sender (Master) V.00 July 2020 (c)Akira Tominaga
*/
#include "SoftwareSerial.h"
#define sRx 7                     // Master sRx (MISO) = Slave sTx
#define sTx 8                     // Master sTx (MOSI) = Slave sRx
#define sSbaud 9600               // SW-serial baud rate
SoftwareSerial sS(sRx, sTx);      // create SW-serial as symbol sS
String strTyped;                  // string from KB and to send to Slave
char rBt;                         // receiving char from software Serial

void setup() {  // ***** Arduino Setup *****
  Serial.begin(sSbaud);           // start hardware serial
  sS.begin(sSbaud);               // start SW-serial
  Serial.println("Master ready");
}

void loop() {  // ***** Arduino Loop *****
  while (Serial.available() < 1) {}
  strTyped = Serial.readStringUntil(0x10); // read KB until DLE
  uint8_t strL = strTyped.length(); // get length
  for (uint8_t i = 0; i < strL; i++) {
    sS.write(strTyped.charAt(i)); // send each byte
  }
  Serial.print(strTyped);         // show what was sent
  Serial.println("was sent by Master");
}
/* *** End of sketch *** */

 次に、受ける側の標準ソフトウェアシリアルによるスケッチです。仮にSlaveと呼びますが、受けた文字をそのまま印字するようにハードウェアシリアルに出します。

/* Software serial - exapmle 
 * Receiver (Slave) V.00 July 2020 (c)Akira Tominaga
*/
#include "SoftwareSerial.h"
#define sRx 7                     // sS-Slave Rx (MOSI) = sS Master Tx
#define sTx 8                     // sS-Slave Tx (MISO) = sS Master Rx
char rBt;                         // receiving char from software Serial
#define sSbaud 9600               // SW-serial baud ratea
SoftwareSerial sS(sRx, sTx);      // create SW-serial sS as symbol

void setup() { // ***** Arduino setup *****
  Serial.begin(sSbaud);           // start hardware serial
  sS.begin(sSbaud);               // start SW-serial sS
  Serial.println("Receiver ready");
}

void loop() {  // ***** Arduino Loop *****
  if (sS.available()) {
    rBt = sS.read();
    Serial.print(rBt);
    if (rBt == 0x0A) {
      Serial.println("was received by Slave");
    }
  }
}
/* *** End of Sketch *** */

この2つのペアによる送受をオシロで観察しても、中身が分り難いのが非同期通信の特徴。手がかりのクロック信号がないですからね。次の写真で下がMasterから出たもの、上がSlaveから返したもの(2020.8.6追加記述:失礼、上に示したレシーバーのソースに1行追加して送り返しています)。スタートビットはH(シリアル通信ではHをスペースという)からL(マーク)になるところを指し、ストップビットは最後に信号線をH(スペース)に戻すだけです。その間に8ビットの列があり、ビットの順番は普通のバイトとは逆に(つまりLSb=Least Significant bit から)送られます。コンピュータによってメモリー内への収容が逆順の機種(Little Endian)も多いのでまあ一理あるわけですが。

f:id:a-tomi:20200805182725j:plain

オシロではなく、ロジックアナライザーなら少しわかりやすいですが、時間計算する以外にタイミングの手がかりが少ないもの。そこで、勝手な電子工作の知恵では、分析のためにはU(0x55)という文字を含めるようにします。Uならオンオフのビットが交互になりますからタイミングが確実にわかりますね。次はUAUと打ち込み送信をロジックアナライザーで見たもの。

f:id:a-tomi:20200805201804j:plain

Sはスタートビットで、Pはストップビット。間のビットの並びを前後逆転すれば0x55、0x41、0x55、0x0Aと簡単に読めます。送信側ではUSBキーボードを打ち込んでハードウェアシリアルに渡されたものをDLE記号が来る前までそのままを送っていますから、最後に0x0AのLF(改行つまりエンターキー)がくっついてます。

ロジックアナライザーは次を使っていますが、今どきは中国製のコンパチモデルというよりも、形状を変えたコピー機が超廉価でずいぶん出回ってるようです。

 

f:id:a-tomi:20200805203049j:plain

 

 ロジックアナライザーで使わないチャネルのプローブ先端はグランドに落としますから、次のようなアナライザー専用のブレッドボードを常備しておくと測定が手短かに行え、かつ、刺し間違いも減ります。

f:id:a-tomi:20200805203409j:plain

これは普通のブレッドボードの一部を強引に切り取ってヤスリをかけただけ。小さくて必要な穴が十分なので、場所を取らず迅速に使え、さしっぱなしで保管できます。

 

とはいえ、シリアル通信の信号解析が圧倒的に楽な方法は、プログラムで分析することです。さきほど紹介した受信側のロジックを使えば簡単です。

 

標準のソフトウェアシリアルはUNOの容量の1割近くを占拠するので、一般的な工作で小マイコンに組み込むために手で書く例(RYO=Roll-Your-Ownプログラム)が次です。

まず送る側。Uの文字を含んで測定しながら定数(sTuS)を設定するだけでよいものです。

/*  Roll-Your-Own software-serial-communication; Sender
 *  Sample sketch V.00  July 31, 2020 (c) Akira Tominaga
*/
#define sRx 7                     // Sender sRx (MISO) = Receiver sTx
#define sTx 8                     // Sender sTx (MOSI) = Receiver sRx
#define sTuS 104-3                // time unit (9600bps) for sending
#define sTuhS 51                  // time unit half for sending                  
char tByte;                       // typed-in character
char sByte;                       // character work area to be sent

void setup() {  // ***** Arduino setlup ***** 
  Serial.begin(9600);             // start hardware serial
  pinMode(sRx,INPUT_PULLUP);      // soft Rx pulled-up
  pinMode(sTx,OUTPUT);            // soft Tx as output
  digitalWrite(sTx,HIGH);         // set it HIGH
  Serial.println("Snd ready");
}

void loop() {  // ***** Arduino loop *****
  while (Serial.available() < 1) {} // wait until type-in
  tByte = Serial.read();          // read typed-in character
  sByte = tByte;                  // set it into work byte to be sent
  sSend();                        // send it via software serial
  Serial.print(tByte);            // print char sent(unchanged byte)
  delayMicroseconds(sTuS*3);        // *** for receiver printing time
}

/**************************************
      User defined functions
* *********************************** */
// *** sSend *** send a charater in sByte
void sSend(void) {
#define LSb B00000001             // Least Significant bit
  digitalWrite(sTx, LOW);
  delayMicroseconds(sTuS);
  
  for (int i = 0; i < 8; i++) {   // do the following for 8bits
    if ((sByte & LSb) == LSb) {   // if one then
      digitalWrite(sTx, HIGH);    // set sTx high
    } else {                      // else
      digitalWrite(sTx, LOW);     // set sTx low
    }
    delayMicroseconds(sTuS);      // take required time
    sByte = sByte >> 1;           // shift a bit to right
  }
  
  digitalWrite(sTx,  HIGH);       // set high as a stoP bit
  delayMicroseconds(sTuS+sTuhS);  // consume time-unit x 1.5
}

/* End of program */

そして次が受ける側のRYOプログラム。こちらはタイミング調整の勝手な生活の知恵として、読取タイミングをどこかのピン(tmChk)に出力します。定数(sTuR)の値を減らしながら、検査すべきビットの真ん中に読取タイミングが来るようにすれば、それだけで以後はもう大丈夫です。

/*  Roll-Your-Own software-serial-communication Receiver
    Sample sketch V.00  July 31, 2020 (c) Akira Tominaga
*/
#define tmChk 6                   // timing checker for measurement
#define sRx 7                     // Receiver sRx (MOSI) = Sender sTx
#define sTx 8                     // Receiver sTx (MISO) = Sender sRx
#define sTuR 104-10               // time unit (9600bps) for receive
#define sTuhR 51-21               // time unit / 2 for receive
byte rByte;                       // character receiving byte

void setup() {  // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial
  pinMode(sRx, INPUT_PULLUP);     // soft Rx pulled-up
  pinMode(sTx, OUTPUT);           // soft Tx as output
  digitalWrite(sTx, HIGH);        // set it high
  pinMode(tmChk, OUTPUT);         // timing checker
  digitalWrite(tmChk, LOW);       // set it LOW
  Serial.println();               // LF for repeated testing
  Serial.println("Rcv ready");
}

void loop() {  // ***** Arduino loop *****
  sRcv();                         // receive data into rByte
  Serial.print(rByte, HEX);
  Serial.print(" ");              // insert blank as a separator
}

/**************************************
      User defined functions
* *********************************** */
// *** sRcv *** receive a charater in rByte
void sRcv(void) {
#define MSb B10000000             // Most-Significant-bit
#define nMSb B01111111            // not Most-Significant-bit
  while(digitalRead(sRx)==HIGH){} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH);    // sampling-time-checker on
    digitalWrite(tmChk, LOW);     // sampling-time-checker off
    if (digitalRead(sRx) == HIGH) { // if sRx high, then
      rByte = rByte | MSb;        // set MSb on
    } else {                      // else
      rByte = rByte & nMSb;       // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(sRx) == LOW) {}; // wait until HIGH (stop bit)
}
/* End of program */

 受信のタイミング信号を含めて記録したものは次です。

f:id:a-tomi:20200805210353j:plain

送る側のRYOプログラムを標準ソフトウェアシリアルの受信側で受けると同じ動きをしますし、逆の組みあわせ、つまり標準ソフトウェアシリアルの送り側を今回のRYOの受け側を組み合わせても、同じ動きをします。つまり他の相手ともつながるということが分ります。

ついでにビットの解析を書けば次のようになります。

f:id:a-tomi:20200805210611j:plain

以上のように、必要部分だけを勝手なRYOプログラムにすると、Arduinoの容量はずいぶん節約されて、UNOでは通常は難しいことも、余裕でできてしまいます。1例としては次の記事などをご参照ください。

テレワークは換気に注意ーArduinoでCO2ロガー(その2):赤外線吸収型 - 勝手な電子工作・・

 

資源の減らすだけでなく、このようなRYOプログラムなら通信のトレースも簡単にできます。

次の例は、マイコンとデバイスの間のシリアル通信でのやりとりを、Arduinoのプログラムで外部から観察したものです。

f:id:a-tomi:20200812172946j:plain

 プログラムは分析相手に合うように適当に加工すればよいものですが、ここで用いたスケッチは次のものです。

/*  Roll-Your-Own software-serial-communication Receiver
    Initial version V.00  Aug. 12, 2020 (c) Akira Tominaga
*/
#define tmChk 6                   // timing checker for adjustment use
#define Line1 7                   // Line1 to analyze
#define Line2 8                   // Line2 to analyze
#define sTuR 104-10               // time unit (9600bps) for receive
#define sTuhR 51-21               // time unit / 2 for receive
byte rByte;                       // character receiving byte
#define numB  128                 // number of bytes to be analyzed
byte rB[numB * 2 + 5];            // analyzed data accumulation area
uint16_t rBi = 0;                 // accumulation area index

void setup() {  // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial
  pinMode(tmChk, OUTPUT);         // timing checker as output
  digitalWrite(tmChk, LOW);       // set it LOW
  pinMode(Line1, INPUT_PULLUP);   // Line1 pulled-up
  pinMode(Line2, INPUT_PULLUP);   // Line1 pulled-up

  Serial.println("Analyzing...");
}

void loop() {  // ***** Arduino loop *****
  if (digitalRead(Line1) == LOW) sRcv1();
  if (digitalRead(Line2) == LOW) sRcv2();
  if (rBi > numB * 2) {
    byte Lnosv = 0x03; // set temp value (not 01 or 02)
    for (rBi = 0; rBi < numB * 2 - 1; rBi = rBi + 2) {
      byte Lno = rB[rBi];
      byte Dat = rB[rBi + 1];
      if (Lnosv == Lno) {
        Serial.print(" "); Serial.print(Dat, HEX);
      } else {
        Serial.println(); Serial.print("Line=");
        Serial.print(Lno, HEX); Serial.print(" ");
        Serial.print(Dat, HEX);
        Lnosv = Lno;
      }
    }
    while (1) {}
  }
}
/**************************************
      User defined functions
* *********************************** */
#define MSb B10000000             // Most-Significant-bit
#define nMSb B01111111            // not Most-Significant-bit

// *** sRcv1 *** receive Line1 charater in rByte
void sRcv1(void) {
  // while(digitalRead(Line1)==HIGH){} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH);    // sampling-time-checker on
    digitalWrite(tmChk, LOW);     // sampling-time-checker off
    if (digitalRead(Line1) == HIGH) { // if Line1 high, then
      rByte = rByte | MSb;        // set MSb on
    } else {                      // else
      rByte = rByte & nMSb;       // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(Line1) == LOW) {}; // wait until HIGH (stop bit)
  rB[rBi] = 0x01; rBi++;
  rB[rBi] = rByte; rBi++;
}

// *** sRcv2 *** receive Line2 charater in rByte
void sRcv2(void) {
  // while(digitalRead(Line2)==HIGH){} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH);    // sampling-time-checker on
    digitalWrite(tmChk, LOW);     // sampling-time-checker off
    if (digitalRead(Line2) == HIGH) { // if Line1 high, then
      rByte = rByte | MSb;        // set MSb on
    } else {                      // else
      rByte = rByte & nMSb;       // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(Line2) == LOW) {}; // wait until HIGH (stop bit)
  rB[rBi] = 0x02; rBi++;
  rB[rBi] = rByte; rBi++;
}

/* End of program */

アナライザープログラムとしてのこの使い方は、ピン7、ピン8を調べたいシリアルラインに接続するだけです。もちろんグランドの接続は前提ですし、クロックスピードは相手に合わせられる範囲だけですが。

 

以上で簡単な説明を終わりたいと思いますが、他のマイコンでも似たようなものですね。ご質問いただいた方のお役に立てば幸いです。

 

©2010 Akira Tominaga, All rights reserved.