目录
3.使用指针直接访问寄存器的方法(标准库函数操作寄存器的方法)
4.改变数据中的某一位而不影响其他位(标准库函数中|=、&=的作用)
[将其位2置1,其他位不变,0x04的二进制是0000 0100:](#将其位2置1,其他位不变,0x04的二进制是0000 0100:)
[将其位2清0,其他位不变,~0x04的二进制是1111 1011:](#将其位2清0,其他位不变,~0x04的二进制是1111 1011:)
所以可以将APB2外设时钟使能寄存器RCC_APB2ENR的位掩码使用宏定义标注出来,保留位就是为后续开发,增加功能预留的位,这里默认为无效位
[访问结构体变量/结构体指针(其中pa = &a(表示pa指针,指向a这个结构体)):](#访问结构体变量/结构体指针(其中pa = &a(表示pa指针,指向a这个结构体)):)
1.以LED闪烁为例子

软件程序能控制硬件最关键的是硬件中的寄存器,寄存器是软件到硬件的桥梁,对于软件来说,寄存器是RAM中的一段存储空间,使用指针来访问这些空间,就可以读写寄存器中的每一位,对于硬件来说,寄存器中的每一位,背后都有一个导线,去控制相应的设备,比如软件给寄存器的位写0或1,对硬件就可能是,开关某个电路或者输出某个高低电平
任何软件包装的库函数,最终都要落实到寄存器中,或者说寄存器就是对读写寄存器操作的一种封装
比如LED闪烁例程中,打开数据生成
使用RCC开启CPIOA的时钟,就要看APB2外设时钟使能寄存器RCC_APB2ENR
初始化GPIO端口,就要看端口配置低寄存器CRL和端口配置高寄存器CRH
给端口写高低电平,就看输出数据寄存器ODR,或者位设置寄存器BSRR/位清除寄存器BRR
2.LED闪烁中开启GPIOA时钟的拆解
开启GPIOA的时钟,就是给RCC_APB2ENR寄存器的位2(IOPAEN)设置位1
RCC_APB2PeriphClockCmd内部
下面会分别介绍这个RCC开启时钟函数内部的操作,大部分标准库函数和RCC开启时钟的函数是类似的,所以会了这个,其他逻辑差不多
3.使用指针直接访问寄存器的方法(标准库函数操作寄存器的方法)
0x40021018:起始地址+偏移地址
起始地址:在STM32F1系列数据手册中2.3存储器映像中有RCC的起始地址
偏移地址:在STM32F1系列数据手册中6.3.7APB2外设时钟使能寄存器中可知,这个寄存器的偏移地址和它是一个32位寄存器
例如:RCC_APB2ENR寄存器存储在地址0x40021018下,长度4字节(32位)
访问此寄存器的三步:
uint32_t *p; // 定义uint32_t类型指针
p = (uint32_t *)0x40021018; // 指针指向目标地址
*p = 0x04; // 在目标地址写入数据
要给这个寄存器写入值,就*p = 一个数据,比如这里的0x04
要读出这个寄存器的值,就直接*p
将上述三个步骤简化为一行:
*(uint32_t *)0x40021018 = 0x04;
使用宏定义替换上述的一行代码:
#define RCC_APB2ENR (*(uint32_t *)0x40021018)
RCC_APB2ENR = 0x04;
代码:

4.改变数据中的某一位而不影响其他位(标准库函数中|=、&=的作用)
上述的直接访问寄存器的方法,有不足的地方,就是虽然成功的将寄存器的某一位置1了,但是其他位都置0,如果想给寄存器的某一位置1而不影响其他位,那么就使用要 |=、&=的方法
例如:有一变量/寄存器uint8_t REG
将其位2置1,其他位不变,0x04的二进制是0000 0100:
REG |= 0x04; // REG |= 0000 0100
任何位和1相或都变成1,任何位和0相或都保持不变
将其位2清0,其他位不变,~0x04的二进制是1111 1011:
REG &= ~0x04; // REG &= 1111 1011
任何位和0相与都变为0,任何位和1相与保持不变
宏定义
这个0x04在 |=、&=、~ 的配合下只对位2进行操作,所以这个0x04称为位2的掩码
所以将0x04使用宏定义替换:
#define BIT2_MASK 0x04 // 0x04是位2的位掩码
REG |= BIT2_MASK; // |=位掩码,将此位置1,其他位不变
REG &= ~BIT2_MASK; // &= ~位掩码,将此位清0,其他位不变
所以可以将APB2外设时钟使能寄存器RCC_APB2ENR的位掩码使用宏定义标注出来,保留位就是为后续开发,增加功能预留的位,这里默认为无效位

代码

使用

开启GPIOA的时钟

关闭GPIOA的时钟

同时开启GPIOA和TIM1的时钟

5.结构体,数据打包好帮手(标志库函数中->中的作用)
定义结构体类型:
struct S {char x; int y;}; //打包数据里面的数据类型为char和int
typedef struct {char x; int y;} S_t; //这个是标志库里面运用最多的和上面是一样的
定义结构体变量/结构体指针:
struct S a;//结构体变量 struct S *pa;//结构体指针
S_t a;//结构体变量 S_t *pa;//结构体指针
访问结构体变量/结构体指针(其中pa = &a(表示pa指针,指向a这个结构体)):
a.x = '#';//结构体变量名a.结构体成员名x = 赋值 pa->x = '#';//结构体指针名pa -> 结构体成员名x = 赋值
a.y = 123;//结构体变量名a.结构体成员名y = 赋值 pa->y = 123;//结构体指针名pa -> 结构体成员名y = 赋值
6.使用结构体描述寄存器(标准库函数中对于寄存器的宏定义)
以下两种方法都是标准库中对于GPIO的宏定义
描述寄存器地址,常规做法:
指针做法虽然简洁易懂,但是很繁琐
结构体做法:
这种做法就很有规范还简单,这里的指针类型不再是uint32_t*,因为这个类型,只能指向一个寄存器,而现在定义了一个GPIO箱子(结构体),这个GPIO结构体一次性可以把所有的寄存器装在一起,所以这个类型是GPIO_t *是一个结构体指针
STM32F1系列数据手册中GPIO寄存器

7.使用结构体打包传参(标准库中初始化函数的使用)
例如GPIO初始化时,是将结构体作为指针传递参数
注意:地址传参才是最好的,不要值传参,值传参会再开辟一个空间来存储传的值,而且还有可能出错,地址传参就不会出现这种情况,地址传参是通过地址找到存储这个值的寄存器,然后复制一份
某函数需要传入一堆参数,常规做法:
定义函数:void function(char x, int y, float z) { ... }
调用函数:function('#', 123, 66.88);
结构体打包传参:
定义结构体:typedef struct {char x, int y, float z} S_t;
定义函数:void function(S_t *p) { ... }
调用函数:
S_t a; //定义结构体变量
a.x = '#'; a.y = 123; a.z = 66.88; //结构体变量赋值
function(&a); //将打包的结构体变量传入函数(地址传递)












