Rotary encoder for Ham radio

JML
This is the manual code. The scan code uses pin 12.
I haven't figured out how to combine them yet.
Mike

can you share the scan code?

functional code is functional. If you are satisfied with the function your current code-version is guess what ? functional.

If you feel ready to learn something new then you can dive into learning arrays.

best regards Stefan

JML
MY code has changed alot since my last scan version.
Under "void enabl();" is where the scan operation occurs.
I have to back up and make a better framework as to what I want to do.
It has been very much a patchwork creation so far.
But if you want to see what I did, have a look.



#include <LiquidCrystal.h>
const int rs = 7, en = 8, d4 = 3, d5 = 4, d6 = 5, d7 = 6;  //set up lcd pins
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

const byte  buttonPin = A2;    // the pin to which the pushbutton is attached
const byte ledPin = 13;       // the pin to which the LED is attached

bool buttonState = 0;         // current state of the button
bool lastButtonState = 0;     // previous state of the button

bool mode = false;



int sig = 12;  //squelch input
int clk = 9;   //pll programming pins
int dat = 10;  //pll programming pins
int ena = 11;  //pll programming pins
//int led = 13;
int sum = 1;   //scan channel
int encoder = 0;   //rotary encoder status
//char freq;
//int buttonState;             // the current reading from the input pin
//int lastButtonState = LOW;   // the previous reading from the input pin
unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers


void setup() {
  pinMode(sig, INPUT_PULLUP);
  pinMode(clk, OUTPUT);
  pinMode(dat, OUTPUT);
  pinMode(ena, OUTPUT);
  //pinMode(led, OUTPUT);
  digitalWrite(clk, LOW);
  digitalWrite(dat, LOW);
  digitalWrite(ena, LOW);
  Serial.begin(9600);
  lcd.begin(16, 2);
  // initialize the button pin as a input with internal pullup enabled
  pinMode(buttonPin, INPUT_PULLUP);
  // initialize the LED as an output:
  pinMode(ledPin, OUTPUT);

}

void loop() {
  static unsigned long timer = 0;
  unsigned long interval = 50;  // check switch 20 times per second
  if (millis() - timer >= interval)
  {
    timer = millis();
    // read the pushbutton input pin:
    buttonState = digitalRead(buttonPin);
    // compare the new buttonState to its previous state
    if (buttonState != lastButtonState)
    {
      if (buttonState == LOW)
      {
        // if the current state is LOW then the button
        // went from off to on:
        digitalWrite(ledPin, !digitalRead(ledPin)); // toggle the output
        mode = !mode;
        if (mode == true)
        {
          lcd.setCursor(0, 1) ;  //print Manual on 2nd line of lcd
          lcd.print("Manual");
        }
        else
        {
          lcd.setCursor(0, 1) ;  //print Scan on 2nd line of lcd
          lcd.print("Scan  ");
        }
      }
    }
    // save the current state as the last state,
    //for next time through the loop
    lastButtonState = buttonState;
  }
  //lcd.clear();
  lcd.setCursor(0, 0) ;

  switch (sum) {      //which channels to scan
    case 1:
      five110();
      lcd.print("145.110Mhz");
      break;
    case 2:
      five170();
      lcd.print("145.170Mhz");
      break;
    case 3:
      five210();
      lcd.print("145.210Mhz");
      break;
    case 4:
      five230();
      lcd.print("145.230Mhz");
      break;


   default:
      five230();
      lcd.print("145.230Mhz");
      // if nothing else matches, do the default
      // default is optional
      break;
      
  }

  enabl();                    // go to Enable pulse

}

// zero:  load 0 bit
void z() {
  digitalWrite(dat, LOW);    // Load 0 on dat
  digitalWrite(clk, HIGH);   // bring clock HIGH
  digitalWrite(clk, LOW);    // Then back low
}
// one:  load 1 bit
void o() {
  digitalWrite(dat, HIGH);   // Load 1 on DAT
  digitalWrite(clk, HIGH);   // Bring pin CLOCK high
  digitalWrite(clk, LOW);    // Then back low
}
void enabl() {
  digitalWrite(ena, HIGH);   // Bring Enable high
  digitalWrite(ena, LOW);    // Then back low
  delay(100);
  while (digitalRead(12) == 1) ; //during signal detected, wait.
  ++sum;                      //increment scan
  if (sum > 4)               //reset channel to 1
  {
    sum = 1;
  }
  delay (100);
}



// programmed channels
void five110() {
  z();         // 8192
  z();         // 4096

  o();         // 2048
  o();         // 1024
  z();         // 512
  z();         // 256

  z();         // 128
  o();         // 64
  o();         // 32
  o();         // 16

  o();         // 8
  z();         // 4
  o();         // 2
  o();         // 1
}

void five170() {
  z();         // 8192
  z();         // 4096

  o();         // 2048
  o();         // 1024
  z();         // 512
  z();         // 256

  o();         // 128
  z();         // 64
  z();         // 32
  z();         // 16

  z();         // 8
  o();         // 4
  o();         // 2
  o();         // 1
}

void five210() {
  z();         // 8192
  z();         // 4096

  o();         // 2048
  o();         // 1024
  z();         // 512
  z();         // 256

  o();         // 128
  z();         // 64
  z();         // 32
  z();         // 16

  o();         // 8
  o();         // 4
  o();         // 2
  o();         // 1
}

void five230() {
  z();         // 8192
  z();         // 4096

  o();         // 2048
  o();         // 1024
  z();         // 512
  z();         // 256

  o();         // 128
  z();         // 64
  z();         // 32
  o();         // 16

  z();         // 8
  z();         // 4
  o();         // 2
  o();         // 1
}



void seven015() {
  z();         // 8192
  z();         // 4096

  o();         // 2048
  o();         // 1024
  z();         // 512
  o();         // 256

  o();         // 128
  o();         // 64
  o();         // 32
  o();         // 16

  o();         // 8
  z();         // 4
  z();         // 2
  z();         // 1
}

I doubt that. It takes some time for the PLL to achieve a locked condition. You'll fly right by an active channel if you don't wait for that. Normally, PLL lock is a digital signal that can be read by the controller microprocessor.

JML
Not sure if this is a working sketch.
been a while since i used it.
Mike

JML
Does this help in showing what I would like to do ?

this is what I suspect too. The question is how long does the code need to wait after setting a channel to await confirmation of the status and how is the hardware solution that was built interfering with that signal? (Since it seems there is now a 3s hardware delay/buffer).

Mike,

you should always post your actual code-version. Now I'm completely confused.
Do you still use the shiftout about which you wrote "it works"
or did you roll back to the o() and z()-Version?
The users that follow your thread know your code so you really don't have to reduce the code. It will be much easier if you always post that code-version you are working on.

This can include testcodes if you try something very new.

In all other cases writing the code consequently in functions means if you have tested one function throughly and it is working as expected and reliably
save the code-version
use save as.... with a different file name and then a new functionality is coded into guess what?:

a new function

best regards Stefan

Hi - no this does not really help.

This is what I got so far:

The Requirements:

  • The system offers three modes: Manual, Preset, and Auto-scan to tune to various channels
  • the relationship between a channel and a frequency is given by the formula frequency = (channel * .005) + 129.135
  • when you turn on the system, it tunes to the last used frequency and mode (so the frequency and mode last used need to be saved in permanent memory).
  • upon first use, a default channel is selected (3576) and the initial mode is ???
  • a momentary push button (mode) is used to circle through modes in that order Manual ➜ Preset ➜ Auto-scan
  • Upon entering a mode, the currently selected frequency remains selected.
  • The current mode and frequency are always displayed on a LCD

In Manual Mode (VFO mode)

  • A rotary encoder is the user interface to go up or down through discrete frequencies between the 140 to 150 Mhz frequency span.
  • The step size is 5Khz.
  • No check is done on the presence of a signal.

In Preset mode

  • A rotary encoder is the user interface to go up or down through a list of known fixed frequencies .
  • No check is done on the presence of a signal.
  • A momentary push button (skip/unskip) is used to toggle the exclusion status of the currently selected channel.
  • the LCD will display an indication of the exclusion status for the selected channel.

In Auto-scan mode

  • The system starts on the currently selected frequency
  • If the squelch Indicator (connected to pin 12) is HIGH, the signal is considered OK and the frequency does not change.
  • If the squelch Indicator (connected to pin 12) is LOW, the signal is considered not OK and the frequency changes to the next upper pre-set frequency that is not excluded.
  • Upon reaching the largest pre-set frequency, the system moves to the lowest pre-set frequency that is not excluded
  • if all pre-set frequencies have been excluded the system stays on the current frequency regardless of the presence of signal on the squelch Indicator
  • After selecting a new channel, the squelch Indicator (connected to pin 12) needs ??? ms to be considered valid to use for making a decision to switch to the next possible channel.

How does this sound?

Where I'm confused is in the Auto-scan mode, how fast does the squelch Indicator react on pin 12 when you select a new Frequency. Do you need to "wait a bit" after setting a Frequency before trusting the indicator and if so how long is "a bit" ?

1 Like

JML
I would say about 20 ms for frequency settling. after PLL load.
You have a good understanding of my project objectives.
Mike

Stefan,
Message #66 is my latest version, manual only tuning.
That version I need for it to pause loading the PLL once it has been changed in frequency.
It presently continuously loads the PLL resulting in a "pop pop " noise in the speaker.
I would like to refine that. then reintroduce the scan function.
Maybe my design tactics need refining as well. I am open to all advice.
I should of not published the old code, because it really has no present relevance, but I wanted to show JML an example of the scan function.
Mike

Hi Mike,

of course you are free to drop or keep or modify any code-version in any direction that you want. And even the way you develop code can be very different. So the following is just meant as a suggestion or some kind of introduction to a method.

You are already using functions and using functions with parameters can me made the base principle to all parts of the code.

For example setting the receiver to a certain frequency is one senseful unit.

So one function - you named it "upload()" does this adjusting the PLL to a certain frequency. Using parameters upload(freq);
enables

  • that the code is easier to understand (the variable named freq is used as input to the function upload()

using the parametr makes the use of the function upload more universal because you could use it in this way

upload(myManualFreq);

upload(freq);

upload(myScanDefaultFreq);

You can imagine this similar as you can connect different antennas to your tranceiver.

Dividing the code into such functional sub-units
set frequency: function upload(freq)
check if button is pressed: checkbutton(myButton_Pin)

and then put all parts needed for manual scanning into another function manualScanning()

Then you can control the mode with a variable

void loop() {
	....

	if (mode == manual) {
	  manualScanning();
    }

    ....
}

if you don't want to work on the manual scan mode the one singular place where this function is called is commented out

void loop() {
	....

	if (mode == manual) {
      // commented out to just not compile it 
      //but it is still present in the source-code
	  //manualScanning();   
    }

    ....
}

This is a case where you simply post the code-version that is creating this "pop pop" noise comibined with the question

"what do I have to change to stop the pop (-music and replace it by funk ehm no just kidding :wink: )

In this case it is something simple as an if-condition

if (freq != lastFreq) {
  upload();
  lastFreq = freq;
}

as long as you don't change variable freq the upload()-function is not called again ==> no PLL-signals no "pop-pop"-noise

You wrote that you successfully tested the shiftout-function.
Well if adjusting the frequency works using the shiftout-function
it works. You could keep it inside the code.

But all these are just suggestions. You can proceed in every way you ever want.

best regards Stefan

I guess you mean post #65
Post #65 is a posting that contains code.

Still I'm confused

So in post #65 this an old codeversion showong your attempt to write the scan-code?

again:
post in a

NEW posting

that code-version you are currently working on.

It is not required to post a new posting after each single small change.
but a snap-shot inbetween that shows if you are using
shiftout or old o() and z()-functions
using switch-case for setting up the frequency
or using an array.

best regards Stefan

OK
I'd try to put something together this morning and post it for review

Here is how I would structure the code with a few files all in the same HamRadio directory.

dependency on the following libraries:
Encoder ➜ Encoder Library, for Measuring Quadarature Encoded Position or Rotation Signals
easyRun ➜// GitHub - bricoleau/easyRun: Doing sereval things at the same time becomes so easy
LiquidCrystal_I2C ➜ GitHub - fdebrabander/Arduino-LiquidCrystal-I2C-library: Library for the LiquidCrystal LCD display connected to an Arduino board.

You have a difference LCD than mine, so would need to adapt that piece
➜ change the initLcd() function to include what's required for your LCD initialisation
➜ modify the screenConfig.h file to change number of lines and columns if you have a 1602
custom char definition might be library dependant. Will need to be adapted.
clear(), setCursor() and print() should be supported by all libraries I've seen so the rest of the code should be fine.

in the setup() function there is this line:

  pinMode(squelchPin, INPUT_PULLUP); // or INPUT depending on the system you connect to

it might just be INPUT depending on how your external device works. (I simulated this by connecting a button).

There is NO control of any external device, the tuneToCurrentFrequencyKHz () function is empty at the moment

// Drive the external hardware to set the frequency
void tuneToCurrentFrequencyKHz(uint32_t freqKHz) {
  // TO DO
  Serial.print(F("Setting Frequency to ")); Serial.print(freqKHz); Serial.println(F(" KHz"));

}

Very lightly tested.

HamRadio.ino

#include <EEPROM.h>
#include <Encoder.h>                // https://www.pjrc.com/teensy/td_libs_Encoder.html
#include <easyRun.h>                // https://github.com/bricoleau/easyRun
#include <LiquidCrystal_I2C.h>      // https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library

#include "screenConfig.h"           // LCD attributes
#include "frequency.h"              // frequency and channel management

// Types
enum t_mode : uint8_t {PRESET, MANUAL, AUTOSCAN, NO_MODE} currentMode = PRESET;

// constants

// The Pins
const uint8_t DTPin       = 2;            // Encoder DT
const uint8_t CLKPin      = 3;            // Encoder CLK
const uint8_t SWPin       = 4;            // Encoder Switch  (pullup activated by the library) 
const uint8_t piezoPin    = 5;            // small audio feedback pin --- piezo --- R ---- GND
const uint8_t skipPin     = 6;            // momentary button     pin --- button --- GND (pullup activated by the library) 
const uint8_t squelchPin  = 12;           // squelch signal Indicator. HIGH means signal is OK

// The modes' label
const char* modeString[] = {"PRESET  ", "MANUAL  ", "AUTOSCAN"};

// instances
LiquidCrystal_I2C lcd(LCD_ADRRESS, LCD_COL, LCD_LINES);
Encoder encoder(DTPin, CLKPin);
button modeButton(SWPin);
button skipButton(skipPin);


// Utility functions

void initLcd() {
  lcd.begin();
  lcd.backlight();
}


void initScreen() {
  lcd.clear();
  lcd.setCursor((LCD_COL - 14) / 2, 0); lcd.print(F("** HAMRADIO **"));
  lcd.createChar (0, lock);
  delay(1000);
  lcd.clear();
}

void configure() {
  encoder.write(0);
  currentChannelIndex = defaultChannelIndex;
  currentFrequencyKHz = channelIndexToFrequencyKHz(currentChannelIndex);
  initScreen();

  // update everything with the current values
  updateMode();
  updateChannel();
  updateFrequency();
  updateSquelchStatus();
}

void updateMode() {
  static t_mode oldMode = NO_MODE;
  if (currentMode != oldMode) { // refresh needed
    oldMode = currentMode;
    lcd.setCursor(0, 0);
    lcd.print(modeString[currentMode]); // 8 characters used from cursor 0 to 7
  }
}

void updateChannel() {
  static uint16_t oldChannelIndex = 0xFFFF;
  static bool oldChannelEnabled = true;
  if (currentChannelIndex != unknownChannelIndex) {
    if ((currentChannelIndex != oldChannelIndex) || (presetChannels[currentChannelIndex].enabled != oldChannelEnabled)) { // refresh needed
      oldChannelIndex = currentChannelIndex;
      oldChannelEnabled = presetChannels[currentChannelIndex].enabled;
      lcd.setCursor(LCD_COL - 6, 0);
      lcd.print(F("      ")); // erase 6 chars
      lcd.setCursor(LCD_COL - 6, 0);
      lcd.write(!presetChannels[currentChannelIndex].enabled ? '[' : ' ');
      lcd.print(presetChannels[currentChannelIndex].channel);
      lcd.write(!presetChannels[currentChannelIndex].enabled ? ']' : ' ');
    }
  } else {
    if (oldChannelIndex != currentChannelIndex) {
      oldChannelIndex = currentChannelIndex;
      lcd.setCursor(LCD_COL - 6, 0);
      lcd.print(F("      ")); // erase 6 chars
    }
  }
}

void updateFrequency() {
  static uint32_t oldFrequency = unknownFrequencyKHz;
  if (currentFrequencyKHz != oldFrequency) { // refresh needed
    oldFrequency = currentFrequencyKHz;
    squelchChrono = millis();

    tuneToCurrentFrequencyKHz(currentFrequencyKHz);

    lcd.setCursor(LCD_COL - 7, 1);
    lcd.print("       "); // erase 7 chars
    lcd.setCursor(LCD_COL - 7, 1);
    lcd.print(currentFrequencyKHz / 1000.0, 3);
  }
}


void updateSquelchStatus() {
  static uint8_t oldSquelchStatus = 0xFF;
  uint8_t squelchStatus = digitalRead(squelchPin);
  if (squelchStatus != oldSquelchStatus) {
    oldSquelchStatus = squelchStatus;
    lcd.setCursor(0, 1);
    if (squelchStatus == HIGH) {
      lcd.write(squelchOK);
    } else {
      lcd.write(squelchKO);
    }
  }
}


void newModeSelected(t_mode __attribute__((unused)) oldMode) {

  // if something mode specific needs to be done when transitioning from oldMode to currentMode, do it there
  switch (currentMode) {
    case PRESET:    break;
    case MANUAL:    break;
    case AUTOSCAN:  break;
    case NO_MODE:   break; // should not happen
  }

  // this is done for all the modes
  encoder.write(0);
  updateMode();
}

void checkMode() {
  t_mode oldMode  = currentMode;
  if (modeButton) {
    switch (currentMode) {
      case PRESET:    currentMode = MANUAL; break;
      case MANUAL:    currentMode = AUTOSCAN; break;
      case AUTOSCAN:  currentMode = PRESET; break;
      case NO_MODE:   currentMode = PRESET; break; // should not happen
    }
    newModeSelected(oldMode); // do what's required to setup the new mode
  }
}

// ------- The state Machine -------

void runPreset() {
  long newEncoderPosition = encoder.read() >> 1; // divide by 2 as I get 2 ticks for one click with mine
  if (newEncoderPosition != 0) {
    tone(piezoPin, 300, 10);
    if (newEncoderPosition > 0) { // we want to change the preset UP
      while (newEncoderPosition--) currentChannelIndex = nextChannelIndex(currentChannelIndex);
    } else if (newEncoderPosition < 0) { // we want to change the preset DOWN
      while (newEncoderPosition++) currentChannelIndex = previousChannelIndex(currentChannelIndex);
    }
    currentFrequencyKHz = channelIndexToFrequencyKHz(currentChannelIndex);
    encoder.write(0);
  }

  if (skipButton) { // (currentChannelIndex != unknownChannelIndex) &&
    tone(piezoPin, 3000, 30);
    presetChannels[currentChannelIndex].enabled = 1 - presetChannels[currentChannelIndex].enabled;
  }
}

void runManual() {
  long newEncoderPosition = encoder.read() >> 1; // divide by 2 as I get 2 ticks for one click with mine
  if (newEncoderPosition != 0) {
    tone(piezoPin, 600, 10);
    if (newEncoderPosition > 0) { // we want to change the frequency UP
      while (newEncoderPosition--) {
        currentFrequencyKHz += frequencyStepKHz;
        if (currentFrequencyKHz > maxFrequencyKHz) {
          currentFrequencyKHz = maxFrequencyKHz;
          break;
        }
      }
    } else if (newEncoderPosition < 0) { // we want to change the frequency DOWN
      while (newEncoderPosition++) {
        currentFrequencyKHz -= frequencyStepKHz;
        if (currentFrequencyKHz < minFrequencyKHz) {
          currentFrequencyKHz = minFrequencyKHz;
          break;
        }
      }
    }
    currentChannelIndex = channelForFrequencyKHz(currentFrequencyKHz); // if it's a known channel, make note of it
    encoder.write(0);
  }

  if ((currentChannelIndex != unknownChannelIndex) && skipButton) {
    tone(piezoPin, 3000, 30);
    presetChannels[currentChannelIndex].enabled = 1 - presetChannels[currentChannelIndex].enabled;
  }
}

void runAutoscan() {
  if (millis() - squelchChrono >= squelchDelay) {
    // we have been long enough at this frequency, check if squelch says it's not active
    if (digitalRead(squelchPin) == LOW) { // HIGH if currentFrequencyKHz is active
      // time to go to next active channel
      currentChannelIndex = nextEnabledChannelIndex(currentChannelIndex);
      currentFrequencyKHz = channelIndexToFrequencyKHz(currentChannelIndex);
    }
  }
}

void runMode() {
  switch (currentMode) {
    case PRESET:    runPreset(); break;
    case MANUAL:    runManual(); break;
    case AUTOSCAN:  runAutoscan(); break;
    case NO_MODE:   break;
  }
}

void setup() {
  pinMode(squelchPin, INPUT_PULLUP); // or INPUT depending on the system you connect to
  pinMode(piezoPin, OUTPUT);

  Serial.begin(115200); Serial.println();
  initLcd();
  configure();
}

void loop() {
  easyRun();    // handle buttons update
  checkMode();
  runMode();
  updateChannel();
  updateFrequency();
  updateSquelchStatus();
}

frequency.cpp

#include <Arduino.h>
#include "frequency.h"

t_channel presetChannels[] = {
  {3195, true},
  {3199, true},
  {3203, true},
  {3207, true},
  {3211, true},
  {3215, true},
  {3219, true},
  {3223, true},
  {3227, true},
  {3231, true},
  {3235, true},
  {3239, true},
  {3243, true},
  {3247, true},
  {3251, true},
  {3255, true},
  {3259, true},
  {3263, true},
  {3267, true},
  {3271, true},
  {3495, true},
  {3498, true},
  {3501, true},
  {3504, true},
  {3507, true},
  {3510, true},
  {3513, true},
  {3516, true},
  {3519, true},
  {3522, true},
  {3525, true},
  {3528, true},
  {3531, true},
  {3534, true},
  {3537, true},
  {3540, true},
  {3543, true},
  {3546, true},
  {3549, true},
  {3552, true},
  {3555, true},
  {3558, true},
  {3561, true},
  {3564, true},
  {3567, true},
  {3570, true},
  {3573, true},
  {3576, true},     // index 47 -> the default channel ?
  {3579, true},
  {3582, true},
  {3585, true},
  {3588, true},
  {3591, true},
  {3594, true},
  {3597, true},
  {3600, true},
  {3603, true},
  {3606, true},
  {3609, true},
  {3612, true},
  {3615, true},
  {3618, true},
  {3621, true},
  {3624, true},
  {3627, true},
  {3630, true},
  {3633, true},
  {3636, true},
  {3639, true},
  {3642, true},
  {3645, true},
  {3648, true},
  {3651, true},
  {3477, true},
  {3483, true},
  {3489, true},
  {3465, true},
  {3696, true},
};

const size_t  defaultChannelIndex = 47;
size_t presetChannelsCount = sizeof presetChannels / sizeof * presetChannels;

const uint16_t unknownChannelIndex = 0xFFFF;
const uint32_t unknownFrequencyKHz = 0xFFFFFFFF;
const uint32_t frequencyStepKHz = 5; // 5KHz steps

const uint32_t squelchDelay = 20;       // how long to wait in ms to decide if frequency is OK
uint32_t squelchChrono;                 // when we tuned to a new Frequency

uint16_t currentChannelIndex;           // unknownChannelIndex if channel not in list

const uint32_t minFrequencyKHz = 140000;   // we handle from 140 Mhz
const uint32_t maxFrequencyKHz = 150000;   // to 150 Mhz

uint32_t currentFrequencyKHz;            // frequency in KHz

uint32_t channelIndexToFrequencyKHz(uint16_t index) {
  return 5ul * presetChannels[index].channel + 129135ul;
}

uint16_t frequencyKHzToChannelIndex(uint32_t freq) {
  uint16_t index = 0xFFFF;
  for (uint16_t i = 0; i < presetChannelsCount; i++) {
    if (channelIndexToFrequencyKHz(presetChannels[i].channel) == freq) {
      index = i;
      break;
    }
  }
  return index;
}

uint16_t nextChannelIndex(uint16_t anIndex) {
  uint16_t nextIndex = defaultChannelIndex;
  if (anIndex == unknownChannelIndex) {
    nextIndex = nearestChannelToFrequencyKHz(currentFrequencyKHz);
  } else {
    nextIndex = (anIndex + 1) % presetChannelsCount;
  }
  return nextIndex;
}

uint16_t previousChannelIndex(uint16_t anIndex) {
  uint16_t previousChannelIndex = defaultChannelIndex;
  if (anIndex != unknownChannelIndex) {
    if (anIndex == 0) previousChannelIndex = presetChannelsCount - 1;
    else previousChannelIndex = anIndex - 1;
  } else {
    anIndex = nearestChannelToFrequencyKHz(currentFrequencyKHz);
  }
  return previousChannelIndex;
}


uint16_t nextEnabledChannelIndex(uint16_t anIndex) {
  bool found = false;
  uint16_t nextChannelIndex = anIndex;

  if (anIndex == unknownChannelIndex) {
    // get the channel closest to the current frequency : to do
    anIndex = nearestChannelToFrequencyKHz(currentFrequencyKHz);
    if (presetChannels[anIndex].enabled) {
      found = true;
      nextChannelIndex = anIndex;
    } else {
      nextChannelIndex = (anIndex + 1) % presetChannelsCount;
      while (nextChannelIndex != anIndex) {
        if (presetChannels[nextChannelIndex].enabled) {
          found = true;
          break;
        } else nextChannelIndex = (nextChannelIndex + 1) % presetChannelsCount;
      }
    }
  } else {
    nextChannelIndex = (nextChannelIndex + 1) % presetChannelsCount;
    while (nextChannelIndex != anIndex) {
      if (presetChannels[nextChannelIndex].enabled) {
        found = true;
        break;
      } else nextChannelIndex = (nextChannelIndex + 1) % presetChannelsCount;
    }
  }

  return found ? nextChannelIndex : defaultChannelIndex;
}

uint16_t nearestChannelToFrequencyKHz(uint32_t freq) { // regardless if it is excluded
  uint32_t delta = 0xFFFFFFFF;
  uint16_t channelIndex = unknownChannelIndex;
  for (uint16_t c = 0; c < presetChannelsCount; c++) {
    uint32_t cFreq = channelIndexToFrequencyKHz(c);
    if (abs(cFreq - freq) < delta) {
      delta = abs(cFreq - freq);
      channelIndex = c;
    }
  }
  if (channelIndex == unknownChannelIndex) channelIndex = defaultChannelIndex; // if we did not find one, return the default
  return channelIndex;
}

uint16_t channelForFrequencyKHz(uint32_t freq) {
  uint16_t channelIndex = unknownChannelIndex;
  for (uint16_t c = 0; c < presetChannelsCount; c++) {
    if (channelIndexToFrequencyKHz(c) == freq) {
      channelIndex = c;
      break;
    }
  }
  return channelIndex;
}

bool isFrequencyKHzInPresetChannels(uint32_t freq) { // regardless if it is excluded
  bool knownChannel = false;
  for (uint16_t c = 0; c < presetChannelsCount; c++) {
    knownChannel = (channelIndexToFrequencyKHz(c) == freq);
    if (knownChannel) break;
  }
  return knownChannel;
}

// Drive the external hardware to set the frequency
void tuneToCurrentFrequencyKHz(uint32_t freqKHz) {
  // TO DO
  Serial.print(F("Setting Frequency to ")); Serial.print(freqKHz); Serial.println(F(" KHz"));

}

frequency.h

#ifndef __FREQ_H__
#define __FREQ_H__

struct __attribute__((packed)) t_channel {
  const uint16_t channel: 15; // 0 to 32767
    uint16_t enabled: 1;      // 0 or 1
  };

  // constants and variables
  extern t_channel presetChannels[];
  extern const size_t  defaultChannelIndex;
  extern size_t presetChannelsCount;
  extern const uint16_t unknownChannelIndex;
  extern const uint32_t unknownFrequencyKHz;
  extern const uint32_t frequencyStepKHz;

  extern uint16_t currentChannelIndex;           // unknownChannelIndex if channel not in list
  extern const uint32_t minFrequencyKHz;        // we handle from 140 Mhz
  extern const uint32_t maxFrequencyKHz;        // to 150 Mhz
  extern uint32_t currentFrequencyKHz;          // frequency in KHz

  extern const uint32_t squelchDelay;           // how long to wait in ms to decide if frequency is OK
  extern uint32_t squelchChrono;                // when we tuned to a new Frequency

  // functions
  uint32_t channelIndexToFrequencyKHz(uint16_t);
  uint16_t frequencyKHzToChannelIndex(uint32_t) ;
  uint16_t nearestChannelToFrequencyKHz(uint32_t);
  uint16_t channelForFrequencyKHz(uint32_t);
  bool     isFrequencyKHzInPresetChannels(uint32_t);

  uint16_t nextChannelIndex(uint16_t);
  uint16_t previousChannelIndex(uint16_t);
  uint16_t nextEnabledChannelIndex(uint16_t);

  void tuneToCurrentFrequencyKHz(uint32_t);
#endif

screenConfig.h

#ifndef __SCREENCONFIGS__
#define __SCREENCONFIGS__

// 2004 LCD with I2C
const uint8_t LCD_ADRRESS = 0x3F;   // 1602 LCD are often using 0x27
const uint8_t LCD_LINES = 4;        // for 1602 use 2
const uint8_t LCD_COL = 20;         // for 1602 use 16

const char squelchKO = ' ';       // just a space
const char squelchOK = 0x00 ;     // custom defined char (lock)

uint8_t lock[] = {
  0b01110,
  0b10001,
  0b10001,
  0b11111,
  0b11011,
  0b11011,
  0b11111,
  0b00000
};

#endif

information.h

/* ============================================
  code is placed under the MIT license
  Copyright (c) 2022 J-M-L
  For the Arduino Forum :https://forum.arduino.cc/t/rotary-encoder-for-ham-radio/945950

  Libraries used have their own license information that applies.

  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.
  ===============================================
*/


//  ----------------------------
//  Requirements:
//  ----------------------------
//  The system offers three modes: Manual, Preset, and Auto-scan to tune to various channels
//  the relationship between a channel and a frequency is given by the formula frequency = (channel * .005) + 129.135
//  when you turn on the system, it tunes to the last used frequency and mode (so the frequency and mode last used need to be saved in permanent memory).
//  upon first use, a default channel is selected (3576) and the initial mode is Preset
//  a momentary push button (mode) is used to circle through modes in that order Manual ➜ Preset ➜ Auto-scan
//  Upon entering a mode, the currently selected frequency remains selected.
//  The current mode and frequency are always displayed on a LCD

//  ----------------------------
//  In Manual Mode (VFO mode)
//  ----------------------------
//  A rotary encoder is the user interface to go up or down through discrete frequencies between the 140 to 150 Mhz frequency span.
//  The step size is 5KHz.
//  No check is done on the presence of a signal.
//  A momentary push button (skip/unskip) is used to toggle the exclusion status of the currently selected channel (if he frequency matches a knwon channel)


//  ----------------------------
//  In Preset mode
//  ----------------------------
//  A rotary encoder is the user interface to go up or down through a list of known fixed frequencies .
//  No check is done on the presence of a signal.
//  A momentary push button (skip/unskip) is used to toggle the exclusion status of the currently selected channel.
//  the LCD will display an indication of the exclusion status for the selected channel.


//  ----------------------------
//  In Auto-scan mode
//  ----------------------------
//  The system starts on the currently selected frequency
//  If the squelch Indicator (connected to pin 12) is HIGH, the signal is considered OK and the frequency does not change.
//  If the squelch Indicator (connected to pin 12) is LOW, the signal is considered not OK and the frequency changes to the next upper pre-set frequency that is not excluded.
//  Upon reaching the largest pre-set frequency, the system moves to the lowest pre-set frequency that is not excluded
//  if all pre-set frequencies have been excluded the system stays on the current frequency regardless of the presence of signal on the squelch Indicator
//  After selecting a new channel, the squelch Indicator (connected to pin 12) needs 20 ms to be considered valid to use for making a decision to switch to the next possible channel.

EEPROM piece is not implemented. so you always restart from initial conditions.

JML
WOW, that is alot of work!
I will test it when I get a I2C lcd. I just ordered one, the one I presently have uses the parallel connection. I do not have an I2C lcd at this time.
I am guessing the tuneToCurrentFrequencyKHz () is where I would put my upload(); function?

void upload() {
  // Do this for MSBFIRST serial
  int data = freq;
  // shift out highbyte
  shiftOut(dat, clock, MSBFIRST, (data >> 8));
  // shift out lowbyte
  shiftOut(dat, clock, MSBFIRST, data);
  digitalWrite(ena, HIGH);   // Bring Enable high
  digitalWrite(ena, LOW);    // Then back low
 
}

Thank you
Mike

Stefan

I'm more of a classic rock guy. :grinning:

Spend a bit of time on it before and after lunch. Not major work when you have clarified the specs (and you know a bit your way around)

If you only have a parallel LCD it’s not very difficult to adjust, a few lines only to change .

JML
Tried compiling the sketch and got this error
no matching function for call to 'LiquidCrystal_I2C::begin()'

I did install all the called for libraries
Mike

I’ll double check if I’m really using the one I linked.
I thought that was the one but maybe not

I’m on my iPhone at the moment so can’t check