OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 26|回复: 0

《STM32H7R7开发指南 V1.1 》第五章 STM32基础知识入门

[复制链接]

1285

主题

1299

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
5507
金钱
5507
注册时间
2019-5-8
在线时间
1446 小时
发表于 2 小时前 | 显示全部楼层 |阅读模式
第五章 STM32基础知识入门

1)实验平台:正点原子STM32H7R7开发板

2)章节摘自【正点原子】STM32H7R7开发指南 V1.1

3)购买链接: https://detail.tmall.com/item.htm?id=820823382459

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32h7rx.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)正点原子STM32开发板技术交流群:756580169


2.jpg

3.png

本章,我们着重介绍STM32的一些基础知识,让大家对STM32开发有一个初步的了解,为后面STM32的学习做铺垫,方便后面的学习。本章内容大家第一次看的时候可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。
本章将分为如下几个小节:
5.1 C语言基础知识复习
5.2 寄存器基础知识
5.3 STM32H7R7系统架构


5.1 C语言基础知识复习
本节我们给大家介绍一下C语言基础知识,对于C语言比较熟练的读者,可以跳过此节,对于基础比较薄弱的读者,建议好好学习一下本节内容。
由于C语言博大精深,不可能我们一小节就全讲明白了,所以本节我们只是复习STM32开发时常用的几个C语言知识点,以便大家的更好的学习并编写STM32代码。


5.1.1 位操作
C语言位操作相信学过C语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面我们先讲解几种位操作符,然后讲解位操作使用技巧。C语言支持如下6种位操作:

1.png
表5.1.1.1 六种位操作

这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信大家学C语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。

1,在不改变其他位的值的状况下,对某几个位进行设值。
这个场景在单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,然后用 | 操作符设值。比如我要改变GPIOA->ODR的状态,可以先对寄存器的值进行&清零操作:

  1. GPIOA->ODR &= 0XFF0F;    /* 将第4-7位清0 */
复制代码
然后再与需要设置的值进行|或运算:
  1. GPIOA->ODR |= 0X0040;    /* 设置bit6的值为1,不改变其他位的值 */
复制代码

2,移位操作提高代码的可读性。
移位操作在单片机开发中非常重要,我们来看看下面一行代码:

  1. SysTick->CTRL |= 1 << 1;
复制代码
这个操作就是将CTRL寄存器的第1位(从0开始算起)设置为1,为什么要通过左移而不是直接设置一个固定的值呢?其实这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第1位设置为1。如果写成:
  1. SysTick->CTRL |= 0X0002;
复制代码
这个虽然也能实现同样的效果,但是可读性稍差,而且修改也比较麻烦。

3,~按位取反操作使用技巧
按位取反在设置寄存器的时候经常被使用,常用于清除某一个/某几个位。我们来看看下面一行代码:

  1. SysTick->CTRL &= ~(1 << 0) ;           /* 关闭SYSTICK */
复制代码
该代码可以解读为 仅设置CTRL寄存器的第0位(最低位)为0,其他位的值保持不变。同样我们也不使用按位取反,将代码写成:
  1. SysTick->CTRL &= 0XFFFFFFFE;    /* 关闭SYSTICK */
复制代码
可见前者的可读性,及可维护性都要比后者好很多。

4,^按位异或操作使用技巧
该功能非常适合用于控制某个位翻转,常见的应用场景就是控制LED闪烁,如:

  1. GPIOB->ODR ^= 1 << 5;
复制代码
执行一次该代码,就会使PB5的输出状态翻转一次,如果我们的LED接在PB5上,就可以看到LED闪烁了。

5.1.2 define宏定义
define是C语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供方便。常见的格式:
  1. #define         标识符       字符串
复制代码
“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:
  1. #define HSE_VALUE    ((uint32_t)16000000)
复制代码
定义标识符HSI_VALUE的值为16000000。这样我们就可以在代码中直接使用标识符HSI_VALUE,而不用直接使用常量16000000,同时也很方便我们修改HSI_VALUE的值。
至于define宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。


5.1.3 ifdef条件编译
单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:
  1. #ifdef 标识符
  2. 程序段1
  3. #else
  4. 程序段2
  5. #endif
复制代码
它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。 其中#else部分也可以没有,即:
  1. #ifdef
  2.        程序段1
  3. #endif
