串口的基本协议如下,不做过多描述;
第一个层次,我们看一下Uart基本硬件逻辑如何交互。大部分Uart的硬件寄存器模型都可以抽象到下面内容:
1)、初始化与使能硬件:包括速率配置、数据模式配置(bit位宽、校验方式、校验使能);
2)、数据的发送:发送数据Tx FIFO寄存器(有一定深度)+ FIFO是否满 状态寄存器;
3)、数据的接受:读取数据Rx FiFo寄存器(有一定深度)+ FIFO是否空闲 状态 寄存器;
4)、串口中断的控制:中断mask/status/clear;
5)、串口DMA控制:DMA相关使能 和 控制;
本文讨论的串口模块 没有使用中断和DMA,串口的速率较低常见115200bps,在大部分简单嵌入式设备中 都没有必要使用中断,用轮询的方式足够。DMA类似,如果用Uart作为 命令行交互 通常不需要用DMA,当用Uart做大量数据传输时使用;
那么第一个层次 Uart硬件的寄存器配置抽象大致如下:
void UartHardWareInit(u32 clk)
配置Uart速率寄存器
配置Uart数据模式寄存器
配置Uart使能寄存器
void UartHardWarePutc(char c)
while 循环检查Tx FiFo状态寄存器 是否 未满;
(空闲后)往Tx FiFo写入c
int UartHardWareGetc(void)
检查Rx FiFo状态寄存器 是否 空闲
若 空闲 return 返回0
若 非空 return 读取Rx FiFo字符
bool UartHardWareTstc(void)
return Rx FiFo状态寄存器 是否 空闲
第二个层次,我们需要抽象一个软件接口,能通过读取字符 和 串口输出字符串 的基本接口;
这个层次的读取字符基本就是透传逻辑接口,在发送一个字符时要考虑平台要求 比如 windows平台要求,当换行时需要补充’\r’
适配Printf打印函数,主要是要先把 变长参数 转化为固定字符串,再通过串口输出字符串即可;
当前这个层次还不涉及复杂的软件逻辑;
//读取一个字符
int UartGetc(void)
return UartHardWareGetc();
//输出一个字符
void UartPutc(char c)
// windows平台要求,当换行时需要补充'\r'
if (c == '\n')
UartHardWarePutc('\r')
UartHardWarePutc(c)
//输出字固定符串
void UartPuts(char *s)
while(*s) { UartPutc(*s++);}
//适配基础打印printf
void Printf(char *fmt, ...)
// 先把变长参数 转换定长字符串
va_start(ap,fmt);
vsprintf(str,fmt,ap);
va_end(ap)
// 调用串口输出字符串
UartPuts(str);
第三个层次,业务逻辑就变得复杂了。类似Shell的基本命令行交互界面,我们先考虑下面几个基本行为;
1)行首提示符,同步回显输入可见字符,回车解析命令:在串口界面 行首带有命令提示符 比如【$】或者【#】,输入可见字符要同步显示到终端界面上,回车后整个字符串 作为命令设备进行解析;
2)可调整光标位置,插入删除字符 :字符串在回车前,可以通过左右方向按键 移动光标位置,在位置处插入字符 或 删字符;
3)可查看历史命令:在终端按上下方向按键时,能查看历史命令并显示到终端,找到后按回车可以直接输入;
4)可用TAB命令补齐:在终端输入部分字符时,通过TAB按键能进行命令补齐,匹配单个时直接补全,多个时显示出来;
5)忽略CTRL+A\CTRL+C等特殊控制命令 部分终端串口界面上经常使用的操作命令,设备需要进行主动忽略;
整个数据链路的交互如下,一定要区分好终端的显示界面 输入输出的概念,当你敲入一个字符后本身不会在终端显示出来,终端的显示内容全部都来自于 嵌入式设备 中Shell交互模型来定义,根据受到的字符 判断 如何显示到终端界面上,通过Tx发送到终端显示出来;
嵌入式设备Shell软件模型,大致我们可以梳理为类似下面的逻辑:
// 解析流程,ms级别的循环调度即可
void ShellCmdProcess(void)
UartGetc() 获取字符,失败return -1
判断是否是方向按键,需要累计3字符状态
中间的过程字符转化忽略按键
3字符累计结果转化为 方向动作 操作符
上下按键操作---历史命令显示 return
左右按键操作---光标左右处理 return
判断是否是忽略按键,如CTRL A等,丢弃不做处理 return
判断是否是删除按键,进行位置删除操作(注意非尾部状态) return
判断是否是TAB按键,进行命令补全,return
判断是否为ENTER回车按键
命令记录,匹配系统命令,查找是否存在 进行回调响应 return
判断是否为特殊响应功能键,进行响应,return
中间输入的普通字符累计(注意非尾部状态)
普通字符回显到终端,累计记录到输入命令串中
下面我们针对这几个case提出一些软件交互行为设计模型,配合ASCII码表更好理解下面描述;
界面行为:依次输入abc,输入每个字符过程回显到终端,回车后换行,行首出现提示符$
RX输入:0x61 0x62 0x63 0x0D
—0x61~0x63依次是abc字符的ASCII,0x0D为回车
Tx输出: 0x61 0x62 0x63 0x0D 0x0D 0x0A 0x24 0x20
—0x61~0x63依次是abc字符的ASCII,0x0D回车0x0A换行0x24为行首$提示符号,0x20为空格;
void ShellCmdProcess(void)
UartGetc() 获取字符,失败return -1
判断是否是方向按键...
判断是否是忽略按键...
判断是否是删除按键...
判断是否是TAB按键...
判断是否为ENTER回车按键
// 3、当识别到ENTER按键时,把累计命令cmd串进行匹配响应
命令字符串记录(用于上下按键查看历史命令,后面讲解)
按记录命令cmd串 匹配查看是否存在内部命令 进行回调响应
// 4、让终端显示进行回车换行,并显示行首的提示符
输出换行和行首的命令提示符号UartPuts("\r\n$ ");
判断是否为特殊响应功能键...
中间输入的普通字符累计(注意非尾部状态)
// 1、每个非控制类的普通显示字符都需要 回显到终端
普通字符回显到终端UartPutc(c)
// 2、同时累计到记录的命令cmd串中
累计记录到输入命令串中(注意中间插入场景,在后面方向按键讲解)
备注:上下左右方向按键 一个按键由三个ASCII组成,0x1b+0x5b+0x41上/ 0x42下/ 0x44/ 0x43右
界面行为:依次输入abc,然后按两次左方向按键 光标移动到a,回删按键删除a(显示bc),再输入A字符(显示Abc),
RX输入:0x61 0x62 0x63 0x1b 0x5b 0x44 0x1b 0x5b 0x44 0x08 0x41
—0x61~0x63依次是abc字符的ASCII,0x1b 0x5b 0x44 三个字符组合为左方向按键,0x08回退, 0x4a为A的ASCII
Tx输出: 0x61 0x62 0x63 0x08 0x08 0x08 0x62 0x63 0x20 0x08 0x08 0x08 0x41 0x62 0x63 0x08 0x08 0x08
—0x61~0x63依次是abc字符的ASCII,两次0x08是两次左移光标;0x08 0x62 0x63 0x20 0x08 0x08 0x08 行为是先左移动到a字符位置,重新输出b c [空格] 就把abc替换为bc[空格]达到删除字符效果,然就再0x8回退三次就返回到原来首行的光标位置;0x41 0x62 0x63 0x08 0x08 0x08 行为是重新输出A b c就把bc替换为Abc达到插入A字符效果,然后再回退到初始位置;
核心思路:shell软件用标志pos记录 始终匹配终端光标位置,len记录已输入的cmd累计长度;
左移:往终端输出 回退0x8控制光标左移,刷新pos;右移:往终端输出cmd[pos],通过输出相同字符让光标右移,刷新pos;
非尾部插入:先往终端输出插入字符,然后把之前cmd[pos]开始子串再输出一遍,就把终端从pos处更新出新字符串,之后再连续回退调整光标位置;
非尾部删除:先往终端输出回退0x8,把cmd[pos]之后字串输出一遍,再通过输出空格替换原来终端显示尾部字符,之后再连续回退调整光标位置;
void ShellCmdProcess(void)
UartGetc() 获取字符,失败return -1
判断是否是方向按键,需要累计3字符状态
// 1、当识别到0x1b字符时 开始匹配后续字符
中间的过程字符转化忽略按键,不进行回显示和统计到cmd串
// 2、根据方向字符最后一个0x41上/ 0x42下/ 0x44/ 0x43右,得到方向操作
3字符累计结果转化为 方向动作 操作符
上下按键操作...
左右按键操作---光标左右处理
// 3、左按键:(当光标pos未在最左)若往左就往终端输出0x8回退光标,更新pos
// 右按键:(当光标pos未在最右len) 就往终端再输出一次本身cmd位置字符,更新pos
左按键:if (pos > 0) { UartPutc(0x8);pos--; }
右按键:if (pos < len) { UartPutc(cmd[pos]);pos++; }
判断是否是忽略按键...
判断是否是删除按键...
// 4、删除时如果在尾部,就回退一格,然后输出空格,在回退,更新pos和len
尾部:if (pos == len) { UartPutc(0x8);UartPutc(0x20);UartPutc(0x8);
cmd[pos] = '\0';pos--;len--}
// 删除时如果非尾部,就要先回退一格,然后重新输出后续字符串+空格替换,最后再回退到初始位置
非尾部:UartPutc(0x8);UartPutns(cmd[pos],len-pos));UartPutc(0x20);多次回退 循环UartPutc(0x8);
memmove(&cmd[pos-1], &cmd[pos], len-pos);cmd[pos] = '\0';len--;pos--;//更新cmd字符串;
判断是否是TAB按键...
判断是否为ENTER回车按键...
判断是否为特殊响应功能键...
中间输入的普通字符累计(注意非尾部状态)
// 1、当插入位置不是在尾部时,需要先输出插入字符,并把原来pos开始的cmd字符到尾部再输出一遍;
memmove(&cmd[pos+1], &cmd[pos], len-pos+1);len++;cmd[pos]=c;
UartPuts(cmd[pos],len-pos);pos++;多次回退 循环UartPutc(0x8);
通过上下按键 查看历史命令的思路如下,每次输入ENTER按键时,把CMD字串记录到历史list中标识index。当按上下按键时先 从Tx输出 让终端的显示行清空,并把对应的list中index记录输出到终端,同时把当前运行的cmd更新为list中对应的cmd记录。
void ShellCmdProcess(void)
UartGetc() 获取字符,失败return -1
判断是否是方向按键
中间的过程字符转化忽略按键,不进行回显示和统计到cmd串
3字符累计结果转化为 方向动作 操作符
// 2、上下按键操作
上下按键:依次回删本行全部输入,然后把record中对应index中cmd找出
把cmd[index]输出到终端,并更新到当前输入的cmd中,更新pos和len
判断是否是忽略按键...
判断是否是删除按键...
判断是否是TAB按键...
判断是否为ENTER回车按键
// 1、当识别到ENTER按键时,先把命令记录到历史数组
命令记录record(cmd),记录到历史命令列表中
匹配系统命令,查找是否存在 进行回调响应
判断是否为特殊响应功能键...
中间输入的普通字符累计
普通字符回显到终端,累计记录到输入命令串中
自动补全的策略如下,当识别到TAB按键的ASCII后,把已经输入的cmd串 和 系统中的命令合集进行匹配。
1)如果没有找到,不做补全操作,运行cmd和终端显示不变;
2)如果找到 并且 只有一个,直接补全为对应的命令串,更新cmd并刷新终端显示完整的命令串;
3)如果找到 发现不止一个,就先换行,然后挨个打印出匹配命令字符串,再换行并输出一遍当前cmd串;
void ShellCmdProcess(void)
UartGetc() 获取字符,失败return -1
判断是否是方向按键...
判断是否是忽略按键...
判断是否是删除按键...
判断是否是TAB按键
把当前输入的cmd字符串 和 系统支持的命令字串进行对比,记录是否找到,找到个数
如果未找到,不做补全
如果找到,并且只有一个,直接补全到当前cmd,并输出到终端显示刷新;
如果找到,并不止一个,就先换行,然后挨个打印出匹配命令字符串,再换行并输出一遍当前cmd串
判断是否为ENTER回车按键...
判断是否为特殊响应功能键...
中间输入的普通字符累计
普通字符回显到终端,累计记录到输入命令串中
要做到忽略CTRL+A\CTRL+C等特殊控制命令,只需要在解析字符前,剔除掉即可。
#define CTRL_© (© - ‘a’ + 1) // CTRL加字符时 受到的ASCII码