パルス波形
ラジコンの送受信系は手軽に手に入る割には飛距離がかなりあります.品物にもよりますが400mは余裕です. この送受信系の信号をマイコンに取り込むと色々と面白いことができます.
このネタは飛行ロボットコンテストに出場するために自作した飛行船に使われていたものです.
どんなパルスが出力されているのか
以下にFUTABAの6chレシーバから出力されていた信号を載せます.ポイントとしては各パルスのH期間はオーバーラップしていないということと, パルス周期が16msecであるということです.
マイコンでパルス幅を取り込むには
ここで扱うマイコンはATMEGA88です.
さて,パルス幅を取り込むには信号のposedgeとnegedgeの時間差を取り込むということです. これの実現にうってつけの機能がAVRには(AVR限らずどんなマイコンにも載っていますが)搭載されています. その機能とはピン変化割り込みです.ピンのレベルが変化すると割り込みがかかるというものです.
プログラムの構造としては以下のようになります. バックグラウンドでハードウエアタイマをフリーランさせておきます.
- ピン変化割り込み発生
- 割り込み発生時のハードウエアタイマの値を保存
- posedgeであれば,タイマの値をold変数に転送
- negedgeであれば現在のタイマの値からold変数の値を減算した値をパルス幅として出力する
簡単ですね.プロポの角度を読み取るという用途であればとても上手く動きます.
6chのパルス幅をデコードできるプログラムを以下に貼っておきます.
typedef struct propo_pulse_t{ uint16_t old_time; uint16_t def_time; //パルス幅測定結果がここに入る }propo_pulse_t; volatile propo_pulse_t propo_pulse[6]; //オブジェクト //パルスデコード //パルス立ち上がりエッジのシステム時間と立下りエッジのシステム時間をそれぞれストアする ISR (PCINT1_vect){ static uint8_t port_state_old; uint8_t port_state_now; uint8_t pulse_def; uint16_t counter_store; port_state_now = PINC; counter_store = TCNT1; pulse_def = (~port_state_old) & port_state_now; if(pulse_def & 0x01){ //アタックのビットが立ったところに現在時刻をストア propo_pulse[0].old_time = counter_store; } if(pulse_def & 0x02){ propo_pulse[1].old_time = counter_store; } if(pulse_def & 0x04){ propo_pulse[2].old_time = counter_store; } if(pulse_def & 0x08){ propo_pulse[3].old_time = counter_store; } if(pulse_def & 0x10){ propo_pulse[4].old_time = counter_store; } if(pulse_def & 0x20){ propo_pulse[5].old_time = counter_store; } pulse_def = (~port_state_now) & port_state_old; port_state_old = port_state_now; //データ更新 //リリースのビットが立ったところで時間の差をとって格納 if(pulse_def & 0x01){ if(counter_store > propo_pulse[0].old_time){ propo_pulse[0].def_time = counter_store - propo_pulse[0].old_time; }else{ propo_pulse[0].def_time = RTC_COMPARE - propo_pulse[0].old_time + counter_store; } } if(pulse_def & 0x02){ if(counter_store > propo_pulse[1].old_time){ propo_pulse[1].def_time = counter_store - propo_pulse[1].old_time; }else{ propo_pulse[1].def_time = RTC_COMPARE - propo_pulse[1].old_time + counter_store; } } if(pulse_def & 0x04){ if(counter_store > propo_pulse[2].old_time){ propo_pulse[2].def_time = counter_store - propo_pulse[2].old_time; }else{ propo_pulse[2].def_time = RTC_COMPARE - propo_pulse[2].old_time + counter_store; } } if(pulse_def & 0x08){ if(counter_store > propo_pulse[3].old_time){ propo_pulse[3].def_time = counter_store - propo_pulse[3].old_time; }else{ propo_pulse[3].def_time = RTC_COMPARE - propo_pulse[3].old_time + counter_store; } } if(pulse_def & 0x10){ if(counter_store > propo_pulse[4].old_time){ propo_pulse[4].def_time = counter_store - propo_pulse[4].old_time; }else{ propo_pulse[4].def_time = RTC_COMPARE - propo_pulse[4].old_time + counter_store; } } if(pulse_def & 0x20){ if(counter_store > propo_pulse[5].old_time){ propo_pulse[5].def_time = counter_store - propo_pulse[5].old_time; }else{ propo_pulse[5].def_time = RTC_COMPARE - propo_pulse[5].old_time + counter_store; } } }
注意事項
レシーバの電源を入れたままプロポの電源を切ると,レシーバが最後に受信したパルス幅が出力され続けます. こと仕様を利用してウォッチドッグ的な処理をしておくといいかもです. 例えば,"パルス幅が30秒間変わらなければノーコン状態になったと自動判断し,着陸シーケンスに入る"のような.
具体的なアプリ
左図のようにプロペラが配置されている飛行船を操縦することを考えます. レシーバとモータアンプを直結したシステムの場合,前進するためにはプロポの左右のスティックを等しい角度に倒す必要があります. もしステック角度が左右で違っていたら,左右に推力差が生じ飛行船は回頭してしまいます.
また,旋回する際は左右のステックの角度を違えないといけません.これまた左右のステックの微妙な操作角度が要求されます.
このシステムの問題は操縦系と運動の座標系が異なっているというところにあります. そこで,これを解決するためにマイコンで座標系の変換を施すことを考えます.
具体的には一次元二つの冗長系を一つの極座標に変換するということです.
左右にあるスティックのうち,左のスティックは一切使わずに右の十字スティックだけで操縦できるようにしてみましょう.
まずは,十字スティックのX軸,Y軸それぞれの角度をx,yとし,これを極座標(r,θ)に変換します.
変換はr=sqrt(x^2+y^2),θ=atan2(y,x)のようになります. ここでつかったatan2は数学ででてくるatanとは異なり,x,yの象限によって角度の符号を変えてくれる関数です.
あとはθの値に応じて前進後進,右旋回,左旋回,右超信地旋回,左超信地旋回等等を切り替え,そしてその強度をrに比例したものにしてやれば, なかなかに良い操縦性を実現することができます.
こういうような処理はソフトウエアを使えばお手の物です.ハードウエアだけでは手が出せない領域です.
最後に
信号がHレベルからLレベルに落ちる遷移を"立ち下がり"と表現しますが,語感としておかしい感じがしませんか? "立ち上がり"はいいと思うんです.でも"立ち下がり"だと立って下がってってどっちやねんって思ってしまいます.
ですので自分はHDLでおなじみのposedge,negedgeを常用しています.
以上蛇足でした