复制代码
条件编译在MDK里面是用得很多,在stm32h7xx_hal_conf.h这个头文件中经常会看到这样的语句:
  1. #ifdef  HAL_GPIO_MODULE_ENABLED
  2.       #include "stm32f7xx_hal_gpio.h"
  3. #endif
复制代码
这段代码的作用是判断宏定义标识符HAL_GPIO_MODULE_ENABLED是否被定义,如果被定义了,那么就引入头文件stm32h7xx_hal_gpio.h。条件编译也是C语言的基础知识,这里也就点到为止吧。

5.1.4 extern外部申明
C语言中extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于extern申明变量可以多次,但定义只有一次。在我们的代码中你会看到看到这样的语句:
extern uint16_t g_usart_rx_sta;
这个语句是申明g_usart_rx_sta变量在其他文件中已经定义了,在这里要使用到。所以,你肯定可以找到在某个地方有变量定义的语句:

  1. uint16_t g_usart_rx_sta;
复制代码
extern的使用比较简单,但是也会经常用到,需要掌握。

5.1.5 typedef类型别名
typedef用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。typedef在MDK用得最多的就是定义结构体的类型别名和枚举类型了。
  1. struct _GPIO
  2. {
  3.         __IO uint32_t MODER;
  4.         __IO uint32_t OTYPER;
  5.         …
  6. };
复制代码
定义了一个结构体GPIO,这样我们定义结构体变量的方式为:
  1. struct  _GPIO  gpiox;                /* 定义结构体变量gpiox */
复制代码
但是这样很繁琐,MDK中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别名GPIO_TypeDef,这样我们就可以在其他地方通过别名GPIO_TypeDef来定义结构体变量了,方法如下:
  1. typedef struct
  2. {
  3.         __IO uint32_t MODER;
  4.         __IO uint32_t OTYPER;
  5.         …
  6. } GPIO_TypeDef;
复制代码
Typedef为结构体定义一个别名GPIO_TypeDef,这样我们可以通过GPIO_TypeDef来定义结构体变量:
  1. GPIO_TypeDef gpiox;
复制代码
这里的GPIO_TypeDef就跟struct _GPIO是等同的作用了,但是GPIO_TypeDef使用起来方便很多。

5.1.6 结构体
经常很多用户提到,他们对结构体使用不是很熟悉,但是MDK中太多地方使用结构体以及结构体指针,这让他们一下子摸不着头脑,学习STM32的积极性大大降低,其实结构体并不是那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下面的“寄存器映射”中讲到一些。
声明结构体类型:

  1.     struct 结构体名
  2.     {
  3.         成员列表;
  4.     }变量名列表;
复制代码
例如:
  1.     struct U_TYPE
  2.     {
  3.         int BaudRate
  4.         int WordLength;
  5.     }usart1, usart2;
复制代码
在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:
  1.     struct  结构体名字   结构体变量列表 ;
复制代码
例如:
  1. struct U_TYPE usart1,usart2;
复制代码
结构体成员变量的引用方法是:
  1. 结构体变量名字.成员名
复制代码
比如要引用usart1的成员BaudRate,方法是:usart1.BaudRate; 结构体指针变量定义也是一样的,跟其他变量没有啥区别。
例如:

  1.     struct U_TYPE *usart3;    /* 定义结构体指针变量usart3 */
复制代码
结构体指针成员变量引用方法是通过“->”符号实现,比如要访问usart3结构体指针指向的结构体的成员变量BaudRate,方法是:
  1.      usart3->BaudRate;
复制代码
上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实例回答一下这个问题。
在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态是由几个属性来决定的,比如串口号,波特率,极性,以及模式。对于这种情况,在我们没有学习结构体的时候,我们一般的方法是:

  1. void usart_init(uint8_t usartx, uiut32_t BaudRate, uint32_t Parity,
  2. uint32_t Mode);
复制代码
这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里面再传入一个/几个参数,那么势必我们需要修改这个函数的定义,重新加入新的入口参数,随着开发不断的增多,那么是不是我们就要不断的修改函数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?
我们使用结构体参数,就可以在不改变入口参数的情况下,只需要改变结构体的成员变量,就可以达到改变入口参数的目的。
结构体就是将多个变量组合为一个有机的整体,上面的函数,usartx,BaudRate,Parity, Mode等这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK中是这样定义的:

  1.     typedef struct
  2.     {
  3.         uint32_t BaudRate;
  4.         uint32_t WordLength;
  5.         uint32_t StopBits;
  6.         uint32_t Parity;
  7.         uint32_t Mode;
  8.         uint32_t HwFlowCtl;
  9.         uint32_t OverSampling;  
  10.     } UART_InitTypeDef;
复制代码
这样,我们在初始化串口的时候入口参数就可以是USART_InitTypeDef类型的变量或者指针变量了,于是我们可以改为:
  1.     void usart_init(UART_InitTypeDef *huart);
复制代码
这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义就可以达到增加变量的目的。
理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可以提高你的代码的可读性。
使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作用就远远不止这个了,同时,MDK中用结构体来定义外设也不仅仅只是这个作用,这里我们只是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲解结构体的一些其他知识。


5.1.7 指针
指针是一个值指向地址的变量(或常量),其本质是指向一个地址,从而可以访问一片内存区域。在编写STM32代码的时候,或多或少都要用到指针,它可以使不同代码共享同一片内存数据,也可以用作复杂的链接性的数据结构的构建,比如链表,链式二叉树等,而且,有些地方必须使用指针才能实现,比如内存管理等。
申明指针我们一般以p开头,如:

  1.     char * p_str = “This is a test!”;
复制代码
这样,我们就申明了一个p_str的指针,它指向This is a test!这个字符串的首地址。我们编写如下代码:
  1. int main(void)
  2. {
  3.     uint8_t temp = 0X88;               /* 定义变量 temp */
  4.     uint8_t *p_num = &temp;            /* 定义指针p_num,指向temp地址 */

  5.     sys_mpu_config();                              /* 配置MPU */
  6.     sys_cache_enable();                            /* 使能Cache */
  7.     HAL_Init();                                    /* 初始化HAL库 */
  8.     sys_stm32_clock_init(240, 5, 2);               /* 配置时钟,600MHz */
  9.     delay_init(600);                               /* 初始化延时 */
  10.     usart_init(115200);                            /* 初始化串口 */
  11.     printf("temp:0X%X\r\n", temp);                 /* 打印temp的值 */
  12.     printf("*p_num: 0X %X\r\n", *p_num);           /* 打印*p_num的值 */
  13.     printf("p_num: 0X %X\r\n", (uint32_t)p_num);   /* 打印p_num的值 */
  14.     printf("&p_num: 0X %X\r\n", (uint32_t)&p_num); /* 打印&p_num的值 */
  15.     while (1);
  16. }
复制代码
此代码的输出结果为:

第五章 STM32基础知识入门6158.png
图5.1.7.1 输出结果

p_num:是uint8_t类型指针,指向temp变量的地址,其值等于temp变量的地址。
*p_num:取p_num指向的地址所存储的值,即temp的值。
&p_num:取p_num指针的地址,即指针自身的地址。
以上,就是指针的简单使用和基本概念说明,指针的详细知识和使用范例大家可以百度学习,网上有非常多的资料可供参考。指针是C语言的精髓,在后续的代码中我们将会大量用到各种指针,大家务必好好学习和了解指针的使用。


5.2 寄存器基础知识
寄存器(Register)是单片机内部一种特殊的内存,它可以实现对单片机各个功能的控制,简单的来说可以把寄存器当成一些控制开关,控制包括内核及外设的各种状态。所以无论是51单片机还是STM32,都需要用寄存器来实现各种控制,以完成不同的功能。
由于寄存器资源非常宝贵,一般都是一个位或者几个位控制一个功能,对于STM32来说,其寄存器是32位的,一个32位的寄存器,可能会有32个控制功能,相当于32个开关,由于STM32的复杂性,它内部有几百个寄存器,所以整体来说STM32的寄存器还是比较复杂的。不过,我们不要被其吓到了,实际上STM32是由于内部有很多外设,所以导致寄存器很多,实际上我们把它分好类,每个外设也就那么几个或者几十个寄存器,学起来就不难了。
从大方向来区分,STM32寄存器分为两类,如表5.2.1所示:


2.png
表5.2.1 STM32寄存器分类

其中,内核寄存器,我们一般只需要关心中断控制寄存器和SysTick寄存器即可,其他三大类,我们一般很少直接接触。而外设寄存器,则是学到哪个外设,就了解哪个外设相关寄存器即可,所以整体来说,我们需要关心的寄存器并不是很多,而且很多都是有共性的,比如STM32H7R7L8H6H有23个定时器,我们只需要学习了其中一个的相关寄存器,其他22个基本都是一样。
说了这么多,给大家举个简单的例子,我们知道寄存器的本质是一个特殊的内存,对于STM32来说,以GPIOB的ODR寄存器为例,其寄存器地址为:0X40020414,所以我们对齐赋值可以写成:
  1.     *(unsigned int *))(0X40020414) = 0XFFFF;
复制代码
这样我们就完成了对GPIOB->ODR寄存器的赋值,全部0XFFF,表示GPIOB所有IO口(16个IO口)都输出高电平。对于我们来说,0X40020414就是一个寄存器的特殊地址,至于它是怎么来的,我们后续再给大家介绍。
虽然上面的代码实现了我们需要的功能,但是从实用的角度来说,这么写肯定是不好的,可读性极差,可维护性也很差,所以一般我们使用结构体来访问,比如改写成这样:
  1. GPIOB->ODR = 0XFFFF;
复制代码
这样可读性就比之前的代码好多了,可维护性也相对好一点。至于GPIOB结构体怎么来的,我们也会在后续给大家介绍。

5.3 STM32H7R7系统架构
STM32H7R7是ST公司基于ARM授权Cortex M7内核而设计的一款芯片,而Cortex M内核使用的是ARM v7-M架构,是为了替代老旧的单片机而量身定做的一个内核,具有低成本、低功耗、实时性好、中断响应快、处理效率高等特点。

5.3.1 Cortex M7内核 & 芯片
ARM公司提供内核(如Cortex M7,简称CM7,下同)授权,完整的MCU还需要很多其他组件。芯片公司(ST、NXP、TI、GD、华大等)在得到CM7内核授权后,就可以把CM7内核用在自己的硅片设计中,添加:存储器,外设,I/O 以及其它功能块。不同厂家设计出的单片机会有不同的配置,包括存储器容量、类型、外设等都各具特色,因此才会有市面上各种不同应用的ARM芯片。Cortex M7内核和芯片的关系如图5.3.1.1所示:

第五章 STM32基础知识入门8039.png
图5.3.1.1 Cortex M7内核 & 芯片关系

可以看到,ARM公司提供CM7内核和调试系统,其他的东西(外设(IIC、SPI、UART、TIM等)、存储器(SRAM、FLASH等)、I/O等)由芯片制造商设计开发。这里ST公司就是STM32H7R7芯片的制造商。

5.3.2 STM32系统架构
STM32H7R7内部系统结构如图5.3.2.1:

第五章 STM32基础知识入门8221.png
图5.3.2.1 STM32H7R7系统结构

上图把STM32H7R7的每条总线,以及每条总线挂载了哪些外设都整理出来,这对我们理解整个STM32H7R7总的框架非常有帮助。
这个图这么复杂,该怎么看?我一般习惯以内核为起点,先看总线,再看总线连接关系,最后再看各个总线分别连接了什么外设,按照这样的顺序查阅,就会清晰很多。
在图5.3.2.1中,我们标记了①②三个标号,①是64位的AXI总线矩阵,②是32位的AHB总线矩阵。①②的关系,我们看下面这张图更能清晰的表达出来,如图5.3.2.2所示。


第五章 STM32基础知识入门8476.png
图5.3.2.2 STM32H7R7的总线架构图

从上图中可以看到STM32H7R7的总线架构图比STM32H7R7的系统总架构图有简化些,就是把STM32H7R7的系统总架构图中的总线部分提取出来,大家可以把它们结合一起看。
图5.3.2.2中的两个域就对应了前面的①②,其中区域①是AXI总线,区域②是AHB总线的。下面分别介绍一下这两个域:
&#61548;区域①
区域①中的各个外设是挂在 64 位 AXI 总线组成 11*10 的矩阵上。11个从接口端外接的主控分别是 SDMMC1、HPDMA1、GPU ICACHE AXI、AXI、DCMIPP、DMA2D、GFXMMMU、ICACHE、AXIM 和 AHB 总线。10 个主接口端外接的从设备分别是AHB 总线、MCE3、MCE1、MCE2、SRAM4、SRAM3、SRAM2、SRAM1和FLASH。
&#61548;区域②
区域②的各个外设是挂在 32 位 AHB 总线组成 8*9 的矩阵上。8个从接口外接的主控分是 AXI 总线、CM7_AHBP 总线、AHB总线、GPDAM1、ETH1、SDMMC2和OTG_HS。9 个主接口外接的从设备分别是 AHB1、SRAM1、SRAM2、AHB2、AHB3、AHB4、AHB6、AXI和AHB5。
总线结构的内容多且复杂,大家也不要想一次阅读就能掌握,把后面的实验都学习了,不懂就回来查阅(请结合官方原文),把后面的实验学习完,就会越来越清晰了。


