Harmony UART Command Service with SAMD21

19 min read

Harmony UART Command Service with SAMD21 🎯

Building on our UART Console Service tutorial, we'll now implement the Microchip Harmony Command Service to create an interactive command-line interface (CLI). This service allows you to build a robust system where users can send commands over UART and receive responses, making your embedded system highly interactive and debuggable.

Table of Contents

What is the Harmony Command Service?

The Harmony Command Service is a middleware component that:

  • Parses incoming text commands from the console
  • Executes predefined command handlers
  • Provides structured command registration and help systems
  • Supports command parameters and arguments
  • Integrates seamlessly with the Console Service

This is particularly useful for:

  • System configuration and debugging
  • Runtime parameter adjustment
  • Device testing and validation
  • Interactive demonstrations

Prerequisites

Before starting this tutorial, ensure you have completed the UART Console Service setup as this tutorial builds directly on that foundation.

Setting Up the Command Service

Step 1: Add the Command Service Component

  1. Open your existing Harmony project with the Console Service already configured
  2. In MPLAB Harmony Configurator (MCC), navigate to Middleware > System Services > Command
  3. Add the Command Service to your project
  4. Connect the console to the command

Command Service Configuration

You may have noticed that the command module is included and has associated FreeRTOS configuration. This is because many Harmony services run as RTOS tasks and it's important to be aware of the settings and priorities so they don't negatively impact your application.

More information on how the command module is designed and works is available on the Microchip Harmony GitHub page.

Step 2: Configure Command Service Settings

Leave the configuration with the default settings. If you want to increase the length of commands or total buffer size, you can change those in the settings. Be sure to increase stack size if the buffer sizes are increased or if you accumulate a lot of commands. The help command available by default allows you to print documentation for your commands, and if the documentation gets large you will not be able to print the full help menu.

We will also increase the heap allocated to FreeRTOS to 8192 as our tasks may be larger than the 4096 bytes allocated by default.

RTOS heap settings

Step 3: Generate Code

Click Generate Code to apply the changes and generate the necessary Command Service files.

Implementing Custom Commands

Step 4: Create Command Handlers

Now let's implement some custom commands. Add the following code to your app.c file:

Update the includes section:

#include "app.h"
#include "definitions.h"                // SYS function prototypes
#include <string.h>
#include <stdio.h>

Navigate to the section for callback functions around line 66. Add the following code:

// Command handler prototypes
static void APP_LedCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv);
static void APP_StatusCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv);
static void APP_EchoCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv);

// Command table
static const SYS_CMD_DESCRIPTOR appCmdTbl[] = {
    {"led",     APP_LedCommand,     ": LED control - led <on|off> \n"},
    {"status",  APP_StatusCommand,  ": Show system status\n"},
    {"echo",    APP_EchoCommand,    ": Echo back parameters - echo <text> \n"},
};

// LED Command Handler
static void APP_LedCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv)
{
    
    
    if (argc != 2) {
        SYS_CONSOLE_PRINT("Usage: led <on|off>\r\n");
        return;
    }
    
    if (strcmp(argv[1], "on") == 0) {
        LED0_Clear();  // Assuming you have LED0 configured in MCC
        SYS_CONSOLE_PRINT("LED turned ON\r\n");
    }
    else if (strcmp(argv[1], "off") == 0) {
        LED0_Set();
        SYS_CONSOLE_PRINT("LED turned OFF\r\n");
    }
    else {
        SYS_CONSOLE_PRINT("Invalid parameter. Use 'on' or 'off'\r\n");
    }
}

// Status Command Handler  
static void APP_StatusCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv)
{
    SYS_CONSOLE_PRINT("=== System Status ===\r\n");
    SYS_CONSOLE_PRINT("Device: SAMD21\r\n");
    SYS_CONSOLE_PRINT("Firmware: v1.0.0\r\n");
    SYS_CONSOLE_PRINT("Uptime: %lu seconds\r\n", xTaskGetTickCount() / 1000);
    SYS_CONSOLE_PRINT("Free Heap: %d bytes\r\n", xPortGetFreeHeapSize());
}

// Echo Command Handler
static void APP_EchoCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv)
{
    SYS_CONSOLE_PRINT("Echo: ");
    for (int i = 1; i < argc; i++) {
        SYS_CONSOLE_PRINT("%s ", argv[i]);
    }
    SYS_CONSOLE_PRINT("\r\n");
}

What we are doing here is registering our functions in the appCmdTbl table. Each entry follows the format of commandName, functionToExecute, help message.

We also need to define the function prototypes using the format:

func_name(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv);

