Introduction: Serial Communication (UART) Between an Arduino Board and the Clock Generator

In this tutorial I will show you how to use a Narrow board equipped with an OLED (but you could use any Arduino board) to send commands to The Clock Generator through a Serial (UART) network. We will send a command to change the frequency and then a command to sweep frequencies from 10 MHz to 20 MHz by step of 0.1 MHz on clock 1. The OLED will allow to log everything.

This relatively simple projects covers many aspects of sending data through UART, receiving them, using a buffer to store complete commands, parsing the commands, calling a function from the ClockGen library and printing on an OLED screen and a TFT screen. So even if you don't have neither a Narrow board or a Clock Generator, you will probably find some code snippet interesting to you.

It does also show an example of communication between an Atmega based board and an STM32 based board using only Arduino programming.

Supplies

  • A 6V to 12V power source.
  • One Narrow board or another Arduino board (UNO, NANO etc).
  • One 0.49" I2C OLED module (you can get it with the Narrow board).
  • 2 * 2 male/female Dupont cables and a breadboard or 2 * 2 female/female Dupont cables.
  • The Clock Generator.
  • One USB A to mini B cable to program the boards.

Step 1: Principle of Working - You Control the Clock Generator From the Serial Monitor

The Clock Generator is a standalone board that provides 3 programmable clocks. Being Arduino compatible you can change the default program. In our example we will upload a new program that allows to communicate with the clock generator using the Serial port to modify the Clock Generator settings. Any board or even the Arduino Serial Monitor can send data as long as the commands are properly formatted. In this example we will use the Arduino IDE's Serial Monitor to send commands and get back the updated state of the device:

1) Loading the Arduino sketch named ClockGen_serial_control.ino to the Clock Generator board using the USB port in DFU mode (you might need to reset the board so the computer can detect the DFU device).

2) A COM port is then enumerated. After launching the Serial Monitor you can type a command. It is sent to the Clock Generator USB/Serial port.

3) The ClockGen_serial_control.ino has a function named processIncomingBytes() that buffers the bytes received until it receives the end of line (\n).

4) It logs each byte received on the TFT screen.

5) When a complete and meaningful package is received the proper function of the ClockGen library is called to modify the configuration of the device.

6) The updated configuration is queried from the ClockGen library.

7) This info is transferred to the USB/Serial interface.

8) It is carried through the USB COM port to the Serial Monitor.

Step 2: Principle of Working - the Controlling Board Controls the Clock Generator

Now we will use the Narrow board to control the Clock Generator:

1) Loading the Arduino sketch named ClockGen_serial_control.ino to the Clock Generator board using the USB port in DFU mode.

2) Loading the Arduino sketch named SendCommands.ino to the Narrow board using the USB COM port (of course this means that you will have to change the board settings in the Arduino IDE). The Narrow board is a good solution here because it has 2 serial ports and can have an OLED attached on top of it.

3) After loading the sketches, we might not need the computer anymore. The SendCommands.ino sketch will send a command to the Clock Generator through one of its Serial ports. If you have only one port available you can use it if you disconnect the USB cable to make sure the USB will not interfere.

4) It will log the command on the OLED.

5) The Serial interface sends data from its Tx pin to the Rx pin of the Clock Generator.

6) The ClockGen_serial_control.ino has a function named processIncomingBytes() that buffers the bytes received until it receives the end of line (\n).

7) Each byte received is logged on the TFT screen to make it more visual for the user.

8) A parser analyzes the command. If it has a meaningful command and the expected parameters, it will call the appropriate ClockGen function to modify the configuration of the device.

9) The current state of the device is read from the device and this info is sent to the Serial interface.

10) The data is sent from the Tx pin of the Clock Gen to the Rx pin of the Controlling board.

11) The processIncomingBytes() function of the SendCommands.ino buffers de received data.

