当前位置: 代码迷 >> 综合 >> 第九次培训任务:学习嵌入式C
  详细解决方案

第九次培训任务:学习嵌入式C

热度:73   发布时间:2024-02-13 08:26:30.0

相关博客:关于嵌入式开发的C语言基础总结

C语言-嵌入式开发中的C语言(新手入门向):在工程下,c文件(.c)和头文件(.h)的关系就像是内容和目录。
收藏了一些嵌入式C的博客。

位运算符

位操作

1.位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:

p q p & q p | q p ^ q
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1

&与运算:全是1才是1

|或运算:全是0才是0

^异或运算:一样是零,不一样是1

~取反运算:

~1=0;   
~0=1;
<< 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 A 的值为 60,变量 B 的值为 13,A << 2 将得到 240,即为 1111 0000
>> 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 A 的值为 60,变量 B 的值为 13,A >> 2 将得到 15,即为 0000 1111

2.位操作的作用:

2.1不改变其他位的值的状况下,对某几个位进行设值

方法:先对需要设置的位用&操作符(对应位&0)进行清零操作,然后用|操作符设值(对应位|你想要设定的值)。

举例:改变 GPIOA-> BSRRL 的状态  

GPIOA-> BSRRL &=0XFF0F;  //将第 4-7 位清 0
GPIOA-> BSRRL |=0X0040; //设置相应位的值,不改变其他位的值

2.2移位操作提高代码的可读性

位操作在单片机开发中也非常重要,我们来看看下面一行代码

    GPIOx->ODR |= (((uint32_t)0x01) << pinpos);

2.3~取反操作使用技巧

  SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时其他位都保留为 1,简单的作法是直接给寄存器设置一个值:

        TIMx->SR=0xFFF7;

这样的作法设置第 3 位为 0,但是这样的作法同样不好看,并且可读性很差。看看库函数代码中怎样使用的:

        TIMx->SR &= (uint16_t)~TIM_FLAG;

而 TIM_FLAG 是通过宏定义定义的值:

    #define TIM_FLAG  ((uint16_t)0x0001)

 看这个应该很容易明白,可以直接从宏定义中看出 TIM_FLAG_ 就是设置的第 0位了,可读性非常强。

具体看 菜鸟教程-C运算符

实例


#include <stdio.h>int main()
{unsigned int a = 60;    /* 60 = 0011 1100 */  unsigned int b = 13;    /* 13 = 0000 1101 */int c = 0;           c = a & b;       /* 12 = 0000 1100 */ printf("Line 1 - c 的值是 %d\n", c );c = a | b;       /* 61 = 0011 1101 */printf("Line 2 - c 的值是 %d\n", c );c = a ^ b;       /* 49 = 0011 0001 */printf("Line 3 - c 的值是 %d\n", c );c = ~a;          /*-61 = 1100 0011 */printf("Line 4 - c 的值是 %d\n", c );c = a << 2;     /* 240 = 1111 0000 */printf("Line 5 - c 的值是 %d\n", c );c = a >> 2;     /* 15 = 0000 1111 */printf("Line 6 - c 的值是 %d\n", c );
}

当上面的代码被编译和执行时,它会产生下列结果:

Line 1 - c 的值是 12
Line 2 - c 的值是 61
Line 3 - c 的值是 49
Line 4 - c 的值是 -61
Line 5 - c 的值是 240
Line 6 - c 的值是 15

define宏定义

参考C语言中宏定义的使用

1.基本语法

宏定义是由源程序中的宏定义命令完成的。在 C 语言中,宏分为有参数和无参数两种。

// 不带参数的宏定义
#define MAX 10/*带参宏定义*/
#define M(y) y*y+3*y/*宏调用*/
k=M(5);
// 第一阶段/*
对象宏
*/#define M_PI 3.14159265358979323846264338327950288/*
函数宏
*/
#define PLUS(x,y) x + y

常见的格式 : #define  标识符  字符串

例如:#define  PLL_M  8  定义标识符 PLL_M 的值为 8。

2.进阶理解

2.1.宏定义的优点

