一位久经沙场的嵌入式er站在初学者角度谈谈嵌入式开发与学习的一些问题

一位久经沙场的嵌入式er站在初学者角度谈谈嵌入式开发与学习的一些问题

在刚刚涉足嵌入式开发的时候,总想找到这样一本书,它能够解决我一些这样那样的疑惑。可是遗憾的是,到如今也没有这样一本书面世,并且我想永远也不可能面世了。由于个人疑惑太多太杂了。这些疑惑在教科书中又难以寻找到答案。C 教程注重讲C 的语法,编译原理注重讲语法,语义的分析。每一门教科书都是有它的注重,因此那些交叉的问题便成了三无论。市场上的那些自称为《XX 宝典》、《XX 圣经》的书却老是说一些可能连做者本身也没搞清楚的问题。因而我想,我想了解的也许是你们都想了解的吧,那么把我学到的一点东西写出来,你们也许就能够少花点时间在上面,留出宝贵的脑力资源去作更有意义的事。编程

语言选择,C 仍是其余网络

刚刚涉及嵌入式开发者老是先阅读一些指导类型文章,而后就开始对开发语言的选择踌躇不决。是C 仍是C++?仍是好像更热门的JAVA?不用犹豫,至少目前看来C 仍是你的选择。嵌入式开发的本质是订制开发,硬件平台林林总总,处理能力高下不一样,若是想保护你学习精力投资的话,C 是最好的“优绩股”。C++的优势在于它的代码重用,可是效率比C低不少,最重要的是,并不是全部芯片的编译器都能支持C++。JAVA 就更不用说起,在一个虚拟平台上开发的优势是不用关心具体的硬件细节,但这不是一个嵌入式开发者的做风,换一种说法,这种开发不能称之为嵌入式开发。函数

 

C被称为高级语言中的低级语言,低级语言中的高级语言,这是由于其一方面有高级语言所具备的接近于人类思想的语言体系,另外一方面同时支持地址与位操做。能够方便的与硬件打交道。嵌入式开发必然要操做IO、硬件地址,没有位操做和指针你又如何方便作到?工具

 

嵌入式开发通常流程学习

嵌入式开发的流程与高层开发大致相似,编码——编译、连接——运行。中间固然能够有联机调试,从新编码等递归过程。但有一些不一样之处。编码

 

首先,开发平台不一样。受嵌入式平台处理能力所限,嵌入式开发通常都采用交叉编译环境开发。所谓交叉编译就是在A 平台上编译B 平台上运行的目标程序。在A 平台上运行的B 平台程序编译器就被称为交叉编译器。一个初入门者,创建一套这样的编译环境也许就要花掉几天的时间。spa

 

其次,调试方式不一样。咱们在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 还能够支持网络、串口下载,甚至调试功能。但不要期望有一个像PC BIOS 那样通用的BootLoader 供你使用,至少你须要做一些移植工做使其符合你的系统,这个移植工做也是你开发的一个部分,做为嵌入式开发个入门者来说,移植或者编写一个BootLoader 会使你受益不浅。

 

没有BootLoader 行不行?固然能够,要么你就牺牲效率直接从ROM 中运行,要么你就本身编写程序搬移代码去RAM 运行,最主要的是,开发过程当中你要有好的调试工具支持在线调试,不然你就得在改动哪怕一个变量的状况下都要去从新烧片验证。继续程序入口的话题,无论过程如何,程序最后在执行时都是变成了机器指令,一个纯的执行程序就是这些机器指令的集合。像咱们在操做系统上的可运行程序都不是纯的执行程序,而是带有格式的.嵌入式学习更多内容请加企鹅意义气呜呜吧久零就易。通常除了包含上面提到的几个段之外,还有程序的长度,校验以及程序入口——就是从哪儿开始执行用户程序。

 

为何有了程序地址还须要有程序的入口呢?这是由于你要真正开始执行的代码并不是必定放置在一个文件的最开始,就算放在最开始,除非你去控制连接,不然在多文件的状况下,编译器也不必定将你的这段程序放置在最后程序的最顶端。像咱们通常有操做系统支持的程序,只需在你的代码中有一个main 做为程序入口——注意这个main 只是大多数编译器约成定俗的入口,除非你利用了别人的初始化库,不然程序入口能够自行设定——便可。显然,带有格式的这种执行文件使用更加灵活,但须要BootLoader 的支持。有关执行文件格式的内容能够看看ELF 文件格式。

 

