FilipDominec / rp2daq

Raspberry Pi Pico firmware for universal hardware control & measurement, along with a user-friendly Python frontend
MIT License
27 stars 4 forks source link

Different response from PICO in windows and ubuntu #7

Closed epsi1on closed 1 year ago

epsi1on commented 1 year ago

Hi, I was trying to diagnose the data problem i've got in the windows. so did try to get infinite blocks by ADC. I simply send a command to PICO, which is :

0x0A,0x04,0x10,0xe8,0x03,0x01,0x01,0x00,0x60,0x00

equivalent to this I think

command

I assume after sending this code to PICO, i'll get a 24 byte of block info. The bitwidth is [3]th byte of header followed by 1500 bytes of sample data. i constantly read 24 byte then 1500 byte over and over. on ubuntu my bitwidth is always 12 but in windows, after block 10 or 11 it will start do get different bitwidth for each block. Note that i execute same code on windows and ubuntu, only need to comment out portname line. I think is because of some transfer problem maybe. do you have any idea what is the problem?

here is the code:

import time
import serial
import struct

## User options
#width, height = 1000, 1000
#channels = [0, 1, 2,] #,  3, 4]     # 0,1,2 are GPIO 26-28;  3 is V_ref and 4 is internal thermometer
#kSPS_per_ch = 100 * len(channels)  # one-second run

import rp2daq
import sys
import tkinter

#rp = rp2daq.Rp2daq()

#port = "/dev/ttyACM0"
port = "COM6"
ser = serial.Serial(port=port, timeout=1)

ser.write(struct.pack(r'<BBB', 1, 0, 1)) 
time.sleep(.15) # 50ms round-trip time is enough

try_port = ser

id_data = ser.read(try_port.in_waiting)[4:] 

print ("connected to " + str(id_data))

dt = bytes([0x0A,0x04,0x10,0xe8,0x03,0x01,0x01,0x00,0x60,0x00])

print(dt)

ser.write(dt) 

time.sleep(.15) 

sizeToRead = 1500

cnt =0;

while True:
    header = ser.read(24)

    print(str(cnt)+ " th block, bitwidth: " + str(header[3]))
    ser.read(sizeToRead)
    cnt = cnt+1

2023-09-01 05_55_40-Windows PowerShell Screenshot from 2023-09-01 09-22-29

FilipDominec commented 1 year ago