2.1.1.方便程序的修改
使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。
相对于==全局变量==两者的区别如下:
1. 宏定义在编译期间即会使用并替换,而全局变量要到运行时才可以。
2. 宏定义的只是一段字符,在编译的时候被替换到引用的位置。在运行中是没有宏定义的概念的。而变量在运行时要为其分配内存。
3. 宏定义不可以被赋值,即其值一旦定义不可修改,而变量在运行过程中可以被修改。
4. 宏定义只有在定义所在文件,或引用所在文件的其它文件中使用。 而全局变量可以在工程所有文件中使用,只要再使用前加一个声明就可以了。换句话说,宏定义不需要extern。


2.1.2.提高程序的运行效率

使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生==函数调用 #800023==时,需要保留调用函数的现场,以便子函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。


2.2.宏定义的缺点

  1. 由于是直接嵌入的,所以代码可能相对多一点;
  2. 嵌套定义过多可能会影响程序的可读性,而且很容易出错,不容易调试。
  3. 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。

2.3.宏还是函数

宏函数,函数比较

==从时间上来看 #0c2ac0==

  1. 宏只占编译时间,函数调用则占用运行时间(分配单元,保存现场,值传递,返回),每次执行都要载入,所以执行相对宏会较慢。

  2. 使用宏次数多时,宏展开后源程序很长,因为每展开一次都使程序增长,但是执行起来比较快一点(这也不是绝对的,当有很多宏展开,目标文件很大,执行的时候运行时系统换页频繁,效率就会低下)。而函数调用不使源程序变长。

==从安全上来看 #0c2ac0==

  1. 函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。

  2. 函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。

  3. 对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。

  4. 宏的定义很容易产生二义性,如:定义==#define S(a) (a)*(a)==,代码==S(a++)==,宏展开变成==(a++)*(a++)==这个大家都知道,在不同编译环境下会有不同结果。

  5. 调用函数只可得到一个返回值,且有返回类型,而宏没有返回值和返回类型,但是用宏可以设法得到几个结果。

  6. 函数体内有Bug,可以在函数体内打断点调试。如果宏体内有Bug,那么在执行的时候是不能对宏调试的,即不能深入到宏内部。

  7. C++中宏不能访问对象的私有成员,但是成员函数就可以。


内联函数
在C99中引入了内联函数(==inline==),联函数和宏的区别在于,==宏是由预处理器对宏进行替代 #80001e==,而==内联函数是通过编译器控制来实现的 #80000f==。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的==展开==,所以取消了函数的参数压栈,减少了调用的开销。可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。

内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。


宏函数的适用范围

  1. 一般来说,用宏来代表简短的表达式比较合适。
  2. 在考虑效率的时候,可以考虑使用宏,或者内联函数。
  3. 还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。

3.使用宏时的注意点

3.1. 算符优先级问题

不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:

#define MULTIPLY(x, y) x * y

==MULTIPLY(1, 2) #80000a==没问题,会正常展开成==1 * 2 #80000a==。有问题的是这种表达式==MULTIPLY(1+2, 3) #800019==,展开后成了==1+2 * 3 #80000f==,显然优先级错了。
对宏体和给引用的每个参数加括号,就能避免这问题。

#define MULTIPLY(x, y) ((x) * (y))

3.2 .分号吞噬问题

有如下宏定义

#define foo(x) bar(x); baz(x)

假设你这样调用:

if (!feral)foo(wolf);

这将被宏扩展为:

if (!feral)bar(wolf);
baz(wolf);

==baz(wolf);==,不在判断条件中,显而易见,这是错误。
如果用大括号将其包起来依然会有问题,例如

#define foo(x)  { bar(x); baz(x); }
if (!feral)foo(wolf);
elsebin(wolf);

判断语言被扩展成:

if (!feral) {bar(wolf);baz(wolf);
}>>++;++<< 
elsebin(wolf);

==else==将不会被执行

通过==do{…}while(0) #80001e==能够解决上述问题

#define foo(x)  do{ bar(x); baz(x); }while(0)
if (!feral)foo(wolf);
elsebin(wolf);

被扩展成:

#define foo(x)  do{ bar(x); baz(x); }while(0)
if (!feral)do{ bar(x); baz(x); }while(0);
elsebin(wolf);

使用do{…}while(0)构造后的宏定义不会受到大括号、分号等的影响,总是会按你期望的方式调用运行。

