江科大自化协STM32学习笔记(部分C语言知识、STM32简介和GPIO口的使用)

本篇文章是根据B站UP主江科大自化协的教学视频STM32入门教程-2023持续更新中,在了解、学习与实操后整理的学习笔记,内容部分来自UP主的课程资料,并包含了一些个人的理解,如有谬误欢迎指正,详细知识点可以观看UP主的视频进行了解。
希望大家都能早日掌握单片机。

文章目录

一、前置知识(C语言)

1、指针

(1)基本概念

​ 计算机的内存中每个存储单元都是有序的,且都是按字节编码的,字节是最小的存储单位。在程序中我们定义的变量存储于类似的缓存中,不过缓存位于CPU内部,且速度更快,同样每个存储单元也有对应编码,我们称其为地址,而指针即为地址

​ 指针是一种数据类型,是地址,指针变量即为存储地址的变量,其定义形式为:

//数据类型 *指针名
int *x;
float *f;
char *ch;
//如上,就分别定义了int、float和char类型的指针变量
  • 由上面的例子可以得到,指针变量也是有数据类型的,因为不同数据类型占的地址空间不同,所以指针变量的类型应与它将指向的变量数据类型相同

(2)指针变量的使用

Ⅰ、指针变量的声明(初始化)

​ 在声明指针变量时,如果不一开始就赋值的话,可能会导致较为严重的错误:

int *p;
*p = 100;
//如上,因为指针变量无初值,“100”的值赋给的缓存地址是不确定(随机)的,这样有可能导致一些重要的缓存位置的数据被替换导致数据丢失出错

​ 因此建议声明时赋空(“NULL”),避免出错:

int *p = NULL;//等同于int *p = 0;
*p = 100;
  • 声明指针变量时一定要先初始化,否则可能会导致出错。
Ⅱ、相关运算符
  • 取地址运算符&:单目运算符&是用来取操作对象的地址。例:&i 为取变量 i 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址);
  • 指针运算符*(间接寻址符):与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例:x = &i,x 为 i 的地址,间接寻址x 则为通过 i 的地址,获取 i 的内容;
  • 关系:*x = &x

代码示例:

int a;//声明了一个普通变量a
int *pa;//声明一个指针变量,指向变量a的地址
pa = &a;//通过取地址符&,获取a的地址,赋值给指针变量
printf("%d",*pa);//通过间接寻址符,获取指针指向的内容
  • 注意:声明中的*符号和其他情况下使用的意义不同,声明中的仅仅表示声明的变量为指针变量,另起一行赋值时不需要带上星号;其他情况下为指针运算符,通过操作对象的地址,获取存储的内容;为符号的复用。

(3)指针与数组

Ⅰ、相同点
  • 数组名是数组第一个元素的地址,即
char str[6]={0,1,2,3,4,5};
char *P=str;
//输入第一个元素,以下两条语句功能相同
scanf("%sn",str);
scanf("%sn",p);
//打印第一个元素,以下两条语句功能相同
printf("%s",*str);
printf("%s",*p);
Ⅱ、不同点
  • 数组名只是一个地址(常量,不可变),而指针是一个左值(可变)

(C语言的术语lvalue左值指用于识别或定位一个存储位置的标识符,必须是可改变的。)

char str[6]={0,1,2,3,4,5};
char *P=str;
//输入第二个元素
//scanf("%sn",str+1);此为错误的用法
scanf("%sn",p+1);
//打印第二个元素
//printf("%s",*(str+1);此为错误的用法
printf("%s",*(p+1));
  • 注意:p+1并不是简单地将地址加1,而是指向数组的下一个元素

2、结构体

(1)基本概念

​ 结构体是一种构造数据类型。数组是同种类型的数据的集合,那么结构体就是不同类型的数据组合成的整体。其定义形式如下:

struct 结构体名称
{
    //结构体成员可以是一种基本的数据类型,也可以是数组,或者另一个结构体
    结构体成员1;
    结构体成员2;
    结构体成员3;
    ……
}
  • 注意:大括号后面的分号不能少,这是一条完整的语句。

(2)定义结构体变量

​ 结构体声明仅是进行一个框架的描绘,并不会为其分配存储空间, 直到定义一个结构体变量。

Ⅰ、基本方式

