sunsili 发表于 2023-7-20 10:49:24

一款运行在MCU平台上的串口命令控制台xc_shell的实现

一款运行在MCU平台上的串口命令控制台xc_shell的实现


1. 前言

玩过Linux的朋友, 是不是对Linux无所不能的串口Shell命令控制台羡慕不已, 要是自己做的STM32F系列低档次的MCU也有这种控制交互能力, 会给调试/维护和配置省下多少麻烦事呀, 比如启动/关闭调试或自检模式, 打印调试信息, 配置系统参数, 传输文件等等, 也有相当多的朋友凭借自己出色的编程能力可以实现这些功能, 这里提出我的这个解决方案, 以作交流.
本平台(xc_shell)具备以下性能特点:
[*]大量主要代码, 和具体硬件无关, 移植性强,代码文件少.
[*]只有在处理用户的输入命令时, 才占用CPU资源, 且代码可裁剪到1KB SRAM和4KB Flash;
[*]用户可以非常灵活的添加按模板编写的命令脚本文件, 自定义扩张能力强.
[*]支持操作系统和非操作系统两种场景应用.
[*]支持Ymodem文件传输协议
[*]支持将Flash的扇区开辟为参数区, 可实现本地/远程升级。
[*]实用Led灯信号管理, 可将65535虚拟信号灯选择输出到1个实体LED灯上, 调试时序和状态非常有用
[*]拥有基础的LED管理, 调试模式设置, 命令帮助指令, 复位指令等基础功能
功能越多设计会越复杂, 为了解释清楚代码, 先向大家解释一下以上功能的基础实现原理, 并提供一个最小的的源码工程。
2. xc_shell平台介绍

2.1 如何实现硬件无关
类比Linux会发现, 设备的硬件接口往往会被虚拟成一个文件(驱动), 而Linux内核完全与硬件系统无任何字节关联, 不同平台驱动不同而已, 故而本xc_shell的串口驱动也采用了相似的思路:1 ) 串口驱动用一个结构体描述, 这样只需在xc_shell.c中用指针指向这个TTYx_HANDLE结构体对象就可以将串口(tty)硬件与内核联系在一起, 聪明的朋友可能会想到, 假如我将带网络的开发板按此结构体,虚拟一个TTY对象, 岂不是就可以实现一个网络远程控制台了!这点确实是可以的!2) 当然诸如多TTY串口实现接口互换等, 都是一个指针和step2中的注入回调处理交换的问题。3)用户在使用api_TxdFrame或api_TxdByte时”bsp_ttyX.c“,会驱动具体MCU的串口将数据发送出去, 收到一帧数据后,若用户设置了inj_RcvFrame回调处理方法,则会在中断中执行用户的回调处理。/*---------------------*
*   指正函数定义
*----------------------*/
typedef void    (*pvFunDummy)(void);

//输入整行,输出逻辑
typedef void    (*pvFunVoid) (void);
typedef void    (*pvFunBool) (bool   bVal);
typedef void    (*pvFunChar) (uint8_tcVal);
typedef void    (*pvFunShort)(uint16_t sVal);
typedef void    (*pvFunWord) (uint32_t wVal);

//输入整行,输出逻辑
typedef bool    (*pbFunVoid) (void);
typedef bool    (*pbFunBool) (bool   bVal);
typedef bool    (*pbFunChar) (uint8_tcVal);
typedef bool    (*pbFunShort)(uint16_t sVal);
typedef bool    (*pbFunWord) (uint32_t wVal);

//输入整形指针,输出逻辑
typedef bool    (*pbFun_pVoid) (void * pVoid);
typedef bool    (*pbFun_pChar) (uint8_t* pStr);
typedef bool    (*pbFun_pShort)(uint16_t * pShor);
typedef bool    (*pbFun_pWord) (uint32_t * pWord);