And finally implement some functionality that is executed when the function is called. The command service is capable of parsing any arguments you pass in with the command and they are available to use within the function callback by checking argc which has the number of parameters received.

argc is 1 by default which contains the command itself. The arguments can be extracted from pointer to array argv. For example this code checks if the argument after the command was 'on' - all parameters will be strings. When working with numbers make sure to convert them using functions like atoi():

if (strcmp(argv[1], "on") == 0) {
    LED0_Clear();  // Assuming you have LED0 configured in MCC
    SYS_CONSOLE_PRINT("LED turned ON\r\n");
}

Step 5: Register Commands

We then need to register our command table with the command service. Add the following code to the APP_Initialize( void ) function:

void APP_Initialize ( void )
{
    /* Place the App state machine in its initial state. */
    appData.state = APP_STATE_INIT;

    // Register commands
    if (!SYS_CMD_ADDGRP(appCmdTbl, sizeof(appCmdTbl)/sizeof(*appCmdTbl), 
                        "app", ": Application commands")) {
        SYS_CONSOLE_PRINT("Failed to create command group\r\n");
    }


    /* TODO: Initialize your application's state machine and other
     * parameters.
     */
}

Step 6: Update Application State Machine

Modify your application state machine to just do nothing:

void APP_Tasks ( void )
{

    /* Check the application's current state. */
    switch ( appData.state )
    {
        /* Application's initial state. */
        case APP_STATE_INIT:
        {
            bool appInitialized = true;

            SYS_CONSOLE_PRINT("UART Command Service Demo\r\n");
            SYS_CONSOLE_PRINT("Type 'help' for available commands\r\n");
            if (appInitialized)
            {

                appData.state = APP_STATE_SERVICE_TASKS;
            }
            break;
        }

        case APP_STATE_SERVICE_TASKS:
        {

            break;
        }

        /* TODO: implement your application state machine.*/


        /* The default state should never be executed. */
        default:
        {
            /* TODO: Handle error in application's state machine. */
            break;
        }
    }
}

Full code is included at the end.

Testing the Command Interface

Step 7: Build and Test

  1. Build and program your project onto the SAMD21 board
  2. Open a terminal application like the trusty plotSerial
  3. Connect to your device's debug port

You should see the welcome message, and you can now try the following commands:

help
status  
led on
led off
echo Hello World

The help and help app commands will give command-related documentation like a list of command tables registered, and help app will show all the text which we included with each command. This is helpful to use as a reminder once we have more commands.

command help

The status command prints some information about our device and time since the device was powered on. The free heap allows us to track heap usage which we set to 8KB. As we see, the free space is just over 3KB, meaning our initial allocation of 4KB would have failed.

command status

The led on or led off command does exactly that - allows you to control the LED0 status.

command led

Finally, the echo hello world command is an example of how to handle multiple arguments. You can test it by adding additional text like: echo this message is from samd21

command led

Note that by default the command module assumes we are using a terminal like PuTTY and will echo each sent character. plotSerial does the same by default, which results in outgoing text showing twice and is indicated by the ">" character. The response from the device will not have this character.

Implementing Command History

The Command Service supports basic command history with up/down arrow keys when used with compatible terminal applications. The plotSerial application already implements command history.

Conclusion

The Harmony Command Service provides a powerful foundation for building interactive embedded systems. By combining it with the Console Service, you can create sophisticated command-line interfaces that make your devices easy to debug, configure, and demonstrate.

This command interface opens up many possibilities:

  • Remote device configuration
  • Real-time debugging and monitoring
  • Automated testing scripts
  • Interactive demonstrations

In future tutorials, we'll explore more advanced features like persistent configuration storage, file system commands, and integration with other Harmony services.

Stay tuned for more embedded systems tutorials! 🚀

Full Source Code

/*******************************************************************************
  MPLAB Harmony Application Source File

  Company:
    Microchip Technology Inc.

  File Name:
    app.c

  Summary:
    This file contains the source code for the MPLAB Harmony application.

  Description:
    This file contains the source code for the MPLAB Harmony application.  It
    implements the logic of the application's state machine and it may call
    API routines of other MPLAB Harmony modules in the system, such as drivers,
    system services, and middleware.  However, it does not call any of the
    system interfaces (such as the "Initialize" and "Tasks" functions) of any of
    the modules in the system or make any assumptions about when those functions
    are called.  That is the responsibility of the configuration-specific system
    files.
 *******************************************************************************/

// *****************************************************************************
// *****************************************************************************
// Section: Included Files
// *****************************************************************************
// *****************************************************************************

#include "app.h"
#include "definitions.h"                // SYS function prototypes
#include <string.h>
#include <stdio.h>
// *****************************************************************************
// *****************************************************************************
// Section: Global Data Definitions
// *****************************************************************************
// *****************************************************************************

