STM32实现呼吸灯效果原理

在 PWM 实现呼吸灯的过程中,核心的 "比较" 发生在定时器内部的计数器(CNT)与捕获比较寄存器(CCR)之间,这是硬件自动完成的操作。结合你提供的呼吸灯代码,具体逻辑如下:

一、谁和谁比较?

  1. 比较双方

    • 计数器(CNT) :定时器内部不断递增(或递减)的数值,范围是 0ARR(自动重装载值,即 htim2.Init.Period)。
    • 捕获比较寄存器(CCR) :存储你通过代码设置的占空比数值(即 pwm_buffer 中的值),是一个固定值(直到下一次更新)。
  2. 比较过程 :定时器工作时,CNT 会从 0 开始递增,每来一个时钟脉冲加 1,直到达到ARR后重置为 0(向上计数模式)。在这个过程中,硬件会实时比较 CNT 和 CCR 的值

    • CNT < CCR 时:PWM 输出 "有效电平"(由 OCPolarity 定义,如高电平,LED 亮)。
    • CNT >= CCR 时:PWM 输出 "无效电平"(如低电平,LED 暗)。

二、如何通过比较实现呼吸灯?

pwm_buffer 数组,本质是一系列动态变化的 CCR 值。结合硬件比较逻辑,过程如下:

  1. 初始状态

    • 假设 pwm_buffer[0] = 0(CCR=0)。
    • 比较结果:CNT 始终 >= 0 → 输出无效电平(LED 暗)。
  2. 亮度上升阶段

    • pwm_buffer 中的值逐渐增大(如从 0→50→100→...→ARR)。
    • CCR 随数组值增大,CNT < CCR 的时间变长 → 有效电平持续时间增加 → LED 逐渐变亮。
  3. 亮度下降阶段

    • pwm_buffer 中的值逐渐减小(如从 ARR→100→50→...→0)。
    • CCR 随数组值减小,CNT < CCR 的时间变短 → 有效电平持续时间减少 → LED 逐渐变暗。
  4. 循环往复 :当 pwm_buffer 数组遍历完成后,重新从第一个值开始,形成 "暗→亮→暗" 的循环,即呼吸效果。

三、举例说明(假设 ARR=100)

  • 若当前 CCR=30(来自pwm_buffer):

    • CNT 从 0→29 时,CNT < 30 → 输出高电平(LED 亮,持续 30 个时钟周期)。
    • CNT 从 30→100 时,CNT >= 30 → 输出低电平(LED 暗,持续 70 个时钟周期)。
    • 占空比 = 30%,LED 较暗。
  • 若当前 CCR=80:

    • 亮的时间占 80%,暗的时间占 20% → LED 较亮。

四、总结

呼吸灯的核心是定时器硬件自动比较 "计数器(CNT)" 和 "捕获比较寄存器(CCR)" ,通过生成动态变化的 CCR 值(pwm_buffer),改变了 "亮 / 暗时间的比例",最终实现了平滑的亮度变化。硬件比较是实时且自动的,代码更新 CCR 值即可控制呼吸效果。

五、代码

这个函数是STM32CubeMX自动生成的,用于初始化TIM2的基本配置

cpp 复制代码
/* TIM2 init function */
// 这个函数是STM32CubeMX自动生成的,用于初始化TIM2的基本配置
void MX_TIM2_Init(void)
{

  /* USER CODE BEGIN TIM2_Init 0 */
  // 用户可以在初始化开始处添加代码
  /* USER CODE END TIM2_Init 0 */

  // 自动生成的局部变量定义
  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};
  TIM_OC_InitTypeDef sConfigOC = {0};

  /* USER CODE BEGIN TIM2_Init 1 */
  // 用户可以在初始化中间添加代码
  /* USER CODE END TIM2_Init 1 */
  
  // 自动生成的定时器基本配置
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 71;            /* 72MHz/(71+1) = 1MHz计数频率 */
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 999;              /* 1MHz/(999+1) = 1kHz PWM频率 */
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  HAL_TIM_Base_Init(&htim2);
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig);
  HAL_TIM_PWM_Init(&htim2);
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_3);
  /* USER CODE BEGIN TIM2_Init 2 */
  // 用户可以在初始化结束处添加代码
  /* USER CODE END TIM2_Init 2 */
  HAL_TIM_MspPostInit(&htim2);

}

初始化tim2的底层硬件,打开tim2通道三和dma通道二。

