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

第五章 STM32基础知识入门


       本章,我们着重介绍STM32的一些基础知识,让大家对STM32开发有一个初步的了解,为后面STM32的学习做铺垫,方便后面的学习。本章内容大家第一次看的时候可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。

       本章将分为如下几个小节:

       5.1 C语言基础知识复习

       5.2 寄存器基础知识

       5.3 STM32G474系统架构


        5.1 C语言基础知识复习

       本节我们给大家介绍一下C语言基础知识,对于C语言比较熟练的读者,可以跳过此节,对于基础比较薄弱的读者,建议好好学习一下本节内容。

       由于C语言博大精深,不可能我们一小节就全讲明白了,所以本节我们只是复习STM32开发时常用的几个C语言知识点,以便大家的更好的学习并编写STM32代码。


       5.1.1 位操作

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


表5.1.1.1 六种位操作


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

       1,在不改变其他位的值的状况下,对某几个位进行设值。

       这个场景在单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,然后用|操作符设值。比如我要改变GPIOA的CRL寄存器bit6(第6位)的值为1,可以先对寄存器的值进行&清零操作:

GPIOA->CRL &= 0XFFFFFFBF;    /* 将第bit6清0 */

       然后再与需要设置的值进行|或运算:

GPIOA->CRL |= 0X00000040;    /* 设置bit6的值为1,不改变其他位的值 */

       2,移位操作提高代码的可读性。

       移位操作在单片机开发中非常重要,下面是delay_init函数的一行代码:

SysTick->CTRL |= 1 << 1;

       这个操作就是将CTRL寄存器的第1位(从0开始算起)设置为1,为什么要通过左移而不是直接设置一个固定的值呢?其实这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第1位设置为1。如果写成:

SysTick->CTRL |= 0X0002;

       这个虽然也能实现同样的效果,但是可读性稍差,而且修改也比较麻烦。

       3,~按位取反操作使用技巧

       按位取反在设置寄存器的时候经常被使用,常用于清除某一个/某几个位。下面是delay_us函数的一行代码:

SysTick->CTRL &= ~(1 << 0) ;    /* 关闭SYSTICK */

       该代码可以解读为仅设置CTRL寄存器的第0位(最低位)为0,其他位的值保持不变。同样我们也不使用按位取反,将代码写成:

SysTick->CTRL &= 0XFFFFFFFE;     /* 关闭SYSTICK */

       可见前者的可读性,及可维护性都要比后者好很多。 

       4,^按位异或操作使用技巧

       该功能非常适合用于控制某个位翻转,常见的应用场景就是控制LED闪烁,如:

GPIOB->ODR ^= 1 << 5;

       执行一次该代码,就会使PB5的输出状态翻转一次,如果我们的LED接在PB5上,就可以看到LED闪烁了。


       5.1.2 define宏定义

       define是C语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供方便。常见的格式:

#define     标识符       字符串

       “标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:

#define   HSE_VALUE   8000000U

       定义标识符HSE_VALUE的值为8000000,数字后的U表示unsigned的意思。

       至于define宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。


       5.1.3 ifdef条件编译

       单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:

#ifdef 标识符
程序段1
#else
程序段2
#endif

       它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。 其中#else部分也可以没有,即:

#ifdef
      程序段1
#endif

       条件编译在MDK里面是用得很多,在stm32g4xx.h这个头文件中经常会看到这样的语句:

#if !defined (STM32G4)
#define STM32G4
#endif

       如果没有定义STM32G4这个宏,则定义STM32G4宏。条件编译也是c语言的基础知识,这里点到为止。


       5.1.4 extern外部申明

       C语言中extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于extern申明变量可以多次,但定义只有一次。在我们的代码中你会看到这样的语句:

extern uint16_t g_usart_rx_sta;

       这个语句是申明g_usart_rx_sta变量在其他文件中已经定义了,在这里要使用到。所以,你肯定可以找到在某个地方有变量定义的语句:

uint16_t g_usart_rx_sta;

extern的使用比较简单,但是也会经常用到,需要掌握。


       5.1.5 typedef类型别名

       typedef用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。typedef在MDK用得最多的就是定义结构体的类型别名和枚举类型了。

struct _GPIO
{
        __IO uint32_t CRL;
        __IO uint32_t CRH;
        …
};

       定义了一个结构体GPIO,这样我们定义结构体变量的方式为:

