STM32教程-02-STM32复习C语言

STM32复习C语言

一、 C语言基础知识复习

1、什么是位操作

按位与 & : 两位都为 1 时结果为 1,否则为 0

c 复制代码
 0000 1101
&1111 1100
------------------------------
 0000 1100

按位取反 ~: 将每一位的 0 变成 1,1 变成 0

c 复制代码
~0000 1101
------------------------------
 1111 0010

按位或 | :只要有一个位为 1,结果就为 1

c 复制代码
 0000 1101
|1111 1100
------------------------------
 1111 1101

按位异或 ^ : 两位不同则为 1,相同则为 0

c 复制代码
 0000 1101
^1111 1100
------------------------------
 1111 0001

左移指令 : 高位溢出舍弃,低位补 0。

c 复制代码
0x01 << 3 
0000 0001
      <<3
_________
0000 1000

右移指令

c 复制代码
0x10 >> 3 
0001 0000
     >> 3
_________
0000 0010

2、深入位运算

对某个寄存器的某一位进行置零或者置一。

利用 & 进行清零操作。

利用 | 进行置位操作。

1、在不改变其他位的情况下对某几个位进行设置。

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

例如需要改变GPIOA->ODR的状态,我们可以先对寄存器的值进行&清零操作

先看按位与与操作

c 复制代码
// 因为 GPIOA->ODR 是 16位寄存器,所以下面我们省略高16位,只写低16位
GPIOA->ODR &= 0XFF0F; // 将第4-7位清0

// 这段代码等价于
GPIOA->ODR = GPIOA->ODR & 0XFF0F;
// 假如 GPIOA->ORD内存储数据为:0x00 00 12 fa
 12 fa
&ff 0f
------------------
 0001 0010 1111 1010
 1111 1111 0000 1111
------------------------------------------------------------
 0001 0010 0000 1010
------------------
 1    2    0    a
// 通过与运算,两位都为1,结果为1的特点,将4-7位清0

再看一个按位或操作

c 复制代码
GPIOA->ODR |= 0X0040
// 0040 转化为 二进制写法
// 假如 GPIOA->ORD内存储数据为:0x00 00 00 00
  0000 0000 0000 0000
| 0000 0000 1000 0000
---------------------------------------------------------------
  0000 0000 1000 0000
// 通过按位或计算,我们成功将ODR寄存器第8位设置为1   
2、移位操作提高代码的可读性

例如将Systick->CTRL的某一位置位1

c 复制代码
SysTick->CTRL |= 1 << 1;
// 这个操作是将1右移1位,CTRL寄存器的第一位设为1(从0开始数),为什么要通过左移而不是直接给一个固定的值呢?
SysTick->CTRL |= 0X0002;
// 这个代码的效果和第一个是一样的
// 这个虽然也能实现同样的效果,但是可读性稍差,而且修改也比较麻烦。
3、~按位取反操作使用技巧

按位取反在设置寄存器的时候经常被使用,常用于清除某一个\几个位。

c 复制代码
SysTick->CTRL &= ~(1 << 0) ; /* 关闭 SYSTICK */
// 1 << 0  将1左移0位,等于其本身
// ~(1<<0) 因为是32位系统,~(00 00 00 01) => ff ff ff fe
// SysTick->CTRL = SysTick->CTRL & (ff ff ff fe)
// 也就是将 CTRL寄存器的最低位置0,其余位保持不变
4、^ 按位异或操作的使用技巧

该功能非常适合用于控制某一个位翻转,常见控制就是控制LED灯闪烁

c 复制代码
GPIOB->ODR ^= 1 << 5;
// 该代码执行一次,就会是PB5的输出状态翻转一次,如果我们的led接在PB5,就可以看到LED闪烁了

// 首先 1被左移5位,那么就是1 0000
// 再和当前 ODR 中的值进行异或,因为异或的特点是相同为0,不同为1
// 假如PB5上为1,因为相同,那么异或后PB5上的值被设为0
// 假如PB5上为0,因为不同,那么异或后PB5上的值被摄为1
// 这样就实现了引脚上的电平翻转

3、define宏定义

1、define的基本用法

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

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

"标识符"所定义的宏名,"字符串"可以是常量、表达式、格式串灯。

c 复制代码
#define  HSE_VALUE ((uint32_t)16000000)

((uint32_t)16000000) 是将常量 16000000转换为无符号32位整形,适应stm32内存结构定义

那么这个宏定义的定义了标识符HSE_VALUE的值是16000000,这样我们就可以在代码中直接使用HSE_VALUE来代表16000000,而不是直接使用16000000这个常量本身,同时也很方便我们修改HSE_VALUE的值。

2、define定义宏

#define机制包括罗了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏

命名约定:宏的名字全部大写,函数的名字不要全部大写

c 复制代码
#include <stdio.h>

