This page introduces a minimal firmware that implements a USB Virtual Serial Port for Microchip PIC18F4550 processor.
The code has been optimized to use minimal amout of memory (both Flash and RAM) and tuned to work well with the Free SDCC C-compiler.
The code size is less than 2500 bytes and it requires about 230 bytes of RAM memory and it is capable of transferring almost 1 MB/sec (if only the PIC could generate any data at that speed!).
While USB standard is realy not that bad once you get into grips with the basics, it can be very intimidating. To make matters worse using USB often requires writing device drivers which can be even more intimidating. If you want to support the most common operating systems, Mac OS X, Linux or Windows, the task just gets bigger, easily beyound the resources of a lone developer.
But if you make your device appear as virtual serial port then the drivers will be provided by the courtesy of the operating system ie all the operating systems have built in drivers for serial ports.
Serial ports are easily accessible from various programming languages including C/C++, Python, Java, VisualBASIC etc.
A word of warning about the source code: it was taken from a project that is fully functional but I had to clean it up a bit and remove some personal stuff. It compiles and there is a high probability that it works but I have not had the time to test it after the clean up. If you find any problems, please let me know.
In priciple you should obtain your own PID and VID for your device from the USB consortium (or more easily from Microchip) but for simple testing and development it is ok to use what I have in the code ie Vendor id = VID = 0x0408 and Product id = PID = 0x000A.
To use the library you need to have 'GNU autotools' and 'SDCC' installed and in your 'path'. You can of course compile the code without the autotools as there are just half a dozen files to compile. To program the firmware into the device you need the Microchip PICKit2 programmer and the 'pk2cmd' software that supports it on the 'path'. If you don't have the PICKit2 you realy should get one, at USD 35 it is a steal and it is supported on Linux, Windows and Mac OS X.
And naturally you need a PIC18F4550 device with 4 MHz xtal. It is also possible to use the other PIC18F series devices and crystals but some changes to the link and config options maybe necessary.
cd to the directory where you have the code and type make. This should compile the code and program it to the device if you have the PICKit2 in readiness. The object code and resulting hex file will be placed in a directory obj parallel to the source directory.
In Mac OS X type ls /dev/tty.usb* to get a list of USB virtual serial ports and then use the built in screen terminal to talk to it.
For example:
ls /dev/tty.usb*
/dev/tty.usbmodem5d11
screen /dev/tty.usbmodem5d11
ls /dev/ttyACM* to get a list of USB virtual serial ports and then use the built in screen terminal to talk to it.
For example:
ls /dev/tty.ACM*
/dev/tty.ACM0
screen /dev/ttyACM0
.inf file that will associate your device with the built in driver.
To install the 'driver' plug in the device and go to the Device Manager. Look for an 'Unknown device' and select 'Properties' for that device, then select 'Install Driver', browse to the cdcacm.inf file which is included with the project files.
Note the file has not been tested after clean up so there maybe some errors.
Also note that if you change the PID/VID of the device (and in the long run you should) then you need to update the cdcacm.inf file and re-install it.
After succesfull installation the device should appear as a COM port and you can use a terminal emulator such as PuTTY or TeraTerm to talk to it.
Note that the library code is written from the device point of view, ie 'tx','put' means that the device sends something to the host which maybe confusing because on this is called 'input' direction in USB terminology and the host code would use 'read' functions to receive the data. Just thought I'd mention this.
usbcdc_handler() at least once every millisecond or in response to USB interrupt, which is the preferred way. If do not use interupts and forgot to call 'usbcdc_handler() regularly then your code will hang if it is waiting for a data transfer to complete.
In the example (see main.c) code this is handled as follows:
void high_isr(void) shadowregs interrupt 1 {
if(PIR2bits.USBIF) {
usbcdc_handler();
PIR2bits.USBIF=0;
}
}
'usbcdc_init()' before you enable the interrupts and you might want to wait for the host OS to configure the device. To do all that use:
usbcdc_init();
INTCONbits.PEIE = 1;
INTCONbits.GIE = 1;
while (usbcdc_device_state != CONFIGURED)
/* wait */;
USB standard calls this direction input.
To send something to the device you put the data in to the cdc_tx_buffer[] buffer and call usbcdc_write() with the number of bytes you want to transfer. Before you put data into the buffer you need to make sure that the buffer is not in use by the USB controller. The sequence is something like:
// send six bytes ie 'Hello\n'
while (usbcdc_wr_busy())
/* wait */;
cdc_tx_buffer[0]= 'H';
cdc_tx_buffer[1]= 'e';
cdc_tx_buffer[2]= 'l';
cdc_tx_buffer[3]= 'l';
cdc_tx_buffer[4]= 'o';
cdc_tx_buffer[5]= '\n';
usbcdc_write(6);
}
The cdc_buffer_tx and cdc_buffer_rx buffer lengths are controller by the USBCDC_BUFFER_LENdefine in the usbcdc.h file. At present they are 32 bytes each, increasing them will buy you some (but not much) more through put at the expense of more RAM usage.
USB standard calls this direction output.
To read what the host has send to your device is slightly more complicated. The data arrives in the cdc_rx_buffer[] buffer. Before you read the buffer you need to check with usbcdc_rd_ready() that buffer is not used by the USB controller. You can check the number of bytes in buffer with the macro usbcdc_rd_len. After you have processed all the incoming data (and you should always process it all)you need to tell the library that the buffer is empty by calling usbcdc_read() , so that the library can accept more data to the buffer.
So in your code you poll for the incoming data with something like:
if (usbcdc_rd_ready()) {
for (i=0; i > usbcdc_rd_len(); i++)
process (cdc_rx_buffer[i]);
usbcdc_read();
}
Above makes the most efficient use of the buffers in terms of avoiding unnecessary data copying and function calls.
However, if all you are interested is sending and receiving bytes you can use the usbcdc_putchar() and usbcdc_getchar() functions to send bytes to the host and receive them from the host.(These functions make use of the above mentioned cdc_tx_buffer[] and cdc_rx_buffer[] buffers so you need to be carefull if you mix and match the character based I/O with the normal usbcdc_read()/usbcdc_write methods.)
Note that when using the usbcdc_putchar() nothing is actually transferred to the host until the cdc_tx_buffer[] is full, so if you want data to be sent before the buffer is full, call usbcdc_flush().
In the example code (see main.c) I've implemented standard putchar() and getchar() as follows, which allows the use of standard C functions like 'printf':
void putchar(char c) wparam {
if (c=='\n')
usbcdc_putchar('\r');
usbcdc_putchar(c);
if (c=='\n')
usbcdc_flush();
}
char getchar() {
usbcdc_flush();
return usbcdc_getchar();
}
printft.h/printft.c.
setup_packet which I'm pretty sure could be refactored to overlap the tx/rx buffers, saving 64 bytes of RAM.
br Kusti