STM32库开发实战指南:基于STM32F4
上QQ阅读APP看书,第一时间看更新

4.5 寄存器映射

我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射。那么什么叫寄存器映射?寄存器到底是什么?

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

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

代码清单4-1 通过绝对地址访问内存单元

    1 //GPIOH 端口全部输出高电平
    2 *(unsigned int*)(0x4002 1C14) = 0xFFFF;

0x4002 1C14在我们看来是GPIOH端口ODR的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为它是指针,得进行强制类型转换,把它转换成指针,即(unsigned int *)0x4002 1C14,然后再对这个指针进行*操作。

刚刚我们说了,通过绝对地址访问内存单元不但不好记忆且容易出错,所以可以通过寄存器别名的方式来操作,具体见代码清单4-2。

代码清单4-2 通过寄存器别名方式访问内存单元

    1 //GPIOH 端口全部输出高电平
    2 #define GPIOH_ODR (unsigned int*)(GPIOH_BASE+0x14)
    3 * GPIOH_ODR = 0xFF;

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

代码清单4-3 通过寄存器别名访问内存单元

    1 //GPIOH 端口全部输出高电平
    2 #define GPIOH_ODR *(unsigned int*)(GPIOH_BASE+0x14)
    3 GPIOH_ODR = 0xFF;

4.5.1 STM32的外设地址映射

片上外设区分为4条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB挂载低速外设,AHB挂载高速外设。相应总线的最低地址被称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中APB1总线的地址最低,片上外设从这里开始,也称为外设基地址。

1. 总线基地址

总线基地址见表4-5。

表4-5 总线基地址

表4-5中的“相对外设基地址的偏移”为该总线地址与“总线基地址”基地址0x40000000的差值。关于地址的偏移后面还会讲到。

2. 外设基地址

总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为XX外设基地址,也称为“XX外设的边界地址”。STM32F4xx外设的具体边界地址请参考《STM32F4xx参考手册》中2.3节存储器映射的“表2:STM32F4xx寄存器边界地址”。或者参考《STM32F4xx参考手册》的存储器映射章节,这两个手册对此都有详细的讲解。

这里面以GPIO这个外设来讲解外设的基地址,具体见表4-6。

表4-6 外设GPIO基地址

从表4-6看到,GPIOA的基址相对于AHB1总线的地址偏移为0,于是可以猜到,AHB1总线的第一个外设就是GPIOA。

3. 外部寄存器

处于XX外设的地址范围内的就是该外设的寄存器。以GPIO外设为例,GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把GPIO的引脚连接到LED的阴极,LED的阳极接电源,然后通过STM32控制该引脚的电平,从而控制LED的亮灭。

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

表4-7 GPIOH端口的寄存器地址列表

有关外部的寄存器说明可参考《STM32F4xx参考手册》中的寄存器描述部分,在编程的时候需要反复查阅外设的寄存器说明。

这里以GPIO端口置位/复位寄存器为例,介绍如何理解寄存器的说明,具体见图4-6。

图4-6 GPIO端口置位/复位寄存器说明

(1)名称

寄存器说明中首先列出了该寄存器的名称,“(GPIOx_BSRR)(x=A…I)”这段的意思是该寄存器名为“GPIOx_BSRR”,其中的“x”可以为英文字母A~I,也就是说这个寄存器说明适用于GPIOA、GPIOB、…、GPIOI,这些GPIO端口中都有这样的一个寄存器。

(2)偏移地址

偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是0x18,从参考手册中可以查到GPIOA外设的基地址为0x40020000,于是可以算出GPIOA的这个GPIOA_BSRR寄存器的地址为:0x40020000+0x18;同理,由于GPIOB的外设基地址为0x40020400,可以算出GPIOB_BSRR寄存器的地址为:0x40020400+0x18。其他GPIO端口以此类推即可。

(3)寄存器位表

本寄存器的位表中列出编号0~31位的名称及权限。表上方的数字为位编号,中间为位名称,最下方为读写权限,其中w表示只写,r表示只读,rw表示可读写。本寄存器中的位权限都是w,所以只能写,如果读本寄存器是无法保证读取到它真正内容的。而有的寄存器位是只读的,一般用于表示STM32外设的某种工作状态,由STM32硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。

(4)位功能说明

位功能说明是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。例如本寄存器中有两种寄存器位,分别为BRy及BSy,其中的y数值可以是0~15,这里的0~15表示端口的引脚号,如BR0、BS0用于控制GPIOx的第0个引脚,若x表示GPIOA,那就是控制GPIOA的第0引脚,而BR1、BS1就是控制GPIOA的第1个引脚。

