你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

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

[复制链接]
STMCU小助手 发布时间:2022-8-23 16:13
前言
一般使用串口来打印调试信息之类的,正点原子的USMART也不错,这边引入了一个类似于Linux 命令行的调试功能,Letter Shell,功能很强大,github的链接在下面。
我们要移植一下这个功能,方便后面的调试。这个功能使用到了串口,接收到串口数据后回传到上位机,直到接收到命令行回车触发,也就是0x0A(LF 换行)以及0x0D(CR 回车),不同的软件,结束符可能也有点区别。确认收到完整的命令,就可以解析命令,根据不同函数的地址就可以进行调用了,这样子调试程序就很方便了。
效果图如下:

20210220214542675.gif

一、FreeRTOS配置
1.FreeRTOS

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

20210124211939416.png

FreeRTOS需要使用时钟基,默认是SysTick,滴答定时器,默认配置是1ms中断一次,然后计时器+1,但是SysTick并不是很准确,我们可以在SYS 选项卡中改一下Timebase Source,这里我们改为定时器3当时钟基。
20210124222836342.png

然后配置一下串口,因为Nucleo板载了ST-LINK V2.1,有一个虚拟串口连接到USART3,所以使能USART3,然后使能中断就可以了。点击GENERATE CODE生成代码。

2021012422385383.png

先打开 “freertos.c”,里面有一个默认的线程,StartDefaultTask,我们在这里加一个LED闪烁的功能,先测试一下生成的工程没问题。

  1. /* USER CODE BEGIN Header_StartDefaultTask */
  2. /**
  3.   * @brief  Function implementing the defaultTask thread.
  4.   * @param  argument: Not used
  5.   * @retval None
  6.   */
  7. /* USER CODE END Header_StartDefaultTask */
  8. void StartDefaultTask(void *argument)
  9. {
  10.   /* USER CODE BEGIN StartDefaultTask */
  11.   /* Infinite loop */
  12.   for(;;)
  13.   {
  14.     HAL_GPIO_TogglePin(LD3_GPIO_Port,LD3_Pin);
  15.     osDelay(100);
  16.   }
  17.   /* USER CODE END StartDefaultTask */
  18. }
复制代码

注意,配置的Keil工程,默认是没有设置下载完自动复位,我们需要按一下Nucleo 板子上面黑色的复位按钮,另外默认的配置下,有一个以太网的初始化 “MX_ETH_Init()”,需要接入网线到路由器,不然初始化不通过,会跳转到Error_Handler();
  1. if (HAL_ETH_Init(&heth) != HAL_OK)
  2. {
  3.   Error_Handler();
  4. }
复制代码

二、移植Letter Shell
下载github上面的源代码,解压出来。

20210124230430874.png

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

我们在STM32Cube 的工程项目下新建一个文件夹 Shell,然后把 src文件夹下的源代码都放进去,遵守开源公约,LICENSE也要记得放进去。放进去之后,demo\stm32-freertos 目录下还有三个文件,我们也拷贝过去覆盖即可。

20210124231354443.png

然后在keil工程下添加这些文件。

20210124231834258.png

编译一下,此时会报错

  1. ..\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”,这样改了一次以后,以后换芯片系列也能很方便的移植了。再次编译,仍然会报错。

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

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

  1. #include "shell.h"
  2. #include "usart.h"
复制代码

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

  1. /**
  2. * @brief 用户shell写
  3. *
  4. * @param data 数据
  5. */
  6. void userShellWrite(char data)
  7. {
  8.     serialTransmit(&debugSerial, (uint8_t *)&data, 1, 0xFF);
  9. }

  10. /**
  11. * @brief 用户shell读
  12. *
  13. * @param data 数据
  14. * @return char 状态
  15. */
  16. signed char userShellRead(char *data)
  17. {
  18.     if (serialReceive(&debugSerial, (uint8_t *)data, 1, 0) == 1)
  19.     {
  20.         return 0;
  21.     }
  22.     else
  23.     {
  24.         return -1;
  25.     }

  26. }
