LECTURE 11 ボリュームでサーボモータを操作する

○ はじめに

前回前々回 で、サーボモータを動かすことが出来ました。ところで、本文書で使っている基板にはボリュームという入力デバイスが付いています。せっかくですからこれを使ってサーボモータを手動で動かしてみたいと思います。ボリュームからの入力については、LECTURE 4 A/D 変換でボリューム(可変抵抗)値を読む で説明しています。

○ コンセプト

以下の様なコンセプトで、処理を行います。

ところで、本文書で筆者が使ったボリュームの回転角度と A/D 入力値の対応は、下図の様になりました。横軸が回転角度で、縦軸が A/D 入力値です。

筆者が使ったボリュームの回転角度と A/D 入力値の対応

図を見ると、開き始めの A/D 入力値の変化が大きく最後の方は変化が小さくなっています。このためサーボモータの動きも、ボリュームの開き始めの変化が大きく最後の方は変化が小さくなりました。上図の特性は、使用するボリュームに応じて異なりますので、気になる方は調べてみて下さい。

○ 回路図

LECTURE 1 ATmega8535 および使用環境について に掲載してある全体回路図に対して、ATMega8535 のボリューム(可変抵抗)部分と OC1A ピンおよびサーボモータと接続する信号線+コネクタ部分を赤枠で囲んだ図を こちら に置いておきます。

○ サンプルプログラムと説明

ボリュームを使ってサーボモータを操作するサンプルプログラムを 11steering.tar.gz からダウンロードできます。はじめに のページから、全てのサンプルプログラムを 固めた tar ファイルをダウンロードすることもできます。

中には

  • Makefile
  • adc.h
  • adc.c
  • util.h
  • util.c
  • timer1.h
  • timer1.c
  • main.c

が格納されています。adc.h/adc.c/util.h/util.c については LECTURE 4 A/D 変換でボリューム(可変抵抗)値を読む の内容と同じ、timer1.h/timer1.c については LECTURE 8 タイマ/カウンタ1を使う の内容と同じで Makefile もほとんど変わらないので、main.c について以下に載せます。なお、以下に掲載しているソースコードには修正 BSD ライセンス条項を含めていませんが、ソースコード配布は修正 BSD ライセンスに従います(はじめに のページの免責事項を参照してください)。

これまでにヒューズビットの設定を行ったことが一度も無い場合は、まず最初に make fuse を実行してください。fuse ターゲットについては、LECTURE 2 LED を点滅させる のサンプルプログラムの項を参照してください。

make 実行後、プログラムをアップロードする場合は make uisp を実行してください。

