在 PWM 实现呼吸灯的过程中,核心的 "比较" 发生在定时器内部的计数器(CNT)与捕获比较寄存器(CCR)之间,这是硬件自动完成的操作。结合你提供的呼吸灯代码,具体逻辑如下:
一、谁和谁比较?
-
比较双方:
- 计数器(CNT) :定时器内部不断递增(或递减)的数值,范围是
0到ARR(自动重装载值,即htim2.Init.Period)。 - 捕获比较寄存器(CCR) :存储你通过代码设置的占空比数值(即
pwm_buffer中的值),是一个固定值(直到下一次更新)。
- 计数器(CNT) :定时器内部不断递增(或递减)的数值,范围是
-
比较过程 :定时器工作时,CNT 会从 0 开始递增,每来一个时钟脉冲加 1,直到达到
ARR后重置为 0(向上计数模式)。在这个过程中,硬件会实时比较 CNT 和 CCR 的值:- 当
CNT < CCR时:PWM 输出 "有效电平"(由OCPolarity定义,如高电平,LED 亮)。 - 当
CNT >= CCR时:PWM 输出 "无效电平"(如低电平,LED 暗)。
- 当
二、如何通过比较实现呼吸灯?
pwm_buffer 数组,本质是一系列动态变化的 CCR 值。结合硬件比较逻辑,过程如下:
-
初始状态:
- 假设
pwm_buffer[0] = 0(CCR=0)。 - 比较结果:
CNT始终>= 0→ 输出无效电平(LED 暗)。
- 假设
-
亮度上升阶段:
pwm_buffer中的值逐渐增大(如从 0→50→100→...→ARR)。- CCR 随数组值增大,
CNT < CCR的时间变长 → 有效电平持续时间增加 → LED 逐渐变亮。
-
亮度下降阶段:
pwm_buffer中的值逐渐减小(如从 ARR→100→50→...→0)。- CCR 随数组值减小,
CNT < CCR的时间变短 → 有效电平持续时间减少 → LED 逐渐变暗。
-
循环往复 :当
pwm_buffer数组遍历完成后,重新从第一个值开始,形成 "暗→亮→暗" 的循环,即呼吸效果。
三、举例说明(假设 ARR=100)
-
若当前 CCR=30(来自
pwm_buffer):- CNT 从 0→29 时,
CNT < 30→ 输出高电平(LED 亮,持续 30 个时钟周期)。 - CNT 从 30→100 时,
CNT >= 30→ 输出低电平(LED 暗,持续 70 个时钟周期)。 - 占空比 = 30%,LED 较暗。
- CNT 从 0→29 时,
-
若当前 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 → 循环直到数组传输完成。
整体工作流程(呼吸灯实现逻辑)
- 启动 PWM :
HAL_TIM_PWM_Start使 TIM2_CH3 开始输出 PWM,但初始占空比可能为 0。 - 配置 DMA :
HAL_DMA_Start_IT让 DMA 准备好从pwm_buffer向CCR3传输数据,并允许 DMA 中断(方便传输完成后处理,如循环传输)。 - 绑定触发源 :
__HAL_TIM_ENABLE_DMA设定 "TIM2 更新事件" 作为 DMA 的触发信号,确保每个 PWM 周期更新一次占空比。 - 启动定时器 :
__HAL_TIM_ENABLE让 TIM2 开始工作,触发后续的计数、更新事件和 DMA 传输。
最终效果:pwm_buffer中的占空比序列会被 DMA 自动、周期性地写入CCR3,TIM2 根据实时的CCR3值输出 PWM,使 LED 按sin²曲线平滑亮暗变化,实现呼吸效果。
关键优势
- 无 CPU 干预:DMA 自动完成占空比更新,CPU 可空闲处理其他任务。
- 实时性强:每个 PWM 周期精准更新一次占空比,呼吸效果平滑无卡顿。
- 低功耗:减少 CPU 频繁中断或轮询,降低系统功耗。