This c language skill refreshed my understanding of structures!

【Said in front of you】


In 2022, no one must have doubts about the role of "Data Structure" in embedded development, right? Whether you're in doubt or not, this article will give you a completely different perspective.

Whenever we talk about data structures, the following must be reproduced in many people's minds:

  • It seems simple, but it is very error-prone linked list in practice;

  • The queue that hangs on the lips every day;

  • The first suspect (no one) of the program running away: stack - actually, I have never used it myself at all;

  • The "stack" that was said together in confusion - in fact, all I think about is malloc, in fact, it has nothing to do with the stack (Stack) for a dime.

  • Trees and Graphs that are almost never touched

Data structure is actually not a fancy term, it is surprisingly very simple - you may use it every day. As a new pit, I will share with you many "very" and "easy to use" data structures in embedded development in the [Very C Structure] series of articles .

ce39e73c989c22f74b108f5f7221cc54.png

【The "form" that everyone can learn】


You don't have to learn the so-called "relational database" to understand the essential meaning of the "table" data structure.

In the C language environment, the essence of a table is an array of structures, that is, an array of structures. here:

  • Tables consist of strips of "Records", sometimes called "Items"

  • The structure is responsible for defining the composition of the content in each "record"

  • A table is an array of structures

In embedded systems, tables have the following characteristics:

  • Is a constant array, decorated with const, generally stored in ROM (such as Flash)

  • Initialized at compile time

  • use at runtime

  • Save data in extremely compact form

  • Can be accessed in the form of "array + subscript"

If a requirement can 1) accept the above characteristics ; or 2) itself has the above characteristics ; or 3) part of the content can be modified to accept the above characteristics - then, you can use tables to store data.

A typical example is: interactive menus.

01e26540db1032747302bcb82e9d77de.png

It's easy to see that each level of menu "could" be essentially a table.


Although in many UI design tools (such as LVGL), the content of the menu is dynamically generated at runtime (implemented with a linked list), but in an embedded system, dynamically generating a table itself is not a "must use" feature. On the contrary, because the function of the product is often fixed - the content of the menu is also fixed, so there is no need to dynamically generate it at runtime - this satisfies the "initialize at compile time" requirement of the table.


Saving the menu in the form of a table has the advantage of saving data in ROM and reducing RAM consumption. At the same time, the access form of the array further simplifies the user code.

Another common example of using tables is Message Map , which is very common in applications of communication protocol stack parsing, and also plays an important role in many bootloaders with compact structures and complex functions.

If you are serious, the menu is just a kind of message map . Tables are not the only way to implement a message map, but they are the simplest, most commonly used, and most data-dense form. In the following examples, we will take the "message map" as an example to discuss the use and optimization of tables in depth.

【Definition of Form】


Generally, a table consists of two parts:

  • record (aka entry)

  • record container

Therefore, the definition of the table is also divided into two parts:

  • Defines the structure type of the record/entry

  • Define the type of container

The general format for the definition of a record is as follows:

typedef struct <表格名称>_item_t  <表格名称>_item_t;


struct <表格名称>_item_t {
    // 每条记录中的内容
};

Here, the function of the typedef line in the first line is "forward declaration"; the function of the line of struct is to define the actual content of the structure. Although we can completely combine "forward declaration" and "struct definition" into one, write:

typedef struct <表格名称>_item_t {
    // 每条记录中的内容
} <表格名称>_item_t;

However, for the following reasons, we still recommend that you stick to the first way of writing:

  • Due to the existence of "forward declaration", we can directly use "<table name>_item_t" in the structure definition to define the pointer;

  • Due to the existence of "forward declarations", pointers can be "crossed" between multiple records of different types.

Taking the message map as an example, a common record structure is defined as follows:

typedef struct msg_item_t msg_item_t;


struct msg_item_t {
    uint8_t chID;                 //!< 指令
    uint8_t chAccess;             //!< 访问权限检测
    uint16_t hwValidDataSize;          //!< 数据长度要求
    bool (*fnHandler)(msg_item_t *ptMSG,   
                      void *pData, 
                      uint_fast16_t hwSize);
};

