////////////////////////////////////////////////////////////////////////////// // A little totally useless gadget: // // // // A frequency meter for the BC312/BC342 WW-II Signal Corps receivers // // with automatic (sort of) IF value addition/subtraction. // // // // Copyright (C) 2026 Giuseppe Perotti, I1EPJ, i1epj@aricasale.it // // // // Version 1.0 // ////////////////////////////////////////////////////////////////////////////// // // // This program is free software: you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation, either version 3 of the License, or // // (at your option) any later version. // // // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see <http://www.gnu.org/licenses/>. // // // // NOTE. The FreqCount library use the microcontroller clock as time base, // // so expect fairly large errors. The resonator used is specified with a // // frequency error of 0.5% and a temperature drift of 0.3%. The frequency // // error can be corrected in software (see FCORR parameter below), the // // drift however can't. // ////////////////////////////////////////////////////////////////////////////// // // // This program uses the FreqCount library. Author copyright notice follows.// // // ////////////////////////////////////////////////////////////////////////////// /* FreqCount Library, for measuring frequencies * http://www.pjrc.com/teensy/td_libs_FreqCount.html * Copyright (c) 2014 PJRC.COM, LLC - Paul Stoffregen <paul@pjrc.com> * * Version 1.1 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <FreqCount.h> #include <EEPROM.h> #include <stdlib.h> // Configuration for BC312/BC342. // All frequency values are in 100 Hz units e.g. 1500 kHz = 15000) #define DEF_SWITCH_FREQ 80000 // frequency at which LO switches from above // to under the received frequency (8000 kHz) #define DEF_IF_VALUE 4700 // IF frequency value (470 kHz) #define DEF_MIN_FREQ 15000 // Minimum value of RX frequency (1500 kHz) #define DEF_MAX_FREQ 180000 // Maximum value of RX frequency (18000 kHz) #define DEF_FCORR 0 // frequency correction factor in ppm to // compensate for resonator frequency error // To be determined sperimentally. // On my Arduino UNO R3 it is -679 ppm #define DEF_BAND_CH_THR 80000 // Threshold past which a band change is // detected. // Configuration end --------------------------------------------- // Compilation options #define SERIAL_CONF // Enable USB/serial configuration commands #define HELP_MENU // Enable help menu command (?) #undef DEBUG_DISPLAY // Do not include the D command (display debug) // // Hic sunt leones // #define DELAY delay(2) // If needed can be reduced to 1 #ifdef SERIAL_CONF #define MAGIC 0xAA // Magic value for EEPROM saved conf data #define MAGICADDRESS 0xAA // Magic address #define COPYR \ "\nBC312/BC342 frequency counter v1.0\n\ Copyright (C) 2026 I1EPJ, i1epj@aricasale.it\n\ Distributed under the GNU GPL v3 or later\n" #endif #ifdef DEBUG_DISPLAY char tdi[6]; #endif // Segments outputs #define SEG_A 7 #define SEG_B 8 #define SEG_C 9 #define SEG_D 10 #define SEG_E 11 #define SEG_F 12 #define SEG_G 13 // Decimal point #define DP A2 // Digits outputs #define DISP_1 A0 // Least significant #define DISP_2 A1 #define DISP_3 2 #define DISP_4 3 #define DISP_5 4 #define DISP_6 6 // // Function prototypes // void displayStringTimed(const char* str, unsigned int displayTime); char bcdASCIIto7seg(char digit); void putDigit(int digitValue, int num); void selectDigit(int num); void DisplayOFF(void); unsigned long count[16]; unsigned long SWF, IF, FMIN, FMAX, BCHTH; unsigned long countswl, countswh, minLOcount, maxLOcount, bandchthr; long FCORR, fcorr; long freq, offsetIF; int digit, i; char DisplayImage[6]; // Error messages char* ov = "Ovrlap"; char* ns = "No sig"; char* fl = "Frq LO"; char* fh = "Frq HI"; #ifdef HELP_MENU #define HELP \ "Available commands:\n\ ? This help text\n\ L Display the copyright and license terms.\n\ M Display/set the minimum frequency.\n\ H Display/set the highest frequency.\n\ I Display/set the IF frequency value.\n\ C Display/set the frequency correction factor in ppm.\n\ S Display/set the frequency at which OL switches\b\ from above to under RX frequency.\n\ B Display/set the band change detection threshold.\n\ W Save configuration parameters in EEPROM.\n\ T Perform a display test.\n\n" #endif #ifdef SERIAL_CONF int nc; char s[20]; #endif // Initializations void setup() { pinMode(A0, OUTPUT); pinMode(A1, OUTPUT); pinMode(A2, OUTPUT); pinMode(2, OUTPUT); pinMode(3, OUTPUT); pinMode(4, OUTPUT); // Pin 5 used by FreqCount pinMode(6, OUTPUT); pinMode(7, OUTPUT); pinMode(8, OUTPUT); pinMode(9, OUTPUT); pinMode(10, OUTPUT); pinMode(11, OUTPUT); pinMode(12, OUTPUT); pinMode(13, OUTPUT); // Blank display DisplayOFF(); // All digit unlit digitalWrite(DP, LOW); // And decimal point too // Start FreqCount library FreqCount.begin(160); // 160 ms gate time = frequency in 100 Hz units * 16 #ifdef SERIAL_CONF Serial.begin(115200); Serial.setTimeout(5000); if (EEPROM.read(MAGICADDRESS) == MAGIC) { // if MAGICADDRESS contains MAGICVALUE we have // saved configuration in EEPROM, read it EEPROM.get(0, SWF); EEPROM.get(4, IF); EEPROM.get(8, FMIN); EEPROM.get(12, FMAX); EEPROM.get(16, BCHTH); EEPROM.get(20, FCORR); } else { // init with default values SWF = DEF_SWITCH_FREQ; IF = DEF_IF_VALUE; FMIN = DEF_MIN_FREQ; FMAX = DEF_MAX_FREQ; FCORR = DEF_FCORR; bandchthr = DEF_BAND_CH_THR; } #endif // Init global variables offsetIF = 0; countswl = 4 * (SWF - IF); // we read for 160ms (16x) a frequency divided by 4 countswh = 4 * (SWF + IF); // ditto bandchthr = 4 * BCHTH; // Band change threshold count value (4xfreq) minLOcount = (4 * (FMIN + IF) * 99) / 100; // less 1% guard maxLOcount = (4 * (FMAX - IF) * 101) / 100; // plus 1% guard fcorr = 1000000 + FCORR; for (i=0; i<6; i++) DisplayImage[i] = ' '; // Display program info and license // Each string must be 6 characters long displayStringTimed("BC342 counter v1_0", 2000); displayStringTimed("[c] 2026 I1EPJ",2000); displayStringTimed("License GPL v3", 1000); displayStringTimed("ready ", 1000); digitalWrite(DP, HIGH); } // Set all displays off void DisplayOFF(void) { digitalWrite(DISP_1, LOW); digitalWrite(DISP_2, LOW); digitalWrite(DISP_3, LOW); digitalWrite(DISP_4, LOW); digitalWrite(DISP_5, LOW); digitalWrite(DISP_6, LOW); } // Converts a BCD or ASCII value to 7-segment binary value char bcdASCIIto7seg(char digit) { // 7-segment display segment names // ---A--- // | | // F B // | | // ---G--- // | | // E C // | | // ---D--- switch (digit) { // Digits as BCD or ASCII value // ABCDEFG case 0: case '0': return 0b01111110; case 1: case '1': return 0b00110000; case 2: case '2': return 0b01101101; case 3: case '3': return 0b01111001; case 4: case '4': return 0b00110011; case 5: case '5': return 0b01011011; case 6: case '6': return 0b01011111; case 7: case '7': return 0b01110000; case 8: case '8': return 0b01111111; case 9: case '9': return 0b01111011; // Other characters // ABCDEFG case ' ': return 0b00000000; case '-': return 0b00000001; case '_': return 0b00001000; case '=': return 0b00001001; case '"': return 0b00100010; case '^': return 0b01100011; // degrees really case '[': return 0b01001110; case ']': return 0b01111000; case '\'': return 0b00000010; case '?': return 0b01100101; case 'A': return 0b01110111; case 'a': return 0b01111101; case 'B': // one version only possible case 'b': return 0b00011111; case 'C': return 0b01001110; case 'c': return 0b00001101; case 'D': // one version only possible case 'd': return 0b00111101; case 'E': return 0b01001111; case 'e': return 0b01101111; case 'F': // one version only possible case 'f': return 0b01000111; case 'G': return 0b01011110; case 'g': return 0b01111011; case 'H': return 0b00110111; case 'h': return 0b00010111; case 'I': return 0b00000110; case 'i': return 0b00000100; case 'J': // one version only possible case 'j': return 0b00111100; // k is not displayable case 'L': return 0b00001110; case 'l': return 0b00000110; // m is not displayable case 'N': return 0b01110110; // better than norhing case 'n': return 0b00010101; // better than nothing case 'O': return 0b01111110; case 'o': return 0b00011101; case 'P': // one version only possible case 'p': return 0b01100111; case 'Q': // one version only possible case 'q': return 0b01110011; case 'R': // one version only possible case 'r': return 0b00000101; // better than nothing case 'S': // one version only possible case 's': return 0b01011011; case 'T': // one version only possible case 't': return 0b00001111; // better than nothing case 'U': return 0b00111110; case 'u': return 0b00011100; case 'v': return 0b00011100; // v is not displayable, use a u instead // w is not displayable // x is not displayable case 'Y': // one version only possible case 'y': return 0b00111011; // z is not displayable default: return 0b00000000; } } // Selects a digit (num is 0 to 5, 0 is is leftmost, i.e. most significant, digit) void selectDigit(int num) { DisplayOFF(); switch (num) { case 0: { digitalWrite(DISP_6, HIGH); break; } case 1: { digitalWrite(DISP_5, HIGH); break; } case 2: { digitalWrite(DISP_4, HIGH); break; } case 3: { digitalWrite(DISP_3, HIGH); break; } case 4: { digitalWrite(DISP_2, HIGH); break; } case 5: { digitalWrite(DISP_1, HIGH); break; } } DELAY; } // Set segment outputs to display digitValue // and activate required display (num is 0 to 5) void putDigit(char digitValue, int num) { char code7seg; // Convert BCD value or ASCII character to 7 segment code7seg = bcdASCIIto7seg(digitValue); // Put all digits off DisplayOFF(); // Activate/Deactivate segment outputs digitalWrite(SEG_A, (code7seg & 0b01000000) ? HIGH : LOW); digitalWrite(SEG_B, (code7seg & 0b00100000) ? HIGH : LOW); digitalWrite(SEG_C, (code7seg & 0b00010000) ? HIGH : LOW); digitalWrite(SEG_D, (code7seg & 0b00001000) ? HIGH : LOW); digitalWrite(SEG_E, (code7seg & 0b00000100) ? HIGH : LOW); digitalWrite(SEG_F, (code7seg & 0b00000010) ? HIGH : LOW); digitalWrite(SEG_G, (code7seg & 0b00000001) ? HIGH : LOW); // And then activate the required 7-segment display selectDigit(num); } // Display a string for a given time in milliseconds. // If the string is more that 6 characters long, scroll it // and take all the time needed to show it fully. // Used only at program start for copyright and license messsages // and for the T command. void displayStringTimed(const char* str, unsigned int displayTime) { int i,p,l; long m1, m2, et; m1 = millis(); l = strlen(str); do { if (l < 7) { for (i = 0; i < 6; i++) { putDigit(str[i], i); DELAY; } } else { // shift string in display m2 = millis(); for (p=0; p<l-5;) { for (i=0; i < 6; i++) { putDigit(str[i+p], i); DELAY; } if ((millis() - m2) > 300) { p++; m2 = millis(); } } } } while ((millis() - m1) < displayTime); } // Main loop. // Keep it not too heavy, since to avoid display flickering it must // execute at least 25 times per second. With the actual code the main loop // executes about 80 times per second on a Arduino UNO R3, so there is ample // room for expansion. // // To verify what the actual refresh rate is, put a scope or a frequency meter // on one of the display outputs (DISP_1...DISP_6). void loop() { if (FreqCount.available()) { // If a new measure is available, shift previous values, read it // and apply frequency correction factor for (i = 0; i < 15; i++) count[i + 1] = count[i]; // Apply frequency correction. We must use a 64 bit integer because // the following calculation does not fit into a long integer uint64_t ftmp = ((uint64_t)FreqCount.read() * fcorr) / 1000000; // Store new value count[0] = ftmp; // set IF frequency offset, if known (see below) if (count[0] < countswl) offsetIF = -IF; if (count[0] > countswh) offsetIF = IF; // if we change band, the offset may change or become undefined, so zero it. // To detect band switching, check if the count has suddenly changed more // than 2MHz (= 20000 100Hz units times 4). e.g. we have switched from band 3 // at 5 MHz to band 4 at 8 MHz. long fdiff = count[15] - count[0]; if (abs(fdiff) > bandchthr) offsetIF = 0; // Calculate receive frequency by averaging 16 readings if ((count[0] > minLOcount) && (count[0] < maxLOcount) && (offsetIF != 0)) { // Sum 16 readings of f*16 = f*256 for (i=0; i<16; i++) freq += count[i]; // Average readings shifting right result 6 places freq = (freq >> 6) + offsetIF; // Store rightmost 5 digits (always significative) // DisplayImage is [0] [1] [2] [3] [4] [5] // MSD|---|---|---|---|LSD for (i = 5; i > 0; i--) { digit = freq % 10; freq /= 10; DisplayImage[i] = digit; } // If MSB is zero store a space instead of the digit value. // No need to check other digits since the minimum frequency // the BC312/342 is capable of is 1500kHz. digit = freq; // only MSD digit remains if (digit == 0) { // blank leading not significant zero if freq < 10 MHz DisplayImage[0] = ' '; } else { // store digit DisplayImage[0] = digit; } digitalWrite(DP,HIGH); } else { // At start or when switching band, if LO frequency is in the overlapping // range shown below, we do not know which offset apply, so display "Ovrlap" // // Band 3 (5-8 MHz, fLO=fRX+470 kHz) // 5470 8470 // |------------------------------| // ///////////// <-- OVERLAP! // |------------------------------| // 7530 10530 // Band 4 (8-11 MHz, fLO=fRX-470 kHz) // // To summarize: // if fLO < 7530 kHz, the offset is certainly -470 kHz (fLO=fRX+470kHz) // if fLO > 8470 kHz, the offset is certainly +470 kHz (fLO=fRX-470kHz) // if 7530 kHz < fLO < 8470 kHz, the program can't tell what it is. // To make it know, just temporarily tune the LO outside that range, // i.e. under 7530kHz (fRX=7060kHz on band 3) or above 8470kHz // (fRX=8940kHz on band 4). // if (offsetIF == 0) { digitalWrite(DP, LOW); for (i = 0; i < 6; i++) DisplayImage[i] = ov[i]; } // IF count[0] is < 1000 signal input is either absent or too low, // so display "No sig" if (count[0] < 1000) { digitalWrite(DP, LOW); for (i=0; i<6; i++) DisplayImage[i] = ns[i]; } else { // We have a reasonable count, but it is too low or too high // for a BC312/BC342, so display "Frq LO" or "Frq HI". if (count[0] < minLOcount) { digitalWrite(DP, LOW); for (i = 0; i < 6; i++) DisplayImage[i] = fl[i]; } if (count[0] > maxLOcount) { digitalWrite(DP, LOW); for (i = 0; i < 6; i++) DisplayImage[i] = fh[i]; } } } } #ifdef DEBUGDISPLAY for (i = 0; i < 6; i++) DisplayImage[i] = tdi[i]; digitalWrite(DP, LOW); #endif // And finally display the result for (i = 0; i < 6; i++) { putDigit(DisplayImage[i], i); } #ifdef SERIAL_CONF // Configuration commands handling if (Serial.available()) { // Put all displays OFF since display scan stops // at first character typed and resumes only after the // terminating CR. DisplayOFF(); // Read a CR-terminated command nc=Serial.readBytesUntil('\r',s, 19); for (i=0; i<nc; i++) { if (s[i] < 32) s[i]='\0'; } s[nc]='\0'; // Command parser // Commands can be issued either upper or lower case // All frequencies are in 100 Hz units, e.g. 1500 kHz = 15000 switch (toupper(s[0])) { case '\0': // no command, CR only, display prompt Serial.print("\ncmd:"); break; case 'B': // band change threshold - syntax is B [<frequency threshold>] if (s[1] != '\0') BCHTH = atol(&s[2]); Serial.print("\nBand change threshod: "); Serial.println(BCHTH); break; case 'S': // Display/set switch OL frequency - syntax is S [<switch OL frequency>] if (s[1] != '\0') SWF = atol(&s[2]); Serial.print("\nSwitch frequency: "); Serial.println(SWF); break; case 'I': // IF frequency // Display/set IF frequency - syntax is I [<IF frequency>] if (s[1] != '\0') IF = atol(&s[2]); Serial.print("\nIF frequency: "); Serial.println(IF); break; case 'M': // Display/set minimum tunable frequency - syntax is M [<minimum frequency>] if (s[1] != '\0') FMIN = atol(&s[2]); Serial.print("\nMinimum frequency: "); Serial.println(FMIN); break; case 'H': // Display/set maximum tunable frequency - syntax is H [<maximum frequency>] if (s[1] != '\0') FMAX = atol(&s[2]); Serial.print("\nMaximum frequency: "); Serial.println(FMAX); break; case 'C': // Display/set frequency correction factor - syntax is C [<correction factor in ppm>] if (s[1] != '\0') FCORR = atol(&s[2]); Serial.print("\nFrequency correction: "); Serial.println(FCORR); break; case 'W': // Save configuration in EEPROM - syntax is W EEPROM.write(MAGICADDRESS, MAGIC); EEPROM.put(0, SWF); EEPROM.put(4, IF); EEPROM.put(8, FMIN); EEPROM.put(12, FMAX); EEPROM.put(16, BCHTH); EEPROM.put(20, FCORR); Serial.println("\nConfiguration saved"); break; case 'L': // Show copyright and license - syntax is L Serial.print(COPYR); displayStringTimed("GPL v3 ", 1000); break; case 'T': // Display test - synats is T Serial.print("\nPerforming display test...\n"); digitalWrite(DP, LOW); displayStringTimed("Start test", 0); displayStringTimed("0123456789", 0); displayStringTimed("aAbcCdeEfgGhHiIjlLnoOpqrstuy", 0); displayStringTimed("-_^=?'\"[]", 0); digitalWrite(DP, HIGH); displayStringTimed("888888", 500); digitalWrite(DP, LOW); displayStringTimed(" ", 500); digitalWrite(DP, HIGH); displayStringTimed("888888", 500); digitalWrite(DP, LOW); displayStringTimed(" ", 500); digitalWrite(DP, HIGH); displayStringTimed("888888", 500); digitalWrite(DP, LOW); displayStringTimed(" ", 500); displayStringTimed("End test", 0); Serial.print("\nDisplay test done\n"); break; #ifdef DEBUG_DISPLAY case 'D': // Display a string - for debug purposes only - syntax is D [<string>] // If no string given show current value if (s[1] != '\0') { for (i=0; i<6; i++) if (i < strlen(s) - 2) tdi[i] = s[i+2]; else tdi[i] = ' '; } Serial.print("Test string: "); for (i=0; i<6; i++) Serial.print(tdi[i]); Serial.println(); break; #endif #ifdef HELP_MENU case '?': // Show help Serial.println(HELP); break; #endif default: Serial.println("\nInvalid command\n"); } // Update configuration values countswl = 4*(SWF - IF); // see above countswh = 4*(SWF + IF); // ditto minLOcount = (4 * (FMIN + IF) * 99) / 100; // less 1% guard maxLOcount = (4 * (FMAX - IF) * 101) / 100; // plus 1% guard bandchthr = 4 * BCHTH; // Band change threshold count value (4xfreq) fcorr = 1000000 + FCORR; #endif } }