0

I have created a program in C# to receive high speed data coming from my Teensy 4.1 uController. The controller is simply measuring voltages at two channels every 1 microsec and sends them over a USB along with a time stamp. The packet length for each reading is 18 Bytes: 4 Bytes (Preamble) + 1 Byte (Packet Length) + 1 Byte (Measurement type) + 12 Bytes (All the info: 4 bytes (time stamp), 4 bytes (voltages1), 4 bytes (voltage2).). Hence I am getting 18 MBps over the USB port. Teensy works at the speeds of a USB 2.0 hence the limit is 480 Mbps / 8 = 60 MBps- so this isnt an issue.

For now, I am only sending data for a short period of time, say 1 second.

In my C# program, I have two methods to display this data: Real-Time processing and passive mode.

  1. In Real-Time processing, I am reading the data in one thread and processing it in another. In this method, I frequently miss data.

  2. In passive mode, I first read data and store it in a memory and after the acquisition time is over, Which is 1 sec in this case, My code stops receiving and starts to process the data. In this method, the frequency of missing data is rather less and sometime no missing data.

I have detached serial port interrupt since then the data missing is much higher.

I have tried double buffering technique but still I am missing data.

I want to fix this issue of missing data. any help would be appreciated.

My code is:

Method to call the threads

// Global variables
int passiveMode = 0;
int serialReadFlag  = 0;
BlockingCollection<byte> fifo_queue = null;
int TEENSYADC = 40;
int PREAMBLE = 85;
int PREAMBLE_COUNT=4;
//
         private void requestResponse()
         {
            serialReadFlag = 1;
            serialPortTeensy.DataReceived -= new SerialDataReceivedEventHandler(this.serialPortTeensy_DataReceived);
            serialPortTeensy.DiscardInBuffer();
            serialPortTeensy.DiscardOutBuffer();
            fifo_queue = new BlockingCollection<byte>();
            serialPortTeensy.WriteLine(RESPONSE + 1); //Request response from Teensy
            passiveMode = 0; // change to 1 to enable passive mode
            if (passiveMode == 0)
            {
                Thread readThread = new Thread(readTSnewDouble);
                readThread.Priority = ThreadPriority.Highest;
                readThread.Start();
                Thread processThread = new Thread(processTSnewDouble);
                processThread.Start();
            }
            else
            {
                Thread readThread = new Thread(readTSnewfast);
                readThread.Priority = ThreadPriority.Highest;
                readThread.Start();
            }
           } 
        private void readTSnew()
        {
            while (serialReadFlag > 0 || serialPortTeensy.BytesToRead > 0) // In Passive mode, a timer interrupt method will change the serialReadFlag to 0
            {
                try
                {
                    
                    int serialbytestoread = serialPortTeensy.BytesToRead;
                    byte[] buffer = new byte[serialbytestoread];
                    serialPortTeensy.Read(buffer, 0, buffer.Length);
                    lock (serialLock)
                    {
                        foreach (var b in buffer)
                            fifo_queue.Add(b);
                    }
                }
                catch
                {

                }
                //Thread.Sleep(1); // If I turn this on, I miss much more data. Wanted to used this to accumulate more data in the serial buffer but then its somehow results in a loss of data.
            }
            
            if (passiveMode == 1) // Passive mode will process after full data is acquired.
            {
                Thread processThread = new Thread(processTSnew);
                processThread.Start();
            }
            
      }

Method to Process the data


            while (serialReadFlag > 0 || fifo_queue.Count > 0)
            {
                try
                {
                    while (fifo_queue.Count > 0)
                    {
                        int incoming_byte = 0;
                        long message1 = 0;
                        long message2 = 0;
                        long message3 = 0;
                        if (preamble_count < PREAMBLE_COUNT)
                        {
                            incoming_byte = fifo_queue.Take();
                        }

                        if (incoming_byte == PREAMBLE && preamble_count < PREAMBLE_COUNT)
                        {
                            preamble_count++;
                        }

                        else if (fifo_queue.Count > 0 && preamble_count == PREAMBLE_COUNT)
                        {
                            int len = fifo_queue.ElementAt(0);
                            if (fifo_queue.Count > len - 1)
                            {
                                len = fifo_queue.Take();
                                int type = fifo_queue.Take();
                                switch (type)
                                {
                                    case TEENSYADC:
                                            if (len == 2)
                                            {
                                                abortReponse();
                                                serialReadFlag = 0;
                                                serialPortTeensy.DataReceived += new SerialDataReceivedEventHandler(this.serialPortTeensy_DataReceived);
                                                break;
                                            }

                                            else
                                            {

                                                message1 = (long)fifo_queue.Take() << 24;
                                                message1 |= (long)fifo_queue.Take() << 16;
                                                message1 |= (long)fifo_queue.Take() << 8;
                                                message1 |= (long)fifo_queue.Take();

                                                message2 = (long)fifo_queue.Take() << 24;
                                                message2 |= (long)fifo_queue.Take() << 16;
                                                message2 |= (long)fifo_queue.Take() << 8;
                                                message2 |= (long)fifo_queue.Take();

                                                message3 = (long)fifo_queue.Take() << 24;
                                                message3 |= (long)fifo_queue.Take() << 16;
                                                message3 |= (long)fifo_queue.Take() << 8;
                                                message3 |= (long)fifo_queue.Take();

                                                servicepResponse(message1, message2, message3);

                                            }
                                        
                                        break;

                                    default:

                                        break;
                                }
                            }
                            preamble_count = 0;
                        }

                        else // not enough data received yet or garbage was received
                        {

                        }
                    }
                }
                catch 
                { }
            }
            fifo_queue.Dispose();