In this example, we made up a communication command system. When we parsed the data frame through the communication front end, we obtained the following content:

  • 8bit instruction

  • The variable length data sent by the user

In order to facilitate instruction parsing, we also need to design the content of each instruction in a targeted manner. Therefore, we added chID to store the instruction code; and added the function pointer fnHandler to bind a processing function for the current instruction; considering that each instruction The minimum valid data length required by an instruction is known, so we use hwValidDataSize to record this information so that we can quickly make judgments when retrieving information. How to use it will be discussed later.

For the form, the container is the place for all the records, it can be simple, but it can't be absent . The simplest container is an array, for example:

const msg_item_t c_tMSGTable[20];

Here, the array of type msg_item_t is the container of the table, and we manually specify the number of elements in the array. In practice, we usually don't manually "limit" the number of elements in the table like this, but simply "lazy" - immerse yourself in initializing the array, and then let the compiler count for us - according to the number of elements we initialize. number to determine the number of elements in an array, for example:

const msg_item_t c_tMSGTable[] = {
    [0] = {
        .chID = 0,
        .fnHandler = NULL,
    },
    [1] = {
        ...
    },
    ...
};

The above writing method is C99 grammar, unfamiliar friends can go to the grammar book again. As a digression, in 2022, even the stubborn Linux will embrace C11. Don't hold on to the C89 specification anymore, at least it's no problem to use a C99 .

The advantage of the above method is mainly to facilitate us to be lazy and reduce the unnecessary "counting" process. So, how do we know how many elements there are in an array in a table? Don't panic, we have sizeof() :

#ifndef dimof
#   dimof(__array)     (sizeof(__array)/sizeof(__array[0]))
#endif

This syntactic sugar dimof() is not invented by me. If you don't believe me, ask Linux. Its principle is simple, when we pass the array name to dimof() , it will:

  1. Get the byte size of the entire target array through sizeof(<array>);

  2. Get the byte size of the first element of the array through sizeof(<array>[0]) - that is, the size of the array element;

  3. Get the number of elements in an array by division.

[Access to the table (traversal)]


Since the essence of a table is an array of structures, the most common operation on a table is to traverse (search). Also take the previous message map as an example:

static volatile uint8_t s_chCurrentAccessPermission;


/*! \brief 搜索消息地图,并执行对应的处理程序
 *! \retval false  消息不存在或者消息处理函数觉得内容无效
 *! \retval true   消息得到了正确的处理
 */
bool search_msgmap(uint_fast8_t chID,
                   void *pData,
                   uint_fast16_t hwSize)
{
    for (int n = 0; n < dimof(c_tMSGTable); n++) {
        msg_item_t *ptItem = &c_tMSGTable[n];
        if (chID != ptItem->chID) {
            continue;
        }
        if (!(ptItem->chAccess & s_chCurrentAccessPermission)) {
            continue;  //!< 当前的访问属性没有一个符合要求
        }
        if (hwSize < ptItem->hwSize) {
            continue;  //!< 数据太小了
        }
        if (NULL == ptItem->fnHandler) {
            continue;  //!< 无效的指令?(不应该发生)
        }
        
        //! 调用消息处理函数
        return ptItem->fnHandler(ptItem, pData, hwSize);
    }
    
    return false;   //!< 没找到对应的消息
}

Don't look at the "predictable" appearance of this function, its essence is actually very simple:

  • Access each entry in the table in turn through a for loop;

  • Determine the number of for loops by dimof

  • After finding the entry, do a series of so-called "gatekeeping work", such as checking permissions, checking data validity and so on - these parts are implemented by specific projects, not necessary to access the form - put here just a reference.

  • If the entry meets the requirements, the corresponding handler is executed through the function pointer.


In fact, the above code hides a feature: it is the message map in this example that allows messages with the same chID - the trick here is: for messages with the same chID value, we can target different access rights (chAccess values). Provides different handler functions. For example, in the communication system, we can design a variety of permissions and modes, such as: read-only mode, write-only mode, security mode, and so on. Different modes correspond to different chAccess values. In this way, even for the same instruction, we can provide different processing functions according to the current mode - this is just an idea for your reference.