复制代码

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

  1. /**
  2. * @brief 用户shell写
  3. *
  4. * @param data 数据
  5. */
  6. void userShellWrite(char data)
  7. {
  8.   HAL_UART_Transmit(&huart3,(uint8_t *)&data, 1,1000);
  9. }


  10. /**
  11. * @brief 用户shell读
  12. *
  13. * @param data 数据
  14. * @return char 状态
  15. */
  16. signed char userShellRead(char *data)
  17. {
  18.   if(HAL_UART_Receive(&huart3,(uint8_t *)data, 1, 0) == HAL_OK)
  19.   {
  20.       return 0;
  21.   }
  22.   else
  23.   {
  24.       return -1;
  25.   }
  26. }
复制代码

再次编译就不会出错了。

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

3.1 定义shell对象

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

  1. Shell shell;
复制代码

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

  1. /**
  2. * @brief shell读取数据函数原型
  3. *
  4. * @param char shell读取的字符
  5. *
  6. * @return char 0 读取数据成功
  7. * @return char -1 读取数据失败
  8. */
  9. typedef signed char (*shellRead)(char *);

  10. /**
  11. * @brief shell写数据函数原型
  12. *
  13. * @param const char 需写的字符
  14. */
  15. typedef void (*shellWrite)(const char);
复制代码

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

3.3 申请缓存区
申请一片缓冲区

  1. char shellBuffer[512];
复制代码

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

  1. /**
  2. * @brief 用户shell初始化
  3. *
  4. */
  5. void userShellInit(void)
  6. {
  7.     shell.write = userShellWrite;
  8.     shell.read = userShellRead;
  9.     shellInit(&shell, shellBuffer, 512);
  10. }
复制代码

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

  1. /* USER CODE BEGIN Includes */
  2. #include "shell_port.h"
  3. /* USER CODE END Includes */
复制代码

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

  1. /* USER CODE BEGIN 2 */
  2.   userShellInit();    //letter shell 的初始化
  3.   /* USER CODE END 2 */
复制代码

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

  1. ..\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

20210124235734855.png

由于我们还没编写串口接收方面的函数,按下字符是不会进行回传的,这里我们先按一下复位按钮,接上网线等到初始化完成后,就可以看到 letter shell 初始化完成后的串口信息。

20210125000101173.png

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

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

