提示:今天是STM32入门学习的第二天,重点学习了GPIO的工作原理,通过库函数、寄存器、位操作来实现跑马灯。
目录
第一讲 STM32GPIO工作原理
1.GPIO功能描述
2.每组GPIO端口的寄存器
第二讲 库函数——跑马灯
1.初始化
2.手把手写跑马灯实验-库函数
第三讲 寄存器——跑马灯
第四讲 位操作——跑马灯
每个GPIO端口有两个32位配置寄存器(GPIOx_CRL,GPIOx_CRH),两个32位数据寄存器 (GPIOx_IDR和GPIOx_ODR),一个32位置位/复位寄存器(GPIOx_BSRR),一个16位复位寄存器(GPIOx_BRR)和一个32位锁定寄存器(GPIOx_LCKR)。
根据数据手册中列出的每个I/O端口的特定硬件特征, GPIO端口的每个位可以由软件分别配置 成多种模式。
4种输入模式
每个I/O端口位可以自由编程,然而I/0端口寄存器必须按32位字被访问(不允许半字或字节访 问)。GPIOx_BSRR和GPIOx_BRR寄存器允许对任何GPIO寄存器的读/更改的独立访问;这 样,在读和更改访问之间产生IRQ时不会发生危险。 下图给出了一个I/O端口位的基本结构
3种最大翻转速度
GPIO的输入工作模式1—输入浮空模式
GPIO的输入工作模式2—输入上拉模式
GPIO的输入工作模式3—输入下拉模式
GPIO的输入工作模式4—模拟模式
GPIO的输出工作模式1—开漏输出模式
GPIO的输出工作模式2—开漏复用输出模式
GPIO的输出工作模式3—推挽输出模式
GPIO的输出工作模式4—推挽复用输出模式
推挽输出:可以输出强高低电平,连接数字器件
开漏输出:只可以输出强低电平,高电平得靠外部电阻拉高。输出端相当于三极管的集电极. 要得到高电平状态需要上拉电阻才行. 适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内)
两个32位配置寄存器(GPIOx_CRL ,GPIOx_CRH) ,
两个32位数据寄存器 (GPIOx_IDR和GPIOx_ODR),
一个32位置位/ 复位寄存器(GPIOx_BSRR),
一个16位复位寄存器(GPIOx_BRR),
一个32位锁定寄存器(GPIOx_LCKR)。
每个I/O端口位可以自由编程,然而I/O端口寄存器必须按32位字被访问(不允许半字或字节访问)
例如:STM32F103ZET6:一共有7组IO口,每组IO口有16个IO,一共16X7=112个IO。GPIOA,GPIOB---GPIOG。每组IO口含下面7个寄存器,也就是7个寄存器,一共可以控制一组GPIO的16个IO口。
32位寄存器配置每个IO口要四位,所以只能配置八个IO口,而有十六个IO口,故需要两个端口配置寄存器。CRL控制0—7的IO口,CRH控制8—15个IO口。
2.1 端口配置低寄存器(GPIOx_CRL)
观察使用时先确定MODY是在输出还是输入,若为00则为输入,此时在看CNYF是输入的那个状态,其中10有上拉和下拉是在ODR这个寄存器里面。
2.2 端口配置高寄存器(GPIOx_CRH)
2.3 端口输入数据寄存器(GPIOx_IDR)
2.4 端口输出数据寄存器(GPIOx_ODR)
输出为1则是高电平,输出为0则是低电平。和输出寄存器一样,主要是这个寄存器控制着上拉还是下拉电阻。其中0为下拉1为上拉。
2.5 端口位设置/清除寄存器(GPIOx_BSRR)
2.6 端口位清除寄存器(GPIOx_BRR)
设置那个IO口就是那个IO口。不过经常以这个寄存器清除低位,以2.5那个寄存器清除高位。
GPIO输出方式:推挽输出,可以输出强高低电平。
库函数的介绍:操作IO口必须引入的源文件和头文件。
头文件:stm32f10x_gpio.h
源文件:stm32f10x_gpio.c
1个初始化函数:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
2个读取输入电平函数:
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
2个读取输出电平函数:
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
4个设置输出电平函数:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);输出高
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);输出低
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
1个初始化函数:此函数调用的时候要输入两个参数,对于第一个参数来说进入是一个结构体里面存放了IO口的七个寄存器,而范围是GPIOA~GPIOG 。第二个参数是来初始化的里面是如下方框里的一个结构体,后面跟着一个指针变量。
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
作用:初始化一个或者多个IO口(同一组)的工作方式和速度。
该函数主要是操作GPIO_CRL(CRH)寄存器,在上拉或者下拉的时候有设置BSRR或者BRR寄存器
GPIOx: GPIOA~GPIOG
typedef struct
{
uint16_t GPIO_Pin; //指定那一组中要初始化的IO口
GPIOSpeed_TypeDef GPIO_Speed; //设置IO口输出速度
GPIOMode_TypeDef GPIO_Mode; //设置工作模式:8种中的一个
}GPIO_InitTypeDef;
注意:外设(包括GPIO)在使用之前,几乎都要先使能对应的时钟。
GPIO_Init函数初始化样:
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->PB.5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIOB.5
2个读取输入电平函数:
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
作用:读取某个GPIO的输入电平。实际操作的是GPIOx_IDR寄存器。
例如:GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5);//读取GPIOA.5的输入电平
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
作用:读取某组GPIO的输入电平。实际操作的是GPIOx_IDR寄存器。
例如:GPIO_ReadInputData(GPIOA);//读取GPIOA组中所有io口输入电平
4个设置输出电平函数:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
作用:设置某个IO口输出为高电平(1)。实际操作BSRR寄存器
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
作用:设置某个IO口输出为低电平(0)。实际操作的BRR寄存器。
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
这两个函数不常用,也是用来设置IO口输出电平。
使能IO口时钟。调用函数RCC_APB2PeriphColckCmd();
不同的IO组,调用的时钟使能函数不一样。
#include"led.h"
#include"stm32f10x.h"
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructture;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);//ʹÄÜʱÖÓ
GPIO_InitStructture.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructture.GPIO_Pin=GPIO_Pin_5;
GPIO_InitStructture.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructture);
GPIO_SetBits(GPIOB,GPIO_Pin_5);
GPIO_InitStructture.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructture.GPIO_Pin=GPIO_Pin_5;
GPIO_InitStructture.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOE,&GPIO_InitStructture);
GPIO_SetBits(GPIOE,GPIO_Pin_5);
}
#include "stm32f10x.h"
#include "delay.h"
#include "led.h"
int main(void)
{
delay_init();
LED_Init();
while(1){
GPIO_SetBits(GPIOB,GPIO_Pin_5);
GPIO_SetBits(GPIOE,GPIO_Pin_5);
delay_ms(500);
GPIO_ResetBits(GPIOB,GPIO_Pin_5);
GPIO_ResetBits(GPIOB,GPIO_Pin_5);
delay_ms(500);
}
}
在了解下GPIO相关的寄存器,在库函数里操作寄存器。
两个32位配置寄存器(GPIOx_CRL ,GPIOx_CRH) ,
两个32位数据寄存器 (GPIOx_IDR和GPIOx_ODR),
一个32位置位/ 复位寄存器(GPIOx_BSRR),
一个16位复位寄存器(GPIOx_BRR),
一个32位锁定寄存器(GPIOx_LCKR)。
#include"led.h"
#include"stm32f10x.h"
void LED_Init(void)
{
RCC->APB2ENR!=1<<3;//寄存器只要第三位,其他不改变
RCC->APB2ENR!=1<<6;//寄存器第六位
GPIOB->CRL&=0XFF0FFFFF;//GPIOB.5
GPIOB->CRL|=0X00300000;//先清零在赋值
GPIO->ODR=1<<5;
GPIOB->CRL&=0XFF0FFFFF;//GPIOB.6
GPIOB->CRL|=0X00300000;//先清零在赋值
GPIO->ODR=1<<5;
}
#include"stm32f10x.h"
#include"delay.h"
#include"led.h"
int main(void)
{
delay_init();
LED_Init();
while(1){
GPIOB->|=1<<5;
GPIOE->|=1<<5;
delay_init(500);
GPIOB->ODR=~(1<<5);
delay_init(500);
}
}
位操作:支持了位带 中,有两个区中实现了位带。其中一个是 SRAM 区的最低 1MB 范围,第二个则是片内外设 区的最低 1MB 范围。这两个区中的地址除了可以像普通的 RAM 一样使用外,它们还都有自 己的“位带别名区”,位带别名区把每个比特膨胀成一个 32 位的字。当你通过位带别名区访 问这些字时,就可以达到访问原始比特的目的。
也可以理解成把每个比特膨胀为一个32位的字,当访问这些字的时候就达到了访问比特的目的,比如说BSRR寄存器有32个位,那么可以映射到32个地址上,我们去访问(读-改-写)这32个地址就达到访问32个比特的目的。
举例如下:
映射关系:
位带区:支持位带操作的地址区
位带别名:对别名地址的访问最终作用到位带区的访问上(注意:这中间有一个地址映射过程)
手把手写跑马灯实验-位带操作。
使能IO口时钟。调用函数RCC_APB2PeriphColckCmd();
初始化IO口模式。调用函数GPIO_Init();
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO口地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
…
#define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出
#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入
#define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出
#define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
int main(void)
{
delay_init();
LED_Init();
while(1){
PBout(5)=1;
PEout(5)=1;
delay_ms(500);
PBout(5)=0;
PEout(5)=0;
delay_ms(500);
}
}
1.蜂鸣器简介
蜂鸣器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。 我们使用的开发板板载的蜂鸣器是电磁式的有源蜂鸣器。这里的有源不是指电源的“源”,而是指有没有自带震荡电路,有源蜂鸣器自带了震荡电路, 一通电就会发声;无源蜂鸣器则没有自带震荡电路,必须外部提供 2~5Khz 左右的方波驱动, 才能发声。
STM32 的单个 IO 最大可以提供 25mA 电流(来自数据手册),而蜂鸣器的驱动电流是 30mA 左右,两 者十分相近,但是全盘考虑,STM32 整个芯片的电流,最大也就 150mA,如果用 IO 口直接驱 动蜂鸣器,其他地方用电就得省着点了…所以,我们不用 STM32 的 IO 直接驱动蜂鸣器,而是 通过三极管扩流后再驱动蜂鸣器,这样 STM32 的 IO 只需要提供不到 1mA 的电流就足够了。
2.硬件设计
1)指示灯 DS0
2)蜂鸣器
NPN 三极管(S8050)来驱动蜂鸣器,R60 主要用于防止蜂鸣器的误发 声。当 PB.8 输出高电平的时候,蜂鸣器将发声,当 PB.8 输出低电平的时候,蜂鸣器停止发声。
3.软件介绍
#include "beep.h"
//初始化 PB8 为输出口.并使能这个口的时钟
//LED IO 初始化
void BEEP_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//使能 GPIOB 端口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //BEEP-->GPIOB.8 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度为 50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据参数初始化 GPIOB.8
GPIO_ResetBits(GPIOB,GPIO_Pin_8); //输出 0,关闭蜂鸣器输出
}
void BEEP_Init(void),该函数的作用就是使能 PORTB 的时钟, 同时配置 PB8 为推挽输出。这里的初始化内容跟跑马灯实验几乎是一样的,这里就不做 深入的讲解。
在 main.c 里面编写如下代码:
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
//ALIENTEK 战舰 STM32 开发板实验 2
//蜂鸣器实验
int main(void)
{
delay_init(); //延时函数初始化
LED_Init(); //初始化与 LED 连接的硬件接口
BEEP_Init(); //初始化蜂鸣器端口
while(1)
{ LED0=0;
BEEP=0;
delay_ms(300);
LED0=1;
BEEP=1;
delay_ms(300);
}
}
1.STM32 IO 口简介
STM32F1 的 IO 口已经有了比较详细的介绍。STM32F1 的 IO 口做输入使用的时候,是通过调用函数 GPIO_ReadInputDataBit()来读取 IO 口的状态的。就可以开始我们的代码编写了。 这一章,我们将通过 ALIENTEK 战舰 STM32 开发板上载有的 4 个按钮(WK_UP、KEY0、 KEY1 和 KEY2),来控制板上的 2 个 LED(DS0 和 DS1)和蜂鸣器,其中 WK_UP 控制蜂鸣器, 按一次叫,再按一次停;KEY2 控制 DS0,按一次亮,再按一次灭;KEY1 控制 DS1,效果同 KEY2;KEY0 则同时控制 DS0 和 DS1,按一次,他们的状态就翻转一次。
2.硬件设计
本实验用到的硬件资源有:
1) 指示灯 DS0、DS1
2) 蜂鸣器
3) 4 个按键:KEY0、KEY1、KEY2、和 WK_UP。
KEY0、KEY1 和 KEY2 是低电平有效的,而 WK_UP 是高电平有效的, 并且外部都没有上下拉电阻,所以,需要在 STM32 内部设置上下拉。
3.软件设计
按键实验工程引入了 key.c 文件以及头文件 key.h。下面我们首先打开 key.c 文件,代码如下:
#include "key.h"
#include "sys.h"
#include "delay.h"
//按键初始化函数
void KEY_Init(void) //IO 初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|
RCC_APB2Periph_GPIOE,ENABLE); //使能 PORTA,PORTE 时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//GPIOE.2~4
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure); //初始化 GPIOE2,3,4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //初始化 WK_UP-->GPIOA.0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 设置成输入,下拉
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.0
}
//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//0,没有任何按键按下;1,KEY0 按下;2,KEY1 按下;3,KEY2 按下 ;4,KEY3 按下 WK_UP
//注意此函数有响应优先级,KEY0>KEY1>KEY2>KEY3!!
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1; //按键按松开标志
if(mode)key_up=1; //支持连按
if(key_up&&(KEY0==0||KEY1==0||KEY2==0||KEY3==1))
{
delay_ms(10); //去抖动
key_up=0;
if(KEY0==0)return KEY0_PRES;
else if(KEY1==0)return KEY1_PRES;
else if(KEY2==0)return KEY2_PRES;
else if(KEY3==1)return WKUP_PRES;
}else if(KEY0==1&&KEY1==1&&KEY2==1&&KEY3==0)key_up=1;
return 0; // 无按键按下
}
接下来我们看看头文件 key.h 里面的代码:
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
#define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)//读取按键 0
#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)//读取按键 1
#define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)//读取按键 2
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键 3(WK_UP)
#define KEY0_PRES 1 //KEY0 按下
#define KEY1_PRES 2 //KEY1 按下
#define KEY2_PRES 3 //KEY2 按下
#define WKUP_PRES 4 //WK_UP 按下(即 WK_UP/WK_UP)
void KEY_Init(void); //IO 初始化
u8 KEY_Scan(u8); //按键扫描函数
#endif
位操作也可以:
#define KEY0 PEin(4) //PE4
#define KEY1 PEin(3) //PE3
#define KEY2 PEin(2) //PE2
#define WK_UP PAin(0) //PA0 WK_UP
主函数:
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "beep.h"
//ALIENTEK 战舰 STM32 开发板实验 3
//按键输入实验
int main(void)
{
u8 key;
delay_init(); //延时函数初始化
LED_Init(); //LED 端口初始化
KEY_Init(); //初始化与按键连接的硬件接口
BEEP_Init(); //初始化蜂鸣器端口
LED0=0; //先点亮红灯
while(1)
{
key =KEY_Scan(0); //得到键值
if(key)
{ switch(t)
{ case WKUP_PRES: //控制蜂鸣器
BEEP=!BEEP;break;
case KEY2_PRES: //控制 LED0 翻转
LED0=!LED0;break;
case KEY1_PRES: //控制 LED1 翻转
LED1=!LED1;break;
case KEY0_PRES: //同时控制 LED0,LED1 翻转
LED0=!LED0;
LED1=!LED1;break;
}
}else delay_ms(10);
}
}