// An Arduino based FT290r Mk 1 display decoder & CTCSS generator. // Nigel Stevens G6RZR 16th June 2022 // Doug Jackson VK1ZDJ 11th March 2023 // // A simple Arduino Nano is powerful enough to read in the data from the FT290R // and display it on an .96" OLED, and it also turns out that it can generate // CTCSS at the same time :-) // operation - Depress the CALL button to toggle CTCSS selection mode. // When in CCTCSS selection mode, rotate frequency dial. 10 Khz digit is // monitored to detect rotation - as dial is rotated, CTCSS frequency is // updated and displayed. When frequency shown is as required, depress // CALL button to store selected frequency in EEPROM and return to normal // frequency display mode. You will note that the radios operation frequency // will have changed - thats because we use the radio frequency encoder to // identify increases / decreases to the selected CTCSS frequency. // That is a small price to pay for such flexible CTCSS operation on this // beautiful old radio. // // When in FM the CTCSS output will be enabled only when transmitting unless // the CTCSS frequency is set to zero. #include #include "SSD1306Ascii.h" #include "SSD1306AsciiWire.h" #include "EEPROM.h" #define I2C_ADDRESS 0x3C #define RST_PIN -1 // Define proper display RST_PIN if required. //Define FT290R display connections #define CE 3 //Chip enable is connected to D3 #define STD 2 //STD is connected to D2 #define R40 5 //R40 is connected to D5 #define R41 6 //R41 is connected to D6 #define R42 7 //R42 is connected to D7 #define R43 8 //R43 is connected to D8 #define LEDLIGHT 9 // Meter LED #define FMTX 10 // FM transmitting input - via 1k resistor #define CALLBtn 14 // Call button input (A0 on Arduino) #define PWM 11 // OCR2A output is our CTCSS output - filter it using a 3KHz LPF // // 510R 510R 0.1uF // PWM ---/\/\/\-----/\/\/\-----||--+ // | | / 1k variable. // 0.1 = 0.1 = \__ Out // | | / // gnd gnd \ // + // gnd volatile int waveIndex = 0; volatile byte waveTable[1000]; const int SamplesPerSecond = 16000000/512; // 16Mhz clock / 512 PWM cycle length volatile int SamplesPerCycle; // the number of samples in the wave table volatile float frequency; const int CTCSSMax=52; // there are 52 elements in this CTCSS Table (stored as 10 * integers // to save space as storing floats costs memory // i.e. 915 = 91.5 Hz ) const int CTCSSTable[CTCSSMax]={0, 670, 693, 719, 741, 770, 799, 825, 854, 885, 915, 948, 974, 1000, 1035, 1072, 1109, 1148, 1188, 1230, 1273, 1318, 1365, 1413, 1462, 1514, 1567, 1598, 1622, 1655, 1679, 1713, 1738, 1773, 1799, 1835, 1862, 1899, 1928, 1966, 1995, 2035, 2065, 2107, 2181, 2257, 2291, 2336, 2418, 2503, 2541, 2704} ; volatile int CTCSSSel=10; //The index into the CTCSS table volatile int CTCSSSet=0; // flag indicating we are in CTCSS Setup mode static int i=0; static char disp[12]={0,0,0,0,0,0,0,0,0,0,0,0}; // Table to hold the 12 nibbles of character data static bool CEUPFLAG = 0; // Global flag which indicates that we are in a chip enabled state SSD1306AsciiWire oled; // create an oled object we can talk to. // We use Timer2 to generate the PWM Sinewave ISR(TIMER2_OVF_vect) { //Timer2 ISR OCR2A = waveTable[waveIndex]; //Write the current wavetable index waveIndex++; if (waveIndex==SamplesPerCycle) waveIndex=0; } void populateWaveTable(float CTCSSFreq) { //Program Counter/Timer 2 (CT2) to create a PWM sine at the CTCSS freq const float TwoPi = 3.1415 * 2 ; const int Amplitude = 127; // //The AC amplitude of the sinewave const int Offset = 127; //The DC offset of the sinewave if (CTCSSFreq==0) { bitClear(TIMSK2, TOIE2); return; } // freq = 0 means we disable the CTCSS generator SamplesPerCycle = int(SamplesPerSecond / CTCSSFreq); // The lengh of the sinewave in the wavetable bitClear(TIMSK2, TOIE2); // CT2 overflow interrupt disable if (SamplesPerCycle <= int(sizeof(waveTable))) { for (int index = 0; index < SamplesPerCycle; index++) //Iterate over each of the samples in the cycle { waveTable[index] = Amplitude * sin(TwoPi * index / SamplesPerCycle) + Offset; //Store the required sinewave in the wave table } bitSet(TIMSK2, TOIE2); //CT2 overflow interrupt enable } } void setup() { Wire.begin(); Wire.setClock(400000L); // Define inputs from FT290 pinMode(CE, INPUT); // set pin to input pinMode(STD, INPUT); // set pin to input pinMode(R40, INPUT); // set pin to input pinMode(R41, INPUT); // set pin to input pinMode(R42, INPUT); // set pin to input pinMode(R43, INPUT); // set pin to input pinMode(LEDLIGHT, OUTPUT); pinMode(PWM, OUTPUT); // set the PWM output pin pinMode(FMTX, INPUT); // FM transmit input pinMode(CALLBtn, INPUT_PULLUP); // Call button input // set up interrupt pins attachInterrupt(digitalPinToInterrupt(CE), ENABLEHANDLER, CHANGE); // Chip enable interupt handler from FT290 display attachInterrupt(digitalPinToInterrupt(STD), READNIBBLE, FALLING); // 4 bits of data are clocked into the arduino on the falling edge of STD // set up OLED display #if RST_PIN >= 0 oled.begin(&Adafruit128x32, I2C_ADDRESS, RST_PIN); #else // RST_PIN >= 0 oled.begin(&Adafruit128x32, I2C_ADDRESS); #endif // RST_PIN >= 0 // get the saved CTCSS frequency EEPROM.get(0, CTCSSSel); if ((CTCSSSel <0) or (CTCSSSel>(CTCSSMax-1))) // If the EEPROM value is out of range, fix it. { CTCSSSel=10; // Most of the repeaters in my area use 91.5Hz - the 10th entry. EEPROM.put(0,CTCSSSel); // Store that as a default } // load up the wavetable with sane values based on the currently selected CTCSS value. populateWaveTable(CTCSSTable[CTCSSSel]/10); // Setup CT2 for PWM output TCCR2A = _BV(COM2A1) | _BV(WGM20); //CT2 Config: // PWM, Phase Correct, // Clear OC2A on compare match when upcounting. // Set OC2A on compare match when down-counting. TCCR2B = _BV(CS20) ; // Internal clock, no prescale bitClear(TIMSK2, TOIE2); //CT2 overflow interrupt disable // Display an initial message oled.setFont(Adafruit5x7); oled.set2X(); oled.setCursor(22,1); oled.println("VK1ZDJ"); // Substitute your callsign oled.set1X(); oled.setCursor(20,6); oled.print("CTCSS "); // a sub message display the current CTCSS frequency oled.print( (float)CTCSSTable[CTCSSSel]/10 ); oled.print("Hz"); oled.set1X(); delay(3000); // leave the message up for a while digitalWrite(LEDLIGHT,HIGH); // Turn on the meter light oled.setFont(lcdnums14x24); // Change the font for future writes to an LCD typeface oled.clear(); } // ProcessCTCSS Setup - We monitor rotation of the dial by watching the frequency change // As in increases, we increment the CTCSS frequency // as it decreases we decrease the CTCSS frequency // Within sane limits void ProcessCTCSSSetup(char digit) { static char oldDigit; static bool FirstRun=true; // flag to skip the update the first time we are called. if (FirstRun==false) { if (digit!=oldDigit) // has there been a change in the radio's dial position? { // yes! - decide if we went clockwise or counterclockwise if (digit>oldDigit) CTCSSSel++; // clockwise if (digit 9 decrease (not used) if ((oldDigit=='9') & (digit=='0')) CTCSSSel=CTCSSSel+2; // outlier for } if (CTCSSSel<0) CTCSSSel=0; if (CTCSSSel>(CTCSSMax-1)) CTCSSSel=CTCSSMax-1; } printCTCSSFreq(); FirstRun=false; oldDigit=digit; // save the current display digit so we can compare next time. } //------------------------------------------------------------------------------ // Main loop. The simple function of the loop is to update the display and allow // setting of the CTCSS Frequency. // // The structure of the received data is as follows: // // disp[9] is the first character of the display // // e.g. for 144.250 MHz display would read 4.250.0 // so disp[9] would be '4' // // disp[8] and disp[2] carry the decimal point // // disp[7], disp[5], disp[3] and disp[1] are the remaining digits // // Finally disp[11] holds bits indicating CLAR, FUNC and MEM // // so 00110001 Should display MEM // 00110010 Should display FUNC // 00110100 Should display CLAR // These could also all be displayed at once with the sequence // 00110111 Would display MEM, FUNC and CLAR // //------------------------------------------------------------------------------ void loop() { // sample the call button - see if it has been pushed - if so, toggle CTCSS setup mode if ((digitalRead(CALLBtn)==0) & (CTCSSSet==0)) { CTCSSSet=1; bitClear(TIMSK2, TOIE2); // Turn off CTCSS generator printCTCSSFreq(); while(digitalRead(CALLBtn)==0){}; // wait for button to go up } if ((digitalRead(CALLBtn)==0) & (CTCSSSet==1)) { CTCSSSet=0; EEPROM.put(0, CTCSSSel); //Save the CTCSS lookup to EEPROM printFreq(); while(digitalRead(CALLBtn)==0){}; // wait for button to go up populateWaveTable(CTCSSTable[CTCSSSel]/10); } // If we are in CTCSS setup mode, and all bits of the disp[] array are valid, do the ProcessCTCSSSetup routine // otherwise, display the current frequency if ((CTCSSSet==1) & (i==11)) { ProcessCTCSSSetup(disp[5]); i=0; } else if (i==11) // i=11 indicates that the most recent batch of data has been read from the FT290 CPU { printFreq(); i=0; } // finally, if we are transmitting in FM mode, enable the CTCSS generator, otherwise leave it disabled // the interrupts slow down the display update otherwise. if (digitalRead(FMTX) == true) bitSet(TIMSK2, TOIE2); //CT2 overflow interrupt enable else bitClear(TIMSK2, TOIE2); //CT2 overflow interrupt disable } // // print the current FT290 frequency by displaying numbers from the disp[] array // void printFreq() { oled.setFont(lcdnums14x24); // ready to display numbers oled.setCursor(1,1); oled.print(disp[9]); if (disp[8] & 0x11) {oled.print(".");} oled.print(disp[7]); oled.print(disp[5]); oled.print(disp[3]); if (disp[2] & 0x11) {oled.print(".");} oled.print(disp[1]); // Now check for "-" character and display oled.setFont(Adafruit5x7); if (disp[11] & 0x02) { oled.setCursor(103,2); oled.print("FUNC"); } else { oled.setCursor(103,2); oled.print(" "); } // Now check for clarifier bit if (disp[11] & 0x04) { oled.setCursor(103,1); oled.print("CLAR"); } else { oled.setCursor(103,1); oled.print(" "); } // check for "M" character if (disp[11] & 0x01) { oled.setCursor(103,3); oled.print("MEM"); } else { oled.setCursor(103,3); oled.print(" "); } } // display the current selected CTCSS frequency void printCTCSSFreq() { static float DISPFreq; oled.setFont(lcdnums14x24); oled.setCursor(1,1); DISPFreq=(float)CTCSSTable[CTCSSSel]/10; // Convert the table value to a displayed frequency if (DISPFreq<100) oled.print(' '); // buffer numbers less than 100 to keep display consistient oled.print(DISPFreq); oled.print(' '); } // Interrupt Service Routine for Chip Enable (CE) void ENABLEHANDLER() { if (digitalRead(CE)) { i=0; //CE has gone high this is the beginning of the data sequence CEUPFLAG=1; } else { CEUPFLAG=0; //CE has gone low this is the end of the data sequence } } // Interrupt Service Routine for STD falling edge void READNIBBLE() { bool R40B = 0; bool R41B = 0; bool R42B = 0; bool R43B = 0; if (CEUPFLAG) { //CE is high so we are in the receiving data zone // Falling edge of STD detected read a nibble into the array R40B = digitalRead(R40); R41B = digitalRead(R41); R42B = digitalRead(R42); R43B = digitalRead(R43); // Now shift the bits into the characters disp[i] = (0<<7) | // Since its only the last four bits that we are using (0<<6) | // shift in 0011 to bits 7-4 as these are the standard (1<<5) | // pattern for ASCII (1<<4) | // (R43B<<3) | // Shift in the actual data from the FT290 processor (R42B<<2) | // Helpfully they used an ASCII-like scheme (R41B<<1) | R40B; i++; } }