GPS時計の新作
- 2016/04/20 11:42
- カテゴリー:make:
先日から取り組んでいたGPS時計ですが,ようやく完成しました。写真はブレッドボードで作った状態ですが,近いうちに基板にハンダ付けして,綺麗にまとめようと思います。
今回はなかなか大変でした。
まず,aitendoで買ったNEO6M-ANT-4Pというモジュールですが,これはu-bloxのNEO6Mというなかなか性能のいいGPSモジュールを搭載しているので,基本性能は抜群です。
ただ,NEO6M-ANT-4Pというモジュールになると,バックアップ電池が死んでいて一晩放置するとCold Startになってしまうという問題がありました。
ですので,GPS側の設定を変更しないといけないようなプログラムを書くことは出来ません。常に初期状態で立ち上がることを前提にしないといけないので,シリアルポートのボーレートは9600bps,センテンスの更新周期は1秒に1度,1PPSパルスは衛星をつかまえないと出てこないという制約で動くようにする必要があります。
次の問題は,データが正しくデコード出来ないという問題です。1PPSで更新せず,センテンスをデコードしてそのまま表示した時には,発生しない問題でした。
GPSからのデータは,テキストによるセンテンスで,UARTから流れてきます。メモリの小さいtiny2313ですので,UARTのバッファは64バイト程度と極めて小さいため,バッファがあふれる前にさっさと処理をしないとダメです。
そこで,流れてくる情報を最小限の物にしたいところですが,これは前述の通りGPSの設定を変えないといけないですから,今回は出来ません。ログを見れば,かなり大量のデータが流れ込んでいますので,処理に手間取っているとリングバッファが1周して大事なデータが上書きされてしまいます。
ですが,1PPSで更新をしないように作った場合には問題は出ず,1PPSでの更新を行うとデコード出来ない場合が出てくるというのは,ちょっと解せません。
調べて見ると,実はLCDの表示に使っていた関数で,ウェイトが極端に長い部分がありました。5usでいいのに,1msも安全のためにとってあったんですね。
で,1PPSで一気に表示を更新する時に,ここで100ms近くも足止めを食らってしまい,その間にデータが流れてくると,未デコードのデータが上書きされて失われてしまうのでした。
これが,随時更新という形にすると,時刻と日付が別々に更新されるので,付加が平均化し,なんとか上書きされずに処理が間に合うようでした。
ということで,ウェイトをぐっと減らして,この問題は解決です。
そして最後は,1PPSとGPGGAやGPRMCというセンテンスが完全に非同期であるという問題です。先日もちょっと書きましたが,1PPSのパルスが出た段階で受け取っている最新の時刻情報が,1PPSの前の情報なのか後ろの情報なのか,不定なのです。
1PPSは非常に正確であり,すべての衛星で完全に同期しています。ですから,この1PPSで表示を更新すれば非常に正確な時計が作れることは明白ではあるのですが,更新に使う時刻情報が1PPSよりも過去のものなら1秒加算しないといけませんし,1PPSの未来の情報なら,そのまま更新に使わねばなりません。
私は,未来の情報が出てくるなんてことはないだろうと思っていましたから,更新時には1秒加算した時刻で更新することにしていました。これで問題がないように思ったのですが,電源を入れて衛星をつかまえた最初のうちは,1秒進んでいる事に気付いてしまったから大変です。
しばらくすると1秒進むのが解消され,正確な時刻になっているのですが,どこで変わってしまったのかわかりません。じーっと見ていても変化せず,気が付いたら変化しているという苦しい状況で,なかなか尻尾をつかめません。
対策は,1秒加算するかしないかで条件分岐すればいいんですが,問題はその条件がわからないことです。GPGGAなりGPRMCで出てくる時刻の情報は,最初は未来のものなのですが,しばらくすると過去の物になります。すぐに変化することもあれば,長い間変化しない場合もあり,しかも変わったことがセンテンスからはわかりません。
そもそも,プログラムにとって,時刻の情報はGPSから出てくる物がすべてですし,これしか時刻情報を持たない以上は比較も出来ず,結局のところGPSから出てくる時刻を信用するほかありません。その時刻が,いつの間にか過去になったり未来になったりするんです。
ははーん,これはGPGGAやGPGMCの時刻情報のうち,1/10秒や1/100秒に違いがあるんじゃないか?
そう思って見てみたのですが,どちらも小数点以下は00です。数年前に使ったGPSモジュールでは,ここが.99になったり.00になったりしたので,きっと今回のモジュールもそうだと思っていたのに,ダメでした。これは使えません。
なら,つかまえた衛星の数と相関があるんじゃないか?
残念ながら,相関はありませんでした。4個でも11個でも,未来から過去に突然切り替わります。
なら,1PPSの有無に相関はないか?ありませんでした。
うーん,じゃステータスがVなら未来,Aなら過去なんじゃない?違いました。
うむむむ,じゃ,情報の信頼性(DOT)の数値に相関は?
この数字は,Cold Start直後は99.99で,衛星をつかまえて時間が経過するとどんどん小さな値になります。1.00以下になったら過去になるとか,そういう相関があるのではないかと,シリアルポートに繋いでダラダラと記録したログを頑張って見てみましたが,残念ながら相関はなし。
このままでは,表示されている時刻のうち,1秒があっているのかあっていないのか,わからないままになってしまいます。せっかく衛星をつかまえている限り狂うことがない正確な時計を作ったのに,1秒が信用出来ないなんて,話になりません。
これだったら,1PPSで更新しないで,センテンスが出てきた瞬間に表示をした方が,まだ正確です。この方法だと未来かも過去かも知れませんが,ずれは1秒以内のはずですから,1PPSで更新する方が状況は悪いです。
悔しい。やっぱりGPS時計は無理なのか・・・
とにかく,頑張ってログをとり続けます。そして,変化したことが分かった時点でログを解析します。
そうすると,1秒に1回送られているセンテンスに書かれている時刻が,2回続けて同じ時刻になっている箇所が見つかりました。1秒に一度なのに,2回同じ時刻が出てくれば,そりゃ1秒遅れてしまいます。
よく調べてみると,電源を入れて衛星をつかまえた直後は,やっぱり1秒未来の時刻が出てきます。しばらくすると,ほぼ現在の時刻に一致した周期で,時刻が送られてきます。
変化するタイミングは不定で,いつ切り替わるかわかりません。また,過去の時刻に切り替わったとしても,ほぼ現在時刻に一致していて,これをそのまま表示しても違和感はないんじゃないんじゃないかと思うくらいです。
でも,とにかく同じ時刻が2度続いてきたことを検知できれば,時刻が未来なのか過去なのかがわかります。
そこで,1回前の秒のデータを残して起き,今回の秒と比較をして変化していない場合には,以後の時刻データは未来の物から過去の物に切り替わったと判定し,1秒加算することにしました。
この対策の問題点は,Hot StartやWarm Startの場合には,電源投入時にすでに過去の時刻に切り替わっているかも知れないので,変化を検知できないということです。
もちろん,同じ時刻が二度続けば1秒加算することになるのですが,1秒加算しないといけない「過去の時刻」が電源投入時から流れてくると,加算する条件がつかまえられずに,ずっと遅れたままになってしまうわけです。
これを防ぐ為には,なからずCold Startで起動する必要があります。ん?電池が死んでいるのは,かえって好都合なんじゃないのか?
ただ,どんな場合でも,同じ時刻が2回続いた時には,これはもう確実に過去の時刻であることがはっきりしますから,1秒加算された状態である事が画面に表示されていれば,その時刻は信用出来るということになります。消えていれば,1秒ずれている可能性があるということですね。
もう1つの問題は,一度過去の時刻に切り替わったあとは,もう二度と未来の時刻を出すことはないという前提になっていることです。当然,未来の時刻を出すように変化することもありえるとおもいますし,その場合には,時刻が1回前のものと比べて,2秒進んだことを検出出来ればよいだけです。
このコードを組み込んでみたのですが,実際に動作したことがないので,正しいかどうかはわかりません。
ということで,問題点がなんとか解決しました。
基本構造は,センテンスのデコードを行い,得られた情報のうち衛星の個数やステータスは随時,時刻と日付については1PPSで更新を行うというものです。
1PPSはINT0で割り込みをかけます。UARTでも割り込みを使いますが,AVRの場合は割り込みの優先度は固定で,優先度を変えたければ割り込み要因そのものを見直すということになりますが,今回は1PPSの優先度を上げたので,上位のINT0でいきます。
Cold Startでは,衛星をつかまえないと1PPSが出てきません。1PPSでしか画面を更新しないと,ずっと画面が固まったままになるので,ステータスがV(つまり1PPSが出てこない)時には,随時更新されるようにしておきます。
また,立ち上がった直後には時刻情報がでたらめですので,時が24以上になったり,月が13以上になっている場合には不正と見なして,時刻や日付を0でリセットします。
タイムゾーンは悩みました。メモリサイズの関係もあり,日本標準時に固定してあったのですが,頑張ってあいたポートにスイッチを繋いで,変更出来るようにしようと思ったのです。
ですが,プラス側の補正はこれまで通りとしても,マイナス側の補正にするとまた日時も調整をし直すコードが必要になったり,調べて見ると時差って1時間単位じゃなく,30分単位だったりして,コードがあふれてしまったのでやめました。
とりあえずタイムゾーンは+9で固定とし,補正値を画面に出しておくことにとどめました。
で,かなり不細工でお恥ずかしいのですが,以下がソースです。
//
// New GPS Clock for NEO6M-ANT-4P
// April 19,2016
// Clock : 16.9344MHz External
// Fuse : CKSEL3-0:1111
// SUT1-0:00
// CKOUT:1
// CKDIV8:1
// 9600bps , 1PPS
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#define F_CPU 16934400UL
#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)
#define LCD_CS _BV(PB5)
#define LCD_DAT _BV(PB6)
#define LCD_WR _BV(PB7)
void LCD_Init(void);
void LCD_E_clk(void);
void LCD_busy_check(void);
void LCD_Control(unsigned char data);
void LCD_WriteData(unsigned char data);
void LCD_Position(unsigned char x, unsigned char y);
void Usart_Init(unsigned int baud);
int Rx_check(void);
void Rx_wait(void);
int Get_RxData(void);
void decode_gga(void);
void decode_rmc(void);
void DisplayUpdate(void);
void LCD_WRITE_BIT(unsigned char c);
void LCD_COMMAND(unsigned char c);
void LCD_DATA(unsigned char addr, unsigned char data);
void LCD_DISP(unsigned char c, unsigned char n, char f);
volatile char Rx_Buf[64]; // ring buffer
volatile unsigned char Rx_write_pointer; // write pointer
unsigned char Rx_read_pointer; // read pointer
unsigned char year; // present year
unsigned char mon; // present mon
unsigned char day; // present day
unsigned char hour; // present hour
unsigned char min; // present min
unsigned char sec; // present sec
unsigned char past = 255; // past sec
char status = 0; // status
unsigned char pps = 0; // PPS flag
unsigned char gga = 0; // GPGGA flag
unsigned char rmc = 0b0100; // GPRMC flag
unsigned char comp = 0; // sec+1 compensate flag
ISR(USART_RX_vect)
{
Rx_Buf[Rx_write_pointer++ & 0x3f] = UDR;
}
ISR(INT0_vect)
{
DisplayUpdate();
}
int main(void)
{
char sentens[3];
int tz;
unsigned char i;
DDRB = 0b11111111;
DDRD = 0b01111000;
PORTB = 0b00010000;
PORTD = 0b10000111;
Usart_Init(9600);
MCUCR = 0b00000011; //INT0 Up Edge
GIMSK = 0b01000000; //INT0 Enable
// LCD Init
LCD_COMMAND(0x02); // SYS EN
LCD_COMMAND(0x06); // LCD ON
LCD_COMMAND(0x52); // RC256k
//LCD Clear
for (i=0 ; i<32 ; i++){
LCD_DATA(i, 0x00);
}
LCD_DATA(15, 0x04);
LCD_DATA( 0,0b0111);
LCD_DISP( 2, 0, -1);
LCD_DISP( 4, 9, -1);
LCD_Init();
LCD_Control(0x01);
LCD_Position(0, 0);
LCD_WriteData('2');
LCD_WriteData('0');
LCD_Position(4, 1);
LCD_WriteData('S');
LCD_WriteData('T');
LCD_WriteData(':');
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
void decode_gga(void)
{
unsigned char i;
LCD_DATA(12, gga ^= 0b0100);
hour = ((Get_RxData() - '0') * 10);
hour += (Get_RxData() - '0');
hour += 9;
// Offset for JST(+09:00)
if (hour >= 24){
hour -= 24;
}
min = ((Get_RxData() - '0') * 10);
min += (Get_RxData() - '0');
sec = ((Get_RxData() - '0') * 10);
sec += (Get_RxData() - '0');
if ((past == sec) && (comp == 0) && (sec < 60)){ // 2回連続で同じ秒が来たときは補正する
comp = 1;
LCD_DATA(20, 0b0001);
}
if (((sec - 2) == past) && (comp == 1)){ // 2秒ずれているときは補正しない
comp = 0;
}
past = sec;
if (comp == 1){
if (++sec == 60){
sec = 0;
if (++min == 60){
min = 0;
if (++hour == 24){
hour = 0;
day++;
}
}
}
}else{
LCD_DATA(20, 0);
}
if (status == 'V'){
pps = 0;
if (hour > 23){
hour = 0;
min = 0;
sec = 0;
}
if (mon > 12){
year = 0;
mon = 0;
day = 0;
}
DisplayUpdate();
}
for(i=0 ; i<6 ; i++){
while(Get_RxData() != ',');
}
LCD_DISP(25, (Get_RxData() - '0'), 1); // Number of Captured Satellites
LCD_DISP(23, (Get_RxData() - '0'), 1); //
}
// GPRMC sentens decode
void decode_rmc(void)
{
unsigned char i;
LCD_DATA(15, rmc ^= 0b0001);
while(Get_RxData() != ',');
status = Get_RxData(); // status
LCD_Position(7, 1);
LCD_WriteData(status); // Status A:active V:invalid
for (i=0 ; i<7 ; i++){
while(Get_RxData() != ',');
}
day = ((Get_RxData() - '0') * 10);
day += (Get_RxData() - '0');
day += (hour < 9);
mon = ((Get_RxData() - '0') * 10);
mon += (Get_RxData() - '0');
year = ((Get_RxData() - '0') * 10);
year += (Get_RxData() - '0');
}
void DisplayUpdate(void)
{
LCD_DATA( 9, pps ^= 0b0010);
LCD_DISP( 5, sec %10, 1);
LCD_DISP( 7, sec /10, 1);
LCD_DISP(10, min %10, 1);
LCD_DISP(13, min /10, 1);
LCD_DISP(16, hour %10, 1);
LCD_DISP(18, hour /10, 1);
LCD_Position(2, 0);
LCD_WriteData(year / 10 + '0');
LCD_WriteData(year % 10 + '0'); // year
LCD_WriteData('/');
LCD_WriteData(mon / 10 + '0');
LCD_WriteData(mon % 10 + '0'); // month
LCD_WriteData('/');
LCD_Position(0, 1);
LCD_WriteData(day / 10 + '0');
LCD_WriteData(day % 10 + '0'); // day
}
// 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
UCSRC = 0x06; // 8bit,stopbit1
}
// 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();
return Rx_Buf[Rx_read_pointer++ & 0x3f];
}
void LCD_Init(void)
{
char i;
_delay_ms(30);
i = PORTB & 0xf0;
PORTB = i | 0x03;
LCD_E_clk();
_delay_ms(10);
PORTB = i | 0x03;
LCD_E_clk();
_delay_us(200);
PORTB = i | 0x03;
LCD_E_clk();
PORTB = i | 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(2);
PORTD &= ~LCD_E;
_delay_us(2);
}
void LCD_busy_check(void)
{
unsigned char i;
DDRB &= 0xf0;
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)
{
char i;
LCD_busy_check();
i = PORTB & 0xf0;
PORTB = i | ((data >> 4) & 0x0f);
LCD_E_clk();
PORTB = i | (data & 0x0f);
LCD_E_clk();
}
void LCD_WriteData(unsigned char data)
{
char i;
LCD_busy_check();
PORTD |= LCD_RS;
i = PORTB & 0xf0;
PORTB = i | ((data >> 4) & 0x0f);
LCD_E_clk();
PORTB = i | (data & 0x0f);
LCD_E_clk();
PORTD &= ~LCD_RS;
}
void LCD_Position(unsigned char x, unsigned char y)
{
LCD_Control(0x80 + (y * 0x40) + x);
}
void LCD_WRITE_BIT(unsigned char c)
{
PORTB &= ~LCD_WR;
_delay_us(5);
if (c>0){
PORTB |= LCD_DAT;
}else{
PORTB &= ~LCD_DAT;
}
PORTB |= LCD_WR;
_delay_us(5);
}
void LCD_COMMAND(unsigned char c)
{
unsigned char i;
PORTB &= ~LCD_CS;
LCD_WRITE_BIT(1);
LCD_WRITE_BIT(0);
LCD_WRITE_BIT(0);
LCD_WRITE_BIT(0);
for (i=0 ; i<8 ; i++){
LCD_WRITE_BIT(c & 0x80);
c <<= 1;
}
PORTB |= LCD_CS;
}
void LCD_DATA(unsigned char addr, unsigned char data)
{
unsigned char i;
PORTB &= ~LCD_CS;
LCD_WRITE_BIT(1);
LCD_WRITE_BIT(0);
LCD_WRITE_BIT(1);
if (addr == 8){
data++;
}
for (i=0 ; i<6 ; i++){
LCD_WRITE_BIT(addr & 0x20);
addr <<= 1;
}
for (i=0 ; i<4 ; i++){
LCD_WRITE_BIT(data & 0x01);
data >>= 1;
}
PORTB |= LCD_CS;
}
void LCD_DISP(unsigned char c, unsigned char n, char f)
//
// Column 1 2 3 4 5 6 7 8 9 10
// 5 7 10 13 16 18 21 23 25 28
{
if (n==0){
LCD_DATA(c , 0b00001111);
LCD_DATA(c+f, 0b00001010);
}else if (n==1){
LCD_DATA(c , 0b00000110);
LCD_DATA(c+f, 0b00000000);
}else if (n==2){
LCD_DATA(c , 0b00001011);
LCD_DATA(c+f, 0b00001100);
}else if (n==3){
LCD_DATA(c , 0b00001111);
LCD_DATA(c+f, 0b00000100);
}else if (n==4){
LCD_DATA(c , 0b00000110);
LCD_DATA(c+f, 0b00000110);
}else if (n==5){
LCD_DATA(c , 0b00001101);
LCD_DATA(c+f, 0b00000110);
}else if (n==6){
LCD_DATA(c , 0b00001101);
LCD_DATA(c+f, 0b00001110);
}else if (n==7){
LCD_DATA(c , 0b00000111);
LCD_DATA(c+f, 0b00000010);
}else if (n==8){
LCD_DATA(c , 0b00001111);
LCD_DATA(c+f, 0b00001110);
}else if (n==9) {
LCD_DATA(c , 0b00001111);
LCD_DATA(c+f, 0b00000110);
}else{
LCD_DATA(c , 0);
LCD_DATA(c+f, 0);
}
}
画面のレイアウトですが,なかなかしっくり来る物がなく,冒頭の写真のように,結局この形に落ち着きました。
まずセグメント部ですが左側の大きな文字の4桁は,真ん中の2桁を使ってつかまえている衛星の数を表示します。その右側の6桁は時刻です。時刻の下にある+09は前述のタイムゾーンによる時差の表示で,UTCに対してこれだけ進んだ時刻が表示されているという印です。
キャラクタ部は,左側から時刻表示です。その右側がステータスの表示で,Aが有効,Vが無効であることを示しています。1PPSはAの時にしか出てきません。
さて,画面の上部にある長方形のセグメントが,ちらちらと点灯するようになっています。ここがなかなか面白いのですが,左側から2つ飛ばしで合計4つが点灯するようになっています。
一番左は,1秒加算しているかどうかを示しています。同じ時刻が続けて飛んできた場合,1PPSに対して過去の時刻になったと判断し1秒加算してから1PPSで更新しますが,加算されているときにここが点灯します。
その左側は,GPRMCセンテンスのデコードを行ったことを示しています。本当はデコード処理中に点灯,処理が終わったら消灯にしていたのですが,それだと点灯時間が短くて見えないので,デコードを始めた時に点灯/消灯させて,ゆっくり点滅するようにしてあります。ここが点滅していれば,少なくともUARTが動作し,GPRMCを見つけていることがわかります。
次の左側は,GPGGAセンテンスのデコードを行った事を示しています。GPRMCと同様です。
最後に一番右は,1PPSで割り込みがかかって,更新が行われるときに点灯/消灯します。1PPSが来ているかどうかがわかりますが,ステータスがVの時には1PPSが来ず,随時更新になるため非常に早い周期で点滅をします。ですので点灯したままに見えるのですが,その場合はまだ1PPSが来ていないんだとわかります。
この4つのインジケータは,もともとデバッグ用に入れたものだったんですが,見ていると楽しいし,動いているという確信が持てるので,見やすくした上で残しておきました。
今回は,GPGGAでは時刻とステータスとつかまえた衛星の数を,GPRMCでは日付を得ています。このインジケータの動きを見ていると,先にGPRMCをデコードしてからGPGGAをデコードし,1PPSで更新するという流れがよくわかります。
UARTは昔作ったGPS時計からそのまま流用です。これもどなたかのライブラリをそのまま使わせて頂いたのですが,もう忘れてしまいました。LCDは自分で書いたもので,以前ここでも紹介したと思いますが,ウェイトが長すぎたために短くしたことと,あとはセグメント側の関数を少し修正し,0から9以外の数字が来たら,表示を消すことにしました。
話が前後しますが,CPUのクロックは手持ちの関係で,16.9344MHzを使いました。これ,44.1kHzを作るには便利なクロックなので,CD何かではよく使われるクロックです。
20ピンあるtiny2313の端子はほぼ使い切っています。あいているのはPD6とPB4くらいのものです。メモリもほぼ使い切っており,これ以上の機能追加は断念しました。
ということで,今回はかなり大変でした。GPSモジュールは私が作ったわけではありませんし,そもそも非同期で9600bpsというゆっくりした速度で出てくるデータを,正確な時刻情報で叩くのですから,最初から心配してはいました。案の定ここが最大の山場となりましたが,綺麗な方法ではないのですが,なんとかねじ伏せたという感じです。
一応これで,原子時計レベルに同期した正確な時計が完成しました。とはいえ桁上がりの部分,特に日付が絡んでくるような部分は不安がありますし,1秒加算した部分が長期的に正しく動作するかも不明ですので,もう少し様子を見たいところです。