Rotary encoder for Ham radio

Stefan,
Not sure where to put the "anti - pop music" code.
after the switch..case function? or at the beginning (after void loop();)?
I keep getting the "lastfreq not declared in this scope" error. ( I declared it before void setup()

do you need me to post the full code?
Mike

Have you posted that code already?

Stefan,
Fixed it, I forgot to capitalize the F in lastFreq

Stefan,
Are you saying it is best to make a new topic if there is major changes in the code?
If that is proper practice I'll do that. Otherwise to reduce confusion I was thinking of posting the revised code here. Just let me know what is normal practice.
Thank You
Mike

it is a user-forum. The rules are not thin lines that divide into exact measured pieces what is allowed and what not.

usually if you have a different aspect that can be discussed on its own,
a new thread is started. Your thread is difficult to divide. The moderators have not complained so just keep it as it is.

Well if you change the title to something like "control HAM Radio with Arduino Nano" everything fits into this thread through the generalised title.

You could bring in some more oversight
by writing two "sharp" signs " # # without space you can write a

bigger text as a headline

This could guide to what a certain part of the thread is about
or telling "old piece of code to show my attempt to..."

I developed a style of avoiding generalised words like "it" "this" etc. and repeating the name of what I mean
like "the mode-switch", the "upload"-function that adjustes the PLL"

best regards Stefan

Hi, I double checked and got the version wrong. What was installed on my system is the old fdebrabander library


I modified my code to fit with your non I2C display using the standard arduino LiquidCrystal library and 4 data lines.

➜ I don't have such a display so did not test but this should work.

Here are the necessary connections:

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

// lcd pins
d4Pin = A0;
d5Pin = A1;
d6Pin = A2;
d7Pin = A3;
rsPin = 7;
enPin = 8;

You could change pins as you see fit, but best would be to keep the rotary encoder on pin 2 and 3 so that you can benefit from interrupts for the managent of the encoder.

if you don't have a piezo, then that's fine, just don't connect anything to pin 5

HamRadio.ino

#include <Encoder.h>                // https://www.pjrc.com/teensy/td_libs_Encoder.html
#include <easyRun.h>                // https://github.com/bricoleau/easyRun
#include <LiquidCrystal.h>

#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 --- R150 Ohms  ---- 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

// lcd pins
const uint8_t d4Pin = A0;
const uint8_t d5Pin = A1;
const uint8_t d6Pin = A2;
const uint8_t d7Pin = A3;
const uint8_t rsPin = 7;
const uint8_t enPin = 8;

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

// instances
LiquidCrystal lcd(rsPin, enPin, d4Pin, d5Pin, d6Pin, d7Pin);

Encoder encoder(DTPin, CLKPin);
button modeButton(SWPin);
button skipButton(skipPin);


// Utility functions

void initLcd() {
  lcd.begin(LCD_COL,LCD_LINES);
}


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



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 = unknownChannelIndex;
  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 configure() {
  encoder.write(0);
  currentChannelIndex = defaultChannelIndex;
  currentFrequencyKHz = channelIndexToFrequencyKHz(currentChannelIndex);
  initScreen();

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

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 ((currentChannelIndex != unknownChannelIndex) && skipButton) {
    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();
}

screenConfig.h

#ifndef __SCREENCONFIGS__
#define __SCREENCONFIGS__

// 2004 LCD 
const uint8_t LCD_COL = 16;         // for 1602 use 16, for 2004 use 20
const uint8_t LCD_LINES = 2;        // for 1602 use 2, for 2004 use 4

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

the other files are unchanged. see post #89

Thanks JML
I may not be able to experiment with it for a couple days.
Mike

no rush :slight_smile:

I made modifications actually as well to frequency.h and frequency.cpp (updated in the original post above). Either get the files from the above post or for convenience here is a zip of everything

HamRadio.zip (6.9 KB)

To All,
To avoid confusion as to what software I am working with I am posting my present working version.
This is a Manual channel select no scan version.
Any future comments, please refer to this version.
This is not just a "get it done" project, but a learning experience for me, and I appreciate all input.
I want to include a scan function and a "VFO" Manual tune mode. Then I will consider this sketch complete.
Thank you
Mike

#include "Arduino.h"
#include "NewEncoder.h"
const byte EncChA_Pin = 3;
const byte EncChB_Pin = 2;
const int minVal   =    1;
const int maxVal   =   78;
const int startVal =    1;

int ChNr = 1; // new added variable ChNr Channel Number

// Pins 2 and 3 should work for many processors, including Uno. See README for meaning of constructor arguments.
// Use FULL_PULSE for encoders that produce one complete quadrature pulse per detnet, such as: https://www.adafruit.com/product/377
// Use HALF_PULSE for endoders that produce one complete quadrature pulse for every two detents, such as: https://www.mouser.com/ProductDetail/alps/ec11e15244g1/?qs=YMSFtX0bdJDiV4LBO61anw==&countrycode=US&currencycode=USD

NewEncoder myEncoderObject(EncChA_Pin, EncChB_Pin, minVal, maxVal, startVal, FULL_PULSE);

int16_t currentValue;
int16_t prevEncoderValue;

#include <LiquidCrystal.h>
const int rs = 7, en = 8, d4 = A0, 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
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 clock = 9; //pll programming pins
int dat = 10;  //pll programming pins
int ena = 11;  //pll programming pins
int sum = 1;   // channel selection
int freq;      //divisor sent to pll
int lastFreq = 3579;
unsigned long lastDebounceTime = 0;                        // the last time the output pin was toggled
unsigned long debounceDelay = 50;                          // the debounce time; increase if the output flickers

//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() {
  lcd.begin(16, 2);
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(sig, INPUT_PULLUP);
  pinMode(clock, OUTPUT);
  pinMode(dat, OUTPUT);
  pinMode(ena, OUTPUT);
  digitalWrite(clock, LOW);
  digitalWrite(dat, LOW);
  digitalWrite(ena, LOW);
  Serial.begin(9600);
  NewEncoder::EncoderState myEncState;

  if (!myEncoderObject.begin()) {
    //Serial.println("Encoder Failed to Start. Check pin assignments and available interrupts. Aborting.");
    while (1) {
      yield();
    }
  } else {
    // store values of currentValue and EncoderClick into variable myEncState
    myEncoderObject.getState(myEncState);
    //Serial.print("Encoder Successfully Started at value = ");
    prevEncoderValue = myEncState.currentValue;
    // Serial.println(prevEncoderValue);
  }
}

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:
        mode = !mode;
        if (mode == true)
        {
          lcd.setCursor(0, 1) ;
          lcd.print("Manual");
        }
        else
        {
          lcd.setCursor(0, 1) ;
          lcd.print("Scan  ");
        }
      }
    }                                                   // save the current state as the last state,
                                                        //for next time through the loop
    lastButtonState = buttonState;
  }
                                                       // create variable of type EncoderState
  NewEncoder::EncoderState myEncState;

                                                       // very important ! update the variable myEncState
                                                       // store actual values into variable myEncState
  myEncoderObject.getState(myEncState);

  switch (myEncState.currentClick) {
    case NewEncoder::UpClick:
      ChNr++; // increment variable ChNr by 1
      // if ChNr is above max
      if (ChNr > 78) {
        ChNr = 1; // set ChNr to min
      }
      //Serial.print("Count up new ChNr");
      //Serial.println(ChNr);
      break;

    case NewEncoder::DownClick:
      ChNr--; // decrement variable ChNr by 1
      // if ChrNr is below lowest number
      if (ChNr < 1) {
        ChNr = 78; // set ChNr to max
      }
      // Serial.print("Count down new ChNr");
      //Serial.println(ChNr);
      break;

    default:
      break;
  }

  sum = ChNr;
  switch (sum) {      //which channels to select
    case 1:
      freq = 3195;
      ncheck();
      break;
    case 2:
      freq = 3199;
      ncheck();
      break;
    case 3:
      freq = 3203;
      ncheck();
      break;
    case 4:
      freq = 3207;
      ncheck();
      break;
    case 5:
      freq = 3211;
      ncheck();
      break;
    case 6:
      freq = 3215;
      ncheck();
      break;
    case 7:
      freq = 3219;
      ncheck();
      break;
    case 8:
      freq = 3223;
      ncheck();
      break;
    case 9:
      freq = 3227;
      ncheck();
      break;
    case 10:
      freq = 3231;
      ncheck();
      break;
    case 11:
      freq = 3235;
      ncheck();
      break;
    case 12:
      freq = 3239;
      ncheck();
      break;
    case 13:
      freq = 3243;
      ncheck();
      break;
    case 14:
      freq = 3247;
      ncheck();
      break;
    case 15:
      freq = 3251;
      ncheck();
      break;
    case 16:
      freq = 3255;
      ncheck();
      break;
    case 17:
      freq = 3259;
      ncheck();
      break;
    case 18:
      freq = 3263;
      ncheck();
      break;
    case 19:
      freq = 3267;
      ncheck();
      break;
    case 20:
      freq = 3271;
      ncheck();
      break;
    case 21:
      freq = 3495;
      ncheck();
      break;
    case 22:
      freq = 3498;
      ncheck();
      break;
    case 23:
      freq = 3501;
      ncheck();
      break;
    case 24:
      freq = 3504;
      ncheck();
      break;
    case 25:
      freq = 3507;
      ncheck();
      break;
    case 26:
      freq = 3510;
      ncheck();
      break;
    case 27:
      freq = 3513;
      ncheck();
      break;
    case 28:
      freq = 3516;
      ncheck();
      break;
    case 29:
      freq = 3519;
      ncheck();
      break;
    case 30:
      freq = 3522;
      ncheck();
      break;
    case 31:
      freq = 3525;
      ncheck();
      break;
    case 32:
      freq = 3528;
      ncheck();
      break;
    case 33:
      freq = 3531;
      ncheck();
      break;
    case 34:
      freq = 3534;
      ncheck();
      break;
    case 35:
      freq = 3537;
      ncheck();
      break;
    case 36:
      freq = 3540;
      ncheck();
      break;
    case 37:
      freq = 3543;
      ncheck();
      break;
    case 38:
      freq = 3546;
      ncheck();
      break;
    case 39:
      freq = 3549;
      ncheck();
      break;
    case 40:
      freq = 3552;
      ncheck();
      break;
    case 41:
      freq = 3555;
      ncheck();
      break;
    case 42:
      freq = 3558;
      ncheck();
      break;
    case 43:
      freq = 3561;
      ncheck();
      break;
    case 44:
      freq = 3564;
      ncheck();
      break;
    case 45:
      freq = 3567;
      ncheck();
      break;
    case 46:
      freq = 3570;
      ncheck();
      break;
    case 47:
      freq = 3573;
      ncheck();
      break;
    case 48:
      freq = 3576;
      ncheck();
      break;
    case 49:
      freq = 3579;
      ncheck();
      break;
    case 50:
      freq = 3582;
      ncheck();
      break;
    case 51:
      freq = 3585;
      ncheck();
      break;
    case 52:
      freq = 3588;
      ncheck();
      break;
    case 53:
      freq = 3591;
      ncheck();
      break;
    case 54:
      freq = 3594;
      ncheck();
      break;
    case 55:
      freq = 3597;
      ncheck();
      break;
    case 56:
      freq = 3600;
      ncheck();
      break;
    case 57:
      freq = 3603;
      ncheck();
      break;
    case 58:
      freq = 3606;
      ncheck();
      break;
    case 59:
      freq = 3609;
      ncheck();
      break;
    case 60:
      freq = 3612;
      ncheck();
      break;
    case 61:
      freq = 3615;
      ncheck();
      break;
    case 62:
      freq = 3618;
      ncheck();
      break;
    case 63:
      freq = 3621;
      ncheck();
      break;
    case 64:
      freq = 3624;
      ncheck();
      break;
    case 65:
      freq = 3627;
      ncheck();
      break;
    case 66:
      freq = 3630;
      ncheck();
      break;
    case 67:
      freq = 3633;
      ncheck();
      break;
    case 68:
      freq = 3636;
      ncheck();
      break;
    case 69:
      freq = 3639;
      ncheck();
      break;
    case 70:
      freq = 3642;
      ncheck();
      break;
    case 71:
      freq = 3645;
      ncheck();
      break;
    case 72:
      freq = 3648;
      ncheck();
      break;
    case 73:
      freq = 3651;
      ncheck();
      break;
    case 74:
      freq = 3477;
      ncheck();
      break;
    case 75:
      freq = 3483;
      ncheck();
      break;
    case 76:
      freq = 3489;
      ncheck();
      break;
    case 77:
      freq = 3465;
      ncheck();
      break;
    case 78:
      freq = 3696;
      ncheck();
      break;
    default:
      freq = 3576;
      ncheck();
      break;
      return (freq);
  }
}

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

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
  lcdisp();
}

