Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

UART String Parsing CLI

Recommended resources

Hello! Let's get started on our next lesson.

In the previous session, you built a robust, interrupt-driven system capable of receiving continuous streams of characters over UART and framing them into complete lines of text. Your firmware can now reliably capture a user's command, such as led on\n, and store it in a string buffer.

Today, we'll give that string meaning. This lesson directly addresses the learning outcome: Implement a command-line interface (CLI) that parses strings received over UART. We will transform the raw text from your line_buffer into executable actions, allowing you to interact with and control your microcontroller in real-time. This is a fundamental skill for debugging, testing, and configuring embedded systems, especially in an automotive context where ECUs often require a diagnostic interface.

1. From a String to an Action: The Anatomy of a CLI

A Command-Line Interface is essentially a text-based dialogue between a user and a machine. It involves three key steps:

  1. Read: Receive a full command from the user. You completed this step in the last lesson.
  2. Parse: Deconstruct the command string to identify the core instruction and any associated parameters (arguments).
  3. Execute: Run the specific code corresponding to the parsed command and arguments.

For example, in the command led on, led is the command, and on is the argument. Our parsing logic needs to separate these two parts so we can first identify the led command and then decide what to do based on the on argument.


2. A Simple but Inflexible Parsing Method

The most straightforward way to parse a command is to compare the entire received string. For this, the C standard library function strcmp() is perfect.

Let's look at a video that demonstrates this basic concept. The presenter is using the STM32 HAL, but the core C logic of comparing strings is identical to what we would do in our bare-metal approach.

Serial UART with STM32 Microcontroller-Transmit and Receive Data

Watch this short segment from the 'Serial UART with STM32' video. The presenter implements a basic CLI to control an LED. Notice how they use strcmp to check if the received string exactly matches "LED on".

Watch from 15:24 to 18:44. Focus on the C code inside the if statement that compares the received string to a hardcoded command. This demonstrates the simplest form of command parsing.

This strcmp-only method works, but it has significant limitations:

  • Poor Scalability: You need a separate strcmp for every possible command variation (led on, led off, led toggle).
  • No Argument Handling: It cannot handle commands with variable arguments, like set_brightness 75.

To build a truly useful CLI, we need a more sophisticated parsing strategy.


3. A Professional Parsing Strategy: In-Place Splitting

In resource-constrained embedded systems, we want to avoid dynamic memory allocation (malloc) and unnecessary data copying. A highly efficient technique is to parse the command string in-place.

The core idea is to find the first space in the command string, which separates the command from its arguments, and replace that space with a null terminator (\0).

Let's say our line_buffer contains "led on\r\n".

  1. Clean up: First, we remove the trailing newline and carriage return characters, turning the buffer into "led on\0".
  2. Find the delimiter: We locate the first space.
  3. Split the string: We replace the space with a null terminator (\0). The buffer now contains led\0on\0.
  4. Assign pointers:
    • A command pointer points to the beginning of the buffer ("led").
    • An args pointer points to the character immediately after the new null terminator ("on").

This gives us two valid, null-terminated C-strings representing the command and its arguments without using any extra memory.

This exact technique is a key takeaway from a talk on building simple CLIs. The following resource summarizes several best practices for this task.

Building a Simple Command-Line Interface - Key Concepts

This page from the Embedded Online Conference summarizes a talk on building a simple CLI. The quiz questions highlight several critical, professional-level concepts.

Read through the four quiz questions. Pay special attention to the second question: 'What is Nathan's primary reason for converting the first space in the incoming command string to a null character...'. The correct answer explains the in-place splitting technique we just discussed. The other questions about non-blocking reception, echoing, and backspace handling are also very relevant.


4. Hands-On: Building Your CLI Parser

Let's integrate this professional parsing strategy into the project from our last lesson. We'll create a process_command function and add a few simple commands.

Step 1: Create a process_command Function Stub

In a relevant file (e.g., main.c or a new cli.c), create the function that will contain our parsing logic. For now, it will just echo the received line.

// In main.c or a new cli.c



#include <string.h> // For strcmp, strchr
#include <stdio.h>  // We will need this later
#include "usart.h"  // For your uart_send_non_blocking function
#include "gpio.h"   // Assuming you have a gpio.c for LED control

void process_command(char* cmd_line)
{
    // For now, just confirm we received the line
    uart_send_non_blocking((uint8_t*)"CMD: ", 5);
    uart_send_non_blocking((uint8_t*)cmd_line, strlen(cmd_line));
}

Step 2: Clean the Input String

Before parsing, it's good practice to "sanitize" the input by removing the trailing \r and \n characters that terminals send.

Update process_command with this logic at the beginning:

void process_command(char* cmd_line)
{
    // Find the end of the string and remove trailing newline/carriage return
    char* p = strchr(cmd_line, '\r');
    if (p) *p = '\0';
    p = strchr(cmd_line, '\n');
    if (p) *p = '\0';
    
    // ... rest of the function
}

Step 3: Implement In-Place Parsing Logic

Now, let's add the core parsing logic to process_command.

void process_command(char* cmd_line)
{
    // ... (sanitizing code from Step 2) ...

    char* command;
    char* args;

    // Find the first space to separate command from arguments
    args = strchr(cmd_line, ' ');
    if (args != NULL) {
        // We have arguments. Terminate the command string.
        *args = '\0';
        // Move the args pointer to the start of the arguments string.
        args++;
    }
    command = cmd_line;

    // ... (command dispatching logic will go here) ...
}

This is a robust check. If strchr returns NULL, it means there was no space, so the args pointer remains NULL, indicating a command without arguments.

Step 4: Add a Command Dispatcher

This is where you'll use strcmp to match the command part and act on it. Let's implement two commands: status and led <on|off>.

Add this logic at the end of your process_command function.

// ... (inside process_command, after parsing logic) ...

if (strcmp(command, "status") == 0) {
    uart_send_non_blocking((uint8_t*)"System OK\r\n", 11);
} 
else if (strcmp(command, "led") == 0) {
    if (args != NULL) {
        if (strcmp(args, "on") == 0) {
            // Assuming PC13 is the built-in LED on the Blue Pill
            gpio_write(GPIOC, 13, 0); // Active low, so 0 is ON
            uart_send_non_blocking((uint8_t*)"OK\r\n", 4);
        } else if (strcmp(args, "off") == 0) {
            gpio_write(GPIOC, 13, 1); // Active low, so 1 is OFF
            uart_send_non_blocking((uint8_t*)"OK\r\n", 4);
        } else {
            uart_send_non_blocking((uint8_t*)"Error: Unknown argument for 'led'. Use 'on' or 'off'.\r\n", 56);
        }
    } else {
        uart_send_non_blocking((uint8_t*)"Error: 'led' command requires an argument ('on' or 'off').\r\n", 60);
    }
} 
else {
    uart_send_non_blocking((uint8_t*)"Error: Unknown command.\r\n", 25);
}

Note: Make sure you have a gpio_write function or similar to control your board's LED.

Step 5: Update main()

Finally, modify your main loop to call process_command. This replaces the simple echo logic from the previous lesson.

// In main.c
// ... (includes and initializations) ...
extern volatile uint8_t g_line_received;



#define LINE_BUFFER_SIZE 64

void process_command(char* cmd_line); // Forward declaration

int main(void)
{
    // ... system initializations ...
    
    char line_buffer[LINE_BUFFER_SIZE];
    uart_send_non_blocking((uint8_t*)"System Ready.\r\n", 15);

    while (1)
    {
        if (g_line_received)
        {
            g_line_received = 0;
            uart_read_line(line_buffer, LINE_BUFFER_SIZE);
            
            // NEW: Call the command processor
            process_command(line_buffer);
        }
        
        // Other tasks can run here...
    }
}

Now, compile and flash your code. Open your serial terminal and try your new commands:

  • status
  • led on
  • led off
  • led (to test argument error handling)
  • foobar (to test unknown command error handling)

5. Studying a More Complete Example

You've now built the core of a functional CLI. However, a production-quality CLI often includes features like echoing characters as they are typed and handling backspace. The speaker from the conference talk provides his source code on GitHub, which is an excellent resource to see a more complete implementation.

Simple CLI Source Code Repository

This is an opportunity to study a complete, well-structured example. You don't need to implement it all now, but reading through the code will solidify your understanding and show you how to handle more advanced features.

Follow the link in the comment from 'nathancharlesjones' to the GitHub repository. In the repo, navigate into the stm32-cli directory and open Core/Src/main.c. Read through the HAL_UART_RxCpltCallback and the main while(1) loop to see how a more complete CLI can be structured, including character echoing and command processing logic.


Conclusion

Congratulations! You have successfully built a command-line interface. This is a significant milestone that transforms your embedded system from a passive device into an interactive one.

Key Takeaways:

  • A CLI is based on a Read-Parse-Execute loop.
  • In-place string splitting using strchr and a null terminator is a memory-efficient and professional technique for parsing commands and arguments.
  • A robust CLI provides clear feedback to the user for both successful commands and errors.
  • Command logic should be separated into a dedicated processing function to keep main() clean.

Preview of the Next Lesson:
Our CLI currently uses our custom uart_send_non_blocking function for all feedback. While functional, this can be cumbersome. What if we could use the familiar, powerful printf function? In the next lesson, "Redirect printf output to UART for streamlined debugging," we will do just that. You will learn how to route all output from printf directly to your UART transmit buffer, making your CLI feedback and general debugging significantly easier and more flexible.