​ 定义形式如下:

struct 结构体名称 结构体变量名

​ 示例如下:

#include <stdio.h>

struct Book
{
	char title[120];
    char author[40];
    float price;
    unsigned int date;
    char publisher[40];
}

int main(void)
{
    struct Book book;
    
    return 0;
}
  • 这样定义的变量为局部变量
Ⅱ、在声明的最后进行定义

​ 定义形式如下:

struct 结构体名称
{
    //结构体成员可以是一种基本的数据类型,也可以是数组,或者另一个结构体
    结构体成员1;
    结构体成员2;
    结构体成员3;
    ……
} 结构体变量名;
  • 这样定义的变量为全局变量
Ⅲ、直接定义结构体变量

定义形式如下:

struct 
{
    //结构体成员可以是一种基本的数据类型,也可以是数组,或者另一个结构体
    结构体成员1;
    结构体成员2;
    结构体成员3;
    ……
} 结构体变量名;
  • 只有关键字struct,没有结构体名称。由于没有结构体名称,在此定义语句后面无法再定义这个类型的其他结构变量。一般情况下,除非变量不会再增加,还是建议采用前两种结构变量的定义形式
Ⅳ、利用typedef进行简化

​ 定义形式如下:

typedef struct 结构体名称
{
    //结构体成员可以是一种基本的数据类型,也可以是数组,或者另一个结构体
    结构体成员1;
    结构体成员2;
    结构体成员3;
    ……
} 简化的结构体变量定义格式("struct 结构体名称")int main(void)
{
    简化的结构体变量定义格式("struct 结构体名称") 结构体变量名; 
}
  • 这样定义的变量为全局变量

​ 示例如下:

typedef struct Book
{
	char title[120];
    char author[40];
    float price;
    unsigned int date;
    char publisher[40];
} infor;
    

int main(void)
{
    infor book;
    
    return 0;
}

(3)访问结构体变量

Ⅰ、使用点号运算符

​ 要访问结构体成员,我们需要引入一个新的运算符——点号(.)运算符。比如book.title就是引用book结构体的title成员,它是一个字符数组;而book.price则是引用book结构体的price成员,它是一个浮点型的变量。示例如下:

typedef struct{
    char title[120];
    char author[40];
    float price;
    unsigned int date;
    char publisher[40];
} infor;
    
int main(void)
{
    infor book;
    book.title[120]="";
    book.author[40]="";
    book.price=;
    book.date=;
    book.publisher=;
}
Ⅱ、使用结构体指针(使用运算符)

​ 要访问结构体成员,我们需要引入一个新的运算符——指针(->)运算符

​ 访问形式如下:

结构体指针(结构体的首地址)->结构体成员名=//意为将值赋给结构体指针指向的结构体成员
//等同于:
(*结构体指针).结构体成员名=//注意:“.”号运算符优先级比“*”号运算符高,因此要加括号

​ 示例如下:

typedef struct{
    char title[120];
    char author[40];
    float price;
    unsigned int date;
    char publisher[40];
} infor;
    
int main(void)
{
    infor book;
    struct *pt;
    pt=&book;
    pt->title[120]="";
    pt->author[40]="";
    pt->price=;
    pt->date=;
    pt->publisher[40]=;
}

3、枚举

(1)基本概念

​ 枚举是C语言中的一种基本数据类型,它的用途是:定义一个取值受限制的整形变量,用于限制变量取值范围,让数据更为简洁和易读,提高可读性。其定义格式如下:

enum 枚举类型名称{
    枚举值名称,
	枚举值名称,
    ……
}
  • 注意:大括号后面的分号不能少,这是一条完整的语句;每个成员名称后的分隔符为“,”,这是与结构体不同的地方。
  • 注意:第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1;若我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。

(2)定义枚举变量

Ⅰ、基本方式

​ 定义形式如下:

enum 枚举类型名称 枚举变量1,枚举变量2
Ⅱ、在声明的最后进行定义

​ 定义形式如下:

enum 枚举类型名称{
    枚举值名称,
	枚举值名称,
    ……
} 枚举变量名;
Ⅲ、直接定义枚举变量

​ 定义形式如下:

enum {枚举值名称,枚举值名称,……} 枚举变量名;
  • 枚举类型名称可以省略;
  • 因为枚举变量类型较长,所以通常用typedef更改变量类型名。

