AVRマイコンで割り込みを使ってみる
マイコン開発の次のステップとして割り込みを使ってみます。
今回はLEDが消灯・点灯・点滅がメインの動作でボタンが押されるとLCDの表示を変え、LEDの動作が変更されるプログラムです。今回もマイコンはATmega88です。
プログラムコードが長くなるので前回使用したLCD関連の関数を別のファイルに分けました。

こんなのを作ります(^o^)/
回路図

LCDは前回と同じように接続しPC0にLEDを、PB0にスイッチを取り付けます。このスイッチが押されると割り込みが発生するようにプログラムします。
何とか想定した通りに動作させる事は出来ましたが、無駄や間違ったコードが含まれる場合がありますσ(^_^;)
メインのソースファイル
まずはコードをどうぞ。
#define F_CPU 1000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdbool.h>
#include <util/delay.h>
#include "lcd.h"
volatile bool flag = true;
/* 矢印を消す関数 */
void lcd_sel_clear()
{
lcd_pos(1,1);
lcd_data(0x20);
lcd_pos(1,7);
lcd_data(0x20);
lcd_pos(2,1);
lcd_data(0x20);
lcd_pos(2,7);
lcd_data(0x20);
}
/* 割り込み時に実行する関数 */
ISR(PCINT0_vect)
{
_delay_ms(200);
cli();
lcd_sel_clear();
flag = false;
sei();
}
int main(void)
{
DDRC = 0b11111111; // PC0にLEDを付ける
PORTC = 0b00000000; // ポートCをL出力
DDRD = 0b11111111; // LCDピンに使用
PORTD = 0b00000000; // ポートD初期化
// 割り込みの設定(PB0を割り込みとして使う)
cli(); // 割り込み禁止
DDRB = 0b00000000; // 入力に設定
PORTB = 0b00000001; // PB0をプルアップ
PCICR = (1 << PCIE0); // ピン・チェンジ・コントロール・レジスタの設定
PCMSK0 = (1 << PCINT0); // ピン・チェンジ・インタラプト・マスク0レジスタの設定
lcd_init();
int pattern = -1;
// 最初の画面
lcd_pos(1,1);
lcd_str(" OFF ON");
lcd_pos(2,1);
lcd_str(" Blink Hi-Blink");
while (1)
{
cli();
pattern++;
flag = true;
switch (pattern)
{
case 0:
lcd_pos(1,1);
lcd_data(0x7E);
sei();
while (flag)
{
PORTC = 0b00000000; // PC0 L
}
break;
case 1:
lcd_pos(1,7);
lcd_data(0x7E);
sei();
while (flag)
{
PORTC = 0b00000001; // PC0 H
}
break;
case 2:
lcd_pos(2,1);
lcd_data(0x7E);
sei();
while (flag)
{
/* 1秒点滅 */
PORTC = 0b00000001;
_delay_ms(500);
PORTC = 0b00000000;
_delay_ms(500);
}
break;
case 3:
lcd_pos(2,7);
lcd_data(0x7E);
sei();
while (flag)
{
/* 0.2秒点滅 */
PORTC = 0b00000001;
_delay_ms(100);
PORTC = 0b00000000;
_delay_ms(100);
}
break;
default:
pattern = -1;
break;
}
}
}
長ったらしいですね。頑張ってください。
10行目の#include "lcd.h"
が今回分けたファイルです。今までの<〇〇.h>と違って自作のヘッダーファイルは「” “」で囲います。
12行目にbool変数を使ってtrueとfalseで判断していますが、普通の変数で1と0を入れても同じ事が出来ると思います。「volatile」と付ける事で意図的に最適化される事を避けています。
45行目のcli();
は割り込みを禁止するアセンブラ命令を実行するマクロです。割り込み許可のsei()を実行していないので必要ないかもしれません。
46〜49行目でポートBを割り込みとして使えるようにしています。今回はピン・チェンジ・インタラプタを使って割り込みを発生させます。avr/interrupt.hをインクルードしておく必要があります。
ピン・チェンジ・インタラプト