// *****************************************************************************
/* Application Data

  Summary:
    Holds application data

  Description:
    This structure holds the application's data.

  Remarks:
    This structure should be initialized by the APP_Initialize function.

    Application strings and buffers are be defined outside this structure.
*/

APP_DATA appData;

// *****************************************************************************
// *****************************************************************************
// Section: Application Callback Functions
// *****************************************************************************
// *****************************************************************************

/* TODO:  Add any necessary callback functions.
*/

// Command handler prototypes
static void APP_LedCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv);
static void APP_StatusCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv);
static void APP_EchoCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv);

// Command table
static const SYS_CMD_DESCRIPTOR appCmdTbl[] = {
    {"led",     APP_LedCommand,     ": LED control - led <on|off> \n"},
    {"status",  APP_StatusCommand,  ": Show system status\n"},
    {"echo",    APP_EchoCommand,    ": Echo back parameters - echo <text> \n"},
};

// LED Command Handler
static void APP_LedCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv)
{
    
    
    if (argc != 2) {
        SYS_CONSOLE_PRINT("Usage: led <on|off>\r\n");
        return;
    }
    
    if (strcmp(argv[1], "on") == 0) {
        LED0_Clear();  // Assuming you have LED0 configured in MCC
        SYS_CONSOLE_PRINT("LED turned ON\r\n");
    }
    else if (strcmp(argv[1], "off") == 0) {
        LED0_Set();
        SYS_CONSOLE_PRINT("LED turned OFF\r\n");
    }
    else {
        SYS_CONSOLE_PRINT("Invalid parameter. Use 'on' or 'off'\r\n");
    }
}

// Status Command Handler  
static void APP_StatusCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv)
{
    SYS_CONSOLE_PRINT("=== System Status ===\r\n");
    SYS_CONSOLE_PRINT("Device: SAMD21\r\n");
    SYS_CONSOLE_PRINT("Firmware: v1.0.0\r\n");
    SYS_CONSOLE_PRINT("Uptime: %lu seconds\r\n", xTaskGetTickCount() / 1000);
    SYS_CONSOLE_PRINT("Free Heap: %d bytes\r\n", xPortGetFreeHeapSize());
}

// Echo Command Handler
static void APP_EchoCommand(SYS_CMD_DEVICE_NODE* pCmdIO, int argc, char** argv)
{
    SYS_CONSOLE_PRINT("Echo: ");
    for (int i = 1; i < argc; i++) {
        SYS_CONSOLE_PRINT("%s ", argv[i]);
    }
    SYS_CONSOLE_PRINT("\r\n");
}

// *****************************************************************************
// *****************************************************************************
// Section: Application Local Functions
// *****************************************************************************
// *****************************************************************************


/* TODO:  Add any necessary local functions.
*/


// *****************************************************************************
// *****************************************************************************
// Section: Application Initialization and State Machine Functions
// *****************************************************************************
// *****************************************************************************

/*******************************************************************************
  Function:
    void APP_Initialize ( void )

  Remarks:
    See prototype in app.h.
 */

void APP_Initialize ( void )
{
    /* Place the App state machine in its initial state. */
    appData.state = APP_STATE_INIT;

    // Register commands
    if (!SYS_CMD_ADDGRP(appCmdTbl, sizeof(appCmdTbl)/sizeof(*appCmdTbl), 
                        "app", ": Application commands")) {
        SYS_CONSOLE_PRINT("Failed to create command group\r\n");
    }


    /* TODO: Initialize your application's state machine and other
     * parameters.
     */
}


/******************************************************************************
  Function:
    void APP_Tasks ( void )

  Remarks:
    See prototype in app.h.
 */

void APP_Tasks ( void )
{

    /* Check the application's current state. */
    switch ( appData.state )
    {
        /* Application's initial state. */
        case APP_STATE_INIT:
        {
            bool appInitialized = true;

            SYS_CONSOLE_PRINT("UART Command Service Demo\r\n");
            SYS_CONSOLE_PRINT("Type 'help' for available commands\r\n");
            if (appInitialized)
            {

                appData.state = APP_STATE_SERVICE_TASKS;
            }
            break;
        }

        case APP_STATE_SERVICE_TASKS:
        {
//            static int i = 0;
//            SYS_CONSOLE_PRINT("Hello from Console %d \n", i);
//            i++;
            break;
        }

        /* TODO: implement your application state machine.*/


        /* The default state should never be executed. */
        default:
        {
            /* TODO: Handle error in application's state machine. */
            break;
        }
    }
}


/*******************************************************************************
 End of File
 */