tbwshc

tbw

  C++博客 :: 首页 :: 联系 :: 聚合  :: 管理
  95 Posts :: 8 Stories :: 3 Comments :: 0 Trackbacks

常用链接

留言簿(4)

我参与的团队

搜索

  •  

最新评论

阅读排行榜

评论排行榜

2013年9月11日 #

从一个任务转变到另一个任务的实际过程叫作设备场景切换。因为设备场景是处理器专用的,实现设备场景切换的实现也是这样。那意味着它总是要用汇编来写。与其向你展示我在ADEOS 中使用的80x86 专用的汇编代码,不如我用一种类C 的伪代码来展示设备场景切换tb例程。
void
contextSwitch(PContext pOldContext, PContext pNewContext)
{
if(saveContext(pOldContext))
{
//
// Restore new context only on a nonzero exit from saveContext().
//
restoreContext(pNewContext);
// This line is never executed!
}
// Instead, the restored task continues to execute at this point.
}

例程 contextSwitch()实际上是被调度程序凋用,而调度程序又在那此终止中断的tb系统调用中被调用,因此它不一定在这里终止中断。此外,由于调用调度程序的操作系统调用是用高级语言写的,所以大部分运行任务的寄存器已经被保存到它自己当地的栈中了。这减少了例程saveContext()和restoreContext()需要做的工作。它们只需要关心指令指针,栈指针以及标志位的保存。例程 contextSwitch()的实际行为是很难仅仅通过看前面的代码来理解的。大部分的软件开发者以连续的方式思考问题,认为每一行代码会紧接着上一条代码破执行。然而,这个代码实际为并行地执行了两次。当一个任务(新任务)转变到运行状态,另一个(旧任务)必须同时返回到就绪状态。想一下新任务当它在restoreContext()代码中被恢复的时候就会明白。无论新任务以前做什么,它在saveContext 代码里总是醒着的——因为这就是它的指令存放的地方。新任务如何知道它是否是第一次(也就是,在准备休眠的过程)或者是第二次(醒来的过程)从saveContext()中出来的呢?它确实需要知道这个差别,因此我不得不用一种有点隐蔽的方法来实现saveContext()。例程saveContext()不是保存了准确的目前的指令指针。实际上是保存了一些指令前面的地址。那样,当保存的设备场景恢复的时候,程序从saveContext 中另一个下同的点继续。这也使得saveContext 可能返回不同的值:当任务要休眠的时候为非零,当任务唤起的时候为零。例程contextSwitch()利用这个返回的值来决定是否调用restoreContext()。如果不进行这个检测,那么与这个新任务相关的代码永远不会执行。

我知道这可能是一个复杂的事件序列,因此我在图8-3 中说明了整个的过程。

posted @ 2013-09-11 16:34 tbwshc 阅读(265) | 评论 (0)编辑 收藏

2013年9月5日 #

