精华内容
下载资源
问答
  • IAR移植letter shell

    2021-03-08 14:57:36
    体验过rtt的finsh组件后,总想在裸机工程中跑一个shell来调试程序,于是找到了一个功能强大的嵌入式shell组件,letter shell。点击查看源码 这里使用基础版本的2.0。更多内容参考源码中README.md文件 一、添加源码...

    简介

    体验过rtt的finsh组件后,总想在裸机工程中跑一个shell来调试程序,于是找到了一个功能强大的嵌入式shell组件,letter shell点击查看源码
    这里使用基础版本的2.0。更多内容参考源码中README.md文件
    显示

    一、添加源码到工程

    添加目录

    二、添加section =“shellCommand”

    不添加提示报错 Error[Pa053]: section/block has not been declared using #pragma section/segment
    shell.c中添加如下内容

    #if defined(__ICCARM__)
    #pragma section ="shellCommand"
    #endif
    

    三、更改.icf文件

    添加一行 keep { section shellCommand };
    (我也不太懂这里的作用是什么,我是看rtt里有使用这个,就参照着写了一句,试了下可以,后面看了下git源码的issue区,发现作者有相关的回复。如果有大神理解keep的作用,还请评论指点一下,方便后来人学习。)
    添加keep现在就能使用基本的

    四、初始化shell

    static SHELL_TypeDef shell;
    /**
     * @brief  初始化 shell
     */
    void shell_init(void)
    {
    	shell.read = segger_read;
        shell.write = segger_write;
        shellInit(&shell);
    }
    

    按照如下shell的read,write类型对接函数接口
    read:typedef signed char (*shellRead)(char *);
    write:typedef void (*shellWrite)(const char);

    五、调用shellTask

    main中调用shellTask(&shell);
    裸机需要关闭宏:SHELL_TASK_WHILE。

    * 命令导出

    写了个测试函数

    int test_func(int argc, char *agrv[])
    {
        SEGGER_RTT_printf(0, "hello word\n");
    	SEGGER_RTT_printf(0, "argc:%d\n", argc);
    	SEGGER_RTT_printf(0, "argv[0]:%s\n", agrv[1]);
    	return 1;
    }
    SHELL_EXPORT_CMD(run_test, test_func, test);
    

    查看帮助 help
    heklp

    运行测试函数 run_test fine
    run_test
    大功告成!!!

    展开全文
  • letter shell确实挺方便的,但是在使用过程中还是遇到一些问题: 我测试的MCU位STM32L031F6 主频位最大32MHz 时问问能正常使用, 但是把主频调到4MHz是用些功能不能使用了,方向键不能用了,具体现象是,发出的命令...

    letter shell确实挺方便的,但是在使用过程中还是遇到一些问题:
    我测试的MCU位STM32L031F6 主频位最大32MHz 时问问能正常使用,
    但是把主频调到4MHz是用些功能不能使用了,方向键不能用了,具体现象是,发出的命令不执行了
    就是这个Shell功能不能用了
    还有一个问题就是输入命令长了,控件也失效了

    展开全文
  • STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell 对应的代码
  • STM32CubeMX Nucleo F767ZI 教程(1) STM32CubeMX Nucleo F767ZI...文章目录STM32CubeMX Nucleo F767ZI 教程(1)前言一、FreeRTOS配置1.FreeRTOS二、移植Letter Shell三、Letter Shell的使用3.1 定义shell对象3.2 定义she

    STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell

    STM32CubeMX Nucleo F767ZI 教程(1)
    STM32CubeMX Nucleo F767ZI 教程(2)
    STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell



    前言

    一般使用串口来打印调试信息之类的,正点原子的USMART也不错,这边引入了一个类似于Linux 命令行的调试功能,Letter Shell,功能很强大,github的链接在下面。
    https://github.com/mcujackson/letter-shell
    我们要移植一下这个功能,方便后面的调试。这个功能使用到了串口,接收到串口数据后回传到上位机,直到接收到命令行回车触发,也就是0x0A(LF 换行)以及0x0D(CR 回车),不同的软件,结束符可能也有点区别。确认收到完整的命令,就可以解析命令,根据不同函数的地址就可以进行调用了,这样子调试程序就很方便了。
    效果图如下:
    在这里插入图片描述


    一、FreeRTOS配置

    1.FreeRTOS

    Letter Shell 仅需要用到串口,然后使用shell 的回调命令,就可以使用了,但是后续我们需要使用到FreeRTOS,这就涉及到中断优先级的问题了,STM32CubeMX有16个优先级,0为最高优先级,15为最低优先级,FreeRTOS可以管理管理包括优先级5-16的中断,我们这里先配置一下FreeRTOS,暂时不需要其他功能,默认配置有一个defaultTask的线程,我们用这个来控制LED以指示系统的正常运行,所以这个按照默认配置就可以了。

    在这里插入图片描述
    FreeRTOS需要使用时钟基,默认是SysTick,滴答定时器,默认配置是1ms中断一次,然后计时器+1,但是SysTick并不是很准确,我们可以在SYS 选项卡中改一下Timebase Source,这里我们改为定时器3当时钟基。
    在这里插入图片描述
    然后配置一下串口,因为Nucleo板载了ST-LINK V2.1,有一个虚拟串口连接到USART3,所以使能USART3,然后使能中断就可以了。点击GENERATE CODE生成代码。
    在这里插入图片描述
    先打开 “freertos.c”,里面有一个默认的线程,StartDefaultTask,我们在这里加一个LED闪烁的功能,先测试一下生成的工程没问题。

    /* USER CODE BEGIN Header_StartDefaultTask */
    /**
      * @brief  Function implementing the defaultTask thread.
      * @param  argument: Not used
      * @retval None
      */
    /* USER CODE END Header_StartDefaultTask */
    void StartDefaultTask(void *argument)
    {
      /* USER CODE BEGIN StartDefaultTask */
      /* Infinite loop */
      for(;;)
      {
        HAL_GPIO_TogglePin(LD3_GPIO_Port,LD3_Pin);
        osDelay(100);
      }
      /* USER CODE END StartDefaultTask */
    }
    

    注意,配置的Keil工程,默认是没有设置下载完自动复位,我们需要按一下Nucleo 板子上面黑色的复位按钮,另外默认的配置下,有一个以太网的初始化 “MX_ETH_Init()”,需要接入网线到路由器,不然初始化不通过,会跳转到Error_Handler();

    if (HAL_ETH_Init(&heth) != HAL_OK)
    {
      Error_Handler();
    }
    

    二、移植Letter Shell

    下载github上面的源代码,解压出来。
    在这里插入图片描述

    demo 有一些平台上面的例程
    doc 是功能演示
    extensions 有一些扩展功能
    src 是源代码
    tools 是一个用于遍历工程中命令导出的工具,位于tools/shellTools.py,需要python3环境运行,可以列出工程中,所有使用SHELL_EXPORT_XXX导出的命令名,以及位置,结合VS Code可以直接进行跳转

    我们在STM32Cube 的工程项目下新建一个文件夹 Shell,然后把 src文件夹下的源代码都放进去,遵守开源公约,LICENSE也要记得放进去。放进去之后,demo\stm32-freertos 目录下还有三个文件,我们也拷贝过去覆盖即可。
    在这里插入图片描述
    然后在keil工程下添加这些文件。
    在这里插入图片描述
    编译一下,此时会报错

    ..\Shell\shell_cfg.h(15): error:  #5: cannot open source input file "stm32f4xx_hal.h": No such file or directory
    

    原因是letter shell的demo使用的是F4的板子, “shell_cfg.h”,里面包含了f4的头文件,#include “stm32f4xx_hal.h” 。我们这是 F7的板子,并且是STM32CubeMX生成的工程文件,所以我们只要替换成 #include “main.h”,这样改了一次以后,以后换芯片系列也能很方便的移植了。再次编译,仍然会报错。

    ..\Shell\shell_port.c(13): error:  #5: cannot open source input file "serial.h": No such file or directory
    

    这是因为 “shell_port.c” 中有 serial.h,看名字,这是与串口相关的文件,我们把这种不需要的头文件删掉,只留下两个头文件就可以了。

    #include "shell.h"
    #include "usart.h"
    

    下面两个函数是shell读写的实现,里面使用的是固件库的函数。

    /**
     * @brief 用户shell写
     * 
     * @param data 数据
     */
    void userShellWrite(char data)
    {
        serialTransmit(&debugSerial, (uint8_t *)&data, 1, 0xFF);
    }
    
    /**
     * @brief 用户shell读
     * 
     * @param data 数据
     * @return char 状态
     */
    signed char userShellRead(char *data)
    {
        if (serialReceive(&debugSerial, (uint8_t *)data, 1, 0) == 1)
        {
            return 0;
        }
        else
        {
            return -1;
        }
        
    }
    

    既然使用了STM32CubeMX来生成工程,这里我们改成HAL库的形式。

    /**
     * @brief 用户shell写
     * 
     * @param data 数据
     */
    void userShellWrite(char data)
    {
      HAL_UART_Transmit(&huart3,(uint8_t *)&data, 1,1000);
    }
    
    
    /**
     * @brief 用户shell读
     * 
     * @param data 数据
     * @return char 状态
     */
    signed char userShellRead(char *data)
    {
      if(HAL_UART_Receive(&huart3,(uint8_t *)data, 1, 0) == HAL_OK)
      {
          return 0;
      }
      else
      {
          return -1;
      }
    }
    

    再次编译就不会出错了。

    三、Letter Shell的使用

    github 上有具体的使用说明。我这里是下载的最新版本,3.0.6,这里也是根据这个版本进行一些解释性的说明。

    3.1 定义shell对象

    在 “shell_port.c” 中定义了 shell 的对象。这个Shell结构体定义了历史的输入,读写对象的函数指针等等。

    Shell shell;
    

    3.2 定义shell读写函数

    定义shell读,写函数,函数原型如下:

    /**
     * @brief shell读取数据函数原型
     *
     * @param char shell读取的字符
     *
     * @return char 0 读取数据成功
     * @return char -1 读取数据失败
     */
    typedef signed char (*shellRead)(char *);
    
    /**
     * @brief shell写数据函数原型
     *
     * @param const char 需写的字符
     */
    typedef void (*shellWrite)(const char);
    

    我们在上面已经进行了修改。

    3.3 申请缓存区

    申请一片缓冲区

    char shellBuffer[512];
    

    3.4 调用shellInit进行初始化

    调用shellInit进行初始化,函数实现如下:

    /**
     * @brief 用户shell初始化
     * 
     */
    void userShellInit(void)
    {
        shell.write = userShellWrite;
        shell.read = userShellRead;
        shellInit(&shell, shellBuffer, 512);
    }
    

    3.5 初步完成shell的初始化

    以上步骤,除了要修改shell的读写函数,其他的在shell_port.c中都已经实现了,所以我们在main.c 添加头文件

    /* USER CODE BEGIN Includes */
    #include "shell_port.h"
    /* USER CODE END Includes */
    

    然后在 main 函数中初始化shell,基本上就移植完毕了。

     /* USER CODE BEGIN 2 */
      userShellInit();    //letter shell 的初始化
      /* USER CODE END 2 */
    

    此时编译一下,还是报错,老样子,是因为没有 “serial.h” 文件,我们双击报错点,跳到异常点,把这个注释掉即可。然后再次编译,就没有报错了。

    ..\Shell\shell_port.h(15): error:  #5: cannot open source input file "serial.h": No such file or directory
    

    我们把程序下载到Nucleo。
    然后打开SecureCRT 或者 MobaXterm 或者 “putty”,建议是使用MobaXterm,因为这个功能强大,且有免费试用版。

    我们需要建立一个串口连接, SecureCRT 点击快速连接, MobaXterm 点击 Session ,然后找到串口的选项,串口的端口就是 STLINK-V2.1的虚拟串口, 波特率为115200,其他的默认不变,点击 OK
    在这里插入图片描述
    由于我们还没编写串口接收方面的函数,按下字符是不会进行回传的,这里我们先按一下复位按钮,接上网线等到初始化完成后,就可以看到 letter shell 初始化完成后的串口信息。

    在这里插入图片描述

    四、Letter Shell的进一步了解

    第三节已经完成了Letter Shell的初始化,在这一步,已经能够shell初始化已经打印的功能,但是要想使用好letter shell,我们还需要对letter shell有进一步的了解。

    4.1 宏定义

    在文件 “shell_cfg.h” 中定义了各种各样的宏,用户可以根据不同的需求来配置宏。
    下面的表格列出了各个宏的意义。

    意义
    SHELL_TASK_WHILE 是否使用默认shell任务while循环
    SHELL_USING_CMD_EXPORT 是否使用命令导出方式
    SHELL_HELP_LIST_USER 是否在输入命令列表中列出用户
    SHELL_HELP_LIST_VAR 是否在输入命令列表中列出变量
    SHELL_HELP_LIST_KEY 是否在输入命令列表中列出按键
    SHELL_ENTER_LF 使用LF作为命令行回车触发
    SHELL_ENTER_CR 使用CR作为命令行回车触发
    SHELL_ENTER_CRLF 使用CRLF作为命令行回车触发
    SHELL_COMMAND_MAX_LENGTH shell命令最大长度
    SHELL_PARAMETER_MAX_NUMBER shell命令参数最大数量
    SHELL_HISTORY_MAX_NUMBER 历史命令记录数量
    SHELL_DOUBLE_CLICK_TIME 双击间隔(ms)
    SHELL_MAX_NUMBER 管理的最大shell数量
    SHELL_GET_TICK() 获取系统时间(ms)
    SHELL_SHOW_INFO 是否显示shell信息
    SHELL_CLS_WHEN_LOGIN 是否在登录后清除命令行
    SHELL_DEFAULT_USER shell默认用户
    SHELL_DEFAULT_USER_PASSWORD 默认用户密码
    SHELL_LOCK_TIMEOUT shell自动锁定超时

    在文件 “shell_cfg.h” 也有具体的说明。

    /**
     * @brief 是否使用默认shell任务while循环,使能宏`SHELL_USING_TASK`后此宏有意义
     *        使能此宏,则`shellTask()`函数会一直循环读取输入,一般使用操作系统建立shell
     *        任务时使能此宏,关闭此宏的情况下,一般适用于无操作系统,在主循环中调用`shellTask()`
     */
    #define     SHELL_TASK_WHILE            1
    
    /**
     * @brief 是否使用命令导出方式
     *        使能此宏后,可以使用`SHELL_EXPORT_CMD()`等导出命令
     *        定义shell命令,关闭此宏的情况下,需要使用命令表的方式
     */
    #define     SHELL_USING_CMD_EXPORT      1
    
    /**
     * @brief 是否使用shell伴生对象
     *        一些扩展的组件(文件系统支持,日志工具等)需要使用伴生对象
     */
    #define     SHELL_USING_COMPANION       0
    
    /**
     * @brief 支持shell尾行模式
     */
    #define     SHELL_SUPPORT_END_LINE      0
    
    /**
     * @brief 是否在输出命令列表中列出用户
     */
    #define     SHELL_HELP_LIST_USER        0
    
    /**
     * @brief 是否在输出命令列表中列出变量
     */
    #define     SHELL_HELP_LIST_VAR         0
    
    /**
     * @brief 是否在输出命令列表中列出按键
     */
    #define     SHELL_HELP_LIST_KEY         0
    
    /**
     * @brief 是否在输出命令列表中展示命令权限
     */
    #define     SHELL_HELP_SHOW_PERMISSION  1
    
    /**
     * @brief 使用LF作为命令行回车触发
     *        可以和SHELL_ENTER_CR同时开启
     */
    #define     SHELL_ENTER_LF              0
    
    /**
     * @brief 使用CR作为命令行回车触发
     *        可以和SHELL_ENTER_LF同时开启
     */
    #define     SHELL_ENTER_CR              0
    
    /**
     * @brief 使用CRLF作为命令行回车触发
     *        不可以和SHELL_ENTER_LF或SHELL_ENTER_CR同时开启
     */
    #define     SHELL_ENTER_CRLF            1
    
    /**
     * @brief 使用执行未导出函数的功能
     *        启用后,可以通过`exec [addr] [args]`直接执行对应地址的函数
     * @attention 如果地址错误,可能会直接引起程序崩溃
     */
    #define     SHELL_EXEC_UNDEF_FUNC       0
    
    /**
     * @brief shell命令参数最大数量
     *        包含命令名在内,超过8个参数并且使用了参数自动转换的情况下,需要修改源码
     */
    #define     SHELL_PARAMETER_MAX_NUMBER  8
    
    /**
     * @brief 历史命令记录数量
     */
    #define     SHELL_HISTORY_MAX_NUMBER    5
    
    /**
     * @brief 双击间隔(ms)
     *        使能宏`SHELL_LONG_HELP`后此宏生效,定义双击tab补全help的时间间隔
     */
    #define     SHELL_DOUBLE_CLICK_TIME     200
    
    /**
     * @brief 管理的最大shell数量
     */
    #define     SHELL_MAX_NUMBER            5
    
    /**
     * @brief shell格式化输出的缓冲大小
     *        为0时不使用shell格式化输出
     */
    #define     SHELL_PRINT_BUFFER          128
    
    /**
     * @brief 获取系统时间(ms)
     *        定义此宏为获取系统Tick,如`HAL_GetTick()`
     * @note 此宏不定义时无法使用双击tab补全命令help,无法使用shell超时锁定
     */
    #define     SHELL_GET_TICK()            HAL_GetTick()
    
    /**
     * @brief shell内存分配
     *        shell本身不需要此接口,若使用shell伴生对象,需要进行定义
     */
    #define     SHELL_MALLOC(size)          0
    
    /**
     * @brief shell内存释放
     *        shell本身不需要此接口,若使用shell伴生对象,需要进行定义
     */
    #define     SHELL_FREE(obj)             0
    
    /**
     * @brief 是否显示shell信息
     */
    #define     SHELL_SHOW_INFO             1
    
    /**
     * @brief 是否在登录后清除命令行
     */
    #define     SHELL_CLS_WHEN_LOGIN        1
    
    /**
     * @brief shell默认用户
     */
    #define     SHELL_DEFAULT_USER          "letter"
    
    /**
     * @brief shell默认用户密码
     *        若默认用户不需要密码,设为""
     */
    #define     SHELL_DEFAULT_USER_PASSWORD ""
    
    /**
     * @brief shell自动锁定超时
     *        shell当前用户密码有效的时候生效,超时后会自动重新锁定shell
     *        设置为0时关闭自动锁定功能,时间单位为`SHELL_GET_TICK()`单位
     * @note 使用超时锁定必须保证`SHELL_GET_TICK()`有效
     */
    #define     SHELL_LOCK_TIMEOUT          0 * 60 * 1000
    

    下面介绍一下比较重要的几个宏:

    4.2 SHELL_TASK_WHILE

    这个宏仅在这里面生效,这是在使用操作系统的时候使用到的,它会一直轮询地读取串口数据,然后调用 shellHandler 解析串口数据。这仅仅是个范例,因为这里没有进行任务切换,所以会一直堵塞在这里,而我们如果要使用FreeRTOS创建一个线程来处理这个任务,需要使用osDelay来进行切换。因为这里一次只读取一个字节,所以如果任务切换间隔过大,可能会丢失某些数据,另外,因为接收串口的时候要进行回显,所以任务间隔过大,也会影响操作体验,所以我们在串口接收中断调用 shellHandler 进行处理。我们不需要在线程中一直读取,所以这个宏 SHELL_TASK_WHILE 设为0,但是设为1也不影响就是了。

    /**
     * @brief shell 任务
     * 
     * @param param 参数(shell对象)
     * 
     */
    void shellTask(void *param)
    {
        Shell *shell = (Shell *)param;
        char data;
    #if SHELL_TASK_WHILE == 1
        while(1)
        {
    #endif
            if (shell->read && shell->read(&data) == 0)
            {
                shellHandler(shell, data);
            }
    #if SHELL_TASK_WHILE == 1
        }
    #endif
    }
    

    4.3 SHELL_USING_CMD_EXPORT

    命令导出功能,通过这个宏,我们就可以在命令行中输入指定函数的名称以及传递函数,就可以直接调用该函数,用于调试,十分方便,所以这个宏不变,默认为1.

    4.4 SHELL_USING_COMPANION

    作者定义的伴生对象,用于一些扩展的组件(文件系统支持,日志工具等),但是目前这个功能还不是很完善,所以这个宏默认设为0,不启用。

    4.5 SHELL_ENTER_CRLF

    这个与 SHELL_ENTER_LF 以及 SHELL_ENTER_CR 互斥,作为命令行回车触发,使用的SecureCRT 以及 MobaXterm 是使用 CR 作为命令行回车触犯,所以这三个的配置为

    #define     SHELL_ENTER_LF              0
    #define     SHELL_ENTER_CR              1
    #define     SHELL_ENTER_CRLF            0
    

    4.6 SHELL_PRINT_BUFFER

    这个宏定义了格式化输出的大小,在 shellPrint 中使用到,使用方法和 printf 类似,只是注意不要超过缓存大小。
    这个宏也是按照默认值就可以了,也可以根据自己的需求进行修改。

    4.7 其他的宏

    其他的宏理解起来很容易,这里也就不进行细说了。
    最终宏的定义如下:

    #define     SHELL_TASK_WHILE            0
    #define     SHELL_USING_CMD_EXPORT      1
    #define     SHELL_USING_COMPANION       0
    #define     SHELL_USING_COMPANION       0
    #define     SHELL_SUPPORT_END_LINE      0
    #define     SHELL_HELP_LIST_USER        0
    #define     SHELL_HELP_LIST_VAR         0
    #define     SHELL_HELP_LIST_KEY         0
    #define     SHELL_HELP_SHOW_PERMISSION  1
    #define     SHELL_ENTER_LF              0
    #define     SHELL_ENTER_CR              1
    #define     SHELL_ENTER_CRLF            0
    #define     SHELL_EXEC_UNDEF_FUNC       0
    #define     SHELL_PARAMETER_MAX_NUMBER  8
    #define     SHELL_HISTORY_MAX_NUMBER    5
    #define     SHELL_DOUBLE_CLICK_TIME     200
    #define     SHELL_MAX_NUMBER            5
    #define     SHELL_PRINT_BUFFER          128
    #define     SHELL_GET_TICK()            HAL_GetTick()
    #define     SHELL_MALLOC(size)          0
    #define     SHELL_FREE(obj)             0
    #define     SHELL_SHOW_INFO             1
    #define     SHELL_CLS_WHEN_LOGIN        1
    #define     SHELL_DEFAULT_USER          "letter"
    #define     SHELL_DEFAULT_USER_PASSWORD ""
    #define     SHELL_LOCK_TIMEOUT          0 * 60 * 1000
    

    4.8 shell的接收处理

    要正确的将串口接收到的数据当参数传递给 shellHandler,先要确保打开串口的中断,HAL_UART_Receive_IT ,使用这个串口中断,需要定义接收到的buffer数组,以及接收buffer的大小。

    #define HAL_USART3_RXBUFFERSIZE         1
    uint8_t HAL_USART3_RxBuffer[HAL_USART3_RXBUFFERSIZE]; //HAL库使用的串口接收缓冲
    

    main函数中打开串口中断

    HAL_UART_Receive_IT(&huart3, (uint8_t *)HAL_USART3_RxBuffer, HAL_USART3_RXBUFFERSIZE);//使能串口中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量
    

    打开串口中断后,在串口中断中就能获取到串口数据了,在HAL库中,串口中断最终会回调 HAL_UART_RxCpltCallback,所以我们就在这个接收串口数据,然后调用 shellHandler 进行解析。

    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
      uint8_t udata;
      if(huart->Instance == USART3)      //串口3的接收部分
      {
        HAL_UART_Receive_IT(&huart3,(uint8_t *)HAL_USART3_RxBuffer, HAL_USART3_RXBUFFERSIZE);     //接收串口的缓存
        udata = HAL_USART3_RxBuffer[0];
        shellHandler(&shell, udata);
      }
    }
    

    编译后下载程序,复位,这时候输入cmd 命令,按下回车,就会列出可以支持的命令了。后续如果自定义了导出函数,也会在这里显示。到这里,就说明了串口的收发是正常的,shell 也移植成功了。
    在这里插入图片描述

    五、使用Letter Shell 进行调试程序

    如同上面的 cmds 命令,我们用户也可以自定义命令,在编写完一个函数之后,我们可以通过SHELL_EXPORT_CMD 导出该函数,这样我们就能通过命令行来调用这个命令执行,以达到调试的目的。
    letter shell 3.0同时支持两种形式的函数定义方式,形如main函数定义的func(int argc, char *agrv[])以及形如普通C函数的定义func(int i, char *str, …),两种函数定义方式适用于不同的场景。

    下面使用LED来举例。

    5.1 普通c函数的命令导出

    void LD1_ON(void)
    {
      HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);
    }
    
    SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_on, LD1_ON, LD1 ON);
    
    void LD1_OFF(void)
    {
      HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET);
    }
    
    SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_off, LD1_OFF, LD1 OFF);
    

    上面写了两个函数,顾名思义,LD1_ON是点亮LD1,LD1_OFF是熄灭LD1,SHELL_EXPORT_CMD 就是导出命令,各个参数分别是 命令属性命令名命令函数命令描述

    /**
         * @brief shell 命令定义
         * 
         * @param _attr 命令属性
         * @param _name 命令名
         * @param _func 命令函数
         * @param _desc 命令描述
         */
        #define SHELL_EXPORT_CMD(_attr, _name, _func, _desc) \
                const char shellCmd##_name[] = #_name; \
                const char shellDesc##_name[] = #_desc; \
                const ShellCommand \
                shellCommand##_name SECTION("shellCommand") =  \
                { \
                    .attr.value = _attr, \
                    .data.cmd.name = shellCmd##_name, \
                    .data.cmd.function = (int (*)())_func, \
                    .data.cmd.desc = shellDesc##_name \
                }
    

    5.1.1 命令属性

    命令属性 这部分在 shell.h 中进行定义,几个属性可以通过 " | " 来进行连接。属性有以下几种:

    /**
     * @brief shell 命令权限
     * 
     * @param permission 权限级别
     */
    #define     SHELL_CMD_PERMISSION(permission) \
                (permission & 0x000000FF)
    
    /**
     * @brief shell 命令类型
     * 
     * @param type 类型
     */
    #define     SHELL_CMD_TYPE(type) \
                ((type & 0x0000000F) << 8)
    
    /**
     * @brief 使能命令在未校验密码的情况下使用
     */
    #define     SHELL_CMD_ENABLE_UNCHECKED \
                (1 << 12)
    
    /**
     * @brief 禁用返回值打印
     */
    #define     SHELL_CMD_DISABLE_RETURN \
                (1 << 13)
    
    /**
     * @brief 只读属性(仅对变量生效)
     */
    #define     SHELL_CMD_READ_ONLY \
                (1 << 14)
    

    在上面的例子中,使用到的命令属性是

    SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN
    

    这个意思是这个命令任何用户均可使用、命令的类型是普通的C函数、执行完函数后,不在命令行上显示返回的结果

    5.1.2 命令名

    命令名 在命令行中,输入命令名就能执行对应的C函数,可以通过命令行的 cmds 命令进行查看
    在上面的例子中,实现了两个命令名,分别是

    led1_on
    led1_off
    

    5.1.3 命令函数

    命令函数 也就是我们在keil工程中,所编写的函数的函数名。
    比如在命令行中,我们输入 led1_on ,那么实际调用的函数是 LD1_ON,输入 led1_off,那么实际调用的函数是 LD1_OFF

    5.1.4 命令描述

    命令描述 命令描述出现在 cmds 命令中,它主要是简明扼要的说明这个函数的功能。

    5.1.5 实际效果

    在这里插入图片描述
    如图所示,在输入cmds命令之后,会打印出我们可执行的命令,这里是多出了两个命令,led1_on 以及 led1_off,我们输入对应的命令,就可以控制LED1的亮灭了。

    5.2 main函数的命令导出

    一般我们写的程序,用普通的c函数的命令导出即可,main函数也可以,使用上也比较类似。使用此方式,一个函数定义的例子如下:

    int func(int argc, char *agrv[])
    {
        printf("%dparameter(s)\r\n", argc);
        for (char i = 1; i < argc; i++)
        {
            printf("%s\r\n", argv[i]);
        }
    }
    SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN), func, func, test);
    

    终端调用

    letter:/$ func "hello world"
    2 parameter(s)
    hello world
    

    5.3 带参的命令导出

    我们编写一个带参的函数,命令行是以空格划分传递的参数。

    void LD1_ON_OFF_ms(uint16_t on_ms,uint16_t off_ms)
    {
      HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);    //点亮LD1
      HAL_Delay(on_ms);                                         //延时
      HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET);  //熄灭LD1
      HAL_Delay(off_ms);                                        //延时
      HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);    //点亮LD1
    }
    SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_on_off_ms, LD1_ON_OFF_ms, LD1 on ms);
    
    

    这个例子是控制LD1亮灭的时间,LD1首先点亮 on_ms 个ms,再熄灭 "off_ms"个ms,再重新点亮。
    输入以下命令,就能看到 LD1 先亮 1000ms,然后熄灭 100ms,最后又点亮的状态。

    letter:/$ led1_on_off_ms 1000 100
    

    这是输入一个数字的例子,也可以传递字符串,比如说我们要使用文件管理系统 FatFs 来管理文件,很多地方就要使用到字符串了,letter shell 支持伴生对象,里面可以扩展文件管理系统的功能。

    fs_support作为letter shell的插件,用于实现letter shell对常见文件系统操作的支持,比如说cd,ls等命令,fs_support依赖于letter shell的伴生对象功能,并且使用到了内存分配和内存释放,所以请确认已经配置好了letter shell

    fs_support并非一个完全实现的letter shell插件,由于文件系统的接口和操作系统以及具体使用的文件系统相关,所以fs_support仅仅通过接入几个基本的接口以实现cdls命令,具体使用时,可能需要根据使用的文件系统接口修改fs_support,letter shell的demo/x86-gcc下有针对linux平台的移植,可以及进行参考

    因为这个功能作者还未完善,所以这部分的命令需要我们自己来实现,下面是我在 FatFs中对 mv 命令的实现,功能是在将 old_path 的文件移动到 new_path,或者是将 old_path的文件移动到 new_path,并且重命名该文件。

    /**
     * @brief 重命名/移动文件或子目录
     */
    void shell_mv(char *old_path,char *new_path)
    {
      FRESULT res;
      
      shell_active = shellGetCurrent();   //Get the currently active shelll
      res = f_rename(old_path, new_path);
      if(res != FR_OK)
      {
        shellWriteString(shell_active,"Operation failed! Error Code:");
        shellPrint(shell_active,"%d\r\n",res);
      }
    }
    SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, mv, shell_mv, rename or moves a file or directory);
    
    

    输入以下命令,就能将 dir1目录下的 file1.txt 移动到 dir2 目录下,并且这个文件名将修改为 file2.txt。

    mv dir1/file1.txt dir2/file2.txt
    

    不过要注意一点,FatFs 的磁盘是以数字打头的,不像window,各个硬盘是以字符打头的,比如 "C:\file.txt"表示 C盘目录下有个 file.txt,在FatFs中,对应的目录是 “0:\file.txt” ,假如我们以 “0:\file.txt” 当成参数传递进去,那么接收是会存在问题的,这是因为第一个输入的是 0,在解析参数的时候,它会把这一整串字符串当成数字来解析。
    我们先找到解析参数的函数,可以看到 if (*string == ‘-’ || (*string >= ‘0’ && *string <= ‘9’)) 那么就是要解析数字,我们在这个判断中,再次判断,从字符开始到后面,如果出现了任何一个不属于数字的字符,我们就判定这个是字符串。而如果在空格或者回车命令前,全数字的字符,那么我们就判断这个是数字,通过这个方式,就能规避 FatFs 的硬盘号会导致参数解析异常的问题了。

    /**
     * @brief 解析参数
     * 
     * @param shell shell对象
     * @param string 参数
     * @return unsigned int 解析结果
     */
    unsigned int shellExtParsePara(Shell *shell, char *string)
    {
    	uint16_t i = 1;
        if (*string == '\'' && *(string + 1))
        {
            return (unsigned int)shellExtParseChar(string);
        }
        else if (*string == '-' || (*string >= '0' && *string <= '9'))
        {
    		while(1)
    		{
    			//寻找下一个表示该参数已经结束的字符
    			if((*(string + i)  == ' ') 		//下一个为空格表示该参数已经结束
    				#if SHELL_ENTER_LF == 1			// LF 表示命令结束
    				|| (*(string + i)  == 0x0A))	
    				#endif
    				#if SHELL_ENTER_CR == 1			// CR 表示命令结束
    				|| (*(string + i)  == 0x0D))
    				#endif
    				#if SHELL_ENTER_CRLF == 1		// CR LF 表示命令结束
    				|| ((*(string + i)  == 0x0A) && (*(string + i + 1)  == 0x0D))
    				#endif
    			break;
    
    			//出现不为小数点或者非数字的字符,表明该参数为字符串
    			if((*(string + i) != '.') || (*(string + i) < '0') || (*(string + i) > '9'))
    				return (unsigned int)shellExtParseString(string);
    
    			i++;
    		}
            return (unsigned int)shellExtParseNumber(string);
        }
        else if (*string == '$' && *(string + 1))
        {
            return shellExtParseVar(shell, string);
        }
        else if (*string)
        {
            return (unsigned int)shellExtParseString(string);
        }
        return 0;
    }
    

    六、其他常用的 Letter Shell 函数

    6.1 shellWriteString

    函数原型如下,这个功能就是 printf 。这个应该很好理解,就不细说了。

    /**
     * @brief shell 写字符串
     * 
     * @param shell shell对象
     * @param string 字符串数据
     * 
     * @return unsigned short 写入字符的数量
     */
    unsigned short shellWriteString(Shell *shell, const char *string)
    

    6.2 shellGetCurrent

    letter shell采取一个静态数组对定义的多个shell进行管理,shell数量可以修改宏SHELL_MAX_NUMBER定义(为了不使用动态内存分配,此处通过数据进行管理),从而,在shell执行的函数中,可以调用shellGetCurrent()获得当前活动的shell对象,从而可以实现某一个函数在不同的shell对象中发生不同的行为,也可以通过这种方式获得shell对象后,调用shellWriteString(shell, string)进行shell的输出。

    /**
     * @brief 获取当前活动shell
     * 
     * @return Shell* 当前活动shell对象
     */
    Shell* shellGetCurrent(void)
    

    可以定义一个全局变量

    Shell *shell_active;      //当前活动的shell
    

    如果我们使用多个串口创建shell 对象,如果通过不同的串口访问文件,那么就不容易知道应该在哪个界面进行输出,正如这个 mv 的命令,首先获取当前活动的shell,再通过 shellWriteString(shell_active,“Operation failed! Error Code:”); 进行输出,这样就可以避免冲突了。

    /**
     * @brief 重命名/移动文件或子目录
     */
    void shell_mv(char *old_path,char *new_path)
    {
      FRESULT res;
      
      shell_active = shellGetCurrent();   //Get the currently active shelll
      res = f_rename(old_path, new_path);
      if(res != FR_OK)
      {
        shellWriteString(shell_active,"Operation failed! Error Code:");
        shellPrint(shell_active,"%d\r\n",res);
      }
    }
    SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, mv, shell_mv, rename or moves a file or directory);
    

    6.3 shellPrint

    这个功能就是 vsnprintf 的简单封装了,用于向字符串中打印数据、数据格式用户自定义,但是要注意,打印的大小不要超过 SHELL_PRINT_BUFFER

    #if SHELL_PRINT_BUFFER > 0
    /**
     * @brief shell格式化输出
     * 
     * @param shell shell对象
     * @param fmt 格式化字符串
     * @param ... 参数
     */
    void shellPrint(Shell *shell, char *fmt, ...)
    {
        char buffer[SHELL_PRINT_BUFFER];
        va_list vargs;
    
        SHELL_ASSERT(shell, return);
    
        va_start(vargs, fmt);
        vsnprintf(buffer, SHELL_PRINT_BUFFER - 1, fmt, vargs);
        va_end(vargs);
        
        shellWriteString(shell, buffer);
    }
    #endif
    

    到这里,介绍就完了,而这也足够应付绝大部分的调试内容了。

    源代码

    展开全文
  • 图示Leter Shell效果演示

    在这里插入图片描述

    [--------------点击下面,进入总目录索引--------------]
    STM32系列精品Demo集 - 总目录


    图示:Letter Shell效果演示
    在这里插入图片描述

    一、为什么要移植Letter shell


    • Linux下强大的命令行工具用起来即为顺手,所以移植一款命令行工具到STM平台非常有意义 。
    • 工程中集成一款交互式shell,为项目开发调试提供便捷,开发者自定义测试命令,使得更加灵活。

    二、移植步骤

    2.1、获取Letter Shell源码

    https://github.com/NevermindZZT/letter-shell

    图示:Letter Shell源码结构
    在这里插入图片描述
    上述源码结构中,若不想深入了解实现机制,只想简单使用起来,后续移植只需关心上述框出文件即可!

    2.2、shell_port.c数据源移植

    shell_port.c主要用于配置Uart数据通道,说白了就是将shell_port.c中的读写接口移植为STM32平台的Uart发送接收接口。

    Shell shell;
    char shellBuffer[512];
    
    /**
     * @brief 用户shell写
     *
     * @param data 数据
     */
    void userShellWrite(char data)
    {
        ControlUartSendData(UART0_CHANNEL, data);
    }
    /**
     * @brief 用户shell读
     *
     * @param data 数据
     * @return char 状态
     */
    signed char userShellRead(char *data)
    {
        *data = 0;
        while (!(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET))
        {
        }
        *data = USART_ReceiveData(USART1);
    	return 0;  //返回0标识数据已经准备OK
    }
    /**
     * @brief 用户shell初始化
     *
     */
    void userShellInit(void)
    {
        char data = 0;
        shell.write = userShellWrite;
        shell.read = userShellRead;
        shellInit(&shell, shellBuffer, 512);
    }
    

    2.3、shell_cfg.h平台配置移植

    图示:shell_cfg.h配置宏定义
    在这里插入图片描述

    !!!!!对于上述宏开关,小伙伴们可自行尝试,博主在移植过程中仅关注了下述几个宏,其它保持默认即可。!!!!!

    /**
     * @brief 是否使用默认shell任务while循环,使能宏`SHELL_USING_TASK`后此宏有意义
     *        使能此宏,则`shellTask()`函数会一直循环读取输入,一般使用操作系统建立shell
     *        任务时使能此宏,关闭此宏的情况下,一般适用于无操作系统,在主循环中调用`shellTask()`
     */
    #define     SHELL_TASK_WHILE            0          /*裸机操作不执行线程,关闭此宏*/
    /**
     * @brief 获取系统时间(ms)
     *        定义此宏为获取系统Tick,如`HAL_GetTick()`
     */
    #define     SHELL_GET_TICK()            0	       /*裸机操作不实现此接口,关闭此宏*/
    /**
     * @brief shell默认用户
     */
    #define     SHELL_DEFAULT_USER          "MicroShell" /*修改默认用户名*/
    

    2.4、启动Letter Shell任务
    2.4.1、启动方式一

    /**
     * @brief shell 任务
     * 
     * @param param 参数(shell对象)
     * 
     */
    void shellTask(void *param)
    {
        Shell *shell = (Shell *)param;
        char data;
        /* 可以看到shell->read如果返回0,则认为串口有数据,调用shellHandler去处理,所以也就衍生了启动方式二*/
        if (shell->read && shell->read(&data) == 0)
        {
            shellHandler(shell, data);
        }
    }
    
    #include "stm32f4xx.h" 
    #include "Delay.h"
    #include "ControlUart.h"
    #include "ModuleConfig.h"
    #include "shell_port.h"
    #include "shell.h"
    int main(void)
    {   
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 
    	delay_init();		     
    	ControlUartInit(UART0_CHANNEL,115200);  
    	userShellInit();
    	while(1)
    	{
    		shellTask(&shell);
    	}
    }
    

    2.4.2、启动方式二

    /* 自定义接口函数 */
    void userShellRun(void)
    {
        char data = 0;
        data = 0;
        while (!(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET))
        {
        }
        data = USART_ReceiveData(USART1);
    	  shellHandler(&shell,data);
        //delay_ms(10);
    }
    
    #include "stm32f4xx.h" 
    #include "Delay.h"
    #include "ControlUart.h"
    #include "ModuleConfig.h"
    #include "shell_port.h"
    #include "shell.h"
    int main(void)
    {   
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 
    	delay_init();		     
    	ControlUartInit(UART0_CHANNEL,115200);  
    	userShellInit();
    	while(1)
    	{
    		userShellRun();
    	}
    }
    

    三、Letter Shell简单使用

    3.1、编写自定义命令对应的执行接口

    • letter shell 3.x同时支持两种形式的函数定义方式:
    1. Main函数形式:func(int argc, char *agrv[])
    2. 普通C函数形式:func(int i, char *str, ...)
    • main函数形式
    /***********************************************************************
     * 函数名称: ShellTestFun0
     * 功能描述: letter:/$ shelltest0 123 'a' "1234"
     * 输入参数: 
     * 输出参数: 
     * 返 回 值: 
     *  其   它: 
     ***********************************************************************/
    int  ShellTestFun0(int i, char ch, char *str)
    {
        printf("input int: %d, char: %c, string: %s\r\n", i, ch, str);
    	//最后必须要加上换行,以避免与shell内置返回打印重叠
    	printf("\r\n");
    	return 0;
    }
    
    • 普通C函数形式

        普通C函数形式,shell会自动对参数进行转化处理,目前支持二进制,八进制,十进制,十六进制整形,字符,字符串

      /***********************************************************************
       * 函数名称: ShellTestFun1
       * 功能描述: letter:/$ shellest1 "hello world"
       * 输入参数: 
       * 输出参数: 
       * 返 回 值: 
       *  其   它: 
       ***********************************************************************/
      int ShellTestFun1(int argc, char *agrv[])
      {
          printf("%dparameter(s)\r\n", argc);
          for (char i = 0; i <= argc; i++)
          {
              printf("%s\r\n", agrv[i]);
          }
      	//最后必须要加上换行,以避免与shell内置返回打印重叠
      	printf("\r\n");
      	return 0;
      }
      

    3.2、注册命令

    /***********************************************************************
    * 函数名称: #define SHELL_EXPORT_CMD(_attr, _name, _func, _desc)
    * 输入参数:
      		_attr :命令属性
      		_name :命令名
      		_func :命令函数
      		_desc :命令描述
    * 输出参数:
    * 返 回 值:
    *  其   它:
    ***********************************************************************/
    /******************************************************************************
    * 注册自定义不同形参的测试指令
    ******************************************************************************/
    SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), shelltest0, ShellTestFun0, test mode0 parameter);
    SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN), shelltest1, ShellTestFun1, test mode1 parameter);
    

    3.3、命令测试

    #终端调用
    MicroShell:/$ shelltest0 12 'a' "hello"
    input int: 12, char: a, string: hello
    Return: 0, 0x00000000
    
    #终端调用
    MicroShell:/$ shelltest1 "hello"
    2parameter(s)
    shelltest1
    hello
    Return: 0, 0x00000000
    

    附录:Demo源码

    https://gitee.com/jiangfengzhang/stm32-f4-letter-shell

    图示:工程源码结构
    shell_customd_cmd.c(自定义命令源文件)
    在这里插入图片描述

    展开全文
  • letter shell 支持在函数体外部,采用定义常量的方式定义命令,例如SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE (SHELL_TYPE_CMD_MAIN)|SHELL_CMD_DISABLE_RETURN,help, shellHelp, show command info...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,382
精华内容 4,552
关键字:

lettershell