序文
マイコン初心者の筆者が、作業を始めてから3日くらいかかり、何とか一応意図通り動くところまで漕ぎ着けた軌跡の話。
筆者は30代半ばにして、初めてマイコンを触った。なんとなくのイメージだが、(趣味で)人がマイコンを初めて触るという経験をするのは10代が多い気がする。
10年くらい前にマイコンを始めようと思って買った参考書(そのまま10年放置状態だったがw)がアセンブリ言語での説明で、今回の開発はこの本を大いに参考にしたのでこのブログも説明は基本的にアセンブリ言語で行う。筆者が参考にした本を下記に示す。以下「参考書」という。
筆者のレベルと想定する読者
筆者は、学生時代に電気、電子回路のことは一応学んだが、殆ど忘れていて基本的な事しか覚えていないというレベル。例えば、トランジスタ、ダイオード、コイル、コンデンサ等の素子がどういう動きをするか位は大まかに分かる。電気に関する簡単な計算は出来る。しかし、複素数、微分、積分、行列を使うか、それ以上のレベルの計算のやり方は忘れた。
プログラミングのレベルとしては、高級言語プログラミングの基本的な事は分かる。例えばExcel VBAでちょっとしたアプリケーションは作れる。しかし、C言語は学生の頃に少し習ったが、殆ど完全に忘れている。何故わざわざアセンブリ言語で開発しているのかと言えば、昔たまたま買ったマイコンの参考書がアセンブリ言語で書かれていて、C言語を勉強するのが面倒臭いというのが大きいw
なので、想定する読者も基本的な電気の知識はあり、高級言語プログラミングの基本的な事は知っている(変数、条件分岐、ループ等その辺の概念は分かる)と言う前提で話を進める。
回路の概要
ロータリーエンコーダの出力をマイコンで受け取るにはどうすれば良いかいろいろ検索したのだが、上位にヒットするのは大抵ArduinoかPICの情報で、言語はSketchと呼ばれるC言語に似たもの(PICはC言語)だ。AVRのアセンブリ言語で開発しようとしてる筆者にはプログラミングとしてはあまり参考にならない。
この記事は、AVRをアセンブリ言語で開発し、ロータリーエンコーダの出力を受け取るにはどうすればいいか示すのを目的にしている(そんな需要あるのか?w)。今回はロータリーエンコーダがCWかCCWのどちらの方向に回ったかだけを判定し、速さなどは問わないものとする。電子ボリュームやセレクタとして使うことを想定している。以下に、ブレッドボードで組んだ回路の写真と全体回路図を示す。
写真1 回路をブレッドボードに組んだ写真
図1 全体回路図
ATmega328P(以下、マイコンと言う)のPORTBの全ビットにLEDを接続してある。PORTBの各ビットが変化すると、それに対応したLEDの点灯状態も変化する。始め、LEDに直列に接続している抵抗は1kΩにしていたのだが、眩し過ぎるので途中で10kΩに変更した。これでも十分に明るい。点灯しているか否か分かれば良いだけならもっと暗くても大丈夫だ。
LEDのカソード側がマイコンのピンに繋がっているので、ピンがLになると点灯する。参考書にも、マイコンのHの出力電流よりもLの入力電流の方が大きいので、ピンに接続するトランジスタやLEDはLになった時に動作するようにした方が良いと書いてあった。
ロータリーエンコーダは千石電商で売っている100円程度の安い物で、インクリメンタル方式でクリックありの物を使用した。インクリメンタル方式のロータリーエンコーダを回すとどのように出力するか分からない人は、各自で検索して欲しい。
ロータリーエンコーダのA相をトランジスタで組んだNOTゲートを通してマイコンの外部割りこみ0のピンINT0に接続する。
ロータリーエンコーダのB相は10kΩの抵抗でVccからプルアップしてマイコンのPINC0に接続する。
ロータリーエンコーダを回すと、A相の出力がH→L、L→Hと変化し、NOTゲートを通してマイコンのINT0はL→H、H→Lと変化する。
何故わざわざNOTゲートを通すのかは、後述の筆者による勘違いのためなのだが、上手く動作した後にこのNOTゲートを外したらまた上手く動かなくなったので、最終的にこのままにしてある。多分NOTゲートを外しても抵抗とコンデンサの値を適切に設定すれば上手く動くと思うがメンドクサイ。
この時、マイコンのINT0がH→Lと変化したタイミングで外部割りこみを発生させ、ロータリーエンコーダのB相が接続されているPINC0の状態により、回転方向がCWかCCWか判定する仕組みだ。
回路設計において、最終的にうまくいった要因はマイコンのピン4 INT0とGND及びピン23 PC0とGND間に接続したコンデンサC0とC1による。これを接続しないとロータリーエンコーダのチャタリングノイズを拾ってしまい、右に1クリック回した時に「右に1クリック、左に1クリック回した」動作をし、左に1クリック回すとその逆になったりする。
酷いと左に1クリック回したはずなのに右に1クリック回した動作をしたり、左に速く数クリック回すと右に数クリック回した動作をしたりと全く安定しなかった。今回の開発は3日くらいかかったが、そのうち2日以上はこの原因を取り除くのに費やしてしまった。
マイコンでロータリーエンコーダを使用する際は、ノイズ対策のコンデンサが必須。これ重要。
ノイズ対策としてコンデンサを接続すると良いという情報は下記の記事を参考にした。Arduinoの記事だが、ロータリーエンコーダからマイコンまでの回路設計と言う意味では非常に参考になった。
第二十一項 ロータリーエンコーダとノイズ対策・割り込み – kusamura
この記事ではノイズ対策として0.01μFのコンデンサを入れているが、手元に無かったので試しに0.047μFのコンデンサを入れてみた。しかし、あまり変化が無かったので、思い切って0.47μFのコンデンサにしてみたところ、容量が大きすぎて誤動作すると言うこともなく、寧ろ非常にスムーズに動作するようになった。
レジスタとは
レジスタとは何か、割り込みとは何かというレベルから説明し始めると、それこそ一冊の本が出来上がるくらいの分量になってしまうので、そこら辺を正確に知りたい人はネットや本で調べて欲しい。
筆者はアセンブリ言語が良く分からない内は高級言語の知識に絡めて以下のように理解した(アセンブリ言語の正確な概念から鑑みれば甚だ不正確な表現が含まれるのは承知いただきたい)。
- 汎用レジスタ
-
高級言語の変数のように使用する。ただし、入れられるデータ型はVisual BASICで言うところのバイト型。一つのレジスタには0~255の値までしか代入出来ない。AVRのR0~R15は制約が多いので、初心者の内は特に理由がない場合はR16から使った方が良い。
きっと、マイコンに慣れてきて汎用レジスタの中にも特殊な役割があるものを知ったり、数が足りなくなったりしたら自然とR0~R15を使うことも出てくるだろう。
- 標準IOレジスタ
-
マイコンの入出力や各機能を使用するのに用いる。ある意味高級言語の関数に近い。例えば入力のPINBというレジスタは、PBx(xは0~7の整数)のピンの値が変化すると同様に変化し、それを読み込むことでピンの値を読み取る事が出来る。
IN R16, PINB
例えば上記のアセンブリ言語のソースは、高級言語風に表現すれば
R16 = PINB()
R16という変数が存在して、PINB()はマイコンのピンB群の状態を返す関数で、その値を変数に代入する…みたいな意味だ。勿論、実際のアセンブリ言語のソースでこのように表記してもエラーになるので注意。
このレジスタに値を代入すると、定められたマイコンの機能が起動したり、機能の動きを変える事が出来る…とイメージすると良いと思う。
- 拡張IOレジスタ
-
高級言語はCPUの挙動やメモリ構造をあまり意識せずに使用出来るが、マイコンをアセンブリ言語で開発すると言うのはマイコンと言うCPUの動きそのものを命令することなので、大変だがマイコンのメモリ構造は理解していないと使いこなすことは出来ない。ここでは深くは触れないので、本やネットで頑張って調べよう。
標準IOレジスタも拡張IOレジスタも、マイコンの機能を起動したりその動きを変えたりと役割は殆ど同じ。違いは、マイコンの中でどのメモリアドレスに配置されているかという部分だ。
マイコンはメモリのあるアドレスに値を代入するという操作一つを取っても、アドレス群ごとに命令が異なる。高級言語のように何でも「変数 = 値」と代入は出来ないのだ。
何故なのかとか考えてはいけない。そういうものなのだ。
フローチャート
プログラムのフローチャートを下図に示す。
図2 フローチャート1/2
図3 フローチャート2/2
要約すると、ロータリーエンコーダが回されたらA相からの出力変化により外部割りこみ0を発生させ、B相からの出力をPINC0で読み取りCWかCCWか判定し、PORTBに接続した8つのLEDを2進数表示で点灯させる仕組みだ。
右に回すと数字が増えていき、255クリック目で全点灯状態になる。
左に回すと数字が減っていき、全点灯状態から255クリック目で全消灯状態になる。
チャタリング防止の為に回路にコンデンサを入れているが、ソフトウェアでもA相の出力変化からPINC0の値を読み取るのに約2msの遅延時間を入れて対策している。
プログラム全文
プログラム全文を下記に示す。
;ロータリーエンコーダを回してLED点灯を変化させるプログラム .include "m328pdef.inc" ;汎用レジスタ .def STACK = R16 .def R_TEMP1 = R17 .def R_TEMP2 = R18 .def R_FLAG = R19 ;定数定義 .EQU B_INT0 = 0 ;外部割り込み0 .EQU B_TMR0 = 2 ;タイマ0 .EQU B_RTY_ENC = 0 ;ロータリーエンコーダがCWかCCWか判定するビット .CSEG RJMP MAIN .ORG 0x0002 RJMP EXT_INT0 ;外部割り込み0 .ORG 0x0020 RJMP TMR0 ;タイマ0 ;外部割り込み0 EXT_INT0: ;ステータス・レジスタの内容を退避 IN STACK, SREG ;外部割りこみ0発生フラグセット SBR R_FLAG, (1<<B_INT0) ;ステータス・レジスタの内容を復帰 OUT SREG, STACK RETI ;タイマ0 TMR0: ;ステータス・レジスタの内容を退避 IN STACK, SREG ;カウンタ停止 LDI R_TEMP1, 0x00 OUT TCCR0B, R_TEMP1 ;タイマ0割り込み発生フラグセット SBR R_FLAG, (1<<B_TMR0) ;ステータス・レジスタの内容を復帰 OUT SREG, STACK RETI ;メインルーチン MAIN: CLI ;全割り込み禁止 ;PORT設定 LDI R_TEMP1, 0B11111111 LDI R_TEMP2, 0B11111111 ;全ポートを1にしてLEDを消灯させる OUT DDRB, R_TEMP1 OUT PORTB, R_TEMP2 LDI R_TEMP1, 0B11111110 ;PORTCのピン0を入力にする LDI R_TEMP2, 0B00000000 ;PORTCのピン0をプルアップなしにする OUT DDRC, R_TEMP1 OUT PORTC, R_TEMP2 LDI R_TEMP1, 0B11111011 ;PORTDのピン2はINT0なので入力にする LDI R_TEMP2, 0B00000000 ;PORTDのピン2をプルアップなしにする OUT DDRD, R_TEMP1 OUT PORTD, R_TEMP2 ;外部割り込み関連 レジスタ設定 LDS R_TEMP1, EICRA CBR R_TEMP1, (1<<ISC00) SBR R_TEMP1, (1<<ISC01) ;INT0がH→Lで割り込み発生 STS EICRA, R_TEMP1 SBI EIMSK, INT0 ;外部割り込み0許可 ;タイマ0関連 レジスタ設定 LDS R_TEMP1, TIMSK0 SBR R_TEMP1, (1<<TOIE0) STS TIMSK0, R_TEMP1 ;タイマ0割り込み許可 ;全割り込み許可 SEI MAIN01: ;外部割り込み発生フラグ OFF ;タイマ0桁溢れフラグOFF CLR R_FLAG MAIN02: ;外部割り込み判定 SBRC R_FLAG, B_INT0 RJMP MAIN03 ;外部割り込み0が発生 RJMP MAIN02 MAIN03: ;タイマ0セット(外部割り込みが発生してから2ms待つ) LDI R_TEMP1, 0x02 ;プリスケーラ8 OUT TCCR0B, R_TEMP1 LDI R_TEMP1, 0x06 ;割り込み時間2ms OUT TCNT0, R_TEMP1 MAIN04: ;タイマ0桁溢れ判定 SBRC R_FLAG, B_TMR0 RJMP MAIN05 ;タイマ0桁溢れ RJMP MAIN04 MAIN05: ;ロータリーエンコーダの回転方向を判定 SBIC PINC, B_RTY_ENC ;PINCのビット0が1ならCW、0ならCCW(PINCのビット0が0なら1行スキップ) RJMP MAIN10 ;ロータリーエンコーダの回転方向がCWの場合 RJMP MAIN20 ;ロータリーエンコーダの回転方向がCCWの場合 MAIN10: ;ロータリーエンコーダの回転方向がCWの場合 IN R_TEMP1, PORTB CPI R_TEMP1, 0x00 ;PORTBが0かどうか判定 BREQ MAIN01 ;PORTBが0なら何もせずにメインルーチンのループに戻る DEC R_TEMP1 OUT PORTB, R_TEMP1 ;PORTBが0でないならデクリメントする RJMP MAIN01 MAIN20: ;ロータリーエンコーダの回転方向がCCWの場合 IN R_TEMP1, PORTB CPI R_TEMP1, 0xFF ;PORTBがFFかどうか判定 BREQ MAIN01 ;PORTBがFFなら何もせずにメインルーチンのループに戻る INC R_TEMP1 OUT PORTB, R_TEMP1 ;PORTBがFFでないならインクリメントする RJMP MAIN01
思い返せば初歩的な部分だと思うが、つまずいた部分としては…
LDI R_TEMP1, 0B11111011 ;PORTDのピン2はINT0なので入力にする LDI R_TEMP2, 0B00000000 ;PORTDのピン2をプルアップなしにする OUT DDRD, R_TEMP1 OUT PORTD, R_TEMP2
このPORTDのピン2を入力に設定する部分。ネットを検索すると、入力に設定しなくてもピンの値が変化すれば割り込みが発生すると書いてあるが、出力に設定しているピンの値を変化させるには、普通はソフトウェア的な処置が必要だろう。
今回のように、ロータリーエンコーダやスイッチで値を入力する場合は、やはり外部割りこみを兼ねるピンは入力に設定するべきだ。
もう一つ、外部割りこみ制御レジスタの部分。
;外部割り込み関連 レジスタ設定 LDS R_TEMP1, EICRA CBR R_TEMP1, (1<<ISC00) SBR R_TEMP1, (1<<ISC01) ;INT0がH→Lで割り込み発生 STS EICRA, R_TEMP1 SBI EIMSK, INT0 ;外部割り込み0許可
ビット | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
EICRA | – | – | – | – | ISC11 | ISC10 | ISC01 | ISC00 |
表1 EICRAレジスタの構造
ISC01 | ISC00 | 割りこみ発生条件 |
0 | 0 | INT0がLow |
0 | 1 | INT0がLow→High、High→Low |
1 | 0 | INT0がHigh→Low |
1 | 1 | INT0がLow→High |
表2 外部割りこみ0発生条件
始め、このレジスタのISC01、ISC00の部分を「01」と設定すると、「INT0がLow→High→Lowとなった時に外部割り込み0が発生する」とずっと勘違いしていた。つまり、形が「凸」のような波形を受信した時に割り込みが発生すると思っていた。
実際にはISC01、ISC00を「01」と設定しているとINT0がLow→Highになった時も、High→Lowになった時も、つまり波形の立ち上がりと立ち下がりの両方で外部割り込み0が発生する。
ロータリーエンコーダを1クリック回した時に外部割りこみ0の動作を2回してしまうのは何故か分からない原因の一つだった。
回路が上手く動かなくてハマっている2日余りのうち1日余りはこのレジスタ設定の勘違いに気付いていないからだった。
ロータリーエンコーダA相の出力とマイコンのINT0の間にNOTゲートを入れたのもこの勘違いによるもの。
まとめ
ロータリーエンコーダはチャタリング対策として、マイコンの入力端子とGNDの間にコンデンサを接続しよう。
ロータリーエンコーダやスイッチで外部割りこみを発生させるときは、そのピンは入力に設定しよう。
AVRの外部割りこみに「波形が立ち上がって立ち下がった時」という発生条件は無い。
以上、マイコン初心者が実験でこの知見を得るのに3日かかった…というお話でした!w
これで、ロータリーエンコーダを値セレクタとして使う準備は整ったので、次の段階に進める。