自作赤外線リモコンコントロールアンプソフトウェアを完成させる

IR-control-ampソフトウェア

このソフトウェア一式はGitHubで公開しています。
IR-control-ampソフトウェア

電源ONからOFFまでの動作を動画にしてみました

この機能はスリープからの復帰(ON)とスリープへの移行(OFF)で実現している。(この目的で Atmega328 “P” を選んだ)
HELLO…の時に、抵抗でコンデンサの突入電流を抑えている。この後に抵抗をMOSFETで短絡する。
入力1(青色)から入力2(緑色)への切り替えで入力セレクタに使ったリレーの音が聞こえる

電源ONからOFFまでの動作

設定モードに入って設定をする動画

電源が入っている状態でエンコーダの長押しで設定モードに入る。
1年半使って、電源断が400回とシステムダウンが12回発生。
電源断はハードウェアの設計が悪いせい(LME49600のBWは切っておいた方が良かった。発熱と電力消費が激しい)だけど, システムダウンはどうもWDTの時間が短いようで。

ここでは初期ボリュームを5に、電源ボタンをリモコンコード0xbf40(TOSHIBA), 0x12(電源ボタン)に設定する様子。 この設定はATmega328P内蔵のEEPROMに書き込むので本当に電源が切れても記憶した設定は消えない。
この動作はこのページで説明している内容 なのでリンクを見に行ってください。

ソースコードにある文章をスクロールしながら表示しているのがわかる

この設計したハードウエアとこの後に作ったソフトウェアを組み込んで完成したアンプの動作です。

ソフトウェアで実現する機能

  1. ステレオ電子ボリューム
  2. 2系統オーディオ入力セレクタ
  3. 突入電流抑制回路をMCUで制御
  4. ディスプレイ
  5. ステレオオーディオレベルメーター,ピークメーター
  6. 赤外線リモコンでコントロール
  7. ロータリーエンコーダーでコントロール

以上の機能をATmega328Pの Flash: 32 kBytes, SRAM: 2kBytesに詰め込みます。
開発環境はAVR標準のAtmel Studio 7
バイナリの書き込みはAVRISP mkⅡ
開発言語はISO C99 で行ないました。

MCUに使うAtmel AVR ATmega328Pのリソース

このソフトウェアに関係のありそうな物を抜粋しました

Parameter NameValue
Program Memory TypeFlash
Program Memory (KB)32
CPU Speed (MIPS)20
RAM Bytes2,048
Data EEPROM (bytes)1024
Digital Communication Peripherals1-UART, 2-SPI, 1-I2C
Capture/Compare/PWM Peripherals1 Input Capture, 1 CCP, 6PWM
Timers2 x 8-bit, 1 x 16-bit
Comparators1
Temperature Range (C)-40 to 85
Operating Voltage Range (V)1.8 to 5.5
Pin Count32
Low PowerYes
Cap Touch Channels16

microchip AVR datasheet

AVR内蔵周辺機能の割り当て表

周辺機能名割り当て補足説明
INT0エンコーダーの入力スイッチスリープ復帰トリガーのLレベル割り込み
INT1赤外線モジュールIRMの入力Lレベル割り込み
TIMER0赤外線リモコンパルスカウント用
TIMER1ステートマシン同期用
TWI(I2C)I2Cディスプレイモジュール通信割り込み機能は未使用
WDTウォッチドッグタイマー

IOピンの割り当て

制御回路図
プロジェクトヘッダーファイル

ビルド&プログラミング

image Windows 10上のAtmel Studio 7でビルドしています。
ATmega328PへのプログラミングはAVRISP mkⅡを使っています。

Atmel Studio 7の設定

F_CPU=8000000ULつまり8MHzクロックの設定です。 C99でかいてます ヒューズビットの設定

とある日のビルドログ

------ 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

状態をコードに

//
// システム状態
//
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

状態機械をコードに

//
//メインループ
//
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に同期して動きます. 常に一定時間間隔で動くので,時間待ちが必要なときにはそれぞれの関数で呼び出し回数を数えて待ち時間を調整します。

//
//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割り込みベクタ
//
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);
    }
}

リモコン受信後の読み込み

//
//リモコン入力に反応する
//状態移行が発生したら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相の位相の変化を元に時計回り,反時計回りを検出します。
早く回されると読み込みが追いつかなくなって変化を検出できなくなる問題にはエンコーダーつまみを大きくすることで角速度を下げる方法で対処とします。

//
//ロータリーエンコーダ読み込み
//
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種類を区別して検出します。

//
//エンコーダ移動&入力スイッチに反応する
//状態移行が発生したら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