Skip to content

16. Interface and Application Programming

In this week we learned about interface and application programming. We were mainly working with Python this week, using Jupyter Notebooks for managing code and documentation side-by-side organically. The notebook was generated as follows, as a mixture of Markdown and Python code snippets:

After finishing everything, I copied over the text into Visual Studio Code and converted all compilable Python snippets to Markdown code snippets to generate this webpage.

PC User Interface

This code will generate a simple GUI to present a user choice as to what to morse out using the morse boards made in embedded programming week. This GUI uses Python3 and the TKinter library for presenting a window with an input field and send the entered word to the morse board via Serial communication:

GUI -> (Text Entry) -> Serial Port -> Morse Board -> Sound out and feedback -> Serial Port

Making a Window

To create a window using TKinter, following code can be used:

from tkinter import *
root = Tk()
root.mainloop()

Text Labels

To add a simple text label, a Label object has to be created and assigned to the window. It also has to be packed to display it:

from tkinter import *

root = Tk()
myLabel = Label(root, text="Hello World!")
myLabel.pack()
root.mainloop()

Sizing the Window

Since this creates a window just barely enough in size to cover the contents of the window, window geometry can be changed by calling the geometry function of the root window:

from tkinter import *

root = Tk()
root.geometry('200x150')
myLabel = Label(root, text="Hello World!")
myLabel.pack()
root.mainloop()

Entering Text

Text entry can easily be added by adding an Entry object to the window for text entry:

from tkinter import *

root = Tk()
root.geometry('200x150')
myLabel = Label(root, text="Please enter a text to morse out:")
myLabel.pack()
myEntry = Entry(root, text="SOS")
myEntry.pack()
root.mainloop()

Doing so, text can be entered, yet not be really used in the code.

Buttons

We now add a button that will print the entered text (by using the Entry field’s get() method) out to the Python console (and later to the Serial Port):

from tkinter import *

def sendToBoard():
    print(myEntry.get())

root = Tk()
root.geometry('200x150')
myLabel = Label(root, text="Please enter a text to morse out:")
myLabel.pack()
myEntry = Entry(root, text="SOS")
myEntry.pack()
myButton = Button(root, text="Send to board", command=sendToBoard)
myButton.pack()
root.mainloop()

Serial Communication

Now, I added the possibility to send to the Serial port by importing the serial library. I added a Serial send command to the function that gets called when pressing the button:

import serial
from tkinter import *

def sendToBoard():
    print(myEntry.get())
    ser.write(myEntry.get())

# Initialize Serial Port
ser = serial.Serial('COM30', 9600)
ser.setDTR()
ser.flush()
# Initialize GUI window
root = Tk()
root.geometry('200x150')
myLabel = Label(root, text="Please enter a text to morse out:")
myLabel.pack()
myEntry = Entry(root, text="SOS")
myEntry.pack()
myButton = Button(root, text="Send to board", command=sendToBoard)
myButton.pack()
root.mainloop()

Listing Available Serial Ports

