GPS 1ppsから10MHzの標準周波数を発生する装置を作成
故障したGPSモジュール(購入時からの部品取り付け不良)を再生して作ったGPSモジュールからダイレクトPULL方式による標準器を作ってしまったので、暫く作りかけで放置状態でした「REF SIGNAL GENERATOR / GPS」が完成しました。まだ、ちょこちょこプログラムは修正するかもしれませんが、ひとまず完成ということでお披露目。
こちらは、現在、周波数カウンターの基準クロックとして活躍してくれている、GPSモジュールにPLL ICを直結してダイレクトに10MHzを発生する標準器モドキです。独立したオシレーターを持たないので、衛星が3個以上安定して補足できないと使えず、瞬間はジッターが多く、短時間での精度が必要な用途には使えません。原理は簡単!GPSモジュール(u-blox 6)から比較的ジッターの少ない2MHzを出して、IC NB3N502(14 MHz to 190 MHz PLL
Clock Multiplier)で5倍して、10MHzを得ています。ケースは100均で購入。室外に設置したGPSアンテナを含めて5000円に満たない簡単標準10MHz発振器です。ジッターが大きく、ここのパルスの精度が必要なオーディオADC/DACの基準クロックなどには使えませんが、周波数カウンターの基準クロックに使う分には十分な性能と思います。
今回、作成したのは、VOCXOをGPS 1PPSクロックで補正して、正確な10MHzを出力する装置です。衛星をロストしても長時間高い精度の周波数でジッターの少ない標準クロックを安定して供給できます。
高い精度といっても諸事情で、±0.01Hz以下程度の性能になっていますが、個人で使用するには何に使うの?ってくらいの十分な性能と思います。
標準周波数発生装置 REF SIGNAL GENERATOR / GPS の概要
主な部品
VOCXO:OFC MC862X4-004W
GPSモジュール:u-blox 6
コントローラ:Arduino Pro mini /8MHz..
DAC:MCP4725
ケース:映像関係のジャンクから流用。ロジックICもこのジャンクから取り外したものを使用
主な機能
標準周波数:10MHz 4ch、
1pps 1ch、
1MHz 1ch(1MHz予定、現状はコネクタのみ)
表示:20 x 2 バックライトあり、
Power&1pps 表示LED
SW: PushするとLCDの時間表示が、年月日表示になる。
ロータリーエンコーダ: 通常、LCD2行目の表示は、補足中の衛星数と時間表示ですが、これを回すと一時的にGPSモジュールから得られる緯度経度などいくつかのデータを表示する。まぁ。ロータリーエンコーダがあるのに使わないのはさみしいので遊びでつけたおまけ程度(^-^;
回路概要
ArduinoのT1タイマーの標準タイマー機能と捕獲割り込み、溢れ割り込みを使った周波数カウンタで、GPS1ppsを捕獲信号としてICP1にVOCXOのクロックをT1に入力してこれをカウントして、目的周波数差分を計測、差分補正分のVOCXOの制御電圧をDACで発生させて、周波数を補正しています。
使用したArduino pro miniは、Arduinoについて何も知らない時に購入した8MHz版でしたので、10MHzを4分周した2.5MHzを時間軸を長くすることで測定精度を確保しています。
VOCXOは、本来5V駆動品と思いますが、3.3Vでも安定動作するようなので、レギュレータを抱き合わせにして、DACの基準電圧を兼ねた4Vで稼働させています。レギュレータがVOCXOと同じ温度に保たれることで、安定したDACの基準電圧を得ています。(5Vで駆動した方が環境温度に対する耐性が高いかもしれませんが、流用電源の関係で安定化すると4Vとなってしまいました。
30分程度で実用レベル(±0.1Hz程度)にはなりますが、残念ながら、完全安定状態(0.004Hz程度まで安定)になるまでの時間は、4分周して周波数測定している関係もあり、2時間程度とちょっと時間がかかります。(現時点のスケッチでは….汗)
こまかい部分は、Arduinoのスケッチを掲載するんでそちらを参考頂ければわかると思いますので回路図は省略。
本当は、作成中につまらないミスで最初に作成した回路図と最終回路で差分があり、修正版回路図を作成してない為(-_-;)
その分、スケッチには不要なHW情報も若干記載
Arduino スケッチ(プログラム)
Arduinoのスケッチを作成するにあたり、いろいろ役に立ちそうな事があったので、応用できそうな部分など、ボチボチ説明できればと思います。
まぁ、拙いソースなのであとで自分が応用する為のメモみたいなものですが、.....
で、いきなりスケッチ(ちょっと長いですが)
/* GPS base 10MHz with MCP4725 +----------------------------+ |Frequency/stability/accuracy| | GPS Status | +----------------------------+ */ #include <Wire.h> #include <LiquidCrystal.h> #include <Adafruit_MCP4725.h> Adafruit_MCP4725 dac; // constructor #include <avr/io.h> #include <avr/interrupt.h> #include <avr/wdt.h> #include <TinyGPS++.h> TinyGPSPlus gps; #include <SoftwareSerial.h> SoftwareSerial gpsSerial(6, 7); // RX, TX #define rs 2 #define enable 3 #define rw 9 #define d0 A3 #define d1 10 #define d2 A2 #define d3 11 #define d4 A1 #define d5 12 #define d6 A0 #define d7 13 LiquidCrystal lcd(rs, rw, enable, d0, d1, d2, d3, d4, d5, d6, d7); #define enter_sw A6 // front SW 10以下 ON 2000以上 OFF #define en_en 4 // EAW - Absolute Contacting Encoder LCD DATA LINE共通 #define ocxo25mhz 5; //OCXO 10MhHzの4分周2.5mhz #define gps1ppm 8; //GPS 1Hz byte debug = 1; // 1: debug code output uint32_t debug_f = 0 ; uint32_t sc = 0; uint32_t ec = 0; /* 精度設定 */ const int stability = 50; /* 前回測定より約0.5Hz以内の変動になれば、補正開始 */ int accuracy = 40; /* 40で決め打ちしている箇所あり。修正は要注意! */ const int accuracy_max = 1200; /* 400の整数倍1600まで可能だが...時間とDAC精度を考えると800で十分 */ /* VOCXO Voltage */ const int dacvoltmax = 4095; const int dacvolt_norm = 2037; int dacvolt = 2048; /* VOCXO Voltage = 2.0V 初期値:2048 */ int dacvolt_d = 10; /* 値はダミー */ volatile unsigned int intcarry; volatile unsigned int start_cnt; volatile unsigned int end_cnt; volatile unsigned long freq100_old; volatile unsigned long freq100; volatile byte ignore; unsigned int gps_loop; volatile unsigned long count; volatile unsigned long count_old; int val; byte enter; int val_old; byte enter_old = 2; byte encode_old = 0; unsigned long millis_old; char gps_loop_msg3[4] = "***"; char freq_msg15[16] = " ********.**Hz "; const byte encode[256] = //もし255なら読み取りミスで無視する。 { 56, 40, 55, 24, 255, 39, 52, 8, 57, 255, 255, 23, 255, 36, 13, 120, 255, 41, 54, 255, 255, 255, 53, 7, 255, 255, 255, 20, 19, 125, 18, 104, 105, 255, 255, 25, 106, 38, 255, 255, 58, 255, 255, 255, 255, 37, 14, 119, 118, 255, 255, 255, 107, 255, 255, 4, 255, 3, 255, 109, 108, 2, 1, 88, 255, 89, 255, 255, 255, 255, 51, 9, 10, 90, 255, 22, 11, 255, 12, 255, 255, 42, 43, 255, 255, 255, 255, 255, 255, 255, 255, 21, 255, 126, 127, 103, 255, 102, 255, 255, 255, 255, 255, 255, 255, 91, 255, 255, 255, 255, 255, 116, 117, 255, 255, 115, 255, 255, 255, 93, 94, 92, 255, 114, 95, 113, 0, 72, 71, 255, 68, 73, 255, 255, 29, 255, 70, 255, 69, 255, 255, 35, 34, 121, 255, 122, 255, 74, 255, 255, 30, 6, 255, 123, 255, 255, 255, 124, 17, 255, 255, 255, 67, 26, 255, 27, 28, 255, 59, 255, 255, 255, 255, 255, 15, 255, 255, 255, 255, 255, 255, 255, 255, 5, 255, 255, 255, 110, 255, 111, 16, 87, 84, 255, 45, 86, 85, 255, 50, 255, 255, 255, 46, 255, 255, 255, 33, 255, 83, 255, 44, 75, 255, 255, 31, 255, 255, 255, 255, 255, 255, 255, 32, 100, 61, 101, 66, 255, 62, 255, 49, 99, 60, 255, 47, 255, 255, 255, 48, 77, 82, 78, 65, 76, 63, 255, 64, 98, 81, 79, 80, 97, 96, 112 }; byte read_Enter() { // Enter SW byte value; int enter_val = analogRead(enter_sw); if (enter_val < 100) value = 1; else value = 0; return value; } byte read_Encoder() { // Encoder(8bits)の値を読む byte value; pinMode(d0, INPUT); pinMode(d1, INPUT); pinMode(d2, INPUT); pinMode(d3, INPUT); pinMode(d4, INPUT); pinMode(d5, INPUT); pinMode(d6, INPUT); pinMode(d7, INPUT); // digitalWrite(en_en, LOW); PORTD &= ~_BV(4); value = digitalRead(d7) * 128 + digitalRead(d6) * 64 + digitalRead(d5) * 32 + digitalRead(d4) * 16; value = value + digitalRead(d3) * 8 + digitalRead(d2) * 4 + digitalRead(d1) * 2 + digitalRead(d0); //value = (PINB & _BV(5)) * 128 + (PINC & _BV(0)) * 64 + (PINB & _BV(4)) * 32 + (PINC & _BV(1)) * 16; //value = value + (PINB & _BV(3)) * 8 + (PINC & _BV(2)) * 4 + (PINB & _BV(2)) * 2 + (PINC & _BV(3)); // digitalWrite(en_en, HIGH); PORTD |= _BV(4); pinMode(d0, OUTPUT); pinMode(d1, OUTPUT); pinMode(d2, OUTPUT); pinMode(d3, OUTPUT); pinMode(d4, OUTPUT); pinMode(d5, OUTPUT); pinMode(d6, OUTPUT); pinMode(d7, OUTPUT); value = encode[value - 1]; if (value == 255) value = encode_old; else encode_old = value; return value; } ISR(TIMER1_CAPT_vect) { /* WDT再起動(WDT Reset) if(カウンターフラグ無効) ICR1に保存された値を保存 カウンターフラグ有効に if(発生回数が規定値) なら周波数を算出・表示 発生回数・オーバーフロー回数を初期化 カウンターフラグ無効 */ wdt_reset(); gps_loop += 1; if (ignore) { if (start_cnt > 0) { /* 最初の1回はICR1値が不安定なので無視 */ ignore = 0; /* GPS 1ppm 監視 WDT start */ { MCUSR = 0; WDTCSR |= 0b00011000; //WDCE WDE set WDTCSR = 0b01000000 | 0b000111; /* /WDIE set 2s WDT */ } } start_cnt = (word)ICR1; /* キャり追い越し補正する */ if ((start_cnt < 6000) && (TIFR1 & 0x01)) { /* CPUの割り込み処理が6サイクル+実行中コマンドだが実測から... */ TIFR1 = 0b00000001; //Timer/Counter Overflow Flag clear(TOV1) if (debug) debug_f = 2; } intcarry = 0; gps_loop = 0; } else if (gps_loop == accuracy) { count_old = count; freq100_old = freq100; end_cnt = (word)ICR1; /* キャり追い越し補正する */ if (debug) { debug_f = intcarry; sc = start_cnt; ec = end_cnt; } if ((end_cnt < 6000) && (TIFR1 & 0x01)) { /* CPUの割り込み処理が6サイクル+実行中コマンドだが実測から3160... */ intcarry += 1; TIFR1 = 0b00000001; //Timer/Counter Overflow Flag clear(TOV1) if (debug) debug_f = 1; } count = ((intcarry * 65536) + end_cnt) - start_cnt; if ((accuracy >= 400)) { freq100 = count / (gps_loop / 400); } else { freq100 = count * 10; // 40で決め打ち } /* 異常値を無視する */ if (abs(freq100 - 1000000000) > 1500) { count = count_old; freq100 = freq100_old; if (debug) debug_f = 0XFFFF; } char s8[9]; int freq100_surplus = freq100 % 100; sprintf(freq_msg15, " %s.%02dHz ", dtostrf((freq100 / 100), 8, 0, s8), freq100_surplus); start_cnt = end_cnt ; gps_loop = 0; intcarry = 0; /* 起動15分後、OCXOの周波数が安定したら周波数補正開始 */ if ((abs(count - count_old) < stability) && (!ignore) && ((millis() > 900000) || (debug))) { unsigned long count_ref; if ((accuracy >= 400)) { count_ref = 1000000000 * (accuracy / 400); dacvolt_d = abs(count - count_ref) * 800 / accuracy ; if (abs(count - count_ref) <= 0) { accuracy = accuracy + ((accuracy < accuracy_max) * 400) ; } else { if (abs(count - count_ref) >= 50) { accuracy = 40; } else if (abs(count - count_ref) >= 5) { // accuracy = accuracy - (accuracy > 400) * 400; accuracy = 400; } } if (count < (count_ref - 1)) dacvolt += dacvolt_d; if (count > (count_ref + 1)) dacvolt -= dacvolt_d; } else { count_ref = 100000000; dacvolt_d = abs(count - count_ref) * 3.3 ; if (abs(count - count_ref) <= 5) { // accuracy = 400 + (count == count_ref) * 400; /* OCXOが十分調整されてる場合のみ*/ accuracy = 400; }
if (count < count_ref) dacvolt += dacvolt_d; if (count > count_ref) dacvolt -= dacvolt_d;
} } } } ISR(TIMER1_OVF_vect) { /* オーバーフロー回数を加算 */ if (!ignore)intcarry += 1; } /* WDT設定(2s)は、WDTCSRを47hにセットして、wdt_reset(); */ ISR(WDT_vect) { wdt_reset(); /* カウンターフラグを無効にする 発生回数・オーバーフロー回数を初期化 */ start_cnt = 0; gps_loop = 0; ignore = 1; { MCUSR = 0; WDTCSR |= 0b00011000; /* WDCE WDE set */ WDTCSR = 0b00000000; /* status clear */ } } void setup() { pinMode(en_en, OUTPUT); digitalWrite(en_en, HIGH); delay(1); lcd.begin(20, 2); lcd.setCursor(0, 0); lcd.print(" OCXO + MCP4725 10MHz"); lcd.setCursor(0, 1); lcd.print(" 2018.11 by Drg.Jr. "); Serial.begin(115200); Serial.println("Hello World!"); Serial.println("OCXO + MCP4725 10MHz"); Serial.println(""); // pulse gen setup init clock // initialize the MCP4725 dac.begin(0x60); delay(5000); gpsSerial.begin(9600); // ソフトウェアシリアルの初期化 gpsSerial.print("\r\n"); gpsSerial.print("\r\n"); /* u-blox 6 Receiver Description Including Protocol Specification を参考 */ gpsSerial.print("$PUBX,41,1,0007,0003,4800,0*13\r\n"); // GPSモジュールのbpsを変更 gpsSerial.begin(4800); // ソフトウェアシリアルの初期化 gpsSerial.print("\r\n"); gpsSerial.print("\r\n"); lcd.clear(); intcarry = 0; start_cnt = 0; end_cnt = 0; freq100_old = 0; freq100 = 0; ignore = 1; gps_loop = 0; millis_old = millis(); /*カウンターセットアップ*/ cli(); TCCR1A = 0; /* TCCR1A タイマモード */ TCCR1B = 0x46; /*ICP, T1入力、立ち上がり,立下りを検知 */ TCNT1 = 0x0000; /* タイマ1の初期値設定 */ TIMSK1 = 0x21; /* 入力キャプチャ1オーバーフロー割り込みの許可 */ // TIFR1 = 0x21; /* 入力キャプチャ1オーバーフロー割り込み要求クリア */ sei(); /* 全体の割り込み許可 */ } void loop() { char msg20[21]; char msg4[5]; byte val; byte enter; dac.setVoltage(dacvolt, false); //DACに値(0-4095)を設定する if ((millis() % 10) == 0) { char acc; if (!ignore) { if (accuracy == accuracy_max) acc = 'o'; else if (accuracy >= 400) acc = '+'; else acc = '-'; } else { acc = 'x'; } sprintf(msg4, "%04d", dacvolt); lcd.setCursor(0, 0); lcd.print(acc); lcd.print(freq_msg15); lcd.print(msg4); val = read_Encoder(); enter = read_Enter(); if (freq100 != freq100_old) { if (dacvolt > dacvoltmax) { dacvolt = dacvoltmax; /* エラー表示を入れる */ } if (dacvolt < 0) { dacvolt = 0; /* エラー表示を入れる */ } if (abs(dacvolt - dacvolt_norm) > 500) { /* 警告表示を入れる */ } } } /* GPS module Accsess */ { while (gpsSerial.available()) { char read_gps = gpsSerial.read(); gps.encode(read_gps); if ((gps.satellites.isUpdated()) && (gps.time.isUpdated())) { char sa[3]; byte jst = gps.time.hour() + 9; if (jst >= 24) jst = jst - 24; if (enter == 0) { sprintf(msg20, "GpsSat.%s %2d:%02d:%02d", dtostrf(gps.satellites.value(), 2, 0, sa), jst, gps.time.minute(), gps.time.second() ); } else { sprintf(msg20, "GpsSat.%s %4d/%02d/%02d", dtostrf(gps.satellites.value(), 2, 0, sa), gps.date.year(), gps.date.month(), gps.date.day() ); } lcd.setCursor(0, 1); if (val != val_old) { lcd.print(" "); lcd.setCursor(0, 1); switch (int(val / 16)) { case 1: lcd.print("Latitude : "); lcd.print(gps.location.lat(), 6); // Latitude in degrees (double) break; case 2: lcd.print("Longitude : "); lcd.print(gps.location.lng(), 6); // Longitude in degrees (double) break; case 3: lcd.print("Speed (m/s)"); lcd.print(gps.speed.mps()); // Speed in meters per second (double) break; case 4: lcd.print("Speed (km/s)"); lcd.print(gps.speed.kmph()); // Speed in kilometers per hour (double) break; case 5: lcd.print("Course (deg)"); lcd.print(gps.course.deg()); // Course in degrees (double) break; case 6: lcd.print("Altitude (m)"); lcd.print(gps.altitude.meters()); // Altitude in meters (double) break; case 7: lcd.print("Altitude (km)"); lcd.print(gps.altitude.kilometers()); // Altitude in kilometers (double) break; case 8: lcd.print("HDOP : "); lcd.print(gps.hdop.value()); // Horizontal Dim. of Precision (100ths-i32) } val_old = val; } else { lcd.print(msg20); } } // if (gpsSerial.overflow()) Serial.println("SoftwareSerial overflow!"); } if ((debug) && ((millis() - millis_old) >= 1000) && (gps_loop == 1)) { millis_old = millis(); Serial.print(msg20 ); Serial.print("," ); Serial.print(freq100 ); Serial.print("," ); Serial.print(count ); Serial.print("," ); Serial.print(accuracy ); Serial.print("," ); Serial.print(dacvolt ); Serial.print("," ); Serial.print(gps.charsProcessed() ); Serial.print("," ); Serial.println(gps.failedChecksum() ); if (debug_f && (debug > 1)) { if (debug_f < 3 )Serial.println(" Overflow Flag clear" ); Serial.print(sc ); Serial.print("," ); Serial.println(ec ); debug_f = 0; sc = 0; ec = 0; } } } // delay(0); }
ハードウェアの改善点
Ardurnoは、16MHz版で2分周の5MHz入力、可能であれば、24MHzまでオーバークロックして10MHzを生で計測が理想と思います。但し、20MHzあたりから、GPSモジュールの感度に大きく影響してくるようで電源周り、電磁波の回り込みなど配慮が必要)GPSモジュールとのコミュニケーションはSoftwareSirialを使用することになるので、新規に作成するなら、最低16MHz版を使用すべきと思います。8MHz版だと4800bpsまで落としてやっと何とか情報を引き出せる状態となっています。
DACの基準電源は、DACの最低動作電圧2.7Vまで下げた方が少しでも細かい制御が可能になり、精度をもう一桁上げられるかもしれません。現状は0.003Hz/1LSB程度)
今回はDACに12bitsのMCP4725を使用していますが、より細かい制御が期待できる秋月電子通商などでも扱っているMCP3425(16Bit ADC I2C 基準電圧内蔵)が良いかもしれません。基準電源も内蔵してい点でも扱いやすくなります。次回作成の機会があれば、使いたいと思います。
回路を簡素化して、コストも下げたい場合は、スイッチサイエンスが取り扱っているA-Star 328PB Micro – 5V/20MHzw
を使うのも良いかと思います。2個の16ビットカウンタと第2のUSARTが追加されているので、DACの代わりにPWMを使い、GPSモジュールとの通信もより簡単になると思います。
(次回へ続く)….予定 (^-^;