⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 嵌入式.txt

📁 嵌入式c杂谈 本文主要介绍嵌入式c 本文主要介绍嵌入式c
💻 TXT
📖 第 1 页 / 共 2 页
字号:
信心也未可知。
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 年

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -