STM32-寄存器地址名称映射分析

1  51单片机访问地址

sfr P0 =0x80;   //P0映射到地址0x80
P0=0x00;        //忘寄存器地址0x80赋值0x00

        sfr是一种扩充数据类型,点用一个内存单位,值域为0-255.利用它可以访问51单片机内部所有的特殊功能寄存器。前一句“sfr p0=0x80”就是将P0映射到地址0x80。后一句“p0=0x00”就是往p0地址(0x80)代表的寄存器写值。

2  STM32单片机访问地址

        对MCU,一切底层配置,最终都是配置寄存器。我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫寄存器映射?寄存器到底是什么?

        在存储器Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。

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

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

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

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

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

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

        GPIOB->ODR=0xFFFF FFFF; 
                 值0xFFFF FFFF是怎么赋值给了GPIOB的ODR寄存器地址的呢?也就是说GPIOB->ODR这种写法,是怎么与GPIOB的ODR寄存器地址映射起来的?       
        寄存器地址名称映射:STM32肯定也是可以这样来设置寄存器的。但是由于STM32的寄存器数目太多了,如果以这样的方式列出来,需要很大的篇幅,而且也不方便开发。所以,MDK采用的方式是通过结构体来将寄存器组织在一起。下面就介绍MDK如何把结构体和地址对应起来的,为什么修改结构体成员变量的值就可以达到操作寄存器的值?这些事情都是在stm32f10x.h文件中完成的。

        定义在stm32f10x.h文件和stm32f4xx.h

typedef struct                             typedef struct
{                                          {
  __IO uint32_t CRL;                         __IO uint32_t MODER;  
  __IO uint32_t CRH;                         __IO uint32_t OTYPER;   
  __IO uint32_t IDR;                         __IO uint32_t OSPEEDR; 
  __IO uint32_t ODR;                         __IO uint32_t PUPDR;  
  __IO uint32_t BSRR;                        __IO uint32_t IDR;   
  __IO uint32_t BRR;                         __IO uint32_t ODR; 
  __IO uint32_t LCKR;                        __IO uint16_t BSRRL;   
} GPIO_TypeDef;                              __IO uint16_t BSRRH; 
                                             __IO uint32_t LCKR;  
                                             __IO uint32_t AFR[2]; 
                                           } GPIO_TypeDef;

        stm32f10x.h一组GPIO有7个成员变量,设GPIOA的基地址为A,成员变量GPIO->CRL偏移值B,则GPIO->CRL的地址就是A+B。GPIOA的基地址挂载在总线APB2,以APB2的基地址加上GPIOA相对于APB2的偏移量(是常量)得到GPIOA的地址GPIOA_BASE。APB2的基地址挂载在外设基地址,以外设基地址加上APB2的基地址相对于外设基地址的偏移量(是常量)得到APB1的基地址AHB1PERIPH_BASE。外设基地址一般定义为基地址PERIPH_BASE先定义一个外设基地址,再定义总线APB2的基地址,再定义GPIO的基地址,再定义GPIOA的寄存器的地址。

STM32-寄存器地址名称映射分析_第1张图片

        stm32f10x.h的第1274行定义的外设基地址:

#define PERIPH_BASE           ((uint32_t)0x40000000)

        stm32f10x.h的第1282行定义的APB1、APB2和AHB外设基地址,片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1挂载低速外设,APB2 和AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。

#define APB1PERIPH_BASE       PERIPH_BASE
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x20000)

        stm32f10x.h的第1315行定义的GPIOA_BASE、GPIOB_BASE、GPIOC_BASE、GPIOD_BASE、GPIOE_BASE、GPIOF_BASE和GPIOG_BASE基地址。总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX 外设基地址”,也叫XX 外设的边界地址。这里面我们以GPIO 这个外设来讲解外设的基地址,因为GPIO都是挂载在APB2总线之上的,所以它的基地址是由APB2总线的基地址+GPIO在APB2总线上的偏移地址决定的。

#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE            (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE            (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE            (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE            (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE            (APB2PERIPH_BASE + 0x2000)

        stm32f10x.h的第1408行通过指针强制将GPIOA_BASE、GPIOB_BASE、GPIOC_BASE、GPIOD_BASE、GPIOE_BASE、GPIOF_BASE和GPIOG_BASE转换成指定的GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、GPIOF和GPIOG地址的GPIO_TypeDef类型指针。这句话的意思就是,GPIOA指向地址GPIOA_BASE,而GPIOA_BASE存放的数据类型是GPIO_TypeDef。

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)

        stm32f10x.h的第1001行定义的有关GPIO的寄存器的结构体变量,结构体里面声明了7个变量,即结构体的7个变量就是GPIOB的7个寄存器。这个时候就明白了“GPIOB->ODR”就是指:GPIOB结构体下的ODR变量。

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

        GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为32bit,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以GPIOB 端口为例,来说明GPIO都有哪些寄存器。

在这里插入图片描述

       此时可以算出GPIOA的基地址位了:GPIOA_BASE=0x40000000+0x10000+0x0800=0x40010800
        这上面就已经知道了GPIOA的基地址,那么那些GPIOA的7个寄存器的地址又是怎么计算出来的呢?
                GPIOA的寄存器的地址=GPIOA基地址+寄存器相对GPIOA基地址的偏移值
        寄存器相对于GPIOA基地址的偏移值可以在上面的寄存器地址映射表中查到。稍微解释一下:GPIO的每个寄存器都是32位的,所以每个寄存器是占用4个地址,也就是说一共占用28个地址。地址偏移范围为(000h-01Bh)。这个地址偏移是相对于GPIOA的基地址而言的。那么你可能又有一个疑问:结构体里面的寄存器又是怎么与地址一一对应的呢?这就涉及到结构体的一个特征,那就是结构体存储的成员的地址是连续的。上面讲到GPIOA是指向GPIO_TypeDef类型的指针,又由于GPIO_TypeDef是结构体,所以自然而然我们就可以算出GPIOA指向的结构体成员变量对应地址了。

3  C 语言对寄存器的封装

3.1  封装总线和外设基地址

        在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名。

STM32-寄存器地址名称映射分析_第2张图片

        首先定义了 “片上外设”基地址PERIPH_BASE,接着在PERIPH_BASE 上加入各个总线的地址偏移, 得到APB1 、APB2 总线的地址APB1PERIPH_BASE 、APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到GPIOA-G的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针读写。

STM32-寄存器地址名称映射分析_第3张图片

        该代码使用 (unsigned int *) 把GPIOB_BSRR 宏的数值强制转换成了地址,然后再用“*”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取STM32 外设的状态。

3.2  封装总线和外设基地址

        用上面的方法去定义地址,还是稍显繁琐,例如GPIOA-GPIOE 都各有一组功能相同的寄存器,如GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入C 语言中的结构体语法对寄存器进行封装。

STM32-寄存器地址名称映射分析_第4张图片

        这段代码用typedef 关键字声明了名为GPIO_TypeDef 的结构体类型,结构体内有7 个成员变量,变量名正好对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间是连续的,其中32 位的变量占用4 个字节,16 位的变量占用2 个字节。

STM32-寄存器地址名称映射分析_第5张图片

         也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的首地址为0x40010C00(这也是第一个成员变量CRL 的地址), 那么结构体中第二个成员变量CRH 的地址即为0x4001 0C00 +0x04 ,加上的这个0x04 ,正是代表CRL 所占用的4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。

         这样的地址偏移与STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器。

STM32-寄存器地址名称映射分析_第6张图片

         这段代码先用GPIO_TypeDef 类型定义一个结构体指针GPIOx,并让指针指向地址GPIOB_BASE(0x4001 0C00),使用地址确定下来,然后根据C 语言访问结构体的语法,用GPIOx->ODR 及GPIOx->IDR 等方式读写寄存器。

        最后,我们更进一步,直接使用宏定义好GPIO_TypeDef 类型的指针,而且指针指向各个GPIO端口的首地址,使用时我们直接用该宏访问寄存器即可。

STM32-寄存器地址名称映射分析_第7张图片

        这里我们仅是以GPIO 这个外设为例,给大家讲解了C 语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。

4  总结与分析

        对于STM32而言,使用“GPIOA->ODR=0x00000000;”来对寄存器赋值的原理,也就是将GPIO下的所有寄存器放在一个结构体内,通过基地址和在基地址上的偏移地址不断转化,最终找到准确的寄存器实际地址来进行赋值。也就是说,和51单片机最大的不同就是:由于STM32的寄存器数目太多,就将其中控制同一外设的寄存器设置成一个结构体(如GPIO、DMA等),通过对结构体的地址和寄存器相对于结构体的偏移地址,来确定某个特定的寄存器。

你可能感兴趣的:(STM32教程,stm32)