当前位置: 首页 > web >正文

普中STM32F103ZET6开发攻略(五)

接续上文:普中STM32F103ZET6开发攻略(四)-CSDN博客

点关注不迷路哟。你的点赞、收藏,一键三连,是我持续更新的动力哟!!!

目录

5. 定时器实验——PWM呼吸灯

5.1 实验目的

5.2 实验原理

5.3 实验环境

5.4 实验思路

5.5 实验代码

5.5.1 delay头、源

🔍 它们的区别:

✅ 一、重点解释 |=

📌 |= 是按位或赋值运算符,它的意思是:“将右侧的位按位或后赋值给左侧变量”。

🚀 具体解释这句作用:

✅ 二、对比两个延时函数的差异

5.5.2 pwm头、源

5.5.3 led头、源

5.6 实验思考和拓展

5.6.1 如何修改PWM参数,实现不同的呼吸频率?

5.6.2 如何实现多个LED的同步或交替呼吸效果?

5.6.3 PWM频率对LED呼吸效果有何影响?如何选择合适的PWM频率?

5.6.4 如何结合按键控制,实现多种呼吸模式的切换?

5.6.5 相比于使用延时函数控制LED亮度变化,

使用定时器中断方式有什么优势?

5.7 注意事项


5. 定时器实验——PWM呼吸灯

5.1 实验目的

  1. 熟悉STM32F10x微控制器的PWM输出功能和基本操作

  1. 掌握STM32标准库函数对定时器PWM模式配置方法

  1. 学会使用PWM技术控制LED亮度,实现呼吸灯效果

  1. 理解PWM频率占空比的计算与设置方法

5.2 实验原理

1.PWM基本原理

PWM(脉宽调制)是一种对模拟信号电平进行数字编码的方法。通过调制PWM波的占空比,可以控制输出电压的平均值,从而控制LED的亮度。PWM信号的占空比是指在一个周期内,高电平持续时间与总周期时间的比值。

2.通用定时器PWM功能简介

STM32F1的通用定时器TIMx (TIM2-TIM5)具有PWM输出功能,高级定时器 TIM1 和 TIM8 可以同时产生多达7路的PWM输出:

(1)每个通道可独立配置PWM模式

(3)可设置PWM频率与占空比

(4)PWM模式1:当计数值小于比较值时,通道输出有效电平

(5)PWM模式2:当计数值小于比较值时,通道输出无效电平

表1 PWM模式

PWM 模式根据计数器 CNT 计数方式,可分为边沿对齐模式中心对齐模式

(1)PWM 边沿对齐模式 当 TIMx_CR1 寄存器中的 DIR 位为低时执行递增计数,计数器 CNT 从 0 计 数到自动重载值(TIMx_ARR 寄存器的内容),然后重新从 0 开始计数并生成计 数器上溢事件。

(2)PWM 中心对齐模式 在中心对齐模式下,计数器 CNT 是工作在递增/递减模式下。开始的时候, 计数器 CNT 从 0 开始计数到自动重载值减 1(ARR-1),生成计数器上溢事件; 然后从自动重载值开始向下计数到 1 并生成计数器下溢事件。之后从 0 开始重 新计数。

中心对齐模式又分为中心对齐模式 1/2/3 三种,具体由寄存器 CR1 位 CMS[1:0]配置。具体的区别就是比较中断标志位 CCxIF 在何时置 1:中心模式 1 在 CNT 递减计数的时候置 1,中心对齐模式 2 在 CNT 递增计数时置 1,中心 模式 3 在CNT递增和递减计数时都置为1。

3.PWM参数设置与计算

(1)PWM频率计算公式:

(2)PWM占空比计算公式:

占空比

其中,Tclk为定时器时钟频率,ARR为自动重装载值,PSC为预分频系数,CCRx为比较寄存器值。

4.呼吸灯原理

呼吸灯效果是通过逐渐增加和减小PWM信号的占空比,使LED亮度逐渐增加和减小,形成类似呼吸的视觉效果。通过控制占空比变化的速度,可以调整"呼吸"的频率。

表2 TIM3复用功能重映像

5.3 实验环境

  • 开发板:STM32F103ZET6

  • IDE:Keil MDK 5 /Visual Studio

  • 调试工具:CMSIS-DAP

5.4 实验思路

