C基础 -- SHELL_EXPORT_CMD(_attr, _name, _func, _desc)宏注册函数详解

1、简介

最近在移植letter-shell这个shell组件到MCU中,发现可以使用SHELL_EXPORT_CMD宏注册相关命令,而不需要先定义数组,这大大方便了代码的开发,进一步分析发现SHELL_EXPORT_CMD主要使用了__attribute__、##、#等参数,借此机会对这个几个参数做一下详细说明,本文以letter-shell中的SHELL_EXPORT_CMD宏为例展开描述,以keil编译环境(arm_cc)说明,gcc编译器有所差别。

2、语法解析

  • 实际使用示例
void cmd_test(void)
{
    
    
	printf("Hello world ! \dn");
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), cmdTest, cmd_test, test cmd);

这个命令就注册了一个cmdTest命令,当我们输入cmdTest时,就会执行到void cmd_test(void)函数,也就是打印“Hello world !”;

SHELL_EXPORT_CMD(_attr, _name, _func, _desc) 定义如下:

#define SHELL_EXPORT_CMD(_attr, _name, _func, _desc) \
          const char shellCmd##_name[] = #_name; \
          const char shellDesc##_name[] = #_desc; \
          SHELL_USED const ShellCommand \
          shellCommand##_name SHELL_SECTION("shellCommand") =  \
          {
      
       \
              .attr.value = _attr, \
              .data.cmd.name = shellCmd##_name, \
              .data.cmd.function = (int (*)())_func, \
              .data.cmd.desc = shellDesc##_name \
          }

以下说明以SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0)|SHELL_CMD_TYPE(SHELL_TYPE_CMD_FUNC), cmdTest, cmd_test, test cmd);为例介绍:
_attr:这个参数代表cmdTest的执行权限;
_name:命令名,我们输入的时候输入的是“cmdTest”字符串,看看这个字符串是怎么由cmdTest转化来的;
_func:函数,这里指cmd_test;
_desc:命令描述,这里指“test cmd”;

在详细介绍这个命令之前,将SHELL_EXPORT_CMD(_attr, _name, _func, _desc)用到的宏或者结构体在下面贴出来;

  • 使用到的数据结构体,ShellCommand结构体:
/**
 * @brief shell command定义
 */
typedef struct shell_command
{
    
    
    union
    {
    
    
        struct
        {
    
    
            unsigned char permission : 8;                       /**< command权限 */
            ShellCommandType type : 4;                          /**< command类型 */
            unsigned char enableUnchecked : 1;                  /**< 在未校验密码的情况下可用 */
            unsigned char disableReturn : 1;                    /**< 禁用返回值输出 */
            unsigned char  readOnly : 1;                        /**< 只读 */
            unsigned char reserve : 1;                          /**< 保留 */
            unsigned char paramNum : 4;                         /**< 参数数量 */
        } attrs;
        int value;
    } attr;                                                     /**< 属性 */
    union
    {
    
    
        struct
        {
    
    
            const char *name;                                   /**< 命令名 */
            int (*function)();                                  /**< 命令执行函数 */
            const char *desc;                                   /**< 命令描述 */
        } cmd;                                                  /**< 命令定义 */
        struct
        {
    
    
            const char *name;                                   /**< 变量名 */
            void *value;                                        /**< 变量值 */
            const char *desc;                                   /**< 变量描述 */
        } var;                                                  /**< 变量定义 */
        struct
        {
    
    
            const char *name;                                   /**< 用户名 */
            const char *password;                               /**< 用户密码 */
            const char *desc;                                   /**< 用户描述 */
        } user;                                                 /**< 用户定义 */
        struct
        {
    
    
            int value;                                          /**< 按键键值 */
            void (*function)(Shell *);                          /**< 按键执行函数 */
            const char *desc;                                   /**< 按键描述 */
        } key;                                                  /**< 按键定义 */
    } data; 
} ShellCommand;
  • 使用到的宏:
    SHELL_USED
#define SHELL_USED                      __attribute__((used))

SHELL_SECTION(x)

