跳转至

5.2 STM32驱动开发-内存和管脚映射

内容

讲解的这部分内容和代码在工程目录中的 Drivers 目录下的 stm32f10x_driver_io.cstm32f10x_driver_io.h 中。

端口复用与重映射

为了使不同器件封装的外设IO功能数量达到最优,可以把一些复用功能重新映射到其他一些不太常用的引脚上。STM32中有很多内置的外设的输入输出引脚具有重映射功能,查看STM32的datasheet可以知道每个内置外设都有若干个输入输出引脚,一般这些引脚的输出端口都是固定不变的,为了让设计工程师可以更好地安排引脚的走向和功能,意法半导体在STM32中引入了外设引脚重映射的概念,即一个外设的引脚除了可以具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚映射到其他的端口上。

简单地讲就是把管脚的外设功能映射到另外一个管脚上,但是这种映射不是随意的,只能映射到某几个引脚上,我们这里拿串口1为例进行讲解:

上图截取的是STM32参考手册的管脚重映射表,从上表中可以看出,默认情况下,串口1失能复用的时候的对应引脚为PA9和PA10,使能复用功能时的对应引脚为PB6和PB7。

所以重映射我们同样要使能复用功能的时候讲解的两个时钟外,还要使能AFIO功能时钟,然后调用重映射函数。详细步骤为:

使能GPIOB时钟:

1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

使能串口1时钟:

1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

使能AFIO时钟:

1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

开启重映射:

1
GPIO_PinRemapConifg(GPIO_Remap_USART1, ENABLE);

这样就将串口的TX和RX两个管脚重映射到PB6和PB7上面了。这里有一个技巧:可以从 GPIO_PinRemapConfig 函数入手查看第一个入口参数的取值范围来得到那些外设具有重映射功能,在stm32f10x_gpio.h 文件中定义了所有外设重映射的宏标识符,以下为摘取的一小部分:

1
2
3
4
5
6
7
8
#define GPIO_Remap_SPI1             ((uint32_t)0x00000001)  /*!< SPI1 Alternate Function mapping */
#define GPIO_Remap_I2C1             ((uint32_t)0x00000002)  /*!< I2C1 Alternate Function mapping */
#define GPIO_Remap_USART1           ((uint32_t)0x00000004)  /*!< USART1 Alternate Function mapping */
#define GPIO_Remap_USART2           ((uint32_t)0x00000008)  /*!< USART2 Alternate Function mapping */
#define GPIO_PartialRemap_USART3    ((uint32_t)0x00140010)  /*!< USART3 Partial Alternate Function mapping */
#define GPIO_FullRemap_USART3       ((uint32_t)0x00140030)  /*!< USART3 Full Alternate Function mapping */
#define GPIO_PartialRemap_TIM1      ((uint32_t)0x00160040)  /*!< TIM1 Partial Alternate Function mapping */
#define GPIO_FullRemap_TIM1         ((uint32_t)0x001600C0)  /*!< TIM1 Full Alternate Function mapping */

从上面可以看书,USART1只有一种重映射,而USART3却存在部分重映射和完全重映射两种。所谓部分重映射就是部分管脚和默认的是一样的,而部分管脚是重新映射到其他管脚上,完全重映射指的就是所有的管脚都重新映射到其他管脚上。以下就是USART3的重映射表:

部分重映射就是PB10、PB11和PB12重映射到PC10、PC11和PC12上。而PB13和PB14和没有重映射的情况时一样的,都是USART3的CTS和RTS管脚。完全重映射就是将这两个脚重新映射到PD11和PD12上去,我们要使用USART3的部分重映射而调用的方法为:

1
GPIO_PinRemapConfig(GPIO_PartialRemap_USART3, ENABLE);

寄存器地址和变量对应关系

之所以要讲解这部分知识,主要是因为常常有用户不明白MDK中那些结构体和寄存器地址是如何对应起来的。

如果读者学过51单片机的话可会知道在使用C语言开发51的过程中经常会引入一个名为reg51.h头文件,下面我们看看它是如何把名字和寄存器联系起来的:

1
sfr P0 = 0x80;