硬件连接与外设准备

  • 选择一个支持 PWM 输出的通用定时器通道(如 TIM3_CH1);

  • 将该通道对应的 GPIO(如 PA6)配置为复用推挽输出

  • 将 LED 灯连接到该 GPIO 端口,通过其电平变化调节亮度。

GPIO 和定时器初始化

  • 使用标准库函数初始化 GPIO 引脚为复用功能;

  • 配置定时器 TIMx:

    • 设置计数模式为向上计数;

    • 配置预分频器 PSC 和自动重装载值 ARR,设定 PWM 输出频率;

    • 启用 PWM 模式(模式1或模式2);

    • 设置 CCR 寄存器初值,控制初始亮度;

    • 启动定时器并使能 PWM 输出通道。

实现呼吸灯效果逻辑

  • 在主循环或定时中断中周期性修改 CCRx 值,改变占空比;

  • 通过逐步增加 CCRx(LED变亮)和逐步减少 CCRx(LED变暗)模拟“呼吸”效果;

  • 可设置方向标志(例如 dir = 1 表示亮度增加,dir = 0 表示亮度减小)控制 PWM 占空比自动反转;

  • 设置一定的延时以控制“呼吸”频率平滑。

调试与效果观察

  • 下载程序后观察 LED 亮度是否平滑变化;

  • 调整 ARR、PSC、占空比步长和延时时间,以达到理想的“呼吸”频率和视觉效果;

  • 可扩展:尝试不同的定时器通道或多个 LED 实现多色呼吸灯。

5.5 实验代码

5.5.1 delay头、源

delay.h

#ifndef __DELAY_H
#define __DELAY_H
​
#include "stm32f10x.h"
​
void Delay_Init(void);
void Delay_ms(u32 nms);
​
#endif
​

delay.c

#include "delay.h"
​
static u32 fac_ms = 0;
​
void Delay_Init(void)
{SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); // HCLK/8fac_ms = SystemCoreClock / 8000;
}
​
void Delay_ms(u32 nms)
{u32 temp;SysTick->LOAD = nms * fac_ms;SysTick->VAL = 0x00;SysTick->CTRL = 0x01;do{temp = SysTick->CTRL;} while ((temp & 0x01) && !(temp & (1 << 16)));SysTick->CTRL = 0x00;SysTick->VAL = 0X00;
}
​
🔍 它们的区别
对比点delay.h 实现SysTick.h 实现
时钟源HCLK/8HCLK(通常)
初始化方式Delay_Init()SysTick_Init(72)
延时精度只有 ms 延时支持 msus 延时
使用寄存器直接操作 SysTick 寄存器也是用 SysTick,但逻辑可能更复杂
精度 & 速度相对较慢一些(因为频率被除以8)精度高,速度快
推荐程度简单项目足够推荐用于需要更高精度的场景

注意:

void Delay_ms(u32 nms)
{u32 temp;SysTick->LOAD = nms * fac_ms;SysTick->VAL = 0x00;SysTick->CTRL = 0x01;do{temp = SysTick->CTRL;} while ((temp & 0x01) && !(temp & (1 << 16)));SysTick->CTRL = 0x00;SysTick->VAL = 0X00;
}
void delay_ms(u16 nms)
{                 u32 temp;          SysTick->LOAD=(u32)nms*fac_ms;              //时间加载(SysTick->LOAD为24bit)SysTick->VAL =0x00;                         //清空计数器SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;    //开始倒数  do{temp=SysTick->CTRL;}while((temp&0x01)&&!(temp&(1<<16)));       //等待时间到达   SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;    //关闭计数器SysTick->VAL =0X00;                         //清空计数器         
} 
​
对比以上两个函数写法以及着重解释:SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;   //开始倒数  
|=这是什么意思?
​

[​以上两个函数解释]  以上两个函数都是实现毫秒级延时的,基于STM32的SysTick定时器实现的。

✅ 一、重点解释 |=

这句:

c
​
​
复制代码
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;

可以拆解为:

c
​
​
复制代码
SysTick->CTRL = SysTick->CTRL | SysTick_CTRL_ENABLE_Msk;
📌 |=按位或赋值运算符,它的意思是:“将右侧的位按位或后赋值给左侧变量”

🚀 具体解释这句作用:

假设当前 SysTick->CTRL = 0x00000002(比如只使能了中断),而 SysTick_CTRL_ENABLE_Msk = 0x00000001(表示开启计数器),

执行后结果:

c
​
​
复制代码
SysTick->CTRL = 0x00000002 | 0x00000001 = 0x00000003

即:原有配置不变,只新增使能计数器的那一位。


✅ 二、对比两个延时函数的差异
项目Delay_ms(你的)delay_ms(SysTick.h中)
CTRL 写法SysTick->CTRL = 0x01; 直接赋值`SysTick->CTRL
CTRL 清除方式SysTick->CTRL = 0x00; 全部清除SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; 只关闭使能位
延时单位u32 可延时更长u16 只适合较短延时(约65秒)
灵活性不保留中断配置等其他功能,简单粗暴保留其他控制位(如中断等)更安全

5.5.2 pwm头、源

pwm.h

#ifndef _pwm_H
#define _pwm_H
​
#include "system.h"
​
void TIM3_CH2_PWM_Init(u16 per,u16 psc);     // PWM初始化函数
void PWM_SetDuty(uint8_t duty);            // 设置占空比函数
​
#endif
​

pwm.c

#include "pwm.h"
​
void TIM3_CH2_PWM_Init(u16 per, u16 psc)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;// 定义定时器基本初始化结构体,用于配置定时器的核心参数TIM_OCInitTypeDef TIM_OCInitStructure;            // 定义定时器输出比较初始化结构体,用于配置PWM输出或电平比较模式GPIO_InitTypeDef GPIO_InitStructure;              // 定义GPIO初始化结构体,用于配置引脚的工作模式、速率和上下拉等参数
​/* 开启时钟 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);  // 使能GPIOB和AFIO时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);                          // 使能TIM3时钟
​/* 配置PB5为复用推挽输出模式 */GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;                              // 复用推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);
​/* 配置TIM3的部分重映射,使TIM3_CH2映射到PB5 */GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);
​/* 初始化定时器基本参数 */TIM_TimeBaseInitStructure.TIM_Period = per;                                   // 自动重装载值TIM_TimeBaseInitStructure.TIM_Prescaler = psc;                                // 预分频器TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;                   // 时钟分频TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;              // 向上计数模式TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
​/* 配置PWM模式 */TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;                             // PWM模式1TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;                // 输出使能TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;                     // 输出极性低有效TIM_OCInitStructure.TIM_Pulse = 0;                                            // 初始CCR值TIM_OC2Init(TIM3, &TIM_OCInitStructure);                                      // 初始化TIM3通道2
​/* 使能预装载 */TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);                             // CCR寄存器预装载使能TIM_ARRPreloadConfig(TIM3, ENABLE);                                           // 自动重装载寄存器预装载使能
​/* 使能定时器 */TIM_Cmd(TIM3, ENABLE);                                                        // 使能TIM3
}
​
void PWM_SetDuty(uint8_t duty)
{uint16_t compareValue;
​/* 限制占空比范围在0-100之间 */if (duty > 100) duty = 100;  // 超过100时强制设置为100
​/* 将占空比值(0-100)转换为比较值(0-500) */compareValue = (uint16_t)((duty * 500) / 100);  // 根据公式计算CCR值
​/* 设置PWM比较值 */TIM_SetCompare2(TIM3, compareValue);  // 设置TIM3通道2比较寄存器的值
}
​

5.5.3 led头、源

led.h

#ifndef __LED_H
#define __LED_H
​
#include "stm32f10x.h"
​
#define LED0_GPIO_PORT GPIOB
#define LED0_GPIO_PIN  GPIO_Pin_5 //LED0
​
#define LED1_GPIO_PORT GPIOE
#define LED1_GPIO_PIN  GPIO_Pin_5 //LED1
​
void LED_Init(void);
void LED0_TOGGLE(void);
void LED1_ON(void);
void LED1_OFF(void);
​
#endif
​

led.c

#include "led.h"
​
void LED_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;// 开启 GPIOB 和 GPIOE 的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOE, ENABLE);
​//配置PB5(LED0)GPIO_InitStructure.GPIO_Pin = LED0_GPIO_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(LED0_GPIO_PORT, &GPIO_InitStructure);
​//配置PE5(LED1)GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure);
​//低电平输出GPIO_ResetBits(LED0_GPIO_PORT, LED0_GPIO_PIN);GPIO_ResetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
}
​
void LED0_TOGGLE(void)
{LED0_GPIO_PORT->ODR ^= LED0_GPIO_PIN;
}
​
void LED1_ON(void)
{GPIO_ResetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
}
​
void LED1_OFF(void)
{GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
}
​

5.6 实验思考和拓展

5.6.1 如何修改PWM参数,实现不同的呼吸频率?

关键参数:呼吸频率由 PWM 占空比变化的速率决定,与定时器更新中断频率相关。

(1)PWM频率计算公式:

(2)PWM占空比计算公式:

占空比

修改方法

  1. 调整定时器更新频率:通过修改 PSC(预分频器)和 ARR(自动重装载值)改变中断周期。

    // 增大ARR或PSC会降低更新频率,减缓呼吸速度
    TIM_TimeBaseStructure.TIM_Period = 999;      // ARR值增大,呼吸变慢
    TIM_TimeBaseStructure.TIM_Prescaler = 7199;  // PSC值增大,呼吸变慢
  2. 改变占空比调整步长:在中断服务函数中,每次更新占空比时调整步长值。

    // 步长越大,亮度变化越快,呼吸频率越高
    static uint16_t pulse = 0;
    static int8_t step = 5;  // 增大步长(如从2→5)可加快呼吸
    ​
    void TIM3_IRQHandler(void) {if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {pulse += step;if (pulse >= 1000 || pulse <= 0) step = -step;TIM_SetCompare1(TIM3, pulse);  // 更新PWM占空比TIM_ClearITPendingBit(TIM3, TIM_IT_Update);}
    }

5.6.2 如何实现多个LED的同步或交替呼吸效果?

同步呼吸

使用同一个定时器的不同通道控制多个 LED,共享占空比变化。

// 配置TIM3通道1和通道2分别控制LED1和LED2
TIM_OC1Init(TIM3, &TIM_OCInitStructure);  // 通道1
TIM_OC2Init(TIM3, &TIM_OCInitStructure);  // 通道2
​
// 中断服务函数中同时更新两个通道的占空比
void TIM3_IRQHandler(void) {TIM_SetCompare1(TIM3, pulse);  // LED1TIM_SetCompare2(TIM3, pulse);  // LED2
}

交替呼吸

  • 使用两个定时器,配置相同频率但相位差 180°;或在中断中控制占空比相位。

    // 方法1:使用两个定时器,延时启动
    TIM2_Int_Init(999, 71);  // 控制LED1
    HAL_Delay(500);          // 延迟0.5秒
    TIM3_Int_Init(999, 71);  // 控制LED2,与LED1相位差180°
    ​
    // 方法2:单定时器控制,占空比互补
    void TIM3_IRQHandler(void) {TIM_SetCompare1(TIM3, pulse);        // LED1TIM_SetCompare2(TIM3, 1000 - pulse);  // LED2(与LED1互补)
    }

5.6.3 PWM频率对LED呼吸效果有何影响?如何选择合适的PWM频率?

影响

  • 频率过低(如 < 100Hz):人眼会感知到 LED 闪烁,呼吸效果不流畅。

  • 频率过高(如 > 20kHz):虽然消除了闪烁,但可能增加功耗,且某些 LED 驱动电路对高频响应不佳。

选择原则

  • 可见光谱范围:推荐 PWM 频率在 100Hz~20kHz 之间,避免人眼感知闪烁。

  • LED 类型:普通 LED 建议 1kHz~5kHz;对频率敏感的 LED(如 RGB 混光)需更高频率(10kHz+)。

  • 功耗优化:在满足视觉效果的前提下,尽量降低频率以减少开关损耗。

    // 配置PWM频率为1kHz(适合大多数LED)
    TIM_TimeBaseStructure.TIM_Period = 999;       // ARR
    TIM_TimeBaseStructure.TIM_Prescaler = 71;     // PSC
    // 实际频率 = 72MHz / (72 × 1000) = 1kHz
5.6.4 如何结合按键控制,实现多种呼吸模式的切换?

实现步骤

  1. 定义多种呼吸模式:如慢呼吸、快呼吸、交替呼吸等。

  2. 按键检测:使用外部中断或轮询方式检测按键状态。

  3. 模式切换:在按键触发时切换模式标志,并更新 PWM 配置。

示例代码:

typedef enum {MODE_SLOW,    // 慢呼吸MODE_FAST,    // 快呼吸MODE_ALTERNATE // 交替呼吸
} BreatheMode;
​
static BreatheMode currentMode = MODE_SLOW;
​
// 按键中断服务函数
void EXTI0_IRQHandler(void) {if (EXTI_GetITStatus(EXTI_Line0) != RESET) {// 切换模式currentMode = (BreatheMode)((currentMode + 1) % 3);EXTI_ClearITPendingBit(EXTI_Line0);}
}
​
// 定时器中断服务函数
void TIM3_IRQHandler(void) {switch (currentMode) {case MODE_SLOW:// 慢呼吸参数step = 2;break;case MODE_FAST:// 快呼吸参数step = 10;break;case MODE_ALTERNATE:// 交替呼吸参数TIM_SetCompare1(TIM3, pulse);TIM_SetCompare2(TIM3, 1000 - pulse);return;  // 跳过下面的通用设置}// 通用呼吸逻辑pulse += step;if (pulse >= 1000 || pulse <= 0) step = -step;TIM_SetCompare1(TIM3, pulse);
}
5.6.5 相比于使用延时函数控制LED亮度变化,
使用定时器中断方式有什么优势?
特性定时器中断延时函数
CPU 占用仅在中断时执行代码,空闲时 CPU 可处理其他任务延时期间 CPU 被阻塞,无法响应其他事件
实时性可精确控制 PWM 周期和占空比变化,亮度曲线平滑延时时间受系统负载影响,亮度变化不均匀
多任务支持可同时处理按键检测、通信等其他任务难以实现并行操作,功能扩展受限
功耗可配合低功耗模式(如 STOP 模式),中断唤醒持续运行,功耗高
精度由定时器时钟决定,精度高(如 ±0.1%)受系统时钟和函数调用开销影响,精度低

推荐场景: 当需要:

  • 精确控制 LED 亮度曲线(如渐变效果);

  • 同时处理多个任务(如按键响应、数据传输);

  • 低功耗设计(如电池供电设备); 使用定时器中断更合适。

5.7 注意事项

  1. PWM初始化前必须先使能相应的定时器时钟和GPIO时钟

  1. GPIO必须配置为复用推挽输出模式

  1. 根据LED连接方式(共阳或共阴),正确设置PWM极性

  1. PWM频率不宜过低,避免LED闪烁现象

  1. 占空比调节范围通常为0-100%,对应比较值0-ARR

  1. 避免在循环中频繁修改占空比,会导致CPU负担过重

  1. 使用标准库函数时,需要注意头文件的包含和依赖关系

http://www.xdnf.cn/news/12096.html

相关文章:

  • 李沐《动手学深度学习》 | 数值稳定性
  • CATIA-CAD 拆图
  • 【优秀三方库研读】quill 开源库中的命名空间为什么要用宏封装
  • 养老实训中心建设规划:新时代养老服务人才实践能力提升工程
  • 【算法训练营Day06】哈希表part2
  • java判断一个字符串(如 str1)是否在给定的一组字符串
  • Python×AI:用LangChain快速搭建LLM应用的全栈方案
  • Vite实战指南
  • Linux容器篇、第一章_02Rocky9.5 系统下 Docker 的持久化操作与 Dockerfile 指令详解
  • SD卡通过读取bin文件替代读取图片格式文件来提高LCD显示速度
  • 半导体制冷片(Thermoelectric Cooler,TEC)
  • 深度学习Sitemap(NuxtSeo)
  • 《Offer来了:Java面试核心知识点精讲》大纲
  • 使用Prometheus实现微服务架构的全面监控
  • 慢SQL调优(二):大表查询
  • (四)docker命令—容器管理命令
  • 在 Spring Boot 中使用 WebFilter:实现请求拦截、日志记录、跨域处理等通用逻辑!
  • 嵌入式学习笔记 - freeRTOS的两种临界禁止
  • 改进社区检测和检索策略大幅提升GraphRAG性能新框架-ArchRAG
  • 策略公开了:年化494%,夏普比率5.86,最大回撤7% | 大模型查询akshare,附代码
  • 从 CLIP 和 Qwen2.5-VL 入门多模态技术
  • 2025Mybatis最新教程(三)
  • fmod产生的误差应该如何解决?
  • 日志项目——日志系统框架设计
  • 卡特兰数简单介绍
  • C++初阶 | 模板
  • C#中的依赖注入Dependency Injection, DI
  • AI 如何改变软件文档生产方式?
  • 图解浏览器多进程渲染:从DNS到GPU合成的完整旅程
  • JavaScript学习笔记(五)