嵌入式shell介绍和学习

前言

在 Linux 中,Shell 是一个应用程序 ,他是用户与 Linux 内核沟通的桥梁。

它负责接收用户输入的命令,根据用户的输入找到其他程序并运行,Shell负责将应用层或者用户输入的命令传递给系统内核,由操作系统内核来完成相应的工作,然后将结果反馈给应用层或者用户。

而在STM32中也可以通过串口模拟也同样实现此类效果,在一个项目的开发阶段,一些嵌入式算法或者流程都可以通用此类方式调试,非常方便也容易找出错误。

① letter-shell

本期主要介绍的是 letter-shell,一个功能强大的嵌入式shell,作者NevermindZZT,目前收获 155 个star,遵循 MIT 开源许可协议。

letter shell 3.0是一个C语言编写的,可以嵌入在程序中的嵌入式shell,通俗一点说就是一个串口命令行,可以通过命令行调用、运行程序中的函数。

简介

letter shell是一个C语言编写的,可以嵌入在程序中的嵌入式shell,主要面向嵌入式设备,以C语言函数为运行单位,可以通过命令行调用,运行程序中的函数

相对2.x版本,letter shell 3.x增加了用户管理,权限管理,以及对文件系统的初步支持

此外3.x版本修改了命令格式和定义,2.x版本的工程需要经过简单的修改才能完成迁移

若只需要使用基础功能,可以使用letter shell 2.x版本

使用说明可参考Letter shell 3.0 全新出发

如果从3.0版本迁移到3.1以上版本,请注意3.1版本对读写函数原型的修改

shell_info

功能

  • 命令自动补全
  • 快捷键功能定义
  • 命令权限管理
  • 用户管理
  • 变量支持
  • 代理函数和参数代理解析

② 移植和使用

1.移植教程

这里测试的硬件平台是STM32F411CEx,这里利用STM32CubeMX生成了带FreeRTOS操作系统的基础工程,且主要初始化了USART1的外设,因为letter-shell是需要利用串口来进行命令行交互的。简单创建两个任务即可。

创建基础工程

注意:

  1. 与之交互的串口工具必须支持相应的协议,建议使用终端软件如secureCRT或MobaXterm
  2. letter-shell是可以裸机开发的,所以即使无操作系统也是可以,这里增加操作系统注意是为了为了体现其功能性。

1688479016713

添加代码到工程

将下载的代码里的src文件夹里的所有文件拷贝到工程目录下,并且创建两个新文件shell_port.cshell_port.h

1688479491986

并在KEIL中添加文件到工程上去:

1688479562290

驱动和配置修改

shell_port.cshell_port.h里编写驱动代码

/**
 * @file shell_port.c
 * @author Letter (NevermindZZT@gmail.com)
 * @brief 
 * @version 0.1
 * @date 2019-02-22
 * 
 * @copyright (c) 2019 Letter
 * 
 */
/* Include files -------------------------------------------------------------*/
#include "shell.h"
#include "usart.h"
#include "shell_port.h"

/* Private macro -------------------------------------------------------------*/
#define SHELL_BUFFER_SIZE 512

/* 创建shell对象 */
Shell shell;
/* 开辟shell缓冲区 */
static char shell_buffer[SHELL_BUFFER_SIZE];
static uint8_t rx_buffer = 0;
static uint8_t g_uart_rec_lock = 0;

/* Private function implementation ------------------------------------------*/

/**
 * @brief 用户shell写
 * 
 * @param data 数据
 * @param len 数据长度
 * 
 * @return short 实际写入的数据长度
 */
signed short User_Shell_Write(char *data, unsigned short len)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)data, (uint16_t)len, 1000);
    return len;
}


/**
 * @brief 用户shell读
 * 
 * @param data 数据
 * @param len 数据长度
 * 
 * @return short 实际读取到
 */
signed short User_Shell_Read(char *data, unsigned short len)
{
    if(g_uart_rec_lock > 0) {
        g_uart_rec_lock = 0;
        *data = rx_buffer;
        return len;
    }
    return 0;
}

/**
 * @brief 用户shell初始化
 * 
 */
void User_Shell_Init(void)
{
    shell.write = User_Shell_Write;
    shell.read = User_Shell_Read;
    
    /* 开启中断 */
    HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);

    shellInit(&shell, shell_buffer, SHELL_BUFFER_SIZE);
}


/**
 * @description: 串口接收中断回调,接收shell交互数据
 * @return {*}
 * @param {UART_HandleTypeDef} *huart 串口
 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)
    {
        g_uart_rec_lock++;
        /* 开启中断 */
        HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);
    }
}

/**
 * @file shell_port.h
 * @author Letter (NevermindZZT@gmail.com)
 * @brief 
 * @version 0.1
 * @date 2019-02-22
 * 
 * @copyright (c) 2019 Letter
 * 
 */

#ifndef __SHELL_PORT_H__
#define __SHELL_PORT_H__

#include "shell.h"

extern Shell shell;

void User_Shell_Init(void);
#endif

打开 shell_cfg.h更改几处配置

#ifndef SHELL_TASK_WHILE
/**
 * @brief 是否使用默认shell任务while循环
 *        使能此宏,则`shellTask()`函数会一直循环读取输入,一般使用操作系统建立shell
 *        任务时使能此宏,关闭此宏的情况下,一般适用于无操作系统,在主循环中调用`shellTask()`
 */
#define     SHELL_TASK_WHILE            0 //这里最好为0,若为rtos,在已有循环情况下这里为1容易两个while(1)重复了
#endif /** SHELL_TASK_WHILE */

