一般而言,在不调用库函数时,ARM的工作要基于对寄存器的操作即读和写库函数实际上是架设在寄存器与用户驱动层之间的代码,向下处理与寄存器直接相关的配置,向上为用户提供配置寄存器的接口。库开发方式与直接配置寄存器方式的区别如下图:
采用寄存器开发方式时,对程序运行占用的资源可以进行严格的控制,同时某些具体的参数可以更加直观,生成的代码量较少;其缺点是***开发速度慢,程序可读性较差,维护比较复杂***。库开发方式虽然会牺牲一部分的资源,但是换来的是极大的便利,在现如今资源相对充足的情况下,一般都采用库开发方式。
以下实验是基于STM32F429IGT6进行的关于LED灯点亮实验的库封装建立和调用,主要目的在于对库函数有一个基本的认识,提升自己的C语言能力,同时对STM32的底层工作机制进行初步了解。同时将自己在整个实验的操作过程中踩到的坑进行插入总结。
GPIO是通用输入输出端口的简称,简单来说就是STM32 可控制的引脚,STM32 芯片的GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。 STM32 芯片的GPIO被分成很多组,每组有16 个引脚,如型号STM32F4IGT6 型号的芯片有GPIOA、GPIOB、GPIOC 至GPIOI 共9 组GPIO,芯片一共176 个引脚,其中GPIO就占了一大部分,所有的GPIO引脚都有基本的输入输出功能。 最基本的输出功能是由STM32 控制引脚输出高、低电平,实现开关控制,如把GPIO引脚接入到LED 灯,那就可以控制LED 灯的亮灭,引脚接入到继电器或三极管,那就可以通过继电器或三极管控制外部大功率电路的通断。 最基本的输入功能是检测外部输入电平,如把GPIO 引脚连接到按键,通过电平高低区分按键是否被按下。
【注】对于下述小标题带号的表示和本次实验关系不大,可忽略不看。
引脚的两保护个二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于VDD_FT 时,上方的二极管导通,当引脚电压低于VSS 时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁。尽管有这样的保护,并不意味着STM32 的引脚能直接外接大功率驱动器件,如直接驱动电机,强制驱动要么电机不转,要么导致芯片烧坏,必须要加大功率及隔离电路驱动。具体电压、电流范围可查阅《STM32F4xx 规格书》。 上拉、下拉电阻,从它的结构我们可以看出,通过上、下拉对应的开关配置,我们可以控制引脚默认状态的电压,开启上拉的时候引脚电压为高电平,开启下拉的时候引脚电压为低电平,这样可以消除引脚不定状态的影响。如引脚外部没有外接器件,或者外部的器件不干扰该引脚电压时,STM32 的引脚都会有这个默认状态。 也可以设置“既不上拉也不下拉模式”,我们也把这种状态称为浮空模式,配置成这个模式时,直接用电压表测量其引脚电压为1 点几伏,这是个不确定值。所以一般来说我们都会选择给引脚设置“上拉模式”或“下拉模式”使它有默认状态。 STM32 的内部上拉是“弱上拉”,即通过此上拉输出的电流是很弱的,如要求大电流还是需要外部上拉。 通过“上拉/下拉寄存器GPIOx_PUPDR”控制引脚的上、下拉以及浮空模式。 需要避开的坑是,上拉电阻并不意味着默认输出是高电平,同理下拉电阻也不意味着默认输出是低电平。这是我一开始就进入的误区,后面在实践中发现,默认的输出一般都是0即低电平,所以无论设置的是上拉电阻还是下拉电阻,其LED灯是默认高亮的(引脚输出为低电平时LED灯才能亮,后续可以看电路原理图)。
GPIO引脚线路经过上、下拉电阻结构后,向上流向“输入模式”结构,向下流向“输出模式”结构。先看输出模式部分,线路经过一个由P-MOS 和N-MOS 管组成的单元电路。这个结构使GPIO具有了 推挽输出 和 开漏输出 两种模式。 所谓的推挽输出模式,是根据这两个MOS 管的 工作方式来命名的。在该结构中输入高电平时,上方的P-MOS 导通,下方的N-MOS 关闭,对外输出高电平;而在该结构中输入低电平时,N-MOS 管导通,P-MOS 关闭,对外输出低电平。当引脚高低电平切换时,两个管子轮流导通,一个负责灌电流,一个负责拉电流,使其负载能力和开关速度都比普通的方式有很大的提高。推挽输出的低电平为0 伏,高电平为3.3 伏,参考图左侧,它是推挽输出模式时的等效电路。 而在开漏输出模式时,上方的P-MOS 管完全不工作。如果我们控制输出为0,低电平,则P-MOS 管关闭,N-MOS 管导通,使输出接地,若控制输出为1 (它无法直接输出高电平)时,则P-MOS 管和N-MOS 管都关闭,所以引脚既不输出高电平,也不输出低电平,为高阻态。为正常使用时必须接上拉电阻(可用STM32 的内部上拉,但建议在STM32 外部再接一个上拉电阻),参考图 7-2 中的右侧等效电路。它具“线与”特性,也就是说,若有很多个开漏模式引脚连接到一起时,只有当所有引脚都输出高阻态,才由上拉电阻提供高电平,此高电平的电压为外部上拉电阻所接的电源的电压。若其中一个引脚为低电平,那线路就相当于短路接地,使得整条线路都为低电平,0 伏。 推挽输出模式一般应用在输出电平为0 和3.3 伏而且需要高速切换开关状态的场合。在STM32 的应用中,除了必须用开漏模式的场合,我们都习惯使用推挽输出模式。 开漏输出一般应用在I2C、SMBUS 通讯等需要“线与”功能的总线电路中。除此之外,还用在电平不匹配的场合,如需要输出5 伏的高电平,就可以在外部接一个上拉电阻,上拉电源为5 伏,并且把GPIO 设置为开漏模式,当输出高阻态时,由上拉电阻和电源向外输出5 伏的电平。 通过 “输出类型寄存器GPIOx_OTYPER”可以控制GPIO端口是推挽模式还是开漏模式。
前面提到的双MOS 管结构电路的输入信号,是由GPIO“输出数据寄存器GPIOx_ODR”提供的,因此我们通过修改输出数据寄存器的值就可以修改GPIO引脚的输出电平。而“置位/复位寄存器GPIOx_BSRR”可以通过修改输出数据寄存器的值从而影响电路的输出。
需要注意的几点如下:
对于同时复位和置位,置位的优先级更高,即如果 BSRR = 0x0100_0100;则会认为是置位,对应的输出引脚(示例为引脚8)为高电位。对于复位和置位寄存器,为0代表的是不对输出寄存器做任何操作,即保留原值不变,而不是将输出数据寄存器ODR置位低电平。实践证明,STM32中对置位寄存器设置为 0x0100 后,再对复位寄存器设置为 0x0100,编译后并不认为复位和置位寄存器同时置1,而是认为有先后顺序,即先置位后复位。(笔者一开始的理解是置位寄存器设置为0x0100后,由于后续没有对置位寄存器进行操作所以置位寄存器保存的值没变,此时对复位寄存器进行赋值后,系统应当认为置位和复位寄存器的第9位都是高电平,则输出应该为高电平,事实上并不是,事实上,其输出时先为高电平后为低电平,具体为什么是这种机制,也许和编译器有关系,也许和本身的硬件机制有关系,暂时不深究)“复用功能输出”中的“复用”是指STM32 的其它片上外设对GPIO引脚进行控制,此时GPIO引脚用作该外设功能的一部分,算是第二用途。从其它外设引出来的“复用功能输出信号”与GPIO本身的数据据寄存器都连接到双MOS 管结构的输入中,通过图中的梯形结构作为开关切换选择。 例如我们使用USART 串口通讯时,需要用到某个GPIO引脚作为通讯发送引脚,这个时候就可以把该GPIO引脚配置成USART 串口复用功能,由串口外设控制该引脚,发送数据。 简单地说,复用功能输出是指用其他输出来替代上述的输出数据寄存器作为GPIO引脚的输出,所以当选择复用功能输出时,对应的输出应该有相应的配置。
看GPIO 结构框图的上半部分,它是GPIO 引脚经过上、下拉电阻后引入的,它连接到施密特触发器,信号经过触发器后,模拟信号转化为0、1 的数字信号,然后存储在“输入数据寄存器GPIOx_IDR”中,通过读取该寄存器就可以了解GPIO 引脚的电平状态。
与“复用功能输出”模式类似,在“复用功能输出模式”时,GPIO引脚的信号传输到STM32 其它片上外设,由该外设读取引脚状态。 同样,如我们使用USART 串口通讯时,需要用到某个GPIO引脚作为通讯接收引脚,这个时候就可以把该GPIO引脚配置成USART 串口复用功能,使USART 可以通过该通讯引脚的接收远端数据。
当GPIO 引脚用于ADC 采集电压的输入通道时,用作“模拟输入”功能,此时信号是不经过施密特触发器的,因为经过施密特触发器后信号只有0、1 两种状态,所以ADC 外设要采集到原始的模拟信号,信号源输入必须在施密特触发器之前。类似地,当GPIO 引脚用于DAC 作为模拟电压输出通道时,此时作为“模拟输出”功能,DAC 的模拟信号输出就不经过双MOS 管结构了,在GPIO 结构框图的右下角处,模拟信号直接输出到引脚。同时,当GPIO用于模拟功能时(包括输入输出),引脚的上、下拉电阻是不起作用的,这个时候即使在寄存器配置了上拉或下拉模式,也不会影响到模拟信号的输入输出。
stm32f4xx.h
//片上外设基地址 #define PERIPH_BASE ((unsigned int)0x40000000) //总线基地址 #define APB1PERIPH_BASE PERIPH_BASE #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000) #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000) #define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000) //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) //RCC外设基地址 #define RCC_BASE (AHB1PERIPH_BASE + 0x3800) //***********************结构体定义******************************* //GPIO结构体定义 #define __IO volatile typedef unsigned int uint32_t; typedef unsigned short uint16_t; 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 uint16_t BSRRL; __IO uint16_t BSRRH; __IO uint32_t LCKR; __IO uint32_t AFRL; __IO uint32_t AFRH; }GPIO_TypeDef; //GPIOx定义 #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) //RCC结构体定义 typedef struct { __IO uint32_t CR; // < RCC 时钟控制寄存器, 地址偏移: 0x00 __IO uint32_t PLLCFGR; // < RCC PLL配置寄存器, 地址偏移: 0x04 __IO uint32_t CFGR; // < RCC 时钟配置寄存器, 地址偏移: 0x08 __IO uint32_t CIR; // < RCC 时钟中断寄存器, 地址偏移: 0x0C __IO uint32_t AHB1RSTR; // < RCC AHB1 外设复位寄存器, 地址偏移: 0x10 __IO uint32_t AHB2RSTR; // < RCC AHB2 外设复位寄存器, 地址偏移: 0x14 __IO uint32_t AHB3RSTR; // < RCC AHB3 外设复位寄存器, 地址偏移: 0x18 __IO uint32_t RESERVED0; // < 保留, 地址偏移:0x1C __IO uint32_t APB1RSTR; // < RCC APB1 外设复位寄存器, 地址偏移: 0x20 __IO uint32_t APB2RSTR; // < RCC APB2 外设复位寄存器, 地址偏移: 0x24 __IO uint32_t RESERVED1[2]; // < 保留, 地址偏移:0x28-0x2C __IO uint32_t AHB1ENR; // < RCC AHB1 外设时钟寄存器, 地址偏移: 0x30 __IO uint32_t AHB2ENR; // < RCC AHB2 外设时钟寄存器, 地址偏移: 0x34 __IO uint32_t AHB3ENR; // < RCC AHB3 外设时钟寄存器, 地址偏移: 0x38 /*RCC后面还有很多寄存器,此处省略*/ } RCC_TypeDef; #define RCC ((RCC_TypeDef *)RCC_BASE)stm32f4xx_gpio.h
#include "stm32f4xx.h" //GPIO引脚定义 #define GPIO_Pin_0 ((uint16_t)0x0001) #define GPIO_Pin_1 ((uint16_t)0x0002) #define GPIO_Pin_2 ((uint16_t)0x0004) #define GPIO_Pin_3 ((uint16_t)0x0008) #define GPIO_Pin_4 ((uint16_t)0x0010) #define GPIO_Pin_5 ((uint16_t)0x0020) #define GPIO_Pin_6 ((uint16_t)0x0040) #define GPIO_Pin_7 ((uint16_t)0x0080) #define GPIO_Pin_8 ((uint16_t)0x0100) #define GPIO_Pin_9 ((uint16_t)0x0200) #define GPIO_Pin_10 ((uint16_t)0x0400) #define GPIO_Pin_11 ((uint16_t)0x0800) #define GPIO_Pin_12 ((uint16_t)0x1000) #define GPIO_Pin_13 ((uint16_t)0x2000) #define GPIO_Pin_14 ((uint16_t)0x4000) #define GPIO_Pin_15 ((uint16_t)0x8000) #define GPIO_Pin_ALL ((uint16_t)0xFFFF) typedef enum { GPIO_Mode_IN = 0x00, //输入模式 GPIO_Mode_OUT = 0x01, //输出模式 GPIO_Mode_AF = 0x02, //复用模式 GPIO_Mode_AN = 0x03 //模拟模式 }GPIOMode_TpyeDef; typedef enum { GPIO_OType_PP = 0x00, //推挽模式 GPIO_OType_OD = 0x01 //开漏模式 }GPIOOType_Typedef; typedef enum { GPIO_Speed_2MHz = 0x00, //引脚速率2MHz GPIO_Speed_25MHz = 0x01, //引脚速率25MHz GPIO_Speed_50MHz = 0x02, //引脚速率50MHz GPIO_Speed_100MHz = 0x03 //引脚速率100MHz }GPIOSpeed_Typedef; typedef enum { GPIO_PuPd_NOPULL = 0x00, //浮空模式 GPIO_PuPd_UP = 0x01, //上拉模式 GPIO_PuPd_DOWN = 0x02 //下拉模式 }GPIOPuPd_TypeDef; //结构体定义,该结构体主要用于存储初始化参数 typedef struct { uint16_t GPIO_Pin; GPIOMode_TpyeDef GPIO_Mode; GPIOOType_Typedef GPIO_OType; GPIOSpeed_Typedef GPIO_Speed; GPIOPuPd_TypeDef GPIO_PuPd; }GPIO_InitTypeDef; void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);stm32f4xx_gpio.c
/* 该函数主要用于存放子函数 */ #include "stm32f4xx_gpio.h" //置位函数 void GPIO_SetBits(GPIO_TypeDef * GPIOx,uint16_t GPIO_Pin) { GPIOx->BSRRL = GPIO_Pin; } //复位函数 void GPIO_ResetBits(GPIO_TypeDef * GPIOx,uint16_t GPIO_Pin) { GPIOx->BSRRH = GPIO_Pin; } //初始化函数 void GPIO_Init(GPIO_TypeDef * GPIOx,GPIO_InitTypeDef * GPIO_InitStruct) { uint32_t pinpos = 0x00, pos = 0x00, currentpin = 0x00; //采用for循环语句,根据输入的信息,对管脚进行逐个初始化,可知,该函数适用于对多个引脚进行初始化配置 for(pinpos = 0x00; pinpos < 16; pinpos++) { pos = ((uint32_t)0x01) << pinpos; currentpin = (GPIO_InitStruct->GPIO_Pin) & pos; if(currentpin == pos) { //模式配置 GPIOx->MODER &= ~((uint32_t)0x03 << (2*pinpos)); GPIOx->MODER |= (((uint32_t)GPIO_InitStruct->GPIO_Mode) << (2*pinpos)); //上拉下拉模式配置 GPIOx->PUPDR &= ~((uint32_t)0x03 << (2*pinpos)); GPIOx->PUPDR |= (((uint32_t)GPIO_InitStruct->GPIO_PuPd) << (2*pinpos)); //对于输出模式和复用模式,可以配置输出速度和输出类型 if((GPIO_InitStruct->GPIO_Mode == GPIO_Mode_OUT) || (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_AF)) { //输出速度配置 GPIOx->OSPEEDR &= ~((uint32_t)0x03 << (2*pinpos)); GPIOx->OSPEEDR |= (((uint32_t)GPIO_InitStruct->GPIO_Speed) << (2*pinpos)); //输出类型配置 GPIOx->OTYPER &= ~((uint32_t)0x01 << (pinpos)); GPIOx->OTYPER |= (((uint32_t)GPIO_InitStruct->GPIO_OType) << (pinpos)); } } } }main.c
/* 此函数的功能是产生一个会变色的灯 */ #include "stm32f4xx_gpio.h" #define TIME ((uint32_t)0x2FFFFF) //函数声明 void Delay(uint32_t nCount); void SystemInit(void); //主函数 int main(void) { //开启GPIOH的外设时钟 //RCC_AHB1ENR |= (1<<7); //初始化 GPIO_InitTypeDef InitStruct; RCC->AHB1ENR |= (1<<7); //定义此变量的目的只是为了暂时存储需要设置的GPIO相关信息,没必要使用指针,采用变量的形式 //由此可以总结,一般情况下只有用于设置有固定位置或相对固定位置的变量时,才会采用指针 //例如被调用的函数由于一般需要改变已经定义的变量,所以一般调用函数的输入变量都是指针类型 //而此处的变量只是用于存储初值信息,后续不需要给子函数更改,只是给子函数调用,所以无需使用指针,直接定义变量 //初值赋值,如下为初始化引脚10-12,对应硬件上的红绿蓝灯 InitStruct.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12; InitStruct.GPIO_Mode = GPIO_Mode_OUT; InitStruct.GPIO_OType = GPIO_OType_PP; InitStruct.GPIO_Speed = GPIO_Speed_2MHz; InitStruct.GPIO_PuPd = GPIO_PuPd_UP; //调用初始化函数 GPIO_Init(GPIOH,&InitStruct); GPIO_SetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12); //LED灯循环闪烁变换颜色 while(1) { //全亮 GPIO_ResetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12); Delay(TIME); GPIO_SetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12); Delay(TIME); //红蓝灯亮 GPIO_ResetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11); Delay(TIME); GPIO_SetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11); Delay(TIME); //红绿灯亮 GPIO_ResetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_12); Delay(TIME); GPIO_SetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_12); Delay(TIME); //蓝绿灯亮 GPIO_ResetBits(GPIOH,GPIO_Pin_11|GPIO_Pin_12); Delay(TIME); GPIO_SetBits(GPIOH,GPIO_Pin_11|GPIO_Pin_12); Delay(TIME); //红灯亮 GPIO_ResetBits(GPIOH,GPIO_Pin_10); Delay(TIME); GPIO_SetBits(GPIOH,GPIO_Pin_10); Delay(TIME); //蓝灯亮 GPIO_ResetBits(GPIOH,GPIO_Pin_11); Delay(TIME); GPIO_SetBits(GPIOH,GPIO_Pin_11); Delay(TIME); //绿灯亮 GPIO_ResetBits(GPIOH,GPIO_Pin_12); Delay(TIME); GPIO_SetBits(GPIOH,GPIO_Pin_12); Delay(TIME); } } //定义时延函数 void Delay( uint32_t nCount) { for(; nCount != 0; nCount--); } //系统时钟函数,暂定为空采取默认时钟配置 void SystemInit(void) { }在进行编译配置时,应当注意,ARM Compiler中选择v5和v6的结果是不一样的,选择v5时其运行结果和教程一直,选择v6时其运行结果不会出现预期的运行结果。具体原因如果有大佬有见解的请帮忙解答一下哈~
最后对本文的部分内容进行说明,原理的知识大部分参考《【野火】零死角玩转STM32——F429挑战者》,对其中有误区或讲的不是很清晰的地方做出了自己的见解和修改,代码也借鉴了例程但是有做改动。