ARM嵌入式系统
嵌入式C语言
关键字
(1) register 与auto
MCU的寄存器读/写速度远远快于RAM内存,换句话来说,对于一个需要频繁读写的变量,将其放在寄存器里比放在RAM内存里的效率更高。为了提高效率,在C语言里,可以通过register关键字来声明变量,编译器尽可能的把变量放在寄存器里。
register int i;
而auto关键字是用来声明自动变量的,由于编译器自动优化,编译器一般默认为auto(一般情况下都省略auto)。
auto int i;
等效于int i;
使用register须注意以下几点:
- 1.register变量必须是能被CPU接受的类型,这通常意味着register变量必须是一个单一的值,并且长度应小于或等于整形的长度。但是,有些机器的寄存器也能存放浮点数。
- 2.register变量可能不存放在内存中,所以不能用取地址运算符
&
。 - 3.只有局部变量和形参可以作为register变量,全局变量不行。
- 4.静态变量不能定位为register.
本来寄存器的数目就不多,如果全局变量和静态变量可行的话,尽量少使用register,以为使用了register就意味着单片机运行全程都少了一个可供使用的寄存器。
(2) continue 、break 与 return
- continue:结束当前循环,开始下一轮循环。
- break:跳出当前循环。
- return:子程序返回语句,可以返回值或者不返还值。
(3) extern与 static
static可用于修饰变量和函数,其中,修饰的变量可分为局部变量和全局变量,它们都存在内存的静态区。
- 1.static修饰静态局部变量:出现在函数内部,生命周期是整个程序的执行过程,由于被static修饰的变量总是存在内存的静态区,即使该函数生命结束,其值也不会别销毁,同样要修改该变量的值,就要到函数内部完成,所以用起来比较安全,起到信息屏蔽的作用。
- 2.static修饰静态全局变量:函数前加static修饰会使函数成为静态函数。此处static的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称为内部函数)。使用内部函数的好处是:不同的人编写不同的函数时不同担心自己的定义的函数是否会与其他文件的函数重名。
- 3.extern声明外部定义:extern的作用是声明函数和变量在外部定义,提示编译器遇到此函数或者变量时在其他模块中寻找定义。值得注意的是:如果函数和变量定时时加了static修饰,那么即使用extern声明外部定义,也不能再其他模块中条用此函数或者变量。
(4)volatile 与 const
- volatile的作用准确来说是防止编译器对代码进行优化二导致没有执行指令或者执行错误。
例如,在模拟时序的时候通常需要对I/O引脚输出高、低电平。加入没有volatile声明,如下的伪代码
/*volatile*/ int *pPTA0_OUT = 0x400FF000u;
*pPTA0_OUT = 1;
*pPTA0_OUT = 0;
不加volatile声明,编译器会认为两次对0x400FF000u地址进行写入操作,而且两次写入之间没有读取该地址的数据,可认为第一次写入是无效的,则编译器就会忽略第一次写入的指令。因此,需要加入volatile来修饰,防止编译器对代码进行优化二忽略了这些指令。
一般来说volatile一般用在如下几个地方:
- ① 在中断服务程序中需要访问的全局变量。
- ② 多任务环境中需要被多个任务共享的变量。
- ③ 硬件寄存器(如:状态寄存器)
-
④ 软件延时程序
- const只读变量(是变量,而不是常量)。编译时,如果直接尝试修改只读变量,则编译器会提示报错,就能防止误修改。对于非指针变量的修饰,const的摆放位置可在数据类型前,也可以在数据类型后。二者是等效的。
const int a = 10;
int const a = 10;
对于指针变量的修饰,const的摆放位置在数据类型前和后的意思是不一样的。
int me;
const int *p1 = &me;//p1可变,*p1不可变。const修饰的是*p1;
int * const p2 = &me;//p2不可变,*p2可变。const修饰的是p2;
const int * const p3 = &me;//p3不可变,*p3也不可变。前者const修饰的是*p3,后面的const修饰的是p3;
前面说直接修改const修饰的变量,编译器会报错。但是通过指针的方式间接修改,则编译器只是提示警告而不会报错。
int main()
{
const int a = 10;
int *b = NULL;
b = &a;//此处编译器警告!
*b = 11;
printf("a = %d\n",a);
return 0;
}
(5)sizeof
sizeof是C/C++的操作符,作用就是返回一个对象或类型所占的内存字节数。
sizeof有2种语法形式:
- ①用于数据类型
sizeof (type_neme);//sizeof(类型); - ②用于变量
sizeof(object);//sizeof(对象);
sizeof是一个关键字,而不是函数。容易将sizeof和strlen搞混。strlen为是C语言提供的函数,用于计算有效字符串的长度(不包括\0
)。
(6)typedef
typedef用来为复杂的声明定义简单的别名,与宏定义有些差异。type的分为C语言的关键字,作用是为一种数据类型定义一个新的别名,目的是给数据类型一个易记且意义明确的新名字,或简化一些比较复杂的类型声明。例如把unsigned long类型重命名为uint32,那么程序员就容易知道uint32是32位的无符号整形,更容易记忆且意义明确。
unsigned long a;
//等效于:
typedef unsigned long uint32;
uint32 a;
指针与数组
C/C++程序中,指针和数组在不少语法上可以相互替换,但是二者之间还是有差异。
指针
指针是特殊的变量,其变量本身存储的值是其他变量的地址,即指向其他变量。不管指针指向任何数据类型变量,其占用的空间都是相同的,一般为4个字节。
指针的一般定义如下;
/*指向的数据类型 * 指针变量 = 指向的变量地震;*/
int * p = NULL;//定义一个指向int型的指针p;
指针定义时如果没有赋初值,那么指针就是野指针。所谓野指针不是NULL指针,而是指向被释放的或者访问受限内存的指针。定义指针变量没有赋初值,指针指向的地址是个随机值,也就是个野指针。
尽管指针可以用数组的形式来访问指向的变量,但指针就是指针不是数组,不会像数组那样开辟新的存储空间,需要自行malloc或者new来申请内存空间,或者指向已有的内存空间。
指针数组与数组指针
- 指针数组:首先他是一个数组,数组的元素都是指针,数组占多少字节由数组本身。他是“存储指针的数组”的简称。
int * p[10];//指针数组
- 数组指针:首先他是一个指针,他指向一个数组。他是“指向数组的指针”的简称。
int (* p)[10];//数组指针
#操作符和##操作符
#转换为字符串
#操作符,这里值的不是用在宏定义#define 开头的#号,而是用在#define后面的#操作符。#操作符就是把宏定义转换为一个字符串。
#define MKSTR(str) #str
void main(void)
{
printf(MKSTR(dufaxing.com\n));//#操作符把dufaxing.com\n替换为“dufaxing.com\n”字符串
}
在嵌入式开发中,#操作符常用语输出调试信息。假定有变量reg,需要输出调试信息时,可以通过下面的方法来实现。输出调试信息时仅需要输入变量名,调试信息里也会包含变量名,从而更方便调试。
#define DEBUG_OUT(var) printf(#var" = %d\n",var)
void main()
{
int reg = 1;
DEBUG_OUT(reg);//展开后的结果为prinrf("reg"" = %d\n",reg),编译时会把两个双引号隔开的字符串当做一个字符串来处理,即等效于:prinrf("reg = %d\n",reg)
}
运行后输出结果reg = 1
##合并变量名
##操作符的用途是合并变量名,这是在嵌入式开发中常见的用法,一般用在寄存器的命名中。常见的I/O口命名都是PTA、PTB、PTC等,通过##操作符把ABC作为参数传递PT(X)的宏定义。
define PT(X) PT##X //PT(A)展开后为PTA
# define PT(X,n,REG) BITBAND_REG(PT##X##_BASE_PTR->##REG,n)
# define PTA0_OUT PT(A,0,PDOR)
调用PTA0_OUT后,代码会展开为BITBAND_REG(PTA_BASE_PTR->PDOR,O),从而避免输入太长代码。
- 注意:宏定义里有#和##操作符的地方都不会进行宏展开
编程思想
跳出滥用全局变量的陷阱
全局变量会增加程序的耦合性(应该尽量做到高内聚,低耦合),尽量避免使用全局变量,如果能用其他办法最好,如果实在没有办法,那就把全局变量定义为static,他没有强符号弱符号之分,而且不会和其他全局变量产生冲突。如果其他问题需要对他进行访问,可以封装成函数,通过函数调用的方式进行访问。
软件分层
- ①每一个软件层都应该由共同完成特定功能的函数或类组成,例如I2C,UART等特定功能。
- ②层与层之间应该是自上而下的依赖关系,下层应用不能依赖于上层应用,例如printf函数这个上层依赖于UART串口通信的底层应用,但如果底层应用也依赖于顶层应用,就会使层次结构混乱。
- ③每个软件层都对上次提供API接口,但隐藏具体的实现细节。对每个层次的内容进行修改时,仅仅改变其实现细节,不改变其API接口,从而不影响顶层的实现。