一个超级精简高可移植的shell命令行C实现

在嵌入式开发中,一般需要使用shell命令行来进行交互。在一些资源非常受限的平台比如8位单片机平台,则各种开源的shell框架都显得过于大和复杂。此时实现一个超级精简的shell命令行则显得很必要。我们这里就来实现一个,方便以后快速集成到任何平台中使用。

前言

有几点开发前提:

  1. 考虑可移植性,不使用任何编译器扩展。

  2. 考虑可移植性,不依赖任何的库函数,仅依赖stdint的数据类型。

  3. 考虑可移植性, 核心代码无需任何修改,只需要调用初始化配置接口来适配不同平台。

  4. 尽可能少的代码量和ram使用量。

  5. 精简,一切都刚好够用,可以快速嵌入任何平台,针对不同需要可以直接在此基础上增加实现。

实现

首先定义一个数据结构,用于记录命令字符串和对应的执行函数和帮助字符串的对应关系

typedef void (*shell_command_pf)(uint8_t *);                      /**< 命令回调函数   */
typedef uint32_t (*shell_read_pf)(uint8_t *buff, uint32_t len);   /**< 底层收接口     */
typedef void (*shell_write_pf)(uint8_t *buff, uint32_t len);      /**< 底层接口     */

/**
 * \struct shell_cmd_cfg
 * 命令信息
*/
typedef struct
{
    uint8_t * name;          /**< 命令字符串   */
    shell_command_pf func;   /**< 命令回调函数 */ 
    uint8_t * helpstr;       /**< 命令帮助信息 */   
}shell_cmd_cfg;

对接口抽象,初始化指定输入输出接口


/**
 * \fn shell_set_itf
 * 设置底层输入输出接口,以及命令列表
 * 调用shell_exec_shellcmd之前,需要先调用该接口初始化
 * \param[in] input \ref shell_read_pf 输入接口
 * \param[in] output \ref shell_write_pf 输出接口
 * \param[in] cmd_list \ref shell_cmd_cfg 命令列表
 * \param[in] enableecho 0:不使能回显, 其他值:使能回显
*/
void shell_set_itf(shell_read_pf input, shell_write_pf output, shell_cmd_cfg* cmd_list, uint8_t enableecho);

非阻塞,主循环调用以下函数进行处理


/**
 * \fn shell_exec
 * 周期调用该函数,读取底层输入,并判断是否有命令进行处理
 * 非阻塞
*/
void shell_exec(void);

通过以下宏配置缓存大小,这里实际可以修改下改为调用者分配,而不是静态数组,这样可以由调用者决定分配和释放,更灵活,也在不用的时候不占用空间。

#define SHELL_CMD_LEN 64                                          /**< 命令缓冲区大小 */

核心逻辑

shell_exec调用 shell_read_line从输入接口读数据到缓冲区,读到一行后调用

shell_exec_cmdlist查询命令信息表,匹配字符串再调用对应的实现函数。

所以核心代码是读命令行,直接看注释即可


/**
 * 读取一行命令
*/
static uint32_t shell_read_line(void)
{
    uint8_t ch;
    uint32_t count;
    /* 初始打印sh> */
    if(s_cmd_buf_au8[0]=='\r')
    {
        shell_putstring("sh>\r\n");
        s_cmd_buf_au8[0] = 0;
    }

    /* 非阻塞读取一个字符 */
    if(shell_getchar(&ch) !=0 )
    {
        return 0;
    }

    /* 遇到除了退格之外的不可打印字符,则认为收到一行命令 
     * 退格需要单独处理,需要删除一个字符
    */
    if((ch == '\r' || ch == '\n' || ch < ' ' || ch > '~') && (ch != '\b'))
    {
        if(s_cmd_buf_index_u32==0)
        {
            /* 缓冲区没有数据就收到了非打印字符串,则打印提示sh> */
            shell_putstring("sh>\r\n");
        }
        else
        {
            /* 收到了非打印字符,且缓冲区有数据则认为收到了一行
             * 返回缓冲区数据长度,并清零计数,打印回车换行
             * 并且添加结束符0
            */
            count = s_cmd_buf_index_u32;
            s_cmd_buf_au8[s_cmd_buf_index_u32]=0;
            s_cmd_buf_index_u32 =0;
            shell_putstring("\r\n");
            return count;
        }
    }
    else 
    {
        if(ch == '\b') 
        {
            /* 退格处理,注意只有有数据才会删除一个字符,添加结束符 */
            if(s_cmd_buf_index_u32 != 0) 
            {
                s_cmd_buf_index_u32--;
                shell_putchar('\b');
                shell_putchar(' ');
                shell_putchar('\b');
                s_cmd_buf_au8[s_cmd_buf_index_u32]= '\0';
            }
        } 
        else 
        {
            /* 可打印字符,添加到缓冲区
             * 如果数据量已经到了缓冲区大小-1,则也认为是一行命令
             * -1是保证最后有结束符0空间
            */
            if(s_enableecho_u8 != 0)
            {
                shell_putchar(ch);
            }
            s_cmd_buf_au8[s_cmd_buf_index_u32++] = ch;
            if(s_cmd_buf_index_u32>=(sizeof(s_cmd_buf_au8)-1))
            {
                count = s_cmd_buf_index_u32;
                s_cmd_buf_au8[s_cmd_buf_index_u32]=0;
                s_cmd_buf_index_u32 =0;
                shell_putstring("\r\n");
                return count;
            }
        } 
    } 
    return 0;
}

代码量很少,直接看源码即可

shell.c/h为核心代码,无需修改

shell_func.c/h为命令实现,需要自己额添加

shell.c


#include <stdint.h>
#include "shell.h"

shell_read_pf s_input_pf = 0;      /* 输入接口指针     */
shell_write_pf s_output_pf = 0;    /* 输出接口指针     */
shell_cmd_cfg* s_cmd_cfg_pst = 0;  /* 命令列表指针     */
uint8_t s_enableecho_u8 = 0;       /* 是否使能echo标志 */
static uint8_t  s_cmd_buf_au8[SHELL_CMD_LEN]="\r"; /* 命令缓冲区 */
static uint32_t s_cmd_buf_index_u32 = 0;               /* 当前命令缓冲区中字符数 */

/**
 * 输出字符接口
*/
static void shell_putchar(uint8_t val)
{
    uint8_t tmp;
    if(s_output_pf != 0)
    {
        tmp = val;
        s_output_pf(&tmp, 1);
    }
}

/**
 * 输出字符串接口
*/
static void shell_putstring(char* str)
{
    uint32_t len = 0;
    uint8_t*p = (uint8_t*)str;
    while(*str++)
    {
        len++;
    }
    s_output_pf(p, len);
}

/**
 * 读字符接口
*/
static int shell_getchar(uint8_t *data)
{
    if(s_input_pf == 0)
    {
        return -1;
    }
  if(0 == s_input_pf(data, 1))
    {
    return -1;
  }
  else
  {
        return 0;
  }
}

/**
 * 判断命令字符串的长度
 * 命令字符串不能有空格
*/
static uint32_t shell_cmd_len(uint8_t *cmd)
{
    uint8_t *p = cmd;
    uint32_t len = 0;
    while((*p != ' ') && (*p != 0)) 
    {
        p++;
        len++;
    }
    return len;
}

/**
 * 判断两个字符串是否相等,相等返回0
*/
static int shell_cmd_check(uint8_t *cmd, uint8_t *str)
{
    uint32_t len1 = shell_cmd_len(cmd);
    uint32_t len2 = shell_cmd_len(str);
    if(len1 != len2)
    {
        return -1;
    }
    for(uint32_t i=0; i<len1; i++)
    {
        if(*cmd++ != *str++)
        {
            return -1;
        }
    }
    return 0;
}