//输入数据帧,输出逻辑
typedef bool    (*pbFun_Buffx)(void * pcBuff, uint16_t len );
typedef bool    (*pbFun_Bytex)(uint8_t * pcByte, uint16_t len );
/*---------------------*
*    TTYx 句柄结构
*----------------------*/
typedef struct TTYx_HANDLE_STRUCT
{
    const char* const name;       //驱动器名
    const uint16_t      rxSize;   //接收大小
    const uint16_t      txSize;   //发送大小
   
    //------------------------------------------------------
    //step1: 用户可用API
    const pvFunWord   init;         //初始化.
    const pbFun_Bytex   api_TxdFrame;   //发送数据帧. (发送帧)
    const pbFunChar   api_TxdByte;    //发送数据字节
   
    //------------------------------------------------------
    //step2: 注入回调函数
    pbFun_Bytex         inj_RcvFrame;   //(ISR)接收数据帧. (接收帧)
    pvFunDummy          inj_TxdReady;   //(ISR)发送完毕回调
   
    //------------------------------------------------------
    //step3: 接收回调函数
    struct TTYx_HANDLE_STRUCT * pvNext; //连接到下一个指令
}TTYx_HANDLE;

[*]可注入的命令脚本(CLI)实现

命令CLI也是一个结构体对象:/*---------------------*
*       CLI指令
*----------------------*/
typedef struct
{
const char * constpcCmdStr;   //指令字符串(只能为小写字母)
const char * constpcHelpStr;   //指令描述,必须以:"\r\n结束". 比如:"help: Returns a list\r\n".
const pFunHook      pxCmdHook;   //指向回调函数的指针,处理成功返回真否者返回0;
uint8_t             ucExpParam;   //指令期望的参数个数
const MEDIA_HANDLE *phStorage;      //指向存储介质,没有的话填充NULL
}Cmd_Typedef_t;
各位朋友可能会使用到非常多的自定义CLI命令, 格式诸如这个网卡的命令:const Cmd_Typedef_t CLI_WizMsg=
{
    //识别关键字
    .pcCmdStr   = "wiz",
    //帮助内容
    .pcHelpStr=
    "\r\n"
" wiz help\r\n"
" wiz rd info\r\n"
" wiz reset\r\n"
" wiz wr ip <D0>.<D1>.<D2>.<D3>\r\n"
" wiz wr mask <D0>.<D1>.<D2>.<D3>\r\n"
" wiz wr way <D0>.<D1>.<D2>.<D3>\r\n"
" wiz wr mac <H0>-<H1>-<H2>-<H3>-<H4>-<H5>\r\n"
" wiz wr port <udp> <bak> <vol> <pic>\r\n"
" wiz wr sip <D0>.<D1>.<D2>.<D3> <port>\r\n"
" wiz wr cip <D0>.<D1>.<D2>.<D3> <port>\r\n"
" wiz load default\r\n"
"\r\n"
" wiz loop open\r\n"
" wiz loop close\r\n"
"\r\n",

//处理函数
.pxCmdHook= &Shell_WIZ_Service,      //见实体函数

//附带数据
.ucExpParam = 0,

#ifdef SHELL_USE_YMODEM
    //存储介质
    .phStorage= NULL,
#endif
};
/*---------------------*
*       CLI 链表节
*----------------------*/
Cmd_List_tWizList   = { &CLI_WizMsg,   NULL,}; //Shell指令的头
如配置IP地址输入“wiz wr ip 192.168.1.250\r\n”则可以了
3. 环境准备


3.1 硬件开发环境
[*]STM32F103系列开发板一块, 带USART1接口。
[*]USB转串口线一根。

3.2 软件开发环境

[*]MDK4.72或以上
[*]SecureCRT串口超级终端

3.3 软件配置

在xc_shell使用“/r/n”作为命令的结束符, 而SecureCRT按下Enter不是输入“/r/n”故而需要按下图设置:此外在《终端》/仿真/高级中选取【本地回显】

4. 代码介绍

4.1 目录结构

