使用寄存器点亮流水灯——第二重奏


说明:因为之前的那个版本好像也不是直接操纵了寄存器的地址,所以对在这里再对之前在STM32中用寄存器方式点亮流水灯的实验做一个补充。

一、理论部分

1.外设工作的本质:外设寄存器控制外设

CPU本身是不能直接控制硬件(如:UART、ADC、GPIO)的,硬件一般是由且对应的控制器来控制。控制器包含了硬件和硬件控制寄存器,硬件控制器的工作是由它内部的硬件控制寄存器来控制。

2.硬件寄存器地址的映射

处理器将各个硬件控制寄存器映射到CPU的地址空间中的一段范围,这样CPU就可以通过读写寄存器来间接控制硬件。**因此一个单片机被设计出来的时候它内部的寄存器地址就已经是固定了的。并且我们通常不能修改它们。**芯片资料的映射表记载了硬件控制器的地址。
先附上STM32寄存器地址映射表的一部分
在这里插入图片描述

3.寄存器地址的计算公式

①寄存器地址 = Base Address(外设基地址)+ Offset(偏移量)
②寄存器地址=总线基地址+外设基于总线地址的偏移量+寄存器相对于外设基地址的偏移量

4.小贴士

STM32单片机中的寄存器的赋值操作数都是32位的。(有些寄存器没有用高16位寄存器,比如:端口输出数据寄存器),大模块(拥有多个寄存器)地址长度通常是1024(查表用减法能减出来)。

二、分析

1.时钟的配置

(1)先查看时钟寄存器的地址

在这里插入图片描述
地址是从0x4002 1000——4002 13FF

(2)在查看APB2时钟寄存器的地址

在这里插入图片描述
即RCC_APB2ENR的寄存器地址为:基地址+偏移地址,即0x4002 1000 + 18 = 0x4002 1018

(3)查看RCC_APB2ENR的功能说明

在这里插入图片描述
在这里插入图片描述
“Reset and Clock Control – Advanced Peripheral Bus 2 Enable Register”,寄存器的地址为:0x4002 1018,加*号取寄存器的值:*0x4002 1018 = 二进制 0000 0000 0000 0100 。

*写为十六进制即为:0x4002 1018 = 0x0004。
至此,理论上我们已经打开了PA引脚的时钟。

2.引脚的配置

(1)查看PA引脚的寄存器地址

在这里插入图片描述

地址为:0x4001 0800——4001 0BFF

(2)引脚配置寄存器的配置(GPIOx_CRL、GPIOx_CRH)

在看之前,我们来算一个东西。算上面的PA引脚的寄存器占多少长度:在这里插入图片描述
计算得占1024即2^10,即占了11位。

在这里插入图片描述
计算得每个引脚的寄存器占了7位。

①低位引脚寄存器地址(PA0-7引脚)

在这里插入图片描述

②高位引脚寄存器地址(PA8-15引脚)

在这里插入图片描述
先只配置PA1引脚
点亮一个灯,主要是怕出错,错了好立马修改;没有错的话,配置其它灯不也迎刃而解了吗?
在这里插入图片描述
端口配置低寄存器的地址:0x4001 0800 + 0 = 0x4001 0800。那么给它赋值即为 *0x4001 0800 = 0x 0000 0030。(推挽输出)
*0x4001 0800 = 0x 0000 0000。(推挽输入)

(3)引脚输出数据寄存器的配置(GPIOx_ODR)

在这里插入图片描述

计算得地址为:0x4001 0800 + 0C = 4001 080C。
赋值为:*(0x4001 080C) = 0x0000 0002; //PA1引脚输出高电平

*(0x4001 080C) = 0x0000 0002; //PA1引脚输出低电平

3.代码及现象

(1)LED灯闪烁

①代码

//法一 不封装寄存器,使LED灯闪烁
int main()
{
	//配置时钟
	*(unsigned int*)0x40021018 = 0x0004; 
	
	//配置引脚
	*(unsigned int*)0x40010800 = 0x00000030;//(将PA1引脚配置为推挽输出模式)
	
	while(1)
	{
		*(unsigned int*)0x4001080C = 0x00000002; //PA1引脚输出高电平
		Delay_s(1);
		*(unsigned int*)0x4001080C = 0x00000000;  //PA1引脚输出低电平
		Delay_s(1);
	}
	
}

//我要解释一下上面的代码
为什么是*(unsigned int*)0x40021018 = 0x0004;这种赋值形式。
首先0x40021018是一个十六进制数,编辑器不知道它是时钟寄存器的地址,那么加上(unsigned int*),使(unsigned int*)0x40021018 将其强转为地址,这时编译器才知道它是一个地址。然后我们根据实验需求给时钟(变量)输入数据(或者说赋值),那么我们就要加*解引用时钟地址,才能给他赋值。

//法二 用自定义的宏封装寄存器,使LED灯闪烁
#include "stm32f10x.h"                  // Device header

#define RCC_APB2ENR  *(unsigned int*)0x40021018  //将APB2的时钟使能寄存器宏定义
#define GPIOA_CRL    *(unsigned int*)0x40010800  //低位引脚寄存器
#define GPIOA_ODR    *(unsigned int*)0x4001080C  //引脚输出寄存器

int main(void)
{
	//配置时钟
	RCC_APB2ENR = 0x0004; 
	//配置引脚
	GPIOA_CRL = 0x00000030;//(将PA1引脚配置为推挽输出模式)
	
	while(1)
	{
		GPIOA_ODR = 0x00000002; //PA1引脚输出高电平
		Delay_s(1);
		GPIOA_ODR = 0x00000000;  //PA1引脚输出低电平
		Delay_s(1);
	}
}


