HF Receiver Software Source Code
As I stated on my YouTube channel, here's the updated source code for the HF receiver. See the post about the source code for the HF transmitter, as there are references to additional files that you will need.
This code is provided with NO WARRANTY or guarantee it will work for you and your project. It is for your information and education purposes only, you are on your own to get it to work.
// Havarti version 2 software for the HF receiver project
// Version date: 07Mar2025
// By Level Up EE Lab (Daryn)
#include <EEPROM.h>
#include "D_si5351mcu.h" // https://github.com/pavelmc/Si5351mcu modified by DWaite to fix issue with spread spectrum
#include <Adafruit_GFX.h> // Core graphics library https://github.com/adafruit/Adafruit-GFX-Library
#include <Adafruit_ILI9341.h> // 2.8in display library
#include <Rotary.h> // Rotary encoder: https://github.com/brianlow/Rotary
#include <Adafruit_FT6206.h> // touchscreen library
#include "HFTGraphics2.h" // put all of the graphics bitmaps into this external file to declutter this main prog
// Input/Output pin definitions
#define FE_CLK_PIN 2 // encoder clock
#define FE_DT_PIN 3 // encoder DT
#define FE_SW_PIN 4 // encoder switch
// digtial pin 5 not used
// digital pin 6 not used
#define FEA_PIN 7 // front end attenuator output
#define TFT_DC_PIN 8 // display SPI DC pin
#define TFT_LED_PIN 9 // display LED pin
#define TFT_CS_PIN 10 // display SPI CS pin
// digital pin 11 SPI MOSI
// digital pin 12 SPI MISO
// digtial pin 13 SPI SCK
#define MAX_CS_PIN A0 // MAX4820 SPI CS pin
#define TFT_RST_PIN A1 // display SPI Reset pin
#define BPF_PIN0 A2 // bandpass filter slot 0
#define BPF_PIN1 A3 // bandpass filter slot 1
// analog pin A4 I2C SDA
// analog pin A5 I2C SCL
#define BPF_PIN2 A6 // bandpass filter slot 2
#define S_METER_PIN A7 // S meter output pin
// 2.8in (320x240) TFT display defines
#define BAR_BUFFER 15 // size of power and SWR buffer for running average
#define BG_COLOR 0x0000 // this is the background color
#define BUTTON_COLOR1 0x275D
#define BUTTON_COLOR2 0x25B7
#define BLINKYLOGO_COUNT 100 // used for flashing the logo, 255 max
// menu modes
#define MENUMODE_FIRST 0
#define MENUMODE_SECOND 1
#define MENUMODE_THIRD 2
// encoder modes
#define ENCODERMODE_FREQUENCY 0
#define ENCODERMODE_STEP 1
// Reception modes
#define RMODE_USB 0
#define RMODE_LSB 1
#define RMODE_CW 2
// define band start and ends. The start is also used as the start frequency
#define BAND_160M_START 1800000UL
#define BAND_160M_END 2000000UL
#define BAND_80M_START 3500000UL
#define BAND_80M_END 4000000UL
#define BAND_60M_START 5330500UL
#define BAND_60M_END 5405000UL
#define BAND_40M_START 7000000UL
#define BAND_40M_END 7350000UL
#define BAND_30M_START 10100000UL
#define BAND_30M_END 10150000UL
#define BAND_20M_START 14000000UL
#define BAND_20M_END 14350000UL
#define BAND_17M_START 18068000UL
#define BAND_17M_END 18168000UL
#define BAND_15M_START 21000000UL
#define BAND_15M_END 21450000UL
#define BAND_12M_START 24890000UL
#define BAND_12M_END 24990000UL
#define BAND_10M_START 28000000UL
#define BAND_10M_END 29700000UL
// other defines
#define RELAY_DELAY 50 // number of milliseconds to pause between SPI commands. In theory this could be as fast as 10ms but 50ms works fine
#define DEBOUNCE_DELAY 50 // milliseconds threshold used for switch debouncing. 50ms seems to work fine
#define MEM_STALL_DELAY 10000 // milliseconds to wait during inactivity (no encoder interation) before writing to EEPROM
#define USB_IF_FREQ 9002400 // upper cut-off of crystal filter at 12VDC
#define LSB_IF_FREQ 8999400 // lower cut-off of crystal filter at 12VDC
#define CW_IF_FREQ 8999100 // mid-range of the crystal filter at 0VDC
#define CW_BFO_FREQ 8999100 // subtract sideToneFreq from this value to set the BFO
#define BFO_CLK 0 // use CLK0 for the BFO
#define VFO_CLK 1 // use CLK1 for the VFO
#define MODE_MINION 0
#define MODE_CAPTAIN 1
#define MODE_SPLIT 2
// use this struct for VFO data. It is used by the two active VFOs and to store settings for each of the 3 filter banks
typedef struct {
unsigned long frequency;
uint8_t band; // one of the BAND_XXX defines
uint8_t rMode; // is either RMODE_USB, RMODE_LSB, or RMODE_CW
uint8_t filterBank; // 0, 1, or 2
} VFO;
// use this struct for data stored in EEPROM
typedef struct {
unsigned long index;
VFO vfoA;
VFO bankVFO0;
VFO bankVFO1;
VFO bankVFO2;
uint8_t presentVFO;
uint8_t tuningStep;
uint8_t brightness;
uint8_t sideToneFreq;
} EESETTINGS;
//declare global variables
Si5351mcu masterOscillator;
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN);
Adafruit_FT6206 ts = Adafruit_FT6206();
Rotary rFreq = Rotary(FE_CLK_PIN, FE_DT_PIN); // Rotary encoder for frequency connects to interrupt pins
uint8_t blinkyLogo = 0; // crude timer for blinkig the logo after a button push
bool blinkLogo = false; // true if we need to blink the logo
int freqColor = 0x07E0; // used for various colors for the frequency display
uint8_t freqIndex = 0; // used to cycle through the frequency colors, 0 through 3
uint8_t shiftX = 32; // used to center the display horizontally
uint8_t touchLoopCount = 0; // used to minimize the number of times through the loop() we check for a touch
uint8_t lastButtonTouch = 0; // button number touched before (1 to 5)
uint8_t newButtonTouch = 0; // button number being touched now (1 to 5)
uint8_t presentCMMode = MODE_SPLIT; // start in split mode operation
VFO VFOs[2]; // two VFOs - A and B, or 0 and 1 by array index
VFO bankVFOs[3]; // keep track of the previous VFO settings when switching bands/filter banks
uint8_t presentMenuMode = MENUMODE_FIRST; // present mode of the menu state machine
uint8_t presentVFO; // there's only two values, 0=VFO A, 1= VFO B
uint8_t tuningStep; // ranges from 0 to 4, 0=100kHz, 4=10Hz
uint8_t encoderMode = ENCODERMODE_FREQUENCY; // start with the encoder adjusting frequency
uint8_t brightness; // ranges from 1 to 10, multiply by 25 to set actual PWM
unsigned long activityTimer = 0; // milliseconds since either the tune or menu encoder were interacted with
bool activityTimerON = false; // true if the activity timer is running
unsigned long VFO_offset; // frequency to offset the VFO (changes depending on USB or LSB)
unsigned long oldFreq; // use this to keep track of how many frequency digits to change
volatile int tuneDir = 0; // this is set to 1 or -1 by the tune interrupt function
uint8_t sideToneFreq; // side tone frequency (0 to 2)
unsigned long promIndex; // used to keep track of EEPROM saves and reads
bool attenuatorEngaged = false; // TRUE if the front end attenuator is engaged
// variables for the encoder
unsigned long lastFEPBTime = 0; // milliseconds since the freq encoder pushbutton was pressed
uint8_t FEButtonState; // state (HIGH or LOW) for the freq encoder pushbutton
uint8_t lastFEButtonState = LOW; // the pushbuttons on these encoders go LOW when pressed
char *bandNames[] = { "empty", "160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m" };
char *receptionModeNames[] = { "USB", "LSB", "CW" };
char *vfoNames[] = { "A", "B" };
char *sideTones[] = { "400Hz", "700Hz", "1kHz" };
long increments[] = { 100000L, 10000, 1000, 100, 10 };
// used to set the USB/LSB transmission mode by band, zero index is empty
// 0=USB, 1=LSB. CW is allowed on all bands
uint8_t SSBModeByBand[] = { 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0 };
uint8_t textOffsets[] = { 26, 26, 20, 14, 8, 2 };
// variables for the bar graph
uint8_t barPointer = 0;
uint8_t numBars[BAR_BUFFER];
void setup() {
Serial.begin(9600);
Wire.begin();
// initialize all input and output pins
pinMode(FEA_PIN, OUTPUT);
pinMode(MAX_CS_PIN, OUTPUT);
pinMode(FE_CLK_PIN, INPUT);
pinMode(FE_DT_PIN, INPUT);
pinMode(FE_SW_PIN, INPUT);
pinMode(TFT_RST_PIN, OUTPUT);
pinMode(TFT_LED_PIN, OUTPUT);
pinMode(BPF_PIN0, INPUT);
pinMode(BPF_PIN1, INPUT);
pinMode(BPF_PIN2, INPUT);
// load settings from EEPROM. Need to find which sector contains the most recently saved data.
// each sector is 36 bytes, there are 28 sectors that fit evenly in 1,024 bytes of EEPROM
// (36*28=1008)
EESETTINGS EEParams;
uint8_t i;
unsigned long reading;
for (i = 0; i < 28; i++) {
EEPROM.get((36 * i), reading);
if (reading > promIndex)
promIndex = reading;
}
EEPROM.get(36 * (promIndex % 28), EEParams);
// initialize VFOB to the same settings as VFOA
VFOs[0].frequency = EEParams.vfoA.frequency;
VFOs[0].band = EEParams.vfoA.band;
VFOs[0].rMode = EEParams.vfoA.rMode;
VFOs[0].filterBank = EEParams.vfoA.filterBank;
VFOs[1].frequency = EEParams.vfoA.frequency;
VFOs[1].band = EEParams.vfoA.band;
VFOs[1].rMode = EEParams.vfoA.rMode;
VFOs[1].filterBank = EEParams.vfoA.filterBank;
bankVFOs[0].frequency = EEParams.bankVFO0.frequency;
bankVFOs[0].band = EEParams.bankVFO0.band;
bankVFOs[0].rMode = EEParams.bankVFO0.rMode;
bankVFOs[0].filterBank = EEParams.bankVFO0.filterBank;
bankVFOs[1].frequency = EEParams.bankVFO1.frequency;
bankVFOs[1].band = EEParams.bankVFO1.band;
bankVFOs[1].rMode = EEParams.bankVFO1.rMode;
bankVFOs[1].filterBank = EEParams.bankVFO1.filterBank;
bankVFOs[2].frequency = EEParams.bankVFO2.frequency;
bankVFOs[2].band = EEParams.bankVFO2.band;
bankVFOs[2].rMode = EEParams.bankVFO2.rMode;
bankVFOs[2].filterBank = EEParams.bankVFO2.filterBank;
presentVFO = EEParams.presentVFO;
tuningStep = EEParams.tuningStep;
brightness = EEParams.brightness;
sideToneFreq = EEParams.sideToneFreq;
// initialize Si5351 and associated frequency control variables
VFO_offset = 9000000;
masterOscillator.init(24998770L); // set the oscillator to the actual crystal frequency determined from calibration
// VFO is set later in the first call to switchVFO();
masterOscillator.setFreq(BFO_CLK, LSB_IF_FREQ);
masterOscillator.setFreq(VFO_CLK, 16074000L);
masterOscillator.enable(BFO_CLK);
masterOscillator.enable(VFO_CLK);
// set attentuator to disengaged initially
digitalWrite(FEA_PIN, LOW);
// Configure interrupt and enable for rotary encoder.
PCICR |= (1 << PCIE2);
PCMSK2 |= (1 << PCINT18) | (1 << PCINT19);
sei();
ts.begin(64); // use 64 to slightly desensitize the screen
tft.begin();
tft.setRotation(3);
tft.setTextWrap(false); // Allow text to run off right edge
int pwmValue = brightness * 25;
if (brightness >= 10) pwmValue = 255;
analogWrite(TFT_LED_PIN, pwmValue);
tft.fillScreen(0x0000);
// display splash screen for a second.
tft.drawBitmap(shiftX + 108, 108, LUEEL_LOGO, 24, 24, ILI9341_YELLOW);
delay(1000);
tft.fillScreen(0x0000);
// set up the static elements of the display
// starting with the static text
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
//tft.drawRect(shiftX, 100, 240, 2, ILI9341_RED);
// draw the meter scales
int hOffs = 0;
tft.fillRect(shiftX + 65, 115, 3, 7, ILI9341_WHITE);
tft.fillRect(shiftX + 185, 115, 3, 7, ILI9341_WHITE);
tft.drawFastHLine(shiftX + 65, 122, 123, ILI9341_WHITE);
for (int j = 0; j < 12; j++) {
hOffs = 10 * j;
tft.drawFastVLine(shiftX + 76 + hOffs, 117, 5, ILI9341_WHITE);
}
tft.setCursor(shiftX + 16, 126);
tft.print("SIG");
tft.setTextSize(1);
tft.setCursor(shiftX + 63, 106);
tft.print("0 5 7 9 +30");
tft.drawBitmap(6, 38, LUEEL_LOGO, 24, 24, ILI9341_YELLOW);
SetCaptainMinionText();
switchVFO(presentVFO, true); // 2nd param is TRUE to force all steps to be followed on startup
switchTuningStep(tuningStep, freqColor);
drawButton(0, BUTTON_COLOR1, ILI9341_BLACK, "VFO", vfoNames[0]);
drawButton(1, BUTTON_COLOR2, ILI9341_BLACK, receptionModeNames[VFOs[presentVFO].rMode], "");
drawButton(2, BUTTON_COLOR1, ILI9341_BLACK, "ATTEN", "OdB");
drawButton(3, BUTTON_COLOR2, ILI9341_BLACK, "RCVR", "MODE");
drawButton(4, BUTTON_COLOR1, ILI9341_BLACK, "NEXT", "MENU");
}
void loop() {
//Serial.println(freemem());
// check for screen touches only every ten cycles through the loop. This minimizes computational cycles, I think?
touchLoopCount += 1;
if (touchLoopCount > 9) {
touchLoopCount = 0;
if (ts.touched()) {
TS_Point p = ts.getPoint();
if ((p.x < 60) && (p.x > 12) && (lastButtonTouch == 0)) {
newButtonTouch = 1 + p.y / 64;
lastButtonTouch = newButtonTouch;
}
} else {
lastButtonTouch = 0;
}
}
// check if encoder has been rotated in the interrupt routine
// don't respond to the encoder if we are in MINION mode
// variable tuneDir: 1=CW, -1=CCW
if ((tuneDir != 0) && (presentCMMode != MODE_MINION)) {
if (encoderMode == ENCODERMODE_FREQUENCY) {
// adjust the frequency but only within the bounds of the amateur band
unsigned long newFrequency = VFOs[presentVFO].frequency + tuneDir * increments[tuningStep];
if (boundsFrequency(newFrequency)) {
//VFOs[presentVFO].frequency = newFrequency;
masterOscillator.setFreq(VFO_CLK, (VFOs[presentVFO].frequency + VFO_offset));
drawFreq(false);
}
//VFOs[presentVFO].frequency = VFOs[presentVFO].frequency + tuneDir * increments[tuningStep];
// send new frequency to transmitter, but only if we are in captain mode
if (presentCMMode == MODE_CAPTAIN) sendMessage(0x41, VFOs[presentVFO].frequency); //0x41="A"
masterOscillator.setFreq(VFO_CLK, (VFOs[presentVFO].frequency + VFO_offset));
drawFreq(false);
tuneDir = 0;
activityTimer = millis();
activityTimerON = true;
} else {
// adjust the tuning step
uint8_t step = tuningStep;
bool shouldUpdate = true;
if (tuneDir == 1) {
step += 1;
if (step > 4) {
step = 4;
shouldUpdate = false;
}
} else {
step -= 1;
if (step < 0) {
step = 0;
shouldUpdate = false;
}
}
tuneDir = 0;
activityTimer = millis();
activityTimerON = true;
if (shouldUpdate) switchTuningStep(step, ILI9341_RED);
}
}
//check encoder push button
int reading;
reading = digitalRead(FE_SW_PIN);
if (reading != lastFEButtonState) {
lastFEPBTime = millis();
}
if ((millis() - lastFEPBTime) > DEBOUNCE_DELAY) {
if (reading != FEButtonState) {
FEButtonState = reading;
if (FEButtonState == LOW) {
activityTimer = millis();
activityTimerON = true;
if (encoderMode == ENCODERMODE_FREQUENCY) {
encoderMode = ENCODERMODE_STEP;
switchTuningStep(tuningStep, ILI9341_RED);
} else {
encoderMode = ENCODERMODE_FREQUENCY;
switchTuningStep(tuningStep, freqColor);
}
}
}
}
lastFEButtonState = reading;
// redraw logo if time is up
if (blinkLogo) {
blinkyLogo += 1;
if (blinkyLogo > BLINKYLOGO_COUNT) {
tft.drawBitmap(6, 38, LUEEL_LOGO, 24, 24, ILI9341_YELLOW);
blinkLogo = false;
}
}
//process screen button pushes
if (newButtonTouch > 0) {
// hide the logo
tft.fillRect(6, 38, 24, 24, ILI9341_BLACK);
blinkyLogo = 0;
blinkLogo = true;
switch (presentMenuMode) {
case MENUMODE_FIRST:
{
switch (newButtonTouch) {
case 1: // switch VFOs
{
uint8_t newVFO = 0;
if (presentVFO == 0) {
newVFO = 1;
}
switchVFO(newVFO, false);
drawButton(0, BUTTON_COLOR1, ILI9341_BLACK, "VFO", vfoNames[newVFO]);
break;
}
case 2: // switch reception mode between USB LSB CW
{
uint8_t newReceptionMode = VFOs[presentVFO].rMode + 1;
if (newReceptionMode > RMODE_CW) newReceptionMode = 0;
updateReceptionMode(newReceptionMode);
drawButton(1, BUTTON_COLOR2, ILI9341_BLACK, receptionModeNames[newReceptionMode], "");
break;
}
case 3: // switch attenuator
{
if (attenuatorEngaged) {
attenuatorEngaged = false;
digitalWrite(FEA_PIN, LOW);
drawButton(2, BUTTON_COLOR1, ILI9341_BLACK, "ATTEN", "OFF");
} else {
attenuatorEngaged = true;
digitalWrite(FEA_PIN, HIGH);
drawButton(2, BUTTON_COLOR1, ILI9341_RED, "ATTEN", "10dB");
}
break;
}
case 4: // switch CAPTAIN, MINION, SPLIT
{
// don't allow user to put us in minion mode, only the transmitter can do that
if (presentCMMode == MODE_SPLIT) {
presentCMMode = MODE_CAPTAIN;
sendMessage(0x49, 0L);
sendMessage(0x41, VFOs[presentVFO].frequency); //0x41="A"
} else {
presentCMMode = MODE_SPLIT;
sendMessage(0x49, 2L);
}
SetCaptainMinionText();
break;
}
case 5:
{
// switch to next menu
presentMenuMode = MENUMODE_SECOND;
int textColor = ILI9341_BLACK;
if (VFOs[presentVFO].filterBank == 0) textColor = ILI9341_RED;
drawButton(0, BUTTON_COLOR1, textColor, bandNames[bankVFOs[0].band], "");
textColor = 0;
if (VFOs[presentVFO].filterBank == 1) textColor = ILI9341_RED;
drawButton(1, BUTTON_COLOR2, textColor, bandNames[bankVFOs[1].band], "");
textColor = 0;
if (VFOs[presentVFO].filterBank == 2) textColor = ILI9341_RED;
drawButton(2, BUTTON_COLOR1, textColor, bandNames[bankVFOs[2].band], "");
drawButton(3, BUTTON_COLOR2, ILI9341_BLACK, "SCAN", "FILT");
break;
}
}
break;
}
case MENUMODE_SECOND:
{
switch (newButtonTouch) {
case 1:
{
// select bank0
if (VFOs[presentVFO].filterBank != 0) {
drawButton(0, BUTTON_COLOR1, ILI9341_RED, bandNames[bankVFOs[0].band], "");
drawButton(1, BUTTON_COLOR2, ILI9341_BLACK, bandNames[bankVFOs[1].band], "");
drawButton(2, BUTTON_COLOR1, ILI9341_BLACK, bandNames[bankVFOs[2].band], "");
switchFilterBank(0);
}
break;
}
case 2:
{
// select bank1
if (VFOs[presentVFO].filterBank != 1) {
drawButton(0, BUTTON_COLOR1, ILI9341_BLACK, bandNames[bankVFOs[0].band], "");
drawButton(1, BUTTON_COLOR2, ILI9341_RED, bandNames[bankVFOs[1].band], "");
drawButton(2, BUTTON_COLOR1, ILI9341_BLACK, bandNames[bankVFOs[2].band], "");
switchFilterBank(1);
}
break;
}
case 3:
{
// select bank0
if (VFOs[presentVFO].filterBank != 2) {
drawButton(0, BUTTON_COLOR1, ILI9341_BLACK, bandNames[bankVFOs[0].band], "");
drawButton(1, BUTTON_COLOR2, ILI9341_BLACK, bandNames[bankVFOs[1].band], "");
drawButton(2, BUTTON_COLOR1, ILI9341_RED, bandNames[bankVFOs[2].band], "");
switchFilterBank(2);
}
break;
}
case 4: // scan band pass filters
{
// This code assumes a filter bank is never left empty. If it is, then the input will float and
// likely result in an erroneous reading.
// The 1.176 multiplier correctly scales the reading to fall within the 1 to 10 range to correspond
// to the ten bands
int reading;
reading = analogRead(BPF_PIN0);
int bank0 = int(1.176 * reading / 100);
reading = analogRead(BPF_PIN1);
int bank1 = int(1.176 * reading / 100);
reading = analogRead(BPF_PIN2);
int bank2 = int(1.176 * reading / 100);
// rebuild all the VFOs based on these scan results
VFOs[0].frequency = getStartFrequency(bank0);
VFOs[0].band = bank0;
VFOs[0].rMode = getRMode(bank0);
VFOs[0].filterBank = 0;
VFOs[1].frequency = VFOs[0].frequency;
VFOs[1].band = bank0;
VFOs[1].rMode = VFOs[0].rMode;
VFOs[1].filterBank = 0;
bankVFOs[0].frequency = VFOs[0].frequency;
bankVFOs[0].band = VFOs[0].band;
bankVFOs[0].rMode = VFOs[0].rMode;
bankVFOs[0].filterBank = 0;
bankVFOs[1].frequency = getStartFrequency(bank1);
bankVFOs[1].band = bank1;
bankVFOs[1].rMode = getRMode(bank1);
bankVFOs[1].filterBank = 1;
bankVFOs[2].frequency = getStartFrequency(bank2);
bankVFOs[2].band = bank2;
bankVFOs[2].rMode = getRMode(bank2);
bankVFOs[2].filterBank = 2;
presentVFO = 0;
switchVFO(presentVFO, true); // 2nd param is TRUE to force all steps to be followed after scanning the BF filters
// refresh the 3 band buttons
drawButton(0, BUTTON_COLOR1, ILI9341_RED, bandNames[bankVFOs[0].band], "");
drawButton(1, BUTTON_COLOR2, ILI9341_BLACK, bandNames[bankVFOs[1].band], "");
drawButton(2, BUTTON_COLOR1, ILI9341_BLACK, bandNames[bankVFOs[2].band], "");
switchTuningStep(tuningStep, freqColor);
break;
}
case 5:
{
// switch to next menu
presentMenuMode = MENUMODE_THIRD;
drawButton(0, BUTTON_COLOR1, ILI9341_BLACK, "ILLUM", "++");
drawButton(1, BUTTON_COLOR2, ILI9341_BLACK, "ILLUM", "--");
drawButton(2, BUTTON_COLOR1, ILI9341_BLACK, "FREQ", "COLOR");
drawButton(3, BUTTON_COLOR2, ILI9341_BLACK, "CWST", sideTones[sideToneFreq]);
break;
}
}
break;
}
case MENUMODE_THIRD:
{
switch (newButtonTouch) {
case 1:
{
// increase brightness
brightness += 1;
if (brightness > 10) brightness = 10;
analogWrite(TFT_LED_PIN, brightness * 25);
break;
}
case 2:
{
// decrease brightness
brightness -= 1;
if (brightness < 1) brightness = 1;
analogWrite(TFT_LED_PIN, brightness * 25);
break;
}
case 3:
{
freqIndex += 1;
if (freqIndex > 3) freqIndex = 0;
switch (freqIndex) {
case 0:
{
freqColor = 0x07E0; // green
break;
}
case 1:
{
freqColor = 0x780F; // purple
break;
}
case 2:
{
freqColor = 0xFD20; // orange
break;
}
case 3:
{
freqColor = 0x07FF; // cyan
break;
}
}
drawFreq(true);
switchTuningStep(tuningStep, freqColor);
break;
}
case 4:
{
// side tone frequency
sideToneFreq += 1;
if (sideToneFreq > 2) sideToneFreq = 0;
drawButton(3, BUTTON_COLOR2, ILI9341_BLACK, "CWST", sideTones[sideToneFreq]);
masterOscillator.setFreq(BFO_CLK, CW_BFO_FREQ - (400 + sideToneFreq * 300));
break;
}
case 5:
{
// switch to next menu
presentMenuMode = MENUMODE_FIRST;
drawButton(0, BUTTON_COLOR1, ILI9341_BLACK, "VFO", vfoNames[presentVFO]);
drawButton(1, BUTTON_COLOR2, ILI9341_BLACK, receptionModeNames[VFOs[presentVFO].rMode], "");
if (attenuatorEngaged) {
drawButton(2, BUTTON_COLOR1, ILI9341_RED, "ATTEN", "10dB");
} else {
drawButton(2, BUTTON_COLOR1, ILI9341_BLACK, "ATTEN", "OFF");
}
drawButton(3, BUTTON_COLOR2, ILI9341_BLACK, "RCVR", "MODE");
break;
}
}
break;
}
}
newButtonTouch = 0;
}
// check to see if it has been a long time since the user interacted with either encoder.
// if so, then write the present settings to EEPROM
if (activityTimerON) {
if ((millis() - activityTimer) > MEM_STALL_DELAY) {
activityTimerON = false;
promIndex += 1;
EESETTINGS EEParams;
EEParams.index = promIndex;
EEParams.vfoA.frequency = VFOs[0].frequency;
EEParams.vfoA.band = VFOs[0].band;
EEParams.vfoA.rMode = VFOs[0].rMode;
EEParams.vfoA.filterBank = VFOs[0].filterBank;
EEParams.bankVFO0.frequency = bankVFOs[0].frequency;
EEParams.bankVFO0.band = bankVFOs[0].band;
EEParams.bankVFO0.rMode = bankVFOs[0].rMode;
EEParams.bankVFO0.filterBank = 0;
EEParams.bankVFO1.frequency = bankVFOs[1].frequency;
EEParams.bankVFO1.band = bankVFOs[1].band;
EEParams.bankVFO1.rMode = bankVFOs[1].rMode;
EEParams.bankVFO1.filterBank = 1;
EEParams.bankVFO2.frequency = bankVFOs[2].frequency;
EEParams.bankVFO2.band = bankVFOs[2].band;
EEParams.bankVFO2.rMode = bankVFOs[2].rMode;
EEParams.bankVFO2.filterBank = 2;
EEParams.presentVFO = presentVFO;
EEParams.tuningStep = tuningStep;
EEParams.brightness = brightness;
EEParams.sideToneFreq = sideToneFreq;
EEPROM.put((promIndex % 28) * 36, EEParams);
}
}
// redraw the S meter every time through the loop
drawSMeter();
// check the serial port for incoming commands
int bytesInBuf = Serial.available();
if (bytesInBuf >= 9) {
int index = 0;
bool finished = false;
do {
if (Serial.peek() == 0x28) // 0x28 is the "(" character
{
finished = true;
} else {
Serial.read();
index += 1;
}
} while ((finished == false) && (index < bytesInBuf));
if (bytesInBuf - index >= 9) {
// if we get here, we've got a 9-byte segment in the buffer that begins with the "(" character
// read it all in and process it
char command;
uint32_t payload = 0;
Serial.read(); // that's the "(" character, we don't need it
command = Serial.read();
byte buf[6];
Serial.readBytes(buf, 6);
payload = ((uint32_t)buf[3] << 24) | ((uint32_t)buf[2] << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[0];
// process the checksum
int checksum = ((int16_t)buf[5] << 8) | (int16_t)buf[4];
checksum = 0; //TODO: checksum is ignored for now
if ((Serial.read() == 0x29) && (checksum == 0)) // that's the ")" character, we don't need it
{
// if we get here, we've processed a 9-byte block and it is good!
switch (command) {
case 'A':
{
//ignore this message unless we are in Minion mode
if ((presentCMMode == MODE_MINION) && (VFOs[presentVFO].frequency != payload)) {
if (boundsFrequency(payload)) {
//VFOs[presentVFO].frequency = payload;
masterOscillator.setFreq(VFO_CLK, (VFOs[presentVFO].frequency + VFO_offset));
drawFreq(false);
}
}
break;
}
case 'D':
{
// mute the receiver
masterOscillator.disable(BFO_CLK);
break;
}
case 'E':
{
// unmute the receiver
masterOscillator.enable(BFO_CLK);
break;
}
case 'F': // change signal modulation mode. 1=USB, 2=LSB, 3=CW
{
int newReceptionMode = (int)payload;
//ignore this message unless we are in Minion mode
if ((presentCMMode == MODE_MINION) && (VFOs[presentVFO].rMode != newReceptionMode)) {
updateReceptionMode(newReceptionMode);
drawButton(1, BUTTON_COLOR2, ILI9341_BLACK, receptionModeNames[newReceptionMode], "");
}
break;
}
case 'I': // change Captain/Minion mode. false=minion, true=captain, 1=captain, 2=minion
{
if ((payload == 2L) && (presentCMMode != MODE_SPLIT)) {
presentCMMode = MODE_SPLIT;
SetCaptainMinionText();
}
if ((payload == 1L) && (presentCMMode != MODE_CAPTAIN)) {
presentCMMode = MODE_CAPTAIN;
SetCaptainMinionText();
} else if ((payload == 0L) && (presentCMMode != MODE_MINION)) {
presentCMMode = MODE_MINION;
SetCaptainMinionText();
}
break;
}
}
}
}
}
}
unsigned long getStartFrequency(uint8_t band) {
switch (band) {
case 1: return BAND_160M_START;
case 2: return BAND_80M_START;
case 3: return BAND_60M_START;
case 4: return BAND_40M_START;
case 5: return BAND_30M_START;
case 6: return BAND_20M_START;
case 7: return BAND_17M_START;
case 8: return BAND_15M_START;
case 9: return BAND_12M_START;
case 10: return BAND_10M_START;
default: return 11111111UL; // should never get here, but if we do, return a strange frequency
}
}
int getRMode(int band) {
if ((band < 1) || (band > 10)) return RMODE_USB;
else return SSBModeByBand[band];
}
void drawSMeter() {
// based on adjustments to the AGC circuit, an S9 signal measures at 1.07V. That also appears to be the max, even with
// distortion. So just for kicks, set the meter to max out at that voltage (scale won't be right, but at least it will
// look pretty :(
numBars[barPointer] = int(4.67 * analogRead(S_METER_PIN) / 55); // a denominator of 55 gives about 18 bars
int xOffs = shiftX + 65;
barPointer += 1;
if (barPointer >= BAR_BUFFER) {
int numB = 0;
barPointer = 0;
int i;
int color = ILI9341_GREEN;
for (i = 0; i < BAR_BUFFER; i++) {
numB += numBars[i];
}
numB = numB / BAR_BUFFER;
for (i = 0; i < 25; i++) {
tft.fillRect(xOffs, 124, 3, 20, color);
if (i > 14) {
color = ILI9341_YELLOW;
}
if (i > 18) {
color = ILI9341_RED;
}
if (i >= numB) {
color = BG_COLOR;
}
xOffs += 5;
}
}
}
void updateReceptionMode(int rMode) {
VFOs[presentVFO].rMode = rMode;
switch (rMode) {
case RMODE_USB:
{
masterOscillator.setFreq(BFO_CLK, USB_IF_FREQ);
VFO_offset = USB_IF_FREQ;
break;
}
case RMODE_LSB:
{
masterOscillator.setFreq(BFO_CLK, LSB_IF_FREQ);
VFO_offset = LSB_IF_FREQ;
break;
}
case RMODE_CW:
{
masterOscillator.setFreq(BFO_CLK, CW_BFO_FREQ - (400 + sideToneFreq * 300));
VFO_offset = CW_IF_FREQ;
break;
}
}
masterOscillator.setFreq(VFO_CLK, (VFOs[presentVFO].frequency + VFO_offset));
}
void switchVFO(int newVFONumber, bool doItAll) {
// if VFOs are same band and same reception mode, then just switch the VFO frequency.
// If VFOs are same band but NOT same reception mode, then just change reception mode.
// Otherwise chage everything
if (!doItAll && (VFOs[0].band == VFOs[1].band)) {
if (VFOs[0].rMode == VFOs[1].rMode) {
presentVFO = newVFONumber;
masterOscillator.setFreq(VFO_CLK, (VFOs[presentVFO].frequency + VFO_offset));
// send new frequency to transmitter, but only if we are in captain mode
if (presentCMMode == MODE_CAPTAIN) sendMessage(0x41, VFOs[presentVFO].frequency); //0x41="A"
drawFreq(true);
return;
} else {
presentVFO = newVFONumber;
updateReceptionMode(VFOs[presentVFO].rMode);
drawFreq(true);
return;
}
}
// if we get here, then we're changing bands (meaning changing filter banks)
presentVFO = newVFONumber;
switchFilterBank(VFOs[presentVFO].filterBank);
drawFreq(true);
}
void commandRelays(int relayStatus) {
SPI.beginTransaction(SPISettings(14000000, MSBFIRST, SPI_MODE0));
digitalWrite(MAX_CS_PIN, LOW);
SPI.transfer(relayStatus);
digitalWrite(MAX_CS_PIN, HIGH);
SPI.endTransaction();
}
void switchFilterBank(int newBank) {
// mute the receiver while we are doing all of these ops
commandRelays(0x02);
delay(RELAY_DELAY);
commandRelays(0x00);
delay(RELAY_DELAY);
commandRelays(0x08);
delay(RELAY_DELAY);
commandRelays(0x00);
// place current vfo parameters into filter bank vfo parameters for existing bank. Band and filterBank are already the same.
bankVFOs[VFOs[presentVFO].filterBank].frequency = VFOs[presentVFO].frequency;
bankVFOs[VFOs[presentVFO].filterBank].rMode = VFOs[presentVFO].rMode;
// copy new bank vfo parameters into current vfo parameters
VFOs[presentVFO].frequency = bankVFOs[newBank].frequency;
VFOs[presentVFO].band = bankVFOs[newBank].band;
VFOs[presentVFO].rMode = bankVFOs[newBank].rMode;
VFOs[presentVFO].filterBank = bankVFOs[newBank].filterBank;
updateReceptionMode(VFOs[presentVFO].rMode);
// take receiver out of mute by switching in the appropriate new filter bank
switch (newBank) {
case 0:
{
commandRelays(0x01);
delay(RELAY_DELAY);
commandRelays(0x00);
delay(RELAY_DELAY);
commandRelays(0x08);
delay(RELAY_DELAY);
commandRelays(0x00);
break;
}
case 1:
{
commandRelays(0x02);
delay(RELAY_DELAY);
commandRelays(0x00);
delay(RELAY_DELAY);
commandRelays(0x04);
delay(RELAY_DELAY);
commandRelays(0x00);
delay(RELAY_DELAY);
commandRelays(0x10);
delay(RELAY_DELAY);
commandRelays(0x00);
delay(RELAY_DELAY);
commandRelays(0x80);
delay(RELAY_DELAY);
commandRelays(0x00);
break;
}
case 2:
{
commandRelays(0x02);
delay(RELAY_DELAY);
commandRelays(0x00);
delay(RELAY_DELAY);
commandRelays(0x04);
delay(RELAY_DELAY);
commandRelays(0x00);
delay(RELAY_DELAY);
commandRelays(0x20);
delay(RELAY_DELAY);
commandRelays(0x00);
delay(RELAY_DELAY);
commandRelays(0x40);
delay(RELAY_DELAY);
commandRelays(0x00);
break;
}
}
drawFreq(true);
}
void drawFreq(bool drawAll) {
// draw frequency digits.
int xOffset = shiftX;
unsigned long scale = 10000000;
unsigned long f = VFOs[presentVFO].frequency;
int oldDigit = 0;
int newDigit = 0;
for (int i = 0; i < 7; i++) {
oldDigit = ((oldFreq / scale) % 10);
newDigit = ((f / scale) % 10);
if ((oldDigit != newDigit) || (drawAll)) {
tft.fillRect(xOffset, 32, 32, 42, BG_COLOR);
switch (newDigit) {
case 0:
{
if (i > 0) {
tft.drawBitmap(xOffset, 32, DIGIT_0, 32, 42, freqColor);
}
break;
}
case 1:
{
tft.drawBitmap(xOffset, 32, DIGIT_1, 32, 42, freqColor);
break;
}
case 2:
{
tft.drawBitmap(xOffset, 32, DIGIT_2, 32, 42, freqColor);
break;
}
case 3:
{
tft.drawBitmap(xOffset, 32, DIGIT_3, 32, 42, freqColor);
break;
}
case 4:
{
tft.drawBitmap(xOffset, 32, DIGIT_4, 32, 42, freqColor);
break;
}
case 5:
{
tft.drawBitmap(xOffset, 32, DIGIT_5, 32, 42, freqColor);
break;
}
case 6:
{
tft.drawBitmap(xOffset, 32, DIGIT_6, 32, 42, freqColor);
break;
}
case 7:
{
tft.drawBitmap(xOffset, 32, DIGIT_7, 32, 42, freqColor);
break;
}
case 8:
{
tft.drawBitmap(xOffset, 32, DIGIT_8, 32, 42, freqColor);
break;
}
case 9:
{
tft.drawBitmap(xOffset, 32, DIGIT_9, 32, 42, freqColor);
break;
}
}
}
xOffset += 32;
if (i == 1) xOffset += 8;
if (i == 4) xOffset += 8;
scale = scale / 10;
}
// draw the decimal points
tft.fillRect(shiftX + 66, 70, 4, 4, freqColor);
tft.fillRect(shiftX + 170, 70, 4, 4, freqColor);
oldFreq = f;
}
void switchTuningStep(unsigned int newStep, unsigned int foreColor) {
int xOffset = shiftX + 72 + 32 * tuningStep;
if (tuningStep > 2) xOffset += 8;
tft.fillRect(xOffset, 76, 32, 3, BG_COLOR);
tuningStep = newStep;
xOffset = shiftX + 72 + 32 * tuningStep;
if (tuningStep > 2) xOffset += 8;
tft.fillRect(xOffset, 76, 32, 3, foreColor);
}
void sendMessage(byte command, uint32_t payload) {
byte buf[9];
buf[0] = 0x28; // "("
buf[1] = command;
buf[2] = payload & 255;
buf[3] = (payload >> 8) & 255;
buf[4] = (payload >> 16) & 255;
buf[5] = (payload >> 24) & 255;
buf[6] = 0x00; // checksum byte 0 -not used
buf[7] = 0x00; // checksum byte 1 -not used
buf[8] = 0x29; // ")"
Serial.write(buf, sizeof(buf));
}
// returns true if frequency was updated to newFrequency
// returns false
bool boundsFrequency(uint32_t newFrequency) {
switch (VFOs[presentVFO].band) {
case 1:
{
VFOs[presentVFO].frequency = min(BAND_160M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_160M_START, VFOs[presentVFO].frequency);
break;
}
case 2:
{
VFOs[presentVFO].frequency = min(BAND_80M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_80M_START, VFOs[presentVFO].frequency);
break;
}
case 3:
{
VFOs[presentVFO].frequency = min(BAND_60M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_60M_START, VFOs[presentVFO].frequency);
break;
}
case 4:
{
VFOs[presentVFO].frequency = min(BAND_40M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_40M_START, VFOs[presentVFO].frequency);
break;
}
case 5:
{
VFOs[presentVFO].frequency = min(BAND_30M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_30M_START, VFOs[presentVFO].frequency);
break;
}
case 6:
{
VFOs[presentVFO].frequency = min(BAND_20M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_20M_START, VFOs[presentVFO].frequency);
break;
}
case 7:
{
VFOs[presentVFO].frequency = min(BAND_17M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_17M_START, VFOs[presentVFO].frequency);
break;
}
case 8:
{
VFOs[presentVFO].frequency = min(BAND_15M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_15M_START, VFOs[presentVFO].frequency);
break;
}
case 9:
{
VFOs[presentVFO].frequency = min(BAND_12M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_12M_START, VFOs[presentVFO].frequency);
break;
}
case 10:
{
VFOs[presentVFO].frequency = min(BAND_10M_END, newFrequency);
VFOs[presentVFO].frequency = max(BAND_10M_START, VFOs[presentVFO].frequency);
break;
}
}
if (newFrequency == VFOs[presentVFO].frequency) return true;
else return false;
}
void SetCaptainMinionText() {
tft.fillRect(shiftX + 5, 9, 84, 16, BG_COLOR);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(shiftX + 5, 9);
switch (presentCMMode) {
case MODE_MINION:
{
tft.print("MINION");
break;
}
case MODE_CAPTAIN:
{
tft.print("CAPTAIN");
break;
}
case MODE_SPLIT:
{
tft.print("SPLIT");
break;
}
}
}
void drawButton(int position, int bColor, int tColor, char *firstLine, char *secondLine) {
tft.fillRect(position * 64, 180, 64, 36, bColor);
tft.setTextSize(2); //size 2 characters take up a space 12px wide by 16px tall
tft.setTextColor(tColor);
int n = strlen(firstLine);
if (secondLine == "") {
tft.setCursor(position * 64 + textOffsets[n], 190);
tft.print(firstLine);
} else {
tft.setCursor(position * 64 + textOffsets[n], 182);
tft.print(firstLine);
n = strlen(secondLine);
tft.setCursor(position * 64 + textOffsets[n], 198);
tft.print(secondLine);
}
}
ISR(PCINT2_vect) {
// lesson learned - do not call the masterOscillator.setFreq() here, do it outside this interrupt loop
// Doing so causes the program to crash because the chip cannot process complex functions within
// an interrupt loop. So just set the volatile variable tuneDir.
unsigned char result = rFreq.process();
if (result) {
if (result == DIR_CW) {
tuneDir = 1;
} else if (result == DIR_CCW) {
tuneDir = -1;
}
}
}
Comments
Post a Comment