MCU串口命令解析器的实现

简介: MCU串口命令解析器的实现
  • 1 编写韦东山老师的嵌入式书籍的输入系统章节

640.png

  • 2 应本公众号粉丝要求,准备造一台智能小车并开源分享

640.png

  • 3 自己工作上项目的学习:机器视觉,图像处理,TKM32F499芯片入门等

640.png

640.png

4 PID温控套件测试(后面会分享PID算法的实战使用)

640.png

所以最近会比较忙一些,也就不会更新太频啦,但是我还是会用心分享我的所见所闻及所经历的东西,希望各位谅解!


在日常工作中,我们经常会跟各种协议打交道,最常见的就是串口协议了,接下来我们将通过几个案例来实现串口解析命令,以下案例基于STM32L431RCT6小熊派开发板。

案例一

实现需求:


协议制定:

指令(字符串) 含义
led_on 打开灯
led_off 关闭灯
motor_on 打开电机
motor_off 关闭电机

1、硬件配置

640.png

640.png

640.png

1、STM32CubeMX软件配置

1.1、RCC配置

640.png

640.png

1.2、串口配置

640.png

640.png

1.3、LED和电机配置

640.png

640.png

1.4、工程生成设置

640.png

640.png

2、软件核心功能实现

main.c

typedef void (*cmd_func)(void);
typedef struct __CMD_PARSE
{
    const char *cmd_type;
    cmd_func  fun_ptr;
} CMD_PARSE;
/*命令表定义===>表驱动法*/
CMD_PARSE CMD_TABLE[CMD_SIZE] =
{
    {"led_on",   led_on_process},
    {"led_off",  led_off_process},
    {"motor_on", motor_on_process},
    {"motor_off", motor_off_process},
};
/*命令匹配*/
int Command_Matching(char *cmd_type)
{
    uint8_t cmd_at = 0 ;
    uint8_t cmd_match_flag = 0 ;
    uint8_t type_num = sizeof(CMD_TABLE) / sizeof(CMD_PARSE);
    for(cmd_at = 0 ; cmd_at < type_num ; cmd_at++)
    {
        if(0 == strcmp(CMD_TABLE[cmd_at].cmd_type, cmd_type))
        {
            cmd_match_flag = 1 ;
            break ;
        }
    }
    if(1 == cmd_match_flag)
    {
        cmd_match_flag = 0 ;
        CMD_TABLE[cmd_at].fun_ptr();
    }
    else
        return -1 ;
    return 0 ;
}
int main(void)
{
    /* USER CODE BEGIN 1 */
    uint8_t cmd_at = 0 ;
    int find_cmd_Index = 0;
    /* USER CODE END 1 */
    /* MCU Configuration--------------------------------------------------------*/
    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();
    /* USER CODE BEGIN Init */
    /* USER CODE END Init */
    /* Configure the system clock */
    SystemClock_Config();
    /* USER CODE BEGIN SysInit */
    /* USER CODE END SysInit */
    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    /* USER CODE BEGIN 2 */
    printf("命令解析器\n");
    /*使能串口接收*/
    HAL_UART_Receive_IT(&huart1, (uint8_t *)&cmd_parse_typedef.Res, 1);
    /* USER CODE END 2 */
    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
        /*如果串口数据接收完成了,则进行处理*/
        if(1 == cmd_parse_typedef.BufferReady)
        {
            for(cmd_at = 0 ; cmd_at < NR(CMD_TABLE) ; cmd_at++)
            {
                if(strcmp((char *)cmd_parse_typedef.cmd_buffer, CMD_TABLE[cmd_at].cmd_type) == 0)
                {
                    find_cmd_Index = cmd_at ;
                    break ;
                }
                else
                    find_cmd_Index = -1 ;
            }
            if(-1 == find_cmd_Index)
                printf("当前指令列表无该指令\n");
            else
                printf("接收到指令%d:%s\n", find_cmd_Index, cmd_parse_typedef.cmd_buffer);
            Command_Matching((char *)cmd_parse_typedef.cmd_buffer);
            memset(&cmd_parse_typedef, 0, sizeof(cmd_parse_typedef));
        }
    }
    /* USER CODE END 3 */
}