其中BRy引脚的说明是“0:不会对相应的ODRx位执行任何操作;1:对相应ODRx位进行复位”。这里的“复位”是将该位设置为0的意思,而“置位”表示将该位设置为1;说明中的ODRx是另一个寄存器的寄存器位,我们只需要知道ODRx位为1的时候,对应的引脚输出高电平,为0的时候对应的引脚输出低电平即可(感兴趣的读者可以查询该寄存器GPIOx_ODR的说明)。所以,如果对BR0写入“1”的话,那么GPIOx的第0个引脚就会输出“低电平”,但是对BR0写入“0”的话,却不会影响ODR0位,所以引脚电平不会改变。要想该引脚输出“高电平”,就需要对“BS0”位写入“1”。寄存器位BSy与BRy是相反的操作。

4.5.2 C语言对寄存器的封装

以上所有关于存储器映射的内容,最终都是为了帮助大家更好地理解如何用C语言控制读写外部寄存器。下面是本章的重点内容。

1. 封装总线和外设基地址

在编程上为了方便读者理解和记忆,我们把总线基地址和外设基地址都以相应的宏加以定义,总线或者外设都以它们的名字作为宏名,具体见代码清单4-4。

代码清单4-4 总线和外设基址宏定义

    1 /* 外设基地址 */
    2 #define PERIPH_BASE              ((unsigned int)0x40000000)
    3
    4 /* 总线基地址 */
    5 #define APB1PERIPH_BASE          PERIPH_BASE
    6 #define APB2PERIPH_BASE          (PERIPH_BASE + 0x00010000)
    7 #define AHB1PERIPH_BASE          (PERIPH_BASE + 0x00020000)
    8 #define AHB2PERIPH_BASE          (PERIPH_BASE + 0x10000000)
    9
   10 /* GPIO外设基地址 */
   11 #define GPIOA_BASE               (AHB1PERIPH_BASE + 0x0000)
   12 #define GPIOB_BASE               (AHB1PERIPH_BASE + 0x0400)
   13 #define GPIOC_BASE               (AHB1PERIPH_BASE + 0x0800)
   14 #define GPIOD_BASE               (AHB1PERIPH_BASE + 0x0C00)
   15 #define GPIOE_BASE               (AHB1PERIPH_BASE + 0x1000)
   16 #define GPIOF_BASE               (AHB1PERIPH_BASE + 0x1400)
   17 #define GPIOG_BASE               (AHB1PERIPH_BASE + 0x1800)
   18 #define GPIOH_BASE               (AHB1PERIPH_BASE + 0x1C00)
   19
   20 /* 寄存器基地址,以GPIOH为例 */
   21 #define GPIOH_MODER                (GPIOH_BASE+0x00)
   22 #define GPIOH_OTYPER               (GPIOH_BASE+0x04)
   23 #define GPIOH_OSPEEDR              (GPIOH_BASE+0x08)
   24 #define GPIOH_PUPDR                (GPIOH_BASE+0x0C)
   25 #define GPIOH_IDR                  (GPIOH_BASE+0x10)
   26 #define GPIOH_ODR                  (GPIOH_BASE+0x14)
   27 #define GPIOH_BSRR                 (GPIOH_BASE+0x18)
   28 #define GPIOH_LCKR                 (GPIOH_BASE+0x1C)
   29 #define GPIOH_AFRL                 (GPIOH_BASE+0x20)
   30 #define GPIOH_AFRH                 (GPIOH_BASE+0x24)

代码清单4-4首先定义了“片上外设”基地址PERIPH_BASE;接着在PERIPH_BASE上加入各个总线的地址偏移,得到APB1、APB2等总线的地址APB1PERIPH_BASE、APB2PERIPH_BASE,然后在其之上再加入外设地址的偏移,得到GPIOA、GPIOH的外设地址;最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针操作读写了,具体见代码清单4-5。

代码清单4-5 使用指针控制BSRR寄存器

    1 /* 控制GPIOH 引脚10输出低电平(BSRR寄存器的BR10置1) */
    2 *(unsigned int *)GPIOH_BSRR = (0x01<<(16+10));
    3
    4 /* 控制GPIOH 引脚10输出高电平(BSRR寄存器的BS10置1) */
    5 *(unsigned int *)GPIOH_BSRR = 0x01<<10;
    6
    7 unsigned int temp;
    8 /* 控制GPIOH 端口所有引脚的电平(读IDR寄存器) */
    9 temp = *(unsigned int *)GPIOH_IDR;

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

2. 封装寄存器列表

用上面的方法定义地址还是稍显烦琐,例如GPIOA~GPIOH都各有一组功能相同的寄存器,如GPIOA_MODER、GPIOB_MODER、GPIOC_MODER等,它们只是地址不一样,但却要为每个寄存器都定义地址。为了更方便地访问寄存器,我们引入C语言中的结构体语法对寄存器进行封装,具体见代码清单4-6。

