嵌入式开发杂谈

时间:2022-03-11 20:07:16

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 还可以支持网络、串口下载,甚至调试功能。但不要指望有一个像PC BIOS 那样通用的BootLoader 供你使用,至少你需要作一些移植工作使其符合你的系统,这个移植工作也是你开发的一个部分,作为嵌入式开发个入门者来讲,移植或者编写一个 BootLoader 会使你受益匪浅。没有BootLoader 行不行?当然可以,要么你就牺牲效率直接从ROM 中运行,要么你就自己编写程序搬移代码去RAM 运行,最主要的是,开发过程中你要有好的调试工具支持在线调试,否则你就得在改动哪怕一个变量的情况下都要去重新烧片验证。继续程序入口的话题,不管过程如何,程序最后在执行时都是变成了机器指令,一个纯的执行程序就是这些机器指令的集合。像我们在操作系统上的可运行程序都不是纯的执行程序,而是带有格式的。一般除了包含上面提到的几个段以外,还有程序的长度,校验以及程序入口——就是从哪儿开始执行用户程序。为什么有了程序地址还需要有程序的入口呢?这是因为你要真正开始执行的代码并非一定放置在一个文件的最开始,就算放在最开始,除非你去控制链接,否则在多文件的情况下,编译器也不一定将你的这段程序放置在最后程序的最顶端。像我们一般有操作系统支持的程序,只需在你的代码中有一个main 作为程序入口——注意这个main 只是大多数编译器约成定俗的入口,除非你利用了别人的初始化库,否则程序入口可以自行设定——即可。显然,带有格式的这种执行文件使用更加灵活,但需要 BootLoader 的支持。有关执行文件格式的内容可以看看ELF 文件格式。

4 编译预处理

        首先看看文件包含,从我们的第一个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))


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

        最后大家看一段将宏用到炉火纯青者的代码,此段代码来自于Das U-boot,这一段代码用7 个简单的宏实现了6 个类似功能的函数!

