キットの中に、1つ、16ピンDIPのICが入っていました。型番は74HC595、調べてみると8ビットシフトレジスタでした。
なるほど、そっか、シフトレジスタね、だけど、7SEG LEDはキットの中に入っていないのね……。まあ、一番初歩のキットだし、仕方のないところ。
今回はシフトレジスタを使って、もう少し、ArduinoによるI/Oポート制御に関する理解を深めたいと思います。
本物の回路図を見たい方は、74HC595で検索して、データシートをご覧になってください。このページで勝手に引用するわけにもいかないので、簡単な回路図を起こしてみました。
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端子に接続します。カスケードに接続しない場合は未使用です。
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という表示装置をご存知ですか?
上図のような形で、細長い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桁を表示することも可能ですので、機会があったら挑戦してみてください。
回路は以下のようになるはず。
PowerPointで回路図引くのも大変でした(上の図)が、この回路をブレッドボード上に構成するのは、なお大変です。
と、言うことで、少しでも手を抜きたいのです。まず、~SRCLR=1に固定してしまいましょう。青い回路(入力プルアップ)も、短時間の動作なら問題ないので、省略します。
ダイオードの電流制限抵抗Rも、短時間の動作確認だけなら、きっとなくても平気?いや、さすがにそれはダメか。
キットに入っていたのは、SN74HC595Nという型番で、テキサスインスツルメンツ社(TI)のICでした。にもかかわらず、キットのCD-ROMに入っていたデータシートはPHILIPS社の物……。なので、TIのデータシートを探し出して確認したところ、絶対最大定格(Absolute Maximum Ratings)という電気的特性スペックに、Io(Continuous 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の値を決めてください。
と、いうことで、完成した回路の写真がこちら↓。
配線に隠れてしまって見えてませんが、ブレッドボードの右端に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」 期待値:○○__ ○○__
自力でコードを起こしながら、気づいては、いました。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が持っている機能を自前で起こしても時間の無駄、ということでしょうか?
もう、練習は十分でしょう。いよいよ次回から、最終目標「部屋の気温と湿度のログを自動取得する」に向けて歩みを進めます。