Home >Product dynamics>Product dynamics
Software design method with complete separation of software and hardware

Foreword

In the conventional embedded software development process, it usually needs to go through steps such as requirements analysis, function design, coding implementation, system testing, release maintenance, etc., to realize the entire life cycle of the product. 


However, due to the simultaneous start-up of software and hardware and the emergence of unexpected situations, there will often be a deviation between the design function and the actual function. In addition, specific functions in embedded software development depend on hardware design, and it is difficult to adjust flexibly for small function changes, such as digital input and output channel binding, analog acquisition channel changes, etc. 


This creates additional expenses for subsequent launches of products with the same positioning but with different configurations. In order to solve these problems, we propose a software design method in which hardware and software can be completely separated. This method abstracts the function of setting the target as an object, and by instantiating the object, dynamically changes the program configuration during operation, so as to achieve the purpose of flexibly switching firmware between products with different configurations at the same location. This method can not only effectively reduce the expenses caused by firmware problems, but also improve the flexibility and maintainability of the product.

what is an object

In object-oriented programming, an object (Object) refers to an entity with certain properties and methods, which is the basic unit of a program. We usually use classes (Class) to define the properties and methods of objects. A class can be thought of as a template or blueprint for an object, which defines its characteristics and behavior. When we create an object, we actually create an instance of a class, which has the properties and methods defined by the class. We usually use objects to represent things or concepts in the real world, such as people, cars, books, and so on. Objects have two basic characteristics of state (properties) and behavior (methods), which can be instantiated (created) and used to build different parts of the program.

The above is a popular explanation of objects, but an important point is raised above, that is, object-oriented programming. As we all know, C language is not an object-oriented development language, so how can we use C language to complete object-oriented development?

In fact, there are methods, but C language does not predefine objects like other high-level languages, but needs to be implemented by itself. In C language, structure (Struct) is a user-defined data type, which allows us to define our own A data structure that contains multiple data members of different types. A structure type is defined by the keyword struct, followed by the structure name and the names and types of the data members. For example, the following code defines a structure type named Person, which contains two integer data members age and name:

struct Person {
   int age;
   bool gender;
   char name[20];
  };
After defining a struct type, we can use it to define struct variables as follows:
struct Person person1;
After defining a structure variable, we can initialize its data members by assignment, for example:
person1. age = 20;
person1. bool = male;
strcpy(person1.name, "Alice");