/**
 * 读取一行命令
*/
static uint32_t shell_read_line(void)
{
    uint8_t ch;
    uint32_t count;
    /* 初始打印sh> */
    if(s_cmd_buf_au8[0]=='\r')
    {
        shell_putstring("sh>\r\n");
        s_cmd_buf_au8[0] = 0;
    }

    /* 非阻塞读取一个字符 */
    if(shell_getchar(&ch) !=0 )
    {
        return 0;
    }

    /* 遇到除了退格之外的不可打印字符,则认为收到一行命令 
     * 退格需要单独处理,需要删除一个字符
    */
    if((ch == '\r' || ch == '\n' || ch < ' ' || ch > '~') && (ch != '\b'))
    {
        if(s_cmd_buf_index_u32==0)
        {
            /* 缓冲区没有数据就收到了非打印字符串,则打印提示sh> */
            shell_putstring("sh>\r\n");
        }
        else
        {
            /* 收到了非打印字符,且缓冲区有数据则认为收到了一行
             * 返回缓冲区数据长度,并清零计数,打印回车换行
             * 并且添加结束符0
            */
            count = s_cmd_buf_index_u32;
            s_cmd_buf_au8[s_cmd_buf_index_u32]=0;
            s_cmd_buf_index_u32 =0;
            shell_putstring("\r\n");
            return count;
        }
    }
    else 
    {
        if(ch == '\b') 
        {
            /* 退格处理,注意只有有数据才会删除一个字符,添加结束符 */
            if(s_cmd_buf_index_u32 != 0) 
            {
                s_cmd_buf_index_u32--;
                shell_putchar('\b');
                shell_putchar(' ');
                shell_putchar('\b');
                s_cmd_buf_au8[s_cmd_buf_index_u32]= '\0';
            }
        } 
        else 
        {
            /* 可打印字符,添加到缓冲区
             * 如果数据量已经到了缓冲区大小-1,则也认为是一行命令
             * -1是保证最后有结束符0空间
            */
            if(s_enableecho_u8 != 0)
            {
                shell_putchar(ch);
            }
            s_cmd_buf_au8[s_cmd_buf_index_u32++] = ch;
            if(s_cmd_buf_index_u32>=(sizeof(s_cmd_buf_au8)-1))
            {
                count = s_cmd_buf_index_u32;
                s_cmd_buf_au8[s_cmd_buf_index_u32]=0;
                s_cmd_buf_index_u32 =0;
                shell_putstring("\r\n");
                return count;
            }
        } 
    } 
    return 0;
}

/**
 * 搜寻命令列表处理命令
*/
static int shell_exec_cmdlist(uint8_t* cmd)
{
    int i;
    if(s_cmd_cfg_pst == 0)
    {
        return -1;
    }
    for (i=0; s_cmd_cfg_pst[i].name != 0; i++)
    {
        if (shell_cmd_check(cmd, s_cmd_cfg_pst[i].name) == 0) 
        {
            s_cmd_cfg_pst[i].func(cmd);
            return 0;
        }            
    } 
    if(s_cmd_cfg_pst[i].name == 0)
    {
        shell_putstring("unkown command\r\n");
        return -1;
    }
    return 0;
}

/**
 * 对外接口,周期执行
*/
void shell_exec(void)
{
    if(shell_read_line() > 0)
    {
        shell_exec_cmdlist(s_cmd_buf_au8);
    }
}

/**
 * 对外接口,初始化配置接口
*/
void shell_set_itf(shell_read_pf input, shell_write_pf output, shell_cmd_cfg* cmd_list, uint8_t enableecho)
{
    s_input_pf = input;
    s_output_pf = output;
    s_cmd_cfg_pst = cmd_list;
    s_enableecho_u8 = enableecho;
}

shell.h


#ifndef SHELL_H
#define SHELL_H

#ifdef __cplusplus
extern "C" {
#endif

#include <stdint.h>

#define SHELL_CMD_LEN 64                                          /**< 命令缓冲区大小 */

typedef void (*shell_command_pf)(uint8_t *);                      /**< 命令回调函数   */
typedef uint32_t (*shell_read_pf)(uint8_t *buff, uint32_t len);   /**< 底层收接口     */
typedef void (*shell_write_pf)(uint8_t *buff, uint32_t len);      /**< 底层发接口     */

/**
 * \struct shell_cmd_cfg
 * 命令信息
*/
typedef struct
{
    uint8_t * name;          /**< 命令字符串   */
    shell_command_pf func;   /**< 命令回调函数 */ 
    uint8_t * helpstr;       /**< 命令帮助信息 */   
}shell_cmd_cfg;

/**
 * \fn shell_exec
 * 周期调用该函数,读取底层输入,并判断是否有命令进行处理
 * 非阻塞
*/
void shell_exec(void);

/**
 * \fn shell_set_itf
 * 设置底层输入输出接口,以及命令列表
 * 调用shell_exec_shellcmd之前,需要先调用该接口初始化
 * \param[in] input \ref shell_read_pf 输入接口
 * \param[in] output \ref shell_write_pf 输出接口
 * \param[in] cmd_list \ref shell_cmd_cfg 命令列表
 * \param[in] enableecho 0:不使能回显, 其他值:使能回显
*/
void shell_set_itf(shell_read_pf input, shell_write_pf output, shell_cmd_cfg* cmd_list, uint8_t enableecho);

#ifdef __cplusplus
}
#endif

#endif

shell_func.c



#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

#include "shell.h"
#include "shell_func.h"
#include "xmodem.h"
#include "uart_api.h"
#include "iot_flash.h"

static void helpfunc(uint8_t* param);
static void rxmemfunc(uint8_t* param);
static void sxmemfunc(uint8_t* param);
static void rxflashfunc(uint8_t* param);
static void sxflashfunc(uint8_t* param);
static void uarttestfunc(uint8_t* param);

/**
 * 最后一行必须为0,用于结束判断
*/
const shell_cmd_cfg g_shell_cmd_list_ast[ ] = 
{
  { (uint8_t*)"help",         helpfunc,         (uint8_t*)"help"}, 
  { (uint8_t*)"rxmem",        rxmemfunc,        (uint8_t*)"rxmem addr len"}, 
  { (uint8_t*)"sxmem",        sxmemfunc,        (uint8_t*)"sxmem addr len"}, 
  { (uint8_t*)"rxflash",      rxflashfunc,      (uint8_t*)"rxflash addr len"}, 
  { (uint8_t*)"sxflash",      sxflashfunc,      (uint8_t*)"sxflash addr len"}, 
  { (uint8_t*)"uarttest",     uarttestfunc,     (uint8_t*)"uarttest"}, 
  { (uint8_t*)0,              0 ,               0},
};

void helpfunc(uint8_t* param)
{
    (void)param;
    unsigned int i;
    printf("\r\n");
    printf("**************\r\n");
    printf("*   SHELL    *\r\n");
    printf("*   V1.0     *\r\n");
    printf("**************\r\n");
    printf("\r\n");
    for (i=0; g_shell_cmd_list_ast[i].name != 0; i++)
    {
        printf("%02d.",i);
        printf("%-16s",g_shell_cmd_list_ast[i].name);
        printf("%s\r\n",g_shell_cmd_list_ast[i].helpstr);
    }
}

uint8_t rxtx_buf[1029];
extern uint32_t g_tick_u32;
static uint32_t getms(void)
{
  return g_tick_u32;
}

static uint32_t io_read(uint8_t* buffer, uint32_t len)
{
  return uart_api_read(buffer, len);
}

static void io_read_flush(void)
{
  uint8_t tmp;
  while(0 != uart_api_read(&tmp, 1));
}

static uint32_t io_write(uint8_t* buffer, uint32_t len)
{
  uart_api_write(buffer, len);
  return len;
}

static uint32_t mem_read(uint32_t addr, uint8_t* buffer, uint32_t len)
{
  memcpy(buffer, (uint8_t*)addr, len);
  return len;
}

static uint32_t mem_write(uint32_t addr, uint8_t* buffer, uint32_t len)
{
  memcpy((uint8_t*)addr, buffer, len);
  return len;
}

static uint32_t flash_read(uint32_t addr, uint8_t* buffer, uint32_t len)
{
  iot_flash_read(IOT_FLASH_SFC_PORT_0, addr, buffer, len);
  return len;
}

static uint32_t flash_write(uint32_t addr, uint8_t* buffer, uint32_t len)
{
  iot_flash_write(IOT_FLASH_SFC_PORT_0, addr, buffer, len);
  return len;
}


