楽しいAVRでGPS時計を作ってみる
- 2009/12/08 14:09
- カテゴリー:make:
先日,MakeTokyoMeeting04を見学し,Maker達の熱意にずいぶん影響を受けたのですが,ともあれGPS Laboさんでわけて頂いた,GPS時計プログラムを書き込んだATtiny2313を生かさねばなりません。
とにかく簡単に試せるという事で,MTM04の直後にGPSモジュールを秋月電子に手配を済ませてあり,翌週の土日から検討に入っています。
LCDは昨年の今頃,大阪のデジットで入手した大型のLCDモジュールを使う事としましたが,予想通りあっさり動いてしまいました。
元々,PICをアセンブラで使い慣れている私としては,個性の強いMSP430やPSoCならまだしも,個性の弱いAVRを今さら使うのもどうかなあ,と感じて試す気にならずにいました。
ところが,GCCが使える(つまり無制限で優秀なCコンパイラが使える)ことと,tiny2313であれば秋月価格で1つ100円と圧倒的に安いことが私にとっては強烈な個性に見えて,軽く遊んでみることにしました。幸いライタは昨年のデジット詣の際に購入済みです。
GCCが使えるという事から,プロセッサのアーキテクチャが無個性であることがうかがい知れるわけですが,逆にPICでは個性が強すぎてGCCをインプリ出来なかったわけですね。先日も話していたのですが,PICは「クセの強さをアセンブラで味わうマイコン」です。
開発環境であるAVRstudioも使いやすいですし,tiny2313はちょっとメモリが少ないですが,最大20MIPSという高速処理と,レジスタと内部メモリが1クロックアクセス出来る事もあり,プログラムを作る上で速度低下に気をつけながらコードを書かずに済むため,コンパイラ任せでも精神的に随分楽です。
わずか100円で20MIPS,豊富なI/OポートをCでグリグリ動かせますから,もうPIC18F84とはおさらばです。ということで,すっかり私はAVRのファンになってしまいました。
AVRを使うと決めた以上,いろいろ試しておきたくなるのが人情というもので,USIという原始的なシリアルインターフェースモジュールを使ってSPIを実現する(8MHzのクロックで4MHzの転送クロックが出せます)プログラムや,LCD関連のライブラリをひと通り準備しました。
これは,MacBookPro上にSunのVirtualBoxという仮想マシンを入れて,Windows環境が実現出来たことが随分大きいです。後日書こうと思っていますが,このVirtualBox,かなり出来がよいです。無償ですが,有償の仮想マシンを買う必要を全く感じません。
余談ですが,x86には仮想マシンを実装するにはやや問題があり,普通に仮想マシンを実装しようとしてもパフォーマンスががた落ちになり,うまくいきません。有償の仮想マシンとして有名なVMwareはこの問題をうまく解決したソフトとして知られていますが,Core2Duoなど仮想マシンを前提とした新しいx86であれば,あまりおかしな事をしなくても仮想マシンをちゃんと実装出来ます。だから,無償の仮想マシンでもなんら問題はないというのが,私の考えだったりします。
実際の所,VirtualBoxがどうやっているかはわかりませんが,仮想マシンを支援するハードウェアを使うかどうかのオプションがあるので,少なくともパフォーマンスへの貢献はあるのでしょう。
話が逸れてしまいましたが,開発関連ツールはWindowsが前提なだけに,自宅にまともなWindows環境が手に入ったことはとても大きなことで,今回のAVRを楽しむことだって,その恩恵だと思います。
それで,GPSクロックですが,どうもうまくありません。
GPS Laboさんは,処理などに時間がかかり,表示された時刻が正確な時刻ではない,という話をされており,これをきちんと表示させるには1PPSの信号のあるGPSモジュールを使うしかない,といわれていました。
実際,平気な顔をして1秒くらい表示が遅れています。いかにGPSが原子時計の精度を持っているといっても,表示が1秒もずれたら,正確な時計としては存在価値がありません。
GPS Laboさんのソフトは,時刻情報の含まれているGPGGAセンテンスを割り込みで一気に80バイトのバッファに取り込み,その後文字列から時刻情報を抽出し,JSTに変換してLCDに転送する仕組みです。初期設定が終わったらひたすら空ループを回すのが,main()の中身だったりします。
それはそれでいいのですが,割り込みは出来るだけ軽く,という考えからすると,時刻情報の抽出やJSTへの変換,LCD表示などはやはりmain()で処理したいところです。ごく普通に,UARTからの割り込みで1バイトをバッファに取り込む,という処理だけを割り込みで行うと,1センテンスの取り込み完了を待たずに続きの処理が出来る事になるので,表示のアップデートにかかる時間が短縮されて,表示の遅れが小さくなりそうです。
そこで,UARTの練習も兼ねて,プログラムの書き直しをしてみることにしました。センテンスの処理を行って時刻情報を抽出する部分は流用させて頂きますが,それ以外は他の方のコードを参考にするか,自分で作るかします。
UARTから受信割り込みがきたら,割り込み処理の関数で1バイトをリングバッファに取り込み,ライトポインタを進め,割り込みから復帰させます。
main()ではループのなかで$をリングバッファからサーチし,以下に続くGPGGAが見つかったら,続けて取得した文字列をデコード,時刻情報を取り出しJSTに変換,LCDへの転送を行います。
リングバッファは1センテンス分というより処理が間に合うだけという感じですので,当初は64バイトを確保しました。
また,ついでにGPRMCセンテンスから日付情報も取得するようにしてみました。
ところが,これでもやっぱり遅れます。考えてみると,UARTから80バイトを取り込むのにかかる時間は多くても100ms程度ですから,これをゼロにしても,1秒の遅れが解消されるわけではありません。なにが原因なんだろう・・・やっぱり1PPS出力がないとダメなのでしょうか。
GPSモジュールとしては,MNEA-0183で送り出す測位情報を外部のマイコンで記録してくれることを期待しているわけですから,必要なのはその測位を行った時刻が正確であることです。リアルタイムで正確な時刻を吐き出すことはしていなくても当然です。だからGT-720Fを使う限りは,もう無理なのかなあと思ったのですが,あきらめずにもう少しだけ頑張ってみることにします。
GT-720Fには,ボーレートを変更するためのツールがあります。9600bpsではなく,これを38400bpsにすれば1センテンスの受信にかかる時間が1/4に短縮されます。
ということでいろいろツールをいじっていると,別の端子から専用のパルスを出すのとは違いますが,UARTへの出力を1秒で正確に出すことで1PPSが可能になったり,測位を1秒に2回とか4回といったように,測位周期を変えるたり出来ます。また,送り出すセンテンス種類を制限したり,それぞれの送信頻度を調整できたりします。
タイムゾーンの設定が出来るとJSTへの変換もいらなくなるのでいいなと思ったのですが,設定を行っても反映されないようなのであきらめました。
これらの設定をいろいろ試行錯誤したところ,AVR側の処理速度と受信バッファの大きさから,38400ボーでは取りこぼしが起きています。64バイトの受信バッファでは,あと数バイトたりない感じです。
悔しいので,96バイトまで増やすことにしました。
64バイトだと,リングバッファを実現するのに,リードポインタもライトポインタも0x3fでANDするだけでサイクリックに回せるのですが,96では真面目に96になったかどうかを判定し,真ならゼロにリセットという処理をせねばなりません。せっかくボーレートを上げても,処理が増える事で速度低下が起きるようなら本末転倒ですが,まあやるだけやってみます。
また測位周期は1秒に1度から1秒に2度にします。これまでは1秒に1度しかGPGGAセンテンスが飛んでこず,よって1秒に1度しか表示が更新されないことになるので,表示のズレが1秒くらい出てきてしまうことは避けられないように思いました。
しかし,1秒に2回にすれば,500msごとにGGAセンテンスが送られてきて,表示も1秒に2回更新されることになりますから,GPSモジュールが本当に1秒未満のズレで時刻を送ってこなければ,表示のズレは500ms以内に収まるはずです。
さらに,処理を軽くするために,GPGGAとGPRMC以外のセンテンスの送信を停止し,さらに日付情報が欲しいだけのGPRMCセンテンスの送信頻度を10秒に一度にします。こうすると,GPGGAが運んでくる時刻情報が他のセンテンスに邪魔されず,1PPSを守って送られてくると考えたのです。
また,AVRの処理速度を根本的に向上させるため,これまで内蔵クロック8MHzだったものを,外部クリスタルによる20MHz駆動としました。UARTのボーレートも正確になり,一石二鳥です。
そんなこんなで,こんなコードになりました。
つづきはこちら
----
//
// GPS Clock Original part2 Dec.7,2009
// clock:20MHz(Exnternal)
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
volatile char Rx_Buf[96]; // ring buffer
volatile unsigned char Rx_write_pointer; // write pointer
unsigned char Rx_read_pointer; // read pointer
unsigned char hh; // present hour
unsigned char dd; // present day
void LCD_Init(void);
void LCD_E_clk(void);
void LCD_busy_check(void);
void LCD_Control(unsigned char);
void LCD_WriteData(unsigned char);
void LCD_Position(unsigned char, unsigned char);
void Usart_Init(unsigned int);
int Rx_check(void);
void Rx_wait(void);
int Get_RxData(void);
void decode_gga(void);
void decode_rmc(void);
int main(void)
{
char sentens[2];
DDRB = 0x0f;
DDRD = 0x78;
PORTD |= _BV(PD6);
LCD_Init();
LCD_WriteData('1');
LCD_WriteData('.');
LCD_WriteData('1');
LCD_WriteData('0'); // version 1.10
_delay_ms(2000);
LCD_Control(0x01);
LCD_Position(10, 0);
LCD_WriteData('S');
LCD_WriteData('A');
LCD_WriteData('T');
LCD_WriteData(':');
LCD_Position(11, 1);
LCD_WriteData('S');
LCD_WriteData('t');
LCD_WriteData(':');
Usart_Init(38400);
sei(); //
while(1){
while (Get_RxData() != '$');
Get_RxData(); // G
Get_RxData(); // P
sentens[0] = Get_RxData();
sentens[1] = Get_RxData();
sentens[2] = Get_RxData();
Get_RxData(); // ,
if (sentens[0] == 'G'){
if (sentens[1] == 'G'){
if (sentens[2] == 'A'){
decode_gga();
}
}
}
if (sentens[0] == 'R'){
if (sentens[1] == 'M'){
if (sentens[2] == 'C'){
decode_rmc();
}
}
}
}
// GPGGA sentens decode & print
void decode_gga(void)
{
unsigned char i;
PORTD &= ~(_BV(PD6));
hh = ((Get_RxData() - '0') * 10);
hh += (Get_RxData() - '0');
hh += 9; // Offset for JST(+09:00)
if (hh >= 24){
hh -= 24;
}
LCD_Position(0, 0);
LCD_WriteData(hh / 10 + '0');
LCD_WriteData(hh % 10 + '0'); // hour
LCD_WriteData(':');
LCD_WriteData(Get_RxData());
LCD_WriteData(Get_RxData()); // min
LCD_WriteData(':');
LCD_WriteData(Get_RxData());
LCD_WriteData(Get_RxData()); // sec
for(i = 0;i < 6;i++){
while(Get_RxData() != ',');
}
LCD_Position(14, 0);
LCD_WriteData(Get_RxData());
LCD_WriteData(Get_RxData()); // number of catched satellites
PORTD |= _BV(PD6);
}
// GPRMC sentens decode & print
void decode_rmc(void)
{
unsigned char i;
PORTD &= ~(_BV(PD6));
while(Get_RxData() != ',');
LCD_Position(14, 1);
LCD_WriteData(Get_RxData()); // Status:V=warning , A=Valid
for (i = 0 ; i < 7 ; i++){
while(Get_RxData() != ',');
}
LCD_Position(7, 1);
LCD_WriteData('/');
LCD_WriteData(Get_RxData());
LCD_WriteData(Get_RxData() + (hh < 9)); // day with offset for JST
LCD_Position(4, 1);
LCD_WriteData('/');
LCD_WriteData(Get_RxData());
LCD_WriteData(Get_RxData()); // month
LCD_Position(0, 1);
LCD_WriteData('2');
LCD_WriteData('0');
LCD_WriteData(Get_RxData());
LCD_WriteData(Get_RxData()); // year
PORTD |= _BV(PD6);
}
//
// USART library
//
// USART init
void Usart_Init(unsigned int baud)
{
unsigned int ubrr = (((F_CPU >> 4) + (baud >> 1)) / baud - 1);
UBRRH = (unsigned char)(ubrr >> 8); // Baudrate High8bit
UBRRL = (unsigned char)ubrr; // Baudrate Low8bit
UCSRB = _BV(RXCIE) | _BV(RXEN); // Receive Interupt Enable
}
// Check for receiving
int Rx_check(void)
{
return (Rx_write_pointer != Rx_read_pointer) ? 1 : 0;
}
// Wait for receiving
void Rx_wait(void)
{
while(!Rx_check());
}
// Get received data
int Get_RxData(void)
{
Rx_wait();
if ( Rx_read_pointer++ == 95){
Rx_read_pointer = 0;
}
return Rx_Buf[Rx_read_pointer];
}
// Interupt
ISR(USART_RX_vect)
{
if ( Rx_write_pointer++ == 95){
Rx_write_pointer = 0;
}
Rx_Buf[Rx_write_pointer] = UDR;
}
//
// LCD Library Nov.30,2009
//
// Pin assign
#define LCD_D7 _BV(PB3)
#define LCD_D6 _BV(PB2)
#define LCD_D5 _BV(PB1)
#define LCD_D4 _BV(PB0)
#define LCD_RS _BV(PD5)
#define LCD_RW _BV(PD4)
#define LCD_E _BV(PD3)
void LCD_Init(void)
{
_delay_ms(30);
PORTB = 0x03;
LCD_E_clk();
_delay_ms(10);
PORTB = 0x03;
LCD_E_clk();
_delay_us(200);
PORTB = 0x03;
LCD_E_clk();
PORTB = 0x02;
LCD_E_clk();
LCD_Control(0x2c); // Function Set(4bit,Duty=1)
LCD_Control(0x08); // Display OFF.Cursor OFF.Blink OFF
LCD_Control(0x06); // Entry Mode(Right Shift)
LCD_Control(0x0c); // Display ON,Cursor OFF,Blink OFF
LCD_Control(0x01); // Display Clear
}
void LCD_E_clk(void)
{
PORTD |= LCD_E;
_delay_us(200);
PORTD &= ~LCD_E;
_delay_us(200);
}
void LCD_busy_check(void)
{
unsigned char i;
DDRB = 0;
PORTD |= LCD_RW;
while(1){
PORTD |= LCD_E;
_delay_ms(1);
i = PINB << 4;
PORTD &= ~LCD_E;
_delay_ms(1);
PORTD |= LCD_E;
_delay_ms(1);
i |= PINB & 0x0f;
PORTD &= ~LCD_E;
_delay_ms(1);
if((i & 0x80) == 0) break;
}
PORTD &= ~LCD_RW;
DDRB = 0x0f;
}
void LCD_Control(unsigned char data)
{
LCD_busy_check();
PORTB = data >> 4;
LCD_E_clk();
PORTB = data & 0x0f;
LCD_E_clk();
}
void LCD_WriteData(unsigned char data)
{
LCD_busy_check();
PORTD |= LCD_RS;
PORTB = data >> 4;
LCD_E_clk();
PORTB = data & 0x0f;
LCD_E_clk();
PORTD &= ~LCD_RS;
}
void LCD_Position(unsigned char x, unsigned char y)
{
LCD_Control(0x80 + (y * 0x40) + x);
}
----
ちなみに,このコードをコンパイルすると2048バイトのバイナリとなり,tiny2313のフラッシュメモリを綺麗に使い切ります。というか足りなくなって,あちこち削っていったのですが・・・
PD6にLEDが繋がっているのですが,これはGPGGAとGPRMCのセンテンスが見つかってから表示が行われるまでの間点灯することにしています。まあ,あんまり意味もないのですが,点滅していると安心します。
表示内容はまずGPGGAに含まれている時刻で,JSTにするため9時間進めています。進めた結果24を越えたら24引いて調整します。これはオリジナルのコードそのままなのですが,この時刻情報はグローバル変数に置いて,日付の処理でも使うようにします。
次にGPGGAの受信している衛星の数です。これはそのまま表示するだけです。
次は日付の表示ですが,これはちょっと面倒です。GPRMCから日付情報を抽出するのですが,これはUTCにおける日付ですから,JSTに変換しないといけません。単純に日だけ1つ進めても,31日ある月と28日しかない月があるので,毎月のデータを持って条件判断しないといけません。
さらに12月31日の場合,月も年も1つ進める必要があり,そうなると年に一度のためにプログラムは大きくなるわ,条件分岐も増えるわで,ろくな事がありません。
そこで,覚悟を持って割り切りました。もう日付だけ進めることにしようと。そうすると,時刻を9と比較し,その結果を時刻に足し込むだけで済んでしまいます。
だから,10月32日とか,2月30日とか,そんなウソを平気でつきます。まあいいじゃないですか,どうせ使うの私だけだし。
最後に,そのデータが有効か無効かを表示させています。Aなら有効,Vなら無効です。でも,こんなの実際は全然必要がありません。受信した衛星の数が分かれば同じ事ですし,GPSモジュールにあるLEDが点滅すれば,それは有効な状態ですから。
当初は,電波のC/Nを表示させようとしたのです。しかし,考えてみると衛星は複数受信されます。それぞれのC/Nがデータとして送られてきますが,すべて表示するスペースはありませんし,どれか1つを選んでも,または平均を取るなどしても,あまり意味はないでしょう。手間が多いだけで効果無しと判断し,表示をやめました。
しかし,それだとスペースが1つあいてしまいます。そこでなんでもいいから,簡単に表示出来るものを無理矢理探したところ,ステータスを出す事になったという,こんな最低な理由です。いやー,本物の商品設計では出来ないようなだらしなさを満喫していますね。
LCDの関数群も結構オリジナルから流用しています。特に真面目にBUSYチェックを行っているところや,4ビット単位で2度転送するところなどはそのままです。ただ,LCDのスペックからいってクロックをもう少し高速に出来ることと,初期化を少し変更しました。あと,表示位置を指定する関数を作っておきました。本当は文字列を表示する関数も用意してあり,PSoCのライブラリと互換を取ってあるのですが,わずか2kバイトのtiny2313では邪魔なだけなのでカットです。
UARTの部分は,他の方のコードを参考にさせて頂きました。リングバッファを用意して,割り込みではひたすら1バイトの受信を行うだけです。
受信したデータを読み出す関数では,もしリングバッファのライトポインタとリードポインタが一致していると,これはUARTからなにも受信されていないということを示していますので,UARTからデータが飛んでくるまでウェイトします。
リングカウンタですから,もしリードが遅く,ライトが速いと,過去のデータが上書きされてしまうことがあります。しかし,このUARTはフロー制御を行っていませんので,UARTの送信側を待たせることは出来ません。だから,上書きされないようにバッファを大きく取るか,処理を高速にするかしかありません。
1センテンスが80バイト程度ですから,64バイトだとちょっと足りません。そこは逐次処理でなんとかなるだろう,と適当に思ったのですが,さすがにそれだと19200ボーが精一杯というところで,96バイトに増やして38400ボーにしたことは前述しました。
ところで先頭のバージョン表示,本当は「GPS CLOCK2 V.1.01」などと表示していたのですが,リングバッファを96バイトにしたことでフラッシュメモリのサイズをオーバーしてしまい,数字だけ表示するという行き当たりばったりの方法でなんとか2048バイトに収めました。
例えば,リングバッファのサイズが64バイトなら,
(pointer++ & 0x3f)
と書けば,pointerが64になると同時にゼロになります。でもこれは64や32といった数だから出来る技で,96にするとANDでマスクしてゼロリセットという方法は出来ません。128なら出来ますが,tiny2313だとRAMが足りません。
で,結局96バイトし,真面目に条件分岐をしています。結果フラッシュメモリに収まらなくなってしまいました。
さて,このプログラムで結果はどうだったか,と言うと,実はあんまり変化がなく,がっかりな感じです。全体の高速化に加え,表示も1秒間に2回更新しているので,1秒という大きなズレはないと信じていたのですが,1秒近いズレが出ます。ただし,遅れるときだけとは限りません。進むこともあります。
GPSモジュールをPCに繋いだ時にちゃんと確認すれば良かったのですが,これはもうGPSモジュールから出てくるデータがずれているとしか思えません。そうだとすると,GPS Laboさんのコードのままでも良かったことになるわけで,私がやったことは結局なんの利益にも繋がらなかったことになります。
ちょっとがっかり,なのですが,これ以上工夫はもう思いつきませんし,かといってこのままでは正確な時計ではない(せめて1秒なら1秒常にずれてくれればいいのですが,ずれる量がコロコロ変わるのです)わけで,その上このGPSモジュールがいかに感度の良いものであっても,私の部屋では室内での受信は難しいものである以上,実用性はほとんどない,という結論になってしまいそうです。
GPSという高度なシステムを使っているわりに,役に立たないものになったというのはいかにも素人の電子工作という風情で笑ってしまいますが,現時点で最高精度を誇る電波時計の横に並べて,どれくらいずれているかを確認することを,この時計の楽しみにしようと思います。後ろ向きですね。
え,素直に緯度と経度を表示したら,ですか?ACアダプタ駆動で移動できないこのGPSが表示するのは,いつも決まった緯度と経度ですよ。それでも面白いですか?
