アフィリエイト広告
アフィリエイト広告

UART 受信回路をロジックICでつくる

シリアル通信といえば、やっぱりなんといっても UART通信じゃないでしょうか。今回は Arduino から出力される UART通信を受信するための回路を、ロジックIC で作ってみようと思います。

なお、「UART (Universal Asynchronous Receiver Transmitter)」という呼び名についてはいろいろ定義があるようですが、ここでは「調歩同期式非同期シリアル通信」のことと解してください。もっというと「Arduino が送受信しているシリアル通信」ということで。

シリアル通信とか調歩同期式とか非同期通信とかについては、ググってください。詳しい解説をしてくださるグーグル先生がたくさんいらっしゃいます。基本的な回路構成などについて「RS-232C通信ボードをロジックICのみで作った」を参考にさせていただきました。ありがとうございます。

Arduinoからのシリアル通信出力を確認する

使用するのは Arduino Nano Every です。シリアル通信出力がどうなっているのか、次のスケッチを動かして、オシロスコープで観察してみます。

  1. void setup() {
  2.   Serial1.begin(9600, SERIAL_8N1); // Connect to Tx1 pin
  3. }
  4. void loop() {
  5.   Serial1.print('A');
  6.   delay(500);
  7. }

Arduino Nano Every では、USBにつながるポートは Serial、シリアルポート Tx1、Rx1 は Serial1 です。そのため、USBをパソコンに挿したままでも、シリアル通信が利用できます。

setup() で、伝送速度を 9600bpsとし、コンフィグに SERIAL_8N1 を指定しています。8N1 はデフォルト値ですので通常は省略していますが、明示的に描いておきました。8N1 とは「データ 8ビット、パリティなし、ストップ 1ビット」を表しています。
loop() で、文字 A を 500msごとに送出しています。そのときのシリアル通信出力は図1 のとおりでした。

シリアル通信出力 (print() のとき)
図1. シリアル通信出力 (print() のとき)

9600bpsなので 1ビットの幅は 104.2μsです。

最初がスタートビット ST の 0。次にデータビットが LSBファーストで 8ビット出力されています。文字 A のアスキーコードは 0x41 です。最後にストップビット SP の 1 がきて通信終了。
全体で 10ビットになっています。

次は、改行コードが付加される println() で送出してみました。(図2)

  1.   Serial1.println('A');

シリアル通信出力 (println() のとき)
図2. シリアル通信出力 (println() のとき)

文字 A (0x41) に続けて、制御コードの CR (0x0d) と LF (0x0a) が続いています。
CR (Carriage Return) は行の先頭に戻る、LF (Line Feed) は 1段行送りする、ということ。昔の機械式タイプライタの操作が、いまでもしっかり残っているんですね。

ともあれ、println() 関数では送出文字のあとに CR+LF が付加されて 3キャラクタになっています。

ついでなので、パリティビットありの 8E1 を試してみましょう。(図3)

  1.   Serial1.begin(9600, SERIAL_8E1);

シリアル通信出力 (パリティビットあり)
図3. シリアル通信出力 (パリティビットあり)

データビットのあとにパリティビット PA が立ち、全体で 11ビットになりました。8E1 は偶数パリティですので、文字 A と LF では 0、CR では 1 になっています。
ちなみに 8O1 とすると奇数パリティになるので、パリティビットは反転します。

その他、コンフィグではビット数を 5~8、ストップビットを 1~2 に設定できます。マニュアルを参照ください。

タイミングダイヤグラム

UART通信の調歩同期は、スタートビットを検出して通信を始めますので、スタートビットの検出をできるだけ速やかに行なう必要があります。そのため、伝送速度の 6〜10倍以上のクロックでサンプリングするのが良いとされているようです。今回は 9600bpsで通信を行ないますので、クロック周波数はその 16倍の 153.6KHzとしました。

図4 のタイミングダイヤグラムに、2キャラクタ分を連続して受信するようすを示します。

タイミングダイヤグラム
図4. タイミングダイヤグラム

スタートビット ST の立ち下がりエッジを検出したらトリガパルス TR を出力します。TR によりカウンタ制御信号 LOAD を HIGH にしてパルストレイン回路のカウンタをスタートさせます。

カウンタは 8ビットで、0x61 (97) を初期値として 0xff (255) まで 0x9e (143) カウントさせます。こうすることで、カウンタ出力の 4番目が、153.6KHzを 16分周した 9600Hzとなり、0x9e カウントの間に 10個のパルスを出力できます。また、出力されたパルスの立ち上がりエッジは受信データの中央に位置し、受信マージンが保てます。
カウンタ出力が 0xff になったら停止信号 SUPPR を出力し、LOAD を LOW にしてカウンタを停止します。このときカウンタは初期値 0x61 をロードして待機します。

