title.png


第4回 シフトレジスタを使ってみる

 キットの中に、1つ、16ピンDIPのICが入っていました。型番は74HC595、調べてみると8ビットシフトレジスタでした。
 なるほど、そっか、シフトレジスタね、だけど、7SEG LEDはキットの中に入っていないのね……。まあ、一番初歩のキットだし、仕方のないところ。
 今回はシフトレジスタを使って、もう少し、ArduinoによるI/Oポート制御に関する理解を深めたいと思います。

シフトレジスタ

 本物の回路図を見たい方は、74HC595で検索して、データシートをご覧になってください。このページで勝手に引用するわけにもいかないので、簡単な回路図を起こしてみました。

ar4_595sch.png

 74HC595は、8ビットのシフトレジスタなのですが、さすがに面倒なので、上の回路図は3ビット分しか書いてません。本物は、出力がQA〜QHまで8本ありますので、そのように読み替えてください。

 左列に並んでいる四角い素子SR-A〜Cがシフトレジスタです。四角い素子をフリップフロップ(F/F)と呼びます。
 クロックSRCLKの立ち上がりエッジ毎に、F/Fに記憶された値がA→B→Cの方向に順次シフトします。SR-Aには、SRCLKが立ち上がった時のSERの値が記憶されます。
 右列に並んだF/F、R-A〜Cに供給されているクロックRCLKは、シフトレジスタのクロックSRCLKと独立しているので、RCLKを供給しなければ、出力QA〜QCが変化することはありません。
 リセット信号~SRCLRに0を印加すると、SR-A〜Cが一斉に0に初期化されます。
 出力許可信号~OEに0を印加すると、出力信号がQA〜QC端子に出てきます。~OE=1の間は、QA〜QC端子は、出力端開放状態(オープン、ハイインピーダンス、HIGH-Z)となります。

 必要なデータを、SER端子とSRCLK端子を使ってシフトインできたところで、一発だけ、RCLKを供給(立ち上がりエッジを入力)してあげると、SR-A〜Cのデータが一気にR-A〜Cに取り込まれて、QA〜QC端子に出力されます。
 なお、QC’は、シフトレジスタをカスケードに接続するためのシフト出力ですので、次段のSER端子に接続します。カスケードに接続しない場合は未使用です。

7SEG LED

 74LS595の具体的な使い方として、QA〜QH端子にLEDを接続するケースで考えましょう。
 普通、8個のLEDを光らせようとすると、「第1回 Arduino初心者スタータキット到着」で作った回路のように、1つのLEDに対して1つのI/Oポートが必要になります(独立に制御する場合)。ArduinoのI/Oポート数は限られているので、LEDを光らせるだけで8本もI/Oポートを使ってしまったら、他の制御がままならないわけです。まして、LEDが8個→16個→24個→32個と増えて行ったら、「I/Oポートが足りないー!」と、なってしまいます。

 「そんなに沢山のLEDを使うか?」って、ことですが、用途によっては使うんですよ。7SEG LEDという表示装置をご存知ですか?

ar4_7seg.png

 上図のような形で、細長いLEDが7つ集合した表示装置です。どのLEDを光らせて、どのLEDを消灯するか?をうまく制御してやると、点灯させた形で、1〜9の数字を表示できる装置になります。ちょっと薄くしておいた、ピリオドがオプションでくっついているものも、たまにあります。その場合は、LEDが8個です。
 数字を、たった4桁(例えば、時計の時・分)表示したいと思った瞬間に、LEDの数は4桁×7個=28個になってしまいます。

 このような場合に、シフトレジスタを使うと、使用するI/Oポートの数を減らすことが可能なわけです。前節で示したように、74HC595には、5本の入力端子がありますが、最悪、~OE(=0)と~SRCLR(=1)は固定入力で構いません。この場合、必要なI/Oポート数は3本(SER、SRCLK、RCLK)です。

 ただし、~OE=0に固定した場合、システムの電源投入後から、Arduinoのプログラムが走って、データがシフトインされるまでの間、LEDの状態が確定しません(点灯するか消灯するか、電源投入時の状態次第で、どうなるかわからない)。無意味な表示が出力される可能性があります。この状態を許容できない場合は、I/Oポートを4本(SER、SRCLK、RCLK、~OE)使って、制御することになります。

 表示する桁数(7SEG LEDの数)を増やす場合、シフトレジスタIC(と7SEG LEDのセット)をカスケードに繋げは良いので、必要な制御信号数は変わりません。複数桁の7SEG LEDを表示に使いたい用途では必需品という訳です。
 シフトレジスタはデシリアライザ(シリアル→パラレル変換)とも呼ばれて、他にもいろいろな用途がありますので、頭の隅に入れておくと良いでしょう。

