Difference between revisions of "USB communication"

From Dooba Wiki
Jump to navigation Jump to search
(Command whitelist)
 
(One intermediate revision by the same user not shown)
Line 479: Line 479:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Again, this can also be done through the substrate:
+
Again, this can also be done through the [[Discover_Dooba#The_Substrate_System|substrate]]:
  
 
<syntaxhighlight lang="ruby">
 
<syntaxhighlight lang="ruby">
 
uses :scli, hello: 'Hello there!\nThis is a neat CLI, have fun :)', prompt: ' ~> '
 
uses :scli, hello: 'Hello there!\nThis is a neat CLI, have fun :)', prompt: ' ~> '
 +
</syntaxhighlight>
 +
 +
=== Command whitelisting ===
 +
 +
The [[Discover_Dooba#The_Substrate_System|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 ''''<syntaxhighlight lang="ruby" inline>cmd_whitelist</syntaxhighlight>'''' parameter allows restricting which commands will be loaded by passing it a whitelist - an array of commands that should be allowed:
 +
 +
<syntaxhighlight lang="ruby">
 +
uses :scli, hello: 'Hello there!\nThis is a neat CLI, have fun :)', prompt: ' ~> ', cmd_whitelist: [ :pwm_set, :vfs_mount, :vfs_ls ]
 
</syntaxhighlight>
 
</syntaxhighlight>
  

Latest revision as of 13:26, 29 July 2019

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.

Usb uart.png

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.

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_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