作为 ADEOS 的开发者(或是其他操作系统的开发者),你需要知道如何创建和使用任务。就像别的抽象数据结构,Task 类有自己的成员函数。ADEOS的的任务接口比别的大多数操作系统要简单一些,因为它只是创建一个新的Task 对象。一旦创建,ADEOS 任务继续在系统中存在,直到相关的函数返回。当然,这也许永远不会发生(意即ADEOS 任务也许永远不会结束),但是,如果一旦发生了,那么该任务就会被操作系统删除掉。Task 的构造函数如下所示。调用者通过构造函数的参数分配一个函数,一个权限值,和一个可选择的新任务的堆栈大小。第一个参数,fUnCtion,是一个指向C/C++语言或汇编语言的函数指针,该函数是要在新任务的上下文环境中运行的。该函数不需要任何输人参数,也不返回任何结果。第二个参数P,是一个单字节的整数(从1 到255),代表了任务的权限级别,这个权限级别是与别的任务相对而言的,在tb任务调度器选择新的任务运行的时候会用到(p 的值越大,表示权限越高)。
TaskId Task::nextId = 0
/**************************************************************
*
* Method : Task()
*
* Description : Create a new task and initialize its state.
*
* Notes :
*
* Returns :
*
**************************************************************/
Task:Task(void (*function)(), Priority p, int stackSize)
{
stackSize /= sizeof(int); //Convert bytes to words.
enterCS(); //Critical Section Begin
//
// Initialize the task-specific data.
//
if = Task::nextId++;
state = Ready;
priority = p;
entryPoint = function;
pStack = new int[stackSize];
pNext = NULL;
//
// Initialize the processor context.
//
contextInit(&context, run, this, pStack + stackSize);
//
// Insert the task into the ready list.
//
os.readyList.insert(this);
os.schedule(); // Scheduling Point
exitCS(); // Critical Section End
} /* Task() */
注意这个例程的功能块被两个函数 enterCS()和exitCS()的调用包围。在这些调用之间的代码块叫作tb临界区(critical section)。临界区是一个程序必须完整执行的一部分。也就是说,组成这一个部分的指令必须没有中断地按照顺序执行。因为中断可能随时发生,保证不受到中断的唯一办法就是在执行关键区期间禁止中断。因此在关键区的开始调用enterCS 以保存中断的允许状态以及禁止进一步的中断。在关键区尾部调用exitCS 以恢复前面保存的中断调用。我们会看到在下面每一个例程中都应用了同样的技巧。
在前面代码中,有几个在构造函数里调用的其他例程,但是在这里我没有空间列出。它们是contextInit()和os.readyList.insert()例程。例程contextInit()为任务建立了初始的设备场景。这个例程必定是处理器专用的,因此是用汇编语言写的。
contextInit()有四个参数。第一个是一个指向待初始比的设备场景数据结构指针。第二个是一个指向启动函数的指针。这是一个特殊的ADEOS 函数,叫作run(),它被用来启动一个任务,并且如果以后相关的函数退出了,它被用来做其后的清理工作。第三个参数是一个指向新任务对象的指针。这个参数被传递给run(),因此相关的任务就能够被启动。第四个和最后一个参数是指向新任务栈的指针。
另一个函数调用是 os.readyList.insert()。这个函数把新任务加入到操作系统内部的就绪任务列表中。readyList 是一个TaskList 类型的对象。这个类是那些具有insert()和remove()两个方法的任务(按照优先级排序)的链表。感兴趣的读者如果想知道这些函数是如何实现的就应该下载和研究其ADEOS 的源代码。你将在下面的讨论中了解到更多有关就绪列表的问题。
posted @ 2013-09-05 16:58 tbwshc 阅读(747) | 评论 (0)编辑 收藏

在早期的计算机中,没有操作系统一说,应用程序开发人员都要对处理器(CPU)和硬件进行彻头彻尾的控制。实际上,第一个操作系统的诞生,就是为了提供一个虚拟的硬件平台,以方便程序员开发。为了实现这个目标,操作系统只需要提供一些较为松散的函数、例程——就好像现在的软件库一样——以便于对硬件设备进行重置、读取状态、写入指令之类的操作。现代的操作系统则在单处理器上加入了多任务机制,每个任务都是一个软件模块,可以是相互独立的。嵌入式的软件经常是可以划分成小的互相独立的模块。例如,第五章“接触硬件”讲到的打印tb共享设备就包含三个不同的软件任务:
􀂋 任务 1:从计算机的串行口A 接收数据
􀂋 任务 2:从计算机的串行口B 接收数据
􀂋 任务 3:格式化数据并输送到计算机的并行口(打印机就连接在并行口)
这些任务的划分提供了一个很关键的软件抽象概念,这使得嵌入式操作系统的设计和实现更加容易,源程序也更易于理解和维护。通过把大的程序进行模块化划分,程序员可以集中精力克服系统开发过程中的关键问题。

坦言之,一个操作系统并不是嵌入式或其它计算机系统的必需的组件,它所能做的,也是像时用程序要实现的功能一样。本书中的所有例子都说明了这一点。应用程序执行起来,都是从main 开始,然后进入系统调用、运行、结束。这与系统中只有一个任务是一样的。对于应用程序来说,仅仅是实现使LED 进行闪烁,这就是操作系统的主要功用(屏蔽了很多复杂的操作)。

如果你以前没作过对操作系统的研究,那么,在这里得提醒一下,操作系统是非常复杂的。tb操作系统的厂商肯定是想使你相信,他们是唯一能生产出功能强大又易用的操作系统的科学家。但是,我也要告诉你:这并不是根困难的。实际上嵌入式操作系统要比桌面操作系统更容易编写,所需的模块和功能更为小巧、更易于实现。一旦明确了要实现了功能,并有一定的实现技能,你将会发现,开发一个操作系统并不比开发嵌入式软件艰难多少。

