#define RCC_APB2ENR (*(volatile unsigned int*)(0x40021000+0x18))
#define GPIOD_CRL (*(volatile unsigned int*)(0x40011400))
#define GPIOA_CRH (*(volatile unsigned int*)(0x40010800+0x004))
#define GPIOD_ODR (*(volatile unsigned int*)(0x40011400+0x00C))
#define GPIOA_ODR (*(volatile unsigned int*)(0x40010800+0x00C))
int main()
{
//使能时钟
RCC_APB2ENR&= 0;
RCC_APB2ENR|= 1<<5;//D口
RCC_APB2ENR|= 1<<2;//A口
//GPIO功能配置
//PD GPIOD_CRL:8 9 10 11 bit 0011推挽输出最大速度为50MHZ;
//PD GPIOA_CRH:0 1 2 3 bit 0011推挽输出最大速度为50MHZ;
GPIOD_CRL &= ~(3<<(2*4));//清0
GPIOA_CRH &= ~3;//清0
GPIOD_CRL |= 3<<(2*4);//配置输出模式
GPIOA_CRH |= 3;
//输出低电平
GPIOD_ODR &=~(1<<2);
GPIOA_ODR &=~(1<<8);
while(1);
}
寄存器定义的解读:
一· 寄存器的地址
基地址:STM32中的每个模块都包含多个寄存器,这些地址通常是连续的,而第一个寄存器的地址就是基地址。
地址偏移量:寄存器在本模块中的地址偏移量。
绝对地址:寄存器的实际地址,它等于基地址+地址偏移量。
二· 寄存器的地址
寄存器定义过程的分析。
以 #define RCC_APB2ENR ((volatile unsigned int) (0x40021000+0x18)) 为例子进行分析。
1.将(0x40021000+0x18)转为地址
首先,从计算机和编译器的角度(0x40021000+0x18)只是一个16进制的整型数据,所以将它强制类型转换为地址类型,即加个asterisk* 指针运算符
(*)(0x40021000+0x18) -----(1)
2.说明地址(0x40021000+0x18)指向的存储单元的数据类型
经过上述强制转换后(0x40021000+0x18)代表一个地址,但是该地址指向的存储单元占多少个字节或者说该存储单元的数据类型是什么都没有指明,所以需要加类型修饰符。由于寄存器存放的都是无符号整形数据,故采用unsigned int 对代码(1)进行修饰,具体如下
(unsigned int*)(0x40021000+0x18) -----(2)
3.使用volatile 修饰地址, 使得处理器每次都是对对应的寄存器单元进行操作
接下来,我们希望处理器在对地址为(0x40021000+0x18)的存储单元进行操作时每次都是直接对该存储单元进行读写而不是经过缓存,这样虽然速度慢一些但能够避免数据读写错误,为满足这一要求可以采用修饰符volatile对代码(2)进行修饰
(volatile unsigned int*)(0x40021000+0x18) -----(3)
4.将地址转换为地址指向的存储单元
考虑到C语言中的指针变量的定义与使用,如下面的例子
int *p, a; //定义了一个指针变量p
p=&a; //将变量a的地址赋给p
该例子中,将变量a的地址赋值给同类型的指针变量p,然后通过“ *p=5;”的方式对a进行赋值,所以想要对地址为(0x40021000+0x18)的存储单元访问,还需要再代码(3)的基础上加上一个指针运算符 ,即
( * (volatile unsigned int)(0x40021000+0x18)) -----(4)
经过上述转换后我们就可以对地址为(0x40021000+0x18)的单元进行读写了,如
( * (volatile unsigned int*)(0x40021000+0x18)) = 5 -----(5)
意思是指将5存入地址为(0x40021000+0x18)的4字节的存储单元中。
参考链接:【新手入门】STM32从0到1,从浅至深知识讲解-原子哥强烈推荐!
https://www.bilibili.com/video/BV1nS4y1q7su/?spm_id_from=333.788.top_right_bar_window_history.content.click&vd_source=0837b77c4a109c097f82d27d0f274e84