Thursday 16 April 2009

Connection from PIC 18F4550 to C# program via USB

OK. The last post covered a C# program to connect via USB to a pre-built interface board. So now I know (almost) how to use .dll files in C#. Next thing is to look at talking to a PIC via USB and C#.
The end result of all this will be to create my own PIC-based hardware that is plug and play via USB, along with software to talk to the PIC-base hardware.

One of my main sources of information was this article on a PIC based weather station (Thanks to Barry B Brey, who wrote the article, and MysteriousLee, who found the article with his super powered searching skills while patiently explaining basic programming stuff while I got stressed).

Firstly I looked at the demo code supplier by Microchip with the MCHPUSB demo program.
To find this you need to look in the 'Microchip Solutions' file, wherever it was put during the installation. On my computer it was installed in the default location of c:\Program Files\.
I wanted a generic demo which I could see what was happening. I chose the "USB Device - MCHPUSB - Generic Driver Demo" as this seemed to be what I wanted (OK in reality I tried a few others but could not figure them out too well...).
I opened the 'USB Device - MCHPUSB - Generic Driver - C18 - PICDEM FSUSB.mcp' PIC code in MPLAB IDE. This was then compiled with no errors.
I then used 'PICDEM FS USB DEMO Tool' to upload the hex code onto the PIC which already had a bootloader (see previous posts).
I opened the C++ example (contained in the "USB Device - MCHPUSB - Generic Driver Demo" folder). I compiled and ran this and it worked - I could read in an analogue value from RA0 of the PIC and this was displayed. Nice. Now I just needed to understand what is going on and redo the code in C#. Easy.
Using a mixture of the article on a PIC based weather station and the C++ example code from Microchip, I eventually got my device going. The code is shown below:

Firstly 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());
}
}
}

Then 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

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_1(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
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 takeReading_Click(object sender, EventArgs e)
{
byte[] pipeOutData = {0x37}; // This is relating to the code on the PIC. If this is received then 10bit ADC value is returned
// This example uses the microchip example "USB Device - MCHPUSB - Generic Driver Demo"
// The PIC code in this example was compiled and programmed onto a PIC18F4550
// An Olimex USB development board was used along with a 0-5V input onto RA0 (analogue input 0)
// The PIC code in the example returns the 10-bit ADC value when a code of HEX:37 (= Decimal:55) is sent.

byte[] pipeInData = new byte[4]; // 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, 1, ref ActualLength, 1000); //Send the command now over USB
// The _MPUSBWrite takes the inputs (outpipe pointer, outpipe data to send, length of data, ??, ??)
_MPUSBRead(inPipe, pipeInData, 3, ref ActualLength, 1000); //Receive the answer from the device firmware through USB
// The _MPUSBRead takes the inputs (inpipe pointer, inpipe data, length of data, ??, ??)

// 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

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
inBuf3.Text = Convert.ToString(pipeInData[3]); // Show the 4th byte of the pipeInData array
}
else
{
MessageBox.Show("No connection. Check hardware");
}
}
}
}

I have tried to include lots of comments on the program but to give a bit of an overview:
The program first needs to define the functions from the .dll. These are at the top of the form1.cs code. Each function must use a DllImport and be defined with the input and output parameters.
When the code is run, the Form1_Load_1 function fires (NOTE: You can get this by double clicking on the background of the form in design mode).
In this function the vid and pid (vendor and product ID numbers) are defined, along with the end points for the in and out pipes (see USB-in-a-nutshell). Only one set of in and out pipes is required in this application.
If there is a connected device with the vid and pid code then the you can say the device is connected (message box to say so) and the in and out pipes can be opened.
Check the vid and pid as I spent a while tyring to get this working but realising I had used a very slightly different pid. This must be EXACTLY correct.
When the button is clicked another function fires which sends data down the out pipe and reads any data back from the in pipe.
The 'USB Device - MCHPUSB - Generic Driver - C18 - PICDEM FSUSB.mcp' code does a few different things but I am only using the analogue read part. The documentation for these examples is bad (in my opinion...) and generally consists of 'read the code'. For whatever reason the program has been designed so that when a value of hex:37 or decimal:55 is sent then the PIC will read the analogue channel and send back the data. This is why the pipeOutData is set to 0x37 (HEX:37). A byte array for the incomming data is also defined but only 4 bytes long as we are only expecting a little bit of data back each time.
If the device is attached, then we send out the 0x37 value and then read whatever comes back to the 'in' pipe. The values of the out and in byte arrays are then shown on the form.
From the data read back I've managed to figure out a few things:
When the 0x37 is sent to the PIC the PIC will respond by sending back that value, then the next byte is part of the reading fom the ADC.
As the ADC is 10 bit, there are 1024 levels. This cannot be transmitted in one byte (1 byte = 8bits = 256 levels), so the next byte gives details of the extra 2bits.
So from the in pipe we get:
pipeIndata[0] = 55 (the code sent to the PIC = hex:37 or decimal:55)
pipeIndata[1] = 0-255 (the 8 least significant bits of the ADC value)
pipeIndata[2] = 0-3 (the 2 most significant bits of the ADC value)

