八、STM32的 GPIO
想要控制 LED 灯,当然是通过控制STM32 芯片的I/O 引脚电平的高低来实现。在 STM32 芯片上,I/O 引脚可以被软件设置成各种不同的功能,如输入或输出,所以被称为 GPIO(General-purpose I/O)。而 GPIO 引脚又被分GPIOA、GPIOB„„GPIOG 不同的组,每组端口分为0~15,共 16 个不同的引脚,对于不同型号的芯片,端口的组和引脚的数量不同,具体请参考相应芯片型号的 datasheet。 于是,控制 LED的步骤就自然整理出来了: 1. GPIO 端口引脚多 --> 就要选定需要控制的特定引脚 2. GPIO 功能如此丰富 --> 配置需要的特定功能 3. 控制 LED 的亮和灭 --> 设置 GPIO 输出电压的高低
继续思考,要控制 GPIO端口,就要涉及到控制相关的寄存器。这时我们就要查一查与 GPIO 相关的寄存器了,可以通过《 STM32 参考手册》来查看。
1. 配置寄存器:选定GPIO 的特定功能,最基本的如:选择作为输入还是输出端口。 2. 数据寄存器:保存了GPIO 的输入电平或 将要输出的电平。 3. 位控制寄存器:设置某引脚的数据为 1 或 0,控制输出的电平。 4. 锁定寄存器:设置某锁定引脚后,就不能修改其配置。 一切要以官方的数据手册为依据。接下来查看各寄存器相应位的含义,进行设置即可。 例如:对 x端口的寄存器 GPIOx_BSRR 的第0位(BS0)进行写1,则 x 端口的第 0 引脚被设置为 1,输出高电平,若要令第0 引脚再输出低电平,则需要向GPIOx_BSRR 的第16位(BR0)写 1。 首先请大家回顾一下在 51 单片机上点亮LED 是怎样实现的。
[cpp] view plain copy
- <span style="font-size:18px;">#include<reg52.h>
- int main (void)
- {
- P0=0;
- while(1);
- }
- </span>
以上代码就可以点亮 P0端口与 LED 阴极相连的LED 灯了,当然,这里省略了启动代码。为什么这个 P0 =0;句子就能控制P0 端口为低电平?很多刚入门 51 单片机的同学还真解释不来,关键之处在于这个代码所包含的头文件
<reg52.h>。在这个文件下有以下的定义:
1. /* BYTE Registers */
2. sfr P0 = 0x80;
3. sfr P1 = 0x90;
.........
20. sfr IP = 0xB8;
21. sfr SCON = 0x98;
22. sfr SBUF = 0x99;
这些定义被称为地址映射。 所谓地址映射,就是将芯片上的存储器 甚至 I/O等资源与地址建立一一对应的关系。如果某地址对应着某寄存器,我们就可以运用c 语言的指针来寻址并修改这个地址上的内容,从而实现修改该寄存器的内容。 正是因为<reg52.h>头文件中有了对于各种寄存器和I/O端口的地址映射,我们才可以在 51 单片机程序中方便地使用P0=0xFF; TMOD =0xFF等赋值句子对寄存器进行配置,从而控制单片机。 Cortex-M3 的地址映射也是类似的。Cortex-M3 有 32 根地址线,所以它的寻址空间大小为 2^32 bit=4GB。 ARM 公司设计时,预先把这 4GB 的寻址空间大致地分配好了。它把地址从 0x4000 0000 至 0x5FFF FFFF( 512MB )的地址分配给片上外设。通过把片上外设的寄存器映射到这个地址区,就可以简单地以访问内存的方式,访问这些外设的寄存器,从而控制外设的工作。结果,片上外设可以使用C 语言来操作。 M3存储器映射见下图
stm32f10x.h 这个文件非常重要,是一个非常底层的文件。所有处理器厂商都会将对内存的操作封装成一个宏,即我们通常说的寄存器,并且把这些实现封装成一个系统文件,包含在相应的开发环境中。这样,我们在开发自己的应用程序的时候只要将这个文件包含进来就可以了。
stm32f10x.h 这个文件中重要的内容就是把 STM32 的所有寄存器进行地址映射。如同51 单片机的<reg52.h>头文件一样,stm32f10x.h像一个大表格,我们在使用的时候就是通过宏定义进行类似查表的操作,大家想像一下没有这个文件的话,我们要怎样访问 STM32的寄存器?有什么缺点?
不进行这些宏定义的缺点有: 1、地址容易写错 2、我们需要查大量的手册来确定哪个地址对应哪个寄存器 3、看起来还不好看,且容易造成编程的错误,效率低,影响开发进度。 在这里我们以外接了 LED灯的外设 GPIOC 为例,在这个文件中有这样的一系列宏定义:
1. #define GPIOC_BASE(APB2PERIPH_BASE + 0x1000)
2. #define APB2PERIPH_BASE(PERIPH_BASE + 0x10000)
3. #define PERIPH_BASE((uint32_t)0x40000000) 外设基地址
首先看到 PERIPH_BASE 这个宏,宏展开为 0x40000000,并把它强制转换为 uint32_t 的 32 位类型数据,这是因为地 STM32 的地址是32 位的,是不是觉得0x40000000 这个地址很熟?是的,这个是 Cortex-M3核分配给片上外设的从 0x40000000 至 0x5FFF FFFF 的 512MB 寻址空间中的第一个地址,我们把0x4000 0000 称为外设基地址。 总线基地址
接下来是宏 APB2PERIPH_BASE,宏展开为 PERIPH_BASE(外设基地址) 加上偏移地址0x1 0000,即指向的地址为 0x40010000。这个APB2PERIPH_BASE宏是什么地址呢? STM32 不同的外设是挂载在不同的总线上的。有AHB 总线、 APB2 总线、 APB1 总线,挂载在这些总线上的外设有特定的地址范围。 其中像 GPIO、串口1、 ADC 及部分定时器是挂载这个被称为 APB2 的总线上,挂载到 APB2 总线上的外设地址空间是从 0x4001 0000 至地址0x4001 3FFF。这里的第一个地址,也就是0x4001 0000,被称为APB2PERIPH_BASE (APB2 总线外设的基地址)。而 APB2 总线基地址相对于外设基地址的偏移量为 0x1 0000 个地址,即为APB2 相对外设基地址的偏移地址。 寄存器组基地址
最后到了宏 GPIOC_BASE,宏展开为APB2PERIPH_BASE (APB2 总线外设的基地址)加上相对 APB2 总线基地址的偏移量0x1000 得到了GPIOC 端口的寄存器组的基地址。这个所谓的寄存器组又是什么呢?它包括什么寄存器?
注意到这个说明中有一个偏移地址: 0x04,这里的偏移地址的是相对哪个地址的偏移呢?下面进行举例说明。
对于 GPIOC 组的寄存器,GPIOC 含有的端口配置高寄存器(GPIOC_CRH)寄存器地址为:GPIOC_BASE +0x04。也就是说,这个偏移地址,就是该寄存器相对所在寄存器组基地址的偏移量。 ST 公司的工程师采用了更巧妙的方式来确定这些地址。 STM32 库对寄存器的封装 ST 的工程师用结构体的形式,封装了寄存器组,在 stm32f10x.h文件中,有以下代码:
1. #define GPIOA((GPIO_TypeDef *) GPIOA_BASE)
2. #define GPIOB((GPIO_TypeDef *) GPIOB_BASE)
3. #define GPIOC((GPIO_TypeDef *) GPIOC_BASE) 有了这些宏,我们就可以定位到具体的寄存器地址,在这里发现了一个陌生的类型GPIO_TypeDef,追踪它的定义,可以在 stm32f10x.h 文件中找到如下代码:
1. typedef struct
2. {
3. __IO uint32_t CRL;
4. __IO uint32_t CRH;
5. __IO uint32_t IDR;
6. __IO uint32_t ODR;
7. __IO uint32_t BSRR;
8. __IO uint32_t BRR;
9. __IO uint32_t LCKR;
10. } GPIO_TypeDef; 这个代码用 typedef 关键字声明了名为GPIO_TypeDef的结构体类型,结构体内又定义了 7 个 __IOuint32_t类型的变量。这些变量每个都为 32位,也就是每个变量占内存空间 4个字节。
在 c 语言中,结构体内变量的存储空间是连续的,也就是说假如我们定义了一个GPIO_TypeDef,这个结构体的首地址(变量 CRL 的地址)若为 0x40011000, 那么结构体中第二个变量(CRH)的地址即为0x40011000 +0x04 ,加上的这个0x04,正是代表 4 个字节地址的偏移量。这个 0x04 偏移量,正是 GPIOx_CRH 寄存器相对于所在寄存器组的偏移地址。同理, GPIO_TypeDef结构体内其它变量的偏移量,也和相应的寄存器偏移地址相符。于是,只要我们匹配了结构体的首地址,就可以确定各寄存器的具体地址了。 以后我们写代码的时候,如果要修改 GPIO的寄存器,就可以用以下的方式来实现。 1. GPIO_TypeDef * GPIOx; //定义一个GPIO_TypeDef型结构体指针GPIOx
2. GPIOx = GPIOA; //把指针地址设置为宏GPIOA地址
3. GPIOx->CRL = 0xffffffff; //通过指针访问并修改GPIOA_CRL 寄存器
通过类似的方式,我们就可以给具体的寄存器写上适当的参数,控制 STM32 了。
|