PIC AVR 工作室別館 arduinoの館->TopPage->資料倉庫->DigiSpark

DigiSparkの小部屋

DigiSparkについて

AT-Tiny85搭載の極小Arduino互換基板DigiDparkについて、使ってみての情報あれこれを纏めておきます。

DigiSparkは、AVR AT-Tiny85チップを搭載した、極小Arduino互換マイコンで基板です。 直接USBコネクタに挿しこんでつなぎ、Arduino-IDEからスケッチを書き込みできます。(ただし、Arduino-IDE側の設定が必要)

USBシリアル変換チップなどを搭載しておらず、Tiny85自体がUSB通信処理を負担していて、 HIDデバイスとしても機能できるという利便性があります。規模が小さいスケッチで済む要件なら、 USBキーボードなどをエミュレートすることなんかもできます。

Arduino Unoなどを持ち出しちゃうほど大事じゃないけど、Arduinoっぽいらくちんが欲しいって場合に便利な極小ボードです。

仕様などの細かいことはここでは公式ページなどにまかせて、ここでは触れず、 実際に使ってみて、色々と応用に役立ちそうな情報(tips)だけ書き溜めておくページです。

DigiSparkのタイマ割り込み

DigiSparkのタイマ周りについて

DigiSparkは、AT-Mega328とは内蔵ペリフェラルやCPU内部のレジスタ周りが異なります。 特に、8ピンAVRなので、大幅に機能が削減されているといってよいでしょう。

このため、AT-MEGA328などのために作られた「タイマー割り込み」関係のライブラリ類が利用できません。

とはいえ、一般のマイコン同様、当然タイマー機能自体は搭載されています。 (タイマー0、タイマー1…ともに8ビットタイマ。ちなみに、mega328は3つのタイマを内蔵してて、タイマ1は16ビットタイマ)

内部コードを参照したわけではないので、詳細までは調べがついているわけではないのですが、 DigiSparkの内部では、タイマー1があれこれに使われているようです。 多分、delay関数とか、millis関数とか、ソフトウェアシリアルとかが、このタイマ1を使ってるんじゃないかな?と想像してます。

なので、タイマ1の内部を色々弄り回すと、この辺のタイミング調整が狂ってしまうだろうと思います。

一方、DigiSparkのタイマ0は、analogWrite関数(PWM出力)で使われているようです。 このため、タイマ割り込みをタイマ0を使って行おうとすると、単純にはPWM出力機能がオーバーライトされて動かなくなったり、 無理やり動かすとしても、レジスタの設定内容によってはPWMの周期などがあれこれ変化してしまったりして、 あまり都合よくありません。

元々、delay関数やmillis関数は、タイミング調整用の機能なので、タイマ割り込み機能が使えるようになれば必須ではなさそうです。 逆に、analogWrite機能は元のArduino環境のまま残しておいたほうが、使い勝手の点で色々便利でしょう。

このため、タイマ1をタイマ割り込み処理用に使う方向で考えることにします。

注意点

ここで触れるスケッチの処理によって、タイマ1関係のSFR(Spesial Function Registor)を書き換えまくるので、 タイミング管理が必要なdelay関数とかmillis関数とか、ソフトウェアシリアル通信とか、もしかしたらUSBデバイス機能とかも、 まともに動かなくなる恐れがあります。(何に影響があるのかは、調査未済なので不明)

タイマ1をタイマ割り込み機能に書き換える

ネット上を色々探してみたものの、DigiSparkでタイマ割り込みを使うための便利なライブラリと言うものは見つかりませんでした。

なので、タイマ1関係のSFRを直接操作して、タイマ1をタイマ割り込みとして使うように設定します。

こことか こことかに、 有益な情報があったので、これらの情報を参考に、色々実験しながら、 タイマ1を使って任意の周期でタイマ割り込みを行うスケッチ(の素材)をでっち上げてみました。

といっても、いわゆる従来型のArduino的なタイマ割り込みライブラリのような便利機能までは考えてません。 従来型Arduino用のタイマ割り込みライブラリは、「関数名(関数の入り口)」と「割り込み周期」を指定するだけで、 あとは内部で予期に計らってくれるんですが、そういう便利なことまでは行ってません。

単に、ISR(Interrupt Service Routine)を呼び出す周期を、ある程度任意に設定できる程度の機能だけ盛り込みました。 元々、メモリ量も限られ、機能も限られたDigiSparkなので、あまり汎用化(複雑化)しないほうが、使いまわしが効くかと思います。

サンプルスケッチ

#include <avr/interrupt.h>

int ledPin1 = 1;
int ledPin2 = 0;
volatile unsigned char vcnt;


ISR(TIM1_COMPA_vect) {
  // 16.5 Mhz / (512*126) = 256 Hz
  vcnt++;
}


