STM32入门指南:了解寄存器与GPIO

STM32 入门 —— 寄存器与 GPIO

STM32 总线构图:

image

寄存器

什么是寄存器

根据百度百科介绍,寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。简单来说,寄存器就是存放东西的东西,存放的东西是指令、数据或地址

存放数据的寄存器最容易理解,不同的数据存在不同的寄存器下,不同的寄存器有不同的地址,要想获得数据,我们直接访问寄存器,就可以直接获得数据

指令、地址寄存器与数据寄存器相似,存放的都是 0/1 编码,由于单片机只认识机器码,机器码都是 0/1 ,只是在特别的规定下,数据寄存器中的 0/1 编码表示数据,而指令寄存器李存放的表示指令

如何找到寄存器地址

查找《STM32中文参考手册_V10》,在手册的第 28 页给出了不同寄存器的地址范围,但是手册并没有直接给出,需要稍加计算,可以得出寄存器地址

下面以读取 PB3 引脚电平为例,介绍查找步骤

首先,找到 GPIOB 的基地址:

image

PB3 引脚相关信息可以查看《STM32F103x8, STM32F103xB数据手册》:

image

可知 PB3 引脚既可以输入也可以输出

然后,需要找到输入端口寄存器的地址偏移:

image

由手册可知,地址偏移为 0x08 ,最终可知地址为:0x40010c00 + 0x08 = 0x40010c08

最后找到对应的位置:

image

这个寄存器的位数是 32 位,虽然高 16 位没有用到,每个寄存器都占据 4 个字节,32位,而 CPU 的总线一次可以操作 32 位

最后得出:PB3 的输入数据位于 0x4001 0C08 这个地址上,这个地址上存放数据的右起第 4 个位就是 PB3 引脚对应的高低电平

访问地址代码如下:

unsigned int *pGPIOB_IDR = (unsigned int *)0x40010C08;
 unsigned char PB3 = *pGPIOB_IDR & 0x8;//取出从右往左数的第4位

直接访问的操作并不好用,每操作一个寄存器就必须去查看数据手册,然后找找这个寄存器的地址
意法半导体公司为了方便大家使用,就把这些寄存器都起了一目了然的名字,把寄存器与地址映射关系放在他们提供的头文件里。这个文件就是 stm32f10x.h

寄存器映射原理

寄存器映射是在存储器的基础上进行的,所以在了解寄存器映射原理之前,需要了解存储器映射原理

存储器映射

存储器在产家制作完成后是一片没有任何信息的物理存储器,而 CPU 要进行访存就涉及到内存地址的概念,因此存储器映射就是为物理内存按一定编码规则分配地址的行为。值得注意,存储器映射一般是由产家规定,用户不能随意更改

image

image

注意:STM32 中,I-Code Bus与D-Code Bus 默认映射到 0x00000000 ~ 0x1FFFFFFF 内存地址段;AHB 系统总线默认映射到0x20000000 ~ 0xDFFFFFFF 和 0xE0100000 ~ 0xFFFFFFFF 两个内存地址段;APB 外设总线默认映射到 0xE0040000 ~ 0xE00FFFFF 内存地址段,但由于 TPIU、ETM 以及 ROM 表占用部分空间,实际可用地址区间为 0xE0042000~0xE00FF000

寄存器

寄存器映射是在存储器映射基础上进行的

STM32 中,对硬件操作,本质上就是对寄存器操作。在存储器片上外设区域,四字节为一个单元,每个单元对应不同的功能。

当我们控制这些单元时就可以驱动外设工作,我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元。

但若每次都是通过这种方式访问地址,不好记忆且易出错。这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名实质上就是寄存器名字。给已分配好地址(通过存储器映射实现)的有特定功能的内存单元取别名的过程就叫寄存器映射。

image

以 GPIO 寄存器 CRL 为例,给出 CRL 定义:

typedef struct
{
	__IO unit32_t CRL;
	__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;

在实际使用时,会有 GPIOA->CRL=0x0000 0000 这种写法,表示将 16 进制数 0 赋值给 GPIOA 的 CRL 寄存器所在的存储单元。而 GPIOA->CRL 就构造了一个寄存器映射。具体过程如下:

#define PERIPH_BASE	((unit32_t)0x40000000)

这里属于存储器级别的映射,将外设基地址映射到 0x40000000 ,可以对应下图:

image

#define APB2PERIPH_BASE	(PERIPH_BASE + 0X10000)

这里对外设基地址进行偏移量为 0x10000 的地址偏移,偏移到 APB2 总线对应外设区:

#define GPIOA_BASE	(APB2PERIPH_BASE + 0x0800)

这里对 APB2 外设基地址进行偏移量为 0x0800 的地址偏移,偏移到 GPIOA 对应区域:

#define GPIOA	((GPIO_TypeDef *) 	GPIOA_BASE)

这里将 GPIOA 宏定义为 GPIOA 基地址经过强制类型转换为 GPIO_TypeDef 的指针,这样的作用是使 GPIOA 结构体内对应的成员按顺序填充内存区域,如图3所示。因此 GPIOA 的 CRL 寄存器就是作为 GPIOA 基地址后的第一个内存块,GPIOA->CRL 的本质就是这个内存块的地址,或者说是用 GPIOA->CRL 给这个地址取了个形象的别名,即寄存器映射。

地址映射原理

在 STM32 开发中,我们通常使用库进行开发。说白了,32 开发是从底层一层一层封装上去的。到我们开发者这里,就是使用最上层的接口进行开发。但是一层一层看下去,还是对寄存器的控制,要控制寄存器,就需要操作寄存器地址。

stm32 地址映射如下:

image

在倒数第三紫色区域是片上外设的地址区域,这里反映了片上外设的地址,我们通过操作这些地址,便能操作这些外设寄存器。
在 stm32 中,有三大总线,AHB 总线,APB1 总线以及 APB2 总线。不同的外设挂载在不同的总线上。比如 GPIO,串口 1 ,ADC 以及部分定时器挂载在 APB2 总线上。
打开 stm32f10x.h 这个文件,这个文件主要包含 stm32 中寄存器地址和结构体类型定义,在使用到固件库的地方都要包含该头文件。这里通过一些宏定义代码来说明一下地址映射与挂载总线的关系。

#define GPIOC_BASE	(APB2PERIPH_BASE + 0x1000)
#define APB2PERIPH_BASE	(PERIPH_BASE + 0x10000)
#define PERIPH_BASE	((unit32_t)0x40000000)

从代码可以看到 APB2 之类的字眼,这不是总线么?注意,有一个 PERIPH_BASE 的地址为 0x40000000 ,这不是片上外设的首地址么。这里,这个地址称作外设基地址。同样,APB2PERIPH_BASE 称作 APB2 总线外设基地址,毕竟都有 base。

总线的基地址:

image

也就是说,该总线上所挂载的模块都在这个地址区间内。下面的图是挂载在总线上面各寄存器以及寄存器组的地址:

image

image

GPIO

对于一个单片机,对基本的代码操作就是点亮 LED 灯,也就是说要对 I/O 口进行操作,STM32 中的 I/O 端口就是 GPIO ,软件可以控制的通用输入输出端口,STM32 通过 GPIO 引脚链接外部设备,达到与外部通信,控制以及采集数据的功能

引脚分类与说明如下:

image

基础知识

STM32F103C8 的开发板里总共有 5 组 I/O 口、 37 个IO口,分别是 GPIOA~GPIOG 。每个 I/O 端口位可以自由编程,可以由软件分别配置成多种模式,但 I/O 端口寄存器必须按 32 位字节访问,不允许半字或单字节访问。

GPIOx_BSRR 和 GPIOx_BRR 寄存器允许对任何 GPIO 寄存器的读/更改的独立访问;这样,在读和更改访问之间产生 IRQ 时不会发生危险。端口位配置 CNFx[1:0]=xxb,MODEx[1:0]=xxb

GPIO_InitTypeDef 是 GPIO 口的一个定义结构体,包含一个 16 位的变量 GPIO_Pin ;一个 GPIOSpeed_TypeDef 枚举结构体GPIO_Speed;一个 GPIOMode_TypeDef 枚举结构体 GPIO_Mode ;这 3 个变量可以在外部被定义,用于初始化或者改变某些 GPIO 的速度跟类型。

STM32 每个 GPI/O 端口有两个 32 位配置寄存器(GPIOx_CRL,GPIOx_CRH),两个32位数据寄存器(GPIOx_IDR,GPIOx_ODR),一个 32 位置位/复位寄存器(GPIOx_BSRR),一个 16 位复位寄存器(GPIOx_BRR)和一个 32 位锁定寄存器(GPIOx_LCKR)。

GPIO 的工作模式主要有八种:4 种输入方式,4 种输出方式,分别为输入浮空,输入上拉,输入下拉,模拟输入;输出方式为开漏输出,开漏复用输出,推挽输出,推挽复用输出。同时,GPIO还支持三种最大翻转速度(2MHz、10MHz、50MHz)

I/O 端口位的基本结构:

image

5v 兼容 I/O 端口位的基本结构:

image

GPIO 电平标准

在 STM32 的数据手册中我们可以查到,STM32 单片机的 I/O 口电平兼容 CMOS 电平和 TTL 电平,逻辑电平 0 所代表得电压范围在 0.8v 以下,大于 2v 的话就代表逻辑 1 。

image

所有 I/O 端口都是 CMOS 和 TTL 兼容(不需软件配置),它们的特性考虑了多数严格的 CMOS 工艺或 TTL 参数:
● 对于 VIH:
− 如果 VDD 是介于 [2.00V~3.08V] ;使用CMOS特性但包含 TTL。
− 如果 VDD 是介于 [3.08V~3.60V] ;使用TTL特性但包含 CMOS。
● 对于 VIL:
− 如果 VDD 是介于 [2.00V~2.28V] ;使用TTL特性但包含 CMOS。
− 如果 VDD 是介于 [2.28V~3.60V] ;使用CMOS特性但包含 TTL。

由于本人不是专业学习电路的,所以这里仅作上述简单介绍

GPIO 端口初始化

GPIO 端口初始化大致可以分为三个步骤:

1.时钟配置

2.输入输出模式设置

3.最大速率设置

时钟配置

对于 STM32 有 5 个时钟源,如下:

简称 时钟源名称 频率范围
HSI 高速内部时钟 8MHZ
HSE 高速外部时钟 4MHZ~16MHZ
LSI 低速内部时钟 40KHZ
LSE 低速外部时钟 32.768KHZ
PLL 锁相环倍频输出 输出频率最大不得超过 72MHz

时钟树如下图:

image

程序刚启动的时候,stm32采用的为内部高速时钟。如果需要采用外部时钟,需要按照如下的方式配置:

  1. 时钟初始化,即将时钟的寄存器采用默认值。

  2. 开始外部时钟且外部时钟起震准备就绪。

  3. 设置 PLLXTPRE(只能在关闭PLL时才能写入此位),可选择分频不分频。

  4. 设置进入 PLL 的源时钟(只能在关闭 PLL 时才能写入此位)。因为采用外部时钟所以只有一种设置。

  5. 设置 PLL 倍频系数 PLLMUL(只有在 PLL 关闭的情况下才可被写入)。

  6. 开启 PLL ,且准备就绪。

  7. 设置 SW ,选择时钟源为系统时钟。

  8. 判断是否是预选的时钟为系统时钟。

输入输出模式设置
  1. GPIO_Mode_AIN 模拟输入 (应用ADC模拟输入,或者低功耗下省电)

  2. GPIO_Mode_IN_FLOATING 浮空输入 (浮空就是浮在半空,可以被其他物体拉上或者拉下,可以用于按键输入)

  3. GPIO_Mode_IPD 下拉输入 (IO内部下拉电阻输入)

  4. GPIO_Mode_IPU 上拉输入 (IO内部上拉电阻输入)

  5. GPIO_Mode_Out_OD 开漏输出(开漏输出:输出端相当于三极管的集电极. 要得到高电平状态需要上拉电阻才行)

  6. GPIO_Mode_Out_PP 推挽输出 (推挽就是有推有拉电平都是确定的,不需要上拉和下拉,IO输出0-接GND, IO输出1 -接VCC,读输入值是未知的 )

  7. GPIO_Mode_AF_OD 复用开漏输出(片内外设功能(I2C的SCL,SDA))

  8. _Mode_AF_PP 复用推挽输出 (片内外设功能(TX1,MOSI,MISO.SCK.SS))

代码 模式
GPIO_Mode_IPU 上拉输入
GPIO_Mode_AIN 模拟输入
GPIO_Mode_IN_FLOATING 浮空输入
GPIO_Mode_IPD 下拉输入
GPIO_Mode_Out_OD 开漏输出
GPIO_Mode_Out_PP 推挽输出
GPIO_Mode_AF_OD 复用开漏输出
GPIO_Mode_AF_PP 复用推挽输出
输入模式名称 作用 原理
上拉输入模式 在默认状态下(GPIO引脚无输入),读取得的 GPIO 引脚数据为 1,高电平 与 VDD 相连的为上拉电阻。再连接到施密特触发器就把电压信号转化为1的数字信号存储在输入数据寄存器( IDR )
下拉输入模式 在默认状态下( GPIO 引脚无输入),读取得的 GPIO 引脚数据为 0,低电平 与 VSS 相连的为下拉电阻。再连接到施密特触发器就把电压信号转化为 0 的数字信号存储在输入数据寄存器( IDR )
浮空输入模式 配置成这个模式直接用电压表测量其引脚电压为1点几伏,这是个不确定值 没有接上拉,也没有接下拉电阻,经由触发器输入
模拟输入模式 把电压信号传送到片上外设模块,如传送至给 ADC 模块,由 ADC 采集电压信号 关闭了施密特触发器,不接上、下拉电阻
输出模式名称 作用 原理
普通推挽输出 推挽输出的供电平为 0 伏,高电平为 3.3 pp*伏 在输出高电平时,P-MOS 导通,低电平时,N-MOS 管导通。两个管子轮流导通,一个负责灌电流,一个负责拉电流,使其负载能力和开关速度都比普通的方式有很大的提高
普通开漏输出 控制输出为 0,低电平;若控制输出为 1,为高阻态 如果我们控制输出为 0,低电平,则使 N-MOS 管导通,使输出接地,若控制输出为1 (无法直接输出高电平),则既不输出高电平,也不输出低电平,为高阻态
复用推挽输出 GPIO 的引脚用作串口的输出
复用开漏输出 用在 IC、SMBUS 这些需要线与功能的复用场合

注意:在使用任何一种开漏模式,都需要接上拉电阻

最大速率设置

GPIO 的输出速率:GPIO 电平每秒切换的最大次数

这个输出速率主要体现 I/O 驱动电路的输出反应能力,通过选择不同的输出驱动速率,实现最佳的噪声与和功耗控制。不难理解,选择输出驱动速率越高,噪声也越大,相应的芯片功耗也会越大

当 STM32 的 GPIO 端口设置为输出模式时,有三种速度可以选择:2MHz、10MHz 和 50MHz,这个速度是指 I/O 口驱动电路的速度,是用来选择不同的输出驱动模块,达到最佳的噪声控制和降低功耗的目的。

对于 STM32 GPIO 输出速率的选择问题,我们在开发应用中应多加注意。如果因为这个输出速率选择导致麻烦,原因往往比较隐晦,很难直接从代码语句或程序逻辑上找到突破。

高频的驱动电路,噪声也高,当你不需要高的输出频率时,请选用低频驱动电路,这样非常有利于提高系统的EMI性能。

当然如果你要输出较高频率的信号,但却选用了较低频率的驱动模块,你很可能会得到失真的输出信号。实际上芯片内部在I/O口的输出部分安排了多个响应速度不同的输出驱动电路,用户可以根据自己的需要选择合适的驱动电路。在满足实际应用需求的前提下,速率就低不就高,这对降低功耗、减少噪声、改善 EMI 都有好处

gpio 不同速率设置对实际开发的影响:
1、LED 闪烁快慢不一致
2、Audio 噪声
其中有 I2S 的音频播放功能。在调试时用到 Printf 串口打印,发现使用 printf 输出时会出现噪音,如果关闭 printf 则正常。直到将 UART 的 TX 输出端口的管脚输出速率由 very high 改为 Low 后噪声消失。
3、SPI 通信异常
用到 SPI 通信。STM32 做主,其它外围器件做从,有时发现 SPI 读取数据总是出错。对于这里的通信出错,如果 SPI 通信端口脚的输出速率选择跟实际通信速率不合适的话也会出现。相比实际速率需求,过高或过低了都会导致通信出错。

注意:GPIO 的引脚速度是指 I/O 口驱动电路的响应速度而不是输出信号的速度,输出信号的速度与你的程序有关

代码示例
/* GPIO_InitTypeDef结构体 */
typedef enum
{
  GPIO_Speed_10MHz = 1,  //枚举常量,值为 1,代表输出速率最高为 10MHz
  GPIO_Speed_2MHz,       //对不赋值的枚举变量,自动加 1,此常量值为 2
  GPIO_Speed_50MHz       //常量值为 3
} GPIOSpeed_TypeDef;
 
typedef enum
{
  GPIO_Mode_AIN = 0x0, //模拟输入模式
  GPIO_Mode_IN_FLOATING = 0x04, //浮空输入模式
  GPIO_Mode_IPD = 0x28, //下拉输入模式
  GPIO_Mode_IPU = 0x48, //上拉输入模式
  GPIO_Mode_Out_OD = 0x14, //开漏输出模式
  GPIO_Mode_Out_PP = 0x10, //通用推挽输出模式
  GPIO_Mode_AF_OD = 0x1C,  //复用功能开漏输出
  GPIO_Mode_AF_PP = 0x18   //复用功能推挽输出
} GPIOMode_TypeDef;
 
typedef struct
{
  uint16_t GPIO_Pin;              /* 指定要配置的引脚 */
  GPIOSpeed_TypeDef GPIO_Speed;   /* 指定GPIO引脚输出的最高频率 */
  GPIOMode_TypeDef GPIO_Mode;     /* 指定GPIO引脚工作状态 */
} GPIO_InitTypeDef;
/* 初始化GPIO -- GPIO_Init() */
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
  uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;
  uint32_t tmpreg = 0x00, pinmask = 0x00;
  /* 断言,用于检查输入的参数是否正确 */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
  assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
  assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
  /*---------------------------- GPIO 的模式配置 -----------------------*/
  /*把输入参数 GPIO_Mode 的低四位暂存在 currentmode*/
  currentmode = ((uint32_t)GPIO_InitStruct -
                 > GPIO_Mode) & ((uint32_t)0x0F);
  /*判断是否为输出模式,输出模式,可输入参数中输出模式的 bit4 位都是 1*/
  if ((((uint32_t)GPIO_InitStruct -
        > GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
  {
    /* 检查输入参数 */
    assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
    /* 输出模式,所以要配置 GPIO 的速率:00(输入模式) 01(10MHz) 10(2MHz) 11 */
    currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
  }
  /*----------------------------配置 GPIO 的 CRL 寄存器 -----------------------
  -*/
  /* 判断要配置的是否为 pin0 ~~ pin7 */
  if (((uint32_t)GPIO_InitStruct -
       > GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
  {
    /*备份原 CRL 寄存器的值*/
    tmpreg = GPIOx->CRL;
    /*循环,一个循环设置一个寄存器位*/
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      /*pos 的值为 1 左移 pinpos 位*/
      pos = ((uint32_t)0x01) << pinpos;
      /* 令 pos 与输入参数 GPIO_PIN 作位与运算,为下面的判断作准备 */
      currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
      /*判断,若 currentpin=pos,说明 GPIO_PIN 参数中含的第 pos 个引脚需要配置*/
      if (currentpin == pos)
      {
        /*pos 的值左移两位(乘以 4),因为寄存器中 4 个寄存器位配置一个引脚*/
        pos = pinpos << 2;
        /*以下两个句子,把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变*/
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
        /* 向寄存器写入将要配置的引脚的模式 */
        tmpreg |= (currentmode << pos);
        /* 复位 GPIO 引脚的输入输出默认值*/
        /*判断是否为下拉输入模式*/
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
          /*下拉输入模式,引脚默认置 0,对 BRR 寄存器写 1 可对引脚置 0*/
          GPIOx->BRR = (((uint32_t)0x01) << pinpos);
        }
        else
        {
          /*判断是否为上拉输入模式*/
          if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
          {
            /*上拉输入模式,引脚默认值为 1,对 BSRR 寄存器写 1 可对引脚置 1*/
            GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
          }
        }
      }
    }
    /*把前面处理后的暂存值写入到 CRL 寄存器之中*/
    GPIOx->CRL = tmpreg;
  }
  /*---------------------------- 以下部分是对 CRH 寄存器配置的 -----------------
  --------当要配置的引脚为 pin8 ~~ pin15 的时候,配置 CRH 寄存器, -----
  ------------- -----这过程和配置 CRL 寄存器类似------------------------------
  ------
  -------读者可自行分析,看看自己是否了解了上述过程--^_^-----------*/
  /* Configure the eight high port pins */
  if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
  {
    tmpreg = GPIOx->CRH;
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      pos = (((uint32_t)0x01) << (pinpos + 0x08));
      /* Get the port pins position */
      currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
      if (currentpin == pos)
      {
        pos = pinpos << 2;
        /* Clear the corresponding high control register bits */
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
        /* Write the mode configuration in the corresponding bits */
        tmpreg |= (currentmode << pos);
        /* Reset the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
          GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
        }
        /* Set the corresponding ODR bit */
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
        {
          GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
        }
      }
    }
    GPIOx->CRH = tmpreg;
  }
}

参考资料:

1.【嵌入式系统】存储器映射与寄存器映射原理

2.STM32寄存器的简介、地址查找,与直接操作寄存器

3.简谈stm32的地址映射

4.STM32入门之GPIO详解

5.STM32入门-GPIO初始化步骤

6.GPIO代码详解

物联沃分享整理
物联沃-IOTWORD物联网 » STM32入门指南:了解寄存器与GPIO

发表评论