Atmega8 USB Terminal

Last week, I recovered an USB atmega8 board from an old project. I decided to make it into a terminal device for other atmega boards. Normally, I use an LCD and some pushbuttons to interact with new circuits. This takes too much time to accomplish some simple things. I wanted to interact with the MCU thru my computer keyboard and screen.

This can be done using some USB to serial converters, but I didn't have one at the time and I had serious driver issues in the past.

Also, this could be achieved by connecting the circuit directly to USB but that also has some drawbacks. First, you're limited to 12Mhz clock speed for USB to work. Also, the USB is very picky about the timing. If you take too much time with the interrupts disabled, you can get issues with the communication. The system may even disconnect the device if it doesn't respond within a certain period of time. Third, if you have bugs in your USB device which makes it continously send bytes or not respond for long periods of time, the Linux kernel can't cope with it. The associated process becomes hung up in an unrecoverable state and the only way to kill it is by removing power from the PC. So, doing development on a board that is directly connected to the USB is a bad idea, unless the primary function of the device you're making includes USB connection.

The Architecture

There are three elements to this system: The host program uses libusb and runs on Linux. It's called termas in the source package. This is a basic terminal application designed to communicate with the controller in a line-based fashion. The associated source module usbtermi can be used to communicate binary data.

The firmware on the USB device is called usbterm. It uses the V-USB system to handle the USB stuff. This firmware contains some I2C slave code as a reusable module. This code is tested on Atmega8, and that's all you need because it's not the end product. You don't need to change it unless you lack the part.

The firmware library for target boards is completely home grown. Although it contains the necessary functions to communicate with the USB device, it still needs some work to make it even easier to use. This is also written for Atmega8, but I should test it on other atmegas to make it portable. I'd like to use some atmega328s for a change!

The Host Program

This is basically a terminal program which communicates with the MCU on a line-by-line basis. Whatever you type goes to the MCU and all data from the MCU is displayed on the screen.

You can also give commands to the terminal program or the USB device thru this program. If a line starts with the character '#', then the next character is a command code. The following commands are recognized:

Quits the program cleanly. This is the only way to do it properly.
Sends a 'test' command the USB device. The USB device should reply immediately with the string "UsbTErm".
Displays the queue pointers on the USB device. I used it for debugging, but left it in place.
Turns on the avail signal.
Turns off the avail signal.
Empties all buffers on the USB device, in case it goes haywire.
Displays all buffered data received from the MCU. Normally the application buffers data until it receives a newline character ('\n'), then the whole line is displayed at once. This command displays the data buffered so far.
In order to use the program as non-root, you need to give your Linux user special privileges. I have the following in my system:
termas> more /etc/udev/rules.d/usbasp.rules 
SUBSYSTEM=="usb", ATTR{idVendor}=="16c0", ATTR{idProduct}=="05dc", OWNER="dodo"
SUBSYSTEM=="usb", ATTR{idVendor}=="6666", ATTR{idProduct}=="1291", OWNER="dodo"
The first line enables the USBASP programmer. The second line allows access to the USBTERM board. The vendor and product IDs are defined in two places in the source code. Once in the host program code and once in the USBTERM firmware.

In the termas/ directory, there is a module called "usbtermi.c". This file contains the definitions for the following functions. You can use them to communicate with the USB board from your own programs.

void usbterm_init(int debuglevel);
   // debuglevel is used for libusb 0 is minimum 3 is maximum

void usbterm_finish();
  // closes the device and finalizes libusb

int usbterm_read(uint8_t *buffer, int len);
  // reads from device-to-pc buffer

int usbterm_writecapacity();
  // returns the number of bytes you can write to the
  // device without overflowing its buffers

int usbterm_write(uint8_t *buffer, int len);
  // writes to the pc-to-device buffer

int usbterm_test(uint8_t *buffer);
  // sends a test command and returns the result in buffer,
  // which must be at least 8 bytes big

void usbterm_stats(int *wsize, int *chead,
                   int *ctail, int *dhead, int *dtail);
void usbterm_enable_avail_signal();
void usbterm_disable_avail_signal();
void usbterm_reset_buffers();
  // these perform the same functions as explained in the
  // termas program commands

void msleep(uint32_t milliseconds);
  // utility function

USBTERM Firmware

This is a pretty straightforward implementation based on V-USB. The device acts as an I2C slave with address 0x6c. The following addresses are used to access the information in the device:
When you read from this address, the device will respond with the amount of data available for reading. i.e. the number of bytes sent by the PC thru the USB channel and present in the buffer currently. The returned value is a single byte.
When you read from this address, the device will respond with the amount of free space in the USB buffer. You can send this much data to the device without overflowing or causing an I2C error. The returned value is again, a single byte.
0x08 (USBTERM_R_BUFFER) - ReadWrite
This is the address for the buffers. Every time you read a byte from this address, that byte is removed from the PC-to-device queue. Every time you write a byte to this address, that byte is stored in the device-to-PC queue. Unlike other I2C devices, the address doesn't increment at each read/write access. You simply read/write to the same address even if you're doing multi-byte operations.
No other addresses are recognized by the device. If given a different address, it will simply abort the transaction by sending a NACK.

The device is also able to signal whether there is some data to be read. It does this by setting its pin PC3 high if there is data in the PC-to-device buffer. This option is by default turned off but can be controlled thru '#A' and '#a' termas commands or the usbtermi functions usbterm_enable_avail_signal() and usbterm_disable_avail_signal().

Target Firmware

There is a library written for target boards. It can be found in lcdser/uste.c. The associated i2c library should also be included. This is pretty much a work in progress. The library takes up around 1k of code space, which is way too much. I believe most of this comes from the i2c code. I had written that when I was just starting out with atmegas. That code contains a lot of waiting loops which would be eliminated if the i2c communication was done using interrupts instead of polling.

The file echo.c in lcdser/ contains a basic echo routine to test out the functionality of the system. It's a good idea to echo sent commands back in general.

The code in echo.c shows the proper way to use the system. When writing to the USB device, always ask beforehand how many bytes you can write. The same applies to reading. I2C has no way of signalling an end of file, I2C masters are supposed to ask for only valid addresses. There is a way of terminating communication in case of a bad address, but that results in garbage data in the I2C buffer, which isn't distinguishable from normal data.

How Well Does It Work

The system works OK if you don't overload the USB device. As long as I use the program as a terminal (give a command, wait a couple of seconds, give another etc.) it seems to work properly with no dropped bytes. However, if I input too many characters within a very short period of time, some bytes get dropped. This is a bug I intend to work on when I'm done with the project I have in mind. For now, it works for my purposes.

Also keep in mind that unlike USB, I2C has no builtin message integrity checks. If a bit gets inverted somewhere, it's not possible to detect it thru hardware. If you intend to use the system as a communication protocol between your PC and the target board, make sure that you put some integrity checks.

Possible Improvements

First of all, there is a bug in the system which causes some bytes to be dropped occasionally. That should be fixed.

It takes quite a bit of code to buffer things and then send them over I2C. I should instead make something which can send a single byte. For example, in the echo code, the data to be output already resides in the output buffer. There is no need to copy it around. I could simply give the i2c code a pointer and then work with that.

Alternatively, I could improve the i2c code to use interrupts and make it call some user defined function to provide more data.


Here is the source code:
The initial version.
The circuit for the USB board uses pin D2 for D+ and pin D4 for D- USB signals. See the V-USB page for more information on the circuit.