基于STM32标准库的PWM呼吸灯
文章目录
一、STM32定时功能介绍
TIM(Timer)定时器是一种用于计数和定时的高精度硬件设备。它内部包含16位计数器、预分频器和自动重装寄存器的时基单元,可以实现对输入时钟的精确计数。当计数值达到设定值时,定时器会触发中断,以便执行相应的操作。这种定时器具有高精度、长定时时间的优点,因此在许多嵌入式系统中得到广泛应用。
根据复杂度和应用场景的不同,TIM定时器分为高级定时器、通用定时器和基本定时器三种类型。
高级定时器具有最高的复杂度,它不仅具备基本的定时中断功能,还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。这种定时器适用于需要对精度和复杂度要求较高的应用场景,例如高精度计时和复杂的定时控制。
通用定时器则适用于大多数应用场景。它具备基本的定时中断功能,同时也可以根据需要选择使用其他一些功能。这种定时器的复杂度适中,可以满足大多数应用的需求。
基本定时器是最简单的一种,它只具备基本的定时中断功能。这种定时器的复杂度最低,因此适用于一些简单应用场景,例如时间戳记录等。
在STM32中,时钟主要分为内部时钟模式和外部时钟模式以及编码器模式。内部模式使用的时钟频率为72MHz,外部时钟模式的频率由外设的时钟频率来决定。
在本次实验中,主要用到的定时器为通用定时器中的输出比较功能,以及内部时钟模式。
二、PWM功能介绍
PWM(Pulse Width Modulation)脉冲宽度调制是一种在电子和电机控制领域广泛应用的控制技术。通过调制一系列脉冲的宽度,PWM可以等效地获得所需的模拟参量,常应用于电机控速等领域。
PWM信号的参数包括频率、占空比和分辨率。频率是指每秒产生的脉冲数,通常用赫兹(Hz)表示。占空比是指一个脉冲周期中高电平所占的时间比例,用百分比表示。分辨率是指占空比变化的最小步距,用百分比表示。
通用定时器的输出比较通道如下所示:
输出比较的模式如下所示:
我们一般采用PWM模式1的向上计数模式。
PWM基本结构以及参数计算如下:
电平进入后,首先进行预分频,再通过ARR自动重装与CNT计数器进行时钟化,得到的时钟信号进入输出比较单元比较CNT与CCR的关系得到电平变化,最后通过极性选择输出到GPIO。
PWM相关参数如下:
PWM频率控制输出电平的快慢;占空比指的是信号高电平的时间占总时钟周期的大小。PWM分辨率指的是电平步进的快慢程度。在波形达到99后自动触发中断归0,这是向上计数的原理。
三、创建工程
创建基本的keil工程请参照这篇博客
基于STM32标准库函数的LED流水灯实验_Constellation_zZ的博客-CSDN博客
在原有基础工程上,我们在左边文件栏“Hardware"添加一个c文件,和一个头文件,统一命名为”PWM"。注意添加时把路径加上
我们在“PWM.h”中添加基本的函数语句
#ifndef __PWM_H
#define __PWM_H
#include "stm32f10x.h" // Device header
#endif
这样,基本的工程就创建好了。
四、LED规律性闪灭实现
4.1 代码思路
通过配置定时中断,每1s产生一次中断。定义GPIOA0口作为端口插上LED灯。在中断函数中写入电平变化。主函数只需要一个while(1){ }就可以了
4.2 配置代码
在“PWM.c"中写入以下代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
extern uint16_t Num;
void Timer_Init(void)//定时产生中断配置
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//APB1使能。
TIM_InternalClockConfig(TIM2);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//配置结构体
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//指定时钟分频,选择不分频
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//选择计数模式,向上计数
TIM_TimeBaseInitStruct.TIM_Period=10000-1;//周期,ARR自动重装的值
TIM_TimeBaseInitStruct.TIM_Prescaler= 7200-1;//PSC预分频器的值
TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器的值,高级定时器才会使用
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//使能更新中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//开启更新中断到NVIC
//NVIC配置
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//选择分组2
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel= TIM2_IRQn;//定时器2在NVIC中的通道
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
//启动定时器
TIM_Cmd(TIM2,ENABLE);
}
//uint16_t Timer_GetCounter(void)
//{
// return TIM_GetCounter(TIM2);
//
//
//
//}
void TIM2_IRQHandler(void)//定时器2中断函数
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)//TIM2判断标志位
{
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
Delay_ms(1000);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
主函数中写出基本配置就好了
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Timer.h"
#include "LED.h"
#include "Delay.h"
uint16_t Num;
int main(void)
{
Timer_Init();//配置函数
while (1)
{
}
}
4.3 烧录结果
烧录结果如下:
五、PWM配置函数解析
注意:以下操作都在PWM.c文件中执行。
5.1 内置时钟使能
在STM32中,控制TIM2使能的是APB1,且需要控制TIM2为内部时钟模式,配置的相关函数为
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//APB1使能。
TIM_InternalClockConfig(TIM2);//TIM2认定内部时钟模式
我们直接这样写入就可以了
5.2 配置时基单元
配置时基单元有点类似于GPIO,也是需要定义一个结构体变量,再将其结构体成员引出来分别进行配置。声明结构体的语句为
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//配置结构体
我们将结构体成员都引出来,逐个配置
根据公式,时钟频率为72MHZ/(PSC+1)/(ARR+1),经过计算,当PSC设置为719,ARR设置为99,时钟频率为1000HZ,也就是0.001s,也就是说,每个电平存在时间为0.001s
//配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//配置结构体
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//指定时钟分频,选择不分频
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//选择计数模式,向上计数
TIM_TimeBaseInitStruct.TIM_Period=100-1;//周期,ARR自动重装的值
TIM_TimeBaseInitStruct.TIM_Prescaler= 720-1;//PSC预分频器的值
TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器的值,高级定时器才会使用
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
这样就配置好了。
5.3 使能更新中断
我们需要配置使能中断,设置为输出比较模式,这样才能将已激活的时钟信号进行输出比较从而得到PWM波形。
输出比较模式的配置有专门的标准库函数,也是结构体类型,我们一个个了解并将其配置。输出比较结构体定义为
TIM_OCInitTypeDef TIM_OCInitStruct;//输出比较结构体定义
使能中断的代码如下:
//使能更新中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//开启更新中断到NVIC
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);//结构体赋初始值,避免没有出现的成员未编译而报错
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1;//输出比较模式,选择PWM1模式
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;//输出极性,高电平有效
TIM_OCInitStruct.TIM_OutputState= TIM_OutputState_Enable;//设置输出使能
TIM_OCInitStruct.TIM_Pulse=0 ;//设置CCR
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
5.4 配置GPIO并启动定时器
此外,由于需要使用LED灯,所以还需要加上GPIO的配置函数。此外,TIM对应GPIO引脚需要查找定义表,TIM2_CH1_ETR引脚复用在PA0上,故我们配置PA0。
//配置GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//改为复用推挽输出,定时器控制引脚输出电平
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_0);
//启动定时器
TIM_Cmd(TIM2,ENABLE);
5.5 输出比较函数
根据前面的公式我们知道,PWM的占空比的公式为CCR/ARR+1。前面我们设定为ARR为99,故我们可以设定CCR在0-100波动,这样算出的占空比就是0-100%了,具体结果会在主函数里展示。我们现在只是定义了一个输出比较函数
在STM32标准库里,系统给出了设置CCR函数为
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1)
我们可以通过这个函数来设定一个不断变化的CCR值。
在这里我们写入以下代码
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare);//设置CCR寄存器的值,可以随时改变CCR的值
}
5.6 头文件以及C文件最终函数
头文件:
#ifndef __PWM_H
#define __PWM_H
#include "stm32f10x.h" // Device header
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
#endif
C文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//APB1使能。
TIM_InternalClockConfig(TIM2);//TIM2认定内部时钟模式
//配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//配置结构体
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//指定时钟分频,选择不分频
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//选择计数模式,向上计数
TIM_TimeBaseInitStruct.TIM_Period=100-1;//周期,ARR自动重装的值
TIM_TimeBaseInitStruct.TIM_Prescaler= 720-1;//PSC预分频器的值
TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器的值,高级定时器才会使用
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//使能更新中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//开启更新中断到NVIC
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);//结构体赋初始值,避免没有出现的成员未编译而报错
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1;//输出比较模式,选择PWM1模式
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;//输出极性,高电平有效
TIM_OCInitStruct.TIM_OutputState= TIM_OutputState_Enable;//设置输出使能
TIM_OCInitStruct.TIM_Pulse=0 ;//设置CCR
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
//TIM对应GPIO引脚需要查找定义表,TIM2_CH1_ETR引脚复用在PA0上
//配置GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//改为复用推挽输出,定时器控制引脚输出电平
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_0);
//启动定时器
TIM_Cmd(TIM2,ENABLE);
}
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare);//设置CCR寄存器的值,可以随时改变CCR的值
}
六、PWM主函数编写解析
6.1 代码思路
在配置函数里,我们写到过CCR设置函数。那么我们可以利用一个for循环,定义一个变量作为CCR,不断改变该变量的值来实现占空比的不断变化,占空比越大,输出信号的幅值越大;占空比越小,输出信号的幅值也越小,那么我们就使得最后输出的电平幅值也在不断地变化,得到了一个电平周期变化的模拟信号,从而得到呼吸灯的效果。
6.2 主函数代码
在for循环中,不断调用设置CCR的函数,不断改变CCR从而实现呼吸灯
最终代码如下:
#include "stm32f10x.h" // Device header
#include "Delay.h"
//#include "OLED.h"
#include "PWM.h"
#include "LED.h"
#include "Delay.h"
uint16_t Num;
int main(void)
{
// OLED_Init();
PWM_Init();//配置函数
// OLED_ShowString(1,1,"a:");
// OLED_ShowString(2,1,"b:");
while (1)
{//在for循环中,不断调用设置CCR的函数实现呼吸灯
for(Num=0;Num<=100;Num++)//幅值不断增大
{
PWM_SetCompare1(Num);
Delay_ms(10);//通过延时函数可改变闪动的周期。
// OLED_ShowNum(1,6,Num,3);
}
for(Num=0;Num<=100;Num++)//幅值不断减小
{
PWM_SetCompare1(100-Num);
Delay_ms(10);
// OLED_ShowNum(2,6,100-Num,3);
}
}
}
七、烧录结果以及波形观察
在PA0端口插上一个LED灯,其他跟之前的实验一样。
烧录结果如下:
打开魔术棒,设置如下:
打开keil的调试界面,点击逻辑分析仪
在界面点击”SET UP“
在弹出界面中设置以下(上面栏里填入”PORTA.0)。
点击”close“,然后点击左边全速运行程序
得到结果如下:
八、总结
通过这次实验,我对STM32的定时中断功能有了更深入的理解,明白了不同基本定时中断模式的运作原理,也探索了通用定时器的输出比较功能。通过精心设计频率、占空比以及分辨率等参数,我成功地模拟出了类模拟信号,并借助PA0引脚的复用输出功能,将信号输出到端口,最终实现了LED呼吸灯的展示效果。
尽管使用标准库的操作可能显得繁琐,但其中的逻辑步骤却十分清晰。这次实验不仅提升了我编程的能力,也增强了我检查和避免错误的能力。我得以更深入地理解如何通过编程实现对硬件的控制,以及如何细致入微地调整参数以达到预期的效果。