
十八、基于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的通道1CH1和通道2CH2可以配对让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);开启这个功能后我们后续修改占空比时就应该去修改通道2CH2的比较值这个值会自动在安全时刻同步给通道1CH1从而让我们实际使用的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呼吸灯。建议初学者先下载官方例程在能够正常运- 行的基础上再对照本文的讲解去理解每一行代码的作用并尝试修改参数如周期值、步长、延时来观察不同的呼吸效果。这才是最快的学习方法。