STM32学习笔记(二)——深入理解STM32的GPIO和AFIO功能
STM32学习笔记(二)——STM32的GPIO和AFIO
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