void lcdisp()
{
  lcd.setCursor(0, 0);
  lcd.print((freq * .005) + 129.135, 3);
  lcd.print("Mhz");
}

Take a look at arrays. The switch statement that picks frequency could be made vastly shorter and easier to read if you use one.

yes, as suggested in #60. Especially as it will get more complex since channels can be enabled or disabled, so even more information will need to be collected / managed.

(cf also frequency.cpp as proposed above for a possible array)

JML
The "Hamradio" sketch must be looking for a different encoder than the one I have.
every other channel is on a detent. FIXED!
Also trying to put the PLL programming function into the Frequency.cpp
and I am getting ena and dat not defined in this scope.
I have not used "h" or "cpp" before other than an include statement. so I don't know where to define those variables. I tried in the hamradio.ino but no luck
This is the PLL prog function

DAT CLOCK AND ENA are output pins

// Drive the external hardware to set the frequency
void tuneToCurrentFrequencyKHz(uint32_t freqKHz) {
   int data = freqKHz;      // this has to be the 3000 series numbers, probably wrong variable here
  // 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
  // TO DO
  Serial.print(F("Setting Frequency to ")); Serial.print(freqKHz); Serial.println(F(" KHz"));

}

Mike

the encoder in my code is read by doing

void runPreset() {
  long newEncoderPosition = encoder.read() >> 1; // divide by 2 as I get 2 ticks for one click with mine

the >> 1 basically divides by 2 the value you read, that's because I get 2 "ticks" for one "click"
if yours generate 4 ticks per click, then you need >> 2 and if you have 1 tick per click then you don't need the >> 1 at all.

before digging into tuneToCurrentFrequencyKHz(), did you get everything else to work? do you see the display on screen, can you change mode, can you change channels in preset mode using the rotary, skip or unskip channels with the other button, change frequency in manual mode etc ?

information should be displayed on the Serial monitor at 115200 bauds, telling you which frequency should be set.

Once this work fine, we can dig into how you drive the external hardware (the frequency is passed in KHz to the function, seems you need a channel number so need to extract channel from this formula frequency = (channel * .005) + 129.135 I suppose

channel = (frequency - 129.135) / 0.005 for a frequency in Hz.

Here we have KHz so may be

// Drive the external hardware to set the frequency
void tuneToCurrentFrequencyKHz(uint32_t freqKHz) {
   int data = (freqKHz - 129135) / 5;
  // 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
  // TO DO
  Serial.print(F("Setting Frequency to ")); Serial.print(freqKHz); Serial.println(F(" KHz"));

}

JML
I got the encoder to give me the proper "clicks" put in >>2 and it worked.
Everything seems to be working. I got the right display on the lcd and serial monitor.
scan works with the squelch .
Though the actual channel number is the 1 thru 78 positions, not the 3000 something number. The "3576" type number is the divisor the PLL (phase-locked loop) uses to compare to the reference (5 khz) to lock on to the frequency.
There are some other things that can be added later if you want to make a "full-fledged deluxe" radio controller.

I didn't think of putting "freqKHz" thru the formula, I think that would work.
Now I just need the pins for the PLL programming to be assigned.
Assignment completed!
I forgot to put in the "pinmode" statements in setup.
I now have a functioning sketch.

Thank you for all your effort
Mike

JML
Got something wierd.
On preset mode, going up channel it is one channel per click, but going down channel it is 4 channels per click. is there more than 2 places to define the encoder to 2?

void runPreset() {
  long newEncoderPosition = encoder.read() >> 1; // divide by 2 as I get 2 ticks for one click with mine

I changed the 2 instances of that statement (from >>1 to >>2), are there more?
Mike

There is a "find" feature in the IDE.

There are indeed only 2 places.

try changing the code for this:

at the beginning of the main .ino file, add the following part (in the constants)

// constants
const int encoderDivider = 2; // 2 ticks per click for my encoder

if you have 4 ticks per click, then make that 4 instead of 2

then change the code of the 2 following functions to read:

void runPreset() {
  long newEncoderPosition = encoder.read(); 
  if ((newEncoderPosition/encoderDivider) != 0) { 
    newEncoderPosition /= encoderDivider;
    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 ((currentChannelIndex != unknownChannelIndex) && skipButton) {
    tone(piezoPin, 3000, 30);
    presetChannels[currentChannelIndex].enabled = 1 - presetChannels[currentChannelIndex].enabled;
  }
}

void runManual() {
  long newEncoderPosition = encoder.read(); 
  if ((newEncoderPosition/encoderDivider) != 0) {  // divide by 2 as I get 2 ticks for one click with mine
    newEncoderPosition /= encoderDivider;
    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;
  }
}

One way to try is to upload the following code and look at the Serial monitor (@115200 bauds)

#include <Encoder.h>                       // https://www.pjrc.com/teensy/td_libs_Encoder.html
// The Pins
const uint8_t DTPin       = 2;            // Encoder DT
const uint8_t CLKPin      = 3;            // Encoder CLK
Encoder encoder(DTPin, CLKPin);
const int encoderDivider = 2; // 2 ticks per click for my encoder


void setup() {
  Serial.begin(115200); Serial.println();
  encoder.write(0);
  Serial.println("Initial Value = 0");

}

void loop() {
  long newValue = encoder.read(); //

  if ((newValue / encoderDivider) != 0) {
    Serial.println(newValue/encoderDivider);
    encoder.write(0);
  }
}

if all is fine when you go clockwise you should see a 1 appearing and if you go counter clockwise you should see -1

JML
Seems you got the bugs out.
The radio has been working good the last couple days.
IF you are interested...... I mentioned a "deluxe" version of the software.
Functions desired are button controlled +600, - 600 khz and none (or selectable) frequency offset.
a tone module (CTCSS) serially controlled. tone selected stored in memory and off, tx only, rx/tx.
Tone on/off and selectable in VFO mode.
transmit/receive function.
If you like a challenge... otherwise what you have done is greatly appreciated!
Thanks
Mike

can you post the final code you got?

also a better requirement documentation would be needed. I don't get all your acronyms :slight_smile:
(something like written in post #71Rotary encoder for Ham radio - #71 by J-M-L)

Hamradio2.ino

#include <Encoder.h>                // https://www.pjrc.com/teensy/td_libs_Encoder.html
#include <easyRun.h>                // https://github.com/bricoleau/easyRun
#include <LiquidCrystal.h>

#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 --- R150 Ohms  ---- 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
int clock                 = 9;            // PLL Program pin
int dat                   = 10;           // PLL Program pin
int ena                   = 11;           // PLL Program pin

// lcd pins
const uint8_t d4Pin = A0;
const uint8_t d5Pin = A1;
const uint8_t d6Pin = A2;
const uint8_t d7Pin = A3;
const uint8_t rsPin = 7;
const uint8_t enPin = 8;

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

// instances
LiquidCrystal lcd(rsPin, enPin, d4Pin, d5Pin, d6Pin, d7Pin);

Encoder encoder(DTPin, CLKPin);
button modeButton(SWPin);
button skipButton(skipPin);


// Utility functions

void initLcd() {
  lcd.begin(LCD_COL, LCD_LINES);
}


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



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 = unknownChannelIndex;
  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 configure() {
  encoder.write(0);
  currentChannelIndex = defaultChannelIndex;
  currentFrequencyKHz = channelIndexToFrequencyKHz(currentChannelIndex);
  initScreen();

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

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() >> 2; // 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 ((currentChannelIndex != unknownChannelIndex) && skipButton) {
    tone(piezoPin, 3000, 30);
    presetChannels[currentChannelIndex].enabled = 1 - presetChannels[currentChannelIndex].enabled;
  }
}

void runManual() {
  long newEncoderPosition = encoder.read() >> 2; // 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(clock, OUTPUT);
  pinMode(dat, OUTPUT);
  pinMode(ena, OUTPUT);
  pinMode(squelchPin, INPUT_PULLUP); // or INPUT depending on the system you connect to
  pinMode(piezoPin, OUTPUT);
  Serial.begin(9600); 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 = 100;       // 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;
    }
    Serial.print(channelIndex);
  }
  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) {
  int clock       = 9;                      // PLL Program pin
  int dat         = 10;                     // PLL Program pin
  int ena         = 11;                     // PLL Program pin
  //pinMode(clock, OUTPUT);
  //pinMode(dat, OUTPUT);
  //pinMode(ena, OUTPUT);
  int data = (freqKHz - 129135) / 5;
  // 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
  // TO DO
  Serial.print(F("Setting Frequency to ")); Serial.print(freqKHz); Serial.println(F(" KHz"));
  Serial.println(data);

}

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 
const uint8_t LCD_COL = 16;         // for 1602 use 16, for 2004 use 20
const uint8_t LCD_LINES = 2;        // for 1602 use 2, for 2004 use 4

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