Fast serial communication with Arduino

In this video I explain how to send data from your Arduino (or STM32) to you computer via the serial port at higher speeds. There are two ways of sending data: one is to send data by using the Serial.println() function which sends the data in “human-readable” format. And the other is by using the Serial.write() function which sends binary data to the serial port. The latter is way more faster but special attention is required when we send the data from the microcontroller and receive it on the computer. The data (typically a number) has to be divided into 1-byte (8-bit) parts then after receiving it on the computer it has to be reconstructed into the original number. Also, the serial port has to be polled by the computer and buffers are needed to read, process and write the data.

As an example, while the Serial.printl() function transfers nearly 93000 24-bit numbers to the computer in 5 seconds, the Serial.write() function is capable of transferring about 550000 (nearly 6x more) 24-bit numbers within the same 5 second time period.



Arduino source code

float StartTime = 0; //Timer 
long loopCounter = 0; //keeps track of the number of iterations
uint32_t testNumber = 0; //a 24-bit number as an example 14480912 - 00000000|11011100|11110110|00010000
byte testbuffer[3]; //buffer that stores 3x8=24 bits (3 bytes) 

void setup() 
{
  Serial.begin(2000000);
  delay(1000); //wait 1 s
  //Serial.println("2000000 test");
}

void loop() 
{
  if (Serial.available() > 0) 
  {
    char commandCharacter = Serial.read(); //we use characters (letters) for controlling the switch-case  
    switch (commandCharacter) //based on the command character, we decide what to do
    { 
      case 'b': //binary    
            testNumber = 0; //test number          
            loopCounter = 0; //counts the iterations of the loop
            StartTime = micros(); //starts the timer
            while (loopCounter < 50000000) //5M points  
            {
            //-------------------------------------------------------------------
            testbuffer[0] = testNumber & 255; //first 8 bits - bitwise AND keeps the first 8 digits
            //Serial.print("Buffer 0: ");
            //Serial.println(testbuffer[0]); //print the first part of the output
            //------------------------------------------------
            testbuffer[1] = (testNumber>>8) & 255; //8-15 bits  
            //shifts the 8-15 bits to the first 8 places (>>8) and keeps only those (&255)
            //Serial.print("Buffer 1: ");
            //Serial.println(testbuffer[1]); //print the second part of the output
            //--------------------------------------------------
            testbuffer[2] = (testNumber>>16) & 255; //16-23 bits
            //shifts the 16-23 bits to the first 8 places (>>16) and keeps only those (&255)
            //Serial.print("Buffer 2: ");
            //Serial.println(testbuffer[2]); //print the third part of the output
            //-------------------------------------------
            Serial.write(testbuffer, sizeof(testbuffer)); //dump the buffer to the serial port (24 bit number in 3 bytes)
            loopCounter++; //Increase the value of the counter (+1)
            testNumber = loopCounter; //this here might be inefficient, we could directly work with a single variable
            
              if(micros() - StartTime >= 5000000) //5 s; If the timer ends before 1M points are transfered, we jump out
              {              
                break; //exit the whole thing              
              }        
            }            
            break;
      //-----------------------------------------------------------------------------------------------------------------
      case 'd': //decimal      
          loopCounter = 0; //counts the iterations of the loop
          StartTime = micros(); //starts the timer
          while (loopCounter < 1000000) //1M points  
          {          
          Serial.println(loopCounter); //dump the number to the serial port with a linebreak (one at a time)
          loopCounter++; //Increase the value of the counter  
          
            if(micros() - StartTime >= 5000000) //5 s; If the timer ends before 1M points are transfered, we jump out
            {
              Serial.println("Time is over");
              break; //exit the whole thing              
            }        
          }            
          break;
      }    
  }
}


C# source code

For the C# code below, you need to create the same controls with the names I use, then associate the code with every functions. The purpose of the code is not to provide with a ready made solution (however I do that) but to show you how the things can be implemented. Use the code as an example to implement your own.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.IO.Ports;

namespace SerialSpeedTester
{
    public partial class Form1 : Form
    {
        //Serial port-related variables
        private SerialPort MCU_serialport; //serial port instance 
        private bool isPortOpened = false;

