写在前面:在前面的学习中,我们学习了STM32的编译环境(MDK)、时钟树以及GPIO的8种工作模式;这节我们学习正式入门STM32---点亮第一个LED灯;即利用GPIO进行电灯,尽管是一个十分简单的实现,但是其步骤也是一个完整的STM32项目,可以说是“麻雀虽小,五脏俱全”;因此,作为入门十分合适;
实验介绍:利用STM32开发板,通过HAL库函数对相关寄存器的控制,实现对开发板上LED灯的控制;
实验硬件:正点原子---STM32F1精英版开发板;
目录
一、GPIO寄存器介绍
1.1 端口配置寄存器(GPIOx_CRL 和 GPIOx_CRH)
1.2 端口输入数据寄存器(GPIOx_IDR)
1.3 端口输出数据寄存器(GPIOx_ODR)
1.4 端口置位/复位寄存器(GPIOx_BSRR)
1.5 端口位清除寄存器(GPIOx_BRR)
1.6 端口配置锁定寄存器(GPIOx_LCKR)
二、GPIO配置步骤
2.1 使能时钟
2.2 设置工作模式
2.3 设置输出状态
2.4 读取输入状态
三、硬件设计
3.1 实现功能
3.2 原理图
四、程序设计
4.1 程序流程图
4.2 程序分析
4.2.1 led驱动函数
4.2.2 main函数
五、实验现象
在前面的学习中,我们对GPIO进行了基本的介绍,基本结构进行了分析以及GPIO的8中工作模式的介绍;详细的内容请大家参考我前面的博客;
STM32--GPIO(8种工作模式)-CSDN博客
STM32F1 每组(这里是 A~D)通用 GPIO 口有 7 个 32 位寄存器控制; 分别为:CRL、CRH、TDR、ODR、BSRR、BRR以及LCKR寄存器;下面对7个寄存器做出详述(注:每组都有7个寄存器,例如:GPIOA、GPIOB都有7个相关的寄存器);
这个寄存器是用于配置GPIO端口的寄存器,其中CRL 控制端口的低八位,CRH 控制端口的 高八位,每个组GPIO有16个端口(例如:GPIOA0-GPIOA15);那么这两个寄存器控制GPIO口的什么呢?用于控制 GPIO 口的工作模式和工作速度;
CRL寄存器:
CRH寄存器:
这两个32位的寄存器,一共为64位,64位共控制16个端口(如:PA0-PA15);那么相当于每4位控制一个IO口的工作模式与工作速度;CRL寄存器控制(0-7)端口,CRH寄存器控制(8-15)端口;
以3-0位(控制PX_0端口)为例:
1-0位控制其输入输出:
3-2位控制其对应的模式:
如0-3位为1010,则表示:端口1工作在复用功能的推挽输出模式,最大速度为2MHz;
又如,我们需要让Px_7端口工作在浮空输入模式,则就要求CRL寄存器的31-28位赋值为:0100;
故通过上面两个寄存器,作用是控制端口的输入输出模式以及工作速度;
但是,我们仔细观察,在上面的寄存器中,如果选择的是输入模式,在上拉/下拉输入模式下,通过上面两个寄存器无法确定其到底为上拉还是下拉模式;此时,就需要另外的寄存器;
该寄存器用于读取GPIOx的输出高电平或低电平;
寄存器低 16 位有效,分别对应每一组 GPIO 的 16 个引脚。当 CPU 写访问该寄存器,可以读取对应引脚的输入电平(IDRy=0、IDRy=1),该寄存器下方为r,表示为只读寄存器,只可以读取值,不能写入值;
该寄存器用于控制 GPIOx 的输出高电平或者低电平;
该寄存器低 16 位有效,分别对应每一组 GPIO 的 16 个引脚。当 CPU 写访问该寄存器,如 果对应的某位写 0(ODRy=0),则表示设置该 IO 口输出的是低电平,如果写 1(ODRy=1),则表示设置该 IO 口输出的是高电平。
除了 ODR 寄存器,还有一个寄存器也是用于控制 GPIO 输出的,它就是 BSRR 寄存器。
该寄存器也用于控制 GPIOx 的输出高电平或者低电平.
该寄存器的31-16(BRy)位控制15-0引脚的ODRy的值,如果为0,对ODRy的值不产生影响,如果为1,则将ODRy的值改为0;
该寄存器的15-0(BSy)位也控制15-0引脚的ODRy的值,如果为0,对ODRy的值不产生影响,如果为1,则将ODRy的值改为1;
当BRy与BSy同时作用时,以BSy为主;
此处,我们需要思考一下,为什么输出一个引脚的值,需要两个寄存器对其进行双重控制呢?
官方给出的解释是:在使用ODR在读和修改之间产生中断时,会对ORD的值产生风险,而BSRR无风险;
我们对其进行简单的分析,ODR是一个可读可写的寄存器,那么对其进行赋值时(确定某个IO口输出电平),首先需要读出ODR寄存器的值,然后再重新为ODR进行赋值。而BSRR是一个只写寄存器,赋值时直接设置即可。BSRR 寄存器改变引脚状态的时候,不会被中断打断,而 ODR 寄存器有被中断打断的风险。
该寄存器用于清除ODR寄存器的值,不经常使用;
该寄存器低 16 位有效,若为0,对应的ODRy位不产生影响;若为1,清除对应位ODRy为0;
当执行正确的写序列设置了位16(LCKK)时,该寄存器用来锁定端口位的配置。位[15:0]用于锁 定GPIO端口的配置。在规定的写入操作期间,不能改变LCKP[15:0]。当对相应的端口位执行了 LOCK序列后,在下次系统复位之前将不能再更改端口位的配置。 每个锁定位锁定控制寄存器(CRL, CRH)中相应的4个位。
以上7个寄存器中,我们需要重视的为CRL、CRH、ODR、IDR以及BSRR寄存器;
一般外设的配置驱动模型为:
1、初始化
分为:时钟设置(包括时钟源与开启时钟);
参数设置;
IO设置(可选);
中断设置(可选);
2、读函数:从外设读取数据(可选);
3、写函数:往外设写入数据(可选);
4、中断服务函数(可选);
在此,我学习的时候就有个疑问与大家分享,为什么STM32进行IO口操作时,必须先要进行时钟设置(使能时钟)?
在学习52单片机的时候,只需要直接将对应的外设寄存器配置好,就直接使用,为什么32要先配置时钟呢?
经过查阅资料发现,51单片机的做法虽然简单,但是这样对于功耗的浪费很大,也就是说尽管有些地方用不到,还需要配置资源。为了降低功耗与发热,芯片厂商需要精准的控制某个设置的开与关,达到节能省电的目的;此外,51只有一个晶振时钟,好控制,而32的晶振4个起步,对于不同的外设需要分配不同的晶振时钟,故STM32中I/O要先进行时钟配置,在进行IO口操作;
GPIO配置步骤:
1、使能时钟 __HAL_RCC_GPIOB_CLK_ENABLE();//宏定义
2、设置工作模式:HAL_GPIO_Init 函数;
3、设置输出状态:HAL_GPIO_WritePin 函数;
HAL_GPIO_TogglePin 函数;
4、读取输入状态:HAL_GPIO_ReadPin函数;
口诀:GPIO配置,一个宏,四个函数;
下面我们对于这四个步骤进行详细说明,包括利用HAL库函数进行相关的配置说明;
STM32 在使用任何外设之前,我们都要先使能其时钟(也就是打开时钟),此处利用一个宏定义:
__HAL_RCC_GPIOX_CLK_ENABLE();
此处,我们看看代码种对于其宏定义的说明是什么。打开任意一个例程源码进行搜索;(此处以GPIOA作为举例);
宏定义的作用就是替代,将被定义的量用定义的量进行替代;
代码中对于此宏的定义是这样说的:
其中关键在于划线这句:
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
其中,SET_BIT的意思我们不知,再次进行查找,右键Go to Definiation of "SET_BIT"其又是一个宏定义:其含义为括号中的两个参数做或运算;
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN)的参数1:RCC->APB2ENR;参数2:RCC_APB2ENR_IOPAEN;分别代表什么意思呢?
RCC_APB2ENR_IOPAEN:这又是一个宏定义,我们同样跳转过去看一下去其含义:
这三行代码可以说是层层宏定义,其表达的意思就是将0X00000002(二进制为:0x00000000000000000000000000000010)左移一位成为0X00000004(二进制为:0x00000000000000000000000000000100);
RCC->APB2ENR:是APB2外设时钟使能寄存器,让其与0X00000004进行或运算,则该寄存器的第2位进行置1;
查看参考手册,将位2置1的作用就是打开IO口时钟;
例如,我们需要使能IO端口B时钟使能,那么怎么设置呢?
__HAL_RCC_GPIOB_CLK_ENABLE();
综上过程,__HAL_RCC_GPIOX_CLK_ENABLE();宏定义的作用就是打开GPIO端口的时钟;
设置工作模式:HAL_GPIO_Init 函数;
那么同样我们对于HAL_GPIO_Init函数进行分析;
对其两个参数进行分析:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
这两个参数都为结构体指针:
结构体指针1:GPIO_TypeDef其定义格式为:
主要包括一些相关的寄存器,当然库函数已经给我们定义了这些结构体变量,例如下面的:
因此也将GPIOA成为GPIO寄存器基地址
结构体指针2:GPIO_InitTypeDef其定义格式为:
该结构体变量需要在使用时对其结构体成员进行定义使用,包括端口的引脚、工作模式、上
下拉方式以及输出速度;
PIN:引脚设置;
Mode:(工作模式)
Pull:上下拉
Speed:速度
例如,我们需要初始化IO端口B的引脚5,使其工作在推挽模式,输出速度为低速,那么怎么设置呢?
GPIO_InitTypeDef gpio_init_struct;//定义第二个结构体变量
设置变量对应的结构体成员;
gpio_init_struct.Pin=GPIO_PIN_5;
gpio_init_struct.Mode=GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Speed=GPIO_SPEED_FREQ_LOW ;HAL_GPIO_Init(GPIOB,&gpio_init_struct);、、引用初始化函数;
设置输出状态:HAL_GPIO_WritePin 函数;
下面我们对于Wirte_Pin进行分析:同样利用右键或者查找找到对应函数的定义;
该函数共有三个参数:GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState
参数1:GPIO_TypeDef *GPIOx结构体指针类型,与设置工作模式的参数相同,此处不进行详述;
参数2:uint16_t GPIO_Pin与上文所说的PIN引脚的相同,选择端口对应的引脚;
参数3:GPIO_PinState此处为一个枚举类型,GPIO的状态0/1;
例如,我们需要设置PB6=1,那么怎么设置呢?
参数1为:GPIOB;参数2为:GPIO_PIN_1;参数3为:GPIO_PIN_SET;
HAL_GPIO_TogglePin 函数;
下面对于TogglePin 函数进行详述:
该函数有两个参数:GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin
参数1:GPIO基地址,上文已经提及;
参数2:GPIO_Pin,端口引脚上文已经提及;
例如,我们需要设置PB10进行翻转,那么怎么设置呢?
参数1为:GPIOB;参数2为:GPIO_PIN_10;
HAL_GPIO_ReadPin函数;
下面我们对于ReadPin函数进行详述:
该函数共有三个参数:GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin这两个参数上文已经介绍过,分别为:GPIO基地址、GPIO_Pin,端口引脚;
该函数的返回值为枚举类型,返回0代表IO为低电平,返回1代表IO为高电平;
例如,我们需要知道PB10的输入,那么怎么设置呢?
参数1为:GPIOB;参数2为:GPIO_PIN_10;在设置一个变量,将返回值赋值给变量即可;
使STM32F1精英版上的LED1进行闪烁;
在该开发板上的LED灯共有3个,本次我们点亮LED0,即对PB5进行操作;我们看到LED0右端接的是VCC3.3V,那么只需要控制左端的PB5输出即可;(输出0点亮,输出1熄灭);
我们还需要确定GPIOB_5的工作模式;我们采用推挽输出,其特点为:输出引脚电平,高电平为VDD,低电平为VSS;
void led_init(void)//led初始化函数
{
__HAL_RCC_GPIOB_CLK_ENABLE();//使能GPIOB时钟
GPIO_InitTypeDef gpio_init_struct; //定义结构体变量
gpio_init_struct.Pin=GPIO_PIN_5;//定义结构体变量成员-引脚号为5;
gpio_init_struct.Mode=GPIO_MODE_OUTPUT_PP;//定义结构体变量成员-工作模式为推挽输出;
gpio_init_struct.Speed=GPIO_SPEED_FREQ_LOW ; 定义结构体变量成员-输出速度为低速;
HAL_GPIO_Init(GPIOB,&gpio_init_struct);//引用GPIO初始化函数;
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);//引用GPIO写入函数;
}
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
led_init(); /* LED初始化 */
while(1)
{
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_5,GPIO_PIN_SET); /* PB5置1 */
delay_ms(500);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_5,GPIO_PIN_RESET); /* PB5置0 */
delay_ms(500);
}
}
32点灯
总结:本文主要讲解了如何在STM32开发板上点亮第一个LED灯,主要的学习内容为:GPIO配置步骤以及GPIO相关的寄存器,硬件设计部分入代码分析;尽管本文的代码难度不难,但是对于程序中相关函数以及寄存器的理解,对于初学者还有一定的难度因此还是需要多加练习;
创作不易,还请大家多多点赞支持!!!