RaspberryPiで作る電波時計への時刻データ送信機の製作記録

送受信機トップ写真

 電波時計が東西の送信所からの送信電波(40kHz/60kHz)を受信できない場合への対応として、RaspberryPiを用いてNTP時刻データを電波時計に送信できる「電波時計への時刻データ送信機」を製作してみた。また、送信データが正しく受信できるかどうかをモニターするための電波時計受信信号モニターも製作した。
尚、今回製作した送信機は電波法に違反しない微弱電波なので、この送信機からある程度離れた電波時計は受信できません。もし、この記事を参考にして製作される場合には、それぞれの責任で電波法に違反しないように注意して下さい。電波時計受信信号モニターを使えば、どこまで信号が届いているか確認できます。

 電波時計は、福島県のおおたかどや山標準電波送信所から送信されている電波(40kHz)か、福岡県と佐賀県の境にあるはがね山標準電波送信所から送信されている電波(60kHz)のどちらかを受信して、正確な時刻を電波時計に設定しています。しかし、電波が届かない場所では、正しい時刻を設定することができず、クオーツに依存した時間刻みになって、しばらくすると正確な時刻からは外れてきます。この「時刻データ送信機」は、NTP時刻データを使って電波時計に送信するので、電波信号を受信できない場所でも使うことができます。
電波時計の時刻データについては、日本標準時グループのサイトを参照して下さい。

●時刻データ送信機の部品構成と配線図

クロックジェネレータが40kHzまたは60kHzの長波を作り、ラズパイで作るタイムコード矩形波をGPIO4から出力し、そのANDを取った波形を2SK2232で増幅して、バーアンテナから送信する。
40kHzまたは60kHzの切り替えSWの状態をGPIO17の割り込みで認識し、出力周波数を切り替える。

<使った部品>
 ・Raspberry Pi 4
 ・Si5351A I2Cクロックジェネレータモジュール:60kHz/40kHzの長波作るため
 ・白色OLED(SSD1306):時刻と送信信号を表示するため
 ・電波時計用バーアンテナは、長さ100mmのものを入手(Amazon)
 ・GPIO17で検知するSWは、60kHz/40kHzを切り替えるためのもの
 ・送信信号は青色LEDでモニターできる

●プログラムの構成

送信所から送信される標準電波信号の出し方については以下のサイトに示されています。
https://jjy.nict.go.jp/jjy/trans/index.html#item1

通常時(毎時15分、45分以外)のタイムコードは、1周期60秒(60ビット)の繰り返しで送出されます。タイムコードの事例は次のウェブページに書かれています。
https://jjy.nict.go.jp/jjy/trans/timecode1.html
前項の部品構成の中で、クロックジェネレータが40kHzまたは60kHzの長波を作り、ラズパイで作るタイムコード矩形波をGPIO4から出力し、そのANDを取った波形を増幅して、バーアンテナから送信する。40kHzまたは60kHzの切り替えは、切り替えSWの状態をGPIO17の割り込みで認識する。

①ある時刻Tの0秒での処理
・コード信号”M”を出力する。GPIO4の”ON”信号を0.2秒維持
・その時刻のタイムコードを算出して、60文字の文字列をセットする。
  例えば、2004年92日(4月1日)17時25分木曜日の例
  (うるう秒情報、停波予告情報、毎時15分、45分情報は考慮しない)
  “M01000101P000100111P000001001P001000010P000000100P100000000P”
 
②次のコード出力時刻Tの1秒のタイマー割り込みをセットする
・現在時刻TのS0秒と次の信号送出時刻Tの1秒の差t0をタイマー割り込みに設定する。
  t0=T(1秒) - T(S0秒)
 絶対現在時刻T(S0秒)を基準に次の絶対処理時刻T(1秒)をセットするので、累積誤差は発生しない。