出力された 10個のパルストレインをシリアルクロック SCK としてシフトレジスタへ送り、受信したシリアルデータをパラレルデータに変換します。最後に、レジスタクロック RCLK (SUPPR と同じ信号) によってシフトレジスタからストレージレジスタへパラレルデータ OD を出力します。

図5 は、スタートビット、ストップビット付近を拡大したものです。

タイミングダイヤグラム (拡大)
図5. タイミングダイヤグラム (拡大)

カウンタ制御信号 LOAD が HIGH になりカウントを開始。カウント 0x68 でシリアルクロック SCK が立ち上がり、0x70 で立ち下がります。これをパルストレインとして出力します。
0xff でカウンタはキャリー CO を出力するので、これを停止信号 SUPPR として LOAD を LOW に。次のクロックでカウンタは停止し、初期値 0x61 をロードします。

あ、しかし、いまこの記事を書きながら気づいたのですが。
これでは、もしボーレート誤差があると、次のスタートビットの検出が間に合わなくなる可能性があります。ちょっと検討が必要ですが、まぁ、今回はこのままいきましょう。(後記で触れます)

ブロックダイヤグラム

図6 は、回路構成のブロックダイヤグラムです。

ブロックダイヤグラム 
図6. フロックダイヤグラム

UART通信のテストデータ TxD は Arduino Nano Every の Tx1 から出力させます。

受信した UART通信データ RxD から、スタートビット検出回路でトリガパルス TR を出力します。シリアルクロック発生回路で 10個のパルストレインを発生させ、シフトレジスタを駆動してパラレルデータに変換、受信データを出力します。
初期化回路は、各カウンタ、フリップフロップの電源オン時の初期化を行ないます。
クロック発振回路は、データサンプリング用の 153.6KHzクロックを発生させます。さらに、シリアルクロック発生回路で 16分周し 9600Hzとします。

テスト用の周辺回路として、バッファレジスタを2段設け、シフトレジスタのストレージレジスタと合わせて、10ビット x 3キャラクタをLED表示できるようにします。

回路図

各部の回路図です。

初期化回路

初期化回路
図7. 初期化回路

毎度おなじみの初期化回路です。

R3、C4 のタイマにより、電源オン時に 100msの初期化パルスを出力します。
R4 はインバータの保護抵抗です。

クロック発振回路

伝送速度を 9600bps とし、その 16倍のクロック周波数 153.6KHz を得るための発振回路です。

秋月電子通商で探してみると、19.6608MHz という、まさにそのための水晶発振子がありました。これをアンバッファインバータ 74HCU04 で発振させ、14段リプルカウンタ 74HC4020 で 128分周 (7段) して 153.6KHz とします。

クロック発振回路
図8. クロック発振回路
段数分周比クロック
(KHz)
伝送速度
(bps)
951238.42400
825676.84800
7128153.69600
664307.219200
532614.438400
4161228.876800
表1. 分周比と伝送速度

この分周比を変更することで、他の伝送速度とすることができます。(表1)

76800bps は一般的な伝送速度ではありませんが、Arduino Nano Every で指定することができます。ちなみに、この速度でも問題なく通信できました。

115200bpsは? UART IC 買いなはれ (推奨)。
14.7456MHz のオシレータなら秋月電子通商にあるけど (やる気かよ)。

ちなみに、ボーレート切り替え回路をつけるなら、Dipスイッチで切り替えるマルチプレクサを使いましょうか。

スタートビット検出回路

SPI通信のときに使ったのと同じ、立ち下がりエッジ検出回路です。受信データ RxD の立ち下がりエッジを検出し、トリガパルス TR を出力します。TR はスタートビットだけでなく、受信信号のすべての立ち下がりエッジで出力されます。

(2023.11.23 追記)
正誤) トリガパルスの信号名として、TGTR の両方が混在しています。同じ信号として読んでください。
スタートビット検出回路
図9. スタートビット検出回路

この回路は、送信側と受信側をつなぐ部分です。非同期な信号を受けるとき、もし入力が変化しているタイミングでサンプリングしてしまうと、フリップフロップの出力が不安定な状態 (メタステーブル) になってしまい、誤動作をおこします。本来は、先頭にもう 1個 Dフリップフロップを置いてメタステーブルを防止しないといけないのですが、怠けてます。
もし、原因不明の誤動作をするようなときは、このあたりも疑ってみるとよいでしょう。

シリアルクロック発生回路

9600Hzの 10個のパルスを出力するパルストレイン回路です。基本的には SPI通信で使ったパルストレイン回路とおなじものですが、16分周回路を兼ねているため、ちょっと複雑です。

シリアルクロック発生回路
図10. シリアルクロック発生回路 (図中、信号名に誤りがあります。 誤:TG 正:TR)