#define SQUARE(x) x*x        
// 宏名为 SQUARE,而(x)是宏的参数,不是宏名的一部分 它替代的是x*x  
// 例如在main程序中写 SQUARE(x) 那么他替代的就是 x * x 
// 当x为3时,SQUARE(3) 就是 3 * 3 = 9
// 当然这样是有一些问题的,例如x不是一个常量,而是一个表达式的时候就会出错
// 例如: SQUARE(3 + 5) ,那么替换的结果就是 3 + 5 * 3 + 5 = 23
// 而实际的计算结果是 64

#define SQUARE1(x) ((x)*(x))
// 为了避免出现上面那个宏出现的问题,必须在宏的参数上加括号,且整体也需要加括号
// 这样调用宏计算 SQUARE1(3 + 5) 是就会((3+5)*(3+5)) = (64)符合预期了

#define DOUBLE(x) (x)+(x)
#define DOUBLE1(x) ((x)+(x))
// 这两个DOUBLE类型也是一样的

int main()
{
	int x = 10;
	int ret = SQUARE(x);
	printf("%d\n",ret); // 100

	int a = 5;
	ret = SQUARE(a+1); // a + 1 * a + 1 = 11
	printf("%d\n", ret); // 11

	int b = 5;
	ret = SQUARE1(b+1); 
	printf("%d\n", ret); // 36

	int c = 4;
	ret = 10 * DOUBLE(4);
	printf("%d\n", ret); // 44

	ret = 10 * DOUBLE1(4);
	printf("%d\n", ret); // 80

	return 0;
}

3、带有副作用的宏

当宏参数在宏的定义中超过一次的时候,如果参数具有副作用,那么你在使用宏的时候可能出现危险,导致不可预知的后果

什么是副作用?

c 复制代码
#include <stdio.h>

int main()
{
	int a = 10;
	// int b = a + 1; 
	// 因为 a + 1 只导致了b = 11,a本身没有变化,所以没有副作用

	int b = ++a; 
	// ++a,先++ 再赋值给b
	// 这导致 b = 11 ,a = 11 这就是具有副作用
	return 0;
}

那么我们来看一个例子,带副作用参数时,宏定义的行为与函数的差异

c 复制代码
#include <stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y)) // 使用三目运算符,宏求较大值

int Max(int x, int y) // 函数求较大值
{
	return x > y ? x : y; // 这也是一个三目运算符
}

int main()
{
	int a = 10;
	int b = 12;
	int m = MAX(a,b); // 参数没有副作用
	printf("%d\n",m);

	m = MAX(a++, b++); 
	// a = 10 , b = 12
	// MAX(a++, b++)
	// 因为是后置++,所以在进入宏定义时候,a = 10 , b = 12
	// 进入宏定义,三目运算符,需要先比较 执行:(a++) > (b++) 
	// 先比较,再++,结果是b++大,此时:a = 11 , b = 13
	// 进入输出流程,因为三目运算符中的判断表达式为假,所以(a++):(b++)中执行b++,所以b++,为14
	// 宏定义输出的值,是三目运算符输出的值,所以是13
	// 最后a = 11,b = 14
	
	printf("%d\n", m); // 13
	printf("a = %d\n", a); // 11
	printf("b = %d\n", b); // 14

	int c = 10;
	int d = 12;
	int ret = Max(c++,d++); // 函数先计算结果再传参
	printf("%d\n",ret);
	return 0;
}
// 输出结果:
	// 看汇编语言,进入函数使用了13条汇编指令,这还没有计算
	// 函数调用的时候:
	// 1、函数的调用:参数的传参,栈帧的创建 13条
	// 2、计算任务 6条
	// 3、返回值 5条
	// 一共使用了21条汇编语句

	// 这点来说,宏比函数的代码规模上要小的多,运行速度要快的多
	// 而且函数的参数必须声明为特定的类型,宏的参数没有类型,是直接替换的
	// 总之这个宏无论是整形、浮点型,只要可以用> 比较,宏的参数是类型无关的

	// 宏的劣势
	// 1、因为宏是替换的,每次进行一次替换,一份宏定义的代码就将被插入程序,否则宏比较短,否则将大幅度增加程序的代码长度
	// 2、宏是不能调试的,
	// 3、宏由于与类型无关,也就是不够严瑾
	// 4、宏可能带来运算符优先级的问题,导致程序出现问题

4、ifdef 条件编译

单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而

当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:

c 复制代码
#ifdef 标识符
	程序段 1 
#else 
	程序段 2 
#endif

它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编

译,否则编译程序段 2。 其中#else 部分也可以没有,即:

c 复制代码
#ifdef 
 程序段 1 
#endif

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

的语句:

c 复制代码
#ifdef HAL_GPIO_MODULE_ENABLED
 #include "stm32f4xx_hal_gpio.h"
#endif

这段代码的作用是判断宏定义标识符 HAL_GPIO_MODULE_ENABLED 是否被定义,如

果被定义了,那么就引入头文件 stm32f4xx_hal_gpio.h。

来看一个例子:

c 复制代码
#include <stdio.h>

#define MAX 70 