Above code works somewhat fine, but the proper Serial port name has to be hardcoded (in my case: COM30, otherwise an error is thrown. To mitigate this problem, I wanted to add a simple drop down menu to the GUI that lets the user choose her Serial Port beforehand. For this, we first need a list of Serial ports available, which can be retrieved and printed in Python as shown in following MWE (thanks https://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python):

import serial.tools.list_ports
ports = serial.tools.list_ports.comports()
for port, desc, hwid in sorted(ports):
        print("{}: {} [{}]".format(port, desc, hwid))

This function output the Serial port list to the Python console (which is directly integrated in Jupyter Notebooks):

Now instead of printing the list of COM ports to the console, we want to use the list as the list of entries in our drop down menu. For this, I first added a generic dropdown menu with mockup values for testing (thanks https://www.delftstack.com/de/howto/python-tkinter/how-to-create-dropdown-menu-in-tkinter/). I also added the close() method for the Serial port after the main loop, so the Serial connection would be closed when the program ends (otherwise the Serial Port is kept busy and on subsequent program starts, no connection to the (busy) COM port is possible):

import serial
from tkinter import *

def sendToBoard():
    print(myEntry.get())
    ser.write(myEntry.get())

OptionList = [
    "Aries",
    "Taurus",
    "Gemini",
    "Cancer"
]

# Initialize GUI window
root = Tk()
root.geometry('200x150')

variable = StringVar(root)
variable.set(OptionList[0])

# Initialize Serial Port
ser = serial.Serial('COM30', 9600)
ser.setDTR()
ser.flush()
myLabel = Label(root, text="Please enter a text to morse out:")
myLabel.pack()
myEntry = Entry(root, text="SOS")
myEntry.pack()
myButton = Button(root, text="Send to board", command=sendToBoard)
myButton.pack()
myDropdown = OptionMenu(root, variable, *OptionList)
myDropdown.pack()

root.mainloop()

ser.close()

I then made the OptionList for the drop down menu be read from the list of available COM ports and made the COM port only connect when the user pressed another button (otherwise the script would try connecting before the user atually had the chance to choose the proper COM port). I also put an initial value into the Entry field properly (using the insert() method of the Entry class, not a parameter in the constructor) and rearranged my order of UI elements slightly:

import serial.tools.list_ports
from tkinter import *

def connectToBoard():
    # Initialize Serial Port
    ser = serial.Serial('COM30', 9600)
    ser.setDTR()
    ser.flush()
    print(OptionList[0])

def sendToBoard():
    print(myEntry.get())
    ser.write(myEntry.get())

OptionList = serial.tools.list_ports.comports()

# Initialize GUI window
root = Tk()
root.geometry('200x150')

variable = StringVar(root)
variable.set(OptionList[0])

myDropdown = OptionMenu(root, variable, *OptionList)
myDropdown.pack()
myConnectButton = Button(root, text="Connect to board (COM port)", command=connectToBoard)
myConnectButton.pack()
myLabel = Label(root, text="Please enter a text to morse out:")
myLabel.pack()
myEntry = Entry(root)
myEntry.insert(4, "SOS")
myEntry.pack()
myButton = Button(root, text="Send to board", command=sendToBoard)
myButton.pack()

root.mainloop()

ser.close()

Fixing Mistakes

Upon trying to send out the entered value to the Serial port, I always got an error message. Turned out I had preemptively used the OptionList entry wrong as input for the Serial initialization function. Adding a little bit of debug code to see what was going on, I figured out that the OptionList[0] element did actually contain way more than just the string COM30. I fixed this by calling just a single member of the OptionList entry when initializing the Serial port (thanks https://pyserial.readthedocs.io/en/latest/pyserial_api.html). I also needed to typecast the striong to send out to the Serial port using the str.encode() function, as this was producing an error otherwise (thanks https://stackoverflow.com/questions/35642855/python3-pyserial-typeerror-unicode-strings-are-not-supported-please-encode-to). Finally, I added a ser.close() right before opening the Serial port to avoid errors when the port was already open:

import serial.tools.list_ports
from tkinter import *

ser = serial.Serial(port=None, baudrate=9600)

def connectToBoard():
    # Initialize Serial Port
    ser.setDTR()
    print(OptionList[0][0])
    ser.port = OptionList[0][0]
    ser.close()
    ser.open()
    ser.flush()

def sendToBoard():
    print("\"" + myEntry.get() + "\" was sent to the Serial port!")
    ser.write(str.encode(myEntry.get()))

OptionList = serial.tools.list_ports.comports()

# Initialize GUI window
root = Tk()
root.geometry('300x200')

variable = StringVar(root)
variable.set(OptionList[0])

myDropdown = OptionMenu(root, variable, *OptionList)
myDropdown.pack()
myConnectButton = Button(root, text="Connect to board (COM port)", command=connectToBoard)
myConnectButton.pack()
myLabel = Label(root, text="Please enter a text to morse out:")
myLabel.pack()
myEntry = Entry(root)
myEntry.insert(4, "SOS")
myEntry.pack()
myButton = Button(root, text="Send to board", command=sendToBoard)
myButton.pack()

root.mainloop()

ser.close()

Here is a view of the somewhat finished GUI so far:

Arduino Firmware

With the GUI basics running, I got to work implementing the Arduino code. For simplicity’s sake, I wrote the program prototyping on an Arduino UNO instead of the ATTiny44 target platform. This was mostly because of pin considerations: the FTDI port on my ATTiny44 board would this time be used for actual Serial communication with the GUI, so I could not use the FTDI pins for that. To attach the buzzer to the board, I would later use the ISP MOSI pin, which is also used for attaching the ISP programmer though. So working with the ATTiny board directly, I would have had to unplug the buzzer every time I had to program the board - inacceptable! I trusted my code to be compatible enough with the ATTiny platform later.

With the Arduino UNO, I could attach the buzzer to the ISP MOSI pin (as would be later on the ATTiny, just with a different pin number) and program as well as communicate with the board using the internal FTDI Serial port. I started out with some code that basically just listened to anything coming in on the Serial port and reacting with morse beeps. I then fleshed it out to parse the message coming in on the Serial port, translate it into morse code using an array storing the corresponding morse patterns and morse it out on the buzzer:

#include <avr/power.h>

byte asciiTableShift = 48; //value to subtract from incoming value to convert from ASCII ('A'=65) to local array index ('A'=index 0)

String alphabet[][2] = { { "0" , "-----" },
                         { "1" , ".----" },
                         { "2" , "..---" },
                         { "3" , "...--" },
                         { "4" , "....-" },
                         { "5" , "....." },
                         { "6" , "-...." },
                         { "7" , "--..." },
                         { "8" , "---.." },
                         { "9" , "----." },
                         { ":" , " " },
                         { ";" , " " },
                         { "<" , " " },
                         { "=" , " " },
                         { ">" , " " },
                         { "?" , " " },
                         { "@" , " " },
                         { "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" , "--.."}
                    };

int ledPin = 8;
int buttonPin = 7;

int ditMs = 100;
int dahMs = 3 * ditMs;
int delayBetweenCharacters = 2 * ditMs; //theoretically, this is 3 dit, but 1 dit is already included in the dit and dah functions
int delayBetweenWords = 6 * ditMs;  //theoretically, this is 7 dit, but 1 dit is already included in the dit and dah functions

int buzzerPin = 11 ; // Deklaration des Buzzer-Ausgangspin
long buzzerLast = 0;
long buzzerNow = 0;
int buzzerFrequency = 440;
int buzzerIntervalMillis = 1000 / buzzerFrequency / 2;
int buzzerIntervalMicros = 1000000 / buzzerFrequency / 2;
boolean buzzerState = 0;

void stringBeep(String outputString = "..-..") {
  for (byte i = 0; i < outputString.length(); i++) {
    if (outputString[i] == '.') {
      ditBeep(buzzerPin);
      Serial.println(outputString[i]);
    } else if (outputString[i] == '-') {
      dahBeep(buzzerPin);
      Serial.println(outputString[i]);
    }
  }
  delay(delayBetweenCharacters);
}

void charBeep(char input) {
  stringBeep(alphabet[input - asciiTableShift][1]);
}

void setup() {
  Serial.begin(9600);
  clock_prescale_set(0);
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
  digitalWrite(buttonPin, HIGH);  //Internal pullup on Button Pin
  pinMode (buzzerPin, OUTPUT) ;// Initialisierung als Ausgangspin
  //stringBeep(alphabet[1]);
  charBeep('O');
  charBeep('K');
  //beep(buzzerPin, 1000);
  //delay(1000);
  //beep(buzzerPin, 500);
  //delay(1000);
  //beep(buzzerPin, 250);
  //delay(1000);
}

void loop() {
  if (Serial.available()) {
    byte characterASCII = Serial.read();
    //byte characterIndex = characterASCII - asciiTableShift;
    byte characterIndex = characterASCII;
    //while (Serial.available()) Serial.read();
    charBeep(characterIndex);
    Serial.println("I read sth.");
    if(characterASCII == '\n') delay(delayBetweenWords);
  }
}

void ditBeep(int buzzerPin) {
  beep(buzzerPin, ditMs);
  delay(ditMs);
}

void dahBeep(int buzzerPin) {
  beep(buzzerPin, dahMs);
  delay(ditMs);
}

void beep(int buzzerPin, int duration) {
  long start = millis();
  while ((millis() - start) < duration) {
    buzzerNow = micros();
    if ((buzzerNow - buzzerLast) >= buzzerIntervalMicros) {
      buzzerLast = buzzerNow;
      buzzerState = !buzzerState;
      digitalWrite(buzzerPin, buzzerState);
    }
  }
}

Fancying it up

Subsequently, I modified the code to automatically capitalize any characters sent to it. I also applied some filtering to make sure only characters from the appropriate range (numbers and capital letters/capitalized small letters, respectively) were beeped out in morse. Last but not least, I added a few (optional) debug methods that would send verbose feedback on the Serial port. Following output - besides the auditory beeping - is returned by the Arduino after sending the string “FabAcademy 2022” to it:

Received: F
Beeping letter: F
..-.

Received: a
Beeping letter: A
.-

Received: b
Beeping letter: B
-...

Received: A
Beeping letter: A
.-

Received: c
Beeping letter: C
-.-.

Received: a
Beeping letter: A
.-

Received: d
Beeping letter: D
-..

Received: e
Beeping letter: E
.

Received: m
Beeping letter: M
--

Received: y
Beeping letter: Y
-.--

Received: SPACE
I read a space and delayed.

Received: 2
Beeping letter: 2
..---

Received: 0
Beeping letter: 0
-----

Received: 2
Beeping letter: 2
..---

Received: 2
Beeping letter: 2
..---

Received: \n
I read a newline and delayed.

Arduino UNO Final Code

This is the final code on the Arduino UNO side:

#include <avr/power.h>

boolean debug = false;
byte asciiTableShift = 48; //value to subtract from incoming value to convert from ASCII ('A'=65) to local array index ('A'=index 0)
String alphabet[][2] = { { "0" , "-----" },
                         { "1" , ".----" },
                         { "2" , "..---" },
                         { "3" , "...--" },
                         { "4" , "....-" },
                         { "5" , "....." },
                         { "6" , "-...." },
                         { "7" , "--..." },
                         { "8" , "---.." },
                         { "9" , "----." },
                         { ":" , " " },
                         { ";" , " " },
                         { "<" , " " },
                         { "=" , " " },
                         { ">" , " " },
                         { "?" , " " },
                         { "@" , " " },
                         { "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" , "--.."}
                    };

int ledPin = 8;
int buttonPin = 7;

int ditMs = 50;
int dahMs = 3 * ditMs;
int delayBetweenCharacters = 2 * ditMs; //theoretically, this is 3 dit, but 1 dit is already included in the dit and dah functions
int delayBetweenWords = 6 * ditMs;  //theoretically, this is 7 dit, but 1 dit is already included in the dit and dah functions

int buzzerPin = 11 ; // Deklaration des Buzzer-Ausgangspin
long buzzerLast = 0;
long buzzerNow = 0;
int buzzerFrequency = 330;
int buzzerIntervalMillis = 1000 / buzzerFrequency / 2;
int buzzerIntervalMicros = 1000000 / buzzerFrequency / 2;
boolean buzzerState = 0;

void stringBeep(String outputString = "..-..") {
  for (byte i = 0; i < outputString.length(); i++) {
    if (outputString[i] == '.') {
      ditBeep(buzzerPin);
      if(debug) Serial.print(outputString[i]);
    } else if (outputString[i] == '-') {
      dahBeep(buzzerPin);
      if(debug) Serial.print(outputString[i]);
    }
  }
  if(debug) Serial.println();
  delay(delayBetweenCharacters);
}

void charBeep(char input) {
  stringBeep(alphabet[input - asciiTableShift][1]);
}

//if input character is a small ASCII letter, return the capital version of it, otherwise return same character
byte capitalizeASCII(byte input){
  if((input >= 97) && (input <=122)){
    return (input - 32);
  }
  return input;
}

void setup() {
  Serial.begin(9600);
  clock_prescale_set(0);
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
  digitalWrite(buttonPin, HIGH);  //Internal pullup on Button Pin
  pinMode (buzzerPin, OUTPUT) ;// Initialisierung als Ausgangspin
  //stringBeep(alphabet[1]);
  charBeep('O');
  charBeep('K');
  //beep(buzzerPin, 1000);
  //delay(1000);
  //beep(buzzerPin, 500);
  //delay(1000);
  //beep(buzzerPin, 250);
  //delay(1000);
}

void loop() {
  if (Serial.available()) {
    byte characterASCII = Serial.read();
    if(debug) Serial.print("Received: ");
    if(characterASCII == '\n'){
      if(debug) Serial.print("\\n");
    } else if(characterASCII == ' '){
      if(debug) Serial.print("SPACE");
    } else {
      if(debug) Serial.write(characterASCII);
    }
    if(debug) Serial.println();
    characterASCII = capitalizeASCII(characterASCII);
    //only beep for proper characters (starting at numbers, going up to letter "Z")
    if(characterASCII == ' '){
      delay(delayBetweenWords);
      if(debug) Serial.println("I read a space and delayed.");
    } else if(characterASCII == '\n'){
      delay(delayBetweenWords);
      if(debug) Serial.println("I read a newline and delayed.");
    } else if((characterASCII >= 48) && (characterASCII <= 90 )){
      if(debug) Serial.print("Beeping letter: ");
      if(debug) Serial.write(characterASCII);
      if(debug) Serial.println();
      charBeep(characterASCII);
    } else {
      if(debug) Serial.println("I read a mystery character.");
    }
    if(debug) Serial.println();
  }
}

void ditBeep(int buzzerPin) {
  beep(buzzerPin, ditMs);
  delay(ditMs);
}

void dahBeep(int buzzerPin) {
  beep(buzzerPin, dahMs);
  delay(ditMs);
}

void beep(int buzzerPin, int duration) {
  long start = millis();
  while ((millis() - start) < duration) {
    buzzerNow = micros();
    if ((buzzerNow - buzzerLast) >= buzzerIntervalMicros) {
      buzzerLast = buzzerNow;
      buzzerState = !buzzerState;
      digitalWrite(buzzerPin, buzzerState);
    }
  }
}

PC UI Quality of Life Updates

I then went on to add some quality of life updates to the Tkinter GUI. Mainly I wanted to see the Serial feedback from the board for debugging in the GUI directly. I also wanted to make the text box automatically send its contents to the board when hitting ENTER, without explicitly having to press the “Send to board” button.

The Serial input part was a little more complex due to the nature of how a Tkinter app is setup, so I mainly stuck to a tutorial I found online (thanks https://robotic-controls.com/learn/python-guis/tkinter-serial). I created a little text log that would print the Serial data streaming back from the board. To avoid using a scroll bar and/or auto scrolling, I used the text widget as is, meaning that whenever text is returned via Serial port, it is added as the top line of text.

The Enter key part was implemented using the bind() function (thanks https://stackoverflow.com/questions/16996432/how-do-i-bind-the-enter-key-to-a-function-in-tkinter):

import serial.tools.list_ports
from tkinter import *

ser = serial.Serial(port=None, baudrate=115200)

def connectToBoard():
    # Intialize Serial Port
    ser.setDTR()
    print(OptionList[0][0])
    ser.port = OptionList[0][0]
    ser.close()
    ser.open()
    ser.flush()
    root.after(100, readSerial)

def sendToBoard():
    print("\"" + myEntry.get() + "\" was sent to the Serial port!")
    ser.write(str.encode(myEntry.get()))

def readSerial():
    if ser.in_waiting > 0:
        myLog.insert('0.0', ser.readline())
    root.after(100, readSerial) #reregister next Serial port check

OptionList = serial.tools.list_ports.comports()

# Initialize GUI window
root = Tk()
root.geometry('300x200')

variable = StringVar(root)
variable.set(OptionList[0])

myDropdown = OptionMenu(root, variable, *OptionList)
myDropdown.pack()
myConnectButton = Button(root, text="Connect to board (COM port)", command=connectToBoard)
myConnectButton.pack()
myLabel = Label(root, text="Please enter a text to morse out:")
myLabel.pack()
myEntry = Entry(root)
myEntry.insert(4, "SOS")
myEntry.bind("<Return>", (lambda event: sendToBoard()))
myEntry.pack()
myButton = Button(root, text="Send to board", command=sendToBoard)
myButton.pack()
myLog = Text(root)
myLog.pack()

root.mainloop()

ser.close()

Here is the GUI with a text log area for showing Serial port input:

In the end, the GUI had all the functionality I wanted it to have for now. I can connect to my board, I can hand data over to it and I get feedback. Purrfect. Time to port the code for the ATTiny44 now.

Porting the Arduino UNO code to the ATTiny44 Echo Board

When adding the SoftwareSerial library and replacing all occurences of Serial by SoftwareSerial, it became obvious that the sketch used way too much memory for the poor ATtiny44:

text section exceeds available space in board
Sketch uses 5512 bytes (134%) of program storage space. Maximum is 4096 bytes.

Global variables use 972 bytes (379%) of dynamic memory, leaving -716 bytes for local variables. Maximum is 256 bytes.
Sketch too big; see http://www.arduino.cc/en/Guide/Troubleshooting#size for tips on reducing it.
Error compiling for board ATtiny24/44/84.

Time to cut some corners. First things first, I cut down the translation array to not include the actual characters next to the morse strings, saving quite some memory. Still too big though:

text section exceeds available space in board
Sketch uses 4998 bytes (122%) of program storage space. Maximum is 4096 bytes.

Global variables use 630 bytes (246%) of dynamic memory, leaving -374 bytes for local variables. Maximum is 256 bytes.
Sketch too big; see http://www.arduino.cc/en/Guide/Troubleshooting#size for tips on reducing it.
Error compiling for board ATtiny24/44/84.

Saving Space Big Time

There really is no need for the translation array to contain strings with dots and dashes, though, anyway. With a little more coding elbow grease, I changed the code to encode the dots and dashes in binary, with a 0 being a dot and a 1 being a dash, meaning that a single byte (8 bits) could store a morse pattern with 8 symbols - much more than the maximum 5 symbols used by numbers. Since not all morse patterns have the same length - e.g. an “E” is just a single symbol, while a “1” is made up of five symbols - another bit was dedicated to declare the beginning of the morse pattern. The morse pattern would be stored in the least significant bits of the byte, with the most significant 1 determining the start of the morse pattern. The most significant bits in front of the start would always be 0. Here’s how the function for that looked like:

void binaryBeep(byte outputBinary = 0b100100){
  //determine starting point of the morse pattern by using bitshift
    //compare whether MSB is 1
    //if so, beep out the next 7 bits (variable bitsToBeep starts out at 7)
    //if not, bitshift left once and check again
    //if equals "1", beep out the next 6 bits
    //...
      //when start is found, advance one more bit, 
  byte bitsToBeep = 7;
  while((outputBinary & 0b10000000) == 0b00000000){
    outputBinary = outputBinary << 1;
    bitsToBeep -= 1;
  }
  while(bitsToBeep > 0){
    outputBinary = outputBinary << 1;
    if((outputBinary & 0b10000000) > 0){
      dahBeep(buzzerPin);
    } else {
      ditBeep(buzzerPin);
    }
    bitsToBeep -= 1;
  }
  delay(delayBetweenCharacters);
}

After stripping down the functions to use binary input only and store all the morse patterns in binary, the size was nearly small enough to put on the ATTiny:

data section exceeds available space in board
Sketch uses 2936 bytes (71%) of program storage space. Maximum is 4096 bytes.
Global variables use 307 bytes (119%) of dynamic memory, leaving -51 bytes for local variables. Maximum is 256 bytes.
Not enough memory; see http://www.arduino.cc/en/Guide/Troubleshooting#size for tips on reducing your footprint.
Error compiling for board ATtiny24/44/84.

This was due to the debug functions still being active. After setting debug = false and thus omitting all the verbose debug messages from the code (which are stored as Strings), compiling for the ATTiny yielded following output:

Sketch uses 2704 bytes (66%) of program storage space. Maximum is 4096 bytes.
Global variables use 183 bytes (71%) of dynamic memory, leaving 73 bytes for local variables. Maximum is 256 bytes.

Testing the New Code on the Arduino UNO

To actually test the code, I went back to the Arduino UNO and added the binary beeping functions and binary morse pattern storage to the UNO’s code and tested it:

#include <avr/power.h>

boolean debug = true;
byte asciiTableShift = 48; //value to subtract from incoming value to convert from ASCII ('A'=65) to local array index ('A'=index 0)

String alphabet[][2] = { { "0" , "-----" },
                         { "1" , ".----" },
                         { "2" , "..---" },
                         { "3" , "...--" },
                         { "4" , "....-" },
                         { "5" , "....." },
                         { "6" , "-...." },
                         { "7" , "--..." },
                         { "8" , "---.." },
                         { "9" , "----." },
                         { ":" , " " },
                         { ";" , " " },
                         { "<" , " " },
                         { "=" , " " },
                         { ">" , " " },
                         { "?" , " " },
                         { "@" , " " },
                         { "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" , "--.."}
                    };

byte binaryAlphabet[] = { 0b111111,
                         0b101111,
                         0b100111,
                         0b100011,
                         0b100001,
                         0b100000,
                         0b110000,
                         0b111000,
                         0b111100,
                         0b111110,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0b101,
                         0b11000,
                         0b11010,
                         0b1100,
                         0b10,
                         0b10010,
                         0b1110,
                         0b10000,
                         0b100,
                         0b10111,
                         0b1101,
                         0b10100,
                         0b111,
                         0b110,
                         0b1111,
                         0b10110,
                         0b11101,
                         0b1010,
                         0b1000,
                         0b11,
                         0b1001,
                         0b10001,
                         0b1011,
                         0b11001,
                         0b11011,
                         0b11100
                    };

int ledPin = 8;
int buttonPin = 7;

int ditMs = 50;
int dahMs = 3 * ditMs;
int delayBetweenCharacters = 2 * ditMs; //theoretically, this is 3 dit, but 1 dit is already included in the dit and dah functions
int delayBetweenWords = 6 * ditMs;  //theoretically, this is 7 dit, but 1 dit is already included in the dit and dah functions

int buzzerPin = 11 ; // Deklaration des Buzzer-Ausgangspin
long buzzerLast = 0;
long buzzerNow = 0;
int buzzerFrequency = 330;
int buzzerIntervalMillis = 1000 / buzzerFrequency / 2;
int buzzerIntervalMicros = 1000000 / buzzerFrequency / 2;
boolean buzzerState = 0;

void stringBeep(String outputString = "..-..") {
  for (byte i = 0; i < outputString.length(); i++) {
    if (outputString[i] == '.') {
      ditBeep(buzzerPin);
      if(debug) Serial.print(outputString[i]);
    } else if (outputString[i] == '-') {
      dahBeep(buzzerPin);
      if(debug) Serial.print(outputString[i]);
    }
  }
  if(debug) Serial.println();
  delay(delayBetweenCharacters);
}

void binaryBeep(byte outputBinary = 0b100100){
  //determine starting point of the morse pattern by using bitshift
    //compare whether MSB is 1
    //if so, beep out the next 7 bits (variable bitsToBeep starts out at 7)
    //if not, bitshift left once and check again
    //if equals "1", beep out the next 6 bits
    //...
      //when start is found, advance one more bit, 
  boolean beeping = false;
  int bitsToBeep = 7;
  while((outputBinary & 0b10000000) == 0b00000000){
    outputBinary = outputBinary << 1;
    bitsToBeep -= 1;
  }
  while(bitsToBeep > 0){
    if((outputBinary & 0b10000000) > 0){
      dahBeep(buzzerPin);
    } else {
      ditBeep(buzzerPin);
    }
    outputBinary = outputBinary << 1;
    bitsToBeep -= 1;
  }
}

void charBeep(char input) {
  stringBeep(alphabet[input - asciiTableShift][1]);
}

void charBeepBinary(byte input) {
  binaryBeep(binaryAlphabet[input - asciiTableShift]);
}

//if input character is a small ASCII letter, return the capital version of it, otherwise return same character
byte capitalizeASCII(byte input){
  if((input >= 97) && (input <=122)){
    return (input - 32);
  }
  return input;
}

void setup() {
  Serial.begin(9600);
  clock_prescale_set(0);
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
  digitalWrite(buttonPin, HIGH);  //Internal pullup on Button Pin
  pinMode (buzzerPin, OUTPUT) ;// Initialisierung als Ausgangspin
  //stringBeep(alphabet[1]);
  charBeep('O');
  charBeep('K');
  charBeepBinary('O');
  charBeepBinary('K');
  //beep(buzzerPin, 1000);
  //delay(1000);
  //beep(buzzerPin, 500);
  //delay(1000);
  //beep(buzzerPin, 250);
  //delay(1000);
}

void loop() {
  if (Serial.available()) {
    byte characterASCII = Serial.read();
    if(debug) Serial.print("Received: ");
    if(characterASCII == '\n'){
      if(debug) Serial.print("\\n");
    } else if(characterASCII == ' '){
      if(debug) Serial.print("SPACE");
    } else {
      if(debug) Serial.write(characterASCII);
    }
    if(debug) Serial.println();
    characterASCII = capitalizeASCII(characterASCII);
    //only beep for proper characters (starting at numbers, going up to letter "Z")
    if(characterASCII == ' '){
      delay(delayBetweenWords);
      if(debug) Serial.println("I read a space and delayed.");
    } else if(characterASCII == '\n'){
      delay(delayBetweenWords);
      if(debug) Serial.println("I read a newline and delayed.");
    } else if((characterASCII >= 48) && (characterASCII <= 90 )){
      if(debug) Serial.print("Beeping letter: ");
      if(debug) Serial.write(characterASCII);
      if(debug) Serial.println();
      charBeep(characterASCII);
      charBeepBinary(characterASCII);
    } else {
      if(debug) Serial.println("I read a mystery character.");
    }
    if(debug) Serial.println();
  }
}

void ditBeep(int buzzerPin) {
  beep(buzzerPin, ditMs);
  delay(ditMs);
}

void dahBeep(int buzzerPin) {
  beep(buzzerPin, dahMs);
  delay(ditMs);
}

void beep(int buzzerPin, int duration) {
  long start = millis();
  while ((millis() - start) < duration) {
    buzzerNow = micros();
    if ((buzzerNow - buzzerLast) >= buzzerIntervalMicros) {
      buzzerLast = buzzerNow;
      buzzerState = !buzzerState;
      digitalWrite(buzzerPin, buzzerState);
    }
  }
}

The UNO has much more memory than the ATTiny board, so compiling with everything included - String morse pattern storage and binary - and with everything active, there were no error messages. Everything fit onto the board:

Sketch uses 5730 bytes (17%) of program storage space. Maximum is 32256 bytes.
Global variables use 1072 bytes (52%) of dynamic memory, leaving 976 bytes for local variables. Maximum is 2048 bytes.

In the test code, I doubled up every letter by beeping it once with the normal, String based function and then immediately with the binary morse function. I also added some distinction to the Serial output, so the feedback from the String beep function and the binary beep function could be distinguished. Following serial output was captured when going through the whole alphabet, a to z (all letters beeped identically - good sign!). Output preceded by ‘s’ came from the string, preceded by ‘b’ from the binary beep function:

Received: a
Beeping letter: A
s
.-
b
.-

Received: \n
I read a newline and delayed.

Received: b
Beeping letter: B
s
-...
b
-...

Received: \n
I read a newline and delayed.

Received: c
Beeping letter: C
s
-.-.
b
-.-.

Received: \n
I read a newline and delayed.

Received: d
Beeping letter: D
s
-..
b
-..

Received: \n
I read a newline and delayed.

Received: e
Beeping letter: E
s
.
b
.

Received: \n
I read a newline and delayed.

Received: f
Beeping letter: F
s
..-.
b
..-.

Received: \n
I read a newline and delayed.

Received: g
Beeping letter: G
s
--.
b
--.

Received: \n
I read a newline and delayed.

Received: h
Beeping letter: H
s
....
b
....

Received: \n
I read a newline and delayed.

Received: i
Beeping letter: I
s
..
b
..

Received: \n
I read a newline and delayed.

Received: j
Beeping letter: J
s
.---
b
.---

Received: \n
I read a newline and delayed.

Received: k
Beeping letter: K
s
-.-
b
-.-

Received: \n
I read a newline and delayed.

Received: l
Beeping letter: L
s
.-..
b
.-..

Received: \n
I read a newline and delayed.

Received: m
Beeping letter: M
s
--
b
--

Received: \n
I read a newline and delayed.

Received: n
Beeping letter: N
s
-.
b
-.

Received: \n
I read a newline and delayed.

Received: o
Beeping letter: O
s
---
b
---

Received: \n
I read a newline and delayed.

Received: p
Beeping letter: P
s
.--.
b
.--.

Received: \n
I read a newline and delayed.

Received: r
Beeping letter: R
s
.-.
b
.-.

Received: \n
I read a newline and delayed.

Received: s
Beeping letter: S
s
...
b
...

Received: \n
I read a newline and delayed.

Received: t
Beeping letter: T
s
-
b
-

Received: \n
I read a newline and delayed.

Received: u
Beeping letter: U
s
..-
b
..-

Received: \n
I read a newline and delayed.

Received: v
Beeping letter: V
s
...-
b
...-

Received: \n
I read a newline and delayed.

Received: w
Beeping letter: W
s
.--
b
.--

Received: \n
I read a newline and delayed.

Received: x
Beeping letter: X
s
-..-
b
-..-

Received: \n
I read a newline and delayed.

Received: y
Beeping letter: Y
s
-.--
b
-.--

Received: \n
I read a newline and delayed.

Received: z
Beeping letter: Z
s
--..
b
--..

Received: \n
I read a newline and delayed.

Here is the actual Arduino UNO code for this part:

#include <avr/power.h>

boolean debug = true;
byte asciiTableShift = 48; //value to subtract from incoming value to convert from ASCII ('A'=65) to local array index ('A'=index 0)

String alphabet[][2] = { { "0" , "-----" },
                         { "1" , ".----" },
                         { "2" , "..---" },
                         { "3" , "...--" },
                         { "4" , "....-" },
                         { "5" , "....." },
                         { "6" , "-...." },
                         { "7" , "--..." },
                         { "8" , "---.." },
                         { "9" , "----." },
                         { ":" , " " },
                         { ";" , " " },
                         { "<" , " " },
                         { "=" , " " },
                         { ">" , " " },
                         { "?" , " " },
                         { "@" , " " },
                         { "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" , "--.."}
                    };

byte binaryAlphabet[] = { 0b111111,
                         0b101111,
                         0b100111,
                         0b100011,
                         0b100001,
                         0b100000,
                         0b110000,
                         0b111000,
                         0b111100,
                         0b111110,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0b101,
                         0b11000,
                         0b11010,
                         0b1100,
                         0b10,
                         0b10010,
                         0b1110,
                         0b10000,
                         0b100,
                         0b10111,
                         0b1101,
                         0b10100,
                         0b111,
                         0b110,
                         0b1111,
                         0b10110,
                         0b11101,
                         0b1010,
                         0b1000,
                         0b11,
                         0b1001,
                         0b10001,
                         0b1011,
                         0b11001,
                         0b11011,
                         0b11100
                    };

int ledPin = 8;
int buttonPin = 7;

int ditMs = 50;
int dahMs = 3 * ditMs;
int delayBetweenCharacters = 2 * ditMs; //theoretically, this is 3 dit, but 1 dit is already included in the dit and dah functions
int delayBetweenWords = 6 * ditMs;  //theoretically, this is 7 dit, but 1 dit is already included in the dit and dah functions

int buzzerPin = 11 ; // Deklaration des Buzzer-Ausgangspin
long buzzerLast = 0;
long buzzerNow = 0;
int buzzerFrequency = 330;
int buzzerIntervalMillis = 1000 / buzzerFrequency / 2;
int buzzerIntervalMicros = 1000000 / buzzerFrequency / 2;
boolean buzzerState = 0;

void stringBeep(String outputString = "..-..") {
  Serial.println('s');
  for (byte i = 0; i < outputString.length(); i++) {
    if (outputString[i] == '.') {
      ditBeep(buzzerPin);
      if(debug) Serial.print(outputString[i]);
    } else if (outputString[i] == '-') {
      dahBeep(buzzerPin);
      if(debug) Serial.print(outputString[i]);
    }
  }
  if(debug) Serial.println();
  delay(delayBetweenCharacters);
}

void binaryBeep(byte outputBinary = 0b100100){
  //determine starting point of the morse pattern by using bitshift
    //compare whether MSB is 1
    //if so, beep out the next 7 bits (variable bitsToBeep starts out at 7)
    //if not, bitshift left once and check again
    //if equals "1", beep out the next 6 bits
    //...
      //when start is found, advance one more bit, 
  boolean beeping = false;
  int bitsToBeep = 7;
  while((outputBinary & 0b10000000) == 0b00000000){
    outputBinary = outputBinary << 1;
    bitsToBeep -= 1;
  }
  Serial.println('b');
  while(bitsToBeep > 0){
    outputBinary = outputBinary << 1;
    if((outputBinary & 0b10000000) > 0){
      dahBeep(buzzerPin);
      if(debug) Serial.print('-');
    } else {
      ditBeep(buzzerPin);
      if(debug) Serial.print('.');
    }
    bitsToBeep -= 1;
  }
  Serial.println();
}

void charBeep(char input) {
  stringBeep(alphabet[input - asciiTableShift][1]);
}

void charBeepBinary(byte input) {
  binaryBeep(binaryAlphabet[input - asciiTableShift]);
}

//if input character is a small ASCII letter, return the capital version of it, otherwise return same character
byte capitalizeASCII(byte input){
  if((input >= 97) && (input <=122)){
    return (input - 32);
  }
  return input;
}

void setup() {
  Serial.begin(9600);
  clock_prescale_set(0);
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
  digitalWrite(buttonPin, HIGH);  //Internal pullup on Button Pin
  pinMode (buzzerPin, OUTPUT) ;// Initialisierung als Ausgangspin
  //stringBeep(alphabet[1]);
  charBeep('O');
  charBeep('K');
  charBeepBinary('O');
  charBeepBinary('K');
  //beep(buzzerPin, 1000);
  //delay(1000);
  //beep(buzzerPin, 500);
  //delay(1000);
  //beep(buzzerPin, 250);
  //delay(1000);
}

void loop() {
  if (Serial.available()) {
    byte characterASCII = Serial.read();
    if(debug) Serial.print("Received: ");
    if(characterASCII == '\n'){
      if(debug) Serial.print("\\n");
    } else if(characterASCII == ' '){
      if(debug) Serial.print("SPACE");
    } else {
      if(debug) Serial.write(characterASCII);
    }
    if(debug) Serial.println();
    characterASCII = capitalizeASCII(characterASCII);
    //only beep for proper characters (starting at numbers, going up to letter "Z")
    if(characterASCII == ' '){
      delay(delayBetweenWords);
      if(debug) Serial.println("I read a space and delayed.");
    } else if(characterASCII == '\n'){
      delay(delayBetweenWords);
      if(debug) Serial.println("I read a newline and delayed.");
    } else if((characterASCII >= 48) && (characterASCII <= 90 )){
      if(debug) Serial.print("Beeping letter: ");
      if(debug) Serial.write(characterASCII);
      if(debug) Serial.println();
      charBeep(characterASCII);
      charBeepBinary(characterASCII);
    } else {
      if(debug) Serial.println("I read a mystery character.");
    }
    if(debug) Serial.println();
  }
}

void ditBeep(int buzzerPin) {
  beep(buzzerPin, ditMs);
  delay(ditMs);
}

void dahBeep(int buzzerPin) {
  beep(buzzerPin, dahMs);
  delay(ditMs);
}

void beep(int buzzerPin, int duration) {
  long start = millis();
  while ((millis() - start) < duration) {
    buzzerNow = micros();
    if ((buzzerNow - buzzerLast) >= buzzerIntervalMicros) {
      buzzerLast = buzzerNow;
      buzzerState = !buzzerState;
      digitalWrite(buzzerPin, buzzerState);
    }
  }
}

Porting Back to the ATTIny44 Echo Board - Pitfalls and Problems

After fixing last things in the ATTiny part and programming it via ISP, it didn’t actually beep at all. I then remembered that I hadn’t renumbered the MOSI pin from 11, as is on the Arduino UNO, to 6, as is on my ATTiny board. After changing the pin number, the buzzer actually came to life. I also stripped down the debug messages so the ATTiny would still return messages via SoftwareSerial, just not as verbose. The ATTiny code uploaded to the board, did not react to Serial input though. Something seemed to be off with the Software Serial port used, so I decided to write an MWE for testing this part of the code separately:

#include <SoftwareSerial.h>

SoftwareSerial mySerial(0, 1); // RX, TX
int counter = 0;

void setup() {
  // put your setup code here, to run once:
  mySerial.begin(300);
  mySerial.println("SoftwareSerial Test");
}

void loop() {
  // put your main code here, to run repeatedly:
  mySerial.println(counter, DEC);
  delay(1000);
  counter++;
}

Spiral Debugging - Adding Things to the MWE

This produced perfectly good Serial output (at a very low bitrate of 300 bit/s, that is). This also worked perfectly fine at a higher, more standard bitrate of 9600 Baud as well as 115200 Baud. I then subsequently added more and more functionality to the board to see what made it stumble:

  • Increasing a counter, sending counter per SoftwareSerial, delaying
  • Echoing SoftwareSerial input when data was available() on the SoftwareSerial port
  • Beeping the buzzer whenever a character was received
  • Using the ditBeep() and dahBeep() functions to beep the buzzer
  • Using the binaryBeep function to beep a byte streaming in on the SoftwareSerial port

The problem with the program freezing occured when adding the binaryAlphabet array into the mix, even though space was not maxed out:

Sketch uses 2990 bytes (72%) of program storage space. Maximum is 4096 bytes.
Global variables use 184 bytes (71%) of dynamic memory, leaving 72 bytes for local variables. Maximum is 256 bytes.
C:\Program Files (x86)\Arduino\hardware\tools\avr/bin/avrdude -CC:\Program Files (x86)\Arduino\hardware\tools\avr/etc/avrdude.conf -v -pattiny44 -cusbtiny -Uflash:w:C:\Users\rgr\AppData\Local\Temp\arduino_build_265364/ATTiny_SoftwareSerial_MWE.ino.hex:i

Here is some exemplary output from the Serial console:

3
4
5
6
7
8
9βΈ®

Execution Instability and PROGMEM

When using a few more variables before, the error message actually warned about potential stability problems (when Global variables were around 80%). This could be remedied by putting the large, constant array in program memory instead of RAM, using the PROGMEM keyword:

PROGMEM const byte binaryAlphabet[] = { 0b111111,
                         0b101111,
                         0b100111,
                         0b100011,
                         0b100001,
                         0b100000,
                         0b110000,
                         0b111000,
                         0b111100,
                         0b111110,
                         0b1,
                         0b1,
                         0b1,
                         0b1,
                         0b1,
                         0b1,
                         0b1,
                         0b101,
                         0b11000,
                         0b11010,
                         0b1100,
                         0b10,
                         0b10010,
                         0b1110,
                         0b10000,
                         0b100,
                         0b10111,
                         0b1101,
                         0b10100,
                         0b111,
                         0b110,
                         0b1111,
                         0b10110,
                         0b11101,
                         0b1010,
                         0b1000,
                         0b11,
                         0b1001,
                         0b10001,
                         0b1011,
                         0b11001,
                         0b11011,
                         0b11100
                    };

The SoftwareSerial output started working properly after doing so again. Here is the compile output (we see: dynamic memory usage went down):

Sketch uses 2990 bytes (72%) of program storage space. Maximum is 4096 bytes.
Global variables use 142 bytes (55%) of dynamic memory, leaving 114 bytes for local variables. Maximum is 256 bytes.
C:\Program Files (x86)\Arduino\hardware\tools\avr/bin/avrdude -CC:\Program Files (x86)\Arduino\hardware\tools\avr/etc/avrdude.conf -v -pattiny44 -cusbtiny -Uflash:w:C:\Users\rgr\AppData\Local\Temp\arduino_build_265364/ATTiny_SoftwareSerial_MWE.ino.hex:i

PROGMEM Pitfalls

Now another problem arose: With binaryAlphabet[] in PROGMEM, the array could not be referenced using a counter variable at all. Following code worked:

binaryBeep(binaryAlphabet[0]);

Yet following did not (freezes when trying to access the array):

int counter = 0;
binaryBeep(binaryAlphabet[counter]);

This was in a loop that would continuously increase counter and try to iterate through the array.

Compiler Optimization Pitfalls

Now when debugging problems like that, we have to remember that modern compilers are smart and perform all kinds of code optimizations when hitting the compile button. Compilers can, e.g., optimize variables. When a variable is declared in code, yet never used as an actual variable that changes during runtime, but de facto only used as a constant, the compiler used by the Arduino IDE will make it a constant in the background, minimizing the amount of variables needed to be stored in RAM. While this example works, for example:

byte counter = 0;
binaryBeep(binaryAlphabet[counter]);
//no increase, just always using 0 (might have been optimized by the compiler to be a constant)

This next one did not work, as counter was actually used as a dynamic variable here:

byte counter = 0;
counter = counter + 1;
binaryBeep(binaryAlphabet[counter]);
//does probably not work because variable is actually used as a variable here

After trying to figure out my code freezes for a while, I then remembered vaguely that using PROGMEM had a few more implications for the code, especially when retrieving data. When reading bytes of the binaryAlphabet array, I cannot just simply use the array as is, but use a specific read function to do so:

byte counter = 0;
counter = counter + 1;
binaryBeep(pgm_read_byte(binaryAlphabet+counter));

What the above code does is go into program Memory and read out a specific address. In this case this would be the address of the array binaryAlphabet, which marks the beginning of the array in storage memory. To access the elements of the array, the address can be incremented by adding an index to it, the function then reads out the memory at that address further downstream. Since arrays are stored as ordered blocks in memory, this does the same thing as using an index variable to access the elements of the array.

Changing all array calls to use the pgm_read_byte() function instead of directly accessing the array made all my problems go away. I implemented the same change in the original code (mind you I was still experimenting with my MWE here!) and it just worked. I only had forgotten to put a delay between letters when changing over to the binary morse beeping function, so I added that and arrived at following working code:

#include <avr/power.h>
#include <SoftwareSerial.h>

SoftwareSerial mySerial(0, 1); // RX, TX

boolean debug = false;  //debug is for verbose feedback
boolean feedback = true;  //echos the dits and dahs on the Serial port
byte asciiTableShift = 48; //value to subtract from incoming value to convert from ASCII ('A'=65) to local array index ('A'=index 0)
//int counter = 0;

PROGMEM const byte binaryAlphabet[] = 
                       { 0b111111,
                         0b101111,
                         0b100111,
                         0b100011,
                         0b100001,
                         0b100000,
                         0b110000,
                         0b111000,
                         0b111100,
                         0b111110,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0b101,
                         0b11000,
                         0b11010,
                         0b1100,
                         0b10,
                         0b10010,
                         0b1110,
                         0b10000,
                         0b100,
                         0b10111,
                         0b1101,
                         0b10100,
                         0b111,
                         0b110,
                         0b1111,
                         0b10110,
                         0b11101,
                         0b1010,
                         0b1000,
                         0b11,
                         0b1001,
                         0b10001,
                         0b1011,
                         0b11001,
                         0b11011,
                         0b11100
                    };

int ledPin = 8;
int buttonPin = 7;

int ditMs = 100;
int dahMs = 3 * ditMs;
int delayBetweenCharacters = 2 * ditMs; //theoretically, this is 3 dit, but 1 dit is already included in the dit and dah functions
int delayBetweenWords = 6 * ditMs;  //theoretically, this is 7 dit, but 1 dit is already included in the dit and dah functions

int buzzerPin = 6 ; // Deklaration des Buzzer-Ausgangspin
long buzzerLast = 0;
long buzzerNow = 0;
int buzzerFrequency = 330;
int buzzerIntervalMillis = 1000 / buzzerFrequency / 2;
int buzzerIntervalMicros = 1000000 / buzzerFrequency / 2;
boolean buzzerState = 0;

void stringBeep(String outputString = "..-..") {
  for (byte i = 0; i < outputString.length(); i++) {
    if (outputString[i] == '.') {
      ditBeep(buzzerPin);
      if(feedback) mySerial.print(outputString[i]);
    } else if (outputString[i] == '-') {
      dahBeep(buzzerPin);
      if(feedback) mySerial.print(outputString[i]);
    }
  }
  if(debug) mySerial.println();
  delay(delayBetweenCharacters);
}

void binaryBeep(byte outputBinary = 0b100100){
  //determine starting point of the morse pattern by using bitshift
    //compare whether MSB is 1
    //if so, beep out the next 7 bits (variable bitsToBeep starts out at 7)
    //if not, bitshift left once and check again
    //if equals "1", beep out the next 6 bits
    //...
      //when start is found, advance one more bit, 
  byte bitsToBeep = 7;
  while((outputBinary & 0b10000000) == 0b00000000){
    outputBinary = outputBinary << 1;
    bitsToBeep -= 1;
  }
  if(feedback) mySerial.println(outputBinary);
  while(bitsToBeep > 0){
    outputBinary = outputBinary << 1;
    if((outputBinary & 0b10000000) > 0){
      dahBeep(buzzerPin);
      if(feedback) mySerial.print('-');
    } else {
      ditBeep(buzzerPin);
      if(feedback) mySerial.print('.');
    }
    bitsToBeep -= 1;
  }
  if(feedback) mySerial.println();
  delay(delayBetweenCharacters);
}

/*
void charBeep(byte input) {
  stringBeep(alphabet[input - asciiTableShift]);
}
*/

void charBeepBinary(byte input) {
  //binaryBeep(binaryAlphabet[input - asciiTableShift]);
  binaryBeep(pgm_read_byte(binaryAlphabet + (input - asciiTableShift)));
}

//if input character is a small ASCII letter, return the capital version of it, otherwise return same character
byte capitalizeASCII(byte input){
  if((input >= 97) && (input <=122)){
    return (input - 32);
  }
  return input;
}

void setup() {
  mySerial.begin(115200);
  mySerial.println(mySerial.available());
  mySerial.println("MorseTiny");
  clock_prescale_set(0);
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
  digitalWrite(buttonPin, HIGH);  //Internal pullup on Button Pin
  pinMode (buzzerPin, OUTPUT) ;// Initialisierung als Ausgangspin
  charBeepBinary('S');
  charBeepBinary('O');
  charBeepBinary('S');
}

void loop() {
  //mySerial.println("Enter loop()");
  if (mySerial.available()) {
    byte characterASCII = mySerial.read();
    if(debug){
      mySerial.print("Received: ");
      if(characterASCII == '\n'){
        mySerial.print("\\n");
      } else if(characterASCII == ' '){
        mySerial.print("SPACE");
      } else {
        mySerial.write(characterASCII);
      }
      mySerial.println();
    }
    characterASCII = capitalizeASCII(characterASCII);
    //only beep for proper characters (starting at numbers, going up to letter "Z")
    if(characterASCII == ' '){
      delay(delayBetweenWords);
      if(debug) mySerial.println("I read a space and delayed.");
    } else if(characterASCII == '\n'){
      delay(delayBetweenWords);
      if(debug) mySerial.println("I read a newline and delayed.");
    } else if((characterASCII >= 48) && (characterASCII <= 90 )){
      if(debug) mySerial.print("Beeping letter: ");
      if(debug) mySerial.write(characterASCII);
      if(debug) mySerial.println();
      //charBeep(characterASCII);
      charBeepBinary(characterASCII);
    } else {
      if(debug) mySerial.println("I read a mystery character.");
    }
    if(debug) mySerial.println();
  }
  //mySerial.println("Exit loop()");
}

void dit(int blinkPin) {
  digitalWrite(blinkPin, HIGH);
  delay(ditMs);
  digitalWrite(blinkPin, LOW);
  delay(ditMs);
}

void dah(int blinkPin) {
  digitalWrite(blinkPin, HIGH);
  delay(dahMs);
  digitalWrite(blinkPin, LOW);
  delay(ditMs);
}

void sosBlink(int blinkPin) {
  dit(blinkPin);
  dit(blinkPin);
  dit(blinkPin);
  delay(delayBetweenCharacters);
  dah(blinkPin);
  dah(blinkPin);
  dah(blinkPin);
  delay(delayBetweenCharacters);
  dit(blinkPin);
  dit(blinkPin);
  dit(blinkPin);
}

void ditBeep(int buzzerPin) {
  beep(buzzerPin, ditMs);
  delay(ditMs);
}

void dahBeep(int buzzerPin) {
  beep(buzzerPin, dahMs);
  delay(ditMs);
}

void sosBeep(int buzzerPin) {
  ditBeep(buzzerPin);
  ditBeep(buzzerPin);
  ditBeep(buzzerPin);
  delay(delayBetweenCharacters);
  dahBeep(buzzerPin);
  dahBeep(buzzerPin);
  dahBeep(buzzerPin);
  delay(delayBetweenCharacters);
  ditBeep(buzzerPin);
  ditBeep(buzzerPin);
  ditBeep(buzzerPin);
}

void beep(int buzzerPin, int duration) {
  long start = millis();
  while ((millis() - start) < duration) {
    buzzerNow = micros();
    if ((buzzerNow - buzzerLast) >= buzzerIntervalMicros) {
      buzzerLast = buzzerNow;
      buzzerState = !buzzerState;
      digitalWrite(buzzerPin, buzzerState);
    }
  }
}

Finishing Touches

This was still a bit bloated though from its history, so I cleaned up the code and removed stuff I did not need anymore. This is the final code used on the board:

#include <avr/power.h>
#include <SoftwareSerial.h>

void beep(int buzzerPin, int duration);
void binaryBeep(byte outputBinary);
void charBeepBinary(byte input);
byte capitalizeASCII(byte input);

SoftwareSerial mySerial(0, 1); // RX, TX

boolean debug = false;  //debug is for verbose feedback
boolean feedback = true;  //echos the dits and dahs on the Serial port
byte asciiTableShift = 48; //value to subtract from incoming value to convert from ASCII ('A'=65) to local array index ('A'=index 0)
//int counter = 0;

PROGMEM const byte binaryAlphabet[] = 
                       { 0b111111,
                         0b101111,
                         0b100111,
                         0b100011,
                         0b100001,
                         0b100000,
                         0b110000,
                         0b111000,
                         0b111100,
                         0b111110,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0,
                         0b101,
                         0b11000,
                         0b11010,
                         0b1100,
                         0b10,
                         0b10010,
                         0b1110,
                         0b10000,
                         0b100,
                         0b10111,
                         0b1101,
                         0b10100,
                         0b111,
                         0b110,
                         0b1111,
                         0b10110,
                         0b11101,
                         0b1010,
                         0b1000,
                         0b11,
                         0b1001,
                         0b10001,
                         0b1011,
                         0b11001,
                         0b11011,
                         0b11100
                    };

byte ledPin = 8;
byte buttonPin = 7;

int ditMs = 50;
int dahMs = 3 * ditMs;
int delayBetweenCharacters = 2 * ditMs; //theoretically, this is 3 dit, but 1 dit is already included in the dit and dah functions
int delayBetweenWords = 6 * ditMs;  //theoretically, this is 7 dit, but 1 dit is already included in the dit and dah functions

int buzzerPin = 6 ; // Deklaration des Buzzer-Ausgangspin
long buzzerLast = 0;
long buzzerNow = 0;
int buzzerFrequency = 330;
int buzzerIntervalMillis = 1000 / buzzerFrequency / 2;
int buzzerIntervalMicros = 1000000 / buzzerFrequency / 2;
boolean buzzerState = 0;

void setup() {
  mySerial.begin(115200);
  mySerial.println(mySerial.available());
  mySerial.println(F("MorseTiny"));
  mySerial.println(F("Send characters to the Serial port to morse!"));
  clock_prescale_set(0);
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
  digitalWrite(buttonPin, HIGH);  //Internal pullup on Button Pin
  pinMode (buzzerPin, OUTPUT) ;// Initialisierung als Ausgangspin
}

void loop() {
  if (mySerial.available()) {
    byte characterASCII = mySerial.read();
    if(debug){
      mySerial.print(F("Received: "));
      if(characterASCII == '\n'){
        mySerial.print(F("\\n"));
      } else if(characterASCII == ' '){
        mySerial.print(F("SPACE"));
      } else {
        mySerial.write(characterASCII);
      }
      mySerial.println();
    } else if (feedback) {
      mySerial.write(characterASCII);
      mySerial.print(F(" -> "));
    }
    characterASCII = capitalizeASCII(characterASCII);
    if (feedback) {
      mySerial.write(characterASCII);
      mySerial.println();
    }
    //only beep for proper characters (starting at numbers, going up to letter "Z")
    if(characterASCII == ' '){
      delay(delayBetweenWords);
      if(debug) mySerial.println(F("I read a space and delayed."));
      if(feedback) mySerial.println(F("SPC"));
    } else if(characterASCII == '\n'){
      delay(delayBetweenWords);
      if(debug) mySerial.println(F("I read a newline and delayed."));
      if(feedback) mySerial.println(F("NL"));
    } else if((characterASCII >= 48) && (characterASCII <= 90 )){
      if(debug) mySerial.print(F("Beeping letter: "));
      if(debug) mySerial.write(characterASCII);
      if(debug) mySerial.println();
      //charBeep(characterASCII);
      charBeepBinary(characterASCII);
    } else {
      if(debug) mySerial.println(F("I read a mystery character."));
      if(feedback) mySerial.println(F("???"));
    }
    if(feedback) mySerial.println();
  }
}

void ditBeep(int buzzerPin) {
  beep(buzzerPin, ditMs);
  delay(ditMs);
}

void dahBeep(int buzzerPin) {
  beep(buzzerPin, dahMs);
  delay(ditMs);
}

void beep(int buzzerPin, int duration) {
  long start = millis();
  while ((millis() - start) < duration) {
    buzzerNow = micros();
    if ((buzzerNow - buzzerLast) >= buzzerIntervalMicros) {
      buzzerLast = buzzerNow;
      buzzerState = !buzzerState;
      digitalWrite(buzzerPin, buzzerState);
    }
  }
}

void binaryBeep(byte outputBinary = 0b100100){
  //determine starting point of the morse pattern by using bitshift
    //compare whether MSB is 1
    //if so, beep out the next 7 bits (variable bitsToBeep starts out at 7)
    //if not, bitshift left once and check again
    //if equals "1", beep out the next 6 bits
    //...
      //when start is found, advance one more bit, 
  if(feedback) mySerial.println(outputBinary, BIN);
  byte bitsToBeep = 7;
  while((outputBinary & 0b10000000) == 0b00000000){
    outputBinary = outputBinary << 1;
    bitsToBeep -= 1;
  }
  while(bitsToBeep > 0){
    outputBinary = outputBinary << 1;
    if((outputBinary & 0b10000000) > 0){
      dahBeep(buzzerPin);
      if(feedback) mySerial.print('-');
    } else {
      ditBeep(buzzerPin);
      if(feedback) mySerial.print('.');
    }
    bitsToBeep -= 1;
  }
  if(feedback) mySerial.println();
  delay(delayBetweenCharacters);
}

void charBeepBinary(byte input) {
  //binaryBeep(binaryAlphabet[input - asciiTableShift]);
  binaryBeep(pgm_read_byte(binaryAlphabet + (input - asciiTableShift)));
}

//if input character is a small ASCII letter, return the capital version of it, otherwise return same character
byte capitalizeASCII(byte input){
  if((input >= 97) && (input <=122)){
    return (input - 32);
  }
  return input;
}

This worked perfectly fine with the Python-based Tkinter GUI. Text can be entered in the GUI and then sent to the board using a Serial port (in this case provided by an FTDI cable). The serial data then gets read on the ATTiny’s SoftwareSerial port, converted into a morese pattern and morsed out using a buzzer. The ATTiny also gives some feedback back on the Serial port which is displayed in the GUI.

Here is a video of the device in interactio with the GUI in action:


Last update: May 11, 2022 11:59:20
Back to top