// 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;
}
}