怎样按照这个多组结构体写入文件式,再写入一些数据?写完之后如图:

嵌入式 C/C++语言精华文章集锦_ASP.NET技巧_动态网站制作指南
嵌入式 C/C++语言精华文章集锦
来源:人气:198
C/C+语言 struct 深层探索 ............................................................................2
C++中 extern &C&含义深层探索........................................................................7
C 语言高效编程的几招...............................................................................11
想成为嵌入式程序员应知道的 0x10 个基本问题 .........................................................15
C 语言嵌入式系统编程修炼...........................................................................22
C 语言嵌入式系统编程修炼之一:背景篇............................................................22
C 语言嵌入式系统编程修炼之二:软件架构篇........................................................24
C 语言嵌入式系统编程修炼之三:内存操作..........................................................30
C 语言嵌入式系统编程修炼之四:屏幕操作..........................................................36
C 语言嵌入式系统编程修炼之五:键盘操作..........................................................43
C 语言嵌入式系统编程修炼之六:性能优化..........................................................46
C/C++语言 void 及 void 指针深层探索 .................................................................50
C/C++语言可变参数表深层探索 .......................................................................54
C/C++数组名与指针区别深层探索 .....................................................................60
C/C++程序员应聘常见面试题深入剖析(1) ..............................................................62
C/C++程序员应聘常见面试题深入剖析(2) ..............................................................67
一道著名外企面试题的抽丝剥茧 ......................................................................74
C/C++结构体的一个高级特性――指定成员的位数 .......................................................78
C/C++中的近指令、远指针和巨指针 ...................................................................80
从两道经典试题谈 C/C++中联合体(union)的使用......................................................81
基于 ARM 的嵌入式
移植真实体验 ................................................................83
基于 ARM 的嵌入式 Linux 移植真实体验(1)――基本概念 ...........................................83
基于 ARM 的嵌入式 Linux 移植真实体验(2)――BootLoader .........................................96
基于 ARM 的嵌入式 Linux 移植真实体验( 3)
―― ..........................................111
基于 ARM 的嵌入式 Linux 移植真实体验(4)――设备驱动 ..........................................120
基于 ARM 的嵌入式 Linux 移植真实体验(5)――应用实例 ..........................................135
深入浅出 Linux 设备驱动编程 .......................................................................144
1.Linux 内核模块..............................................................................144
2.字符设备驱动程序 ...........................................................................146
3.设备驱动中的并发控制 .......................................................................151
4.设备的阻塞与非阻塞操作 .....................................................................157
C/C+语言 struct 深层探索
出处:PConline 作者:宋宝华
1. struct 的巨大作用
面对一个人的大型 C/C++程序时,只看其对 struct 的使用情况我们就可以对其编写者的编程经
验进行评估。因为一个大型的 C/C++程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结
构体可以将原本意义属于一个整体的数据组合在一起。从某种程度上来说,会不会用 struct,怎样用
struct 是区别一个开发人员是否具备丰富开发经历的标志。
在网络协议、 通信控制、 嵌入式系统的 C/C++编程中, 我们经常要传送的不是简单的字节流 (char
型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。
经验不足的开发人员往往将所有需要传送的内容依顺序保存在 char 型数组中,通过指针偏移的
方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序
就要进行非常细致的修改。
一个有经验的开发者则灵活运用结构体,举一个例子,假设网络或控制协议中需要传送三种报
文,其格式分别为 packetA、packetB、packetC:
struct structA
struct structB
struct structC
优秀的程序设计者这样设计传送的报文:
struct CommuPacket
int acketT //报文类型标志
union //每次传送的是三种报文中的一种,使用 union
struct structA packetA; struct structB packetB;
struct structC packetC;
在进行报文传送时,直接传送 struct CommuPacket 一个整体。
假设发送函数的原形如下:
// pSendData:发送字节流的首地址,iLen:要发送的长度
Send(char * pSendData, unsigned int iLen);
发送方可以直接进行如下调用发送 struct CommuPacket 的一个实例 sendCommuPacket:
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
假设接收函数的原形如下:
// ecvData:发送字节流的首地址,iLen:要接收的长度
//返回值:实际接收到的字节数
unsigned int Recv(char * pRecvData, unsigned int iLen);
接收方可以直接进行如下调用将接收到的数据保存在 struct CommuPacket 的一个实例 recvCommuPacket 中:
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
接着判断报文类型进行相应处理:
switch(recvCommuPacket. iPacketType)
case PACKET_A:
… //A 类报文处理
case PACKET_B:
… //B 类报文处理
case PACKET_C:
… //C 类报文处理
以上程序中最值得注意的是
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
中的强制类型转换:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再转化为 char 型指针,
这样就可以直接利用处理字节流的函数。
利用这种强制类型转化,我们还可以方便程序的编写,例如要对 sendCommuPacket 所处内存初始化为 0,可以这
样调用标准库函数 memset():
memset((char *)&sendCommuPacket,0, sizeof(CommuPacket));
2. struct的成员对齐
Intel、微软等公司曾经出过一道类似的面试题:
#include &iostream.h&
#pragma pack(8)
struct example1
struct example2
example1 struct1;
#pragma pack()
int main(int argc, char* argv[])
example2 struct2;
cout && sizeof(example1) &&
cout && sizeof(example2) &&
cout && (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) &&
问程序的输入结果是什么?
不明白?还是不明白?下面一一道来:
2.1 自然对界
struct 是一种复合数据类型,其构成元素既可以是基本数据类型(如 int、long、float 等)的变量,也可以是
一些复合数据类型(如 array、struct、union 等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,
以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各
个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中 size 最大的成员对齐。
struct naturalalign
在上述结构体中,size 最大的是 short,其长度为 2 字节,因而结构体中的 char 成员 a、c 都以 2 为单位对齐,
sizeof(naturalalign)的结果等于 6;
如果改为:
struct naturalalign
其结果显然为 12。
2.2 指定对界
一般地,可以通过下面的方法来改变缺省的对界条件:
· 使用伪指令#pragma pack (n),编译器将按照 n 个字节对齐;
· 使用伪指令#pragma pack (),取消自定义字节对齐方式。
注意: 如果#pragma pack (n)中指定的 n 大于结构体中最大成员的 size,则其不起作用,结构体
仍然按照 size 最大的成员进行对界。
#pragma pack (n)
struct naturalalign
#pragma pack ()
当 n 为 4、8、16 时,其对齐方式均一样,sizeof(naturalalign)的结果都等于 12。而当 n 为 2
时,其发挥了作用,使得 sizeof(naturalalign)的结果为 6。
在 VC++ 6.0 编译器中,我们可以指定其对界方式(见图 1),其操作方式为依次选择 projetct &
setting & C/C++菜单,在 struct member alignment 中指定你要的对界方式。
图 1 在 VC++ 6.0 中指定对界方式
另外,通过__attribute((aligned (n)))也可以让所作用的结构体成员对齐在 n 字节边界上,但
是它较少被使用,因而不作详细讲解。
2.3 面试题的解答
至此,我们可以对 Intel、微软的面试题进行全面的解答。
程序中第 2 行#pragma pack (8)虽然指定了对界为 8,但是由于 struct example1 中的成员最大
size 为 4(long 变量 size 为 4),故 struct example1 仍然按 4 字节对界,struct example1 的 size
为 8,即第 18 行的输出结果;
struct example2 中包含了 struct example1,其本身包含的简单数据成员的最大 size 为 2 (short
变量 e),但是因为其包含了 struct example1,而 struct example1 中的最大成员 size 为 4,struct
example2 也应以 4 对界,#pragma pack (8)中指定的对界对 struct example2 也不起作用,故 19 行的
输出结果为 16;
由于 struct example2 中的成员以 4 为单位对界,故其 char 变量 c 后应补充 3 个空,其后才是
成员 struct1 的内存空间,20 行的输出结果为 4。
3. C 和 C++间 struct 的深层区别
在 C++语言中 struct 具有了“类” 的功能,其与关键字 class 的区别在于 struct 中成员变量
和函数的默认访问权限为 public,而 class 的为 private。
例如,定义 struct 类和 class 类:
struct structA
class classB
a.a = 'a'; //访问 public 成员,合法
b.a = 'a'; //访问 private 成员,不合法
许多文献写到这里就认为已经给出了 C++中 struct 和 class 的全部区别,实则不然,另外一点
需要注意的是:
C++中的 struct 保持了对 C 中 struct 的全面兼容(这符合 C++的初衷——“a better c”),
因而,下面的操作是合法的:
//定义 struct
struct structA
structA a = {'a' , 'a' ,1}; // 定义时直接赋初值
即 struct 可以在定义的时候直接以{ }对其成员变量赋初值,而 class 则不能,在经典书目
《thinking C++ 2nd edition》中作者对此点进行了强调。
4. struct 编程注意事项
看看下面的程序:
1. #include &iostream.h&
2. struct structA
5. char *cM
7. int main(int argc, char* argv[])
9. structA instant1,instant2;
10. char c = 'a';
11. instant1.iMember = 1;
12. instant1.cMember = &c;
13. instant2 = instant1;
14. cout && *(instant1.cMember) &&
15. *(instant2.cMember) = 'b';
16. cout && *(instant1.cMember) &&
17. return 0;
14 行的输出结果是:a
16 行的输出结果是:b
Why?我们在 15 行对 instant2 的修改改变了 instant1 中成员的值!
原因在于 13 行的 instant2 = instant1 赋值语句采用的是变量逐个拷贝,这使得 instant1 和
instant2 中的 cMember 指向了同一片内存,因而对 instant2 的修改也是对 instant1 的修改。
在 C 语言中,当结构体中存在指针型成员时,一定要注意在采用赋值语句时是否将 2 个实例中的
指针型成员指向了同一片内存。
在 C++语言中,当结构体中存在指针型成员时,我们需要重写 struct 的拷贝构造函数并进行“=”
操作符重载。
C++中 extern &C&含义深层探索
作者: 宋宝华 e-mail:
出处: 太平洋电脑网
C++语言的创建初衷是“a better C”,但是这并不意味着 C++中类似 C 语言的全局变量和函数
所采用的编译和连接方式与 C 语言完全相同。作为一种欲与 C 兼容的语言,C++保留了一部分过程式语
言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。
但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与 C
有明显的不同。
2.从标准头文件说起
某企业曾经给出如下的一道面试题:
为什么标准头文件都有类似以下的结构?
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern &C& {
#ifdef __cplusplus
#endif /* __INCvxWorksh */
显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用
是防止该头文件被重复引用。
#ifdef __cplusplus
extern &C& {
#ifdef __cplusplus
的作用又是什么呢?我们将在下文一一道来。
3.深层揭密 extern &C&
extern &C& 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,
被它修饰的目标是“C”的。让我们来详细解读这两重含义。
(1)被 extern &C&限定的函数或变量是 extern 类型的;
extern 是 C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,
其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:
仅仅是一个变量的声明,其并不是在定义变量 a,并未为 a 分配内存空间。变量 a 在所有模块中作
为一种全局变量只能被定义一次,否则会出现连接错误。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字 extern 声明。
例如,如果模块 B 欲引用该模块 A 中定义的全局变量和函数时只需包含模块 A 的头文件即可。这样,
模块 B 中调用模块 A 中的函数时,在编译阶段,模块 B 虽然找不到该函数,但是并不会报错;它会在
连接阶段中从模块 A 编译生成的目标代码中找到此函数。
与 extern 对应的关键字是 static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个
函数或变量只可能被本模块使用时,其不可能被 extern “C”修饰。
(2)被 extern &C&修饰的变量和函数是按照 C 语言方式编译和连接的;
未加 extern “C”声明时的编译方式
首先看看 C++中对类似 C 的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言 C 则不支持。函数被 C++编译后在符
号库中的名字与 C 语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被 C 编译器编译后在符号库中的名字为_foo,而 C++编译器则会产生像_foo_int_int 之类
的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled
name”)。_foo_int_int 这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来
实现函数重载的。例如,在 C++中,函数 void foo( int x, int y )与 void foo( int x, float y )
编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成
员变量可能与全局变量同名,我们以&.&来区分。而本质上,编译器在进行编译时,与函数的处理相似,
也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
未加 extern &C&声明时的连接方式
假设在 C++中,模块 A 的头文件如下:
// 模块 A 头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
在模块 B 中引用该函数:
// 模块 B 实现文件 moduleB.cpp
#include &moduleA.h&
实际上,在连接阶段,连接器会从模块 A 生成的目标文件 moduleA.obj 中寻找_foo_int_int 这样
加 extern &C&声明后的编译和连接方式
加 extern &C&声明后,模块 A 的头文件变为:
// 模块 A 头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern &C& int foo( int x, int y );
在模块 B 的实现文件中仍然调用 foo( 2,3 ),其结果是:
(1)模块 A 编译生成 foo 的目标代码时,没有对其名字进行特殊处理,采用了 C 语言的方式;
(2)连接器在为模块 B 的目标代码寻找 foo(2,3)调用时,寻找的是未经修改的符号名_foo。
如果在模块 A 中函数声明了 foo 为 extern &C&类型,而模块 B 中包含的是 extern int foo( int x,
int y ) ,则模块 B 找不到模块 A 中的函数;反之亦然。
所以,可以用一句话概括 extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生
都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么
做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):
实现 C++与 C 及其它语言的混合编程。
明白了 C++中 extern &C&的设立动机,我们下面来具体分析 extern &C&通常的使用技巧。
4.extern &C&的惯用法
(1)在 C++中引用 C 语言中的函数和变量,在包含 C 语言头文件(假设为 cExample.h)时,需进
行下列处理:
extern &C&
#include &cExample.h&
而在 C 语言的头文件中,对其外部函数只能指定为 extern 类型,C 语言中不支持 extern &C&声明,
在.c 文件中包含了 extern &C&时会出现编译语法错误。
笔者编写的 C++引用 C 函数例子工程中包含的三个文件的源代码如下:
/* c 语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
/* c 语言实现文件:cExample.c */
#include &cExample.h&
int add( int x, int y )
return x +
// c++实现文件,调用 add:cppFile.cpp
extern &C&
#include &cExample.h&
int main(int argc, char* argv[])
如果 C++调用一个 C 语言编写的.DLL 时,当包括.DLL 的头文件或声明接口函数时,应加 extern &C&
(2)在 C 中引用 C++语言中的函数和变量时,C++的头文件需添加 extern &C&,但是在 C 语言中不
能直接引用声明了 extern &C&的该头文件,应该仅将 C 文件中将 C++中定义的 extern &C&函数声明为
extern 类型。
笔者编写的 C 引用 C++函数例子工程中包含的三个文件的源代码如下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern &C& int add( int x, int y );
//C++实现文件 cppExample.cpp
#include &cppExample.h&
int add( int x, int y )
return x +
/* C 实现文件 cFile.c
/* 这样会编译出错:#include &cExample.h& */
extern int add( int x, int y );
int main( int argc, char* argv[] )
add( 2, 3 );
如果深入理解了第 3 节中所阐述的 extern &C&在编译和连接阶段发挥的作用,就能真正理解本节
所阐述的从 C++引用 C 函数和 C 引用 C++函数的惯用法。对第 4 节给出的示例代码,需要特别留意各个
C 语言高效编程的几招
编写高效简洁的 C
语言代码,是许多软件工程师追求的目标。本文就工作中的一些体会和经验做相关的阐述,不对的地方
各位指教。
第 1 招:以空间换时间
计算机程序中最大的矛盾是空间和时间的矛盾,那么,从这个角度出发逆向思维来考虑程序的效率问题,我们就有了解决
的第 1 招--以空间换时间。
例如:字符串的赋值。
方法 A,通常的办法:
#define LEN 32
char string1 [LEN];
memset (string1,0,LEN);
strcpy (string1,&This is an example!!&
const char string2[LEN]=&This is an example!&
cp=string2;
(使用的时候可以直接用指针来操作。 )
从上面的例子可以看出, A
的效率是不能比的。在同样的存储空间下,
B 直接使用指针就可以操作了,而
A 需要调用
两个字符函数才能完成。 B 的缺点在于灵活性没有
A 好。在需要频繁更改一个字符串内容的时候,
A 具有更好的灵活性;
如果采用方法 B,则需要预存许多字符串,虽然占用了 大量的内存,但是获得了程序执行的高效率。
如果系统的实时性要求很高,内存还有一些,那我推荐你使用该招数。
该招数的边招--使用宏函数而不是函数。举例如下:
#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
int BIT_MASK (int_bf)
return ((IU&&(bw##_bf))-1)&&(bs##_bf);
void SET_BITS(int_dst,int_bf,int_val)
_dst=((_dst) & ~ (BIT_MASK(_bf)))I\
(((_val)&&&(bs##_bf))&(BIT_MASK(_bf)))
SET_BITS(MCDR2,MCDR2_ADDRESS,RegisterNumb
#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
#define bmMCDR2_ADDRESS BIT_MASK
(MCDR2_ADDRESS)
#define BIT_MASK(_bf)(((1U&&(bw##_bf))-1)&&
#define SET_BITS(_dst,_bf,_val)\
((_dst)=((_dst)&~(BIT_MASK(_bf)))I
(((_val)&&(bs##_bf))&(BIT_MASK(_bf))))
SET_BITS(MCDR2,MCDR2_ADDRESS,RegisterNumb
函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。大家要知道的是,函数调用是要使用系统的
栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时, CPU
在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些 CPU
时间。而宏函数不存在这个问
题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏
函数的时候,该现象尤其突出。
D 方法是我看到的最好的置位操作函数,是
ARM 公司的一部分,在短短的三行内实现了很多功能,几乎涵盖了所有
的位操作功能。 C 方法是其变体,其中滋味还需大家仔细体会。
第 2 招:数学方法解决问题
现在我们演绎高效 C 语言编写的第二招--采用数学方法来解决问题。
数学是计算机之母,没有数学的依据和基础,就没有计算机的发展,所以在编写程序的时候,采用一些数学方法会对程序
的执行效率有数量级的提高。
举例如下,求 1~100 的和。
for (I=1; I&=100; I++){
I=(100*(1+100))/2
这个例子是我印象最深的一个数学用例,是我的饿计算机启蒙老师考我的。当时我只有小学三年级,可惜我当时不知道用
公式 Nx(N+1)/2 来解决这个问题。方法
100 次才解决问题,也就是说最少用了
100 个赋值、
100 个判断、
和 j);而方法
F 仅仅用了
1 个加法、
1 个乘法、
1 次除法。效果自然不言而喻。所以,现在我在编程序的时候,
更多的是动脑筋找规律,最大限度地发挥数学的威力来提高程序运行的效率。
第 3 招:使用位操作
实现高效的 C 语言编写的第三招--使用位操作,减少除法和取模的运算。
在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作。一般的位操作
是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。举例台如下:
J=456-(456&&4&&4);
在字面上好象 H
麻烦了好多,但是,仔细查看产生的汇编代码就会明白,方法
G 调用了基本的取模函数和除法函数,
既有函数调用,还有很多汇编代码和寄存器参与运算;而方法 H
则仅仅是几句相关的汇编,代码更简洁、效率更高。当然,
由于编译器的不同,可能效率的差距不大,但是,以我目前遇到的 MS C,ARM C
来看,效率的差距还是不小。相关汇编
代码就不在这里列举了。
运用这招需要注意的是,因为 CPU 的不同而产生的问题。比如说,在
PC 上用这招编写的程序,并在
PC 上调试通过,在
移植到一个 16 位机平台上的时候,可能会产生代码隐患。所以只有在一定技术进阶的基础下才可以使用这招。
第 4 招:汇编嵌入
高效 C 语言编程的必杀技,第四招--嵌入汇编。
“在熟悉汇编语言的人眼里,
C 语言编写的程序都是垃圾”。这种说法虽然偏激了一些,但是却有它的道理。汇编语言是效
率最高的计算机语言,但是,不可能靠着它来写一个操作系统吧?所以,为了获得程序的高效率,我们只好采用变通的方
法--嵌入汇编、混合编程。
举例如下,将数组一赋值给数组二,要求每一个字节都相符。 char string1[1024], string2[1024];
for (I=0; I&1024; I++)
*(string2+I)=*(string1+I)
for(I=0; I&1024; I++)
*(string2+I)=*(string1+I);
#ifdef_ARM_
MOV R0,string1
MOV R1,string2
LDMIA R0!,[R3-R11]
STMIA R1!,[R3-R11]
ADD R2,R2,#8
CMP R2, #400
是最常见的方法,使用了 1024
次循环;方法 J
则根据平台不同做了区分,在 ARM
平台下,用嵌入汇编仅用 128
次循环就完成了同样的操作。这里有朋友会说,为什么不用标准的内存拷贝函数呢?这是因为在源数据里可能含有数据为
0 的字节,这样的话,标准库函数会提前结束而不会完成我们要求的操作。这个例程典型应用于
LCD 数据的拷贝过程。根
据不同的 CPU,熟练使用相应的嵌入汇编,可以大大提高程序执行的效率。
虽然是必杀技,但是如果轻易使用会付出惨重的代价。这是因为,使用了嵌入汇编,便限制了程序的可移植性,使程序在
不同平台移植的过程中,卧虎藏龙、险象环生!同时该招数也与现代软件工程的思想相违背,只有在迫不得已的情况下才
可以采用。切记。
使用 C 语言进行高效率编程,我的体会仅此而已。在此已本文抛砖引玉,还请各位高手共同切磋。希望各位能给出更好的
方法,大家一起提高我们的编程技巧。
摘自《单片机与嵌入式系统应用》 2003.9
想成为嵌入式程序员应知道的 0x10 个基本问题
-|endeaver
C 语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年,我既参加也组织了许多这种测试,在这过程中我意识到这些测
试能为带面试者和被面试者提供许多有用信息,此外,撇开面试的压力不谈,这种测试也是相当有趣的。
从被面试者的角度来讲,你能了解许多关于出题者或监考者的情况。这个测试只是出题者为显示其对 ANSI
标准细节的知识而不是技术技巧而
设计吗?这个愚蠢的问题吗?如要你答出某个字符的 ASCII
值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗?这标志着出题
者也许花时间在微机上而不上在嵌入式系统上。如果上述任何问题的答案是&是&的话,那么我知道我得认真考虑我是否应该去做这份工作。
从面试者的角度来讲,一个测试也许能从多方面揭示应试者的素质:最基本的,你能了解应试者 C
语言的水平。不管怎么样,看一下这人如何
回答他不会的问题也是满有趣。应试者是以好的直觉做出明智的选择,还是只是瞎蒙呢?当应试者在某个问题上卡住时是找借口呢,还是表现
出对问题的真正的好奇心,把这看成学习的机会呢?我发现这些信息与他们的测试成绩一样有用。
有了这些想法,我决定出一些真正针对嵌入式系统的考题,希望这些令人头痛的考题能给正在找工作的人一点帮住。这些问题都是我这些年实
际碰到的。其中有些题很难,但它们应该都能给你一点启迪。
这个测试适于不同水平的应试者,大多数初级水平的应试者的成绩会很差,经验丰富的程序员应该有很好的成绩。为了让你能自己决定某些问
题的偏好,每个问题没有分配分数,如果选择这些考题为你所用,请自行按你的意思分配分数。
预处理器( Preprocessor)
1 . 用预处理指令#define
声明一个常数,用以表明 1
年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
o; #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
o; 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
o; 意识到这个表达式将使一个
16 位机的整型数溢出-因此要用到长整型符号
L,告诉编译器这个常数是的长整型数。
o; 如果你在你的表达式中用到
UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
2 . 写一个&标准&宏
MIN ,这个宏输入两个参数并返回较小的一个。
#define MIN(A,B) ((
&= (B) ? (A) : (B))
这个测试是为下面的目的而设的:
o; 标识#define
在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准
C 的一部分,宏是方便产生嵌入代码的唯一方
法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
o; 三重条件操作符的知识。这个操作符存在
C 语言中的原因是它使得编译器能产生比
if-then-else 更优化的代码,了解这个用法是很重要的。
o; 懂得在宏中小心地把参数用括号括起来
o; 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
3. 预处理器标识#error
的目的是什么?
如果你不知道答案,请看参考文献 1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读
C 语言课本的附录去找出象
这种问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。
死循环( Infinite loops)
4. 嵌入式系统中经常要用到无限循环,你怎么样用
C 编写死循环呢?
这个问题用几个解决方案。我首选的方案是:
一些程序员更喜欢如下方案:
这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们
这样做的基本原理。如果他们的基本答案是: &我被教着这样做,但从没有想到过为什么。
&这会给我留下一个坏印象。
第三个方案是用 goto
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的
BASIC/FORTRAN 程序员。
数据声明( Data declarations)
a 给出下面的定义
a) 一个整型数(
An integer)
b)一个指向整型数的指针(A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an intege)
d)一个有 10
个整型数的数组(An array of 10 integers)
10 个指针的数组,该指针是指向一个整型数的。(
An array of 10 pointers to integers)
f) 一个指向有
10 个整型数数组的指针(A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(
A pointer to a function that takes an integer as an argument
and returns an integer)
10 个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数(An array of ten pointers to functions t
hat take an integer argument and return an integer )
a) // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了
一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。
应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为
什么出准备呢?
6. 关键字 static
的作用是什么?
这个简单的问题很少有人能回答完全。在 C
语言中,关键字 static
有三个明显的作用:
o; 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
o; 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变
o; 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然
不懂得本地化数据和代码范围的好处和重要性。
7.关键字 const
有什么含意?
我只要一听到被面试者说: &const
意味着常数&,我就知道我正在和一个业余者打交道。去年
Dan Saks 已经在他的文章里完全概括了
的所有用法,因此 ESP(译者:
Embedded Systems Programming)的每一位读者应该非常熟悉
const 能做什么和不能做什么.如果你从没有
读到那篇文章,只要能说出 const 意味着&只读&就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道
更详细的答案,仔细读一下 Saks 的文章吧。)
如果应试者能正确回答这个问题,我将问他一个附加的问题:
下面的声明都是什么意思?
const int *a;
int const *
前两个的作用是一样, a
是一个常整型数。第三个意味着 a
是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个
意思 a 是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着
a 是一个指向常整
型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留
下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关
键字 const 呢?我也如下的几下理由:
const 的作用是为给读你代码的人传达非常有用的信息,实际上, 声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果
你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用 const
的程序员很少会留下的垃圾让别人来清
o; 通过给优化器一些附加的信息,使用关键字
const 也许能产生更紧凑的代码。
o; 合理地使用关键字
const 可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少
8. 关键字 volatile
有什么含意?并给出三个不同的例子。
一个定义为 volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到
这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是 volatile
变量的几个例子:
o; 并行设备的硬件寄存器(如:状态寄存器)
o; 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
o; 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分 C
程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、
RTOS 等等打交道,所有这些都要求用到
volatile 变量。不懂得
volatile 的内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得 volatile
完全的重要性。
o; 一个参数既可以是
const 还可以是
volatile 吗?解释为什么。
o; 一个指针可以是
volatile 吗?解释为什么。
o; 下面的函数有什么错误:
int square(volatile int *ptr)
return *ptr * *
下面是答案:
o; 是的。一个例子是只读的状态寄存器。它是
volatile 因为它可能被意想不到地改变。它是
const 因为程序不应该试图去修改它。
o; 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个
buffer 的指针时。
o; 这段代码有点变态。这段代码的目的是用来返指针*ptr
指向值的平方,但是,由于*ptr
指向一个 volatile
型参数,编译器将产生类似下面
int square(volatile int *ptr)
return a *
的值可能被意想不到地该变,因此 a
可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
return a *
位操作( Bit manipulation)
9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量
a,写两段代码,第一个设置
bit 3,第二个清除
在以上两个操作中,要保持其它位不变。
对这个问题有三种基本的反应
o; 不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
bit fields。
Bit fields 是被扔到
C 语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可
重用的。我最近不幸看到 Infineon
为其较复杂的通信芯片写的驱动程序,它用到了 bit fields
因此完全对我无用,因为我的编译器用其它的方
式来实现 bit fields 的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
#defines 和
bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:
#define BIT3 (0x1 && 3)
void set_bit3(void) {
a |= BIT3;
void clear_bit3(void) {
a &= ~BIT3;
一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、
|=和&=~操作。
访问固定的内存位置( ing fixed memory locations)
10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为
0x67a9 的整型变量的值为
6。编译器是一个纯粹的 ANSI
编译器。写代码去完成这一任务。
这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换( typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同
而不同。典型的类似代码如下:
ptr = (int *)0x67a9;
*ptr = 0xaa55;
A more obscure approach is:
一个较晦涩的方法是:
*(int * const)(0x67a9) = 0xaa55;
即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。
中断( Interrupts)
11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展―让标准
C 支持中断。具代表事实是,产生了一个新的关键
字__interrupt。下面的代码就使用了__interrupt
关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
__interrupt double compute_area (double radius)
double area = PI * radius *
printf(&\nArea = %f&, area);
这个函数有太多的错误了,以至让人不知从何说起了:
o; ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
o; ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
o; 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在
R 中做浮点运算。此外, ISR
应该是短而有效率的,在 ISR
中做浮点运算是不明智的。
o; 与第三点一脉相承,
printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,
那么你的被雇用前景越来越光明了。
代码例子( Code examples)
12 . 下面的代码输出是什么,为什么?
void foo(void)
unsigned int a = 6;
int b = -20;
(a+b & 6) ? puts(&& 6&) : puts(&&= 6&);
这个问题测试你是否懂得 C
语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是
&&6&。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。 因此-20
变成了一个非常大的正整数,所以
该表达式计算出的结果大于 6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了
得不到这份工作的边缘。
13. 评价下面的代码片断:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */
对于一个 int
位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;
这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而
C 机程序往往把硬件作为一个无法避免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应
试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看
到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧...
动态内存分配( Dynamic memory allocation)
14. 尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(
heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发
生的问题是什么?
这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在 ESP
杂志中被广泛地讨论过了(主要是 P.J.
Plauger, 他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一
个小节目:
下面的代码片段的输出是什么,为什么?
if ((ptr = (char *)malloc(0)) ==
puts(&Got a null pointer&);
puts(&Got a valid pointer&);
这是一个有趣的问题。最近在我的一个同事不经意把 0
值传给了函数 malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的
代码,该代码的输出是&Got a valid pointer&。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的
答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。
15 Typedef 在 C
语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;
以上两种情况的意图都是要定义 dPS
作为一个指向结构 s
指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是: typedef
更好。思考下面的例子:
dPS p1,p2;
tPS p3,p4;
第一个扩展为
struct s * p1, p2;
上面的代码定义 p1
为一个指向结构的指, p2
为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了 p3
两个指针。
晦涩的语法
16 . C 语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
int a = 5, b = 7,
c = a+++b;
这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作
者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:
c = a++ +
这段代码持行后 a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编
写风格,代码的可读性,代码的可修改性的好的话题。
好了,伙计们,你现在已经做完所有的测试了。这就是我出的 C
语言测试题,我怀着愉快的心情写完它,希望你以同样的心情读完它。如果是
认为这是一个好的测试,那么尽量都用到你的找工作的过程中去吧。天知道也许过个一两年,我就不做现在的工作,也需要找一个。
Nigel Jones 是一个顾问,现在住在
Maryland,当他不在水下时,你能在多个范围的嵌入项目中找到他。 他很高兴能收到读者的来信,他的
email 地址是:
References
o; Jones, Nigel, &In Praise of the #error directive,& Embedded Systems Programming, September 1999, p. 114.
o; Jones, Nigel, & Efficient C Code for Eight-bit MCUs ,& Embedded Systems Programming, November 1998, p. 66
C 语言嵌入式系统编程修炼
C 语言嵌入式系统编程修炼之一:背景篇
作者:宋宝华 更新日期:
来源:yesky.com
不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势必要求其编程语言具备较强的硬件直接操作能
力。无疑,汇编语言具备这样的特质。但是,归因于汇编语言开发过程的复杂性,它并不是嵌入式系统开发的一般选择。
而与之相比,C 语言--一种&高级的低级&语言,则成为嵌入式系统开发的最佳选择。笔者在嵌入式系统项目的开发过程中,
一次又一次感受到 C 语言的精妙,沉醉于 C 语言给嵌入式开发带来的便利。
图 1 给出了本文的讨论所基于的硬件平台,实际上,这也是大多数嵌入式系统的硬件平台。它包括两部分:
(1) 以通用处理器为中心的协议处理模块,用于网络控制协议的处理;
(2) 以数字信号处理器(DSP)为中心的信号处理模块,用于调制、解调和数/模信号转换。
本文的讨论主要围绕以通用处理器为中心的协议处理模块进行,因为它更多地牵涉到具体的 C 语言编程技巧。而 DSP
编程则重点关注具体的数字信号处理算法,主要涉及通信领域的知识,不是本文的讨论重点。
着眼于讨论普遍的嵌入式系统 C 编程技巧,系统的协议处理模块没有选择特别的 CPU,而是选择了众所周知的 CPU 芯
片--80186,每一位学习过《微机原理》的读者都应该对此芯片有一个基本的认识,且对其指令集比较熟悉。80186 的字长
是 16 位,可以寻址到的内存空间为 1MB,只有实地址模式。C 语言编译生成的指针为 32 位(双字),高 16 位为段地址,低
16 位为段内偏移,一段最多 64KB。
图 1 系统硬件架构
协议处理模块中的
和 RAM 几乎是每个嵌入式系统的必备设备,前者用于存储程序,后者则是程序运行时指令及
数据的存放位置。系统所选择的 FLASH 和 RAM 的位宽都为 16 位,与 CPU 一致。
实时钟芯片可以为系统定时,给出当前的年、月、日及具体时间(小时、分、秒及毫秒),可以设定其经过一段时间即
向 CPU 提出中断或设定报警时间到来时向 CPU 提出中断(类似闹钟功能)。
NVRAM(非易失去性 RAM)具有掉电不丢失数据的特性,可以用于保存系统的设置信息,譬如网络协议参数等。在系统
掉电或重新启动后,仍然可以读取先前的设置信息。其位宽为 8 位,比 CPU 字长小。文章特意选择一个与 CPU 字长不一致
的存储芯片,为后文中一节的讨论创造条件。
UART 则完成 CPU 并行数据传输与 RS-232 串行数据传输的转换,它可以在接收到[1~MAX_BUFFER]字节后向 CPU 提出中
断,MAX_BUFFER 为 UART 芯片存储接收到字节的最大缓冲区。
键盘控制器和显示控制器则完成系统人机界面的控制。
以上提供的是一个较完备的嵌入式系统硬件架构,实际的系统可能包含更少的外设。之所以选择一个完备的系统,是
为了后文更全面的讨论嵌入式系统 C 语言编程技巧的方方面面,所有设备都会成为后文的分析目标。
嵌入式系统需要良好的软件开发环境的支持,由于嵌入式系统的目标机资源受限,不可能在其上建立庞大、复杂的开
发环境,因而其开发环境和目标运行环境相互分离。因此,嵌入式应用软件的开发方式一般是,在宿主机(Host)上建立开
发环境,进行应用程序编码和交叉编译,然后宿主机同目标机(Target)建立连接,将应用程序下载到目标机上进行交叉调
试,经过调试和优化,最后将应用程序固化到目标机中实际运行。
CAD-UL 是适用于 x86 处理器的嵌入式应用软件开发环境,它运行在 Windows 操作系统之上,可生成 x86 处理器的目标
代码并通过 PC 机的 COM 口(RS-232 串口)或以太网口下载到目标机上运行,如图 2。其驻留于目标机 FLASH 存储器中的
monitor 程序可以监控宿主机 Windows 调试平台上的用户调试指令,获取 CPU 寄存器的值及目标机存储空间、I/O 空间的内
图 2 交叉开发环境
后续章节将从软件架构、内存操作、屏幕操作、键盘操作、性能优化等多方面阐述 C 语言嵌入式系统的编程技巧。软
件架构是一个宏观概念,与具体硬件的联系不大;内存操作主要涉及系统中的 FLASH、 RAM 和 NVRAM 芯片;屏幕操作则涉及
显示控制器和实时钟;键盘操作主要涉及键盘控制器;性能优化则给出一些具体的减小程序时间、空间消耗的技巧。
在我们的修炼旅途中将经过 25 个关口,这些关口主分为两类,一类是技巧型,有很强的适用性;一类则是常识型,在
理论上有些意义。
C 语言嵌入式系统编程修炼之二:软件架构篇
作者:宋宝华 更新日期:
模块划分的&划&是规划的意思, 意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。
C 语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能(依功能进行划分在面向对象设计中成为一个错误,
牛顿定律遇到了&相对论),C 语言模块化程序设计需理解如下概念:
(1) 模块即是一个.c 文件和一个.h 文件的结合,头文件(.h)中是对于该模块接口的声明;
(2) 某模块提供给其它模块调用的外部函数及数据需在.h 中文件中冠以 extern 关键字声明;
(3) 模块内的函数和全局变量需在.c 文件开头冠以 static 关键字声明;
(4) 永远不要在.h 文件中定义变量!定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概
念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如:
/*module1.h*/
int a = 5; /* 在模块 1 的.h 文件中定义 int a */
/*module1 .c*/
#include &module1.h& /* 在模块 1 中包含模块 1 的.h 文件 */
/*module2 .c*/
#include &module1.h& /* 在模块 2 中包含模块 1 的.h 文件 */
/*module3 .c*/
#include &module1.h& /* 在模块 3 中包含模块 1 的.h 文件 */
以上程序的结果是在模块 1、2、3 中都定义了整型变量 a,a 在不同的模块中对应不同的地址单元,这个世界上从来不
需要这样的程序。正确的做法是:
/*module1.h*/
/* 在模块 1 的.h 文件中声明 int a */
/*module1 .c*/
#include &module1.h& /* 在模块 1 中包含模块 1 的.h 文件 */
int a = 5; /* 在模块 1 的.c 文件中定义 int a */
/*module2 .c*/
#include &module1.h& /* 在模块 2 中包含模块 1 的.h 文件 */
/*module3 .c*/
#include &module1.h& /* 在模块 3 中包含模块 1 的.h 文件 */
这样如果模块 1、2、3 操作 a 的话,对应的是同一片内存单元。
一个嵌入式系统通常包括两类模块:
(1)硬件驱动模块,一种特定硬件对应一个模块;
(2)软件功能模块,其模块的划分应满足低偶合、高内聚的要求。
多任务还是单任务
所谓&单任务系统&是指该系统不能支持多任务并发操作,宏观串行地执行一个任务。而多任务系统则可以宏观并
行(微观上可能串行)地&同时&执行多个任务。
多任务的并发执行通常依赖于一个多任务操作系统 (OS), 多任务 OS 的核心是系统调度器, 它使用任务控制块 (TCB)
来管理任务调度功能。TCB 包括任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指
针等信息。调度器在任务被激活时,要用到这些信息。此外,TCB 还被用来存放任务的&上下文&(context)。任务的上
下文就是当一个执行中的任务被停止时,所要保存的所有信息。通常,上下文就是计算机当前的状态,也即各个寄存
器的内容。 当发生任务切换时, 当前运行的任务的上下文被存入 TCB, 并将要被执行的任务的上下文从它的 TCB 中取出,
放入各个寄存器中。
嵌入式多任务 OS 的典型例子有 Vxworks、ucLinux 等。嵌入式 OS 并非遥不可及的神坛之物,我们可以用不到 1000
行代码实现一个针对 80186 处理器的功能最简单的 OS 内核,作者正准备进行此项工作,希望能将心得贡献给大家。
究竟选择多任务还是单任务方式,依赖于软件的体系是否庞大。例如,绝大多数手机程序都是多任务的,但也有
一些小灵通的协议栈是单任务的,没有操作系统,它们的主程序轮流调用各个软件模块的处理程序,模拟多任务环境。
单任务程序典型架构
(1)从 CPU 复位时的指定地址开始执行;
(2)跳转至汇编代码 startup 处执行;
(3)跳转至用户主程序 main 执行,在 main 中完成:
a.初试化各硬件设备;
b.初始化各软件模块;
c.进入死循环(无限循环),调用各模块的处理函数
用户主程序和各模块的处理函数都以 C 语言完成。用户主程序最后都进入了一个死循环,其首选方案是:
有的程序员这样写:
这个语法没有确切表达代码的含义,我们从 for(;;)看不出什么,只有弄明白 for(;;)在 C 语言中意味着无条件循环才
明白其意。
下面是几个&著名&的死循环:
(1)操作系统是死循环;
(2)WIN32 程序是死循环;
(3)嵌入式系统软件是死循环;
(4)多线程程序的线程处理函数是死循环。
你可能会辩驳,大声说:&凡事都不是绝对的,2、3、4 都可以不是死循环&。Yes,you are right,但是你得不到鲜
花和掌声。实际上,这是一个没有太大意义的牛角尖,因为这个世界从来不需要一个处理完几个消息就喊着要 OS 杀死它的
WIN32 程序,不需要一个刚开始 RUN 就自行了断的嵌入式系统,不需要莫名其妙启动一个做一点事就干掉自己的线程。有
时候,过于严谨制造的不是便利而是麻烦。君不见,五层的 TCP/IP 协议栈超越严谨的 ISO/OSI 七层协议栈大行其道成为事
实上的标准?
经常有网友讨论:
printf(&%d,%d&,++i,i++); /* 输出是什么?*/
c = a+++b; /* c=? */
等类似问题。面对这些问题,我们只能发出由衷的感慨:世界上还有很多有意义的事情等着我们去消化摄入的食物。
实际上,嵌入式系统要运行到世界末日。
中断服务程序
中断是嵌入式系统中重要的组成部分,但是在标准 C 中不包含中断。许多编译开发商在标准 C 上增加了对中断的支持,
提供新的关键字用于标示中断服务程序 (ISR),类似于__interrupt、#program interrupt 等。当一个函数被定义为 ISR
的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
中断服务程序需要满足如下要求:
(1)不能返回值;
(2)不能向 ISR 传递参数;
(3) ISR 应该尽可能的短小精悍;
(4) printf(char * lpFormatString,…)函数会带来重入和性能问题,不能在 ISR 中采用。
在某项目的开发中,我们设计了一个队列,在中断服务程序中,只是将中断类型添加入该队列中,在主程序的死循环
中不断扫描中断队列是否有中断,有则取出队列中的第一个中断类型,进行相应处理。
/* 存放中断的队列 */
typedef struct tagIntQueue
int intT /* 中断类型 */
struct tagIntQueue *
IntQueue lpIntQueueH
__interrupt ISRexample ()
intType = GetSystemType();
QueueAddTail(lpIntQueueHead, intType);/* 在队列尾加入新的中断 */
在主程序循环中判断是否有中断:
If( !IsIntQueueEmpty() )
intType = GetFirstInt();
switch(intType) /* 是不是很象 WIN32 程序的消息解析函数? */
/* 对,我们的中断类型解析很类似于消息驱动 */
case xxx: /* 我们称其为&中断驱动&吧? */
按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。
硬件驱动模块
一个硬件驱动模块通常应包括如下函数:
(1)中断服务程序 ISR
(2)硬件初始化
a.修改寄存器,设置硬件参数(如 UART 应设置其波特率,AD/DA 设备应设置其采样速率等);
b.将中断服务程序入口地址写入中断向量表:
/* 设置中断向量表 */
m_myPtr = make_far_pointer(0l); /* 返回 void far 型指针 void far * */
m_myPtr += ITYPE_UART; /* ITYPE_UART: uart 中断服务程序 */
/* 相对于中断向量表首地址的偏移 */
*m_myPtr = &UART _I /* UART _Isr:UART 的中断服务程序 */
(3)设置 CPU 针对该硬件的控制线
a.如果控制线可作 PIO(可编程 I/O)和控制信号用,则设置 CPU 内部对应寄存器使其作为控制信号;
b.设置 CPU 内部的针对该设备的中断屏蔽位,设置中断方式(电平触发还是边缘触发)。
( 4)提供一系列针对该设备的操作接口函数。
例如,对于 LCD,其驱动模块应提供绘制像素、画线、绘制矩阵、显示
字符点阵等函数;而对于实时钟,其驱动模块则需提供获取时间、设置时间等函数。
C 的面向对象化
在面向对象的语言里面,出现了类的概念。类是对特定数据的特定操作的集合体。类包含了两个范畴:数据和操作。
而 C 语言中的 struct 仅仅是数据的集合,我们可以利用函数指针将 struct 模拟为一个包含数据和操作的&类&。下面的 C
程序模拟了一个最简单的&类&:
#ifndef C_Class
#define C_Class struct
C_Class A *A_ /* this 指针 */
void (*Foo)(C_Class A *A_this); /* 行为:函数指针 */
/* 数据 */
我们可以利用 C 语言模拟出面向对象的三个特性:封装、继承和多态,但是更多的时候,我们只是需要将数据与行为
封装以解决软件结构混乱的问题。C 模拟面向对象思想的目的不在于模拟行为本身,而在于解决某些情况下使用 C 语言编
程时程序整体框架结构分散、数据和函数脱节的问题。我们在后续章节会看到这样的例子。
本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、
中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。
请记住:软件结构是软件的灵魂!结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。
C 语言嵌入式系统编程修炼之三:内存操作
作者:宋宝华 更新日期:
在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的 MOV 指令,而除 C/C++以外的其它编程
语言基本没有直接访问绝对地址的能力。在嵌入式系统的实际调试中,多借助 C 语言指针所具有的对绝对地址单元内容的
读写能力。 以指针直接操作内存多发生在如下几种情况:
(1) 某 I/O 芯片被定位在 CPU 的存储空间而非 I/O 空间,而且寄存器对应于某特定地址;
(2) 两个 CPU 之间以双端口 RAM 通信,CPU 需要在双端口 RAM 的特定单元(称为 mail box)书写内容以在对方 CPU 产
(3) 读取在 ROM 或 FLASH 的特定单元所烧录的汉字和英文字模。
unsigned char *p = (unsigned char *)0xF000FF00;
以上程序的意义为在绝对地址 0xFxFF00(80186 使用 16 位段地址和 16 位偏移地址)写入 11。
在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的数据类别。上例中 p++后的结果是 p=
0xF000FF01,若 p 指向 int,即:
int *p = (int *)0xF000FF00;
p++(或++p)的结果等同于:p = p+sizeof(int),而 p-(或-p)的结果是 p = p-sizeof(int)。
同理,若执行:
long int *p = (long int *)0xF000FF00;
则 p++(或++p)的结果等同于:p = p+sizeof(long int) ,而 p-(或-p)的结果是 p = p-sizeof(long int)。
记住: CPU 以字节为单位编址,而 C 语言指针以指向的数据类型长度作自增和自减。理解这一点对于以指针直接操作
内存是相当重要的。
首先要理解以下三个问题:
(1)C 语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;
(2)调用函数实际上等同于&调转指令+参数传递处理+回归位置入栈&,本质上最核心的操作是将函数生成的目标代
码的首地址赋给 CPU 的 PC 寄存器;
(3)因为函数调用的本质是跳转到某一个地址单元的 code 去执行,所以可以&调用&一个根本就不存在的函数实体,
晕?请往下看:
请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到,186 CPU 启动后跳转至绝对地址 0xFFFF0(对
应 C 语言指针是 0xF000FFF0,0xF000 为段地址,0xFFF0 为段内偏移)执行,请看下面的代码:
typedef void (*lpFunction) ( ); /* 定义一个无参数、无返回类型的函数指针类型*/
/* 定义一个函数指针,指向 CPU 启动后所执行第一条指令的位置*/
lpFunction lpReset = (lpFunction)0xF000FFF0;
lpReset(); /* 调用函数 */
在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用:lpReset(),它实际上起
到了&软重启&的作用,跳转到 CPU 启动后第一条要执行的指令的位置。
记住: 函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换
一个地址开始执行指令!
数组 vs.动态申请
在嵌入式系统中动态内存申请存在比一般系统编程时更严格的要求,这是因为嵌入式系统的内存空间往往是十分有限
的,不经意的内存泄露会很快导致系统的崩溃。
所以一定要保证你的 malloc 和 free 成对出现,如果你写出这样的一段程序:
char * function(void)
p = (char *)malloc(…);
if(p==NULL)
… /* 一系列针对 p 的操作 */
在某处调用 function(),用完 function 中动态申请的内存后将其 free,如下:
char *q = function();
上述代码明显是不合理的,因为违反了 malloc 和 free 成对出现的原则,即&谁申请,就由谁释放&原则。不满足这个
原则,会导致代码的耦合度增大,因为用户在调用 function 函数时需要知道其内部细节!
正确的做法是在调用处申请内存,并传入 function 函数,如下:
char *p=malloc(…);
if(p==NULL)
function(p);
而函数 function 则接收参数 p,如下:
void function(char *p)
… /* 一系列针对 p 的操作 */
基本上,动态申请内存方式可以用较大的数组替换。对于编程新手,笔者推荐你尽量采用数组!嵌入式系统可以以博
大的胸襟接收瑕疵,而无法&海纳&错误。毕竟,以最笨的方式苦练神功的郭靖胜过机智聪明却范政治错误走反革命道路的
给出原则:
(1)尽可能的选用数组,数组不能越界访问(真理越过一步就是谬误,数组越过界限就光荣地成全了一个混乱的嵌
入式系统);
(2)如果使用动态申请,则申请后一定要判断是否申请成功了,并且 malloc 和 free 应成对出现!
关键字 const
const 意味着&只读&。区别如下代码的功能非常重要,也是老生长叹,如果你还不知道它们的区别,而且已经在程序
界摸爬滚打多年,那只能说这是一个悲哀:
const int *a;
int const *
(1)关键字 const 的作用是为给读你代码的人传达非常有用的信息。例如,在函数的形参前添加 const 关键字意味着
这个参数在函数体内不会被修改,属于&输入参数&。在有多个形参的时候,函数的调用者可以凭借参数前是否有 const 关
键字,清晰的辨别哪些是输入参数,哪些是可能的输出参数。
(2)合理地使用关键字 const 可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改,这样
可以减少 bug 的出现。
const 在 C++语言中则包含了更丰富的含义,而在 C 语言中仅意味着:&只能读的普通变量&,可以称其为&不能改变的
变量&(这个说法似乎很拗口,但却最准确的表达了 C 语言中 const 的本质),在编译阶段需要的常数仍然只能以#define
宏定义!故在 C 语言中如下程序是非法的:
const int SIZE = 10;
char a[SIZE]; /* 非法:编译阶段不能用到变量 */
关键字 volatile
C 语言编译器会对用户书写的代码进行优化,譬如如下代码:
int a,b,c;
a = in(0x100); /*读取 I/O 空间 0x100 端口的内容存入 a 变量*/
a = inWord (0x100); /*再次读取 I/O 空间 0x100 端口的内容存入 a 变量*/
很可能被编译器优化为:
int a,b,c;
a = inWord(0x100); /*读取 I/O 空间 0x100 端口的内容存入 a 变量*/
但是这样的优化结果可能导致错误,如果 I/O 空间 0x100 端口的内容在执行第一次读操作后被其它程序写入新值,则
其实第 2 次读操作读出的内容与第一次不同,b 和 c 的值应该不同。 在变量 a 的定义前加上 volatile 关键字可以防止编译
器的类似优化,正确的做法是:
volatile int a;
volatile 变量可能用于如下几种情况:
(1) 并行设备的硬件寄存器(如:状态寄存器,例中的代码属于此类);
(2) 一个中断服务子程序中会访问到的非自动变量(也就是全局变量);
(3) 多线程应用中被几个任务共享的变量。
CPU 字长与存储器位宽不一致处理
在背景篇中提到,本文特意选择了一个与 CPU 字长不一致的存储芯片,就是为了进行本节的讨论,解决 CPU 字长与存
储器位宽不一致的情况。80186 的字长为 16,而 NVRAM 的位宽为 8,在这种情况下,我们需要为 NVRAM 提供读写字节、字
的接口,如下:
typedef unsigned char BYTE;
typedef unsigned int WORD;
/* 函数功能:读 NVRAM 中字节
* 参数:wOffset,读取位置相对 NVRAM 基地址的偏移
* 返回:读取到的字节值
extern BYTE ReadByteNVRAM(WORD wOffset)
LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 为什么偏移要×2? */
return *lpA
/* 函数功能:读 NVRAM 中字
* 参数:wOffset,读取位置相对 NVRAM 基地址的偏移
* 返回:读取到的字
extern WORD ReadWordNVRAM(WORD wOffset)
WORD wTmp = 0;
LPBYTE lpA
/* 读取高位字节 */
lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 为什么偏移要×2? */
wTmp += (*lpAddr)*256;
/* 读取低位字节 */
lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* 为什么偏移要×2? */
wTmp += *lpA
/* 函数功能:向 NVRAM 中写一个字节
*参数:wOffset,写入位置相对 NVRAM 基地址的偏移
* byData,欲写入的字节
extern void WriteByteNVRAM(WORD wOffset, BYTE byData)
/* 函数功能:向 NVRAM 中写一个字 */
*参数:wOffset,写入位置相对 NVRAM 基地址的偏移
* wData,欲写入的字
extern void WriteWordNVRAM(WORD wOffset, WORD wData)
子贡问曰:Why 偏移要乘以 2?
子曰:请看图 1,16 位 80186 与 8 位 NVRAM 之间互连只能以地址线 A1 对其 A0,CPU 本身的 A0 与 NVRAM 不连接。因此,
NVRAM 的地址只能是偶数地址,故每次以 0x10 为单位前进!
图 1 CPU 与 NVRAM 地址线连接
子贡再问:So why 80186 的地址线 A0 不与 NVRAM 的 A0 连接?
子曰:请看《IT 论语》之《微机原理篇》,那里面讲述了关于计算机组成的圣人之道。
本篇主要讲述了嵌入式系统 C 编程中内存操作的相关技巧。掌握并深入理解关于数据指针、函数指针、动态申请内存、
const 及 volatile 关键字等的相关知识, 是一个优秀的 C 语言程序设计师的基本要求。 当我们已经牢固掌握了上述技巧后,
我们就已经学会了 C 语言的 99%,因为 C 语言最精华的内涵皆在内存操作中体现。
我们之所以在嵌入式系统中使用 C 语言进行程序设计,99%是因为其强大的内存操作能力!
如果你爱编程,请你爱 C 语言;
如果你爱 C 语言,请你爱指针;
如果你爱指针,请你爱指针的指针!
C 语言嵌入式系统编程修炼之四:屏幕操作
作者:宋宝华 更新日期:
现在要解决的问题是,嵌入式系统中经常要使用的并非是完整的汉字库,往往只是需要提供数量有限的汉字供必要的
显示功能。例如,一个微波炉的 LCD 上没有必要提供显示&电子邮件&的功能;一个提供汉字显示功能的空调的 LCD 上不需
要显示一条&短消息&,诸如此类。但是一部手机、小灵通则通常需要包括较完整的汉字库。
如果包括的汉字库较完整,那么,由内码计算出汉字字模在库中的偏移是十分简单的:汉字库是按照区位的顺序排列
的,前一个字节为该汉字的区号,后一个字节为该字的位号。每一个区记录 94 个汉字,位号则为该字在该区中的位置。因
此,汉字在汉字库中的具体位置计算公式为:94*(区号-1)+位号-1。减 1 是因为数组是以 0 为开始而区号位号是以 1 为开
始的。只需乘上一个汉字字模占用的字节数即可,即:(94*(区号-1)+位号-1)*一个汉字字模占用字节数,以 16*16 点阵字
库为例,计算公式则为:(94*(区号-1)+(位号-1))*32。汉字库中从该位置起的 32 字节信息记录了该字的字模信息。
对于包含较完整汉字库的系统而言,我们可以以上述规则计算字模的位置。但是如果仅仅是提供少量汉字呢?譬如几
十至几百个?最好的做法是:
# define EX_FONT_CHAR(value)
# define EX_FONT_UNICODE_VAL(value) (value),
# define EX_FONT_ANSI_VAL(value) (value),
定义结构体:
typedef struct _wide_unicode_font16x16
WORD /* 内码 */
BYTE data[32]; /* 字模点阵 */
#define CHINESE_CHAR_NUM … /* 汉字数量 */
字模的存储用数组:
Unicode chinese[CHINESE_CHAR_NUM] =
EX_FONT_CHAR(&业&)
EX_FONT_UNICODE_VAL(0x4e1a)
{0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0x44, 0x46, 0x24, 0x4c, 0x24, 0x48, 0x14, 0x50,
0x1c, 0x50, 0x14, 0x60, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00}
EX_FONT_CHAR(&中&)
EX_FONT_UNICODE_VAL(0x4e2d)
{0x01, 0x00, 0x01, 0x00, 0x21, 0x08, 0x3f, 0xfc, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08,
0x21, 0x08,
0x3f, 0xf8, 0x21, 0x08, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00}
EX_FONT_CHAR(&云&)
EX_FONT_UNICODE_VAL(0x4e91)
{0x00, 0x00, 0x00, 0x30, 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xff, 0xfe, 0x03, 0x00,
0x07, 0x00,
0x06, 0x40, 0x0c, 0x20, 0x18, 0x10, 0x31, 0xf8, 0x7f, 0x0c, 0x20, 0x08, 0x00, 0x00}
EX_FONT_CHAR(&件&)
EX_FONT_UNICODE_VAL(0x4ef6)
{0x10, 0x40, 0x1a, 0x40, 0x13, 0x40, 0x32, 0x40, 0x23, 0xfc, 0x64, 0x40, 0xa4, 0x40, 0x28, 0x40,
0x2f, 0xfe,
0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40}
要显示特定汉字的时候,只需要从数组中查找内码与要求汉字内码相同的即可获得字模。如果前面的汉字在数组中以
内码大小顺序排列,那么可以以二分查找法更高效的查找到汉字的字模。
这是一种很有效的组织小汉字库的方法,它可以保证程序有很好的结构。
系统时间显示
从 NVRAM 中可以读取系统的时间,系统一般借助 NVRAM 产生的秒中断每秒读取一次当前时间并在 LCD 上显示。关于时
间的显示,有一个效率问题。因为时间有其特殊性,那就是 60 秒才有一次分钟的变化,60 分钟才有一次小时变化,如果
我们每次都将读取的时间在屏幕上完全重新刷新一次,则浪费了大量的系统时间。
一个较好的办法是我们在时间显示函数中以静态变量分别存储小时、分钟、秒,只有在其内容发生变化的时候才更新
extern void DisplayTime(…)
static BYTE byHour,byMinute,byS
BYTE byNewHour, byNewMinute, byNewS
byNewHour = GetSysHour();
byNewMinute = GetSysMinute();
byNewSecond = GetSysSecond();
if(byNewHour!= byHour)
… /* 显示小时 */
byHour = byNewH
if(byNewMinute!= byMinute)
… /* 显示分钟 */
byMinute = byNewM
if(byNewSecond!= bySecond)
… /* 显示秒钟 */
bySecond = byNewS
这个例子也可以顺便作为 C 语言中 static 关键字强大威力的证明。当然,在 C++语言里,static 具有了更加强大的威
力,它使得某些数据和函数脱离&对象&而成为&类&的一部分,正是它的这一特点,成就了软件的无数优秀设计。
动画是无所谓有,无所谓无的,静止的画面走的路多了,也就成了动画。随着时间的变更,在屏幕上显示不同的静止
画面,即是动画之本质。所以,在一个嵌入式系统的 LCD 上欲显示动画,必须借助定时器。没有硬件或软件定时器的世界
是无法想像的:
(1) 没有定时器,一个操作系统将无法进行时间片的轮转,于是无法进行多任务的调度,于是便不再成其为一个多
任务操作系统;
(2) 没有定时器,一个多媒体播放软件将无法运作,因为它不知道何时应该切换到下一帧画面;
(3) 没有定时器,一个网络协议将无法运转,因为其无法获知何时包传输超时并重传之,无法在特定的时间完成特
定的任务。
因此,没有定时器将意味着没有操作系统、没有网络、没有多媒体,这将是怎样的黑暗?所以,合理并灵活地使用各
种定时器,是对一个软件人的最基本需求!
在 80186 为主芯片的嵌入式系统中,我们需要借助硬件定时器的中断来作为软件定时器,在中断发生后变更画面的显
示内容。在时间显示&xx:xx&中让冒号交替有无,每次秒中断发生后,需调用 ShowDot:
void ShowDot()
static BOOL bShowDot = TRUE; /* 再一次领略 static 关键字的威力 */
if(bShowDot)
showChar(’:’,xPos,yPos);
showChar(’ ’,xPos,yPos);
bShowDot = ! bShowD
无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在 C 语言中哪怕用到一丁点的面向对象思想,软
件结构将会有何等的改观!
笔者曾经是个笨蛋,被菜单搞晕了,给出这样的一个系统:
图 1 菜单范例
要求以键盘上的&← →&键切换菜单焦点,当用户在焦点处于某菜单时,若敲击键盘上的 OK、CANCEL 键则调用该焦点
菜单对应之处理函数。我曾经傻傻地这样做着:
/* 按下 OK 键 */
void onOkKey()
/* 判断在什么焦点菜单上按下 Ok 键,调用相应处理函数 */
Switch(currentFocus)
case MENU1:
menu1OnOk();
case MENU2:
menu2OnOk();
/* 按下 Cancel 键 */
void onCancelKey()
/* 判断在什么焦点菜单上按下 Cancel 键,调用相应处理函数 */
Switch(currentFocus)
case MENU1:
menu1OnCancel();
case MENU2:
menu2OnCancel();
终于有一天,我这样做了:
/* 将菜单的属性和操作&封装&在一起 */
typedef struct tagSysMenu
char * /* 菜单的文本 */
BYTE xP /* 菜单在 LCD 上的 x 坐标 */
BYTE yP /* 菜单在 LCD 上的 y 坐标 */
void (*onOkFun)(); /* 在该菜单上按下 ok 键的处理函数指针 */
void (*onCancelFun)(); /* 在该菜单上按下 cancel 键的处理函数指针 */
}SysMenu, *LPSysM
当我定义菜单时,只需要这样:
static SysMenu menu[MENU_NUM] =
&menu1&, 0, 48, menu1OnOk, menu1OnCancel
& menu2&, 7, 48, menu2OnOk, menu2OnCancel
& menu3&, 7, 48, menu3OnOk, menu3OnCancel
& menu4&, 7, 48, menu4OnOk, menu4OnCancel
OK 键和 CANCEL 键的处理变成:
/* 按下 OK 键 */
void onOkKey()
menu[currentFocusMenu].onOkFun();
/* 按下 Cancel 键 */
void onCancelKey()
menu[currentFocusMenu].onCancelFun();
程序被大大简化了,也开始具有很好的可扩展性!我们仅仅利用了面向对象中的封装思想,就让程序结构清晰,其结
果是几乎可以在无需修改程序的情况下在系统中添加更多的菜单,而系统的按键处理函数保持不变。
面向对象,真神了!
模拟 MessageBox 函数
MessageBox 函数,这个 Windows 编程中的超级猛料,不知道是多少入门者第一次用到的函数。还记得我们第一次在
Windows 中利用 MessageBox 输出 &Hello,World!&对话框时新奇的感觉吗?无法统计,这个世界上究竟有多少程序员学习
Windows 编程是从 MessageBox (&Hello,World!&,…)开始的。在我本科的学校,广泛流传着一个词汇,叫做&’Hello,World’
级程序员&,意指入门级程序员,但似乎&’Hello,World’级&这个说法更搞笑而形象。
图 2 经典的 Hello,World!
图 2 给出了两种永恒经典的 Hello,World 对话框,一种只具有&确定&,一种则包含&确定&、&取消&。是的,MessageBox
的确有,而且也应该有两类!这完全是由特定的应用需求决定的。
嵌入式系统中没有给我们提供 MessageBox,但是鉴于其功能强大,我们需要模拟之,一个模拟的 MessageBox 函数为:
/******************************************
/* 函数名称: MessageBox
/* 功能说明: 弹出式对话框,显示提醒用户的信息
/* 参数说明: lpStr --- 提醒用户的字符串输出信息
/* TYPE --- 输出格式(ID_OK = 0, ID_OKCANCEL = 1)
/* 返回值: 返回对话框接收的键值,只有两种 KEY_OK, KEY_CANCEL
/******************************************
typedef enum TYPE { ID_OK,ID_OKCANCEL }MSG_TYPE;
extern BYTE MessageBox(LPBYTE lpStr, BYTE TYPE)
BYTE keyValue = -1;
ClearScreen(); /* 清除屏幕 */
DisplayString(xPos,yPos,lpStr,TRUE); /* 显示字符串 */
/* 根据对话框类型决定是否显示确定、取消 */
switch (TYPE)
case ID_OK:
DisplayString(13,yPos+High+1, & 确定 &, 0);
case ID_OKCANCEL:
DisplayString(8, yPos+High+1, & 确定 &, 0);
DisplayString(17,yPos+High+1, & 取消 &, 0);
DrawRect(0, 0, 239, yPos+High+16+4); /* 绘制外框 */
/* MessageBox 是模式对话框,阻塞运行,等待按键 */
while( (keyValue != KEY_OK) || (keyValue != KEY_CANCEL) )
keyValue = getSysKey();
/* 返回按键类型 */
if(keyValue== KEY_OK)
return ID_OK;
return ID_CANCEL;
上述函数与我们平素在 VC++等中使用的 MessageBox 是何等的神似啊?实现这个函数,你会看到它在嵌入式系统中的
妙用是无穷的。
本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系统屏幕显示方面一些很巧妙的处理方法,灵活使用它们,
我们将不再被 LCD 上凌乱不堪的显示内容所困扰。
屏幕乃嵌入式系统生存之重要辅助,面目可憎之显示将另用户逃之夭夭。屏幕编程若处理不好,将是软件中最不系统、
最混乱的部分,笔者曾深受其害。
C 语言嵌入式系统编程修炼之五:键盘操作
作者:宋宝华 更新日期:
处理功能键
功能键的问题在于,用户界面并非固定的,用户功能键的选择将使屏幕画面处于不同的显示状态下。例如,主画面如
图 1 主画面
当用户在设置 XX 上按下 Enter 键之后,画面就切换到了设置 XX 的界面,如图 2:
图 2 切换到设置 XX 画面
程序如何判断用户处于哪一画面,并在该画面的程序状态下调用对应的功能键处理函数,而且保证良好的结构,是一
个值得思考的问题。
让我们来看看 WIN32 编程中用到的&窗口&概念,当消息(message)被发送给不同窗口的时候,该窗口的消息处理函数
(是一个 callback 函数)最终被调用, 而在该窗口的消息处理函数中, 又根据消息的类型调用了该窗口中的对应处理函数。
通过这种方式,WIN32 有效的组织了不同的窗口,并处理不同窗口情况下的消息。
我们从中学习到的就是:
(1)将不同的画面类比为 WIN32 中不同的窗口,将窗口中的各种元素(菜单、按钮等)包含在窗口之中;
(2)给各个画面提供一个功能键&消息&处理函数,该函数接收按键信息为参数;
(3)在各画面的功能键&消息&处理函数中,判断按键类型和当前焦点元素,并调用对应元素的按键处理函数。
/* 将窗口元素、消息处理函数封装在窗口中 */
struct windows
BYTE currentF
ELEMENT element[ELEMENT_NUM];
void (*messageFun) (BYTE keyValue);
/* 消息处理函数 */
void messageFunction(BYTE keyValue)
BYTE i = 0;
/* 获得焦点元素 */
while ( (element [i].ID!= currentFocus)&& (i & ELEMENT_NUM) )
i++;
/* &消息映射& */
if(i & ELEMENT_NUM)
switch(keyValue)
element[i].OnOk();
在窗口的消息处理函数中调用相应元素按键函数的过程类似于&消息映射&,这是我们从 WIN32 编程中学习到的。编程
到了一个境界,很多东西都是相通的了。其它地方的思想可以拿过来为我所用,是为编程中的&拿来主义&。
在这个例子中,如果我们还想玩得更大一点,我们可以借鉴 MFC 中处理 MESSAGE_MAP 的方法,我们也可以学习 MFC 定
义几个精妙的宏来实现&消息映射&。
处理数字键
用户输入数字时是一位一位输入的,每一位的输入都对应着屏幕上的一个显示位置(x 坐标,y 坐标)。此外,程序还
需要记录该位置输入的值,所以有效组织用户数字输入的最佳方式是定义一个结构体,将坐标和数值捆绑在一起:
/* 用户数字输入结构体 */
typedef struct tagInputNum
BYTE byN /* 接收用户输入赋值 */
BYTE xP /* 数字输入在屏幕上的显示位置 x 坐标 */
BYTE yP /* 数字输入在屏幕上的显示位置 y 坐标 */
}InputNum, *LPInputN
那么接收用户输入就可以定义一个结构体数组,用数组中的各位组成一个完整的数字:
InputNum inputElement[NUM_LENGTH]; /* 接收用户数字输入的数组 */
/* 数字按键处理函数 */
extern void onNumKey(BYTE num)
if(num==0|| num==1) /* 只接收二进制输入 */
/* 在屏幕上显示用户输入 */
DrawText(inputElement[currentElementInputPlace].xPos,
inputElement[currentElementInputPlace].yPos, &%1d&, num);
/* 将输入赋值给数组元素 */
inputElement[currentElementInputPlace].byNum =
/* 焦点及光标右移 */
moveToRight();
将数字每一位输入的坐标和输入值捆绑后,在数字键处理函数中就可以较有结构的组织程序,使程序显得很紧凑。
整理用户输入
继续第 2 节的例子,在第 2 节的 onNumKey 函数中, 只是获取了数字的每一位,因而我们需要将其转化为有效数据,譬
如要转化为有效的 XXX 数据,其方法是:
/* 从 2 进制数据位转化为有效数据:XXX */
void convertToXXX()
for (i = 0; i & NUM_LENGTH; i++)
XXX += inputElement[i].byNum*power(2, NUM_LENGTH - i - 1);
反之,我们也可能需要在屏幕上显示那些有效的数据位,因为我们也需要能够反向转化:
/* 从有效数据转化为 2 进制数据位:XXX */
void convertFromXXX()
for

我要回帖

更多关于 将结构体数组写入文件 的文章

 

随机推荐