Top Page STM32F103でOCXO Calibratorを作る(3)
自作・実験工程のメモとして
29.Apr.2020


 10MHz OCXOの校正器を作り始めて失敗を続けてきましたが、番外編「PIC24FV32KA302でバイナリカウンターを試す」で掲載したようにカウンター部をPIC側に丸投げして、計測結果だけを受け取る方式でまとめました。
このページに掲載していない情報は、3つ前からの前編続編番外編を参照ください。

 また、番外編の基板に2階建て基板を加えてまとめてしまおうかと考えましたが、3枚作るも4枚作るも変わりないと考えて基板も再設計することにしました。
今後は、派生したテーマで遊んでみるかも知れませんが、AE-GNSSを使った本テーマの完結編として掲載します。

続編「STM32F103でOCXO Calibrator Part4」を掲載しました。(2020/06/14)

10MHz OCXO(左上) とアッテネータ(右上)で測定中の校正器
 

Edit by bluegriffon3.1

1.OCXO 校正器(V4)のブロックダイヤグラム(BDG)と回路

 前編・続編との違いは、前置カウンタ(FrontCounter)をロジックICで処理していたものをPIC24FV32KA301に変えたイメージです。
今回で回路的には最終版というつもりでつくっていますが、少し冗長度を持たせた部分も残しています。


■.OCXO 校正器(V4)のBDG
1.前置カウンタ(FrontCounter)に相当するところは、番外編ではDIP-28のPIC24FV32KA302でしたが、ピン数が多すぎて収まりが悪いので、DIP-20のPIC24FV32KA301に変更しました。
2.その他、GATE信号用のNAND回路が必要になり74HC00を使っています。
3.その影響で、ICを載せられるサイズの関係から74HC00の残った回路で以前の74HC04(74HC14)の代わりをしています。

■.OCXO 校正器(V4)の回路
クリックでpdfファイルが開きます。(2020/05/06:訂正済み)
1.以前は74HC04(74HC14)のインバータが余っていましたが、74HC00で代替させるためできるだけ簡潔にまとめました。
2.STM32F103側に空きポートが出たので、使わないと思いますがピンソケットを植えられるようにしました。
3.PIC側の動作確認用にLEDポート(RB4)を付けましたが、動かしてみるとRB4が出力指定にできず、隣のRA3から100Ωを通じて接続しています。
4.ケースを閉めてしまえば関係ありませんが、一度もPIC24FV32KA301で実験せず設計したため最後にミスが出てしまいました。

綿棒の毛羽立ち跡がありますが、基板裏面で100Ωを使ってジャンパーしました。
5.温度計を追加しようとしたところ、回路が間違っていることが分かりました。(2020/05/06:訂正)

AE-BME280基板を付けて電源ONすると、電源が入りません。(ショート状態)
取りあえず基板上でジャンパーを飛ばし、AE-BME280基板のピンを1本抜いて対応しました。

2.最終:PCBの設計

 前回と同様にネジ穴やコネクタの位置は変更せず、前作を踏襲した構成にしています。
また、パターンは自動配線を使わず、一貫して手動配線で行っていますが、まだ十分に余裕が有ります。
今回が4回目のPCB設計ですが、以前から使っていたFusionPCBの5枚(OCS便$12.9)が急遽休止になり、普通の試作($4.9+DHL)で5枚を発注しました。

■.PCBの3Dビューアー(表面)
1.今回の肝となる部分は、PIC24FV32KA301(20pin)で、他のレイアウトにあまり影響なく配置できたと思います。
2.その他、STM32の使えそうなポート(PB12〜PB15)は、ピンソケットが挿せるようにパターンを引いています。
3.また、一瞬?だけ使いましたが、PICのI2C2用端子も引き出せるようにしています。(SCL2/SDA2)

■.PCBの3Dビューアー(裏面)

以前は表面に配置していたものを、一部表面が手狭になったことから裏面にパターンを移しています。


■.USB 2.0 Type-Cのハンダ付け

今回新たに採用したUSBソケットの拡大写真を添付しておきます。(Amazon等で売られている安価なUSB顕微鏡を使用)
3個ほど取付ましたが、USB顕微鏡タイプの拡大ツールが無いとはんだ付け後の確認は難しいと思います。



3.PIC24FV32KA301側のプログラム

 カウンターの主要部分は番外編で完成していますが、PICを省サイズ品に切り替えたため、新プロジェクトを作りMCC等を最初からやり直して、最後に番外編の内容からコピペをして移行しました。
LCDは、ケース内に収めたときに不要になるので、デバッグ終了時はmain()の先頭で定数を定義して、if文でLCD関係の実行を回避しています。


MCCのメモ
PostscalerとPLL Enableのみ設定


T1CKを選択。PR1もここで設定する。

INTは3本とも使用するので、それぞれ立ち上がり・立下りの設定と、I2Cは、「Master」の確認のみ。

UARTは、速度の設定と、「Redirect Printf to UART」にチェックを入れておきました。


INT0は、立ち上がり割り込みをかけるので念のためプルダウンを設定しました。

当初出力にするつもりだったPB4を入力指定にし、代替としてRA3を出力指定。


PIC24FV32KA301(main.c)のソースコード  (ソースコードの表示にSyntaxHighlighterを使用しています。)
/**
  Generated main.c file from MPLAB Code Configurator

  @Company
    Microchip Technology Inc.

  @File Name
    main.c

  @Summary
    This is the generated main.c using PIC24 / dsPIC33 / PIC32MM MCUs.

  @Description
    This source file provides main entry point for system initialization and application code development.
    Generation Information :
        Product Revision  :  PIC24 / dsPIC33 / PIC32MM MCUs - 1.167.0
        Device            :  PIC24FV32KA301
    The generated drivers are tested against the following:
        Compiler          :  XC16 v1.50
        MPLAB 	          :  MPLAB X v5.35
 * 
 * 
 * 
 *                  A6_V40calib_TIM1_U2TX_24FV32KA.X
 *                  対象基板は、「OCXO Calibrator V4」を想定
 * 
 * クロックをTIMER1でカウントして、U2TX でSTM32(RX3)に送る。
 * STM32側は、STM_SSD1309_GPSFC_5V40
 *  
 * LCD は、アイテンドの「基板付きI2CキャラクタLCDモジュール(16x2) [ATD1602CPB]」
 * 参照は、「組込み系システム開発室by藤原技研M」のブログです。(2020/04/04現在)
 * 
 * この方法で、TIMER1は最高周波数が50MHz以上まで伸びる。
 * しかも、入力レベルも-10dBm程度(50MHz)までカウントする。
 * 
 * 2020/04/27:
 * PIC24FV32KA302(28pin) ==> PIC24FV32KA301(20pin)
 *  
 * 
*/

/*
    (c) 2020 Microchip Technology Inc. and its subsidiaries. You may use this
    software and any derivatives exclusively with Microchip products.

    THIS SOFTWARE IS SUPPLIED BY MICROCHIP "AS IS". NO WARRANTIES, WHETHER
    EXPRESS, IMPLIED OR STATUTORY, APPLY TO THIS SOFTWARE, INCLUDING ANY IMPLIED
    WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A
    PARTICULAR PURPOSE, OR ITS INTERACTION WITH MICROCHIP PRODUCTS, COMBINATION
    WITH ANY OTHER PRODUCTS, OR USE IN ANY APPLICATION.

    IN NO EVENT WILL MICROCHIP BE LIABLE FOR ANY INDIRECT, SPECIAL, PUNITIVE,
    INCIDENTAL OR CONSEQUENTIAL LOSS, DAMAGE, COST OR EXPENSE OF ANY KIND
    WHATSOEVER RELATED TO THE SOFTWARE, HOWEVER CAUSED, EVEN IF MICROCHIP HAS
    BEEN ADVISED OF THE POSSIBILITY OR THE DAMAGES ARE FORESEEABLE. TO THE
    FULLEST EXTENT ALLOWED BY LAW, MICROCHIP'S TOTAL LIABILITY ON ALL CLAIMS IN
    ANY WAY RELATED TO THIS SOFTWARE WILL NOT EXCEED THE AMOUNT OF FEES, IF ANY,
    THAT YOU HAVE PAID DIRECTLY TO MICROCHIP FOR THIS SOFTWARE.

    MICROCHIP PROVIDES THIS SOFTWARE CONDITIONALLY UPON YOUR ACCEPTANCE OF THESE
    TERMS.
*/

/**
  Section: Included Files
 */

// delay関数を使うのに必要。クロック周波数÷2が命令実行サイクル
#define FCY 16000000  // クロック周波数FOSC(32MHz)÷2

#include "mcc_generated_files/system.h"

#include <libpic30.h> // __delay_ms()に必要
#include <stdio.h>      // sprintf()に必要
#include <string.h>     // strlen()に必要
#include <ctype.h>
#include "p24FV32KA301.h"   // ANSB TRISBbits. 等に必要
#include "i2c2.h"       // エラーが出るのでmcc_generated_files/より移動
// 以下もコンパイルは通るが警告>が出るので記述
#include "tmr1.h"
#include "ext_int.h"
#include "uart2.h"

// =================================================

#define Reset() asm("reset")    // リセット(アセンブラ命令)の定義

#define I2CLCD_AQM0802A 0x3E   // 3FのLCDでも3Eと記述

// 64ビット(倍精度浮動小数点型)指定の件
// ■■ project > properties > xc16gcc で ?Use64-bit doubleを指定したが、
// 結局、本プログラム中では浮動小数点を使用しなかった。

// タイマー関連のグローバル変数 ui64overf
uint64_t ui64overf = 0;
uint16_t ui16count = 0; // 
uint8_t countBusy = 0;
uint8_t CountOk = 0;

char outBuff[22];

char temptxt[80];

// 警告が出るので関数のプロトタイプ宣言を以下に記述した。
//void TMR1_JOB(void);
//void ui64bitTOhex16(uint64_t , char* );

// ここからLCD関係  ■■■■■■■■■■■■■■■■■■■■■■■■■
//「組込み系システム開発室by藤原技研M」のブログより

/**
 * コマンド書き込み
 * @param device_address    :I2Cデバイスのアドレス
 * @param controlByte   :コントロールバイト
 * @param cmdData   :コマンド
 * @return
 */
uint8_t I2C2_write(uint8_t device_address, uint8_t controlByte, uint8_t cmdData) {
    I2C2_MESSAGE_STATUS status = I2C2_MESSAGE_PENDING;
    uint8_t write_buffer[2];

    write_buffer[0] = controlByte;
    write_buffer[1] = cmdData;

    I2C2_MasterWrite(write_buffer, 2, device_address, &status);
    int cnt = 0;

    while (status == I2C2_MESSAGE_PENDING && cnt++ < 10) {
        __delay_us(100);
    }
    return (status == I2C2_MESSAGE_COMPLETE);
}

/**
 * LCDの指定アドレスに表示する
 * @param str: 表示したい文字列
 * @param num:表示バイト数
 * @param LcdAddr LCDのDDRAM表示アドレス
 */
void WireLcdDisplay(char *str, int num, uint8_t LcdAddr) {
    uint8_t dt[65]; //16文字まで対応 64文字に拡大
    int i;
    I2C2_MESSAGE_STATUS status = I2C2_MESSAGE_PENDING;

    // Set DDRAM address コマンドをセットする
    dt[0] = 0x40;

    // 文字列をアスキーコードに変換して配列に格納する
    for (i = 0; i < num; i++) {
        dt[i + 1] = (uint8_t) str[i];
    }

    // Slaveへ制御コード:0x00
    // コマンド:0x80 + DDRAMのアドレス
    I2C2_write(I2CLCD_AQM0802A, 0x00, 0x80 | LcdAddr);
    I2C2_MasterWrite(dt, num + 1, I2CLCD_AQM0802A, &status);
    int cnt = 0;

    while (status == I2C2_MESSAGE_PENDING && cnt++ < 10) {
        __delay_us(100);
    }
    __delay_ms(1); // 追加して呼び出し側(main)に記述しない。
}