[Problems introduced by multiple instances]


The previous example shows us the general details of table usage, which is sufficient for many embedded application scenarios. But the thinking friends must have found the problem:

If there are multiple message maps in my system (the number of messages in each message map is different), how can I reuse the code?

8fd3ffc2ee48b074ebbac4ef06737455.gif

In order to take care of the little friends who are still confused, I will translate this question for you:

  • There will be multiple message maps (multiple tables) in the system, which means that there will be an array of multiple tables in the system;

  • The previous message map access function  search_msgmap() is bound to an array (that is, c_tMSGTable):

    • It will only traverse this fixed array c_tMSGTable;

    • The number of for loops is also only for the array c_tMSGTable;

In short, search_msgmap()  is now bound to a certain message map (array), if you want it to support other message maps (other arrays), you must find a way to decouple it from a specific array, replace In other words, when using search_msgmap() , provide a pointer to the target's message map and the number of elements in the message map .

A revised plan for a headache doctor's head and foot pain doctor is about to come out:

bool search_msgmap(msg_item_t *ptMSGTable,
                   uint_fast16_t hwCount,
                   uint_fast8_t chID,
                   void *pData,
                   uint_fast16_t hwSize)
{
    for (int n = 0; n < hwCount; n++) {
        msg_item_t *ptItem = &ptMSGTable[n];
        if (chID != ptItem->chID) {
            continue;
        }
        ...
        
        //! 调用消息处理函数
        return ptItem->fnHandler(ptItem, pData, hwSize);
    }
    
    return false;   //!< 没找到对应的消息
}

Suppose we have multiple message maps, corresponding to different working modes:

const msg_item_t c_tMSGTableUserMode[] = {
    ...
};
const msg_item_t c_tMSGTableSetupMode[] = {
    ...
};


const msg_item_t c_tMSGTableDebugMode[] = {
    ...
};


const msg_item_t c_tMSGTableFactoryMode[] = {
    ...
};

When using it, you can:

typedef enum {
    USER_MODE = 0,    //!< 普通的用户模式
    SETUP_MODE,       //!< 出厂后的安装模式
    DEBUG_MODE,       //!< 工程师专用的调试模式
    FACTORY_MODE,     //!< 最高权限的工厂模式
} comm_mode_t;


bool frame_process_backend(comm_mode_t tWorkMode,
                           uint_fast8_t chID,
                           void *pData,
                           uint_fast16_t hwSize)
{
    bool bHandled = false;
    switch (tWorkMode) {
        case USER_MODE:
            bHandled = search_msgmap(
                          c_tMSGTableUserMode, 
                          dimof(c_tMSGTableUserMode),
                          chID,
                          pData,
                          hwSize);
            break;
         case SETUP_MODE:
            bHandled = search_msgmap(
                          c_tMSGTableSetupMode, 
                          dimof(c_tMSGTableUserMode),
                          chID,
                          pData,
                          hwSize);
            break;
         ...
    }


    return bHandled;
}

It looks pretty good, right? Neither nor nor! Very early.

[Complete body of table definition]


As we said earlier, the definition of a table is divided into two parts:

  • Defines the structure type of the record/entry

  • Define the type of container

Among them, regarding the definition of the container, we said that the array is the simplest form of the container. So what is the complete body of the container definition?

bfcf77b34dee507c175a6cf9c4e78ed8.png

"It's still a structure" !

Yes, the essence of a table entry is a structure, and the essence of a table container is also a structure:

typedef struct <表格名称>_item_t  <表格名称>_item_t;


struct <表格名称>_item_t {
    // 每条记录中的内容
};


typedef struct <表格名称>_t  <表格名称>_t;


struct <表格名称>_t {
    uint16_t hwItemSize;
    uint16_t hwCount;
    <表格名称>_item_t *ptItems;
};

It is easy to find that the table container is defined as a structure called <table name>_t, which contains three crucial elements:

  • ptItems : a pointer to an array of items;

  • hwCount : the number of elements in the entry array

  • hwItemSize : the size of each item


