Program structure and flow

The purpose of this page is to help you dive into the RFzero programs and change or add functionality. The purpose is NOT to teach you how to write programs in Arduino. There are many places where you can learn that.

If all what you want to do is to use the RFzero programs, or third party programs, the way they are, then you don’t need to understand anything about the underlying program structure and flow.

If you have wishes to this page please add them to this thread in the user group.

Presentation from the intermediate webinar 2020-06-20.


General Arduino structure

Generally speaking an Arduino sketch, i.e. program, consists of only one file called “filename”.ino. However, often other files are used to break down the program into small, and more manageable files with the .cpp and .h extensions.

The .ino file ALWAYS has at least two functions

  • void setup()
    this is the first function that runs and it only runs ones
  • void loop()
    when setup() has finished the next in line is the the loop() function and this runs over and over again hence the name loop.

The main Arduino file .ino.

In the header file other header files may be included. In the header file variables and functions are declared.

Header file .h that matches the .cpp file.

In the .cpp files the variables and functions are written.

The .cpp file that contains the extra functions.

To minimize confusion it is a good idea to use matching filenames for the .h and .cpp files. There can be as many .h and .cpp files as you like.

The above structure is also the case for the RFzero programs, and in most cases at least a set of command.cpp/~.h, config.cpp/~h, display.cpp/~.h and global.cpp/~h files are also used.


Hardware and software interactions

The RFzero is both a hardware and software platform, that goes hand in hand through the RFzero and Arduino libraries. The RFzero programs, that comes with the RFzero library, are there for your use, modification or inspiration. You can write your own programs using the library or write everything yourself from scratch, the RFzero hardware doesn’t care.

The RFzero programs can be grouped into different categories

  • The beacon and transmitter programs that are configured and then put into operation and “forgotten”
  • The continuous user interaction programs like the signal generator and VFO
  • Service programs with no or limited interaction like the GPSDO, QO-100 and Multi LO programs
  • Test programs that are outside any category

General H/W and S/W interactions of most RFzero programs.

To use the any of the RFzero libraries it is important include them in the declaration, just like any other library you want to use

  • #include <RFzero.h>
  • #include <RFzero_modes.h>
  • #include <RFzero_util.h>

If you forget to include a library, you will get an error message, when you try to compile the program.


Program structure

Generally speaking most of the RFzero programs follow the same structure. First the various libraries are included, and the variables are created. Then the RFzero is initialized, and the configuration parameters are loaded from the EEPROM, and some program specific one time settings are evoked in the setup() function.

Next the loop() function runs continuously servicing the program specific functionality, e.g. maintaining the Si5351A frequency, modulating the signal, scanning rotary encoder(s), checking push buttons … Also the yield() function is called regularly to look for USB and GPS data.

Very simplified flow chart.

Another way to depict the program structure is shown below, where the function calls and variable flows between the program files are illustrated.

Program structure showing how functions are called and values are transferred.


MMI configuration flow

When an MMI command is received from the USB port a series of events occur

  1. The MMI command is received by the yield() function in the .ino file
  2. The MMI command is sent to the command parser function in the command.cpp file
    If the command is a “config” command a configuration flag is set, and the program is configuration mode
  3. If the command is valid, and includes data to be saved, this is written to the EEPROM
    A configuration has changed flag is also set
  4. When the “exit” command is received, and if the configuration flag is set, the configuration is loaded from the EEPROM
  5. The loaded EEPROM values are written to the global variables
  6. The display is updated if relevant
  7. Finally the new global variables affect the program flow

MMI configuration flow where the numbers indicate the sequence of events.


Inside the files

Most of the RFzero programs have nine files

  • The program .ino file
  • A commands.cpp and a commands.h file
  • A config.cpp and a config.h file
  • A display.cpp and a display.h file
  • A global.cpp and a global.h file

When other files are found they always come in pair named “FileName”.cpp and “FileName”.h. This is done to break down the size of the individual files. This makes it easier to group functions and ease the overview. It does, however, add a bit more complexity, but the tradeoff is still to use the extra files.

Fundamentally, there is nothing wrong in having everything in the .ino file, but it will become very big, when the functionality of the program grows. For the same reason the smaller RFzero programs, with limited functionality, may only consist of the .ino file.