XC_SHELL
├──┬── BSP_LIB    BSP库,硬件相关驱动代买
│├──── bsp_ledx.c   基础LED驱动      
│└──── bsp_tty0.c   调试串口驱动

├──┬──MDK-ARM   工程文件
│└──── Project.uvproj

├──┬──SHELL_CFG   SHELL配置头文件
│└──── user_eval.h

├──┬──SHELL_CORESHELL内核文件
│├──── xc_shell.c   SHELL内核文件
│├──── xc_ymodem.cYmodem传输协议(默认关闭,在xc_shell.h中启动)
│├──── xc_iap.c   Flash的IAP操作,需要bsp_flash.c驱动支持
│└──── shell_iap.cshell的用户脚本模板

├──┬──SHELL_INC   SHELL头文件
│├──── bsp_type.h   驱动结构定义
│├──── xc_shell.h   SHELL头文件
│└──── xconfig.h    硬件无关配置文件

├──┬──STM32F10x_StdPeriph_Lib_V3.5.0STM32的标准外设库
│└──── ......

└──┬──USER      用户文件   
   ├─ .....      
   └──── main.c      main文件


4.2 工程设置要点

1) 设置使用微库:
2)配置包含和路径,注意使用了“--c99”标准,如图
3) 编译工程,无错误警告后烧写程序到开发板运行。
4.3 最终效果

按图输入一下指令,SHELL平台会回复相关信息。其中输入“led set 0=1”会将信号1分配到物理LED0上;输入“led set 0=2”会将信号2分配到物理LED0上。这样用户编写程序代码时相当于拥有了超级多的LED信号可用,调试时序非常有用。
5. 添加自己的指令脚本

5.1 源代码示例

假设我要编写一个自己的指令脚本, 来读取MCU的关键信息,关键字为mcu, 文件命名为shell_mcu.c;当输入“mcu rd 0”时显示MCU的FLASH大小,输入“mcu rd 1”时读取MCU的唯一ID信息。shell_mcu.c源代码:/*********************************Copyright (c)*********************************
**                              
**                                 FIVE工作组
**
**---------------------------------File Info------------------------------------
** File Name:               shell_mcu.c
** Last modified Date:      2017/9/17 15:13:57
** Last Version:            V1.0
** Description:             shell测试
**
**------------------------------------------------------------------------------
** Created By:            wanxuncpx
** Created date:            2017/9/17 15:14:08
** Version:               V1.0
** Descriptions:            none
**------------------------------------------------------------------------------
** HW_CMU:                  STM32F103
** Libraries:               STM32F10x_StdPeriph_Lib_V3.5.0
** version                  V3.5
*******************************************************************************/

/******************************************************************************
更新说明:
******************************************************************************/

/******************************************************************************
*********************************编 译 控 制 ********************************
******************************************************************************/
#define MCU_SHELL               //注释掉时屏蔽iap shell功能

#include "xc_shell.h"       //Shell支持文件,含bool,uint8_t..以及串口数据收发操作
/******************************************************************************
********************************* 文件引用部分 ********************************
******************************************************************************/
/*---------------------*
*   模块驱动引用
*----------------------*/
//#include "net_w5500.h"

#ifdef MCU_SHELL
/******************************************************************************
********************************** Shell实例 **********************************
******************************************************************************/
/*---------------------*
*      CLI指令服务
*----------------------*/
extern bool Shell_MCU_Service(void * pcBuff, uint16_t len );

/*---------------------*
*       CLI 结构
*----------------------*/
const Cmd_Typedef_t CLI_McuMsg=
{
    //识别关键字
    "mcu",
   
    //帮助内容
"\r\n"
" mcu rd <d>\t\t- Read FLASH information.\r\n"
"\r\n",

//处理函数
&Shell_MCU_Service,

//附带数据
0,
#ifdef SHELL_USE_YMODEM
    //存储介质
    NULL,
#endif
};

