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

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に書き込むので本当に電源が切れても記憶した設定は消えない。

この動作はこのページで説明している内容なのでリンクを見に行ってください。

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

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

  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 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内蔵周辺機能の割り当て表

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

IOピンの割り当て

ビルド&プログラミング

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

Atmel Studio 7の設定

8MHzクロックの設定
F_CPU=8000000ULつまり8MHzクロックの設定です。
C99でかいてます
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の対応をしようとするとボタンだらけになるので,入力に対する出力の対応はアプリケーションの内部状態に応じて作動するモデルになった。

IRamp-application-model-02
おおざっぱにモデルを作ってみた
IRamp-application-model-03
状態を取り出して整理する

有限状態機械

取り出した状態を実行する状態機械(ステートマシン)はこれで設計します. 有限オートマトン -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();
}

システムの初期化処理

IRamp-init-01
初期化の流れ

システムの駆動

メインループでスリープしながら,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割り込みによって一定時間間隔で取り込んだ入力スイッチの状態の数を数えて時間を検出し,それらを検出します.

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

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください