Thursday 14 May 2009

PIC to C# data transfer via USB

This has been a bit of a break through.
This post details a C# program and PIC code to send data taken on a PIC analogue input to a C# program and display the data as a time series.

The first changes were made to the PIC code. Please see previous posts for some background to this. Basically I have been using code from here (from www.piccoder.co.uk) . This follows the generic USB code from Microchip but with some additions.
The Piccoder script had been designed to change LEDs or do a summation when a certain data code was sent to the PIC via the USB.

I have added another case to add another function to the PIC. This is set up so that when I send the PIC a code of 0x40 the PIC will read the ADC and send the data back via the USB pipe.

There are two sections of code to add to the Piccoder example:
One is in user.h. Add the line for the READ_ADC as shown here:
enum
{
READ_VERSION = 0x00,
ID_BOARD = 0x31,
UPDATE_LED = 0x32,
READ_ADC = 0x40,
DO_SUM = 0xEE,
RESET = 0xFF
}CMD;


The other is in user.c. Put this as an additional case:
case READ_ADC:
// This case has been added. When 0x40 is sent then the device will do this:
// Want it to read in a value from the ADC and return it via the USB pipe.

ConvertADC(); // Start conversion
while( BusyADC() ); // Wait for completion
ADCint=ReadADC(); // read the ADC


ADC.v[0] = ADRESL; // Save the lower part (8 bits)
ADC.v[1] = ADRESH; // Save the upper part (2 bits)

dataPacket._byte[1]=LSB(ADC);
dataPacket._byte[2]=MSB(ADC);

counter = 0x03; // This is a command saying that data should be sent back

DisplayValue(ADCint); // This displays the ADC value on the LCD screen

break;


The DisplayValue function is an additional function to put the data onto an attached LCD. Dont worry about this at the moment and comment that line out. I might do another post about that sometime soon...

oh yes - also you must initialise the ADC and include the ADC files at the top of that code:
Put this in the include list at the top:
#include '<'adc.h'>' (Do not use the ' quotes - they just make the arrow brackets appear)

And also put this initialise function into your code:
/************Initialise ADC Subroutine********************/
void mInitADC(void)
{
/* Initialise everything for the ADC. MUST include adc.h
/** ADC ***********************************************************/
// Inputs: AN0, AD Clock = Fosc/32, Acq.Time = 6TAD, Right justified, Vref+=VDD, Vref-=VSS
OpenADC( ADC_FOSC_32 & ADC_RIGHT_JUST & ADC_6_TAD, ADC_CH0 & ADC_REF_VDD_VSS, 14 );

return;
}

This function should also be called in the main program initialise routine.
OK so that is the PIC code sorted. This should compile OK. All that has been added is an additional case which, when called with the value 0x40, will return with an ADC value. You should note that the USB can only send bytes so the 10bit ADC value must be converted into 2 bytes, one with thr 8 least significant bits (LSB) and one with the 2 most significant bits (MSB). These two values are sent back as two bytes on the data packet. The first byte (dataPacket._byte[0]) will be the code sent to the PIC.

Next we need to look at the C# code. It turned out it was pretty easy to view the value as a number (just look at the data sent back, just like my previous post) but turning that into a graph became a bit tricky.
C# does not come with any built-in graphing tools. You can produce your own by drawing evey element of the graph but that is a LOT of work and has been done before. So I looked into open source options. One suggested to me (cheers MysteriousLee) was ZedGraph.
ZedGraph is an open source charting class library. Download the source file from the download pages.
You then need to ensure that C# knows about it so you must right click on the background to the 'Toolbox'. Click on 'Choose Items...' and it will bring up a window. Click on 'Browse' and then navigate to where you saved the ZedGraph files. Search around in the there until you find the ZedGraph.dll file (it should be pretty easy to find) and click OK. This will allow you to insert a ZedGraphControl onto your form. Try this out - it brings up a whole chart with axis etc...
You must also make sure that you insert:
using ZedGraph;

at the top of the form so that everything will work.

So now you've got a graph. But how do you make it display anything?
Luckily, like most things in life, someone has done it before.
In this case you are looking for an example of dynamic data. An example using ZedGraph can be found here . This was the one I used.
I'm still playing with all the functions and making it do what I want, but here is the code from my first trial which just shows the ADC data being read in and displayed.
This follows on from my other C# programs so the program.cs file has not changed (see previous posts). So here is the form1.cs code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices; // For DLL import
using ZedGraph; // For graphing stuff - ZedGraph used here....

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
// This function gets the version number of the DLL
[DllImport("mpusbapi.dll")]
private static extern int _MPUSBGetDLLVersion();

// Ths function counts the number of devices on the USB
[DllImport("mpusbapi.dll")]
private static extern int _MPUSBGetDeviceCount(byte[] array);

// This opens a link to the PIC
[DllImport("mpusbapi.dll")]
private static extern IntPtr _MPUSBOpen(int a, byte[] b, byte[] c, int d, int e);
[DllImport("mpusbapi.dll")]
private static extern IntPtr _MPUSBWrite(IntPtr a, byte[] b, int c, ref int d, int e);
[DllImport("mpusbapi.dll")]
private static extern IntPtr _MPUSBRead(IntPtr a, byte[] b, int c, ref int d, int e);
[DllImport("mpusbapi.dll")]
private static extern IntPtr _MPUSBReadInt(IntPtr a, byte[] b, int c, int[] d, int e);
[DllImport("mpusbapi.dll")]
private static extern bool _MPUSBClose(IntPtr a);

//************* These are the global variables ***************************************//
private static IntPtr outPipe; // Pointer for the outpipe
private static IntPtr inPipe; // Pointer for the in pipe
private enum MP_MODE { MP_WRITE, MP_READ }; // List of modes that the pipes can be in
bool AttachedState = false;
int valueADC; // This is to store the calculated ADC real 10bit value
int tickStart = 0; // for the graph

public Form1()
{
InitializeComponent();


//******** Initialise the graph**************
GraphPane myPane = zgc1.GraphPane;
myPane.Title.Text = "Test of Dynamic Data Update with ZedGraph\n" +
"(After 25 seconds the graph scrolls)";
myPane.XAxis.Title.Text = "Time, Seconds";
myPane.YAxis.Title.Text = "Sample Potential, Volts";

// Save 1200 points. At 50 ms sample rate, this is one minute
// The RollingPointPairList is an efficient storage class that always
// keeps a rolling set of point data without needing to shift any data values
RollingPointPairList list1 = new RollingPointPairList(1200);


// Initially, a curve is added with no data points (list is empty)
// Color is blue, and there will be no symbols
LineItem curve1 = myPane.AddCurve("Channel 1", list1, Color.Blue, SymbolType.None);


// Save the beginning time for reference
tickStart = Environment.TickCount;

// Scale the axes
zgc1.AxisChange();

}

private void Form1_Load(object sender, EventArgs e)
{
// This is the default PIC vendor and product id numbers
byte[] vid_pid = Encoding.ASCII.GetBytes("vid_04d8&pid_000c");
// This is the output data stream pipe (End Point 1)
byte[] out_pipe = Encoding.ASCII.GetBytes("\\MCHP_EP1");
// This is the input data stream pipe (End Point 1)
byte[] in_pipe = Encoding.ASCII.GetBytes("\\MCHP_EP1");
ConnectData.Text = "Waiting";
int count = _MPUSBGetDeviceCount(vid_pid); // This checks for a connection
if (count > 0)
{
// This case if there is a connection
MessageBox.Show("Opened Data Pipes - Congratulations...");
ConnectData.Text = "Connected!!!!";
AttachedState = true; // tell the global variable so this can be used to make decisions
// If there is a connection then you want to open the in and out pipes for EP1
// The generic code is _MPUSBOpen( device number, vid_pid, end point, type of connection (0=write(out),1=read(in))
outPipe = _MPUSBOpen(0, vid_pid, out_pipe, (int)MP_MODE.MP_WRITE, 0);
inPipe = _MPUSBOpen(0, vid_pid, in_pipe, (int)MP_MODE.MP_READ, 0);
}
else
{
MessageBox.Show("Failed to open data pipes...please try again");
ConnectData.Text = "No Connection";
AttachedState = false; // tell the global variable so this can be used to make decisions
// Application.Exit(); // This will close the form if required
}
}




