C 语言嵌入式系统编程修炼之道
作者: 宋宝华
出处: 天极网
责任编辑: 方舟
不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势必要求其编程语
言具备较强的硬件直接操作能力。无疑,汇编语言具备这样的特质。但是,归因于汇编语言
开发过程的复杂性,它并不是嵌入式系统开发的一般选择。而与之相比,C语言--一种"高级
的低级"语言,则成为嵌入式系统开发的最佳选择。笔者在嵌入式系统项目的开发过程中,
一次又一次感受到C语言的精妙,沉醉于C语言给嵌入式开发带来的便利。
图 1 给出了本文的讨论所基于的硬件平台,实际上,这也是大多数嵌入式系统的硬件平
台。它包括两部分:
(1) 以通用处理器为中心的协议处理模块,用于网络控制协议的处理;
(2) 以数字信号处理器(DSP)为中心的信号处理模块,用于调制、解调和数/模信
号转换。
本文的讨论主要围绕以通用处理器为中心的协议处理模块进行,因为它更多地牵涉到具
体的C语言编程技巧。而DSP编程则重点关注具体的数字信号处理算法,主要涉及通信领域
的知识,不是本文的讨论重点。
着眼于讨论普遍的嵌入式系统C编程技巧,系统的协议处理模块没有选择特别的CPU,
而是选择了众所周知的CPU芯片--80186,每一位学习过《微机原理》的读者都应该对此芯
片有一个基本的认识,且对其指令集比较熟悉。80186 的字长是 16 位,可以寻址到的内存
空间为 1MB,只有实地址模式。C语言编译生成的指针为 32 位(双字),高 16 位为段地址,
低 16 位为段内编译,一段最多 64KB。
图 1 系统硬件架构
协议处理模块中的FLASH和RAM几乎是每个嵌入式系统的必备设备,前者用于存储程
序,后者则是程序运行时指令及数据的存放位置。系统所选择的FLASH和RAM的位宽都为
16 位,与CPU一致。
实时钟芯片可以为系统定时,给出当前的年、月、日及具体时间(小时、分、秒及毫秒),
可以设定其经过一段时间即向CPU提出中断或设定报警时间到来时向CPU提出中断(类似闹
钟功能)。
NVRAM(非易失去性RAM)具有掉电不丢失数据的特性,可以用于保存系统的设置信
息,譬如网络协议参数等。在系统掉电或重新启动后,仍然可以读取先前的设置信息。其位
宽为 8 位,比CPU字长小。文章特意选择一个与CPU字长不一致的存储芯片,为后文中一节
的讨论创造条件。
UART则完成CPU并行数据传输与RS-232 串行数据传输的转换,它可以在接收到
[1~MAX_BUFFER]字节后向CPU提出中断,MAX_BUFFER为UART芯片存储接收到字节的
最大缓冲区。
键盘控制器和显示控制器则完成系统人机界面的控制。
以上提供的是一个较完备的嵌入式系统硬件架构,实际的系统可能包含更少的外设。之
所以选择一个完备的系统,是为了后文更全面的讨论嵌入式系统C语言编程技巧的方方面
面,所有设备都会成为后文的
分析
定性数据统计分析pdf销售业绩分析模板建筑结构震害分析销售进度分析表京东商城竞争战略分析
目标。
嵌入式系统需要良好的软件开发环境的支持,由于嵌入式系统的目标机资源受限,不可
能在其上建立庞大、复杂的开发环境,因而其开发环境和目标运行环境相互分离。因此,嵌
入式应用软件的开发方式一般是,在宿主机(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语言作为一种结构化的程序
设计
领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计
语言,在模块的划分上主
要依据功能(依功能进行划分在面向对象设计中成为一个错误,牛顿定律遇到了相对论),
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*/
extern int a; /* 在模块 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语言完成。用户主程序最后都进入了一个死循
环,其首选
方案
气瓶 现场处置方案 .pdf气瓶 现场处置方案 .doc见习基地管理方案.doc关于群访事件的化解方案建筑工地扬尘治理专项方案下载
是:
while(1)
{
}
有的程序员这样写:
for(;;)
{
}
这个语法没有确切表达代码的含义,我们从 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 intType; /* 中断类型 */
struct tagIntQueue *next;
}IntQueue;
IntQueue lpIntQueueHead;
__interrupt ISRexample ()
{
int intType;
intType = GetSystemType();
QueueAddTail(lpIntQueueHead, intType);/* 在队列尾加入新的中断 */
}
在主程序循环中判断是否有中断:
While(1)
{
If( !IsIntQueueEmpty() )
{
intType = GetFirstInt();
switch(intType) /* 是不是很象 WIN32 程序的消息解析函数? */
{
/* 对,我们的中断类型解析很类似于消息驱动 */
case xxx: /* 我们称其为"中断驱动"吧? */
…
break;
case xxx:
…
break;
…
}
}
}
按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。
硬件驱动模块
一个硬件驱动模块通常应包括如下函数:
(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 _Isr; /* 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
#endif
C_Class A
{
C_Class A *A_this; /* this 指针 */
void (*Foo)(C_Class A *A_this); /* 行为:函数指针 */
int a; /* 数据 */
int b;
};
我们可以利用 C 语言模拟出面向对象的三个特性:封装、继承和多态,但是更多的时
候,我们只是需要将数据与行为封装以解决软件结构混乱的问题。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 value; /* 内码 */
BYTE data[32]; /* 字模点阵 */
}Unicode;
#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,bySecond;
BYTE byNewHour, byNewMinute, byNewSecond;
byNewHour = GetSysHour();
byNewMinute = GetSysMinute();
byNewSecond = GetSysSecond();
if(byNewHour!= byHour)
{
… /* 显示小时 */
byHour = byNewHour;
}
if(byNewMinute!= byMinute)
{
… /* 显示分钟 */
byMinute = byNewMinute;
}
if(byNewSecond!= bySecond)
{
… /* 显示秒钟 */
bySecond = byNewSecond;
}
}
这个例子也可以顺便作为 C 语言中 static 关键字强大威力的证明。当然,在 C++语言里,
static 具有了更加强大的威力,它使得某些数据和函数脱离"对象"而成为"类"的一部分,正是
它的这一特点,成就了软件的无数优秀设计。
动画显示
动画是无所谓有,无所谓无的,静止的画面走的路多了,也就成了动画。随着时间的变
更,在屏幕上显示不同的静止画面,即是动画之本质。所以,在一个嵌入式系统的 LCD 上
欲显示动画,必须借助定时器。没有硬件或软件定时器的世界是无法想像的:
(1) 没有定时器,一个操作系统将无法进行时间片的轮转,于是无法进行多任务的调
度,于是便不再成其为一个多任务操作系统;
(2) 没有定时器,一个多媒体播放软件将无法运作,因为它不知道何时应该切换到下
一帧画面;
(3) 没有定时器,一个网络协议将无法运转,因为其无法获知何时包传输超时并重传
之,无法在特定的时间完成特定的任务。
因此,没有定时器将意味着没有操作系统、没有网络、没有多媒体,这将是怎样的黑暗?
所以,合理并灵活地使用各种定时器,是对一个软件人的最基本需求!
在 80186 为主芯片的嵌入式系统中,我们需要借助硬件定时器的中断来作为软件定时
器,在中断发生后变更画面的显示内容。在时间显示"xx:xx"中让冒号交替有无,每次秒中
断发生后,需调用 ShowDot:
void ShowDot()
{
static BOOL bShowDot = TRUE; /* 再一次领略 static 关键字的威力 */
if(bShowDot)
{
showChar(’:’,xPos,yPos);
}
else
{
showChar(’ ’,xPos,yPos);
}
bShowDot = ! bShowDot;
}
菜单操作
无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在 C 语言中哪怕
用到一丁点的面向对象思想,软件结构将会有何等的改观!
笔者曾经是个笨蛋,被菜单搞晕了,给出这样的一个系统:
图 1 菜单范例
要求以键盘上的"�8 →"键切换菜单焦点,当用户在焦点处于某菜单时,若敲击键盘上
的 OK、CANCEL 键则调用该焦点菜单对应之处理函数。我曾经傻傻地这样做着:
/* 按下 OK 键 */
void onOkKey()
{
/* 判断在什么焦点菜单上按下 Ok 键,调用相应处理函数 */
Switch(currentFocus)
{
case MENU1:
menu1OnOk();
break;
case MENU2:
menu2OnOk();
break;
…
}
}
/* 按下 Cancel 键 */
void onCancelKey()
{
/* 判断在什么焦点菜单上按下 Cancel 键,调用相应处理函数 */
Switch(currentFocus)
{
case MENU1:
menu1OnCancel();
break;
case MENU2:
menu2OnCancel();
break;
…
}
}
终于有一天,我这样做了:
/* 将菜单的属性和操作"封装"在一起 */
typedef struct tagSysMenu
{
char *text; /* 菜单的文本 */
BYTE xPos; /* 菜单在 LCD 上的 x 坐标 */
BYTE yPos; /* 菜单在 LCD 上的 y 坐标 */
void (*onOkFun)(); /* 在该菜单上按下 ok 键的处理函数指针 */
void (*onCancelFun)(); /* 在该菜单上按下 cancel 键的处理函数指针 */
}SysMenu, *LPSysMenu;
当我定义菜单时,只需要这样:
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 --- 输出
格式
pdf格式笔记格式下载页码格式下载公文格式下载简报格式下载
(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(); /* 清除屏幕 */