十八、基于HC32F4A0与天空星开发板的PWM呼吸灯实战:从TimerA配置到占空比动态调节
十八、基于HC32F4A0与天空星开发板的PWM呼吸灯实战:从TimerA配置到占空比动态调节
大家好,我是老李,一个在嵌入式行业摸爬滚打了十来年的工程师。最近在用华大半导体的HC32F4A0芯片做项目,发现它的定时器功能相当强大,尤其是PWM输出,配置起来很灵活。正好手边有块天空星开发板,今天就带大家从零开始,手把手实现一个经典的PWM呼吸灯。这个实验虽然简单,但却是理解定时器、PWM以及硬件控制逻辑的绝佳入门项目。无论你是刚接触嵌入式的新手,还是想了解HC32F4A0这款芯片的具体用法,跟着我一步步走,保证你能点亮这个“会呼吸”的LED。
咱们的目标很明确:让连接在开发板PA02引脚上的LED灯,实现从暗到亮、再从亮到暗的平滑渐变效果。整个过程我会拆解成几个清晰的步骤,从最基础的寄存器保护关闭,到最后的呼吸效果函数编写,每个环节都会配上代码和原理讲解,确保你看得懂、学得会、做得出来。
1. 硬件连接与核心概念
在开始写代码之前,咱们先把硬件和原理搞清楚。
硬件连接:这次实验,我们需要将一个LED灯连接到天空星开发板上。关键的一点是,必须连接在PA02引脚上。这是因为我们计划使用TimerA_5的通道1(简称TMRA_5_CH1)来输出PWM信号,而PA02引脚正好复用了这个功能。如果你接错了引脚,程序跑起来灯是不会亮的。
注意:请根据你的LED规格(通常是3V或5V),串联一个合适的限流电阻(比如220Ω或330Ω),防止电流过大烧坏LED或芯片引脚。
PWM呼吸灯原理:呼吸灯的本质是LED亮度的平滑变化。LED的亮度直接由流过它的电流大小决定,而我们通常用电压来控制。单片机引脚输出的是数字信号,只有0(低电平)和1(高电平)两种状态,怎么实现“半亮”这种中间状态呢?答案就是PWM(脉冲宽度调制)。
你可以把PWM信号想象成一个高速开关。在一个固定的时间周期内(比如1毫秒),如果开关只闭合了0.2毫秒,其余0.8毫秒断开,那么LED得到的平均电压就很低,看起来就很暗。如果开关闭合了0.8毫秒,断开0.2毫秒,平均电压就高,LED就亮。这个“闭合时间占整个周期的比例”,就是我们常说的占空比。
呼吸灯,就是让单片机自动、循环地改变输出PWM波的占空比。占空比从0%慢慢增加到100%,LED就从灭慢慢变到最亮;然后再从100%慢慢减小到0%,LED就从最亮慢慢变灭,如此循环,就形成了呼吸效果。
理解了这些,咱们就可以进入实战配置环节了。使用HC32F4A0的定时器产生PWM,一般遵循下面这个流程,我会逐一详细解释。
2. 手把手配置PWM输出
2.1 第一步:关闭寄存器写保护
HC32F4A0芯片为了系统安全,对很多关键寄存器设置了写保护。在修改它们之前,必须先解除“锁”。这就像你要调整家里电箱的开关,总得先把外面的保护盖打开吧。
// 关闭相关外设的寄存器写保护 LL_PERIPH_WE(LL_PERIPH_GPIO | LL_PERIPH_FCG | LL_PERIPH_PWC_CLK_RMU);这行代码使用了芯片提供的库函数LL_PERIPH_WE,它允许我们对指定的外设寄存器进行写入操作。这里我们一次性解锁了GPIO、时钟控制单元(FCG)和电源时钟复位管理单元(PWC_CLK_RMU)的写保护。这是后续配置GPIO和时钟的基础。
2.2 第二步:使能定时器时钟
单片机内部各个功能模块(外设)就像一个个独立的车间,要使某个车间运转,必须先给它通上电。在芯片里,“通电”就是使能对应的时钟。
我们要用的是TimerA Unit 5,所以需要打开它的时钟。
// 使能 TimerA Unit 5 的时钟 FCG_Fcg2PeriphClockCmd(FCG2_PERIPH_TMRA_5, ENABLE);2.3 第三步:配置GPIO引脚复用功能
单片机的引脚通常身兼数职,既可以做普通的输入输出(GPIO),也可以作为串口、定时器等特殊功能的引脚。这叫做“引脚复用”。我们的PA02引脚需要扮演“TimerA_5通道1输出”这个角色。
查一下芯片数据手册(第43页的引脚复用表),可以找到PA02对应的复用功能编号。这里我们使用GPIO_FUNC_5。
// 将PA02引脚配置为复用功能,选择功能5 (即TMRA_5_CH1) GPIO_SetFunc(GPIO_PORT_A, GPIO_PIN_02, GPIO_FUNC_5);提示:
GPIO_SetFunc函数的三个参数分别是端口号、引脚号和功能编号。如果你以后想用其他引脚的PWM,比如PA04的Timer6/7通道A,就需要去查表找到对应的功能编号。
2.4 第四步:配置定时器(TimerA)基本参数
这是核心步骤之一。我们需要配置定时器以什么样的方式工作。HC32的库提供了一个结构体stc_tmra_init_t来设置这些参数。
stc_tmra_init_t stcTmraInit; // 定义初始化结构体 // 先用默认值填充这个结构体,避免出现随机值 (void)TMRA_StructInit(&stcTmraInit); // 然后修改为我们需要的参数 stcTmraInit.sw_count.u8CountMode = TMRA_MD_SAWTOOTH; // 计数模式:锯齿波模式 stcTmraInit.u8CountReload = TMRA_CNT_RELOAD_ENABLE; // 计数器溢出后自动重装初始值 stcTmraInit.u8CountSrc = TMRA_CNT_SRC_SW; // 时钟源:使用内部系统时钟(PCLK) stcTmraInit.sw_count.u8CountDir = TMRA_DIR_UP; // 计数方向:向上计数 stcTmraInit.u32PeriodValue = 10000; // 周期值,决定PWM的频率 // 将配置好的参数初始化到 TimerA Unit 5 (void)TMRA_Init(CM_TMRA_5, &stcTmraInit);我来解释一下这几个关键参数:
- 锯齿波模式 (TMRA_MD_SAWTOOTH):这是最常用的PWM模式。计数器从0开始向上数,数到我们设定的
u32PeriodValue(这里是10000)后,瞬间归零,然后重新开始数,波形像锯齿一样。这个“数到顶再归零”的过程,就是一个PWM周期。 - 自动重装 (TMRA_CNT_RELOAD_ENABLE):必须开启,这样PWM才能连续不断地输出波形。
- 内部时钟源 (TMRA_CNT_SRC_SW):使用芯片内部的时钟,简单稳定。
- 周期值 (u32PeriodValue):设为10000。这个值越大,计数器数完一个周期的时间就越长,PWM波的频率就越低。你可以根据实际需要调整它。PWM频率 = 时钟源频率 / (分频系数 * 周期值)。
2.5 第五步:配置PWM输出参数
定时器本身只会“数数”,我们需要告诉它:数到哪个值的时候,把输出引脚的电平翻转一下,从而形成PWM波。这就要用到另一个结构体stc_tmra_pwm_init_t。
stc_tmra_pwm_init_t stcPwmInit; // 定义PWM初始化结构体 // 使用默认参数初始化PWM结构体 (void)TMRA_PWM_StructInit(&stcPwmInit); // 设置比较值,这个值直接决定PWM的占空比 // 占空比 = (比较值 / 周期值) * 100% // 这里初始设为2000,占空比就是 2000 / 10000 = 20% stcPwmInit.u32CompareValue = 2000; // 将PWM配置初始化到 TimerA Unit 5 的通道1 (CH1) (void)TMRA_PWM_Init(CM_TMRA_5, TMRA_CH1, &stcPwmInit);这里最重要的就是u32CompareValue(比较值)。在锯齿波模式下,当计数器向上数的值小于这个比较值时,PWM输出一种电平(比如高电平);当计数器值大于比较值但小于周期值时,输出另一种电平(低电平)。所以,比较值越大,高电平时间越长,占空比越大,LED越亮。
2.6 第六步:开启通道缓存功能(实现平滑变化的关键)
这是一个HC32F4A0非常实用的高级功能!通常我们改变PWM占空比,是直接修改比较寄存器。但如果在新旧值切换的瞬间,正好赶上计数器在刷新,可能会导致输出产生一个极窄的毛刺脉冲。
芯片提供了“比较值缓存”功能来避免这个问题。你可以把它理解为一个“双缓冲”机制:我们平时修改的是一个“后台”缓存寄存器,而定时器真正使用的是“前台”工作寄存器。只有在某个安全时刻(比如计数器归零的“谷底”),后台缓存的值才会一次性同步到前台,这样切换就非常平滑,不会产生毛刺。
在我们的例子里,TimerA的通道1(CH1)和通道2(CH2)可以配对,让CH2的比较寄存器作为CH1的缓存。
// 设置TimerA_5通道1的缓存传输条件为“计数器谷底”(即计数器归零时) TMRA_SetCompareBufCond(CM_TMRA_5, TMRA_CH1, TMRA_BUF_TRANS_COND_VALLEY); // 使能TimerA_5通道1的缓存功能 TMRA_CompareBufCmd(CM_TMRA_5, TMRA_CH1, ENABLE);开启这个功能后,我们后续修改占空比时,就应该去修改通道2(CH2)的比较值,这个值会自动在安全时刻同步给通道1(CH1),从而让我们实际使用的CH1输出非常干净平滑的PWM波。这是实现高质量呼吸灯效果的一个小秘诀。
2.7 第七步:使能定时器与PWM输出
所有参数都设好了,现在可以“开工”了。
// 使能 TimerA_5 通道1 的PWM输出功能 TMRA_PWM_OutputCmd(CM_TMRA_5, TMRA_CH1, ENABLE); // 启动 TimerA_5 计数器,开始工作 TMRA_Start(CM_TMRA_5);执行完这两行,PA02引脚就应该开始输出一个占空比为20%的固定PWM波了。如果你接好了LED,此时它应该会发出微弱的光。
3. 编写呼吸灯效果函数
静态的PWM没意思,咱们来让它“呼吸”起来。原理很简单:在一个循环里,不断改变PWM的比较值(占空比)。
库函数TMRA_SetCompareValue是用来动态修改比较值的:
void TMRA_SetCompareValue(CM_TMRA_TypeDef *TMRAx, uint32_t u32Ch, uint32_t u32Value);TMRAx: 定时器单元,我们的是CM_TMRA_5。u32Ch: 定时器通道。注意:由于我们开启了通道1的缓存,且缓存基准是通道2,所以这里我们应该修改通道2的值。u32Value: 要设置的新比较值,范围在0到周期值(10000)之间。
基于这个函数,我们来写呼吸灯函数:
/** * @brief 呼吸灯效果函数,调用一次完成一次“呼吸”(渐亮再渐灭) * @param None * @retval None * @note 由于开启了通道1的缓存(基准为通道2),所以修改的是通道2的比较值。 */ void pwm_breathing_lamp(void) { static uint32_t brightness; // 当前亮度值,对应比较值 uint32_t step = 10; // 每次亮度变化的步长,值越小变化越平滑,但循环时间越长 uint32_t delayTime = 1; // 每次变化后的延时(毫秒),控制呼吸速度 // 首先,将通道2的比较值设为一个较大的值(这里是0xFFFF), // 确保通道1在初始化后能从这个缓存值获取到一个明确的初始占空比。 // 你也可以根据实际情况调整或省略这一步。 TMRA_SetCompareValue(CM_TMRA_5, TMRA_CH2, 0xFFFF); // 渐亮过程:比较值从1000增加到9000 for(brightness = 1000; brightness < 9000; brightness += step) { TMRA_SetCompareValue(CM_TMRA_5, TMRA_CH2, brightness); // 修改通道2,通道1会自动同步 delay_ms(delayTime); // 稍作延时,让肉眼能观察到变化 } delay_ms(50); // 在最亮处稍作保持 // 渐灭过程:比较值从9000减少到1000 for(brightness = 9000; brightness > 1000; brightness -= step) { TMRA_SetCompareValue(CM_TMRA_5, TMRA_CH2, brightness); delay_ms(delayTime); } delay_ms(50); // 在最暗处稍作保持 // 函数结束,一次呼吸完成。可以在主循环中反复调用此函数。 }代码逻辑解析:
- 我们用一个
brightness变量代表亮度,它直接对应PWM的比较值。 - 第一个
for循环让比较值从1000逐步增加到9000。占空比从10%增加到90%,LED逐渐变亮。 - 第二个
for循环让比较值从9000逐步减少到1000。占空比从90%减少到10%,LED逐渐变暗。 - 每个循环中,调用
TMRA_SetCompareValue更新比较值,然后通过delay_ms函数等待一小段时间。step(步长)和delayTime(延时)这两个参数共同决定了呼吸的速度和平滑度。步长越小、延时越短,呼吸效果越平滑,但一个周期的耗时也越长。你可以根据实际效果调整它们。 - 记得我们在主循环里要不断地调用这个
pwm_breathing_lamp()函数。
4. 实验效果与代码获取
将完整的代码编译后下载到天空星开发板,你就能看到连接在PA02引脚的LED灯柔和地渐亮渐灭,就像在呼吸一样。
如果你在调试过程中,想观察比较值的变化,可以在for循环里加上printf语句打印brightness的值到串口,方便确认程序是否在正确运行。
本实验的完整代码例程,可以在天空星开发板的资料包中找到。路径通常为:
【HC32F4A0PITB版本】资料 -> 第03章软件资料 -> 代码例程 -> 009PWM呼吸灯。建议初学者先下载官方例程,在能够正常运- 行的基础上,再对照本文的讲解去理解每一行代码的作用,并尝试修改参数(如周期值、步长、延时)来观察不同的呼吸效果。这才是最快的学习方法。