private void timer1_Tick_1(object sender, EventArgs e)
{
byte[] pipeOutData = new byte[3]; // This defines the size (4 bytes) of the output data message (pipeOut)
byte[] pipeInData = new byte[3]; // This is the incmming data array (pipeIn).


pipeOutData[0] = 0x40; // Send the command to the PIC whic will return the ADC value

// It must be big enough to contain all the data expected on the pipe.
int ActualLength = 0; // Not sure why you need an actual length????
if (AttachedState == true)
{
_MPUSBWrite(outPipe, pipeOutData, 4, ref ActualLength, 1000); //Send the command now over USB
// The _MPUSBWrite takes the inputs (outpipe pointer, outpipe data to send, length of data, Sent data length, Receive Delay)
_MPUSBRead(inPipe, pipeInData, 4, ref ActualLength, 1000); //Receive the answer from the device firmware through USB
// The _MPUSBRead takes the inputs (inpipe pointer, inpipe data, length of data, Sent data length, Receive Delay)

// Want to have a look at the returned data using the text boxes below:
inBuf0.Text = Convert.ToString(pipeInData[0]); // Show the 1st byte of the pipeInData array
inBuf1.Text = Convert.ToString(pipeInData[1]); // Show the 2nd byte of the pipeInData array
inBuf2.Text = Convert.ToString(pipeInData[2]); // Show the 3rd byte of the pipeInData array

valueADC = pipeInData[1] + (256 * pipeInData[2]); // pipeInData[1] has the 8 LSB and pipeInData[2] has the 2 MSB
ADCvalue.Text = Convert.ToString(valueADC); // Display the actual ADC value

test.Text = Convert.ToString((valueADC * 100 / 1023)); // TEST line - just to check things out
}

//*************This is all for the graph test********************//
// Make sure that the curvelist has at least one curve
if (zgc1.GraphPane.CurveList.Count <= 0)
return;

// Get the first CurveItem in the graph
LineItem curve1 = zgc1.GraphPane.CurveList[0] as LineItem;


// If no curves initialised then stop
if (curve1 == null ) // If no curves initialised then stop
return;

// Get the PointPairList
IPointListEdit list1 = curve1.Points as IPointListEdit;


// Time is measured in seconds
double time = (Environment.TickCount - tickStart) / 1000.0;

// Display the ADC value...
list1.Add(time, (valueADC*500/1024));



// Keep the X scale at a rolling 30 second interval, with one
// major step between the max X value and the end of the axis
Scale xScale = zgc1.GraphPane.XAxis.Scale;
if (time > xScale.Max - xScale.MajorStep)
{
xScale.Max = time + xScale.MajorStep;
xScale.Min = xScale.Max - 30.0;
}

// Make sure the Y axis is rescaled to accommodate actual data
zgc1.AxisChange();

// Force a redraw
zgc1.Invalidate();
}



}
}


Hopefully the comments help you out. Again - please email if you would like the full C# files. This initialised the graph. Data is put into a buffer of 1200 samples long. Everything works on a timer function set to whatever you like (I'm using 10milliseconds). Each time there is a timer click then the program will send 0x40 to the PIC, get a returned ADC value and then add this into the list of displayed data which will then be shown on an updated graph.
It all works pretty well and looks quite nice.
i need to know more about the ZedGraph commands but that is it for this post.

Notes on file names in MPLAB

