持续更新的基于STM32F103XX型号的STM32教程
目录
引言:
笔记:
STM32的三种开发方式(寄存器开发 标准库函数开发 HAL库开发):
STM32的4个独立时钟源(HSI HSE LSI LSE):
STM32的AHB总线与APB总线:
开启STM32的外设模块时钟
为什么要开启外设模块时钟?
外设模块时钟使能化处理:
GPIO的8种模式,4种数据传输速率,配置与开启:
GPIO的8种工作模式(4种输入,4种输出,3种数据传输速率):
4种输入:
4种输出:
3种数据传输速率:
GPIO口的配置:
GPIO的开启:
总结:
中断:
中断的概念:
中断系统:
中断优先级:
中断嵌套:
注意:
EXTI外部中断:
EXTI外部中断的基本结构(来自江协科技PPT):
EXTI如何工作:
EXTI外部中断支持的触发方式(4种):
EXTI支持的GPIO口:
EXTI的响应触发方式(2种):
EXTI如何开启:
引言:
我主要是根据哔哩哔哩up主江协科技进行学习,其中大部分内容为自己手写,其余来自参考官方参考手册/江协科技/野火公司等,使用的开发板系列为:STM32F108XX系列。
此处附上网课地址:STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili
我所使用的开发环境为windows系统,所用的软件为Keil uVision5。
此处附上keil5的下载地址与相应mdk的下载地址:
keil5: https://www.keil.com/download/product/
MDK525:https://pan.baidu.com/s/15ox_4y5BorNfVBh5tYThtQ 提取码:wjhd
MDK安装教程:https://blog.csdn.net/DaXiongRen/article/details/120072979
如果不想下载keil5软件,也可以使用vscode中的keil插件
如下为具体的操作过程:https://blog.csdn.net/Johnor/article/details/134353311
笔记:
STM32的三种开发方式(寄存器开发 标准库函数开发 HAL库开发):
寄存器开发:此方法需要用户自己去操作底层寄存器进行功能设计(类似于51单片机的开发),但由于STM32的寄存器过多,故不建议直接使用寄存器进行开发。
标准库函数开发:此方法即使用官方提供的标准库进行开发(函数的使用可以参照官方提供的库函数文档),且较为简单,易上手。
HAL库开发: 此方法为使用官方提供的图形化的一键生产代码的工具进行开发,在这个工具中我们只要根据你想要的功能,点点鼠标,就可以生成你想要的功能,上手很快,但是性能损失比较大。
STM32的4个独立时钟源(HSI HSE LSI LSE):
HSI是高速内部时钟,RC振荡器,频率为8MHz,精度不高。
HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。
LSI是低速内部时钟,RC振荡器,频率为40kHz,提供低功耗时钟。
LSE是低速外部时钟,接频率为32.768kHz的石英晶体。
如果大家想深入了解STM32的时钟,大家可以去看一下此链接:https://zhuanlan.zhihu.com/p/376597895
STM32的AHB总线与APB总线:
AHB是高速总线,是一种系统总线,它主要负责连接处理器,DMA等一些内部接口。AHB 系统由主模块、从模块和基础结构3部分组成,整个AHB总线上的传输都由主模块发出,由从模块负责回应。
APB是低速总线,它主要负责连接外围设备,它又分为APB1(36MHZ)和APB2(72MHZ),它的总线架构不像 AHB支持多个主模块,在APB里面唯一的主模块就是APB 桥。APB桥就是连接AHB和APB中间的玩意。
以下分别为APB1和APB2所控制的不同外设的详细分布(来自官方的开发手册):
APB1:
#define RCC_APB1Periph_TIM2 ((uint32_t)0x00000001)
#define RCC_APB1Periph_TIM3 ((uint32_t)0x00000002)
#define RCC_APB1Periph_TIM4 ((uint32_t)0x00000004)
#define RCC_APB1Periph_TIM5 ((uint32_t)0x00000008)
#define RCC_APB1Periph_TIM6 ((uint32_t)0x00000010)
#define RCC_APB1Periph_TIM7 ((uint32_t)0x00000020)
#define RCC_APB1Periph_TIM12 ((uint32_t)0x00000040)
#define RCC_APB1Periph_TIM13 ((uint32_t)0x00000080)
#define RCC_APB1Periph_TIM14 ((uint32_t)0x00000100)
#define RCC_APB1Periph_WWDG ((uint32_t)0x00000800)
#define RCC_APB1Periph_SPI2 ((uint32_t)0x00004000)
#define RCC_APB1Periph_SPI3 ((uint32_t)0x00008000)
#define RCC_APB1Periph_USART2 ((uint32_t)0x00020000)
#define RCC_APB1Periph_USART3 ((uint32_t)0x00040000)
#define RCC_APB1Periph_UART4 ((uint32_t)0x00080000)
#define RCC_APB1Periph_UART5 ((uint32_t)0x00100000)
#define RCC_APB1Periph_I2C1 ((uint32_t)0x00200000)
#define RCC_APB1Periph_I2C2 ((uint32_t)0x00400000)
#define RCC_APB1Periph_USB ((uint32_t)0x00800000)
#define RCC_APB1Periph_CAN1 ((uint32_t)0x02000000)
#define RCC_APB1Periph_CAN2 ((uint32_t)0x04000000)
#define RCC_APB1Periph_BKP ((uint32_t)0x08000000)
#define RCC_APB1Periph_PWR ((uint32_t)0x10000000)
#define RCC_APB1Periph_DAC ((uint32_t)0x20000000)
#define RCC_APB1Periph_CEC ((uint32_t)0x40000000)
APB2:
#define RCC_APB2Periph_AFIO ((uint32_t)0x00000001)
#define RCC_APB2Periph_GPIOA ((uint32_t)0x00000004)
#define RCC_APB2Periph_GPIOB ((uint32_t)0x00000008)
#define RCC_APB2Periph_GPIOC ((uint32_t)0x00000010)
#define RCC_APB2Periph_GPIOD ((uint32_t)0x00000020)
#define RCC_APB2Periph_GPIOE ((uint32_t)0x00000040)
#define RCC_APB2Periph_GPIOF ((uint32_t)0x00000080)
#define RCC_APB2Periph_GPIOG ((uint32_t)0x00000100)
#define RCC_APB2Periph_ADC1 ((uint32_t)0x00000200)
#define RCC_APB2Periph_ADC2 ((uint32_t)0x00000400)
#define RCC_APB2Periph_TIM1 ((uint32_t)0x00000800)
#define RCC_APB2Periph_SPI1 ((uint32_t)0x00001000)
#define RCC_APB2Periph_TIM8 ((uint32_t)0x00002000)
#define RCC_APB2Periph_USART1 ((uint32_t)0x00004000)
#define RCC_APB2Periph_ADC3 ((uint32_t)0x00008000)
#define RCC_APB2Periph_TIM15 ((uint32_t)0x00010000)
#define RCC_APB2Periph_TIM16 ((uint32_t)0x00020000)
#define RCC_APB2Periph_TIM17 ((uint32_t)0x00040000)
#define RCC_APB2Periph_TIM9 ((uint32_t)0x00080000)
#define RCC_APB2Periph_TIM10 ((uint32_t)0x00100000)
#define RCC_APB2Periph_TIM11 ((uint32_t)0x00200000)
开启STM32的外设模块时钟
为什么要开启外设模块时钟?
由于STM32十分复杂,而且在控制不同外设时,所需的时钟频率各不相同(如果控制外设都是用告诉时钟,那么无异于杀鸡焉用牛刀,过分浪费性能了),而且为了控制单片机的功率,故ST公司使用了多个时钟源的设置,因此在我们想用某个外设时就要开启相应地时钟。
外设模块时钟使能化处理:
在开启外设模块时钟之前,我们需要新进行总线的选择(如我们在使用GPIOA时,我门就需要选择APB2总线;在使用TIM2计时器时,我门就要选择APB1总线)。
外设模块时钟使能的初始化函数:
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState)
//APB1总线的外设模块时钟使能初始化函数
//此处第一个参数为你所使用的外设名字
//第二个参数为 ENABLE
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
//APB2总线的外设模块时钟使能初始化函数
//此处第一个参数为你所使用的外设名字
//第二个参数为 ENABLE
假设我们使用的是GPIOA外设,其初始化代码就为:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//此处ENABLE就是使能的意思
//RCC为系统时钟
假设我们使用的TIM2计时器外设,其初始化代码就为:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
GPIO的8种模式,4种数据传输速率,配置与开启:
GPIO的8种工作模式(4种输入,4种输出,3种数据传输速率):
4种输入:
GPIO_Mode_AIN = 0x0 模拟输入 GPIO无效,引脚直接接入内部ADC
GPIO_Mode_IN_FLOATING = 0x04 浮空输入 可读取引脚电平,若引脚悬空,则电平不确定
GPIO_Mode_IPD = 0x28 下拉输入 可读取引脚电平,内部连接上拉电阻,悬空时默认高电平
GPIO_Mode_IPU = 0x48 上拉输入 可读取引脚电平,内部连接下拉电阻,悬空时默认低电平
4种输出:
GPIO_Mode_Out_OD = 0x14 开漏输出 可输出引脚电平,高电平为高阻态,低电平接VSS
GPIO_Mode_Out_PP = 0x10 推挽输出 可输出引脚电平,高电平接VDD,低电平接VSS
GPIO_Mode_AF_OD = 0x1C 复用开漏输出 由片上外设控制,高电平为高阻态,低电平接VSS
GPIO_Mode_AF_PP = 0x18 复用推挽输出 由片上外设控制,高电平接VDD,低电平接VSS
3种数据传输速率:
GPIO_Speed_10MHz
GPIO_Speed_2MHz
GPIO_Speed_50MHz
GPIO口的配置:
在官方提供的开发手册中,是使用一个结构体对引脚进行初始化配置:
typedef struct
{
uint16_t GPIO_Pin; //引脚选择
GPIOSpeed_TypeDef GPIO_Speed; //数据传输速率选择
GPIOMode_TypeDef GPIO_Mode; //引脚工作模式选择
}GPIO_InitTypeDef; //官方提供的GPIO初始化类的定义名
假设我们使用的是引脚1,传输速率为2MHZ,模式为推挽输出,那么其初始化代码应为:
GPIO_InitTypeDef GPIO_IUse; //此处我使用的结构体名字GPIO_IUsee
GPIO_IUse.GPIO_Mode = GPIO_Mode_Out_PP; //将模式设置为推挽输出模式
GPIO_IUse.GPIO_Pin = GPIO_Pin_1; //将初始化引脚设置为我所使用的1号引脚
GPIO_IUSe.GPIO_Speed = GPIO_Speed_2MHz; //将数据传输缩率设置为2MHZ
注意:此处此处的GPIO_Speed一般为50MHz
GPIO的开启:
GPIO的初始化函数:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
//第一个参数为自己使用的外设模块编号
//第二个参数为我们自己定义的GPIO初始化结构体
假设我们使用的为GPIOA外设模块,结构体为我们刚GPIO_IUse结构体,所以其初始化代码为:
GPIO_Init(GPIOA,GPIO_IUse);
总结:
开启GPIO的总过程:
1.开启外设模块时钟
2.GPIO口初始化结构体的配置
3.GPIO口的初始化
总流程代码展示:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//外设模块时钟使能化
GPIO_InitTypeDef GPIO_INITled;//定义GPIO初始化结构体的名字
GPIO_INITled.GPIO_Mode = GPIO_Mode_Out_PP; //定义引脚的工作模式
GPIO_INITled.GPIO_Pin = GPIO_Pin_7;//定义所使用的引脚
GPIO_INITled.GPIO_Speed = GPIO_Speed_50MHz;//定义工作频率
GPIO_Init(GPIOB,&GPIO_INITled);//GPIO引脚开启
中断:
中断的概念:
中断就是指计算机在进行某一程序的运行时,由于本身硬件或者用户的操作而不得不放弃当前的任务1去执行另一个任务2,在完成任务2后再回来继续完成任务1。
举个例子:
假设刚开始我在一旁打游戏,突然我妈喊我去打扫卫生,由于害怕挨骂,所以我不得不放弃打游戏而去打扫卫生,在打扫完卫生之后,我又回去继续打游戏。
下图为相对应的流程图:
中断系统:
实现中断作用的软/硬件系统被称为中断系统。
中断优先级:
在单片机有多个中断源同时申请中断时,为了使单片机更好的反应中断,故人为设定了几种中断优先级,使CPU根据中断事物的缓急进行中断,使单片机优先响应更加紧急的中断源,更好的完成任务。
中断嵌套:
当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。
继续以刚才的例子打比方:
在我还没打扫完卫生时,我的女神突然给我发了消息(懂得都懂),这时我肯定停止打扫卫生,去和女神聊天了,跟女神聊完之后,再回来继续打扫卫生,打扫完卫生之后,再回来打游戏。
注意:
在STM32中,共有68个可屏蔽中断通道,包含EXTI,TIM,ADC等多个外设,且这些中断由NVIC统一管理。
此处NVIC寄存器在另一篇博客中讲解:NVIC寄存器
EXTI外部中断:
EXTI外部中断的基本结构(来自江协科技PPT):
EXTI如何工作:
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序(此处设计到上文提到的中断优先级和中断嵌套)
EXTI外部中断支持的触发方式(4种):
1.上升沿触发 引脚由低电平变为高电平时触发
2.下降沿触发 引脚由低电平变为高电平时触发
3.双边沿触发 引脚由低电平变为高电平时触发/由高电平变为低电平时也触发
4.软件触发
EXTI支持的GPIO口:
支持所有的GPIO口,但是相同的Pin不能同时触发中断
例如:
GPIOA_Pin_1 和 GPIOB_Pin_1就不能同时触发,因为GPIO的中断是以组为一个单位的,同组间的外部中断同一时间只能使用一个。
EXTI的响应触发方式(2种):
1.中断响应
2.事件响应
EXTI如何开启:
1.开启外设模块时钟和AFIO时钟。
2.配置与开启GPIO引脚。
3.初始化中断线程。
4.配置外部中断的优先级。
5.选择外部中断处理函数。
注意:
其中步骤二与先前所讲方法一样
步骤四讲解的链接:NVIC寄存器详解(基于STM32F103XX型号讲解)-CSDN博客
因此步骤二,四在此处就不多讲了。
步骤一:开启外设模块时钟。
在使用EXTI外设时,不仅要打开外设模块的时钟,同时也要打开AFIO时钟;
相应代码:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开外设模块时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //打开AFIO时钟
步骤三:初始化中断线程。
GPIO_EXTILineConfig()
函数:
GPIO_EXTILineConfig()
函数用于配置外部中断与特定GPIO引脚之间的映射关系。
假设此处我使用的GPIOA的0引脚,其初始化代码为:
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);
中断初始化结构体的内容以及其含义:
typedef struct
{
uint32_t EXTI_Line; //所使用的中断线
EXTIMode_TypeDef EXTI_Mode; //所使用的中断模式
EXTITrigger_TypeDef EXTI_Trigger; //所使用的中断触发方式
FunctionalState EXTI_LineCmd; //中断使能控制
}EXTI_InitTypeDef;
中断初始化结构体的配置过程:
EXTI_InitTypeDef EXTI_extiINIT; //定义EXTI初始化的名字为EXTI_extiINIT
EXTI_extiINIT.EXTI_Line = EXTI_Line0; //此时我使用0号中断线
EXTI_extiINIT.EXTI_Mode = EXTI_Mode_Interrupt; //此处我使用的模式为中断模式
EXTI_extiINIT.EXTI_Trigger = EXTI_Trigger_Rising; //此处我是用上升沿触发
/*
EXTI_Trigger_Falling 下降沿触发
EXTI_Trigger_Rising_Falling 双沿触发(上升沿和下降沿都可触发)
*/
EXTI_extiNIT.EXTI_LineCmd = ENABLE; //EXTI使能开启
EXTI_Init(&EXTI_extiINIT); //对EXTI进行初始化
步骤五:选择外部中断处理函数。
根据中断线进行选择:
在使用不同的GPIO口时,就用相应的IRQHandler。
注意:
在使用5-9引脚时,使用EXTI9_5_IRQHandler;
在使用10-15引脚时,使用EXTI15_10_IRQHandler;
EXTI外部中断总结过程以及代码:
1.开启外设模块时钟和AFIO时钟。
2.配置与开启GPIO引脚。
3.初始化中断线程。
4.配置外部中断的优先级(NVIC寄存器的选组)。
5.选择外部中断处理函数(NVIC寄存器的配置以及初始化)。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启外设模块时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO时钟
//对GPIO进行配置
GPIO_InitTypeDef GPIO_PBDINIT;
GPIO_PBDINIT.GPIO_Mode = GPIO_Mode_IPU;
GPIO_PBDINIT.GPIO_Pin = GPIO_Pin_0;
GPIO_PBDINIT.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_PBDINIT);
//初始化中断线程设计
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource8);
//EXTI外部中断初始化配置
EXTI_InitTypeDef EXTI_PBDINIT;
EXTI_PBDINIT.EXTI_Line = EXTI_Line8;
EXTI_PBDINIT.EXTI_LineCmd = ENABLE;
EXTI_PBDINIT.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_PBDINIT.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_Init(&EXTI_PBDINIT);
//配置外部中断的优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//NVIC寄存器的配置以及初始化
NVIC_InitTypeDef NVIC_PBDINIT;
NVIC_PBDINIT.NVIC_IRQChannel = EXTI9_5_IRQn;
NVIC_PBDINIT.NVIC_IRQChannelCmd = ENABLE;
NVIC_PBDINIT.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_PBDINIT.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_PBDINIT);