3.3. 宏参数重复调用

有如下宏定义:

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

当有如下调用时==next = min (x + y, foo (z));==,宏体被展开成==next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));==,可以看到,foo(z)有可能会被重复调用了两次,做了重复计算。更严重的是,如果foo是不可重入的(foo内修改了全局或静态变量),程序会产生逻辑错误。

3.4.宏定义中的# ## \ @#

3.4.1.#

名称:字符串化操作符

其作:将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串。

使用条件:只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。

3.4.2.##

名称:符号连接操作符

作用:将宏定义的多个形参名连接成一个实际参数名

使用条件:只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。

3.4.3.@#

名称:字符化操作符

作用:将传入的单字符参数名转换成字符,以一对单引用括起来。

使用条件:只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。

3.4.4.\

名称:行继续操作符

作用:当定义的宏不能用一行表达完整时,可以用"\"表示下一行继续此宏的定义。

ifdef条件编译

  当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:
#ifdef   标识符
程序段 1
#else
程序段 2
#endif
它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译,否则编译程序段 2。 其中#else 部分也可以没有,即:
#ifdef
程序段 1
#endif

    #ifndef/#define/#endif使用详解:

#ifndef/#define/#endif的格式:

    #ifndef A_H意思是"if not define a.h"  如果不存在a.h

    接着的语句应该#define A_H  就引入a.h

    最后一句应该写#endif   否则不需要引入

extern变量申明

C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。 这里面要注意,对于 extern 声明变量可以多次,但定义只有一次。在我们的代码中你会看到看到这样的语句:

          extern u16 USART_RX_STA;

这个语句是申明 USART_RX_STA 变量在其他文件中已经定义了,在这里要使用到。所以,你肯定可以找到在某个地方有变量定义的语句:

          u16 USART_RX_STA;

对于函数也是同样的应用。

typedef类型别名

typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。

下面的实例为单字节数字定义了一个术语 BYTE:

typedef unsigned char BYTE;

在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写,例如:

BYTE  b1, b2;

按照惯例,定义时会大写字母,以便提醒用户类型名称是一个象征性的缩写,但您也可以使用小写字母,如下:

typedef unsigned char byte;

typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。

struct _GPIO{__IO uint32_t MODER;__IO uint32_t OTYPER;…}

上面定义了一个结构体 GPIO,这样我们定义变量的方式为:

        struct _GPIO GPIOA;//定义结构体变量 GPIOA

但是这样很繁琐,MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别
名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量了。

方法如下:

typedef struct{__IO uint32_t MODER;__IO uint32_t OTYPER;…} GPIO_TypeDef

Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结构体

变量:
GPIO_TypeDef _GPIOA,_GPIOB;

这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。

当然typedef在程序代码可移植性上也有很大的帮助。比如在一些头文件中使用typedef如下:

typedef signed char int8_t;typedef signed short int int16_t;typedef signed int int32_t;typedef signed __INT64 int64_t;

结构体

声明结构体类型:

            Struct 结构体名{
成员列表;
}变量名列表;

例如:
Struct U_TYPE {
Int BaudRate
Int WordLength;
}usart1,usart2;

在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:

    Struct 结构体名字 结构体变量列表 ;

    例如:struct U_TYPE usart1,usart2;

    结构体成员变量的引用方法是:

    结构体变量名字.成员名

    比如要引用 usart1 的成员 BaudRate,方法是:usart1.BaudRate;

 

结构体指针变量定义也是一样的,跟其他变量没有啥区别。

    例如:struct U_TYPE *usart3;//定义结构体指针变量 usart1;

    结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 usart3 结构体指针指向的结

构体的成员变量 BaudRate,方法是:

    Usart3->BaudRate;

使用结构体的好处是防止函数的入口参数过多,当然也利于增加变量时不用修改函数定义,对于一组描述

同一对象的参数,用结构体使他们形成一个整体,也有利于代码的可读性,不会使变量定义显得混乱。

具体应用举例

定义时的糟糕习惯,定义变量或函数的名字信手拈来用到谁是谁

int abc 

int ijk

不够了在往后面lmn ,回头就忘了变量名是什么功能

  相关解决方案