I previously turned my Windows IoT Core-based Raspberry Pi 2 into an oscilloscope, but I didn’t stop there. The next gadget in my target is the Particle Photon. This is an amazing little device. However ,unlike the Raspberry Pi 2, it doesn’t have an HDMI output (it’s only about the size of an HDMI connector). I wasn’t sure how best to display the signal data. I took inspiration from a post from Hackster.io – Sending sound over the Internet. They used the TCP capabilities of the Photon to stream audio data to a client. That’s all I needed to bring out the oscilloscope from the Photon.
Although my previous oscilloscope had an HDMI display, it was pretty much limited to the millisecond range, had temporal consistency issues, and had to have an external ADC IC. The oscilloscope firmware I wrote for the Photon was able to capture samples at 89 microsecond resolution with none of the temporal consistency issues. The Photon also sports eight 12bit ADC converters, so no external circuitry is needed.
Hardware
- Particle Photon
- Any Windows Universal App-Capable Computer
- Any 3.3V signal as an input
Circuit
You can use any 3.3V signal as an input. The software included in this project is expecting a signal that has somewhat of a repeating pattern.
Software
There are two parts to this software package. The first part is the Particle Photon, which can be downloaded here. Get your Photon online by following the guide. Open the Particle Apps site, create a new app, and copy/paste the code. Hit verify to compile.
Open your dashboard in another browser window. In your Particle Apps window, select flash to upload the app to your Photon. After your Photon reboots, on your dashboard you will see an event from your Photon called IP. This is the Photon reporting its local IP address; you’ll need this for the client.
The second part of this software package is the Windows Universal app, which you can download here.
Open the solution in Visual Studio and run it. In the first textbox, enter the IP address that the Photon reported after you flashed it. The second textbox is the port number. This needs to be the same as what is defined in the Photon app. Right now, both apps default to port 8007. Now you can hit the Connect button, and the signal that the Photon is capturing will be displayed. The refresh rate will be 0.5 seconds. It will display how many data points are being received with each refresh and what the trigger index offset is. It also displays the microseconds per sample.
Code
In the Photon app, the initialization is done with the following code. You can see we’re setting up a TCP server and starting its service. If you want to use a different port, you can change that here. This is also where we read the device IP address and publish it to the Particle dashboard.
#define BUFFER_SIZE 500 #define ANALOGPIN A0 #define PORT 8007 #define UNKNOWNSIGNALWIDTH 16000 TCPClient client; TCPServer server = TCPServer(PORT); char myIpAddress[24]; int16_t dataArray[BUFFER_SIZE]; int TSArray[BUFFER_SIZE]; int average = 0; int minv = 4096; int maxv = 0; int sampleDelay = 500; int offsetCrossingIndexs[2] = {0,0}; int offsetCrossings[2] = {0,0}; void setup() { IPAddress myIp = WiFi.localIP(); sprintf(myIpAddress, "%d.%d.%d.%d", myIp[0], myIp[1], myIp[2], myIp[3]); Particle.publish("IP",myIpAddress); delay(1000); server.begin(); }
The first thing we do in the loop function is capture the signal samples, which we’re doing in a for loop. Note that before we enter the signal capture for loop we store the starting timestamp. As we capture each sample, we’re also storing the starting timestamp offset in microseconds for those samples.
void loop() { unsigned long startMSTS = micros(); for(int i = 0; i < arraySize(dataArray); i += 1) { dataArray[i] = analogRead(ANALOGPIN); TSArray[i] = micros() - startMSTS; } //Check to see if the micros() overflowed during the sample. If so, try again if (TSArray[0] > TSArray[arraySize(dataArray)-1]){ return; }
In the next section of the loop function we’re doing our signal processing. These are the same functions that I coded in my Windows IoT Core Oscilloscope project recoded for the Photon.
processStats(); normalizeDataToAverage(); if (!processOffsetCrossings()) { //A clean signal has not been found. Particle.publish("Signal","Unknown"); offsetCrossingIndexs[0] = 0; offsetCrossings[0] = TSArray[0]; bool unknownSignalFound = false; //See if there is at least UNKNOWNSIGNALWIDTH microsecs worth of signal for(int i = 1; i < arraySize(dataArray); i += 1) { if ((TSArray[i] - TSArray[0]) > UNKNOWNSIGNALWIDTH){ offsetCrossingIndexs[1] = i; offsetCrossings[1] = TSArray[i]; unknownSignalFound = true; Particle.publish("Signal",String(TSArray[i])); break; } } if (!unknownSignalFound) { delay(1000); Particle.publish("Signal","Error"); return; } }
In the last section of the loop function we’re checking to see if there is a client that has connected to our TCP server. If there is a connection, we send our signal data.
TCPClient check = server.available(); if (check.connected()) { client = check; Particle.publish("Server","Connected"); } if (client.connected()) { write_socket(client, dataArray); } delay(sampleDelay); }
The write_socket function preps our signal data for writing to our client. We prepend the write buffer with the detected signal trigger offset and the calculated sample rate.
void write_socket(TCPClient socket, int16_t *buffer) { int tcpIdx = 0; uint8_t tcpBuffer[BUFFER_SIZE*2]; //First int is the trigger offset index tcpBuffer[tcpIdx] = offsetCrossingIndexs[0] & 0xff; tcpBuffer[tcpIdx+1] = (offsetCrossingIndexs[0] >> 8); tcpIdx += 2; //Second int is the microseconds per sample int samplerate = (offsetCrossings[1] - offsetCrossings[0]) / (offsetCrossingIndexs[1] - offsetCrossingIndexs[0]); tcpBuffer[tcpIdx] = samplerate & 0xff; tcpBuffer[tcpIdx+1] = (samplerate >> 8); tcpIdx += 2; //The rest of ints is the data array for(int i = 0; i < arraySize(dataArray); i += 1) { tcpBuffer[tcpIdx] = buffer[i] & 0xff; tcpBuffer[tcpIdx+1] = (buffer[i] >> 8); tcpIdx += 2; } socket.flush(); socket.write(tcpBuffer, tcpIdx); //Particle.publish("Written",String(tcpIdx)); }
The signal processing functions are functionally the same as my Windows IoT Core versions.
void processStats() { minv = 4096; maxv = 0; average = 0; float sum = 0; for(int i = 0; i < arraySize(dataArray); i += 1) { sum += dataArray[i]; if (dataArray[i] < minv){ minv = dataArray[i]; } if (dataArray[i] > maxv){ maxv = dataArray[i]; } } average = sum / arraySize(dataArray); minv = minv - average; maxv = maxv - average; } void normalizeDataToAverage() { for(int i = 0; i < arraySize(dataArray); i += 1) { dataArray[i] = dataArray[i] - average; } } bool processOffsetCrossings() { offsetCrossingIndexs[0] = 0; offsetCrossingIndexs[1] = 0; offsetCrossings[0] = 0; offsetCrossings[1] = 0; bool crossSet = false; bool fullCycle = false; int crossingIndex = 0; int triggerValue = maxv * 0.5; for(int i = 0; i < arraySize(dataArray); i += 1) { if (crossingIndex > 1){ fullCycle = true; break; } if (!crossSet && dataArray[i] > triggerValue){ crossSet = true; } if (crossSet && dataArray[i] < triggerValue){ offsetCrossingIndexs[crossingIndex] = i; offsetCrossings[crossingIndex] = TSArray[i]; ++crossingIndex; crossSet = false; } } return fullCycle; }
In the Universal Windows App we connect to the Photon TCP server with a TCP socket object. After we establish a connection we start a timer to enter into a display sample data cycle.
_Connect = new DelegateCommand((x) => { DnsEndPoint hostEntry = new DnsEndPoint(this.IP, this.Port); _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs(); socketEventArg.RemoteEndPoint = hostEntry; socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(async delegate (object s, SocketAsyncEventArgs e) { // Retrieve the result of this request await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { this.Status = e.SocketError.ToString(); RaisePropertyChanged("Connect"); RaisePropertyChanged("Receive"); RaisePropertyChanged("Disconnect"); }); Receive.Execute(null); }); _socket.ConnectAsync(socketEventArg); }, (y) => { return _socket == null || (_socket != null && !_socket.Connected); }); _Receive = new DelegateCommand(async (x) => { await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { this.Status = "Listening"; }); receiveTimer = new Timer(this.Receive_Timer_Tick, null, 0, System.Threading.Timeout.Infinite); }, (y) => { return _socket != null && _socket.Connected; });
The display sample data cycle is controlled by a timer. When data is received the first two Ints are stripped off. First is the trigger offset. The second is the sample rate. The sample rate normally doesn’t change but could if we were to upgrade the system to support selectable sample rates. The rest of the data is copied into an array that can be displayed on our graph. Note how we offset the point index with the trigger index, which keeps the signal lined up on subsequent data refreshes.
private void Receive_Timer_Tick(object state) { receiveTimer.Dispose(); bool go = false; if (_socket != null && _socket.Connected) { if (socketEventArg == null) { socketEventArg = new SocketAsyncEventArgs(); socketEventArg.RemoteEndPoint = _socket.RemoteEndPoint; socketEventArg.SetBuffer(new Byte[MAX_BUFFER_SIZE], 0, MAX_BUFFER_SIZE); socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(async delegate (object s, SocketAsyncEventArgs e) { if (e.SocketError == SocketError.Success && e.BytesTransferred > 0) { //First 2 ints contain the trigger index offset and the sample rate. var triggerIndex = (Int16)((e.Buffer[1] << 8) | e.Buffer[0]); var samplerate = (Int16)((e.Buffer[3] << 8) | e.Buffer[2]); Int16[] data = new Int16[(e.BytesTransferred - 4) / 2]; for (int i = 2; i < e.BytesTransferred / 2; i++) { int bi = i * 2; byte upper = e.Buffer[bi + 1]; byte lower = e.Buffer[bi]; data[i - 2] = (Int16)((upper << 8) | lower); } int index = -triggerIndex; await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { this.Trigger = triggerIndex; this.SampleTime = samplerate; this.Status = string.Format("{0} Points Received", data.Length); this.Points = null; this.Points = data.Select(d => new ScatterDataPoint() { XValue = samplerate * index++, YValue = d, }); }); } else { await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { this.Status = e.BytesTransferred > 0 ? e.SocketError.ToString() : "No data received"; }); } _clientDone.Set(); }); } _clientDone.Reset(); _socket.ReceiveAsync(socketEventArg); go = _clientDone.WaitOne(TIMEOUT_MILLISECONDS); if (!go) { this.Disconnect.Execute(null); } } if (go && _socket != null && _socket.Connected) { receiveTimer = new Timer(this.Receive_Timer_Tick, null, 0, System.Threading.Timeout.Infinite); } }
This project provides just the basics you need to get started with the ability to do some serious signal processing with the Photon. It’s a great device with huge potential.
The post Particle Photon Oscilloscope appeared first on Falafel Software Blog.