5.3.3 存储器映射
STM32是一个32位单片机,他可以很方便的访问4GB以内的存储空间(2^32 = 4GB),因此Cortex M7内核将图5.3.2.1中的所有结构,包括:FLASH、SRAM、外设及相关寄存器等全部组织在同一个4GB的线性地址空间内,我们可以通过C语言来访问这些地址空间,从而操作相关外设(读/写)。数据字节以小端格式(小端模式)存放在存储器中,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
存储器本身是没有地址信息的,我们对存储器分配地址的过程就叫存储器映射。这个分配一般由芯片厂商做好了,ST将所有的存储器及外设资源都映射在一个4GB的地址空间上(8个块),从而可以通过访问对应的地址,访问具体的外设。其映射关系如图5.3.3.1所示:


第五章 STM32基础知识入门9444.png
图5.3.3.1 STM32H7R7存储器映射

存储块功能介绍
ST将4GB空间分成8个块,每个块512MB,如上图所示,从图中我们可以看出有很多保留区域(Reserved),这是因为一般的芯片制造厂家是不可能把4GB空间用完的,同时,为了方便后续型号升级,会将一些空间预留(Reserved)。8个存储块的功能如表5.3.3.1所示:


3.png
表5.3.3.1 STM32 存储块功能及地址范围

这里我们重点挑前面3个存储块给大家介绍。第一个块是Block 0,用于存储代码,即FLASH空间,其功能划分如表5.3.3.2所示:

4.png
表5.3.3.2 STM32 存储块0的功能划分

可以看到,我们用户FLASH大小是64KB,这是对于我们使用的STM32H7R7L8H6H来说,如果是其他型号, FLASH就可能不一样了。 当然,如果ST喜欢,也是可以随时推出更大容量的STM32H7R7单片机的,因为这里保留了一大块地址空间。但是ST为降低成本,打造高性能、低价位的芯片,所以设置了这款STM32H7R7L8H6H芯片。因为只有64KB的FLASH,所以我们用到外部FLASH来扩容。STM32H7R7的出厂固化BootLoader占用差不多60KB FLASH空间。DTCM RAM可用于运行指令,也可以存储数据。
第二个块是Block 1,用于存储数据,即RAM空间,其功能划分如表5.3.3.3所示:


5.png
表5.3.3.3 STM32 存储块1的功能划分

可以看到STM32H7R7的AXI SRAM和SRAM分配基本不是连续的地址空间,我们重点注意各块SRAM的特性对比,具体如下:
DTCM RAM,用于数据存取,特点是速度很快,和内核一样。
AXI SRAM,数据带宽是64bit,挂在AXI总线上。
SRAM1,数据带宽是32bit,挂在AHB总线上。
SRAM2:数据带宽是32bit,挂在AHB总线上。
Backup SRAM,数据带宽是32bit,挂在AHB总线上。主要用于系统进入低功耗模式后,继续保存数据(Vbat引脚外接电池)。
大家可以参考5.3.2小节,进行理解。
第三个块是Block 2,用于外设访问,STM32内部大部分的外设都是放在这个块里面的。该存储块被分成了APB和AHB两部分,其中APB又被分为APB1,APB2,APB4和APB5。AHB分为AHB1,AHB2,AHB3,AHB4和AHB5。Block 2功能划分如表5.3.3.4所示:


6.png
表5.3.3.4 STM32 存储块2的功能划分

同样可以看到,各个总线之间,都有预留地址空间,方便后续扩展。关于STM32各个外设具体挂在哪个总线上面,大家可以参考前面的 STM32H7R7系统结构图进行查找对应。