If you need to add a function, that can be accessed from other files, you will have to add the function declaration to the relevant header file, i.e “filename”.h.

If you want to add a new parameter, i.e. variable, please see “Adding new parameter” section below.

commands.cpp/~.h

In commands.cpp file the MMI commands, coming from the USB port, are parsed, i.e. split into pieces. If the command is valid the value(s) is saved to the EEPROM, help text displayed or the current values are shown. If the command is invalid an error message is shown. The command parser flow is as follows:

  • Receives the MMI strings from the yield() function
  • Trims the received strings
  • Checks if the program is in run or config mode
  • If “config” is received the program is put into config mode
  • Checks if the command is valid and processes the variable(s)
  • When the “exit” command is received, and the configuration was changed, a function in config.cpp is called to load all the parameters – both changed and unchanged

“High level code” showing the functional principle of the commands.cpp file.

config.cpp/~.h

In the config.cpp files the configuration parameters are read from the EEPROM. In the config.h files the EEPROM map addresses are defined.

When the program start the LoadConfiguration() function called by the setup() function and the EEPROM data is read. Afterwards the EEPROM is only loaded when changes to the configuration have been made and an “exit” command has been evoked.

  • Loads parameters from the EEPROM
  • Relies on the EEPROM map
  • Checks to see if the parameter value is valid, and if not a valid value is written to the EEPROM
  • Sets global configurable variables, and updates the display

“High level code” showing the functional principle of the config.cpp file.

global.cpp/~.h

The global.cpp and global.h files are where the global variables are declared. A variable may be needed for the program flow or as configurable parameter.

A variable has to be declared in both files, and in the global.h file it must be declared as an external variable, e.g.

  • global.h: extern int displayMode;
  • global.cpp: int displayMode;

This is because the variable is declared in the global.cpp file, but is accessible to other files via the global.h file. Global variables are directly accessible. Thus, no function calls are needed to get or set them.

display.cpp/~.h

In the display.cpp file all the functions that initialize or write to the display are located. Each function has a matching function declaration in the display.h file. In most cases the information displayed are global variables or called by the relevant display function, e.g. GPS data. On rare occasions the information to be displayed is parsed as a variable in the function call.

  • Initializes the display. It must be called after changing the display mode
  • Update function(s) must be called whenever there is a change
  • Update functions handle the different display modes
  • Parameters are often global or GPS variables, otherwise they are passed as parameters

All the functions in the display.cpp file are controlled by the chosen display mode. This means that inside each function there is an “if” and a series of “else if” sections for each of the display modes. Each prints the relevant information at the right place. Sometimes a section for a display mode may be empty or sometimes it has been removed. The end result is the same, the display mode is not supported. If you need a new mode just add a new section with the relevant “else if” display mode check.

“High level code” showing the functional principle of the display.cpp file.

Displays are slow devices. Thus, don’t update the information if hasn’t been changed. Especially the TFT displays are slow since the data to be printed also includes the position, text size, background and foreground colors.


The Arduino yield() function

The yield() function is a standard Arduino function that is little known. It is of the type “weak”. This means that it can be “replaced” by a custom function with the same name by the programmer. The yield() function is included in the standard Arduino delay functions. This way background jobs can be serviced even during delays that would otherwise block if they are not interrupt driven.

In many of the RFzero programs the yield() function has been replaced by a custom yield() function, that receives the GPS and USB data. If the yield() function is not called the first sign is that the USB port will not respond, and the RFzero will eventually stall.

It is therefore important to call the yield() function in continuous, long or slow loops. Sometimes the yield() functions may be called more than ones if the loop contains a lot of functionality.

You may add functionality to the yield() function if you like. However, make sure it is fast and not executed more than necessary, i.e. add a counter to throttle back calling the extra functionality.


The RFzero library

The RFzero library is a collection of functions, where some are specific to the RFzero, while others are generic. You can read more about the RFzero library here.

The only functionality that the RFzero library does by default is to measure the crystal frequency of the Si5351A, and collects the GPS data, but it does’t process the GPS data automatically. This has to be done by calling the gps.nmeaAutoParse() function.

The crystal frequency result is available via a function call to freqCount.getReferenceFrequency() function. Please remember that when the frequency counter is used for counting external frequencies, like done in the FrequencyCounter program, the Si5351A frequency counter is disabled.

In the RFzero programs the use of interrupts are limited to measuring the Si5351A crystal frequency and timing of the modulation. This allows you to have more interrupts available and less conflicts. But just like interrupt functions the use of functionality executed by the yield() function should be kept small and fast.


Adding new parameters

If you want to add a new parameter, i.e. a global variable, the process is as follows

  1. Create the parameter in the global.h file
    e.g. external int myNewParameter;
    remember the external declaration
  2. Create the parameter in the global.cpp file
    e.g. int myNewParameter;
    you may assign a value to it here if you want, e.g. int myNewParameter = 73;
  3. Assign an EEPROM address to the new parameter, while observing the EEPROM map and the size of the parameter, and create it in the config.h file
    e.g. #define EEPROM_MyOwn_myNewParameter 1000
    where 1000 is the address in the EEPROM
  4. Load the value, from the EEPROM, of the parameter in the LoadConfiguation() function in the config.cpp file
    e.g. myNewParameter = eeprom.readInteger(EEPROM_MyOwn_myNewParameter, 0);
  5. In the commands.cpp file add a command that makes it possible to change the parameter
    Hint: find an existing command that is similar to the new parameter then copy, paste and edit it to match the criteria of the new parameter
  6. In the “rd cfg” command, in the commands.cpp file, add the new parameter so it will be included when reading the configuration. Otherwise it is may be difficult to know its value
  7. In the “?”/”help” command, in the commands.cpp file, add a description to the help text

The new parameter is now available to the program, and may be used to control the program flow and/or shown on the display.

If you want to remove a parameter the process is the same, but removing it from the use across all the files.


The Template program

If you want to write a program almost from scratch the Template program has all of the above, and is a fast way to start a new program. Alternatively you may use one of the other RFzero programs, that comes closest to what you want, and modify it to match the requirements.


The GPSDO program

The GPSDO program is a simple program that incorporates all of the above. All it does is to set and maintain a single output frequency and update the display. Thus, it is a good program to study before diving into the other programs.

The below code is from the version 1.0.0 of the RFzero library.

The GPSDO.ino file

The GPSDO.ino file consists of four sections

  • The declarations
  • The setup() function
  • The loop() function
  • The yield() function, that replaces the default Arduino yield() function
The declaration section

The first thing that happens in the declaration is to include the core RFzero library and the RFzero utilities library.

Then comes the includes of the global, display, config and commands header files. If some of the files use a function in one of the the other files, it is important that the includes are done in the right order, e.g. if a variable has not been compiled yet, it is not visible to the rest of the program.

Finally the variable, that are only visible to the .ino file, are declared.

The setup() function

In the setup() function the first thing it does, is to initialize the RFzero library, that also starts the Si5351A frequency counter. The call to the initialization function must include the type of EEPROM used.

The next thing is to start the USB port. It take up to 2 s for the USB H/W to be ready to receive or send data. Thus a fixed delay is used.

Then follows an EEPROM check to see if it has some basic data which means that some configuration has happened, and it is safe to proceed. If the EEPROM is not configured, the program automatically enters the configuration mode, and waits for the user to do something, i.e. at least an “exit” command, before proceeding.

If the EEPROM was configured a welcome message is printed on the USB port.

Then all of the GPSDO configuration parameters are loaded, and if a display is connected a splash screen is shown.

If the warm up parameter has been set the program waits for the set duration.

If the program has been configured to wait for a valid GPS the program will wait.

Finally the RF is enabled and a timestamp is made. The time stamp is used later on in the loop() function.

The loop() function

In the loop function the set GPSDO frequency is maintained. This is done every time the Si5351A reference frequency has changed. This can at most be ones per second, even if the loop is much much faster.

Ones per loop cycle the yield() function is called.

After running for five minutes the Si5351A reference frequency is saved to the EEPROM, so the GPSDO will be spot on the next time the program is started.

The yield() function

The yield() function collects incoming USB data and calls the gps.nmeaAutoParse() function.

Every time a character is received, from the USB port, the buffer is checked to see if there is still room for more characters. Otherwise the buffer is cleared.

