All of our projects have some sort of output, why else would we make them? This output can be in the form of LEDs or LCD displays. But what about interacting with a computer? Well, the traditional method of doing so is via the serial port. This is the most widely used because of its simplicity. Rewind a few decades, and you will find many more computers with serial ports. But, if you have noticed, those ports have since been replaced by USB. USB has brought massive advantages to the electronics space, most notably for the end user. It is very easy to use, just plug it in and the computer will handle the rest. Much more convenient than the traditional serial devices which had to be manually configured. The only problem is that USB is very complicated considering that the datasheet is over 600 pages long! For this reason, implementing USB is no easy task. Luckily for you, I will be showing you how to implement USB into your electronics projects.
The first and most logical step is to select a microcontroller that will interface to our computer over USB. And it can’t be any old microcontroller. The infamous mega328 won’t be suitable here. Instead, I’m looking at a very similar microcontroller, which is the ATmega16u2. Why did I pick this microcontroller specifically? Well, for a few reasons. First, it is extremely similar to the original AVR series, such as the mega328. That means that you won’t have to learn too many new skills, just the usb part. Second is that it is fairly avaliable, with it being in stock on Mouser, with more to come later this year. Finally, and the most important, it has a hardware USB implementation. Some of you may already have come up with an objection with regards to the mega328. Which would be the V-USB library. Yes, it is true that you can use such a software library to bit-bang USB. But, for beginners, I believe that it would be best to stick to hardware usb. Who knows, I may just upload a V-USB video in the future as well. If you don’t want to use this microcontroller, you may pick another, make sure that it has USB capability. Just be warned that this video may not align with it completely, although the general ideas will be the same.
You will likely make a prototype for your project as well, and in a lot of cases, that is done on a breadboard or perfboard. In that case, the first thing you may notice about this microcontroller is its size. I mean, it’s smaller than my thumb! What happens next really depends on how comfortable you are with SMD soldering. If you are, then feel free to produce your own dev board or solder it on an adapter board. If you aren’t, then take a look at these adapters. They are pretty large, but no surface mount soldering is required. It also makes it really easy to swap the microcontrollers inside.
Anyways, for the assembly of the prototype, I setup the bare minimum components required to run the microcontroller, along with a few buttons and LEDs for testing purposes. These microcontrollers require very few parts to function, but there is one component that you will need to properly use USB. And that is either an 8MHz or 16MHz crystal. This is because the USB hardware requires a 48MHz clock, and the internal PLL needs either an 8 or 16MHz clock to generate those 48MHz. You will also need 22 ohm resistors in series with the USB datalines. Anyways, the circuit is really simple with few extra parts. Go check the link the in description if you want a schematic that you can follow along with.
The next part is just soldering our prototype together. I didn’t bother to make a PCB for this case since I wanted the get to the software part as quickly as possible. I did end up using the solderless adapter since this will make for a useful drop in programmer down the line if necessary. Feel free to make a PCB at this stage if you want or just place everything on a breadboard.
Now for the part that you have all been waiting for, the USB protocol. This is also the hard part, so buckle in. To write code for USB, you should probably understand how it works at a low level. If you aren’t interested, feel free to skip ahead directly to the implementation. Anways, we will start at the lowest level, which is the bits themselves. Taking a look at the physical USB interface, we have four connections. One is power, another is ground, and D+ and D-. These aren’t your typical data lines, where one is a clock and the other is data. Rather, USB uses what is called a differential pair. It’s called that because the data lines usually have inverted logic states from one another. This is useful for eliminating noise, which can be a big deal at the high speeds it reaches. But how can it use essentially one signal to deliver so much data, where does one bit end and the other begin? Well, let’s take a look.
First off, I need to explain some relevant terminology. There are four basic states, J, K, SE0, and SE1. J and K are opposites of each other. And their polarities depend on whether you are running in low or full speed. They also set D+ and D- opposite eachother, as in D+ would be high and D- would be low for a J in full-speed mode, and the opposite in low-speed. K is always the opposite of J. This is how speed is determined by the computer as well. The J state is the idle state, and whichever polarity it is in will determine the speed of the communications. There are two more levels, which are a SE0 and a SE1. They are what they sound like, either both 1’s or both 0’s. Now, these levels by themselves do not represent the 1’s and 0’s of the data however. For that, we use a special form of data transfer.
It is called the NRZI line coding. It stands for non-return to zero inverted. Basically, we are looking at the transitions between the J’s and K’s to retrieve our data. Let me draw the lines in between the bits here. If there are two J’s or two K’s in a row, no transition, then we will get a 1 for our data. But, if we transition from a J to a K or a K to a J, we will get a 0 for our data. As you can see, the J’s and K’s don’t carry the data, but the transitions do. This is also how the devices can recover some sort of data clock without having a dedicated pin for it. That’s also why the transfers always start with a KJKJKJKK sync pattern so that both sides get an idea of how fast the clock is. But a problem arises if there are too many non-transitions in a row, and the clock could be lost. That’s why the protocol requires some bit stuffing, where a transition is forced after six consecutive non-transitions. This transition does not count as part of the data. And finally, and end to the data is represented with a SE0.
That brings us up the chain into the next highest level of organization. Which is comprised of packets. If you are familiar with internet communication, this is a similar idea. Each packet is a tiny bit of information put together in a package. The first part of every packet is a SYNC. Then comes the PID. The PID basically tells us what the packet is about, whether it is data, addressing, or acknowledgements. The PID is only four bits long, and the other four bits are just the bitwise compliments. Afterwards, we may or may not have some more data depending on the type of packet, but more on that in a second. Finally, at the end of the packet we have an EOP, which is a couple of Js and a SE0. That is the basic structure of a packet.
But with so many different types of data, we need a way to organize these packets. And there are actually three types of packets that you should be aware of. The first is a token packet. These packets are only sent by the host, no devices are allowed to send these. They basically intiate communication. These packets use four distinct PIDs, OUT, IN, SETUP, and SOF. OUT and IN are self-explanatory, they indicate that the next packet will either be a host-to-device or device-to-host data transfer. The SETUP is similar to an OUT, but it has more to do with device configuration. OUT, IN, and SETUP PIDs are all followed by a seven-bit device address and a 4-bit endpoint. That means each bus can have at most 127 devices attached. And endpoints are device specific data locations that indicate different functions for the data being received, but more on endpoints later. The final PID in token packets are SOF. SOFs are not as important, but they basically denote the timing of the USB. They occur every 1ms and no devices respond to it.
The next type of packet is the data packet. Data packets follow after a token packet and are either in the IN or OUT direction. The PID of a data packet can either be DATA0 or DATA1. There is little real difference between the two PIDs other than the device must alternate between them on each consecutive set of data packets. After the PID, you will find the actual data. For full-speed devices, you can have 64 bytes per data packet. But in low-speed devices you are limited to just 8 bytes. After the data, there is a 16 bit CRC to ensure the correctness of the data.
And finally, we have the handshake packet. This is the smallest packet of the three, because it is literally just a PID. The three PIDs are ACK, NACK, and STALL. ACK and NACK are self-explanatory in that the device either receieved the data correctly or was busy. The STALL is a little different because it means there is an outstanding error that needs to be resolved.
I organized the presentation of the previous three packets because I wanted you to see how they are connected. And a combination of these packets in this order will get what is called a transaction. The host initiates all transactions because, remember, the host is the only one who can issue a token packet. They are also named after their token packet PIDs, which are either OUT, IN, or SETUP.
Going further, collections of the transactions are grouped into categories called transfers. And there are four basic types of transfers. Control transfers are used for command and status operations that are intiated by the host. Control transfers can be used for things like getting device information when it is intially plugged in so that the host knows what to do with the new device. Isochrononous transfers are transfers that occur continuously, and with time sensitivity. For example, audio applications where the sound data must be continuously streamed so that the continuity is not broken in the sound. That means if an error occurs, no attempt at retransfer will occur so that the timely continuity is preserved. Interrupt transfers are similar to isochronous transfers in that they occur regularly. The difference is that the data transmitted is small. This means that it can be repeated to ensure full data delivery. This is useful for applications like keyboards. The final transfer type is the bulk transfer. This is for large amounts of data that doesn’t have to be transmitted quickly. Things like hard drives and mass storage devices will use bulk transfers.
Since the host controls all data transfers on the bus, you may be wondering how it differentiates between each transfer method. Well, for that we have to look at device endpoints. An endpoint on a device is sort of like a destination or origin for certain data. A keyboard might use endpoint 1 for the keys pressed for example. Each endpoint has to configure itself in a certain way, such as which transfer type to use and whether data comes in or out of it. Endpoint 0 is the only required endpoint and it must always be configured. Since it is also a control type endpoint, data can go both in and out of it. Besides endpoint 0, full-speed devices can have up to 15 extra endpoints. Low-speed devices can only have 2 additional endpoints.
Speaking of endpoint 0, it is the endpoint that the host uses to initialize the communcation with the device. To setup the device, the host will first give it a new address, since every new device starts with address 0. Once the device has received the new address, further configuration can occur in the form of descriptors. Descriptors are exactly what they sound like, they describe the device and its functions. There’s data for the manufacturer of the device, what it can do, and more.
Ok, that was a lot of information on USB. Let’s get to the fun part and actually implement it. If you wanted to, you could simply use the core USB peripheral and its registers, but I do not recommend that simply for the reason that it can get messy and confusing. That’s why I recommend using some sort of library, either the official Microchip ASF-USB library, or the open-source LUFA library. I will leave a link to both in the description. For this introductory example, we will be creating a USB keyboard, using LUFA. This is the best project to learn USB with since the device logic is simple and you don’t have to write any drivers for your PC.
To get started with it, simply download the files to a directory. I’d recommend copying the keyboard demo so that you have an example to work with. First, edit your makefile. In our case, the MCU should be the atmega16u2. If you decided to use a 16MHz crystal, change the F_CPU value to that. And finally, change the LUFA_PATH to where you have installed lufa. Since we are using the ATmega16u2, we do have to make some modifications to the code, since it includes references to PORTE. So go through remove all references to the Joystick and Buttons headers. Do the same with the LEDs, since they heavily interfere with PORTC. We will instead write code to detect our own buttons and turn on the LEDs. And to do that, head over to the CALLBACK_HID_Device_CreateHIDReport function. Where if statements used to check joystick and button code, you can replace those with your own conditions. I replaced them to check the four buttons on my prototype. Once we are done editing, compile the code by using make. Upload the hex file using avrdude like you normally would. If you are confused still, you can find my modified example in the description.
And already, we can see it working! The best part is that you can extend this however you want! Since this is USB, you can add further functionality like a serial port at the same time as the keyboard. Simply reference the demo projects that are titled with multiple names, like keyboardmouse for example. You can also write custom USB implementations if you would like, but more on that in a future video. There is so much that can be done with this! If you want to dive deeper or need to debug your program, you can get more information on your USB devices. If you are running Linux, dmesg is great to see USB device status. Wireshark can sniff the USB packets and is avaliable on Windows and Mac as well.
Well this video is getting rather long, so I will end it here. I plan on making more USB related videos in the future. Anyways, if you enjoyed this video and learned something new, please consider subscribing so that you can see my other videos. Have a good one!