Z9[D2ZUY1[R~NU1CT(4F8BN.png

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

  1. /**
  2. * @brief 是否使用默认shell任务while循环,使能宏`SHELL_USING_TASK`后此宏有意义
  3. *        使能此宏,则`shellTask()`函数会一直循环读取输入,一般使用操作系统建立shell
  4. *        任务时使能此宏,关闭此宏的情况下,一般适用于无操作系统,在主循环中调用`shellTask()`
  5. */
  6. #define     SHELL_TASK_WHILE            1

  7. /**
  8. * @brief 是否使用命令导出方式
  9. *        使能此宏后,可以使用`SHELL_EXPORT_CMD()`等导出命令
  10. *        定义shell命令,关闭此宏的情况下,需要使用命令表的方式
  11. */
  12. #define     SHELL_USING_CMD_EXPORT      1

  13. /**
  14. * @brief 是否使用shell伴生对象
  15. *        一些扩展的组件(文件系统支持,日志工具等)需要使用伴生对象
  16. */
  17. #define     SHELL_USING_COMPANION       0

  18. /**
  19. * @brief 支持shell尾行模式
  20. */
  21. #define     SHELL_SUPPORT_END_LINE      0

  22. /**
  23. * @brief 是否在输出命令列表中列出用户
  24. */
  25. #define     SHELL_HELP_LIST_USER        0

  26. /**
  27. * @brief 是否在输出命令列表中列出变量
  28. */
  29. #define     SHELL_HELP_LIST_VAR         0

  30. /**
  31. * @brief 是否在输出命令列表中列出按键
  32. */
  33. #define     SHELL_HELP_LIST_KEY         0

  34. /**
  35. * @brief 是否在输出命令列表中展示命令权限
  36. */
  37. #define     SHELL_HELP_SHOW_PERMISSION  1

  38. /**
  39. * @brief 使用LF作为命令行回车触发
  40. *        可以和SHELL_ENTER_CR同时开启
  41. */
  42. #define     SHELL_ENTER_LF              0

  43. /**
  44. * @brief 使用CR作为命令行回车触发
  45. *        可以和SHELL_ENTER_LF同时开启
  46. */
  47. #define     SHELL_ENTER_CR              0

  48. /**
  49. * @brief 使用CRLF作为命令行回车触发
  50. *        不可以和SHELL_ENTER_LF或SHELL_ENTER_CR同时开启
  51. */
  52. #define     SHELL_ENTER_CRLF            1

  53. /**
  54. * @brief 使用执行未导出函数的功能
  55. *        启用后,可以通过`exec [addr] [args]`直接执行对应地址的函数
  56. * @attention 如果地址错误,可能会直接引起程序崩溃
  57. */
  58. #define     SHELL_EXEC_UNDEF_FUNC       0

  59. /**
  60. * @brief shell命令参数最大数量
  61. *        包含命令名在内,超过8个参数并且使用了参数自动转换的情况下,需要修改源码
  62. */
  63. #define     SHELL_PARAMETER_MAX_NUMBER  8

  64. /**
  65. * @brief 历史命令记录数量
  66. */
  67. #define     SHELL_HISTORY_MAX_NUMBER    5

  68. /**
  69. * @brief 双击间隔(ms)
  70. *        使能宏`SHELL_LONG_HELP`后此宏生效,定义双击tab补全help的时间间隔
  71. */
  72. #define     SHELL_DOUBLE_CLICK_TIME     200

  73. /**
  74. * @brief 管理的最大shell数量
  75. */
  76. #define     SHELL_MAX_NUMBER            5

  77. /**
  78. * @brief shell格式化输出的缓冲大小
  79. *        为0时不使用shell格式化输出
  80. */
  81. #define     SHELL_PRINT_BUFFER          128

  82. /**
  83. * @brief 获取系统时间(ms)
  84. *        定义此宏为获取系统Tick,如`HAL_GetTick()`
  85. * @note 此宏不定义时无法使用双击tab补全命令help,无法使用shell超时锁定
  86. */
  87. #define     SHELL_GET_TICK()            HAL_GetTick()

  88. /**
  89. * @brief shell内存分配
  90. *        shell本身不需要此接口,若使用shell伴生对象,需要进行定义
  91. */
  92. #define     SHELL_MALLOC(size)          0

  93. /**
  94. * @brief shell内存释放
  95. *        shell本身不需要此接口,若使用shell伴生对象,需要进行定义
  96. */
  97. #define     SHELL_FREE(obj)             0

  98. /**
  99. * @brief 是否显示shell信息
  100. */
  101. #define     SHELL_SHOW_INFO             1

  102. /**
  103. * @brief 是否在登录后清除命令行
  104. */
  105. #define     SHELL_CLS_WHEN_LOGIN        1

  106. /**
  107. * @brief shell默认用户
  108. */
  109. #define     SHELL_DEFAULT_USER          "letter"

  110. /**
  111. * @brief shell默认用户密码
  112. *        若默认用户不需要密码,设为""
  113. */
  114. #define     SHELL_DEFAULT_USER_PASSWORD ""

  115. /**
  116. * @brief shell自动锁定超时
  117. *        shell当前用户密码有效的时候生效,超时后会自动重新锁定shell
  118. *        设置为0时关闭自动锁定功能,时间单位为`SHELL_GET_TICK()`单位
  119. * @note 使用超时锁定必须保证`SHELL_GET_TICK()`有效
  120. */
  121. #define     SHELL_LOCK_TIMEOUT          0 * 60 * 1000
复制代码

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

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

  1. /**
  2. * @brief shell 任务
  3. *
  4. * @param param 参数(shell对象)
  5. *
  6. */
  7. void shellTask(void *param)
  8. {
  9.     Shell *shell = (Shell *)param;
  10.     char data;
  11. #if SHELL_TASK_WHILE == 1
  12.     while(1)
  13.     {
  14. #endif
  15.         if (shell->read && shell->read(&data) == 0)
  16.         {
  17.             shellHandler(shell, data);
  18.         }
  19. #if SHELL_TASK_WHILE == 1
  20.     }
  21. #endif
  22. }
复制代码

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 作为命令行回车触犯,所以这三个的配置为

  1. #define     SHELL_ENTER_LF              0
  2. #define     SHELL_ENTER_CR              1
  3. #define     SHELL_ENTER_CRLF            0
复制代码

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

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

  1. #define     SHELL_TASK_WHILE            0
  2. #define     SHELL_USING_CMD_EXPORT      1
  3. #define     SHELL_USING_COMPANION       0
  4. #define     SHELL_USING_COMPANION       0
  5. #define     SHELL_SUPPORT_END_LINE      0
  6. #define     SHELL_HELP_LIST_USER        0
  7. #define     SHELL_HELP_LIST_VAR         0
  8. #define     SHELL_HELP_LIST_KEY         0
  9. #define     SHELL_HELP_SHOW_PERMISSION  1
  10. #define     SHELL_ENTER_LF              0
  11. #define     SHELL_ENTER_CR              1
  12. #define     SHELL_ENTER_CRLF            0
  13. #define     SHELL_EXEC_UNDEF_FUNC       0
  14. #define     SHELL_PARAMETER_MAX_NUMBER  8
  15. #define     SHELL_HISTORY_MAX_NUMBER    5
  16. #define     SHELL_DOUBLE_CLICK_TIME     200
  17. #define     SHELL_MAX_NUMBER            5
  18. #define     SHELL_PRINT_BUFFER          128
  19. #define     SHELL_GET_TICK()            HAL_GetTick()
  20. #define     SHELL_MALLOC(size)          0
  21. #define     SHELL_FREE(obj)             0
  22. #define     SHELL_SHOW_INFO             1
  23. #define     SHELL_CLS_WHEN_LOGIN        1
  24. #define     SHELL_DEFAULT_USER          "letter"
  25. #define     SHELL_DEFAULT_USER_PASSWORD ""
  26. #define     SHELL_LOCK_TIMEOUT          0 * 60 * 1000
复制代码

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

  1. #define HAL_USART3_RXBUFFERSIZE         1
  2. uint8_t HAL_USART3_RxBuffer[HAL_USART3_RXBUFFERSIZE]; //HAL库使用的串口接收缓冲
复制代码

在main函数中打开串口中断

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

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

  1. void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
  2. {
  3.   uint8_t udata;
  4.   if(huart->Instance == USART3)      //串口3的接收部分
  5.   {
  6.     HAL_UART_Receive_IT(&huart3,(uint8_t *)HAL_USART3_RxBuffer, HAL_USART3_RXBUFFERSIZE);     //接收串口的缓存
  7.     udata = HAL_USART3_RxBuffer[0];
  8.     shellHandler(&shell, udata);
  9.   }
  10. }
复制代码

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

2021012523522544.png

五、使用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函数的命令导出
  1. void LD1_ON(void)
  2. {
  3.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);
  4. }

  5. SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN, led1_on, LD1_ON, LD1 ON);

  6. void LD1_OFF(void)
  7. {
  8.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET);
  9. }

  10. 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 就是导出命令,各个参数分别是 命令属性,命令名,命令函数,命令描述。

  1. /**
  2.      * @brief shell 命令定义
  3.      *
  4.      * @param _attr 命令属性
  5.      * @param _name 命令名
  6.      * @param _func 命令函数
  7.      * @param _desc 命令描述
  8.      */
  9.     #define SHELL_EXPORT_CMD(_attr, _name, _func, _desc) \
  10.             const char shellCmd##_name[] = #_name; \
  11.             const char shellDesc##_name[] = #_desc; \
  12.             const ShellCommand \
  13.             shellCommand##_name SECTION("shellCommand") =  \
  14.             { \
  15.                 .attr.value = _attr, \
  16.                 .data.cmd.name = shellCmd##_name, \
  17.                 .data.cmd.function = (int (*)())_func, \
  18.                 .data.cmd.desc = shellDesc##_name \
  19.             }
