If what you need does not exist, them create it yourself.

Current TopicJeroen Steeman - Micro AVR Morse code call sign generator

I needed (wanted) a low budget call sign generator that can transmit my call sign (PD9JS) as audio at predetermined intervals while in a QSO. Searched the Internet, but nothing usable came up.

Mentioning this to a colleague at work, the next day he dropped an Arduino on my desk and said to look at the 'ATTiny AVR'. So I did. Being worked with Microchip ARM Cortex controllers, the Arduino was a breath of simple fresh air to work with (if you don't need accurate timing stuff). Ordered a few ATtiny85's for a buck from CN that arrived in no time. And so the AVR Morse code project started.

In the mean time the requirements had expanded a bit. I also wanted it to be autonomous and have the ability to serve as a call sign generator for repeaters. Having the ability to key the repeater, send the call sign and fall back into receive, or if a QSO was underway, mix the call sign Morse code audio with the current QSO audio. And I wanted it to be powered by a rechargeable LION cell.

The things I used are: 1. An Arduino UNO. A ATTiny85. A small breadboard and a couple (4) of wires. I am not going to go into details about how to wire the UNO to the the ATTiny to program it as there are many excellent sites that show step by step how to accomplish this.

ATtiny Morse Code Generator Schematic

ATTiny Morde Code Generator Schematic

On the left of the schematic. In the design I used a rechargeable LION 3v button cell. As these batteries have a user manual all to themselves, the idea was never to let the circuit drain the battery completely. For this I used one of the AD ports attached to a voltage divider across the battery. If the voltage gets to low, the ATTiny powers off. Do take care not to overcharge the cell while in operation as this is not part of this design.

On the right of the schematic is the switch for PTT (you may need to add an additional resistor if you rig sinks current). I use it on a Baufeng UVR3 as is and it works fine without any excessive current being drawn. The audio out for the Morse code tone is decoupled via C1 and after this a variable voltage divider is used to determine the volume of the tone mixed with the TX audio of the 'microphone'.

Two LEDs have been added to visually indicate when the PTT and Morse code signals are active, but these are not compulsory for operations, like installation into the microphone itself.

ATTiny Morde Code Generator 'autorouted' PCB design

Functional Requirements

  • Programmable TX interval time. How often to send the Morse code call sign.
  • Programmable Morse code tone frequency. The audio tone frequency of the Morse signals.
  • Programmable Morse character speed (WPM).How many words per minute to send the Morse code at.
  • Variable PPT pre and post key-up and release times for repeater use.
  • Shutdown on low battery voltage. To extend rechargeable battery life.
  • Go into 'sleep mode' while in 'quite interval' between transmissions. Conserving power.
  • Using SMD components it should fit in a microphone housing.(Not the PCB as shown!!)

ATTiny Morse Code Generator Arduino code

Explanation of how the software functions explained below..

#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/interrupt.h>
#include <avr/power.h>
#include <stdint.h>

#define UNIT_LENGTH 120 //ms for morse speed
#define VL_LOW 189 // low battery indicator level 12:1 dividor

static const int numReadings = 3;
static const struct { const char letter, *code; } MorseMap[] =
{
  { 'A', ".-" },
  { 'B', "-..." },
  { 'C', "-.-." },
  { 'D', "-.." },
  { 'E', "." },
  { 'F', "..-." },
  { 'G', "--." },
  { 'H', "...." },
  { 'I', ".." },
  { 'J', ".---" },
  { 'K', "-.-" },
  { 'L', ".-.." },
  { 'M', "--" },
  { 'N', "-." },
  { 'O', "---" },
  { 'P', ".--." },
  { 'Q', "--.-" },
  { 'R', ".-." },
  { 'S', "..." },
  { 'T', "-" },
  { 'U', "..-" },
  { 'V', "...-" },
  { 'W', ".--" },
  { 'X', "-..-" },
  { 'Y', "-.--" },
  { 'Z', "--.." },
  { ' ', " " }, //Gap between word, seven units
  { '1', ".----" },
  { '2', "..---" },
  { '3', "...--" },
  { '4', "....-" },
  { '5', "....." },
  { '6', "-...." },
  { '7', "--..." },
  { '8', "---.." },
  { '9', "----." },
  { '0', "-----" },
  { '.', "·–·–·–" },
  { ',', "--..--" },
  { '?', "..--.." },
  { '!', "-.-.--" },
  { ':', "---..." },
  { ';', "-.-.-." },
  { '(', "-.--." },
  { ')', "-.--.-" }
};

uint8_t watchdog_counter, mcucr1, mcucr2;
int readings[numReadings];      // the readings from the analog input
int index = 0;                  // the index of the current reading
int total = 0;                  // the running total
int average = 0;

ISR(WDT_vect) {
  watchdog_counter++; // this is where the watch dog interupt lands up
}

void enable_watchdog(void) {
    wdt_reset();
    cli();
    mcucr1 = MCUCR | _BV(BODS) | _BV(BODSE);  //turn off the brown-out detector
    mcucr2 = mcucr1 & ~_BV(BODSE);
    MCUCR = mcucr1;
    MCUCR = mcucr2;
    MCUSR = 0x00;
    WDTCR |= _BV(WDCE) | _BV(WDE);
    WDTCR = _BV(WDIE) | _BV(WDP2) | _BV(WDP1);    //1024ms
    sei();
}

void disable_watchdog(void)
{
    wdt_reset();
    cli();
    MCUSR = 0x00;
    WDTCR |= _BV(WDCE) | _BV(WDE);
    WDTCR = 0x00;
    sei();
}

void setup()
{
  pinMode(0, OUTPUT); // led morse
  pinMode(1, OUTPUT); // audio morse
  pinMode(2, OUTPUT); // tx on (high active)
  pinMode(A3, INPUT); // analog to read battery voltage
  digitalWrite(0, LOW); // ensure off at start
  digitalWrite(2, LOW); // ensure off at start
  ADCSRA &= ~(1<<ADEN); //Disable ADC, saves ~230uA
  analogReference(INTERNAL);
  for (int thisReading = 0; thisReading < numReadings; thisReading++)
    readings[thisReading] = 0;
  enable_watchdog(); //Setup watchdog to go off after 1sec
}

void loop()
{
  sleep_cpu(); //Go to sleep!
  if(watchdog_counter > 60)
  {
    disable_watchdog(); // stop WDT (else we could run into ourselves)
    watchdog_counter = 0; // reset the timer counter
    // check battery voltage
    averageVoltage();
    int voltage = average;
    if(voltage < VL_LOW)           
    {
      sleep_cpu(); //Go to sleep until power on reset
    }
    String morseWord = encode("PD9JS");
    
    digitalWrite(2, HIGH); // tx on 
    delay(UNIT_LENGTH * 4); // wait for tx to power up
    for (int i = 0; i <= morseWord.length(); i++)
    {
      switch (morseWord[i])
      { 
      case '.': //dit
        digitalWrite(0, HIGH); 
        TinyTone(239, 5, UNIT_LENGTH);
        digitalWrite(0, LOW); 
        delay(UNIT_LENGTH);
      break;
      case '-': //dah
        digitalWrite(0, HIGH); 
        TinyTone(239, 5, UNIT_LENGTH *3);
        digitalWrite(0, LOW); 
        delay(UNIT_LENGTH);
        break;
      case ' ': //gap
        delay(UNIT_LENGTH * 2);
      }
    }
    delay(UNIT_LENGTH);
    digitalWrite(2, LOW); // tx off
    enable_watchdog();
  }
}

void TinyTone(unsigned char divisor, unsigned char octave, unsigned long duration)
{
  TCCR1 = 0x90 | (11-octave); // for 8MHz clock
  OCR1C = divisor-1;         // set the OCR
  delay(duration);
  TCCR1 = 0x90;              // stop the counter
}

String encode(const char *string)
{
  size_t i, j;
  String morseWord = "";
  for (i = 0; string[i]; ++i)
  {
    for (j = 0; j < sizeof MorseMap / sizeof *MorseMap; ++j)
    {
      if (toupper(string[i]) == MorseMap[j].letter)
      {
        morseWord += MorseMap[j].code;
        break;
      }
    }
    morseWord += " "; //Add tailing space to seperate the chars
  }
  return morseWord;
}

void averageVoltage() 
{
        total= total - readings[index];        
        readings[index] = analogRead(A3);
        total= total + readings[index];       
        index = index + 1;                    
        if (index >= numReadings)              
          index = 0;                          
        average = total / numReadings;          
        delay(1);        // delay in between reads for stability            
}

The included libraries are

#include <avr/sleep.h>

sleep the SLEEP instruction allows an application to reduce its power consumption considerably.

#include <avr/wdt.h>

wdt Watchdog timer handling.

#include <avr/interrupt.h>

interrupt As it says, the global handling of interrupts.

#include <avr/power.h>

power Manage the 'power reduction register'.

#include <stdint.h>

stdint Handling standard integer types.

The constants and fixed code editable values are

UNIT_LENGTH is the speed that the Morse code is sent at. It is set to 120, however you can increase it to slow down the words per minute, or decrease this value to speed up the WPM.

VL_LOW is the value the ADC should be or below this value to shut down ARV and go to permanent HALT state. You only really need this if you want to use a rechargeable LION cell and do not want it to go below 2-2.2 volts to protect the cell from damage.

numReadings is how many times to read the ADC to get an average of the battery voltage value.

MorseMap[] the array that holds the character to Morse 'symbol' relation. It is used to look up and then translate text to Morse code letters.

The variables are:

watchdog_counter, mcucr1, mcucr2 are unsigned integers, mcucr1 and mcucr2 are used to get and then set state registers to turn off brown out detection to conserve device power usage, as BOD is not required. watchdog_counter is used to count the amount of times (seconds) that the watchdog has been triggered.

readings[numReadings], index, total and average are used to determine the average battery voltage for x readings. (where x in the code = 3 (changeable).

The routines are:

ISR(WDT_vect) this is where the watchdog lands when it fires. Used to increment the watchdog_counter which is used in another routine to determine whether to start the Morse transmission or not.

enable_watchdog ensures that the BOD is off and sets the watchdog to trigger every 1024ms. (Just not a second, but hey, I don't have a RTC onboard)

disable_watchdog, once our time counter reaches the point that it is time to start the Morse code transmission, we need to stop the interrupts else we may run into our self while we are still busy sending.

setup() is run once when the ATTIny is powered up. This routine is used to configure the pin functions, set register values to turn off functions we do not use that do eat power if not switched off and lastly to start the watch dog.

loop() does exactly what it is called, runs in a loop. The steps it loops through are described below

  • Put the cpu in low energy consumption mode (sleep). The watchdog routine will still function in this mode.
  • Check if the timer counter is at the critical level (is it time to transmit) ... if not, fall though, put cpu in sleep mode and check the timer again.
  • If timer counter critical
    • Disable the watchdog (else we keep counting for nothing.
    • Reset the timer counter to zero.
    • Check if the battery is still healthy
      • If yes then fall through.
      • If NO then stop everything and shut down.
    • Get the text string to convert to Morse code.
    • Instruct TX to turn on
    • Wait for x time.
    • Convert each character of the text string to Morse code characters and 'clock' them in a loop to the 'Morse led' and using the specified audio frequency send them to the audio pin until all text characters have been sent.
    • Wait for x time.
    • Turn off TX.
    • Enable watchdog.

What's next:

Currently everything runs in program memory except the declared variables and these are put in SRAM and it's pretty full for this little tiny AVR. There is still lots of space in the 8k program memory, so putting constants like the huge text to Morse array into program (flash) would free up much needed space to be able to put more than a simple call sign in the text to transmit. I have tried it, can get it into the program flash space, but cannot reference (read out) it from the application itself. Maybe you will have better luck...If so, please let me know how to do it ;)

Then something I completely overlooked! EEPROM 512 Bytes is available in the ATTiny85. Really stupid of me!! This is perfect to separate all the variables from the code. The Morse array, call sign, speed, frequency and more should be put as variables in the EEPROM, this way the program can be generic and the operations can be controlled by changing values only in the EEPROM.

Last but not least, the PCB design is just an experiment to see how Eagle auto routes for a single sided PCB. I highly recommend you DO NOT USE IT. I have not made it and never will in this way. If you want the Eagle ATTiny project files, here they are. You may need the Atmel library, the one I used for this project is here.