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



前言

一般使用串口来打印调试信息之类的,正点原子的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_LENGTHshell命令最大长度
SHELL_PARAMETER_MAX_NUMBERshell命令参数最大数量
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_USERshell默认用户
SHELL_DEFAULT_USER_PASSWORD默认用户密码
SHELL_LOCK_TIMEOUTshell自动锁定超时

在文件 “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

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

源代码

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内置 "cmd-list" 命令获取所有命令列表 支持 tab 键补全命令 支持 backspace 回退,这个在 putty 上有 bug ,还没修复。在secureCRT正常。 支持上下箭头回溯历史,这个功能没有做的很好; 支持左右箭头编辑当前命令行输入; 提供 shell_cmdparam()函数转换命令后所跟的数字参数(字符串转整型),详见 demo 提供 shell_option_suport() 函数,使命令行支持 getopt()函数,详见 demo 系统共有9个文件,全部与硬件无关,编译语言要在 C99 以上(keil 在 project -> Options .. -> c/c++ -> C99 勾上) kernel.h // 一些必要的宏定义 shell.c,shell.h //具体的命令行解释的实现 ustdio.c,ustdio.h //非标准输出文件,重新链接 printf ,并提供一个小巧型的 printk 函数 avltree.c,avltree.h //平衡二叉树支持,shell 默认用链表建立查询机制,有必要可在shell.h 中开启二叉树 getopt.c,getopt.h //网上找的 getopt() 源码 除了 getopt.c,getopt.h 两个文件是我从网上找的源码,主要实现命令行的 getopt()解析,其他的都是笔者所写。 使用: 1,首先把 shell.c ustdio.c 加入工程,这两个文件是必须的。 如果不用 getopt()函数可以不添加getopt.c文件。 shell默认使用链表来构建查询系统,但也提供了平衡二叉树的方式,在shell.h中有开启的开关,如不需要可以不添加avltree.c。 include "shell.h" 2,先对硬件进行基本的初始化。 撰写串口发送函数,形如 void usart_puts(char * str , uint16_t len) ; 调用函数 shell_init("shell >",usart_puts); //初始化shell的输入标志和默认输出指向 串口接收以包为单位。 3,新建全局变量 struct shell_input serial_shell ; 并初始化 SHELL_INPUT_INIT(&serial;_shell ,usart_puts);初始化输入缓存和输出交互; 4,串口接收到一整包函数后,调用 shell_input(&serial;_shell , packet , pktlen) ;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值