C语言嵌入式系统编程修炼软件架构篇.docx
《C语言嵌入式系统编程修炼软件架构篇.docx》由会员分享,可在线阅读,更多相关《C语言嵌入式系统编程修炼软件架构篇.docx(26页珍藏版)》请在冰豆网上搜索。
C语言嵌入式系统编程修炼软件架构篇
发表于2007/2/269:
57:
35
软件结构是软件的灵魂!
结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。
一个高尚的程序员应该是写出如艺术作品般程序的程序员。
模块划分
模块划分的"划"是规划的意思,意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。
C语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能(依功能进行划分在面向对象设计中成为一个错误,牛顿定律遇到了相对论),C语言模块化程序设计需理解如下概念:
(1)模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明(对函数的声明);
(2)某模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明(各模块共用的变量需定义为全局变量,以使各模块之间调用);
(3)模块内的函数和全局变量需在.c文件开头冠以static关键字声明;
(4)永远不要在.h文件中定义变量!
定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。
如:
/*module1.h*/
inta=5;/*在模块1的.h文件中定义inta*/
/*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*/
externinta;/*在模块1的.h文件中声明inta*/,全局变量
/*module1.c*/
#include"module1.h"/*在模块1中包含模块1的.h文件*/
inta=5;/*在模块1的.c文件中定义inta*/,这里应当是局部变量
/*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,youareright,但是你得不到鲜花和掌声。
实际上,这是一个没有太大意义的牛角尖,因为这个世界从来不需要一个处理完几个消息就喊着要OS杀死它的WIN32程序,不需要一个刚开始RUN就自行了断的嵌入式系统,不需要莫名其妙启动一个做一点事就干掉自己的线程。
有时候,过于严谨制造的不是便利而是麻烦。
君不见,五层的TCP/IP协议栈超越严谨的ISO/OSI七层协议栈大行其道成为事实上的标准?
经常有网友讨论:
printf("%d,%d",++i,i++);/*输出是什么?
*/
c=a+++b;/*c=?
*/
等类似问题。
面对这些问题,我们只能发出由衷的感慨:
世界上还有很多有意义的事情等着我们去消化摄入的食物。
实际上,嵌入式系统要运行到世界末日。
中断服务程序
中断是嵌入式系统中重要的组成部分,但是在标准C中不包含中断。
许多编译开发商在标准C上增加了对中断的支持,提供新的关键字用于标示中断服务程序(ISR),类似于__interrupt、#programinterrupt等。
当一个函数被定义为ISR的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
中断服务程序需要满足如下要求:
(1)不能返回值;
(2)不能向ISR传递参数;
(3)ISR应该尽可能的短小精悍;
(4)printf(char*lpFormatString,…)函数会带来重入和性能问题,不能在ISR中采用。
在某项目的开发中,我们设计了一个队列,在中断服务程序中,只是将中断类型添加入该队列中,在主程序的死循环中不断扫描中断队列是否有中断,有则取出队列中的第一个中断类型,进行相应处理。
/*存放中断的队列*/
typedefstructtagIntQueue
{
intintType;/*中断类型*/
structtagIntQueue*next;
}IntQueue;
IntQueuelpIntQueueHead;
__interruptISRexample()
{
intintType;
intType=GetSystemType();
QueueAddTail(lpIntQueueHead,intType);/*在队列尾加入新的中断*/
}
在主程序循环中判断是否有中断:
While
(1)
{
If(!
IsIntQueueEmpty())
{
intType=GetFirstInt();
switch(intType)/*是不是很象WIN32程序的消息解析函数?
*/
{
/*对,我们的中断类型解析很类似于消息驱动*/
casexxx:
/*我们称其为"中断驱动"吧?
*/
…
break;
casexxx:
…
break;
…
}
}
}
按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。
硬件驱动模块
一个硬件驱动模块通常应包括如下函数:
(1)中断服务程序ISR
(2)硬件初始化
a.修改寄存器,设置硬件参数(如UART应设置其波特率,AD/DA设备应设置其采样速率等);
b.将中断服务程序入口地址写入中断向量表:
/*设置中断向量表*/
m_myPtr=make_far_pointer(0l);/*返回voidfar型指针voidfar**/
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程序模拟了一个最简单的"类":
#ifndefC_Class
#defineC_Classstruct
#endif
C_ClassA
{
C_ClassA*A_this;/*this指针*/
void(*Foo)(C_ClassA*A_this);/*行为:
函数指针*/
inta;/*数据*/
intb;
};
我们可以利用C语言模拟出面向对象的三个特性:
封装、继承和多态,但是更多的时候,我们只是需要将数据与行为封装以解决软件结构混乱的问题。
C模拟面向对象思想的目的不在于模拟行为本身,而在于解决某些情况下使用C语言编程时程序整体框架结构分散、数据和函数脱节的问题。
我们在后续章节会看到这样的例子。
总结
本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。
请记住:
软件结构是软件的灵魂!
结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。
系统分类:
软件开发
用户分类:
软件设计
标签:
无标签
来源:
转贴
发表评论阅读全文(751)|回复(0)
1
C语言嵌入式系统编程修炼(背景篇)
发表于2007/2/269:
56:
30
不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势必要求其编程语言具备较强的硬件直接操作能力。
无疑,汇编语言具备这样的特质。
但是,归因于汇编语言开发过程的复杂性,它并不是嵌入式系统开发的一般选择。
而与之相比,C语言--一种"高级的低级"语言,则成为嵌入式系统开发的最佳选择。
笔者在嵌入式系统项目的开发过程中,一次又一次感受到C语言的精妙,沉醉于C语言给嵌入式开发带来的便利。
图1给出了本文的讨论所基于的硬件平台,实际上,这也是大多数嵌入式系统的硬件平台。
它包括两部分:
(1)以通用处理器为中心的协议处理模块,用于网络控制协议的处理;
(2)以数字信号处理器(DSP)为中心的信号处理模块,用于调制、解调和数/模信号转换。
本文的讨论主要围绕以通用处理器为中心的协议处理模块进行,因为它更多地牵涉到具体的C语言编程技巧。
而DSP编程则重点关注具体的数字信号处理算法,主要涉及通信领域的知识,不是本文的讨论重点。
着眼于讨论普遍的嵌入式系统C编程技巧,系统的协议处理模块没有选择特别的CPU,而是选择了众所周知的CPU芯片--80186,每一位学习过《微机原理》的读者都应该对此芯片有一个基本的认识,且对其指令集比较熟悉。
80186的字长是16位,可以寻址到的内存空间为1MB,只有实地址模式。
C语言编译生成的指针为32位(双字),高16位为段地址,低16位为段内编译,一段最多64KB。
图1系统硬件架构
协议处理模块中的FLASH和RAM几乎是每个嵌入式系统的必备设备,前者用于存储程序,后者则是程序运行时指令及数据的存放位置。
系统所选择的FLASH和RAM的位宽都为16位,与CPU一致。
实时钟芯片可以为系统定时,给出当前的年、月、日及具体时间(小时、分、秒及毫秒),可以设定其经过一段时间即向CPU提出中断或设定报警时间到来时向CPU提出中断(类似闹钟功能)。
NVRAM(非易失去性RAM)具有掉电不丢失数据的特性,可以用于保存系统的设置信息,譬如网络协议参数等。
在系统掉电或重新启动后,仍然可以读取先前的设置信息。
其位宽为8位,比CPU字长小。
文章特意选择一个与CPU字长不一致的存储芯片,为后文中一节的讨论创造条件。
UART则完成CPU并行数据传输与RS-232串行数据传输的转换,它可以在接收到[1~MAX_BUFFER]字节后向CPU提出中断,MAX_BUFFER为UART芯片存储接收到字节的最大缓冲区。
键盘控制器和显示控制器则完成系统人机界面的控制。
以上提供的是一个较完备的嵌入式系统硬件架构,实际的系统可能包含更少的外设。
之所以选择一个完备的系统,是为了后文更全面的讨论嵌入式系统C语言编程技巧的方方面面,所有设备都会成为后文的分析目标。
嵌入式系统需要良好的软件开发环境的支持,由于嵌入式系统的目标机资源受限,不可能在其上建立庞大、复杂的开发环境,因而其开发环境和目标运行环境相互分离。
因此,嵌入式应用软件的开发方式一般是,在宿主机(Host)上建立开发环境,进行应用程序编码和交叉编译,然后宿主机同目标机(Target)建立连接,将应用程序下载到目标机上进行交叉调试,经过调试和优化,最后将应用程序固化到目标机中实际运行。
CAD-UL是适用于x86处理器的嵌入式应用软件开发环境,它运行在Windows操作系统之上,可生成x86处理器的目标代码并通过PC机的COM口(RS-232串口)或以太网口下载到目标机上运行,如图2。
其驻留于目标机FLASH存储器中的monitor程序可以监控宿主机Windows调试平台上的用户调试指令,获取CPU寄存器的值及目标机存储空间、I/O空间的内容。
图2交叉开发环境
后续章节将从软件架构、内存操作、屏幕操作、键盘操作、性能优化等多方面阐述C语言嵌入式系统的编程技巧。
软件架构是一个宏观概念,与具体硬件的联系不大;内存操作主要涉及系统中的FLASH、RAM和NVRAM芯片;屏幕操作则涉及显示控制器和实时钟;键盘操作主要涉及键盘控制器;性能优化则给出一些具体的减小程序时间、空间消耗的技巧。
在我们的修炼旅途中将经过25个关口,这些关口主分为两类,一类是技巧型,有很强的适用性;一类则是常识型,在理论上有些意义。
So,let’sgo.
系统分类:
软件开发
用户分类:
软件设计
标签:
无标签
来源:
转贴
发表评论阅读全文(652)|回复(0)
0
C51编程:
多任务程序设计的结构
发表于2007/1/821:
05:
44
C51编程:
多任务程序设计的结构,纯属个人观点,希望大家借签一下,提出更好的意见。
[小师⊕][156次]01-7-31下午
08:
48:
27
C51的一些特征技巧可供利用:
1.时间的模糊性.
在大多数情况下,时间是具有模糊性的.象秒,分钟,小时..,从长的时间角度,即使你计秒的时
间被退后0.5秒,
在大多数情况下都是允许的,包括一些显示.还有象扫描键盘,你可在20MS去抖,也可在30,30MS时
间去抖,这个时间范围是有一定弹性的.又如闪烁要求400MS,你可在410MS去刷新,下次在
2*400MS,只要保证长的周期定时是准确的,个别时间是可推迟的。
这样的情形会在许多地方发
生,这就给设计多任务程序提供了一个基础.
2.消息的周期循环性.
消息指系统函数(定时类的),模块之间有状态变化,模块内部有状态请求而相应产生的标志数
据或变量数据,它的特点是它的遍历整个模块,直到有模块接收它后让它消失,没有模块接收时,循
环一周被自身消失.
举个例,有T0计数器0.1MS产生一个中断,让其他所有模块都知道,模块不能消灭它,它只能被自
己消灭:
voidtimer0(void)interrupt1/*T0中断*/
{
fSYS_100us=1;
}
bitfSYS_TimeNow;
#defineTimer0_MainLoop(){fSYS_TimeNow=0;if(fSYS_100us)
{fSYS_TimeNow=1;fSYS_100us=0;}}
unsignedcharuCount;
main()
{
init();
uCount="100";
while
(1){
Time0_MainLoop();
Task0();
if(fSYS_TimeNow)Task1();
Task2();
if(fSYS_TimeNow){
uCount--;
if(uCount==0){
uCount=100;
Task3();
}
}
}
}
这样消息具有自我生成消失发布的能力,而且使模块具有独立性(Time0_MainLoop();可放在
WHILE中的任何地方而不影响它的作用).
而象键盘之类产生的消息,常常是每个模块接收到它后,就使它消失,避免其他模块也接收.
消息在多任务程序中的作用:
相当与桥梁,使模块间既相互独立又相互连接。
比如说,有个按键
产生的消息,打开设置画面显示:
nSetScreenOn,让其它的模块中相应程序运行,这是连接。
如
果你的程序没写到设置画面显示部分,完全不影响整个程序,照常运行正确,这是独立性。
消息通常用队列存储,一如按键缓冲队列,一般包括函数:
NewsPush(unsignedcharnData)压
入消息,unsignedcharNewsPop()弹出消息,NewsRead()宏定义的读队列中最前端的消息,
fNewsEnable表示有消息需要各个模块接收,gNewsNum消息个数。
一个模块接收后执行NewsPop
();fNewsEnable=0消灭消息。
现在举个例,P1.0键盘扫描模块作为多任务模块,并产生消息nKeyPush按键按下,nKeyPop松
开。
nKeyPush时P1.1=1,nKeyPop时P1.1=0.
/**********************************/
voidtimer0(void)interrupt1/*T0中断*/{
fSYS_1ms=1;
}
bitfSYS_TimeNow; /*1MS时间到消息*/
#defineTimer0_MainLoop(){fSYS_TimeNow=0;if(fSYS_1ms)
{fSYS_TimeNow=1;fSYS_1ms=0;}}
main(){
init();
while
(1){
Timer0_MainLoop(); /*系统时间循环*/
/*----------------------------------*/
if(fSYS_TimeNow) /*如果有1MS到的消息产生*/
Key_MainLoop(); /*按键检查循环*/
}
/*----------------------------------*/
Work_MainLoop(); /*按键的任务循环*/
/*----------------------------------*/
/*消息处理中心,可写成宏较直观*/
fNewsEnable=0;
if(gNewsNum)fNewsEnable=1; /*有消息,通知*/
}
}
/*按键检查循环模块*/
unsignedcharmKeyTask; /*多任务中模块的任务号,代表模块的执行点*/
sbitfKeyIn="P1"^0;
unsignedcharmKeyTime;
voidKey_MainLoop(){
switch(mKeyTask){
case0:
/*现在是常规状态*/
if(fKeyIn==0){
mKeyTime=30; /*大概30MS的去抖时间*/
mKeyTask++;
}
break;
case1:
/*按下去抖29-30MS*/
mKeyTime--;
if(mKeyTime==0)mKeyTask++;
break;
case2:
/*判断按键是否保持按下*/
if(fKeyIn==0){
NewsPush(nKeyPush); /*压入按键按下消息*/
mKeyTask++;
}
elsemKeyTask="0";
break;
case3:
/*判断按键是否松开*/
if(fKeyIn){
mKeyTime=30; /*大概30MS的去抖时间*/
mKeyTask++;
}
break;
case4:
/*松开去抖29-30MS*/
mKeyTime--;
if(mKeyTime==0)mKeyTask++;
break;
case5:
/*判断按键是否保持按下*/
if(fKey