Recently I made a contribution to CYD-Klipper firmware to support crowpanel 2.8 inch display.
While going through the repository and understanding how the firmware was implemented, I came across a module called serial_console.cpp. In this module, the author implemented a terminal-like feature directly through the serial port. The purpose of this was to change device configurations during runtime without the need to rebuild the code and re-flash the MCU.
Additionally, a user can add more commands to the serial_console, which can take arguments to further enhance the console's functionality. The implementation felt so interesting and cool that I decided to create my own serial prompt.
I wanted to develop the project with the following goals and constraints:
- It should be fully developed in C within a single header file.
- I should be able to test the prompt on my Linux machine without needing to constantly flash and build the code to the MCU.
- It should be portable across various MCUs such as ESP32, STM, and Raspberry Pi Pico (which I mostly use).
At the end the serial console prompt should look like:
Hello from Serial Prompt
Type '?' for help
> ?
print : print func
mult : mult num1 num2
>Design- For - It should be fully developed using C in a single header file.
serial_prompt $ tree
.
├── main.c
└── serial_prompt.h
1 directory, 2 filesFor this I will be having all the logic under serial_prompt.h and main.c will be including this header to make use of the serial prompt.
- For - I should be able to test the prompt in my Linux machine without needing to flash build continuously to the MCU.
To emulate mcu serial console in my terminal I need to perform some manually configuration to my linux terminal.
/*1.*/
void disable_input_buffering() {
/* store the original state */
tcgetattr(STDIN_FILENO, &original_tio);
struct termios new_tio = original_tio;
new_tio.c_lflag &= ~ICANON & ~ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
}This portion of code switches the terminal input mode from the default line-buffered mode (where input is processed only after pressing Enter) to a raw mode, where each key press is immediately available to the program. It also disables the echoing of typed characters.
/*2.*/
void restore_input_buffering() {
/ restore original state */
tcsetattr(STDIN_FILENO, TCSANOW, &original_tio);
}Restores the previous state of terminal.
/*3.*/
void handle_interrupt(int signal) {
restore_input_buffering();
exit(-2);
}We need to also restore the previous state of the terminal if we press CTRL+C to exit the code.
int main(int argc, char **argv) {
/*3.*/
signal(SIGINT, handle_interrupt);
/*1.*/
disable_input_buffering();
...
/*2.*/
restore_input_buffering();
}And the main function.
- For - "It should be portable across various MCUs like
ESP32,STMandRaspberry Pi Pico(Mostly that I used)"
To make it portable across various targets, I have let users define the PRINT and READ macros based on the supported API for the board. Eg
/* Arduno */
#define PRINT Serial.print
#define READ Serial.readNote: I haven't tried used EspIDF as I have been using Arduino framework on esp32.
Include serial_prompt.h header file in your project to use it.
Step 1 : Replace with the serial print/write function, here I have taken example for Arduino framework .
#define PRINT Serial.print
#define READ Serial.readFor Raspbery pi pico use rasspberry_pico_main.c
#define PRINT printf
#define READ() \
({ \
int c; \
int ret = read(STDIN_FILENO, &c, 1); \
(ret > 0) ? c : -1; \
})Step 2 : Include the header
#include "serial_prompt.h"Step 3 : Add your cmd handlers Note: command handler should has **int <handle_name>(int argc, char argv) definations.
int sample(int argc, char **argv)
{
Serial.println("Serial console commands:");
Serial.println("");
Serial.println("Sample -> this sample");
Serial.println("");
return 0;
}Step 4 : Add command name and its handler to commands array
COMMANDS(
{"sample", sample}
);Step 5 : Call it periodically
void loop() {
serial_run();
}Step 6 : Flash the firmware to the device you see the following
Hello from Serial Prompt
Type '?' for help
>If you type "?" you should see the list of cmd available
> ?
sample : sample cmd description
> sample
Sample handler calledStep 7 : For commands taking arguments eg.
int multiply_handler(int argc, char **argv) {
if (argc < 3) {
Serial.println("mult num1 num2\n");
return 0;
}
int num1 = atoi(argv[1]);
int num2 = atoi(argv[2]);
Serial.println(num1 * num2);
return 0;
}
COMMANDS(
{"sample", "sample cmd description", sample},
{"mult", "mult num1 num2", multiply_handler},
);output
Hello from Serial Prompt
Type '?' for help
> ?
print : print func
mult : mult num1 num2
> mult 3 4
12
> mult 12 46
552
>Code flowInitialization:
- When the system starts, the
setup()function is executed. - The
serial_greet()function is called, displaying a greeting message to the user:"Hello from Serial Prompt\nType '?' for help\n> ".
Command Input:
- The program enters the
loop()function, whereserial_run()is called repeatedly. - Inside
serial_run(), the program waits for user input through the serial connection, capturing characters until a newline (\n) is received.
Tokenizing Input:
- The input is tokenized into individual words (such as the command and its arguments) using
tokenize(). - The first token is assumed to be the command (e.g.,
help).
Command Lookup:
- The
find()function is called to check if the command exists in thecommands[]array. - If the command is found, the corresponding handler function (like
help()) is invoked. - If the command is unknown, an error message
"Unknown Command"is printed.
Command Execution:
- In this case, if the user types
help, thehelp()handler is executed, printing a description of available commands:
Prompting for Next Input:
- After the command is executed, the terminal displays the prompt (
>) again, awaiting the next input from the user.
Repeat:
- The loop continues, processing further commands entered by the user, calling the appropriate handlers, and displaying results until the system is powered off or reset.




Comments