struct  _GPIO  gpiox;  /* 定义结构体变量gpiox */

       但是这样很繁琐,MDK中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别名GPIO_TypeDef,这样我们就可以在其他地方通过别名GPIO_TypeDef来定义结构体变量了,方法如下:

typedef struct
{
        __IO uint32_t CRL;
        __IO uint32_t CRH;
        …
} GPIO_TypeDef;

       Typedef为结构体定义一个别名GPIO_TypeDef,这样我们可以通过GPIO_TypeDef来定义结构体变量:

GPIO_TypeDef gpiox;

       这里的GPIO_TypeDef就跟struct _GPIO是等同的作用了,但是GPIO_TypeDef使用起来方便很多。


       5.1.6 结构体

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

       声明结构体类型:

struct 结构体名
{
     成员列表;
}变量名列表;

       例如:

struct U_TYPE
{
    int BaudRate
    int WordLength; 
}usart1, usart2;

       在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:

struct  结构体名字   结构体变量列表 ;

       例如:

struct U_TYPE usart1,usart2;

       结构体成员变量的引用方法是:

结构体变量名字.成员名

       比如要引用usart1的成员BaudRate,方法是:usart1.BaudRate;结构体指针变量定义也是一样的,跟其他变量没有啥区别。

       例如:

struct U_TYPE *usart3;    /* 定义结构体指针变量usart3 */

       结构体指针成员变量引用方法是通过“->”符号实现,比如要访问usart3结构体指针指向的结构体的成员变量BaudRate,方法是:

usart3->BaudRate;

       上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实例回答一下这个问题。

       在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态是由几个属性来决定的,比如串口号,波特率,极性,以及模式。对于这种情况,在我们没有学习结构体的时候,我们一般的方法是:

void usart_init(uint8_t usartx, uiut32_t BaudRate, uint32_t Parity, 
uint32_t Mode);

       这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里面再传入一个/几个参数,那么势必我们需要修改这个函数的定义,重新加入新的入口参数,随着开发不断的增多,那么是不是我们就要不断的修改函数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?

       我们使用结构体参数,就可以在不改变入口参数的情况下,只需要改变结构体的成员变量,就可以达到改变入口参数的目的。

       结构体就是将多个变量组合为一个整体,上面的函数,usartx,BaudRate,Parity, Mode等这些参数,他们对于串口而言,是一个整体,都是用来设置串口参数的,所以我们可以通过定义一个结构体将他们组合在一起。MDK中是这样定义的:

typedef struct
{ 
   uint32_t BaudRate;
   uint32_t WordLength;
   uint32_t StopBits;
   uint32_t Parity;
   uint32_t Mode;
   uint32_t HwFlowCtl;
   uint32_t OverSampling;   
} UART_InitTypeDef;

       这样,我们在初始化串口的时候入口参数就可以是USART_InitTypeDef类型的变量或者指针变量了,于是我们可以改为:

void usart_init(UART_InitTypeDef *huart);

       这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义就可以达到增加变量的目的。

       理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可以提高你的代码的可读性。

       使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作用就远远不止这个了,同时,MDK中用结构体来定义外设也不仅仅只是这个作用,这里我们只是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲解结构体的一些其他知识。


       5.1.7 指针

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

       申明指针我们一般以p开头,如:

char * p_str = “This is a test!”;

       这样,我们就申明了一个p_str的指针,它指向This is a test!这个字符串的首地址。我们编写如下代码:

int main(void)
{ 
uint8_t temp = 0X88;      /* 定义变量 temp */
    uint8_t *p_num = &temp;   /* 定义指针p_num,指向temp的地址 */
 
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(85, 2, 2, 4, 8);   /* 设置时钟,170Mhz */
    delay_init(170);                          /* 延时初始化 */
    usart_init(115200);                      /* 初始化串口 */
    printf("temp:0X%X\r\n", temp);                   /* 打印temp的值 */
    printf("*p_num: 0X %X\r\n", *p_num);            /* 打印*p_num的值 */
    printf("p_num: 0X %X\r\n", (uint32_t)p_num); /* 打印p_num的值 */
    printf("&p_num: 0X %X\r\n", (uint32_t)&p_num); /* 打印&p_num的值 */
    while (1);  
}

       此代码的输出结果为:


图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所示:


表5.2.1 STM32寄存器分类


       其中,内核寄存器,我们一般只需要关心中断控制寄存器和SysTick寄存器即可,其他三大类,我们一般很少直接接触。而外设寄存器,则是学到哪个外设,就了解哪个外设相关寄存器即可,所以整体来说,我们需要关心的寄存器并不是很多,而且很多都是有共性的,比如STM32G474VET6有17个定时器,我们只需要学习了其中一个的相关寄存器,其他的基本都是一样。

       说了这么多,给大家举个简单的例子,我们知道寄存器的本质是一个特殊的内存,对于STM32来说,以GPIOB的ODR寄存器为例,其寄存器地址为:0x48000414,所以我们对其赋值可以写成:

(*(unsigned int *))(0x48000414) = 0XFFFF;

       这样我们就完成了对GPIOB->ODR寄存器的赋值,全部0XFFFF,表示GPIOB所有IO口(16个IO口)都输出高电平。对于我们来说,0x48000414就是一个寄存器的特殊地址,至于它是怎么来的,我们后续再给大家介绍。

       虽然上面的代码实现了我们需要的功能,但是从实用的角度来说,这么写肯定是不好的,可读性极差,可维护性也很差,所以一般我们使用结构体来访问,比如改写成这样:

GPIOB->ODR = 0XFFFF;

       这样可读性就比之前的代码好多了,可维护性也相对好一点。至于GPIOB结构体怎么来的,我们也会在后续给大家介绍。


        5.3 STM32G474系统架构

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


       5.3.1 Cortex M4内核 & 芯片

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


图5.3.1.1 Cortex M4内核&芯片关系


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


       5.3.2 STM32系统架构

       STM32G474系统架构如图5.3.2.1:


图5.3.2.1 STM32G474系统架构


       主系统由32位多层AHB总线矩阵构成,其用于主控总线之间的访问仲裁管理。接下来我们介绍一下这些单元。


       1. I Code总线(I - bus)

       这是Cortex M4内核的指令总线,连接闪存指令接口(如:FLASH),用于获取指令。此总线访问的对象是代码存储器,包括:内部Flash、SRAM和通过FSMC连接的外部存储器。


       2. D Code总线(D - Bus)

       这是Cortex M4内核的数据总线,连接闪存存储器数据接口(如:SRAM、FLASH等),用于各种数据访问,如常量、变量等。


       3. 系统总线(S - Bus)

       这是Cortex M4内核的系统总线,连接所有外设(如:GPIO、SPI、IIC、TIM等),用于控制各种外设工作,如配置各种外设相关寄存器等。


       4. DMA总线

       DMA是直接存储访问控制器,可以实现数据的自动搬运,整个过程不需要CPU处理。如可以实现DMA传输内存数据到DAC,输出任意波形,传输过程不需要CPU参与,可以大大节省CPU支,从而更高效的处理事务。STM32G474的DMA控制器,可以实现内存到外设、外设到内存、内存到内存的数据传输。


       5. 内部FLASH

       内部FLASH即单片机的硬盘,用于代码和数据的存储,CPU通过ICode总线经FLASH接口访问内部FLASH,FLASH最高访问速度是有限的,因此以170M速度访问时,需要加上适当的延迟时间。


       6. 内部SRAM

       内部SRAM即单片机的内存,用于数据存储,直接挂载在总线矩阵上面,CPU通过DCode总线实现零延时访问SRAM,最快总线频率可达170Mhz,从而保证高效高速的访问内存。


       7. FSMC

       FSMC即灵活的静态存储控制器,实际上就是一个外部总线接口,可以用来访问外部SRAM、NAND/NOR FLASH、LCD等。它也是直接挂在总线矩阵上面的,以方便CPU快速访问外挂器件。

       8. AHB/APB桥

       AHB和两个APB总线之间的完全同步连接,用户可以灵活选择总线频率,最高170Mhz。这三个总线上面挂载了STM32内部绝大部分外设。


       9. 总线矩阵

       总线矩阵协调内核系统总线和DMA主控总线之间的访问仲裁,仲裁利用轮换算法,保证各个总线之间的有序访问,从而确保工作正常。


       5.3.3 存储器映射

       STM32是一个32位单片机,他可以很方便的访问4GB以内的存储空间(2^32 = 4GB),因此Cortex M4内核将图5.3.2.1中的所有结构,包括:FLASH、SRAM、外设及相关寄存器等全部组织在同一个4GB的线性地址空间内,我们可以通过C语言来访问这些地址空间,从而操作相关外设(读/写)。数据字节以小端格式(小端模式)存放在存储器中,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。

       存储器本身是没有地址信息的,我们对存储器分配地址的过程就叫存储器映射。这个分配一般由芯片厂商做好了,ST将所有的存储器及外设资源都映射在一个4GB的地址空间上(8个块),从而可以通过访问对应的地址,访问具体的外设。其映射关系如图5.3.3.1所示:


图5.3.3.1 STM32G474存储器映射


       存储块功能介绍

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


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


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


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


       可以看到,我们用户FLASH大小是512KB,这是对于我们使用的STM32G474VET6来说,如果是其他型号,可能FLASH会更小,当然,如果ST喜欢,也是可以随时推出更大容量的STM32单片机的,因为这里保留了一大块地址空间。还有,STM32的出厂固化BootLoader非常精简,整个BootLoder只占了28KB FLASH空间。

       第二个块是Block 1,用于存储数据,即SRAM空间,其功能划分如表5.3.3.3所示:


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


       这个块仅使用了128KB大小(仅大容量STM32G474型号才有这么多SRAM,比如STM32G474VET6等),用于SRAM访问,同时也有大量保留地址用于扩展。

       第三个块是Block 2,用于外设访问,STM32内部大部分的外设都是放在这个块里面的,该存储块里面包括了AHB1、AHB2、APB1和APB2四个总线相关的外设。其功能划分如表5.3.3.4所示:


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


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


       5.3.4 寄存器映射

       给存储器分配地址的过程叫存储器映射,寄存器是一类特殊的存储器,它的每个位都有特定的功能,可以实现对外设/功能的控制,给寄存器的地址命名的过程就叫寄存器映射。

       举个简单的例子,大家家里面的纸张就好比通用存储器,用来记录数据是没问题的,但是不会有具体的动作,只能做记录,而你家里面的电灯开关,就好比寄存器了,假设你家有8个灯,就有8个开关(相当于一个8位寄存器),这些开关也可以记录状态,同时还能让电灯点亮/关闭,是会产生具体动作的。为了方便区分和使用,我们会给每个开关命名,比如厨房开关、大厅开关、卧室开关等,给开关命名的过程,就是寄存器映射。

       当然STM32内部的寄存器有非常多,远远不止8个开关这么简单,但是原理是差不多的,每个寄存器的每一个位,一般都有特定的作用,涉及到寄存器描述,大家可以参考《STM32G4xx参考手册_V7(英文版).pdf》相关章节的寄存器描述部分,有详细的描述。


       1. 寄存器描述解读

       我们以GPIO的ODR寄存器为例,其参考手册的描述如图5.3.4.1所示:


图5.3.4.1 端口输出数据寄存器描述


       ① 寄存器名字

       每个寄存器都有一个对应的名字,以简单表达其作用,并方便记忆,这里GPIOx_ODR表示寄存器英文名,x可以从A~G,说明有7个这样的寄存器,对应每个端口(注意:芯片的IO端口数以实际选型为准)。

       ② 寄存器偏移量及复位值

       地址偏移量表示相对该外设基地址的偏移,比如GPIOB,我们由《STM32G4xx参考手册_V7(英文版).pdf》第83页可知其外设基地址是:0x48000400。那么GPIOB_ODR寄存器的地址就是:0x48000414。知道了外设基地址和地址偏移量,我们就可以知道任何寄存器的实际地址。

       复位值表示该寄存器在系统复位后的默认值,可以用于分析外设的默认状态。这里都是0。

       ③ 寄存器位表

       描述寄存器每一个位的作用(共32bit),这里表示ODR寄存器的第15位(bit),位名字为ODR15,rw表示该寄存器可读写(r,可读取;w,可写入)。

       ④ 位功能描述

       描述寄存器每个位的功能,这里表示位0~15,对应ODR0~ODR15,每个位控制一个IO口的输出状态。

       其他寄存器描述,参照以上方法解读接口。


       2. 寄存器映射举例

       从前面的学习我们知道GPIOB_ODR寄存器的地址为:0x48000414,假设我们要控制GPIOB的16个IO口都输出1,则可以写成:

*(unsigned int *)(0x48000414) = 0xFFFF;

       这里我们先要将0x48000414强制转换成unsigned int类型指针,然后用*对这个指针的值进行设置,从而完成对GPIOB_ODR寄存器的写入。

       这样写代码功能是没问题,但是可读性和可维护性都很差,使用起来极其不便,因此我们将代码改为:

#define GPIOB_ODR  *(unsigned int *)(0x48000414)
GPIOB_ODR = 0xFFFF;

       这样,我们就定义了一个GPIOB_ODR的宏,来替代数值操作,很明显,GPIOB_ODR的可读性和可维护性,比直接使用数值操作来的直观和方便。这个宏定义过程就可以称之为寄存器的映射。

       当然,为了简单,我们只举了一个简单实例,实际上大量寄存器的映射,使用结构体是最方便的方式,stm32g474xx.h里面使用结构体方式对STM32G474的寄存器做了详细映射,等下我们再介绍。


       3. 寄存器地址计算

       STM32G474大部分外设寄存器地址都是在存储块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),STM32G474内部有四个总线(APB1、APB2、AHB1和AHB2),对应的总线基地址如表5.3.4.1所示:


表5.3.4.1 总线基地址


       上表中APB1的基地址,也叫外设基地址,表中的偏移量就是相对于外设基地址的偏移量。

       外设基于总线基地址的偏移量(PERIPH_OFFSET),这个不同外设偏移量不一样,我们可以在《STM32G4xx参考手册_V7(英文版).pdf》的第83页里面找到具体的偏移量,以GPIO为例,其偏移量如表5.3.4.2所示:


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


       上表的偏移量,就是外设基于APB2总线基地址的偏移量(PERIPH_OFFSET)。

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


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


       上表的偏移量,就是寄存器基于外设基地址的偏移量(REG_OFFSET)。

       因此,我们根据前面的公式,很容易可以计算出GPIOB_ODR的地址:

GPIOB_ODR地址 = APB2总线基地址 + GPIOB外设偏移量 + 寄存器偏移量

       所以得到:GPIOB_ODR地址 = 0x4800 0000 + 0x400 + 0x14 = 0x4800 0414。

       关于寄存器地址计算我们就讲到这里,通过本节的学习,其他寄存器的地址大家都应该可以熟练掌握并计算出来。


       4. stm32g474xx.h寄存器映射说明

       STM32G474所有寄存器映射都在stm32g474xx.h里面完成,包括各种基地址定义、结构体定义、外设寄存器映射、寄存器位定义(占了绝大部分)等,整个文件有1W多行,非常庞大。我们没有必要对该文件进行全面分析,因为很多内容都是相似的,我们只需要知道寄存器是如何被映射的,就可以了,至于寄存器位定义这些内容,知道是怎么回事就可以了。

       我们还是以GPIO为例进行说明,看看stm32g474xx.h是如何对GPIO的寄存器进行映射的,通过对GPIO寄存器映射,了解stm32g474xx.h的映射规则。

       stm32g474xx.h文件主要包含五个部分内容,如表5.3.4.4所示:


表5.3.4.4 stm32g474xx.h文件主要组成部分


       寄存器映射主要涉及到表5.3.4.4中加粗的两个组成部分:外设寄存器结构体类型定义和寄存器映射,总结起来,包括3个步骤:

       1, 外设寄存器结构体类型定义

       2, 外设基地址定义

       3, 寄存器映射(通过将外设基地址强制转换为外设结构体类型指针即可)

       以GPIO为例,其寄存器结构体类型定义如下:

typedef struct
{
  __IO uint32_t MODER;     /* GPIO_MODER寄存器,相对外设基地址偏移量:0x00 */
  __IO uint32_t OTYPER;   /* GPIO_OTYPER寄存器,相对外设基地址偏移量:0x04 */
  __IO uint32_t OSPEEDR;   /* GPIO_OSPEEDR寄存器,相对外设基地址偏移量:0x08 */
  __IO uint32_t PUPDR;     /* GPIO_PUPDR寄存器,相对外设基地址偏移量:0x0C */
  __IO uint32_t IDR;       /* GPIO_IDR寄存器,相对外设基地址偏移量:0x10 */
  __IO uint32_t ODR;       /* GPIO_ODR寄存器,相对外设基地址偏移量:0x14 */
  __IO uint32_t BSRR;      /* GPIO_BSRR寄存器,相对外设基地址偏移量:0x18 */
  __IO uint32_t LCKR;      /* GPIO_LCKR寄存器,相对外设基地址偏移量:0x1C */
  __IO uint32_t AFR[2];  /* GPIO_ARF[2]寄存器,相对外设基地址偏移量:0x20-0x24 */ 
__IO uint32_t BRR;  /* GPIO_BBR寄存器,相对外设基地址偏移量:0x28 */
} GPIO_TypeDef;

       GPIO外设基地址定义如下:

#define PERIPH_BASE   (0x40000000UL)     /* 外设基地址 */
 
#define APB1PERIPH_BASE  PERIPH_BASE      /* APB1总线基地址 */
#define APB2PERIPH_BASE  (PERIPH_BASE + 0x00010000UL) /* APB2总线基地址 */
#define AHB1PERIPH_BASE  (PERIPH_BASE + 0x00020000UL)  /* AHB1总线基地址 */
#define AHB2PERIPH_BASE  (PERIPH_BASE + 0x08000000UL)  /* AHB2总线基地址 */
 
#define GPIOA_BASE   (AHB2PERIPH_BASE + 0x0000UL) /* GPIOA基地址 */
#define GPIOB_BASE   (AHB2PERIPH_BASE + 0x0400UL)  /* GPIOB基地址 */
#define GPIOC_BASE   (AHB2PERIPH_BASE + 0x0800UL)  /* GPIOC基地址 */
#define GPIOD_BASE   (AHB2PERIPH_BASE + 0x0C00UL)  /* GPIOD基地址 */
#define GPIOE_BASE   (AHB2PERIPH_BASE + 0x1000UL)  /* GPIOE基地址 */
#define GPIOF_BASE   (AHB2PERIPH_BASE + 0x1400UL)  /* GPIOF基地址 */
#define GPIOG_BASE   (AHB2PERIPH_BASE + 0x1800UL)  /* GPIOG基地址 */

       GPIO外设寄存器映射定义如下:

#define GPIOA               ((GPIO_TypeDef *)GPIOA_BASE) /* GPIOA寄存器地址映射 */
#define GPIOB               ((GPIO_TypeDef *)GPIOB_BASE) /* GPIOB寄存器地址映射 */
#define GPIOC               ((GPIO_TypeDef *)GPIOC_BASE) /* GPIOC寄存器地址映射 */
#define GPIOD               ((GPIO_TypeDef *)GPIOD_BASE) /* GPIOD寄存器地址映射 */
#define GPIOE               ((GPIO_TypeDef *)GPIOE_BASE) /* GPIOE寄存器地址映射 */
#define GPIOF               ((GPIO_TypeDef *)GPIOF_BASE) /* GPIOF寄存器地址映射 */
#define GPIOG               ((GPIO_TypeDef *)GPIOG_BASE) /* GPIOG寄存器地址映射 */

       以上三部分代码,就完成了STM32G474内部GPIOA~GPIOG的寄存器映射,其原理其实是比较简单的,包括两个核心知识点:1,结构体地址自增;2,地址强制转换;

       结构体地址自增,我们第一步就定义了GPIO_TypeDef结构体类型,其成员包括:MODER、OTYPER、OSPEEDR、PUPDR、IDR、ODR、BSRR、LCKR、AFRH、AFRL和BBR,每个成员是uint32_t类型,也就是4个字节,这样假设:MODER地址是0的话,OTYPER就是0x04,OSPEEDR就是0x08,PUPDR就是0x0C,以此类推。

       地址强制转换,以GPIOB为例,GPIOB外设的基地址为:GPIOB_BASE(0x4800 0400),我们使用GPIO_TypeDef将该地址强制转换为GPIO结构体类型指针:GPIOB,这样GPIOB->MODER的地址就是:GPIOB_BASE(0X4800 0400),GPIOB->OTYPER的地址就是:GPIOB_BASE + 0x04(0x4800 0404),GPIOB->OSPEEDR的地址就是:GPIOB_BASE + 0x08(0x4800 0408),以此类推。

       这样我们就使用结构体方式完成了对GPIO寄存器的映射,其他外设的寄存器映射也都是这个方法,这里就不一一介绍了。关于stm32g474xx.h的寄存器映射,我们就介绍到这里。 


请使用浏览器的分享功能分享到微信等