<タイマー割り込み>ある時刻T(n);(n=1秒~59秒)での処理
・60文字の信号文字列から、n番目のコードを読み出し、そのコードに対応するパルス幅の時間
 ONn秒のコード信号を出力する。GPIO4の”ON”信号をONn秒保持
  ONn = Dn + Pn
     ONn:信号ON時間(出力するコードにより、0.2sec/0.5sec/0.8sec)
     Dn:OLEDへ情報を表示する時間(0.1sec)
     Pn:time.sleep(Pn)する時間
・現在時刻Snと次の信号送出時刻T(n+1)の差tn=n+1 - Snをタイマー割り込みに設定する。

各信号の”ON”長さは、標準電波の出し方に記載されている通り。

コードパルス幅
マーカー(M)(P0~P5)0.2s ±5ms
2進の00.8s ±5ms
2進の10.5s ±5ms

信号を”ON”にした後で、現在の時刻や出力するコードをモニターするためにOLEDに表示しているが、その表示時間に0.1秒程度要しているので、”ON”の保持時間ONnは、パルス幅からDn=0.1秒を差引いた時間Pn=ONn ― Dnをtime.sleepに設定している。

●試験に使った受信信号モニター

 受信機は、電波時計受信モジュールを使い、送られた信号が受信できているかどうかをモニターするために用意した。信号を受信すると青色LEDが点滅。40kHzと60kHzを切り替えできる。

●プログラム例

クロックジェネレータの PLLgen5351 モジュールは、「Si5351A I2Cクロックジェネレーターを使ったクロック出力プログラム例」を参照のこと
白色OLEDの oledSSD1306 モジュールは、「I2C制御の有機ELディスプレイモジュール(制御IC:SSD1306)文字表示プログラム例」を参照のこと

import time
import threading
from datetime import datetime
from datetime import timedelta
import RPi.GPIO as GPIO
import oledSSD1306 as OLED
import PLLgen5351 as PLL
import signal

SIG_OUT = 4		# パルス信号送出
SW_IN = 17		# 周波数変更SW

