おさらい

この記事は前回の記事の続きです。

前回の記事では、GPIO が 4本しか無い ATtiny10 でも ADC を使って 20ボタン識別を実現できそうだというお話でした。

最大20ボタン識別基本回路

今回の記事では実際に ATtiny10 で動かすソフトウェアを実装します。ちなみにアセンブラでいきます。

回路の仮組み

ソフトウェアのお話と宣言しておきながら回路の話ですが、使用するピンなんかを決定しておかないといけませんのでまず組んじゃいます。
こんな感じです。

20ボタンテスト回路

INT0割込みが使えるのは PB2 だけなので、BTNIN は PB2 に決定です。あとは適当に PB1 を BTNOUT に、PB0 を LED にしておきます。
LED は押したボタンによって光り方を変えて識別がうまくいったか判断するための出力用です。
ボタンは実際に 20個も置くのは面倒なので、テスト回路ではリード線で抵抗の足を触って実験することにします。

あと、ATtiny10 にはプログラムを書き込む必要があるので、そのプログラマとの接続を盛り込むとこんな感じになります。

右側にプログラマへの接続箇所が追加されていることの他に、謎のジャンパー JP が追加されています。これは、プログラミング時の信号線となる PB0 と PB1 には LED や抵抗がぶら下がっているとプログラミングの邪魔をするので、これを切り離すためのものです。通常動作ではショート、プログラミング時には開放です。もちろんジャンパーの操作以外に、動作時にはプログラマとの接続は外しておかなければなりません。

ところで肝心なことですが、ATtiny10 は極小なのでそのままではブレッドボードに載せられません。こんな変換基板を使ったりします。

秋月電子の通販ページより

私は手持ちが無かったので、万能基板のかけらでこんな感じでがんばりました。一個作っておけば何度も使えますし。

手製変換基板

何はともあれ、ブレッドボードに組むとこうなりました。

20ボタンテスト回路

ソフトウェア仕様

こんな感じで作ります。

  • 何もしていない時はパワーダウンで待機
  • ボタンが押されたら、ボタン番号を Lチカで表示
  • Lチカ終了までボタンが押され続けていた場合、その後ボタンが離されたときに Lチカで表示
  • システムクロックはタイミングがシビアそうな 8MHz で。これで動けば低クロックも動くだろう
  • せっかくなので、Lチカタイミングなど各種時間待ちには WDT を使ってみる

Lチカは、押されたボタンの番号 0~19 を 2進数 5bit で点滅させます。以下のように bit値 0 なら 1回、1 なら 2回点滅として、MSB から点滅させます。

ボタン番号点滅パターンボタン番号点滅パターン
0● ● ● ● ●10● ●● ● ●● ●
1● ● ● ● ●●11● ●● ● ●● ●●
2● ● ● ●● ●12● ●● ●● ● ●
3● ● ● ●● ●●13● ●● ●● ● ●●
4● ● ●● ● ●14● ●● ●● ●● ●
5● ● ●● ● ●●15● ●● ●● ●● ●●
6● ● ●● ●● ●16●● ● ● ● ●
7● ● ●● ●● ●●17●● ● ● ● ●●
8● ●● ● ● ●18●● ● ● ●● ●
9● ●● ● ● ●●19●● ● ● ●● ●●
OFF
Lチカパターン

ソフトウェア解説

まずはソースコードと HEXファイルを上げておきます。アセンブルしなくてもとりあえず試せます。

ソースコードと HEXファイル


解説はこのソースコードの中から主要な部分を引用しながら行いますので、全容はこれを参照してください。

各種定義

いろんな値の定義についてです。この節はあまり本質的な話ではないので、読み飛ばしてもよいと思います。

; ボタン数 2 ~ 20
.equ BTN_NUM = 20

; ピン定義
.EQU PNO_BTNOUT = 0
.EQU PNO_LED = 1
.EQU PNO_BTNIN = 2
.EQU PNO_RESET = 3

ここはボタンの数とピンの定義です。見たまんまです。ボタン数なんかは限界を超えて試してみても面白いかもしれないですね。