stm32l4xx_it.h

#include <stdint.h>
#define CMD_STR_SIZE 30
/*串口接收结构体*/
typedef struct __CMD
{
        /*接收计数*/
  int rx_count ;
        /*接收单个字符*/
  uint8_t Res ;
        /*是否已经接收完成?*/
  uint8_t BufferReady  : 1 ;
        /*数据缓存区*/
  uint8_t cmd_buffer[CMD_STR_SIZE] ;
        /*数据备份缓存区*/
  uint8_t cmd_buffer_temp[CMD_STR_SIZE] ;
}CMDSTR_PARSE ; 
extern CMDSTR_PARSE  cmd_parse_typedef ;

stm32l4xx_it.c

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *UartHandle)
{
    if(UartHandle->Instance == USART1)
    {
        HAL_UART_Receive_IT(&huart1, (uint8_t *)&cmd_parse_typedef.Res, 1);
        if('\n' != cmd_parse_typedef.Res)
        {
            if(cmd_parse_typedef.rx_count < CMD_STR_SIZE - 1)
                cmd_parse_typedef.cmd_buffer_temp[cmd_parse_typedef.rx_count++] = cmd_parse_typedef.Res ;
            else
                cmd_parse_typedef.rx_count = 0 ;
        }
        else
        {
            //如果接收的是\n,则上一个接收的数据为'\r'结束
            if('\r' == cmd_parse_typedef.cmd_buffer_temp[cmd_parse_typedef.rx_count - 1])
            {
                //添加结束符
                cmd_parse_typedef.cmd_buffer_temp[cmd_parse_typedef.rx_count - 1] = 0x00 ;
                memcpy(cmd_parse_typedef.cmd_buffer, cmd_parse_typedef.cmd_buffer_temp, CMD_STR_SIZE);
                cmd_parse_typedef.BufferReady = 1;
            }
        }
    }
}
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    /*如果发生了串口溢出,则清溢出标志后再次开启终端接收*/
    if(__HAL_UART_GET_FLAG(huart, UART_FLAG_ORE) != RESET)
    {
        memset(&cmd_parse_typedef, 0, sizeof(cmd_parse_typedef));
        __HAL_UART_CLEAR_OREFLAG(huart);
        HAL_UART_Receive_IT(&huart1, (uint8_t *)&cmd_parse_typedef.Res, 1);
    }
}

案例一中,软件逻辑实现较为简单,即通过串口中断接收协议数据,然后通过遍历调用对应的函数执行相应的逻辑,但是如果想把项目做得更具可复用性的话,我们还需要改改代码,让它更低耦合。


执行效果:

640.png

案例二

基于案例一,我们对代码做一些升华,修改main.c部分如下:


main.c

