STM32复习C语言
- [一、 C语言基础知识复习](#一、 C语言基础知识复习)
-
- 1、什么是位操作
- 2、深入位运算
-
- 1、在不改变其他位的情况下对某几个位进行设置。
-
- 1、在不改变其他位的值的状况下,对某几个位进行设值
- 2、移位操作提高代码的可读性
- 3、~按位取反操作使用技巧
- [4、^ 按位异或操作的使用技巧](#4、^ 按位异或操作的使用技巧)
- 3、define宏定义
- [4、ifdef 条件编译](#4、ifdef 条件编译)
- [5、typedef 类型别名](#5、typedef 类型别名)
- 6、结构体
一、 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);
这样,无论任何时候,我们只需要修改结构体成员变量,往结构体中加入新的成员变量,而不需要修改函数定义就可以达到增加入入口函数的目的了,好处是不用修改任何函数定义就可以达到增加变量的目的。