(3)访问枚举成员

​ 示例如下:

int main(void)
{
    enum Week{Mon=1,Tue,Wed} day;
    day = Wed;
    return 0;
}

(4)实例

Ⅰ、常规用法
#include <stdio.h>

enum WeekDay{Mon=1,Tue,Wed,Thu,Fri,Sat,Sun};

int main(void)
{
    enum WeekDay day;
    day = Mon;			//day = 1;
    day = Tue;			//day = 2;
    printf("%dn",day);
    return 0;
}
Ⅱ、遍历枚举类型
#include <stdio.h>
enum Week{Mon=1, Tue, Wed, Thu, Fri, Sat, Sun} day;
int main(){
    // 遍历枚举元素
    for (day = Mon; day <= Sun; day++)
    {
        printf("枚举元素:%dn", day);
    }
}
  • 仅当枚举类型值连续才可遍历。
Ⅲ、STM32中与结构体结合的用法
typedef enum
{ 
  GPIO_Speed_10MHz = 1,
  GPIO_Speed_2MHz, 
  GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;

typedef struct
{
  uint16_t GPIO_Pin; 
  GPIOSpeed_TypeDef GPIO_Speed; 	//定义枚举变量
  GPIOMode_TypeDef GPIO_Mode;   
}GPIO_InitTypeDef;

int main(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;	//定义结构体变量
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
}
//节选

二、STM32F103基本介绍

1、STM32简介

​ STM32是ST公司基于ARM Cortex-M内核开发的32位微控制器,常应用在嵌入式领域,如智能车、无人机、机器人、无线通信、物联网、工业控制、娱乐电子产品等。STM32功能强大、性能优异、片上资源丰富、功耗低,是一款经典的嵌入式微控制器。目前STM32共有四个系列,分别是高性能系列、主流系列、超低功耗系列和无线系列,对应的都有不同的产品,如:

  • 高性能系列:STM32F2、F4、F7、H7等;
  • 主流系列:STM32F0、F1、F3、G0、G4等;
  • 超低功耗系列:STM32L0、L1、L4、L4+、L5、U5等;
  • 无线系列:STM32WL、WB等。

2、系统结构

请添加图片描述

3、STM32F103C8T6基本信息

在这里插入图片描述

  • 系列:主流系列STM32F1

  • 内核:ARM Cortex-M3

  • 主频:72MHz

  • RAM:20K(SRAM)

  • ROM:64K(Flash)

  • 供电:2.0~3.6V(标准3.3V)

  • 封装:LQFP48

4、STM32F103C8T6片上资源/外设(Peripheral)

在这里插入图片描述

5、引脚定义

在这里插入图片描述

6、最小系统电路

在这里插入图片描述

三、GPIO通用IO口的使用

1、GPIO简介

​ GPIO(General Purpose Input Output)通用输入输出口,可配置为8种输入输出模式,引脚电平:0V~3.3V,部分引脚可容忍5V。

​ 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等。

​ 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。

2、GPIO位结构和工作模式

在这里插入图片描述

​ GPIO位结构由引脚端口、驱动器和寄存器组成。由图可以将结构分为两部分,一是上半部分,为输入模式;二是下半部分,为输出模式

  • 输入模式:共四种输入模式,上拉电阻和下拉电阻将其分为三种模式,分别为上拉输入(连接上拉电阻)、下拉输入(连接下拉电阻)和浮空输入(上拉、下拉电阻均不连接);后通过施密特触发器对电压进行整形然后输入;不通过施密特触发器的为模拟输入,引脚直接接入内部ADC

上拉电阻接通时,保证引脚为高电平;

下拉电阻接通时,保证引脚为低电平;

施密特触发器执行逻辑:如果输入电压大于某一阈值,输入就会瞬间升为高电平;如果输入电压小于某一阈值,输入就会瞬间降为低电平。

  • 输出模式:共四种输出模式,输出可以由输出数据寄存器或者片上外设控制(复用输出);在输出控制部分,由两个MOS管将其分为三种模式,分别为推挽输出(两个MOS管都有效)、开漏模式(N—MOS管有效)关闭(配置为输入模式时,两个MOS管都无效)

MOS管相当于一种电子开关,通过信号来控制开关的导通和关闭,开关负责使IO口输出高或低电平;

推挽输出模式(强推输出模式)下,高低电平均有较强的驱动能力,STM32对IO口有绝对的控制权;

开漏输出模式下,只有低电平有驱动能力,高电平没有驱动能力;常作为通信协议的驱动方式,在多机通信的情况下,这个模式可以避免各个设备的相互干扰;还可用于输出5V的电平信号

  • 一个端口只能有一个输出,但可以有多个输入。即在输入模式下,输出都是无效的;在输出模式下,输入模式也是有效的。

​ 通过配置GPIO的端口配置寄存器,端口可以配置成以下8种模式:

在这里插入图片描述

3、不同外设的GPIO配置

​ 内容较多,见STM32F10XXX参考手册。

4、GPIO常用寄存器

(1)端口配置低高寄存器(GPIOx_CRL/GPIOx_CRH)(x=A…E)

​ 端口配置寄存器共16位,但每4位数据表示1位,共需要64位,而STM32中每个寄存器都为32位,因此分为端口配置低寄存器和端口配置高寄存器。通过端口配置寄存器可以配置GPIO工作模式端口输出速度

输出速度可以限制输出引脚的最大翻转速度,作用是降低功耗、提高稳定性,一般情况下配置为50MHz

(2)端口输入数据寄存器(GPIOx_IDR)(x=A…E)

​ 输入数据共16位,但寄存器共32位,因此寄存器高16位为空。

(3)端口输出数据寄存器(GPIOx_ODR)(x=A…E)

​ 输出数据共16位,但寄存器共32位,因此寄存器高16位为空。

(4)端口位设置/清除寄存器(GPIOx_BERR)(x=A…E)

​ 高16位用于清除,低16位用于设置。

  • 高16位:为0不影响;为1清0;
  • 低16为:为0不影响;为1置1。

(5)端口位清除寄存器(GPIOx_BER)(x=A…E)

​ 高16位为空,低16位用于清除,方法同上。

(5)端口位配置锁定寄存器(GPIOx_LCKR)(x=A…E)

​ 高15位为空,低17位用于锁定,较少使用。

5、GPIO常用函数

(1)RCC常用函数

​ 在RCC时钟控制的函数库中,我们最经常用到的是以下三个函数:

//AHB系统总线时钟控制
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
//APB2总线时钟控制
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
//APB1总线时钟控制
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
/*第一个参数为外设选择,与STM32互联型的设备在下列列表中选择:
 *	   @arg RCC_AHBPeriph_DMA1
 *     @arg RCC_AHBPeriph_DMA2
 *     @arg RCC_AHBPeriph_SRAM
 *     @arg RCC_AHBPeriph_FLITF
 *     @arg RCC_AHBPeriph_CRC
 *     @arg RCC_AHBPeriph_OTG_FS    
 *     @arg RCC_AHBPeriph_ETH_MAC   
 *     @arg RCC_AHBPeriph_ETH_MAC_Tx
 *     @arg RCC_AHBPeriph_ETH_MAC_Rx
 *
 *     @arg RCC_APB2Periph_AFIO, RCC_APB2Periph_GPIOA, RCC_APB2Periph_GPIOB,
 *          RCC_APB2Periph_GPIOC, RCC_APB2Periph_GPIOD, RCC_APB2Periph_GPIOE,
 *          RCC_APB2Periph_GPIOF, RCC_APB2Periph_GPIOG, RCC_APB2Periph_ADC1,
 *          RCC_APB2Periph_ADC2, RCC_APB2Periph_TIM1, RCC_APB2Periph_SPI1,
 *          RCC_APB2Periph_TIM8, RCC_APB2Periph_USART1, RCC_APB2Periph_ADC3,
 *          RCC_APB2Periph_TIM15, RCC_APB2Periph_TIM16, RCC_APB2Periph_TIM17,
 *          RCC_APB2Periph_TIM9, RCC_APB2Periph_TIM10, RCC_APB2Periph_TIM11  
 *
 *     @arg RCC_APB1Periph_TIM2, RCC_APB1Periph_TIM3, RCC_APB1Periph_TIM4,
 *          RCC_APB1Periph_TIM5, RCC_APB1Periph_TIM6, RCC_APB1Periph_TIM7,
 *          RCC_APB1Periph_WWDG, RCC_APB1Periph_SPI2, RCC_APB1Periph_SPI3,
 *          RCC_APB1Periph_USART2, RCC_APB1Periph_USART3, RCC_APB1Periph_USART4, 
 *          RCC_APB1Periph_USART5, RCC_APB1Periph_I2C1, RCC_APB1Periph_I2C2,
 *          RCC_APB1Periph_USB, RCC_APB1Periph_CAN1, RCC_APB1Periph_BKP,
 *          RCC_APB1Periph_PWR, RCC_APB1Periph_DAC, RCC_APB1Periph_CEC,
 *          RCC_APB1Periph_TIM12, RCC_APB1Periph_TIM13, RCC_APB1Periph_TIM14
 *
 *	   其他设备在下列列表中选择:
 *     @arg RCC_AHBPeriph_DMA1
 *     @arg RCC_AHBPeriph_DMA2
 *     @arg RCC_AHBPeriph_SRAM
 *     @arg RCC_AHBPeriph_FLITF
 *     @arg RCC_AHBPeriph_CRC
 *     @arg RCC_AHBPeriph_FSMC
 *     @arg RCC_AHBPeriph_SDIO
 *
 *可用按位或(|)来选择多个外设
 *
 *第二个参数为选择使能或失能,选择:ENABLE or DISABLE
 */

​ GPIO连接在APB2总线上,且假定以PA0为接口,则使能GPIO的函数为:

void RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

(2)GPIO常用函数

​ 在GPIO通用IO口的函数库中,我们最经常用到的是以下函数:

//复位函数,调用这个函数后,所指定的GPIO外设就会被复位
void GPIO_DeInit(GPIO_TypeDef* GPIOx);
//复位函数,可以复位AFIO外设
void GPIO_AFIODeInit(void);
//初始化函数,功能:用结构体的参数来初始化GPIO口
//初始化时,我们需要先定义一个结构体变量,然后给结构体赋值,最后调用初始化函数,函数内部会自动读取结构体的值,然后把外设的各个参数配置好。
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
//功能:把结构体变量赋一个默认值
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);
//以下四个函数为GPIO的读取函数,均有返回值
//功能:读取输入数据寄存器某一个端口的输入值
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
//功能:读取整个输入数据寄存器
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
//功能:读取输出数据寄存器的某一位,用于输出模式下,查看输入的值
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
//功能:读取整个输出数据寄存器
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
//以下四个函数为GPIO的写入函数
//功能:将指定端口引脚设置为高电平
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);
//功能:同时对16个端口进行写入操作
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
/*参数注释:
 *①GPIO_TypeDef* GPIOx,GPIO端口选择,值为GPIOx(x=A~G);
 *可用按位或(|)来选择多个端口
 *
 *②uint16_t GPIO_Pin,IO口引脚选择,一个GPIO端口有16个引脚,所以其值为GPIO_Pin_x(x=0~15)
 *可用按位或(|)来选择多个引脚
 *
 *③BitAction BitVal,指定写入的数据值,这个参数可以是BitAction枚举中的一个值,值为:
 *	@arg Bit_RESET:清除端口值,即置低电平
 *  @arg Bit_SET:设置端口值,即置高电平
 *
 *④uint16_t PortVal,指定要写入端口输出数据寄存器的值
 *
 *⑤GPIO_InitTypeDef* GPIO_InitStruct,GPIO初始化结构体的地址
 */

/*初始化结构体定义:
 *①定义一个结构体变量
 *GPIO_InitTypeDef GPIO_InitStructure;
 *结构体如下:
 *typedef struct
 *{
 *  uint16_t GPIO_Pin;             
 *  GPIOSpeed_TypeDef GPIO_Speed;  
 *  GPIOMode_TypeDef GPIO_Mode;    
 *}GPIO_InitTypeDef;
 *由此可知该结构体有三个成员,分别为GPIO_Pin、GPIO_Speed和GPIO_Mode,下一步就是分别赋值
 *
 *②给结构体赋值:
 *GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置推挽输出
 *GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;		   //使用GPIO的0号引脚
 *GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度为50MHz
 *
 *GPIO_Mode,用于设置工作模式,其枚举如下:
 *typedef enum
 *{ GPIO_Mode_AIN = 0x0,			//模拟输入模式(Analog IN)
 *  GPIO_Mode_IN_FLOATING = 0x04,	//浮空输入模式
 *  GPIO_Mode_IPD = 0x28,			//下拉输入模式(In Pull Down)
 *  GPIO_Mode_IPU = 0x48,			//上拉输入模式(In Pull Up)
 *  GPIO_Mode_Out_OD = 0x14,		//开漏输出模式(Out Open Drain)
 *  GPIO_Mode_Out_PP = 0x10,		//推挽输出模式(Out Push Pull)
 *  GPIO_Mode_AF_OD = 0x1C,			//复用开漏模式(Atl Open Drain)
 *  GPIO_Mode_AF_PP = 0x18			//复用推挽模式(Atl Push Pull)
 *}GPIOMode_TypeDef;
 *
 *GPIO_Pin,选择引脚,值为GPIO_Pin_x(x=0~15和All)
 *
 *GPIO_Speed,选择输出速度,其枚举如下:
 *typedef enum
 *{ 
 *  GPIO_Speed_10MHz = 1,
 *  GPIO_Speed_2MHz, 
 *  GPIO_Speed_50MHz
 *}GPIOSpeed_TypeDef;
 *常用速度为50MHz
 */

6、GPIO初始化

​ 以用低电平驱动的方式点亮一个接在PA0口的LED,并使其闪烁为例。

(1)使用RCC开启GPIO的时钟

​ GPIO连接在APB2总线上,且以PA0为接口,则使能时钟的函数为:

void RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

(2)使用GPIO_Init函数初始化GPIO

​ 为了保证高低电平的强驱动能力,选用推挽输出,代码如下:

//定义结构体变量(局部变量)
GPIO_InitTypeDef GPIO_InitStructure;
//访问结构体变量(引出结构体成员)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;		 //使用GPIO的0号引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度为50MHz

(3)使用输出或输入的函数控制GPIO口

​ 调用输入的函数,GPIO_SetBits()、GPIO_ResetBits()和GPIO_WriteBit()均可单独设置引脚低电平,代码如下:

GPIO_ResetBits(GPIOA,GPIO_Pin_0);
Delay_ms(250);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
Delay_ms(250);
		
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
Delay_ms(250);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
Delay_ms(250);
//通过强制转换为BitAction的枚举类型来直接输入高低电平
GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)0);
Delay_ms(250);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)1);
Delay_ms(250);

7、GPIO应用实例

(1)LED闪烁

​ LED有来两种驱动方式:高电平驱动和低电平驱动,对STM32来说,两种驱动方式均可,但是因为很多单片机或者芯片,都使用了高电平弱驱动、低电平强驱动的规则,这样可以在一定程度上避免高低电平冲突,所以常使用低电平驱动方式