Thank you a lot. This is truly interesting; I will prioritize making it work on Windows reliably. BTW I already was testing rp2daq on Windows lately and encountered another issue (#5).

I didn't have to comment out portname line - could you please link what is the problem?

What do the msg headers look like when you get different bitwidth, as you write? The only supported bitwidths in the firmware are 8, 12 or 16 bits and the value should be hard-compiled, so any change in this value now rather indicates corrupted data flow...

epsi1on commented 1 year ago

Here is entire block header (24 byte) in windows and ubuntu, plus 10 first bytes of samples in each block:

header-windows

header-ubuntu

It is weird, but randomly code works right on windows! I was wondering if you get same result as me on your windows machine too? and here is the code

import time
import serial
import struct
from sys import platform
import sys
import tkinter

if platform == "linux" or platform == "linux2":
    port = "/dev/ttyACM0" # for ubuntu
elif platform == "win32":
    port = "COM6" # for windows

ser = serial.Serial(port=port, timeout=1)

ser.write(struct.pack(r'<BBB', 1, 0, 1)) 
time.sleep(.15) # 50ms round-trip time is enough

try_port = ser

id_data = ser.read(try_port.in_waiting)[4:] 

print ("connected to " + str(id_data))

dt = bytes([0x0A,0x04,0x10,0xe8,0x03,0x01,0x01,0x00,0x60,0x00])

ser.write(dt) 

time.sleep(.15) 

sizeToRead = 1500

cnt =0;

while True:
    header = ser.read(24)

    samples = ser.read(sizeToRead)
    print(f'{cnt:04d}'+ " th block, header (24 byte): "+ header.hex() + ", samples (first 10 bytes): "+ samples[:10].hex()  )

    cnt = cnt+1

Thanks again for the cool opensource project

FilipDominec commented 1 year ago

As if the 9th block were not fully read on windows, so its payload data get wrongly interpreted as headers of the 10th and following messages. The firmware is probably the same as I use and it only depends on the OS - it might be buffer overflow problem.

From the last two bytes you transmit (0x60,0x00) I can see you are using fastest rate possible (500 ksps). Would you try changing these two bytes, e.g., to (0xC0,0x03) so that you measure only at 50 ksps?

epsi1on commented 1 year ago

As if the 9th block were not fully read on windows, so its payload data get wrongly interpreted as headers of the 10th and following messages. The firmware is probably the same as I use and it only depends on the OS - it might be buffer overflow problem.

From the last two bytes you transmit (0x60,0x00) I can see you are using fastest rate possible (500 ksps). Would you try changing these two bytes, e.g., to (0xC0,0x03) so that you measure only at 50 ksps?

I can confirm that after replacing last two bytes from 0x60,0x00 to 0xC0,0x03, the problem is fixed on windows. windows-header-2

FilipDominec commented 1 year ago

OK, thanks.

I will devise a new solution to the inter-process communication, replacing the Queue object with a Pipe, and get back to you.

epsi1on commented 1 year ago

thanks, I just found out if there is a small gap in reading data in PC side, this do happen. I think it is something with windows, not with your code. so changing the code will not do any help... Is there any other way to connect to PICO with LibUSB etc. instead of serial port?

FilipDominec commented 1 year ago

There are few questions to be resolved experimentally, and right now I have no time to run such experiments on Windows.

Where does the data corruption come from? It looks like buffer/queue overflow, which wouldn't occur without 1. data rate limits on the receiving side, and simultaneously 2. limited buffer size.

  1. USB hardware as well as related OS subsystems on modern computers (5000 or at least 480 Mbit/s) are much faster than max data rate coming from pico (12 Mbit/s). But some objects in Python are fairly inefficient (while some are fine). Aside of this, a Python process can intermittently stop pulling USB data if any procedure [in any of its sub-threads] is busy, imagine re-plotting a complicated oscilloscope screen. To remedy this, I configured the rp2daq.py module to use a separate process to efficiently receive and enqueue data from USB.

  2. On Linux, this works like a charm; if the computer is super busy, it accumulates some megabytes in the inter-process queue, but when it finally gets to pull them from the queue it never loses a single byte. I have little experience with Windows, though, and the queue there may have a bit different implementation or default parameters: Its limited size, or a stricter timeout are possible way how you lose data. Maybe I should try a multiprocessing.Pipe instead of multiprocessing.Queue.

Fixing any of these two problems (speed / queue size) would probably prevent data losses. Of course, the clean way is to make receiving data both fast & rock solid.

One only learns this by real-life testing! So thank you once again for reporting this early.

epsi1on commented 1 year ago

OK thanks. let me check with CPP and CreateFile() api on windows and see if it works without packet loss. I'll post result here...

epsi1on commented 1 year ago

after some optimization, and separating reading and processing thread, the C# code finally worked. on my PC there is no need for lower level APIs like win32 CreateFile(), with the SerialPort itself it could solve problem. Thanks

cmd

FilipDominec commented 1 year ago

Thanks for noting. The task ahead of me is making it 500 ksps sampling reliably work with Python also on Windows, anyway. So it could be useful if you point out some tricks that you found effective.

epsi1on commented 1 year ago

Thanks for noting. The task ahead of me is making it 500 ksps sampling reliably work with Python also on Windows, anyway. So it could be useful if you point out some tricks that you found effective.

Sure, still working on it. here is the code that creates above result so far:

static void TestDirect()
      {
          var sport = new SerialPort("COM6", 268435456);

          {//https://stackoverflow.com/a/73668856
              sport.Handshake = Handshake.None;
              sport.DtrEnable = true;
              sport.RtsEnable = true;
              sport.StopBits = StopBits.One;
              sport.DataBits = 8;
              sport.Parity = Parity.None;
              sport.ReadBufferSize = 1024 * 10000;
          }

          sport.Open();

          string ver;

          //read device identifier
          {
              var dt = new byte[] { 1, 0, 1 };

              sport.Write(dt, 0, dt.Length);

              Thread.Sleep(100);

              var l = 34;

              if (sport.BytesToRead != l)
                  throw new Exception("Unexpected Resonse Length");

              dt = new byte[34];

              sport.Read(dt, 0, dt.Length);
          }

          {
              var dt = new byte[] { 0x0A, 0x04, 0x10, 0xe8, 0x03, 0x01, 0x01, 0x00, 0x60, 0x00 };

              sport.Write(dt, 0, dt.Length);
          }

          var str = sport.BaseStream;

          var header = new byte[24];
          var samples = new byte[1500];

          while (true)
          {
              //ReadExactAmount(sport, header.Length, header);
              //ReadExactAmount(sport, samples.Length, samples);
              ReadArray(sport.BaseStream, header);
              ReadArray(sport.BaseStream, samples);

              Console.WriteLine("header: " + BitConverter.ToString(header));
          }

      }

public static void ReadArray(this Stream stream, byte[] array)
      {
          var buf = array;

          var counter = 0;

          var l = array.Length;

          while (counter < l)
          {
              var remain = l - counter;

              var rdr = stream.Read(buf, counter, remain);
              counter += rdr;
          }
      }
epsi1on commented 1 year ago

Still am not able to have it in multy thread mode. Do you see any mis configuration in this? for example DTR and RTS are ok for connecting to PICO? or stop bit etc.

{//https://stackoverflow.com/a/73668856
      sport.Handshake = Handshake.None;
      sport.DtrEnable = true;
      sport.RtsEnable = true;
      sport.StopBits = StopBits.One;
      sport.DataBits = 8;
      sport.Parity = Parity.None;
      sport.ReadBufferSize = 1024 * 10000;
}
FilipDominec commented 1 year ago

Your solution indeed does not use multithreading nor multiprocessing. More complicated, real-world programs may need to separate data reception process from other tasks to avoid GUI lags and possible data missing. This depends on what you wish your application to accomplish - if it is super simple, you can be fine with this short code.

Please note that while I appreciate your effort, I have no capacity for developing any C# interface/programs right now. When I make a demo oscilloscope app in Python, I will notice you. Apparently a lot of concepts can be directly transferred from Python to C# code.

Regarding your latter post: USB is commonly treated as a "serial port", but probably none of these serial port parameters are relevant for it. You can change any of them to see they have totally no effect whatsoever. (Perhaps excepting the ReadBufferSize, which I don't know: you would have to set it to e.g. 1 byte to see if it somehow limits data rate.)

epsi1on commented 1 year ago

Finally i've made it working. The only problem i still facing, is the detecting of frequency with FFT. I've done some dummy algorithm to find frequency. the frequency is found without problem, but the phase keeps changing :) have look at the image which is a 25% pwm signal made by arduino with 490Hz frequency.

Animation

also code is updated in the repository so you can see how multi threading is working. I'm not good at FFT and math stuff at all. thanks anyways, i'll close the issue for now

epsi1on commented 1 year ago

Also here is the working code so far which is used in above gif image... https://github.com/epsi1on/SimpleOscilloscope/blob/main/src/POC/SimpleOsciloscope.UI/HardwareInterface/RpiPicoDaqInterface.cs

FilipDominec commented 1 year ago

Very nice.

I can implement edge trigger for the ADC acquisition like common oscilloscopes have. Then you will have a nice waveform that would not jump across your screen. OK?

epsi1on commented 1 year ago

Very nice.

I can implement edge trigger for the ADC acquisition like common oscilloscopes have. Then you will have a nice waveform that would not jump across your screen. OK?

i did the SimpleOsciloscope project as a POC (Proof of Concept). It meant to have limited features. like only 1 channel data, simple yet ugly display (as you can see in the image) and having a buggy frequency detection algorithm. for the edge detection, if you can make the python or C version, then i probably am able to translate it into C#. Here is a raw ADC dump of 500ksps sample for a 25% PWM signal you can simply parse with python. each line a short integer value of sample.

samples.csv

If you are ok, then we could continue this discussion in other repository (only for future reference)? I made an issue there:

https://github.com/epsi1on/SimpleOscilloscope/issues/2