|
○ はじめに
前回 と 前々回 で、サーボモータを動かすことが出来ました。ところで、本文書で使っている基板にはボリュームという入力デバイスが付いています。せっかくですからこれを使ってサーボモータを手動で動かしてみたいと思います。ボリュームからの入力については、LECTURE 4 A/D 変換でボリューム(可変抵抗)値を読む で説明しています。
○ コンセプト
以下の様なコンセプトで、処理を行います。
- LECTURE 9 サーボモータを動かす(高速PWM機能使用) と同じくタイマ/カウンタ1 の高速 PWM 機能を使ってサーボモータを動かす
- LECTURE 4 A/D 変換でボリューム(可変抵抗)値を読む と同じく A/D 変換によりボリューム値を読み込む
- ↑で読み込んだボリューム値に応じてタイマ/カウンタ1 の OCR1A(比較 A レジスタ)の値を変化させ、サーボモータの角度を制御する
ところで、本文書で筆者が使ったボリュームの回転角度と 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 を設定することで、ボリュームの開度に応じてサーボモータの 回転角度が変化することになります。
以上で解説は終わりです。ボリュームをくるくる回すと、サーボモータがそれについてくるのがちょっと楽しかったりします…