资源描述
C语言嵌入式系统编程考前须知之背景篇本文的讨论主要围绕以通用处理港为中心的协议处理模块进行,因为它更多地牵 涉到具体的C语言编程技巧
不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势 必要求其编程语言具备较强的硬件直接操作能力。无疑,汇编语言具备这样的特 质。但是,归因于汇编语言开发过程的复杂性,它并不是嵌入式系统开发的一般 选择。而与之相比,c语言一一种〃高级的低级〃语言,那么成为嵌入式系统开发的 最正确选择。笔者在嵌入式系统工程的开发过程中,一次又一次感受到C语言的精 妙,沉醉于c语言给嵌入式开发带来的便利。
图1给出了本文的讨论所基于的硬件平台,实际上,这也是大多数嵌入式系 统的硬件平台。它包括两局部:
(1)以通用处理器为中心的协议处理模块,用于网络控制协议的处理;
(2)以数字信号处理器(DSP)为中心的信号处理模块,用于调制、解调 和数/模信号转换。
本文的讨论主要围绕以通用处理器为中心的协议处理模块进行,因为它更多 地牵涉到具体的C语言编程技巧。而DSP编程那么重点关注具体的数字信号处理算 法,主要涉及通信领域的知识,不是本文的讨论重点。
着眼于讨论普遍的嵌入式系统C编程技巧,系统的协议处理模块没有选择特 别的CPU,而是选择了众所周知的CPU芯片一80186,每一位学习过《微机原理》 的读者都应该对此芯片有一个基本的认识,且对其指令集比拟熟悉。80186的字 长是16位,可以寻址到的内存空间为1MB,只有实地址模式。C语言编译生成的 指针为32位(双字),高16位为段地址,低16位为段内编译,一段最多64KB。
协议处理模块中的FLASH和RAM儿乎是每个嵌入式系统的必备设备,前者用 于存储程序,后者那么是程序运行时指令及数据的存放位置。系统所选择的FLASH 和RAM的位宽都为16位,与CPU一致。
实时钟芯片可以为系统定时,给出当前的年、月、日及具体时间(小时、分、从宏观上给出了一个嵌入式系统软件所包含的主要元素。
请记住:软件结构是软件的灵魂!结构混乱的程序面目可憎,调试、测试、 维护、升级都极度困难。
C语言嵌入式系统编程考前须知之内存操作在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的 MOV指令,而除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;
*p=ll;
以上程序的意义为在绝对地址OxFOOOO+OxFFOO (80186使用16位段地址和 16位偏移地址)写入11。
在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的 数据类别。上例中p++后的结果是p二OxFOOOFFOl,假设p指向int,即:
int *p = (int *)OxFOOOFFOO;
p++(或++p)的结果等同于:p=p+sizeof(int),而p-(或-p)的结果是p二 p-sizeof(int)。
同理,假设执行:
long int *p = (long int *)OxFOOOFFOO;
那么p++(或++p)的结果等同于:p = p+sizeof (long int),而p-(或-p)的结 果是 p = p-sizeof (long int) o
记住:CPU以字节为单位编址,而C语言指针以指向的数据类型长度作自增 和自减。理解这一点对于以指针直接操作内存是相当重要的。
函数指针
首先要理解以下三个问题:
(1) C语言中函数名直接对应于函数生成的指令代码在内存中的地址,因 此函数名可以直接赋给指向函数的指针;
(2)调用函数实际上等同于〃调转指令+参数传递处理+回归位置入栈〃, 本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的PC寄存器;
(3)因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可 以〃调用〃一个根本就不存在的函数实体,晕?请往下看:
请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到,186 CPU启动后跳转至绝对地址OxFFFFO (对应C语言指针是OxFOOOFFFO, Oxl-'OOO 为段地址,OxFFFO为段内偏移)执行,请看下面的代码:
typedef void (*lp) ( ) ; /*定义一个无参数、无返回类型的*/
/*函数指针类型*/
Ip IpReset = (Ip)OxFOOOFFFO; /* 定义一个函数指针,指向*/
/* CPU启动后所执行第,条指令的位置*/
IpReset (); /* 调用函数 */
在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了 这样的函数调用:IpReset (),它实际上起到了〃软重启〃的作用,跳转到CPU启 动后第一条要执行的指令的位置。
记住:函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质 上只是换一个地址开始执行指令!
数组vs.动态申请
在嵌入式系统中动态内存申请存在比一般系统编程时更严格的要求,这是因 为嵌入式系统的内存空间往往是十分有限的,不经意的内存泄露会很快导致系统 的崩溃。
所以一定要保证你的malloc和free成对出现,如果你写出这样的一段程序:
char * (void)
(
char *p;
p = (char *)malloc(•,,);
if(p二二NULL)• • • • f
…/* 一系列针对p的操作*/
return p;在某处调用0,用完中动态申请的内存后将其free,如下:
上述代码明显是不合理的,因为违反了 mail。。和free成对出现的原那么,即 〃谁申请,就由谁释放〃原那么。不满足这个原那么,会导致代码的耦合度增大,因为用户在调用函数时需要知道其内部细节!
正确的做法是在调用处申请内存,并传入函数,如下:
char *p=malloc (•,,); if(p=NULL)
• • •・
(p);
• • •
free(p);
p=NULL;
而函数那么接收参数p,如下:
void (char *p)
(
…/* 一系列针对p的操作*/
基本上,动态申请内存方式可以用较大的数组替换。对于编程新手,笔者推 荐你尽量采用数组!嵌入式系统可以以博大的胸襟接收瑕疵,而无法〃海纳〃错误。 毕竟,以最笨的方式苦练神功的郭靖胜过机智聪明却范政治错误走反革命道路的 杨康。
给出原那么:
(1)尽可能的选用数组,数组不能越界访问(真理越过一步就是谬误,数 组越过界限就光荣地成全了一个混乱的嵌入式系统);
(2)如果使用动态申请,那么申请后一定要判断是否申请成功了,并且malloc 和free应成对出现!
在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的 MOV指令,而除C/C++以外的其它编程语言基本没有直接访问绝对■地址的能力
关键字const
const意味着〃只读〃。区别如下代码的功能非常重要,也是老生长叹,如果 你还不知道它们的区别,而且已经在程序界摸爬滚打多年,那只能说这是一个悲 哀:
const int a; int const a; const int *a; int * const a; int const * a 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 = inWord (Ox 100) ; /*读取I/O空间0x100端口的内容存入a变量*/ b = a;
a = inWord (0x100) ; /*再次读取1/0空间0x100端口的内容存入a变 量*/
c = a;
很可能被编译器优化为:
int a, b, c;
a = inWord (Ox 100) ; /*读取I/O空间0x100端口的内容存入a变量*/ b = a;
c = 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 IpAddr = (BYTE*) (NVRAM + wOffset * 2); /* 为什么偏移 要 X2? */
return *IpAddr;
/*函数功能:读NVRAM中字
*参数:wOffset,读取位置相对NVRAM基地址的偏移
*返回:读取到的字
*/
extern WORD ReadWordNVRAM(WORD wOffset) (
WORD wTmp = 0;
LPBYTE IpAddr;
/*读取高位字节*/
IpAddr = (BYTE*) (NVRAM + wOffset * 2); /* 为什么偏移要 X 2? */
wTmp += (*lpAddr)*256;
/*读取低位字节*/
IpAddr = (BYTE*) (NVRAM + (wOffset +1) * 2); /* 为什么偏移要
X2? */
wTmp += *IpAddr;
return wTmp;
}
/*函数功能:向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之间互连只能以地址线Al对其AO, CPU本身的AO与NVRAM不连接。因此,NVRAM的地址只能是偶数地址,故每 次以0x10为单位前进!
子贡再问: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()
# define EX_FONT_UNICODE_VAL () 0,
# define EX_FONT_ANSI_VAL()(),
定义结构体:
typedef struct _widc_unicode_fon116x16
(
WORD ; /* 内码 */
BYTE data [32]; /* 字模点阵 */
}Unicode;
^define CHINESE_CHAR_NUM ••• /* 汉字数量 */
字模的存储用数组:
Unicode Chinese[CHINESE CHAR NUM]二
EX_FONT_CHAR (〃业〃)
EX_FONT_UNICODE_VAL(0x4ela)
{0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0x44, 0x46, 0x24, 0x4c, 0x24, 0x48, 0x14, 0x50, Oxlc, 0x50, 0x14, 0x60, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, Oxff, Oxfe, 0x00, 0x00, 0x00, 0x00}
EX FONT_ CHAR (〃中〃)
EX_FONT_UNICODE_VAL(0x4e2d)
{0x01, 0x00, 0x01, 0x00, 0x21, 0x08, 0x3f, Oxfc, 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, Oxff, Oxfe, 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, Oxla, 0x40, 0x13, 0x40, 0x32, 0x40, 0x23, Oxfc, 0x64,
0x40, 0xa4, 0x40, 0x28, 0x40, 0x2f, Oxfe,
0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20,
0x40, 0x20, 0x40}
)
要显示特定汉字的时候,只需要从数组中查找内码与要求汉字内码相同的即 可获得字模。如果前面的汉字在数组中以内码大小顺序排列,那么可以以二分查 找法更高效的查找到汉字的字模。
这是一种很有效的组织小汉字库的方法,它可以保证程序有很好的结构。
系统时间显示
从NVRAM中可以读取系统的时间,系统一般借助NVRAM产生的秒中断每秒读 取一次当前时间并在LCD上显示。关于时间的显示,有一个效率问题。因为时间 有其特殊性,那就是60秒才有一次分钟的变化,60分钟才有一次小时变化,如 果我们每次都将读取的时间在屏幕上完全重新刷新一次,那么浪费了大量的系统时 间。
一个较好的方法是我们在时间显示函数中以静态变量分别存储小时、分钟、 秒,只有在其内容发生变化的时候才更新其显示。
秒及毫秒),可以设定其经过一段时间即向CPU提出中断或设定报警时间到来时 向CPU提出中断(类似闹钟功能)。
NVRAM (非易失去性RAM)具有掉电不丧失数据的特性,可以用于保存系统 的设置信息,譬如网络协议参数等。在系统掉电或重新启动后,仍然可以读取先 前的设置信息。其位宽为8位,比CPU字长小。文章特意选择一个与CPU字长不 一致的存储芯片,为后文中一节的讨论创造条件。
UART那么完成CPU并行数据传输与RS-232串行数据传输的转换,它可以在接 收到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空间的内容。
后续章节将从软件架构、内存操作、屏幕操作、键盘操作、性能优化等多方 面阐述C语言嵌入式系统的编程技巧。软件架构是一个宏观概念,与具体硬件的 联系不大;内存操作主要涉及系统中的FLASH、RAM和NVRAM芯片;屏幕操作那么 涉及显示控制器和实时钟;键盘操作主要涉及键盘控制器;性能优化那么给出一些
extern void DisplayTimc(•,,) (
static BYTE byHour,byMinute, bySecond;
BYTE byNewllour, byNewMinute, byNewSecond;
byNewHour = GetSysHour ();
byNewMinute = GetSysMinute();
byNewSecond = GetSysSecondO ;
if(byNewHour!= byHour)
{ _…/*显示小时*/
byHour = byNewHour;
)
i f(byNewMinute!= byMinute)
{…/*显ZK分钟*/
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 (J ,xPos,yPos);
)
bShowDot = ! bShowDot;
菜单操作
无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在C 语言中哪怕用到一丁点的面向对象思想,软件结构将会有何等的改观!
要求以键盘上的〃 〃键切换菜单焦点,当用户在焦点处于某菜单时,假设 敲击键盘上的OK、CANCEL键那么调用该焦点菜单对应之处理函数。我曾经傻傻地 这样做着:
/*按下OK键*/
void onOkKey()
(
/*判断在什么焦点菜单上按下Ok键,调用相应处理函数*/ Switch(currentFocus)
(case MENU1:
menulOnOk ();break;
case MENU2:
menu20n0k ();break;
• • ♦
)
)
/* 按下 Cancel 键 */
void onCancelKey()
(
/*判断在什么焦点菜单上按下Cancel键,调用相应处理函数*/ Switch(currentFocus)
(case MENU1:
menulOnCancel ();break;
case MENU2:
mcnu20nCanccl ();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[MENl_NUM]=
({, 〃menul〃, 0, 48, menulOnOk, menulOnCancel
)
(〃 menu2〃, 7, 48, menu20n0k, menu20nCancel
)
(“menu3,,> 7, 48, menu30n0k, menu30nCancel
)
{〃 menu,,7, 48, menu40n0k, menu40nCancel
}
• • •
);
OK键和CANCEL键的处理变成:
/*按下OK键*/
void onOkKey()
(
menu [currentFocusMenu]. onOkFunO ;
}
/* 按下 Cancel 键 */
void onCancelKey 0
(
menu[currentFocusMenu]. onCancelFunO ; )
程序被大大简化了,也开始具有很好的可扩展性!我们仅仅利用了面向对象 中的封装思想,就让程序结构清晰,其结果是几乎可以在无需修改程序的情况下 在系统中添加更多的菜单,而系统的按键处理函数保持不变。
面向对象,真神了!
模拟MessageBox函数
MessageBox函数,这个Windows编程中的超级猛料,不知道是多少入门者 第一次用到的函数。还记得我们第一次在Windows中利用MessageBox输出 “Hello, World!〃对话框时新奇的感觉吗?无法统计,这个世界上究竟有多少程序 员学习Windows编程是从MessageBox("Hello, World!”,…)开始的。在我本科的 学校,广泛流传着一个词汇,叫做〃'Hello, World'级程序员〃,意指入门级程 序员,但似乎〃'Hello, World'级〃这个说法更搞笑而形象。
嵌入式系统中没有给我们提供MessageBox,但是鉴于其功能强大,我们需 要模拟之,一个模拟的MessageBox函数为:
/WX
I *T**T**T* *T* *?**1^*T**T**T^*T**T^ *T* *7**T*
/* 函数名称:MessageBox
/*功能说明:弹出式对话框,显示提醒用户的信息
/*参数说明:IpStr — 提醒用户的字符串输出信息
/* TYPE --输出格式(ID OK = 0, ID OKCANCEL = 1)
/*返回值:返回对话框接收的键值,只看两种KEY_0K, KEY_CANCEL
!xlzviz*Az «2x
*T* *T* f *1* *Y* *T*,&、*Y* *T* *T**T* *T*。、。、*T* *T**Y* *T* *.**T* *T* *Y*。、。、*T*,.、*Y* f。、*Y*
typedef enum TYPE { ID_0K, ID_OKCANCEL }MSG_TYPE; extern BYTE MessageBox?LPBYTE IpStr, BYTE TYPE) (
BYTE key = -1;
ClearScreenO ; /* 清除屏幕 */
DisplayString(xPos, yPos, IpStr, TRUE) ; /* 显示字符串 */ /*根据对话框类型决定是否显示确定、取消*/ switch (TYPE) (case ID_0K:
DisplayString(13, yPos+High+1, 〃 确定”,0); break;case ID_OKCANCEL:
DisplayString(8, yPos+High+1, 〃 确定”,0);DisplayString(17, yPos+High+1, 〃 取消 〃,0); break;
default: break;
)
DrawRect (0, 0, 239, yPos+High+16+4); /* 绘制外框 */ /* MessageBox是模式对话框,阻塞运行,等待按键*/ while( (key != KEY_OK) || (key != KEY_CANCEL)) (key = getSysKey 0;
)
/*返回按键类型*/ if(key== KEY_OK) (return ID OK;
) else {return ID CANCEL;
}
上述函数与我们平素在VC++等中使用的MessageBox是何等的神似啊?实现 这个函数,你会看到它在嵌入式系统中的妙用是无穷的。
总结
本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系统屏幕显示方面 一些很巧妙的处理方法,灵活使用它们,我们将不再被LCD上凌乱不堪的显示内 容所困扰。
屏幕乃嵌入式系统生存之重要辅助,面目可憎之显示将另用户逃之夭夭。屏 幕编程假设处理不好,将是软件中最不系统、最混乱的局部,笔者曾深受其害。
C语言嵌入式系统编程考前须知之键盘操作
处理功能键
让我们来看看WIN32编程中用到的〃窗口〃概念,当消息(message)被发送 给不同窗口的时候,该窗口的消息处理函数(是一个callback函数)最终被调 用,而在该窗口的消息处理函数中,乂根据消息的类型调用了该窗口中的对应处 理函数。通过这种方式,WIN32有效的组织了不同的窗口,并处理不同窗口情况 下的消息。
我们从中学习到的就是:
(1)将不同的画面类比为WIN32中不同的窗口,将窗口中的各种元素(菜 单、按钮等)包含在窗口之中;
(2)给各个画面提供一个功能键〃消息〃处理函数,该函数接收按键信息为 参数;
(3)在各画面的功能键〃消息〃处理函数中,判断按键类型和当前焦点元素, 并调用对应元素的按键处理函数。
/*将窗口元素、消息处理函数封装在窗口中*/
struct windows
(
BYTE currentFocus;
ELEMENT element[ELEMENT_NUM];
void (*messageFun) (BYTE key); • • •
};
/*消息处理函数*/
void message(BYTE key)
(
BYTE i = 0;
/*获得焦点元素*/
whi1e ( (element . TD!= currentFocus)&& (i < ELEMENT NUM))( 一 i++;
}
/* 〃消息映射〃*/
if(i < ELEMENT_NUM)
(switch(key)
(case OK:
element. OnOk ();break;
• • •)
)
在窗口的消息处理函数中调用相应元素按键函数的过程类似于〃消息映射〃, 这是我们从WIN32编程中学习至I」的。编程到了一个境界,很多东西都是相通的了。 其它地方的思想可以拿过来为我所用,是为编程中的〃拿来主义〃。
在这个例子中,如果我们还想玩得更大一点,我们可以借鉴MFC中处理MESSAGEJ1AP的方法,我们也可以学习MFC定义几个精妙的宏来实现〃消息映射〃。
处理数字键
用户输入数字时是一位一位输入的,每一位的输入都对应着屏幕上的一个显 示位置(x坐标,y坐标)。此外,程序还需要记录该位置输入的值,所以有效 组织用户数字输入的最正确方式是定义一个结构体,将坐标和数值捆绑在一起:
/*用户数字输入结构体*/
typedef struct taglnputNum
{
BYTE byNum; /*接收用户输入赋值*/
BYTE xPos; /*数字输入在屏幕上的显示位置x坐标*/
BYTE yPos; /*数字输入在屏幕上的显示位置y坐标*/
}InputNum, *LPInputNum;
那么接收用户输入就可以定义一个结构体数组,用数组中的各位组成一个完 整的数字:
InputNum inputE1 ementlNLNLLENGTlif; 7* 接收用户数字输入的数组
*/
/*数字按键处理函数*/
extern void onNumKey(BYTE num)
(
if (num==0 num—1) /*只接收二进制输入*/
(
/*在屏幕上显示用户输入*/
DrawText(inputElement[currentElementlnputPlace]. xPos, inputElement[currentElementlnputPiace]. yPos,num);
/*将输入赋值给数组元素*/
inputElement[currentElementlnputPlace]. byNum = num;
/*焦点及光标右移*/
moveToRight ();
将数字每一位输入的坐标和输入值捆绑后,在数字键处理函数中就可以较有 结构的组织程序,使程序显得很紧凑。
整理用户输入
继续第2节的例子,在第2节的onNumKey函数中,只是获取了数字的每一 位,因而我们需要将其转化为有效数据,譬如要转化为有效的XXX数据,其方法 是:
/*从2进制数据位转化为有效数据:XXX */ void convertToXXXO (
BYTE i;
XXX = 0;
for (i = 0; i < NUM^LENGTH; i++) {
XXX += inputElement. byNum*power(2, NUM LENGTH - i - 1); )
)
反之,我们也可能需要在屏幕上显示那些有效的数据位,因为我们也需要能 够反向转化:
/*从有效数据转化为2进制数据位:XXX */
void convertFromXXX() (
BYTE i;
XXX = 0;
for (i = 0; i < NUM LENGTH; i++) {
inputElement. byNum = XXX / power(2, NU
展开阅读全文