STM32F103系列芯片的地址映射和寄存器映射原理,以及GPIO端口的初始化设置


一、STM32F103系列芯片的地址映射和寄存器映射原理

1、什么是寄存器

(1)基本概念

现代的计算机主要包括三级存储,寄存器、内存储器和外存储器,存储数据的速率也依次递减。

寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令数据地址

存放数据的寄存器:如果你需要读取一个数据,直接到这个寄存器所在的地方来问问他,数据是多少就行了。问寄存器这个动作,叫做访问寄存器。不同的数据会存放在不同的寄存器,例如:引脚PA2与PB8的高低电平数据(1或0)肯定放在不同的寄存器里,那么怎么区分不同的寄存器呢?通过地址,不同的寄存器有不同的地址,就像老张行李寄存处在101号店铺,老王行李寄存处在258号店铺。

指令地址寄存器与数据寄存器类似,里边存放的都是0和1,只是特别的规定下,数据寄存器里面存放的0和1表示数据,指令寄存器里存放的表示指令。

(2)举例说明

比如,我们找到 GPIOB 端口的输出数据寄存器 ODR 的地址是 0x4001 0C0C(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解),ODR 寄存器是 32bit,低 16bit有效,对应着 16 个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指
针的操作方式,让 GPIOB 的 16 个 IO 都输出高电平,具体见代码

通过绝对地址访问内存单元

 // GPIOB 端口全部输出 高电平
 *(unsigned int*)(0x4001 0C0C) = 0xFFFF;

0x4001 0C0C在我们看来是 GPIOB端口 ODR的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int *)0x4001 0C0C,然后再对这个指针进行 * 操作。刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作,具体见代码

通过寄存器别名方式访问内存单元

 // GPIOB 端口全部输出 高电平
  #define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
 * GPIOB_ODR = 0xFF;

为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面,具体见代码

通过寄存器别名访问内存单元

 // GPIOB 端口全部输出 高电平
 #define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
 GPIOB_ODR = 0xFF

(3)地址映射

为了保证CPU执行指令时可正确访问存储单元,需将用户程序中的逻辑地址转换为运行时由机器直接寻址的物理地址,这一过程称为地址映射

2、地址映射和寄存器映射原理

(1)存储器映射

存储器本身没有地址,给存储器分配地址的过程叫存储器映射,我们知道地址是连续的如果我们需要给存储器分配地址,可以将地址分为多组,将每一组地址映射给对应的存储器,这就是存储器映射,具体见下图。
在这里插入图片描述

存储器区域功能划分
在这 4GB 的地址空间中,ARM 已经粗线条的平均分成了 8 个块,每块 512MB,每个块也都规定了用途,具体分类见表格 6-1。每个块的大小都有 512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。

存储器功能分类表
在这里插入图片描述

在这 8个 Block里面,有 3个块非常重要,也是我们最关心的三个块。Block0用来设计成内部 FLASH,Block1 用来设计成内部 RAM,Block2 用来设计成片上的外设。

(2)寄存器映射

以STM32为例,操作硬件本质上就是操作寄存器。在存储器片上外设区域,四字节为一个单元,每个单元对应不同的功能。当我们控制这些单元时就可以驱动外设工作,我们可以找到每个单元的起始地址,然后通过C 语言指针的操作方式来访问这些单元。但若每次都是通过这种方式访问地址,不好记忆且易出错。这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名实质上就是寄存器名字。给已分配好地址(通过存储器映射实现)的有特定功能的内存单元取别名的过程就叫寄存器映射。

GPIOA下的某个寄存器,挂载在GPIOA下,地址为GPIOA基地址+偏移量;
GPIOA挂载在APB2总线,地址为APB2总线基地址+GPIOA偏移量;
ABP2挂载加外设基地址,地址为外设基地址+ABP2偏移量

下面通过查找GPIOB端口相关寄存器进行举例:

第一步,找到GPIOB的基地址
也就是找到GPIOB的小区。结论是,所有GPIOB相关的寄存器,都住在0x4001 0C00到0x4001 0FFF范围内。
在这里插入图片描述

第二步,找到端口输入寄存器的地址偏移
找到存储数据的地址,结论是0x4001 0C00+0x08 = 0x4001 0C08
在这里插入图片描述

第三步,找到存储数据的最终位置
PB3的数据位于从右往左数第4个。
在这里插入图片描述

我们可以简单地直接访问这个地址:

unsigned int *GPIOB_IDR=(unsigned int *)0x40010C08;
unsigned char PB3 = *GPIOB_IDR & 0x8;//取出从右往左数的第4位

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

二、GPIO初始化设置步骤(时钟配置、输入输出模式设置、最大速率设置)

(1)基本介绍