编译预处理

首先看看文件包含,从咱们的第一个C 程序Hello World! 开始,咱们就使用头文件包含,可是另人惊奇的是,不少人在作了很长时间的开发之后仍然对文件的包含没有正确的认识或者是概念不清,有更多的人却把头文件和与之相关联的库混淆。

 

为了照顾这些初学者,这里罗嗦一下,其实文件包含的本质就是把一个大的文件截成几个小文件便于管理和阅读,若是你包含了那个文件,那么你把这个文件的全部内容原封不动的复制到你包含其的文件中,效果是彻底同样的,另外一方面,若是你编译了一些中间代码,如库文件,能够经过提供头文件来告知调用者你的库包含的函数和调用格式,可是真正的代码已经变成了目标代码以库文件形式存在了。至于包含文件的后缀如.h 只是告诉使用者,这是一个头文件,你用任何别的名字,编译器都通常不会在乎。

 

那些对头文件和库还混淆的朋友应该恍然大悟了吧,其实头文件只能保证你的程序编译不出现语法错误,可是直到最后连接的时候才会真正使用到库,那些只把一个头文件拷贝来就想拥有一个库的人不再要犯这样的错误了。若是你的工程中源程序数目繁多令你以为管理困难,把他们所有包含在一个文件中也何尝不可。

 

另外一个初学者经常遇到的问题就是因为重复包含引发的困惑。若是一个文件中包含了另外一个文件两次或两次以上极可能引发重复定义的问题,可是没有人蠢到会重复包含两次同一个文件的,这种问题都是隐式的重复包含,好比A 文件中包含了B 文件和C 文件,B 文件中又包含了C 文件,这样,A 文件实际上已经包含了C 文件两次。不过一个好的头文件巧妙的利用编译预处理避免了这种状况。在头文件中你可能发现这样的一些预处理:

#ifndef __TEST_H__

#define __TEST_H__

… …

#endif /* __TEST_H__ */

 

这三行编译预处理前两行通常位于文件最顶端,最后文件位于文件最末端,它的意思是,若是没有定义__TEST_H__那么就定义__TEST_H__同时下面的代码一直到#endif 前参与编译,反之不参与编译。多么巧妙的设计,有了这三行简洁的预处理,这个文件即便被包含几万次也只能算一次。

 

咱们再来看看宏的使用。初学者在看别人代码的时候老是想,为何用那么多宏呢?看得人一头雾水,的确,有时候宏的使用会下降代码的可读性。但有时宏也能够提升代码的可读性,看看下边这两段代码:

1)

#define SCC_GSMRH_RSYN 0x00000001 /* receive sync timing */

#define SCC_GSMRH_RTSM 0x00000002 /* RTS* mode */

#define SCC_GSMRH_SYNL 0x0000000c /* sync length */

#define SCC_GSMRH_TXSY 0x00000010 /* transmitter/receiver sync*/

#define SCC_GSMRH_RFW 0x00000020 /* Rx FIFO width */

#define SCC_GSMRH_TFL 0x00000040 /* transmit FIFO length */

#define SCC_GSMRH_CTSS 0x00000080 /* CTS* sampling */

#define SCC_GSMRH_CDS 0x00000100 /* CD* sampling */

#define SCC_GSMRH_CTSP 0x00000200 /* CTS* pulse */

#define SCC_GSMRH_CDP 0x00000400 /* CD* pulse */

#define SCC_GSMRH_TTX 0x00000800 /* transparent transmitter */

#define SCC_GSMRH_TRX 0x00001000 /* transparent receiver */

#define SCC_GSMRH_REVD 0x00002000 /* reverse data */

#define SCC_GSMRH_TCRC 0x0000c000 /* transparent CRC */

#define SCC_GSMRH_GDE 0x00010000 /* glitch detect enable */

*(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 赋值的操做,你能够写一个相似下边的函数来实现:

void outb(unsigned char val, unsigned int *addr)

{

*addr = val;

}

 

仅仅是一句语句的函数,却要调用一个函数,若是不用函数呢,重复写上面的语句又显得罗嗦。不如用下面的宏实现。

#define outb(b, addr) (*(volatile unsigned char *)(addr) = (b))

 

因为不须要调用子函数,宏提升了运行效率,可是浪费了程序空间,这是因为凡是用到此宏的地方,都要替换为一句其代替的语句。开发者须要根据系统需求取舍时间与空间。

相关文章
相关标签/搜索