// LM: This sketch computes and displays a gated oscillator's frequency, // based on data received from a binary counter TTL circuit and a GPS // (or other) pulse-per-second time duration reference. // // Si5351 references support an optional test clock generator. // 16x2 LCD displays the computed result (row 1) and raw data (row 2). // // V.2.2 DELIM separator // // V.2.3 Limited custom time base support (fixed calibration constant) // // V.3.0 Support non-GPS precision time base - E.g., 10 MHz OCXO // Assume LCD present - LCD conditional compile directives removed // // V.3.1 Runtime support for imprecise time base (i.e. approximately 10 MHz) // Adjustable calibration and frequency correction. // // Encapsulate button press to use a single rotary encoder pushbutton // for both measurement and calibration contexts. // // License: Creative Commons: https://creativecommons.org/licenses/by/3.0/us/ #define VERSION 3.1 #include #include #include // V 3.0 Store custom time base calibration Adafruit_SI5351 clockgen = Adafruit_SI5351(); #include // Frequency (result) display // V 2.* Reserve DIO 0, 1 for serial programming #define ROTARY_ENCODER_CLK 0 // V 3.0 Runtime calibration uses rotary encoder #define ROTARY_ENCODER_DT 1 // #define KBITS 2 // Interrupt pin for count of Hz / CYCLES_PER_COUNT #define Q9 3 // Duplicate of KBITS, but not involved in interrupt #define INFO_LED 13 // Reserved #define PULSE_STATE A0 // State of signal derived from PPS #define ENABLE_MEASUREMENT A1 // Gated with PPS derived signal #define PUSHBUTTON A2 // V 3.1 Was REQUEST_MEASUREMENT in previous versions #define STEP_CLOCK A3 // Version 2 // A4 and A5 are i2c #define MEASUREMENT_DURATION 4 // Number of seconds for which measurement is enabled (hardware) #define SERIAL_BAUD 9600 const char DELIM = ','; // Delimit frequency count const long TWOSEC = 2000; const long ONESEC = 1000; const long HALFSEC = 500; const long QTRSEC = 250; const long LONG_PRESS_MS = 1500; // V 3.0 Minimum time (ms) to classify as 'long press' const long JIFFY = 20; // Software debounce const int NDPINS = 10; int DIOpins[NDPINS] = {4, 5, 6, 7, 8, 9, 10, 11, 12, Q9}; const int CYCLES_PER_COUNT = 1024; // Presently the count from Q9 (74HC4040 pin 14) volatile unsigned long bCount = 0; // Count of blocks - See CYCLES_PER_COUNT definition above unsigned long remainder = 0; // Modulus vBase10 mod(CYCLES_PER_COUNT) as binary unsigned long vBase10 = 0; // Value after conversion to decimal const int ROWS = 2; // If 20x4 change ROWS to 4 const int COLS = 16; // If 20x4 change COLS to 20 LiquidCrystal_I2C lcd(0x27, COLS, ROWS); // Instantiate String s1 = ""; String s2 = ""; String sTmp; // V 3.0 // Implementation assumes custom time base frequency is in the vicinity of 10 MHz boolean custom_time_base_mode = false; // Support non-GPS timebase const unsigned long TIME_BASE = 39999958UL; // Example stable frequency in Hz × 4 const unsigned long TEN_MHZ = 40000000UL; // 4 × 10 MHz (Units are cycles per 4 seconds or 1/4 Hz) const int TB_LENGTH = 8; // For string representation of time base const unsigned long power_of_ten[TB_LENGTH] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000}; const byte VALIDITY_MARKER = 64; // Indicates that EEPROM contains calibrated time base unsigned long timeBase = TIME_BASE; // Variable subject to calibration adjustment char sTB[TB_LENGTH]; // Time base as string (character array) int editPos = TB_LENGTH - 1; // For calibration - current digit position const char EDIT_SYMBOL = '^'; // Mark digit being edited int last_clk_state; // Rotary encoder clock value corresponding to no change boolean indent = false; // Kludge - Rotary encoder changes once between indents const byte SHORT_PRESS = 1; // V 3.1 Encapsulate pushbutton press detection const byte LONG_PRESS = 2; void setup() { #ifdef SERIAL_BAUD Serial.begin(SERIAL_BAUD); // If needed for debugging Serial.println("Freq Counter " + String(VERSION)); Serial.println("www.lloydm.net"); Serial.println(); Serial.println("Serial interface enabled!"); #endif pinMode(ROTARY_ENCODER_CLK, INPUT); // V 3.0 custom time base calibration pinMode(ROTARY_ENCODER_DT, INPUT); last_clk_state = digitalRead(ROTARY_ENCODER_CLK); pinMode(PULSE_STATE, INPUT); pinMode(ENABLE_MEASUREMENT, OUTPUT); pinMode(PUSHBUTTON, INPUT); // 'Request measurement' or 'calibrate' pinMode(INFO_LED, OUTPUT); digitalWrite(INFO_LED, LOW); // HIGH-enable digitalWrite(ENABLE_MEASUREMENT, LOW); // HIGH-enable for (int j = 0; j < NDPINS; j++) { // Q0 - Q8 not used in V2.1 pinMode(DIOpins[j], INPUT); // From counter outputs (remainder bits) } pinMode(STEP_CLOCK, OUTPUT); digitalWrite(STEP_CLOCK, HIGH); // LOW-HIGH to step (NAND) if (digitalRead(PUSHBUTTON) == HIGH) { // Held down during startup or reset -> custom_time_base_mode = true; getTimeBase(); // Default or stored EEPROM value } boolean clockGenOK = true; if (clockgen.begin() != ERROR_NONE) { infoCode(1); clockGenOK = false; delay(ONESEC); // Test clock generator is not required } lcd.init(); // Hard-coded splash for now if (custom_time_base_mode) { displayLCD("Custom Time Base"," Release button"); while (digitalRead(PUSHBUTTON) == HIGH) // Wait for release ; delay(HALFSEC); } splash(); if (clockGenOK) { #ifdef TEST // Configure test frequency init_5MHz(); #else // init_6p25MHz(); init_40MHz(); #endif clockgen.enableOutputs(true); // Enable clock generator } // Count ~KHz (cycles / CYCLES_PER_COUNT) attachInterrupt(digitalPinToInterrupt(KBITS), isrCount, FALLING); //Next commented-out for off-board testing. Uncomment for production use clearBinaryLEDs(); // Placement is independent of interrupt status } void loop() { byte bp = buttonPress(); if (custom_time_base_mode && (bp == LONG_PRESS)) calibrate(); else if (bp > 0) { // Ignore PB duration in GPS mode scheduleMeasurement(); infoCode(2); // Allow time to read binary remainder processAndDisplayMeasurementResult(); } // Loop until another button press } byte buttonPress() { if (digitalRead(PUSHBUTTON) == HIGH) { long start_press = millis(); debouncePB(); if (custom_time_base_mode && (millis() - start_press > LONG_PRESS_MS)) return LONG_PRESS; else return SHORT_PRESS; } return 0; } void debouncePB() { delay(JIFFY); while (digitalRead(PUSHBUTTON) == HIGH) ; // Debounce } void init_5MHz() { // Set Si5351 channel 0 output to 5 MHz // Multiply 25 MHz by 15 = 375 MHz. clockgen.setupPLLInt(SI5351_PLL_A, 15); // Divide by 75 = 5 MHz. clockgen.setupMultisynthInt(0, SI5351_PLL_A, 75); } void init_6p25MHz() { // Set Si5351 channel 0 output to 6.25 MHz // Multiply 25 MHz by 16 = 400 MHz. clockgen.setupPLLInt(SI5351_PLL_A, 16); // Divide by 64 = 6.25 MHz. clockgen.setupMultisynthInt(0, SI5351_PLL_A, 64); } void init_40MHz() { // Set Si5351 channel 0 output to 40 MHz // Multiply 25 MHz by 16 = 400 MHz. clockgen.setupPLLInt(SI5351_PLL_A, 16); // Divide by 10 = 40 MHz. clockgen.setupMultisynthInt(0, SI5351_PLL_A, 10); } void scheduleMeasurement() { bCount = 0; displayLCD(" Scheduling "," measurement "); while (digitalRead(PULSE_STATE) == HIGH) delay(random(QTRSEC)); digitalWrite(ENABLE_MEASUREMENT, HIGH); displayLCD(" Measurement "," enabled... "); while (digitalRead(PULSE_STATE) == LOW) delay(random(HALFSEC)); // Measurement in progress - ENABLE_MEASUREMENT and PULSE_STATE are HIGH while (digitalRead(PULSE_STATE) == HIGH) delay(random(HALFSEC)); // Measurement complete - Remove from schedule digitalWrite(ENABLE_MEASUREMENT, LOW); return; } void computeTotalCount() { // V2.1 replacement for V.1 processData(); vBase10 = bCount * CYCLES_PER_COUNT; remainder = 1024 - remComplement(); if (remainder > 0) // Kludge correction for clocking once at end of remainder -= 1; // 4-sec measurement interval (- 1/4 Hz) vBase10 += remainder; if (custom_time_base_mode) { vBase10 = adjustCount(vBase10); } } void freq() { // Format s1 as frequency in Hz unsigned long jFreq = vBase10 / MEASUREMENT_DURATION; s1 = String(jFreq); // Integer part int len = s1.length(); // V 2.2 Delimit millions, thousands if (len > 6) { s1 = s1.substring(0, len-6) + DELIM + s1.substring(len-6); len++; } if (len > 3) { s1 = s1.substring(0, len-3) + DELIM + s1.substring(len-3); } if (MEASUREMENT_DURATION == 4) { // To do: Generalize unsigned long jMod = vBase10 % MEASUREMENT_DURATION; if (jMod == 1) s1.concat(".25"); else if (jMod == 2) s1.concat(".5"); else if (jMod == 3) s1.concat(".75"); } s1.concat(" Hz"); } void isrCount() { bCount++; } void myClear() { lcd.clear(); lcd.setCursor(0,0); } void displayLCD(String line1, String line2) { int row = ROWS/2 - 1; // Same as if..else myClear(); lcd.setCursor(0, row); lcd.print(line1); lcd.setCursor(0, row+1); lcd.print(line2); lcd.backlight(); } void splash() { displayLCD("Freq Counter " + String(VERSION)," www.lloydm.net "); } // Development and debug utilities + V2 void processAndDisplayMeasurementResult() { detachInterrupt(digitalPinToInterrupt(KBITS)); computeTotalCount(); // V2 method for remainder freq(); // Compute frequency and set Line 1 of display addData(); // Debug data, etc. Line 2 displayLCD(s1, s2); attachInterrupt(digitalPinToInterrupt(KBITS), isrCount, FALLING); } int remComplement() { // Version 2 // Hans G0UPL algorithm // Interrupt disabled by caller - bCount will not increment int jCount = 0; while (digitalRead(Q9) == LOW) { jCount++; stepClock(); } while (digitalRead(Q9) == HIGH) { jCount++; stepClock(); } return jCount; } void clearBinaryLEDs() { // Version 2 // Same as computing complement of remainder, but witout compute detachInterrupt(digitalPinToInterrupt(KBITS)); while (digitalRead(Q9) == LOW) stepClock(); while (digitalRead(Q9) == HIGH) stepClock(); attachInterrupt(digitalPinToInterrupt(KBITS), isrCount, FALLING); } void addData() { // V 2.1 line 2 s2 = ""; s2.concat(String(bCount)); s2.concat("*1024+"); s2.concat(String(remainder)); while (s2.length() < COLS) s2.concat(" "); } void stepClock() { // Version 2 digitalWrite(STEP_CLOCK, LOW); delay(1); digitalWrite(STEP_CLOCK, HIGH); delay(1); } void infoCode(int iCode) { for (int i = 0; i < iCode; i++) { onInfoLED(); delay(500); offInfoLED(); delay(500); } } void onInfoLED() { digitalWrite(INFO_LED, HIGH); } void offInfoLED() { digitalWrite(INFO_LED, LOW); } // V 2.3 forward unsigned long adjustCount(unsigned long four_sec_count) { unsigned long long product = (long long) four_sec_count * (long long) timeBase; return (unsigned long) (product / (long long) TEN_MHZ); } // V.3.0 custom time base runtime calibration support // Generic EEPROM utilities from previous sketches void writeEEPROM(String s) { int sLen = s.length(); if (sLen > EEPROM.length()) return; for (int i=0; i (EEPROM.length() - jStart)) return; for (int i=0; i EEPROM.length()) sLen = EEPROM.length(); sTmp = ""; for (int i=0; i (EEPROM.length() - jStart)) sLen = EEPROM.length() - jStart; sTmp = ""; for (int i=0; i EEPROM.length()) len = EEPROM.length(); for (int i=0; i (EEPROM.length() - jStart)) len = EEPROM.length() - jStart; for (int i=0; i 57) sTB[editPos] = '0'; displaySelectedDigit(); } void decrementSelectedDigit() { if (!isIndent()) return; sTB[editPos]--; if (sTB[editPos] < 48) sTB[editPos] = '9'; displaySelectedDigit(); } void saveAndExit() { stb2tb(); sTmp = "@"; sTmp.concat(sTB); writeEEPROM(sTmp, 0); String s = sTmp.substring(1,TB_LENGTH+1); for (int i=TB_LENGTH; i