C语言嵌入式系统编程修炼之软件架构篇27页精选文档.docx
《C语言嵌入式系统编程修炼之软件架构篇27页精选文档.docx》由会员分享,可在线阅读,更多相关《C语言嵌入式系统编程修炼之软件架构篇27页精选文档.docx(13页珍藏版)》请在冰豆网上搜索。
C语言嵌入式系统编程修炼之软件架构篇27页精选文档
C语言嵌入式系统编程修炼之软件架构篇--
模块划分
模块划分的"划"是规划的意思,意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。
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语言编程时程序整体框架结构分散、数据和函数脱节的问题。
我们在后续章节会看到这样的例子。
总结
本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。
请记住:
软件结构是软件的灵魂!
结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。
C语言嵌入式系统编程修炼之内存操作--
数据指针
在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV指令,而除C/C++以外的其它编程语言基本没有直接访问绝对地址的能力。
在嵌入式系统的实际调试中,多借助C语言指针所具有的对绝对地址单元内容的读写能力。
以指针直接操作内存多发生在如下几种情况:
(1)某I/O芯片被定位在CPU的存储空间而非I/O空间,而且寄存器对应于某特定地址;
(2)两个CPU之间以双端口RAM通信,CPU需要在双端口RAM的特定单元(称为mailbox)书写内容以在对方CPU产生中断;
(3)读取在ROM或FLASH的特定单元所烧录的汉字和英文字模。
譬如:
unsignedchar*p=(unsignedchar*)0xF000FF00;
*p=11;
以上程序的意义为在绝对地址0xF0000+0xFF00(80186使用16位段地址和16位偏移地址)写入11。
在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的数据类别。
上例中p++后的结果是p=0xF000FF01,若p指向int,即:
int*p=(int*)0xF000FF00;
p++(或++p)的结果等同于:
p=p+sizeof(int),而p-(或-p)的结果是p=p-sizeof(int)。
同理,若执行:
longint*p=(longint*)0xF000FF00;
则p++(或++p)的结果等同于:
p=p+sizeof(longint),而p-(或-p)的结果是p=p-sizeof(longint)。
记住:
CPU以字节为单位编址,而C语言指针以指向的数据类型长度作自增和自减。
理解这一点对于以指针直接操作内存是相当重要的。
函数指针
首先要理解以下三个问题:
(1)C语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;
(2)调用函数实际上等同于"调转指令+参数传递处理+回归位置入栈",本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的PC寄存器;
(3)因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可以"调用"一个根本就不存在的函数实体,晕?
请往下看:
请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到,186CPU启动后跳转至绝对地址0xFFFF0(对应C语言指针是0xF000FFF0,0xF000为段地址,0xFFF0为段内偏移)执行,请看下面的代码:
typedefvoid(*lpFunction)();/*定义一个无参数、无返回类型的*/
/*函数指针类型*/
lpFunctionlpReset=(lpFunction)0xF000FFF0;/*定义一个函数指针,指向*/
/*CPU启动后所执行第一条指令的位置*/
lpReset();/*调用函数*/
在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用:
lpReset(),它实际上起到了"软重启"的作用,跳转到CPU启动后第一条要执行的指令的位置。
记住:
函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令!
数组vs.动态申请
在嵌入式系统中动态内存申请存在比一般系统编程时更严格的要求,这是因为嵌入式系统的内存空间往往是十分有限的,不经意的内存泄露会很快导致系统的崩溃。
所以一定要保证你的malloc和free成对出现,如果你写出这样的一段程序:
char*function(void)
{
char*p;
p=(char*)malloc(…);
if(p==NULL)
…;
…/*一系列针对p的操作*/
returnp;
}
在某处调用function(),用完function中动态申请的内存后将其free,如下:
char*q=function();
…
free(q);
上述代码明显是不合理的,因为违反了malloc和free成对出现的原则,即"谁申请,就由谁释放"原则。
不满足这个原则,会导致代码的耦合度增大,因为用户在调用function函数时需要知道其内部细节!
正确的做法是在调用处申请内存,并传入function函数,如下:
char*p=malloc(…);
if(p==NULL)
…;
function(p);
…
free(p);
p=NULL;
而函数function则接收参数p,如下:
voidfunction(char*p)
{
…/*一系列针对p的操作*/
}
基本上,动态申请内存方式可以用较大的数组替换。
对于编程新手,笔者推荐你尽量采用数组!
嵌入式系统可以以博大的胸襟接收瑕疵,而无法"海纳"错误。
毕竟,以最笨的方式苦练神功的郭靖胜过机智聪明却范政治错误走反革命道路的杨康。
给出原则:
(1)尽可能的选用数组,数组不能越界访问(真理越过一步就是谬误,数组越过界限就光荣地成全了一个混乱的嵌入式系统);
(2)如果使用动态申请,则申请后一定要判断是否申请成功了,并且malloc和free应成对出现!
关键字const
const意味着"只读"。
区别如下代码的功能非常重要,也是老生长叹,如果你还不知道它们的区别,而且已经在程序界摸爬滚打多年,那只能说这是一个悲哀:
constinta;
intconsta;
constint*a;
int*consta;
intconst*aconst;
(1)关键字const的作用是为给读你代码的人传达非常有用的信息。
例如,在函数的形参前添加const关键字意味着这个参数在函数体内不会被修改,属于"输入参数"。
在有多个形参的时候,函数的调用者可以凭借参数前是否有const关键字,清晰的辨别哪些是输入参数,哪些是可能的输出参数。
(2)合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改,这样可以减少bug的出现。
const在C++语言中则包含了更丰富的含义,而在C语言中仅意味着:
"只能读的普通变量",可以称其为"不能改变的变量"(这个说法似乎很拗口,但却最准确的表达了C语言中const的本质),在编译阶段需要的常数仍然只能以#define宏定义!
故在C语言中如下程序是非法的:
constintSIZE=10;
chara[SIZE];/*非法:
编译阶段不能用到变量*/
关键字volatile
C语言编译器会对用户书写的代码进行优化,譬如如下代码:
inta,b,c;
a=inWord(0x100);/*读取I/O空间0x100端口的内容存入a变量*/
b=a;
a=inWord(0x100);/*再次读取I/O空间0x100端口的内容存入a变量*/
c=a;
很可能被编译器优化为:
inta,b,c;
a=inWord(0x100);/*读取I/O空间0x100端口的内容存入a变量*/
b=a;
c=a;
但是这样的优化结果可能导致错误,如果I/O空间0x100端口的内容在执行第一次读操作后被其它程序写入新值,则其实第2次读操作读出的内容与第一次不同,b和c的值应该不同。
在变量a的定义前加上volatile关键字可以防止编译器的类似优化,正确的做法是:
volatileinta;
volatile变量可能用于如下几种情况:
(1)并行设备的硬件寄存器(如:
状态寄存器,例中的代码属于此类);
(2)一个中断服务子程序中会访问到的非自动变量(也就是全局变量);
(3)多线程应用中被几个任务共享的变量。
CPU字长与存储器位宽不一致处理
在背景篇中提到,本文特意选择了一个与CPU字长不一致的存储芯片,就是为了进行本节的讨论,解决CPU字长与存储器位宽不一致的情况。
80186的字长为16,而NVRAM的位宽为8,在这种情况下,我们需要为NVRAM提供读写字节、字的接口,如下:
typedefunsignedcharBYTE;
typedefunsignedintWORD;
/*函数功能:
读NVRAM中字节
*参数:
wOffset,读取位置相对NVRAM基地址的偏移
*返回:
读取到的字节值
*/
externBYTEReadByteNVRAM(WORDwOffset)
{
LPBYTElpAddr=(BYTE*)(NVRAM+wOffset*2);/*为什么偏移要×2?
*/
return*lpAddr;
}
/*函数功能:
读NVRAM中字
*参数:
wOffset,读取位置相对NVRAM基地址的偏移
*返回:
读取到的字
*/
externWORDReadWordNVRAM(WORDwOffset)
{
WORDwTmp=0;
LPBYTElpAddr;
/*读取高位字节*/
lpAddr=(BYTE*)(NVRAM+wOffset*2);/*为什么偏移要×2?
*/
wTmp+=(*lpAddr)*256;
/*读取低位字节*/
lpAddr=(BYTE*)(NVRAM+(wOffset+1)*2);/*为什么偏移要×2?
*/
wTmp+=*lpAddr;
returnwTmp;
}
/*函数功能:
向NVRAM中写一个字节
*参数:
wOffset,写入位置相对NVRAM基地址的偏移
*byData,欲写入的字节
*/
externvoidWriteByteNVRAM(WORDwOffset,BYTEbyData)
{
…
}
/*函数功能:
向NVRAM中写一个字*/
*参数:
wOffset,写入位置相对NVRAM基地址的偏移
*wData,欲写入的字
*/
externvoidWriteWordNVRAM(WORDwOffset,WORDwData)
{
…
}
子贡问曰:
Why偏移要乘以2?
子曰:
请看图1,16位80186与8位NVRAM之间互连只能以地址线A1对其A0,CPU本身的A0与NVRAM不连接。
因此,NVRAM的地址只能是偶数地址,故每次以0x10为单位前进!
图1CPU与NVRAM地址线连接
子贡再问:
Sowhy80186的地址线A0不与NVRAM的A0连接?
子曰:
请看《IT论语》之《微机原理篇》,那里面讲述了关于计算机组成的圣人之道。
总结
本篇主要讲述了嵌入式系统C编程中内存操作的相关技巧。
掌握并深入理解关于数据指针、函数指针、动态申请内存、const及volatile关键字等的相关知识,是一个优秀的C语言程序设计师的基本要求。
当我们已经牢固掌握了上述技巧后,我们就已经学会了C语言的99%,因为C语言最精华的内涵皆在内存操作中体现。
我们之所以在嵌入式系统中使用C语言进行程序设计,99%是因为其强大的内存操作能力!
如果你爱编程,请你爱C语言;
如果你爱C语言,请你爱指针;
如果你爱指针,请你爱指针的指针!
希望以上资料对你有所帮助,附励志名言3条:
1、上帝说:
你要什么便取什么,但是要付出相当的代价。
2、目标的坚定是性格中最必要的力量源泉之一,也是成功的利器之