// これによって出力されるデータは以下の通りになります
// 【I2C2_writeによって出力されるデータ】
// 0x7C :LCDのアドレス
// 0x00 :コントロールバイト(RS=0:コマンド)※次はコマンドを送る
// 0x80 :コマンドバイト(SetDDRAMaddress + DDRAMアドレス)
//
// 【I2C2_MasterWriteによって出力されるデータ】
// 0x7C :LCDアドレス
// 0x40 :コントロールバイト(RS=1:データ)※次からデータを送る
// 表示データ(num個)

/**
 * LCD の初期化処理 
 */
void WireLcdInit(void) {
    I2C2_MESSAGE_STATUS status = I2C2_MESSAGE_PENDING;
    uint8_t sendData[ ] = {0x38, 0x39, 0x14, 0x70, 0x56, 0x6C, 0x38, 0x0C, 0x01};

    I2C2_MasterWrite(sendData, sizeof (sendData) + 1, I2CLCD_AQM0802A, &status);
    int cnt = 0;

    while (status == I2C2_MESSAGE_PENDING && cnt++ < 10) {
        __delay_us(100);
    }
    __delay_ms(1); // 追加して呼び出し側(main)に記述しない。
}


// =================================================
// 追加関数  jlb.jp
// 表示位置を指定して LCD に表示する
// ll = 行:0 or 1   ,cc 桁:0 to 15
// =================================================

void WireLcd_lxc_Print(uint8_t ll, uint8_t cc, char* txt) {
    uint8_t ca, lc;
    int ln = strlen(txt);
    if (ln > 16) ln = 16;
    ca = cc;
    switch (ll) {
        case 1:
            lc = 0x40;
            break;
        default:
            lc = 0x00;
    }
    if (ca > 15) ca = 15;
    lc += ca;

    WireLcdDisplay(txt, ln, lc);
}

// =================================================
// 追加関数  jlb.jp
// 画面表示をクリヤーする
// 
// =================================================

void WireLcd_Clr(void) {
    char sptext[17] = "                ";
    WireLcdDisplay(sptext, 16, 0x00);
    WireLcdDisplay(sptext, 16, 0x40);
}

// ここまでは、LCD関係の記述 ■■■■■■■■■■■■■■■■■■■■■■■■■

// ここから外部信号割り込み ■■■■■■■■■■■■■■■■■■■■■■■■■

// =================================================
// INT0 は、~RESET信号(Low active)を入れるので、
// 予めMCCで 「NegativeEdge」を指定しておく。
// 
// ■■■■■
// INT1 は、前回まで ~GON信号を使い [PositiveEdge]だったが、
// V4.0からは GATE信号(High active)に変更する。
// GATE信号の終了を判断するので、予め■MCC■で
//「NegativeEdge」を指定しておく。(2020/04/10)
// =================================================

// =================================================
// 追加関数  jlb.jp
// GATE(INT1)外部割込みの処理関数
// GATEがOFF 時に、処理する。
// CountOk = 1 をセットする。
// ext_intc に連携する。
// EXT_INT1 と INT0 の■2つ■を指定すると ext_int.c に
//  void __attribute__ ((weak)) EX_INT0_CallBack(void)
// {
//    // Add your custom callback code here
// }
// が出現する。そちらに記述か?
// ===========
// カウンター値のクリヤーをINT0 から INT2に変更
// INT0 は、PICのソフトリセットとする。(2020/04/12)
// =================================================

// INT1で処理のため、不要(多分)
void EX_INT2_CallBack(void) { // カウンター値のクリヤー信号(LOWアクティブ)
   // TMR1_Stop(); // タイマーストップ
  //  TMR1_Counter16BitSet(0); // カウンタークリヤー
  //  ui16count = 0;
  //  ui64overf = 0;
   // TMR1_Start(); // タイマースタート
}


void EX_INT1_CallBack(void) { // GATE信号のOFF時に呼び出し(LOWアクティブ)
    // if (TRISBbits.TRISB14 == 0) { // 念のためビットを確認する。
    TMR1_Stop(); // タイマーストップ
    CountOk = 1; // カウント完了フラグをON(1)
    ui16count = TMR1_Counter16BitGet(); // 16bit 読み出し。
    ui64overf = ui64overf << 16; // オーバーフローを16bitシフト
    ui64overf = ui64overf + ui16count; // 合計カウント数
    TMR1_Counter16BitSet(0); // カウンタークリヤー
    TMR1_Start(); // タイマースタート

}

void EX_INT0_CallBack(void) { // PICリセット信号(HIGHアクティブ)の処理

     Reset(); // 試しにリセット
}
// ここまで外部信号割り込み ■■■■■■■■■■■■■■■■■■■■■■■■■

// ここから、タイマー ■■■■■■■■■■■■■■■■■■■■■■■■■

/*
 * タイマーがオーバーフローしたときの処理関数
 * tmr1.c に記述する。
 * 
 * void TMR1_CallBack(void);
 * void TMR1_JOB(void); // jlb ■■ここ■■加筆した。
 * 
 * 
 * void __attribute__ ((weak)) TMR1_CallBack(void)
 * {
 *   // Add your custom callback code here
 *    TMR1_JOB(); // ■■ここ■■加筆した。
 * }
 * と加筆した。
 * この TMR1_JOB(); を次に記述する。
 */
void TMR1_JOB(void) {
    ui64overf++; // オーバーフローカウンタを +1する。
}

// ここまで、タイマー ■■■■■■■■■■■■■■■■■■■■■■■■■


/*************************************************************
 * 合計されたカウント合計値(計算は、EX_INT1_CallBack(void) 部)を
 * 文頭部に有る char outHex[] のフォーマットに[4bit]毎に変換する。
 *          4bit ==> 1Byte文字  0x30 を OR して Hex文字に変換
 * 0x0 => '0', 0x1 => '1', 0xa => 'A', ,,, 0xf => 'F'   
 * さらに2文字のチェックサム(Check Sum)を付加する。(2020/04/10)
 * in64val  ==>    char outStr[21];
 *************************************************************/
void ui64bitTOhex16(uint64_t in64val, char * outHex) {
    int8_t i;
    uint8_t tmpB1;
    uint16_t sum16 = 0;
    outHex[0] = '['; // START Mark
    outHex[19] = ']'; // END Mark
        outHex[20] = 0x0; // 0x0
    for (i = 0; i < 16; i++) {
        tmpB1 = in64val & 0x0F;
        if (tmpB1 > 9) {
            tmpB1 += 55;
        } else {
            tmpB1 |= 0x30;
        }
        outHex[i + 1] = tmpB1;
        sum16 += tmpB1;
        in64val = in64val >> 4;
    }
    for (i = 0; i < 2; i++) { // ここからチェックSUM
        tmpB1 = sum16 & 0x0F;
        if (tmpB1 > 9) {
            tmpB1 += 55;
        } else {
            tmpB1 |= 0x30;
        }
        outHex[i + 17] = tmpB1;
        sum16 >>= 4;
    }
}

/*************************************************************
 * 文頭部に有る char outHex[] のフォーマットで
 * シリアル送信するための変換
 *  char outHex[];  ==>   U2TX
 *************************************************************/
void Serialwrite(char *outHex) {
    int8_t i,j;
    uint8_t uwd;
    j = strlen(outHex);
    for (i = 0; i < j; i++) {
        uwd = outHex[i];
        UART2_Write(uwd);
    }
}

/*
    ■■■■■■■■■■■  Main application  ■■■■■■■■■■■
 */
int main(void) {
    uint8_t DisplayON = 0; // 0 = LCD OFF,   1 = LCD ON
    // initialize the device
    SYSTEM_Initialize(); // 外部割込み EXT_INTの初期化 タイマー初期化を含む
    __delay_ms(1); // 念のため

    // MCCの TMR1 ==> Registers ==> 「Register:PR1」で設定可
    // TMR1_Period16BitSet(0xFFFF); // TIMER1(16bit)用

    TMR1_Start(); //タイマースタート

    if (DisplayON == 1) {
        WireLcdInit(); // LCD初期化
        WireLcd_lxc_Print(0, 0, "A6 TIMER 1");
        WireLcd_lxc_Print(1, 1, "2020/04/27 ");
        __delay_ms(1); // 1mSec待ちでも (CountOk == 1) まで上記表示時を保持

    }

    while (1) {

        if (CountOk == 1) {

            // カウント数の送信
            ui64bitTOhex16(ui64overf, outBuff);
            Serialwrite(outBuff);

            if (DisplayON == 1) {
                // LCD への表示部分
                WireLcd_Clr(); // 画面表示をクリヤーする
                WireLcd_lxc_Print(0, 0, "PIC24KA301_TMR1");
                sprintf(temptxt, "%13lld", ui64overf); // %lld を付けるlong long OK
                strcat(temptxt, "/G");
                WireLcd_lxc_Print(1, 0, temptxt); // 2行目の4桁目から表示

            }

            // 目視確認用のLED
            if (LATAbits.LATA3 == 0) {
                LATAbits.LATA3 = 1; // LED off (high)
            } else {
                LATAbits.LATA3 = 0; // LED on (low)
            }


            ui64overf = 0;
            CountOk = 0; // カウント完了状態をクリヤー
        }

    }
    return 1;
}

/**
 End of File
 */
余分な記述や無関係なコメントが残っているかも知れません。



4.STM32F103側プログラム

 STM32側のプログラムは、2つ前の続編で公開していますが、PICとの連携も含めた最終版として掲載しておきます。
何かの参考になれば幸いです。


STM32F103(Blue pill)のソースコード(参考)   (ソースコードの表示にSyntaxHighlighterを使用しています。)
/************************************************
   STM32F103 によるOCXO Calibrator
   2020/03/28より
   V3.0の基板用
   V4.0の基板用  STM_SSD1309_GPSFC_5V41.ino

   (c)2020 jlb.jp
 ************************************************/


#include <Wire.h>
// I2C EEPROM アドレス
#define DEVICE_ADDRESS 0x50  //I2C EEPROM スレーブアドレス 24C256

#include <U8g2lib.h>
#include <SPI.h>
#include <wirish_time.h>          // STM32
// #include <EEPROM.h>            // STM32
#include "libmaple/timer.h"        // STM32 GitHub より入手、
/*
   timer.h ファイルは、
   \Documents\ArduinoIDE1.8\hardware\Arduino_STM32\STM32F1\system\libmaple
   \stm32f1\include\libmaple
   (\libmaple フォルダは追加作成)
   に保存した。
*/
// OLED SSD1309 Board Pin definitions                       [b4 AD9959 Signal]
#define SSD_SDA  PA7      // Serial DATA Pin(out) SPI:MOSI  [AD9959=IO0]
#define SSD_MISO PA6      // Serial DATA Pin(in)  SPI:MISO  [AD9959=IO2] not use
#define SSD_SCL  PA5      // Serial DATA Pin(out) Low       [AD9959=SCK]
#define SSD_CS   PA4      // Chip Select Active Low(out)    [AD9959=CSB]
#define SSD_RST  PA3      // Reset(out)                     [AD9959=P3]
#define SSD_DC   PA2      // Data/Command(out)              [AD9959=P2]
//
U8G2_SSD1309_128X64_NONAME0_F_4W_HW_SPI u8g2(U8G2_R0, SSD_CS, SSD_DC, SSD_RST);
//
#define ADC_In  PA0     // アナログ入力ピンの定義(予備)          ■V2.2:V3.0■
#define SWa_In  PA1     // コントロールSW アナログ入力ピンの定義 ■V2.2:V3.0■

#define coGSET  PA8     // Counter Gate Active High(out)Low edge
#define ciGATE  PA15    // Counter Gate ON                          ■V2.2:V3.0■
#define coPicREST  PB3     // Counter reset Active High(out)          ■V4■
#define coRSET  PB4     // Counter reset Active LOW(out)               ■V41■
#define LED_OUT PB8     // Counter Input 1/16 Active Low edge(increment)
#define ciPPS   PB9     // Counter Input 1sec Active High edge(increment)


#define LED_PIN PC13    // PC13 or 32 or D32
// ==============================================
//  自作関数のプロトタイプ宣言
void handle_timer1(void);
void handle_timer2(void);
void handle_timer3(void);
void Opening_Text(void);
void func_print(uint8_t, const char*);
void Text_setCursor(uint8_t, uint8_t);
void drawTxline(uint8_t);

int mid(char, char, int, int);
int cmmSTRcmm(char, char, int);
int8_t extractGPGGA(char*);
int8_t extractGNRMC(char*);
int8_t extractGPZDA(char*);
uint8_t jobSelect(uint8_t);        // F0キーにより、設定等の処理選択
int8_t buttonSW(void);            // M ボタンの状態を返す
uint8_t confPRMset(uint8_t, uint8_t); // PRM set
uint64_t hex16Bto64bit(uint64_t *, char * );

void dFRQtoStr(char, double);       // 周波数のフォーマット
void dFRQtoStrV(char, double);       // 周波数のフォーマット
//uint8_t write(uint16_t, uint8_t);
//uint8_t read(uint16_t);

void RegZero(void);               // カウンター用レジスタのゼロクリヤー
void PortReset(void);             // ポート初期セット&ゲートF/F・PICカウンタ値のリセット
void PicReset(void);             // PICのCPUリセット
void PpsSet(void);                // スタート・リスタート時の PPS セット

// ==============================================

// ==============================================
// タイマー割り込み関係
volatile int32_t tIntCount1;  // Timer1:割込み回数
volatile int32_t tIntCount2;  // Timer1:割込み回数
volatile int32_t tIntCount3;  // Timer3:割込み回数(int内で使用=volatile を付ける)
// ==============================================
// シリアル受信処理関係
uint8_t RXposi = 0;
uint8_t RCVok = 0;
uint8_t PICposi = 0;
uint8_t PICok = 0;
byte RXchar, PCchar, PICchar;
char RXbuff[256];
char RXbuff2[256];
char PICbuff[32];     // '[',16文字,SUMx2,']'PICからのカウンターデータ受取
char tmpbuff[64];
char useGPS[5];
char nowTime[22];
char nowDate[22];
char logdata[256];
char tmptmp[32];

// uint16_t SSt;                 // キー操作用変数
int8_t Keycnt = 0;
uint8_t FuncEnable = 0;       // Function キー有効ビット F0:bit4, F1:bit3, F2:bit2, F3:bit1,
uint8_t jobPRM = 0;           // 編集画面の処理No
uint8_t bpsPRM = 0;           // BPSの指数
uint8_t hostPRM = 0;          // HOSTへ送出するデータ選択 0:GPS Data, 1:LOG Data, 2:None
uint8_t gatePRM = 0;          // GATE時間の指数
uint8_t dispNo = 0;           // 表示番号 0:初期画面ならびに計測中, 1:各パラメータ表示中
uint8_t Counting = 0;         // 計数中 = 1;
uint16_t gpsLoss = 0;         // GPSエラー回数
uint16_t ppsLoss = 0;         // PPS信号間隔のエラー回数
uint32_t CountTime = 0;       // 計測回数
uint32_t picLoss = 0;        // PICデータ受信エラー回数

// GPSとの通信速度変更用(チェックSUM(27:2桁)は、実測値) \r\n は、Serial.println で付加する。
const char bps96set[] = "$PMTK251,9600*17";     // (1秒中のデータ占有率 65.1%)(default)
const char bps192set[] = "$PMTK251,19200*22";   // (1秒中のデータ占有率 32.6%)
const char bps384set[] = "$PMTK251,38400*27";   // (1秒中のデータ占有率 16.3%)
const char bps576set[] = "$PMTK251,57600*2C";   // (1秒中のデータ占有率 10.9%)20cmで時々崩れ?
const char bps1152set[] = "$PMTK251,115200*1F"; // (1秒中のデータ占有率 5.4%)*使用しない*
const char hotStart[] = "$PMTK101*32";          // コールドスタート ANS="$PMTK809*33"
const char warmStart[] = "$PMTK102*31";         // ワームスタート ANS="$PMTK809*33"
const char coldStart[] = "$PMTK103*30";         // コールドスタート ANS="$PMTK605*31"

// カウンター用レジスタ?
volatile uint64_t ui64Reg = 0;       // 符号無し(64bit) 周波数カウント用
//uint64_t ui64PicReg = 0;
//uint64_t ui64tmp;
uint8_t sumchk;

double dFrqReg = 0;         // 浮動小数点(64bit) 周波数計算用
double dAvgReg = 0;         // 浮動小数点(64bit) アベレージ計算用
double dMaxReg = 0;         // 浮動小数点(64bit) 最高値保持
double dMinReg = 0;         // 浮動小数点(64bit) 最小値保持
double dTmpReg = 0;         // 浮動小数点(64bit) 汎用
double dP_PReg = 0;         // 浮動小数点(64bit) p-p 振れ幅
int16_t AddCnt = 0;         // 加算回数
char fmtFrq[22];
double B4dFrqReg = 0;       // 浮動小数点(64bit) 周波数計算用
float IncDec = 0;           // 増減計算用

// (int内で使用=volatile を付ける)
volatile int16_t ppsCnt = 0;  // ゲートON時のPPSカウント用
uint8_t countDone = 0;        // カウント完了
volatile uint8_t gateON = 0;  // ゲートの状態
//uint8_t Running = 0;          // カウント中
volatile uint8_t reStart = 0; // PPS信号が欠落したときの再開フラグ

char tmpsec[3] = "00";  // ■臨時■ 秒単位でのLOG送信に使用する。


// CSVのヘッダー(1行目)のデータ列(2020/03/20 17:00 現在)
// Num=連番, YYMMDD=年月日,HHMMSS=時分秒 ,GpsQ=QPS品質, Sat=使用衛星数,
// Freq=周波数, (+-)=前差, gpsLoss=GPS信号ロス回数, ppsLoss=PPS信号ロス回数
const char logheader[] = "Num,YYMMDD,HHMMSS,GpsQ,Sat,Freq,(+-),gpsLoss,ppsLoss,picLoss";


// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// ◆◆◆◆◆電源投入時の初期画面◆◆◆◆◆
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
//
// □■■■□□■■■■□□■■■■■□■□□□■
// ■□□□■□■□□□■□■□□□□□■■□□■
// ■□□□■□■□□□■□■□□□□□■■□□■
// ■□□□■□■■■■□□■■■■□□■□■□■
// ■□□□■□■□□□□□■□□□□□■□□■■
// ■□□□■□■□□□□□■□□□□□■□□■■
// □■■■□□■□□□□□■■■■■□■□□□■
void Opening_Text()
{
  u8g2.clearBuffer();          // clear

  Text_setCursor(0, 1);     // 桁・行
  u8g2.print("OCXO Calibrator (GPS)");
  Text_setCursor(1, 2);   // 桁・行
  u8g2.print("V41 & Ver.0.04.29");

  Text_setCursor(0, 3);     // 桁・行
  u8g2.print(" (c) 2020, jlb.jp ");
  FuncEnable = B00001111;
  func_print(0, "MENU");
  func_print(1, "GATE");
  func_print(2, "STRT");
  func_print(3, "CngD");

  u8g2.sendBuffer();          // OLEDに送信(描画する)
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// カウンター用レジスタのゼロクリヤー
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
void RegZero() {
  ui64Reg = 0;
  dFrqReg = 0;
  dAvgReg = 0;
  dMaxReg = 0;
  dMinReg = 0;
  dTmpReg = 0;
  dP_PReg = 0;
  AddCnt = 0;
  ppsCnt = 0;
  gateON = 0;
  gpsLoss = 0;
  ppsLoss = 0;
  tIntCount3 = 0;
  //Running = 0;  // 未使用?
  B4dFrqReg = 0;       // 浮動小数点(64bit) 周波数計算用
  IncDec = 0;           // 増減計算用
  picLoss = 0;      // PICデータ受信エラー回数

  // 重要 入れないとPICからの古いデータが残る場合が有る。
  while (Serial3.available()) {
    PICchar = Serial3.read();
    delay(2);   // 9600bps時に1文字1.04mSsec
  }
  PICok = 0;
}

// GATE用D-F/Fのリセット    V41時は反転(LOW アクティブ)
void PortReset() {
  digitalWrite(coRSET, LOW);
  delay(1);                         // 1mS
  digitalWrite(coRSET, HIGH);
  delay(1);                         // 1mS
}
// PICのリセット
void PicReset() {
  digitalWrite(coPicREST, HIGH);
  delay(1);                         // 1mS
  digitalWrite(coPicREST, LOW);
  delay(10);                         // 10mS
}


// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// PPS 割り込みのカウント
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
void intPps() {     // PPS 割込み

  if (tIntCount3 >= 12) {        // 前回のPPS信号から1.2秒以上経過している。

    digitalWrite(coGSET, LOW);
    gateON = 0;                 // GATEの状態OFF
    reStart = 1;                // PPS信号が欠落した後の再開フラグ SET

  } else {
    // 正常処理
    if (digitalRead(coGSET) == HIGH) {
      gateON = 1;     // GATEの状態ON

      // 最終手前のPPS(次のPPSでカウント完了)なら、GSETをOFFする。
      if (ppsCnt <= 1) digitalWrite(coGSET, LOW);

      ppsCnt--;
    } else {
      gateON = 0;     // GATEの状態OFF

      if (Counting == 1) countDone = 1;  // カウント完了
      ppsCnt = 0;     // PPS Count Zero
      // delay(1);     // delay() は機能しない。

    }
  }
  tIntCount3 = 0;       // PPS信号の間隔を監視するカウンタ
}


// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
//  PPS パラメータセット <<共通使用>>
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
void PpsSet() {
  switch (gatePRM) {
    case 1:
      ppsCnt = 10;      // 11秒/回
      break;
    case 2:
      ppsCnt = 100;     // 1分41秒/回
      break;
    case 3:
      ppsCnt = 1000;     // 16分41秒/回
      break;
    case 4:
      ppsCnt = 10000;     // 2時間46分41秒/回
      break;
    default:
      ppsCnt = 1;
      break;
  }
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// double の数値を周波数表記に変換する
//
// 小数点以下の位をGATE時間に応じて可変(Variable)する(2020/04/15)
//
//      01234567890123456
//      98765432.1234       ----sprintf 後の状態
//      98 765 432.1234Hz   ----書式変換後のイメージ
//                              10MHz台まで表示する。桁の先頭がzeroの場合は空白。
//                              3桁カンマは付けずに空白。少数以下4桁まで。
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
void dFRQtoStrV(char* fmttmp, double freqIN) {
  double dtmp;
  char strtmp[22];
  int8_t i, j, ln;

  dtmp = freqIN;

  switch (gatePRM) {
    case 1:
      sprintf(strtmp, "%10.1lf", dtmp);
      break;
    case 2:
      sprintf(strtmp, "%11.2lf", dtmp);
      break;
    case 3:
      sprintf(strtmp, "%12.3lf", dtmp);
      break;
    case 4:
      sprintf(strtmp, "%13.4lf", dtmp);
      break;
    default:
      sprintf(strtmp, "%10.1lf", dtmp);
      strtmp[9] = ' ';
      break;
  }
  ln = strlen(strtmp) + 2;
  j = 0;
  for (i = 0; i < ln; i++) {
    switch (i) {
      case 2:
      case 5:
        fmttmp[j] = ' ';
        j++;
        fmttmp[j] = strtmp[i];
        j++;
        break;
      default:
        fmttmp[j] = strtmp[i];
        j++;
        break;
    }
  }
  fmttmp[15] = 0x0;
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// double の数値を周波数表記に変換する
//
// アベレージの表示専用にする(2020/04/15)
//
//      01234567890123456
//      98765432.1234       ----sprintf 後の状態
//      98 765 432.1234Hz   ----書式変換後のイメージ
//                              10MHz台まで表示する。桁の先頭がzeroの場合は空白。
//                              3桁カンマは付けずに空白。少数以下4桁まで。
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
void dFRQtoStr(char* fmttmp, double freqIN) {
  double dtmp;
  char strtmp[22];
  int8_t i, j, ln;

  dtmp = freqIN;

  sprintf(strtmp, "%13.4lf", dtmp);
  ln = strlen(strtmp) + 2;
  j = 0;
  for (i = 0; i < ln; i++) {
    switch (i) {
      case 2:
      case 5:
        fmttmp[j] = ' ';
        j++;
        fmttmp[j] = strtmp[i];
        j++;
        break;
      default:
        fmttmp[j] = strtmp[i];
        j++;
        break;
    }
  }
  fmttmp[15] = 0x0;
}
/*************************************************************
   受信した文字データ(SUM付)を uint64_t に変換し、SUM Check をする。
   戻り値 uint64_t  <==    char inHex[20];

   引数にSUM Checkの結果(sumFlag)を戻すため、アドレス渡しで関数を定義
   使用時は、'&' を付ける。 hex16Bto64bit( &sumFlag, char *inHex)

   sumFlag = 0;エラーなし
   sumFlag = 1;エラー有り(発生はしない予想)
 *************************************************************/
uint64_t hex16Bto64bit( uint8_t * sumFlag, char *inHex) {
  int8_t i;
  uint8_t tmpB1, tmpB2, tmpsum = 0;
  uint64_t out64val = 0;
  uint16_t sum = 0;
  for (i = 15; i >= 0; i--) { // 文字配列の後から取り出す
    tmpB1 = inHex[i];
    sum += tmpB1;
    if (tmpB1 > 0x39) {
      tmpB2 = tmpB1 - 55;
      tmpB2 &= 0x0F;
    } else {
      tmpB2 = tmpB1 & 0x0F;
    }
    out64val = out64val << 4;
    out64val = out64val | tmpB2;
  }
  for (i = 17; i >= 16; i--) {  // Check Sum
    tmpB1 = inHex[i];
    if (tmpB1 > 0x39) {
      tmpB2 = tmpB1 - 55;
      tmpB2 &= 0x0F;
    } else {
      tmpB2 = tmpB1 & 0x0F;
    }
    tmpsum <<= 4;
    tmpsum |= tmpB2;
  }

  if (tmpsum == (uint8_t)sum) { // LSB側8bitを照合
    *sumFlag = 0;
  } else {
    *sumFlag = 1;
  }
  return out64val;
}

////////////////////////////////////////////////////////////
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// ◆◆◆◆◆タイマー割込み◆◆◆◆◆
// 0.1秒ごとに数を増分する  
// OLED の表示サイクル用に使用する        0.01Sec(timer1)
// PPS信号が欠落したことの検出に使用する。 0.1Sec(timer3)
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

void handle_timer1() {
  tIntCount1++;
  if (tIntCount1 > 500) tIntCount1 = 0;    // 500 x 0.01S = 5 Sec 以上はクリアー
}
void handle_timer2() {
  tIntCount2++;        // 1mSec毎にカウント
  if (tIntCount2 > 5000) tIntCount2 = 0;    // 5000 x 1mS = 5 Sec 以上はクリアー
}
void handle_timer3() {
  if (gateON == 1)  tIntCount3++;        // GATEがONの間、0.1Sec毎にカウント
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
//
// □■■■□□■■■■■□■■■■■□■□□□■□■■■■□
// ■□□□■□■□□□□□□□■□□□■□□□■□■□□□■
// ■□□□□□■□□□□□□□■□□□■□□□■□■□□□■
// □■■■□□■■■■□□□□■□□□■□□□■□■■■■□
// □□□□■□■□□□□□□□■□□□■□□□■□■□□□□
// ■□□□■□■□□□□□□□■□□□■□□□■□■□□□□
// □■■■□□■■■■■□□□■□□□□■■■□□■□□□□
//
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

void setup(void) {
  disableDebugPorts();      // JTAG_Debug を使わないことで、PA15,PB3,PB4を使えるようにする。
  pinMode(ADC_In, INPUT_ANALOG);
  pinMode(SWa_In, INPUT_ANALOG);


  // pinMode(ciCOUNT_T, INPUT_PULLUP);           //  ■V2.2:追加■


  pinMode(coGSET, OUTPUT);
  pinMode(ciGATE, INPUT);
  pinMode(coPicREST, OUTPUT);     // PIC AllRESET
  pinMode(coRSET, OUTPUT);      // Device Regi RESET
  //  pinMode(ciCOUNT, INPUT);
  // #define ciCOUNT_T PA1       // Counter Input Timer2(TIM2_CH2)      ■V2.2:追加■
  pinMode(ciPPS, INPUT);

  pinMode(LED_PIN, OUTPUT);
  pinMode(LED_OUT, OUTPUT);

  digitalWrite(LED_PIN, HIGH);
  digitalWrite(LED_OUT, HIGH);

  digitalWrite(coGSET, LOW);

  PortReset();     // GATE用D-F/Fの初期化
  PicReset();     // PICの初期化

  attachInterrupt(ciPPS, intPps, RISING);    // PPS 割り込み関数の指定。RISING
  // attachInterrupt(ctQ7, intPictest, FALLING);    // 臨時テスト

  RegZero();      // レジスターの初期化

  SPI.begin();

  u8g2.begin();
  //  u8g2.setFont(u8g2_font_6x12_tr);  // 7bit 等幅、21文字、5行(行12dot)、●●これが良さそう●●
  u8g2.setFont(u8g2_font_6x12_t_symbols); // ?記号を使用するために、こちらのフォントを使う。
  u8g2.setFontMode(1);  /* activate transparent font mode */

  Wire.begin();           // I2C EEPROM用、他

  Opening_Text();         // 初期画面を表示

  // ■■■■■■■■■■■■■■■
  // EEPROM読み出し・初期化
  // ■■■■■■■■■■■■■■■
  uint8_t tmp;
  tmp = read(0); // ROMからの読み出し Address = 0
  if (tmp >= 0 && tmp <= 2) {   // パラメータが範囲内   0 〜 2
    bpsPRM = tmp;               //  = PRMへ読み出し値セット
  } else {                      // パラメータが範囲外 = ROM新品または、故障??
    write(0, 0);               // 0 = (default) を書込み
  }
  tmp = read(1); // ROMからの読み出し Address = 1
  if (tmp >= 0 && tmp <= 2) {
    hostPRM = tmp;
  } else {
    write(1, 0);
  }
  tmp = read(2); // ROMからの読み出し Address = 2
  if (tmp >= 0 && tmp <= 2) {
    gatePRM = tmp;
  } else {
    write(2, 0);
  }
  // ■■■■■■■■■■■■■■■

  // ★★★タイマー割込み 1 の初期設定 START★★★▼▼▼▼▼▼ OLEDの表示サイクル用
  Timer1.pause();                   // タイマー停止
  Timer1.setPrescaleFactor(7200);   // 分周値set 72MHz ÷ 7200 = 10KHz
  Timer1.setOverflow(100);          // 10KHz x 100 = 0.01秒でオーバーフォロー割込み
  // ------
  Timer1.attachInterrupt(           // 割り込みハンドラの登録
    TIMER_UPDATE_INTERRUPT,         // 条件は、カウンターオーバーフロー更新時
    handle_timer1);                  // 呼び出す関数
  // ------
  Timer1.setCount(0);               // カウンタを0に設定
  Timer1.refresh();                 // タイマ更新
  Timer1.resume();                  // タイマースタート
  // --------------------------------------
  // ★★★タイマー割込み 2 の初期設定 START★★★▼▼▼▼▼▼ (処理時間の計測に臨時使用)汎用
  Timer2.pause();                   // タイマー停止
  Timer2.setPrescaleFactor(7200);   // 分周値set 72MHz ÷ 7200 = 10KHz (uint32)
  Timer2.setOverflow(10);           // 10KHz x 10 = 1mSsecでオーバーフォロー割込み (uint32)
  // ------
  Timer2.attachInterrupt(           // 割り込みハンドラの登録
    TIMER_UPDATE_INTERRUPT,         // 条件は、カウンターオーバーフロー更新時
    handle_timer2);                  // 呼び出す関数
  // ------
  Timer2.setCount(0);               // カウンタを0に設定
  Timer2.refresh();                 // タイマ更新
  Timer2.resume();                  // タイマー2スタート
  // ★★★タイマー割込みの初期設定 END  ★★★▲▲▲▲▲▲
  // --------------------------------------
  // --------------------------------------
  // ★★★タイマー割込み 3 の初期設定 START★★★▼▼▼▼▼▼ PPS信号の欠落検知用
  Timer3.pause();                   // タイマー停止
  Timer3.setPrescaleFactor(7200);   // 分周値set 72MHz ÷ 7200 = 10KHz (uint32)
  Timer3.setOverflow(1000);          // 10KHz x 1000 = 0.1秒でオーバーフォロー割込み (uint16)
  // ------
  Timer3.attachInterrupt(           // 割り込みハンドラの登録
    TIMER_UPDATE_INTERRUPT,         // 条件は、カウンターオーバーフロー更新時
    handle_timer3);                  // 呼び出す関数
  // ------
  Timer3.setCount(0);               // カウンタを0に設定
  Timer3.refresh();                 // タイマ更新
  Timer3.resume();                  // タイマー2スタート
  // ★★★タイマー割込みの初期設定 END  ★★★▲▲▲▲▲▲
  // --------------------------------------


  Serial.begin(9600);     // USBシリアル(PC側) STM32は、速度指定しても最高速度で動く。
  //while (!Serial);         // Arduino UNO 書込み後のUSBシリアルの解放を待つ。
  //while (!Serial.isConnected()) delay(10);  // STM32 USB-シリアル用 isConnected()を使う。

  // ここでROMからリードしたbpsをセット GNSS用
  switch (bpsPRM) {
    case 1:
      Serial1.begin(19200);              // Serial1(PA9 PA10)
      break;
    case 2:
      Serial1.begin(38400);              // Serial1(PA9 PA10)
      break;
    default:
      Serial1.begin(9600);               // Serial1(PA9 PA10)
      break;
  }
  Serial3.begin(38400); // PIC側と合わせる


  delay(3000);
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
//
// ■□□□□□□■■■□□□■■■□□■■■■□
// ■□□□□□■□□□■□■□□□■□■□□□■
// ■□□□□□■□□□■□■□□□■□■□□□■
// ■□□□□□■□□□■□■□□□■□■■■■□
// ■□□□□□■□□□■□■□□□■□■□□□□
// ■□□□□□■□□□■□■□□□■□■□□□□
// ■■■■■□□■■■□□□■■■□□■□□□□
//
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// Loop一回の処理時間(タイマーで実測:2020/04/17)
// スタンバイ時:Max16mS(1mS〜12mS)
// スタート時 :Max194mS
// 計測中   :Max16mS(1mS〜15mS)
// ストップ時 :Max239mS
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

void loop(void) {

  //  digitalWrite(LED_PIN, !digitalRead (ciPPS));   // LED_PIN を PPS入力に同期させる

  digitalWrite(LED_OUT, !digitalRead (ciPPS));   // LED_PIN を PPS入力に同期させる

  switch (buttonSW()) {     // キーが押されているか確認。
    // パラメータ編集メニューへ
    case 10:              // 長押し(20)F0 ===>短押し(10)へ変更(2020/04/16)
      if ((FuncEnable & 0x08) > 0) {    // 許可フラグ
        jobPRM = jobSelect(jobPRM);        // ★★★設定処理等の選択画面に行く
        u8g2.clearBuffer();   // ★★★設定処理等の選択画面からの帰りにクリアー
        Text_setCursor(1, 1);   // 桁・行
        u8g2.print(" Config MENU END ");
        u8g2.sendBuffer();            // OLEDに送信(描画する)
        delay(500);
      }
      break;
    // GATE time切換
    case 11:          // 短押しF1、長押しは無効として処理。
      if ((FuncEnable & 0x04) > 0) {    // 許可フラグ
        if (dispNo == 0) {
          gatePRM++;
          if (gatePRM > 3) gatePRM = 0;    // GATE time を変更、通常0〜3
        } else {
          // CLEAR  第2画面
          PicReset();   // PICをリセットする。(INT0割り込みで)
          delay(1);
        }

      }
      break;
    // GATE time切換
    case 21:          // 長押しF1で、10,000secの設定
      if ((FuncEnable & 0x04) > 0) {    // 許可フラグ
        if (dispNo == 0) {
          gatePRM = 4;
        }
      }
      break;
    // 計測スタート・ストップ
    case 12:          // 短押しF2、長押しは無効として処理。
      if ((FuncEnable & 0x02) > 0) {    // 許可フラグ
        if (Counting == 0) {
          Counting = 1;     // カウントスタート
          RegZero();
          PortReset();
          PicReset();

          // ゲート時間により、
          PpsSet();                       // gatePRMからppsCntへ時間をセット

          digitalWrite(coGSET, HIGH);
        } else {
          digitalWrite(coGSET, LOW);
          Counting = 0;     // カウントストップ
          CountTime = 0;
          delay(50);
        }
      }
      break;
    // 表示画面の切替
    case 13:          // 短押しF3、長押しは無効として処理。
      if ((FuncEnable & 0x01) > 0) {    // 許可フラグ
        if (dispNo == 0) {
          dispNo = 1;     // 2画面へ変更
        } else {
          dispNo = 0;     // 1画面へ変更
        }
      }
      break;
  } // switch() end

  // ■■■もし、PPS信号が欠落したときの再開フラグが立ったら。■■■
  if (reStart != 0) {

    ppsLoss++;      // PPSのロス回数を+1

    PortReset();    // GATE用D-F/Fのリセット

    PpsSet();                       // gatePRMからppsCntへ時間をセット
    ui64Reg = 0;                    // クリヤー
    reStart = 0;
    digitalWrite(coGSET, HIGH);

  }

  // ■■■PPSが長時間停止したときのカウント処理停止■■■
  if (tIntCount3 > 200000) { // タイマー割り込み 100mS * 20000 = 20000S(約33.3分) で
    // カウント処理を停止する。(PPS信号が約33.3分到達しなかった)

    digitalWrite(coGSET, LOW);
    Counting = 0;     // カウントストップ
    gateON = 0;
    tIntCount3 = 0;
  }

  // --------------------------------------
  if (Serial1.available()) { // ■Serial1(PA9 PA10)からUSBシリアル(PC側)■

    RXchar = Serial1.read();
    switch (RXposi) {
      case 0:
        if (RXchar == '$')
        {
          RXbuff[RXposi] = RXchar;
          RXposi++;
        } else {
          RXposi = 0;
          RCVok = 0;
        }
        break;
      case 255:                 // バッファ数 255 一杯なら吐き捨てる
        RXposi = 0;
        RCVok = 0;
        break;
      default:
        if (RXchar == 0x0a)      // エンドマークなら '*'又は <lf>0x0a
        {
          RXbuff[RXposi] = 0x00;      // 文字配列に0x00を書き込む
          RCVok = 1;
          RXposi = 0;
        } else {
          RXbuff[RXposi] = RXchar;    // それ以外なら RXbuff[] に取り込む
          RXposi++;
        }
    }
    if (hostPRM == 0) {         // 0:GPSデータを受け付ける状態
      Serial.write(RXchar);
    }
  }
  // --------------------------------------
  if (RCVok == 1)
  {


    Text_setCursor(1, 3);     // 桁・行
    u8g2.print(RXbuff);


    if (strncmp(RXbuff, "$GPGGA", 6) == 0) {
      strcpy(RXbuff2, RXbuff);
      extractGPGGA(RXbuff2);        // GPS数を抽出
    }
    if (strncmp(RXbuff, "$GPZDA", 6) == 0) {
      strcpy(RXbuff2, RXbuff);
      extractGPZDA(RXbuff2);        // UTCと年月日を抽出してJSTに変換する
    }
    RCVok = 0;
  }
  // --------------------------------------
  if (Serial.available()) {   // ■USBシリアル(PC側)からSerial1(PA9 PA10)■

    PCchar = Serial.read();
    Serial1.write(PCchar);
  }

  // --------------------------------------
  if (Serial3.available()) { // ■Serial3(PB11)からカウンターデータ受取■

    PICchar = Serial3.read();
    switch (PICposi) {
      case 0:
        if (PICchar == '[') // '['= START Mark
        {
          PICposi++;
        } else {
          PICposi = 0;
          PICok = 0;
        }
        break;
      case 19:
        if  (PICchar == ']') // 0から数えて19番目が ']'= END Mark なら正常
        {
          PICbuff[PICposi - 1] = 0x00;
          PICok = 1;
        }
        PICposi = 0;
        break;
      default:
        if  (PICchar != ']') // ']'= END Mark 以外なら
        {
          PICbuff[PICposi - 1] = PICchar;
          PICposi++;
        } else {
          PICposi = 0;
          PICok = 0;
        }
        break;
    }

    // ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    if (hostPRM == 2) {         // 2:PICデータをHOSTへ送信する状態■テスト■
      Serial.write(PICchar);
    }
    // ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■


  }

  // --------------------------------------
  // ■■■■■■周波数カウントが完了した時の処理■■■■■■
  // ■■■■■■周波数カウントが完了した時の処理■■■■■■
  // ■■■■■■周波数カウントが完了した時の処理■■■■■■
  // ■■■■■■周波数カウントが完了した時の処理■■■■■■
  // ■■■■■■周波数カウントが完了した時の処理■■■■■■
  // ■■■■■■周波数カウントが完了した時の処理■■■■■■
  if (countDone == 1 && PICok == 1)  {
    CountTime++;          // 計測回数

    // (sumchk != 0) SUM Check Error
    // 1〜2cm間の伝送路のため、発生可能性はゼロに近いと思われるため
    // SUM Check Error 時の処理は記述せず。(2020/04/10)
    // そのまま処理して、picLoss を加算する。
    ui64Reg = hex16Bto64bit(&sumchk, PICbuff);
    if (sumchk > 0) picLoss++; // もし発生したら、ログ等への引き渡しのみ

    dFrqReg = (double)ui64Reg;      // double に変換

    ui64Reg = 0;                    // クリヤー

    // ゲート時間により、
    switch (gatePRM) {
      case 1:
        dFrqReg = dFrqReg / 10;
        break;
      case 2:
        dFrqReg = dFrqReg / 100;
        break;
      case 3:
        dFrqReg = dFrqReg / 1000;
        break;
      case 4:     // 実験的な(通常は選択できない)扱い
        dFrqReg = dFrqReg / 10000;
        break;
      default:
        break;
    }

    // アベレージに加算
    dAvgReg += dFrqReg;
    AddCnt++;

    // 最高値の更新
    if (dMaxReg < dFrqReg) dMaxReg = dFrqReg;
    // 最低値の更新
    if (dMinReg != 0) {
      if (dMinReg > dFrqReg) dMinReg = dFrqReg;
    } else {
      dMinReg = dFrqReg;
    }
    // 振れ幅 p-p の更新
    dP_PReg = dMaxReg - dMinReg;

    // ■■■■■■■■■■■■■■■■■■■■■■■■
    // 一つ前の測定結果との誤差を求める。
    // ■■■■■■■■■■■■■■■■■■■■■■■■
    if (B4dFrqReg == 0) {
      B4dFrqReg = dFrqReg;
      IncDec = 0;
    } else {
      IncDec = dFrqReg - B4dFrqReg;
      B4dFrqReg = dFrqReg;
    }

    PICok = 0;            // Serial3からの読み込みもクリヤー
    countDone = 0;       // 周波数カウントが完了した時の処理■■完了■■

    if (Counting == 1) {    // カウント継続中なら

      // ゲート時間により、
      PpsSet();                       // gatePRMからppsCntへ時間をセット

      PortReset();    // GATE用D-F/Fのリセット

      digitalWrite(coGSET, HIGH);   // カウントの継続セット
    }

    // ■■■■■■■■■■■■■■■■■■■■■■■■
    // カウント完了ごとに、(LOG指定なら)LOG出力
    // ■■■■■■■■■■■■■■■■■■■■■■■■

    if (hostPRM == 1) {         // 1:LOGデータをHOSTへ送信する状態■テスト■
      if (CountTime == 1 ) {        // もし計測1回目なら
        Serial.println(logheader); // <cr><lf>を付けてヘッダー出力
      }

      sprintf(tmptmp, "%d", CountTime);
      strcpy(logdata, tmptmp);
      strcat(logdata, ",");
      strcat(logdata, nowDate);
      strcat(logdata, ",");
      strcat(logdata, nowTime);
      strcat(logdata, ",");
      strcat(logdata, useGPS);
      strcat(logdata, ",");

      switch (gatePRM) {    // GATE時間に応じて小数点以下を増減する(2020/04/29)
        case 1:
          sprintf(tmptmp, "%10.1lf", dFrqReg);
          break;
        case 2:
          sprintf(tmptmp, "%11.2lf", dFrqReg);
          break;
        case 3:
          sprintf(tmptmp, "%12.3lf", dFrqReg);
          break;
        case 4:
          sprintf(tmptmp, "%13.4lf", dFrqReg);
          break;
        default:
          sprintf(tmptmp, "%8.0lf", dFrqReg);
          break;
      }
      strcat(logdata, tmptmp);
      strcat(logdata, ",");
//      sprintf(tmptmp, "%+10.4f", IncDec);    // + - 記号付き、少数以下3桁、計8桁

      switch (gatePRM) {    // GATE時間に応じて小数点以下を増減する(2020/04/29)
        case 1:
          sprintf(tmptmp, "%+8.1f", IncDec);
          break;
        case 2:
          sprintf(tmptmp, "%+9.2f", IncDec);
          break;
        case 3:
          sprintf(tmptmp, "%+10.3f", IncDec);
          break;
        case 4:
          sprintf(tmptmp, "%+11.4f", IncDec);
          break;
        default:
          sprintf(tmptmp, "%+6.0f", IncDec);
          break;
      }      
      strcat(logdata, tmptmp);

      strcat(logdata, ",");
      sprintf(tmptmp, "%d", gpsLoss);
      strcat(logdata, tmptmp);
      strcat(logdata, ",");
      sprintf(tmptmp, "%d", ppsLoss);
      strcat(logdata, tmptmp);
      strcat(logdata, ",");
      sprintf(tmptmp, "%d", picLoss); // PICデータのSUM Checkエラー回数 デバッグ用
      strcat(logdata, tmptmp);
      Serial.println(logdata);       // <cr><lf>を付けて出力
    }

  }   // ■■■■■■周波数カウントが完了処理 終了 ■■■■■■

  // --------------------------------------

  // ■■■■■■■■■■■■■■■■■■■■■■■■
  // OLEDへの表示処理
  // ■■■■■■■■■■■■■■■■■■■■■■■■

  if (tIntCount1 >= 20) { // タイマー割り込み 10mS * 20 = 200mS 毎に表示を更新する。■■■■
    // *100mS 毎でも遅れは無い模様。 memo->( digitalRead(ciGON) == LOW && )
    tIntCount1 = 0;

    u8g2.clearBuffer();          // clear the internal memory

    if (dispNo == 0) {                // 基本画面
      Text_setCursor(0, 0);   // 桁・行
      u8g2.print("Frq:");
      dFRQtoStrV(fmtFrq, dFrqReg); // 周波数データを表示形式に変換
      Text_setCursor(4, 0);     // 桁・行
      u8g2.print(fmtFrq);
      Text_setCursor(19, 0);   // 桁・行
      u8g2.print("Hz");
      Text_setCursor(0, 1);     // 桁・行
      u8g2.print(useGPS);

      Text_setCursor(4, 1);     // 桁・行
      u8g2.print(",g");
      Text_setCursor(6, 1);     // 桁・行
      u8g2.print(gpsLoss);
      u8g2.print(",p");
      u8g2.print(ppsLoss);      // PS信号が欠落したとき   デバッグ用

      Text_setCursor(13, 1);    // 桁・行
      u8g2.print(nowTime);

      Text_setCursor(1, 2);     // 桁・行
      u8g2.print("GATE=");
      Text_setCursor(6, 2);     // 桁・行
      switch (gatePRM) {
        case 1:
          u8g2.print("10s");
          break;
        case 2:
          u8g2.print("100s");
          break;
        case 3:
          u8g2.print("1000s");
          break;
        case 4:     // 実験的な扱い(長押しで選択)
          u8g2.print("10^4s");    // 表示桁数節約のため 10の4乗 表記
          break;
        default:
          u8g2.print("1s");
          break;
      }

      sprintf(tmptmp, "%5d", CountTime);  // 計測回数(最小55時間連続でカウントできる桁数)
      Text_setCursor(11, 2);     // 桁・行
      u8g2.print(tmptmp);

      sprintf(tmptmp, "%5d", ppsCnt);   // GATEがOFFするまでの pps回数
      Text_setCursor(16, 2);     // 桁・行
      u8g2.print(tmptmp);




      Text_setCursor(1, 3);     // 桁・行
      u8g2.print("SEND=");
      Text_setCursor(6, 3);     // 桁・行
      switch (hostPRM) {
        case 0:
          u8g2.print("GPS");
          break;
        case 1:
          u8g2.print("LOG");
          break;
        case 2:
          u8g2.print("PIC");
          break;
      }

      // 仮■■ 変更したパラメータを表示する。デバッグ用
      char aa = 0x30 | jobPRM;
      char bb = 0x30 | bpsPRM;
      char cc = 0x30 | hostPRM;
      char dd = 0x30 | gatePRM;
      Text_setCursor(10, 3);    // 桁・行
      // u8g2.print(aa);
      u8g2.print(bb);
      u8g2.print(cc);
      u8g2.print(dd);

      // ************************************************************************
      //sprintf(tmptmp, "%2d", tIntCount3); // PPS信号の間隔(0.1Sec単位)デバッグ用
      //Text_setCursor(14, 3);     // 桁・行
      //  u8g2.print(tmptmp);
      // ************************************************************************
      sprintf(tmptmp, "%2d", picLoss); // PICデータのシリアル受信エラー回数 デバッグ用
      Text_setCursor(19, 3);     // 桁・行
      u8g2.print(tmptmp);

      if (Counting == 0) {        // 停止中
        FuncEnable = B00001111;
        func_print(0, "MENU");
        func_print(1, "GATE");
        func_print(2, "STRT");
        func_print(3, "CngD");
      } else {                    // カウント中
        FuncEnable = B00000011;
        func_print(2, "STOP");
        func_print(3, "CngD");
      }


    } else {                      // 拡張 第2画面■■■■■■■■
      //
      Text_setCursor(0, 0);     // 桁・行
      u8g2.print("Ave:");
      if (AddCnt > 0) {
        dTmpReg = dAvgReg / (double)AddCnt;
        dFRQtoStr(fmtFrq, dTmpReg); // 周波数データを表示形式に変換
        Text_setCursor(4, 0);     // 桁・行
        u8g2.print(fmtFrq);
      }
      Text_setCursor(19, 0);   // 桁・行
      u8g2.print("Hz");
      //
      Text_setCursor(0, 1);     // 桁・行
      u8g2.print("Max:");
      dFRQtoStrV(fmtFrq, dMaxReg); // 周波数データを表示形式に変換
      Text_setCursor(4, 1);     // 桁・行
      u8g2.print(fmtFrq);
      Text_setCursor(19, 1);   // 桁・行
      u8g2.print("Hz");
      //
      Text_setCursor(0, 2);     // 桁・行
      u8g2.print("Min:");
      dFRQtoStrV(fmtFrq, dMinReg); // 周波数データを表示形式に変換
      Text_setCursor(4, 2);     // 桁・行
      u8g2.print(fmtFrq);
      Text_setCursor(19, 2);   // 桁・行
      u8g2.print("Hz");
      //
      Text_setCursor(0, 3);     // 桁・行
      u8g2.print("P-P:");
      dFRQtoStrV(fmtFrq, dP_PReg); // 周波数データを表示形式に変換
      Text_setCursor(4, 3);     // 桁・行
      u8g2.print(fmtFrq);
      Text_setCursor(19, 3);   // 桁・行
      u8g2.print("Hz");

      if (Counting == 0) {        // 停止中
        FuncEnable = B00000101;
        func_print(1, "pCLR");
        func_print(3, "CngD");
      } else {                    // カウント中
        FuncEnable = B00000001;
        func_print(3, "CngD");
      }
    }
    // ■■■■■■■■■■■■■■■■■■■■■■■■
    u8g2.sendBuffer();          // OLEDに送信(描画する)
    /////////////////////////////////////
  }



} // Loop End
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
//   「ファンクション」表示関数
//   表示器の最下行はファンクション表示エリアとして、4つのボタンに割り当てる。
//   一つのボタンは4文字で、左1文字分を空けてBox描画で反転表示する。
//   fnmb : ボタン番号
//   str  : 表示文字(4文字以内)
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
void func_print(uint8_t fnmb, const char* str)
{
  int8_t boxORframe = 1;    // 0 = Box描画,  1 = フレーム描画 切替予定
  int8_t len;
  len = strlen(str);
  if ((len == 0) || (len > 4)) return;

  switch (fnmb) {
    case 0:
      u8g2.setDrawColor(1);          // 反転用の処理のため
      if (boxORframe == 0)  u8g2.drawBox(5, 50, 27, 13);   // Box描画
      else
        u8g2.drawFrame(5, 50, 27, 13);   // フレーム描画
      u8g2.setDrawColor(2);          //  color 2 set
      Text_setCursor(1, 4);   // 桁・行
      u8g2.print(str);
      //      u8g2.drawLine(6, 63, 31, 63);   // (x1, y1, x2, y2)
      break;
    case 1:
      u8g2.setDrawColor(1);          // 反転用の処理のため
      if (boxORframe == 0)  u8g2.drawBox(35, 50, 27, 13);  // Box描画
      else
        u8g2.drawFrame(35, 50, 27, 13);
      u8g2.setDrawColor(2);          //  color 2 set
      Text_setCursor(6, 4);   // 桁・行
      u8g2.print(str);
      //      u8g2.drawLine(36, 63, 61, 63);   // (x1, y1, x2, y2)
      break;
    case 2:
      u8g2.setDrawColor(1);          // 反転用の処理のため
      if (boxORframe == 0)  u8g2.drawBox(65, 50, 27, 13);  // Box描画
      else
        u8g2.drawFrame(65, 50, 27, 13);
      u8g2.setDrawColor(2);          //  color 2 set
      Text_setCursor(11, 4);   // 桁・行
      u8g2.print(str);
      //      u8g2.drawLine(66, 63, 91, 63);   // (x1, y1, x2, y2)
      break;
    case 3:
      u8g2.setDrawColor(1);           // 反転用の処理のため
      if (boxORframe == 0)  u8g2.drawBox(95, 50, 27, 13);   // Box描画
      else
        u8g2.drawFrame(95, 50, 27, 13);
      u8g2.setDrawColor(2);           //  color 2 set
      Text_setCursor(16, 4);   // 桁・行
      u8g2.print(str);
      //      u8g2.drawLine(96, 63, 121, 63);   // (x1, y1, x2, y2)
      break;
  }
}
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
//  文字出力の位置を、文字単位で指定する。
//  左上が0カラムの0行(合計:21文字x5行)
//  対象とするフォント:   u8g2.setFont(u8g2_font_6x12_mf);
//  *1文字目の左1ドットは、空ける。
//   cc   : 桁 0から20
//   ll   : 行 0から4
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
void Text_setCursor(uint8_t cc, uint8_t ll) // cc:0 to 20, ll:0 to 4  Use
{
  uint8_t cp, lp;
  cp = cc * 6 + 1;
  lp = (ll + 1) * 12 - 1;

  u8g2.setCursor(cp, lp);
}
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
//  上記同様にテキストの行を指定して反転させる。
//  行は、0から4の5行として指定する。
//  対象とするフォント:   u8g2.setFont(u8g2_font_6x12_mf);
//  *1文字目の左1ドットは、空ける。
//   cc   : 桁 0から20
//   ll   : 行 0から4
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
void drawTxline(uint8_t ll)
{
  uint8_t l1;
  l1 = ll * 12 + 1;
  u8g2.drawBox(0, l1, 127, 12);
}
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// バイト単位で文字列を抽出するMid関数
// 128文字以内を制限しないため、int8_t ではなく int 型を使用する。
// buf[]:抽出先
//  str[]:抽出される文字列
//  pos  :抽出開始位置
//  len  :文字数
// return は、抽出した文字数を返す。
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
int mid(char *buf, char *str, int pos, int len)
{
  int i;
  int j = 0;
  for (i = 0; str[i] != '\0'; i++) {  // 対象の文字列の文字数分を末尾まで順に
    if (i >= pos && i < pos + len) {  // 指定位置と長さ分を
      buf[j] = str[i];                // buf[j]に格納
      j++;
    }
  }
  buf[j] = '\0'; //bufの末尾にNULL文字を格納
  return strlen(buf);
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// int cmmSTRcmm(char buf[], char str[], int cnt)
// 128文字以内を制限しないため、int8_t ではなく int 型を使用する。
//
// ','で挟まれた文字列を抽出する。
//
// buf[] :抽出先文字列
// str[] :検索対象文字列
// cnt   :何個目の ',' 以後の文字列以降を抽出するのか指定
//        注)指定個数+1以上の ',' が発見できない「不成立」
// return
// 0〜n:抽出した文字数
// -1  :不成立
//
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
int cmmSTRcmm(char *buf, char *str, int cnt)
{
  int c, i, h = 0, p1 = 0, p2 = 0;
  int j = 0;
  c = strlen(str);
  if (cnt >= c) return -1;   // 文字列が探したい位置より短い (-1)
  for (i = 0; c > i; i++)
  {
    if (str[i] == ',') {
      h++;
      if (h == cnt) p1 = i;       // p1に',' cnt個目の位置セット
      if (h == cnt + 1) p2 = i;   // p2に',' cnt+1個目の位置セット
    }
  }
  if (h < cnt + 1)  return -1;    // 指定個数+1以上の ',' が発見できない (-1)
  if (p1 + 1 == p2) {             // ','間に文字列ゼロ
    buf[0] = 0x00;
    return 0;
  }
  for (i = 1; (p1 + i) < p2 ; i++)
  {
    buf[j] = str[p1 + i];
    j++;
  }
  buf[j] = 0x00;
  return p2 - p1 - 1;           // 変換した文字数を返す
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// GPSデータから時分秒と使用中のGPS数を抽出して
// useGPS,nowTime にセットする。
// 対象データ:$GPGGA
// $GPGGA,001251.000,3445.9102,N,13527.9952,E,1,13,0.72,36.5,M,34.2,M,,*50<CR><LF>
// 時分秒.000, 北緯, 東経,「GPSのクオリティ0=No,1=OK,2=DGPS」, 使用衛星数, ]ほか
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

int8_t extractGPGGA(char *str) {
  int8_t ci = 0;
  char fn[2] = "0";
  char tmp[16];
  char tmps[16];

  // 使用GPS数を抽出
  if (cmmSTRcmm(tmp, str, 6) > 0) {
    switch (tmp[0]) {
      case '2':         // 0:受信不能 1:単独測位 2:DGPS
        fn[0]++;
      case '1':
        fn[0]++;
        strcpy(useGPS, fn);
        ci = cmmSTRcmm(tmp, str, 7);

        if (ci == 0) {
          strcat(useGPS, ",00");        // ゼロ文字 "00"表記
        }
        if (ci == 1) {
          strcat(useGPS, ",0");                    // 1文字 先頭に"0”を付加
          strncat(useGPS, tmp, 1);
        }
        if (ci >= 2) {
          strcat(useGPS, ",");
          strncat(useGPS, tmp, 2);    // 先頭二桁のみ抽出
        }
        break;
      default:
        gpsLoss++;
        strcpy(useGPS, "0,00");        // ゼロ文字 "00"表記
        break;
    }
  }
  return ci;                                     // 使用GPSの桁数を返す。0-1-2
}
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// GPSデータから時分秒と年月日を抽出する。
// useGPS,nowTime nowDateにセットする。    使わない予定 ==> GPZDA
// 対象データ:$GNRMC
// $GNRMC,001252.000,A,3445.9100,N,13527.9957,E,0.11,275.89,040120,,,A*7C<CR><LF>
// 時分秒.000,ステータスA有効:V無効,緯度,経度,速度,方向,日月年,地磁気,,モード*チェックSUM
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

int8_t extractGNRMC(char *str) {
  int8_t jhh, jdd, jmm, cdd, cmm, leapY;
  int16_t jyy;
  char fn[2] = "0";
  char tmp[16];
  char tmps[16];

  cdd = 0;

  // 時分秒を抽出
  if (cmmSTRcmm(tmp, str, 1) >= 10) {  //  HHMMDD.000 十文字以上なら
    mid(tmps, tmp, 0, 2);    // 2桁時
    jhh = atoi(tmps);        // UTC時間を数字に
    jhh = jhh + 9;                  // UTC +9H = JST
    if (jhh >= 24) {
      jhh = jhh - 24;  // 24H以上なら24Hを引く
      cdd = 1;
    }
    sprintf(tmps, "%02d", jhh);     // 2文字に変換
    strcpy(nowTime, tmps);          // JST出力
    strcat(nowTime, ":");
    mid(tmps, tmp, 2, 2);    // 2桁分
    strcat(nowTime, tmps);
    strcat(nowTime, ":");
    mid(tmps, tmp, 4, 2);    // 2桁秒
    strcat(nowTime, tmps);
    strcpy(tmpsec, tmps);     // 秒を記憶する■■
  } else {
    return 0;
  }

  if (cmmSTRcmm(tmp, str, 9) == 6) {  //  DDMMYY 6文字なら
    mid(tmps, tmp, 4, 2);   // 2桁・年
    jyy = atoi(tmps);       // UTC年を数字に
    jyy = jyy + 2000;
    //////////// 閏年の判定スタート
    if (jyy % 4 == 0) {
      if (jyy % 100 == 0) {
        if (jyy % 400 == 0) {
          leapY = 1;
        } else {
          leapY = 0;
        }
      } else {
        leapY = 1;
      }
    } else {
      leapY = 0;
    }
    //////////// 閏年の判定エンド
    mid(tmps, tmp, 0, 2);   // 2桁・日
    jdd = atoi(tmps);       // UTC日を数字に
    jdd = jdd + cdd;        // 24Hのオーバーフロー加算
    mid(tmps, tmp, 2, 2);   // 2桁・月
    jmm = atoi(tmps);       // UTC日を数字に
    //////////// 月の最終日の判定
    switch (jmm) {
      case 4:
      case 6:
      case 9:
      case 11:
        if (jdd > 30) {
          jdd = 1;
          cmm = 1;
        } else {
          cmm = 0;
        }
        break;
      case 2:
        if (leapY == 0) {
          if (jdd > 28) {
            jdd = 1;
            cmm = 1;
          } else {
            cmm = 0;
          }
        } else {
          if (jdd > 29) {
            jdd = 1;
            cmm = 1;
          } else {
            cmm = 0;
          }
        }
        break;
      default:            // 1 3 5 7 8 10 12
        if (jdd > 31) {
          jdd = 1;
          cmm = 1;
        } else {
          cmm = 0;
        }
        break;
    }
    //////////// 月の最終日の判定

    jmm = jmm + cmm;
    if (jmm > 12) {
      jmm = 1;
      jyy++;              // 年を進める
    }
    sprintf(tmps, "%04d", jyy);     // 4文字
    strcpy(nowDate, tmps);          // JST出力
    strcat(nowDate, "/");
    sprintf(tmps, "%02d", jmm);     // 2文字
    strcat(nowDate, tmps);          // JST出力
    strcat(nowDate, "/");
    sprintf(tmps, "%02d", jdd);     // 2文字
    strcat(nowDate, tmps);          // JST出力
    return 1;                      // 正常(時分秒と年月日の両方)抽出時は、1を返す。
  } else {
    return 0;
  }
}
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// GPSデータから時分秒と年月日を抽出する。
// useGPS,nowTime nowDateにセットする。
// 対象データ:$GPZDA
// $GPZDA,001251.000,04,01,2020,,*54<CR><LF>
// 協定世界時(UTC)時分秒.000, DD,MM,YYYY,,*チェックSUM
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

int8_t extractGPZDA(char *str) {
  int8_t jhh, jdd, jmm, cdd, leapY;
  int16_t jyy;
  char fn[2] = "0";
  char tmp[16];
  char tmps[16];

  cdd = 0;

  // 時分秒を抽出
  if (cmmSTRcmm(tmp, str, 1) >= 10) {  //  HHMMDD.000 十文字以上なら
    mid(tmps, tmp, 0, 2);    // 2桁時
    jhh = atoi(tmps);        // UTC時間を数字に
    jhh = jhh + 9;                  // UTC +9H = JST
    if (jhh >= 24) {
      jhh = jhh - 24;  // 24H以上なら24Hを引く
      cdd = 1;
    }
    sprintf(tmps, "%02d", jhh);     // 2文字に変換
    strcpy(nowTime, tmps);          // JST出力
    strcat(nowTime, ":");
    mid(tmps, tmp, 2, 2);    // 2桁分
    strcat(nowTime, tmps);
    strcat(nowTime, ":");
    mid(tmps, tmp, 4, 2);    // 2桁秒
    strcat(nowTime, tmps);
    strcpy(tmpsec, tmps);     // 秒を記憶する■■
  } else {
    return 0;
  }

  if (cmmSTRcmm(tmp, str, 4) == 4) {  //  DDMMYY 6文字なら
    jyy = atoi(tmp);       // UTC年を数字に

    //////////// 閏年の判定スタート
    if (jyy % 4 == 0) {
      if (jyy % 100 == 0) {
        if (jyy % 400 == 0) {
          leapY = 1;
        } else {
          leapY = 0;
        }
      } else {
        leapY = 1;
      }
    } else {
      leapY = 0;
    }
    //////////// 閏年の判定エンド

    if (cmmSTRcmm(tmp, str, 2) == 2) {   // 2桁・日
      jdd = atoi(tmp);       // UTC日を数字に
      jdd = jdd + cdd;        // 24Hのオーバーフロー加算
    } else {
      return 0;
    }
    if (cmmSTRcmm(tmp, str, 3) == 2) {   // 2桁・月
      jmm = atoi(tmp);       // UTC月を数字に

    } else {
      return 0;
    }

    //////////// 月の最終日の判定
    switch (jmm) {
      case 4:
      case 6:
      case 9:
      case 11:
        if (jdd > 30) {
          jdd = 1;
          jmm++;
        }
        break;
      case 2:
        if (leapY == 0) {     // 閏年ではない
          if (jdd > 28) {
            jdd = 1;
            jmm++;
          }
        } else {              // 閏年である
          if (jdd > 29) {
            jdd = 1;
            jmm++;
          }
        }
        break;
      default:            // 1 3 5 7 8 10 12
        if (jdd > 31) {
          jdd = 1;
          jmm++;
        }
        break;
    }
    //////////// 最終月の判定

    if (jmm > 12) {
      jmm = 1;
      jyy++;              // 年を進める
    }
    sprintf(tmps, "%04d", jyy);     // 4文字
    strcpy(nowDate, tmps);          // JST出力
    strcat(nowDate, "/");
    sprintf(tmps, "%02d", jmm);     // 2文字
    strcat(nowDate, tmps);          // JST出力
    strcat(nowDate, "/");
    sprintf(tmps, "%02d", jdd);     // 2文字
    strcat(nowDate, tmps);          // JST出力
    return 1;                      // 正常(時分秒と年月日の両方)抽出時は、1を返す。
  } else {
    return 0;
  }
}
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// 4つのキーによるパラメータ設定・変更操作
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

// ◆◆◆◆◆F0キーによる設定等の処理選択◆◆◆◆◆
uint8_t jobSelect(uint8_t def)        // F0キーにより、設定等の処理選択
{
  int8_t i, j, p, xl;
  int8_t stateRet = 0;
  const char titlMsg[] = "Config MENU";
  const char* selMsg[] = {  "0.UART bps Set",
                            "1.SEND to HOST",
                            "2.First GATE time"
                         };   // このアイテム(item)の有効数-1(行数-1)を次に記述。
  const uint8_t item = 2;
  uint8_t tmpprm;
  j = def;
  p = 0;

  while (1)
  {
    u8g2.clearBuffer();        // OLEDクリヤー

    u8g2.setDrawColor(1);         // 反転用の処理のため
    u8g2.drawBox(0, 2, 128, 12);  // Box描画
    u8g2.setDrawColor(2);         //  color 2 set

    Text_setCursor(0, 0);       // 桁・行
    u8g2.print(titlMsg);       // タイトルの行を表示

    for (i = 0; i <= item; i++)
    {
      Text_setCursor(2, i + 1);   // 桁・行
      u8g2.print(selMsg[i]);
    }

    xl = (j + 2) * 12;                // マーカー位置指定
    u8g2.drawGlyph(5, xl , 0x25b6);   //   0x21d2⇒  0x25b6?

    FuncEnable = B00001111;
    func_print(0, "RETN");
    func_print(1, " UP ");
    func_print(2, "DOWN");
    func_print(3, "ENTR");
    u8g2.sendBuffer();          // OLEDに送信(描画する)


    switch (buttonSW()) {
      case 10:
        stateRet = 1;
        break;
      case 11:
        //     case 21:
        j--;
        if (j < 0) j = item;
        break;
      case 12:
        //     case 22:
        j++;
        if (j > item) j = 0;
        break;
      case 13:
        //     case 23:
        switch (j) {
          case 0:                     // UART BPS set
            tmpprm = bpsPRM;
            bpsPRM = confPRMset(j, tmpprm);
            switch (bpsPRM) {    // 以前の状態に関係なく速度切換を実行する。(2020/01/28)
              case 1:
                Serial1.println(bps192set);      // <cr><lf>を付けて速度をGPSに指示
                delay(50);
                Serial1.println(bps192set);      // <cr><lf>を付けて速度をGPSに指示(2回目)
                delay(50);
                Serial1.end();                    // シリアルを一度止める
                delay(50);
                Serial1.begin(19200);             // Serial1(PA9 PA10) オープン
                break;
              case 2:
                Serial1.println(bps384set);      // <cr><lf>を付けて速度をGPSに指示
                delay(50);
                Serial1.println(bps384set);      // <cr><lf>を付けて速度をGPSに指示(2回目)
                delay(50);
                Serial1.end();                    // シリアルを一度止める
                delay(50);
                Serial1.begin(38400);             // Serial1(PA9 PA10) オープン
                break;
              case 0:
              default:
                Serial1.println(bps96set);       // <cr><lf>を付けて速度をGPSに指示
                delay(50);
                Serial1.println(bps96set);       // <cr><lf>を付けて速度をGPSに指示(2回目)
                delay(50);
                Serial1.end();                    // シリアルを一度止める
                delay(50);
                Serial1.begin(9600);              // Serial1(PA9 PA10) オープン
                break;
            }

            if (bpsPRM != tmpprm) {        // 書き替えが有った
              write(0, bpsPRM);            // ここにROM書込み
            }
            break;
          case 1:                     // SEND to HOST
            tmpprm = hostPRM;
            hostPRM = confPRMset(j, tmpprm);          // 新しい方向をセット

            if (hostPRM != tmpprm) {        // 書き替えが有った
              write(1, hostPRM);           // ここにROM書込み
            }
            break;
          case 2:                     // GATE time
            tmpprm = gatePRM;
            gatePRM = confPRMset(j, tmpprm);

            if (gatePRM != tmpprm) {       // 書き替えが有った
              write(2, gatePRM);           // ここにROM書込み
            }
            break;
        }   // switch (j) end
        break;

    }   // switch (buttonSW()) end

    if (stateRet == 1) {     // jobSelect(uint8_t)を抜ける
      return j;
    }
  }  // while (1) end
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// ◆◆◆◆◆M ボタンの状態を返す◆◆
// 何れかのボタンが押されて放された(F0=10,F1=11,F2=12,F3=13)を待つ。
// 旧設定
// F0 < 80, 300 < F1 > 430, 580 < F2 < 790, 800 < F3 < 1100
// New   (2020/04/16)
// F0 < 109, 110 < F1 > 548, 549 < F2 < 823, 823 < F3 < 1200,
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
int8_t buttonSW() {
  int8_t retVal = 0;      // = 0 の記述は必須。
  int8_t repeat10 = 0;
  int8_t count = 0;
  uint16_t asw;

  // while (1) {
  asw = analogRead(SWa_In);
  if (asw < 111) {  // F0ボタンなら return 10
    while (asw < 111) { // F0ボタンを放すまで待つ
      delay(50);
      asw = analogRead(SWa_In);
      count++;
      if (count >= 20) {        // 1000mS 以上押されたら10プラス
        repeat10 = 10;
      }
    }
    delay(50);
    return 10 + repeat10;
  }

  if ((asw > 110) && (asw < 549)) { // F1ボタンなら Plus

    while ((asw > 110) && (asw < 549)) { // F1ボタンを放すまで待つ
      delay(50);
      asw = analogRead(SWa_In);
      count++;
      if (count >= 20) {        // 1000mS 以上押されたら10プラス
        repeat10 = 10;
      }
    }
    delay(50);
    return 11 + repeat10;
  }

  if ((asw > 548) && (asw < 824)) {   // F2ボタンなら Minus
    while ((asw > 548) && (asw < 824)) { // F2ボタンを放すまで待つ
      delay(50);
      asw = analogRead(SWa_In);
      count++;
      if (count >= 20) {        // 1000mS 以上押されたら10プラス
        repeat10 = 10;
      }
    }
    delay(50);
    return 12 + repeat10;
  }

  if ((asw > 823) && (asw < 1200)) {  // F3ボタンなら return 13
    while ((asw > 823) && (asw < 1200)) { // F3ボタンを放すまで待つ
      delay(50);
      asw = analogRead(SWa_In);
      count++;
      if (count >= 20) {        // 1000mS 以上押されたら10プラス
        repeat10 = 10;
      }
    }
    delay(50);
    return 13 + repeat10;
  }

  // } // while end
}
///////////////////////////////////////////////////////////////////////
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// 処理選択画面を展開し、選択された値を返す。
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
uint8_t confPRMset(uint8_t jobNo, uint8_t prm)
{
  int8_t i, j, p, xl;
  int8_t stateRet = 0;
  const char* titlMsg[] = {
    "0.UART BPS Set",
    "1.SEND to HOST",
    "2.First GATE time"
  };
  const char selMsg[3][3][22] = {
    { "9,600 bps(default)",   // jobNo = 0
      "19,200 bps(32.6%)",
      "38,400 bps(16.3%)"
    },
    { "GPS data(default)",    // jobNo = 1
      "LOG data",
      "PIC"
    },
    { "1 sec(default)",        // jobNo = 2
      "10 sec",
      "100 sec"
    }
  };
  int8_t item;
  uint8_t tmpprm = 0;

  switch (jobNo) {    // 選択する項目(selMsg)の数を処理No毎にセットする。012
    case 0:
      item = 2;
      break;
    case 1:
      item = 2;
      break;
    case 2:
      item = 2;
      break;
  };
  j = prm;
  p = 0;

  while (1)
  {
    u8g2.clearBuffer();        // OLEDクリヤー

    u8g2.setDrawColor(1);         // 反転用の処理のため
    u8g2.drawBox(0, 2, 128, 12);  // Box描画
    u8g2.setDrawColor(2);         //  color 2 set
    Text_setCursor(0, 0);       // 桁・行
    u8g2.print(titlMsg[jobNo]);       // タイトルの行を表示

    for (i = 0; i <= item; i++)
    {
      Text_setCursor(2, i + 1);   // 桁・行
      u8g2.print(selMsg[jobNo][i]);
    }

    xl = (j + 2) * 12;                // マーカー位置指定
    u8g2.drawGlyph(5, xl , 0x25b6);   //   0x21d2⇒  0x25b6?

    func_print(0, "RETN");
    func_print(1, " UP ");
    func_print(2, "Down");
    func_print(3, "ENTR");
    u8g2.sendBuffer();          // OLEDに送信(描画する)

    switch (buttonSW()) {
      case 10:
        tmpprm = prm;            // 初期値で戻る
        stateRet = 1;
        break;
      case 11:
      case 21:
        j--;
        if (j < 0) j = item;
        break;
      case 12:
      case 22:
        j++;
        if (j > item) j = 0;
        break;
      case 13:
        switch (j) {
          case 0:
          case 1:
          case 2:
            tmpprm = j;
            stateRet = 1;       // 終了処理をセット
            break;
        }   // switch (j) end
        break;

    }   // switch (buttonSW()) end

    if (stateRet == 1) {     // jobSelect(int8_t)を抜ける
      return tmpprm;
    }
  }  // while (1) end
}
///////////////////////////////////////////////////////////////////////
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// EEPROM 24C256 等の外部EEPROMのI2C読み書き関数。
// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
// 1バイト書込
uint8_t write(uint16_t address, uint8_t data)  {
  uint8_t rc;
  Wire.beginTransmission(DEVICE_ADDRESS);
  Wire.write((byte)(address >> 8));   // アドレス上位
  Wire.write((byte)(address & 0xFF)); // アドレス下位
  Wire.write(data);
  rc = Wire.endTransmission();
  delay(6);
  return rc;
}

// 1バイト読込
uint8_t read(uint16_t address)  {
  Wire.beginTransmission(DEVICE_ADDRESS);
  Wire.write((byte)(address >> 8));   // アドレス上位
  Wire.write((byte)(address & 0xFF)); // アドレス下位
  Wire.endTransmission();
  Wire.requestFrom(DEVICE_ADDRESS, 1);
  return  Wire.read();
}

// ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

      



5.ケース加工

 完結編としてまとめるために、ケース加工について触れておきます。
私はケース加工が一番苦手と感じており、加工の粗を隠すために化粧パネルの類を今回のような一品物にも用意してきました。
ケース:タカチ電機工業 CU-N型メタルケース CU-13N


OLEDとSW部の加工(OLEDのギリギリの大きさまでくり抜きます)
OLEDの加工穴がパネルの大半を占めています。

OLEDの取付とSW部

測定入力は同軸を使わず、少しツイストした線材で済ませました。

後面パネル(USB 2.0 Type-C)

後面にもパネルを両面テープで貼付け。




6.OCXO校正の実際

 ここまで来てやっと本題に入れることに成りますが、一つだけあるOCXOを実際に校正してみることにしました。
このOCXOは今までの計測で約30mHzほどプラスに発振しており若干の変動も有ることを確認していました。
これ以上下手に調整してしまうと、元に戻せない可能性も有りますが、素人校正にチャレンジしてみました。


OCXO調整ポイントと調整手順

1.1時間以上通電後に、GATE1000秒で測定時に10,000,000.031Hzを示したとき、上記写真の多回転ボリュームの中点が4.95Vを確認。
2.半固定ツマミを回して中点を:5.00Vに調整し、その時の周波数を確認(10,000,000.066Hz)
3.エクセル(私は、フリーのLibreOffice)等で、2点間の電圧対周波数のグラフを作成し、目盛10,000,000.000Hz付近の電圧を設定(≒4.91V)

エクセルのグラフ

(1)(2)(3)は、前写真の1.〜3.に相当
周波数は、10MHzに対する相対値を使用しています。(単位:mHz)

校正後の参考写真1(GATE=1万秒を2回経過した直後)

約5mHzプラス方向にズレが有るようです。
手持ちのデジタルテスターが、少数点以下2桁しか表示しないため、これが限界です。
1万秒(2.78時間)を2回測ることが大変でした。


参考写真2(その後、画面を切り替えてアベレージを確認)

1回目はMin側の値、2回目はMax側の値でした。
このOCXOの特性か、数mHzの変動は常にあるようです。

校正後のLOG取得状況

GPSクオリティ部分であるAの後の計測で、-0.006Hz変化しています。
1?に変化したということは、『「DGPS:相対測位」から「単独測位」に変わった(基地局を見失った)タイミングでPPS信号が揺らいだのかな??』というぐらいの想像を働かせるしか有りません。
いずれにしても完全・完璧な計測は無理で、長時間の計測時はLOG機能を使って天空(GPS)の変化に思いを馳せる必要が有りそうです。

更に微調整を実施  (2020/05/01)

少しずつ半固定ツマミを回して、更に半分ぐらいまでの誤差に追い込みましたが、何回も行ったり来たりを繰り返しました。
これ以上は触らないようにします。
OCXOは当面これ以上に利用する用途は無く、『10MHzに対して、0.003Hz(0.0003ppm=0.3ppb)程度の誤差に収まった』と自己満足に浸るだけとなりました。


7.BOM

 後で部品の手配先を見失うことが有るので、書きだしておきます。
チップの抵抗・コンデンサは、個別に揃えるのではなくパックになった品を購入し、不足分や良く使用する値は追加購入しています。



Reference Value 手配先 品番等 注単位
基本部分 C1,2,4,5,7,8,9,14,15,16 0.1u 10 Amazon等のパック品
0805(2012)
C13,17 10u 2
C11 1u 1
C10 470p 1
D1 LED_AK 1 秋月 I-11577 1
L1,2 2uH 2 秋月 P-11179 5
J1 Conn_Coaxial 1 秋月 C-13195 1
J4 USB_Type-C_AE-5077CR-16SMC2-BK-TR 1 秋月 C-14356 1
Q3 2SC1815 1 秋月 I-04268 10
Q2 2SC3355 1 秋月 I-09742 1
Q1 2SK241 1 AliEx

R21 10k 1 Amazon等のパック品
0805(2012)
R7,13,22 1k 3
R5 1M 1
R4 2.2k 1
R8,9 22k 2
R2,3,12,15,16,20,23 22R 7
R10 330R 1
R18,19 4.7k 2
R14,17 5.1k 2
R6 680R 1
R11 82R 1
U2 74HC00 1 秋月 I-10856 1
U1 74HC74 1 秋月 I-10879 1
U5 CAT24C256 1 AliEx

U3 PIC24FV32KA301 1 RS-Online 742-1170 1
U4 STM32F103(BP) 1 Amazon等の品
VR1 RV50k 1 秋月 P-03281 1
RTC用バッテリーBUP(必要時) BT1 Battery_Cell 1 秋月 P-01443 1
C12 0.1u 1 Amazon等の品
D2,3 SBD 2 秋月 I-07687 10
使用しない予定 R1 51R 1 Amazon等の品
C3 10u 1 Amazon等の品
C6 10u 1 Amazon等の品
F1 ジャンパーでショート 1


フロントパネル OLED 2.42" OLEDデジタルIIC I2C SPIシリアル12864 128X64 SSD1309 1 Amazon又はAliEx
1
SW1,2,3,4 SW_Push 4 秋月 P-09825 1
R1 10k 1 Amazon等の品
R2,3,4 1k 3 Amazon等の品
ピンソケット・ピンヘッダ・ケーブル・コネクタ等は省略しています。
2SK241と24C256の手配先をAliExpressとしていますが、国内でも入手できると思います。



9.その他気づいたこと

(1)その他






99.追記用

大幅な改定・追記用