嵌入式C语言编程心得课件.docx
- 文档编号:25069928
- 上传时间:2023-06-04
- 格式:DOCX
- 页数:16
- 大小:24.48KB
嵌入式C语言编程心得课件.docx
《嵌入式C语言编程心得课件.docx》由会员分享,可在线阅读,更多相关《嵌入式C语言编程心得课件.docx(16页珍藏版)》请在冰豆网上搜索。
嵌入式C语言编程心得课件
一、.H文件与.C文件的关系:
迄今为止,写过的程序都是一些很简单的程序,从来没有想到要自己写.H文件,也不知道.H文件到底什么用,与.C文件什么关系。
只是最近写键盘程序,参考别人的程序时,发现别人写的严格的程序都带有一个“KEY.H”,里面定义了.C文件里用到的自己写的函数,如Keyhit()、Keyscan()等。
经过查找资料得知,.H文件就是头文件,估计就是Head的意思吧,这是规范程序结构化设计的需要,既可以实现大型程序的模块化,又可以实现根各模块的连接调试。
1、.H文件介绍:
在单片机C程序设计中,项目一般按功能模块化进行结构化设计。
将一个项目划分为多个功能,每个功能的相关程序放在一个C程序文档中,称之为一个模块,对应的文件名即为模块名。
一个模块通常由两个文档组成,一个为头文件*.h,对模块中的数据结构和函数原型进行描述;另一个则为C文件*.c,对数据实例或对象定义,以及函数算法具体实现。
2、.H文件的作用
作为项目设计,除了对项目总体功能进行详细描述外,就是对每个模块进行详细定义,也就是给出所有模块的头文件。
通常H头文件要定义模块中各函数的功能,以及输入和输出参数的要求。
模块的具体实现,由项目组成根据H文件进行设计、编程、调试完成。
为了保密和安全,模块实现后以可连接文件OBJ、或库文件LIB的方式提供给项目其他成员使用。
由于不用提供源程序文档,一方面可以公开发行,保证开发人员的所有权;另一方面可以防止别人有意或无意修改产生非一致性,造成版本混乱。
所以H头文件是项目的详细设计和团队工作划分的依据,也是对模块进行测试的功能说明。
要引用模块内的数据或算法,只要用包含include指定模块H头文件即可。
3、.H文件的基本组成
/*如下为键盘驱动的头文档*/
#ifndef_KEY_H_//防重复引用,如果没有定义过_KEY_H_,则编译下句
#define_KEY_H_//此符号唯一,表示只要引用过一次,即#include,则定义符号_KEY_H_
/////////////////////////////////////////////////////////////////
charkeyhit(void);//击键否
unsignedcharKeyscan(void);//取键值
/////////////////////////////////////////////////////////////////
#endif
.c文件是整个程序中的一个或几个函数组成,在别的.C文件里可以调用它,不只是在主函数中。
这样做可以增强程序的模块化,提高程序的可读性。
当编制好一个模块时你可以保存在一个工程下,文件名改为**.C。
这样在另一个.C文件中的文件头处只要你对它进行说明就可以调用它。
比如#includestring.c
那么你就可以在你说明了的.C文件中调用它,这样一个模块可以在很多处调用,使得编程复杂度降低。
程序编译时把你所说明的文件复制到你调用处程序就可以运行了。
.H文件是对单片机一些端口及一些常用的程序的库说明,
比如我们在程序中用到端口一即P1,我们能够用它是因为在库中已对它进行了说明,对P1赋予了一个端口地址,这样程序编译是才不会出错,不然会提示你UNDIFINED。
分开多个文件写就有很多的.c和.h文件了,当然要加上#include语句把其它的包含进来,分开多个文件写有利于管理,其实我觉得.c和.h区别不是很大,只是.h一般都是写定义、声明的东西,.c文件一般都写函数的具体实现
h文件就是头文件,一般进行声明、宏定义等。
比如要编写流水灯的程序,h文件这样写:
#ifndef_LED_H
#define_LED_H
defineLED_ONP1.1=1;
defineLED_OFFP1.1=0;
//还可以进行函数的声明
//voidLED_twikle(void);
#endif
在你的c文件中要加入#include"LED.h"
二、尽量使用宏定义#define
开始看别人的程序时,发现程序开头,在文件包含后面有很多#define语句,当时就想,搞这么多标示符替换来替换去的,麻不麻烦啊,完全没有理解这种写法的好处。
原来,用一个标示符表示常数,有利于以后的修改和维护,修改时只要在程序开头改一下,程序中所有用到的地方就全部修改,节省时间。
#defineKEYNUM65//按键数量,用于Keycode[KEYNUM]
#defineLINENUM8//键盘行数
#defineROWNUM8//键盘列数
注意的地方:
1、宏名一般用大写
2、宏定义不是C语句,结尾不加分号
三、不要乱定义变量类型
以前写程序,当需要一个新的变量时,不管函数内还是函数外的,直接在程序开头定义,虽然不是原则上的错误,但是很不可取的作法。
下面说一下,C语言中变量类型的有关概念:
从变量的作用范围来分,分为局部变量和全局变量:
1、全局变量:
是在函数外定义的变量,像我以前定义在程序开头的变量都是全局变量,这里我就犯了一个大忌,使用了过多的全局变量。
带来的问题有两个:
一是,全局变量在程序全部执行过程中都占用资源;二是,全局变量过多使程序的通用性变差,因为全局变量是模块间耦合的原因之一。
2、局部变量:
在函数内部定义的变量,只在函数内部有效。
从变量的变量值存在的时间分为两种:
1、静态存储变量:
程序运行期间分配固定的存储空间。
2、动态存储变量:
程序运行期间根据需要动态地分配存储空间。
具体又包括四种存储方式:
auto,static,register,extern
1、局部变量,不加说明默认为auto型,即动态存储,如果不赋初值,将是一个不确定的值。
而将局部变量定义为static型的话,则它的值在函数内是不变的,且初值默认为0。
staticunsignedcharsts;//按键状态变量
staticunsignedcharNowkeycode;//此时的键码
staticunsignedcharPrekeycode;//上一次的键码
staticunsignedcharKeydowntime;//矩形键盘按下去抖时间变量
staticunsignedcharKeyuptime;//矩形键盘释放去抖时间变量
staticunsignedcharOnoffdowntime;//关机键按下去抖时间变量
staticunsignedcharOnoffuptime;//关机键释放去抖时间变量
staticunsignedcharonoff_10ms;//判断关机键中断次数变量,累计150次大约为3S,因为前后进了两个10ms中断
2、全局变量,编译时分配为静态存储区,可以被本文件中的各个函数引用。
如果是多个文件的话,如果在一个文件中引用另外文件中的变量,在此文件中要用extern声明。
不过如果一个全局变量定义为static的话,就只能在此一个文件中使用。
static函数内部函数和外部函数
当一个源程序由多个源文件组成时,C语言根据函数能否被其它源文件中的函数调用,将函数分为内部函数和外部函数。
内部函数
(又称静态函数)
如果在一个源文件中定义的函数,只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用,这种函数称为内部函数。
定义一个内部函数,只需在函数类型前再加一个“static”关键字即可,如下所示:
static函数类型函数名(函数参数表){……}
关键字“static”,译成中文就是“静态的”,所以内部函数又称静态函数。
但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。
使用内部函数的好处是:
不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名,因为同名也没有关系。
外部函数
外部函数的定义:
在定义函数时,如果没有加关键字“static”,或冠以关键字“extern”,表示此函数是外部函数:
[extern]函数类型函数名(函数参数表){……}
调用外部函数时,需要对其进行说明:
[extern]函数类型函数名(参数类型表)[,函数名2(参数类型表2)……];
四、特殊关键字constvolatile的使用
1、const
const用于声明一个只读的变量
constunsignedchara=1;//定义a=1,编译器不允许修改a的值
作用:
保护不希望被修改的参数
constunsignedcharKey_code[KEYNUM]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,
0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,
0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,
0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,
0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,
0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,
0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,
0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,0x40,
0x41
};//键码
constunsignedcharLine_out[LINENUM]={0xFE,0xFD,0xFB,0xf7,0xEF,0xDF,0xBF,0x7F};//行输出编码
constunsignedcharRow_in[ROWNUM]={0xFE,0xFD,0xFB,0xf7,0xEF,0xDF,0xBF,0x7F};//列输入编码
2、volatile
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
staticinti=0;
intmain(void)
{
...
while
(1)
{
if(i)
dosomething();
}
}
/*Interruptserviceroutine.*/
voidISR_2(void)
{
i=1;
}
程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。
如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
五、C语言中extern的用法
extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
另外,extern也可用来进行链接指定。
extern变量
在一个源文件里定义了一个数组:
chara[6];在另外一个文件里用下列语句进行了声明:
externchar*a;请问,这样可以吗?
答案与分析:
1)、不可以,程序运行时会告诉你非法访问。
原因在于,指向类型T的指针并不等价于类型T的数组。
externchar*a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。
应该将声明改为externchara[]。
2)、例子分析如下,如果a[]="abcd",则外部变量a=0x12345678(数组的起始地址),而*a是重新定义了一个指针变量a的地址可能是0x87654321,直接使用*a是错误的.3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。
4)、extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。
函数
extern函数1常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?
答案与分析:
如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。
即下述两个函数声明没有明显的区别:
externintf();和intf();当然,这样的用处还是有的,就是在程序中取代include“*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。
extern函数2当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。
但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该如何解决?
答案与分析:
目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。
以避免这种错误。
宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。
extern“C”在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?
答案与分析:
C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern“C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。
下面是一个标准的写法:
//在.h文件的头上#ifdef__cplusplus#if__cplusplusextern"C"{#endif#endif/*__cplusplus*/……//.h文件结束的地方#ifdef__cplusplus#if__cplusplus}#endif#endif/*__cplusplus*/
六、c++中->是什么意思,如何使用
->是指针的指向运算符,通常与结构体一起使用。
具体使用方法可以参考如下程序:
#include
struct stu // 定义一个结构体
{
char name[10]; // 姓名
int num; // 学号
int age; // 年龄
};
void main()
{
struct stu *s; // 定义一个结构体指针
char str[]="ZhangLi";
s->name = str; // 对结构体中的成员变量name进行赋值
s->num = 2015120; // 对结构体中的成员变量num进行赋值
s->age = 18; // 对结构体中的成员变量age进行赋值
}
七、warning:
lastlineoffileendswithoutanewline怎么解决?
原因:
编译器要求有空行作为程序的结束
解决办法:
将光标移到提示告警的代码最后一行(有代码的那一行)然后按住del键,直到确定下面没有回车行,最后回车一下或多下即可。
八、__I、__O、__IOvolatile是什么?
怎么用?
__I、__O、__IO是什么意思?
这是ST库里面的宏定义,定义如下:
#define__Ivolatileconst/*!
#define__Ovolatile/*! #define__IOvolatile/*! 显然,这三个宏定义都是用来替换成volatile和const的,所以我们先要了解这两个关键字的作用: volatile 简单的说,就是不让编译器进行优化,即每次读取或者修改值的时候,都必须重新从内存或者寄存器中读取或者修改。 一般说来,volatile用在如下的几个地方: 1、中断服务程序中修改的供其它程序检测的变量需要加volatile; 2、多任务环境下各任务间共享的标志应该加volatile; 3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义; 我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。 搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。 不懂得volatile的内容将会带来灾难。 假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。 1)一个参数既可以是const还可以是volatile吗? 解释为什么。 2);一个指针可以是volatile吗? 解释为什么。 3);下面的函数有什么错误: intsquare(volatileint*ptr) { return*ptr**ptr; } 1)是的。 一个例子是只读的状态寄存器。 它是volatile因为它可能被意想不到地改变。 它是const因为程序不应该试图去修改它。 2);是的。 尽管这并不很常见。 一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。 3)这段代码有点变态。 这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码: intsquare(volatileint*ptr) { inta,b; a=*ptr; b=*ptr; returna*b; } 由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。 结果,这段代码可能返不是你所期望的平方值! 正确的代码如下: longsquare(volatileint*ptr) { inta; a=*ptr; returna*a; } const 只读变量,即变量保存在只读静态存储区。 编译时,如何尝试修改只读变量,则编译器提示出错,就能防止误修改。 const与define 两者都可以用来定义常量,但是const定义时,定义了常量的类型,所以更精确一些(其实const定义的是只读变量,而不是常量)。 #define只是简单的文本替换,除了可以定义常量外,还可以用来定义一些简单的函数,有点类似内置函数。 const和define定义的常量可以放在头文件里面。 (小注: 可以多次声明,但只能定义一次) const与指针 intme; constint*p1=&me;//p1可变,*p1不可变const修饰的是*p1,即*p1不可变 int*constp2=&me;//p2不可变,*p2可变const修饰的是p2,即p2不可变 constint*constp3=&me;//p3不可变,*p3也不可变前一个const修饰的是*p3,后一个const修饰的是p3,两者都不可变 前面介绍了volatile和const的用法,不知道大家了解了没? 了解了后,下面的讲解就更加容易了: __I: 输入口。 既然是输入,那么寄存器的值就随时会外部修改,那就不能进行优化,每次都要重新从寄存器中读取。 也不能写,即只读,不然就不是输入而是输出了。 __O: 输出口,也不能进行优化,不然你连续两次输出相同值,编译器认为没改变,就忽略了后面那一次输出,假如外部在两次输出中间修改了值,那就影响输出 __IO: 输入输出口,同上 为什么加下划线? 原因是: 避免命名冲突 一般宏定义都是大写,但因为这里的字母比较少,所以再添加下划线来区分。 这样一般都可以避免命名冲突问题,因为很少人这样命名,这样命名的人肯定知道这些是有什么用的。 经常写大工程时,都会发现老是命名冲突,要不是全局变量冲突,要不就是宏定义冲突,所以我们要尽量避免这些问题,不然出问题了都不知道问题在哪里。 九、C语言中关于枚举类型 1.enum枚举的定义 枚举类型定义的一般形式为: enum 枚举名{ 枚举值表 }; 在枚举值表中应罗列出所有可用值。 这些值也称为枚举元素。 例如: 该枚举名为weekday,枚举值共有7个,即一周中的七天。 凡被说明为weekday类型变量的取值只能是七天中的某一天。 2.枚举变量的说明 如同结构和联合一样,枚举变量也可用不同的方式说明,即先定义后说明,同时定义说明或直接说明。 设有变量a,b,c被说明为上述的weekday,可采用下述任一种方式: enum weekday{ sun,mou,tue,wed,thu,fri,sat }; enum weekday a,b,c; 或者为: enum weekday{ sun,mou,tue,wed,thu,fri,sat }a,b,c; 或者为: enum { sun,mou,tue,wed,thu,fri,sat }a,b,c; 3、枚举类型变量的赋值和使用 枚举类型在使用中有以下规定: a.枚举值是常量,不是变量。 不能在程序中用赋值语句再对它赋值。 例如对枚举weekday的元素再作以下赋值: sun=5; mon=2; sun=mon; 都是错误的。 b.枚举元素本身由系统定义了一个表示序号的数值,从0开始顺序定义为0,1,2…。 如在weekday中,sun值为0,mon值为1,…,sat值为6。 int main(){ enum weekday { sun,mon,tue,wed,thu,fri,sat } a,b,c; a=sun; b=mon; c=tue; printf("%d,%d,%d",a,b,c); return 0; } 说明: 只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量。 如: a=sum; b=mon; 是正确的。 而: a=0; b=1; 是错误的。 如一定要把数值赋予枚举变量,则必须用强制类型转换。 如: a=(enumweekday)2; 其意义是将顺序号为2的枚举元素赋予枚举变量a,相当于: a=tue; 还应该说明的是枚举元素不是字符常量也不是字符串常量,使用时不要加单、双引号。 int main(){ enum b
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 嵌入式 语言 编程 心得 课件