void rxmemfunc(uint8_t* param)
{
  uint32_t addr;
  uint32_t len;
  uint8_t* p = param;
  int res = 0;
  //if(3 == sscanf((const char*)param, "%*s %s %d %d", type, &addr, &len))
  while(1)
  {
    if((*p > 'z') || (*p < 'a'))
    {
      break;
    }
    else
    {
      p++;
    }
  }
  while(1)
  {
    if(*p != ' ')
    {
      break;
    }
    else
    {
      p++;
    }
  }
  addr = atoi((const char*)p);
  while(1)
  {
    if((*p > '9') || (*p < '0'))
    {
      break;
    }
    else
    {
      p++;
    }
  }
  while(1)
  {
    if(*p != ' ')
    {
      break;
    }
    else
    {
      p++;
    }
  }
  len = atoi((const char*)p);

  xmodem_cfg_st cfg=
  {
    .buffer = rxtx_buf,
    .crccheck = 1,
    .getms = getms,
    .io_read = io_read,
    .io_read_flush = io_read_flush,
    .io_write = io_write,
    .start_timeout = 60,
    .packet_timeout = 1000,
    .ack_timeout = 1000,
    .mem_write = mem_write,
    .addr = addr,
    .totallen = len,
  };
  xmodem_init_rx(&cfg);
  while((res = xmodem_rx()) == 0);
  printf("res:%d\r\n",res);
}

void sxmemfunc(uint8_t* param)
{
  uint32_t addr;
  uint32_t len;
  uint8_t* p = param;
  int res = 0;
  //if(3 == sscanf((const char*)param, "%*s %s %d %d", type, &addr, &len))
  while(1)
  {
    if((*p > 'z') || (*p < 'a'))
    {
      break;
    }
    else
    {
      p++;
    }
  }
  while(1)
  {
    if(*p != ' ')
    {
      break;
    }
    else
    {
      p++;
    }
  }
  addr = atoi((const char*)p);
  while(1)
  {
    if((*p > '9') || (*p < '0'))
    {
      break;
    }
    else
    {
      p++;
    }
  }
  while(1)
  {
    if(*p != ' ')
    {
      break;
    }
    else
    {
      p++;
    }
  }
  len = atoi((const char*)p);

  xmodem_cfg_st cfg=
  {
    .buffer = rxtx_buf,
    .plen = 1024,
    .getms = getms,
    .io_read = io_read,
    .io_read_flush = io_read_flush,
    .io_write = io_write,
    .start_timeout = 60,
    .packet_timeout = 1000,
    .ack_timeout = 1000,
    .mem_read = mem_read,
    .addr = addr,
    .totallen = len,
  };
  xmodem_init_tx(&cfg);
  while((res = xmodem_tx()) == 0);
  printf("res:%d\r\n",res);
}


void rxflashfunc(uint8_t* param)
{
  uint32_t addr;
  uint32_t len;
  uint8_t* p = param;
  int res = 0;
  //if(3 == sscanf((const char*)param, "%*s %s %d %d", type, &addr, &len))
  while(1)
  {
    if((*p > 'z') || (*p < 'a'))
    {
      break;
    }
    else
    {
      p++;
    }
  }
  while(1)
  {
    if(*p != ' ')
    {
      break;
    }
    else
    {
      p++;
    }
  }
  addr = atoi((const char*)p);
  while(1)
  {
    if((*p > '9') || (*p < '0'))
    {
      break;
    }
    else
    {
      p++;
    }
  }
  while(1)
  {
    if(*p != ' ')
    {
      break;
    }
    else
    {
      p++;
    }
  }
  len = atoi((const char*)p);

  xmodem_cfg_st cfg=
  {
    .buffer = rxtx_buf,
    .crccheck = 1,
    .getms = getms,
    .io_read = io_read,
    .io_read_flush = io_read_flush,
    .io_write = io_write,
    .start_timeout = 60,
    .packet_timeout = 1000,
    .ack_timeout = 1000,
    .mem_write = flash_write,
    .addr = addr,
    .totallen = len,
  };
  xmodem_init_rx(&cfg);
  while((res = xmodem_rx()) == 0);
  printf("res:%d\r\n",res);
}

void sxflashfunc(uint8_t* param)
{
  uint32_t addr;
  uint32_t len;
  uint8_t* p = param;
  int res = 0;
  //if(3 == sscanf((const char*)param, "%*s %s %d %d", type, &addr, &len))
  while(1)
  {
    if((*p > 'z') || (*p < 'a'))
    {
      break;
    }
    else
    {
      p++;
    }
  }
  while(1)
  {
    if(*p != ' ')
    {
      break;
    }
    else
    {
      p++;
    }
  }
  addr = atoi((const char*)p);
  while(1)
  {
    if((*p > '9') || (*p < '0'))
    {
      break;
    }
    else
    {
      p++;
    }
  }
  while(1)
  {
    if(*p != ' ')
    {
      break;
    }
    else
    {
      p++;
    }
  }
  len = atoi((const char*)p);

  xmodem_cfg_st cfg=
  {
    .buffer = rxtx_buf,
    .plen = 1024,
    .getms = getms,
    .io_read = io_read,
    .io_read_flush = io_read_flush,
    .io_write = io_write,
    .start_timeout = 60,
    .packet_timeout = 1000,
    .ack_timeout = 1000,
    .mem_read = flash_read,
    .addr = addr,
    .totallen = len,
  };
  xmodem_init_tx(&cfg);
  while((res = xmodem_tx()) == 0);
  printf("res:%d\r\n",res);
}