We can also access the data members of the structure variable through the access operator [], for example:
printf("Person name: %s, age: %d\n", [person1.name](http://person1.name), person1.age);
In the C language, the structure type also supports multiple inheritance (struct and class), structure and pointer, structure pointer and function and other usages. Structure is a very powerful data type in C language, which can conveniently represent and manipulate complex data structures. Therefore, we can also abstractly define product functions in the form of structures.

Create object

Assuming that our product is an IO control device, then the main functions we need to design are mainly IO control and detection. After classifying the above requirements, all its functions can be classified into IO operations. Then we can expand on this basis and extract the common parts.
 
#define MAX_PIN_ITEM 10

struct Object_pin{
     bool mode;
     bool state;
     uint32_t pin;
     void* port;
};

struct io_funt
{
   void (*gpio_set)(void* port, uint32_t pin);
   void (*gpio_reset)(void* port, uint32_t pin);
   bool (*gpio_read)(void* port, uint32_t pin);
};

struct Object_IO
{
   struct Object_pin io_list[MAX_PIN_ITEM];
   struct io_funt IO_FUN;
   void (*exetuct_fun)(struct Object_pin* pin);
   void (*gpio_init)(struct Object_pin* pin);
};

struct Object_IO SYS_IO;
 
void test_exetuct_fun(struct Object_pin* pin)
{
     if(pin->mode)
     {
       if(pin->state)
       {
      
         SYS_IO.IO_FUN.gpio_set(pin->port, pin->pin);
       }
       else
       {
         SYS_IO.IO_FUN.gpio_reset(pin->port, pin->pin);
       }
     }
     else
     {
        pin->state = SYS_IO.IO_FUN.gpio_read(pin->port,pin->pin);
     }
}

void your_gpio_set(void* port, uint32_t pin);
void your_gpio_reset(void* port, uint32_t pin);
bool your_gpio_read(void* port, uint32_t pin);
void your_sys_gpio_init_function(struct Object_pin* pin);

void system_io_init(void)
{
   SYS_IO.IO_FUN.gpio_set = your_gpio_set;
   SYS_IO.IO_FUN.gpio_reset = your_gpio_reset;
   SYS_IO.IO_FUN.gpio_read = your_gpio_read;
   SYS_IO.gpio_init = your_sys_gpio_init_function;
   SYS_IO.exetuct_fun = test_exetuct_fun;
   for(uint8_t ii = 0; ii < sizeof(SYS_IO.io_list)/sizeof(SYS_IO.io_list[0]); ii++)
   {
     SYS_IO.gpio_init(&SYS_IO.io_list[ii]);
   }
}

void system_io_handler(void)
{
   for(uint8_t ii = 0; ii < sizeof(SYS_IO.io_list)/sizeof(SYS_IO.io_list[0]); ii++)
   {
     SYS_IO.exetuct_fun(&SYS_IO.io_list[ii]);
   }
}

In the above code snippet, we use the struct Object_pin structure to define a single IO, use the struct Object_IO structure to complete the definition of the entire IO list and the corresponding execution function interface, and define the system initialization function and executor function at the same time, pay attention to the code The actual execution function is not written in , and needs to be implemented and defined by the actual user. Careful friends may have discovered that in the example, the number of members is all defined by the MAX_PIN_ITEM macro. Obviously, this cannot solve the problem of dynamically modifying the number of IOs in the program, so we need to modify the program, as follows:
struct Object_pin{
     bool mode;
     bool state;
     uint8_t pin;
     uint8_t port;
};

struct Object_IO
{
   uint8_t li_cnt;
   struct Object_pin *io_list;
   struct io_funt IO_FUN;
   void (*exetuct_fun)(struct Object_pin* pin);
   void (*gpio_init)(struct Object_pin* pin);
};

struct Object_IO SYS_IO;
In the above code, we changed the structure member io_list from an array object to a pointer object, and at the same time modified the member variables of the Object_pin structure type, which resulted in that the io_list did not directly store specific PIN structure objects, but is an unknown space, so how should we use this unknown pointer object? This requires providing a system parser and the corresponding configuration file. The parser parses the configuration file to generate a PIN structure object, and points the io_list to the newly generated object to achieve the purpose of dynamic configuration.

Configuration file

According to the io_list object type in the structure, the pointer points to the address of the struct Object_pin type, so we only need to create the corresponding structure list according to the Object_pin structure type. First, check the structure member type of the struct Object_pin, It is found that it can be abstracted as a key-value pair for storage, so that the json format can be selected to identify the configuration. The content of the Object_pin object can be easily identified through the json format. At the same time, json provides an array type that can clearly express multiple objects. You can choose to store the file in the device's own storage space or external space, and load the file into the memory for analysis when using it.
[{
     "mode": 1,
     "state": 0,
     "pin" : 0,
     "port": 0
},
{
     "mode": 1,
     "state": 0,
     "pin" : 1,
     "port": 0
},{
     "mode" : 0,
     "state": 0,
     "pin" : 0,
     "port" : 1
},
{
     "mode" : 0,
     "state": 0,
     "pin" : 1,
     "port" : 1
}]
parser
After the configuration file determines that the json format is selected, there are many parsing libraries that can be selected. You can choose the famous cjson parsing library or other parsing libraries, or you can write the corresponding parsing library yourself. Here we choose our own software library to complete the parsing task, the main interface used is
jsonObj* jsonParse(char* str);
int getJsonObjInteger(jsonObj* obj, const char* key);
After importing the software library, create a variable of type jsonObj to store the parsed member variable _root, use the jsonParse function to parse the entire json format string, and store it in the _root variable, and use the getJsonObjInteger function to retrieve it from the _root variable Just get the members we want. By storing the parsed variable and assigning it to the io_list pointer in SYS_IO, and assigning the parsed quantity to the li_cnt member in SYS_IO to mark the number of objects stored in the current pointer address, the dynamic Load the IO configuration.


This paper introduces an object-based software design method with completely separable hardware and software to solve some problems in embedded software development. This method abstracts each module in the system into an object, and dynamically changes the program configuration through the instantiation of the object. The article explains in detail the concept and definition of objects, and how to use C language for object-oriented programming. In addition, the article also introduces the functional abstraction and definition of IO operations in the system, and proposes a method of dynamically loading IO configurations, and realizes flexible switching of IOs through configuration files and parsers. The method can reduce the expense caused by the firmware problem in the embedded software development, and improve the flexibility and maintainability of the product. At the end of the article, the relevant code samples and the format of the configuration file are given.