复制代码

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

  1. /**
  2. * @brief shell 命令权限
  3. *
  4. * @param permission 权限级别
  5. */
  6. #define     SHELL_CMD_PERMISSION(permission) \
  7.             (permission & 0x000000FF)

  8. /**
  9. * @brief shell 命令类型
  10. *
  11. * @param type 类型
  12. */
  13. #define     SHELL_CMD_TYPE(type) \
  14.             ((type & 0x0000000F) << 8)

  15. /**
  16. * @brief 使能命令在未校验密码的情况下使用
  17. */
  18. #define     SHELL_CMD_ENABLE_UNCHECKED \
  19.             (1 << 12)

  20. /**
  21. * @brief 禁用返回值打印
  22. */
  23. #define     SHELL_CMD_DISABLE_RETURN \
  24.             (1 << 13)

  25. /**
  26. * @brief 只读属性(仅对变量生效)
  27. */
  28. #define     SHELL_CMD_READ_ONLY \
  29.             (1 << 14)
复制代码

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

  1. SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC)|SHELL_CMD_DISABLE_RETURN
复制代码

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

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

  1. led1_on
  2. 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的亮灭了。

20210126222149487.png

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

  1. int func(int argc, char *agrv[])
  2. {
  3.     printf("%dparameter(s)\r\n", argc);
  4.     for (char i = 1; i < argc; i++)
  5.     {
  6.         printf("%s\r\n", argv);
  7.     }
  8. }
  9. SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN), func, func, test);