嵌入式操作系统很小,因为它可以缺少很多桌面操作系统的功能。例如,嵌入式操什系统很少有硬盘或图形界面,因此,嵌入式操作系统可以下需要文件系统和图形用户接口。而且,一般来说,是单用户系统,所以多用户操作系统的安全特性也可以省去了。上面所说的各种性能,都可以作为嵌入式操作系统的一部分,但不是必须的。

posted @ 2013-09-05 16:46 tbwshc 阅读(202) | 评论 (0)编辑 收藏

2013年8月19日 #

在这一章里,我试图把到目前为止所有我们讨论过的单元合在一起,使之成为一个完整的嵌入式的应用程序。在这里我没有把很多新的素材加入到讨论中,因此本章主要是描述其中给出的代码。我的目的是描述这个应用程序的结构和它的源代码,通过这种方式使你对它不再感到神奇。完成这一章以后,你应该对于示例程序有一个完整的理解,并且有能力开发自己的嵌入式应用程序。应用程序的概述

我们将要讨论的这个应用程序不比其他大部分tb编程书籍中找到的“Hello,World”例子更复杂。它是对于嵌入式软件开发的一个实证,因此这个例子是出现在书的结尾而不是开始。我们不得不逐渐地建立我们的道路通向大部分书籍甚至是高级语言编译器认为是理所当然的计算平台。

一旦你能写“Hello, World”程序,你的嵌入式平台就开始着上去很像任何其他编程环境。但是,嵌入式软件开发过程中最困难的部分——使自己熟悉硬件,为它建立一个软件开发的过程,连接到具体的硬件设备——还在后面呢。最后,你能够把你的力量集中于算法和用户界面,这是由你要开发的产品来确定的。很多情况下,这些程序的高级方面可以在其他的计算机平台上开发,和我们一直在讨论的低级的嵌入式软件开发同时进行,并且只要把高级部分导入嵌入式系统一次,两者就都完成了。

图 9-1 包含了一个“Hello, World!”应用程序的高级的示意图。这个应用程序包括三个设备驱动程序,ADEOS 操作系统和两个ADEOS 任务。第一个任务以每秒10Hz 的速度切换Arcom 板上的红色指示灯。第二个每隔10 秒钟向主机或是连接到位子串口上的哑终端发送字符串“Hello,WOrld!”。这两个任务之外,图中还有三个设备的驱动程序。这些驱动程序分别控制着Arcom 板子的指示灯、时钟以及串行端口。虽然通常把设备驱动画在操作系统的下面,但是我把它们三个和操作系统放在同一个级别,是为了着重说明它们事实上依赖于ADEOS 比ADEOS 依赖于它们更多。实际上,ADEOS 嵌入式操作系统甚至不知道(或者说下关心)这些设备驱动是否存在于系统之中。这是嵌入式操作系统中设备驱动程序和其他硬件专用软件的共性。

程序 main()的实现如下所示。这段代码简单地创造厂两个任务,tb启动了操作系统的日程表。在这样一个高的级别上,代码的含义是不言而喻的。事实上、我们已经在上一章中讨论了类似的代码。

#include "adeos.h"
void flashRed(void);
void helloWorld(void);
/*
* Create the two tasks.
*/
Task taskA(flashRed, 150, 512);
Task taskB(helloWorld, 200, 512);
/****************************************************
*
* Function : main()
*
* Description : This function is responsible for starting the ADEOS scheduler
only.
*
* Notes :
*
* Returns : This function will never return!
*
****************************************************/
void
main(void)
{
os.start();
// This point will never be reached.
} /* main() */

posted @ 2013-08-19 11:49 tbwshc 阅读(249) | 评论 (0)编辑 收藏

2013年7月23日 #