In the images below, you will see that I have missed some data in the real-time mode but not in the passive mode. But keep in mind that the data is sometimes also missed in the passive mode but its less frequent.

plot for the data received in the real-time mode

plot for the data received in the passive mode

I have recently started programming in C# and I dont have much knowledge on performing thread-safe operations.

  • 1
    All three of these lines potentially discard data.: `serialPortTeensy.DiscardInBuffer(); serialPortTeensy.DiscardOutBuffer(); fifo_queue = new BlockingCollection();` – Ben Voigt Feb 23 '21 at 23:02
  • Why not customize the [SerialPort.ReadBufferSize Property](https://learn.microsoft.com/en-us/dotnet/api/system.io.ports.serialport.readbuffersize?view=dotnet-plat-ext-5.0) to a large enough value? – kunif Feb 24 '21 at 01:25
  • 1
    Hi and welcome. I'm wondering if I understand this. Your data rate is too high to be a UART -- I guessing that the USB is acting as a CDC. The PC (?) will be very busy handling data (18 MB/sec), and the processing the data. It seems like you may be getting a data overrun (data coming faster than your code can process). – bobwki Feb 24 '21 at 04:03
  • @Ben, I am using those lines before I start the acquisition thread to make sure the buffer is clear. In my real code, I have a delay of 1000 msec after I discqrd the buffer. – Nabil Khalid Feb 24 '21 at 05:27
  • @bobwki, thanks for your reply. The data speed is 18 Mbits per sec not Bytes. So it shouldnt be a problem since USB supports 480 Mbits per sec. – Nabil Khalid Feb 24 '21 at 05:30
  • @Kunfi, I have already set it to the maximum value of 2147483647 but it doesnt help. – Nabil Khalid Feb 24 '21 at 05:32
  • 1
    *"The data speed is 18 Mbits per sec not Bytes"* -- @bobwki has a valid point. You need to recheck your arithmetic: 18 bytes (not bits) every 1 microsec. Nor does `4` + `1` + `15` equal `18`. *"So it shouldnt be a problem since USB supports 480 Mbits per sec"* -- That's the raw transfer rate. You will not be able to transfer payload at that rate with the overhead of the USB protocol. *"4 Bytes (Preamble)"* -- That seems excessive. How do you validate the message contents? What kind of ADC generates 32-bit values? – sawdust Feb 24 '21 at 07:34
  • @sawdust, my mistake. Its 18 MBps. I have updated my question and applied the corrections. The speed limit is 60 MBps (480Mbps/8) so do you think this could be a problem? I am validating based on the length of the packet. I have 2 ADCs each is 12 bits - using a 16-bit long register. Also, the timestamps are 16 bits. – Nabil Khalid Feb 24 '21 at 15:20
  • 1
    In your question, you are only showing the Windows C# code. Why do you think the issue is on the Windows side and not on the Teensy side? You need sophisticated buffer management and flow control on a microcontroller to achieve 18 MBps over USB. – Codo Feb 24 '21 at 16:16
  • @codo, the reasons I don't think Teensy has a problem are as follows. Firstly, I miss no or less frequently in the passive mode. This is the first thing confirming that teensy isn't missing it. Secondly, if I add a thread to sleep (which I have commented on in my above code) I see more missing points. This means the problem occurs when windows leave the acquisition thread aside then I start to miss data. I have attached two images with my question. They might help understand how the missing looks like. – Nabil Khalid Feb 24 '21 at 18:31
  • I recommend you change the FIFO queue. Instead of pushing and retrieving each single byte, push and retrieve chunks of 1000 bytes or more. In the current setup, you have probably have 20 million memory allocations and 20 million synchronization calls per second plus heavy work for the garbage collector. This might be the limiting factor. – Codo Feb 24 '21 at 18:53
  • There is one more problem with your setup. As USB has flow control, it never loses data. It will slow down the transmission if the consumer cannot keep up with it. I'm pretty sure the USB based serial ports use it as well. So if your Windows software cannot keep up with the stream of data, USB slows down the transmission. If data is lost, it should be because the buffers on the Teensy are full and the Teensy has to discard data. From your description it sounds as if this case isn't properly handled. – Codo Feb 24 '21 at 19:07
  • @Codo, Thanks for your comment. I am using serial.read which reads all the data in the buffer and then pushes all the data into the FIFO queue. So this is already been taken care of. What I am not currently being able to do is that I wait until there is a big chunk of data collected in the buffer and then read it and push into FIFO. To do so, I added thread.sleep so that there is some data accumulated in the buffer after each chunk read. However, wen I enable thread.sleep. I start to miss more data. – Nabil Khalid Feb 24 '21 at 21:37
  • @Codo, Currently the loop is operating like a free flow. It will check number of bytes read all of them and then push into FIFO. This could be 1000 of byte or even 1 byte. I believe if I could somehow achieve this that it waits for a small time period before each each to accumulate more data in the buffer, I might be able to solve the problem. But I am unable to do so. I have tried Thread.sleep, Task.await, Thread.spinwait but I get same problem with all of them. – Nabil Khalid Feb 24 '21 at 21:37
  • @Codo, to further confirm if teensy has a buffer issue or not, what I have tried is that I have introduced thread.sleep between asking teensy to send the data and initializing acquisition thread. I can still read the data even 1 sec after teensy has sent all the data. So the data remains in the USB buffer. I dont know what goes wrong when I introduce this thread.sleep inside the reading thread between each read cycle! I tend to miss data for the duration the thread is asleep. – Nabil Khalid Feb 24 '21 at 21:42
  • *"I have 2 ADCs each is 12 bits - using a 16-bit long register. Also, the timestamps are 16 bits."* -- So more than 33% of the message is filler bytes, dead weight (plus the silly preamble)! You're burdening the transmission with **50% more bytes than necessary**, and then processing these useless filler bytes. When your scheme is at the processing limits, then you need to have an efficient design. – sawdust Feb 24 '21 at 22:31
  • *"To do so, I added thread.sleep so that there is some data accumulated in the buffer after each chunk read."* -- You do not have a realtime system. Your userspace program executes asynchronously with respect to I/O events. Blindly inserting (hard) timing delays (instead of using efficient blocking syscalls) is often counterproductive. – sawdust Feb 24 '21 at 22:42
  • I've been mulling this in the background, and finally realized why it's been bothering me -- but maybe it was obvious to everyone but me so didn't need saying. This isn't using a UART, it's using the CDC (Communication Device Class) mechanisms, which use typically use the "Serial" like drivers. So the bit rate doesn't matter! Makes sense now. – bobwki Feb 25 '21 at 13:47
  • @sawdust, I am sure the size isn't a problem. I have reduced it but still no luck. I have set the serial Read buffer to max (2147483647), which means that my read buffer alone is enough to store (2147483647/18000000)= 119 secs of my data before it will be full. Whereas, my measurements are less than a second but I am still missing data in the real-time mode and also sometimes in the passive mode. – Nabil Khalid Feb 26 '21 at 01:47
  • *"I have reduced it but ..."* -- That's suspiciously vague and dismissive. `//Thread.Sleep(1); // If I turn this on, ...` -- This idea makes no sense; why introduce (at least) a 1 millisec delay when packets are supposedly arriving every microsec? (Yet another arithmetic/numbers flub?) And, since a delay here does cause *"much more"* data loss, then that implies that you've got receiver buffer overrun and apparently no flow control. BTW *"passive mode"* is a meaningless descriptor. It's more like a *deferred* or *batch* mode. – sawdust Feb 26 '21 at 08:32
  • @sawdust, the idea of adding thread.sleep makes perfect sense. The computer should wait until a good amount of data is accumulated in the buffer and then read it into a collection. In the current scenario, the computer is constantly looking for new bytes and that is just a waste of resources. A computer can read and process this data much faster than the controller is sending. In my opinion, even a delay of 10 ms shouldn't hurt given the huge size of the buffer and high processing speed of the computer. Thanks for your suggestion regarding changing the descriptor. – Nabil Khalid Feb 26 '21 at 20:12
  • *"In my opinion, even a delay of 10 ms shouldn't hurt given the huge size of the buffer and high processing speed of the computer."* -- And yet you still have a problem of losing data. Obviously your opinion is flawed. Clearly you don't understand how much copying of data actually occurs. There is no single *"buffer"* that resolves the issue; there are several buffers in the I/O path that can be overrun. – sawdust Feb 27 '21 at 04:18

0 Answers0