复制代码

终端调用

  1. letter:/$ func "hello world"
  2. 2 parameter(s)
  3. hello world
复制代码

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

  1. void LD1_ON_OFF_ms(uint16_t on_ms,uint16_t off_ms)
  2. {
  3.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);    //点亮LD1
  4.   HAL_Delay(on_ms);                                         //延时
  5.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_RESET);  //熄灭LD1
  6.   HAL_Delay(off_ms);                                        //延时
  7.   HAL_GPIO_WritePin(LD1_GPIO_Port,LD1_Pin,GPIO_PIN_SET);    //点亮LD1
  8. }
  9. 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,最后又点亮的状态。

  1. 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仅仅通过接入几个基本的接口以实现cd,ls命令,具体使用时,可能需要根据使用的文件系统接口修改fs_support,letter shell的demo/x86-gcc下有针对linux平台的移植,可以及进行参考

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

  1. /**
  2. * @brief 重命名/移动文件或子目录
  3. */
  4. void shell_mv(char *old_path,char *new_path)
  5. {
  6.   FRESULT res;

  7.   shell_active = shellGetCurrent();   //Get the currently active shelll
  8.   res = f_rename(old_path, new_path);
  9.   if(res != FR_OK)
  10.   {
  11.     shellWriteString(shell_active,"Operation failed! Error Code:");
  12.     shellPrint(shell_active,"%d\r\n",res);
  13.   }
  14. }
  15. 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。

  1. 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 的硬盘号会导致参数解析异常的问题了。

  1. /**
  2. * @brief 解析参数
  3. *
  4. * @param shell shell对象
  5. * @param string 参数
  6. * @return unsigned int 解析结果
  7. */
  8. unsigned int shellExtParsePara(Shell *shell, char *string)
  9. {
  10.         uint16_t i = 1;
  11.     if (*string == '\'' && *(string + 1))
  12.     {
  13.         return (unsigned int)shellExtParseChar(string);
  14.     }
  15.     else if (*string == '-' || (*string >= '0' && *string <= '9'))
  16.     {
  17.                 while(1)
  18.                 {
  19.                         //寻找下一个表示该参数已经结束的字符
  20.                         if((*(string + i)  == ' ')                 //下一个为空格表示该参数已经结束
  21.                                 #if SHELL_ENTER_LF == 1                        // LF 表示命令结束
  22.                                 || (*(string + i)  == 0x0A))        
  23.                                 #endif
  24.                                 #if SHELL_ENTER_CR == 1                        // CR 表示命令结束
  25.                                 || (*(string + i)  == 0x0D))
  26.                                 #endif
  27.                                 #if SHELL_ENTER_CRLF == 1                // CR LF 表示命令结束
  28.                                 || ((*(string + i)  == 0x0A) && (*(string + i + 1)  == 0x0D))
  29.                                 #endif
  30.                         break;

  31.                         //出现不为小数点或者非数字的字符,表明该参数为字符串
  32.                         if((*(string + i) != '.') || (*(string + i) < '0') || (*(string + i) > '9'))
  33.                                 return (unsigned int)shellExtParseString(string);

  34.                         i++;
  35.                 }
  36.         return (unsigned int)shellExtParseNumber(string);
  37.     }
  38.     else if (*string == '
  39. [b][size=3]六、其他常用的 Letter Shell 函数[/size][/b]
  40. [b]6.1 shellWriteString[/b]
  41. 函数原型如下,这个功能就是 printf 。这个应该很好理解,就不细说了。

  42. [code]/**
  43. * @brief shell 写字符串
  44. *
  45. * @param shell shell对象
  46. * @param string 字符串数据
  47. *
  48. * @return unsigned short 写入字符的数量
  49. */
  50. 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的输出。

  1. /**
  2. * @brief 获取当前活动shell
  3. *
  4. * @return Shell* 当前活动shell对象
  5. */
  6. Shell* shellGetCurrent(void)
复制代码

可以定义一个全局变量

  1. Shell *shell_active;      //当前活动的shell
复制代码

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

  1. /**
  2. * @brief 重命名/移动文件或子目录
  3. */
  4. void shell_mv(char *old_path,char *new_path)
  5. {
  6.   FRESULT res;

  7.   shell_active = shellGetCurrent();   //Get the currently active shelll
  8.   res = f_rename(old_path, new_path);
  9.   if(res != FR_OK)
  10.   {
  11.     shellWriteString(shell_active,"Operation failed! Error Code:");
  12.     shellPrint(shell_active,"%d\r\n",res);
  13.   }
  14. }
  15. 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。

  1. #if SHELL_PRINT_BUFFER > 0
  2. /**
  3. * @brief shell格式化输出
  4. *
  5. * @param shell shell对象
  6. * @param fmt 格式化字符串
  7. * @param ... 参数
  8. */
  9. void shellPrint(Shell *shell, char *fmt, ...)
  10. {
  11.     char buffer[SHELL_PRINT_BUFFER];
  12.     va_list vargs;

  13.     SHELL_ASSERT(shell, return);

  14.     va_start(vargs, fmt);
  15.     vsnprintf(buffer, SHELL_PRINT_BUFFER - 1, fmt, vargs);
  16.     va_end(vargs);

  17.     shellWriteString(shell, buffer);
  18. }
  19. #endif
复制代码

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



&& *(string + 1))
    {
        return shellExtParseVar(shell, string);
    }
    else if (*string)
    {
        return (unsigned int)shellExtParseString(string);
    }
    return 0;
}[/code]
六、其他常用的 Letter Shell 函数
6.1 shellWriteString
函数原型如下,这个功能就是 printf 。这个应该很好理解,就不细说了。

  1. /**
  2. * @brief shell 写字符串
  3. *
  4. * @param shell shell对象
  5. * @param string 字符串数据
  6. *
  7. * @return unsigned short 写入字符的数量
  8. */
  9. 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的输出。

  1. /**
  2. * @brief 获取当前活动shell
  3. *
  4. * @return Shell* 当前活动shell对象
  5. */
  6. Shell* shellGetCurrent(void)