        //Buffer-related variables
        private List<byte[]> incomingData = new List<byte[]>();
        private List<int> fileBuffer = new List<int>(); //buffer for writing into file
        private byte[] serialBuffer; //buffer for capturing the serial port datastream
        private byte[] bufferArray; //main buffer that stores the flattened serialBuffer
        private int totalBufferLength = 0; //length of the total captured AND converted 24-bit numbers
        private int ConvertedNumber; //Final result which is our 24-bit number. This gets stored in the fileBuffer, line by line

        //String-related variables
        private string incomingString = ""; //the total incoming data as a string
        private List<string> incomingStringBuffer = new List<string>();

        string filepath = String.Format(@"{0}\", Application.StartupPath); //puts the file in the same folder as the exe file

        //Timing related variables for managing the buffers
        private DateTime dt1, dt2; //dt1/2 are the variables which serve the purpose as timers for the buffers
        private double elapsedSeconds; //the elapsed time in seconds is stored in this variable

        public Form1()
        {
            InitializeComponent();
        }

        private void comPortComboBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            MCU_serialport = new SerialPort(); //create new instance
            MCU_serialport.BaudRate = 2000000; //might be higher in the future
            MCU_serialport.PortName = comPortComboBox.Text;
            MCU_serialport.Parity = Parity.None;
            MCU_serialport.DataBits = 8;
            MCU_serialport.StopBits = StopBits.One;
            MCU_serialport.NewLine = "\n"; //could be "\r\n" also?

            try
            {
                MCU_serialport.Open(); //try to open the port
                isPortOpened = true; //change the value of the variable

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error!"); //Something is wrong
            }
        }