#define SHELL_SECTION(x)                __attribute__((section(x)))
  • 命令详细解析
    下面对SHELL_EXPORT_CMD(_attr, _name, _func, _desc)逐行介绍,先介绍第1、2行代码之前,先介绍##和#的作用:
 ## 连接符:在带参数的宏定义中,  用来将两个Token连接为一个Token ,从而形成一个新的子串。 注意这里连接的对象是Token就行,而不一定是宏的变量。
#符:是将其后面的宏参数进行字符串化操作(Stringfication),即把宏参数变为一个字符串,简单说就是在对它所引用的宏变量 通过替换后在其左右各加上一个双引号。#符,也就是把传递过来的参数当成字符串进行替代。
const char shellCmd##_name[] = #_name;

那么这行代码的就可以翻译为const char shellCmd_name[] = “_name”,当然需要注意的是_name需要将传进来的实际_name代替。第二行代码同理(const char shellDesc##_name[] = #_desc;);

下面介绍最后一部分:

 SHELL_USED const ShellCommand \
          shellCommand##_name SHELL_SECTION("shellCommand") =  \
          {
    
     \
              .attr.value = _attr, \
              .data.cmd.name = shellCmd##_name, \
              .data.cmd.function = (int (*)())_func, \
              .data.cmd.desc = shellDesc##_name \
          }

先介绍修饰符 SHELL_USED:

#define SHELL_USED                      __attribute__((used))

used意义如下:

unused:表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息。
used: 向编译器说明这段代码有用,即使在没有用到的情况下编译器也不会警告

定义的变量ShellCommand :

const ShellCommand \
          shellCommand##_name

这个变量可以展开为 const ShellCommand shellCommand_name,注意ShellCommand 是结构体;

再介绍修饰符SHELL_SECTION:

#define SHELL_SECTION(x)                __attribute__((section(x)))

这个宏代表可以将变量添加到某个输入段中,那么结合上面的代码,就可以理解为将shellCommand_name这个变量放入到了shellCommand这个输入段中,这个时候查看.map文件就可以发现文件里面多了一个名为shellCommand_name.shellCommand的输入段,可见shellCommand就是这个输入段后缀。

=  \
          {
    
     \
              .attr.value = _attr, \
              .data.cmd.name = shellCmd##_name, \
              .data.cmd.function = (int (*)())_func, \
              .data.cmd.desc = shellDesc##_name \
          }

这一部分代码主要是对定义的常量shellCommand_name进行初始化操作;

3、 如何执行

介绍到这里,我们基本可以知道SHELL_EXPORT_CMD(_attr, _name, _func, _desc)宏定义了一个常量,并且将这个常量放入输入段中,那么这个输入段怎么才能被执行到呢?要做到这个,我们必须要获取到shellCommand这个输入段的起始地址,不同编译获取这个输入段的起始地址的方法如下:

     #if defined(__CC_ARM) || (defined(__ARMCC_VERSION) && __ARMCC_VERSION >= 6000000)
        shell->commandList.base = (ShellCommand *)(&shellCommand$$Base);
        shell->commandList.count = ((unsigned int)(&shellCommand$$Limit)
                                - (unsigned int)(&shellCommand$$Base))
                                / sizeof(ShellCommand);

    #elif defined(__ICCARM__) || defined(__ICCRX__)
        shell->commandList.base = (ShellCommand *)(__section_begin("shellCommand"));
        shell->commandList.count = ((unsigned int)(__section_end("shellCommand"))
                                - (unsigned int)(__section_begin("shellCommand")))
                                / sizeof(ShellCommand);
    #elif defined(__GNUC__)
        shell->commandList.base = (ShellCommand *)(&_shell_command_start);
        shell->commandList.count = ((unsigned int)(&_shell_command_end)
                                - (unsigned int)(&_shell_command_start))
                                / sizeof(ShellCommand);
    #else
        #error not supported compiler, please use command table mode
    #endif

注意在keil使用的arm_cc编译器中的&shellCommand$$Base地址即为shellCommand这个输入段的起始地址。获取到了起始地址,根据结构体的偏移,我们就可以很方便的调用这个输入段中的所有注册的函数。就此我们就可以调用到所有SHELL_EXPORT_CMD注册的函数了;

参考文档:
attribute((section(x))) 使用详解 https://www.cxyzjd.com/article/qq_42370291/103639349》

猜你喜欢

转载自blog.csdn.net/jisuanji111111/article/details/126719491
今日推荐