回路を設計する

 筆者が購入したキットの中には、7SEG LEDが含まれていないので、普通のLEDで代用します。数字の形にはならないので、数字が表示されるようなプログラムにはしません。数字を表示するには、それ用のエンコーダ(?デコーダ?どっち?)が必要となりますが、これは外付け回路というより、ソフトウェアで実現すべきものなので、プログラミング上の課題となります。
 さらに工夫すると、7SEG LED 1つで16進数1桁を表示することも可能ですので、機会があったら挑戦してみてください。

 回路は以下のようになるはず。

ar4_shift.png

 PowerPointで回路図引くのも大変でした(上の図)が、この回路をブレッドボード上に構成するのは、なお大変です。

 と、言うことで、少しでも手を抜きたいのです。まず、~SRCLR=1に固定してしまいましょう。青い回路(入力プルアップ)も、短時間の動作なら問題ないので、省略します。

  ダイオードの電流制限抵抗Rも、短時間の動作確認だけなら、きっとなくても平気?いや、さすがにそれはダメか。

 キットに入っていたのは、SN74HC595Nという型番で、テキサスインスツルメンツ社(TI)のICでした。にもかかわらず、キットのCD-ROMに入っていたデータシートはPHILIPS社の物……。なので、TIのデータシートを探し出して確認したところ、絶対最大定格(Absolute Maximum Ratins)という電気的特性スペックに、Io(Continuus output current)が35mAとありました。さらに、Continuous current through VCC or GNDの規定が70mAでした。

※74シリーズだし、論理的には一緒、電気的特性も一緒かもしれませんが、念のため。

 はい、読み解きましょう。「絶対最大定格」とは、「何があっても、この値以上で使ってはダメ」という値で、指定値を超えて使った場合、ICが壊れたり、周辺装置にも悪影響を与えるかもしれない、という警告です。
 Io=35mAなので、1つの端子に流して良い電流は最大35mAですよ。
 さらに、VCC、GNDに流して良い電流が70mAなので、全端子の合計電流で70mAを超えてはダメですよ。

 すなわち、Ioの規定で、R>Vcc/Io=5V/35mA≒142.9Ωが必要。
 さらに、70mAの規定を1端子に読み替えて、Io’=70mA/8本≒8.75mAなので、同様にR>571Ωが必要。
 この計算は出力端子の電流しか入っていないのと、そもそも「絶対最大定格」は、何があっても超えてはならないスペックなので、スペックぎりぎりで使うべきものではない、ことから、マージンを持って使う必要があります。
 Rには571Ωにマージンを持たせて、1〜10[kΩ]くらいで使うのが良いでしょう。

 もし、お手元のICが他社製であれば、絶対最大定格の数値が異なる可能性がありますので、確認して使うか、あるいは、多少の違いがあっても問題にならない程度の十分なマージンを持って、Rの値を決めてください。

 と、いうことで、完成した回路の写真がこちら↓。

ar4_circt.jpg

 配線に隠れてしまって見えてませんが、ブレッドボードの右端に74HC595が配置してあります(写真は、フライングで16進数0xaaを表示したところです)。
 でも、作ってみたら、意外と簡単でした。ブレッドボード偉大です。ユニバーサル基板に半田付けだったら、こう簡単にはできませんでしたね。

