前言
在常规嵌入式软件开发流程中,通常需要经过需求分析、功能设计、编码实现、系统测试、发布维护等步骤,以实现产品的整个生命周期。但是,由于软硬件同时开工和意外情况的出现,往往会导致设计功能与实际功能之间的偏差。此外,嵌入式软件开发中特定功能依赖于硬件设计,对于小的功能变更,如数字量输入输出通道绑定、模拟量采集通道变更等,难以灵活调整。这给后续推出相同定位产品但配置不同的产品带来了额外的开支。为了解决这些问题,我们提出一种软硬件可完全分离的软件设计方法。该方法将设定目标功能作为对象进行抽象,并通过将对象进行实例化,在运行过程中动态改变程序配置,从而实现在同定位不同配置产品之间灵活切换固件的目的。这种方法不仅可以有效减少固件问题带来的开支,而且可以提高产品的灵活性和可维护性。
什么是对象?
在面向对象编程中,对象(Object)是指具有某些属性和方法的实体,它是程序的基本构成单位。我们通常使用类(Class)来定义对象的属性和方法。一个类可以看作是对象的模板或蓝图,它定义了对象的特征和行为。当我们创建一个对象时,实际上是创建了一个类的实例,它拥有类定义的属性和方法,我们通常使用对象来表示真实世界中的事物或概念,例如人、汽车、书籍等等。对象具有状态(属性)和行为(方法)两个基本特征,它可以被实例化(创建)并被用于构建程序的不同部分。
以上是对对象的通俗解释,但上面提出了一个重要的点,即面向对象编程。众所周知,C语言并非一门面向对象的开发语言,那么我们又该如何使用C语言完成面向对象开发呢?
其实是有方法的,只不过C语言没有像其他高级语言那样预先定义对象而是需要自己实现,在C语言中,结构体(Struct)是一种用户自定义的数据类型,它允许我们定义自己的数据结构,包含多个不同类型的数据成员。结构体类型由关键字struct定义,后跟结构体名称和数据成员的名称和类型。例如,下面的代码定义了一个名为Person的结构体类型,包含两个整数类型的数据成员age和name:
struct Person {
int age;
bool gender;
char name[20];
};
定义结构体类型后,我们可以使用它来定义结构体变量,如下所示:
struct Person person1;
定义结构体变量后,我们可以通过赋值来初始化它的数据成员,例如:
person1.age = 20;
person1.bool = male;
strcpy(person1.name, "Alice");
我们也可以通过访问运算符[]来访问结构体变量的数据成员,例如:
printf("Person name: %s, age: %d\n", [person1.name](http://person1.name), person1.age);
在C语言中,结构体类型还支持多重继承(struct和类),结构体和指针,结构体指针和函数等多种用法。结构体在C语言中是一种非常强大的数据类型,可以方便地表示和操作复杂的数据结构。所以我们也可以将产品功能通过结构体的方式进行抽象定义。
创建对象
假设我们的产品是一款IO类控制设备,那么我们所需要设计的主要功能则主要是IO的控制与检测,将上述需求进行归类,其所有的功能都可以归类到IO的操作上来,那么我们就可以此为基础进行扩展,提炼出其中公共的部分。
#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 izeof(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 izeof(SYS_IO.io_list[0]); ii++)
{
SYS_IO.exetuct_fun(&SYS_IO.io_list[ii]);
}
}
在上面的代码片段中我们使用struct Object_pin结构体对单个IO进行定义,使用struct Object_IO结构体完成了对整个IO列表以及对应执行函数接口的定义, 同时定义了系统初始化函数和执行器函数,注意代码中并未编写实际的执行函数,需要由实际用户去实现定义。有细心的朋友可能发现了,在例子中,成员个数全部由MAX_PIN_ITEM宏定义,很明显这并不能解决在程序中动态修改IO数量的问题,所以我们需要对程序进行修改,如下代码:
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;
在上面代码中,我们将结构体成员io_list由数组对象修改为指针对象,同时修改了Object_pin结构体类型的成员变量,这就导致io_list所存储的并非直接是一个个具体的PIN结构体对象,而是一片未知的空间,所以我们应该如何使用这个未知指针对象呢?这就需要提供系统解析器以及对应的配置文件了,通过解析器对配置文件进行解析生成PIN结构体对象,将io_list指向新生成的对象从而达到动态配置的目的。
配置文件
根据结构体中的io_list对象类型可得知,该指针指向struct Object_pin类型的地址,所以我们仅需要根据Object_pin结构体类型来创建对应的结构体列表即可,首先查看struct Object_pin的结构体成员类型,发现可以将其抽象为键值对的方式进行存储,这样即可选择json格式对配置进行标识,通过json格式能够轻松的标识Object_pin对象的内容,同时json提供了数组类型可以明确表达多个对象,可以选择将文件存储在设备自身存储空间或外部空间中,使用时将文件加载到内存中进行解析。
[{
"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
}]
解析器
配置文件确定选用json格式后能够选择的解析库就很多了,可以选择著名的cjson解析库或其他解析库也可以自己编写相应的解析库。此处我们选择自有软件库完成解析任务,主要用到的接口为
jsonObj* jsonParse(char* str);
int getJsonObjInteger(jsonObj* obj, const char* key);
导入软件库后创建jsonObj类型的变量用于存储解析后的成员变量 _root,使用jsonParse函数对整个json格式字符串进行解析,并将其存储在_root变量中,使用getJsonObjInteger函数从_root变量中获取我们想要的成员即可。通过对解析后的变量进行存储并赋值给SYS_IO中的io_list指针,同时将解析到的数量赋予SYS_IO中的li_cnt成员用以标记当前指针地址中所存储的对象数量,通过以上方式即完成了动态的加载IO配置。
结语
本文介绍了一种基于对象的软硬件可完全分离的软件设计方法,以解决嵌入式软件开发中的一些问题。该方法将系统中的各个模块抽象成对象,并通过对象的实例化来动态改变程序配置。文章详细解释了对象的概念和定义,以及如何使用C语言进行面向对象编程。另外,文章还介绍了系统中IO操作的功能抽象和定义,并提出了一种动态加载IO配置的方法,通过配置文件和解析器实现了IO的灵活切换。该方法可以减少嵌入式软件开发中的固件问题带来的开支,提高产品的灵活性和可维护性。文章最后给出了相关代码示例和配置文件的格式。
特点
(1)软硬件独立
·软件设计独立于硬件。
·可以更换硬件而不需要重新设计软件。
(2)高度可维护
·独立的软硬件设计使得软件维护变得更加容易。
·可以根据硬件更新进行软件升级而不影响现有的软件系统。
(3)提高生产效率
·可以通过可维护的软件设计方法来降低软件开发成本。
·通过可软硬件完全分离的设计方法,可以大大提高软件生产效率。
设计方法
(1)基于对象的设计方法
·将系统中的各个模块抽象成对象,以实现对系统的可维护性和可扩展性。
·基于面向对象的设计原则,使得软件设计更加模块化、可复用、可扩展。
(2)硬件设计
·根据系统需求,进行硬件的选择和设计。
·确保硬件的性能、功能和接口与软件设计要求相匹配。
·保证硬件的可扩展性,以适应未来系统需求的变化。
领取专属 10元无门槛券
私享最新 技术干货