So doing all this, you should be able to send data from the host computer to the PIC and receive data back from the PIC, all via USB. Good eh?
Thats my week challenge over so I think I deserve a pint. Please leave a comment or email if you would like any of the source code or have any ideas or suggestions.

Tuesday 14 April 2009

USB, C# and Velleman K8055 USB Interface Board

The first step to using a PIC with USB connectivity was to find out a bit about USB communications.
The first thing to do was read up about USB protocol.
The 650ish page USB document is something I have not yet read (I have a little bit of a life) but I think there will be times I need to reference it.
The guide 'USB in a nutshell' is a great simple starter document.
So I read that and had a bit of an idea about the USB protocol. I learn better by doing practical things so I set myself a challenge to write a simple program to talk via USB to some inputs/outputs.
I saw the Velleman kit K8055 USB Experiment Interface Board from Maplin Electronics (around £30) and thought that would be useful to have a play around with. This was to learn about the host computer side of USB interfacing with a known and working hardware board. I could then play with the PIC hardware later.
I had decided to write my code in C# using the visual studio 2008 express edition as I had been told of the benefits - ease of use and benefits of both visual basic and C++. I'd not used it before so that will also be a learning curve.

The kit K8055 came with some example code written in Visual Basic and some code written in C++. It also came with a DLL to talk to the board (K8055D.dll).

I have now played around with C# and have managed to figure out how C# code can be written in order to use the functions within the K8055D.dll. This took a while so I'll try and explain it here...

Firstly the K8055D.dll file must be put into the C:\Windows\System32 folder.
(I made the mistake of using the updated .dll file that Velleman supply. This caused an amount of head scratching as the open device functions are slightly different.
I reverted back to the older K8055D.dll file.)

The functions within the .dll are outlined in the document: "MAN_UK_K8055_DLL.pdf" supplied with the kit. This gives all the names of the functions and some descriptions of their function - though it's pretty obvious.

Next came sorting out the C# code. The process involves using the DllImport command which imports the various functions from the .dll.

Two main things to realise:

1: You must put the following with the 'using' commands at the top of the code:
using System.Runtime.InteropServices;

This will allow the DllImport command to work.

2: You then have to import the .dll function.
This is done with the generic code:
[DllImport("DllName.dll")]
static extern type functionName(param1, param2....);

Where the bits in italic are changed depending upon your application.
An actual example from my code is:
[DllImport("K8055D.dll")]
static extern long OpenDevice(long CardAddress);

This Opens the device depending upon the card address. The function can then be called, for example:
DevOpen = OpenDevice(0);

This opens device 0 and returns the card address (which is a long integer).

The code in full is here:

Within my program.cs:

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

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


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

}
}