Dフリップフロップ U8B は TR でセット、SUPPR でリセットされる SRラッチです。
SPI通信のときはもう 1個 Dフリップフロップを入れていましたが、カウンタの LD 入力がクロックに同期しているので不要だと気づきました。なので、なくしました。

カウンタは 4ビット同期カウンタ 74HC161 をカスケード接続し、8ビットカウンタとしています。TR で SRラッチがセットされ、LOAD が HIGH になるとカウントを開始します。1段目 U9 の出力 QD が 16分周された 9600Hz ですので、これをシリアルクロック SCK として出力します。

カウンタの初期値は 0x61 ですので、データ入力 A B C D は U7 を 0x6、U9 を 0x1 にそれぞれ設定しています。
ちなみに、初期値を 0x51 とするとパルストレインが 11個になるので、パリティビットありに対応できます。切り替えするなら、データ入力端子に Dipスイッチをつけるなりすればよろしいかと。

ゲート U6B が必要な理由

0xff までカウントすると 2段目 U7 がキャリー CO を出力します。この信号で SRラッチをリセットすれば良いのですが、ここで問題が生じました。カウントが 0xef から 0xf0 に遷移するとき CO にハザードが発生、それが SRラッチをリセットしてしまうのです。

カウンタをカスケード接続した場合、2段目 U7 はイネーブル ENT が HIGH の間に 0xe から 0xf に遷移します。そのとき、ENT が LOW になるまでの一瞬だけ CO が出力されます。これはカスケード接続のしくみ上、しかたがありません。
そこで、このハザードの影響をなくすために、1段目 U9 が 0xf であることをリセットの条件に加えることにしました。実際には、U9 のどれか 1つのビットの HIGH を確認すれば良いので、SCK と CO を NANDゲート U6B に通し、停止信号 SUPPR としています。

データ受信回路 (シリアル・パラレル変換)

シフトレジスタ 74HC595 によるシリアル・パラレル変換回路です。

データ受信回路 (シリアル・パラレル変換)
図11. データ受信回路 (シリアル・パラレル変換)

10ビットを出力するために 74HC595 をカスケード接続しています。

通信が正しく行なわれるためにスタートビットが 0、ストップビットが 1 だと確認することも必要なのですが、電子工作的にはデータビットのみでもいいんじゃないでしょうか。つくっておいてこんなこと言うのもなんですけど。
データビットのみを出力するなら、シリアルクロック発生回路のカウンタ初期値を 0x71 として、パルストレインを 9個にすればよいです。そうすると、ストップビットも (パリティビットも) 読まないし、スタートビットは QH´に捨てればよいので、シフトレジスタを 1個にできます。

SRCLROE に初期化信号を入れていますが、このあたりはテキトーなので、電源オン時に出力がオール 1 になったりならなかったりします。あしからず。シフトレジスタの初期化、うまい方法を考えたいですねぇ。

データ表示LED回路 (テスト用)

データ表示LED回路 (テスト用)
図12. データ表示LED回路 (テスト用)

シフトレジスタの出力を 10個の LED列で表示する、受信データ確認のための回路です。

LED電流値は 0.8mAです。いつもは 0.5mAにしているのですが、瞬間的な点滅がわかるように、すこし多くしています。

バッファレジスタ回路 (テスト用)

バッファレジスタ回路、3キャラクタを表示できるように用意したテスト用回路です。8回路 Dフリップフロップ 74HC377 を使っています。

バッファレジスタ回路 (テスト用)
図13. バッファレジスタ回路 (テスト用)

ようするに、Dフリップフロップを 10個並べて 10ビットのデータを記憶できるようにした回路。同じものがシフトレジスタ 74HC595 の出力側にも内蔵されています。それと合わせて 3キャラクタを順送りに記憶させています。
Arduino Nano Every が文字 A と制御コード CR、LF を送ってくると、それぞれのビット列を LEDが表示し、確認できます。

テスト信号発生回路

UART受信回路のテストを行なうために、テスト信号を発生させる回路をつくります。
Arduino Nano Every からの信号の受信と、パソコンからの信号の受信も試してみました。

Arduino Nano Every からの受信

図14. Arduino Nano Every 接続図

Arduino Nano Every からテスト用のシリアル信号を出力します。

送信端子は Tx1 です。Arduino UNO とは異なり、USB と同時に使用が可能です。

ダイオード D1 は、電源の逆流防止用です。1N4007 を使っていますが、30V 1A もあれば十分ですので、部品箱にある適当なものを使ってください。
電源電圧が 12Vになっていますが、9V程度がおすすめです。ちなみに、+5V出力端子に 5V突っ込むのはお勧めしません。壊れても、しりません。

