文章目录
在嵌入式系统开发中,使用串口Shell控制台是一种非常常见且高效的调试方式。本文将基于STM32平台,分析一个简洁但功能完整的Shell控制台实现,包括命令输入、编辑、历史记录以及命令执行等关键功能。
一、总体结构
该Shell控制台主要包含以下功能模块:
- 命令输入缓冲与解析
- 光标控制与编辑
- 命令历史管理
- 串口接收中断处理
- 命令执行与回显
二、串口接收机制
在 shell_uart_start_rx()
函数中,通过调用 HAL_UART_Receive_IT()
启动中断接收模式,确保串口能够异步接收字符:
c
HAL_UART_Receive_IT(&huart1, &uart_rx_ch, 1);
每当串口接收到一个字符,就会触发 HAL_UART_RxCpltCallback()
回调函数:
c
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
shell_input_char((char)uart_rx_ch); // 处理字符
HAL_UART_Receive_IT(&huart1, &uart_rx_ch, 1); // 继续接收
}
}
该回调函数负责将接收到的字符送入 shell_input_char()
进行处理,并重新启动中断接收。
三、命令输入与处理逻辑
shell_input_char(char ch)
是命令输入的核心处理函数,它处理普通字符、退格键、回车键、以及ESC转义序列(用于方向键)。
关键逻辑包括:
- 普通字符:调用
shell_insert_char(ch)
插入到当前光标位置; - 回车键:调用
shell_handle_enter()
执行命令并保存历史; - 退格键:调用
shell_backspace()
删除前一个字符; - ESC序列:用于处理上下左右键,控制历史切换或光标移动。
四、命令编辑与显示
shell_refresh_line()
是命令行重绘函数,它会:
- 回车至行首;
- 显示提示符与当前命令;
- 移动光标到正确位置。
其原理是通过ANSI转义序列实现,例如 \x1b[C
表示右移光标一格。
在编辑命令时,插入字符或删除字符均会触发此函数以实时刷新显示。
五、历史命令管理
Shell控制台维护一个历史命令环形缓冲区,通过 history[][]
存储最多 SHELL_HISTORY_NUM
条命令。
关键函数:
shell_save_history(const char *cmd)
:保存新命令;shell_show_history(int index)
:根据历史索引回显旧命令。
方向键 ↑ 和 ↓ 会通过修改 history_index
实现历史命令的切换回显。
六、命令执行
在按下回车后:
c
shell_handle_enter()
该函数会将命令字符串传入 cmd_execute()
:
c
if (cmd_len > 0) {
shell_save_history(cmd_buf);
cmd_execute(cmd_buf); // 执行命令
}
cmd_execute()
是命令解析与执行的封装接口,可以根据具体项目自行实现命令注册与执行框架。
七、初始化与使用
Shell初始化函数 shell_init()
中:
c
cmd_init(); // 初始化命令表
printf("Welcome to STM32 Shell\r\n");
printf(PROMPT);
同时应在程序中调用 shell_uart_start_rx()
启动串口接收。
如果使用RTOS,Shell可以集成在一个独立的 shell_task()
线程中。
八、小结
该Shell控制台框架具有以下特点:
- 支持插入、删除、方向键控制;
- 支持多条命令历史;
- 串口异步接收,适配RTOS或裸机环境;
- 可扩展的命令执行接口。
适用于大多数STM32嵌入式项目,有助于提升调试效率和交互能力。
完整代码注释:
shell.c:
c
#include "shell.h"
#include <string.h>
#include <stdio.h>
#include "usart.h" // 包含串口 huart1 的定义
#include "cmd.h" // 包含命令执行相关定义
#define PROMPT "> " // 命令提示符
// 命令行输入缓冲区
static char cmd_buf[SHELL_CMD_MAX_LEN];
// 当前命令的长度
static uint8_t cmd_len = 0;
// 当前光标在命令行中的位置
static uint8_t cursor_pos = 0;
// 历史命令缓存(环形历史)
static char history[SHELL_HISTORY_NUM][SHELL_CMD_MAX_LEN];
// 历史命令条数
static int history_count = 0;
// 当前访问历史命令的索引,-1 表示未访问历史命令
static int history_index = -1;
// ESC序列处理状态(0:无,1:收到 ESC,2:收到 ESC[)
static uint8_t esc_state = 0;
// 串口接收字符缓冲
static uint8_t uart_rx_ch;
/**
* @brief 启动串口接收中断(初始化后调用一次)
*/
void shell_uart_start_rx(void) {
HAL_UART_Receive_IT(&huart1, &uart_rx_ch, 1);
}
/**
* @brief 串口接收完成中断回调函数(在stm32_hal库中调用)
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 将接收到的字符交给 shell 处理
shell_input_char((char)uart_rx_ch);
// 继续接收下一个字符
HAL_UART_Receive_IT(&huart1, &uart_rx_ch, 1);
}
}
/**
* @brief 重绘整行命令和光标位置
*/
static void shell_refresh_line(void)
{
printf("\r"); // 回到行首
printf(PROMPT); // 打印提示符
printf("%s", cmd_buf); // 打印当前命令
printf(" \r"); // 用空格覆盖残留字符,防止显示残留
printf("\r"); // 再次回到行首
// 将光标移到 cmd_buf 中 cursor_pos 的位置
int pos = strlen(PROMPT) + cursor_pos;
for (int i = 0; i < pos; i++) {
printf("\x1b[C"); // ANSI 控制码,光标右移
}
}
/**
* @brief 保存输入命令到历史记录
*/
static void shell_save_history(const char *cmd)
{
// 空命令不保存
if (cmd[0] == '\0') return;
// 如果和上一条命令相同则不重复保存
if (history_count == 0 || strcmp(cmd, history[0]) != 0) {
if (history_count < SHELL_HISTORY_NUM) {
history_count++;
}
// 历史命令向后移动一位,腾出第一个位置
for (int i = history_count - 1; i > 0; i--) {
strcpy(history[i], history[i - 1]);
}
// 复制当前命令到历史[0]
strncpy(history[0], cmd, SHELL_CMD_MAX_LEN);
}
history_index = -1; // 重置历史访问状态
}
/**
* @brief 处理回车:执行命令并清空缓冲
*/
static void shell_handle_enter(void)
{
cmd_buf[cmd_len] = '\0'; // 确保字符串结尾
printf("\r\n");
if (cmd_len > 0) {
shell_save_history(cmd_buf); // 保存历史
cmd_execute(cmd_buf); // 执行命令(调用命令系统)
}
// 重置缓冲区
cmd_len = 0;
cursor_pos = 0;
memset(cmd_buf, 0, sizeof(cmd_buf));
history_index = -1;
printf(PROMPT); // 打印提示符
}
/**
* @brief 删除光标前一个字符
*/
static void shell_backspace(void)
{
if (cursor_pos == 0) return; // 光标在起点,不能删除
// 后面字符左移覆盖当前字符
memmove(&cmd_buf[cursor_pos - 1], &cmd_buf[cursor_pos], cmd_len - cursor_pos);
cmd_len--;
cursor_pos--;
cmd_buf[cmd_len] = '\0';
shell_refresh_line(); // 重绘命令行
}
/**
* @brief 插入字符到光标当前位置
*/
static void shell_insert_char(char ch)
{
if (cmd_len >= SHELL_CMD_MAX_LEN - 1) return; // 缓冲区满
// 将光标右侧字符右移,腾出插入空间
memmove(&cmd_buf[cursor_pos + 1], &cmd_buf[cursor_pos], cmd_len - cursor_pos);
cmd_buf[cursor_pos] = ch;
cmd_len++;
cursor_pos++;
cmd_buf[cmd_len] = '\0';
shell_refresh_line(); // 重绘命令行
}
/**
* @brief 显示某一条历史命令
*/
static void shell_show_history(int index)
{
if (index < 0 || index >= history_count) {
return;
}
strcpy(cmd_buf, history[index]);
cmd_len = strlen(cmd_buf);
cursor_pos = cmd_len;
shell_refresh_line();
}
/**
* @brief Shell 接收字符输入处理函数(包括普通字符、ESC序列、回车等)
*/
void shell_input_char(char ch)
{
if (esc_state == 0) {
if (ch == 0x1B) {
esc_state = 1; // 收到 ESC,开始解析转义序列
} else if (ch == '\r' || ch == '\n') {
shell_handle_enter(); // 回车键
} else if (ch == 127 || ch == '\b') {
shell_backspace(); // 删除键
} else if (ch >= 0x20 && ch <= 0x7E) {
shell_insert_char(ch); // 可打印字符
}
} else if (esc_state == 1) {
if (ch == '[') {
esc_state = 2; // 收到 ESC + [,进入方向键解析状态
} else {
esc_state = 0; // 非预期,重置状态
}
} else if (esc_state == 2) {
esc_state = 0; // 处理完一组 ESC 序列后立即清空状态
if (ch == 'A') { // ↑ 上键
if (history_count > 0 && history_index < history_count - 1) {
history_index++;
shell_show_history(history_index);
}
} else if (ch == 'B') { // ↓ 下键
if (history_index > 0) {
history_index--;
shell_show_history(history_index);
} else if (history_index == 0) {
history_index = -1;
cmd_len = 0;
cursor_pos = 0;
cmd_buf[0] = '\0';
shell_refresh_line();
}
} else if (ch == 'C') { // → 右键
if (cursor_pos < cmd_len) {
cursor_pos++;
shell_refresh_line();
}
} else if (ch == 'D') { // ← 左键
if (cursor_pos > 0) {
cursor_pos--;
shell_refresh_line();
}
}
}
}
/**
* @brief Shell 初始化(显示欢迎信息和提示符)
*/
void shell_init(void)
{
cmd_init(); // 初始化命令系统
printf("Welcome to STM32 Shell\r\n");
printf(PROMPT);
}
/**
* @brief Shell任务(可集成到RTOS任务中,目前为空)
*/
void shell_task(void)
{
// 空任务占位
}
shell.h:
c
#ifndef __SHELL_H__
#define __SHELL_H__
#include <stdint.h>
#define SHELL_CMD_MAX_LEN 64
#define SHELL_HISTORY_NUM 5
#ifdef __cplusplus
extern "C" {
#endif
// 初始化 shell
void shell_init(void);
// 串口接收到字符时调用
void shell_input_char(char ch);
// 主循环处理函数(可选)
void shell_task(void);
void shell_uart_start_rx(void);
#ifdef __cplusplus
}
#endif
#endif // __SHELL_H__