对于法二:我有点不解为啥自定义的宏在这里能够被赋值呢?
下面是我的一个测试,他会直接报错。

#include "stm32f10x.h"                  // Device header


#define RCC_APB2ENR  0x40021018  //将APB2的时钟使能寄存器宏定义
#define GPIOA_CRL    0x40010800  //低位引脚寄存器
#define GPIOA_ODR    0x4001080C  //引脚输出寄存器


//法二 用自定义的宏封装寄存器,使LED灯闪烁
int main(void)
{
	
	
	//配置时钟
	RCC_APB2ENR = 0x0004; 

	//配置引脚
	GPIOA_CRL = 0x00000030;//(将PA1引脚配置为推挽输出模式)
	


	while(1)
	{
		GPIOA_ODR = 0x00000002; //PA1引脚输出高电平
		Delay_s(1);
		GPIOA_ODR = 0x00000000;  //PA1引脚输出低电平
		Delay_s(1);
	}
}

在这里我在定义宏的时候将其前面的: *(unsigned int * )去掉了。
报错原因是说:自定义的这三个宏RCC_APB2ENR、GPIOA_CRL、GPIOA_ODR是不可修改的左值,不能被赋值。
那么这是为什么呢?
原因很简单:可以这样理解。
(I)#define RCC_APB2ENR 0x40021018
这种定义宏相当于定义的是一个宏常量RCC_APB2ENR,其值为0x40021018,自然不能修改宏常量的值。
(II)#define RCC_APB2ENR *(unsigned int*) 0x40021018
解释1:这是什么,这是在做映射。加上(unsigned int*),相当于将0x40021018强转为int型变量RCC_APB2ENR的地址,再加上*,取int型变量RCC_APB2ENR的值。即RCC_APB2ENR *(unsigned int*) 0x40021018为一个宏变量,可以对其进行赋值。(事实上C语言没有宏变量,我这样说只是为了便于理解)

解释2:这个宏定义实际上创建了一个指针表达式,(unsigned int*)0x40021018将地址0x40021018强制转换为一个指向unsigned int(无符号整型)的指针,然后*操作符表示解引用该指针,即访问该地址处的值。因此,RCC_APB2ENR不再是一个简单的数值常量,而是一个通过间接寻址访问特定内存地址的快捷方式。所以,当执行RCC_APB2ENR = 0x0004时,实际上是通过这个宏间接修改了地址0x40021018处的寄存器值,而不是修改宏变量自身。这种用法允许程序直接操控硬件寄存器,进行配置或读取操作。
解释二更好:这是一种间接寻址的方式。

②现象

20240518_001

(2)LED流水灯

①代码
#include "stm32f10x.h"                  // Device header
//用的引脚是PA1、PB12、PC13、PC15

#define RCC_APB2ENR  *(unsigned int*)0x40021018  //将APB2的时钟使能寄存器宏定义
//PA引脚寄存器宏定义
#define GPIOA_BASE   0x40010800  			//PA引脚的基地址
#define GPIOA_CRL    *(unsigned int*)0x40010800  //PA低位引脚寄存器
#define GPIOA_ODR    *(unsigned int*)0x4001080C  //PA引脚输出寄存器
//PB引脚寄存器宏定义
#define GPIOB_BASE   0x40010C00
#define GPIOB_CRH    *(unsigned int*)0x40010C04	
#define GPIOB_ODR    *(unsigned int*)0x40010C0C

//PB引脚寄存器宏定义
#define GPIOC_BASE   0x40011000
#define GPIOC_CRH    *(unsigned int*)0x40011004	
#define GPIOC_ODR    *(unsigned int*)0x4001100C

//法二 用自定义的宏封装寄存器,使LED灯闪烁
int main(void)
{
	
	
	//配置时钟
	RCC_APB2ENR = 0x001C;    //打开PA、PB、PC引脚的时钟

	//配置引脚
	GPIOA_CRL = 0x00000030;	   //	   将PA1引脚配置为推挽输出模式
	GPIOB_CRH = 0x00030000;	   //    将PB12引脚配置为推挽输出模式
	GPIOC_CRH = 0x30300000;	   //    将PC13和PC15引脚配置为推挽输出模式

	
	GPIOA_ODR = ~0x00000000;   
	GPIOB_ODR = ~0x00000000; 
	GPIOC_ODR = ~0x00000000; //引脚先全部置位高电平,都不亮。
	while(1)
	{
		
		
		GPIOA_ODR = ~0x00000002;  //PA1引脚输出低电平
		Delay_s(1);
		GPIOA_ODR = ~0x00000000;  //所有引脚输出高电平
		Delay_s(1);
		
		
		GPIOB_ODR = ~0x00001000;  //PB12引脚输出低电平
		Delay_s(1);
		GPIOB_ODR = ~0x00000000;  //所有引脚输出高电平
		Delay_s(1);
		
		
		GPIOC_ODR = ~0x00002000;  //PC13引脚输出低电平
		Delay_s(1);
		GPIOC_ODR = ~0x00000000;  //所有引脚输出高电平
		Delay_s(1);
		
		
		GPIOC_ODR = ~0x00008000;  //PC15引脚输出低电平
		Delay_s(1);
		GPIOC_ODR = ~0x00000000;  //所有引脚输出高电平
		Delay_s(1);
		
	}
	
}


②现象

20240518_002

三、总结

1.对于自定义宏封装寄存器的理解。
2.左移右移运算符的理解,虽然我没有用,我不喜欢用,但还是要理解。
3.对于特殊寄存器架构的理解,以及寄存器操控外设的理解。

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值