开漏输出模式下,只有低电平有驱动能力,高电平没有驱动能力,因此无法进行高电平驱动。

​ 代码如下(LED连接PA0口):

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*
 *第一步,使用RCC开启GPIO的时钟
 *第二步,使用GPIO_Init函数初始化GPIO
 *第三步,使用输出或输入的函数控制GPIO口
*/
int main(void)
{
	//第一步,开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//第二步,初始化GPIO
	//定义结构体变量(局部变量)
	GPIO_InitTypeDef GPIO_InitStructure;
	//访问结构体变量(引出结构体成员)
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;		 //使用GPIO的0号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度为50MHz
	//使用GPIO_Init函数初始化GPIO
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	//第三步,控制GPIO口
	
	while(1)
	{
		GPIO_ResetBits(GPIOA,GPIO_Pin_0);
		Delay_ms(250);
		GPIO_SetBits(GPIOA,GPIO_Pin_0);
		Delay_ms(250);
		
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
		Delay_ms(250);
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
		Delay_ms(250);
		//通过强制转换为BitAction的枚举类型来直接输入高低电平
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)0);
		Delay_ms(250);
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)1);
		Delay_ms(250);
	}
}

(2)LED流水灯

​ 实现基本流水灯(LED连接PA0~PA6),代码如下:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	//第一步,开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//第二步,初始化GPIO
	//定义结构体变量(局部变量)
	GPIO_InitTypeDef GPIO_InitStructure;
	//访问结构体变量(引出结构体成员)
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;		 //使用GPIO的所有引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度为50MHz
	//使用GPIO_Init函数初始化GPIO
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	while(1)
	{
        //低电平驱动,因此需要取反
		GPIO_Write(GPIOA,~0x0001);	//0000 0000 0000 0001
		Delay_ms(500);
		GPIO_Write(GPIOA,~0x0002);	//0000 0000 0000 0010
		Delay_ms(500);
		GPIO_Write(GPIOA,~0x0004);	//0000 0000 0000 0100
		Delay_ms(500);
		GPIO_Write(GPIOA,~0x0008);	//0000 0000 0000 1000
		Delay_ms(500);
		GPIO_Write(GPIOA,~0x0010);	//0000 0000 0001 0000
		Delay_ms(500);
		GPIO_Write(GPIOA,~0x0020);	//0000 0000 0010 0000
		Delay_ms(500);
		GPIO_Write(GPIOA,~0x0040);	//0000 0000 0100 0000
		Delay_ms(500);
	}
}