int main()
{
#if 1+1<MAX 
// 若1+1==2则执行编译,注意条件编译指令中,不能判断变量,但是可以使用#define 定义的常
	printf("hehe\n");
#endif
	printf("haha\n");
}
// 输出:
// hehe
// haha

// 多分支的条件编译指令
int main()
{
#if MAX < 50
	printf("hehe\n");
#elif M>=50 && M<=80
	printf("haha\n");
#else 
	printf("heihei\n");
#endif
	return 0;
}
// 输出 hehe

5、typedef 类型别名

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

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

c 复制代码
struct _GPIO
{
	 __IO uint32_t MODER;
	 __IO uint32_t OTYPER;
	 // ...
};

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

c 复制代码
struct _GPIO gpiox; /* 定义结构体变量 gpiox */

但是这样很繁琐,MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义

一个别名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量了,方法如下:

c 复制代码
typedef struct
{
	 __IO uint32_t MODER;
	 __IO uint32_t OTYPER;
	 ...
} GPIO_TypeDef;

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

c 复制代码
GPIO_TypeDef gpiox;

这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了,但是 GPIO_TypeDef 使用起来方便很多。

6、结构体

经常很多用户提到,他们对结构体使用不是很熟悉,但是 MDK 中太多地方使用结构体以

及结构体指针,这让他们一下子摸不着头脑,学习 STM32 的积极性大大降低,其实结构体并

不是那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下面的"寄存

器映射"中讲到一些。

声明结构体类型:

c 复制代码
struct 结构体名
{
 成员列表;
 }变量名列表;

例如:

c 复制代码
struct U_TYPE 
{
	 int BaudRate 
	 int WordLength;
}usart1, usart2;

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

c 复制代码
struct 结构体名字 结构体变量列表 ;

例如:

c 复制代码
struct U_TYPE usart1,usart2;

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

c 复制代码
结构体变量名字.成员名

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

c 复制代码
struct U_TYPE *usart3; /* 定义结构体指针变量 usart3 */

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

c 复制代码
usart3->BaudRate;

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

c 复制代码
void usart_init(uint8_t usartx, uiut32_t BaudRate, uint32_t Parity,uint32_t Mode);

这里就使用了一个函数去实现串口的初始化,但是很显然这个函数的参数众多,但是如果有一天,我们希望往这个函数中再传入一个或者几个参数,那么我们势必需要去修改这个函数的定义,重新假如新的入口函数,随着开发不断增多,那么我们就需要不断修改函数定义,这给我们带来很大的麻烦。

所以我们使用结构体参数,就可以在不改变函数入口的情况下,只需要修改结构体的成员变量达到修改入口参数的目的。

结构体就是将多个变量组合为一个有机的整体,上面的函数,usartx,BaudRate,Parity,Mode 等这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的:

c 复制代码
typedef struct
{
	uint32_t BaudRate;
	uint32_t WordLength;
	uint32_t StopBits;
	uint32_t Parity;
	uint32_t Mode;
	uint32_t CLKPolarity;
	uint32_t CLKPhase;
	uint32_t CLKLastBit; 
} UART_InitTypeDef;

这样我们在初始化串口入口的参数就可以设定为USART_InitTypeDef类型的变量或者指针变量了,于是我们可以改为:

c 复制代码
void usart_init(UART_InitTypeDef *huart);

这样,无论任何时候,我们只需要修改结构体成员变量,往结构体中加入新的成员变量,而不需要修改函数定义就可以达到增加入入口函数的目的了,好处是不用修改任何函数定义就可以达到增加变量的目的。

相关推荐
Nautiluss2 小时前
一起调试XVF3800麦克风阵列(三)
linux·人工智能·嵌入式硬件·音频·语音识别·dsp开发·智能音箱
潇氡2 小时前
C语言“指针变量“在初始化和做函数参数时的注意事项
c语言
程芯带你刷C语言简单算法题2 小时前
Day39~实现一个算法确定将一个二进制整数翻转为另一个二进制整数,需要翻转的位数
c语言·开发语言·学习·算法·c
永远前进不waiting3 小时前
C语言复习——2
c语言·开发语言
DIY机器人工房3 小时前
简单理解:为什么错误计数器一般要选 uint32_t 类型?
stm32·单片机·嵌入式硬件·嵌入式·diy机器人工房
上大科技蔡生3 小时前
CS5567:具有宽占空比范围的60V同步降压DCDC控制器
单片机·嵌入式硬件·fpga开发·dcdc
lingzhilab3 小时前
零知IDE——基于STM32F103RBT6的PAJ7620U2手势控制WS2812 RGB灯带系统
stm32·单片机·嵌入式硬件
三佛科技-187366133973 小时前
BP85956D集成VCC电容电机驱动BUCK电源芯片(12V300mA应用电路)
stm32·单片机·物联网
爱睡觉的王宇昊4 小时前
PCB设计完全指南:从软件选择到基础规范(通用电路篇详解)
笔记·stm32·单片机·嵌入式硬件·学习