cpp 复制代码
// 这个函数是STM32CubeMX自动生成的,用于初始化TIM2的底层硬件
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{

  if(tim_baseHandle->Instance==TIM2)
  {
  /* USER CODE BEGIN TIM2_MspInit 0 */
  // 用户可以在初始化开始处添加代码
  /* USER CODE END TIM2_MspInit 0 */
    /* TIM2 clock enable */
    // 自动生成的时钟使能代码
    __HAL_RCC_TIM2_CLK_ENABLE();

    /* TIM2 DMA Init */
    // 自动生成的DMA配置代码
    /* TIM2_UP Init: 使用更新事件触发DMA写CCR3 */
    hdma_tim2_ch3_up.Instance = DMA1_Channel2; /* F1映射:TIM2_UP -> DMA1_Channel2 */
    hdma_tim2_ch3_up.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_tim2_ch3_up.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_tim2_ch3_up.Init.MemInc = DMA_MINC_ENABLE;
    hdma_tim2_ch3_up.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_tim2_ch3_up.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_tim2_ch3_up.Init.Mode = DMA_CIRCULAR;
    hdma_tim2_ch3_up.Init.Priority = DMA_PRIORITY_LOW;
    HAL_DMA_Init(&hdma_tim2_ch3_up);

    /* 启动时钟 */
    __HAL_RCC_DMA1_CLK_ENABLE();
    
    // 改为更新事件DMA请求
    __HAL_LINKDMA(tim_baseHandle,hdma[TIM_DMA_ID_UPDATE],hdma_tim2_ch3_up);

    /* TIM2 interrupt Init */
    // 自动生成的中断配置代码
    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);
  /* USER CODE BEGIN TIM2_MspInit 1 */
  // 用户可以在初始化结束处添加代码
  /* USER CODE END TIM2_MspInit 1 */
  }
}

生成一个平滑的呼吸灯 PWM 占空比序列,通过将该序列按时间依次输出到 LED 的 PWM 通道,即可实现 LED 从暗→亮→暗的呼吸效果。

cpp 复制代码
void MX_TIM2_PWM_Init(void)
{
  /* 动态生成平滑呼吸曲线:duty = sin^2(0..pi) * Period */
  float period = (float)htim2.Init.Period;
  float pi = 3.1415926f;
  for (int i = 0; i < PWM_BUFFER_SIZE; i++) {
    float phase = (pi * (float)i) / (float)(PWM_BUFFER_SIZE - 1);
    float s = sinf(phase);
    float duty = s * s * period;
    int val = (int)(duty + 0.5f);
    if (val < 0) val = 0;
    if (val > (int)period) val = (int)period;
    pwm_buffer[i] = (uint16_t)val;
  }
}

以下这段代码 Start_Breathing_LED(void) 是启动呼吸灯功能的核心函数,通过定时器 PWM + DMA的方式实现 LED 的自动呼吸效果,无需 CPU 频繁干预

cpp 复制代码
void Start_Breathing_LED(void)
{
  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
  HAL_DMA_Start_IT(&hdma_tim2_ch3_up, (uint32_t)pwm_buffer, (uint32_t)&htim2.Instance->CCR3, PWM_BUFFER_SIZE);
  __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_UPDATE);
  __HAL_TIM_ENABLE(&htim2);
}

以下是逐行代码的详细解释:

1. HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);

  • 功能:启动 TIM2 定时器的通道 3(CH3)的 PWM 输出功能。
  • 细节
    • &htim2 是 TIM2 的句柄(包含定时器的配置参数,如周期、预分频等)。
    • TIM_CHANNEL_3 指定启用通道 3,该通道需提前配置为 PWM 模式(如TIM_OCMODE_PWM1),且对应引脚已配置为复用输出(通常在HAL_TIM_MspPostInit中完成)。
    • 调用后,TIM2_CH3 开始输出 PWM 波形,但此时占空比由CCR3寄存器的初始值决定,尚未进入呼吸模式。

