1. 项目概述
最近在使用国民技术的N32G031K8Q7-1开发板做一个LED呼吸灯效果的项目。这个开发板上有两个板载LED(LED1和LED2),我打算用PWM(脉冲宽度调制)技术让它们实现呼吸灯效果,并且是交错闪烁的模式。也就是说,当一个LED渐亮时,另一个LED渐暗,交替进行。
这种效果在很多电子产品上都能看到,比如笔记本电脑的睡眠指示灯、路由器的状态灯等。通过这个项目,不仅可以学习PWM的基本原理,还能掌握如何在实际硬件上实现这种视觉效果。
2. 硬件准备与环境搭建
2.1 开发板介绍
我使用的是国民技术的N32G031K8Q7-1开发板,这是一款基于ARM Cortex-M0内核的32位微控制器开发板。主要特点包括:
- 工作频率最高48MHz
- 内置64KB Flash和8KB SRAM
- 丰富的外设接口,包括多个定时器/PWM模块
- 板载两个LED灯(LED1和LED2)
2.2 开发环境配置
为了开发这个项目,我们需要准备以下工具:
- 开发工具链:我使用的是Keil MDK-ARM开发环境,配合N32G031的器件支持包
- 下载工具:可以通过板载的SWD接口进行程序下载和调试
- 硬件连接:只需要一根USB线连接开发板和电脑即可
提示:在开始编程前,请确保已经正确安装了芯片支持包和驱动程序,否则可能会出现无法识别设备的问题。
3. PWM原理与实现
3.1 PWM基础原理
PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过改变脉冲宽度来控制模拟信号的技术。在LED控制中,PWM通过快速开关LED来模拟不同的亮度级别。
关键参数:
- 频率:PWM信号的开关频率
- 占空比:高电平时间占整个周期的比例(0-100%)
对于LED呼吸灯效果,我们需要动态改变占空比,让LED从暗到亮再到暗循环变化。
3.2 定时器配置
在N32G031芯片中,PWM功能是通过定时器的输出比较功能实现的。我们需要配置以下几个关键寄存器:
- TIMx_ARR(自动重装载寄存器):决定PWM的周期
- TIMx_PSC(预分频器):决定定时器的时钟频率
- TIMx_CCRx(捕获/比较寄存器):决定PWM的占空比
具体到我们的项目,使用了TIM1和TIM8两个定时器:
- TIM1的通道3N控制LED1
- TIM8的通道3控制LED2
3.3 代码实现解析
让我们来看一下关键的初始化代码:
c复制/* TIM1基础配置 */
TIM_InitTimBaseStruct(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.Period = 99; // ARR值
TIM_TimeBaseStructure.Prescaler = PrescalerValue;
TIM_TimeBaseStructure.ClkDiv = 0;
TIM_TimeBaseStructure.CntMode = TIM_CNT_MODE_UP;
TIM_InitTimeBase(TIM1, &TIM_TimeBaseStructure);
/* TIM8基础配置 */
TIM_InitTimBaseStruct(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.Period = 99;
TIM_TimeBaseStructure.Prescaler = PrescalerValue;
TIM_TimeBaseStructure.ClkDiv = 0;
TIM_TimeBaseStructure.CntMode = TIM_CNT_MODE_UP;
TIM_InitTimeBase(TIM8, &TIM_TimeBaseStructure);
这里我们将ARR设置为99,这意味着PWM的分辨率是100级(0-99)。Prescaler的值是根据系统时钟计算得出的,确保PWM频率在合适的范围内。
4. 呼吸灯效果实现
4.1 主循环设计
呼吸灯效果的核心在于主循环中的PWM占空比变化。我们使用两个for循环分别控制两个LED的亮度变化:
c复制while (1)
{
// LED1渐亮
for(i = 0; i<=100; i++)
{
PWM_SetCmp3N(100 - i);
Delay_ms(10);
}
// LED1渐灭
for(i = 0; i<=100; i++)
{
PWM_SetCmp3N(i);
Delay_ms(10);
}
// LED2渐亮
for(z = 0; z<=100; z++)
{
PWM_SetCmp3(z);
Delay_ms(10);
}
// LED2渐灭
for(z = 0; z<=100; z++)
{
PWM_SetCmp3(100 - z);
Delay_ms(10);
}
}
这段代码实现了LED1和LED2交替呼吸的效果。每个LED的亮度变化周期大约2秒(100步×10ms×2)。
4.2 延时函数实现
精确的延时对于呼吸灯效果的平滑性很重要。我们实现了一个简单的毫秒级延时函数:
c复制void Delay_ms(uint32_t ms)
{
uint32_t j, k;
for (j = 0; j < ms; j++)
{
// N32G031 48MHz 下,大约 1ms 的循环
for (k = 0; k < 4800; k++)
{
__NOP(); // 空指令,保证延时精准
}
}
}
注意:这种软件延时方式会占用CPU资源。在实际产品中,可以考虑使用定时器中断来实现更精确且不占用CPU的延时。
5. GPIO与定时器通道配置
5.1 引脚复用配置
LED1和LED2分别连接到PB1(TIM1_CH3N)和PB6(TIM8_CH3)引脚。我们需要正确配置这些引脚的复用功能:
c复制// PB1配置为TIM1_CH3N
GPIO_InitStructure.Pin = GPIO_PIN_1;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.GPIO_Current = GPIO_DC_LOW;
GPIO_InitStructure.GPIO_Alternate = GPIO_AF1_TIM1;
GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);
GPIO_ConfigPinRemap(GPIOB_PORT_SOURCE, GPIO_PIN_1, GPIO_AF1);
// PB6配置为TIM8_CH3
GPIO_InitStructure.Pin = GPIO_PIN_6;
GPIO_InitStructure.GPIO_Alternate = GPIO_AF2_TIM8;
GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);
GPIO_ConfigPinRemap(GPIOB_PORT_SOURCE, GPIO_PIN_6, GPIO_AF2);
5.2 定时器输出比较配置
对于TIM1的通道3N(LED1)和TIM8的通道3(LED2),我们需要配置输出比较模式:
c复制// TIM1-CH3N配置
TIM_InitOcStruct(&TIM_OCInitStructure);
TIM_OCInitStructure.OcMode = TIM_OCMODE_PWM1;
TIM_OCInitStructure.OutputState = TIM_OUTPUT_STATE_ENABLE;
TIM_OCInitStructure.OutputNState = TIM_OUTPUT_NSTATE_ENABLE;
TIM_OCInitStructure.Pulse = CCR3_Val;
TIM_OCInitStructure.OcPolarity = TIM_OC_POLARITY_HIGH;
TIM_OCInitStructure.OcNPolarity = TIM_OC_POLARITY_HIGH;
TIM_InitOc3(TIM1, &TIM_OCInitStructure);
TIM_ConfigOc3Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);
// TIM8-CH3配置
TIM_InitOcStruct(&TIM_OCInitStructure);
TIM_OCInitStructure.OcMode = TIM_OCMODE_PWM1;
TIM_OCInitStructure.OutputState = TIM_OUTPUT_STATE_ENABLE;
TIM_OCInitStructure.Pulse = CCR3_Val;
TIM_OCInitStructure.OcPolarity = TIM_OC_POLARITY_HIGH;
TIM_InitOc3(TIM8, &TIM_OCInitStructure);
TIM_ConfigOc3Preload(TIM8, TIM_OC_PRE_LOAD_ENABLE);
6. 效果优化与变体实现
6.1 呼吸灯速度调整
呼吸灯的变化速度可以通过调整两个参数来控制:
- 步进间隔时间:修改Delay_ms(10)中的参数
- 步进数量:修改for循环中的上限值(当前是100)
例如,要实现更快的呼吸效果:
c复制for(i = 0; i<=50; i++) // 减少步数
{
PWM_SetCmp3N(100 - i*2); // 增大步进幅度
Delay_ms(5); // 减少间隔时间
}
6.2 同步呼吸效果
如果要实现两个LED同步呼吸(同时渐亮渐灭),可以修改主循环:
c复制while (1)
{
for(i = 0; i<=100; i++)
{
PWM_SetCmp3N(100 - i); // LED1渐亮
PWM_SetCmp3(i); // LED2渐灭
Delay_ms(10);
}
for(i = 0; i<=100; i++)
{
PWM_SetCmp3N(i); // LED1渐灭
PWM_SetCmp3(100 - i); // LED2渐亮
Delay_ms(10);
}
}
7. 常见问题与调试技巧
7.1 LED不亮或亮度异常
如果遇到LED不亮或亮度异常的情况,可以按照以下步骤排查:
- 检查硬件连接:确认LED确实连接到正确的引脚
- 验证GPIO配置:确保引脚已正确配置为复用功能输出
- 检查定时器使能:确认TIM_Enable函数已被调用
- 测量PWM信号:用示波器检查引脚是否有PWM信号输出
7.2 呼吸效果不平滑
如果呼吸效果出现跳动或不平滑,可能是以下原因:
- 延时不准确:调整延时函数的精度
- 步进幅度过大:增加步数(如从100增加到200)
- PWM频率过低:提高定时器的频率(减小ARR值或增大Prescaler)
7.3 高级定时器特殊配置
对于TIM1和TIM8这样的高级定时器,需要特别注意:
c复制TIM_EnableCtrlPwmOutputs(TIM1, ENABLE); // 必须启用PWM输出
TIM_EnableCtrlPwmOutputs(TIM8, ENABLE);
如果忘记启用这个设置,即使配置正确也不会有PWM输出。
8. 完整代码实现
以下是完整的呼吸灯实现代码,包含了所有必要的初始化和配置:
c复制#include "main.h"
TIM_TimeBaseInitType TIM_TimeBaseStructure;
OCInitType TIM_OCInitStructure;
uint16_t CCR3_Val;
uint16_t PrescalerValue = 0;
uint16_t i, z;
void RCC_Configuration(void);
void GPIO_Configuration(void);
void PWM_SetCmp3N(uint16_t Compare);
void PWM_SetCmp3(uint16_t Compare);
void Delay_ms(uint32_t ms);
int main(void)
{
RCC_Configuration();
GPIO_Configuration();
PrescalerValue = (uint16_t)(SystemCoreClock / 12000000) - 1;
/* TIM1配置 */
TIM_InitTimBaseStruct(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.Period = 99;
TIM_TimeBaseStructure.Prescaler = PrescalerValue;
TIM_TimeBaseStructure.ClkDiv = 0;
TIM_TimeBaseStructure.CntMode = TIM_CNT_MODE_UP;
TIM_InitTimeBase(TIM1, &TIM_TimeBaseStructure);
/* TIM8配置 */
TIM_InitTimBaseStruct(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.Period = 99;
TIM_TimeBaseStructure.Prescaler = PrescalerValue;
TIM_TimeBaseStructure.ClkDiv = 0;
TIM_TimeBaseStructure.CntMode = TIM_CNT_MODE_UP;
TIM_InitTimeBase(TIM8, &TIM_TimeBaseStructure);
/* TIM1-CH3N PWM配置 */
TIM_InitOcStruct(&TIM_OCInitStructure);
TIM_OCInitStructure.OcMode = TIM_OCMODE_PWM1;
TIM_OCInitStructure.OutputState = TIM_OUTPUT_STATE_ENABLE;
TIM_OCInitStructure.OutputNState = TIM_OUTPUT_NSTATE_ENABLE;
TIM_OCInitStructure.Pulse = CCR3_Val;
TIM_OCInitStructure.OcPolarity = TIM_OC_POLARITY_HIGH;
TIM_OCInitStructure.OcNPolarity = TIM_OC_POLARITY_HIGH;
TIM_InitOc3(TIM1, &TIM_OCInitStructure);
TIM_ConfigOc3Preload(TIM1, TIM_OC_PRE_LOAD_ENABLE);
/* TIM8-CH3 PWM配置 */
TIM_InitOcStruct(&TIM_OCInitStructure);
TIM_OCInitStructure.OcMode = TIM_OCMODE_PWM1;
TIM_OCInitStructure.OutputState = TIM_OUTPUT_STATE_ENABLE;
TIM_OCInitStructure.Pulse = CCR3_Val;
TIM_OCInitStructure.OcPolarity = TIM_OC_POLARITY_HIGH;
TIM_InitOc3(TIM8, &TIM_OCInitStructure);
TIM_ConfigOc3Preload(TIM8, TIM_OC_PRE_LOAD_ENABLE);
TIM_ConfigArPreload(TIM1, ENABLE);
TIM_ConfigArPreload(TIM8, ENABLE);
TIM_EnableCtrlPwmOutputs(TIM1, ENABLE);
TIM_EnableCtrlPwmOutputs(TIM8, ENABLE);
TIM_Enable(TIM1, ENABLE);
TIM_Enable(TIM8, ENABLE);
while (1)
{
for(i = 0; i<=100; i++)
{
PWM_SetCmp3N(100 - i);
Delay_ms(10);
}
for(i = 0; i<=100; i++)
{
PWM_SetCmp3N(i);
Delay_ms(10);
}
for(z = 0; z<=100; z++)
{
PWM_SetCmp3(z);
Delay_ms(10);
}
for(z = 0; z<=100; z++)
{
PWM_SetCmp3(100 - z);
Delay_ms(10);
}
}
}
void RCC_Configuration(void)
{
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_TIM1 | RCC_APB2_PERIPH_TIM8, ENABLE);
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB | RCC_APB2_PERIPH_AFIO, ENABLE);
}
void GPIO_Configuration(void)
{
GPIO_InitType GPIO_InitStructure;
GPIO_InitStruct(&GPIO_InitStructure);
/* PB1配置为TIM1_CH3N */
GPIO_InitStructure.Pin = GPIO_PIN_1;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.GPIO_Current = GPIO_DC_LOW;
GPIO_InitStructure.GPIO_Alternate = GPIO_AF1_TIM1;
GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);
GPIO_ConfigPinRemap(GPIOB_PORT_SOURCE, GPIO_PIN_1, GPIO_AF1);
/* PB6配置为TIM8_CH3 */
GPIO_InitStructure.Pin = GPIO_PIN_6;
GPIO_InitStructure.GPIO_Alternate = GPIO_AF2_TIM8;
GPIO_InitPeripheral(GPIOB, &GPIO_InitStructure);
GPIO_ConfigPinRemap(GPIOB_PORT_SOURCE, GPIO_PIN_6, GPIO_AF2);
}
void PWM_SetCmp3N(uint16_t Compare)
{
TIM_SetCmp3(TIM1, Compare);
}
void PWM_SetCmp3(uint16_t Compare)
{
TIM_SetCmp3(TIM8, Compare);
}
void Delay_ms(uint32_t ms)
{
uint32_t j, k;
for (j = 0; j < ms; j++)
{
for (k = 0; k < 4800; k++)
{
__NOP();
}
}
}
9. 项目扩展思路
这个基础项目可以进一步扩展,实现更复杂的效果:
- 多LED控制:增加更多LED,实现流水灯或更复杂的灯光秀
- 亮度曲线优化:使用非线性变化(如指数曲线)使呼吸效果更自然
- 外部控制:通过按键或串口命令改变呼吸速度或模式
- 低功耗优化:在电池供电场景下优化代码降低功耗
在实际项目中,我曾用类似的方法实现了一个可调节亮度的LED指示灯,用户可以通过长按/短按按键来调整亮度和呼吸速度,效果非常不错。