This hwItemSize is actually used to make up the number. Because of the 4-byte alignment of pointers in a 32-bit system, a 2-byte hwCount will generate 2-byte bubbles horizontally and vertically. For those who don't understand this, you can refer to the article " Talking about C Variables - Alignment (3) "


Taking the previous message map as an example, let's see how the new container should be defined and used:

typedef struct msg_item_t msg_item_t;


struct msg_item_t {
    uint8_t chID;                 //!< 指令
    uint8_t chAccess;             //!< 访问权限检测
    uint16_t hwValidDataSize;     //!< 数据长度要求
    bool (*fnHandler)(msg_item_t *ptMSG,   
                      void *pData, 
                      uint_fast16_t hwSize);
};


typedef struct msgmap_t msgmap_t;


struct msgmap_t {
    uint16_t hwItemSize;
    uint16_t hwCount;
    msg_item_t *ptItems;
};


const msg_item_t c_tMSGTableUserMode[] = {
    ...
};


const msgmap_t c_tMSGMapUserMode = {
    .hwItemSize = sizeof(msg_item_t),
    .hwCount = dimof(c_tMSGTableUserMode),
    .ptItems = c_tMSGTableUserMode,
};

Now that it is defined, search_msgmap() should also be updated accordingly:

bool search_msgmap(msgmap_t *ptMSGMap,
                   uint_fast8_t chID,
                   void *pData,
                   uint_fast16_t hwSize)
{
    for (int n = 0; n < ptMSGMap->hwCount; n++) {
        msg_item_t *ptItem = &(ptMSGMap->ptItems[n]);
        if (chID != ptItem->chID) {
            continue;
        }
        ...
        
        //! 调用消息处理函数
        return ptItem->fnHandler(ptItem, pData, hwSize);
    }
    
    return false;   //!< 没找到对应的消息
}

Seeing this, I believe that many of my friends have no turbulence in their hearts...

4631fa7787f0302c5d7b6ae3ee9c49ee.png

"Yes... a little more elegant... and then what?"

"That's it!? That's it?!"

Don't worry, the following is the moment to witness the miracle.

【Be elegant...】


In the previous example, we noticed that the initialization of the table is done in two parts:

const msg_item_t c_tMSGTableUserMode[] = {
    [0] = {
        .chID = 0,
        .fnHandler = NULL,
    },
    [1] = {
        ...
    },
    ...
};


const msgmap_t c_tMSGMapUserMode = {
    .hwItemSize = sizeof(msg_item_t),
    .hwCount = dimof(c_tMSGTableUserMode),
    .ptItems = c_tMSGTableUserMode,
};

So, can we combine them into one? so:

  • All initializations are written together;

  • Avoid naming completely unused arrays of entries:

To do this, we can use an "anonymous array"-like function:

What we imagined:

const msgmap_t c_tMSGMapUserMode = {
    .hwItemSize = sizeof(msg_item_t),
    .hwCount = dimof(c_tMSGTableUserMode),
    .ptItems = const msg_item_t c_tMSGTableUserMode[] = {
          [0] = {
              .chID = 0,
              .fnHandler = NULL,
          },
          [1] = {
              ...
          },
          ...
      },
};

What it looks like after using the "anonymous array" (that is, after removing the array name):

const msgmap_t c_tMSGMapUserMode = {
    .hwItemSize = sizeof(msg_item_t),
    .hwCount = dimof(c_tMSGTableUserMode),
    .ptItems = (msg_item_t []){
          [0] = {
              .chID = 0,
              .fnHandler = NULL,
          },
          [1] = {
              ...
          },
          ...
      },
};

532a87581d19727d838f0f2c782bc379.png

In fact, this is not "black magic", but a widely used GNU extension called " Compound literal ", which is essentially a way of "omitting" the name of an array or structure. Syntax for initializing an array or structure. For specific grammar introduction, friends can refer to this article " The Highest Realm of Anonymity in C Language Grammar ".

3b752b83371be03e94aebf7a9a189f55.png

The sharp-eyed friends may have found the problem: since we omitted the variable name, how to get the number of array elements through dimof()?

