// Last updated July 10, 2025 // Copyright (c) 2025 K.Tomoyasu JO4MLG/JH4*** // https://agc.ne.jp/e-craft/qrp_cw_transceiver_v3/ // Version 2.5.5 Si5351a OLED IambicB_keyer equipped 40mBAND CW Transceiver. #include <Wire.h> #include <SSD1306AsciiAvrI2c.h> //https://github.com/greiman/SSD1306Ascii #include <Rotary.h> // https://github.com/buxtronix/arduino/tree/master/libraries/Rotary #include <EEPROM.h> // --- Global setup --- const unsigned long DEFAULT_FREQ = 19796940UL; const int EEPROM_ADDR = 0; unsigned long lastFreqWriteTime = 0; bool freqChanged = false; bool freqWritten = true; unsigned long eeprom_cached_FREQ; // TX/RX State transition management unsigned long txDelayStartTime = 0; // For measuring transmission start waiting time bool txTransitionActive = false; // Flags in Transition bool txActive = false; unsigned long rxDelayStartTime = 0; // For measuring reception start waiting time bool rxTransitionActive = false; // interval control for bar graph unsigned long previousMillis = 0; const long interval = 100; // S meter rewrite cycle(mS) int previousBarLength = -1; SSD1306AsciiAvrI2c oled; Rotary r = Rotary(2, 3); // Si5351A definitions #define Si5351A_ADDR 0x60 #define MSNA_ADDR 26 #define MSNB_ADDR 34 #define MS0_ADDR 42 #define MS1_ADDR 50 #define MS2_ADDR 58 #define CLK0_CTRL 16 #define CLK1_CTRL 17 #define CLK2_CTRL 18 #define OUTPUT_CTRL 3 #define XTAL_LOAD_C 183 #define PLL_RESET 177 // I/O pins #define SW_STEP 5 #define TX_SW 9 // Transmission control input from Straight(manual) key #define TX_ON 7 #define AF_mute 8 // AF Mute Control #define SIDETONE_PIN 10 // SideTone output // Frequency setup const long LOW_FREQ = 19796940L; const long HI_FREQ = 19996940L; const long IF_FREQ = 12796940L; unsigned long FREQ = DEFAULT_FREQ; unsigned long FREQ_OLD = FREQ; unsigned long txFreq_OLD = 0; int STEP = 100; // --- constant value --- const int SIDETONE_FREQ = 800; // sidetone frequency (Hz) const unsigned long TX_ON_DELAY_MS = 3; // Delay from clk1 on to TX_ON on (mS) const unsigned long RX_ON_DELAY_MS = 3; // Delay from TX_ON OFF to AF_mute release (mS) // Variable to hold the state of TX_SW (manual key) bool manualTxSwitchPressed = false; unsigned long lastManualTxReadTime = 0; const unsigned long MANUAL_TX_DEBOUNCE_DELAY = 10; // Manual key debouncing // --- keyer pin assignment --- const int DIT_PIN = 11; const int DAH_PIN = 12; const int SPEED_PIN = A1; // --- keyer function void updatePaddleLatch(); void loadWPM(); void runKeyerLogic(); // keyer variables uint8_t nextElementToKey = 0; // The next code to be sent by the keyer #define DIT_L 0x01 #define DAH_L 0x02 uint8_t keyerControl = 0; uint8_t keyerState; enum KSTYPE {IDLE, KEYED_PREP, KEYED, INTER_ELEMENT}; unsigned long ditTime = 50; unsigned long ktimer; bool lastWasDit = false; // Debounce Variables unsigned long lastDitStateChangeTime = 0; unsigned long lastDahStateChangeTime = 0; const long KEYER_DEBOUNCE_DELAY = 5; // paddle debounce delay time (mS) // --- function(Si5351A / OLED / VFO) --- const long ppm_correction = 22; // Specify the deviation of the PLL reference oscillation in ppm units(1ppm 25Hz) void rotary_encoder(); void Fnc_Stp(); void drawBar(int currentBarLength); void Set_CLK_Enable(uint8_t clk0, uint8_t clk1, uint8_t clk2); void Freq_Disp(unsigned long Fre); void Step_Disp(int Stp); void Si5351_write(byte Reg, byte Data); void Si5351_init(); void PLL_Set(char pll, uint32_t freq); void MS_Set(uint8_t clk_no); void Parameter_write(uint8_t REG_ADDR, uint32_t Pa1, uint32_t Pa2, uint32_t Pa3); void setTxOnState(bool on); // --- Setup --- void setup() { // Rotary Encoder setup r.begin(); attachInterrupt(0, rotary_encoder, CHANGE); attachInterrupt(1, rotary_encoder, CHANGE); // Pin Modes pinMode(SW_STEP, INPUT_PULLUP); pinMode(TX_SW, INPUT_PULLUP); // TX_SW (for manual key) pinMode(DIT_PIN, INPUT_PULLUP); pinMode(DAH_PIN, INPUT_PULLUP); pinMode(TX_ON, OUTPUT); pinMode(AF_mute, OUTPUT); pinMode(SIDETONE_PIN, OUTPUT); setTxOnState(false); digitalWrite(AF_mute, HIGH); EEPROM.get(EEPROM_ADDR, FREQ); if (FREQ < LOW_FREQ || FREQ > HI_FREQ) FREQ = DEFAULT_FREQ; FREQ_OLD = FREQ; eeprom_cached_FREQ = FREQ; oled.begin(&Adafruit128x64, 0x3C); //For 0.96 inch 128x64, 1.3 inch OLED is (&SH1106_128x64, 0x3C) Si5351_init(); PLL_Set('A', FREQ); MS_Set(0); // Multi-synthesizer setting for CLK0 (receive) unsigned long initialTxFreq = FREQ - IF_FREQ; PLL_Set('B', initialTxFreq); MS_Set(1); // Multi-synthesizer setting for CLK1 (transmit) txFreq_OLD = initialTxFreq; // Record the initial transmission frequency Si5351_write(CLK1_CTRL, 0x6D); // Set CLK1 to 4mA Freq_Disp(FREQ); Step_Disp(STEP); oled.setFont(font5x7); oled.setCursor(0, 4); // 5th line oled.write("1__3__5__7_9+20+40+60"); // For S meter Set_CLK_Enable(1, 0, 0); keyerState = IDLE; sei(); } // --- Loop --- void loop() { unsigned long now = millis(); if (digitalRead(SW_STEP) == LOW) Fnc_Stp(); bool currentRawManualTx = (digitalRead(TX_SW) == LOW); static bool rawManualTxState = false; static bool previousManualTxState = false; // Retain previous state of manual key if (currentRawManualTx != rawManualTxState) { lastManualTxReadTime = now; rawManualTxState = currentRawManualTx; } if (now - lastManualTxReadTime > MANUAL_TX_DEBOUNCE_DELAY) { manualTxSwitchPressed = rawManualTxState; } if (manualTxSwitchPressed && !previousManualTxState) { // The moment I was pushed tone(SIDETONE_PIN, SIDETONE_FREQ); } else if (!manualTxSwitchPressed && previousManualTxState) { // The moment we were separated noTone(SIDETONE_PIN); } previousManualTxState = manualTxSwitchPressed; loadWPM(); // Update Keyer Speed updatePaddleLatch(); // Paddle Debouncing runKeyerLogic(); // Executing keyer logic bool requestTx = manualTxSwitchPressed || (keyerState == KEYED_PREP) || (keyerState == KEYED); if (requestTx) { if (!txTransitionActive) { // Transition from receiving to sending begins txDelayStartTime = now; txTransitionActive = true; rxTransitionActive = false; // Reset Transition to Inbound digitalWrite(AF_mute, LOW); // AF Mute Cancel Set_CLK_Enable(0, 1, 0); // Enable CLK1 (transmit) } if (txTransitionActive && (now - txDelayStartTime >= TX_ON_DELAY_MS)) { if (keyerState == IDLE && manualTxSwitchPressed) { setTxOnState(true); } txActive = true; // Marks TX as active } oled.setFont(font5x7); oled.setCursor(0, 6); if (digitalRead(TX_ON) == HIGH) { oled.write("_Transmitting_"); // Alternative display as transmission indicator } } else { // If there is no request to send if (!rxTransitionActive) { rxDelayStartTime = now; rxTransitionActive = true; txTransitionActive = false; // Reset Transition to Send setTxOnState(false); // When there is no longer a transmission request, set TX_ON to LOW. txActive = false; // Mark TX inactive Set_CLK_Enable(1, 0, 0); // Enable CLK0 (receive) } oled.setCursor(0, 6); oled.clearToEOL(); if (rxTransitionActive && (now - rxDelayStartTime >= RX_ON_DELAY_MS)) { digitalWrite(AF_mute, HIGH); // AF Mute Cancel rxTransitionActive = false; // Transition to reception complete } } // --- Frequency change detection --- if (FREQ != FREQ_OLD) { PLL_Set('A', FREQ); MS_Set(0); // Multi-synthesizer associated with CLK0 (receive) unsigned long txFreq_new = FREQ - IF_FREQ; PLL_Set('B', txFreq_new); MS_Set(1); // Multi-synthesizer associated with CLK1 (transmit) txFreq_OLD = txFreq_new; // Update transmit frequency Freq_Disp(FREQ); FREQ_OLD = FREQ; freqChanged = true; lastFreqWriteTime = now; freqWritten = false; eeprom_cached_FREQ = FREQ; } // --- bar graph --- if (now - previousMillis >= interval) { previousMillis = now; int sensorValue = analogRead(A0); // Read the AGC voltage const int MIN_ADC = 190; const int MAX_ADC = 588; sensorValue = constrain(sensorValue, MIN_ADC, MAX_ADC); int currentBarLength = map(sensorValue, MAX_ADC, MIN_ADC, 0, 128); if (currentBarLength != previousBarLength) { drawBar(currentBarLength); previousBarLength = currentBarLength; } } // --- EEPROM write process --- if (freqChanged && !freqWritten && (now - lastFreqWriteTime >= 2000)) { unsigned long storedFreq; EEPROM.get(EEPROM_ADDR, storedFreq); if (storedFreq != eeprom_cached_FREQ) { EEPROM.put(EEPROM_ADDR, eeprom_cached_FREQ); } freqWritten = true; freqChanged = false; } } //--- Displays bar graphs one at a time --- void drawBar(int currentBarLength) { oled.setFont(font5x7); int targetDisplayLength = currentBarLength; int previousDisplayLength = previousBarLength; if (targetDisplayLength > previousDisplayLength) { for (int i = previousDisplayLength; i < targetDisplayLength; i++) { oled.setCursor(i, 5); if (i % 2 == 0) { oled.write("L"); } else { oled.write(" "); } } } else if (targetDisplayLength < previousDisplayLength) { for (int i = targetDisplayLength; i < previousDisplayLength; i++) { oled.setCursor(i, 5); oled.write(" "); } } } void Set_CLK_Enable(uint8_t clk0, uint8_t clk1, uint8_t clk2) { uint8_t val = 0; if (!clk0) val |= (1 << 0); if (!clk1) val |= (1 << 1); if (!clk2) val |= (1 << 2); Si5351_write(OUTPUT_CTRL, val); } void Freq_Disp(unsigned long Fre) { unsigned long displayFreq = Fre - IF_FREQ; char buf[12]; sprintf(buf, "%lu", displayFreq); oled.setFont(fixednums8x16); oled.setCursor(8, 1); oled.print(buf[0]); oled.print("."); oled.print(buf[1]); oled.print(buf[2]); oled.print(buf[3]); oled.print("."); oled.print(buf + 4); } void Step_Disp(int Stp) { oled.setFont(font5x7); oled.setCursor(100, 2); oled.print((Stp == 1000) ? " 1K" : (Stp == 100) ? "100" : " 10"); } void Si5351_write(byte Reg, byte Data) { Wire.beginTransmission(Si5351A_ADDR); Wire.write(Reg); Wire.write(Data); Wire.endTransmission(); } // --- Be careful with the Register settings --- void Si5351_init() { Si5351_write(OUTPUT_CTRL, 0xFF); // All outputs disabled Si5351_write(CLK0_CTRL, 0x80); // Power down CLK0 Si5351_write(CLK1_CTRL, 0x80);// Power down CLK1 Si5351_write(CLK2_CTRL, 0x80);// Power down CLK2 Si5351_write(XTAL_LOAD_C, 0x92);// Set crystal load capacitance Si5351_write(PLL_RESET, 0xA0);// Reset all PLLs Si5351_write(CLK0_CTRL, 0x4D);// CLK0: PLLA, 4mA, (RX) Si5351_write(CLK1_CTRL, 0x6D);// CLK1: PLLB, 4mA, (TX) Si5351_write(CLK2_CTRL, 0x6C);// CLK2: PLLB, 2mA, (I won't use it so anything is fine) Si5351_write(OUTPUT_CTRL, 0xFC); // Enable CLK0 CLK1 at init } void PLL_Set(char pll, uint32_t freq) { uint8_t addr = (pll == 'A') ? MSNA_ADDR : MSNB_ADDR; uint64_t pll_freq = (uint64_t)freq * 32; pll_freq = (pll_freq * (1000000L + ppm_correction)) / 1000000L; // Added for frequency correction uint32_t mult = pll_freq / 25000000; uint32_t l = pll_freq % 25000000; uint32_t num = (uint64_t)l * 1048575 / 25000000; uint32_t denom = 1048575; uint32_t P1 = 128 * mult + ((128 * num) / denom) - 512; uint32_t P2 = (128 * num) % denom; uint32_t P3 = denom; Parameter_write(addr, P1, P2, P3); } void MS_Set(uint8_t clk_no) { uint8_t addr; if (clk_no == 0) addr = MS0_ADDR; else if (clk_no == 1) addr = MS1_ADDR; else addr = MS2_ADDR; uint32_t P1 = 128 * 32 - 512; // Divide by 32 uint32_t P2 = 0; uint32_t P3 = 1; Parameter_write(addr, P1, P2, P3); } void Parameter_write(uint8_t REG_ADDR, uint32_t Pa1, uint32_t Pa2, uint32_t Pa3) { Si5351_write(REG_ADDR, (Pa3 >> 8) & 0xFF); Si5351_write(REG_ADDR + 1, Pa3 & 0xFF); Si5351_write(REG_ADDR + 2, (Pa1 >> 16) & 0x03); Si5351_write(REG_ADDR + 3, (Pa1 >> 8) & 0xFF); Si5351_write(REG_ADDR + 4, Pa1 & 0xFF); Si5351_write(REG_ADDR + 5, ((Pa3 >> 12) & 0xF0) | ((Pa2 >> 16) & 0x0F)); Si5351_write(REG_ADDR + 6, (Pa2 >> 8) & 0xFF); Si5351_write(REG_ADDR + 7, Pa2 & 0xFF); } // Rotary encoder ISR void rotary_encoder() { unsigned char result = r.process(); if (result) { FREQ += (result == DIR_CW) ? STEP : -STEP; FREQ = constrain(FREQ, LOW_FREQ, HI_FREQ); freqChanged = true; } } void Fnc_Stp() { STEP = (STEP == 10) ? 1000 : ((STEP == 1000) ? 100 : 10); // 10 -> 1000 -> 100 -> 10 の順に切り替え delay(10); Step_Disp(STEP); while (digitalRead(SW_STEP) == LOW) delay(10); } // --- Helper function for controlling TX_ON --- void setTxOnState(bool on) { if (on) { digitalWrite(TX_ON, HIGH); } else { digitalWrite(TX_ON, LOW); } } // --- keyer functions --- void updatePaddleLatch() { unsigned long now = millis(); bool ditState = (digitalRead(DIT_PIN) == LOW); bool dahState = (digitalRead(DAH_PIN) == LOW); if (ditState != ((keyerControl & DIT_L) > 0) && (now - lastDitStateChangeTime > KEYER_DEBOUNCE_DELAY)) { if (ditState) keyerControl |= DIT_L; else keyerControl &= ~DIT_L; lastDitStateChangeTime = now; } if (dahState != ((keyerControl & DAH_L) > 0) && (now - lastDahStateChangeTime > KEYER_DEBOUNCE_DELAY)) { if (dahState) keyerControl |= DAH_L; else keyerControl &= ~DAH_L; lastDahStateChangeTime = now; } } void loadWPM() { int wpm = map(analogRead(SPEED_PIN), 0, 1023, 12, 40); ditTime = 1200UL / wpm; } void runKeyerLogic() { unsigned long now = millis(); bool ditPressed = (keyerControl & DIT_L); bool dahPressed = (keyerControl & DAH_L); switch (keyerState) { case IDLE: if (manualTxSwitchPressed) nextElementToKey = 0; else if (ditPressed) { nextElementToKey = DIT_L; keyerState = KEYED_PREP; } else if (dahPressed) { nextElementToKey = DAH_L; keyerState = KEYED_PREP; } else { noTone(SIDETONE_PIN); nextElementToKey = 0; } break; case KEYED_PREP: if (nextElementToKey & DIT_L) { lastWasDit = true; nextElementToKey &= ~DIT_L; } else if (nextElementToKey & DAH_L) { lastWasDit = false; nextElementToKey &= ~DAH_L; } else { keyerState = IDLE; setTxOnState(false); noTone(SIDETONE_PIN); return; } setTxOnState(true); tone(SIDETONE_PIN, SIDETONE_FREQ); ktimer = now; keyerState = KEYED; break; case KEYED: if (ditPressed && !lastWasDit) nextElementToKey |= DIT_L; if (dahPressed && lastWasDit) nextElementToKey |= DAH_L; if (now - ktimer >= (lastWasDit ? ditTime : ditTime * 3)) { setTxOnState(false); noTone(SIDETONE_PIN); ktimer = now; keyerState = INTER_ELEMENT; } break; case INTER_ELEMENT: if (now - ktimer >= ditTime) { if (nextElementToKey) keyerState = KEYED_PREP; else if (ditPressed && dahPressed) { nextElementToKey = lastWasDit ? DAH_L : DIT_L; keyerState = KEYED_PREP; } else if (ditPressed) { nextElementToKey = DIT_L; keyerState = KEYED_PREP; } else if (dahPressed) { nextElementToKey = DAH_L; keyerState = KEYED_PREP; } else keyerState = IDLE; } break; } }