5.3.4 寄存器映射
给存储器分配地址的过程叫存储器映射,寄存器是一类特殊的存储器,它的每个位都有特定的功能,可以实现对外设/功能的控制,给寄存器的地址命名的过程就叫寄存器映射。
举个简单的例子,大家家里面的纸张就好比通用存储器,用来记录数据是没问题的,但是不会有具体的动作,只能做记录,而你家里面的电灯开关,就好比寄存器了,假设你家有8个灯,就有8个开关(相当于一个8位寄存器),这些开关也可以记录状态,同时还能让电灯点亮/关闭,是会产生具体动作的。为了方便区分和使用,我们会给每个开关命名,比如厨房开关、大厅开关、卧室开关等,给开关命名的过程,就是寄存器映射。
当然STM32内部的寄存器有非常多,远远不止8个开关这么简单,但是原理是差不多的,每个寄存器的每一个位,一般都有特定的作用,涉及到寄存器描述,大家可以参考《STM32H7Rx参考手册_V6(英文版).pdf》相关章节的寄存器描述部分,有详细的描述。
1. 寄存器描述解读
我们以GPIO的ODR寄存器为例,其参考手册的描述如图5.3.4.1所示:


第五章 STM32基础知识入门13021.png
图5.3.4.1 端口输出数据寄存器描述

①寄存器名字
每个寄存器都有一个对应的名字,以简单表达其作用,并方便记忆,这里GPIOx_ODR表示寄存器英文名,x可以从A~G,M~P,说明有11个这样的寄存器(每个端口有一个)。
②寄存器偏移量及复位值
地址偏移量表示相对该外设基地址的偏移,比如GPIOB,我们由《STM32H7Rx参考手册_V6(英文版).pdf》文档的第153页,可知其外设基地址是:0x58020400。那么GPIOB_ODR寄存器的地址就是:0x58020414。知道了外设基地址和地址偏移量,我们就可以知道任何一个寄存器的实际地址。
复位值表示该寄存器在系统复位后的默认值,可以用于分析外设的默认状态。这里全部是0。
③寄存器位表
描述寄存器每一个位的作用(共32bit),这里表示ODR寄存器的第15位(bit),位名字为ODR15,rw表示该寄存器可读写(r,可读取;w,可写入)。
④位功能描述
描述寄存器每个位的功能,这里表示位0~15,对应ODR0~ODR15,每个位控制一个IO口的输出状态。
其他寄存器描述,参照以上方法解读接口。
2. 寄存器映射举例
从前面的学习我们知道GPIOB_ODR寄存器的地址为:0x5802 0414,假设我们要控制GPIOB的16个IO口都输出1,则可以写成:

  1. *(unsigned int *)(0x58020414) = 0XFFFF;
复制代码
这里我们先要将0x5802 0414强制转换成unsigned int类型指针,然后用*对这个指针的值进行设置,从而完成对GPIOB_ODR寄存器的写入。
这样写代码功能是没问题,但是可读性和可维护性都很差,使用起来极其不便,因此我们将代码改为:

  1. #define GPIOB_ODR                 *(unsigned int *)(0x58020414)
  2. GPIOB_ODR = 0XFFFF;
复制代码
这样,我们就定义了一个GPIOB_ODR的宏,来替代数值操作,很明显,GPIOB_ODR的可读性和可维护性,比直接使用数值操作来的直观和方便。这个宏定义过程就可以称之为寄存器的映射。
为了简单,我们只举了一个简单实例,实际上大量寄存器的映射,使用结构体是最方便的方式,stm32h7r7xx.h里面使用结构体方式对STM32H7R7的寄存器做了详细映射,等下再介绍。
3. 寄存器地址计算
STM32H7R7大部分外设寄存器地址都是在Block 2上面的,见图5.3.3.1。具体某个寄存器地址,由三个参数决定:1、总线基地址(BUS_BASE_ADDR);2,外设基于总线基地址的偏移量(PERIPH_OFFSET);3,寄存器相对外设基地址的偏移量(REG_OFFSET)。可以表示为:

寄存器地址 = BUS_BASE_ADDR + PERIPH_OFFSET + REG_OFFSET
总线基地址(BUS_BASE_ADDR),STM32H7R7内部的总线基地址如表5.3.4.1所示:

7.png
表5.3.4.1 总线基地址

上表中APB1的基地址,也叫外设基地址,表中的偏移量就是相对于外设基地址的偏移量。
外设基于总线基地址的偏移量(PERIPH_OFFSET),这个不同外设偏移量不一样,我们可以在STM32H7R7存储器映射图(图5.3.3.1)里面找到具体的偏移量,以GPIO为例,其偏移量如表5.3.4.2所示:


8.png
表5.3.4.2 GPIO外设基地址及相对总线偏移量

上表的偏移量,就是外设基于APB2总线基地址的偏移量(PERIPH_OFFSET)。
知道了外设基地址,再在参考手册里面找到具体某个寄存器相对外设基地址的偏移量就可以知道该寄存器的实际地址了,以GPIOB的相关寄存器为例,如表5.3.4.3所示:


9.png
表5.3.4.3 GPIOB寄存器相对外设基地址的偏移量

上表的偏移量,就是寄存器基于外设基地址的偏移量(REG_OFFSET)。
因此,我们根据前面的公式,很容易可以计算出GPIOB_ODR的地址:
GPIOB_ODR地址 = AHB4总线基地址 + GPIOB外设偏移量 + 寄存器偏移量
所以得到:GPIOB_ODR地址 = 0X5802 0000 + 0X0400 + 0X14 = 0X5802 0414
关于寄存器地址计算我们就讲到这里,通过本节的学习,其他寄存器的地址大家都应该可以熟练掌握并计算出来。
4. stm32h7r7xx.h寄存器映射说明
STM32H7R7所有寄存器映射都在stm32h7r7xx.h里面完成,包括各种基地址定义、结构体定义、外设寄存器映射、寄存器位定义(占了绝大部分)等,整个文件有2万多行,非常庞大。我们没有必要对该文件进行全面分析,因为很多内容都是相似的,我们只需要知道寄存器是如何被映射的,就可以了,至于寄存器位定义这些内容,知道是怎么回事就可以了。
我们还是以GPIO为例进行说明,看看stm32h7r7xx.h是如何对GPIO的寄存器进行映射的,通过对GPIO寄存器映射,了解stm32h7r7xx.h的映射规则。
stm32h7r7xx.h文件主要包含五个部分内容,如表5.3.4.4所示:


10.png
表5.3.4.4 stm32h7r7xx.h文件主要组成部分

寄存器映射主要涉及到表5.3.4.4中加粗的两个组成部分:外设寄存器结构体类型定义和寄存器映射,总结起来,包括3个步骤:
1、外设寄存器结构体类型定义
2、外设基地址定义
3、寄存器映射(通过将外设基地址强制转换为外设结构体类型指针即可)
以GPIO为例,其寄存器结构体类型定义如下:

  1. typedef struct
  2. {
  3.   __IO uint32_t MODER;            /* GPIO_MODER 寄存器,相对外设基地址偏移量:0X00 */
  4.   __IO uint32_t OTYPER;           /* GPIO_OTYPER 寄存器,相对外设基地址偏移量:0X04 */
  5.   __IO uint32_t OSPEEDR;         /* GPIO_OSPEEDR 寄存器,相对外设基地址偏移量:0X08 */
  6.   __IO uint32_t PUPDR;           /* GPIO_PUPDR 寄存器,相对外设基地址偏移量:0X0C */
  7.   __IO uint32_t IDR;            /* GPIO_IDR 寄存器,相对外设基地址偏移量:0X10 */
  8.   __IO uint32_t ODR;             /* GPIO_ODR 寄存器,相对外设基地址偏移量:0X14 */
  9.   __IO uint32_t BSRR;             /* GPIO_BSRR 寄存器,相对外设基地址偏移量:0X18 */
  10.   __IO uint32_t LCKR;             /* GPIO_LCKR 寄存器,相对外设基地址偏移量:0X1C */
  11.   __IO uint32_t AFR[2];          /* GPIO_AFR 寄存器,相对外设基地址偏移量:0X20 */
  12. } GPIO_TypeDef;
