OCXO の VFC ピンでの可変範囲を GPS の 1PPS を使って調べた | tech - 氾濫原 でとりあえず可変範囲ぐらいはわかったので、半固定抵抗を使ってだいたい10MHzぐらいにあわせてみたいと思います。
でもって、回路やコードの構成を変えました。
MPU のクロック自体を OCXO の 10MHz とする
前回は mbed デフォルト (内蔵の12MHz CRクロックを逓倍した48MHz) でやっていました。別にこれでもいいのですが、せっかく高精度な 10MHz があるのに、これをクロックにしないのももったいないなという感じになりました。
それに、LPC1114 自体のクロック源をOCXOにすれば、内蔵カウンタでフリーランさせた値がそのまま周波数カウントになるので、GPIOを1ピン節約できます。
つまり 10MHz 5逓倍で 50MHz にして LPC1114 最大クロックで動かしてみたいと思います。OCXO の周波数カウントは5分周すれば元の10MHzになりそうです。
システムクロックとして外部クロック(水晶発振器)を使うには
用語ですが「システムオシレータ」と「システムクロック」に区別があって、システムオシレータは外部水晶発振子を使うことを想定した内蔵発振回路、システムクロックはシステムオシレータまたはBYPASSされた外部クロックを指しています。
まず OCXO は発振器なので、内蔵システムオシレータは使わないようにします。
ドキュメントを見るわけですが、システムオシレータコントロールレジスタ (SYSOSCCTRL) の BYPASS が日本語版だと間違っているので原文を見る必要があります。
Bypass enabled. PLL input (sys_osc_clk) is fed directly from the XTALIN pin bypassing the oscillator. Use this mode when using an external clock source instead of the crystal oscillator.
となっていて、XTALIN から直接クロックを入力する場合には、内蔵のシステムオシレータをバイパスするという意味になります。
まぁとにかく XTALIN には外部クロックを直接入れれば良いのですが (XTALOUT はフロート)、ここで罠があります。XTALIN は他のピンと違って 1.8V 以上入力してはならないことになっています。
5V クロックなら 5.6k / 2.4k、3.3V クロックなら 1.2k / 1k あたりの分圧が必要です。
ただ、どうも OCXO の出力そのままだとうまく分圧できなかったので、一旦バッファ (74HCU04しかなかったのでこれで…) で受けてからクロック入力に使いました。
外部クロック設定コード
ぶっちゃけかなりハマりました。デバッグするためには CLKOUT を有効にして、どの段階までちゃんと出力が出ているかを確認する必要があります。
レジスタ自体はデータシート通りに記述すれば良いのですが、関連レジスタがかなり多いのでたいへんです……
ちょっとハマったのは SYSPLLCTRL の MSEL が実際の分周数-1を設定する必要がありました。(データシートにももちろん書いてありますが…)
やってることは
- (mbed の初期化では IRC=12MHz を PLL で 48MHz にされています)
- PLL を設定しなおすので、メインクロックソースを IRC にしておく (12MHz動作に)
- 一旦 PLL 回路の電源を切る (ついでのシステムオシレータ回路も確実に切っておく)
- システムオシレータをバイパスする設定をする
- PLL 関連レジスタを適当に設定して 5 逓倍できるようにする
- PLL のクロックソースをシステムクロックに
- PLL 回路の電源を入れ、PLL のロックを待つ
- メインクロックを PLL 出力に変える (この時点で 50MHz 動作に)
- IRC の電源を切る (切らなくてもいいんですが)
- SystemCoreClock を50MHzに更新しておく
という感じです。
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;
}
周波数カウント部分の変更
タイマーをタイマーとして使い、システムクロックをそのままカウントします。キャプチャピンはGPS1PPSをキャプチャするようにし、キャプチャのタイミングでキャプチャレジスタにタイマカウンタをコピー(ハードウェア動作)させて割込みを生成します。GPIO のピンチェンジ割込みは使いません。
レジスタの設定は問題ないのですが、割込み周りでだいぶハマりました。
- mbed の InterruptManager で TIMER_32_0_IRQn の割込みを設定しようとしたがうまくいかなかった
- NVIC_EnableIRQ しないとダメだった
- extern "C" を忘れてて割込みが呼ばれずしばらくはまった
また、前回よりも精度を高めるため、10秒、100秒、1000秒のゲートでの誤差を表示するようにしました。dHz (デシヘルツ) cHz (センチヘルツ) mHz (ミリヘルツ) 単位で表示しているのが誤差の項目です。
±1カウントエラーがあるので、多少 +1/-1 がでることがあります。
1000秒単位での調整は面倒なので、10秒単位の調整しかしてませんが、このように手で雑に調整してもこのぐらいの精度が出ていました。
コード
LPC1114 はSRAMが4KBと少ないため、カウントされた値をそのまま持つのではなく、誤差を signed で持つようにしています。そもそもOCXOの可変範囲が±5Hz程度しかないので、それ以上の情報を持つ必要がありません。ということで int8_t の配列で履歴を持って、直近のn件を合計して見る形になっています。
これにより、例えばカウンタをそのまま持つには10Mは16bitに納まらないので32bitの配列にしなければなりませんが、誤差だけを保持すれば4分の1の容量ですみます。というか uint32_t だとLPC1114では1000個の履歴を持てません。
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;
Serial serial(USBTX, USBRX);
extern "C" void TIMER32_0_IRQHandler (void) {
static uint32_t prev = 0;
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;
int16_t error = static_cast<int32_t>(CLOCK) - static_cast<int32_t>(pps_counter);
if (abs(error) < 15) {
error_index = (error_index + 1) % HISTORY;
errors[error_index] = error;
if (error_count < HISTORY) {
error_count++;
}
updated = 1;
}
LPC_TMR32B0->IR = (1<<4/*CR0 Interrupt*/);
}
int main() {
set_system_clock_to_external_10mhz();
NVIC_EnableIRQ(TIMER_32_0_IRQn);
// 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 (updated) {
updated = 0;
serial.printf("[%d] last: %+d\n", error_count, errors[error_index]);
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 (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 (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);
}
}
}
}