Unless the received character is a Line Feed the character is appended to the buffer. If a Line Feed character is received the USB data is sent to the ParseCommand() function in the comands.cpp file. When returning from the parser function the USB buffer is cleared.

If there was GPS data to parse it may also be echoed to the USB port, if the GPS echo parameter is enabled. Since the GPS data may also printed on the display, i.e. if displayMode is larger than 0, the Display_GPSUpdate() function is also called. The displayAutoUpdate variable is used to prevent the GPS data to be printed before the display mode has been loaded, the display is ready and the splash screen has been shown.

The commands files

The commands.h file

The commands.h file only has two variables namely the configuration mode flag and configuration changed flag. It has just one function the command parser. All are included in the commands.h file, so they can be accessed from other parts of the program.

The #ifndef _COMMANDS_H, #define _COMMANDS_H and #endif make sure that the files are only included/complied ones.

The commands.cpp file

The commands.cpp file also includes its own commands.h file and the other .h files. The latter makes it possible for the parser function to access the global variables and functions in the other files.

The ParseComand function first trims the received command and removes duplicate spaces. Then the command falls through the if, else if tree to see if there is a match. If not an error is printed.

When there is a command match the variable(s) included with the command is processed. Below is an example of processing the “wr display” command

The strncmp() function, (0 == strncmp(“wr display “, str, sizeof(“wr display “) – 1)), compares the received string with “wr display “. If there is a match zero (0) is returned.

Next is the use of the sscanf() function, (1 == sscanf(&str[sizeof(“wr display “) – 1], “%u”, &value)), that extracts the variable, display mode, from the string.

Finally, there is a check if the display mode is valid, and if so it is written to the EEPROM, the configuration changed flag is set and a no-error code is set in the command status comStatus variable. If there was an error the relevant error code is set.

The config files

The config.h file

The config.h file contains the EEPROM map definition and access to the LoadConfiguration () function.

The #ifndef _CONFIG_H, #define _CONFIG_H and #endif make sure that the files are only included/complied ones.

The config.cpp file

The LoadConfiguration() function loads the values from the EEPROM. When the splash boolean parameter is set the display initializing function is called showing the spalsh screen. When the function is called the current display mode is stored so the display can be properly initialized if it has been changed.

One by one the EEPROM parameters are loaded. If there is a mismatch between the read value and a valid value, a default value is used, and this is also written to the EEPROM. This is done to prevent invalid situations that could otherwise lock up the program.

Finally the display is updated, if the display mode has been changed.

The global files

The global.h file

In the global.h file the global variables are made accessible to other parts of the program.

The #ifndef _GLOBAL_H, #define _GLOBAL_H and #endif make sure that the files are only included/complied ones.

The global.cpp file

The initial values for the global variables are set in the global.cpp file.

E.g. setting a variable like the display mode to -1 will ensure that any valid EEPROM value, 0-255, will be diffent forcing a relevant action in the config.cpp file.

The display files

Please remember that the display update functions should only be called when there is a change. This is because displays are slow devices.

The display.h file

The display.h file has three functions available to other parts of the program.

The #ifndef _DISPLAY_H, #define _DISPLAY_H and #endif make sure that the files are only included/complied ones.

The display.cpp file

The display.cpp file contains all declaration and includes necessary for the display variants, i.e. LCDs, LCDs via I2C and TFTs and their H/W connections.

The hdopGlyphs() function defines a custom set of characters used for the GPS HDOP bars in the LCDs.

Within each of the three functions

  • Display_StartScreen(bool splash)
  • Display_LocalNormalDSTUpdate()
  • Display_GPSUpdate()

there are, up to, six if, else if sections each handling the specific display for the function both when it comes which data to print, at which X,Y location and how to print it.


The other programs

Like the GPSDO program the beacon, transmitter (WSPR transmitter) and service (Multi LO and QO-100 ) programs only have the same nine files, i.e. .ino, commands.cpp/~h, config.cpp/~h, display.cpp/~h and global.cpp/~h. But the continuous user interaction programs (signal generator and VFO) have many other pairs of .cpp/.h files because they have to service rotary encoders, push buttons, keypad, calculate atenuator bits, set filters etc. The code for these peripheral devices and functions put in their own file pairs.