初探STM32F4(1)--GPIO

   日期:2020-05-12     浏览:106    评论:0    
核心提示:这里写目录标题一级目录二级目录三级目录一级目录二级目录三级目录嵌入式

通用I/O

  • GPIO概述
  • GPIO硬件架构与寄存器
    • GPIO硬件架构
    • GPIO工作模式
    • GPIO相关寄存器
      • 4个配置寄存器
      • 2个数据寄存器
      • 1个置位/复位寄存器
      • 1个锁存寄存器
      • 2个复用功能寄存器
  • GPIO库文件架构与代码剖析
    • 寄存器版本
      • GPIO结构体定义
      • GPIO地址映射
      • GPIO初始化(寄存器配置)
      • GPIO赋值操作
    • HAL库版本
      • GPIO结构体定义
      • GPIO初始化设置
      • GPIO赋值操作
  • GPIO外部中断

本文从两个方面剖析GPIO:

  • 结合《STM32F4xx中文参考手册》,从硬件和寄存器的角度剖析GPIO
  • 结合库例程,从软件角度剖析GPIO如何配置,同时剖析嵌入式代码规范

阅读完本文,要能回答以下问题:

  1. 简述GPIO口的硬件架构与配置寄存器
  2. 配置GPIO口寄存器的代码流程
  3. 然后就是结合配置GPIO的代码回答一些嵌入式代码规范的问题:
    1. 寄存器前面的类型修饰符的含义,例如__IO uint32_t ODR
    2. 为啥要用volatile类型修饰符?
    3. 使用unsigned int与int声明后续配置有什么区别?
    4. #define与typedef有什么区别?
    5. 为什么嵌入式代码中有这么多宏定义?
    6. GPIO定义的结构体中的这些配置寄存器变量是怎么映射到实际地址的?
    7. 嵌入式代码中常常需要进行位操作,结合配置GPIO的代码,谈谈如何进行位操作?
    8. 嵌入式代码中常常需要对多位进行位操作,结合配置GPIO的代码,谈谈如何进行位的遍历?
    9. M3M4内核通过位带映射支持直接对位进行操作,谈谈你对位带操作的认识?
    10. 嵌入式代码进入带参数的函数调用中去时,常常会先使用assert_param()函数,结合代码谈谈==assert_param()==的作用

GPIO概述

每组GPIO口包括4个32位的配置寄存器、2个32位的数据寄存器、1个32位的置位/复位寄存器、1个32位锁定寄存器和2个32位复用功能选择寄存器,一共10个寄存器。研究清楚这些寄存器有什么用、以及如何配置,也就搞懂了GPIO。

GPIO硬件架构与寄存器

GPIO硬件架构

GPIO工作模式

GPIO有八种工作模式,四种输入工作模式:
1.== 输入浮空模式==。上拉/下拉电阻不起作用,外部电平直接输入到芯片内部,适合电流要求小的场合。信号送至输入数据寄存器
2.== 输入上拉模式==。上拉电阻起作用。低电平输入时对芯片外部有灌电流。信号送至输入数据寄存器
3. 输入下拉模式。下拉电阻起作用。高电平输入时对芯片外部有拉电流。信号送至输入数据寄存器
4. 模拟模式。上拉/下拉电阻不起作用,信号沿模拟输入线输入到芯片内部ADC

四种输出工作模式:

  1. 开漏输出模式。CPU通过置位/复位寄存器或直接写操作,设置好输出数据寄存器。输出驱动器只有一个MOS管工作,当输出1时,MOS管截止,通过外接上拉电阻实现输出高电平。当输出0时,MOS管导通,输出低电平。
  2. 开漏复用输出模式。输出的数据来自CPU的片上复用外设。
  3. 推挽式输出。输出驱动器的NMOS与PMOS管均工作,当输出1时,PMOS导通,输出高电平,当输出0时,NMOS导通,输出低电平。
  4. 推挽式复用输出模式。输出的数据来自CPU的片上复用外设。

GPIO上电复位后,默认工作在输入浮空状态。

GPIO相关寄存器

注意,每组GPIO口通过10个相关寄存器管理好,一组包括16个GPIO口,那么,如果某个寄存器需要2位配置一个口,那刚好均分位,如果某个寄存器只需要1位配置一个口,那一般高16位保留。

4个配置寄存器

1、端口模式寄存器(GPIOx_MODER)

  • 每两个位控制一个IO口:00—输入浮空模式(复位状态);01—通用输出模式;10—复用功能模式;11—模拟模式。不用记忆、会查DATASHEET看懂就行

2、端口输出类型寄存器(GPIOx_OTYPER)

  • 配置成推挽或开漏输出,每1位就能控制一个IO口,所以高16位保留

