STM32F103VET6点亮LED灯(详解版)

目录

前言

一、原理简介

二、代码实现

三、代码分析

总结


前言

        上篇文章详细讲述了在Keil5如何从无到有创建适合自己STM32芯片型号的模板工程,那么本文就将讲述万物起源——点灯程序,无论学习是学习51单片机还是STM32,点灯往往是我们最先完成的一个程序,其重要性不言而喻,本文将讲解在点灯过程中可能遇到的问题,结合数据手册,讲解库函数封装的操作。


一、原理简介

        首先点灯之前应该从开发板原理图和芯片参考手册入手,开发板原理图可以让我们直观了解板子拥有哪些外设,而参考手册就是芯片的说明书,我们可以从上面得知这些外设资源的配置方法,两者缺一不可。

        首先通过原理图得出你的开发板上搭载的LED所接的GPIO端口,我使用的是PB0,如果你所使用的板子没有LED,需要自行外接的话,可以参考下面的硬件连接图。

        接下来来到芯片参考手册,可以看到GPIOB端口是挂载在APB2总线上的,所以待会儿配置的时候需要开启APB2的时钟。

STM32F103VET6点亮LED灯(详解版)_第1张图片

        然后我们再具体来看看 GPIO端口的基本结构,可以看到,我们想要让GPIO端口输出数据,我们需要将下图中的三个区域配置好,总结下来就是需要配置端口的输入输出模式、速度、上下拉模式以及具体的引脚。

STM32F103VET6点亮LED灯(详解版)_第2张图片

        接下来就需要去了解GPIO的寄存器描述,通过寄存器描述我们可以学会如何配置寄存器,因为我使用的是PB0,并将其配置成推挽输出模式,所以我们能够用到下图中的几个寄存器。

STM32F103VET6点亮LED灯(详解版)_第3张图片

        但是使用标准库函数进行编程,我们只需要调用库函数就能实现GPIO口的初始化,不需要单独去配置。

二、代码实现

        首先在上次新建的标准库版本的样例工程的本地User文件夹内创建一个led文件夹,在led文件夹里分别创建led.c和led.h文件。

STM32F103VET6点亮LED灯(详解版)_第4张图片

然后打开工程,进入到keil5内将新创建的文件添加到USER组里。

STM32F103VET6点亮LED灯(详解版)_第5张图片

接下来记得点击魔法棒选项,将led头文件的路径包含进去。

STM32F103VET6点亮LED灯(详解版)_第6张图片

代码如下:

led.c

#include "led.h"   

// LED初始化函数
void LED_Init()
{
		// 使能GPIOB时钟
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
        
        GPIO_InitTypeDef GPIO_InitStructure;
		// 设置GPIOB的第0位
		GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
		// 设置为推挽输出模式
		GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
		// 输出速度为50MHz
		GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; 
		// 初始化GPIOB
		GPIO_Init(GPIOB, &GPIO_InitStructure);
		
		// 设置GPIOB的第0位为高电平,LED为熄灭状态
		GPIO_SetBits(GPIOB, GPIO_Pin_0);
}