Good eyesight, young man!

The solution is not only there, but also simple and rude:

const msgmap_t c_tMSGMapUserMode = {
    .hwItemSize = sizeof(msg_item_t),
    
    .hwCount = dimof((msg_item_t []){
          [0] = {
              .chID = 0,
              .fnHandler = NULL,
          },
          [1] = {
              ...
          },
          ...
      }),
      
    .ptItems = (msg_item_t []){
          [0] = {
              .chID = 0,
              .fnHandler = NULL,
          },
          [1] = {
              ...
          },
          ...
      },
};

so? ...

For elegant initialization...

Are we going to write the same content twice? ! !

It's stupid to write by hand, but macros do!

#define __impl_table(__item_type, ...)                   \
    .ptItems = (__item_type []) {                        \
        __VA_ARGS__                                      \
    },                                                   \
    .hwCount = sizeof((__item_type []) { __VA_ARGS__ })  \
             / sizeof(__item_type),                      \
    .hwItemSize = sizeof(__item_type)


#define impl_table(__item_type, ...)                     \
    __impl_table(__item_type, __VA_ARGS__)

With the above syntactic sugar, we can easily make the initialization of the entire table simple and elegant:

const msgmap_t c_tMSGMapUserMode = {
    impl_table(msg_item_t, 
          [0] = {
              .chID = 0,
              .fnHandler = NULL,
          },
          [1] = {
              ...
          },
          ...
    ),
};

Are you comfortable now?

【No nesting dolls...】


Remember the previous example of multiple instances?

const msg_item_t c_tMSGTableUserMode[] = {
    ...
};
const msg_item_t c_tMSGTableSetupMode[] = {
    ...
};


const msg_item_t c_tMSGTableDebugMode[] = {
    ...
};


const msg_item_t c_tMSGTableFactoryMode[] = {
    ...
};

Now of course it should be changed to the following form:

const msgmap_t c_tMSGMapUserMode = {
    impl_table(msg_item_t, 
        ...
    ),
};


const msgmap_t c_tMSGMapSetupMode = {
    impl_table(msg_item_t, 
        ...
    ),
};


const msgmap_t c_tMSGMapDebugMode = {
    impl_table(msg_item_t, 
        ...
    ),
};


const msgmap_t c_tMSGMapFactoryMode = {
    impl_table(msg_item_t, 
        ...
    ),
};

But... aren't they all of type msgmap_t ? Why not make an array?

typedef enum {
    USER_MODE = 0,    //!< 普通的用户模式
    SETUP_MODE,       //!< 出厂后的安装模式
    DEBUG_MODE,       //!< 工程师专用的调试模式
    FACTORY_MODE,     //!< 最高权限的工厂模式
} comm_mode_t;


const msgmap_t c_tMSGMap[] = {
    [USER_MODE] = {
        impl_table(msg_item_t, 
            ...
        ),
    },
    [SETUP_MODE] = {
        impl_table(msg_item_t, 
            ...
        ),
    },
    [DEBUG_MODE] = {
        impl_table(msg_item_t, 
            ...
        ),
    },
    [FACTORY_MODE] = {
        impl_table(msg_item_t, 
            ...
        ),
    },
};

Is it interesting? Going a step further, we can make a new table, and the element of the table is msgmap_t?

typedef struct cmd_modes_t cmd_modes_t;


struct cmd_modes_t {
    uint16_t hwItemSize;
    uint16_t hwCount;
    msgmap_t *ptItems;
};

Then you can start nesting the dolls:

const cmd_modes_t c_tCMDModes = {
    impl_table(msgmap_t,
        [USER_MODE] = {
            impl_table(msg_item_t, 
                [0] = {
                    .chID = 0,
                    .fnHandler = NULL,
                },
                [1] = {
                    ...
                },
                ...
            ),
        },
        [SETUP_MODE] = {
            impl_table(msg_item_t, 
                ...
            ),
        },
        [DEBUG_MODE] = {
            impl_table(msg_item_t, 
                ...
            ),
        },
        [FACTORY_MODE] = {
            impl_table(msg_item_t, 
                ...
            ),
        },
    ),
};

