目录
前言
一、原理简介
二、代码实现
三、代码分析
总结
上篇文章详细讲述了在Keil5如何从无到有创建适合自己STM32芯片型号的模板工程,那么本文就将讲述万物起源——点灯程序,无论学习是学习51单片机还是STM32,点灯往往是我们最先完成的一个程序,其重要性不言而喻,本文将讲解在点灯过程中可能遇到的问题,结合数据手册,讲解库函数封装的操作。
首先点灯之前应该从开发板原理图和芯片参考手册入手,开发板原理图可以让我们直观了解板子拥有哪些外设,而参考手册就是芯片的说明书,我们可以从上面得知这些外设资源的配置方法,两者缺一不可。
首先通过原理图得出你的开发板上搭载的LED所接的GPIO端口,我使用的是PB0,如果你所使用的板子没有LED,需要自行外接的话,可以参考下面的硬件连接图。
接下来来到芯片参考手册,可以看到GPIOB端口是挂载在APB2总线上的,所以待会儿配置的时候需要开启APB2的时钟。
然后我们再具体来看看 GPIO端口的基本结构,可以看到,我们想要让GPIO端口输出数据,我们需要将下图中的三个区域配置好,总结下来就是需要配置端口的输入输出模式、速度、上下拉模式以及具体的引脚。
接下来就需要去了解GPIO的寄存器描述,通过寄存器描述我们可以学会如何配置寄存器,因为我使用的是PB0,并将其配置成推挽输出模式,所以我们能够用到下图中的几个寄存器。
但是使用标准库函数进行编程,我们只需要调用库函数就能实现GPIO口的初始化,不需要单独去配置。
首先在上次新建的标准库版本的样例工程的本地User文件夹内创建一个led文件夹,在led文件夹里分别创建led.c和led.h文件。
然后打开工程,进入到keil5内将新创建的文件添加到USER组里。
接下来记得点击魔法棒选项,将led头文件的路径包含进去。
代码如下:
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);
那接下来看看我们调用了这个函数到底进行了哪些操作,右转跳转到定义处。
可以看到简介说这个函数用于APB2高速总线外设的使能,除了我们所使用的GPIOB,还可以使能下图中框中的外设,函数RCC_APB2PeriphClockCmd接受两个参数:RCC_APB2Periph和NewState。RCC_APB2Periph是一个32位的无符号整数,代表了APB2总线上的一个或多个外设。这些外设的定义通常在头文件中。
例如RCC_APB2Periph_GPIOA,RCC_APB2Periph_ADC1等。
NewState是一个枚举类型的变量,它的值可以是ENABLE或DISABLE,用于控制相应外设的时钟是否使能。
调用此函数后,函数会首先检查输入参数的有效性,然后根据NewState的值来决定是使能还是禁用相应的外设时钟。如果NewState的值为ENABLE,则通过位或操作使能相应的外设时钟;如果NewState的值为DISABLE,则通过位与操作禁用相应的外设时钟,都是对APB2ENR寄存器进行操作。
时钟使能之后,就定义了一个结构体,这个结构体里包含了所使用的GPIO端口、输出模式以及输出速度,在这里,将PB0配置成推挽输出,输出速度50MHz,那这些参数为什么这么写呢,这就得来到GPIO的头文件里查看一下了。
找到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在初始化函数里不需要取地址符号。
那为什么在调用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初始化函数的主要操作逻辑是:
首先进行参数的合法性检查,确保传入的GPIO端口和初始化结构体的成员符合要求。
获取GPIO模式的具体数值和输出速度。
配置低8位引脚的控制模式。
循环处理每一个引脚,根据引脚的位置和配置信息,设置对应的控制寄存器和输出寄存器。
最后将更新后的寄存器值写入相应的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->BRR
、GPIOx->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初始化过程中都对哪些寄存器进行了操作,虽然标准库函数很便捷,调用之后就能实现很多功能,但是我们能够在会用的基础上深挖底层的原理,能够对我们在之后学习过程中有所帮助。