typedef void (*cmd_func)(void);
typedef struct __CMD_PARSE
{
    const char *cmd_type;
    const char *cmd_help;
    cmd_func  fun_ptr;
} CMD_PARSE;
CMD_PARSE CMD_TABLE[CMD_SIZE];
#define NR(array) sizeof(array) / sizeof(array[0])
//初始化命令,这里注册了一个默认的list_cmd命令,用于查询当前结构体中所有注册的指令
void Cmd_init(void)
{
    Register_Cmd("list_cmd", "list all cmd info", list_cmd_callback);
}
//注册命令
/*
  cmd:指令
  cmd_help:指令功能描述
  ptr:该指令对应的执行函数
*/
int Register_Cmd(char *cmd, char *cmd_help, cmd_func ptr)
{
    static uint8_t cmd_index = 0 ;
    if(cmd_index > CMD_SIZE - 1)
    {
        printf("注册命令失败,表越界\n");
        return -1 ;
    }
    CMD_TABLE[cmd_index].cmd_type = cmd ;
    CMD_TABLE[cmd_index].cmd_help = cmd_help ;
    CMD_TABLE[cmd_index].fun_ptr = ptr ;
    cmd_index++ ;
    return cmd_index ;
}
/*命令匹配*/
int Command_Matching(char *cmd_type)
{
    uint8_t cmd_at = 0 ;
    uint8_t cmd_match_flag = 0 ;
    uint8_t type_num = NR(CMD_TABLE);
    for(cmd_at = 0 ; cmd_at < type_num ; cmd_at++)
    {
        if(0 == strcmp(CMD_TABLE[cmd_at].cmd_type, cmd_type))
        {
            cmd_match_flag = 1 ;
            break ;
        }
    }
    if(1 == cmd_match_flag)
    {
        cmd_match_flag = 0 ;
        CMD_TABLE[cmd_at].fun_ptr();
    }
    else
        return -1 ;
    return 0 ;
}
int main(void)
{
    /* USER CODE BEGIN 1 */
    uint8_t cmd_at = 0 ;
    int find_cmd_Index = 0;
    /* USER CODE END 1 */
    /* MCU Configuration--------------------------------------------------------*/
    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();
    /* USER CODE BEGIN Init */
    /* USER CODE END Init */
    /* Configure the system clock */
    SystemClock_Config();
    /* USER CODE BEGIN SysInit */
    /* USER CODE END SysInit */
    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    /* USER CODE BEGIN 2 */
    //开启串口接收中断
    HAL_UART_Receive_IT(&huart1, (uint8_t *)&cmd_parse_typedef.Res, 1);
    //初始化list_cmd
    Cmd_init();
    //注册指令
    Register_Cmd("led_on",   "Open  BearPi IA1 LED", led_on_process);
    Register_Cmd("led_off",  "Close BearPi IA1 LED", led_off_process);
    Register_Cmd("motor_on", "Open  BearPi IA1 MOTOR", motor_on_process);
    Register_Cmd("motor_off", "Close BearPi IA1 MOTOR", motor_off_process);
    //开机上电即执行list_cmd,打印当前已经注册的所有指令
    list_cmd_callback();
    /* USER CODE END 2 */
    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
        if(1 == cmd_parse_typedef.BufferReady)
        {
            for(cmd_at = 0 ; cmd_at < NR(CMD_TABLE) ; cmd_at++)
            {
                if(strcmp((char *)cmd_parse_typedef.cmd_buffer, CMD_TABLE[cmd_at].cmd_type) == 0)
                {
                    find_cmd_Index = cmd_at ;
                    break ;
                }
                else
                    find_cmd_Index = -1 ;
            }
            if(-1 == find_cmd_Index)
                printf("当前指令列表无该指令\n");
            else
                printf("当前输入:%s指令\n", cmd_parse_typedef.cmd_buffer);
            Command_Matching((char *)cmd_parse_typedef.cmd_buffer);
            memset(&cmd_parse_typedef, 0, sizeof(cmd_parse_typedef));
        }
    }
    /* USER CODE END 3 */
}

针对案例一的修改以后,不用再直接修改命令表CMD_TABLE里的数据,而是通过一个函数,调用一次则自动往后增加一个指令,这样用起来显然会比较舒服一些,我自己项目也经常会这么用,在不考虑代码执行性能的条件下相当灵活。


运行结果:

640.png

640.png

案例三

一个超牛逼的命令解析器:cmd-parser由物联网大佬杰杰所造,他也是我们开源以及嵌入式社区的朋友,不得不说这个解析器做得真香!

640.png

以下是他的Github,有兴趣的朋友也可以关注一下,杰杰在开源软件方面在同龄人里做得东西都相当出色,大家要多多向他学习!

Github仓库地址

https://github.com/jiejieTop/cmd-parser

解析器功能

简单来说,我希望我的开发板,可以通过命令执行一些处理,比如说我用串口发一个命令A,开发板就执行A的一些处理,或者,在调试某些AT模组的时候,当我收到模组返回的一些指令后,自动执行一些处理。当然,还有其他的地方可以用得上的,兄弟们自行挖掘!!

