VFC を PWM によるデジタルコントロールにしてみました。
前回は多回転ボリュームを使って手動で調整をしました。これだと10秒単位ぐらいならともかく、1000秒単位の調整は面倒くさすぎてやっていられません。のでここをMCUで自動化します。
PWM による VFC のコントロール
OCXO から出ている Vref を PWM でスイッチングしてローパスフィルタにかけてコントロール電圧を生成します。DAC デバイスの電源電圧を Vref にするという形でもよさそうですが手元に DAC デバイスがないので PWM でやっています。
PWM の周波数は約30Hzです。これは mbed の PwmOut の period_us() に int の最大値 (1<<15) を設定した値になります。(なんでこのAPIはintなんでしょうかね?)
これにより、int16_t 変数でパルス幅を保持して、pulsewidth_us() が最大の分解能を持つようにしています。
PWMの周波数が30Hzぐらいなので、外部フィルタとしてかなり低い周波数のローパスフィルタを使っています。応答性が高くてもあまり意味がないないし困るので1Hzぐらいのローパスフィルタでも良さそうです。
また、PWM 周波数を変えた直後はグリッチが発生するので、2秒間のGPS1PPS割込みを無視するようなコードを加えました。
制御
PID制御などではなく、まずは適当にインクリメント・デクリメントし続けるような実装にしました。
当然 PID 制御したほうがいいのですが、とりあえず頭を使いたくなかったので…
結果
シリアルに出している直近1000秒ごとの誤差合計を、雑なワンライナーで整形して Google Spreadsheet でグラフ化してみました。主にソフトウェア的要因と思われる過補正を繰替えしている雰囲気を感じられますが、概ね±10mHz(±1ppb)におさまっています。
GPSDO
調整を自動化したので、これはもはやGPSDOと呼べそうです。PID制御にするなど、ソフトウェア的な制御改良の余地はありますが、GPSDOの大枠が実装できました。
どちらかといえばあとはアナログ回路の要素が多くなります。といっても以下らへんを実装すれば実用的になりそうです。
- 出力が矩形波なので高次のローパスフィルタないしバンドパスフィルタを設計実装する
- 波形整形するバッファを使っていないのでバッファのしかたを変える
- GPSのアンテナ入力を外部入力に変える
- GPS FIx されず、1PPS 信号がきてないときのタイムアウト処理を追加
- ケースに入れる
アナログまわりが勉強中なので現状の技術力だと難しい面もありますが、製作例もあるのでなんとかはなりそうです。とはいえ、作っても現状だと応用先がスペアナぐらいしかないので、あんまりやる気がありません。
かなり簡単な構成でppbオーダーの高精度なクロックが得られるのは面白いです。もうちょっとアナログ回路の技術力があればさくっと作りあげられそうです。
コード
#include "mbed.h"
#include <stdint.h>
#include <inttypes.h>
void set_system_clock_to_external_10mhz() {
#ifdef DEBUG_CLOCK
// XXX: set CLKOUT to debug (dp24)
LPC_IOCON->PIO0_1 = (0b001<<0/*FUNC=CLKOUT*/);
LPC_SYSCON->CLKOUTCLKSEL = 0b11; /*0b00=IRC, 0b01=SysOsc, 0b10=WDT, 0b11=Main*/
LPC_SYSCON->CLKOUTDIV = 1;
LPC_SYSCON->CLKOUTUEN = 0;
LPC_SYSCON->CLKOUTUEN = 1;
#endif
// Power down config (irc=on, sysosc=on)
LPC_SYSCON->PDRUNCFG &= ~(
(1<<0/*IRCOUT_PD*/) |
(1<<1/*IRC_PD*/)
);
// Ensure clock source to internal in temporary
LPC_SYSCON->MAINCLKSEL = 0b00;
LPC_SYSCON->MAINCLKUEN = 0x00;
LPC_SYSCON->MAINCLKUEN = 0x01;
while ( (LPC_SYSCON->MAINCLKUEN & 0b1) == 0);
// Power down config (syspll=off, sysosc=off)
LPC_SYSCON->PDRUNCFG |= (
(1<<5/*SYSOSC_PD*/) |
(1<<7/*SYSPLL_PD*/)
);
// Set System OSC = BYPASS (clock fed to XTALIN directly 1.8V / XTALOUT must be floating)
LPC_SYSCON->SYSOSCCTRL = (1<<0/*BYPASS*/);
// PLL Clock Source = System OSC
LPC_SYSCON->SYSPLLCLKSEL = (0b01<<0/*SEL*/);
// Update PLL Source
LPC_SYSCON->SYSPLLCLKUEN = 0;
LPC_SYSCON->SYSPLLCLKUEN = 1;
while ( (LPC_SYSCON->SYSPLLCLKUEN & 0b1) == 0 );
// Set PLL
// M = F_clkout / F_clkin
// FCCO = 2 * P * F_clkout (P = {1, 2, 4, 8}) (FCCO=156-320MHz)
// F_clkout = 50MHz / M = 5 / P = 2 / FCCO = 200MHz
LPC_SYSCON->SYSPLLCTRL = ( (5 - 1)<<0/*MSEL*/) | (0b01<<5/*PSEL*/);
// Power down config (syspll=on)
LPC_SYSCON->PDRUNCFG &= ~(
(1<<7/*SYSPLL_PD*/)
);
// Wait for PLL Lock
while ( (LPC_SYSCON->SYSPLLSTAT & 0b1) == 0 );
// Update Main Clock to PLL Output
LPC_SYSCON->MAINCLKSEL = 0b11;
LPC_SYSCON->MAINCLKUEN = 0x00;
LPC_SYSCON->MAINCLKUEN = 0x01;
while ( (LPC_SYSCON->MAINCLKUEN & 0b1) == 0);
// Power down config (irc=off)
LPC_SYSCON->PDRUNCFG |= (
(1<<0/*IRCOUT_PD*/) |
(1<<1/*IRC_PD*/)
);
SystemCoreClock = 50000000;
}
constexpr uint32_t CLOCK = 10000000;
constexpr uint16_t HISTORY = 1000;
int8_t errors[HISTORY];
uint16_t error_index = 0;
uint16_t error_count = 0;
volatile bool updated = 0;
volatile uint8_t skip_second = 0;
Serial serial(USBTX, USBRX);
PwmOut osc_control(dp1);
extern "C" void TIMER32_0_IRQHandler (void) {
static uint32_t prev = 0;
LPC_TMR32B0->IR = (1<<4/*CR0 Interrupt*/);
uint32_t count = LPC_TMR32B0->CR0;
uint32_t pps_counter;
if (prev < count) {
pps_counter = count - prev;
} else {
// overflowed
pps_counter = (0xffffff - prev) + count + 1;
}
prev = count;
if (skip_second > 0) {
skip_second--;
return;
}
int16_t error = static_cast<int32_t>(pps_counter) - static_cast<int32_t>(CLOCK);
if (abs(error) < 15) {
error_index = (error_index + 1) % HISTORY;
errors[error_index] = error;
if (error_count < HISTORY) {
error_count++;
}
updated = 1;
}
}
int16_t pulsewidth = 16500;
void vcf_plus(uint8_t count) {
pulsewidth -= count;
osc_control.pulsewidth_us(pulsewidth);
skip_second = 2;
serial.printf("osc_control = %d\n", pulsewidth);
}
void vcf_minus(uint8_t count) {
pulsewidth += count;
osc_control.pulsewidth_us(pulsewidth);
skip_second = 2;
serial.printf("osc_control = %d\n", pulsewidth);
}
int main() {
set_system_clock_to_external_10mhz();
NVIC_EnableIRQ(TIMER_32_0_IRQn);
osc_control.period_us(1<<15);
osc_control.pulsewidth_us(pulsewidth);
// enable 32bit counter
LPC_SYSCON->SYSAHBCLKCTRL |= (1<<9/*CT32B0 32bit counter clock*/);
// Capture pin dp14 (for gps 1pps)
LPC_IOCON->PIO1_5 |= (0b010<<0/*FUNC=CT32B0_CAP0*/);
// Match output (not used)
// LPC_IOCON->PIO1_6 |= (0b010<<0/*FUNC=CT32B0_MAT0*/);
// LPC_IOCON->PIO1_7 |= (0b010<<0/*FUNC=CT32B0_MAT1*/);
// LPC_IOCON->PIO0_1 |= (0b010<<0/*FUNC=CT32B0_MAT2*/);
// LPC_IOCON->R_PIO0_11 |= (0b011<<0/*FUNC=CT32B0_MAT3*/);
// Prescaler
LPC_TMR32B0->PR = 4;
// Capture on CAP0 Rising Edge (to CR0) and Enable Interrupt
LPC_TMR32B0->CCR =
(1<<0/*CAP0RE*/) |
(1<<2/*CAP0I*/);
LPC_TMR32B0->MCR = 0;
LPC_TMR32B0->CTCR = (0b00<<0/*Counter/Timer Mode=Timer*/);
LPC_TMR32B0->TCR = (1<<0/*Counter Enable*/);
serial.baud(115200);
for (;;) {
if (serial.readable()) {
char c = serial.getc();
serial.putc(c);
serial.printf(" %d\n", c);
if (c == '+' || c == 'p') {
vcf_plus(100);
} else
if (c == '-' || c == 'm') {
vcf_minus(100);
}
}
if (updated) {
updated = 0;
int8_t last = errors[error_index];
serial.printf("[%d] last: %+d\n", error_count, last);
if (last >= 2) {
vcf_minus(1);
} else
if (last <= -2) {
vcf_plus(1);
}
if (error_count >= 10) {
int32_t sum = 0;
for (int i = 0; i < 10; i++) {
sum += errors[ (error_index + HISTORY - i) % HISTORY ];
}
serial.printf("[%d] %+ddHz\n", error_count, sum);
if (sum >= 2) {
vcf_minus(1);
} else
if (sum <= -2) {
vcf_plus(1);
}
}
if (error_count >= 100) {
int32_t sum = 0;
for (int i = 0; i < 100; i++) {
sum += errors[ (error_index + HISTORY - i) % HISTORY ];
}
serial.printf("[%d] %+dcHz\n", error_count, sum);
if (sum >= 2) {
vcf_minus(1);
} else
if (sum <= -2) {
vcf_plus(1);
}
}
if (error_count >= 1000) {
int32_t sum = 0;
for (int i = 0; i < 1000; i++) {
sum += errors[ (error_index + HISTORY - i) % HISTORY ];
}
// 1000_0000000
serial.printf("[%d] %+dmHz\n", error_count, sum);
if (sum >= 2) {
vcf_minus(1);
} else
if (sum <= -2) {
vcf_plus(1);
}
}
}
}
}