ボリュームでサーボモータを操作中の動画です(10秒。YouTube に接続します)。

  • main.c
        1    #include <avr/interrupt.h>
        2    #include <avr/io.h>
        3    #include <avr/sleep.h>
        4    
        5    #include "adc.h"
        6    #include "timer1.h"
        7    #include "util.h"
        8    
        9    
       10    #define ICR1_AS_TOP     19999
       11    #define OCR1A_LIMIT_CCW 2100
       12    #define OCR1A_CENTER    1500
       13    #define OCR1A_LIMIT_CW  900
       14    
       15    static void timer1_ic_interrupt(void);
       16    
       17    
       18    int
       19    main(void)
       20    {
       21            DDRD |= _BV(DDD5);  /* PD5 ピンを出力に設定 */
       22            pullup();
       23            PORTA &= ~_BV(PINA0); /* PINA0 プルアップ無効 */
       24    
       25            /* (8MHz/64) = 125KHz */
       26            adc_init(ADC_CK_64, ADC_NO_INT);
       27            adc_set_ch(PINA0);
       28    
       29            timer1_ic_enable();
       30            timer1_ic_set_interrupt(timer1_ic_interrupt);
       31            timer1_set_wave_mode(WGM1_FAST_PWM_ICR1);
       32            timer1_set_coma_mode(COM_A_HIGH_FIRST);
       33            timer1_set_count(0);
       34            timer1_set_icr1(ICR1_AS_TOP);
       35            timer1_set_ocr1a(OCR1A_LIMIT_CW);
       36    
       37            sei();
       38    
       39            /* (8MHz/8)/20000 = 50Hz */
       40            timer1_start(TIMER1_CK_8);
       41        
       42            while (1){
       43                    sleep_mode();
       44            }
       45    
       46            return 0; /* 警告を避けるため */
       47    }
       48    
       49    
       50    /*
       51     * タイマ/カウンタ1 捕獲割込ハンドラ(TOP値到達)
       52     *
       53     * ボリューム開度に応じて OCR1A 値を計算しデューティ比を変更する。
       54     * ボリューム開度に対応する OCR1A 値は以下で計算する
       55     *(R2 はボリューム抵抗最大値、R1 はプルアップ抵抗値)
       56     *
       57     *   OCR1A = OCR1A_LIMIT_CW + (OCR1A_LIMIT_CCW - OCR1A_LIMIT_CW) * 
       58     *               data / (ADC_MAX * (R2 / (R1 + R2)))
       59     *
       60     * ライブラリに頼って浮動小数点計算を行うと、プログラムサイズ
       61     * が 8KB で納まらないため、実値を使って以下の様に計算しておく。
       62     *
       63     *   R2 = 50K
       64     *   R1 = 10K
       65     *   (OCR1A_LIMIT_CCW - OCR1A_LIMIT_CW) / (ADC_MAX * (R2 / (R1 + R2))) = 1.4076
       66     *
       67     * 上記を 10 倍して計算し、最後に 10 で割って正しい値を得る。
       68     *
       69     *   OCR1A = OCR1A_LIMIT_CW + (14 * data) / 10;
       70     */
       71    static void 
       72    timer1_ic_interrupt(void)
       73    {
       74            uint16_t ocr1a;
       75    
       76            adc_start();
       77            adc_wait();
       78    
       79            ocr1a = OCR1A_LIMIT_CW + (14 * adc_get()) / 10;
       80            timer1_set_ocr1a(ocr1a);
       81    }
       82    
       83    /* End of main.c */
    
    
    • 18 〜 47 行目 (main())

      • pullup() の前に PORTD5 のみ出力方向に設定
      • pullup() (pullup.h、PORTD5 以外の全入力ポートの内部プルアップを有効化)
      • PINA0 ピンは入力として使用するので、プルアップを無効化
      • adc_init() (adc.h、A/D 変換器初期化)
      • adc_set_ch() (adc.h、PINA0 をアナログ入力として設定)
      • timer1_ic_enable() (timer1.h、カウンタ最大値 ICR1 到達時の捕獲割り込み有効化)
      • timer1_ic_set_interrupt() (timer1.h、捕獲割り込み時に実行するユーザ関数の登録)
      • timer1_set_wave_mode() (timer1.h、波形生成種別を設定)
      • timer1_set_coma_mode() (timer1.h、比較A出力モードを設定)
      • timer1_set_count() (timer1.h、TCNT1 の初期値を設定)
      • timer1_set_icr1() (timer1.h、ICR1 の値を設定)
      • timer1_set_ocr1a() (timer1.h、OCR1A の値を設定)

      を行った後、sei() (avr/interrupt.h) を実行してシステムの割り込みを有効化しています。

      A/D 変換器の処理については、LECTURE 4 A/D 変換でボリューム(可変抵抗)値を読む の main-noint.c を、タイマ/カウンタ1 の処理については LECTURE9 サーボモータを動かす(高速PWM機能使用) の main.c を参照してください。

      以後 sleep_mode() にてアイドル動作に移行し、サーボモータの角度制御は捕獲割り込みハンドラに任せます。

    • 50 〜 81 行目 (timer1_ic_interrupt())

      捕獲割り込み時に実行されるユーザ関数です。

      本関数は TCNT1 がカウンタ最大値(ICR1)に到達する度(50Hz = 20ms 毎)に実行され、 その時点でのボリューム値を A/D 変換によって読み込み、ボリューム値に応じた OCR1A の値を計算して再設定します。

      A/D 変換による入力値を data、R1 をプルアップ抵抗値、R2 をボリューム抵抗最大値として、 OCR1A を以下の計算式で計算します。

      OCR1A = OCR1A_LIMIT_CW + (OCR1A_LIMIT_CCW - OCR1A_LIMIT_CW) * 
                  data / (ADC_MAX * (R2 / (R1 + R2)))
      

      timer1_ic_interrupt() の関数コメントにある通りに、実値を使って計算を進めると

      OCR1A = OCR1A_LIMIT_CW + 1.4 * data
      

      となります。つまり OCR1A は、OCR1A_LIMIT_CW を下限として data に比例して変化します (0 <= data <= 約 853.182(0x355)、853.182 については こちら を参照してください。なお、筆者の環境では 855 まで上がりました)。

      76 〜 77 行目で A/D 変換を行い、79 行目で新しい OCR1A の値を計算しています。その後 80 行目で新しい OCR1A を設定することで、ボリュームの開度に応じてサーボモータの 回転角度が変化することになります。

以上で解説は終わりです。ボリュームをくるくる回すと、サーボモータがそれについてくるのがちょっと楽しかったりします…