#define PCI_HOSE_OP(rw, size, type) /
int pci_hose_##rw##_config_##size(struct pci_controller *hose, /
pci_dev_t dev, /
int offset, type value) /
{ /
    return hose->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 大段代码注释

        在程序调试中,我们往往需要临时注释掉一大段代码,如果这段代码中已经有很多注释,那么用/* */这种方式注释会遇到匹配问题,很难一下将整段代码注释掉,// 这种注释很多编译器不支持,即使支持,把几十行代码用这种方式注释也要花掉你很多时间,有好的编辑器倒是支持用这种方式注释掉大段代码,但并非每个人都有用这样的编辑器。如果用预编译很容易处理这个问题,在你需要注释的代码前和代码后分别加:

#if 0

#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(“Build date: %s, /n Build Time: %s/n”,__DATE__, __TIME__);

        这样,程序子每次运行的时候就会打印出其最后编译的日期和时间,再也不用疑惑是否烧错程序版本了。

5 volatile 和cache

        这两个词的搭配看上去似乎风马牛不相及。但是它们却可以给你带来同样的困惑——逻辑正确无误的程序却能得到非意料的结果.对于一个初入门者来说,一段自认为相当不错的代码却得到这样的结果令其非常沮丧,也许一个心理承受能力差的人从此对嵌入式开发失去信心也未可知。

I volatile
        volatile 这个ANSI C 关键字在经典的C 教程中很少提及,高层编程的人也可能永远都不会用到,但是作为嵌入式开发者来说,这个关键字使用频率应该很高。volatile 的字面意思为“不稳定的,易变的”。一般用它定义一些IO 端口的变量。现在假定我们要对一个设备进行初始化,此设备的某一个寄存器地址为0xff800000。我们先看一段程序:

int *output = (int *)0xff800000; /* 定义一个IO 端口*/
int init(void)
{
    int i;
    for(i=0; i<10; i++)
    {
        *output = i;
    }
}

        一般的编译器都带有优化功能,那么这段代码被优化会是什么结果呢?编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output 这个指针赋值为9,所以编译器最后给你编译的代码结果相当于为:

int init(void)
{
    *output = 9;
}


        试想一下,如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化后的程序并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作了只做了一次然而从代码角度看是没有任何问题的。这时候就是 volatile 出场的时候了,volatile 就是通知编译器,这个声明的变量是一个不稳定的,在遇到此变量时候不要优化。对于上面的代码只需在声明时加上volatile 即可。

volatile int *output = (volatile int *)0xff800000; /* 定义一个IO 端口*/

II cache

        我们都知道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
CACHE RAM
外部设备
中断
0x00000000
0x00100000
0x00000000
0x00040000
0x00080000
0x000c0000


图1 一个DMA 操作示例

        图1 中,RAM 内存从地址0x00000000 到0x000ffffff 1M范围内的内容全部被调入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 一个高效的双缓存算法

        在很多场合需要用到双缓存,用一个缓存在接收数据的同时处理另一个缓存。看看一般缓存切换的写法:

#define BUFLEN 256
int buf[2][BUFLEN]
void dealDounBuffer()
{
    static int bufFlag = 0;
    int new, old;
    old = bufFlag;
    if(0 == bufFlag )
    {
        bufFlag = 1;
        new = bufFlag;
    }
    else
    {
        bufFlag = 0;
        new = bufFlag;
    }
    receiver(&buf[new][0]); /* 用下一个缓存接收数据*/
    deal(&buf[old][0]); /* 处理刚刚接收的数据*/
}


再看一个改进的算法:

#define BUFLEN 256
int buf[2][BUFLEN]
void dealDounBuffer()
{
    static int bufFlag = 0;
    int new, old;
    old = bufFlag;
    bufFlag = (bufFlag + 1) %2
    new = bufFlag;
    receiver(&buf[new][0]);
    deal(&buf[old][0]);
}

        看看我设计的(未加求证是否首创或独创,如有前人,多有冒犯)一个算法:

#define BUFLEN 256
int buf[2][BUFLEN]
void dealDounBuffer()
{
    static int bufFlag = 0;
    receiver(&buf[1 - bufFlag][0]);
    deal(&buf[bufFlag][0]);
    bufFlag = 1 – bufFlag;
}

        第一个算法最接近人类思想,也是初学者一般的容易想到的算法,但是用到了if 判断语句,效率不高,程序也显冗长。第二个算法有进步简化了代码,但是用到CPU 不善长的取余运算,不过此算法的优点是不仅仅适合双缓存体系,也可以适合多缓存体系。第三种算法专门针对双缓存设计,即没有费时的判断,也没有取余运算,用一个简单的减法运算实现了双缓存切换。
        与此相仿的是在多任务中多缓存的处理中有这样一种情况:有两个任务A 和B,A 任务负责接收数据,B 任务负责处理数据,A 任务接收数据后通过消息机制通知B 任务处理新到达的数据。由于A 接收的数据是有一定的时延,为不使B 任务形成饥饿状态,我们一般使B 任务先接收到两个缓存的数据才开始真正的处理,以保证它总有就绪的数据可用。看一个普通用法,此处代码为描述,非真实函数。

#define BUFNUM 16
#define BUFLEN 256
char buf[BUFNUM][ BUFLEN];
void taskA(void *p)
{
    static int bufFlag = 0;
    while(1)
    {
        recv(&buf[bufFlag]); /* 接收数据到目前BUF */
        msgSend(taskB, &bufFlag, sizeof(int)); /* 给任务B 发送消息告知bufFlag */
}
        bufFlag = (1 + bufFlag) % BUFNUM;
    }
void taskB(void *p)
{
    int firstTimeFlag = 1;
    int bufFlag;
    int bufFlagTemp;
    while(1)
    {
        if(1 == firstTimeFlag) /* 如果此任务是开始运行*/
        {
            msgRecv(taskA, &bufFlagTemp, sizeof(int));
            msgRecv(taskA, &bufFlag, sizeof(int));
            dealData(&buf[bufFlagTemp][0]);
            dealData(&buf[bufFlag][0]);
            firstTimeFlag = 0;
        }
        else
        {
            msgRecv(taskA, &bufFlag, sizeof(int));
            dealData(&buf[bufFlag][0]);
        }
    }
}

        此算法的思想是这样的,如果任务B 是“开始运行”状态,那么在接到一个数据缓存的情况下不做处理,继续等待第二个缓存数据的到来,然后把这两个缓存数据连续地给真正的数据处理函数。如果不是“开始运行”状态则收到一个缓存就交给处理函数一个缓存。这个算法的问题在于任务是无限循环运行的,但仅仅因为判别是否为第一次运行就必须每次循环都执行一次判断语句。看看一个改进的算法。taskA 不做改动,taskB 改为:

void taskB(void *p)
{
    int firstTimeFlag = 1;
    int bufFlag;
    int bufFlagTemp;
    msgRecv(taskA, &bufFlagTemp, sizeof(int));
    msgRecv(taskA, &bufFlag, sizeof(int));
    dealData(&buf[bufFlagTemp][0]);
    while(1)
    {
        dealData(&buf[bufFlag][0]);
        msgRecv(taskA, &bufFlag, sizeof(int));
    }
}

        因为msgRecv 这类消息接收都是阻塞式的,所以开始运行的时候如果数据未到任务B 就阻塞在数据接收上,直到接收到两个缓存数据在开始交给处理数据,和算法改进前的执行效果是完全相同的。仅仅是结构调整就避免了每次循环都做一个无谓的判断。

7 任务优先级安排

        时下有很多嵌入式实时操作系统可供选择,这种操作系统一般都是支持优先级的强占式操作系统。它们的根本特点就是一旦一个高优先级的任务就绪就可以马上获得 CPU 资源得以运行。任务优先级的安排在这类型操作系统中非常关键,优先级安排不当,轻者让你的系统运行不够理想,重则完全失控。如果你的任务调度是基于优先级的,那么你的任务必须是可阻塞的。一个非阻塞的任务会使比它优先级低的任务永远得不到运行机会。在优先级的安排上,如果两个任务无任何关系,那么赋予那个运行时间短(从运行到阻塞)或者运行频率低的任务更高的优先级,这样会使整个系统中的任务的平均响应时间最短。对于单向任务间通信的两个任务,一般赋予接收消息或信号量的任务更高的优先级。双向通信的两个任务优先级可以互为高低。如果你的消息和信号量不是在任务运行前申请和初始化,那么切记把初始化放在先得以运行的那么任务中,否则会造成先运行的任务无法阻塞。在优先级安排的时候一既要合理使用消息、信号量等任务间通信又不能滥用,避免造成死锁。

2006 年1 月
参考资料
[1] ANDREW S. TANENBAUM, ALBERTS. WOODHULL, 操作系统:设计与实现(第2版),北京:电子工业出版社,1998 年
[2] Jean J. Labrosse, 嵌入式实时操作系统uC/OS-II(第2 版),北京:北京航空航天大学出版社,2002 年