        private void sendButton_Click(object sender, EventArgs e)
        {
            if (isPortOpened) //if the port is opened
            {
                try
                {
                    MCU_serialport.WriteLine(sendTextBox.Text.ToString()); //sends the content of the send textbox
                    if (binaryInputCheckBox.Checked)
                    {
                        serialCheckTimer.Start(); //start polling the serial port once we sent out the command
                        printTimer.Start(); //start updating the terminal with a certain frequency (timer intervall)
                    }
                    else
                    {
                        serialStringTimer.Start(); //start polling the serial port for reading strings
                    }
                    dt1 = DateTime.Now; //"note down" the time now, so dt2 can be compared to the starting time
                }
                catch
                { 
                    //not implemented
                }
            }
            else
            {
                MessageBox.Show("You haven't opened the serial port!"); //tell the user that he forgot to open the serial port
            }
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (isPortOpened) //If the port is opened
            {
                try
                {
                    MCU_serialport.Close();
                }
                catch
                {
                    //not implemented
                }
            }
            else
            {
                //do nothing as there is not 
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            getAvailablePorts(); //fetch the ports if there are any
        }


        void getAvailablePorts() // get the available ports and list them to the combobox
        {
            String[] ports = SerialPort.GetPortNames(); //fill up the ports array
            comPortComboBox.Items.AddRange(ports);
        }

        private void serialCheckTimer_Tick(object sender, EventArgs e)
        {
            //this timer supposed to create ticks that checks the buffer on the serial port and grabs it if there's something available
            //So, the timer is technically polling the serial port at every tick interval (what is a good time? 10 ms, 100 ms?)

            int bytesToRead = 0; //This variable stores the bytes to read. we can have max 2^32 bytes - 4 gigabytes

            serialCheckTimer.Stop(); //while we process the data, we are not supposed to run the clock while doing the buffering

            try
            {
                bytesToRead = MCU_serialport.BytesToRead; //tries to check the amount of bytes in the serial buffer
            }
            catch 
            {
            //not implemented yet(?)
            }

            if (MCU_serialport.IsOpen) //if the serial port is open (selected in the drop-down list)
            {
                if (bytesToRead != 0) //if there is something waiting on the serial port buffer
                {
                    //Since serialBuffer is always a new instance, it starts from "scratch", it is empty every time the code reaches this point
                    serialBuffer = new byte[bytesToRead]; //creates an array called serialBuffer that stores the bytes to read                   

                    if (MCU_serialport.IsOpen) //maybe this condition can be deleted(?)
                    {
                        MCU_serialport.Read(serialBuffer, 0, bytesToRead); //serialBuffer[bytesToRead] 
                    }

                    //incoming data is just being expanded with every tick reading, it will be copied and emptied somewhere else
                    incomingData.Add(serialBuffer); //now this becomes a matrix: incomigData[serialBuffer[bytesToRead]]

                    totalBufferLength += bytesToRead; //adds the bytes to read to the total buffer length
                    totalBufferSizeLabel.Text = totalBufferLength.ToString(); //this shows how many bytes are read from the serial port

                    //The invoming data is always 24 bits or 3 bytes, so the data shoud be grouped into 3 bytes then converted into decimal
                    //The incoming data is always the serialBuffer variable which holds all the bytes we just recently fetched from the serial buffer

                    /* --temporarily commented, but needed for debugging
                    foreach (int number in serialBuffer)
                    {                        
                        //This part converts the content of the serialBuffer(!, 3 bytes) into something readable
                        receiveTextBox.AppendText(number.ToString() + Environment.NewLine); //Appendtext automatically scrolls to the bottom of the text box   
                    }
                    */

                }
                serialCheckTimer.Start(); //restarts the timer after we finished - ticking can be continued
            }

        }

        private void printTimer_Tick(object sender, EventArgs e)
        {
            //this timer should update the terminal at every tick. The tick should be synced to the speed
            //receiveTextBox.AppendText("Dump Buffer: " + Environment.NewLine);

            bufferArray = new byte[totalBufferLength]; //by instantiating a new array, this buffer is always fresh (empty)

            bufferArray = incomingData.SelectMany(a => a).ToArray(); //flatten the matrix into a vector (array) and pass it to a new array
            incomingData.Clear(); //incomingData is cleared out here after passing its values to the other buffer

            int bufferArrayLength = bufferArray.Length; //length of the converted array (total number of bytes)
            int bufferArrayNumbers = bufferArrayLength / 3; //number of numbers (1 number is 3 byte or 24 bit, so we have to divide the array length by 3)
            buffersizeLabel.Text = bufferArrayNumbers.ToString(); //shows the buffer size (how many numbers are stored in a tick)

            /* --temporarily commented, but needed for debugging
            //receiveTextBox.AppendText("Array Length: " + bufferArrayLength.ToString() + " Numbers: " + bufferArrayNumbers.ToString() + Environment.NewLine);
            foreach (int numbers in bufferArray) //this serves only a test purpose, it prints all the 3 components of the 3-byte number
            {
                receiveTextBox.AppendText(numbers.ToString() + Environment.NewLine); //Appendtext automatically scrolls to the bottom of the text box  
            }
            */

            //we have to recreate the original numbers (012)(345)(678)...
            for (int i = 0; i < bufferArrayNumbers; i++) //this for() iterates through the number of 24-bit numbers
            {
                ConvertedNumber = 0; //converted number which is the incoming 3 bytes put into a 32-bit integer
                
                //MSB - 24 comes in:
                ConvertedNumber = bufferArray[((3 * i) + 2)] << 16; //23-16 bit is shifted to its place
                //receiveTextBox.AppendText("Step 1: " + ConvertedNumber.ToString() + Environment.NewLine);                
                ConvertedNumber |= bufferArray[((3 * i) + 1)] << 8; //15-8 bit is shifted to its place and combined with the previous number using bitwise OR
                //receiveTextBox.AppendText("Step 2: " + ConvertedNumber.ToString() + Environment.NewLine);
                ConvertedNumber |= bufferArray[3 * i]; //7-0 bit is combined with the previous 2 numbers using the bitwise OR
                //receiveTextBox.AppendText("Converted number: " + ConvertedNumber.ToString() + Environment.NewLine);

                //maybe here we can add another array or list and store some data, then dump it into a txt file with another timer ticks (elapsed seconds....etc)
                //Strategy would be the same buffering: collect X number of data, dump into a file, clear, collect, dump (append), clear...repeat...
                fileBuffer.Add(ConvertedNumber); //putting every new numbers in the fileBuffer

                if (enablePrintCheckBox.Checked) //if we want, we can print the final numbers on the terminal - at high speeds, this can make it to freeze up
                {
                    receiveTextBox.AppendText("Converted number: " + ConvertedNumber.ToString() + Environment.NewLine);
                }
            }

            dt2 = DateTime.Now; //check the time now - this (dt2) is compared to dt1 in the next steps
            elapsedSeconds = ((TimeSpan)(dt2 - dt1)).TotalSeconds; //calculate the difference between the start time and the currently checked time

            if (elapsedSeconds > 3) //for now, the buffer holds the data for 3 seconds
            {
                //print to file
                printToFile(); // print (append) to file                

                //reset timer - 3 second counting starts over
                dt1 = DateTime.Now;                
            }
        }

        private void serialStringTimer_Tick(object sender, EventArgs e)
        {
            //This function polls the serial for reading strings (Serial.println() is used on the Arduino)
            int bytesToRead = 0; //This variable stores the bytes to read. we can have max 2^32 bytes - 4 gigabytes

            serialStringTimer.Stop(); //while we process the data, we are not supposed to run the clock while doing the buffering

            try
            {
                bytesToRead = MCU_serialport.BytesToRead; //tries to check the amount of bytes in the serial buffer
            }
            catch
            {
                //not implemented yet(?)
            }

            if (MCU_serialport.IsOpen) //if the serial port is open (selected in the drop-down list)
            {
                if (bytesToRead != 0) //if there is something waiting on the serial port buffer
                {                    
                    incomingString = MCU_serialport.ReadExisting(); //read the existing data from the serial port

                    //receiveTextBox.AppendText("Incoming string: " + incomingString); //print it on the serial port

                    //incoming data is just being expanded with every tick reading, it will be copied and emptied somewhere else
                    incomingStringBuffer.Add(incomingString); //
                    
                }
                serialStringTimer.Start(); //Ticking is restarted after the above task is done
            }

            dt2 = DateTime.Now; //check the time now - this (dt2) is compared to dt1 in the next steps
            elapsedSeconds = ((TimeSpan)(dt2 - dt1)).TotalSeconds; //calculate the difference between the start time and the currently checked time

            if (elapsedSeconds > 3) //for now, the buffer holds the data for 3 seconds
            {
                //print to file
                printToFile(); // print (append) to file                

                //reset timer - 3 second counting starts over
                dt1 = DateTime.Now;
            }
        }

        private void printToFile()
        {
            //When the checkbox is checked, this function dumps the content of the fileBuffer into a txt file
            //If the streamwriter does not see the file, it creates it. If it exists, the next buffer dump will write to the end ("append")

            if (fileSaveCheckBox.Checked == true) //if file saving is enabled
            {
                if (binaryInputCheckBox.Checked) //24-bit binary data is expected - Serial.write(data, lenght)
                {
                    using (System.IO.StreamWriter sw = File.AppendText(filepath + "outputFile.txt"))
                    {
                        foreach (int number in fileBuffer) //dump all the lines into the file line by line
                        {
                            sw.WriteLine(number);
                        }
                    }
                    //clear buffer
                    fileBuffer.Clear(); //free up the buffer 
                                        //clearing the buffer should only happen if we saved the data into an external file
                                        //however if we let the fileBuffer grow, it might lead to problems.
                }
                else //string is expected - Serial.println(data)
                {
                    using (System.IO.StreamWriter sw = File.AppendText(filepath + "outputFile.txt"))
                    {
                        foreach (string text in incomingStringBuffer) //dump all the lines into the file line by line
                        {
                            sw.WriteLine(text);
                        }
                    }
                    //clear buffer
                    incomingStringBuffer.Clear();
                }
            }
        }
    }
}


Windows 10 compatible executable

The below exe file is the same terminal I introduced in the video. The baud rate is “hard coded” to 2000000, so you have to use the same on your microcontroller in the Arduino code. For usage, always refer to the video and see how I use it. The best and fastest way to use it is always to directly print the data into a file. The terminal dumps the data into the output file as long as there is incoming data on the serial port.

Previous
Previous

Salvaging stepper motors from floppy disk drives

Next
Next

MAX31855 - A better thermocouple module