sfr是一种扩展数据类型,占用一个内存单元,取值范围为0~255。利用它可以访问51单片机内部的特殊功能寄存器。然后我们向地址为0x80的寄存器设值的方法就是:

1
P0 = value;

在STM32的开发过程中也可以这样做,但是STM32的寄存器数量非常多,如果对每个寄存器都通过这种定义变量的方式一一列出来的话,不仅不方便开发,而且也显得杂乱无章,不系统。所以意法半导体在其固件库中采用结构体来将众多寄存器组织在一起,下面我们以GPIOA的几个寄存器来讲解固件库是如何做到将结构体和寄存器地址对应起来的,为什么我们修改结构体成员变量的值就可以操作对应寄存器的值。而完成这个对应的关键在 stm32f10x.h 中。

首先查看STM32参考手册中的寄存器地址映射表:

通过这个表我们可以看出,GPIOA的7个寄存器都是32位的,所以每个寄存器占用4个字节,一共就是28个字节,相对地址偏移范围为 000h~01Bh,这个地址偏移是相对于GPIOA的基地址而说的。而GPIOA的基地址是由APB2总线的基地址+GPIOA在APB2总线上的偏移地址计算得到的,以此类推,就可以得到GPIOA相对于STM32起始地址的基地址。下面打开 stm32f10x.h 文件定位到 GPIO_TypeDef 处:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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;

然后定位到

1
#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)

可以看出GPIOA是将GPIOA_BASE强制转换为GPIO_TypeDef指针,然后查看GPIOA_BASE的宏定义:

1
#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)

通过不断迭代寻找,可以找到最顶层:

1
2
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)
#define PERIPH_BASE           ((uint32_t)0x40000000)

这样就可以计算出来GPIOA的基地址为:

1
GPIOA_BASE = 0x40000000 + 0x10000 + 0x0800 = 0x40010800

上面是我们通过固件库提供的宏定义计算出来的,那这个和STM32硬件上实际的GPIOA基地址是一致的吗?以下是STM32参考手册上的截图,可以看到两者是一致的:

解决完获得GPIOA的基地址是多少的问题,那GPIOA的7个寄存器的地址又是如何计算出来的的呢?实际上这个是通过 相对地址偏移 计算得出的,即

1
GPIOA某个寄存器的地址 = GPIOA基地址 + 该寄存器相对于GPIOA基地址的偏移地址

那么在结构体中这些寄存器有时怎么与地址一一对应的呢?这里就涉及到C语言结构体的一个特性:结构体成员变量地址按照其定义的顺序连续排列,也就是说GPIO_TypeDef结构体中定义的成员变量的顺序和STM32硬件上GPIOx各寄存器地址的顺序是一致的,这也就是为什么在固件库中进行如下操作:

1
GPIOA->BRR = value;

就是设置地址为0x40010800(GPIOA基地址) + 0x014(BRR寄存器相对偏移) = 0x40010814的寄存器BRR的值了。本质上和操作51单片机寄存器的方法一样。

GPIO口的位带操作

位带(bit-band)操作简单来说就是把一个bit膨胀为一个32bit的字,当访问这个字的时候就可以达到访问这个bit的目的,比如BSRR寄存器有32个位,那么位带操作就会将这32个位映射到32个地址上,我们只要访问这32个地址就可以达到访问这32位的目的。

对于上图,我们往地址Address0写入1,则就可以达到往寄存器的第0位Bit0写1的目的,当然这里仅仅是简单介绍一下,不想讲得过于复杂,作为Cortex-M3存储器系统所支持的一种操作数据的方式,在硬件I/O密集型的底层程序中有很多的优越性。如果想要更加深入了解的话,可以参考《Cortex-M3权威指南》第五章(P87~P92)。我们在 stm32f10x_io.h 中定义的位带操作如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#define BIT_BAND(addr, bit_nums) ((addr & 0XF0000000) + 0X2000000 + \
                                 ((addr & 0XFFFFF) << 5) + (bit_nums << 2))
#define MEM_ADDR(addr)          *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bit_nums)   MEM_ADDR(BIT_BAND(addr, bit_nums))

