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 files
For 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
,STM
andRaspberry 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.read
Note: 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.read
For 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 called
Step 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
Please log in or sign up to comment.