GPIO 是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,STM32 芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。STM32 芯片的 GPIO 被分成很多组,每组有 16 个引脚,如型号为 STM32F103VET6 型号的芯片有 GPIOA、GPIOB、GPIOC至 GPIOE共 5组 GPIO,芯片一共 100个引脚,其中 GPIO就占了一大部分,所有的 GPIO 引脚都有基本的输入输出功能。

最基本的输出功能
是由 STM32 控制引脚输出高、低电平,实现开关控制,如把 GPIO引脚接入到 LED灯,那就可以控制 LED灯的亮灭,引脚接入到继电器或三极管,那就可以通过继电器或三极管控制外部大功率电路的通断。

最基本的输入功能
检测外部输入电平,如把 GPIO 引脚连接到按键,通过电平高低区分按键是否被按下。

(2)工作模式

GPIO框架图
在这里插入图片描述
由 GPIO 的结构决定了 GPIO 可以配置成以下模式:

由 GPIO 的结构决定了 GPIO 可以配置成以下模式:
GPIO 8 种工作模式:
typedef enum{
	GPIO_Mode_AIN = 0x0, // 模拟输入
 	GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入
 	GPIO_Mode_IPD = 0x28, // 下拉输入
	GPIO_Mode_IPU = 0x48, // 上拉输入
	GPIO_Mode_Out_OD = 0x14, // 开漏输出
	GPIO_Mode_Out_PP = 0x10, // 推挽输出
	GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出
	GPIO_Mode_AF_PP = 0x18 // 复用推挽输出
} GPIOMode_TypeDef;

在固件库中,GPIO 总共有 8 种细分的工作模式,稍加整理可以大致归类为以下三类:

输入模式(模拟/浮空/上拉/下拉)
在输入模式时,施密特触发器打开,输出被禁止,可通过输入数据寄存器 GPIOx_IDR
读取 I/O 状态。其中输入模式,可设置为上拉、下拉、浮空和模拟输入四种。上拉和下拉
输入很好理解,默认的电平由上拉或者下拉决定。浮空输入的电平是不确定的,完全由外
部的输入决定,一般接按键的时候用的是这个模式。模拟输入则用于 ADC 采集。

输出模式(推挽/开漏)
在输出模式中,推挽模式时双 MOS 管以轮流方式工作,输出数据寄存器 GPIOx_ODR
可控制 I/O 输出高低电平。开漏模式时,只有 N-MOS 管工作,输出数据寄存器可控制 I/O
输出高阻态或低电平。输出速度可配置,有 2MHz\10MHz\50MHz的选项。此处的输出速度
即 I/O 支持的高低电平状态最高切换频率,支持的频率越高,功耗越大,如果功耗要求不
严格,把速度设置成最大即可。

在输出模式时施密特触发器是打开的,即输入可用,通过输入数据寄存器 GPIOx_IDR
可读取 I/O 的实际状态。

复用功能(推挽/开漏)
复用功能模式中,输出使能,输出速度可配置,可工作在开漏及推挽模式,但是输出信号源于其它外设,输出数据寄存器 GPIOx_ODR 无效;输入可用,通过输入数据寄存器可获取 I/O 实际状态,但一般直接用外设的寄存器来获取该数据信号。

(3)初始化

下面以操作寄存器来点亮LED了解GPIO初始化步骤

1. 首先配置时钟使能

时钟控制名字叫做RCC,属于AHB总线。GPIOB属于APB2
在这里插入图片描述
由下图可知AHB总线包含RCC时钟控制,GPIO是属于APB2
在这里插入图片描述
 我们已经知道,GPIO端口B的地址从0x4001 0C00开始。接下来只寻找时钟使能寄存器的地址:
 复位和时钟控制RCC的地址从0x4002 1000开始

 
APB2外设时钟使能寄存器(RCC_APB2ENR),偏移地址是0x18,所以APB2的地址就是0x4002 1018。
RCC_APB2ENR,位3是IOPBEN,名字是IO端口B时钟使能,就是我们想要的。把RCC_APB2ENR的位3赋值为1,就是开启GPIOB时钟。

在这里插入图片描述

2. 配置为通用输出

由于STM32的每个IO都需要4个位来配置,所以一个32位的寄存器最大只能配置8个IO,由于STM32的每个IO都需要4个位来配置,所以一个32位的寄存器最大只能配置8个IO

查看原理图可以看到点亮红色灯为PB5
在这里插入图片描述

配置引脚PB5,使用的寄存器是GPIOB_CRL。下面我们来寻找这个寄存器的地址。
在这里插入图片描述

那么需要的寄存器是低位的寄存器GPIOB_CRL,它的地址是0x4001 0c00,端口号为5
找到需要操作的寄存器后,把它配置为通用输出。 我们需要的是输出高低电平,所以要设置为输出。输出模式又有好几种输出:

00:通用推挽输出模式
01:通用开漏输出模式
10:复用功能推挽输出模式
11:复用功能开漏输出模式

由此可得:

// 开启GPIOB 端口时钟
	RCC_APB2ENR |= (1<<3);

	//清空控制PB5的端口位
	GPIOB_CRL &= ~( 0x0F<< (4*5));	
	// 配置PB5为通用推挽输出,速度为10M
	GPIOB_CRL |= (1<<4*5);

这样就把对应的IO口输入输出模式调好了

3. 设置输出值

查看GPIO输出文档(GPIOx_ODR)
在这里插入图片描述

地址偏移量0x0c,由于低电平有效所以需要将第5位设置成0,其它端口设置成1,可以实现LED的点亮

// PB5 输出 低电平
	GPIOB_ODR &= ~(1<<5);

三、解决两个问题

1.嵌入式C程序代码对内存(RAM)中的各变量的修改操作,与对外部设备(寄存器—>对应相关管脚)的操作有哪些相同与差别?

嵌入式C程序代码对内存(RAM)中的变量和对外设(寄存器)的操作,主要差别如下:

相同点:

都可以通过赋值操作来修改值,如int a=10; write_reg(REG, 20);

都可以通过指针来访问,如*ptr = 30;

差异点:

存储位置不同,变量在RAM中,外设寄存器在外设芯片内部。

访问方法不同,变量直接操作,外设需要通过读写函数接口。

访问速度,RAM访问速度快,外设寄存器通过总线需要更长时间。

访问范围,变量任意读写,外设寄存器只能访问已开放的范围。

初始化需求,变量可以不初始化直接使用,外设需要在使用前初始化外设和寄存器。

错误处理,变量错误难定位,外设可以通过状态位判断错误类型。

访问权限,变量在程序内任意访问,外设需要按手册了解每个寄存器的读写属性。

所以总体来说,变量访问更简单直接,外设寄存器访问需要考虑外设接口规范和性能限制。这给嵌入式程序设计带来一定的难度。

2、为什么51单片机的LED点灯编程要比STM32的简单?
51单片机LED点灯编程比STM32简单主要有以下几点原因:

51单片机IO口直接连接LED,只需设置IO口为输出模式并输出高电平即可点亮LED。STM32需要通过外设驱动LED,增加了一层外设驱动的复杂度。

51单片机IO口操作直接使用单指令SETB/CLR即可,STM32需要配置GPIO外设寄存器,增加了配置步骤。

51单片机没有时钟和电源管理模块,LED点亮不需要配置这些外设。STM32需要配置时钟树、电源管理等外设。

51单片机没有内存保护,直接使用数据区域内存操作IO口。STM32需要考虑内存映射区和外设寄存器访问权限。

51单片机开发环境相对简单,如KEIL只需添加几行代码即可。STM32需要使用更复杂的开发环境如MDK/IAR/SW4STM32。

51单片机指令集简单,不需要学习复杂的ARM指令。STM32需要学习ARM指令集知识。

51单片机例程少,例如LED点亮只需几行代码。STM32例程复杂度较高,需要了解更多外设知识。

所以总体来说,51单片机的硬件结构和软件开发环境都较为简单,对初学者LED点亮编程难度小,上手更快。STM32由于外设驱动的增加,开发难度相对更高。

四、解释嵌入式中常见的register和volatile 关键字

与PC平台上的一般程序不同,嵌入式C程序经常会看见 register和volatile 关键字,下面我们将解释这两个变量修饰符的作用,并用C代码示例进行说明。

1、register关键字

(1)介绍

在嵌入式系统中,寄存器是位于CPU内部的高速存储器,用于存储临时数据和执行指令。使用寄存器变量可以提高程序的执行速度和效率,因为寄存器的访问速度比内存快得多。

当使用register关键字声明变量时,编译器会尽可能地将该变量存储在寄存器中,以便快速访问。然而,嵌入式系统的编译器可能会忽略register关键字,因为寄存器的数量有限,编译器需要根据需要进行优化和分配寄存器。

(2)应用

关键性能代码:对于一些关键性能代码,例如内部循环或频繁执行的代码段,可以使用register关键字声明相关的变量。这样可以减少对内存的访问延迟,从而提高代码的执行速度。

中断处理程序:在嵌入式系统中,中断处理程序的执行时间通常要求非常短。通过使用register关键字声明一些关键变量,可以减少对内存的访问,提高中断处理程序的响应速度。

硬件接口:与外部硬件设备进行通信时,通常需要频繁读写寄存器。通过使用register关键字声明与硬件接口相关的变量,可以加快对寄存器的访问速度,提高数据传输的效率。

