1. 引言
作为嵌入式入门新手,一般最开始的接触程序是如何点亮一个LED。而对于一些人来说,只需要单片机实现简单的控制功能,例如本人学习期间需要控制两个引脚交替输出。当时在网上下载了一个demo,但是这个程序定时部分很粗糙(就是烂大街的Delay函数),远远达不到个人预期,并且网上的呼吸灯程序千篇一律,并没有一个合适的指定频率的呼吸灯。虽然过去几年了,但是还是想写一篇很入门的文章,给那些也曾经有我这样的遭遇的人。
注:本文关于定时器部分只是采用了官方提供的API,并未涉及复杂寄存器配置,只针对GPIO管脚进行相关初始化,并且针对难以理解的地方给出详细的解释,并在文章末尾给出源码,代码简单易懂、易于修改,适合初学者阅读。
省流指示:本文使用官方定时器API实现了一个易于修改指定频率的呼吸灯。
2. 呼吸灯
2.1 环境搭建
关于环境搭建,请参考我的一篇文章【STM32F407】Note_01 STM32 编程环境搭建 -- Keil与VS code组合,个人比较喜欢 Keil + VS code组合开发项目。以下仅作为参考:
2.2 LED控制
本人所使用的是野火STM32F407开发板,因此需要找到该开发板电路原(《野火STM32F407_霸天虎V2开发板原理图20220219》)理图中LED相关电路。
可以看到,图中提供了4个LED,但是实际上只有两个LED(代号分别为D48和D6),其中下面三个LED合成一个RGB LED(D6)。四个LED右边连接PC3、PF6、PF7、PF8(即GPIOC8、GPIOF6、GPIOF7、GPIOF8)四个引脚,左侧连接3.3V电源。因此,如果我们需要是D48 LED量亮,则引脚PC3得输出低电平。
应注意到:如果要使用D6 LED,应使用短路帽插在J73上,是1、2两端导通。
有了以上理解,我们编程需要做的就是对我们所需要的引脚进行初始化即可。以D6 LED为例,若要使用这三个,则其初始化函数如下:
c
void LED_GPIO_Config(void)
{
//============================= 定义一个GPIO_InitTypeDef类型的结构体
GPIO_InitTypeDef GPIO_InitStructure;
//============================= 开启总线时钟,因为LED连接的管脚挂载在AHB1总线上,因此开启AHB1总线时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
//============================= 引脚设置
GPIO_InitStructure.GPIO_Pin = GreenLED_PIN; // 选择需要控制的引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 设置引脚模式为输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 设置引脚输出类型为推挽输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 设置引脚为上拉模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // 设置引脚速率为2MHz,不是越大越好,具体情况选择不同的速率,速率越大噪声越大
GPIO_Init(GreenLED_GPIO_PORT, &GPIO_InitStructure); // 初始化GPIO引脚结构体
// 初始化其他LED只需要修改响应的管脚和对应的PORT即可
// GPIO_InitStructure.GPIO_Pin = RedLED_PIN;
// GPIO_Init(RedLED_GPIO_PORT, &GPIO_InitStructure);
// GPIO_InitStructure.GPIO_Pin = BlueLED_PIN;
// GPIO_Init(BlueLED_GPIO_PORT, &GPIO_InitStructure);
GreenLED_OFF; // 上电默认关闭呼吸灯
}
配置要点:
- 开启相应引脚的时钟。因为引脚都是挂载在总线上,因此需要开启总线时钟。
- 定义通用输入输出接口结构体
GPIO_InitTypeDef
- 配置结构体的引脚号 、模式 (输入或输出,因为我们需要引脚输出低电平,所以使用输出模式)、输出类型(不同的输出类型会影响引脚不同状态是默认上拉还是默认下来)、上下拉(下拉、既不上拉也不下拉、上拉,上拉:推挽模式下,上拉指的是GPIO引脚默认输出高电平)、速度(一般设置2MHz,但是视具体情况,速度越高噪声相应越大)
- 最后初始化结构体
2.3 频率控制
2.3.1 定时器初始化
这里需要使用两个关键APISystemInit
和SysTick_Config
。
c
void SysClock_Init(void)
{
SystemInit(); // 初始化系统时钟,默认为168MHz
if (SysTick_Config(SystemCoreClock / 1000)) // 定时周期1ms
{
while (1) // 设置失败陷入循环
{
/* code */
}
}
}
SystemInit
初始化系统时钟,并将时钟频率设为169MHz,SysTick_Config(uint32_t ticks)
为系统嘀嗒定时器配置。SysTick_Config
形参ticks
表示系统嘀嗒计数器从设置的ticks
开始递减,当递减到0的时候产生中断。因为前面已经将系统设置为168MHz,即每秒钟系统执行168M个时钟,则1毫秒执行168M/1000个时钟。因此,我可通过系统嘀嗒计时器计数168M/1000个时钟即可产生一次中断,即1毫秒产生一次中断。注意宏定义SystemCoreClock
的值为168000000,即对应168MHz。
同时我们只需要另一个计时器,每次产生中断,对计时器更新即可。系统嘀嗒定时器的中断函数名是固定的(这个主要跟它的启动文件startup_stm32f40_41xxx.s
有关,不可随意更改,但是可以通过中断函数调用我们自己写得函数即可),他在stm32f4xx_it.c
中的void SysTick_Handler(void)
。
对于很多初学者来说,最大的困惑是系统怎么跳转到这个中断函数SysTick_Handler
的。目前,对于刚入门的盆友来说,只需要知道,系统产生中断,系统会通过中断向量表自动跳转到该中断函数SysTick_Handler
,然后执行中断函数,后面当你慢慢深入了解了中断系统。
2.3.2 周期设置
通过设定不同的周期,实现指定频率的呼吸灯。上一小节我们设置了一个计数器,没产生一个中断就计数+1,其物理意义也是当前已经经过了几个ms。当我们设置指定周期之后,当计数器对周期进行取余,余数为0,即可对LED进行反反转。
c
void Ticker_Handle(void)
{
if (isStartTick)
{
Ticker = (Ticker++) % 0xFFFF;
if (Ticker % Period == 0)
{
GreenLED_TOGGLE;
}
}
}
2.4 详细代码
BreathingLED.h
c
#ifndef _BREATHINGLED_H_
#define _BREATHINGLED_H_
#include "stm32f4xx.h"
// 引脚定义
/*******************************************************/
// R 红色灯
#define RedLED_PIN GPIO_Pin_6
#define RedLED_GPIO_PORT GPIOF
#define RedLED_GPIO_CLK RCC_AHB1Periph_GPIOF
#define RedLED_ON GPIO_SetBits(RedLED_GPIO_PORT, RedLED_PIN)
#define RedLED_OFF GPIO_ResetBits(RedLED_GPIO_PORT, RedLED_PIN)
#define RedLED_TOGGLE GPIO_ToggleBits(RedLED_GPIO_PORT, RedLED_PIN)
// G 绿色灯
#define GreenLED_PIN GPIO_Pin_7
#define GreenLED_GPIO_PORT GPIOF
#define GreenLED_GPIO_CLK RCC_AHB1Periph_GPIOF
#define GreenLED_ON GPIO_ResetBits(GreenLED_GPIO_PORT, GreenLED_PIN)
#define GreenLED_OFF GPIO_SetBits(GreenLED_GPIO_PORT, GreenLED_PIN)
#define GreenLED_TOGGLE GPIO_ToggleBits(GreenLED_GPIO_PORT, GreenLED_PIN)
// B 蓝色灯
#define BlueLED_PIN GPIO_Pin_8
#define BlueLED_GPIO_PORT GPIOF
#define BlueLED_GPIO_CLK RCC_AHB1Periph_GPIOF
#define BlueLED_ON GPIO_ResetBits(BlueLED_GPIO_PORT, BlueLED_PIN)
#define BlueLED_OFF GPIO_SetBits(BlueLED_GPIO_PORT, BlueLED_PIN)
#define BlueLED_TOGGLE GPIO_ToggleBits(BlueLED_GPIO_PORT, BlueLED_PIN)
/************************************************************/
extern u8 isStartTick; // 开始计数
extern u16 Ticker; // 嘀嗒计数器
extern u16 Period; // 周期
void Set_Freq(u16 t);
void BreathingLED_Init(void);
void LED_GPIO_Config(void);
void SysClock_Init(void);
void Ticker_Handle(void);
#endif // _BREATHINGLED_H_
BreathingLED.c
c
#include "BreathingLED.h"
u8 isStartTick; // 开始计数
u16 Ticker; // 嘀嗒计数器
u16 Period; // 周期
/**
* @brief 初始化控制LED的IO
* @param 无
* @retval 无
*/
void BreathingLED_Init(void)
{
Ticker = 0;
Period = 500; // 默认2Hz,周期500ms
isStartTick = 0;
SysClock_Init();
LED_GPIO_Config();
}
/**
* @brief LED管脚初始化
*
*/
void LED_GPIO_Config(void)
{
//============================= 定义一个GPIO_InitTypeDef类型的结构体
GPIO_InitTypeDef GPIO_InitStructure;
//============================= 开启总线时钟,因为LED连接的管脚挂载在AHB1总线上,因此开启AHB1总线时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
//============================= 引脚设置
GPIO_InitStructure.GPIO_Pin = GreenLED_PIN; // 选择需要控制的引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 设置引脚模式为输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 设置引脚输出类型为推挽输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 设置引脚为上拉模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // 设置引脚速率为2MHz,不是越大越好,具体情况选择不同的速率,速率越大噪声越大
GPIO_Init(GreenLED_GPIO_PORT, &GPIO_InitStructure); // 初始化GPIO引脚结构体
// 初始化其他LED只需要修改响应的管脚和对应的PORT即可
// GPIO_InitStructure.GPIO_Pin = RedLED_PIN;
// GPIO_Init(RedLED_GPIO_PORT, &GPIO_InitStructure);
// GPIO_InitStructure.GPIO_Pin = BlueLED_PIN;
// GPIO_Init(BlueLED_GPIO_PORT, &GPIO_InitStructure);
GreenLED_OFF; // 上电默认关闭呼吸灯
}
/**
* @brief 系统时钟初始化
*
*/
void SysClock_Init(void)
{
SystemInit(); // 初始化系统时钟,默认为168MHz
if (SysTick_Config(SystemCoreClock / 1000)) // 定时周期1ms
{
while (1) // 设置失败陷入循环
{
/* code */
}
}
}
/**
* @brief 设置呼吸灯频率
*
* @param t : 周期,单位ms,范围≥1ms
*
*
* @retval None
*/
void Set_Freq(u16 t)
{
Period = t;
isStartTick = 1;
}
void Ticker_Handle(void)
{
if (isStartTick)
{
Ticker = (Ticker++) % 0xFFFF;
if (Ticker % Period == 0)
{
GreenLED_TOGGLE;
}
}
}
stm32f4xx_it.c
c
/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx_it.h"
#include "main.h"
/** @addtogroup Template_Project
* @{
*/
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*/
/* Private functions ---------------------------------------------------------*/
/******************************************************************************/
/* Cortex-M4 Processor Exceptions Handlers */
/******************************************************************************/
/**
* @brief This function handles NMI exception.
* @param None
* @retval None
*/
void NMI_Handler(void)
{
}
/**
* @brief This function handles Hard Fault exception.
* @param None
* @retval None
*/
void HardFault_Handler(void)
{
/* Go to infinite loop when Hard Fault exception occurs */
while (1)
{
}
}
/**
* @brief This function handles Memory Manage exception.
* @param None
* @retval None
*/
void MemManage_Handler(void)
{
/* Go to infinite loop when Memory Manage exception occurs */
while (1)
{
}
}
/**
* @brief This function handles Bus Fault exception.
* @param None
* @retval None
*/
void BusFault_Handler(void)
{
/* Go to infinite loop when Bus Fault exception occurs */
while (1)
{
}
}
/**
* @brief This function handles Usage Fault exception.
* @param None
* @retval None
*/
void UsageFault_Handler(void)
{
/* Go to infinite loop when Usage Fault exception occurs */
while (1)
{
}
}
/**
* @brief This function handles SVCall exception.
* @param None
* @retval None
*/
void SVC_Handler(void)
{
}
/**
* @brief This function handles Debug Monitor exception.
* @param None
* @retval None
*/
void DebugMon_Handler(void)
{
}
/**
* @brief This function handles PendSVC exception.
* @param None
* @retval None
*/
void PendSV_Handler(void)
{
}
/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
Ticker_Handle();
// TimingDelay_Decrement();
}
main.c
c
/**
*
* @name 呼吸灯
*
* @brief 实现指定频率的呼吸灯
*
* 其他:\
*
*/
#include "main.h"
/**
* @brief 主函数
* @details 呼吸灯的实现主要采用嘀嗒定时器实现指定频率的呼吸
* @param 无
* @retval 无
*/
int main(void)
{
u16 T = 500; // 周期,单位ms, T ≥ 1ms
BreathingLED_Init(); // 呼吸灯初始化
Set_Freq(T); // 这是周期500ms,即2Hz
while (1) // 进入循环,每次嘀嗒定时器请求中断,对呼吸灯进行反转
{
}
}
3 总结
- 希望对刚入门的盆友有一点点帮助。
- 本文写得比较粗糙,如有不对的地方,欢迎在评论区指正。