C语⾔怎么进⾏编程⼤型项⽬,如何对⼀个⼤的项⽬进⾏模块
编程
当你在⼀个项⽬⼩组做⼀个相对较复杂的⼯程时,意味着你不再独⾃单⼲。你需要和你的⼩组成员分⼯合作,⼀起完成项⽬,这就要求⼩组成员各⾃负责⼀部分⼯程。⽐如你可能只是负责通讯或者显⽰这⼀块。这个时候,你就应该将⾃⼰的这⼀块程序写成⼀个模块,单独调试,留出接⼝供其它模块调⽤。最后,⼩组成员都将⾃⼰负责的模块写完并调试⽆误后,由项⽬组长进⾏组合调试。像这些场合就要求程序必须模块化。模块化的好处是很多的,不仅仅是便于分⼯,它还有助于程序的调试,有利于程序结构的划分,还能增加程序的可读性和可移植性。
初学者往往搞不懂如何模块化编程,其实它是简单易学,⽽且⼜是组织良好程序结构⾏之有效的⽅法之⼀.
本⽂将先⼤概讲⼀下模块化的⽅法和注意事项,最后将以初学者使⽤最⼴的keil c编译器为例,给出模块化编程的详细步骤。
模块化程序设计应该理解以下概述:
(1) 模块即是⼀个.c ⽂件和⼀个.h ⽂件的结合,头⽂件(.h)中是对于该模块接⼝的声明
这⼀条概括了模块化的实现⽅法和实质:将⼀个功能模块的代码单独编写成⼀个.c⽂件,然后把该模块的接⼝函数放在.h⽂件中.举例:假如你⽤到液晶显⽰,那么你可能会写⼀个液晶驱动模块,以实现字符、汉字和图像的现实,命名为: led_device.c,该模块的.c⽂件⼤体可以写成:
注:此处只写出这两个函数,第⼀个延时函数的作⽤范围是模块内,第⼆个,它是其它模块需要的。为了简化,此处并没有写出函数体.
.h⽂件中给出模块的接⼝.在上⾯的例⼦中,向LCD写⼊字符函数:wr_lcd (uchar dat_comm,uchar content)就是⼀个接⼝函数,因为其它模块会调⽤它,那么.h⽂件中就必须将这个函数声明为外部函数(使⽤extrun关键字修饰),另⼀个延时函数:void delay (uint us)只是在本模块中使⽤(本地函数,⽤static关键字修饰),因此它是不需要放到.h⽂件中的。
.h⽂件格式如下:
这⾥注意三点:
1. 在keil 编译器中,extern这个关键字即使不声明,编译器也不会报错,且程序运⾏良好,但不保证使⽤其它编译器也如此。强烈建议加上,养成良好的编程规范。
2. .c⽂件中的函数只有其它模块使⽤时才会出现在.h⽂件中,像本地延时函数static void delay (uint us)即使出现在.h⽂件中也是在做⽆⽤功,因为其它模块根本不去调⽤它,实际上也调⽤不了它(static关键字的限制作⽤)。
3.注意本句最后⼀定要加分号”;”,相信有不少同学遇到过这个奇怪的编译器报错: error C132: 'xxxx': not in formal parameter list,这个错误其实是.h的函数声明的最后少了分号的缘故。
模块的应⽤:假如需要在LCD菜单模块lcd_menu.c中使⽤液晶驱动模块lcd_device.c中的函数void wr_lcd (uchar dat_comm,uchar content),只需在LCD菜单模块的lcd_menu.c⽂件中加⼊液晶驱动模块的头⽂件lcd_device.h即可.
(2) 某模块提供给其它模块调⽤的外部函数及数据需在.h 中⽂件中冠以extern 关键字声明;
这句话在上⾯的例⼦中已经有体现,即某模块提供给其它模块调⽤的外部函数和全局变量需在.h 中⽂件中冠以extern 关键字声明,下⾯重点说⼀下全局变量的使⽤。使⽤模块化编程的⼀个难点(相对于新⼿)就是全局变量的设定,初学者往往很难想通模块与模块公⽤的变量是如何实现的,常规的做法就是本句提到的,在.h⽂件中外部数据冠以extern关键字声明。⽐如上例的变量value就是⼀个全局变量,若是某个模块也使⽤这个变量,则和使⽤外部函数⼀样,只需在使⽤的模块.c⽂件中包含#include“lcd_device.h”即可。
另⼀种处理模块间全局变量的⽅法来⾃于嵌⼊式操作系统uCOS-II,这个操作系统处理全局变量的⽅法⽐较特殊,也⽐较难以理解,但学会之后妙⽤⽆穷,这个⽅法只需⽤在头⽂件中定义⼀次。⽅法为:
在定义所有全局变量(uCOS-II将所有全局变量定义在⼀个.h⽂件内)的.h头⽂件中:
当编译器处理.C⽂件时,它强制xxx_EXT(在相应.H⽂件中可以到)为空,(因为xxx_GLOBALS已经定义)。所以编译器给每个全局变量分配内存空间,⽽当编译器处理其他.C ⽂件时,xxx_GLOBAL没有定义,xxx_EXT 被定义为extern,这样⽤户就可以调⽤外部全局变量。为了说明这个概念,可以参见uC/OS_II.H,其中包括以下定义:
同时,uCOS_II.H 有中以下定义:
#define OS_GLOBALS
#include “includes.h”
当编译器处理uCOS_II.C 时,它使得头⽂件变成如下所⽰,因为OS_EXT 被设置为空。
INT32U OSIdleCtr;
大文件发送
INT32U OSIdleCtrRun;
INT32U OSIdleCtrMax;
这样编译器就会将这些全局变量分配在内存中。当编译器处理其他.C ⽂件时,头⽂件变成了如下的样⼦,因为OS_GLOBAL没有定义,所以OS_EXT 被定义为extern。
extern INT32U OSIdleCtr;
extern INT32U OSIdleCtrRun;
extern INT32U OSIdleCtrMax;
在这种情况下,不产⽣内存分配,⽽任何 .C⽂件都可以使⽤这些变量。这样的就只需在 .H⽂件中定义⼀次就可以了。
(3) 模块内的函数和全局变量需在.c ⽂件开头冠以static 关键字声明;
这句话主要讲述了关键字static的作⽤。Static是⼀个相当重要的关键字,他能对函数和变量做⼀些约束,⽽且可以传递⼀些信息。⽐如上例在LCD驱动模块.c⽂件中定义的延时函数static void delay (uint us),这个函数冠以static修饰,⼀⽅⾯是限定了函数的作⽤范围只是在本模块中起作⽤,另⼀⽅⾯也给⼈传达这样的信息:该函数不会被其他模块调⽤。下⾯详细说⼀下这个关键字的作⽤,在C 语⾔中,关键字static 有三个明显的作⽤:
1.在函数体,⼀个被声明为静态的变量在这⼀函数被调⽤过程中维持其值不变。
2.在模块内(但在函数体外),⼀个被声明为静态的变量可以被模块内所⽤函数访问,但不能被模块外其它函数访问。它是⼀个本地的全局变
量。
3.在模块内,⼀个被声明为静态的函数只可被这⼀模块内的其它函数调⽤。那就是,这个函数被限制在声明它的模块的本地范围内使⽤。
前两个都⽐较容易理解,最后⼀个作⽤就是刚刚举例中提到的延时函数(static void delay (uint us)),本地化函数是有相当好的作⽤的。
(4) 永远不要在.h ⽂件中定义变量!
呵呵,似乎有点危⾔耸听的感觉,但我想也不会有多少⼈会在.h⽂件中定义变量的。
⽐较⼀下代码:
代码⼀:
以上程序的结果是在模块1、2、3 中都定义了整型变量a,a 在不同的模块中对应不同的地址元,这个世界上从来不需要这样的程序。正确的做法是:
代码⼆:
这样如果模块1、2、3 操作a 的话,对应的是同⼀⽚内存单元。
注:
⼀个嵌⼊式系统通常包括两类(注意是两类,不是两个)模块:
(1)硬件驱动模块,⼀种特定硬件对应⼀个模块;
(2)软件功能模块,其模块的划分应满⾜低偶合、⾼内聚的要求。
下⾯以keil C 编译器为例,讲⼀下模块化编程的步骤。
下⾯这个程序分为三层,共7个模块,共同为主程序服务(它们之间也会相互调⽤)。
程序的结构图如下所⽰:
序主要模块和功能简介:
⼀、底层驱动
1. 红外键盘:程序通过红外键盘进⾏操作。红外键盘独占定时器0和外部中断0,以实现红外解码和键盘键值的识别。红外键盘定义了五个按键,分别为上翻、下翻、左翻、右翻和确认键。
2. LCD液晶显⽰:程序主要通过LCD显⽰信息,LCD液晶显⽰驱动提供显⽰汉字、图形和ASCII码的函数接⼝。可以全屏、单⾏显⽰汉字,任意位置显⽰ASCII码,还可以全屏、半屏显⽰图形。
⼆、功能模块
1. LCD菜单程序:菜单程序可以使⼈机交互更加⽅便、容易。本菜单程序的菜单级别深度受RAM⼤⼩的限制,每增加⼀级菜单将多消耗4字节的RAM。菜单程序主要完成菜单功能函数的调度,LCD显⽰刷新。
2. 计算器程序:实现65536以内的加、减、乘、除,超出范围会出现溢出,溢出发⽣时,LCD显⽰“错误:出现溢出”的错误提⽰,同时本次运算被忽略。对于负数会显⽰“-”号,除数为零时LCD显⽰“错误:除数为零”的错误提⽰。
3. 开机次数记忆程序:主要对基于IIC总线的EEPROM进⾏读写,单⽚机每次上电后,将开机次数写⼊EEPROM.
4. 串⼝测试程序:进⼊该程序后,单⽚机向电脑发送字符串“Hello Word!”,发送数字24(以字符的形式显⽰)。编写此程序的⽬的是为了能够⽅便的向电脑发送字符串和变量,便于程序的调试。串⼝占⽤串⼝资源,与频率测量程序共享定时器1
5. 频率测量:复⽤定时器1,占⽤外部中断1,实现5~20KHZ频率的测量.
三、主程序
主程序主要完成程序的初始化,LCD菜单显⽰,监视键盘程序并根据键值更新菜单。
步骤为:
1.新建⼯程。
2.点击File—New(或者点击快捷图标:
),新建⼀个⽂档。
3.点击File—Save(或者点击快捷图标:
),保存新建的⽂档,在⽂件名后填写LCD_device.c(液晶驱动模块: LCD_device,提供显⽰汉字、字符和图像的接⼝),点击确定。
在该⽂档内编写LCD驱动程序。
4. 点击File—New(或者点击快捷图标:
),再新建⼀个⽂档。
5. 点击File—Save(或者点击快捷图标:
),保存新建的⽂档,在⽂件名后填写LCD_device.h(液晶驱动模块的头⽂件,模块的接⼝和全局变量在这⾥声明(感谢⽹友杨康佳指正这⾥的错误,原⽂将“声明”写成了“定义”,头⽂件⼀般⽤来声明变量和接⼝的))。点击确定。在该⽂档中整理全局变量和接⼝函数。以上
步骤之后的效果见下图: