嵌入式开发杂谈.docx
- 文档编号:11925992
- 上传时间:2023-04-16
- 格式:DOCX
- 页数:13
- 大小:27.26KB
嵌入式开发杂谈.docx
《嵌入式开发杂谈.docx》由会员分享,可在线阅读,更多相关《嵌入式开发杂谈.docx(13页珍藏版)》请在冰豆网上搜索。
嵌入式开发杂谈
嵌入式开发杂谈
1前言
在刚刚涉足嵌入式开发的时候,总想找到这样一本书,它可以解决我一些这样那样的疑惑。
但遗憾的是,到现在也没有这样一本书面世,而且我想永远也不可能面世了。
因为我的疑惑太多太杂了。
这些疑惑在教科书中又难以寻找到答案。
C教程注重讲C的语法,编译原理注重讲语法,语义的分析。
每一门教科书都是有它的注重,所以那些交叉的问题便成了三不管。
市场上的那些自称为《XX宝典》、《XX圣经》的书却总是说一些可能连作者自己也没搞清楚的问题。
于是我想,我想了解的也许是大家都想了解的吧,那么把我学到的一点东西写出来,大家也许就可以少花点时间在上面,留出宝贵的脑力资源去做更有意义的事。
杂七杂八,不成体系,固命名为杂谈。
基于共享精神,本篇文档保留版权,但可以允许非商业目的任意复制、拷贝,商业用途需经本人同意。
2语言选择,C还是其他
刚刚涉及嵌入式开发者总是先阅读一些指导类型文章,然后就开始对开发语言的选择踌躇不决。
是C还是C++?
还是好像更热门的JAVA?
不用犹豫,至少目前看来C还是你的选择。
嵌入式开发的本质是订制开发,硬件平台林林总总,处理能力高下不同,如果想保护你学习精力投资的话,C是最好的“优绩股”。
C++的优点在于它的代码重用,但是效率比C低很多,最重要的是,并非所有芯片的编译器都能支持C++.。
JAVA就更不用提及,在一个虚拟平台上开发的优点是不用关心具体的硬件细节,但这不是一个嵌入式开发者的作风,换一种说法,这种开发不能称之为嵌入式开发。
C被称为高级语言中的低级语言,低级语言中的高级语言,这是因为其一方面有高级语言所具有的接近于人类思想的语言体系,另一方面同时支持地址与位操作。
可以方便的与硬件打交道。
嵌入式开发必然要操作IO、硬件地址,没有位操作和指针你又如何方便做到?
3嵌入式开发一般流程
嵌入式开发的流程与高层开发大体类似,编码——编译、链接——运行。
中间当然可以有联机调试,重新编码等递归过程。
但有一些不同之处。
首先,开发平台不同。
受嵌入式平台处理能力所限,嵌入式开发一般都采用交叉编译环境开发。
所谓交叉编译就是在A平台上编译B平台上运行的目标程序。
在A平台上运行的B平台程序编译器就被称为交叉编译器。
一个初入门者,建立一套这样的编译环境也许就要花掉几天的时间。
其次,调试方式不同。
我们在Windows或者Linux上开发的程序可以马上运行察看运行结果,也可以利用IDE来调试运行过程,但是嵌入式开发者却至少需要作一系列工作才能达到这种地步。
目前最流行的是采用JTAG方式连接到目标系统上,将编译成功的代码下载运行,高级的调试器几乎可以像VC环境一样任意的调试程序。
再者,开发者所了解层次结构不同。
高层软件开发者把工作的重点放在对应用需求的理解和实现上。
嵌入式开发者对整个过程细节必须比高层开发者有更深的认识。
最大不同之处在于有操作系统支持的程序不需要你关心程序的运行地址以及程序链接后各个程序块最后的位置。
像Windows,Linux这类需要MMU支持的操作系统,其程序都是放置在虚拟地址空间的一个固定的内存地址。
不管程序在真正RAM空间的地址位置在哪里,最后都由MMU映射到虚拟地址空间的一个固定的地址。
为什么程序的运行与存放的地址要相关呢?
学过汇编原理,或者看过最后编译成机器码程序的人就知道,程序中的变量、函数最后都在机器码中体现为地址,程序的跳转,子程序的调用,以及变量调用最后都是CPU通过直接提取其地址来实现的。
编译时指定的TEXT_BASE就是所有一切地址的参考值。
如果你指定的地址与最后程序放置的地址不一致显然不能正常运行。
但也有例外,不过不寻常的用法当然要付出不寻常的努力。
有两种方法可以解决这个问题。
一种方法是在程序的最起始编写与地址无关的代码,最后将后面的程序自搬移到你真正指定的TEXT_BASE然后跳转到你将要运行的代码处。
另一种方法是,TEXT_BASE指定为你程序的存放地址,然后将程序搬移到真正运行的地址,有一个变量将后者的地址记录下来作为参考值,在以后的符号表地址都以此值作为参考与偏移值合成为其真正的地址。
听起来很拗口,实现起来也很难,在后面的内容中有更好的解决办法——用一个BootLoader支持。
另外,一个完整的程序必然至少有三个段TEXT(正文,也就是最后用程序编译后的机器指令)段、BSS(未初始变量)段DATA(初始化变量)段。
前面讲到的TEXT_BASE只是TEXT段的基址,对于另外的BSS段和DATA段,如果最后的整个程序放在RAM中,那么三个段可以连续放置,但是,如果程序是放置在ROM或者FLASH这种只读存储器中,那么你还需要指定你的其他段的地址,因为代码在运行中是不改变的,而后两者却不同。
这些工作都是在链接的时候完成,编译器必然为你提供了一些手段让你完成这些工作。
还是那句话,有操作系统支持的编程屏蔽了这些细节,让你完全不用考虑这些头痛的问题。
但是嵌入式开发者没有那么幸运,他们总是在一个冷冰冰的芯片上从头做起。
CPU上电复位总是从一个固定的地址去找程序,开始其繁忙的工作。
对于我们的PC来说这个地址就是我们的BIOS程序,对于嵌入式系统,一般没有BIOS支持,RAM不能在掉电情况下保留你的程序,所以必须将程序存放在ROM或FLASH中,但是一般来讲,这些存储器的宽度和速度都无法与RAM相提并论。
程序在这些存储器上运行会降低运行速率。
大多数的方案是在此处存放一个BootLoader,BootLoader所完成的功能可多可少,一个基本的BootLoader只完成一些系统初始化并将用户程序搬移到一定地址,然后跳转到用户程序即交出CPU控制权,功能强大的BootLoad还可以支持网络、串口下载,甚至调试功能。
但不要指望有一个像PCBIOS那样通用的BootLoader供你使用,至少你需要作一些移植工作使其符合你的系统,这个移植工作也是你开发的一个部分,作为嵌入式开发个入门者来讲,移植或者编写一个BootLoader会使你受益匪浅。
没有BootLoader行不行?
当然可以,要么你就牺牲效率直接从ROM中运行,要么你就自己编写程序搬移代码去RAM运行,最主要的是,开发过程中你要有好的调试工具支持在线调试,否则你就得在改动哪怕一个变量的情况下都要去重新烧片验证。
继续程序入口的话题,不管过程如何,程序最后在执行时都是变成了机器指令,一个纯的执行程序就是这些机器指令的集合。
像我们在操作系统上的可运行程序都不是纯的执行程序,而是带有格式的。
一般除了包含上面提到的几个段以外,还有程序的长度,校验以及程序入口——就是从哪儿开始执行用户程序。
为什么有了程序地址还需要有程序的入口呢?
这是因为你要真正开始执行的代码并非一定放置在一个文件的最开始,就算放在最开始,除非你去控制链接,否则在多文件的情况下,编译器也不一定将你的这段程序放置在最后程序的最顶端。
像我们一般有操作系统支持的程序,只需在你的代码中有一个main作为程序入口——注意这个main只是大多数编译器约成定俗的入口,除非你利用了别人的初始化库,否则程序入口可以自行设定——即可。
显然,带有格式的这种执行文件使用更加灵活,但需要BootLoader的支持。
有关执行文件格式的内容可以看看ELF文件格式。
4编译预处理
首先看看文件包含,从我们的第一个C程序HelloWorld!
开始,我们就使用头文件包含,但是另我惊奇的是,很多人在做了很长时间的开发以后仍然对文件的包含没有正确的认识或者是概念不清,有更多的人却把头文件和与之相关联的库混淆。
为了照顾这些入门读者,请允许我在这儿罗嗦一下,其实文件包含的本质就是把一个大的文件截成几个小文件便于管理和阅读,如果你包含了那个文件,那么你把这个文件的所有内容原封不动的复制到你包含其的文件中,效果是完全一样的,另一方面,如果你编译了一些中间代码,如库文件,可以通过提供头文件来告知调用者你的库包含的函数和调用格式,但是真正的代码已经变成了目标代码以库文件形式存在了。
至于包含文件的后缀如.h只是告诉使用者,这是一个头文件,你用任何别的名字,编译器都一般不会在意。
那些对头文件和库还混淆的朋友应该恍然大悟了吧,其实头文件只能保证你的程序编译不出现语法错误,但是直到最后链接的时候才会真正使用到库,那些只把一个头文件拷贝来就想拥有一个库的人再也不要犯这样的错误了。
如果你的工程中源程序数目繁多令你觉得管理困难,把他们全部包含在一个文件中也未尝不可。
另一个初学者常常遇到的问题就是由于重复包含引起的困惑。
如果一个文件中包含了另一个文件两次或两次以上很可能引起重复定义的问题,但是没有人蠢到会重复包含两次同一个文件的,这种问题都是隐式的重复包含,比如A文件中包含了B文件和C文件,B文件中又包含了C文件,这样,A文件实际上已经包含了C文件两次。
不过一个好的头文件巧妙的利用编译预处理避免了这种情况。
在头文件中你可能发现这样的一些预处理:
#ifndef__TEST_H__
#define__TEST_H__
……
#endif/*__TEST_H__*/
这三行编译预处理前两行一般位于文件最顶端,最后文件位于文件最末端,它的意思是,如果没有定义__TEST_H__那么就定义__TEST_H__同时下面的代码一直到#endif前参与编译,反之不参与编译。
多么巧妙的设计,有了这三行简洁的预处理,这个文件即使被包含几万次也只能算一次。
我们再来看看宏的使用。
初学者在看别人代码的时候总是想,为什么用那么多宏呢?
看得人一头雾水,的确,有时候宏的使用会降低代码的可读性。
但有时宏也可以提高代码的可读性,看看下边这两段代码:
1)
#defineSCC_GSMRH_RSYN0x00000001/*receivesynctiming*/
#defineSCC_GSMRH_RTSM0x00000002/*RTS*mode*/
#defineSCC_GSMRH_SYNL0x0000000c/*synclength*/
#defineSCC_GSMRH_TXSY0x00000010/*transmitter/receiversync*/
#defineSCC_GSMRH_RFW0x00000020 /*RxFIFOwidth*/
#defineSCC_GSMRH_TFL0x00000040 /*transmitFIFOlength*/
#defineSCC_GSMRH_CTSS0x00000080/*CTS*sampling*/
#defineSCC_GSMRH_CDS0x00000100 /*CD*sampling*/
#defineSCC_GSMRH_CTSP0x00000200/*CTS*pulse*/
#defineSCC_GSMRH_CDP0x00000400 /*CD*pulse*/
#defineSCC_GSMRH_TTX0x00000800 /*transparenttransmitter*/
#defineSCC_GSMRH_TRX0x00001000 /*transparentreceiver*/
#defineSCC_GSMRH_REVD0x00002000/*reversedata*/
#defineSCC_GSMRH_TCRC0x0000c000/*transparentCRC*/
#defineSCC_GSMRH_GDE0x00010000 /*glitchdetectenable*/
*(int*)0xff000a04=SCC_GSMRH_REVD|SCC_GSMRH_TRX|SCC_GSMRH_TTX|
SCC_GSMRH_CDP|SCC_GSMRH_CTSP|SCC_GSMRH_CDS|SCC_GSMRH_CTSS;
2)
*(int*)0xff000a04=0x00003f80;
这是对某一个寄存器的赋值程序,两者完成的是完全相同的工作。
第一段代码略显冗长,第二段代码很简洁,但是如果你如果想改动此寄存器的设置的时候显然更喜欢看到的是第一段代码,因为它现有的值已经很清楚,要对那些位赋值只要用相应得宏定义即可,不必每次改变都拿笔再重新计算一次。
这一点对于嵌入式开发者很重要,有时我们调试一个设备的时候,一个关键寄存器的值也许会被我们修改很多次,每一次都计算每一位所对应得值是一件很头疼的事。
另外利用宏也可以提高代码的运行效率,子程序的调用需要压栈出栈,这一过程如果过于频繁会耗费掉大量的CPU运算资源。
所以一些代码量小但运行频繁的代码如果采用带参数宏来实现会提高代码的运行效率,比如我们常常用到的对外部IO赋值的操作,你可以写一个类似下边的函数来实现:
voidoutb(unsignedcharval,unsignedint*addr)
{
*addr=val;
}
仅仅是一句语句的函数,却要调用一个函数,如果不用函数呢,重复写上面的语句又显得罗嗦。
不如用下面的宏实现。
#defineoutb(b,addr)(*(volatileunsignedchar*)(addr)=(b))
由于不需要调用子函数,宏提高了运行效率,但是浪费了程序空间,这是由于凡是用到此宏的地方,都要替换为一句其代替的语句。
开发者需要根据系统需求取舍时间与空间。
最后大家看一段将宏用到炉火纯青者的代码,此段代码来自于DasU-boot,这一段代码用7个简单的宏实现了6个类似功能的函数!
#definePCI_HOSE_OP(rw,size,type)\
intpci_hose_##rw##_config_##size(structpci_controller*hose,\
pci_dev_tdev,\
intoffset,typevalue)\
{\
returnhose->rw##_##size(hose,dev,offset,value);\
}
PCI_HOSE_OP(read,byte,u8*)
PCI_HOSE_OP(read,word,u16*)
PCI_HOSE_OP(read,dword,u32*)
PCI_HOSE_OP(write,byte,u8)
PCI_HOSE_OP(write,word,u16)
PCI_HOSE_OP(write,dword,u32)
最后,我们再来看几个巧妙利用预编译和宏的例子。
I大段代码注释
在程序调试中,我们往往需要临时注释掉一大段代码,如果这段代码中已经有很多注释,那么用/**/这种方式注释会遇到匹配问题,很难一下将整段代码注释掉,//这种注释很多编译器不支持,即使支持,把几十行代码用这种方式注释也要花掉你很多时间,有好的编辑器倒是支持用这种方式注释掉大段代码,但并非每个人都有用这样的编辑器。
如果用预编译很容易处理这个问题,在你需要注释的代码前和代码后分别加:
#if0
…
#endif
一段代码立马从编译器视线中消失,如果再想临时加入,只需要将0改为1即可。
是不是很方便?
II行调试
在调试一段代码出现错误时我们往往需要确定错误出现在哪一行,像我们懒于使用仿真器而习惯于用printf来观看运行过程的人来说,往往首先为自己的平台编写一个类printf的函数可以输出格式化的字符串,在熟练使用编译器宏之前我们往往喜欢加上一些这样的代码:
printf(“1\n”);
…
printf(“2\n”);
…
printf(“3\n”);
…
用此办法来确定程序停止的位置,事实上有一个更好的办法可以达到这个目的。
大多数编译器支持这样一个宏__LINE__,这个宏在编译中会替换为宏所在文件中的行号。
有了这个宏,你只需要将一句语句不加修改的拷贝粘贴就足以完成上面的功能了。
printf(“%d\n”,__LINE__);
…
printf(“%d\n”,__LINE__);
…
printf(“%d\n”,__LINE__);
…
III在程序中加入编译时间
__DATE__,__TIME__这两个宏会在编译过程中替换为目前日期和时间的字符串。
所以不妨在你的程序开头中加入这样的语句:
printf(“Builddate:
%s,\nBuildTime:
%s\n”,__DATE__,__TIME__);
这样,程序子每次运行的时候就会打印出其最后编译的日期和时间,再也不用疑惑是否烧错程序版本了。
5volatile和cache
这两个词的搭配看上去似乎风马牛不相及。
但是它们却可以给你带来同样的困惑——逻辑正确无误的程序却能得到非意料的结果.对于一个初入门者来说,一段自认为相当不错的代码却得到这样的结果令其非常沮丧,也许一个心理承受能力差的人从此对嵌入式开发失去信心也未可知。
Ivolatile
volatile这个ANSIC关键字在经典的C教程中很少提及,高层编程的人也可能永远都不会用到,但是作为嵌入式开发者来说,这个关键字使用频率应该很高。
volatile的字面意思为“不稳定的,易变的”。
一般用它定义一些IO端口的变量。
现在假定我们要对一个设备进行初始化,此设备的某一个寄存器地址为0xff800000。
我们先看一段程序:
int*output=(int*)0xff800000;/*定义一个IO端口*/
intinit(void)
{
inti;
for(i=0;i<10;i++)
{
*output=i;
}
}
一般的编译器都带有优化功能,那么这段代码被优化会是什么结果呢?
编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,所以编译器最后给你编译的代码结果相当于为:
intinit(void)
{
*output=9;
}
试想一下,如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化后的程序并不能达到目的。
反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作了只做了一次然而从代码角度看是没有任何问题的。
这时候就是volatile出场的时候了,volatile就是通知编译器,这个声明的变量是一个不稳定的,在遇到此变量时候不要优化。
对于上面的代码只需在声明时加上volatile即可。
volatileint*output=(volatileint*)0xff800000;/*定义一个IO端口*/
IIcache
我们都知道Intel的PII以后的CPU都有一款与之对应得赛扬CPU。
其唯一不同就是少了一些cache的容量。
事实上,大多数的微处理器或微控制器都带了cache,cache是价格与性能的一种折衷方案。
因为一般CPU(为描述方便,不论微处理器或微控制器,本文中都将称之为CPU)的速度都比现行体系的RAM快,CPU对内存的访问降低了其运行的整体速度。
为与CPU的速度匹配,CPU都在内部集成了一定容量的cache。
cache运行周期一般于CPU相匹配,但其价格较高,很难大量采用。
cache的使用是基于计算机中的一个有名的“局部原理”,即计算机在某一段时间内无论对代码还是数据都局限在一定的范围内。
这个原理是严谨而好事的计算机科学家的猜想加大量统计的结果。
想想也是,我们程序中有大量的循环代码,这些代码不就是一遍一遍的执行一些固定区域的语句吗,同样循环中的数据操作不也是对一些固定区域的数据如数组操作吗!
基于这种理论,CPU在执行代码时同时将此代码所在块的内容全部读入指令cache,读取数据时把此数据所在块的内容全部读入数据cache,在余下时间里,只要执行的代码未超出此范围,则CPU无需在外部RAM中再行访问代码,数据同样如此。
写操作相同,如果某一个块内存正好被装入cache,那么,CPU对此内存块的写操作也只在cache中进行。
但是要注意的是,cache的容量是远远小于外部RAM的,CPU在访问新的代码和数据时如果不在cache内部就发生缺失,专业术语称之为“未命中”,反之称之为“命中”,在未命中的情况下,CPU会将现有cache中的某一个块基于某种算法用新的内容替换。
正是这种命中与未命中却给我们的嵌入式开发初入门者带来如同volatile一样的困惑。
以CPU为核心看,我们将CPU直接参与的事件称之为同步事件,CPU未直接参与的称之为异步事件。
cache的操作都是同步的,但是如果你在写一个外部设备的驱动,而且为了减少CPU的参与你用了DMA来搬移数据,那么DMA搬移数据这个事件便是异步事件。
DMA
控制器
CPU
CACHERAM
外部设备
中断
0x00000000
0x00100000
0x00000000
0x00040000
0x00080000
0x000c0000
图1一个DMA操作示例
图1中,RAM内存从地址0x00000000到0x000ffffff1M范围内的内容全部被调入cache中,现在假定外部设备有新的数据到来并发生了中断,CPU在设定完DMA控制器以后继续其工作,DMA根据设定将1M的新数据装入RAM中并通知CPU新的数据到了。
注意问题来了,现在假定CPU要对新的数据操作了,因为此外部设备的数据被存放在从0x00000000的RAM中,而此段数据又恰好被cache命中,那么CPU将直接访问cache,中的数据,可是cache中的数据并非刚刚得到的新数据。
我的一个师弟在调试一个网卡驱动时就遇到这个问题,看上去一切都没问题,代码没有问题,用sniffer从网络上看,数据也确实发到他的网卡,网卡中断级的调试也显示其收到了正确地内容,但是应用层却总是得到是似是而非的东西。
真凶就在于cache,CPU并不知道此时cache中的数据已经过时,解决的办法就是在CPU访问异步事件控制的数据前一定要强行刷新cache中的内容,反之,从内存到外部设备搬移数据前一定要回写内存。
一般CPU都提供了cache的刷新和回写机制,甚至有的CPU还有cache保护,即强制其不要对某一范围内的内存使用cache机制。
6一个高效的双缓存算法
在很多场合需要用到双缓存,用一个缓存在接收数据的同时处理另一个缓存。
看看一般缓存切换的写法:
#defineBUFLEN256
intbuf[2][BUFLEN]
voiddealDounBuffer()
{
staticintbufFla
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 嵌入式 开发 杂谈