html
在STM32F1/F4或ESP32上,UART外设默认常配置为“无回显+无处理”模式,导致终端发送的ESC [ A(↑)被拆解为三个独立字节0x1B 0x5B 0x41,且因未禁用硬件流控/软件流控干扰,部分字节可能丢失。必须关闭UART的USART_CR1_RXNEIE中断直驱式接收,改用DMA+空闲中断(IDLE)或环形缓冲区+字符级状态机解析。关键点:禁止启用HAL_UARTEx_ReceiveToIdle_IT()以外的自动过滤,确保0x1B不被误判为“起始标志”而丢弃后续字节。
采用三态有限状态机(FSM)识别ESC序列,避免阻塞式轮询:
- STATE_IDLE:等待
0x1B; - STATE_ESC_SEEN:收到
0x1B,等待0x5B(‘[’); - STATE_CSI_RECEIVED:收到
[后,缓存后续最多4字节(含A/B/C/D),超时则清空并回归IDLE。
该设计内存开销仅需5字节状态变量+4字节CSI缓存,远低于动态分配方案。
定义结构体如下(总RAM占用可控在256~512B内):
typedef struct { char entries[CFG_SHELL_HISTORY_CNT][CFG_SHELL_CMD_MAX_LEN]; uint8_t head; // 下一条新命令写入位置(0 ~ N-1) uint8_t tail; // 最旧命令位置(有效历史从tail开始) uint8_t count; // 当前有效条目数(≤ CFG_SHELL_HISTORY_CNT) uint8_t cursor; // 当前回溯索引(0=最新输入,1=上一条…) } shell_hist_t;
上下键触发时,必须原子保存当前编辑态:
curr_line[CMD_MAX] 当前未提交的输入行 64B
curr_pos 光标在
curr_line中的偏移(0=行首) 1B
回溯时先将curr_line[curr_pos]右侧内容暂存至临时缓冲区,再载入历史项并重置curr_pos = strlen(history_entry),最后恢复右侧残留字符——此即“非破坏性覆盖”核心逻辑。
采用双缓冲+事件标记模式:
所有环形缓冲区访问均通过宏封装:
#define HIST_IDX(h, i) ((h)->tail + (i)) % CFG_SHELL_HISTORY_CNT #define HIST_GET(h, i) ((h)->entries[HIST_IDX(h,i)]) // 使用前强制校验:if ((i) < (h)->count) { ... }
在shell_hist_add()中,若count == MAX,则tail = (tail + 1) % MAX,确保始终满足count ≤ MAX且head/tail永不越界。
下表为不同实现策略的RAM占用基准(单位:字节):
构建可复现的测试用例集:
- 连续按↑键10次,验证
cursor不溢出且历史不重复; - 在第5个字符处按↑,检查原输入右半段是否完整保留;
- 注入乱序ESC序列(如
0x1B 0x5B 0x41 0x1B 0x5B 0x42),确认状态机不锁死; - 满缓冲后持续输入新命令,验证
tail推进与旧条目覆盖正确性。
ESP32 UART驱动默认启用UART_HW_FLOWCTRL_DISABLE但隐含UART_PATTERN_DET_EN,会截断含0x0A/0x0D的CSI序列。须调用uart_set_pattern_en(UART_NUM_0, false)禁用模式检测,并将UART_ISR中uart_read_bytes()改为单字节读取+超时重试,避免DMA批量吞掉ESC序列中间字节。
- ✅ 所有字符串操作使用
strncpy_s或带长度检查的自定义safe_strcpy; - ✅ 历史缓冲区定义为
static且置于.bss段,规避栈溢出; - ✅ ANSI解析FSM在
default:分支强制回归STATE_IDLE防死锁; - ✅ 每次
printf输出前校验UART Tx FIFO空间,避免阻塞; - ✅ 在
shell_init()中预填充hist.entries[0][0] = ‘0’,消除未初始化风险。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/253951.html