void uarttestfunc(uint8_t* param)
{
  (void)param;
  uint8_t tmp[64];
  uint32_t len;
  uint32_t total = 0;
  while(1)
  {
    while(0 != (len = uart_api_read(tmp, sizeof(tmp))))
    {
      uart_api_write(tmp, len);
      total+=len;
    }
    if(total >= 10*1024)
    {
      break;
    }
  }
}

shell_func.h

#ifndef SHELL_FUNC_H
#define SHELL_FUNC_H

#include <stdint.h>

#ifdef __cplusplus
 extern "C" {
#endif
 
extern const shell_cmd_cfg g_shell_cmd_list_ast[ ];
   
#ifdef __cplusplus
}
#endif

#endif

测试

添加命令行只需要在shell_func.c中先申明

实现函数,比如

static void helpfunc(uint8_t* param);

然后在数组g_shell_cmd_list_ast中添加一行

用于指定命令行字符和对应的实现函数,以及帮助字符串。

比如

{ (uint8_t*)"help",         helpfunc,         (uint8_t*)"help"},

然后实现函数


void helpfunc(uint8_t* param)
{
  (void)param;
  unsigned int i;
  printf("\r\n");
  printf("**************\r\n");
  printf("*   SHELL    *\r\n");
  printf("*   V1.0     *\r\n");
  printf("**************\r\n");
  printf("\r\n");
  for (i=0; g_shell_cmd_list_ast[i].name != 0; i++)
  {
      printf("%02d.",i);
      printf("%-16s",g_shell_cmd_list_ast[i].name);
      printf("%s\r\n",g_shell_cmd_list_ast[i].helpstr);
  }
}

然后初始化接口,执行


  uart_bsp_init(115200);
  shell_set_itf(uart_api_read, uart_api_write, (shell_cmd_cfg*)g_shell_cmd_list_ast, 1);
  while(1)
  {
      shell_exec();
  }

此时就可以输入对应命令字符串回车来执行对应的函数,比如

总结

以上以最简单的方式,实现了命令行shell。代码量和占用ram非常小,可以快速嵌入资源非常受限的开发平台。

Shell.c和shell.h完全可移植,无需任何修改。

只需初始化shell_set_itf设置对应的底层接口,然后shell_func.c中添加对应的命令实现即可。

非阻塞方式实现,方便应用。

以最简形式实现,支持退格,但是不支持历史命令等,但是可以快速的修改代码实现。

命令行以数组静态添加,而不是动态添加和使用编译器的扩展将代码放置于指定段的方式来添加,是因为考虑可移植性和简单性。也可以快速修改使用后者。

有很多人总是在感慨为什么嵌入式领域热衷于”造轮子”,这是由于其特定的需求决定的,在嵌入式领域有诸多前提条件的限制,比如资源就是一个必须要考虑的前提,所以很难一个”轮子”适应所有路况。比如在8位单片机上几k的ram,十几k的rom,这时候也要使用比如shell命令行,那么就不可能使用大而全的框架,则存储资源,运行占用CPU等等都会无法满足。。甚至编译器都是专用针对嵌入式场景的,可能都不能用各种花哨的处理方式编译器扩展等(比如获取段地址,大小,指定代码位于某个段),另外在嵌入式领域编译器相关的扩展也会影响可移植性,其实不建议过多使用。

正是因为有这么多的限制所以在嵌入式领域才会有这么的多需要定制开发,重复”造轮子”的事,而积累自己的轮子,积累自己的小代码库,组件库,则是嵌入式高手积累经验的一条路径。积累了足够多的自己的轮子,将来面对需求就能够得心应手,才能快速开发出不同需求的”轮子”。