I was trying to create a new project based upon a previous project, but in a different folder. Easy right?
I thought just copy to another folder and rename. This did not work so well.
Here are some things to consider:
Firstly move the files (don't forget to include everything like the .c and .h files) to your chosen location.
Then open the project.
Choose 'Save Project as' from the 'Project' Menu. (I would not use the 'save workspace as' option as this caused me some hassles).
You then need to make sure that the directories all point to the correct place. Do this by checking in Project - Build Options - Project. Then click on the Directories tab and go through all the directories paths in the drop down menu.
Lastly - the code will not fully compile if the full path name exceeds 62 characters. This was really annoying and not quite sure why this is. The code will build but the correct hex output file will not be created. So keep this in mind when doing all these steps.

Wednesday 13 May 2009

Controlling PIC through C# code

In this post I will try to explain how to send and receive data to a PIC from C# code via the USB interface.
In this example I will be using PIC code that has already been written.
The code along with some good notes is available here (from www.piccoder.co.uk) .
I will be using the PIC code from this example, but with my own C# code.
First thing to do is to check that the example works correctly on the device.
To do this I loaded the code onto the PIC.
I then opened the supplied C# code and tried to run that.
The code compiled and ran but did nothing with the PIC.
This was a bit strange and took a while to sort out. It turns out that there are some diferences between the 2005 and 2008 versions of C# Visual Studio and also with Vista and XP. The code was written using a machine on XP, but I am using Vista (please dont if you have the choice...). The only way I could get the code to work was to take the .exe output file (found in the obj/debug/ folder and entitled "WindowsApplication3.exe"). This can then be run but with XP compatibility mode. This can be done by right clicking on the file and looking at the file properties. Under the 'Compatibility' heading you can choose to run the program in Windows XP (service pack 2) Compatibility mode. Then the program will work correctly.

This was a hassle and the available C# code cannot be easily used/debugged as it will not run under Vista and the newer version of Visual Studio. I want to be able to use the same PIC code and do basically the same thing (switch LEDs on/off via C# program and USB). So lets try that....

A C# program was written that would control the two output LEDs on the PIC. These were on pins RD2 and RD3 and are labelled LED3 and LED4 (check the piccoder article ).
Nothing really clever in this code, just a very simple way of showing how to send data.
Basically when a check box is checked in the C# form then data is sent to the PIC. This data consists of 3 bytes. The first byte is a code to tell the PIC what to do (in this case the code used is 0x32 which means change the LEDs) the next item is the LED number (in this case either 0x03 or 0x04) and the last byte is the satus of the LED (either 0x01 (ON) or 0x00 (OFF)). Easy eh?

The code is here. Please email for the full files as, unless you can recreate the form, then it is hard to use this code in C#. One of these days I will sort out a zip file for all these examples.

Here is the program.cs code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
static class Program
{
///
/// The main entry point for the application.
///

[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}

Next is the form.cs code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices; // For DLL import

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
// This function gets the version number of the DLL
[DllImport("mpusbapi.dll")]
private static extern int _MPUSBGetDLLVersion();

// Ths function counts the number of devices on the USB
[DllImport("mpusbapi.dll")]
private static extern int _MPUSBGetDeviceCount(byte[] array);

// This opens a link to the PIC
[DllImport("mpusbapi.dll")]
private static extern IntPtr _MPUSBOpen(int a, byte[] b, byte[] c, int d, int e);
[DllImport("mpusbapi.dll")]
private static extern IntPtr _MPUSBWrite(IntPtr a, byte[] b, int c, ref int d, int e);
[DllImport("mpusbapi.dll")]
private static extern IntPtr _MPUSBRead(IntPtr a, byte[] b, int c, ref int d, int e);
[DllImport("mpusbapi.dll")]
private static extern IntPtr _MPUSBReadInt(IntPtr a, byte[] b, int c, int[] d, int e);
[DllImport("mpusbapi.dll")]
private static extern bool _MPUSBClose(IntPtr a);

//************* These are the global variables ***************************************//
private static IntPtr outPipe; // Pointer for the outpipe
private static IntPtr inPipe; // Pointer for the in pipe
private enum MP_MODE { MP_WRITE, MP_READ }; // List of modes that the pipes can be in
bool AttachedState = false;

public Form1()
{
InitializeComponent();

}

private void Form1_Load(object sender, EventArgs e)
{
// This is the default PIC vendor and product id numbers
byte[] vid_pid = Encoding.ASCII.GetBytes("vid_04d8&pid_000c");
// This is the output data stream pipe (End Point 1)
byte[] out_pipe = Encoding.ASCII.GetBytes("\\MCHP_EP1");
// This is the input data stream pipe (End Point 1)
byte[] in_pipe = Encoding.ASCII.GetBytes("\\MCHP_EP1");
ConnectData.Text = "Waiting";
int count = _MPUSBGetDeviceCount(vid_pid); // This checks for a connection
if (count > 0)
{
// This case if there is a connection
MessageBox.Show("Opened Data Pipes - Congratulations...");
ConnectData.Text = "Connected!!!!";
AttachedState = true; // tell the global variable so this can be used to make decisions
// If there is a connection then you want to open the in and out pipes for EP1
// The generic code is _MPUSBOpen( device number, vid_pid, end point, type of connection (0=write(out),1=read(in))
outPipe = _MPUSBOpen(0, vid_pid, out_pipe, (int)MP_MODE.MP_WRITE, 0);
inPipe = _MPUSBOpen(0, vid_pid, in_pipe, (int)MP_MODE.MP_READ, 0);
Set_LED(0x03, false); // Initialise the output pins to zero
Set_LED(0x04, false); // Initialise the output pins to zero
}
else
{
MessageBox.Show("Failed to open data pipes...please try again");
ConnectData.Text = "No Connection";
AttachedState = false; // tell the global variable so this can be used to make decisions
// Application.Exit(); // This will close the form if required
}
}


public void Set_LED(uint LED, bool LEDstate)
{
// This is a subroutine which, when called, will update the LED status.
// This is called with two variables: LED = number of LED to change & LEDstate = state to put LED into

// From the PIC code from www.piccoder.co.uk this is required:
// To set the LED's, the host must send the UPDATE_LED
// command which is defined as 0x32, followed by the LED to update,
// then the state to chance the LED to.
//
// i.e. <0x01><0x01>
//
// Would activate LED 1
//
// ENSUR YOU LOOK AT THE CORRECT LED - I was using LEDs 3 and 4.
// The receive buffer size must be equal to or larger than the maximum
// endpoint size it is communicating with. In this case, it is set to 64 bytes.

byte[] pipeOutData = new byte[3]; // This defines the size (4 bytes) of the output data message (pipeOut)

pipeOutData[0] = 0x32;
pipeOutData[1] = (byte)LED;
pipeOutData[2] = (byte)(LEDstate?1:0);

byte[] pipeInData = new byte[3]; // This is the incmming data array (pipeIn).
// It must be big enough to contain all the data expected on the pipe.

int ActualLength = 0; // Not sure why you need an actual length????

if (AttachedState == true)
{
_MPUSBWrite(outPipe, pipeOutData, 4, ref ActualLength, 1000); //Send the command now over USB
// The _MPUSBWrite takes the inputs (outpipe pointer, outpipe data to send, length of data, Sent data length, Receive Delay)
_MPUSBRead(inPipe, pipeInData, 4, ref ActualLength, 1000); //Receive the answer from the device firmware through USB
// The _MPUSBRead takes the inputs (inpipe pointer, inpipe data, length of data, Sent data length, Receive Delay)

// Want to have a look at the returned data using the text boxes below:
outBuf0.Text = Convert.ToString(pipeOutData[0]); // Show the first byte of the pipeOutData array
outBuf1.Text = Convert.ToString(pipeOutData[1]); // Show the 2nd byte of the pipeOutData array
outBuf2.Text = Convert.ToString(pipeOutData[2]); // Show the 3rd byte of the pipeOutData array

inBuf0.Text = Convert.ToString(pipeInData[0]); // Show the 1st byte of the pipeInData array
inBuf1.Text = Convert.ToString(pipeInData[1]); // Show the 2nd byte of the pipeInData array
inBuf2.Text = Convert.ToString(pipeInData[2]); // Show the 3rd byte of the pipeInData array

}
else
{
MessageBox.Show("No connection. Check hardware");
}

}

private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
if (((CheckBox)sender).Checked == true)
{
((CheckBox)sender).Text = "LED 1 ON";
Set_LED(0x03, true);
}
else
{
((CheckBox)sender).Text = "LED 1 OFF";
Set_LED(0x03, false);
}

}

private void LEDcheckBox2_CheckedChanged(object sender, EventArgs e)
{
if (((CheckBox)sender).Checked == true)
{
((CheckBox)sender).Text = "LED 2 ON";
Set_LED(0x04, true);
}
else
{
((CheckBox)sender).Text = "LED 2 OFF";
Set_LED(0x04, false);
}
}


}
}


The next thing is to now change the PIC code that it will send back analogue data at regular intervals and this data will be displayed on a moving graph....