解析器特色

  • 用户无需关心命令的存储区域与大小,由编译器静态分配。
  • 加入哈希算法超快速匹配命令,时间复杂度从O(n*m)变为O(n)。
  • 命令支持忽略大小写。
  • 非常易用与非常简洁的代码(不足150行)。

使用方法

1、注册命令 在工程中的任意位置均可调用(在函数外)

REGISTER_CMD(test1, test1_cmd);

2、cmd初始化

cmd_init();

3、解析命令

cmd_parsing("test1");

目前本代码只支持MDK与IAR的编译器,对于GCC还没有移植,不过我想要移植也不困难!欢迎大家一起提交pr!

1、在小熊派上使用cmd-parser

1.1 添加头文件及路径到Keil MDK

640.png

640.png

640.png

640.png

1.2、编写源代码

这里还是一样,借用案例一的工程,对main.c做下改造。


main.c

#include "cmd.h"
void led_on_process(void);
void led_off_process(void);
void motor_on_process(void);
void motor_off_process(void);
/*注册命令*/
REGISTER_CMD(led_on, led_on_process);
REGISTER_CMD(led_off, led_off_process);
REGISTER_CMD(motor_on, motor_on_process);
REGISTER_CMD(motor_off, motor_off_process);
int main(void)
{
    /* USER CODE BEGIN 1 */
    /* USER CODE END 1 */
    /* MCU Configuration--------------------------------------------------------*/
    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();
    /* USER CODE BEGIN Init */
    /* USER CODE END Init */
    /* Configure the system clock */
    SystemClock_Config();
    /* USER CODE BEGIN SysInit */
    /* USER CODE END SysInit */
    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    /* USER CODE BEGIN 2 */
    /*命令初始化*/
    cmd_init();
    HAL_UART_Receive_IT(&huart1, (uint8_t *)&cmd_parse_typedef.Res, 1);
    /* USER CODE END 2 */
    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
        if(1 == cmd_parse_typedef.BufferReady)
        {
            printf("接收到指令:%s\n", cmd_parse_typedef.cmd_buffer);
            cmd_parsing((char *)cmd_parse_typedef.cmd_buffer);
            memset(&cmd_parse_typedef, 0, sizeof(cmd_parse_typedef));
        }
    }
    /* USER CODE END 3 */
}

有木有很轻量?看起来简直舒服爆啦!


执行结果:

640.png

当然,除了杰杰开源的cmd-parser,还有很多优秀的指令解析器,比如RT-Thread的finsh,还有比如世伟兄之前发的一期项目源码分析的letter-shell,原理都差不多:


第2期 | letter-shell,一个功能强大的嵌入式shell


这些都是非常优秀的作品,大家都可以学习使用下,有那么好用的轮子为啥不用?所以咱们在工作中要避免重复造轮子,这样才能提高工作效率,做出漂亮的产品!

项目下载

公众号后台回复:命令 即可获取这几个案例的下载链接。

往期精彩

华为LiteOS智慧路灯项目案例学习笔记(一)


最近收集的开源项目专栏(持续更新,收好车轮,方便造车)


推荐三个我工作中经常使用的驱动大全wiki(建议收藏并转发让更多人知道!)