/*---------------------*
*   CLI链表节(输出)
*----------------------*/
Cmd_List_tMcuList= {&CLI_McuMsg      ,NULL}; //IAP指令链表

/******************************************************************************
********************************* 函 数 声 明 *********************************
******************************************************************************/
/******************************************************************************
/ 函数功能:STM32F103控制函数
/ 修改日期:2015/7/14 20:22:02
/ 输入参数:none
/ 输出参数:none
/ 使用说明:需要执行约10s
******************************************************************************/
static bool FLASH_ioctl(uint8_t cmd,void * param)
{
    #define UID_ADDR            0x1FFFF7E0//闪存容量寄存器,值对应KB单位
    #define MAC_ADDR            0x1FFFF7E8//MCU的唯一ID号,共12个字节
    #define UID_SIZE            2         //UID的字节数
    #define MAC_SIZE            12          //MAC的字节数

    //step1: 检查参数
    if(!param)return false;
      
    //step2: 处理数据
    switch(cmd){
      case 0 : {       //获取FLASH的的UID
      uint16_t * ptDst = (uint16_t *)((uint32_t)param+1);
      
      *ptDst = *(uint16_t *)UID_ADDR;
      *(uint8_t*)param =UID_SIZE;
      return true;
      }
      case 1 : {       //获取芯片的MAC地址
      uint32_t * ptDst = (uint32_t *)((uint32_t)param+1);
      uint32_t * ptSrc = (uint32_t *)MAC_ADDR;
   
      *ptDst++ = *ptSrc++;
      *ptDst++ = *ptSrc++;
      *ptDst++ = *ptSrc++;
      *(uint8_t*)param = MAC_SIZE;
      return true;
      }
      default:return false;
    }
}

/******************************************************************************
/ 函数功能:文件系统Shel指令处理
/ 修改日期:2013/9/10 19:04:15
/ 输入参数:输入当前的程序版本
/ 输出参数:none
/ 使用说明:none
******************************************************************************/
bool Shell_MCU_Service(void * pcBuff, uint16_t len )
{
    uint8_t    *ptRxd;          //用于接收指令处理
    int         i;
    uint16_t    retval;
    uint8_t   buff;
   
    //处理指令
    //--------------------------------------------------------------------------
    ptRxd = (uint8_t *)pcBuff;
   
    if(StrComp(ptRxd,"rd ")) //读取FLASH信息
    {
      int wval;
      
      if(1 != sscanf((void *)ptRxd,"%*s%d",&wval) )return false;
      if( wval>2 )return false;
      if(0==wval) {
            FLASH_ioctl(0,buff);
            retval = *(uint16_t *)(buff+1) ;
            printf("->Flash:\t%dKB\r\n",retval);
            return true;
      }
      else if(1==wval) {
            FLASH_ioctl(1,buff);
            printf("->MAC:\t ");
            for(i=0; i<buff-1; i++){printf("%02X-",buff);}
            printf("%02X\r\n",buff);
            return true;
      }
      else return false;
    }
    else if(StrComp(ptRxd,"help\r\n"))      //指令帮助
    {
      shell_SendStr((void *)CLI_McuMsg.pcHelpStr);
      return true;
    }
    else return false;
}

/******************************************************************************
***********************************   END************************************
******************************************************************************/
#endif
5.2 实现步骤
1) 将该文件添加到工程下。2) 在main.c中用extern 引用McuList,源代码为:/*---------------------*
*   Shell指令链表
*----------------------*/
extern Cmd_List_tMcuList;
3)在main.c初始化时添加://----------------------------------------------------------
//step1: shell初始化
shell_Init(115200,ledx_cfg);      //初始化shell接口
CLI_AddCmd(&McuList);   //添加模块指令到链表
4)编译工程文件。
5)下载到开发板运行即可在终端下看到新支持的CLI指令:


页: [1]
查看完整版本: 一款运行在MCU平台上的串口命令控制台xc_shell的实现