#ifndef SHELL_GET_TICK
/**
 * @brief 获取系统时间(ms)
 *        定义此宏为获取系统Tick,如`HAL_GetTick()`
 * @note 此宏不定义时无法使用双击tab补全命令help,无法使用shell超时锁定
 */
#define     SHELL_GET_TICK()            HAL_GetTick() //根据需要设置
#endif /** SHELL_GET_TICK */

#ifndef SHELL_DEFAULT_USER
/**
 * @brief shell默认用户
 */
#define     SHELL_DEFAULT_USER          "letter"//根据需要修改名称
#endif /** SHELL_DEFAULT_USER */

#ifndef SHELL_USING_FUNC_SIGNATURE
/**
 * @brief 使用函数签名
 *        使能后,可以在声明命令时,指定函数的签名,shell 会根据函数签名进行参数转换,
 *        而不是自动判断参数的类型,如果参数和函数签名不匹配,会停止执行命令
 */
#define     SHELL_USING_FUNC_SIGNATURE  1
#endif /** SHELL_USING_FUNC_SIGNATURE */

这样驱动就移植完成了,在Task里或者Main中直接调用即可:

#include "shell_port.h"

void Shell_Task(void *argument)
{
  /* USER CODE BEGIN Shell_Task */
  User_Shell_Init();
  /* Infinite loop */
  for(;;)
  {
    shellTask(&shell);
  }
  /* USER CODE END Shell_Task */
}

2.测试使用

移植,上电测试,接上串口效果如图:

1688800262106

输入help查看所有命令:

1688800425430

这里增加了3个命令,分别为:

  • 传入结构体,修改全局变量数值
  • 读取全局变量结果
  • 将当前系统中所有任务列举出来,并展示剩余堆栈空间

相关测试代码如下:

typedef struct pid_def
{
  /* data */
  float P;
  float I;
  float D;
}PID;

PID user_pid = {1.2,1.3,1.4};
int set_pid(PID *new_pid)
{
  user_pid.P = new_pid->P;
  user_pid.I = new_pid->I;
  user_pid.D = new_pid->D;
  printf("new pid: P:%f I:%f D:%f\r\n",user_pid.P,user_pid.I,user_pid.D);
  return 0;
}
SHELL_EXPORT_CMD_AGENCY(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC),set_pid,set_pid,set pid param,&(PID){SHELL_PARAM_FLOAT(p1),SHELL_PARAM_FLOAT(p2),SHELL_PARAM_FLOAT(p3)});


int read_pid(void)
{
  printf("user pid: P:%f I:%f D:%f\r\n",user_pid.P,user_pid.I,user_pid.D);
  return 0;
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), read_pid, read_pid, read pid test);

int list_task(void)
{
  printf("    thread        pri    status        surplus stack  \r\n");
  printf("-------------    ----- ----------     --------------  \r\n");
  //获取当前运行的线程数
  uint32_t array_items = osThreadGetCount();
  osThreadId_t* thread_array = (osThreadId_t*)calloc(array_items,sizeof(osThreadId_t));
  if(thread_array == NULL)
  {
    printf(" %s-%d calloc fail!\r\n",__FILE__,__LINE__);
    return 1;
  }
  //枚举活动线程
  osThreadEnumerate(thread_array,array_items);
  //遍历输出
  osPriority_t  pri;//优先级
  osThreadState_t status; //状态
  //uint32_t stack_size;//堆栈大小
  uint32_t stack_space;//剩余堆栈大小
  for(int i = 0;i<array_items;i++)
  {
    pri = osThreadGetPriority(thread_array[i]);
    status = osThreadGetState(thread_array[i]);
    //stack_size = osThreadGetStackSize(thread_array[i]);
    stack_space = osThreadGetStackSpace(thread_array[i]);


    printf("%-*.*s  %3d  ",configMAX_TASK_NAME_LEN,configMAX_TASK_NAME_LEN,osThreadGetName(thread_array[i]),pri);

    if(status == osThreadInactive)          printf(" Inactive      ");
    else if(status == osThreadReady)        printf(" Ready         ");
    else if(status == osThreadRunning)      printf(" Running       ");
    else if(status == osThreadBlocked)      printf(" Blocked       ");
    else if(status == osThreadTerminated)   printf(" Terminated    ");
    else if(status == osThreadError)        printf(" Error         ");
    else if(status == osThreadReserved)     printf(" optimization  ");
    printf(" 0x%08x \r\n",stack_space);
  }
  printf("\r\n");
  return 0;
}

SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), ps, list_task, List threads in the system);

其中SHELL_EXPORT_CMD_AGENCYSHELL_EXPORT_CMD两个宏定义的数据可以增加在shell上的命令。

三个命令测试效果如下:

1688800833031

③ 应用场景和优势

在一些项目需要前期调试参数的时候,或者是一些算法需要测试的时候,shell的功能就非常的强大了,可以在不影响主要功能的情况的下,调用系统中的相关应用接口,并且测试输出结果。总体来说是一个非常不错的应用工具。在某些RTOS里已经集成类似的功能,如RT-Thread等。

后记

最近发现了有许多通用嵌入式的开源软件,准备最近都测试一下,提高一下自己使用工具的能力,能够更加快捷方便的调试嵌入式设备。

参考文章

letter-shell | 一个功能强大的嵌入式shell_letter shell_Mculover666的博客-CSDN博客

posted @ 2023-07-08 15:28  一月一星辰  阅读(588)  评论(0编辑  收藏  举报