USART0を使ったシリアル通信を行う。シリアル通信はGCC Developer Liteで提供される専用のライブラリを直接使用し、文字及び文字列の送受信や書式化文字列の送信を行う。
シリアル通信ライブラリのプロトタイプ宣言はrs.hとして提供され、ソースにインクルードする事で参照される。
#include <rs.h> // USART通信ライブラリ
また、シリアル通信を行う前には必ず初期化処理を必要とし、実際の通信処理を行う内部のサブルーチンは割り込みを使用しているので、割り込みを許可しておく。
// 送受信用バッファ // バッファの大きさは任意(2~255の間)ですが、大きすぎるとRAMエリアを圧迫するだけです。 // 通常の使用においては数バイト程度で十分です。またバッファはグローバル変数としてください。 static char txb[10], rxb[10]; // USART0を初期化 ボーレートは115200[bps] rs0_init (br115200, txb, sizeof(txb), rxb, sizeof(rxb)); // 通信は割り込みで処理されます // 初期化後は必ず割り込みを許可状態にしてください sei ();
その他のサブルーチンは以下のものが用意されている。詳細はソースのコメントを参照の事。
・1文字送信 void rs0_putc (char); void rs1_putc (char); ・文字列送信 void rs0_puts (char *); void rs1_puts (char *); ・指定サイズのバイナリバイト列送信 void rs0_putb (char *, short); void rs1_putb (char *, short); ・受信バッファデータサイズ取得 short rs0_rx_buff (void); short rs1_rx_buff (void); ・受信バッファクリア void rs0_rx_purge (void); void rs1_rx_purge (void); ・1文字受信(受信されるまで待つ) char rs0_getc (void); char rs1_getc (void); ・文字列受信(入力時のエコーバックあり,CRで入力完了) short rs0_gets (char *, short); short rs1_gets (char *, short); ・簡易書式化文字列送信 short rs0_printf (const char *, ...); short rs1_printf (const char *, ...);
USART0を使ったシリアル通信を行う。avr-libcで提供される標準I/Oルーチンを中継し、シリアル通信はGCC Developer Liteで提供される専用のライブラリを間接的に使用する。
初期化まではUSART1.cとほぼ同様だが、fdevopenで初期化された以後はC言語でよく使われる標準I/Oルーチン(putcharやprintf等)を使用する事ができる。
// USART0を初期化 ボーレートは115200[bps] rs0_init (br115200, txb, sizeof(txb), rxb, sizeof(rxb)); // 標準入出力をlibm32.a内のルーチンに割り当て fdevopen(rs0_putchar, rs0_getchar);
標準I/Oルーチンではまかないきれない機能は、シリアル通信ライブラリで提供されるサブルーチンで補填出来る。
ついでにタイマ/カウンタ2を使用して時間待ちルーチンを作成。
ウェイトの分解能を1msとし、16MHz/64=250kHzのクロックを250回数えるまでループさせて1msの待ちを作っている。
void wait (unsigned long pause_ms) { unsigned long i; TCCR2 = 0x00; // stop TCNT2 = 0; // set count OCR2 = 250; // set compare TCCR2 = 0x0b; // start timer clk/64 for (i = 0; i < pause_ms; i++) { loop_until_bit_is_set(TIFR, OCF2); TIFR |= _BV(OCF2); } TCCR2 = 0x00; // stop }
PB7にシンク接続されたLEDを点滅させる。
AVRの内部レジスタは avr/io.h をインクルードする事でデータシートに記載されている名称で参照出来る。なお、io.hは複数のデバイスのヘッダを取りまとめており、ATmega128の場合はavr/iom128.h、ATmega128Aの場合はavr/iom128a.hが実体である。
#include <avr/io.h>
PB7を出力ポートに設定するにはDDRBの7ビット目を1に設定する。
DDRB |= _BV(DD7); // PB7を出力
PB7へ0を書くとLEDの両端子に電位差が生じ抵抗で制限された電流が流れて点灯、1を書くとLEDの両端子が同電位になり消灯する。ちなみに、_BV() は引数で指定した数値をビットに変換するavr-libcで提供されるマクロである。
PORTB |= _BV(PORT7); // 消灯 PORTB &= ~_BV(PORT7); // 点灯
点滅の周期はutil/delay.hで宣言される_delay_ms関数を使用し、ミリ秒単位で設定された時間だけ待機させることで実現。
_delay_ms (500); // 500ms待つ
PA0~PA7へそれぞれLEDをシンク接続し点滅させる。
出力端子の初期化は以下の通り。
DDRA = 0xff; // PA0..7を出力に
点灯させるビットが増えると一つ一つ指定するのが面倒なので、8ビットまとめて指定する関数を用いている。ここでは1を指定したビットのLEDを点灯させる事としたため論理反転している。
void LED (int d) { PORTA = ~d; }
PD4~7にDIPスイッチ、PB0~PB3にプッシュボタンを接続し、逐次それらの状態を取り込んでLED1~8に反映させる。この回路は端子をGNDに接地させた際にLOWの論理を確定する以外の機能を持たないため、HIGHはチップ内蔵のプルアップ抵抗を機能させる事で実現する。
入力端子の初期化は以下の通り。
// ディップスイッチ(PD4..7)を構成 DDRD &= ~(_BV(DD4) | _BV(DD5) | _BV(DD6) | _BV(DD7)); // PD4..7を入力に PORTD |= (_BV(PORT4) | _BV(PORT5) | _BV(PORT6) | _BV(PORT7)); // PD4..7の内蔵プルアップ抵抗をON
// プッシュボタン(PB0..4)を構成 DDRB &= ~(_BV(DD0) | _BV(DD1) | _BV(DD2) | _BV(DD3)); // PB0..3を入力に PORTB |= (_BV(PORT0) | _BV(PORT1) | _BV(PORT2) | _BV(PORT3)); // PB0..3の内蔵プルアップ抵抗をON
DIPスイッチは4ビットまとめて数値として取り込む関数を用意している。ONの時にGND(=0)になるため論理反転している。
unsigned char DIP1_STAT (void) { return (~(PIND >> 4)) & 0xf; }
4つのプッシュボタンの取り込みも同様に各々関数を用意している。押下の時にGND(=0)になるためbit_is_clearで1を返す事とする。
// プッシュボタン1の状態取り込み unsigned char PB1_STAT (void) { return bit_is_clear (PINB, PIN0); } // プッシュボタン2の状態取り込み unsigned char PB2_STAT (void) { return bit_is_clear (PINB, PIN1); } // プッシュボタン3の状態取り込み unsigned char PB3_STAT (void) { return bit_is_clear (PINB, PIN2); } // プッシュボタン4の状態取り込み unsigned char PB4_STAT (void) { return bit_is_clear (PINB, PIN3); }
PD0/INT0に入力される信号の立ち下がりエッジで外部割り込みを発生させ、割り込み関数で変数をインクリメントしPA0~PA7へシンク接続したLEDに2進数表示させる。
ついでにスリープモードも試す。
AVRの割り込み処理に関する記述はavr/interrupt.hに記載されているため、本ヘッダファイルのインクルードが必須である。また、デバイス毎に異なる割り込みベクタ等のマクロ定義はavr/io.hに記述されている。
#include <avr/interrupt.h> // 割り込み処理ルーチン #include <avr/io.h>
PD0を入力端子とし、内蔵プルアップをONにする。
// 外部割り込み入力端子(PD0/INT0)を構成 DDRD &= ~_BV(DD0); // PD0を入力に PORTD |= _BV(PORT0); // 内蔵プルアップ抵抗をON
INT0を立ち下がりエッジで割り込みを発生させる。
// INT0の割り込み条件を立下りエッジに MCUCR |= _BV(ISC01); // ISC01を1に MCUCR &= ~_BV(ISC00); // ISC00を0に // INT0/1割り込みを許可 EIMSK |= _BV (INT0); // 割り込み許可 sei ();
INT0の割り込み要因で実行される関数を用意するのだが、AVRの割り込み処理の記述方法が決まっている。ISRというマクロで割り込み要因のベクタ番号を指定し、関数の様に処理ルーチンを記述する。INT0の割り込みベクタはio.hにINT0_vectというマクロで定義されているのでそれを使用する。
// カウンタ static unsigned char cnt = 0; // 割り込み処理ルーチン ISR(INT0_vect) { PORTA = ~(++cnt); }
AVRにはデバイスの電力をコントロールする機能があり、avr/sleep.hに記述されている。
#include <avr/sleep.h>
ひとまず今回は機能の制約が少ないIDLEを使用する。
set_sleep_mode (SLEEP_MODE_IDLE);
これだけでは実際には省電力に遷移しないため、遷移させたいタイミングでsleep_modeを実行する。ここではINT0の割り込みで自動的にIDLEから復帰する、main関数内で復帰していればIDLEに遷移させる、といった具合に割り振った。
for (;;) { sleep_mode (); // スリープモードに突入(INT0割り込みにて復帰) PORTB ^= _BV(PORT7); // 復帰する度にD2の状態を反転 }
PF0~PF7の端子に入力されるDC0~5Vの電圧を計測し、シリアル通信で送信する。
ここではA/D変換するにあたり基準となる電圧(DC5V)をAVCC端子に接続した。
A/D変換器の初期化は以下の通り。変換時間等は変換精度等の必要に応じて設定する事。
// A/D変換許可, 変換クロックCK/16 ADCSRA = _BV(ADEN) | _BV(ADPS2);
内部にはA/D変換器は1つしかないため、8つの端子からの入力を同時に行う事はできない。任意のチャネルを選択し、変換を励起し、変換が終了するまでチャネルを切り替える事はできない。
変換開始から終了の待機及び変換されたデータを取得する関数は以下の通り。
// 任意CHのA/D変換 unsigned short a2d (char ch) { ADMUX = _BV(REFS0) | (ch & 7); // リファレンス,CH指定 ADCSRA |= _BV(ADSC); // 変換開始 loop_until_bit_is_clear (ADCSRA, ADSC); // 変換終了待ち return ADC; }
DC0~5V入力された電圧に応じて0~1023の10ビットの精度で値が取得される。
mainのループではプッシュボタン1を押すと0~1023の値として、プッシュボタン2を押すと5.00Vといった電圧値として8ch分の変換値をターミナルに対して文字列として送信する。
・PushButton1を押している間 ADC=[ 338, 609, 617, 802, 639, 887, 670, 495 ] ・PushButton2を押している間 ADC=[ 0.57V,0.32V,1.21V,1.18V,1.55V,1.93V,1.82V,1.78V ]
タイマ/カウンタ0のオーバーフローを使用して一定間隔で割り込みを励起する。
タイマ/カウンタ0のオーバーフロー割り込みを許可するのは以下の通り。
// タイマ/カウンタ0割り込みマスク構成 TIMSK |= _BV(TOIE0); // タイマカウンタ0オーバーフロー割り込み許可
タイマ/カウンタ0をCLK/1024(16MHz/1024=15.625kHz)で標準動作をさせるには以下の通り。クロックを選択した瞬間からタイマとして動作を開始する。
TCCR0 = _BV(CS02) | _BV(CS00); // 標準動作,CLK/1024
15.625kHz毎にTCNT0の値がインクリメントされ、TOP値の255を超えて0にリセットされるとオーバーフロー割り込みが発生(15.625kHz/256=約61Hz)する。タイマ/カウンタ0のオーバフロー割り込みルーチン名は TIMER0_OVF_vect としてavr-libcにて決められているので、その様に記述。ここでは割り込みが発生したらさらにソフト的に分周し、約0.5秒毎にLEDを点滅させている。
ISR (TIMER0_OVF_vect) { static int cnt = 0; // 1/16MHz * 1024 * 256 秒毎に割り込み発生 if (cnt++ > 31) { // 約0.5秒毎にLEDを反転 cnt = 0; PORTB ^= _BV(PORT7); // XORでビット反転 } }
mainは諸々の初期化を行った後は例のごとくしつこく省電力モードに移行し続ける以外の処理は行っていない。
while (1) { // 無限ループ sleep_mode (); }
PWM信号を出力できる様に構成し、PWMの1周期(コンペアマッチ)毎に割り込みを発生させてデューティーの更新を行う。 PWM信号はPB5,PB6,PB7,PE3,PE4,PE5端子にオシロスコープ等を接続し、波形にて確認する。
PWM信号を出力するPB5~7,PE3~5端子を出力に設定する。
DDRB = _BV(PINB7) | _BV(PINB6) | _BV(PINB5); // PB7-5出力 DDRE = _BV(PINE5) | _BV(PINE4) | _BV(PINE3); // PE5-3出力
タイマ/カウンタ1を高速PWMモードに設定する。ICR1がPWMの周期、OCR1AがPB5(OC1A)・OCR1BがPB6(OC1B)・OCR1CがPB7(OC1C)のデューティーとなり、ここでは周期を8ms(=125Hz=CLK/256/500)としたので以下の通りに設定する。
// タイマ/カウンタ1を高速PWMモードに設定 (ICR1でTOP) TCNT1 = 0; ICR1 = 500; // TOP値 62.5kHz/500=8ms周期 OCR1A = cnt[0]; // PB5(OC1A)のデューティー OCR1B = cnt[1]; // PB6(OC1B)のデューティー OCR1C = cnt[2]; // PB7(OC1C)のデューティー TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(COM1C1) | _BV(WGM11); TCCR1B = _BV(CS12) | _BV(WGM13) | _BV(WGM12); // φ/256=62.5kHz
TCNT1の値がICR1と一致でコンペアマッチA割り込みを励起させるには、TIMSKのOCIE1Aビットを1にする。
// タイマ/カウンタ割り込みマスク設定 TIMSK = _BV(OCIE1A);
タイマ/カウンタ1のコンペアマッチAオーバフロー割り込みルーチン名は TIMER1_COMPA_vectとしてavr-libcにて決められているので、その様に記述。割り込み発生毎に各々のPWMのデューティ値をインクリメントし、500以上になったら0にリセットしている。
SIGNAL(TIMER1_COMPA_vect){ int i; // 1周期毎に比較レジスタ更新 for (i = 0; i < 3; i++) if (cnt[i]++ > 500) cnt[i] = 0; OCR1A = cnt[0]; OCR1B = cnt[1]; OCR1C = cnt[2]; }
タイマ/カウンタ3もほぼ同様である。
マイコンに内蔵されたEEPROMの読み書きを行う。操作及び状況の確認はシリアル通信にて行う。
内蔵EEPROM関連のサブルーチン等は avr/eeprom.h に定義される。
#include <avr/eeprom.h> // EEPROM関連
EEPROMへのアクセスを行う際はそれ以前の内部的なEEPROMへの処理が完了している必要があるため eeprom_busy_wait 関数にて処理完了を待ち、さらにアクセス中は他の処理に影響が及ぶとの事で割り込み等は禁止にする必要がある。
cli (); eeprom_busy_wait (); eeprom_write_byte ((uint8_t *)i, c); sei ();
avr-libcで提供される特殊なメモリアクセス方法の紹介。
AVRはハーバードアーキテクチャーであるため、プログラムコード領域(フラッシュROM)とワーク領域(SRAM)が分離されている。つまり連続・不連続に関わらず同一のメモリマップ上には配置されないため、一般的なC言語の記述ではフラッシュROM上に置かれたデータ類へのアクセスが出来ない。
例えば
const char hoge[] = "TEST DATA\n";
といった宣言を行うとフラッシュROM内にhogeのデータが確保され、プログラムの実行が開始される際にhogeをSRAM上にコピーしたものがアクセス対象となる。つまり、ある程度サイズに余裕のあるフラッシュROM上に不変の固定データを置いたつもりでも、それと同等のサイズのデータがSRAM上に別途確保されるので、非常に小さいSRAMを圧迫する結果となる。
avr-libcにはSRAMにコピーせずにフラッシュROM上に確保したデータへアクセスするために、宣言時と利用時に特殊な命令が用意されている。
const char gyao[27000] PROGMEM = {0,0,0,0,0,0,};