复制代码
GPIO外设基地址定义如下:
  1. #define PERIPH_BASE          (0x40000000UL)                  /* 外设基地址 */

  2. #define AHB4PERIPH_BASE  (PERIPH_BASE + 0x18020000UL)        /* AHB4总线基地址 */

  3. #define GPIOA_BASE        AHB4PERIPH_BASE                    /* GPIOA基地址 */
  4. #define GPIOB_BASE       (AHB4PERIPH_BASE + 0x0400UL)        /* GPIOB基地址 */
  5. #define GPIOC_BASE       (AHB4PERIPH_BASE + 0x0800UL)        /* GPIOC基地址 */
  6. #define GPIOD_BASE       (AHB4PERIPH_BASE + 0x0C00UL)        /* GPIOD基地址 */
  7. #define GPIOE_BASE       (AHB4PERIPH_BASE + 0x1000UL)        /* GPIOE基地址 */
  8. #define GPIOF_BASE       (AHB4PERIPH_BASE + 0x1400UL)        /* GPIOF基地址 */
  9. #define GPIOG_BASE       (AHB4PERIPH_BASE + 0x1800UL)        /* GPIOG基地址 */
  10. #define GPIOM_BASE       (AHB4PERIPH_BASE + 0x3000UL)        /* GPIOM基地址 */
  11. #define GPION_BASE       (AHB4PERIPH_BASE + 0x3400UL)        /* GPION基地址 */
  12. #define GPIOO_BASE       (AHB4PERIPH_BASE + 0x3800UL)        /* GPIOO基地址 */
  13. #define GPIOP_BASE       (AHB4PERIPH_BASE + 0x3C00UL)        /* GPIOP基地址 */
复制代码
GPIO外设寄存器映射定义如下:
  1. #define GPIOA           ((GPIO_TypeDef *) GPIOA_BASE)  /* GPIOA寄存器地址映射 */
  2. #define GPIOB           ((GPIO_TypeDef *) GPIOB_BASE)  /* GPIOB寄存器地址映射 */
  3. #define GPIOC           ((GPIO_TypeDef *) GPIOC_BASE)  /* GPIOC寄存器地址映射 */
  4. #define GPIOD           ((GPIO_TypeDef *) GPIOD_BASE)  /* GPIOD寄存器地址映射 */
  5. #define GPIOE           ((GPIO_TypeDef *) GPIOE_BASE)  /* GPIOE寄存器地址映射 */
  6. #define GPIOF           ((GPIO_TypeDef *) GPIOF_BASE)  /* GPIOF寄存器地址映射 */
  7. #define GPIOG           ((GPIO_TypeDef *) GPIOG_BASE)  /* GPIOG寄存器地址映射 */
  8. #define GPIOH           ((GPIO_TypeDef *) GPIOH_BASE)  /* GPIOH寄存器地址映射 */
  9. #define GPIOM           ((GPIO_TypeDef *) GPIOM_BASE)  /* GPIOM寄存器地址映射 */
  10. #define GPION           ((GPIO_TypeDef *) GPION_BASE)  /* GPION寄存器地址映射 */
  11. #define GPIOO           ((GPIO_TypeDef *) GPIOO_BASE)  /* GPIOO寄存器地址映射 */
  12. #define GPIOP           ((GPIO_TypeDef *) GPIOP_BASE)  /* GPIOP寄存器地址映射 */
复制代码
以上三部分代码,就完成了STM32H7R7内部GPIOA~GPIOH、GPIOM~GPIOP的寄存器映射,其原理其实是比较简单的,包括两个核心知识点:1,结构体地址自增;2,地址强制转换;
结构体地址自增,我们第一步就定义了GPIO_TypeDef结构体类型,其成员包括:MODER、OTYPER、OSPEEDR、PUPDR、IDR、ODR、BSRR、LCKR和AFR[2],每个成员是uint32_t类型,也就是4个字节,这样假设:MODER地址是0的话,OTYPER就是0X04,OSPEEDR就是0X08,PUPDR就是0X0C,以此类推。
地址强制转换,以GPIOB为例,GPIOB外设的基地址为:GPIOB_BASE(0X5802 0400),我们使用GPIO_TypeDef将该地址强制转换为GPIO结构体类型指针:GPIOB,这样GPIOB->MODER的地址就是:GPIOB_BASE(0X5802 0400),GPIOB->OTYPER的地址就是:GPIOB_BASE + 0X04(0X5802 0404),GPIOB->OSPEEDR的地址就是:GPIOB_BASE + 0X08(0X5802 0408),以此类推。
这样我们就使用结构体方式完成了对GPIO寄存器的映射,其他外设的寄存器映射也都是这个方法,这里就不一一介绍了。关于stm32h7r7xx.h的寄存器映射,我们就介绍到这里。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

如发现本坛存在违规或侵权内容, 请点击这里发送邮件举报 (或致电020-38271790)。请提供侵权说明和联系方式。我们将及时审核依法处理,感谢配合。

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2026-3-20 12:03

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表