​ 实现往返流水灯,代码如下:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	//第一步,开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//第二步,初始化GPIO
	//定义结构体变量(局部变量)
	GPIO_InitTypeDef GPIO_InitStructure;
	//访问结构体变量(引出结构体成员)
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;		 //使用GPIO的所有引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度为50MHz
	//使用GPIO_Init函数初始化GPIO
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	while(1)
	{
		unsigned int i;
		for(i=0;i<7;i++)
		{
			GPIO_Write(GPIOA,~(0x0001<<i));
			Delay_ms(250);
		}
		for(i=0;i<7i++)
		{
			GPIO_Write(GPIOA,~(0x0040>>i));
			Delay_ms(250);
		}
	}
}

(3)GPIO控制有源蜂鸣器


​ 有源蜂鸣器同样使用低电平驱动,不过接口为PB12,代码如下:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

int main(void)
{
	//第一步,开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	//第二步,初始化GPIO
	//定义结构体变量(局部变量)
	GPIO_InitTypeDef GPIO_InitStructure;
	//访问结构体变量(引出结构体成员)
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;		 //使用GPIO的12号引脚
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度为50MHz
	//使用GPIO_Init函数初始化GPIO
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	//第三步,控制GPIO口
	
	while(1)
	{
		GPIO_ResetBits(GPIOB,GPIO_Pin_12);
		Delay_ms(100);
		GPIO_SetBits(GPIOB,GPIO_Pin_12);
		Delay_ms(100);
		GPIO_ResetBits(GPIOB,GPIO_Pin_12);
		Delay_ms(100);
		GPIO_SetBits(GPIOB,GPIO_Pin_12);
		Delay_ms(700);
	}
}

(4)按键控制LED

  • 实际效果:两个按键对应两个LED,按一次亮,再按一次灭。

​ 按键1连接PB1,按键2连接PB11;LED1连接PA1,LED2连接PA2,使用模块化编程,代码如下:

①LED.c
#include "stm32f10x.h"                  // Device header

void LED_Init(void)
{
	//RCC使能时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	//使两个端口默认高电平,使LED处于未点亮的状态
	GPIO_SetBits(GPIOA,GPIO_Pin_1 | GPIO_Pin_2);
}

//点亮对应的LED
void LED_ON(unsigned char i)
{
	if(i==1)
	{
		GPIO_ResetBits(GPIOA,GPIO_Pin_1);
	}
	if(i==2)
	{
		GPIO_ResetBits(GPIOA,GPIO_Pin_2);
	}
}

//熄灭对应的LED
void LED_OFF(unsigned char i)
{
	if(i==1)
	{
		GPIO_SetBits(GPIOA,GPIO_Pin_1);
	}
	if(i==2)
	{
		GPIO_SetBits(GPIOA,GPIO_Pin_2);
	}
}

