呼吸灯效果在电子产品中极为常见,从手机通知灯到电脑设备状态指示,这种明暗渐变的效果既能传递信息又不会造成视觉干扰。使用STM32F103C8T6这款性价比极高的Cortex-M3内核单片机实现呼吸灯,是嵌入式开发入门的经典练手项目。
这个蓝色小开发板虽然只有64KB Flash和20KB RAM,但内置丰富的外设资源。实现呼吸灯主要利用其通用定时器(TIM)的PWM输出功能,通过调节占空比改变LED亮度。相比简单的delay循环方案,硬件PWM方案不占用CPU资源,亮度变化更加平滑稳定。
主控芯片采用STM32F103C8T6最小系统板,其GPIO输出电流最大可达25mA,直接驱动普通LED无需额外电路。LED选择标准5mm草帽灯,工作电流建议控制在10mA以内。若使用高亮度LED,需串联适当限流电阻:
注意:开发板上的LED通常已连接限流电阻,直接使用PA1、PA2等GPIO即可,无需外接电路。
STM32F103C8T6的定时器通道与GPIO对应关系如下表:
| 定时器 | 通道 | 重映射前引脚 | 重映射后引脚 |
|---|---|---|---|
| TIM2 | CH1 | PA0 | PA15 |
| TIM2 | CH2 | PA1 | PB3 |
| TIM3 | CH1 | PA6 | PB4 |
| TIM3 | CH2 | PA7 | PB5 |
推荐使用TIM3_CH2(PA7)引脚,其默认功能就是PWM输出,无需重映射配置。连接方式:
使用Keil MDK-ARM开发环境,配置步骤如下:
呼吸灯效果需要约100Hz的PWM频率(周期10ms),亮度变化肉眼才能感知平滑。配置TIM3的PWM模式:
c复制// PWM周期计算:PWM频率 = 72MHz / (PSC+1) / (ARR+1)
// 设PSC=71, ARR=999 → 72MHz/72/1000 = 100Hz
TIM_HandleTypeDef htim3;
void PWM_Init(void)
{
TIM_OC_InitTypeDef sConfigOC = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71; // 分频系数72-1
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 999; // 自动重装载值1000-1
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim3);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0; // 初始占空比0%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
}
亮度变化采用正弦函数曲线更符合人眼感知特性,但为简化实现,这里使用线性变化配合查表法:
c复制// 亮度曲线表(256级)
const uint16_t breathTable[256] = {
0, 1, 2, 3, 4, 6, 8, 10, 13, 16, 20, 24, 29, 34, 40, 46,
// ...中间数值省略...
999, 996, 992, 987, 981, 974, 966, 957, 947, 936, 924, 911
};
void Breath_LED_Update(void)
{
static uint8_t dir = 0; // 0:渐亮 1:渐暗
static uint16_t index = 0;
if(dir == 0) {
if(++index >= 255) dir = 1;
} else {
if(--index == 0) dir = 0;
}
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, breathTable[index]);
HAL_Delay(10); // 每10ms更新一次亮度
}
为避免主循环被延时阻塞,可采用DMA自动更新PWM占空比:
c复制uint16_t pwmData[512]; // 存储一个完整呼吸周期的亮度值
void DMA_PWM_Config(void)
{
// 填充正弦波亮度数据
for(int i=0; i<512; i++) {
pwmData[i] = (sin(i*3.1416/256)+1) * 500;
}
// 配置DMA
hdma_tim3_ch2.Instance = DMA1_Channel2;
hdma_tim3_ch2.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tim3_ch2.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tim3_ch2.Init.MemInc = DMA_MINC_ENABLE;
hdma_tim3_ch2.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_tim3_ch2.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_tim3_ch2.Init.Mode = DMA_CIRCULAR;
hdma_tim3_ch2.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_tim3_ch2);
__HAL_LINKDMA(&htim3, hdma[TIM_DMA_ID_CC2], hdma_tim3_ch2);
HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_2, (uint32_t*)pwmData, 512);
}
问题1:LED亮度变化不平滑
问题2:LED完全不亮
问题3:呼吸周期不稳定
通过一个定时器控制多个LED,实现复杂灯光效果:
c复制// 使用TIM3的4个通道控制4个LED
void MultiLED_Init(void)
{
// 初始化TIM3的4个PWM通道
// 每个通道使用不同的亮度曲线
// 通过DMA同时更新所有CCRx寄存器
}
添加光敏电阻检测环境光强度,动态调整PWM最大值:
c复制void AutoBrightness_Adjust(void)
{
uint16_t light = ADC_Read(PA0); // 读取光敏传感器
uint16_t maxDuty = light * 1000 / 4095; // 12位ADC转PWM范围
TIM3->ARR = maxDuty; // 动态调整自动重装载值
}
通过蓝牙或WiFi模块接收控制指令,改变呼吸频率和模式:
c复制void UART_Receive_Callback(uint8_t *data)
{
switch(data[0]) {
case 'S': // 设置速度
breathSpeed = data[1] * 10;
break;
case 'M': // 设置模式
breathMode = data[1];
break;
}
}
在实际项目中,我发现使用硬件PWM结合DMA的方案最稳定可靠,CPU占用率几乎为零。对于需要精确控制的场景,建议采用查表法预计算亮度曲线,避免实时计算带来的性能波动。调试时可以用逻辑分析仪捕获PWM波形,确保占空比变化符合预期。