14.C语言基础

C语言基础:位操作、宏定义、条件编译、类型别名

笔记基于正点原子官方视频
视频连接https://www.bilibili.com/video/BV1Wx411d7wT?p=71&spm_id_from=333.1007.top_right_bar_window_history.content.click
如有侵权,联系删除

一、位操作
1.简介
  • 位操作:6种位操作运算符
    在这里插入图片描述
    GPIOA->CRL&=0XFFFFFF0F; //将第4-7位清0
    GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值
    GPIOA->ODR|=1<<5;
    TIMx->SR = (uint16_t)~TIM_FLAG;
2.位操作在单片机开发中的一些实用技巧。

1)不改变其他位的值的状况下,对某几个位进行设值。
这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,然后用|操作符设值。比如我要改变 GPIOA->ODR 的状态,可以先对寄存器的值进行&清零操作

GPIOA->ODR &=0XFF0F; //将第 4-7 位清 0

然后再与需要设置的值进行|或运算

GPIOA->ODR |=0X0040; //设置相应位的值,不改变其他位的值 

2) 移位操作提高代码的可读性。
移位操作在单片机开发中也非常重要,我们来看看下面一行代码

GPIOA->ODR| = 1 << 5;

这个操作就是将 ODR 寄存器的第 5 位设置为 1,为什么要通过左移而不是直接设置一个固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第 5 位设置为 1,其他位的值不变。如果你写成

GPIOA->ODR =0x0020;

这样的代码可读性非常差同时也不好重用。
3) ~取反操作使用技巧
例如 GPIOA->ODR 寄存器的每一位都用来设置一个 IO 口的输出状态,某个时刻我们希望去设置某一位的值为 0,同时其他位都为 1,简单的作法是直接给寄存器设置一个值:

GPIOA->ODR =0xFFF7;

这样的作法设置第 3 位为 0,但是这样的写法可读性很差。看看如果我们使用取反操作怎么实现:

GPIOA->ODR= (uint16_t)~(1<<3);

看这行代码应该很容易明白,我们设置的是 ODR 寄存器的第 3 位为 0,其他位为 1,可读性非常强。

二、define宏定义关键词

define是C语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供方便。
常见的格式:

#define 标识符 字符串

“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。
例如:

#define HSI_VALUE ((uint32_t)16000000)

定义标识符 HSI_VALUE 的值为 16000000。这样我们就可以在代码中直接使用标识符HSI_VALUE,而不用直接使用常量 16000000,同时也很方便我们修改 HSI_VALUE 的值。

三、ifdeff 和 #if defined条件编译

单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:

#ifdef 标识符
程序段1 
#else 
程序段2 
#endif

它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译,否则编译程序段 2。 其中#else 部分也可以没有,即:

#ifdef 
程序段 1 
#endif

这个条件编译在 MDK 里面是用得很多的,在 stm32f4xx_hal_conf.h 这个头文件中会看到这样的语句:

#ifdef HAL_GPIO_MODULE_ENABLED
 #include "stm32f4xx_hal_gpio.h"
#endif 

这段代码的作用是判断宏定义标识符 HAL_GPIO_MODULE_ENABLED 是否被定义,如果被定义了,那么就引入头文件 stm32f4xx_hal_gpio.h。对于条件编译,还有个常用的格式,如下:

#if defined XXX1
程序段 1
#elif defined XXX2
 程序段 2
 …
#elif defined XXXn
 程序段 n
 …
#endif

这种写法的作用实际跟 ifdef 很相似,不同的是 ifdef 只能在两个选择中判断是否定义,而 if defined 可以在多个选择中判断是否定义。

四、typedef类型别名(定义变量类型,且用简单名代替复杂名)

typedef unsigned int uint32_t; //把unsigned int uint32_t简化为uint32_t
其实还可以再简一层
typedef uint 32_t u32 ; //把uint32_t简化为u32,这样就可以直接写变量名u32了
这里u32就等价于uint32_t就等价于unsigned int uint32_t
要调用变量unsigned int uint32_t直接写u32就可以了
在程序里我们经常看到u8 a ,这里的u8就相当于unsigned char uint8_t

typedef unsigned 				char uint8_t;   
typedef unsigned short 		int uint16_t;
typedef unsigned					 int uint32_t;       //把unsigned	int uint32_t简化为uint32_t
typedef unsigned 				 __int64 uint64_t;

定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象
typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。
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 是等同的作用了。

五、extern变量申明

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

extern u16 USART_RX_STA; 

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

u16 USART_RX_STA; 

的出现。下面通过一个例子说明一下使用方法。在 Main.c 定义的全局变量 id,id 的初始化都是在 Main.c 里面进行的。

Main.c 文件
u8 id;//定义只允许一次
main()
{
id=1;
printf("d%",id);//id=1
test();
printf("d%",id);//id=2
}

但是我们希望在main.c的 changeId(void)函数中使用变量id,这个时候我们就需要在main.c里面去申明变量 id 是外部定义的了,因为如果不申明,变量 id 的作用域是到不了 main.c 文件中。看下面 main.c 中的代码:

extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行
void test(void){
id=2;
}

在 main.c 中申明变量 id 在外部定义,然后在 main.c 中就可以使用变量 id 了。
对于 extern 申明函数在外部定义的应用,后面可以去百度查询。

六、static关键字
  • Static申明的局部变量,存储在静态存储区。
  • 它在函数调用结束之后,不会被释放。它的值会一直保留下来。
  • 所以可以说static申明的局部变量,具有记忆功能。
  • Static申明的函数作用域仅限当前.c文件,其他.c无法调用
七、__weak弱函数
  • 经测试,weak函数和重写的函数不能放在同一文件。
  • 避免重复定义
  • 如果用户没有重写,那么会执行弱函数,如果用户重写该函数,那么会执行重写的函数。
  • 说白了就是相同的函数但是弱函数优先级最低
八、结构体:构造类型

建议单独进行百度学习

声明结构体类型:

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

例如:

Struct G_TYPE {
uint32_t Pin;
uint32_t Mode;
uint32_t Speed;
}GPIOA,GPIOB;

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

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

例如:

struct G_TYPE GPIOA,GPIOB;

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

结构体变量名字.成员名

比如要引用 GPIOA 的成员 Mode,方法是:GPIOA. Mode;
结构体指针变量定义也是一样的,跟其他变量没有啥区别。
例如:

struct G_TYPE *GPIOC;//定义结构体指针变量 GPIOC;

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

GPIOC-> Speed;

上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实例回答一下这个问题。
在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如 IO 口。它的初始化状态是由几个属性来决定的,比如模式,速度等。对于这种情况,在我们没有学习结构体的时候,我们一般的方法是:

void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed);

这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入上下拉 Pull 这个入口参数。于是我们的定义被修改为:

void HAL_GPIO_Init (uint32_t Pin, uint32_t Mode, uint32_t Speed,uint32_t Pull);

但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下,只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。
结构体就是将多个变量组合为一个有机的整体。上面的函数中 Pin, Mode,
Speed 和 Pull 这些参数,他们对于 GPIO 而言,是一个有机整体,都是来设置 IO 口参数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的:

typedef struct
{
 uint32_t Pin; 
 uint32_t Mode; 
 uint32_t Pull; 
 uint32_t Speed; 
 uint32_t Alternate;
}GPIO_InitTypeDef;

于是,我们在初始化 GPIO 口的时候入口参数就可以是 GPIO_InitTypeDef 类型的变量或者指针变量了,MDK 中是这样做的:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义就可以达到增加变量的目的。
理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可以提高你的代码的可读性。
使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲解结构体的一些其他知识。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值