USB communication
Introduction
The ioNode's micro-USB port allows flashing it from a computer, but that's not the only use.
The ability to communicate easily with a host computer offers many possibilities including logging, monitoring, control and user interfacing.
The 'scom' library allows very 'raw' access to this, offering many different usage possibilities. On the other hand, the 'scli' library provides a much higher level of abstraction. SCLI is actually a 'USB command-line interface' framework, allowing us to define commands and parse their arguments with ease.
Note: like most tutorials, this makes use of eloop and the substrate system.
How it works
The ioNode features a USB-to-UART chip. One side of the chip is wired to the first UART (UART0) of the atmega1284p microcontroller. The other side of the chip is hooked to a micro-B USB connector.
From the point of view of a host computer, this makes the ioNode appear as just another 'serial port' device (COM ports on windows or /dev/ttyUSB on linux).
This means that "talking through USB" is in fact extremely simple - we just need to use the UART0.
Note: UART communication makes use of AVR interrupts - don't forget to call 'sei()' (to enable interrupts) after your initialization (unless you're using the substrate system, which takes care of calling it for you).
The scom library
The scom library makes it relatively easy to read and write through USB.
Basic usage
Let's see how to use the scom library to read and write through USB.
Initialization
First, we need to initialize scom by calling 'scom_init', which requires a RX buffer and possibly some callbacks:
/* Initialize SCOM * rxbuf -> Pointer to a buffer to be used by scom * rxbuf_size -> Size of the buffer pointed to by 'rxbuf' * rx_callback -> Callback function to be called when a byte is received (x is the received byte) * overflow_callback -> Callback function to be used when a receive overflow occurs (x is the received byte) */ void scom_init(uint8_t *rxbuf, uint8_t rxbuf_size, void (*rx_callback)(uint8_t x), void (*overflow_callback)(uint8_t x))
Writing
We can then send data by calling either 'scom_write' for a single byte:
/* Send a single byte * d -> Byte to send */ void scom_write(uint8_t d)
or 'scom_send' for an array of bytes:
/* Send an array of bytes * d -> Pointer to array of bytes to send * s -> Number of bytes to send */ void scom_send(uint8_t *d, uint8_t s)
Reading
Once interrupts are enabled, our 'rx_callback' will get called everytime a byte is received through the UART:
/* Receive callback * x -> Byte received through UART */ void example_rx_callback(uint8_t x) { // do something with 'x'... }
When a byte is received, it is also stored in the RX buffer that was supplied to 'scom_init' earlier. If that buffer completely fills up, our 'overflow_callback' will also get called for every byte that can't be stored into it (due to being full).
Both of these functions (rx_callback & overflow_callback) will be called directly from the interrupt service routine, which means they must execute in the shortest time possible so as not to block any other interrupts.
When some data from the RX buffer has been processed and can be discarded to free up the buffer, we can call the following:
/* Free up some data from the RX buffer * s -> Number of bytes to free */ void scom_consume(uint8_t s)
Note: don't call this method from within an interrupt routine, it may take longer than expected and block other interrupts. Basically, don't call this from your rx_callback or overflow_callback!
Example
Here is a complete example using scom:
dfe.conf:
name: scom_example type: app mmcu: atmega1284p freq: 10000000 deps: - eloop - scom
scom_example.c:
1 #include <scom/scom.h> 2 3 #include "substrate.h" 4 5 // RX Buffer for SCOM 6 #define RXBUF_SIZE 128 7 uint8_t rxbuf[RXBUF_SIZE]; 8 9 // RX Callback 10 void on_rx(uint8_t x) 11 { 12 // Received a byte, simply echo it back 13 scom_write(x); 14 } 15 16 // Overflow Callback 17 void on_overflow(uint8_t x) 18 { 19 // RX Buffer is full! 20 } 21 22 // Initialization 23 void init() 24 { 25 // Initialize SCOM 26 scom_init(rxbuf, RXBUF_SIZE, on_rx, on_overflow); 27 28 // Initialize Substrate 29 substrate_init(); 30 31 // Send 'hello\n' 32 scom_send((uint8_t *)"hello\n", 6); 33 } 34 35 // Main Loop 36 void loop() 37 { 38 // Update Substrate 39 substrate_loop(); 40 41 // Don't keep any data in the RX Buffer 42 if(scom_rxbuf_pos) { scom_consume(scom_rxbuf_pos); } 43 }
Text-mode Terminal (scom_term)
If you will be exchanging text-based messages, maybe the 'scom_term' abstraction is a better fit for you.
Here, you can send complex text using format strings. Also, you don't need to implement a callback function to handle every character. Instead, scom_term will call back your function for every line of text that is received. Finally, this will be called as part of the main loop, NOT in the interrupt handler, which means much less complexity.
One important difference to note here is the need to periodically update the scom_term system by repeatedly calling 'scom_term_update' from your main loop.
Initialization
Let's start by initializing scom_term by calling 'scom_term_init'. This time, only a single callback is required, which will be called for each line of text received through the UART:
/* Initialize SCOM Terminal * rx_callback -> Callback function to be called when a line of text is received (x is the text, s is the length) */ void scom_term_init(uint8_t (*rx_callback)(uint8_t *x, uint8_t s))
Writing
Sending text is easier with 'scom_term':
/* Send some text * fmt -> Format string */ void scom_term_printf(char *fmt, ...)
Please have a look at format strings to get more details about what can be printed.
Reading
Once interrupts are enabled, our 'rx_callback' will get called everytime a full line of text is received through the UART:
/* Receive callback * x -> Pointer to text * s -> Size of text in bytes * Return value: number of bytes consumed */ uint8_t example_rx_callback(char *x, uint8_t s) { // do something with 'x'... return 0; }
Because this rx_callback will be called from the main loop (and not some interrupt routine), we can call 'scom_consume' directly from it if we need to. However if we do, we must indicate it by returning a non-zero value.
Most cases should return 0 and let 'scom_term' manage the RX buffer itself.
History
The 'scom_term' abstration also includes input history management.
To push a line of text to the history, use the following:
/* Push history */ void scom_term_hist_push(char *s, uint8_t l)
Example
Let's now have a look at a complete example using scom_term:
dfe.conf:
name: scom_example type: app mmcu: atmega1284p freq: 10000000 deps: - eloop - scom
scom_example.c:
1 #include <scom/term.h> 2 3 #include "substrate.h" 4 5 uint8_t ask_name; 6 7 // RX Callback 8 uint8_t on_rx(char *x, uint8_t s) 9 { 10 if(ask_name) 11 { 12 scom_term_printf("hello there, %t! nice to meet you!\n", x, s); 13 ask_name = 0; 14 } 15 16 return 0; 17 } 18 19 // Initialization 20 void init() 21 { 22 // Initialize SCOM Terminal 23 scom_term_init(on_rx); 24 25 // Initialize Substrate 26 substrate_init(); 27 28 // Send 'hello\n' 29 scom_term_printf("hello\n"); 30 scom_term_printf("what is your name? "); 31 ask_name = 1; 32 } 33 34 // Main Loop 35 void loop() 36 { 37 // Update Substrate 38 substrate_loop(); 39 40 // Update SCOM Terminal 41 scom_term_update(); 42 }
The scli library (Serial Command-Line Interface)
Instead of using 'scom' directly (or even the 'scom_term' abstraction), most applications will use the 'scli framework.
Once initialized, scli handles lines of text received over USB, matching them against a set of registered commands. If the command is found, its associated handler function is called to perform any actual work. The handler can parse arguments easily using mechanisms provided by the scli framework.
The scli framework also takes care of displaying a 'hello' message upon boot, as well as displaying a customizable prompt to the user on each line.
Two basic commands are included in scli and automatically provided:
- clear -> Clears the screen by printing a large number of empty lines.
- help -> Displays the list of available commands
How to use
To use scli, we first need to initialize it by calling scli_init. Then we need to update it by calling scli_update repeatedly from our main loop.
We can choose to do this ourselves, but obviously it is recommended to use the provided substrate generator to make things easier.
Obviously, we will need to add 'scli' to our dependencies in dfe.conf before anything:
name: scli_example type: app mmcu: atmega1284p freq: 10000000 deps: - eloop - scli
Without substrate
If you're not using the substrate system, the example below shows how to use scli:
1 #include <scli/scli.h> 2 #include <avr/interrupt.h> 3 4 // Initialization 5 void init() 6 { 7 // Initialize Serial Command Line Interface 8 scli_init(); 9 10 // Enable Interrupts 11 sei(); 12 } 13 14 // Main Loop 15 void loop() 16 { 17 // Update SCLI 18 scli_update(); 19 }
With substrate
If using the substrate system, just add 'uses :scli' to your substrate file.
substrate:
uses :scli
That's it! Just make sure you initialize and update your substrate (the usual stuff).
1 #include "substrate.h" 2 3 // Initialization 4 void init() 5 { 6 // Initialize substrate 7 substrate_init(); 8 } 9 10 // Main Loop 11 void loop() 12 { 13 // Update substrate 14 substrate_update(); 15 }
Not only is this much cleaner and easier, but doing this through substrate has another great advantage: many other libraries provide SCLI commands and will auto-inject them through substrate if scli is 'used'.
Creating commands
To register a new command, we will use the 'scli_def_cmd' function:
/* Define a new command * c -> Pointer to scli_cmd structure * cmd -> Command name * handler -> Callback function for the command */ void scli_def_cmd(struct scli_cmd *c, char *cmd, void (*handler)(char **args, uint16_t *args_len))
When our command is used, scli will call our handler function, passing it two pointers.
These pointers can be used with the generic str_next_arg function (provided by the util library) to extract any arguments that may have been passed to our command:
1 #include <scli/scli.h> 2 #include <util/str.h> 3 4 #include "substrate.h" 5 6 // Structure to hold our example command 7 struct scli_cmd example_cmd; 8 9 // Handler function for our example command 10 void example_cmd_handler(char **args, uint16_t *args_len) 11 { 12 char *arg; 13 uint16_t len; 14 uint16_t i; 15 16 // Run through all arguments 17 i = 0; 18 while(str_next_arg(args, args_len, &arg, &len)) 19 { 20 // Echo back individual arguments 21 scli_printf(" argument %i -> %t\n", i, arg, len); 22 23 // Increase argument counter 24 i = i + 1; 25 } 26 } 27 28 // Initialization 29 void init() 30 { 31 // Initialize substrate 32 substrate_init(); 33 34 // Register our example "echo" command 35 scli_def_cmd(&example_cmd, "echo", example_cmd_handler); 36 } 37 38 // Main Loop 39 void loop() 40 { 41 // Update substrate 42 substrate_loop(); 43 }
If we're using the substrate, commands can be registered in a nicer way (no need to declare a structure to hold the command) directly from there:
register_scli_cmd cmd: 'echo', meth: :example_cmd_handler
This removes the need to call 'scli_def_cmd' with a pre-allocated scli_cmd structure.
Like most things in the substrate, it doesn't matter where you place this 'register_scli_cmd' - it will work the same whether you place it before or after.
Setting a hello message
Having a 'hello' message be displayed after initialization is often a nice addition for an external interface.
We can achieve this simply by calling 'scli_init_m' instead of the classic 'scli_init'. This other version of the initialization function expects a format string hello message that will be displayed just after initialization completes.
4 // Initialization 5 void init() 6 { 7 // Initialize Serial Command Line Interface with hello message 8 scli_init_m("Hello there!\nThis is a neat CLI, have fun :)"); 9 10 // Enable Interrupts 11 sei(); 12 }
Of course, if you're using the substrate system, things are much easier - just add the 'hello' parameter to your substrate:
uses :scli, hello: 'Hello there!\nThis is a neat CLI, have fun :)'
Changing the prompt
By default, scli has an empty ("") prompt. This can be changed at any moment by calling 'scli_set_prompt' (or 'scli_set_prompt_n' if the prompt string is not zero-terminated):
/* Change the scli prompt * prompt -> Pointer to the prompt string */ void scli_set_prompt(char *prompt)
/* Change the scli prompt * prompt -> Pointer to the prompt string * len -> Length of the prompt string */ void scli_set_prompt_n(char *prompt, uint8_t len)
Again, this can also be done through the substrate:
uses :scli, hello: 'Hello there!\nThis is a neat CLI, have fun :)', prompt: ' ~> '
Command whitelisting
The substrate system allows other firmware elements (libraries) to register commands with SCLI (only if SCLI is included in the target, otherwise nothing is done).
However, there may be times where we don't want all of those extra commands. For such cases, the 'cmd_whitelist
' parameter allows restricting which commands will be loaded by passing it a whitelist - an array of commands that should be allowed:
uses :scli, hello: 'Hello there!\nThis is a neat CLI, have fun :)', prompt: ' ~> ', cmd_whitelist: [ :pwm_set, :vfs_mount, :vfs_ls ]
Talking to the device from a linux host
Once our application is ready, we can build it, flash it into an ioNode and start talking to it - but how?
On linux, many solutions exist for actually using this '/dev/ttyUSB'. The one we use here at Dooba most of the time is GNU Screen, which is easy to use and readily available in all major linux distributions (just try 'apt-get install screen' or 'yum install screen' or something like that :D).
By default, the scom stack (on top of which scli stands) is configured to operate at 19200 baud. This means that in order to talk to it, we must run screen as follows (assuming our ioNode is '/dev/ttyUSB0'):
screen /dev/ttyUSB0 19200
To exit, you can use the 'CTRL+a d' keyboard sequence. An interesting list of keyboard sequences for screen can be found here: https://www.gnu.org/software/screen/manual/screen.html#Default-Key-Bindings