代码清单4-6 使用结构体对GPIO寄存器组进行封装

    1 typedef unsigned             int uint32_t; /*无符号32位变量*/
    2 typedef unsigned short       int uint16_t; /*无符号16位变量*/
    3
    4 /* GPIO寄存器列表*/
    5 typedef struct {
    6      uint32_t MODER;     /*GPIO模式寄存器                   地址偏移: 0x00        */
    7      uint32_t OTYPER;    /*GPIO输出类型寄存器               地址偏移: 0x04        */
    8      uint32_t OSPEEDR;   /*GPIO输出速度寄存器               地址偏移: 0x08        */
    9      uint32_t PUPDR;     /*GPIO上拉/下拉寄存器              地址偏移: 0x0C        */
   10      uint32_t IDR;       /*GPIO输入数据寄存器               地址偏移: 0x10        */
   11      uint32_t ODR;       /*GPIO输出数据寄存器               地址偏移: 0x14        */
   12      uint16_t BSRRL;     /*GPIO置位/复位寄存器低16位部分    地址偏移: 0x18        */
   13      uint16_t BSRRH;     /*GPIO置位/复位寄存器高16位部分    地址偏移: 0x1A        */
   14      uint32_t LCKR;      /*GPIO配置锁定寄存器               地址偏移: 0x1C        */
   15      uint32_t AFR[2];    /*GPIO复用功能配置寄存器           地址偏移: 0x20-0x24   */
   16 } GPIO_TypeDef;

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

图4-7 GPIO_TypeDef结构体成员的地址偏移

也就是说,假设所定义的GPIO_TypeDef结构体的首地址为0x4002 1C00(这也是第一个成员变量MODER的地址),那么结构体中第二个成员变量OTYPER的地址即为0x4002 1C00+0x04,0x04正是代表MODER所占用的4个字节地址的偏移量。其他成员变量相对于结构体首地址的偏移,在代码清单4-6右侧注释中已给出,其中的BSRR寄存器分成了低16位BSRRL和高16位BSRRH, BSRRL置1,引脚输出高电平,BSRRH置1,引脚输出低电平,这里分开只是为了方便操作。

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

代码清单4-7 通过结构体指针访问寄存器

    1 GPIO_TypeDef * GPIOx;          //定义一个GPIO_TypeDef结构体指针GPIOx
    2 GPIOx = GPIOH_BASE;             //把指针地址设置为宏GPIOH_BASE地址
    3 GPIOx->BSRRL = 0xFFFF;         //通过指针访问并修改GPIOH_BSRRL寄存器
    4 GPIOx->MODER = 0xFFFFFFFF;     //修改GPIOH_MODER寄存器
    5 GPIOx->OTYPER =0xFFFFFFFF;     //修改GPIOH_OTYPER寄存器
    6
    7 uint32_t temp;
    8 temp = GPIOx->IDR;             //读取GPIOH_IDR寄存器的值到变量temp中

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

最后,再进一步,直接使用宏定义好GPIO_TypeDef类型的指针,而且指针指向各个GPIO端口的首地址,实际使用时直接用该宏访问寄存器即可,具体见代码清单4-8。

代码清单4-8 定义好GPIO端口的首地址址针

    1 /*使用GPIO_TypeDef把地址强制转换成指针*/
    2 #define GPIOA                   ((GPIO_TypeDef *) GPIOA_BASE)
    3 #define GPIOB                   ((GPIO_TypeDef *) GPIOB_BASE)
    4 #define GPIOC                   ((GPIO_TypeDef *) GPIOC_BASE)
    5 #define GPIOD                   ((GPIO_TypeDef *) GPIOD_BASE)
    6 #define GPIOE                   ((GPIO_TypeDef *) GPIOE_BASE)
    7 #define GPIOF                   ((GPIO_TypeDef *) GPIOF_BASE)
    8 #define GPIOG                   ((GPIO_TypeDef *) GPIOG_BASE)
    9 #define GPIOH                   ((GPIO_TypeDef *) GPIOH_BASE)
   10
   11
   12
   13 /*使用定义好的宏直接访问*/
   14 /*访问GPIOH端口的寄存器*/
   15 GPIOH->BSRRL = 0xFFFF;         //通过指针访问并修改GPIOH_BSRRL寄存器
   16 GPIOH->MODER = 0xFFFFFFF;      //修改GPIOH_MODER寄存器
   17 GPIOH->OTYPER =0xFFFFFFF;      //修改GPIOH_OTYPER寄存器
   18
   19 uint32_t temp;
   20 temp = GPIOH->IDR;             //读取GPIOH_IDR寄存器的值到变量temp中
   21
   22 /*访问GPIOA端口的寄存器*/
   23 GPIOA->BSRRL = 0xFFFF;         //通过指针访问并修改GPIOA_BSRRL寄存器
   24 GPIOA->MODER = 0xFFFFFFF;      //修改GPIOA_MODER寄存器
   25 GPIOA->OTYPER =0xFFFFFFF;      //修改GPIOA_OTYPER寄存器
   26
   27 uint32_t temp;
   28 temp = GPIOA->IDR;             //读取GPIOA_IDR寄存器的值到变量temp中

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