STM32学习笔记(二)——STM32的GPIO和AFIO
一、STM32芯片引脚分布
1.1 特殊功能引脚和普通功能引脚
STM32F103C8T6共有48个引脚
- 特殊功能引脚(11个)
- Vdd+Vss给芯片供电引脚 共三队,六个
- VDDA+VSSA模拟电压 两个(A~Analog模拟的)
- BOOT0启动方式选择引脚 一个
- VBAT备用电池引脚(Battery)一个
- NRST复位引脚 一个
- 普通IO口引脚(37个)
其中特殊功能引脚是为了维持芯片的运行,所以被特殊化了
普通IO引脚是我们可以去应用的
1.2 STM32芯片IO引脚复用
IO复用:IO引脚身兼数职的现象叫IO复用
- 如:下图的PA9和PA10
- 不仅可以做普通IO口,
- 还能做串口通信的发送端口和接收端口,
- 还能做定时器的通道引脚
- 作为芯片设计人员,希望STM32芯片的功能越强大越好
- 所以设计人员向STM32芯片内加了各种外设模块,来执行各种功能,比如ADC模块,用来做模数转换;I2C模用来通信等,详见下图
- 每一个模块都能执行特定的功能,每一个模块也会占用不同的引脚资源,芯片的引脚资源明显不够,所以必须让同一个引脚去身兼数职
1.3 引脚的通用功能和复用功能辨析
通用功能(普通IO):我们通过编程让CPU直接通过GPIO模块对对应的IO引脚进行直接的控制
复用功能:我们通过编程让CPU控制串口模块,串口模块自动的去控制GPIO模块,跟外接进行通信
总结:
通用功能:我们直接对IO引脚进行控制
复用功能:通过控制其他外设,间接对IO口进行控制,复用功能可有多个
1.4 IO引脚的重映射
IO重映射就说,将某个片上外设的复用功能,移动到其它的IO引脚上
- 若我们想用PA9和PA10的串口功能,又想用PA9和PA10的定时器功能,就会产生冲突,这时我们可以将串口功能重映射到其它引脚上,避免了冲突
1.5 最小系统板引脚图
B站UP铁头山羊老师整理的,清晰的很
- 如,PA9和PA10,可以当作通用IO,也可以复用定时器和串口
- 当既想用PA9、PA10的串口和定时器时,查重映射部分,看到串口可重映射到PB6和PB7
1.6 C8T6引脚功能总结
二、STM32的GPIO
2.1 GPIO简介
- STM32的CPU通过控制片上外设来执行相应的功能
![]()
- 其中有一种外设叫GPIO,GPIO根据CPU的指令去控制对应的引脚,进行读写操作
![]()
2.2 GPIO的寄存器组
寄存器组概念
- CPU控制GPIO这个片上外设模块,执行引脚的读写操作
- 可CPU怎么控制GPIO这个片上外设呢,他们之间怎么通信呢?
- CPU通过寄存器组去控制GPIO外设
![]()
寄存器组的组成
- 一个寄存器组由若干个寄存器组成
- 一个GPIO外设,可以控制16个IO口
- GPIO寄存器组由以下组成
- 配置寄存器
- 输入数据寄存器
- 输出数据寄存器
2.3 GPIO的8种工作模式
- GPIO的工作模式有以下8种
![]()
- 中文之华美,我们将名称拆开理解
- 输入输出
- 输入:单片机IO口读取到连接的设备的电平
- 输出:单片机IO口向连接的设备发送电平
- 通用复用
- 通用:CPU直接控制GPIO
- 复用:CPU控制片上外设,片上外设控制GPIO,间接控制
- 只有输出才有通用、复用功能区分
- 输入只是读取外部电平,无论是CPU读取,还是片上外设读取都一样,都是那个电平信号
- 推挽开漏
- IO引脚内部由2个MOS管组成
- 推挽模式:通过电压控制MOS交替导通,输出高、低电压
- 开漏模式: 可以输出低电平,和高阻抗
![]()
- 上拉下拉(针对输入模式)
- 如果IO引脚工作在输入模式,IO引脚内阻会无穷大,输入是测电压的,可以想象成万用表,万用表测量电阻两端电压时,是并联的,万用表的内存是无穷大的
- 当输入高低电压时一切正常,但若悬空时,这时IO引脚会类似于天线,接收外界电磁波,产生干扰
- 解决办法,就说加 上拉电阻或下拉电阻
- 当接上拉电阻时:引脚悬空时,引脚会被拉高到高电平;引脚正常输入高低电平时,一切正常
- 当接下拉电阻时:引脚悬空时,引脚会被拉到低电平; 引脚正常输入高低电平时,一切正常
![]()
2.4 IO口的最大输出速度
最大输出速度:IO允许输出电平的最大切换频率
- 受控于上升时间和下降时间,若频率太快,保持时间就会很小,输出不了有效的电平
![]()
STM32C8T6的最大输出速度分为三挡
- 2MHZ:每秒钟切换200万次
- 10MHZ:每秒钟切换1000万次
- 50MHZ:每秒钟切换5000万次
选择原则:选择满足要求的最小值,以此降低功耗
-
2.5 GPIO内部结构
B站铁头山羊:2.2节 10:42
三、是的终于到了-----点灯
单纯点灯方法应用程序: 点我点我点我
3.1 学会看原理图
PC13是我们要控制的引脚,当PC13为低电平时,LED导通,点亮;当PC13为高电平时,LED不导通,不亮。
我们要去看想要控制的器件的原理图,了解是什么电平,什么方式驱动这个想要控制的器件的
3.2 新建工程
- “工程是包含了我们所有编写的代码的东西”
- 我们用到的代码,包括,从网上找到的函数库+自己编写的功能代码
- 从标准库建立一个工程挺复杂的(想学习的可以去看正点原子的前几节课),避免重复造轮子,我们引入工程模板的概念,将常用的库函数功能,集成到一个模板里,而后只需要调用我们所需的库函数,专注于编写我们自己的功能代码即可
- 网上已经有很多大神,帮我们建立了工程模板,如正点原子的,洋桃电子的,海创电子的,铁头山羊的,因为最近在和铁老师学习,我们接下来用铁老师的模板。
3.3 工程模板结构
- User:用户自己编写的代码
- Startup:单片机上电后,会先执行启动文件里的程序,而后执行主函数里的程序
- PAL库:对标准库的补充
- 标注库:ST提供的官方库函数
3.4 GPIO的标准库编程接口
标准库的编程接口,是和寄存器一一对应的
3.4 开始编程
3.4.1 IO引脚初始化
初始化:使用IO引脚前做一些参数配置的工作
IO引脚初始化步骤:
- 使能GPIO端口时钟
- 选择模式和其它参数(如:推挽输出?最大传输速度?)
![]()
一、啥是使能GPIO端口时钟 ??????
为啥要使能时钟啊 ???
- CPU控制的片上外设,都为时序电路,时序电路需要时钟
- 默认状态,片上外设的时钟,都是关闭的,为了省电
- 所以用到哪个片上外设,就要开启它对应的时钟
![]()
不是,那咋使能时钟啊 ???
- 我们需要向RCC外设的寄存器里写值(忽略)
- 我们只需要调用对应的函数即可
![]()
二、GPIO_Init咋用啊??????
GPIO_Init简介:
- 用于初始化单个IO引脚
- 使用时填入两个参数
- 参数1:选择想要初始化引脚的端口号,如出书画PC13,参数填GPIOC
- 参数2:初始化各种参数,为结构体类型的指针。库函数帮我们声明好了结构体类型,这个结构体类型名称叫GPIO_InitTypeDef,也就是它给了一张表格,表格叫GPIO_InitTypeDef,你需要将你用的参数,填写到表格中。
![]()
结构体知识补充:
- 声明结构体类型,再定义结构体变量
- 类比:结构体类型=int类型
- 类比:定义结构体类变量=定义整形变量。
- 类比:GPIO_InitTypeDef a = int a
具体操作步骤
将库函数给你表格,写上你自己的名字,也就是定义结构体类型变量GPIO_InitTypeDef GPIOInitStruct(名称自定义);
填写结构体变量的参数:
- 端口C一共有16个引脚,用的哪一个引脚?
- GPIO的模式是啥?推挽输出,输入上拉?
- 若是输出,输出的最大速度是啥?
调用GPIO_Init()函数,填入两个参数
3.4.2 ODR的写入和读取
ODR(Output Data Register)输出数据寄存器
- 此寄存器可以写入也可以读取
- 写入:控制MOS管通断
- 读取:读出上次写入ODR的值
3.4.3 IDR的读取
IDR(Intput Data Register)输入数据寄存器
3.4.4 IO的翻转
IO翻转:先让灯亮,延时一会,再让灯灭,在延时一会,以此循环
编程实现:
- 实现延时功能:库函数中没有这个功能,引入PAL库
- 引入头文件#include “stm32f10x_pal.h”
- 对PAL 库初始化 PAL_Init()
- 使用PAL库中的延时函数 PAL_Delay() 单位ms
方案1的实现:
#include "stm32f10x.h"
#include "stm32f10x_pal.h"
int main(void)
{
GPIO_InitTypeDef GPIOInitStruct;
PAL_Init();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//使能GPIO外设时钟
GPIOInitStruct.GPIO_Pin = GPIO_Pin_13;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_OD;//输出开漏
GPIOInitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC,&GPIOInitStruct);//初始化GPIO外设C组
while(1)
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,0);//给PC13引脚写0,点亮灯
PAL_Delay(100);//延时100ms
GPIO_WriteBit(GPIOC,GPIO_Pin_13,1);//给PC13引脚写0,熄灭灯
PAL_Delay(100);//延时100ms
}
}
方案2的实现:
方案2中的翻转是如何做到的呢?
- ODR寄存器的可以读取上次写入的值
- 我们只需要读取上次写的是0还是1,若是1 这次写0;若是0,这次写1,就可以实现翻转的功能
![]()
#include "stm32f10x.h"
#include "stm32f10x_pal.h"
int main(void)
{
GPIO_InitTypeDef GPIOInitStruct;
PAL_Init();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//使能GPIO外设时钟
GPIOInitStruct.GPIO_Pin = GPIO_Pin_13;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_OD;//输出开漏
GPIOInitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC,&GPIOInitStruct);//初始化GPIO外设C组
GPIO_WriteBit(GPIOC,GPIO_Pin_13,0);//给PC13引脚写0,点亮灯
while(1)
{
PAL_Delay(100);//延时100ms
//翻转代码
if(GPIO_ReadOutputDataBit(GPIOC,GPIO_Pin_13) == 0)
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,1);
}
else
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,0);
}
}
}
四、按键编程
单纯按键应用程序: 点我点我点我
4.1 按键的电路设计
外接上拉电阻:
- 按键打开,IO口读取到的是高电平
- 按键闭合,IO口读取到的是低电平
内部上拉电阻
- 由于单片机内部本来就有两个电阻
- 可设置输入模式,来启用内部的上拉电阻,这样按键电路,节省了一个电阻
按键电路:连接时,连接1-3端 或者2-4端
4.2 按键编程的原理
当按键按下时,IO口读取到的是低电平 当按键松开时,IO口读取到的值是高电平,我们可以不断的读取IO引脚的值,来检测按键的状态
但我们习惯于捕捉,按键按下或者松开那一瞬间的值,因为只有在按键状态变化的瞬间才去切换LED的亮灭
- 如:电脑的鼠标左键,当我们按下按键时,鼠标不会有反映,只有当我们松开按键的瞬间,鼠标才完成了点击这个动作,所以我们捕捉的是按键发生变化的瞬间。
- 方法:我们声明两个变量
- previous 上次
- current 当前
- 若上次读到IDR寄存器里值是1,当前IDR寄存器是0,表示按键按下的瞬间
*若读到从0到1,是松开按键的瞬间
1.先定义两个变量,存储上一次读取到的值,和这一次读取到的值
2.然后将当前值,赋给上一次值
3.在读取当前值
4.若当前值和上一次值不想等,则代表,按键状态发生了变化
5.那是按下的变化,还是松开的变化呢
6.只需要判断当前值,当前值是0,则是按下的瞬间
7.当前值是1,则是松开的瞬间
4.3 按键控制灯的亮灭代码实现
1.PC13连接的是灯 要把PC13初始化为输出开漏模式
2.PA0连接的按键 初始化为输入上拉模式
3.读取PA0的电平,看其是否是按键按下后又松开那个瞬间,若是
4.编写灯翻转程序
#include "stm32f10x.h"
#include "stm32f10x_pal.h"
int main(void)
{
uint8_t current = 1;
uint8_t previous = 1;
GPIO_InitTypeDef GPIOInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//开启GPIOC时钟
PAL_Init();
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIOInitStruct.GPIO_Pin = GPIO_Pin_13;
GPIOInitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC,&GPIOInitStruct);//初始化GPIOC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIOA时钟
GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;//代码一行一行执行,所以直接给结构体变量直接赋值就行
GPIOInitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOA,&GPIOInitStruct);//初始化GPIOA
while(1)
{
previous = current ;
current = GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0);
if(current != previous)
{
if(current == 0)//按键按下
{
}
else
{
//切换灯的亮灭状态
if( GPIO_ReadOutputDataBit(GPIOC,GPIO_Pin_13) == 0 )
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,1);
}
else
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,0);
}
}
}
}
}
4.4 按键消抖
4.4.1 按键抖动产生原因
- 按键按下时,弹簧片与另一个引脚连通,不按下时不连通,
- 因为其物理结构,会产生抖动
- IO引脚会捕获这种抖动,从而对实验产生干扰
4.4.2 按键消抖方法
硬件电路时固定的,我们考虑用软件消抖方法。
由于抖动的时间不会超过10ms,所以我们可以等待10ms,直到按键电平稳定的时候,再去判断其电平状态
代码实现如下
- 采集当前值和上一次值,如果当前值不等于上次的值
- 那么,等待10ms
- 再次采集当前值,如果当前值,还是不等于上次值
- 那么执行对应的程序
#include "stm32f10x.h"
#include "stm32f10x_pal.h"
int main(void)
{
uint8_t current = 1;
uint8_t previous = 1;
GPIO_InitTypeDef GPIOInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//开启GPIOC时钟
PAL_Init();
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIOInitStruct.GPIO_Pin = GPIO_Pin_13;
GPIOInitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC,&GPIOInitStruct);//初始化GPIOC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIOA时钟
GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;//代码一行一行执行,所以直接给结构体变量直接赋值就行
GPIOInitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_Init(GPIOA,&GPIOInitStruct);//初始化GPIOA
while(1)
{
previous = current ;
current = GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0);//第一次读取
if(current != previous)//第一次检测 不相等
{
PAL_Delay(10);//延时10ms
current = GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0);//再次读取
if(current != previous)//再次检测
{
if(current == 0)//按键按下
{
}
else
{
//切换灯的亮灭状态
if( GPIO_ReadOutputDataBit(GPIOC,GPIO_Pin_13) == 0 )
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,1);
}
else
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,0);
}
}
}
}
}
}
4.4.3 大佬的肩膀-PAL库按键消抖
大佬的库中,关于按键的函数有两条
使用方法:
使用方法详细介绍:
- 引用头文件:#include “stm32f10x_pal_button.h”
- 声明句柄:PalButton_HandleTypeDef hbutton1;//button1的句柄变量,(最好写主函数外面)
- 填写按键初始化参数
- 哪个引脚组
- 哪个引脚
- 外部上拉电阻 还是内部上拉电阻
- 编写按键按下执行的功能函数,并将函数名,填写到句柄回调函数参数中
- 调用初始化函数PAL_Button_Init( )
- 在While中写执行函数PAL_Button_Proc( )
标准库和PAL库对比
代码:
#include "stm32f10x.h"
#include "stm32f10x_pal.h"
#include "stm32f10x_pal_button.h"
PalButton_HandleTypeDef hbutton1;//button1的句柄变量
static void OnButtonlRealeased(void)
{
//对PC13亮灭切换
if(GPIO_ReadOutputDataBit(GPIOC,GPIO_Pin_13) == 0)
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,1);
}
else
{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,0);
}
}
int main(void)
{
GPIO_InitTypeDef GPIOinitStruct;
PAL_Init();
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIOinitStruct.GPIO_Pin = GPIO_Pin_13;
GPIOinitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIOinitStruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC,&GPIOinitStruct);
//设置按键PA0 内部上拉
hbutton1.Init.GPIOx = GPIOA;
hbutton1.Init.GPIO_Pin = GPIO_Pin_0;
hbutton1.Init.Button_Mode = Button_Mode_IPU;
hbutton1.Init.ButtonReleasedCallback = OnButtonlRealeased;
PAL_Button_Init(&hbutton1);
while(1)
{
PAL_Button_Proc(&hbutton1);
}
}
4.4.3 补充 句柄
句柄:
- 编程=数据结构+算法
- 如:描述人要有年龄,身高,性别(数据结构)+行为,走,跑,跳(算法)
- 句柄就说一种数据结构,用来存储,表示这个实体的所有数据
- 而函数,算法,是对句柄里的数据执行的某种操作
例子: 现在创建一个句柄,也就是描述一个茶壶的信息,
有总容积,当前水量
然后对这个茶壶进行三种操作
- 创建一个10L的空茶壶:其实就是对茶壶这个数据结构进行操作
- 向茶壶中注入水:其实就是对茶壶这个数据结构进行操作
- 将茶壶中水倒出:其实就是对茶壶这个数据结构进行操作
4.4.3 补充 回调函数(HAL库编程也会用到)
什么是回调函数:
将我们定义的函数名称告诉给PAL库,这样PAL库将在适当的时机调用这个函数(自动调用,我搞不懂怎么调用的)
但是用法是,将想要在按键松开时执行的程序,自己封装成一个自定义函数,然后把函数名传递给句柄中的一个参数
例:我们将切换PC13亮灭状态的代码,封装成一个函数 void OnButtonlPressed()
我们希望按键松开时执行这个函数,这样我们把我们定义的函数名,给到PAL库,也就是给到这个句柄中的一个参数
五、AFIO
5.1 AFIO简介
- IO引脚可以进行复用,
- IO复用,就说同一个IO引脚身兼数职,有多个功能
- IO引脚直接被CPU控制–通用
- IO引脚被片上外设进行控制–复用,同一个IO引脚复用功能有多个
- 如下图,若我们想同时使用,片上外设1和片上外设2,那么引脚就冲突了,这是我们需要复用功能重映射来解决这个问题,也就是左图黄色的小框框,
- 这个小框框,将某一个开关的引脚,重映射到别的位置,这个方式就说复用功能的重映射
- AFIO也是一个片上外设
![]()
5.2 IO复用功能重映射表
官方编程手册8.3小节
B站UP铁头山羊:2.5节 14:00
B站UP铁头山羊: 2.5AFIO
5.3 AFIO编程
标准库接口位置:和GPIO外设在一起
有四个编程接口:
使用方法:
调用上图函数,第一参数填写要重映射外设功能的名称,开启重映射
如:
重映射外设USART1
重映射外设TIM1文章来源:https://www.toymoban.com/news/detail-811905.html
文章来源地址https://www.toymoban.com/news/detail-811905.html
到了这里,关于STM32学习笔记(二)——STM32的GPIO和AFIO的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!