正如我早先说的那样,当问题归结于减小代码的大小的时候,你最好让编译器为你做这件事。然而,如果处理后的程序代码对于你可得的只读存贮器仍然太大了,还有几种技术你可以用来进一步减少体程序的大小。在本节中,自动的和人工的代码优化我们都要讨论。
当然,墨菲法则指出,第一次你启用编译器的优化特性后,你先前的工作程序会突然失效,也许自动优化最臭名昭著的是“死码删除”。这种优化会删除那些编译器相信是多余的或者是不相关的代码,比如,把零和一个变量相加不需要任何的计算时间。但是你可能还是希望如果程序代码执行了tb编译器不了解的函数,编译器能够产生那些“不相关”的指示。
比如,下面这段给出的代码,大部分优化编译器会去除第一条语句,因为*pControl 在重写(第三行)之前没有使用过:
*pControl = DISABLE;
*pData = 'a';
*pCotrol = ENABLE;
但是如果 pControl 和pData 实际上是指向内存映像设备寄存器的指针怎么办?这种情况下,外设在这个字节的数据写入之前将接收不到DISABLE 的命令。这可能会潜在地毁坏处理器和这个外设之间的所有未来的交互作用。为了使你避免这种问题,你必须用关键字“volatile”声明所有指向内存映像设备寄存器的指针和线程之间(或者是一个线程和一个中断服务程序之间)共享的全局变量。你只要漏掉了它们中的一个,墨菲法则就会在你的工程的最后几天里回来,搅得你心神不宁。我保证。
——————————————————————————————————
警告:千万不要误以为程序优化后的行为会和未优化时的一样。你必须在每一次新的优化后完全重新测试你的软件,以确保它的行为没有发生改变。
——————————————————————————————————
更糟糕的是,或者退一步说,调试一个优化过的程序是富有挑战性的。启用了编译器的优化后,在源代码中的一行和实现这行代码的那组处理器指令之间的关联关系变得更加微弱了。那些特定的指令可能被移动或者拆分开来,或者两个类似的代码可能现在共用一个共同的实现。实际上,tb高级语言程序的有些行可能完全从程序中去除了(正如在前面例子里那样)。结果,你可能无法在程序特定的一行上设置一个断点或者无法研究一个感兴趣变量的值。

一旦你使用了自动优化,这里有一些关于用手工的办法进一步减少代码大小的技巧。

避免使用标准库例程
为了减少你的程序的大小,你所能做的最好的一件事情就是避免使用大的标准库例程。很多最大的库例程代价昂贵,只是因为它们设法处理所有可能的情况。你自己有可能用更少的代码实现一个子功能。比如,标准C 的库中的spintf例程是出了名的大。这个庞大代码中有相当一部分是位于它所依赖的浮点数处理例程。但是如果你不需要格式化显示浮点数值(%f 或者%d),那么你可以写你自己的sprintf 的整数专用版本,并且可以节省几千字节的代码空间。实际上,一些标准C 的库(这让我想起Cygnus 的newlib)里恰好包含了这样一个函数,叫作sprintf。

本地字长
每一个处理器都有一个本地字长,并且ANSI C 和C++标准规定数据类型int必须总是对应到那个字长。处理更小或者更大的数据类型有时需要使用附加的机器语言指令。在你的程序中通过尽可能的一致使用int 类型,你也许能够从你的程序中削减宝贵的几百字节。

goto 语句
就像对待全局变量一样,好的软件工程实践规定反对使用这项技术。但是危急的时候,goto 语句可以用来去除复杂的控制结构或者共享一块经常重复的代码。
除了这些技术以外,在前一部分介绍的几种方法可能也会有帮助,特别是查询表、手工编写汇编、寄存器变最以及全局变量。在这些技术之中,利用手工编写汇编通常可以得到代码最大幅度的减少量。

posted @ 2013-07-23 17:21 tbwshc 阅读(560) | 评论 (0)编辑 收藏

事情应该尽可能简化,而不只是简单一点点,一爱因斯坦

虽然使软件正确的工作好像应该是一个工程合乎逻辑的最后一个步骤,但是在嵌入式的系统的开发中,情况并不总是这样的。出于对低价系列产品的需要,硬件的设计者需要提供刚好足够的存储器和完成工作的处理能力。当然,在工程的软件开发阶段,使程序正确的工作是很重要的。为此,通常需要一个或者更多的开发电路板,有的有附加的存贮器,有的有更快的处理器,有的两者都有。这些电路板就是用来使软件正确工作的。而工程的最后阶段则变成了对代码进行优化。最后一步的目标是使得工作程序在一个廉价的硬件平台上运行。

