NXP

在BLE芯片QN9021上实现呼吸灯效果

2019-07-12 12:51发布

        本文记述在低功耗蓝牙芯片QN9021上,利用PWM+定时器实现呼吸灯效果的过程,以及过程的一点心得体会。QN9021是NXP的一款低功耗蓝牙SoC芯片,集成了一个Cortex-M0内核,这里没有用到它的蓝牙功能,因此把它当作一个M0核的单片机即可。        呼吸灯是指灯的亮度由暗逐渐变亮,然后由亮逐渐变暗的一种视觉效果,因为变化的过程与人的呼吸节奏相似,因此称为“呼吸灯”。其基本原理是不断调整PWM的占空比,从而达到灯的亮度渐变的效果。这里有3个比较重要的值:1)PWM的周期        PWM的周期越大,频率越小,闪烁感越强烈。一般人眼能觉察的闪烁频率最大为50Hz,即只要频率大于这个值,则人眼看不出明显的闪烁。但是PWM的频率越大,对单片机定时器的要求也就越高,因此需要根据单片机的性能,合理选择PWM的频率。经过试验,我发现PWM频率取1KHz是比较合适的,即PWM周期为1000us。2)占空比的调整周期        上文说到,亮度渐变的效果是通过不断调整PWM占空比实现,但要以什么样的时间间隔来调整占空比,需要仔细推敲。如果间隔太小,接近甚至等于PWM的周期,则容易造成PWM时序紊乱。间隔太大,亮度过度不平滑,会有阶梯感。经过试验,调整周期取10ms是比较合适的,即1s内调整100次。3)调整的步长        在“吸气”阶段,高电平的占空比是不断增加的。在“呼气”阶段,高电平的占空比不断减小。但是每次增加或减少多少合适呢?由于PWM的周期我取的是1000us,因此高电平的宽度调整范围是0—1000。需要注意的是,在某些单片机上,占空比不能设置为0(使用单片机自带的PWM外设的情况,如果使用定时器实现PWM则另当别论)。        从0开始,如果每次增加1,则需要调整1000次,才能达到最亮。而调整一次的时间间隔是10ms,也就是说要花费10s的时间,才能从最暗到最亮。这显然与人的呼吸节奏不匹配。若每次增加5,则需要5s,仍然有些长。结合人的呼吸节奏,“吸气”时间为1s比较合适,即高电平取值每次增加10。“呼气”时间比“吸气”时间略长,我这里取1:1.5,即“呼气”时长为1.5s,换算下来呼气阶段高电平宽度每次减少7,大致符合这个比例关系。每次“呼吸”之间,有短暂的停顿,取1—2s均可。        下面说说一次完整的“呼吸”过程分为哪几个步骤。我这里大致分为三个步骤:        第1步:吸气过程,高电平宽度从0开始,每10ms增加10,调整100次达到最大,耗时1s。        第2步:呼气过程,高电平从1000开始,每10ms减少7,调整约140次达到最小,耗时约1.4s。        第3步:停顿过程,熄灭LED(因为占空比最低时,LED不一定是灭的),等待1s,然后开始下一个呼吸周期。        接下来看下呼吸灯的软件算法。要实现呼吸灯的效果,要求单片机有2个定时器,或者1个定时器+1个PWM。其中,一个定时器用于产生PWM,另一个定时器提供占空比调整和停顿延时需要的时基。其实,只要单片机的定时器精度够高,两个功能复用同一个定时器,也是可以的,这里不详细说,读者可以自行尝试。我这里使用的是QN9021的Timer1提供10ms定时,Timer2的PWM模式来控制LED。部分代码如下:/* 全局变量定义 */ uint8_t g_10ms_flag = 0; // 10ms定时时间到的标志 uint8_t g_step = 1; // 用于标识呼吸过程进行到第几步 uint8_t g_wait_cnt = 0; // 用于停顿过程的计时 uint16_t g_pwm_h = 0; // 用于记录PWM高电平的宽度 /* 主函数 */ int main(void) { timer_init(QN_TIMER1, timer1_irq_callback); // Timer1初始化 timer_init(QN_TIMER2, NULL); // Timer2初始化 // Timer1定时产生10ms中断,通过置位g_10ms_flag变量体现 timer_config(QN_TIMER1, TIMER_PSCAL_DIV, TIMER_COUNT_MS(10, TIMER_PSCAL_DIV)); // Timer2产生PWM输出,已提前将输出引脚配置为P26 timer_pwm_config(QN_TIMER2, TIMER_PSCAL_DIV, // 设置定时器预分频 TIMER_COUNT_US(1000, TIMER_PSCAL_DIV), // 设置PWM周期为1000us TIMER_COUNT_US(1, TIMER_PSCAL_DIV)); // 设置初始高电平宽度为1us timer_enable(QN_TIMER1, MASK_ENABLE); // 启动Timer1 timer_enable(QN_TIMER2, MASK_ENABLE); // 启动Timer2 while(1) { if(g_10ms_flag == 1) // 判断10ms定时是否到 { g_10ms_flag = 0; if(g_step == 1) // 当前过程为第一阶段 { if(g_pwm_h <= 1000) { g_pwm_h += 10; // 高电平按10us步长增加 // 占空比更新为新的值 timer_pwm_config(QN_TIMER2, TIMER_PSCAL_DIV, TIMER_COUNT_US(1000, TIMER_PSCAL_DIV), TIMER_COUNT_US(g_pwm_h, TIMER_PSCAL_DIV)); timer_enable(QN_TIMER2, MASK_ENABLE); // 重新启动Timer2 } else // 高电平宽度达到最大 { g_step = 2; // 转入第二阶段 } } else if(g_step == 2) // 当前过程为第二阶段 { if(g_pwm_h >= 7) { g_pwm_h -= 7; // 高电平按7us步长减小 // 占空比更新为新的值 timer_pwm_config(QN_TIMER2, TIMER_PSCAL_DIV, TIMER_COUNT_US(1000, TIMER_PSCAL_DIV), TIMER_COUNT_US(g_pwm_h, TIMER_PSCAL_DIV)); timer_enable(QN_TIMER2, MASK_ENABLE); // 重新启动Timer2 } else // 高电平宽度达到最小 { g_step = 3; // 转入第三阶段,重置相关变量值 g_pwm_h = 0; g_wait_cnt = 100; // 设置停顿时长为10ms*100=1s } } else if(g_step == 3) // 当前过程为第三阶段,禁能Timer2和LED { timer_enable(QN_TIMER2, MASK_DISABLE); gpio_write_pin(LED5_PIN, (enum gpio_level)GPIO_HIGH); } } } }/* Timer1中断回调函数 */ void timer1_irq_callback(void) { g_10ms_flag = 1; // 10ms定时周期到,置位标志变量 if(g_step == 3) // 当前处于第三阶段,计时变量递减 { g_wait_cnt--; if(g_wait_cnt == 0) // 计时变量减为0,等待结束 { g_pwm_h = 0; // 重置相关变量,重新开始下一个呼吸周期 g_step = 1; } } }