目录
相关文章
|
4月前
|
安全 Linux 应用服务中间件
Linux命令show-installed的深入解析
`show-installed`是Linux中一个假设的命令,模拟显示已安装软件包的功能。它结合了`apt`、`yum`等包管理器的特性,提供跨发行版的兼容性,展示包的名称、版本、安装时间和来源。可用参数如`-n`过滤名称,`-v`显示版本,`-s`显示来源,`-t`显示时间。注意需root权限运行,大系统中可能影响性能。最佳实践包括定期审查、使用过滤、结合其他命令和备份数据。
|
1月前
|
网络协议 开发工具 C语言
Jetson错误(二):wget命令提示无法解析主机地址的问题解决
对于解决在NVIDIA Jetson平台上使用wget命令时出现的无法解析主机地址的问题,提供了两种解决方法:一种是临时修改DNS服务器为Google的公共DNS,另一种是永久修改DNS设置。
110 5
|
2月前
|
存储 缓存 NoSQL
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
71 0
|
3月前
|
图形学 开发者 存储
超越基础教程:深度拆解Unity地形编辑器的每一个隐藏角落,让你的游戏世界既浩瀚无垠又细节满满——从新手到高手的全面技巧升级秘籍
【8月更文挑战第31天】Unity地形编辑器是游戏开发中的重要工具,可快速创建复杂多变的游戏环境。本文通过比较不同地形编辑技术,详细介绍如何利用其功能构建广阔且精细的游戏世界,并提供具体示例代码,展示从基础地形绘制到植被与纹理添加的全过程。通过学习这些技巧,开发者能显著提升游戏画面质量和玩家体验。
159 3
|
3月前
|
数据采集 运维 监控
运维笔记:流编辑器sed命令用法解析
运维笔记:流编辑器sed命令用法解析
59 5
|
3月前
|
Linux 数据安全/隐私保护 Perl
解锁Linux高手秘籍:文件操作+命令解析大揭秘,面试场上让你光芒万丈,技术实力惊艳四座!
【8月更文挑战第5天】Linux作为服务器与嵌入式系统的基石,其文件管理和命令行操作是技术人员必备技能。本文从文件操作和基础命令两大方面,深入浅出地解析Linux核心要义,助你在面试中脱颖而出。首先探索文件系统的树状结构及操作,包括使用`ls -la`浏览文件详情、`touch`创建文件、`rm -r`慎删目录、`cp`与`mv`复制移动文件、以及利用`find`搜索文件。接着掌握命令行技巧,如用`cat`、`more`和`less`查看文件内容;借助`grep`、`sed`与`awk`处理文本;运用`ps`、`top`和`kill`管理进程;并通过`chmod`和`chown`管理文件权限。
79 8
|
3月前
|
运维 Ubuntu Shell
Docker命令宝典:解锁容器化技术的无限可能,从镜像管理到容器操作,全面解析与实战指南!
【8月更文挑战第3天】Docker简化了应用的部署与运行,掌握其基本命令对开发者和运维人员至关重要。通过`docker images`可查看本地镜像;使用`docker pull`拉取如最新版Ubuntu镜像;`docker rmi`用于删除不再需要的镜像。运行容器可通过`docker run`命令,结合`-it`等选项提供交互式环境。`docker ps`显示运行中的容器,加上`-a`则列出所有容器。`docker stop`和`docker start`分别用于停止和重启容器,而`docker rm`则删除容器。
84 5
|
4月前
|
Linux Docker 容器
ip addr命令解析
ip addr命令解析
573 1
|
3月前
|
运维 Rust 监控
Linux高效运维必备:fd命令深度解析,文件描述符管理从此得心应手!
【8月更文挑战第23天】本文介绍了一款名为fd的命令行工具,该工具基于Rust语言开发,旨在以更直观的语法和更快的速度替代传统的`find`命令。通过本文,您可以了解到如何安装fd以及一些基本用法示例,比如使用正则表达式匹配文件名、排除特定目录等。此外,文章还展示了如何结合`ps`和`lsof`命令来查找特定文件并显示其文件描述符,从而帮助您更好地管理和监控Linux系统中的文件与进程。
136 0
|
4月前
|
Linux
《解析 Linux 命令:systemd-delta》
`systemd-delta`: 解析Linux服务配置差异。概览: 显示服务单元文件与默认配置的对比,助于配置问题排查与系统审计。特点: 清晰展示修改点,涵盖启动选项等。示例: `systemd-delta [--plain] &lt;service&gt;`. 注意: 理解默认配置,谨慎修改,定期检查。掌握此命令,深化系统服务配置洞察,优化Linux管理。#Linux #systemd-delta
下一篇
无影云桌面