void setup()  {
  vcnt = 0;

  cli();
  //initialize the timer1
  TCCR1  = (1<<CS13 | 1<<CS11) ; // Timer1 prescaling 512 and clearing other bits
                                 //  disconnected from output pin OC1A
  //TCCR1  = (1<<CS13 | 1<<CS10) ; // Timer1 prescaling 256 and clearing other bits
  //                               //  disconnected from output pin OC1A
  //TCCR1  = (1<<CS13) ; // Timer1 prescaling 128 and clearing other bits
  //                               //  disconnected from output pin OC1A
  TIMSK  |= (1<<OCIE1A);     // enable timer1 compare interrupt
  //set the timer1 count
  OCR1C = 126-1; // Count 126 cycles before calling ISR interrupt (0..255)
  //initialize global interrupts before beginning loop
  sei();
} 


void loop()  {
  analogWrite(ledPin1, vcnt);
  analogWrite(ledPin2, vcnt);
}

スケッチの動作など

書き込めば普通に動くと思います。オンボードのLEDが、analogWriteでホワンホワンと瞬きます。

ブログ記事で多少触れてましたが、 説明にちょっと間違えがあったりするので、もう少し整理しなおします。

DigiSparkは、個体によって、オンボードのLEDが「P0」「P1」のどちらについているかが異なっているみたいです。 なので、スケッチ中では、両方一まとめにして同じ値を出力しています。 どちらか必要なほうだけ残しておけば事足ります。

setup関数内で、タイマ1関係のSFRを初期化しています。

TCCR1に設定していることは、大きく2つあって、タイマ1の「クロック→カウントアップのディバイダ」と、 「OC1Aピンへの出力無効化」です。

デフォルトではディバイダに512を設定してあるので、16.5MHz÷512=約32.2kHzごとにカウンタが1カウントアップします。 カウンタマッチを1/256秒ごとに発生させたいと思ったので、タイマ1のコンペアレジスタ「OCR1C」に「126-1」を設定しました。 これにより、毎秒256回のタイマ割り込みが発生します。(16.5MHz÷512÷256≒126)

これによって、メイン処理側で変数vnctをanalogWriteで出力すると、1/256秒ごとに出力値が変化し、 LEDの輝度が0~255までの256段階でホワンホワンと1秒周期で変化するわけです。

コメント化してあるのは、比較的使う機会が多そうなディバイダの設定値のバリエーションです。 256と128のパターンをコメントで書き入れてあります。ここにないディバイダ値の場合は、 データシートのTCCR1の設定テーブルを参照してください。

タイマ割り込み処理の実体

ISR(TIM1_COMPA_vect)という関数が、タイマコンペア一致割り込みの際に呼び出される関数です。 要は、タイマ割り込み処理の実体です。

ここでは、タイマのカウンタマッチイベントが発生するごとに割り込みが行われます。 つまり、512クロックごとにカウンタが1アップし、それが126回カウントしたら、割り込みイベントが1回発生し、この関数が呼び出されます。

タイマ割り込み関数内は、極力短い時間で処理が終わるように書くのが基本の「き」なので、 変数「vcnt」のカウントアップだけ行っています。あまり長いと、1回の割り込み周期内で処理が完了せず、 おかしな処理を行ってしまいます。

また、そこまで長くなくても、やはり短く抑えておく必要があります。 CPUにはタイマ割り込みの他にも割り込み要因が色々利用できるようになっているので、 複数の割り込み処理同士がお互いに処理時間を食いつぶし合うと、本来処理しなければならなかった割り込み要因が、 次に発生した割り込み要因で上書きされ、割り込み処理を忘れてしまう(処理が抜ける)こともあるので、注意が必要です。 (UART受信割り込みとか、ADC完了割り込みとか色々)

割り込み処理内で変更が掛かる変数の扱いについて

このサンプルスケッチでは、割り込み処理内で変更している変数「vcnt」は8ビット(1バイト)変数なので、 loop関数(メイン処理)側で、そのまま読み書きできます。ただ、しいて言えば、「volatile」属性をつけて宣言しておく必要があります。

(「volatile」を付けないと、コンパイラが最適化処理をする際に、「メインルーチン内で更新されていないから、定数として扱ってよい」 と誤解され、実際のスケッチが動く際に、割り込み処理で更新した変数の値が参照されない恐れがあります)

さらに、もし「int型」や「long型」など、2バイト以上の変数を扱う場合は、メイン処理側で読み書きをする際に、 一旦割り込みを禁止してから行う必要があります。

(「cli(); → 変数読み書き → sei();」とする →アトミックアクセスなどと呼びます)

複数バイト変数の場合、メモリ(SRAM)上に複数のバイト値として連なって書き込まれていますが、 スケッチがこれを読み書きする際に、内部のアセンブラコードは、1バイトずつしか読み書きできません。 そのため、もしこの変数アクセスしている最中にタイマ割り込み要因が生じてしまうと、 割り込みの処理前後で上位バイトデータと下位バイトデータの内容の整合性が取れなくなる恐れがあります。 (わかりにくいバグになる)

DigiUSB.hライブラリについて

…続きます…