スケッチです。

  1. void setup() {
  2. // Serial.begin(9600); // Connect to serial monitor
  3.   Serial1.begin(9600, SERIAL_8N1); // Connect to TxD(D0) pin
  4. }
  5. void loop() {
  6.   for (byte value = 0x00; value <= 0xff; value++) {
  7. // Serial.println(char(value));
  8.     Serial1.write(value);
  9.     Serial1.print('\r'); // CR 0x0d
  10.     Serial1.print('\n'); // LF 0x0a
  11.     delay(500);
  12.   }
  13. }

2行目と 9行目をアンコメントすると、シリアルモニタへも出力できます。確認用にどうぞ。
3行目がシリアル通信の接続で、9600bps 8N1 です。
11行目。発生させた 0x00~0xff のアップカウントデータ value を、Serial1.write() でそのまま送信しています。12行目で制御コード CR を、13行目で LF を、連続して送信します。以上の 3キャラクタを 500ms間隔で繰り返し送信します。

Arduino が 9600bps をどのように生成しているのか知らないのですが、ボーレート誤差はなく、正常にシリアルデータを受信できることを確認しました。また、この受信回路の最高速度 76800bps でも問題なく受信できました。

パソコンからの受信

図15. USBシリアル変換モジュール接続図

パソコンからの受信もためしてみましょう。

パソコンのシリアル通信ポートは USB ですので、UART に変換しなければいけません。秋月電子通商で購入した USBシリアル変換モジュール AE-FT234X を使うことにしました。

このモジュールの出力 TxD は 3.3V なので、バッファ 74HCT244 を利用して 5V へレベル変換しています。74HCT244 は、ジャンク箱にあった入力が TTLレベルのバッファ IC です。
レベル変換はトランジスタでも TTLゲート でもなんでもいいです。利用できるものをうまく使いましょ。

データ送信は Arduino IDE のシリアルモニタからできます。簡単だけど、いちいち送信ボタンを押さないといけないのが面倒です。また、制御コードは単独では送れません。プルダウンリストから「CRおよびLF」とか選択すると、改行コードが付加されます。

ubuntu では端末から cu コマンドで送信できます。古い記事ですが、こちらも参照ください。

$ cu -s 9600 -l /dev/ttyUSB0
Connected.

送信したデータが送り返されるエコーがありませんので、パソコン側でキーを押しても無反応ですが、LED表示は押したキーのアスキーコードを表示します。ちなみに、制御コード CR (0x0d) は Ctrl+m、LF (0x0a) は Ctrl+j で送信できます。
終了は、Ctrl+c のあとに ~. です。

~.
Disconnected.

Windows ならターミナルエミュレータソフトの Tera Term とか利用するのが簡単なんじゃないでしょうか。ネットワークエンジニアにはおなじみの定番ソフトです。

ということで、パソコンからのシリアル通信もうまく受信できました。

後記

今回は、Arduino Nano Every から送出されるシリアル通信データを受信する回路を、ロジックIC でつくってみました。

「調歩同期」が理解できなかった

調歩同期式非同期シリアル通信について、その概要を学んだのはかなり昔のことです。でも、具体的にどうして送信側と受信側のボーレートが同期できるのか、ずっと曖昧模糊としていました。それが理解できたのは最近、「クロックドメインが異なる回路を同期させるには、入力信号より短い周期でサンプリングしなければならない」ことに気づいたときでした。

わかっている人には当たり前のこと。でも、当たり前のことに気づいていないのが、俺、なのです。

ボーレート誤差への対応について

この記事を書いている途中に気づいた、ボーレート誤差があったときの対応 * について、ちょっと考えておきましょう。

ボーレート誤差とは送信側と受信側のボーレートクロックの誤差で、もし送信側の周期が短いと、次のキャラクタのスタートビットが早くきてしまい、読み落としてしまう可能性があります。一般にボーレート誤差は 2~3%以内とされていますので、5%程度早まっても、次のスタートビットが読み込めるようにしないといけません。
今回の回路では 7クロック分にあたりますので、カウンタ停止信号 SUPPR をカウント値 0xf8 で出力し、データ半ビット分だけ早くすればいいんじゃないでしょうか。で、0xf8 を捉えるには、下位カウンタ U9 のキャリー CO を上位カウンタ U7 のイネーブル ENP に入れれば 0xf0~0xff 間で CO を出力するので、そいつと SCK を AND すればいいはず。
あれ? つまり、U7 の ENT と ENP を入れ替えるだけじゃん?

ってことで、即やってみたら、うまくいった。みたいだ。


さてと、そんなこんなで UART受信回路ができあがりました。となると、今度は送信回路をつくらないといけませんねぇ。一休みして、体力が回復したら、ボチボチやってみます。

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