48行目PCICR = (1 << PCIE0);
でPCIE0にビットをセットします。

49行目PCMSK0 = (1 << PCINT0);
でPCINT0(PB0)にビットをセットします。これでピン状態が変更されると割り込みが発生します。
LCDに表示
前回と同じくLCDモジュールの初期化をlcd_init()で行います。今回の実験中、文字化けするなあと思っていたら初期化関数を忘れていました。
55〜58行目でLCDに表示する最初の画面を作っています。lcd_pos(1, 1);
でカーソルの位置を指定しています。1つ目の値で1行目、2つ目の値で1文字目と指定しています。
while文
もう1度cli( )を実行しているのは割り込み関数から戻って来た時用です。
65行目で変数patternをインクリメントします。この変数は52行目でint pattern = -1;
としているので0になります。
66行目で変数flagをtrueとします。後のwhile文で使用します。
今回はswitchi文を無限ループで繰り返します。
switchi文
switch (条件式)
{
case 数値:
実行文;
break;
case 数値:
実行文;
break;
}
条件式の値と同じcaseの数値の所へジャンプします。こうする事で色々なパターンに対応出来ます。
最初は変数patternの中は0なので68行目のcase0にジャンプします。lcd_pos(1, 1);
でカーソルの位置を指定し、lcd_data(0x7E);
で「→」を表示します。「0x7E」はLCDモジュールのデータシートに載っています。
そしてsei();
で割り込みを許可します。何故ここまで割り込みを禁止していたかと言うと、LCD関数を実行中に割り込みが発生するとLCDとのデータのやり取りが中途半端な状態になり文字化け等を起こすからです。
case0はOFF、case1はON、case2は点滅、case3は高速点滅となっています。
割り込み発生
PB0のスイッチが押されLになると割り込みが発生します。スイッチを押しっぱなしにしていた場合、離すとHになるのでこの場合も割り込みが発生します。
ISR(PCINT0_vect)
{
_delay_ms(200);
cli();
lcd_sel_clear();
flag = false;
sei();
}
ISR(PCINT0_vect)関数はPCINT0〜7(ATmega88の場合PB0〜7)の割り込み設定したピンの変化が発生した際に実行されます。今回はPB0のみ設定されています。
_delay_ms(200);
はスイッチのチャタリング対策で入れています。外部プルアップにしたりバイパスコンデンサ等を入れたりすればより良いチャタリング対策になるのだと思うのですが、今回は実験なのでシンプルに内部プルアップを使用してスイッチのみとしています。
ここで再びLCD関数を使用するので「cli( )」で割り込み禁止にしています。
lcd_sel_clear();
はLCDに表示されている「→」を消す為の関数です。
変数flagにfalseを入れ、「sei( )」で再び割り込みを許可します。これで割り込み関数は終わり割り込みが起きた所に戻ります。
割り込み実行後
割り込み関数が終了するとcaseの中のwhile文の中に戻ってきます。しかし変数flagはfalseになっているのでwhile文から抜け出し次の行のbreak;
を実行します。breakを実行するとswitch文から抜け出します。
再び無限ループの頭に戻り、割り込みを禁止し変数patternをインクリメントします。そして変数flagをtrueに戻します。
今度の変数patternは「1」なのでcase1が実行されPC0がHになりLEDが点灯します。もう一度スイッチを押すと変数patternが「2」になりcase2を、更にもう一度押すとcase3を実行します。
ではもう一度押すとどうなるでしょうか?
変数patternは「4」になります。しかしcaseは0〜3までしかありません。その場合default:
の部分が実行され変数patternに「-1」を代入します。breakでswitch文から抜け出し無限ループの頭に戻ります。そうすると再び変数patternが「0」になりcase0が実行されます。
ヘッダーファイル
今後の練習も兼ねてヘッダーファイルを作ってみました。こうする事で他のプロジェクトでLCDを使う事になってもコピペで済みます。main.cのファイルもスッキリします。
#ifndef _LCD_H_
#define _LCD_H_
/* レジスター・セレクト・シグナルは何番ピンを使用するのか決める */
#define LCD_RS 0b00000001
/* イネーブル・シグナルは何番ピンを使用するのか決める */
#define LCD_E 0b00000010
/* LCDのデータビットに使うポートの指定(PIN4,5,6,7) */
#define LCD_PORT PORTD
// 4ビット送信
void lcd_out(char code, char rs);
// コマンド送信
void lcd_cmd(char cmd);
// データ送信
void lcd_data(char asci);
// 表示位置指定
void lcd_pos(char line, char col);
// ディスプレイ全消去
void lcd_clear(void);
// 文字列送信
void lcd_str(char *str);
// LCDモジュール初期化
void lcd_init(void);
#endif // _LCD_H_
1行目と最後の行の#ifndef〜#endifはヘッダーファイルの重複インクルードを避ける為のものです。_LCD_H_は適当に付けた名前です。ヘッダーファイル名から取っています。
#ifndef _LCD_H_
は_LCD_H_が定義されていない場合という意味なので、一度定義されればヘッダーファイルが再び呼び出されても1回目しかコンパイルされません。
4〜20行目まではLCDに使用するポート指定等をしなかった場合、警告と共にデフォルトで定義するようにしました。
22行目以降はプロトタイプ宣言です。
LCDのソースファイル
こちらのソースファイルの中にLCD関数の実体を記述します。
lcd.hをインクルードし忘れないように記述します。
/* lcd.h */
#include "lcd.h"
#include <avr/io.h>
#include <util/delay.h>
void lcd_out(char code, char rs) {
LCD_PORT = (code & 0xF0) | (LCD_PORT & 0x0F);
if (rs == 0)
LCD_PORT = LCD_PORT & ~LCD_RS;
else
LCD_PORT = LCD_PORT | LCD_RS;
_delay_ms(1);
LCD_PORT = LCD_PORT | LCD_E;
_delay_ms(1);
LCD_PORT = LCD_PORT & ~LCD_E;
}
void lcd_cmd(char cmd) {
lcd_out(cmd, 0);
lcd_out(cmd<<4, 0);
_delay_ms(2);
}
void lcd_data(char asci) {
lcd_out(asci, 1);
lcd_out(asci<<4, 1);
_delay_ms(0.05);
}
void lcd_pos(char line, char col) {
if (line == 1)
lcd_cmd(0x80 + col - 1);
else if (line == 2)
lcd_cmd(0xC0 + col -1);
}
void lcd_clear(void) {
lcd_cmd(0x01);
}
void lcd_str(char *str) {
while(*str != '\0') {
lcd_data(*str);
str++;
}
}
void lcd_init(void) {
_delay_ms(15);
lcd_out(0x30, 0);
_delay_ms(5);
lcd_out(0x30, 0);
_delay_ms(1);
lcd_out(0x30, 0);
_delay_ms(1);
lcd_out(0x20, 0); // 4ビットモードに設定
_delay_ms(1);
lcd_cmd(0x28); // 2行表示設定, 液晶2行, フォント5x7ドット
lcd_cmd(0x0C); // 画面表示ON, カーソルOFF, カーソル位置で点滅0FF
lcd_cmd(0x06); // カーソル移動右(インクリメントモード), 表示をシフトOFF
lcd_cmd(0x01); // 画面クリア
}
中身は前回と同じなので説明は省きます。
まとめ
今回はチャタリングと割り込みのタイミングに苦労しました。ボタンを押しても1つずつ矢印が移動せずに1つ飛ばしに移動したり、ボタンを押す度に文字化けを起こしたりしていました。
ファイルを作った場所が違っていて自作のヘッダーファイルが上手く機能しなかったりと遠回りしましたが何とかゴール出来ました。

左上のFileからではなく右の所から右クリックしてファイルを作りましょう。この辺の情報が意外と見つからないので載せておきます。他のやり方があるのかもしれませんがまだ見付けていません^^;

名前の変更も右の所から出来ます。
最後に動画も載せておきます。