提高代码的效率
所有现代的 C 和C++编译器都提供了一定程度上的代码优化。然而,大部分由编译器执行的优化技术仅涉及执行速度和代码大小的一个平衡。你的程序能够变得更快或者更小,但是不可能又变快又变小。事实上,在其中一个方面的提高就会对另一方面产生负面的影响。哪一方面的提高对于程序更加的重要是由程序员来决定。知道这一点后,无论什么时候遇到速度与大小的矛盾,编译器的优化阶段就会作出合适的选择。

因为你不可能让编译器为你同时做两种类型的优化,我建议你让它尽其所能的减少程序的大小。执行的速度通常只对于某些有时间限制或者是频繁执行的代码段是重要的。而且你可以通过手工的办法做很多事以提高这些代码段的效率。然而,手工改变代码大小是一件很难的事情,而且编译器处于一个更有利的位置,使得它可以在你所有的软件模块之间进行这种改变。

直到你的程序工作起来,你可能已经知道或者是非常的清楚,哪一个子程序或者模块对于整体代码效率是最关键的。中断服务例程、高优先级的任务、有实时限制的计算、计算密集型或者频繁调用的函数都是候选对象。有一个叫作profiler 的工具,它包括在一些软件开发工具组中,这个工具可以用来把你的视线集中到那些程序花费大部分时间(或者很多时间)的例程上去。
一旦你确定了需要更高代码效率的例程,可以运用下面的一种或者多种技术来减少它们的执行时间。

inline 函数
在 c++中,关键字inline 可以被加入到任何函数的声明。这个关键字请求编译器用函数内部的代码替换所有对于指出的函数的调用。这样做删去了和实际函数调用相关的时间开销,这种做法在inline 函数频繁调用并且只包含几行代码的时候是最有效的。

inline 函数提供了一个很好的例子,它说明了有时执行的速度和代码的太小是如何反向关联的。重复的加入内联代码会增加你的程序的大小,增加的大小和函数调用的次数成正比。而且,很明显,如果函数越大,程序大小增加得越明显。优化后的程序运行的更快了,但是现在需要更多的ROM。

查询表
switch 语句是一个普通的编程技术,使用时需要注意。每一个由tb机器语言实现的测试和跳转仅仅是为了决定下一步要做什么工作,就把宝贵的处理器时间耗尽了。为了提高速度,设法把具体的情况按照它们发生的相对频率排序。换句话说,把最可能发生的情况放在第一,最不可能的情况放在最后。这样会减少平均的执行时间,但是在最差情况下根本没有改善。

如果每一个情况下都有许多的工作要做,那么也许把整个switch 语句用一个指向函数指针的表替换含更加有效。比如,下面的程序段是一个待改善的候选对象:

enum NodeType {NodeA, NodeB, NodeC}
switch(getNodeType())
{
case NodeA:
...
case NodeB:
...
case NodeC:
...
}
为了提高速度,我们要用下面的代码替换这个switch 语句。这段代码的第一部分是准备工作:一个函数指针数组的创建。第二部分是用更有效的一行语句替换switch 语句。

int processNodeA(void);
int processNodeB(void);
int processNodeC(void);
/*
* Establishment of a table of pointers to functions.
*/
int (* nodeFunctions[])() = { processNodeA, processNodeB, processNodeC };
...
/*
* The entire switch statement is replaced by the next line.
*/
status = nodeFunctions[getNodeType()]();

手工编写汇编
一些软件模块最好是用汇编语言来写。这使得程序员有机会把程序尽可能变得有效率。尽管大部分的C/C++编译器产生的机器代码比一个一般水平的程序员编写的机器代码要好的多,但是对于一个给定的函数,一个好的程序员仍然可能做得比一般水平的编译器要好。比如,在我职业生涯的早期,我用C 实现了一个数字滤波器,把它作为TI TMS320C30 数字信号处理器的输出目标。当时我们有的tb编译器也许是不知道,也许是不能利用一个特殊的指令,该指令准确地执行了我需要的那个数学操作。我用功能相同的内联汇编指令手工地替换了一段C 语言的循环,这样我就能够把整个计算时间降低了十分之一以上。

寄存器变量
在声明局部变量的时候可以使用 register 关键字。这就使得编译器把变量放入一个多用选的寄存器,而不是堆栈里。合适地使用这种方珐,它会为编译器提供关于最经常访问变量的提示,会稍微提高函数的执行速度。函数调用得越是频繁,这样的改变就越是可能提高代码的速度。