【Differentiation……】


In the previous example, we can update the function frame_process_backend() function according to the new definition:

extern const cmd_modes_t c_tCMDModes;


bool frame_process_backend(comm_mode_t tWorkMode,
                           uint_fast8_t chID,
                           void *pData,
                           uint_fast16_t hwSize)
{
    bool bHandled = false;
    
    if (tWorkMode > FACTORY_MODE) {
        return false;
    }
    
    return search_msgmap( &(c_tCMDModes.ptItems[tWorkMode]), 
                          chID,
                          pData,
                          hwSize);
}

Is it particularly elegant?

Another benefit of defining the container as a struct is that it can give the table more differentiation, which means that we can put in other things besides items related to the array of items, such as:

  • Add more members to the struct - add more information to the table

  • Add more function pointers (in OOPC concept, add more "methods")

The existing frame_process_backend() uses the same processing function search_msgmap () for each message map ( msgmap_t ) , which obviously lacks the possibility of differentiation. What if each message map could potentially have its own special handler function?

To achieve this, we can extend msgmap_t:

typedef struct msgmap_t msgmap_t;


struct msgmap_t {
    uint16_t hwItemSize;
    uint16_t hwCount;
    msg_item_t *ptItems;
    bool (*fnHandler)(msgmap_t *ptMSGMap,
                      uint_fast8_t chID,
                      void *pData,
                      uint_fast16_t hwSize);
};

When initializing, we can specify a different handler function for each message map:

extern     
bool msgmap_user_mode_handler(msgmap_t *ptMSGMap,
                      uint_fast8_t chID,
                      void *pData,
                      uint_fast16_t hwSize);


extern     
bool msgmap_debug_mode_handler(msgmap_t *ptMSGMap,
                      uint_fast8_t chID,
                      void *pData,
                      uint_fast16_t hwSize);


const cmd_modes_t c_tCMDModes = {
    impl_table(msgmap_t,
        [USER_MODE] = {
            impl_table(msg_item_t, 
                ...
            ),
            .fnHandler = &msgmap_user_mode_handler,
        },
        [SETUP_MODE] = {
            impl_table(msg_item_t, 
                ...
            ),
            .fnHandler = NULL; //!< 使用默认的处理函数
        },
        [DEBUG_MODE] = {
            impl_table(msg_item_t, 
                ...
            ),
            .fnHandler = &msgmap_debug_mode_handler,
        },
        [FACTORY_MODE] = {
            impl_table(msg_item_t, 
                ...
            ),
            //.fnHandler = NULL  什么都不写,就是NULL(0)
        },
    ),
};

At this point, we update the frame_process_backend()  function to make the above differentiated functions possible:

bool frame_process_backend(comm_mode_t tWorkMode,
                           uint_fast8_t chID,
                           void *pData,
                           uint_fast16_t hwSize)
{
    bool bHandled = false;
    msgmap_t *ptMSGMap = c_tCMDModes.ptItems[tWorkMode];
    if (tWorkMode > FACTORY_MODE) {
        return false;
    }
    
    //! 调用每个消息地图自己的处理程序
    if (NULL != ptMSGMap->fnHandler) {
         return ptMSGMap->fnHandler(ptMSGMap, 
                                    chID,
                                    pData,
                                    hwSize);
    }
    //! 默认的消息地图处理程序
    return search_msgmap( ptMSGMap, 
                          chID,
                          pData,
                          hwSize);
}

【Say later】


Don't say anything...you can do it. See you next time.

—— The End ——

Recommended in the past

You can never imagine that C language can play like this!

Good project, don't keep it private! Open source wheels for microcontroller development

MCU development never uses data structures?

After the person who wrote the bad code left...

I'm stumped, what is the role of C language enumeration end?

30 solutions to common problems with single-chip microcomputers! Normal people I don't tell them

Click on the card above to follow me

e84580e729b2cf010ea77e56e6c2f8be.png

Everything you ordered looks good , I take it seriously as I like it

Guess you like

Origin blog.csdn.net/u010632165/article/details/123469904