データをシフトインする

 シフトレジスタの解説で説明したように、SERとSRCLK端子を制御して、必要なデータをシフトレジスタにシフトインします。具体的には、セットしたいデータをQ[7〜0(ビット)]で表すと、以下の様になります。

 この一連の作業を関数にして持っておきます。引数としてQの値をintで引きます。Qの値は8ビット整数で、下の桁から(LSBファーストで)出力します。

  void clock_pulse (
  int pin,
  int usec)
  {
    // generate clock pulse at #pin
    // pulse shape: usec LOW - usec HIGH - return to LOW
    digitalWrite (pin, LOW);
    delayMicroseconds (usec);
    digitalWrite (pin, HIGH);
    delayMicroseconds (usec);
    digitalWrite (pin, LOW);
  }
  void shift_in (
  int Q,
  int usec)
  {
    // shift in 8 bit Q data
    // order of shift data: from LSB(bit 0) to MSB(bit 7)
    // clock pulse: usec LOW - usec HIGH

    // shift in
    for (idx=0; idx<8; idx++)
    {
      digitalWrite (pin_SER, Q&0x01);
      clock_pulse (pin_SRCLK, usec);
      Q=(Q>>1);
    }
    // latch
    clock_pulse (pin_RCLK, usec);
    // output
    digitalWrite (pin_OEN, LOW);
  }
  void setup (void)
  {
    // pin mode
    pinMode (pin_SER, OUTPUT);
    pinMode (pin_SRCLK, OUTPUT);
    pinMode (pin_RCLK, OUTPUT);
    pinMode (pin_OEN, OUTPUT);
    // initial
    digitalWrite (pin_SRCLK, LOW);
    digitalWrite (pin_RCLK, LOW);
    digitalWrite (pin_OEN, HIGH);
  }


 ちょっと、Arduinoのドキュメントを読んでもわからなかったのですが、delayMicroseconds()は、どうやって時間を計測しているのでしょう?途中で割り込みとか入らないのでしょうか?まあ、多少時間が伸びる分にはOKというスペックで使うのが無難ですかね?さすがに、指定時間より短い時間しかdelayしなかった、なんてことはないのでしょうから。

信号波形を決定する

 ざっくりプログラム(関数)は起こしてみたものの、前節で示した関数の引数usecに与える時間を確定する必要があります。74HC595のデータシートを見てみましょう。クロックパルスのHIGH幅とLOW幅の時間規定twがあります。5Vで使用する場合は、最小17nsで最大規定はありません。

 そうか、そういうことですか。単位がns(=10-3μs)なんですね。delayMicroseconds()とは、まったくオーダが合わない。さて、どうしたものか。16MHzのArduinoでは≒16MIPSですので1命令の実行時間が1/16MHz=0.0625μs=62.5nsです。つまり、I/OポートをHIGH出力し、次の命令で最速でLOW出力に切り替えても、パルス幅は精々62.5ns前後ということで、twの規定は余裕で満たしてしまうのですね。

 念のため、ATmega328の電気的特性も確認しておきましょう。I/Oポートのトランジションタイム、っと……無いですね。念のため確認しておこうと思ったのですが……。参考までに、同じI/Oバッファを使っていると思われる、SPIのRise/Fall timeがtypで3.6ns。やはりnsオーダ、全く問題ないみたいですね。

 と、言うことで、delayMicroseconds()を削除してしまいましょう。

  void clock_pulse (
  int pin)
  {
    // generate clock pulse at #pin
    // pulse shape: LOW - HIGH - return to LOW
    digitalWrite (pin, LOW);
    digitalWrite (pin, HIGH);
    digitalWrite (pin, LOW);
  }


 nsオーダに比べると、結構太い(長い)パルスが生成されているはずです。逆に、クロックパルス幅twのMAX規定がついていたら、苦労するところでしたね。
 こうなってくると、クロックの立ち上がりエッジに対するSERのセットアップ/ホールド時間も、全く問題ないですね。チェックするだけ時間の無駄でしょう。

 時に、このプログラムで、パルス幅はどのくらいになるのでしょう?オシロスコープかロジックアナライザで確認したいところですが、持ち合わせておりません。うまく計測できるかどうかわかりませんが、micros()を使って、計測してみましょう。

  プログラム:
  unsigned long t1;
  unsigned long t2;
  pinMode (2, OUTPUT);
  t1=micros ();
  digitalWrite (2, LOW);
  digitalWrite (2, HIGH);
  digitalWrite (2, LOW);
  digitalWrite (2, HIGH);
  digitalWrite (2, LOW);
  digitalWrite (2, HIGH);
  digitalWrite (2, LOW);
  digitalWrite (2, HIGH);
  digitalWrite (2, LOW);
  digitalWrite (2, HIGH);
  t2=micros ();
  Serial.println (t1);
  Serial.println (t2);
  Serial.println ((t2 - t1)/10.);


  実行サンプル:
  116
  152
  3.60

 digitalWrite()を10回繰り返して前後の時間をmicros()で計測しました。表示された数値は、Arduinoが起動してからの経過時間μsですから、前後の時間の差を取れば、計測点間の処理時間が割り出せます。
 最後の数値3.60は、計測した実行時間を10で割って、I/Oポートを1回出力するのに要した時間(digitalWrite() 1回分の実行時間)を表示しています。筆者の環境では3.6μsでした。よって、修正したclock_pulse()が生成するパルス幅はおよそ3.6μsだと考えられます。

