[经验] 嵌入式C语言修炼之道.软件架构篇

念慈菴   2017-8-15 17:33 楼主
模块划分   模块划分的"划"是规划的意思,意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。C语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能(依功能进行划分在面向对象设计中成为一个错误,牛顿定律遇到了>相对论),c语言模块化程序设计需理解如下概念:   (1) 模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明;   (2) 某模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明;   (3) 模块内的函数和全局变量需在.c文件开头冠以static关键字声明;   (4) 永远不要在.h文件中定义变量!定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如:
/*module1.h*/ int a = 5; /* 在模块1的.h文件中定义int a */ /*module1 .c*/ #include "module1.h" /* 在模块1中包含模块1的.h文件 */ /*module2 .c*/ #include "module1.h" /* 在模块2中包含模块1的.h文件 */ /*module3 .c*/ #include "module1.h" /* 在模块3中包含模块1的.h文件 */
  以上程序的结果是在模块1、2、3中都定义了整型变量a,a在不同的模块中对应不同的地址单元,这个世界上从来不需要这样的程序。正确的做法是:
/*module1.h*/ extern int a; /* 在模块1的.h文件中声明int a */ /*module1 .c*/ #include "module1.h" /* 在模块1中包含模块1的.h文件 */ int a = 5; /* 在模块1的.c文件中定义int a */ /*module2 .c*/ #include "module1.h" /* 在模块2中包含模块1的.h文件 */ /*module3 .c*/ #include "module1.h" /* 在模块3中包含模块1的.h文件 */
  这样如果模块1、2、3操作a的话,对应的是同一片内存单元。   一个嵌入式系统通常包括两类模块:   (1)硬件驱动模块,一种特定硬件对应一个模块;   (2)软件功能模块,其模块的划分应满足低偶合、高内聚的要求。   多任务还是单任务   所谓"单任务系统"是指该系统不能支持多任务并发操作,宏观串行地执行一个任务。而多任务系统则可以宏观并行(微观上可能串行)地"同时"执行多个任务。   多任务的并发执行通常依赖于一个多任务操作系统(OS),多任务OS的核心是系统调度器,它使用任务控制块(TCB)来管理任务调度功能。TCB包括任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。调度器在任务被激活时,要用到这些信息。此外,TCB还被用来存放任务的"上下文"(context)。任务的上下文就是当一个执行中的任务被停止时,所要保存的所有信息。通常,上下文就是计算机当前的状态,也即各个寄存器的内容。当发生任务切换时,当前运行的任务的上下文被存入TCB,并将要被执行的任务的上下文从它的TCB中取出,放入各个寄存器中。   嵌入式多任务OS的典型例子有Vxworks、ucLinux等。嵌入式OS并非遥不可及的神坛之物,我们可以用不到1000行代码实现一个针对80186处理器的功能最简单的OS内核。   究竟选择多任务还是单任务方式,依赖于软件的体系是否庞大。例如,绝大多数手机程序都是多任务的,但也有一些小灵通的协议栈是单任务的,没有操作系统,它们的主程序轮流调用各个软件模块的处理程序,模拟多任务环境。 单任务程序典型架构   (1)从CPU复位时的指定地址开始执行;   (2)跳转至汇编代码startup处执行;   (3)跳转至用户主程序main执行,在main中完成:   a.初试化各硬件设备;   b.初始化各软件模块;   c.进入死循环(无限循环),调用各模块的处理函数   用户主程序和各模块的处理函数都以C语言完成。用户主程序最后都进入了一个死循环,其首选方案是:
while(1) { }
  有的程序员这样写:
for(;;) { }
  这个语法没有确切表达代码的含义,我们从for(;;)看不出什么,只有弄明白for(;;)在C语言中意味着无条件循环才明白其意。   下面是几个"著名"的死循环:   (1)操作系统是死循环;   (2)WIN32程序是死循环;   (3)嵌入式系统软件是死循环;   (4)多线程程序的线程处理函数是死循环。   你可能会辩驳,大声说:"凡事都不是绝对的,2、3、4都可以不是死循环"。Yes,you are right,但是你得不到鲜花和掌声。实际上,这是一个没有太大意义的牛角尖,因为这个世界从来不需要一个处理完几个消息就喊着要OS杀死它的WIN32 程序,不需要一个刚开始RUN就自行了断的嵌入式系统,不需要莫名其妙启动一个做一点事就干掉自己的线程。有时候,过于严谨制造的不是便利而是麻烦。君不见,五层的TCP/IP协议栈超越严谨的ISO/OSI七层协议栈大行其道成为事实上的标准?   经常有网友讨论:
printf("%d,%d",++i,i++); /* 输出是什么?*/ c = a+++b; /* c=? */
  等类似问题。面对这些问题,我们只能发出由衷的感慨:世界上还有很多有意义的事情等着我们去消化摄入的食物。   实际上,嵌入式系统要运行到世界末日。   中断服务程序   中断是嵌入式系统中重要的组成部分,但是在标准C中不包含中断。许多编译开发商在标准C上增加了对中断的支持,提供新的关键字用于标示中断服务程序 (ISR),类似于__interrupt、#program interrupt等。当一个函数被定义为ISR的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。   中断服务程序需要满足如下要求:   (1)不能返回值;   (2)不能向ISR传递参数;   (3) ISR应该尽可能的短小精悍;   (4) printf(char * lpFormatString,…)函数会带来重入和性能问题,不能在ISR中采用。   在某项目的开发中,我们设计了一个队列,在中断服务程序中,只是将中断类型添加入该队列中,在主程序的死循环中不断扫描中断队列是否有中断,有则取出队列中的第一个中断类型,进行相应处理。
/* 存放中断的队列 */ typedef struct tagIntQueue {  int intType; /* 中断类型 */  struct tagIntQueue *next; }IntQueue; IntQueue lpIntQueueHead; __interrupt ISRexample () {  int intType;  intType = GetSystemType();  QueueAddTail(lpIntQueueHead, intType);/* 在队列尾加入新的中断 */ }
  在主程序循环中判断是否有中断:
While(1) {  If( !IsIntQueueEmpty() )  {   intType = GetFirstInt();   switch(intType) /* 是不是很象WIN32程序的消息解析函数? */   {    /* 对,我们的中断类型解析很类似于消息驱动 */    case xxx: /* 我们称其为"中断驱动"吧? */     …     break;    case xxx:     …     break;    …   }  } }
  按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。 硬件驱动模块   一个硬件驱动模块通常应包括如下函数:   (1)中断服务程序ISR   (2)硬件初始化   a.修改寄存器,设置硬件参数(如UART应设置其波特率,AD/DA设备应设置其采样速率等);   b.将中断服务程序入口地址写入中断向量表:
/* 设置中断向量表 */ m_myPtr = make_far_pointer(0l); /* 返回void far型指针void far * */ m_myPtr += ITYPE_UART; /* ITYPE_UART: uart中断服务程序 */ /* 相对于中断向量表首地址的偏移 */ *m_myPtr = &UART _Isr; /* UART _Isr:UART的中断服务程序 */
  (3)设置CPU针对该硬件的控制线   a.如果控制线可作PIO(可编程I/O)和控制信号用,则设置CPU内部对应寄存器使其作为控制信号;   b.设置CPU内部的针对该设备的中断屏蔽位,设置中断方式(电平触发还是边缘触发)。   (4)提供一系列针对该设备的操作接口函数。例如,对于LCD,其驱动模块应提供绘制像素、画线、绘制矩阵、显示字符点阵等函数;而对于实时钟,其驱动模块则需提供获取时间、设置时间等函数。   C的面向对象化   在面向对象的语言里面,出现了类的概念。类是对特定数据的特定操作的集合体。类包含了两个范畴:数据和操作。而C语言中的struct仅仅是数据的集合,我们可以利用函数指针将struct模拟为一个包含数据和操作的"类"。下面的C程序模拟了一个最简单的"类":
#ifndef C_Class #define C_Class struct #endif C_Class A {  C_Class A *A_this; /* this指针 */  void (*Foo)(C_Class A *A_this); /* 行为:函数指针 */  int a; /* 数据 */  int b; };
  我们可以利用C语言模拟出面向对象的三个特性:封装、继承和多态,但是更多的时候,我们只是需要将数据与行为封装以解决软件结构混乱的问题。C模拟面向对象思想的目的不在于模拟行为本身,而在于解决某些情况下使用C语言编程时程序整体框架结构分散、数据和函数脱节的问题。我们在后续章节会看到这样的例子。   总结   本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。   请记住:软件结构是软件的灵魂!结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。
本帖最后由 念慈菴 于 2017-8-15 17:42 编辑

回复评论 (7)

点赞  2017-8-16 13:40
学习
点赞  2017-8-17 11:11
写的好长,谢谢分享。
东西有点长,晚上还有点事,只稍稍看了开头一段,有两个地方,想说说来着
强者为尊,弱者,死无葬身之地
点赞  2017-8-27 23:21
引用: (1) 模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明;


怎么说呢?
先来说 模块 是一个什么定义,对我来说,我是这么理解
整体,我认为是一个系统,一个大一点的分法 就是 子系统;
再往下就是模块了。

那么,模块到底是一个什么东西,这里面是一个粒度的问题,如果只是这样说,没人知道其实怎么回事。
那我们就拿一个比较简单的例子来做一个对比好了。

假如说:
Linux系统,我会认为 音频 是一个子系统,视频,图像是一个子系统。
而在音频之下,我会把具体的编码解码作为一个模块。

而在有的地方,由于这个系统本身足够简单,其实已经没太大必要划分子系统这个层级。

比如说一个空调遥控器
难道你还要纠结把 按键面板 和 显示屏 分为一个子系统,然后把下面的什么东西分为具体的模块?

我个人的看法是,到底是怎么划分模块,其实划分多大并不重要,重点是你的划分依据和立足点。
比如我的立足点是:一个相对独立的功能。
比方说,Linux系统的音频子系统,为什么我要划分成一个子系统,那是因为假如有人拿另一个UNIX-like系统,那么很可能这个音频子系统几乎不需要改动,就可以直接替换进去给这个系统负责音频。

那么,说回正题。
既然,模块地立足点是一个功能,一个相对独立的功能。
那么,这个功能的实现,是不是一定要只有一个.c文件呢?

我觉得不一定,比方说,如果这个模块只是一个 矩阵按键模块,那这估计只是一个一两百行还不到的小源码,绰绰有余,没问题。

但是,如果这个模块是一个 配合gsensor来做各种人体姿态判断的模块,那它可能超过1K行。

但是,行数不是最重要的问题。
我们划分模块的根本目的是 分而治之。
比如说,gsensor的数据获得来源,存储方式(这和数据深度有关),又比如在姿态判断的时候,我们可能需要根据不同的运动类型,人体状态来划分不同的情况。

这里面可能有10种,20多种不同的情况,如果我们非要把它们融合在一个源文件里,那么看着是会很难受的,也不容易替换,辨识。
这个时候,很可能你是有多少种情况你就分多少个源文件。
看起来很多,但是你每次面对一个情形你只需要看一个源文件,它可能只有一两百行。

如果是你,你愿意哪一种?

但是,这么多的.c是不是要分别对应那么多个.h呢?
其实不然,.h的主要意义是什么?

是给外部提供一个接口定义。
那么,既然都是 gsensor判断出的人体姿态,为什么就不能一个.h出去呢?

那这个时候,其实我们就变成了
一个模块,多个.c和一个.h

当然,这个.h只是作为外部接口出去,它的边界是这个模块的功能。
如果在内部,也需要划分,那么,多分几个.h也是可能的,只不过这些.h就没必要分出去,但它可能在这几十个.c中,同时被若干个使用到。

因此,模块 和 .c .h并非对有数量上的 一一对应关系。

当然,这看不同的人,出于什么观点,都是有可能的。
强者为尊,弱者,死无葬身之地
点赞  2017-8-27 23:34
第二个。
关于头文件 .h

首先,c编译器只认.c,如果你不信,你可以把原来 .h文件的内容保持不变,然后把后缀名随便改成一个乱七八糟的东西,但是你在引用的时候,当然也要用那个乱七八糟的后缀名。

你会发现,通过编译毫无问题。

但是你把.c改成其他后缀名,哪怕.h,你都会发现立马出错。

这个小实验充分说明,对于c编译器来说,只有.c是亲儿子,其他的爱叫啥叫啥,它不管。

至于说 在.h里不能定义,只能声明变量这个事情。
其实道理很简单。
那就是一定要搞清楚 定义 和 声明 的区别。
一定要记住,定义涉及 内存分配,而声明不过是对一个名字,对,就只是那个名字而已的 引用。
回想一下你是怎么使用.h的,你是不是哪用到了就哪#include了,#include意味着什么,展开啊。
里面写了啥就直接展开啥。
那就是了,你都到处include了,如果你里面定义了变量,那岂不意味着你到处定义同一个变量,那肯定要出错啊。
而声明就不同了,只是引用嘛,爱引用几次引用几次。
强者为尊,弱者,死无葬身之地
点赞  2017-8-27 23:38
好,继续!!!
点赞  2017-9-15 16:02
感谢楼主分享,认真读了这个文章,好贴!!!!
点赞  2024-3-5 18:23
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复