12) When a complete packet is received, this raw data is sent to the OLED for logging. So, the user knows precisely if the Clock Generator was properly programmed. He can read the current state of the device that was transmitted back without any modification.

The arrows printed with dotted lines show an alternate path.

Step 3: The UART Network and the Format of Our Data Packages

Arduino compatible boards support UART communication. In its simplest form it is an asynchronous 2 wire communication system.

Both wires transport data. One wire sends data from the Tx pin of device 1 to the Rx pin of device 2. The other wire sends data from the Tx pin of device 2 to the Rx pin of device 1.

Being asynchronous, there isn't any clock line. So, the receiver and the sender have to send each bit at a defined period of time that is set by the baud rate. We will use a baud rate of 9600 bauds, which is slow, because we won't mind much about our physical implementation and simply use Dupont cables between the devices without much care. You can experiment with much higher baud rates (up to 1000000?). Of course, if the baud rate is not the same on the sending and receiving devices the apparent data received would be totally wrong.

The commands passed will be formatted as

command optional param 1;...optional param n \n

That is one command word followed by parameters separated by ; and the end of line byte (\n).

When sending commands from the Serial Monitor the end of line will be added by the Serial Monitor when typing enter. So, you will not type it. But you will need to include it in the package when sending programmatically.

The two pictures attached here are showing:

1) Sending the command "c0 sf 1000000 \n". You can see each data bit that was transferred (bullets) plus the technical bits. All are spaced equally with a period depending on the baud rate. The logic analyzer indeed shows the data in ASCII format c0(sp)sf(sp)1000000LF

2) Receiving back the current value of clock 0 frequency. The logic analyzer shows:

clock(sp)0(sp)frequency:(sp)1000000(sp)Hz(sp)CRLF

Step 4: Explanations on the Clock Generator Library

The Pandauino_Clock_Generator.h library defines a class and an instance of this class named « ClockGenerator ».

A couple of functions can be called from within any C++ program and particularly from an Arduino sketch. For example:

void begin(); initializes the program: Set up the Si5351A chip, retrieves values from EEPROM etc...

void run(); to be called in the Sketch loop to check the interaction with buttons etc

void setFrequency(int clockId, uint32_t frequency); to set the frequency in Hz of one of the clocks (id= 0,1 or 2)

uint32_t getFrequency(int clockId); to get the frequency in Hertz...

See the Clock the Clock Generator user’s manual for a full explanation.

Step 5: The Programming Environment and Software Prerequisites

  • To program the Clock Generator, which is an STM32 based board, you will need to add the board definitions and set the board parameters in the Arduino IDE correctly. See the Clock Generator user’s manual.
  • To program the Narrow board, which is an Atmega644 or Atmega1284 based board, you will also need to add the boards definition and set the board parameters in the Arduino IDE correctly. See the Narrow user's manual.
  • You can use another Arduino board or Arduino compatible board as the controlling board as long as it has enough flash (> 13 KB) and RAM (> 2 KB).

To control the board from the Serial Monitor you will need all the libraries referenced in the Clock Generator library so you will be able to compile the ClockGen_serial_control.ino sketch:

To control the board from the controlling board you will need the OLED library, that is a modified Adafruit library to support 64x32 OLEDs: https://github.com/mrguen/Adafruit_SSD1306-master-...

Finally you will find the ClockGen_serial_control.ino sketch in

https://github.com/mrguen/ClockGen/tree/main/examp...

And the sendCommands.ino sketch in

https://github.com/mrguen/ClockGen/tree/main/examp...

Step 6: The « ClockGen_serial_control.ino » Sketch

This sketch is made of

• The setup() function that:

o Starts the Serial com

o calls ClockGenerator.begin(); to set up the Clock Generator

o calls info(); to print over the Serial com the syntax of the possible commands.

void setup() { 
  mySerial.begin(9600); 
  while (!mySerial) {} 
  ClockGenerator.begin(DEBUG, 9600); 
  if (mySerial==Serial)  info(); 
}
  • The loop() function that:

o calls a function named processIncomingByte() to concatenate the received bytes whenever there is something inside the Serial buffer.

o Calls the function testSweep() that will change the clock frequencies if this is the time to do so

o Calls the function run() that catches any human interaction on clock gen (Menu and selector)

void loop()
{ 
  // if mySerial data available, process it 
  while (mySerial.available () > 0) { processIncomingByte (mySerial.read ());} 
  ClockGenerator.testSweep();     // If sweep is launched, changes the frequency after the programmed period of time 
  ClockGenerator.run();           // allows human interaction 
}
  • The processIncomingByte() function that concatenates the incoming bytes until it receives the \n sign or up to a maximum allowed length. Then it calls the function analyzeCommand(). It also log to the TFT each byte received so the user can check immediately if it is the expected data.
void processIncomingByte (const byte inByte)
{

  // Blank screen when expecting a new commande
  if (input_pos == 0) {  
    tft.fillScreen(ST7735_BLACK);
    tft.setCursor(2,50);
    tft.setFont(&FreeSans9pt7b);
    tft.setTextSize(1);
    tft.setTextColor(ST7735_WHITE);
    tft.setTextWrap(true);
  }

  // Prints each byte received on the TFT
  tft.print(char(inByte));
  delay(100);

  if (DEBUG) {
    Serial.print("Byte: ");
    Serial.write(inByte);
    Serial.write("\n");
  }
    
  switch (inByte) {

    case '\n':   // end of text
      input_line [input_pos] = 0;  // terminating null byte

      delay(1000);
      ClockGenerator.displayAllClocks();
      
      // terminator reached! process input_line here ...
      analyzeCommand(input_line);

      // reset buffer for next time
      input_pos = 0;  
      memset(input_line, 0, sizeof(input_line));      
      break;

    case '\r':   // discard carriage return
      break;

    default:
      // keep adding if not full ... allow for terminating null byte
      if (input_pos < (MAX_INPUT - 1))
        input_line [input_pos++] = inByte;
      break;
      
  }  // end of switch

} // end of processIncomingByte  <br>
  • The analyzeCommand() functionthat will check whether the concatenated value of bytes received is properly formatted. This function analyses the first letter than the next letters etc... expecting only a certain set of possible letters at each rank. When the passed data is meaningful it does

o calls the appropriate public function of the ClockGenerator instance.

o calls a print method that is used to print over the Serial com the new state of the ClockGenerator.

For example if the frequency was changed the printFreq () function is called, that calls the ClockGenerator.getFrequency(clockId) and sends the result over the serial network. This way the user who passed ordered to change the frequency gets back the effective frequency after change.