表示する数値?

 そろそろ、実際に動作させてみますよ。どんな数値を表示させるか?ですね。0xaa→0x55とか、0x33→0xccとか?テストとしては、良いでしょうね。ここでは、もう少し、PCとの連携を意識してみましょう。「第3回 非同期シリアル通信をPCからモニタする」で作った、PCからシリアル経由でデータを受け取る方法を応用し、PCから送られてきた2桁の16進数を、8つのLEDを光らせて表示することにしましょう。

 結構大きいコードになって来たので、リンクしておきます。

  プログラム : shift_8led.ino

 Arduinoに転送して実行したら、シリアルモニタウインドウを開いて、上段の入力フィールドに16進数2桁を入力し、「送信」してみてください。送信した数値を2進数に読み替え、値が1の桁に相当するLEDが光って、0に相当するLEDが消灯すれば、期待通りです。次々送信すれば、その都度、LEDの光り方が変わります。
 送信する16進数は、0〜9の数字と、a〜fのアルファベット小文字で構成されている必要があります。大文字のA〜Fでは認識しませんので、注意してください。

  送信データ:「55」 期待値:_○_○ _○_○
  送信データ:「aa」 期待値:○_○_ ○_○_
  送信データ:「f0」 期待値:○○○○ ____
  送信データ:「81」 期待値:○___ ___○
  送信データ:「33」 期待値:__○○ __○○
  送信データ:「cc」 期待値:○○__ ○○__

shiftOut()を使う

 自力でコードを起こしながら、気づいては、いました。Language ReferenceにshiftOut()という関数が用意されていることに。
 筆者は、シフトレジスタ側の立場に立って、「シフトレジスタICに、shift inする」と言う関数名にしたのですが、Arduinoに準備された関数は、Arduino側の立場に立って、「Arduinoから、shift outする」と言う関数名になっています。立ち位置の違いから、呼び名が異なってしまいましたが、同じ目的で使用可能な関数です。
 と、言うことで、以下の置き換えが可能です。

  shift_in (xtoi (lbuf));

    ↓

  shiftOut (pin_SER, pin_SRCLK, LSBFIRST, xtoi (lbuf));
  // latch
  clock_pulse (pin_RCLK, usec);
  // output
  digitalWrite (pin_OEN, LOW);


 筆者の関数shift_in()では、シフトインした後でラッチと出力許可もしているため、その分は別途実行する必要があります。
 シフトインに必要な時間を計測してみましょう。shift_8led.inoでは、常にシフトインにかかった時間を計測していて、2桁の16進数を送信する代わりに文字列"st"を送信すると、計測した時間を表示するようになっています。また、#defineでshiftOut()を使用するように変更できるようにしてあるので、両者の実行時間を計測して比べてみます。

  計測サンプル:
  shift_in() : 151μs/1回
  shiftOut() : 119μs/1回

 Cでポートをたたいているshift_in()より、IDEが持っていたshiftOut()の方が、2割ほど短い時間でシフトインできているようですね。7SEG LEDの桁数が増えてくると、パフォーマンスも気になってきますので、shiftOut()を使った方がベストな様です。
 教訓は、IDEが持っている機能を自前で起こしても時間の無駄、ということでしょうか?

 もう、練習は十分でしょう。いよいよ次回から、最終目標「部屋の気温と湿度のログを自動取得する」に向けて歩みを進めます。


btn_arduino.png

btn_prev.png btn_next.png

home