(3)示例代码

#include <stdio.h>
int main()
{
	register int count=0;//使用register关键字声明变量count
	
	while(count<10)
	{
		printf("The value of count is: %d\n",count);
		count++;
	 } 
	
	return 0; 
}

在上面的示例中,我们使用 “register” 关键字声明了一个整数变量 “count”。这将提示编译器将该变量存储在寄存器中,以提高访问速度。然而,实际上,编译器可能会忽略 “register” 关键字,并根据优化策略自动决定变量的存储位置。

2、volatile关键字

(1)介绍

在嵌入式系统中,volatile关键字用于告诉编译器变量的值可能会在意料之外的时间被修改,因此编译器不应该对该变量进行优化。

嵌入式系统中,有些变量的值可能会被硬件或者其他任务异步地修改,而编译器通常会对变量进行优化,例如将变量的值缓存在寄存器中,以提高访问速度。然而,这种优化可能会导致程序出现错误,因为编译器不知道变量的值可能会在意料之外的时间被修改。

使用volatile关键字可以告诉编译器不要对变量进行优化,每次访问变量时都从内存中读取或写入变量的值。这样可以确保程序始终使用最新的变量值,而不是使用缓存的值。

(2)应用

外设寄存器:嵌入式系统通常需要与外部设备进行通信,例如控制器、传感器等。这些设备通常通过特定的寄存器与嵌入式系统进行交互。使用volatile关键字可以确保每次访问寄存器时都是从内存中读取或写入最新的值。

中断处理程序:嵌入式系统经常会使用中断来处理外部事件,例如定时器溢出、外部输入等。中断处理程序通常需要访问和更新共享的状态变量。使用volatile关键字可以确保中断处理程序对这些变量的访问是原子的,并且不会被编译器优化。

多任务间通信:在多任务系统中,任务之间需要进行通信和共享数据。使用volatile关键字可以确保任务在读取和修改共享数据时,始终使用最新的值,避免数据不一致性的问题。

嵌入式系统的状态变量:嵌入式系统通常会有一些状态变量,用于表示系统的状态或者标志位。这些变量可能会被不同的任务或者中断处理程序修改。使用volatile关键字可以确保对这些状态变量的读取和修改是可见的,并且不会被编译器优化。

(3)示例代码

示例代码1:
在程序中对GPIO相关寄存器的定义

1. #define PINSEL0 (*((volatile unsigned long *) 0xE002C000))
2. #define PINSEL1 (*((volatile unsigned long *) 0xE002C004))
3. #define PINSEL2 (*((volatile unsigned long *) 0xE002C008))
4. #define PINSEL3 (*((volatile unsigned long *) 0xE002C00C))

寄存器的定义应该用volatile修饰,避免其在编译过程中被编译器优化,产生意想不到的后果。

示例代码2:

#include <stdio.h>

int main()
{
	
	volatile int x=5; //使用volatile关键字声明变量x
	
	while (x==5)
	{
		//循环等待,编译器不会优化对x的读取 
		
	} 
	
	return 0; 
	
 } 

在上面的代码中,使用volatile关键字声明了变量x。循环会持续等待,因为编译器会将对变量x的读取保留在循环中,以便在变量发生改变时立即响应。


总结

1)嵌入式C程序代码对内存(RAM)中的各变量的修改操作,与对外部设备(寄存器—>对应相关管脚)的操作有哪些相同与差别?

在嵌入式C程序中,对内存中的变量进行修改操作和对外部设备的操作有相同和差异之处。相同之处是,无论是修改内存中的变量还是对外部设备进行操作,都需要通过特定的指令或函数来进行读写操作。此外,无论是修改内存中的变量还是对外部设备进行操作,都需要使用特定的地址或寄存器来定位变量或设备,并且通过读取或写入相应的值来改变状态或数据。

2)为什么51单片机的LED点灯编程要比STM32的简单?

LED点灯编程在51单片机上相对简单的原因主要有以下几点:首先,51单片机的引脚功能相对简单,通常只需要将引脚设置为输出模式并给相应的引脚赋值即可实现LED的点亮和熄灭。其次,51单片机的开发环境和编程工具相对成熟,有很多成熟的开发板和开发套件可供选择,开发者可以直接使用这些工具进行开发,减少了开发的复杂性。此外,51单片机的指令集相对简单,编程接口也相对统一,具有一定的易用性和可读性。

参考文献

STM32F103系列芯片的地址和寄存器映射原理、LED轮流闪烁实现

STM32F103系列芯片的地址映射和寄存器映射原理&&GPIO端口的初始化设置

STM32F103系列芯片的地址和寄存器映射原理、LED轮流闪烁实现

嵌入式中的 register和volatile关键字