void analyzeCommand(char* data) {
  char firstLetter;
  int clockId;
  char thirdLetter;
  char fourthLetter;

  char dataTreat[50];
  char command[3];
  char buff[50];
  int value;

  removeSpaces(dataTreat, data);
  strtolower(data, dataTreat);
  
  firstLetter = data[0];
  
  //mySerial.println(data);
  //mySerial.println(firstLetter);
  //return;

  // Single Clock settings
  if ((firstLetter == 'c'))
  {
    strncpy(buff, data+1, 1);
    clockId = atoi(buff);
  
    // mySerial.println(clockId);
    
    if (!( (clockId == 0) || (clockId == 1) || (clockId == 2) )) {
    mySerial.println("Unknown clock id");
    return;    
    }

    if (strlen(data) < 4) { //c0sf + frequency on 4 digits minimum
      mySerial.println("Ill-formated command");
      return;
    }

    strncpy(buff, data+2, 2);
        
    // **************** Set frequency
    if (strcmp(buff,"sf") == 0) {

      // mySerial.println("sf ");
      
      strncpy(buff, data+4, strlen(data) -4);
      value = atoi(buff);

      if (value < SI5351_CLKOUT_MIN_FREQ) {
        mySerial.println("Frequency too low");
        return;
      }

      if (value > SI5351_CLKOUT_MAX_FREQ) {
        mySerial.println("Frequency too high");
        return;
      }
      
      ClockGenerator.setFrequency(clockId, value);
      ClockGenerator.displayAllClocks(); // pb sans doute de editMode
      printFreq(clockId);
    
    // **************** Get frequency
    } else if (strcmp(buff,"gf") == 0) {
      printFreq(clockId);

    // **************** Get phase step
    } else if (strcmp(buff,"gt") == 0) {
      mySerial.print("Clock ");
      mySerial.print(clockId);
      mySerial.print(" phase Step: ");
      mySerial.print(ClockGenerator.getPhaseStep(clockId));
      mySerial.println(" Hz"); 
 
    // **************** Set phase indice
    } else if (strcmp(buff,"sp") == 0) {

      strncpy(buff, data+4, strlen(data) -4);
      value = atoi(buff);

      if (value > 127) {
        mySerial.println("Phase indice must be less than 128");
      }
      ClockGenerator.setPhase(clockId, value); 
      ClockGenerator.displayAllClocks();
      printPhase(clockId);
                   
    // **************** Get phase
    } else if (strcmp(buff,"gp") == 0) {
       printPhase(clockId);
    
    // **************** Set drive 
    } else if (strcmp(buff,"sd") == 0) {
      
      strncpy(buff, data+4, strlen(data) -4);
      value = atoi(buff);

      if (! ((value < 4) && (value >= 0)) ) {
        mySerial.println("Drive indice must be 0 (2 mA), 1 (4 mA), 2 (6 mA) or 3 (8 mA)");
        return;
      }
      ClockGenerator.setDrive(clockId, value);
      ClockGenerator.displayAllClocks();
      printDrive(clockId); 
      
    } else if (strcmp(buff,"gd") == 0) {
      printDrive(clockId);  

    // **************** Set sweep params
    } else if (strcmp(buff,"ss") == 0) {
      
      strncpy(buff, data+4, strlen(data)-4); 

      char* ptr = strtok(buff, delimiter);
      int startFreq = atoi(ptr);
      int stopFreq = atoi(strtok(NULL, delimiter));

      bool active = true; // (boolean)activeInt;
      if (strcmp(strtok(NULL, delimiter),"0") == 0) active = false; 
      
      ClockGenerator.setSweepParamCx(clockId, startFreq, stopFreq, active);
      printCxSweep(clockId);
      return;     
      
    // **************** Get sweep params
    } else if (strcmp(buff,"gs") == 0) {
      printCxSweep(clockId);
      return;  
    }  
  
  // Parameters not clock specific   
  } else { 
    strncpy(buff, data, 2);    

    // **************** Get phase tied clocks 
    if (strcmp(buff,"gp") == 0) {      
      printPhaseTied();
      return;
    }
        
    // **************** Set phase tied clocks
    if (strcmp(buff,"sp") == 0) {
      strncpy(buff, data+2, 1);
      value = atoi(buff);      
      ClockGenerator.setPhaseTiedClocks((byte)value); 
      ClockGenerator.displayAllClocks();
      printPhaseTied();   
      return; 
    }

    // **************** Set global sweep params
    strncpy(buff, data, 8); 
    buff[8] = '\0';       // !!! Pourquoi fonctionne dans les autres cas alors que nécessaire a priori d'ajouter \0
    
    if (strcmp(buff,"sweepset") == 0) {
      
      strncpy(buff, data+8, strlen(data)-8);    
//      mySerial.println(buff);
      
      // initialize first part (string, delimiter)
      char* ptr = strtok(buff, delimiter);
      int nbStep = atoi(ptr);
      float period = atof(strtok(NULL, delimiter));  
//      mySerial.println(period, DEC);
      
      ClockGenerator.setNbSweep(nbStep);
      ClockGenerator.setSweepPeriod(period);
      ClockGenerator.displayAllClocks();
      printSweep();
      return;        
    }

    // **************** Get global sweep params
    strncpy(buff, data, 8);    
    if (strcmp(buff,"sweepget") == 0) {
      printSweep();
      return; 
    }

    // **************** Sweep start 
    strncpy(buff, data, 10);    
    if (strcmp(buff,"sweepstart") == 0) {
      if(ClockGenerator.initSweep()) {
        mySerial.println("Sweep should start");      
      } else {
        mySerial.println("There isn't any clock with sweep activated");              
      }
      return;
    }
    
    // **************** Sweep stop 
    strncpy(buff, data, 9);    
    if (strcmp(buff,"sweepstop") == 0) {
      ClockGenerator.stopSweep();
      mySerial.println("Sweep should stop");        
      return;
    }

    strncpy(buff, data, 4);
    buff[4] = '\0';      
    // **************** Set calibration
    if (strcmp(buff,"scal") == 0) {
      strncpy(buff, data+4, strlen(data)-4); 
      float cal = atof(buff);            
      ClockGenerator.setCalibration(cal); 
      printCalibration();   
      return; 
    }

    // **************** Get calibration
    if (strcmp(buff,"gcal") == 0) {
      printCalibration();   
      return; 
    }
    
    mySerial.println("Unknown command");
    if (mySerial==Serial)  info(); 
  }

}<br>
  • A couple of functions that prints over the Serial com the value of Clock Generator parameters. For example, the printFreq() function :
