Project

General

Profile

USB Communication

Introduction

Communicating with a host system through USB can seem like a daunting and complex task.
USB in itself is a very large topic, and not an easy one to grasp at every level.

Without going into too much detail, a highly generic approach to the problem is to have your device present itself as a serial port to the host computer.
This allows any type of communication at decent speeds, without ever needing a specific driver (USB Serial Ports are so common that every major OS ships with appropriate drivers out-of-the-box).

If you're using the ioNode, everything you need for USB communication is already included.

Note: the examples below use the eloop library to automatically provide a main loop.

The scom library

The scom library (packaged with the SDK) makes it easy to implement any form of host communication.

Using the scom_term abstraction provided by the library, you can easily exchange text messages between your device and a host computer, without having to care about UART control and configuration.
However, implementing custom protocols is just as easy.

To use the library, add it to the list of dependencies in your application's dfe.conf:

# Firmware Element Configuration
# ...
deps:
  - scom

Baudrate

By default, scom expects serial communication at 19200 baud.

To talk to an scom-based application from Linux, the easiest for many people is to use screen:

screen /dev/ttyUSB0 19200

This default baudrate can be changed by defining the SCOM_BAUDRATE macro at the application level.
To do this, modify your application's dfe.conf:

# Firmware Element Configuration
# ...
deps:
  - scom

compiler_options:
  - -DSCOM_BAUDRATE=9600

Initialization

First, we need to intialize the library.

When a message is received from the host, scom will execute a callback inside of which we can place some application logic.
This callback is the only argument to the initialization method.

Also, when using the scom_term abstraction, the scom_term_update method needs to be called repeatedly from your main loop (this method takes care of clearing the buffer and calling your handler as soon as a message is available).

Finally, scom relies on the AVR's interrupts, which means that at the end of your init method you should call sei (from avr/interrupt.h) to enable interrupts.

#include <avr/interrupt.h>
#include <scom/term.h>

uint8_t msg_handler(uint8_t *x, uint8_t s)
{
    // ...

    return 0;
}

void init()
{
    // Initialize Serial Communication
    scom_term_init(msg_handler);

    // Enable Interrupts
    sei();
}

void loop()
{
    scom_term_update();
}

You might notice that the handler method (msg_handler) actually returns a value.
This value indicates whether the handler has consumed the data itself.
This allows for some more advanced exchange scenarios where the handler might consume the data right away and wait for more.

Unless you call scom_consume (see below for details) inside your handler, it should always return 0.
If your handler needs to consume the input, it should return anything other than 0.

Sending data to the host

To send some data to the host, the following methods are available:

  • Send a printf-style format string

void scom_term_printf(char *fmt, ...)
Arguments:

  • Send a NULL-terminated string

void scom_term_print(char *s)
Arguments:

  • s -> Pointer to NULL-terminated string to be sent

  • Send a buffer

void scom_term_tx(uint8_t *d, uint8_t s)
Arguments:

  • d -> Pointer to data to be sent
  • s -> Number of bytes to send

  • Send a single byte

void scom_term_write(uint8_t d)
Arguments:

  • d -> Byte to send

Going further

It is also possible to skip the scom_term abstraction completely and instead, build your own.

This time, the initialization method requires us to specify a few things:
  • a receive buffer (as well as its size in bytes)
  • a data callback (will be called when a byte is successfully received)
  • [OPTIONAL] an overflow callback (will be called when a byte cannot be received because the buffer is full)
#include <avr/interrupt.h>
#include <scom/scom.h>

#define RXBUF_SIZE 256
uint8_t rxbuf[RXBUF_SIZE];

void data_handler(uint8_t x)
{
    // ...
}

void overflow_handler(uint8_t x)
{
    // ...
}

void init()
{
    scom_init(rxbuf, RXBUF_SIZE, data_handler, overflow_handler);

    sei();
}

Here, the callbacks (data + overflow) will be called directly from within the ISR (Interrupt Service Routine).
This means that these callbacks should be as fast as possible, to avoid blocking the communication.

Data will be automatically placed in the receive buffer until filled.

Data can be read by your application directly from the buffer specified in the scom_init method, up to the number of bytes indicated by the scom_rxbuf_pos variable.
To free up space from the receive buffer (after some data has been read, for example), call the scom_consume method, which will clear the specified size from the start of the buffer:

void scom_consume(uint8_t s)
Arguments:

  • s -> Number of bytes to free

Data can be sent to the host using the scom_write and scom_send methods:

void scom_write(uint8_t d)
Arguments:

  • d -> Byte to send

void scom_send(uint8_t *d, uint8_t s)
Arguments:

  • d -> Pointer to data to be sent
  • s -> Number of bytes to send