And within a form:

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace MyProject1
{
public partial class Form1 : Form
{
[DllImport("K8055D.dll")]
static extern long OpenDevice(long CardAddress);
[DllImport("K8055D.dll")]
static extern void CloseDevice();
[DllImport("K8055D.dll")]
static extern int SearchDevices();
[DllImport("K8055D.dll")]
static extern void SetAllDigital();
[DllImport("K8055D.dll")]
static extern void ClearAllDigital();
[DllImport("K8055D.dll")]
static extern void SetDigitalChannel(long SetChannel);

public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
long DevOpen;
DevOpen = OpenDevice(0);
textBox1.Text = Convert.ToString(DevOpen);
button1.Text = "Device Connected";
SetDigitalChannel(1);
}

private void LED1_Click(object sender, EventArgs e)
{

if (LED1.Text == "I am ON")
{
LED1.Text = "I am OFF";
ClearAllDigital();
}
else
{
LED1.Text = "I am ON";
SetAllDigital();

}
}

private void button2_Click(object sender, EventArgs e)
{
CloseDevice();
// this.Close();

}


}

}


I hope that is helpful to people trying to do the same thing.
One thing to note is the with de-bugging turned on, when the form is closed then you will get a "vshost.exe has stopped working" error. This does not happen if you run without de-bugging, or if you run the .exe file for the project. I am unsure why this happens - if anyone has an idea of how to correctly stop this problem, please add a comment or email.

Wednesday 1 April 2009

Starting a PIC project in MPLAB

This post is a work in progress - I'll get back to it later....

Loading a program onto the PIC

Once I had the Olimex USB development board I wanted to upload a simple program, just to prove everything was working OK. I usually start very simple (i.e. just set an output high so that an LED will light) and then build upon that. I have always found small steps to be the best approach.

First thing to do was to download and install Microchip's MPLAB (I used version 8.10). This is a free download. Just follow the instructions to install it - there should not be any complications.

Secondly, I wanted to program in C, hence I needed a C compiler. I decided to use Microchip's C18 Student edition, as this was free. This was downloaded and installed easily.

Lastly, you need to install the USB bootloader program. This is an additional program which is designed to work with a 'bootloader' so that the PIC can be programmed directly through the USB connection (without any fancy programmer).
I'll take a step back....

What is a bootloader?
A bootloader is a small program which is installed onto the PIC using a PIC programmer. The program has been written to enable the rest of the PIC memory to be programmed via the USB connection. This means that no additional programmer is required and the PIC can be programmed directly from a computer. It is a very small piece of code (around 512B). Microchip have written their own bootloader and an application note on the bootloader. Numerous other people have also written their own bootloader code.

The PIC18F4550 on the Olimex board came with the Microchip bootloader already programmed.

In order to program the PIC an additional program is required (the USB bootloader program). This takes the hex file created by the compiler (MPLAB) and talks to the bootloader code in the PIC in order to re-program the PIC.
The USB bootlader program is called MCHPFS_USB and is also free from Microchip but does not come with so much documentation. This has been written to demonstrate their PICDEM FS USB development board.

The PIC needs to be placed into 'program' mode - so that the bootloader can program the chip, rather than letting the usual program code run. This is done by pressing and holding the reset and input buttons. The reset can then be released and then, after a short delay, the input button.

When this happens, windows will look for a driver. This was found on my computer at: c:/Program Files/Microchip Solutions/USB tools/MCHPUSB Custom Driver/MCHPUSB driver/Release/mchpusb.inf
Once found it should not be required again.

So the basic flow of programming is:
1: Write C code
2: Compile with MPLAB (MPLAB can compile and bring together code from various places and compiles it into the HEX file required to write to the PIC)
3: The HEX file could be written to the PIC with a programmer. OR...
4: Use MCHPFS_USB to program the PIC via the bootloader. This requires the PIC to be connected and in 'bootload' mode and requires a correct HEX file.

So that's what I did. I took the basic PIC18F4550 assembler file (this can be found in the MPLAB file: on my machine c:/Program Files/Microchip/MPASM suite/Template/Code/PIC18F4550TEP.ASM ), loaded this into MPLAB. Compiled it.
Then I praogrammed it into my PIC using MCHPFS_USB. There was a warning that I was about to change some parameters but I did not think too much about it.
It did not work.
Worse still, my bootloader also did not work.
Obviously I had done something very wrong and some of you might have guessed what it was. I'll explain in the next post.