2. HAL_DMA_Start_IT(&hdma_tim2_ch3_up, (uint32_t)pwm_buffer, (uint32_t)&htim2.Instance->CCR3, PWM_BUFFER_SIZE);

  • 功能 :启动 DMA 通道,并配置其从内存(pwm_buffer)向外设(CCR3寄存器)传输数据,同时使能 DMA 中断。
  • 参数解析
    • &hdma_tim2_ch3_up:TIM2_CH3 对应的 DMA 通道句柄(需提前在 CubeMX 中配置,如 STM32F103 中 TIM2_CH3 通常映射到 DMA1_Channel2)。
    • (uint32_t)pwm_buffer源地址 ,即存储呼吸灯占空比序列的数组(你之前生成的sin²曲线数据)。
    • (uint32_t)&htim2.Instance->CCR3目标地址 ,即 TIM2 通道 3 的捕获比较寄存器(CCR3),PWM 的占空比由该寄存器的值决定。
    • PWM_BUFFER_SIZE传输次数 ,即需要从pwm_buffer传输到CCR3的数据总量(数组长度)。
  • 作用 :DMA 会自动将pwm_buffer中的值依次写入CCR3,无需 CPU 手动调用__HAL_TIM_SET_COMPARE,减少 CPU 负担。

3. __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_UPDATE);

  • 功能:使能 TIM2 的 "更新事件(Update Event)" 触发 DMA 传输。
  • 细节
    • 定时器的 "更新事件" 指计数器(CNT)从最大值(ARR)溢出并重置为 0 的时刻(向上计数模式)。
    • TIM_DMA_UPDATE 表示将 DMA 触发源绑定到 TIM2 的更新事件。
    • 使能后,每次 TIM2 发生更新事件(即一个 PWM 周期结束时),会自动触发 DMA 传输下一个占空比数据到CCR3

4. __HAL_TIM_ENABLE(&htim2);

  • 功能:启动 TIM2 定时器,使其开始计数。
  • 细节
    • 定时器启动后,计数器(CNT)开始从 0 递增,达到 ARR 后触发更新事件,进而触发 DMA 传输下一个占空比。
    • 结合前面的配置,此时整个流程会自动运行:TIM2计数 → 溢出触发更新事件 → DMA传输下一个占空比到CCR3 → TIM2根据新的CCR3输出PWM → 循环直到数组传输完成

整体工作流程(呼吸灯实现逻辑)

  1. 启动 PWMHAL_TIM_PWM_Start 使 TIM2_CH3 开始输出 PWM,但初始占空比可能为 0。
  2. 配置 DMAHAL_DMA_Start_IT 让 DMA 准备好从pwm_bufferCCR3传输数据,并允许 DMA 中断(方便传输完成后处理,如循环传输)。
  3. 绑定触发源__HAL_TIM_ENABLE_DMA 设定 "TIM2 更新事件" 作为 DMA 的触发信号,确保每个 PWM 周期更新一次占空比。
  4. 启动定时器__HAL_TIM_ENABLE 让 TIM2 开始工作,触发后续的计数、更新事件和 DMA 传输。

最终效果:pwm_buffer中的占空比序列会被 DMA 自动、周期性地写入CCR3,TIM2 根据实时的CCR3值输出 PWM,使 LED 按sin²曲线平滑亮暗变化,实现呼吸效果。

关键优势

  • 无 CPU 干预:DMA 自动完成占空比更新,CPU 可空闲处理其他任务。
  • 实时性强:每个 PWM 周期精准更新一次占空比,呼吸效果平滑无卡顿。
  • 低功耗:减少 CPU 频繁中断或轮询,降低系统功耗。
相关推荐
云雾J视界9 小时前
C语言位运算深度应用:嵌入式硬件寄存器控制与低功耗优化实践
c语言·stm32·嵌入式硬件·低功耗·数据压缩·寄存器
芋头莎莎9 小时前
MCU单片机驱动WS2812,点亮RGB灯带各种效果
单片机·嵌入式硬件
芋头莎莎11 小时前
STM32利用AES加密数据、解密数据
数据结构·stm32·算法
无垠的广袤11 小时前
【CPKCOR-RA8D1】Home Assistant 物联网 ADC 电压温度计
嵌入式硬件·物联网·智能家居·瑞萨
诸葛务农12 小时前
光电对抗分类及外场静爆试验操作规程
人工智能·嵌入式硬件·分类·数据挖掘
点灯小铭15 小时前
基于单片机的多波形信号发生器设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
SXSBJS_XYT17 小时前
在资源有限的M0单片机上运行RTOS
单片机·rt-thread·rtos
gfanbei21 小时前
ARM V8 Cortex R52 上电运行在什么状态?— Deepseek 解答
linux·arm开发·嵌入式硬件
小刘爱玩单片机1 天前
【stm32协议外设篇】- PAJ7620手势识别传感器
c语言·stm32·单片机·嵌入式硬件
Jerry丶Li1 天前
二十七、通信接口
c语言·stm32·单片机·嵌入式硬件