void printFreq(int clockId) {
  mySerial.print("Clock ");
  mySerial.print(clockId);
  mySerial.print(" frequency: ");
  mySerial.print(ClockGenerator.getFrequency(clockId));
  mySerial.println(" Hz");  
}
  • Miscellaneous functions to format data and the info() function that helps the user by printing the command syntax.

Step 7: Simple Use of the « ClockGen_serial_control.ino » Sketch

You can upload the « ClockGen_serial_control.ino » sketch to the Clock Generator, then open the Serial Monitor and type commands.

For example, if you type « c0 sf 1000000 » and the enter key, then you will set the clock 0 frequency to 1 MHz and the Serial monitor will then print this frequency so you are certain the command was executed properly.

The possible commands are printed in the Serial Monitor when you launch it and if you type something that is not recognized as a command.

You will need to modify a little the sketch in order for it to answer to the Serial Monitor. It must use the Serial (i.e. USBSerial) com instead of Serial1 (that will be used to communicate with a standalone controlling board)

At the beginning of the sketch modify the #define mySerial like that:

//#define mySerial Serial1    // Use Serial1 to communicate with a standalone device through the Rx and Tx pins
#define mySerial Serial       // Use Serial (=SerialUSB) to communicate with the Serial Monitor through the USB connector

Step 8: The « SendCommands.ino » Sketch

This sketch will be uploaded to the controlling board. It will

  • Send commands to the Clock Generator
  • Log the command sent and the resulting state on a 0.49 » OLED mounted on top if it

It is composed of :

  • in the setup() function it sends a couple of commands. For each function it:
    • first, log the command on the OLED
    • then sends the command to the Clock Generator
    • and waits for the Clock Generator to receive a complete line:
  display.clearDisplay();
  display.setCursor(0,0);
  display.println("c0 sf 1000000");
  display.display();

  mySerial.print("c0 sf 1000000\n"); // Need to add the \n that is recognized by the receiving software as end of data

  processed = false;
  while (!processed) {
    while (mySerial.available () > 0) { processIncomingByte (mySerial.read ());}
  }
  delay(pauseDelay);<br>
  • a processIncomingByte() function to buffer the data received. it is quite the same as presented here before in the ClockGen_serial_control.ino sketch. But instead of calling a parser function, when a complete line is received it is printed on the OLED:
void processIncomingByte (const byte inByte)
{
  switch (inByte) {

    case '\n':   // end of text
      input_line [input_pos] = 0;  // terminating null byte

      // terminator reached! process input_line here ...
      delay(pauseDelay);
      display.clearDisplay();
      display.setCursor(0,0);
      display.println(input_line);
      display.display();
      delay(pauseDelay);

      // reset buffer for next time
      input_pos = 0;  
      memset(input_line, 0, sizeof(input_line));     
      processed = true; 
      break;

    case '\r':   // discard carriage return
      break;

    default:
      // keep adding if not full ... allow for terminating null byte
      if (input_pos < (MAX_INPUT - 1))
        input_line [input_pos++] = inByte;
      break;
      
  }  // end of switch

} // end of processIncomingByte  <br>
  • we also have to set up the OLED (in the ClockGen_serial_control.ino we did not set up the TFT because it was done by the ClockGen library begin() function)

For this, we include in the header:

#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

then we instantiate the OLED object

Adafruit_SSD1306 display(OLED_RESET);

And set it up in the setup() function

void setup()   {                
  mySerial.begin(9600);
  
  // by default, we'll generate the high voltage from the 3.3v line internally! (neat!)
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C 
  // init done
  
  // Clear the buffer.
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0,0);

  display.println("Sending");
  display.println("commands");
  display.println("to the");
  display.println("ClockGen");
  display.display();
  delay(pauseDelay);

...

Step 9: Setting Up Our Mini Toolchain Made of a « Controlling Board » and the Clock Generator

Now, since we would like to present a mini toolchain, we will be using a 1284 Narrow board to send the commands to the Clock Generator instead of typing them in the Serial Monitor. And we will log the result on a tiny 0.49 » OLED display mounted on top of it. This will allow us to monitor the state of this mini toolchain without the need of a computer.

Physical set-up

  • Power the 1284 Narrow board with a Vin source, for example a 12V power supply
  • Power the Clock Generator from the same source of power
  • Connect the 1284 Tx or Tx1 pin to the Clock Generator Rx pin
  • Connect the 1284 Rx or Tx1 pin to the Clock Generator Tx pin

You can use Serial (Rx, Tx) or Serial1 (Rx1, Tx1) ports. Simply choose the appropriate define of mySerial in the SendCommands.ino sketch

#define mySerial Serial     // If you want to user the first serial port. In this case disconnect the USB after programming to avoid interference with the USB
//#define mySerial Serial1  // If you want to user the second serial port (on the 1284 Narrow board for example Rx1 = 10, Tx1 = 11). 

Step 10: Running Our Mini Toolchain

Whenever you reset the Controlling board, it will run the sketch that will do this :

  • Print a welcome message "Sending commands to the Clock Generator" on the OLED.
  • Send the command "c0 sf 1000000" to set the frequency of clock 1 @ 1 MHz and print this command on the OLED.
  • Receive "Clock 0 frequency: 1000000 Hz" and print this on the OLED.
  • Send the command "sweepset 100;0.1" to define 100 sweep steps with a sweep period of 0.1 second and print this command on the OLED.
  • Receive "Sweep steps: 100, sweep period: 0.10 s" and print this on the OLED.
  • Send the command "c0 ss 10000000 ;20000000 ;1" to set the start (10 MHz) and stop (20 MHz) frequencies of clock 1 (id=0) and activate the sweep (1=true) and print this command on the OLED.
  • Receive "Clock 0, start: 10000000, stop: 20000000, active: 1" and print this on the OLED.
  • Send the command "sweepstart" to launch the frequency sweep and print this command on the OLED.
  • Receive "Sweep should start" and print this on the OLED.
  • The frequency changes on the Clock Generator TFT screen each 0.1 second and sweeps from 10 MHz to 20 Mhz.

I hope this tutorial gave you a global, as well as a quite in depth, understanding of the working of this mini toolchain. This shows some useful capabilities of the Clock Generator and it would apply also to other Pandauino? boards, like the Arduino compatible Frequency Counter.