目录
写在前面的话:前面我们对新建工程文件以及STM32的时钟配置做了讲解,相信大家都有了一定的了解,对代码的大致框架以及STM32内部的时钟有了深入的认识,本次入门的第一讲最基础的就是对端口GPIO的应用。
一、原理简介
1.1 原理图
首先我们先看一下自己的STM32开发板的原理图,以我使用的为例如图所示:
STM32F103VET6的单片机有足足100个引脚,端口分别是GPIOA、GPIOB、GPIOC、GPIOD、GPIOE,每个端口下有分有GPIO_Pin0到GPIO_Pin15一共16个引脚,引脚有什么用,熟悉51单片机的同学可能会熟悉一些,可作为输入输出引脚,还可以作为串口等通信引脚等等,在STM32上引脚的功能大大提升,功能比51更加强大。
1.2 GPIO功能描述简介
可以稍微看一下GPIO的功能描述,如图所示(图源自STM32参考手册):
可以看到控制引脚的寄存器很多,引脚的模式也很多,对于初学者不是很友好,这些寄存器我们在下一节代码里会再拿出来分析,下一节我们会学习代码究竟怎么实现对这些寄存器的配置的,这里先知道一下GPIO的寄存器,每个IO端口都有7个寄存器来控制,分别是:配置模式的2个32位的端口配置寄存器GPIOx_CRL和GPIOx_CRH,2个32位的数据寄存器GPIOx_IDR和GPIOx_ODR,1个32 位的置位/复位寄存器GPIOx_BSRR,1个 16位的复位寄存器GPIOx_BRR;1个32 位的锁存寄存器GPIOx_LCKR。本节我们主要关注和用到的寄存器是CRL、CRH、BRR、BSRR寄存器。
1.3 硬件连接
本文我们要完成的是点亮LED,从最简单的角度去思考,我们需要做的就是让一个端口输出高电平或者低电平实现点亮或者关闭LED灯的效果,我们先来看一下硬件连接图:
我购买的开发板上只有一个LED灯可够编程,因此我只需要实现对PB0即GPIOB.0引脚的配置和操作,PB0设置低电平LED点亮,高电平LED熄灭。
二、代码实现与原理分析(详细)
2.1 代码实现
按照我们之前新建工程的博文的格式规范,我们先在工程文件user文件夹下新建名为led的文件夹,并创建led.c和led.h文件,如下图所示:
代码如下:
led.c:
#include "led.h"
//LED 初始化程序
void led_init(void)
{
GPIO_InitTypeDef GPIO_LED; //定义GPIO结构体变量
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能GPIOB端口的时钟
GPIO_LED.GPIO_Pin = GPIO_Pin_0; //LED端口配置
GPIO_LED.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_LED.GPIO_Speed = GPIO_Speed_2MHz; //IO口速度为2MHz
GPIO_Init(GPIOB, &GPIO_LED); //根据设定参数初始化GPIOB0
GPIO_SetBits(GPIOB,GPIO_Pin_0); //GPIOB0输出高电平,初始化LED灭
}
led.h:
#ifndef __LED_H
#define __LED_H
#include "main.h"
void led_init(void);
#endif
main.c:
/*
STM32F103VET6
SYSCLK = 72M
HCLK = 72M
APB1 = 36M
APB2 = 72M
*/
#include "main.h"
int i,j;
int main(void)
{
led_init(); //LED初始化
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_0); //点亮LED
for(i=0;i<=1000;i++) for(j=0;j<=2000;j++); //软件延时一段时间
GPIO_SetBits(GPIOB,GPIO_Pin_0); //熄灭LED
for(i=0;i<=1000;i++) for(j=0;j<=2000;j++); //软件延时一段时间
}
}
main.h:
#ifndef __MAIN_H
#define __MAIN_H
//标准头文件
#include "stm32f10x.h"
//用户自定义头文件
#include "led.h"
#endif
这是我们本节文章实现需要用到的代码,编译测试无错误无警告可以正常使用
2.2 配置步骤
第一步:首先先来看在我们的led.c里面初始化LED的函数led_init();我们要使用引脚这时候就需要对引脚进行初始化操作,我们从ST标准库中找到GPIO有关的头文件:stm32f10x_gpio.h,按照之前讲过的方法先打开头文件拉到文件最底下找函数,眼睛一瞥找到了这个叫GPIO_Init的函数,如图:
中文翻译下我们就知道这是对GPIO口的初始化函数,要完成对这个函数的调用我们可以看到函数的入口参数有GPIOx(端口号)和一个GPIO的结构体,因此在led_init()里我们先定义一个结构体变量:
GPIO_InitTypeDef GPIO_LED; //定义GPIO结构体变量
选中结构体跳转过去看定义,可以看到结构体里的成员变量有:
通过后面官方的注释也可以更加清晰的了解,结构体有三个成员变量,分别是:引脚号、引脚输出速度、引脚模式。
接着我们就对我们所需要的功能进行结构体初始化,那我们怎么知道应该填入的参数是什么样子的呢,可以通过看GPIO_Init函数的本体,如图:
可以看到在GPIO_Init函数内部通过框框里的三个函数来验证输入参数的合法性,通过一次跳转①②③可以查看我们需要输入的参数的规范,如下为配置的LED引脚:
GPIO_LED.GPIO_Pin = GPIO_Pin_0; //LED端口配置
GPIO_LED.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_LED.GPIO_Speed = GPIO_Speed_2MHz; //IO口速度为2MHz
引脚号为GPIO_Pin_0(我们LED的引脚是GPIOB.0),引脚模式为GPIO_Mode_Out_PP(推挽输出),引脚速度为GPIO_Speed_2MHz(2MHz)
引脚模式 | 说明 |
---|---|
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 | 复用推挽输出 |
本次我们选用的是推挽输出,推挽输出和开漏输出的最大区别是,推挽输出可以输出高低电平,但是开漏输出只能输出低电平若要输出高电平需要外接上拉电阻,我们点亮LED灯需要高低电平的切换因此选择推挽输出,有关这两种输出方式后续的文章会写到。 | |
对于输出速度的选择,我们使用的最低的2MHz,这里因为控制LED对信号带宽没有要求,因此选择速度低功耗也小。 | |
最后只需调用初始化函数就可以将引脚口初始化了: |
GPIO_Init(GPIOB, &GPIO_LED); //根据设定参数初始化GPIOB0
第二步:在使用GPIO端口前以及STM32任何外设前,我们都需要开启对应外设的时钟,通过上一讲时钟的讲解我们知道GPIOB挂载在APB2下,因此我们在stm32f10x_rcc.h中找到对APB2时钟设置的函数RCC_APB2PeriphClockCmd,如图进行开启GPIOB口的时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能GPIOB端口的时钟
第三步完成以上两部后我们的LED引脚初始化就算是完成了,最后我们通过给LED引脚给高电平使其初始化后保持熄灭的状态。
2.3 原理分析
2.3.1 GPIOx_CRL、GPIOx_CRH
以上的函数配置我们都没有直接对我们说的GPIO的寄存器进行操作,这是因为ST标准库已经对寄存器进行了封装,我们调用的函数其实最底层完成的工作就是的对寄存器进行读写操作,接下来我们看一下我们在初始化引脚的时候对究竟对什么寄存器进行了什么操作。
我们来看GPIO_Init(GPIOB, &GPIO_LED);究竟做了什么事情
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;
}
}
首先进入函数就对几个局部参数进行初始化并且对输入参数的合法性进行了检查,然后是
currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);
以我们之前设置好的GPIOB.0为例,这里运行完成后 currentmode=0x00000000;
下一步进入if语句执行:
currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
currentmode =0x00000002
接着我们引脚号是在0~7中所以会进入如下if中去配置CRL
if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
进入后先读取一次GPIOB口CRL寄存器的内容(tmpreg = GPIOx->CRL;),我们来看一下这个CRL寄存器的定义:
32位寄存器,分别初始化低8个口:0~7号的引脚口,前面两位是配置位后面两位是模式位。
tmpreg读取到的GPIOB口0~7的值为 0x44444444,这和参考手册里提到的端口默认值是一致的,默认是浮空输入状态。
此后程序进入for循环里判断我们配置的是什么引脚:
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = ((uint32_t)0x01) << pinpos;
/* Get the port pins position */
currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
......
......
}
通过我们设好的参数进行一系列位操作,大家可以一步一步在纸上写出来进行的操作,最后我们得到的tmpreg=0x44444442,对照寄存器表格可以看出来配置的是PIN0引脚设置为速度2Mhz推挽输出。
最后写入寄存器值完成配置:GPIOx->CRL = tmpreg; GPIO_Init()完成配置
同理要是我们配置的是8~15引脚口就会对CRH寄存器操作过程是一样的。
2.3.2 GPIOx_BRR、GPIOx_BSRR
我们配置引脚的过程操作到了CRL和CRH寄存器,我们写高低电平的时候就会操作到BRR和BSRR寄存器,如下代码:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BSRR = GPIO_Pin;
}
这是ST标注库中对引脚口操作的函数GPIO_SetBits(GPIOB,GPIO_Pin_0);,看名字就知道是给选定的引脚口置位,在置位是操作的就是BSRR寄存器:
当我们置位时,输入的参数就是0x0001,可以通过表格看到是对PIN0置位为1。
再来看一下复位的寄存器和函数,函数是GPIO_ResetBits(GPIOB,GPIO_Pin_0);函数本体如下:
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BRR = GPIO_Pin;
}
复位时操作的是BRR寄存器:
输入的参数是0x0001,可以通过表格看到是对PIN0复位为0。
这就是我们在使用LED灯,对引脚初始化配置的了CRL/CRH,翻转LED就是对BRR、BSRR操作。接下来我们开始测试我们的代码是否符合预期。
三、仿真测试
3.1 软件逻辑分析仪DEBUG
本次仿真我们会用到KEIL5里的逻辑分析仪的功能,如下是操作步骤:
第一步:点击①魔术包,选择Targrt选项卡将我们单片机系统板外部搭建的外部晶振设置正确,我的板子使用的是外部8M的晶振,这里填入8.0,这个的作用是给仿真时提供正确的时间信息。
第二步:选择Debug选项卡选择Use Simulator,勾选Run to main(),然后在下面的Dialog DLL:填入:DARMSTM.DLL,后面的Parameter填入:-pSTM32F103VE 前者是固定的后者根据自己的单片机型号,例如是F103C8T6的就填入-pSTM32F103C8,依次类推。
第三步:接着就可以进入DEBUG界面,点击①进入后再点击②处调出来Logic Analyzer调用出逻辑分析仪界面如图:
第四步:点击①创建信号,再点击②处新建,填入portb.0代表的是GPIOB0,如果使用其他的引脚例如GPIOA11,则填入为porta.11,填入后点击空白处完成写入,接着继续配置portb.0:
第五步:选中刚刚创建的信号,点击②处修改为Bit,最后完成创建点击③,如图:
第六步:信号配置完成,接下来点击①处的运行按钮,再点击②处的停止按钮,在逻辑分析仪上就会显示出来我们GPIOB.0引脚的电平变化,完成本次软件仿真模拟测试。
3.2 硬件效果
我们将代码下载到单片机上运行,如下是运行起来的效果,LED小灯安装我们预期的一样点亮熄灭循环交替:
四、小结
本节我们实现了单片机控制LED灯,学会了对引脚最基础的配置和使用,可以看到这一节我们并没有控制时间,只是运用了软件for循环做的延时函数,相信大家一定不是很喜欢,下一节会讲STM32滴答时钟配置出我们的延时函数!做出精确的延时控制us、ms、s级的延时。