The idea for creating a USB sound card based on a PIC came from discussions of other people creating one on the Microchip USB forum. The hardware of the card is based on all Microchip products. The software uses a modified version of the Microchip USB framework and is interrupt driven instead of the traditional polling. The device is a USB composite device as far as the hardware is concerned. The first device is an implementation of the USB Audio 1.0 interface and the other device is a custom interface based on WinUSB. The purpose of the custom interface is for programming the device serial number, upgrading the firmware, and in the future any other configuration that isn’t supported directly by USB Audio 1.0. The sound card runs at a sample rate of 48KHz, 32KHz or 24KHz selectable by the OS with 12 bits per sample. The quality approaches commercial grade as the sample rate is higher then CDs.


The hardware for the sound card is based on the Microchip PIC18F2550 USB processor. The processor is clocked at 48MHz which is the maximum rate for this processor and is also a multiple of the audio sample rate. The microcontroller is connected via the SPI port to the Microchip MCP4822 12bit D/A converter. Volume control is implemented by the Microchip MCP41010 variable resistor acting as a voltage divider to attenuate the signal. These voltage dividers are controlled via SPI by the microcontroller via a dedicated bit-banged SPI port. The purpose of using a separate port is so that volume control can run inside the processor at a different priority level then sample output. The Microchip MCP6022 op-amp is used as a buffer before and after the voltage divider. The final op-amp output is connected to a 330uF capacitor which acts as a high-pass filter to remove the DC component of the signal.
The MCP6022 op-amp has an output drive capacity large enough to power a pair of cheap headphones without much distortion. The resistance of my headphones is approximately 40ohms. The op-amps are not powerful enough to drive speakers which are usually 8 or 4 ohms. The MCP41010 and MCP6022 can output near rail to rail voltage although some nonlinear effects can be heard if the volume is too loud or the signal is near the minimum or maximum. Since USB has a single rail 5V power supply, the sound card only uses a positive rail instead of a traditional duel rail system which is how standard 16bit PCM is encoded. As a result, the signal needs to be biased in software to 2.5V. One side effect that I couldn’t fix was that when no audio is being played, changing the volume control changes the bias offset which can be heard as noise.
The output of the D/A is passed through a RC low-pass filter to remove the staircase output. The cutoff frequency of the low-pass filters is approximately 10.6 KHz which is in the neighborhood of half the 24 KHz sample rate. At the time I built the hardware, the maximum sample rate I could obtain with the firmware was 24 KHz. Since then a friend who knows more about signal processing then me has told me that if the sample rate is high enough the staircase output should no be audible because the frequency components should be higher then the sample rate. Also, headphones or speakers filter out very high frequencies. I have bypassed the filter on one of the units I built and the audio does sound better. If you build this sound card you might want to remove the filter and one of the op-amp stages.
The software for USB Audio Streamer is composed of several components. These are the audio card firmware, the card INF file for the custom interface, a C++ library that wrap WinUSB, and a command line utility for flashing. I am giving out the source code as a learning tool for other people. I don’t care what you do with it but I am not responsible for any damage that might happen from using it. NOTE: Improper use of hardware or software may cause serious injury or death.:)
I use Visual Studio 2005 Professional but I have heard that the project will build with Visual Studio Express. To build the project it is necessary to download the latest version of the WDK to get the headers and library files for WinUSB. The default directories in Visual studio need to be modified to point to the directories of these resources in the WDK. If you use Visual Studio Express it's necessary to recreate all the project files and change the default calling convention to _stdcall.
| Directory | Function |
| inc | General Include Directory |
| miscdevices/audiostream | Sound card firmware(Build first) |
| usbdvcpdriver | INF and WinUSB resources |
| usbdrvclilib | C++ WinUSB wrapper(Build Second) |
| deviceutils/usbdevprogramer | Programmer command line utility(Build last) |
The sound card firmware is based on a modified version of the Microchip USB framework. The framework has been collapsed into a single source file with callbacks between the framework and the main code. The framework( in my case it’s more of a library ) has been modified to use interrupts instead of the traditional polling method. Instead of calling USBDriverService in a loop, this function is called whenever the USB interrupt fires. The library is response for handling control requests on endpoint 0. The framework handles standard USB requests and the body code is responsible for handling class specific USB requests nonstandard requests. The body code is responsible for handling I/O on all other endpoints.
USB Audio Streamer is a composite device of a USB Audio 1.0 device and a custom WinUSB interface. With USB Audio 1.0 all of the sample data is transferred through a separate endpoint then control requests. Control requests such as controlling volume is through endpoint 0(the control endpoint.) WinUSB uses it’s own endpoint. Since each function of the card has a separate endpoint, each of the functions can be implemented at a different priority level without preemption problem. The PIC18F2550 has two interrupt priorities plus the main handler. This gives a total of three priorities. Since playing sound data is the most critical function it was given the highest priority. Next comes control endpoint requests which include the standard requests plus Audio 1.0 requests. Since these functions have relatively low timeouts, these were assigned low priority. WinUSB functions are in the main body code because actions such as querying the firmware version, settings the serial number, or flashing the firmware are not time critical at all.
| Endpoint | Priority | Function |
| EP 2 | Interrupt(High) | Streams the audio data to the D/A converter |
| EP 0 | Interrupt(Low) | Control Endpoint function and Audio 1.0 requests |
| EP 1 | Main(Background) | Non standard functions |
The low priority interrupt handler performs two functions. The first is to check the ISO endpoint through the SIE for data. If data is available the buffer is passed to the high priority interupt code and next buffer that makes up a cicular queue is returned to the SIE. The buffers are in the USB ram of the PIC so that the data is not copied between buffers. In Audio 1.0, the host always sends exactly the number of samples to fit in a frame which is 196, 128 or 96 bytes depending on the sample rate used.
The second function of the low priority interrupt handler is SOF(start of frame) processing. Upon receiving the SOF, the PIC changes the current play buffer to the next available buffer and starts timer 2 which is the sample clock. Timer 2 is always reset on the SOF to keep the sample clock synchronized when clock drift is present. If the SOF occurs before the current buffer is played, playback is changed to the next buffer. If the SOF occurs after the buffer is finished, the last sample is stretch. Since the clock drift is normally small, most of the time samples are not dropped. The time of the last sample is slightly different. This is not noticeable in the audio and is only barely noticeable when playing SIN waves.
The high priority interrupt is driven off timer 2 and has the function of formating the data in the format required by the converter and loading the D/A converter through the SPI port of the PIC. The samples are converted from 16 bits signed PCM to 12 bit unsigned and the endianess is swapped. After the buffer is finished, timer 2 is stopped so that if the clock of the card is ahead of the host, the last sample is stretched.
All Audio 1.0 requests are handled via an extension of the USB framework. The low priority interrupt calls USBDriverService which services the control request. If the request is a standard USB request, the request is handled by the framework. If it isn't, the framework calls back into the main code which examines the request. If the request is recognized, the main code claims the request. If the request is a read request, the main body sets the address of a variable to sent to the host. If it's a write request, a variable is written to but the firmware sets a completion routine to be called at the end of the request.
The card only handles two types of requests which are volume control and sample rate selection. The current value of both of these is stored in a global variable. If the volume is adjusted, new values are loaded into the variable resistors. If the sampling rate is adjusted, a new period is loaded into PR2 for timer 2.
The main body code polls endpoint 1 for custom requests in a loop. If the request is anything other then setting the serial or flashing the firmware, the request is handled in the main body code. Otherwise, the processor is put into a special programming mode. Upon entering this mode, all interrupts are disabled and the USB library is used in the traditional polling mode as program memory blocks are moved from the host. Flashing the microcontroller is reusable code that is used for multiple projects.
The host side consists of a C++ library that wraps WinUSB and a command line C utility for configuring the card. These utilities are used for multiple projects and will be discussed in a separate topic.
MCP41010 Digital Pot Documentation
Eagle and PDF Versions of Schematic and Layout
Source for USB Audio StreamerV1.0