复制代码

可以定义一个全局变量

  1. Shell *shell_active;&nbsp; &nbsp;&nbsp; &nbsp;//当前活动的shell
复制代码

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

  1. /**
  2. * @brief 重命名/移动文件或子目录
  3. */
  4. void shell_mv(char *old_path,char *new_path)
  5. {
  6. &nbsp;&nbsp;FRESULT res;

  7. &nbsp;&nbsp;shell_active = shellGetCurrent();&nbsp; &nbsp;//Get the currently active shelll
  8. &nbsp;&nbsp;res = f_rename(old_path, new_path);
  9. &nbsp;&nbsp;if(res != FR_OK)
  10. &nbsp;&nbsp;{
  11. &nbsp; &nbsp; shellWriteString(shell_active,"Operation failed! Error Code:");
  12. &nbsp; &nbsp; shellPrint(shell_active,"%d\r\n",res);
  13. &nbsp;&nbsp;}
  14. }
  15. 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。

  1. #if SHELL_PRINT_BUFFER &gt; 0
  2. /**
  3. * @brief shell格式化输出
  4. *
  5. * @param shell shell对象
  6. * @param fmt 格式化字符串
  7. * @param ... 参数
  8. */
  9. void shellPrint(Shell *shell, char *fmt, ...)
  10. {
  11. &nbsp; &nbsp; char buffer[SHELL_PRINT_BUFFER];
  12. &nbsp; &nbsp; va_list vargs;

  13. &nbsp; &nbsp; SHELL_ASSERT(shell, return);

  14. &nbsp; &nbsp; va_start(vargs, fmt);
  15. &nbsp; &nbsp; vsnprintf(buffer, SHELL_PRINT_BUFFER - 1, fmt, vargs);
  16. &nbsp; &nbsp; va_end(vargs);

  17. &nbsp; &nbsp; shellWriteString(shell, buffer);
  18. }
  19. #endif
复制代码

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

————————————————
转载:fafuwxm

R`)~GN97DP3}]5R[IQUN}XE.png
收藏 评论0 发布时间:2022-8-23 16:13

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版