class TimeCal:
	def __init__(self):
		self.waveStop = False
		self.tm = 0
		self.dataNo = 0
		self.dtStart = 0
		self.timeCode = ""
		self.dtNow = 0
		self.freq = 1		# 60kHz
		self.swOn = False
		self.freqDisp = ["40kHz", "60kHz"]

		GPIO.setmode(GPIO.BCM)
		GPIO.setup(SIG_OUT, GPIO.OUT)
		GPIO.output(SIG_OUT, GPIO.LOW)		# Signal Off
		GPIO.setup(SW_IN, GPIO.IN)
		GPIO.add_event_detect(SW_IN, GPIO.RISING, callback=self.changeFreq, bouncetime=300)
		self.oled = OLED.OLED()
		freq40 = [15, 1, 1, 50, 1200, 1, 8]		# 40kHz
		freq60 = [23, 1, 1, 50, 1200, 1, 8]		# 60kHz
		self.pll = PLL.PLL([freq40, freq60])
		self.pll.freqGen(self.freq)	# 40kHz/60kHz
	
	def changeFreq(self, ch):
		self.tm.cancel()
		self.dataNo = -1
		self.swOn = True
		
	def periodicTimer(self):
		self.tm.cancel()
		if self.waveStop or self.swOn:
			return
		if self.dataNo == -1:
			dtPrev = datetime.now()
			while True:
				if self.waveStop or self.swOn:
					return
				dtNow = datetime.now()
				dispStr = [self.freqDisp[self.freq], dtNow.strftime('%Y年%m月%d日'), dtNow.strftime('  %H:%M:%S'), "Standby"]
				self.oled.dispText(dispStr)
				if dtNow.minute != dtPrev.minute:
					self.dtStart = datetime(dtNow.year, dtNow.month, dtNow.day, dtNow.hour, dtNow.minute, 0, 0)
					self.dataNo = 0
					break
		self.dtNow = datetime.now()
		if self.dataNo == 0:	# 0秒のコード'M'を送信し、時間的余裕のあるここでTimeCodeを作成
			self.sendTimeData("M")
			self.makeTimeCode(self.dtNow)		# dtNowからタイムコードを作成
		else:
			self.sendTimeData(self.timeCode[self.dataNo])
			
		self.dataNo += 1
		dtNext = self.dtStart + timedelta(seconds = self.dataNo)
		self.dtNow = datetime.now()
		setInter = (dtNext - self.dtNow).seconds + (dtNext - self.dtNow).microseconds/1000000
		if setInter < 0 or setInter > 1:
			self.dataNo = -1
			return
		self.tm = threading.Timer( setInter , self.periodicTimer )
		self.tm.start()			# 次のタイマーセット
		if self.dataNo >= 60:	# 次のタイマーが0秒の時、基準の時間(分)を1分進める
			self.dataNo = 0
			self.dtStart = self.dtStart + timedelta(seconds = 60)
	
	def sendTimeData(self, code):
		GPIO.output(SIG_OUT, GPIO.HIGH)		# Signal On
		if code == '0':
			onTime = 0.8
		elif code == '1':
			onTime = 0.5
		else:
			onTime = 0.2
		dispStr = [self.freqDisp[self.freq], self.dtNow.strftime('%Y年%m月%d日'), self.dtNow.strftime('  %H:%M:%S = ' + code), '']
		self.oled.dispText(dispStr)
		time.sleep(onTime - 0.1)		# OLED表示に0.1秒必要のため
		GPIO.output(SIG_OUT, GPIO.LOW)		# Signal Off
		
	def makeTimeCode(self, dt):
		# 毎時15分、45分も通常時と同じコードを送信, うるう年の予告も行わない。うるう秒(LS1,LS2)=0とする
		days = (dt.date() - datetime(dt.year, 1, 1).date()).days + 1
		weekNo = (dt.weekday() + 1) % 7		# 日=0, 月=1, ...., 土=6
		timeCodeA = "M{:03b}0{:04b}P00{:02b}0{:04b}P00{:02b}0{:04b}P".format(
			int(dt.minute/10), dt.minute%10, int(dt.hour/10), dt.hour%10, int(days/100), int((days%100)/10))
		pa1Data = timeCodeA[1:4] + timeCodeA[5:9]		# Parityを計算する要素を集める
		pa2Data = timeCodeA[12:14] + timeCodeA[15:19]
		pa1, pa2 = 0, 0
		for i in range(len(pa1Data)):
			pa1 += int(pa1Data[i])
		pa1 = pa1 % 2
		for i in range(len(pa2Data)):
			pa2 += int(pa2Data[i])
		pa2 = pa2 % 2
		timeCodeB = "{:04b}00{:b}{:b}0P0{:04b}{:04b}P{:03b}000000P".format(
			days%10, pa1, pa2, int((dt.year%100)/10), dt.year%10, weekNo)
		self.timeCode = timeCodeA + timeCodeB

	def systemEnd(self, sig, frame):
		self.waveStop = True
		self.tm.cancel()
		self.oled.cleanDisp()
		GPIO.output(SIG_OUT, GPIO.LOW)		# Signal Off
		GPIO.cleanup()
		
	def main(self):
		signal.signal(signal.SIGINT, self.systemEnd)
		self.dataNo = -1
		self.tm = threading.Timer( 1 , self.periodicTimer )
		self.tm.start()
		while not self.waveStop:
			if self.swOn:
				self.freq += 1
				self.freq %= 2
				self.pll.freqGen(self.freq)	# Freq Change
				self.swOn = False
				self.dataNo = -1
				self.tm.cancel()
				self.tm = threading.Timer( 1 , self.periodicTimer )
				self.tm.start()
			else:
				pass

if __name__ == "__main__":
	timeCal = TimeCal()
	timeCal.main()

●試験状況

送信機からの送信信号(青色LED)を、受信機が受信している(青色LED)様子。
尚、実際の電波時計もこの送信機からの信号を受信して時間が正しく表示されているのを確認している。

送受信機トップ写真