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

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

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


文章目录

  • STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell
  • 前言
  • 一、FreeRTOS配置
    • 1.FreeRTOS
  • 二、移植Letter Shell
  • 三、Letter Shell的使用
    • 3.1 定义shell对象
    • 3.2 定义shell读写函数
    • 3.3 申请缓存区
    • 3.4 调用shellInit进行初始化
    • 3.5 初步完成shell的初始化
  • 四、Letter Shell的进一步了解
    • 4.1 宏定义
    • 4.2 SHELL_TASK_WHILE
    • 4.3 SHELL_USING_CMD_EXPORT
    • 4.4 SHELL_USING_COMPANION
    • 4.5 SHELL_ENTER_CRLF
    • 4.6 SHELL_PRINT_BUFFER
    • 4.7 其他的宏
    • 4.8 shell的接收处理
  • 五、使用Letter Shell 进行调试程序
    • 5.1 普通c函数的命令导出
      • 5.1.1 命令属性
      • 5.1.2 命令名
      • 5.1.3 命令函数
      • 5.1.4 命令描述
      • 5.1.5 实际效果
    • 5.2 main函数的命令导出
    • 5.3 带参的命令导出
  • 六、其他常用的 Letter Shell 函数
    • 6.1 shellWriteString
    • 6.2 shellGetCurrent
    • 6.3 shellPrint


前言

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


一、FreeRTOS配置

1.FreeRTOS

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

STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第2张图片
FreeRTOS需要使用时钟基,默认是SysTick,滴答定时器,默认配置是1ms中断一次,然后计时器+1,但是SysTick并不是很准确,我们可以在SYS 选项卡中改一下Timebase Source,这里我们改为定时器3当时钟基。
STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第3张图片
然后配置一下串口,因为Nucleo板载了ST-LINK V2.1,有一个虚拟串口连接到USART3,所以使能USART3,然后使能中断就可以了。点击GENERATE CODE生成代码。
STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第4张图片
先打开 “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上面的源代码,解压出来。
STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第5张图片

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

我们在STM32Cube 的工程项目下新建一个文件夹 Shell,然后把 src文件夹下的源代码都放进去,遵守开源公约,LICENSE也要记得放进去。放进去之后,demo\stm32-freertos 目录下还有三个文件,我们也拷贝过去覆盖即可。
STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第6张图片
然后在keil工程下添加这些文件。
STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第7张图片
编译一下,此时会报错

..\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
STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第8张图片
由于我们还没编写串口接收方面的函数,按下字符是不会进行回传的,这里我们先按一下复位按钮,接上网线等到初始化完成后,就可以看到 letter shell 初始化完成后的串口信息。

STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第9张图片

四、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 也移植成功了。
STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第10张图片

五、使用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 实际效果

STM32CubeMX Nucleo F767ZI 教程(3) 串口调试工具 Letter Shell_第11张图片
如图所示,在输入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

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

源代码

你可能感兴趣的:(Nucleo,stm32,shell,串口通信)