C语⾔在嵌⼊式系统编程时的注意事项
姓名:李涛
学号:***********
【嵌⽜导读】:在嵌⼊式开发的⼯程中,由于C语⾔的通俗易懂,因此被选为嵌⼊式开发的⾸要选择。本⽂介绍了使⽤C语⾔在嵌⼊式开发的过程中⼀些技巧和思路以及注意事项。
【嵌⽜⿐⼦】:C语⾔,模块化开发
【嵌⽜提问】:C语⾔在嵌⼊式开发的⼤体思路?
【嵌⽜正⽂】:
C语⾔嵌⼊式系统编程注意事项
不同于⼀般形式的软件编程,嵌⼊式系统编程建⽴在特定的硬件平台上,势必要求其编程语⾔具备较强的硬件直接操作能⼒。⽆疑,汇编语⾔具备这样的特质。但是,归因于汇编语⾔开发过程的复杂性,它并不是嵌⼊式系统开发的⼀般选择。⽽与之相⽐,C语⾔--⼀种“⾼级的低级”语⾔,则成为嵌⼊式系统开
发的最佳选择。笔者在嵌⼊式系统项⽬的开发过程中,⼀次⼜⼀次感受到C语⾔的精妙,沉醉于C语⾔给嵌⼊式开发带来的便利。
⼤多数嵌⼊式系统的硬件平台。它包括两部分:
(1) 以通⽤处理器为中⼼的协议处理模块,⽤于⽹络控制协议的处理;
(2) 以数字信号处理器(DSP)为中⼼的信号处理模块,⽤于调制、解调和数/模信号转换。
本⽂的讨论主要围绕以通⽤处理器为中⼼的协议处理模块进⾏,因为它更多地牵涉到具体的C语⾔编程技巧。⽽DSP编程则重点关注具体的数字信号处理算法,主要涉及通信领域的知识,不是本⽂的讨论重点。
着眼于讨论普遍的嵌⼊式系统C编程技巧,系统的协议处理模块没有选择特别的CPU,⽽是选择了众所周知的CPU芯⽚--80186,每⼀位学习过《微机原理》的读者都应该对此芯⽚有⼀个基本的认识,且对其指令集⽐较熟悉。80186的字长是16位,可以寻址到的内存空间为1MB,只有实地址模式。C语⾔编译⽣成的指针为32位(双字),⾼16位为段地址,低16位为段内编译,⼀段最多64KB。
协议处理模块中的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串⼝)或以太⽹⼝下载到⽬标机上运⾏。其驻留于⽬标机FLASH存储器中的monitor程序可以监控宿主机Windows调试平台上的⽤户调试指令,获取CPU寄存器的值及⽬标机存储空间、I/O空间的内容。
后续章节将从软件架构、内存操作、屏幕操作、键盘操作、性能优化等多⽅⾯阐述C语⾔嵌⼊式系统的编程技巧。软件架构是⼀个宏观概念,与具体硬件的联系不⼤;内存操作主要涉及系统中的FLASH、RAM和NVRAM芯⽚;屏幕操作则涉及显⽰控制器和实时钟;键盘操作主要涉及键盘控制器;性能优化则给出⼀些具体的减⼩程序时间、空间消耗的技巧。
在我们的修炼旅途中将经过25个关⼝,这些关⼝主分为两类,⼀类是技巧型,有很强的适⽤性;⼀类则是常识型,在理论上有些意义。
So, let’s go.
C语⾔嵌⼊式系统编程注意事项之软件架构篇
模块划分的“划”是规划的意思,意指怎样合理的将⼀个很⼤的软件划分为⼀系列功能独⽴的部分合作完成系统的需求。
模块划分
模块划分的“划”是规划的意思,意指怎样合理的将⼀个很⼤的软件划分为⼀系列功能独⽴的部分合作完成系统的需求。C语⾔作为⼀种结构化的程序设计语⾔,在模块的划分上主要依据功能(依功能进⾏划分在⾯向对象设计中成为⼀个错误,⽜顿定律遇到了相对论),C语⾔模块化程序设计需理解如下概念:
(1) 模块即是⼀个.c⽂件和⼀个.h⽂件的结合,头⽂件(.h)中是对于该模块接⼝的声明;
(2) 某模块提供给其它模块调⽤的外部函数及数据需在.h中⽂件中冠以extern关键字声明;
(4) 永远不要在.h⽂件中定义变量!定义变量和声明变量的区别在于定义会产⽣内存分配的操作,是汇编阶段的概念;⽽声明则只是告诉包含该声明的模块在连接阶段从其它模块寻外部函数和变量。如:
/*module1.h*/
int a = 5; /* 在模块1的.h⽂件中定义int a */
/*module1 .c*/
#include “module1.h” /* 在模块1中包含模块1的.h⽂件 */
/*module2 .c*/
#i nclude “module1.h” /* 在模块2中包含模块1的.h⽂件 */
/*module3 .c*/
#i nclude “module1.h” /* 在模块3中包含模块1的.h⽂件 */
以上程序的结果是在模块1、2、3中都定义了整型变量a,a在不同的模块中对应不同的地址单元,这个世界上从来不需要这样的程序。正确的做法是:
/*module1.h*/
extern int a; /* 在模块1的.h⽂件中声明int a */
/*module1 .c*/
#i nclude “module1.h” /* 在模块1中包含模块1的.h⽂件 */
int a = 5; /* 在模块1的.c⽂件中定义int a */
/*module2 .c*/
#i nclude “module1.h” /* 在模块2中包含模块1的.h⽂件 */
/*module3 .c*/
#i nclude “module1.h” /* 在模块3中包含模块1的.h⽂件 */
这样如果模块1、2、3操作a的话,对应的是同⼀⽚内存单元。
⼀个嵌⼊式系统通常包括两类模块:
(1)硬件驱动模块,⼀种特定硬件对应⼀个模块;
(2)软件功能模块,其模块的划分应满⾜低偶合、⾼内聚的要求。
多任务还是单任务c语言struct头文件
所谓“单任务系统”是指该系统不能⽀持多任务并发操作,宏观串⾏地执⾏⼀个任务。⽽多任务系统则可以宏观并⾏(微观上可能串⾏)地“同时”执⾏多个任务。
多任务的并发执⾏通常依赖于⼀个多任务操作系统(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() )
{
switch(intType) /* 是不是很象WIN32程序的消息解析函数? */
{
/* 对,我们的中断类型解析很类似于消息驱动 */
case xxx: /* 我们称其为“中断驱动”吧? */
…
break;
case xxx:
…
break;
…
}
}
}
按上述⽅法设计的中断服务程序很⼩,实际的⼯作都交由主程序执⾏了。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论