IR-control-ampソフトウェア
このソフトウェア一式はGitHubで公開しています。
IR-control-ampソフトウェア
電源ONからOFFまでの動作を動画にしてみました
この機能はスリープからの復帰(ON)とスリープへの移行(OFF)で実現している。(この目的で Atmega328 “P” を選んだ)
HELLO…の時に、抵抗でコンデンサの突入電流を抑えている。この後に抵抗をMOSFETで短絡する。
入力1(青色)から入力2(緑色)への切り替えで入力セレクタに使ったリレーの音が聞こえる
設定モードに入って設定をする動画
電源が入っている状態でエンコーダの長押しで設定モードに入る。
1年半使って、電源断が400回とシステムダウンが12回発生。
電源断はハードウェアの設計が悪いせい(LME49600のBWは切っておいた方が良かった。発熱と電力消費が激しい)だけど, システムダウンはどうもWDTの時間が短いようで。
ここでは初期ボリュームを5に、電源ボタンをリモコンコード0xbf40(TOSHIBA), 0x12(電源ボタン)に設定する様子。
この設定はATmega328P内蔵のEEPROMに書き込むので本当に電源が切れても記憶した設定は消えない。
この動作はこのページで説明している内容 なのでリンクを見に行ってください。
ソースコードにある文章をスクロールしながら表示しているのがわかる
ソフトウェアで実現する機能
- ステレオ電子ボリューム
- 2系統オーディオ入力セレクタ
- 突入電流抑制回路をMCUで制御
- ディスプレイ
- ステレオオーディオレベルメーター,ピークメーター
- 赤外線リモコンでコントロール
- ロータリーエンコーダーでコントロール
以上の機能をATmega328Pの Flash: 32 kBytes, SRAM: 2kBytesに詰め込みます。
開発環境はAVR標準のAtmel Studio 7
バイナリの書き込みはAVRISP mkⅡ
開発言語はISO C99 で行ないました。
MCUに使うAtmel AVR ATmega328Pのリソース
このソフトウェアに関係のありそうな物を抜粋しました
Parameter Name | Value |
---|
Program Memory Type | Flash |
Program Memory (KB) | 32 |
CPU Speed (MIPS) | 20 |
RAM Bytes | 2,048 |
Data EEPROM (bytes) | 1024 |
Digital Communication Peripherals | 1-UART, 2-SPI, 1-I2C |
Capture/Compare/PWM Peripherals | 1 Input Capture, 1 CCP, 6PWM |
Timers | 2 x 8-bit, 1 x 16-bit |
Comparators | 1 |
Temperature Range (C) | -40 to 85 |
Operating Voltage Range (V) | 1.8 to 5.5 |
Pin Count | 32 |
Low Power | Yes |
Cap Touch Channels | 16 |
microchip AVR datasheet
AVR内蔵周辺機能の割り当て表
周辺機能名 | 割り当て | 補足説明 |
---|
INT0 | エンコーダーの入力スイッチ | スリープ復帰トリガーのLレベル割り込み |
INT1 | 赤外線モジュールIRMの入力 | Lレベル割り込み |
TIMER0 | 赤外線リモコンパルスカウント用 | |
TIMER1 | ステートマシン同期用 | |
TWI(I2C) | I2Cディスプレイモジュール通信 | 割り込み機能は未使用 |
WDT | ウォッチドッグタイマー | |
IOピンの割り当て
制御回路図
プロジェクトヘッダーファイル
ビルド&プログラミング
Windows 10上のAtmel Studio 7でビルドしています。
ATmega328PへのプログラミングはAVRISP mkⅡを使っています。
Atmel Studio 7の設定
とある日のビルドログ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| ------ Build started: Project: AudioAmpApplication, Configuration: Debug AVR ------
Build started.
Project "AudioAmpApplication.cproj" (default targets):
Target "PreBuildEvent" skipped, due to false condition; ('$(PreBuildEvent)'!='') was evaluated as (''!='').
Target "CoreBuild" in file "C:Program Files (x86)AtmelStudio7.0VsCompiler.targets" from project "AudioAmpApplication.cproj" (target "Build" depends on it):
Using "RunCompilerTask" task from assembly "C:Program Files (x86)AtmelStudio7.0ExtensionsApplicationAvrGCC.dll".
Task "RunCompilerTask"
Shell Utils Path C:Program Files (x86)AtmelStudio7.0shellUtils
C:Program Files (x86)AtmelStudio7.0shellUtilsmake.exe all
make: Nothing to be done for `all'.
Done executing task "RunCompilerTask".
Using "RunOutputFileVerifyTask" task from assembly "C:Program Files (x86)AtmelStudio7.0ExtensionsApplicationAvrGCC.dll".
Task "RunOutputFileVerifyTask"
Program Memory Usage : 15178 bytes 46.3 % Full
Data Memory Usage : 1051 bytes 51.3 % Full
Done executing task "RunOutputFileVerifyTask".
Done building target "CoreBuild" in project "AudioAmpApplication.cproj".
Target "PostBuildEvent" skipped, due to false condition; ('$(PostBuildEvent)' != '') was evaluated as ('' != '').
Target "Build" in file "C:Program Files (x86)AtmelStudio7.0VsAvr.common.targets" from project "AudioAmpApplication.cproj" (entry point):
Done building target "Build" in project "AudioAmpApplication.cproj".
Done building project "AudioAmpApplication.cproj".
Build succeeded.
========== Build: 1 succeeded or up-to-date, 0 failed, 0 skipped ==========
|
関係のありそうな部分を貼り付けておきます
- Program Memory Usage
15178 bytes / 32768 = 0.463
46.3 % Full - Data Memory Usage
1051 bytes / 2048 = 0.513
51.3 % Full
プログラムメモリは余裕がありますが
データメモリは余裕があるように見えても
実行中のスタック領域を残さないといけないので
ほぼリソースを使い切ったと考えています。
ユーザーからの入力を処理するモデルを考える
これだけでアプリケーションの全ての機能を使えるようにする。
内部状態無しで入力と出力が1対1の対応をしようとするとボタンだらけになるので,
入力に対する出力の対応はアプリケーションの内部状態に応じて作動するモデルを作る。
つまりステートマシンのこと
ステートマシン
取り出した状態を実行する状態機械(ステートマシン)はこれで設計します。
有限オートマトン -Wikipedia
状態をコードに
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| //
// システム状態
//
typedef enum {
DEEP_SLEEPING_STATE = 0,
NORMAL_STATE,
PENDING_SYSTEM_POWER_ON_STATE,
PENDING_SYSTEM_POWER_OFF_STATE,
INFO_DISP_STATE,
MUTE_STATE,
OMIT_DISPLAY_STATE,
DEBUG_DISP_STATE,
} system_state_t;
//自動状態移行の間隔
#define STATE_SHIFT_PENDING_TIME 800
|
状態機械をコードに
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
| //
//メインループ
//
for(;;) {
//状態推移用周期関数呼び出し
periodic_func_of_system_state();
//
//ステートマシンの状態によって
//それぞれの処理をする
//
switch (SysVal.system_state)
{
//
case DEEP_SLEEPING_STATE: {
//
//システムOFF中の呼び出し
//入力スイッチ、赤外線リモコンのLレベル割り込みでスリープからの復帰をするが、
//入力スイッチ割り込みはその割り込みハンドラ内でPOWER ON待機状態に移行するために
//赤外線リモコン割り込み発生時のみここに来る。
//
IRR_frame_t frame = {0};
IOwrite_to_red_green_blue_LED(0b100);
if (is_power_onoff_remocon_frame_block (&frame)) {
switch_to_system_state (PENDING_SYSTEM_POWER_ON_STATE);
} else {
IOwrite_to_red_green_blue_LED(0b000);
_delay_ms(1);
if (IRR_getCaptureCondition() == IRR_CAPTURE_IDLE) {
switch_to_system_state (PENDING_SYSTEM_POWER_OFF_STATE);
}
}
} continue; //ループの先頭へ
//
case PENDING_SYSTEM_POWER_ON_STATE: {
//
//system power ON待機状態のためにここでPOWER ON
//
system_power_on(false);
//
//スリープ復帰後ウォッチドッグタイマーを有効にする
//
WDT_ENABLE();
} continue; //ループの先頭へ
//
case PENDING_SYSTEM_POWER_OFF_STATE: {
//
//スリープ中はウォッチドッグタイマーを無効にする
//
wdt_disable();
//
//system power OFF待機状態のためにここでPOWER OFF
//
system_power_off();
} continue; //ループの先頭へ
//
case MUTE_STATE: {
//
//ミュート状態
//
elevol_set_mute();
//外部入力に反応する
bool isShiftState = react_to_external_input();
//情報を表示する
SysVal.system_state_workspace = display_mute_information (SysVal.system_state_workspace, SysVal.volume);
if (isShiftState) {
//状態変化前にミュート解除
elevol_clear_mute(SysVal.volume);
}
} break;
//
case OMIT_DISPLAY_STATE: {
//
//表示省略状態
//
//ディスプレイをOFFする
display_power_save();
//外部入力に反応する
bool isShiftState = react_to_external_input();
if (isShiftState) {
//
//時間のかかる処理なので、一旦ウォッチドッグタイマーを無効にする
//
wdt_disable();
//状態変化前にディスプレイをONする
display_prepare();
//
//ウォッチドッグタイマーを有効にする
//
WDT_ENABLE();
}
} break;
//
case DEBUG_DISP_STATE: {
//
//デバッグ情報表示状態
//情報表示はすでに行われているので
//ここでは何もしない
//
//外部入力に反応する
react_to_external_input();
} break;
//
case INFO_DISP_STATE: {
//
//情報表示状態
//
//外部入力に反応する
react_to_external_input();
//情報を表示する
display_information (SysVal.volume);
} break;
//
case NORMAL_STATE: {
//
//通常状態
//
//レベルメーターを表示する
display_audio_level_meter (&SysVal.audio_level);
//外部入力に反応する
react_to_external_input();
} break;
}
//
//タスク処理終了後
//アイドルモードでスリープ
//赤外線リモコン割り込み、タイマ割り込み等があるまで
//ここで停止する
//
set_sleep_mode(SLEEP_MODE_IDLE);
sleep_mode();
//
//ウォッチドッグタイマーをリセットする
//
wdt_reset();
}
|
システムの初期化処理
システムの駆動
メインループでスリープしながら,TIMER1に同期して動きます. 常に一定時間間隔で動くので,時間待ちが必要なときにはそれぞれの関数で呼び出し回数を数えて待ち時間を調整します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| //
//TIMER1割り込み開始
//このソフトウェアのステートマシンはこのタイマ割り込みに同期して動きます
//
static inline void timer1_start()
{
#if F_CPU == 8000000UL
//
//CTCモード (8分周 * (1+3599)) / 8MHz= 3.6ms
//
TCCR1A = 0b00000000;
TCCR1B = 0b00001010;
TIMSK1 = _BV(OCIE1A); //比較A割り込みを設定
OCR1A = 3599;
#else
#error "F_CPU is not valid"
#endif
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| //
//タイマ1割り込みベクタ
//
ISR(TIMER1_COMPA_vect)
{
static uint8_t division_counter = 0; //分周用
//
//オーディオレベル取り込み
//
periodic_capture_audio_level (&SysVal.audio_level);
//ロータリーエンコーダ読み込み
capture_rotary_encoder();
//分周カウンタのカウント
division_counter = (division_counter + 1) & 15; // 1/16分周
//分周カウンタのカウント満了毎で読み込む
if (division_counter == 0) {
//入力スイッチ読み込み
SysVal.input_switch_conditions = (SysVal.input_switch_conditions << 1) | (IOread_from_input_switch() & 1);
}
}
|
リモコン受信後の読み込み
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| //
//リモコン入力に反応する
//状態移行が発生したらtrueを返す
//
static bool react_to_remocon_input( IRR_frame_t *frame )
{
bool retval = false;
if (frame->type == ERROR) {
return false;
}
//リモコンコードの確認
switch (get_ircode_mapping(&frame->u))
{
//電源ON/OFF
case IRCODE_onoff: {
retval = switch_to_system_state (PENDING_SYSTEM_POWER_OFF_STATE);
//repeat無効にする
frame->u.ir.data_code = 0x00;
} break;
//入力切替
case IRCODE_switch_source: {
shift_to_next_audio_source();
//repeat無効にする
frame->u.ir.data_code = 0x00;
//変化を表示する
retval = switch_to_system_state (INFO_DISP_STATE);
} break;
//音量+
case IRCODE_volume_up: {
SysVal.volume = elevol_setvolume(SysVal.volume+1);
//変化を表示する
retval = switch_to_system_state (INFO_DISP_STATE);
} break;
//音量ー
case IRCODE_volume_down: {
SysVal.volume = elevol_setvolume(SysVal.volume-1);
//変化を表示する
retval = switch_to_system_state (INFO_DISP_STATE);
} break;
//ミュート
case IRCODE_mute: {
retval = switch_to_system_state (MUTE_STATE);
//repeat無効にする
frame->u.ir.data_code = 0x00;
} break;
//表示省略
case IRCODE_omit_display: {
retval = switch_to_system_state (OMIT_DISPLAY_STATE);
//repeat無効にする
frame->u.ir.data_code = 0x00;
} break;
//その他
case IRCODE_total_num :
case IRCODE_not_found : break;
}
return retval;
}
|
ロータリーエンコーダ入力の読み込み
スイッチにはチャッタリングがあるのですが,このアプリケーションではチャッタリング継続時間より遅く読み込むことでチャッタリングの対処とします。
タイマ1割り込み毎にロータリーエンコーダのA相とB相の位相の変化を元に時計回り,反時計回りを検出します。
早く回されると読み込みが追いつかなくなって変化を検出できなくなる問題にはエンコーダーつまみを大きくすることで角速度を下げる方法で対処とします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| //
//ロータリーエンコーダ読み込み
//
static void capture_rotary_encoder()
{
static uint8_t enc = 0; //以前に呼ばれたときの値を保存する変数
bool clockwise, anticlockwise;
enc <<= 2;
enc |= (IOread_from_renc_A() <<1) | IOread_from_renc_B(); clockwise = (enc >>2 &1) & (enc &1) & (enc >>3 &1) & (~enc >>1 &1);
anticlockwise = (enc >>2 &1) & (enc &1) & (~enc >>3 &1) & (enc >>1 &1);
SysVal.renc_rotation_travel += clockwise; // 時計回りで増加
SysVal.renc_rotation_travel -= anticlockwise; //反時計回りで減少
}
|
入力スイッチの読み込み
検出しなければならないのは
タイマ1割り込みによって一定時間間隔で取り込んだ入力スイッチの状態の数を数えて時間を検出し,3種類を区別して検出します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| //
//エンコーダ移動&入力スイッチに反応する
//状態移行が発生したらtrueを返す
//
static bool react_to_rotenc_sw_input()
{
bool retval = false;
//エンコーダ入力の処理
if (SysVal.renc_rotation_travel != 0) {
//ボリュームの変更
SysVal.volume = elevol_setvolume(SysVal.volume + SysVal.renc_rotation_travel);
//入力を受け付けたのでクリアする
SysVal.renc_rotation_travel = 0;
//変化を表示する
retval = switch_to_system_state (INFO_DISP_STATE);
}
if (0 == SysVal.input_switch_conditions) {
//
//入力スイッチが押されていない、つまり何もしない
//
} else if (0x100 <= SysVal.input_switch_conditions) {
//
//入力スイッチの長押しを検出した
//
IOwrite_to_red_green_blue_LED(0b100);
//
if (0x1000000000000000 <= SysVal.input_switch_conditions) { // //長押し検出 -> システムリセット
//
software_reset();
} else if ((SysVal.input_switch_conditions & 1) == 0) {
//
//長押し&戻し検出 -> 電源を切る
//
retval = switch_to_system_state (PENDING_SYSTEM_POWER_OFF_STATE);
}
} else if ((SysVal.input_switch_conditions & 1) == 0) {
//
//入力スイッチの押し下げ後、離されたのを検出したので、入力切替する
//
//入力を受け付けたのでクリアする
SysVal.input_switch_conditions = 0;
//入力切替する
shift_to_next_audio_source();
//変化を表示する
retval = switch_to_system_state (INFO_DISP_STATE);
}
return retval;
}
|
アプリケーションソースコード
ApplicationIncl.h
AudioAmpApplication.c