3、端口输出速度寄存器(GPIOx_OSPEEDR)

  • 有四种输出速度,每两个位控制一个IO口

4、端口上拉下拉寄存器(GPIOx_PUPDR)

  • 有三种情况(无上拉下拉、上拉、下拉),每两个位控制一个IO口

2个数据寄存器

5、端口输入数据寄存器(GPIOx_IDR)

  • 每个GPIO的输入有两种情况,用一位来记录,所以高16位保留,这些位是只读模式的。

6、端口输出数据寄存器(GPIOx_ODR)

  • 每个GPIO的输出有两种情况,用一位来记录,所以高16位保留,这些位是可读可写的。

1个置位/复位寄存器

7、端口置位/复位寄存器(GPIOx_BSRR)

  • 高16位为复位位、低16位为置位位。这些位为只写,0无影响,1有作用。
  • ==已经有数据寄存器了,为啥还要这个寄存器?==如果只有数据寄存器,每次设置某一个IO口的取值时,需要先进行读操作,保证其他IO口不受影响,再设置这个IO口。有了置位/复位寄存器,无需进行读操作了,把不用操作的IO口写0就不会影响到那些IO口了。

1个锁存寄存器

8、端口锁存寄存器(GPIOx_LCKR)

  • 用的不多,当IO口被锁住时,无法执行CPU的写操作了。

2个复用功能寄存器

9、复用功能低位寄存器(GPIOx_AFRL)

每4位控制一个IO口的复用功能,低位寄存器控制低8位的8个GPIO口的复用功能。一个GPIO口可以复用16个复用功能

10、复用功能高位寄存器(GPIOx_AFRH)

每4位控制一个IO口的复用功能,高位寄存器控制高8位的8个GPIO口的复用功能。

GPIO库文件架构与代码剖析

两个层次,首先是寄存器版本例程,学习如何操作相应寄存器实现操控GPIO口。

寄存器版本

GPIO结构体定义

根据上文所讲的,每组GPIO口对应的10个相关寄存器,定义了以下结构体:

typedef struct
{
  __IO uint32_t MODER;    
  __IO uint32_t OTYPER;   
  __IO uint32_t OSPEEDR;  
  __IO uint32_t PUPDR;    
  __IO uint32_t IDR;      
  __IO uint32_t ODR;      
  __IO uint32_t BSRR;     
  __IO uint32_t LCKR;     
  __IO uint32_t AFR[2];   
} GPIO_TypeDef;

看了上面的结构体定义,我产生了几个疑问:

(1)数据类型_IO uint32_t是什么声明类型?

#define __IO volatile 
typedef unsigned  int uint32_t;

_IO是volatile类型修饰符,uint32_t是unsigned int。

(2)为啥要用volatile类型修饰符?
volatile的意思是告诉编译器,在编译源代码时,对这个变量不要使用优化。具体看这篇文章.

比如写这个io端口的时候,如果没有这个volatile,很可能由于编译器的优化,会先把值先写到一个缓冲区,到一定时候
再写到io端口,这样就不能使数据及时的写到io端口,有了volatile说明以后,就不会再经过cache,write buffer这种,而是直接写到io端口,从而避免了读写io端口的延时

(3)使用unsigned int与int声明的区别?
unsigned int占四个字节,int是默认有符号的即signed int也占4个字节。有符号无符号在使用上有区别,例如我要把寄存器ODR的32位全置1,十进制下,按照无符号类型我要赋值232-1,有符号类型我要赋值-231

(4)#define与typedef有什么区别?

  • 在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明。
  • #define 宏名 字符串 。其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。
  • 关键字typedef在编译阶段有效,由于是在编译阶段,因此typedef有类型检查的功能。
    define则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查。具体看这篇文章.

(5)结构体中的这些变量是怎么跟实际地址映射到一起的呢?

GPIO地址映射

使用上述声明的结构体定义了11组GPIO

#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)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK ((GPIO_TypeDef *) GPIOK_BASE)

这时候又有疑问了,GPIO的地址映射是什么样的?

每组GPIO的地址是用外设总线基地址+偏移量定义好的

#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)
#define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)
#define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000)
#define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400)
#define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800)
#define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00)
#define GPIOI_BASE (AHB1PERIPH_BASE + 0x2000)
#define GPIOJ_BASE (AHB1PERIPH_BASE + 0x2400)
#define GPIOK_BASE (AHB1PERIPH_BASE + 0x2800)

外设总线基地址是用外设基地址+偏移量定义好的


#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
#define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000)

外设基地址是绝对地址了。