// 延迟函数
void Delay(uint32_t count)
{
    volatile uint32_t i;
    for(i=0; i

led.h

#ifndef __LED_H
#define	__LED_H

#include "stm32f10x.h"

void LED_Init(void);
void Delay(uint32_t count);
#define   LED_ON   	GPIO_ResetBits(GPIOB, GPIO_Pin_0);
#define   LED_OFF   GPIO_SetBits(GPIOB, GPIO_Pin_0);

#endif /* __LED_H */

main.c

#include "stm32f10x.h"  
#include "led.h"      

int main()
{
	LED_Init();  // 初始化LED
	while(1)     
	{
		LED_ON;       // 打开LED
		Delay(1000);  // 延时
		LED_OFF;      // 关闭LED
        Delay(1000);
	}	
}

        代码中的延时函数使用了一个简单的for循环来实现延时,这个函数的延时时间并不精确,因为它受到CPU时钟速度的影响。

三、代码分析

        首先分析一下LED_Init()函数,这是使能APB2总线上GPIOBD时钟的,我所使用的是PB0口,GPIOB是挂载在APB2总线上的。

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

那接下来看看我们调用了这个函数到底进行了哪些操作,右转跳转到定义处。

STM32F103VET6点亮LED灯(详解版)_第7张图片

        可以看到简介说这个函数用于APB2高速总线外设的使能,除了我们所使用的GPIOB,还可以使能下图中框中的外设,函数RCC_APB2PeriphClockCmd接受两个参数:RCC_APB2Periph和NewState。RCC_APB2Periph是一个32位的无符号整数,代表了APB2总线上的一个或多个外设。这些外设的定义通常在头文件中。

        例如RCC_APB2Periph_GPIOA,RCC_APB2Periph_ADC1等。

        NewState是一个枚举类型的变量,它的值可以是ENABLE或DISABLE,用于控制相应外设的时钟是否使能。

STM32F103VET6点亮LED灯(详解版)_第8张图片

        调用此函数后,函数会首先检查输入参数的有效性,然后根据NewState的值来决定是使能还是禁用相应的外设时钟。如果NewState的值为ENABLE,则通过位或操作使能相应的外设时钟;如果NewState的值为DISABLE,则通过位与操作禁用相应的外设时钟,都是对APB2ENR寄存器进行操作。

        时钟使能之后,就定义了一个结构体,这个结构体里包含了所使用的GPIO端口、输出模式以及输出速度,在这里,将PB0配置成推挽输出,输出速度50MHz,那这些参数为什么这么写呢,这就得来到GPIO的头文件里查看一下了。

STM32F103VET6点亮LED灯(详解版)_第9张图片

        找到stm32f10x_gpio.h文件,里面定义了结构体,里面就包含GPIO端口,速度和模式,并且将所有模式和速度都以枚举的方式列举了出来。

GPIO_Mode_AIN 模拟输入模式
GPIO_Mode_IN_FLOATING 浮动输入模式,输入引脚既不拉高也不拉低。
GPIO_Mode_IPD 输入下拉模式,输入引脚被拉低。
GPIO_Mode_IPU 输入上拉模式,输入引脚被拉高。
GPIO_Mode_Out_OD 开漏输出模式,输出引脚可以被外部设备拉高。
GPIO_Mode_Out_PP 推挽输出模式,输出引脚可以被驱动为高或低。
GPIO_Mode_AF_OD 复用开漏输出模式,引脚可以被配置为特定的外设功能,并且可以被外部设备拉高。
GPIO_Mode_AF_PP 复用推挽输出模式,引脚可以被配置为特定的外设功能,并且可以被驱动为高或低。

这些都配置好了,就该对GPIO进行初始化了,所以调用了GPIO初始化函数,并将刚定义的结构体以取地址的方式传入进去。

GPIO_Init(GPIOB, &GPIO_InitStructure);

右键跳转到GPIO_Init()函数的定义处,可以看到接收的两个参数都是指针变量,那为什么只有后面的结构体需要取地址操作呢?那是因为当初始化设定GPIO口时,在stm32f10x.h文件中,已经定义了输入GPIOx的定义,所以GPIOx在初始化函数里不需要取地址符号。

STM32F103VET6点亮LED灯(详解版)_第10张图片

STM32F103VET6点亮LED灯(详解版)_第11张图片

        那为什么在调用GPIO_Init函数时要对GPIO_InitStructure使用"&"取地址符号呢?这是因为GPIO_InitStructure是一个结构体指针变量。在C语言中,如果要将一个结构体作为参数传递给函数,通常需要传递该结构体变量的地址,而不是直接传递结构体本身的数值。这样可以确保函数可以修改结构体的内容,并且可以节省内存,因为传递地址比传递整个结构体所占用的内存要更加高效。

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;
  /* Check the parameters */
  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 Mode Configuration -----------------------*/
  currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);
  if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
  { 
    /* Check the parameters */
    assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
    /* Output mode */
    currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
  }
/*---------------------------- GPIO CRL Configuration ------------------------*/
  /* Configure the eight low port pins */
  if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
  {
    tmpreg = GPIOx->CRL;
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      pos = ((uint32_t)0x01) << pinpos;
      /* Get the port pins position */
      currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
      if (currentpin == pos)
      {
        pos = pinpos << 2;
        /* Clear the corresponding low 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);
        }
        else
        {
          /* Set the corresponding ODR bit */
          if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
          {
            GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
          }
        }
      }
    }
    GPIOx->CRL = tmpreg;
  }
/*---------------------------- GPIO CRH Configuration ------------------------*/
  /* 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;
  }
}

那我们来具体看看调用此函数到底都进行了什么操作,这段GPIO初始化函数的主要操作逻辑是:

  1. 首先进行参数的合法性检查,确保传入的GPIO端口和初始化结构体的成员符合要求。

  2. 获取GPIO模式的具体数值和输出速度。

  3. 配置低8位引脚的控制模式。

  4. 循环处理每一个引脚,根据引脚的位置和配置信息,设置对应的控制寄存器和输出寄存器。

  5. 最后将更新后的寄存器值写入相应的GPIO寄存器,完成GPIO端口的初始化配置。

其中

  • ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F):获取GPIO模式的低四位,即模式的具体数值。

  • ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10):判断是否为输出模式,并检查输出速度是否合法。

  • GPIOx->CRL:控制低8位引脚的寄存器,用于配置引脚的控制模式。

  • GPIOx->BRRGPIOx->BSRR:这两个寄存器分别用于复位和设置对应的引脚。BRR用于复位对应的引脚,BSRR用于设置对应的引脚。

        以上配置之后,就将PB0置位为1,这是因为LED是低电平点亮,所以将PB0置位也就是让LED处于默认熄灭状态,至此,GPIO的初始化完成。

        然后在主函数中写一个无限循环,在循环中不断对GPIO口进行操作,利用延时函数就可以实现LED闪烁的效果了。

	while(1)     
	{
		LED_ON;       // 打开LED
		Delay(1000);  // 延时
		LED_OFF;      // 关闭LED
        Delay(1000);
	}	

总结

        本文结合参考手册讲述了GPIO的配置,成功点亮了LED,实现了LED闪烁效果,并详细讲述了GPIO初始化过程中都对哪些寄存器进行了操作,虽然标准库函数很便捷,调用之后就能实现很多功能,但是我们能够在会用的基础上深挖底层的原理,能够对我们在之后学习过程中有所帮助。

你可能感兴趣的:(STM32学习,stm32,嵌入式硬件,单片机)