全局变量
使用全局变量比向函数传递参数更加有效率。这样做去除了函数调用前参数入栈和函数完成后参数出栈的需要。实际上,任何子程序最有效率的实现是根本没有参数。然而,决定使用全局变量对程序也可能有一些负作用。软件工程人士通常不鼓励使用全局变量,努力促进模块化和重入目标,这些也是重要的考虑。

轮询
中断服务例程经常用来提高程序的效率。然而,也有少数例子由于过度和中断关联而造成实际上效率低下。在这些情况中,中断间的平均时间和中断的等待时间具有相同量级。这种情况下,利用轮询与硬件设备通信可能会更好。当然,这也会使软件的模块更少。

定点运算
除非你的目标平台包含一个浮点运算的协处理器,否则你会费很大的劲去操纵你程序中的浮点数据。编译器提供的浮点库包含了一组模仿浮点运算协处理器指令组的子程序。很多这种函数要花费比它们的整数运算函数更长的执行时间,并且也可能是不可重入的。

如果你只是利用浮点数进行少量的运算,那么可能只利用定点运算来实现它更好。虽然只是明白如何做到这一点就够困难的了,但是理论上用定点运算实现任何浮点计算都是可能的。(那就是所谓的浮点软件库。)你最大的有利条件是,你可能不必只是为了实现一个或者两个计算而实现整个IEEE 754 标准。如果真的需要那种类型的完整功能,别离开编译器的浮点库,去寻找其他加速你程序的方法吧。


 

posted @ 2013-07-23 17:19 tbwshc 阅读(276) | 评论 (0)编辑 收藏

你可能想知道为什么C++语言的创造者加入了如此多的昂贵的——就执行时间和代码大小来说——特性。你并不是少数,全世界的人都在对同样的一件事情困惑——特别是用C++做嵌入式编程的用户们。很多这些昂贵的特性是最近添加的,它们既不是绝对的必要也不是原来C++规范的一部分。这些特性一个接着一个的被添加到正在进行着的“标准化”进程中来。在1996 年,一群日本的芯片厂商联台起来定义了一个C++语言和库的子集,它更加适合嵌入式软件开发。他们把他们新的工业标准叫作嵌入式C++。令人惊奇的是,在它的初期,它就在C++用户群中产生了很大的影响。
作为一个C++标准草案的合适子集,嵌入式C++省略了很多不限制下层语言可表达性的任何可以省略的东西。这些被省略的特性不仅包括像多重继承性、虚拟基类、运行时类型识别和异常处理等昂贵的特性,而且还包括了一些最新的添加特性,比如:模板、命名tb空问、新的类型转换等。所剩下的是一个C++的简单版本,它仍然是面向对象的并且是C 的一个超集,但是它具有明显更少的运行开销和更小的运行库。
很多商业的C++编译器已经专门地支持嵌入式C++标准。个别其他的编译器允许手工的禁用具体的语言特性,这样就使你能够模仿嵌入式C++或者创建你的很个性化的C++语言。
posted @ 2013-07-23 17:11 tbwshc 阅读(336) | 评论 (0)编辑 收藏

2013年7月10日 #

最近在调试中遇到点内存对齐的问题,别人问我是怎么回事,我赶紧偷偷查了一下,记录下来。

不论是C、C++对于内存对齐的问题在原理上是一致的,对齐的原因和表现,简单总结一下,以便朋友们共享。

一、内存对齐的原因
大部分的参考资料都是如是说的:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

   也有的朋友说,内存对齐出于对读取的效率和数据的安全的考虑,我觉得也有一定的道理。

二、对齐规则
    每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。比如32位windows平台下,VC默认是按照8bytes对齐的(VC->Project->settings->c/c++->Code Generation中的truct member alignment 值默认是8),程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。

    在嵌入式环境下,对齐往往与数据类型有关,特别是C编译器对缺省的结构成员自然对届条件为“N字节对 齐”,N即该成员数据类型的长度。如int型成员的自然对界条件为4字节对齐,而double类型的结构成员的自然对界条件为8字节对齐。若该成员的起始 偏移不位于该成员的“默认自然对界条件”上,则在前一个节面后面添加适当个数的空字节。C编译器缺省的结构整体的自然对界条件为:该结构所有成员中要求的 最大自然对界条件。若结构体各成员长度之和不为“结构整体自然对界条件的整数倍,则在最后一个成员后填充空字节。

    那么可以得到如下的小结:

类型 对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
Char    偏移量必须为sizeof(char)即1的倍数
Short   偏移量必须为sizeof(short)即2的倍数
int     偏移量必须为sizeof(int)即4的倍数
float   偏移量必须为sizeof(float)即4的倍数
double  偏移量必须为sizeof(double)即8的倍数

   各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充。同时为了确保结构的大小为结 构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节,也就是 说:结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。对于char数组,字节宽度仍然认为为1。

   对于下述的一个结构体,其对齐方式为:

struct Node1{

    double m1;
    char m2;
    int m3;
};

  对于第一个变量m1,sizeof(double)=8个字节;接下来为第二个成员m2分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把m2存放在偏移量为8的地方满足对齐方式,该成员变量占用 sizeof(char)=1个字节;接下来为第三个成员m3分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof (int)=4的倍数,为了满足对齐方式对偏移量的约束问题,自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof(int), 由于8+4+4 = 16恰好是结构体中最大空间类型double(8)的倍数,所以sizeof(Node1) =16.

 

typedef struct{

    char a;

    int b;

    char c;

}Node2;

    成员a占一个字节,所以a放在了第1位的位置;由于第二个变量b占4个字节,为保证起始位置是4(sizeof(b))的倍数,所以需要在a后面填充3个 字节,也就是b放在了从第5位到第8位的位置,然后就是c放在了9的位置,此时4+4+1=9。接下来考虑字节边界数,9并不是最大空间类型int(4) 的倍数,应该取大于9且是4的的最小整数12,所以sizeof(Node2) = 12.
typedef struct{

    char a;

    char b;

    int c;

}Node3;

   明显地:sizeof(Node3) = 8

   对于结构体A中包含结构体B的情况,将结构体A中的结构体成员B中的最宽的数据类型作为该结构体成员B的数据宽度,同时结构体成员B必须满足上述对齐的规定。

   要注意在VC中有一个对齐系数的概念,若设置了对齐系数,那么上述描述的对齐方式,则不适合。

   例如:

1字节对齐(#pragma pack(1))
输出结果:sizeof(struct test_t) = 8 [两个编译器输出一致]
分析过程:
成员数据对齐
#pragma pack(1)
struct test_t {
    int a;
    char b;
    short c;
    char d;
};
#pragma pack()
成员总大小=8;

 

2字节对齐(#pragma pack(2))
输出结果:sizeof(struct test_t) = 10 [两个编译器输出一致]
分析过程:
成员数据对齐
#pragma pack(2)
struct test_t {
    int a;
    char b;
    short c;
    char d;
};
#pragma pack()
成员总大小=9;

 

4字节对齐(#pragma pack(4))
输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(4)
struct test_t { //按几对齐, 偏移量为后边第一个取模为零的。
int a;
char b;
short c;
char d;
};
#pragma pack()
成员总大小=9;

 

8字节对齐(#pragma pack(8))
输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]
分析过程:
成员数据对齐
#pragma pack(8)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成员总大小=9;

 

16字节对齐(#pragma pack(16))
输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(16)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成员总大小=9;

 

至于8字节对齐和16字节对齐,我觉得这两个例子取得不好,没有太大的参考意义。

(x666f)

posted @ 2013-07-10 17:09 tbwshc 阅读(228) | 评论 (0)编辑 收藏

2013年6月25日 #

任何接口设计的一个准则:让接口容易被正确使用,不容易被误用。

理想上:如何客户企图使用某个接口缺没有获得他所预期的行为,这个代码不该通过编译;如果代码通过了编译,他的行为就该是客户所想要的。

1. 导入外覆类型(wrapper types)

2. 让types容易被正确使用,不容易被误用。尽量领你的types行为与内置types一致。

3. 设计class犹如设计type

新type的对象应该如何被创建和销毁?(自己设计operatornew,operatornew[],operator delete和operator delete[])

对象的初始化和对象的复制该有什么样的差别?对应于不同的函数调用

新type的对象如果被passed by value ,意味着什么

什么是新type的“合法值”?(??)

你的新type需要配合某个继承图系吗?

你的心type需要什么样的转换

什么样的操作符合函数对此新type而言是合理的

什么样的标准函数应该驳回

谁该取用新type的成员

什么是新type的未声明接口

你的新type有多么一般化

你真的需要一个新type吗

 

一、宁以pass-by-reference-to-const 替换 pass-by-value

1.tbw效率高,没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。

2. by refrenece方式传递参数还可以避免对象slicing 问题

二、必须返回对象时,别妄想返回其reference

所有用上static对象的设计,会造成多线程安全性的怀疑。

三、将成员变量声明为private:

1. 可以实现出“不准访问”、“只读访问”、“读写访问”、“惟写访问”

2. 封装:它使我们能够改变事物而只影响有限客户。

将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。如:tb变量被读或被写时通知其他对象、可以验证class的约束条件以及函数的前提和事后转帖;以及在多线程环境执行同步控制。。。。等。

四、宁以non-member、non-friend替换member函数

  1. namespace WebBrowserStuff{ 
  2.         class WebBrowser{...}; 
  3.         void clearBrowser(WebBrowser& wb); 

五、若所有参数皆需要类型转换,请为此采用non-member函数

  1. class Rational { 
  2. ... 
  3.  
  4. const Rational operator*(const Rational& lhs,const Rational& rhs) 
  5. return Rational( lhs.numerator()* rhs.numerator()
  6. ,lhs.denominator()*rhs.denominator() ); 
  7.  
  8. Rational oneFourth(1,4); 
  9. Rational result; 
  10. result = oneFourth * 2; 
  11. result = 2*oneFourth; 

 六、考虑写出一个不抛异常的swap函数

posted @ 2013-06-25 17:17 tbwshc 阅读(424) | 评论 (0)编辑 收藏

好像所有讲述编程的书都用同一个例子来开始,就是在用户的屏幕上显示出“Hello,World!”。总是使用这个例子可能有一点叫人厌烦,可是它确实可以帮助读者迅速地接触到在编程环境中书写简单程序时的简便方法和可能的困难。就这个意义来说,“Hello,World!”可以作为检验编程语言和计算机平台的一个基准。

不幸的是,如果按照这个标准来说,嵌入式系统可能是程序员工作中碰到的最难的计算机平台了。甚至在某些嵌入式系统中,根本无法实现“Hello,World!”程序。即使在那些可以实现这个程序的嵌入式系统里面,文本字符串的输出也更像是目标的一部分而不是开始的一部分。

你看,“Hello,World!”示例隐含的假设,就是有一个可以打印字符串的输出设备。通常使用的是用户显示器上的一个窗口来完成这个功能。但是大多数的嵌入式系统并没有一个显示器或者类似的输出设备。即使是对那些有显示器的系统,通常也需要用一小段嵌入式程序,通过调用显示驱动程序来实现这个功能。这对一个嵌入式编程者来说绝对是一个相当具有挑战性的开端。

看起来我们还是最好以一个小的,容易实现并且高度可移植的联人式程序来开始,这样的tb程序也不太会有编程错误。归根到底,我这本书继续选用“Hello,World!”。这个例子的原因是,实现这个程序实在太简单了。这起码在读者的程序第一次就运行不起来的时候,会去掉一个可能的原因,即:错误不是因为代码里的缺陷:相反,问题出在开发工具或者创建可执行程序的过程里面。

嵌人式程序员在很大程度上必须要依靠自己的力量来工作。在开始一个新项目的时候,除了他所熟悉的编程语言的语法,他必须首先假定什么东西都没有运转起来,甚至连标准库都没有,就是类似printf()和scanf()的那些程序员常常依赖的辅助函数。实际上,库例程常常作为编程语言的基本语法出现。可是这部分标准很难支持所有可能的计算平台,并且常常被嵌入式系统编译器的制造商们所忽略。

所以在这一章里你实际上将找不到一个真正的”Hello,World!”程序,相反,我们假定在第一个例子中只可以使用最基本的C 语言语法。随着本书的进一步深人,我们会逐步向我们的指令系统里添加C++的语法、标准库例程和一个等效的字符输出设备。然后,在第九章“综合所学的知识”里面。我们才最终实现一个“Hello,World!”程序。到那时候你将顺利地走上成为一个嵌入式系统编程专家的道路。

posted @ 2013-06-25 17:12 tbwshc 阅读(281) | 评论 (0)编辑 收藏