標準電波 JJY もどきを M5StickC / M5Atom の Ticker で生成する

# 1. はじめに
　電波が届かないところにある電波時計を救うためのリピータが販売[^1][^2]されています。電子工作キットも販売[^3]されています。標準電波を受信してそのまま転送する方式だけでなく、NTP や GPS など別ルートから疑似標準電波を生成する方法も一般的です。NTP で得た時刻を元に微弱電波を発信する範囲であれば、M5StickC[^4] などの Wifi IoT デバイスを使ってコンパクトで安価に実現することが可能です。先例[^5][^6]を参考にしながら、プログラミングやプリント基板の勉強を楽しむことにします。
[^1]: [共立エレショップ - 電波時計用リピーター(時刻合わせ)シリーズ特集](https://eleshop.jp/shop/e/eP18NTP/)
[^2]: [Mizuho Communications Laboratory - 電波時計用JJYリピーター](https://mizuho-lab.com/jjy/jjy_repeater.html)
[^3]: [YS電子工作ラボ - JJY 日本標準時シミュレーター キット](http://www.ys-labo.com/JJY%20Simulator%20Kit/JJY%20sum.html)
[^4]: [M5Stack Docs - M5StickC](https://docs.m5stack.com/#/en/core/m5stickc)
[^5]: [Qiita @fumi38g - JJYシミュレータ](https://qiita.com/fumi38g/items/a0b96351364f05f84419)
[^6]: [mgo-tec電子工作 - M5Stack Yahooニュース・天気予報・時計に、電波時計 JJY 発信モジュールを追加して、マルチタスクで動かしてみた](https://www.mgo-tec.com/blog-entry-m5stack-jjy-watch-yhaoo-news.html)

# 2. アプローチ
　標準電波の仕様はNICT[^7]にあります。その中の通常時のタイムコードを疑似的に生成することにします。
（「通常時（毎時１５分、４５分以外）のタイムコード（例）」出典：NICT[^8]）
![timecode-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/07b37a63-2e33-a625-c165-7b74351f73d0.png)
　タイムコードでは、1秒毎に以下のパルス幅のいずれかを発信します。

- 0.2s ±5ms: マーカー（M）、ポジションマーカー（P0 ～ P5）
- 0.5s ±5ms: 2 進の 1
- 0.8s ±5ms: 2 進の 0

　0.1 秒毎のタイミングで、その時点の日付（年・月・日・曜日）や時刻（時・分・秒）から JJY 信号の制御（オンにする・オフにする・何もしない）は一意に定まります。0.1 秒のタイミング取得にはタイマー割り込みが使えます。この考え方で実装すれば割り込みだけで処理を完結でき、バックグラウンドでの実行も可能になります。また、タイムコードの途中からでもパルスを出力でき、動作開始の確認が容易になります。
[^7]: [NICT - 標準電波（電波時計）の運用状況 - 標準電波の出し方](https://jjy.nict.go.jp/jjy/trans/index.html)
[^8]: [NICT - 通常時（毎時１５分、４５分以外）のタイムコード（例）](https://jjy.nict.go.jp/jjy/trans/timecode1.html)

# 3. タイマー割り込み
　デバイスには M5StickC / M5Atom を選び、開発環境には Arduino IDE[^9] を使います。タイマー割り込みとしては Ticker[^10]を使用します。ハードウェアタイマー割り込み[^11]も使用できそうですが、今回の用途ではオーバースペックです。

### Ticker
　Arduino IDE のライブラリとして提供されている Ticker は、ハードウェアタイマー割り込みの様な精度は期待できませんが、サンプルプログラムに倣って簡単に使用できます。今回は 100ms 周期の処理であり、必要な精度は NICT の仕様から ±5ms なので Ticker は打って付けと言えます[^12]。

### ハードウェアタイマー割り込み
　ESP32 におけるハードウェアタイマー割り込みの実力は解説記事[^13][^14]に詳しいです。ミリ秒以下の周期でも少ない遅延で処理できます。応用事例として 16セグメント LED のダイナミック表示を 2ms 周期のハードウェア割り込みで処理した例[^15]があります。使用にあたっては FreeRTOS[^16]の知識も必要になるなど敷居は高い[^17]です。

[^9]: [M5Stack Docs - Arduino IDE](https://docs.m5stack.com/#/en/arduino/arduino_home_page)
[^10]: [GitHub espressif - Ticker](https://github.com/espressif/arduino-esp32/tree/master/libraries/Ticker)
[^11]: [GitHub espressif - esp32-hal-timer.h, esp32-hal-timer.c](https://github.com/espressif/arduino-esp32/tree/master/cores/esp32)
[^12]: [Lang-Ship - ESP32のタイマークラス Tickerを調べる](https://lang-ship.com/blog/work/esp32-ticker/)
[^13]: [CQ出版 - インターフェース2020年1月号 特集 第4章 ESP32リアルタイム処理の研究](https://interface.cqpub.co.jp/magazine/202001/)
[^14]: [Kenta IDA - ESP32特集の内容紹介](https://www.slideshare.net/ciniml/esp32-220999594)
[^15]: [Yoshiyuki Uehara - How to make GPIO Signals to Control Hardware](https://www.slideshare.net/YoshiyukiUehara/how-to-make-gpio-signals-to-control-hardware-227027550)
[^16]: [Amazon - FreeRTOS](https://aws.amazon.com/jp/freertos/)
[^17]: [Lang-Ship - ESP32の高精度タイマー割り込みを調べる](https://lang-ship.com/blog/work/esp32-timer/)

# 4. コーディング

## 4.1 Ticker 定義
　JJY 信号（TCO: Time Code Output）を生成する関数 TcoGen() を 100ms 周期で起動する処理は、Ticker クラスを用いて以下の様にコーディングできます。

```cpp
#include <Ticker.h>

// Ticker for TCO(Time Code Output) generation
const int ticker_period = 100;  // 100ms
Ticker    tk;

void setup() {
  M5.begin();

  // start Ticker for TCO
  tk.attach_ms(ticker_period, TcoGen);
}  
```

## 4.2 JJY 信号の生成
　100ms 毎に起動する TcoGen() は、現在時刻（0.1 秒）を調べ 0.0 秒、0.2 秒、0.5 秒、0.8 秒の場合に JJY 信号の処理を呼び出します。現在時刻（0.1 秒）は、timeval 構造体の tv_usec メンバでマイクロ秒として取得します。併せて getlocaltime() で日付時刻情報も取得しておきます。

```cpp
struct tm      td;  // time of day .. year, month, day, hour, minute, second
struct timeval tv;  // time value .. milli-second. micro-second

// main task of TCO
void TcoGen()
{
  if (!getLocalTime(&td)) {
    Serial.println("[TCO]Failed to obtain time");
    return;
  }

  gettimeofday(&tv, NULL);
  int tv_100ms = tv.tv_usec / 100000;
  switch (tv_100ms) {
  case 0: Tco000ms(); break;
  case 2: Tco200ms(); break;
  case 5: Tco500ms(); break;
  case 8: Tco800ms(); break;
  default: break;
  }
}

```

　以下は、現在時刻（0.1 秒）の 0.0 秒、0.2 秒、0.5 秒、0.8 秒の各々の処理です。

- 0.0 秒では、とにかく JJY 信号をオンにする
- 0.2 秒では、マーカーを送出する時刻（秒）の場合、JJY 信号をオフにする
- 0.5 秒では、現在の日時から、1 を送出する時刻（秒）の場合、JJY 信号をオフにする
- 0.8 秒では、とにかく JJY 信号をオフにする

```cpp
const int marker = 0xff;  // marker code which TcoValue() returns 

// TCO task at every 0ms
void Tco000ms()
{
  TcOn();
}

// TCO task at every 200ms
void Tco200ms()
{
  if (TcoValue() == marker)
    TcOff();
}

// TCO task at every 500ms
void Tco500ms()
{
  if (TcoValue() != 0)
    TcOff();
}

// TCO task at every 800ms
void Tco800ms()
{
  TcOff();
}
```

　日付および時刻から、現在時刻（秒）で送出すべき信号（マーカー、1、0）を返す関数 TcoValue() の中身です。case 文が 60 個並ぶ単純な造りです。

```c++
const int marker = 0xff;  // marker code which TcoValue() returns 

// TCO value
//   marker, 1:not zero, 0:zero
int TcoValue()
{
  int bcd_hour = Int3bcd(td.tm_hour);
  int parity_bcd_hour = Parity8(bcd_hour);

  int bcd_minute = Int3bcd(td.tm_min);
  int parity_bcd_minute = Parity8(bcd_minute);

  int year = td.tm_year + 1900;
  int bcd_year = Int3bcd(year);

  static const int days_of_month[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  int days = td.tm_mday;
  for (int i = 0; i < td.tm_mon; ++i)  // td.tm_mon starts from 0
    days += days_of_month[i];
  if ((td.tm_mon >= 2) && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)))
    ++days;
  int bcd_days = Int3bcd(days);

  int day_of_week = td.tm_wday;

  int tco;
  switch (td.tm_sec) {
  case  0: tco = marker;              break;
  case  1: tco = bcd_minute & 0x40;   break;
  case  2: tco = bcd_minute & 0x20;   break;
  case  3: tco = bcd_minute & 0x10;   break;
  case  4: tco = 0;                   break;
  case  5: tco = bcd_minute & 0x08;   break;
  case  6: tco = bcd_minute & 0x04;   break;
  case  7: tco = bcd_minute & 0x02;   break;
  case  8: tco = bcd_minute & 0x01;   break;
  case  9: tco = marker;              break;

  case 10: tco = 0;                   break;
  case 11: tco = 0;                   break;
  case 12: tco = bcd_hour & 0x20;     break;
  case 13: tco = bcd_hour & 0x10;     break;
  case 14: tco = 0;                   break;
  case 15: tco = bcd_hour & 0x08;     break;
  case 16: tco = bcd_hour & 0x04;     break;
  case 17: tco = bcd_hour & 0x02;     break;
  case 18: tco = bcd_hour & 0x01;     break;
  case 19: tco = marker;              break;
  
  case 20: tco = 0;                   break;
  case 21: tco = 0;                   break;
  case 22: tco = bcd_days & 0x200;    break;
  case 23: tco = bcd_days & 0x100;    break;
  case 24: tco = 0;                   break;
  case 25: tco = bcd_days & 0x080;    break;
  case 26: tco = bcd_days & 0x040;    break;
  case 27: tco = bcd_days & 0x020;    break;
  case 28: tco = bcd_days & 0x010;    break;
  case 29: tco = marker;              break;

  case 30: tco = bcd_days & 0x008;    break;
  case 31: tco = bcd_days & 0x004;    break;
  case 32: tco = bcd_days & 0x002;    break;
  case 33: tco = bcd_days & 0x001;    break;
  case 34: tco = 0;                   break;
  case 35: tco = 0;                   break;
  case 36: tco = parity_bcd_hour;     break;
  case 37: tco = parity_bcd_minute;   break;
  case 38: tco = 0;                   break;
  case 39: tco = marker;              break;

  case 40: tco = 0;                   break;
  case 41: tco = bcd_year & 0x80;     break;
  case 42: tco = bcd_year & 0x40;     break;
  case 43: tco = bcd_year & 0x20;     break;
  case 44: tco = bcd_year & 0x10;     break;
  case 45: tco = bcd_year & 0x08;     break;
  case 46: tco = bcd_year & 0x04;     break;
  case 47: tco = bcd_year & 0x02;     break;
  case 48: tco = bcd_year & 0x01;     break;
  case 49: tco = marker;              break;
  
  case 50: tco = day_of_week & 0x04;  break;
  case 51: tco = day_of_week & 0x02;  break;
  case 52: tco = day_of_week & 0x01;  break;
  case 53: tco = 0;                   break;
  case 54: tco = 0;                   break;
  case 55: tco = 0;                   break;
  case 56: tco = 0;                   break;
  case 57: tco = 0;                   break;
  case 58: tco = 0;                   break;
  case 59: tco = marker;              break;
  default: tco = 0;                   break;
  }
  return tco;
}

int Int3bcd(int a)
{
  return (a % 10) + (a / 10 % 10 * 16) + (a / 100 % 10 * 256);
}

int Parity8(int a)
{
  int pa = a;
  for (int i = 1; i < 8; ++i) {
    pa += a >> i;
  }
  return pa % 2;
}
```


## 4.3 40kHz 信号の生成
　Arduino IDE for ESP32 で用意されている LEDC ライブラリを使用します。LEDCのパラメータについて参考記事[^40]があります。
　LEDC 信号のオンオフは、デューティの設定変更で行います。PWM において頻繁なデューティ変更は想定内です。信号オフはデューティ 0% です。モニター用に M5StickC の内蔵 LED も同時にオンオフします。LOW でオン、HIGH でオフです。

[^40]: [Qiita: ESP32においてLEDC(LED PWM Controller)に設定する分解能をExcelシートで検討する](https://qiita.com/BotanicFields/items/e74c449c0bef0820fcd1)

```c++
// PWM for TCO signal
const uint8_t  ledc_pin        = 26;
const uint8_t  ledc_channel    = 0;
const double   ledc_frequency  = 40000;  // 40kHz
const uint8_t  ledc_resolution = 8;      // 2^8 = 256
const uint32_t ledc_duty_on    = 128;    // 50% 
const uint32_t ledc_duty_off   = 0;      // 0 

// for monitoring
const int led_pin    = 10;    // led_pin to monitor
bool      led_enable = true;  // 

void setup()
{
  M5.begin();

  // start TCO signal source(40kHz)
  ledcSetup(ledc_channel, ledc_frequency, ledc_resolution);
  ledcAttachPin(ledc_pin, ledc_channel);

  // for monitoring
  pinMode(led_pin, OUTPUT);
}

void TcOn()
{
  ledcWrite(ledc_channel, ledc_duty_on);
  if (led_enable)
    digitalWrite(led_pin, LOW);
}

void TcOff()
{
  ledcWrite(ledc_channel, ledc_duty_off);
  digitalWrite(led_pin, HIGH);
}
```

## 4.4 その他

### Wifi 設定
　WiFiManager[^18] を使用しています。Library Manager で以下のライブラリをインストールする必要があります。

- WiFiManager by tzapu, tablatronix

　類似のライブラリが多数ありますが、上記は GitHub で Star が突出して多いです。"Release v2.0.3-apha Development" を使用しています。アルファ版というのが気にはなります。Wifi 接続の方法については GitHub の WiFiManager の説明を参照ください。

[^18]: [GitHub - tzapu/WiFiManager](https://github.com/tzapu/WiFiManager)

```c++
#include <WiFi.h>
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager

// initialize Wifimanager
WiFiManager wm;

void setup()
{
  M5.begin();

  // connect Wifi
  // wm.resetSettings();  // for testing
  if (!wm.autoConnect())
    Serial.println("Failed to connect");
  else
    Serial.println("connected...yeey :)");
}
```

### NTP 設定
　通常の NTP の設定です。この設定により NTP による時刻合わせが 1 時間毎に実行される[^19]とのことです。

[^19]: [Qiita @h_nari - ESP8266のntpの設定は1行で](https://qiita.com/h_nari/items/d0374d1e1e36b9d988c0)

```c++
// for NTP
const int   gmt_offset = 3600 * 9;  // JST-9
const int   daylight   = 3600 * 0;  // No daylight time
const char* ntp_server = "pool.ntp.org";
struct tm      td;  // time of day .. year, month, day, hour, minute, second

void setup()
{
  M5.begin();

  // start NTP
  configTime(gmt_offset, daylight, ntp_server);
  while (!getLocalTime(&td)) {
    Serial.println("Waiting to obtain time");
    delay(100);
  }
  Serial.println(&td, "%A %B %d %Y %H:%M:%S");  
}
```

# 5. Ticker 周期のばらつき
　Ticker 割り込み毎に、前回の割り込みからの経過時間のばらつきを集計してみました。経過時間 100ms を中央値 0 としています。
（計測約 62 時間）

|範囲|回数|
|:-:|:-:|
|～ -50ms|17|
|-50ms ～ -5ms|11|
|-5ms ～ -0.5ms|19337|
|-0.5ms ～ -0.05ms|21813|
|-0.05ms ～ 0.05ms|2159817|
|0.05ms ～ 0.5ms|22338|
|0.5ms ～ 5ms|18773|
|5ms ～ 50ms|11|
|50ms ～|19|

- 平均値: -0.0000853ms
- 標準偏差: 1.1360784ms
- 最小値: -902.830ms
- 最大値: 929.487ms

　Ticker 周期のずれが ±5ms の範囲外となったケースが、62時間で 58 回発生しています。最大値、最小値も ±900ms を超える値が記録されました。Ticker の周期が乱れる原因は NTP や Wifi を含めたシステム処理と競合し、そのシステム処理がネットワークの状況で遅延することよるものと予想しています。しかしながら、規定範囲（±5ms）内は 99.997% であり、電波時計の方でリトライもすることから実用上は問題なさそうです。標準偏差から ±6σ = ±6.83ms は ±5ms に収まっていません。業界トップレベルの品質とは言えません。

# 6. ハードウェア
　40kHz の疑似 JJY 信号は、M5StickC の GPIO26 から出力しています。GPIO26 と GND の間を、電線で途中 1k オーム程度の抵抗器を途中に挟んで接続します。疑似 JJY 信号電流が 3mA 程度の強さで流れます。電波時計の至近距離に電線を這わせると、電線の周りに生じた磁界を電波時計が受信してくれます。時刻合わせの様子を動画をアップ[^20]しました。

![clock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/449eefdd-aa63-a831-7d9b-f8154729a21d.png)
![IMG_4663.JPEG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/2263928c-e127-7cd0-88fd-2fcdba40f424.jpeg)

[^20]: [youtube - JJY Simulator by M5StickC for a radio controlled clock](https://youtu.be/S_t3g5wqyh8)


# 6.1 波形の改善
　GPIO が出力する 40kHz の信号は矩形波であり不要な周波数成分を大量に含みます。常時使用する場合、ノイズを極力減らし正弦波に近づけたく思います。ローパスフィルタで高い周波数成分を取り除きます。ローパスフィルタの抵抗とコンデンサの値は、オシロスコープで波形を見ながら決めました。カットオフ周波数を 40kHz 付近にしても波形は十分丸くなりませんでした。あとで増幅することを前提に 16kHz 程度のカットオフ周波数で 2 段構成としています。
![bf-018.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/f744fa01-1a43-cfe3-f2d9-7cb493248aca.png)

　観測した波形です。
![DS1Z_QuickPrint1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/92a91cd1-c9f1-9177-d1f5-fba49645e71c.png)

- CH1（黄色）: GPIO26 - 40kHz の矩形波
- CH2（水色）: ローパスフィルタ 2段後のアンプの入力 - 2.45V を中心に 440mV の振幅
- CH3（紫色）: ループアンテナ直前の抵抗にかかる電圧 - グランドを中心に 2.74V の振幅

　CH2 の 0V 位置は CH3 と同じ高さで重なっています。

### 別のハードウェア構成
　JJY 信号を GPIO から直接を出すのではなく、モニター LED と同じレベル信号を GPIO から出す様にし、外部のアナログスイッチを制御して 40kHz 信号をオン・オフする構成が考えられます。40kHz の信号源は、別の GPIO から常時出力して波形整形するか、または外部の質のよい発振器を用いることができます。アナログスイッチは、40kHz 信号を直接オン・オフするか、または増幅器のゲイン制御をすることが考えられます。

# 6.2 プリント基板
　上記回路を収容し、アンテナのループパターンを基板上に作り込んだプリント基板を製作しました。設計には KiCad[^21] を使用し、ループパターンは公開されている Python ツール[^22]を使用しました[^23]。
　プリント基板を使った時刻合わせの様子を YouTube[^24][^25][^26]に置きました。電波時計（の中にあるフェライトバーアンテナ）の長手方向の延長線上に、ループパターンが直角になる様に置くのが最もよさそうです。ループパターンとフェライトバーアンテナの巻線が平行になり磁界による結合が最大になります。70cm の距離から電波時計を合わせることができました。90cm 離れると難しい模様です。電波時計の対面方向では 30cm の距離で時刻合わせができましたが 50cm 離れると難しくなります。
![IMG_4794.JPEG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/b05b8bcd-1ef5-fb02-320b-b230f6cf83ad.jpeg)
![IMG_4793.JPEG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/c6ef78c8-a5e0-9f57-ffa6-58abfc915eb7.jpeg)
<img width="768" alt="IMG_4784.JPEG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/9503f594-b802-d69e-4bcb-81c4b5db98c6.jpeg">
<img width="768" alt="IMG_4786.JPEG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378410/54d866e0-1c30-ab88-3243-41188b9eb34f.jpeg">

[^21]: [KIcad](https://kicad-pcb.org/)
[^22]: [GitHub - jedrzejboczar/kicad-coil-generator](https://github.com/jedrzejboczar/kicad-coil-generator)
[^23]: [Qiita @BotanicFields - KiCad においてループアンテナパターンを Python で作成する](https://qiita.com/BotanicFields/items/d165617dfa303cef7ab2)
[^24]: [YouTube - BF-018: JJY Antenna for M5StickC: longitudinal direction](https://youtu.be/LF4P73INSro)
[^25]: [YouTube - BF-018: JJY Antenna for M5StickC - with straight connecter](https://youtu.be/gpBwHdANwyw)
[^26]: [YouTube - BF-018: JJY Antenna for M5StickC - with right-angle connecter](https://youtu.be/ODojBB31gAc)

# 7. おわりに
　コードを GitHub[^27]に置きました。プリント基板の委託販売[^28]をしています。かなり実用的な工作ができました。参考にさせていただいた皆様に感謝いたします。

# 8. 追伸
　M5Atom[^29][^30]用のコードおよびプリント基板も作成しました[^31]。プリント基板の委託販売[^32]をしています。


[^27]: [GitHub - botanicfields/BF-018](https://github.com/botanicfields/BF-018)
[^28]: [スイッチサイエンス - M5StickC用JJYアンテナ基板](https://www.switch-science.com/catalog/6527/)
[^29]: [M5Atom lite](https://docs.m5stack.com/#/en/core/atom_lite)
[^30]: [M5Atom Matrix](https://docs.m5stack.com/#/en/core/atom_matrix)
[^31]: [GitHub - botanicfields/BF-018A](https://github.com/botanicfields/BF-018A)
[^32]: [スイッチサイエンス - M5Atom Lite/Matrix用JJYアンテナ基板](https://www.switch-science.com/catalog/6746/)