#define SRAM2_BASE ((uint32_t)0x2001C000) 
#define SRAM3_BASE ((uint32_t)0x20020000) 
#define PERIPH_BASE ((uint32_t)0x40000000) 

  uint32_t Mode;      

  uint32_t Pull;      

  uint32_t Speed;     

  uint32_t Alternate;  
}GPIO_InitTypeDef;

跟寄存器形式相比较,似乎少了一些定义,然后发现定义的Mode变量既可以设置端口模式,还可以设置端口输出类型。

#define GPIO_MODE_INPUT ((uint32_t)0x00000000) 
#define GPIO_MODE_OUTPUT_PP ((uint32_t)0x00000001) 
#define GPIO_MODE_OUTPUT_OD ((uint32_t)0x00000011) 
#define GPIO_MODE_AF_PP ((uint32_t)0x00000002) 
#define GPIO_MODE_AF_OD ((uint32_t)0x00000012) 

怎么实现一个32位的变量去设置两个32位的寄存器的呢?那就要看后续GPIO初始化的程序。

GPIO初始化设置

void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_Initure;
    __HAL_RCC_GPIOB_CLK_ENABLE();           //使能IO时钟
	
    GPIO_Initure.Pin=GPIO_PIN_0|GPIO_PIN_1; //PB1,0
    GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;  //推挽输出
    GPIO_Initure.Pull=GPIO_PULLUP;          //上拉
    GPIO_Initure.Speed=GPIO_SPEED_HIGH;     //¸高速
    HAL_GPIO_Init(GPIOB,&GPIO_Initure);
	
    HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);	//PB0置1
    HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_SET);	//PB1置1
}

首先是对GPIO_InitTypeDef的成员变量赋值,然后进入HAL_GPIO_Init()函数

HAL库版本的GPIO初始化代码有100多行,比寄存器版本的代码多了10倍,为什么?我们接着研究

1、首先,HAL_GPIO_Init()接收两个入口参数,第一个变量是寄存器版本定义的GPIO_TypeDef变量类型,由此我们知道,GPIO原始的寄存器地址声明在这个变量里面,实际上还是要操作这些配置寄存器。第二个变量是GPIO_InitTypeDef类型,是我们新定义的类型

	HAL_GPIO_Init(GPIOB,&GPIO_Initure);

2、进入函数内部的第一步,就是进行入口参数的有效性判断。几乎是带参数的函数前面都有调用assert_param()

  
  assert_param(IS_GPIO_ALL_INSTANCE(GPIOx));
  assert_param(IS_GPIO_PIN(GPIO_Init->Pin));
  assert_param(IS_GPIO_MODE(GPIO_Init->Mode));
  assert_param(IS_GPIO_PULL(GPIO_Init->Pull));

assert_param()可以对表达式进行判断,如果表达式为真,什么都不执行,如果表达式为假,进入错误日志函数。这个函数常用于调试阶段检查是否有参数输入错误。

  #define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))

  #define IS_GPIO_SPEED(SPEED) (((SPEED) == GPIO_SPEED_FREQ_LOW) || ((SPEED) == GPIO_SPEED_FREQ_MEDIUM) || \ ((SPEED) == GPIO_SPEED_FREQ_HIGH) || ((SPEED) == GPIO_SPEED_FREQ_VERY_HIGH))

3、有效性判断后,开始进行遍历寻找需要设置的IO口,当IO口被索引到之后,开始正式配置寄存器。

for(position = 0; position < GPIO_NUMBER; position++)
  {
    
    ioposition = ((uint32_t)0x01) << position;
    
    iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;

    if(iocurrent == ioposition)
    {
    //开始配置寄存器
    }

4、配置寄存器的操作同上,按照先清零(=取反再位与)再设置(位或)的先后顺序来配置

        assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
        
        temp = GPIOx->OSPEEDR; 
        temp &= ~(GPIO_OSPEEDER_OSPEEDR0 << (position * 2));
        temp |= (GPIO_Init->Speed << (position * 2));
        GPIOx->OSPEEDR = temp;

GPIO赋值操作

    HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);	//PB0置1

同样,第一个入口参数是寄存器版本定义的GPIO_TypeDef变量类型,由此我们知道,实际上还是操作这些配置寄存器。函数内部就是设置BSRR寄存器,很容易看明白。

void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
  
  assert_param(IS_GPIO_PIN(GPIO_Pin));
  assert_param(IS_GPIO_PIN_ACTION(PinState));

  if(PinState != GPIO_PIN_RESET)
  {
    GPIOx->BSRR = GPIO_Pin;
  }
  else
  {
    GPIOx->BSRR = (uint32_t)GPIO_Pin << 16;
  }
}

GPIO外部中断

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服