//通过检测输出数据寄存器的数据,得到当前的LED状态,再用相反作用的函数使其状态变化
void LED_Turn(unsigned char i)
{
	if(i == 1)
	{
		if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_1) == 0)//检测输出数据寄存器的数据是否为0
		{
			GPIO_SetBits(GPIOA,GPIO_Pin_1);				 //若为0,即LED点亮时,对应数据位置1,即熄灭
		}
		else
		{
			GPIO_ResetBits(GPIOA,GPIO_Pin_1);
		}
	}
	if(i == 2)
	{
		if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_2) == 0)//检测输出数据寄存器的数据是否为0
		{
			GPIO_SetBits(GPIOA,GPIO_Pin_2);				 //若为0,即LED点亮时,对应数据位置1,即熄灭
		}
		else
		{
			GPIO_ResetBits(GPIOA,GPIO_Pin_2);
		}
	}
}

  • 51单片机使用按键控制LED亮灭时,是直接将数据取反,但32不是直接对寄存器进行操作,而是调用已经封装好的库函数,因此不能直接取反。所以STM32通过函数检测输出数据寄存器的数据,得到当前的LED状态,再用相反作用的函数使其状态变化以达到取反的效果,使用的函数为:
GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
②Key.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

void Key_Init(void)
{
	//RCC使能时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	//初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
}

//消除抖动后返回对应键码
uint8_t Key_GetNum(void)
{
	uint8_t KeyNum = 0;
	
	if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0)
	{
		Delay_ms(20);
		while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0);
		Delay_ms(20);							//消除抖动
		KeyNum = 1;
	}
	if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0)
	{
		Delay_ms(20);
		while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0);
		Delay_ms(20);							//消除抖动
		KeyNum = 2;
	}
	
	return KeyNum;
}

③main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"

uint8_t KeyNum;

int main(void)
{
	LED_Init();		//LED端口初始化
	Key_Init();		//按键端口初始化
	while(1)
	{
		KeyNum = Key_GetNum();
		if(KeyNum == 1)
		{
			LED_Turn(1);
		}
		if(KeyNum == 2)
		{
			LED_Turn(2);
		}
	}
}

(5)光敏传感器控制蜂鸣器

  • 实际效果:当光敏传感器被遮蔽时,蜂鸣器鸣响。
  • 光敏传感器:当亮度高于阈值时,输出指示灯亮,表示输出低电平;当亮度低于阈值时,输出指示灯灭,表示输出高电平;阈值可通过上面的电位器调节。
  • 光敏传感器初始化时,需要选择上拉工作模式,因为光敏传感器的工作原理就是将光敏电阻与上拉电阻串联,通过分压来实现对亮度的感知

​ 蜂鸣器连接PB12口,光敏传感器连接PB13口,使用模块化编程,代码如下:

①Buzzer.c

#include "stm32f10x.h"                  // Device header

void Buzzer_Init(void)
{
	//RCC使能时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	//初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	//使蜂鸣器默认静默
	GPIO_SetBits(GPIOB,GPIO_Pin_12);
}

//蜂鸣器鸣响
void Buzzer_ON(void)
{
	GPIO_ResetBits(GPIOB,GPIO_Pin_12);
}

//蜂鸣器静默
void Buzzer_OFF(void)
{
	GPIO_SetBits(GPIOB,GPIO_Pin_12);
}

//蜂鸣器状态翻转
//通过检测输出数据寄存器的数据,得到当前的蜂鸣器状态,后使用函数改变蜂鸣器当前状态
void Buzzer_Turn(void)
{
	if(GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_12) == 0)//检测输出数据寄存器的数据是否为0
	{
		GPIO_SetBits(GPIOB,GPIO_Pin_12);			  //若为0,即蜂鸣器鸣响,对应数据位置1,即使蜂鸣器静默
	}
	else
	{
		GPIO_ResetBits(GPIOB,GPIO_Pin_12);
	}
}

②LightSensor.c

#include "stm32f10x.h"                  // Device header

//光敏传感器初始化
void LightSensor_Init(void)
{
	//RCC使能时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	//初始化GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		//使用上拉输出模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

//返回光敏传感器的电平情况
uint8_t LightSensor_Get(void)
{
	return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13);
}

③main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "Buzzer.h"
#include "LightSensor.h"

int main(void)
{
	Buzzer_Init();			//蜂鸣器初始化
	LightSensor_Init();		//光敏传感器初始化
	while(1)
	{	
		if(LightSensor_Get() ==1)//如果光敏传感器接收的亮度低于阈值,则蜂鸣器鸣响
		{
			Buzzer_ON();
		}
		else
		{
			Buzzer_OFF();
		}
	}
}