UART串口Shell软硬件模型分析总结

文章目录

    • 层次一、最底层逻辑配置交互----如何从Uart硬件读写单个字节数据
    • 层次二、抽象串口软件模块交互----基于串口对接输入输出流 和 Printf适配
    • 层次三、类似Shell封装抽象交互----基于串口交互命令行界面(命令解析、补全、修改、记录)
      • case1 依次输入字符abc回车----可见字符回显,回车解析,行首提示符
      • case2 输入abc左移动两次光标删除a插入A----移动光标,指定位置删除与插入
      • case3 输入多条命令后 按上下按键----查看历史命令
      • case4 输入部分字符后按TAB按键----自动补全命令
      • case5 忽略CTRL+A\CTRL+C等特殊控制命令

串口的基础协议总结见:UART串口协议简单总结 https://blog.csdn.net/runafterhit/article/details/114990221
本文用于UART串口协议软硬件模型分析,不再讲解基础协议,目的讲解清楚 从逻辑模型 到 软件Shell设计 的交互过程。
整个过程以类似伪代码的方式 目的是描述主要细节,能帮助理解整个交互中的设计点;

层次一、最底层逻辑配置交互----如何从Uart硬件读写单个字节数据

串口的基本协议如下,不做过多描述;
UART串口Shell软硬件模型分析总结_第1张图片
第一个层次,我们看一下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状态寄存器 是否 空闲

层次二、抽象串口软件模块交互----基于串口对接输入输出流 和 Printf适配

第二个层次,我们需要抽象一个软件接口,能通过读取字符 和 串口输出字符串 的基本接口;
这个层次的读取字符基本就是透传逻辑接口,在发送一个字符时要考虑平台要求 比如 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封装抽象交互----基于串口交互命令行界面(命令解析、补全、修改、记录)

第三个层次,业务逻辑就变得复杂了。类似Shell的基本命令行交互界面,我们先考虑下面几个基本行为;
1)行首提示符,同步回显输入可见字符,回车解析命令:在串口界面 行首带有命令提示符 比如【$】或者【#】,输入可见字符要同步显示到终端界面上,回车后整个字符串 作为命令设备进行解析;
2)可调整光标位置,插入删除字符 :字符串在回车前,可以通过左右方向按键 移动光标位置,在位置处插入字符 或 删字符;
3)可查看历史命令:在终端按上下方向按键时,能查看历史命令并显示到终端,找到后按回车可以直接输入;
4)可用TAB命令补齐:在终端输入部分字符时,通过TAB按键能进行命令补齐,匹配单个时直接补全,多个时显示出来;
5)忽略CTRL+A\CTRL+C等特殊控制命令 部分终端串口界面上经常使用的操作命令,设备需要进行主动忽略;
整个数据链路的交互如下,一定要区分好终端的显示界面 输入输出的概念,当你敲入一个字符后本身不会在终端显示出来,终端的显示内容全部都来自于 嵌入式设备 中Shell交互模型来定义,根据受到的字符 判断 如何显示到终端界面上,通过Tx发送到终端显示出来;
UART串口Shell软硬件模型分析总结_第2张图片

嵌入式设备Shell软件模型,大致我们可以梳理为类似下面的逻辑:

// 解析流程,ms级别的循环调度即可
void ShellCmdProcess(void)
    UartGetc() 获取字符,失败return -1
        判断是否是方向按键,需要累计3字符状态
            中间的过程字符转化忽略按键
            3字符累计结果转化为 方向动作 操作符
                上下按键操作---历史命令显示 return
                左右按键操作---光标左右处理 return
        判断是否是忽略按键,如CTRL A等,丢弃不做处理 return
        判断是否是删除按键,进行位置删除操作(注意非尾部状态) return
        判断是否是TAB按键,进行命令补全,return
        判断是否为ENTER回车按键
        	命令记录,匹配系统命令,查找是否存在 进行回调响应 return
        判断是否为特殊响应功能键,进行响应,return
        中间输入的普通字符累计(注意非尾部状态)
        	普通字符回显到终端,累计记录到输入命令串中       

下面我们针对这几个case提出一些软件交互行为设计模型,配合ASCII码表更好理解下面描述;

case1 依次输入字符abc回车----可见字符回显,回车解析,行首提示符

界面行为:依次输入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串中
            累计记录到输入命令串中(注意中间插入场景,在后面方向按键讲解)

UART串口Shell软硬件模型分析总结_第3张图片

case2 输入abc左移动两次光标删除a插入A----移动光标,指定位置删除与插入

备注:上下左右方向按键 一个按键由三个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);

case3 输入多条命令后 按上下按键----查看历史命令

通过上下按键 查看历史命令的思路如下,每次输入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),记录到历史命令列表中
            匹配系统命令,查找是否存在 进行回调响应
        判断是否为特殊响应功能键...
        中间输入的普通字符累计
        	普通字符回显到终端,累计记录到输入命令串中 

case4 输入部分字符后按TAB按键----自动补全命令

自动补全的策略如下,当识别到TAB按键的ASCII后,把已经输入的cmd串 和 系统中的命令合集进行匹配。
1)如果没有找到,不做补全操作,运行cmd和终端显示不变;
2)如果找到 并且 只有一个,直接补全为对应的命令串,更新cmd并刷新终端显示完整的命令串;
3)如果找到 发现不止一个,就先换行,然后挨个打印出匹配命令字符串,再换行并输出一遍当前cmd串;

void ShellCmdProcess(void)
    UartGetc() 获取字符,失败return -1
        判断是否是方向按键...
        判断是否是忽略按键...
        判断是否是删除按键...
        判断是否是TAB按键
            把当前输入的cmd字符串 和 系统支持的命令字串进行对比,记录是否找到,找到个数
                如果未找到,不做补全
                如果找到,并且只有一个,直接补全到当前cmd,并输出到终端显示刷新;
                如果找到,并不止一个,就先换行,然后挨个打印出匹配命令字符串,再换行并输出一遍当前cmd串
        判断是否为ENTER回车按键...
        判断是否为特殊响应功能键...
        中间输入的普通字符累计
        	普通字符回显到终端,累计记录到输入命令串中 

case5 忽略CTRL+A\CTRL+C等特殊控制命令

要做到忽略CTRL+A\CTRL+C等特殊控制命令,只需要在解析字符前,剔除掉即可。
#define CTRL_© (© - ‘a’ + 1) // CTRL加字符时 受到的ASCII码

你可能感兴趣的:(ARM嵌入式类,单片机,嵌入式硬件)