; ADC設定 クロック 1/64 125kHz
.EQU ADCSRA_ADC_OFF	= (0 << ADEN) | (0 << ADSC) | (0 << ADATE) |               (0 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (0 << ADPS0)
.EQU ADCSRA_ADC_ON	= (1 << ADEN) | (0 << ADSC) | (0 << ADATE) | (1 << ADIF) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (0 << ADPS0)

ADC の動作に関する ADCSRAレジスタに設定する値の定義です。ADC は変換時以外は OFF にしておくので、ON にする場合と OFF にする場合の値を定義しておきます。
ADC の変換クロックは 50~200kHz にする必要があり、今回はシステムクロックを 8MHz にするので ADCクロックのプリスケーラを 1/64 にして 125kHz を得るようにしています。


; WDT
.EQU WDTCSR_OFF		= 0	; ZERO でいい
.EQU WDTCSR_16ms	= (1 << WDIF) | (1 << WDIE) | (0 << WDE) | (0 << WDP3) | (0 << WDP2) | (0 << WDP1) | (0 << WDP0);
.EQU WDTCSR_32ms	= (1 << WDIF) | (1 << WDIE) | (0 << WDE) | (0 << WDP3) | (0 << WDP2) | (0 << WDP1) | (1 << WDP0);
.EQU WDTCSR_64ms	= (1 << WDIF) | (1 << WDIE) | (0 << WDE) | (0 << WDP3) | (0 << WDP2) | (1 << WDP1) | (0 << WDP0);
.EQU WDTCSR_125ms	= (1 << WDIF) | (1 << WDIE) | (0 << WDE) | (0 << WDP3) | (0 << WDP2) | (1 << WDP1) | (1 << WDP0);
.EQU WDTCSR_250ms	= (1 << WDIF) | (1 << WDIE) | (0 << WDE) | (0 << WDP3) | (1 << WDP2) | (0 << WDP1) | (0 << WDP0);
.EQU WDTCSR_500ms	= (1 << WDIF) | (1 << WDIE) | (0 << WDE) | (0 << WDP3) | (1 << WDP2) | (0 << WDP1) | (1 << WDP0);
.EQU WDTCSR_1s		= (1 << WDIF) | (1 << WDIE) | (0 << WDE) | (0 << WDP3) | (1 << WDP2) | (1 << WDP1) | (0 << WDP0);

今回は時間待ちに WDT を使うので、その定義です。WDTCSRレジスタに設定する待ち時間の値をいろいろ定義しておきました。


; SLEEPモード
.EQU SMCR_ADC		= (0 << SM2) | (0 << SM1) | (1 << SM0) | (1 << SE)
.EQU SMCR_POWERDOWN	= (0 << SM2) | (1 << SM1) | (0 << SM0) | (1 << SE)

sleep の動作モードの定義です。今回 AD変換中は雑音低減動作を利用しますので、このための値を SMCR_ADC として定義します。SMCRレジスタにこのモードを指定してから sleep すると、プロセッサの動作を停止することで雑音を抑えて AD変換に専念し、変換が終わったら割込みで目覚めるようになります。もう一つの SMCR_POWERDOWN はパワーダウンモードの値です。


; レジスタ
; Z の上位は必ず 0 にする
.DEF ARG = R24
.DEF CONST_CCPID = R25
#define LP1 YL
#define LP2 YH
#define TMP ZL
#define ZERO ZH

汎用レジスタは 16個しかないので、私は Y、Z レジスタはこんな感じでループ変数やテンポラリ、そして定数 0 として使い回しちゃいます。定数 0 レジスタは結構便利です。WDT の設定時などに必要な CCPIDレジスタへ設定する固定ID も定数としてレジスタに置くことにします。このようにレジスタ数を犠牲にすることを許容できれば、クロック数の節約やコード長の節約につながります。

初期化

.ORG 0
    rjmp    RESET   ; 1 RESET
    reti            ; 2 INT0 
    reti            ; 3 PCINT0
    reti            ; 4 TIM0_CAPT
    reti            ; 5 TIM0_OVF
    reti            ; 6 TIM0_COMPA
    reti            ; 7 TIM0_COMPB
    reti            ; 8 ANA_COMP
    reti            ; 9 WDT
    reti            ; 10 LVM
    reti            ; 11 ADC

ここは割り込みベクタです。今回は INT0(ベクタ2)、ADC(ベクタ11)、WDT(ベクタ9)の割り込みを使用しますが、いずれも sleep から目覚めるために利用するだけです。割込み処理として行うことは無いので、ここは reti でよいです。


RESET:
    ; 定数初期化
    ldi ZERO, 0
    ldi CONST_CCPID, 0xd8

    ; RESET PIN,  プルアップ
    ldi TMP, (1 << PNO_RESET)
    out PUEB, TMP

    ; システムクロック 8MHz
    ldi TMP, 0x00 ; 1分周
    out CCP, CONST_CCPID
    out CLKPSR, TMP

    ; SP設定
    ldi TMP, LOW(RAMEND)
    out SPL, TMP

    ; LED を OUT/Low、BTNOUT を OUT/Low に
    out PORTB, ZERO
    ldi TMP, (1 << PNO_LED) | (1 << PNO_BTNOUT)
    out DDRB, TMP

    ; ADC
    ldi TMP, PNO_BTNIN
    out ADMUX, TMP  ; ADC 入力ピン設定
    ldi TMP, ADCSRA_ADC_OFF
    out ADCSRA, TMP ; ADCクロックなど

コメントで書いているとおりです。GPIO の入出力の向きであったり ADC を接続するピンなど、最初に一度設定しておけばよいものはここで設定してしまいます。

メインループ

MAIN:
    sei
    sbic PINB, PNO_BTNIN
    rjmp MAIN_LOC1

    ; ボタン解放待ち
    rcall SUB_WAIT_BTN_RELEASE

    ; ボタン解放確認表示 LED 1回点灯
    rcall SUB_SHOW_BIT0

MAIN_LOC1:

ここにはマイコン起動後と、ボタンが押された時の処理を終えてから再び入ってきます。なので、まずはどのボタンも押されていないことを確認できるまでここで待つことにします。ここに来た時点でボタンが押されていなければすり抜けます。PINBレジスタの PNO_BTNIN が High ならボタン押下中、Low ならボタン開放中です。
ここに来た時点でボタンが押されていたらその開放を待ち、開放後に LED を 1回点灯させることで動作状態を表示します。


MAIN_LOC1:
    ; SLEEP = パワーダウン
    ldi TMP, SMCR_POWERDOWN
    out CCP, CONST_CCPID
    out SMCR, TMP
    ; INT0 割込み許可
    ldi TMP, (1 << INT0)
    out EIMSK, TMP  ; INT0割込み許可
    out EIFR, TMP   ; INT0割込み要求フラグクリア

    ; 眠る
    sleep
    ; 目覚める

    out EIMSK, ZERO ; INT0割込み不可

SMCRレジスタを設定して、sleep したらパワーダウンするようにします。sleep する前には INT0割込みも許可しておき、sei しておく必要もあります。sei はメインループ突入時に行われています。
sleep から目覚めたら INT0割込みはもう不要なので禁止しておきます。


    ; ボタン読み取り
    rcall SUB_READ_BUTTON
    cpi ARG, (BTN_NUM + 1)
    brcc MAIN       ; 不明なボタン

    rcall SUB_PROC  ; ボタンの処理
    rjmp MAIN

ボタン読み取り処理 SUB_READ_BUTTON を呼び出します。読めたボタンは ARG に設定されるので、これがボタン数を超えているなら無効ボタンとして無視して再びメインループに再突入します。有効なボタン番号であれば、ボタン処理 SUB_PROC を実行してからメインループに再突入します。
ボタン処理 SUB_PROC は、ボタン番号を Lチカで表示する処理になっています。

ボタン読み取り処理

初期化とメインループの説明まででだいぶテキストを費やしましたが、いよいよ本題です。ソースコードは少し飛びます。

; ボタン制御用レジスタ
.DEF LASTBTN1 = R16
.DEF LASTBTN2 = R17
.DEF ADVALUE = R18
.DEF VTH_L = R19
.DEF VTH_H = R20

; ボタン電圧間隔(16bit AD変換値 * 256)
#define BTN_VSTEP INT(0.5 + 255.0 * 256 / BTN_NUM)
; ボタン電圧閾値初期値(VSTEP の 1/2 と四捨五入オフセット 0x80)
#define BTN_VTH_INITIAL INT(BTN_VSTEP / 2 + 0x80)

ボタン制御に使うレジスタの定義と、ボタン電圧(変換値)の閾値の定義です。ボタンの閾値を図で確認するとこんな感じです。

ボタンの閾値とその間隔

ADC はフルスケールを 0~255 の変換値にしますから、上図の閾値の間隔 1.0 は変換値にして

閾値の間隔 = 255 / ボタン数

ということになり、これを BTN_VSTEP とします。そしてこの半分(0.5倍)の値を BTN_VTH_INITIAL として define しています。BTN_VTH_INITIAL を閾値変数の初期値として、ループで BTN_VSTEP ずつ足していって AD変換値と比べてボタンを判別するわけです。

しかしこの式をよく見ると、256 を掛けたり 0.5 を足したりしています。

#define BTN_VSTEP INT(0.5 + 255.0 * 256 / BTN_NUM)

閾値の間隔は小数になりますが、この値は繰り返し加算していきますから、小数を切り捨てた値を使うと上の方のボタンで誤差の蓄積が無視できなくなってしまいます。ですので 256倍して 16bit の値にしてから丸めています。そして閾値を比較する際に 1/256 します。この 1/256 の値というのは 16bit値の上位バイトそのものですから、実質的にはそのような割り算は不要です。これは整数部 8bit 小数部 8bit の固定小数点の値と見ることができます。もうひとつ、加算している 0.5 の方は、計算の結果を四捨五入するための値です。小数に 0.5 を足して切り捨てれば、それは四捨五入になります。ボタン数が20個の場合 BTN_VSTEP は 3264 になり、これは後で 1/256 しますので 12.75相当になります。この端数 0.75 の考慮があるのと無いのとでは、数値を積み上げた時の誤差も違ってくるというものです。


SUB_READ_BUTTON:
    ; ボタン読み取り(同じ値が 3連続で読めるまで繰り返し)
    ldi ARG, 0xfe   ; ボタン番号と重複しないダミーの値
SCAN_LP1:
    mov LASTBTN2, LASTBTN1
    mov LASTBTN1, ARG
    rcall SUB_ADC_BUTTON

    ; 同じ値が3度読めたかチェック
    cpse ARG, LASTBTN2
    rjmp SCAN_LP1
    cpse ARG, LASTBTN1
    rjmp SCAN_LP1

    ret

ここで rcall している SUB_ADC_BUTTON がボタン電圧からボタン番号を読み取る処理ですが、ここは同じボタン番号が 3度連続で得られるまで繰り返す処理です(SCAN_LP1 のループ)。ボタンが押された瞬間はチャタリングなどによってボタン電圧が不安定なことが考えられるので、その対処です。実際には結構安定して読めていて、2度読みでも充分な気がしますが、念押しで3度読みです。

ところでこの同値3度読み判定、なかなかコンパクトに書けてると思いませんか?LASTBTN1 の初期値は不定ですが、それで問題なく動作します。


SUB_ADC_BUTTON:
    ; BTNIN をアナログ入力へ
    sbi DIDR0, PNO_BTNIN    ; BTNIN デジタル入力禁止
    sbi PORTB, PNO_BTNOUT   ; BTNOUT を H へ(読み取り電圧出力)
    cbi PUEB, PNO_BTNIN     ; BTNIN プルアップ停止

AD変換処理の入り口です。コメントのままですが、BTNOUT に High を出力してボタン電圧を作り出しています。下図の切り替えです。

before
after

    ; SLEEPモード = AD変換
    ldi TMP, SMCR_ADC
    out CCP, CONST_CCPID
    out SMCR, TMP

    ; AD変換
    ldi TMP, ADCSRA_ADC_ON
    out ADCSRA, TMP
    sleep   ; 変換開始

    ; BTNIN をデジタル入力へ
    sbi PUEB, PNO_BTNIN     ; BTNIN プルアップ
    cbi PORTB, PNO_BTNOUT   ; BTNOUT を L へ(キーリリース検出)
    cbi DIDR0, PNO_BTNIN    ; BTNIN デジタル入力

    in ADVALUE, ADCL ; 変換値取得

    ; ADC OFF
    cbi ADCSRA, ADEN

AD変換を雑音低減モードで行うため、そのように設定してから sleep します。これによってプロセッサコアが停止すると同時に AD変換が開始されます。AD変換が完了すると割込みによって sleep から目覚め、処理を再開します。
AD変換完了後はもうボタン電圧はいらないので、ピンの状態をもろもろ戻します。AD変換値を取得したら ADC 自体も停止します。


    ; ボタンが離されてないことの確認
    ldi ARG, 0xff
    sbic PINB, PNO_BTNIN
    ret ; ボタンリリース

BTNIN のピン状態を確認して、ボタンが離されていたら(ピンが High だったら) 0xff で ret します。この状態でボタンが離されているということは AD変換値は当てにならないので、見る必要はありません。


    // 閾値スキャン
    ldi VTH_L, LOW(BTN_VTH_INITIAL)
    ldi VTH_H, HIGH(BTN_VTH_INITIAL)
BTN_SCAN:
    inc ARG
    cp VTH_H, ADVALUE
    brcc BTN_RET
    ; 即値足し算を即値引き算で代用
    ;addi VTH_L, LOW(BTN_VSTEP)
    ;adci VTH_H, HIGH(BTN_VSTEP)
    ;brcc BTN_SCAN
    subi VTH_L, -LOW(BTN_VSTEP)
    sbci VTH_H, -HIGH(BTN_VSTEP) - 1
    brcs BTN_SCAN
    ldi ARG, 0xff   ; 該当ボタン無し
BTN_RET:
    ret

ここが閾値を 0.5 から 1.0 ずつ増やしながら AD変換値と比べ、ボタンを識別しているループです。閾値変数は VTH_H:VTH_L の 16bit値で、上位バイトが整数部、下位バイトが小数部です。AD変換値と VTH_H を比較して AD変換値の方が小さかったらボタン特定完了です。ARG がボタン番号をカウントしています。
また閾値変数が溢れた時もループ終了で、その場合は該当ボタン無しです(この ldi ARG, 0xffinc ARG にすると、最上位ボタンの上の抵抗の上にボタンを追加して識別できます)。

閾値変数の進め方についてコメントを付けていますが、この部分

    subi VTH_L, -LOW(BTN_VSTEP)
    sbci VTH_H, -HIGH(BTN_VSTEP) - 1

AVR には何故か addi に相当する即値加算命令がありません。ですが即値引き算 subi や sbci はあるので、これで足したい数の負の値を引けば即値足し算の代用になります。ただしその場合、通常の足し算とキャリーの論理が反対になるので、多バイト加算時はそれを踏まえる必要があります。足し算として見たときに繰り上がりが発生していないときにキャリーフラグが立ちますから、次の sbci では上記コードにあるように -1 調整しているのです。

その他の部分

他の部分は今回の実験の本質とはあまり関係ないので、説明は割愛します。WDT で時間待ちをする方法なんかは見るところがあるかもしれません。
長いコードではないのでなんとかなるでしょう。

続く!

コードの主要なロジックについて説明を終えたところで記事がだいぶ長くなってしまいましたので、今回はここまでにします。アセンブラなりの手続きの多さはありますが、順を追ってみれば前回の記事で出した結果をそのまま実装しているだけだということが分かると思います。
次回は実際の動作と制御信号の波形を見てみて、どの程度安定しているのかなどを確認したいと思います。

このシリーズ

今回の記事です

関連記事