一个纯C软件工程的设计

前言

说起来C语言是我的编程入门语言,但是自从工作后我就慢慢的开始从C开始转到C++,很少使用到C,而在最近半年由于工作的需要,C++也写得少了,Python成为了我的主要使用语言。伴随着这些新语言的使用,它们优秀语言特性和程序设计模式让我受益匪浅,因此现在当我再一次回到纯C开发时,我也希望能够借用上一些设计方法和思路,让C写出来的代码模块的独立性更好,通用性更强。

目标

软件需要实现的目标很简单,功能就是接受外部通信发过来的数据包,将数据包解析为已经定义好的指令,然后按照指令协议控制硬件完成指定的操作,软件工作在一个运行着Linux操作系统的嵌入式设备上。

由于软件是运行在嵌入式设备中(不过硬件性能不错),需要和专用的硬件模块进行交互且不需要界面,又是在Linux平台之上的,同时也希望尽可能的保持高效,所以我首先想到的语言是C了。为什么不用C++?也许是惯性吧,感觉在Linux平台之上写不需要UI的底层应用用C++会比较奇怪。

设计方案

分析设计需求,其实当前软件需要需要完成的功能是非常简单的。我将整个软件划分4个模块:

  • 通信数据接收模块
  • 指令解析模块
  • 指令调度
  • 硬件操作模块

以下为上面四个模块做详细的解释。

1. 通信数据接收模块

这部分的主要作用打通过外界与本机的物理通信链路,接收外部传来的数据,这部分相对独立,不管是本机与外部通信是使用哪种方式,它的都只要将传递的有效的数据信息部分传递给命令解析模块即可。

但是我们唯一需要限定的是,不管使用什么样的通信方式,不管使用怎样的协议传输数据,需要保证有效数据的数据格式一致。

2. 指令解析模块

由于数据接收模块可以提供一致的数据,所以指令解析模块就只需要参照这种固定的格式,将数据流变成结构化的数据,存放在相应的结构体中。

3. 指令调度模块

这相当于一个流程控制的模块,接收解析出的指令作为输入,对应按照指定的调度规范,调整指令执行的流程、顺序、或参数等操作,让后调用硬件操作模块来执行指令。

4. 硬件操作模块

硬件操作模块主要的作用的作用就是,对外提供实现对硬件设备控制的接口。从功能上来看,它的独立性是更强的。对于较低的层次接口来说,只需要提供通用硬件寄存器的读写操作,对于较高层次的接口来说,可以各个硬件单项功能的寄存器操作步骤集合做封装。

指令的结构设计

虽然说在设计各个模块尽可能相互独立,但是有些因素基本上对所有模块都有影响,这些因素我们应该尽量确定细节,以由于这些因素的改变对整个程序的影响。在这个工程中指令功能规范设计就是其中之一,所以我们在编写所有模块之前,首先就需要确定功能规范,围绕着此规范在去写各个模块中的逻辑实现。

为此我构造了一个专门处理指令规范的结构体,这个结构体是全局的,它为所有模块共用。它由以下几个部分组成:

  • 指令ID(int)
  • 指令名称(char *)
  • 指令参数解析函数(函数指针)
  • 指令执行函数(函数指针)

以下对结构中的每个部分进行说明:

1. 指令ID

指令都都是唯一的,我们使用ID标示这个指令,数值类型为整形。为了方便标示,我们用一个枚举型变量来代替宏来定义每个指令的ID值,利用枚举型变量内部元素常量特性,同时默认定义时,后一个定义的元素值为前一个元素值加1的特点,保证每一个指令ID值唯一,另外在所有指令定义完成后,在最后增加一个枚举元素来记录总的指令数量。

在当前的设计中,一旦结构体初始化,其指令ID期望是固定的,所以可以将其设置为const属性。

2. 指令名称

指令名称用来存储一个字符串,数据类型是一个字符串指针,虽然这并不是必须的,但是字符串的描叙对指令是个很好的说明。同样当ID确定时,名称也不应该改变,所以需要将其设置为const属性。

3. 指令参数解析函数

不同的指令所需要的参数可能不一样,有些指令设置可以不需要参数,所以将将当前指令的解析函数用函数指针保存起来,好处是:1. 不同指令直接对应各自独立的解析方法,不需混在一起要很多个if判断 2. 即使解析方法发生了变化,只要将函数指针重新设置为新的解析函数即可。不过,一般情况下相同指令的解析函数不会动态变化。

4. 指令执行函数

指令执行函数是只将指令规定的功能,通过硬件操作模块的接口,对硬件进行配置,让其完成对应工作。和指令前面类似,这里保存的也是一个函数指针。

指令数据的结构设计

前面所提到的指令解析函数,它的输入是无格式的数据流,它的输出是有意义的结构化的数据。于是,那我们就必须先完成这个数据的结构的设计。

设计思路有两种:1. 对每个不同的指令设计一个存放数据的结构体,2. 综合所有指令所带的数据,统一记录在一个结构体中。考虑到在后面指令调度模块可能会需要缓冲指令和数据,所以这样来看第二方法更加简单合适。

指令数据的结构设计如下:

  • 执行序号(int)
  • 指令ID(int)
  • 所有指令的数据元素(void *)
  • 其他数据

以下一一说明:

1. 执行序号

序号标示,当前指令在整个控制系统中的唯一编号,序号由外部控制中心产生,通过通信传递过来。

执行序号在反馈应答的应用中有很重要的作用,由于有缓存的存在,当前执行的指令可能路后于是很早前的控制中心发送过来的指令,当指令接收和指令完成后,都发送应答给控制中心,那通过执行序号就可以很好的区分它们。

2. 指令ID

与指令结构体中的指令ID一样

3. 所有指令的数据元素

由于我们采用了,用一个结构记录所有指令的数据,那么之一部分实际上是包含了一系列数据元素的。这样做是我们在缓存指令和数据后,回溯缓存的指令和数据时更方便。

4. 其他数据

其他数据是为那些可能需要传输大量数据的指令而设计的,一般不需要。

Compartir