// IO interfaces address mapping.
#define GPIOA_ODR_ADDR           (GPIOA_BASE + 12)
#define GPIOB_ODR_ADDR           (GPIOB_BASE + 12)
#define GPIOC_ODR_ADDR           (GPIOC_BASE + 12)
#define GPIOD_ODR_ADDR           (GPIOD_BASE + 12)
#define GPIOE_ODR_ADDR           (GPIOE_BASE + 12)
#define GPIOF_ODR_ADDR           (GPIOF_BASE + 12)
#define GPIOG_ODR_ADDR           (GPIOG_BASE + 12)

#define GPIOA_IDR_ADDR           (GPIOA_BASE + 8)
#define GPIOB_IDR_ADDR           (GPIOB_BASE + 8)
#define GPIOC_IDR_ADDR           (GPIOC_BASE + 8)
#define GPIOD_IDR_ADDR           (GPIOD_BASE + 8)
#define GPIOE_IDR_ADDR           (GPIOE_BASE + 8)
#define GPIOF_IDR_ADDR           (GPIOF_BASE + 8)
#define GPIOG_IDR_ADDR           (GPIOG_BASE + 8)

#define PA_IN(n)                  BIT_ADDR(GPIOA_IDR_ADDR, n)
#define PA_OUT(n)                 BIT_ADDR(GPIOA_ODR_ADDR, n)
#define PB_IN(n)                  BIT_ADDR(GPIOB_IDR_ADDR, n)
#define PB_OUT(n)                 BIT_ADDR(GPIOB_ODR_ADDR, n)
#define PC_IN(n)                  BIT_ADDR(GPIOC_IDR_ADDR, n)
#define PC_OUT(n)                 BIT_ADDR(GPIOC_ODR_ADDR, n)
#define PD_IN(n)                  BIT_ADDR(GPIOD_IDR_ADDR, n)
#define PD_OUT(n)                 BIT_ADDR(GPIOD_ODR_ADDR, n)
#define PE_IN(n)                  BIT_ADDR(GPIOE_IDR_ADDR, n)
#define PE_OUT(n)                 BIT_ADDR(GPIOE_ODR_ADDR, n)
#define PF_IN(n)                  BIT_ADDR(GPIOF_IDR_ADDR, n)
#define PF_OUT(n)                 BIT_ADDR(GPIOF_ODR_ADDR, n)
#define PG_IN(n)                  BIT_ADDR(GPIOG_IDR_ADDR, n)
#define PG_OUT(n)                 BIT_ADDR(GPIOG_ODR_ADDR, n)

以上代码就是GPIO口的位带操作的具体实现,比如我们写下如下代码:

1
PAout(1) = 1

就是设置GPIOA的第2个管脚为1,其实际上是设置了寄存器ODR的某个位,BIT_ADDR宏所定义的公式就是来计算GPIO的某个端口对应的位带地址。有了这个位带定义,就可以像51/AVR单片机一样操作STM32的IO端口了,比如要GPIOA的第7个IO端口输出1,则可以使用:

1
PAout(6) = 1

而要判断GPIOA的第15个IO端口是否为1,则可以使用

1
if (PAin(14) == 1) ...

总结

如果不用位带操作GPIO口,只使用固件库函数操作GPIO口也是可以的,使用位带操作GPIO口和固件库操作GPIO口还是有些不同的,位带操作本质上还是一种对寄存器的直接操作,没有定义额外的成员变量进行操作,只不过 使用了一些“数学技巧” 来计算寄存器的地址,也即没有上面所讲解的结构体和寄存器要一一对应的问题,而固件库操则是为了方便、灵活地管理相关寄存器,首先定义该外设为结构体,并将该外设的相关寄存器抽象成结构体的成员变量,由于外设基地址和相对地址偏移的确定,再加上C语言中 变量名就是地址别名 这个特性,当固件库把相关寄存器所对应的成员变量按照硬件实际上的地址高低顺序排列时,就实现了抽象变量名和物理寄存器地址的一一对应,而这个对应关系也是固件库操作和位带等直接对寄存器操作的最大差别。

参考