开发环境: IDE:MounRiver Studio MCU:GD32VF103RB
1、呼吸灯的工作原理呼吸灯,就是指灯光设备的亮度随着时间由暗到亮逐渐增强,再由亮到暗逐渐衰减,很有节奏感地一起一伏,就像是在呼吸一样,因而被广泛应用于 手机、电脑等 电子设备的指示灯中。 要使用数字器件控制灯光的强弱,我们很自然就想到 PWM(脉冲宽度调制)技术。假如以LED 作为灯光设备,且由控制器输出的 PWM 信号可以直接驱动 LED,PWM 信号中的低电平可点亮 LED 灯。当 LED 以较高的频率进行开关(亮灭)切换时,由于视觉暂留效应,人眼是看不到 LED 灯的闪烁现象的,反映到人眼中能感觉到的是亮度的差别。即以一定的时间长度为周期,LED 灯亮的平均时间越长,亮度就越高,反之越暗。因此,我们可以使用高频率的 PWM 信号,通过调制信号的占空比,控制 LED 灯的亮度。
2、呼吸灯实现
2.1简单方式
笔者先用最简单的方式来实现,也就是定时改变比较寄存器的值。
1.初始化 GPIO 下面分析具体的定时器配置代码。本实验使用 PA7 作为定时器 PWM 输出通道,先对它进行初始化。作 PWM 输出通道的引脚需要被配置为复用推挽输出模式。
- /*
- brief configure PWM GPIO
- param[in] none
- param[out] none
- retval none
- */
- static void timer_gpio_init(void)
- {
- /* enable the GPIOA clock */
- rcu_periph_clock_enable(RCU_GPIOA);
- /* Configure PA7(TIMER2_CH1) as alternatefunction */
- gpio_init(GPIOA, GPIO_MODE_AF_PP,GPIO_OSPEED_50MHZ, GPIO_PIN_7);
- }
复制代码
2.配置定时器模式 在timer2_init()函数中,完成了呼吸灯所需要的定时器 PWM 输出模式配置。
- /*
- brief configure the TIMER peripheral
- param[in] none
- param[out] none
- retval none
- */
- void timer2_init(void)
- {
- timer_oc_parameter_struct timer_ocintpara;
- timer_parameter_struct timer_initpara;
- rcu_periph_clock_enable(RCU_TIMER2);
- timer_deinit(TIMER2);
- timer_gpio_init();
- /* TIMER2 configuration */
- timer_initpara.prescaler = 107;
- timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
- timer_initpara.counterdirection = TIMER_COUNTER_UP;
- timer_initpara.period = 500;
- timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
- timer_initpara.repetitioncounter = 0;
- timer_init(TIMER2,&timer_initpara);
- /*CH1 configuration in PWM mode */
- timer_ocintpara.outputstate = TIMER_CCX_ENABLE;
- timer_ocintpara.outputnstate =TIMER_CCXN_DISABLE;
- timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
- timer_ocintpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
- timer_ocintpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
- timer_ocintpara.ocnidlestate =TIMER_OCN_IDLE_STATE_LOW;
- timer_channel_output_config(TIMER2,TIMER_CH_1, &timer_ocintpara);
- timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_1, 250);
- timer_channel_output_mode_config(TIMER2,TIMER_CH_1, TIMER_OC_MODE_PWM0);
- timer_channel_output_shadow_config(TIMER2,TIMER_CH_1, TIMER_OC_SHADOW_DISABLE);
- timer_primary_output_config(TIMER2, ENABLE);
- /* auto-reload preload enable */
- timer_auto_reload_shadow_enable(TIMER2);
- timer_enable(TIMER2);
- }
复制代码
这个定时器的模式配置主要分为三个部分,分别为时基初始化,输出模式初始化。
Ø 时基初始化 代码中前面的部分是定时器的时基初始化,这部分主要负责配置定时器的定时周期、时钟频率、计数方式等。它使用到库函数timer_init()函数,利用结构体timer_parameter_struct进行配置,该结构体有以下成员: 1) period 定时周期,实质是存储到重载寄存器CAR的数值,脉冲计数器从 0 累加到这个值上溢或从这个值自减至 0 下溢。这个数值加 1 然后乘以时钟源周期就是实际定时周期。 本实验中向该成员赋值为 255,即定时周期为(255+1)* T ,T 为定时器的时钟周期。 2) prescaler 对定时器时钟CLK 的预分频值,分频后作为脉冲计数器TIMERx_CNT的驱动时钟,得到脉冲计数器的时钟频率为:CNT=CLK/(N+1),其中 N 为即为赋给本成员的时钟分频值。 本实验给 prescaler 成员赋值为 107,即对时钟108分频,所以定时器的时钟周期 T 为 108/108000000。 3) clockdivision 时钟分频因子。怎么又出现一个配置时钟分频的呢?要注意这个clockdivision和上面的 prescaler 是不一样的。prescaler 预分频配置是对CLK进行分频,分频后的时钟被输出到脉冲计数器CNT。 本实验中是使用内部时钟CLK 作为定时器时钟源的,没有进行滤波所以配置clockdivision为任何数值都没有影响。 4) alignedmode 本成员配置的为脉冲计数器 CNT 的计数模式,分别为向上计数,向下计数,及中央对齐模式。向上计数即 CNT 从 0 向上累加到 period 中的值,(重载寄存器 CAR 的值),产生上溢事件;向下计数则 CNT 从period 的值累减至0,产生下溢事件。而中央对齐模式则为向上、向下计数的合体,CNT 从 0 累加到period 的值减 1 时,产生一个上溢事件,然后向下计数到 1 时,产生一个计数器下溢事件,再从 0 开始重新计数。
Ø 输出模式配置 在本函数代码的后面是关于定时器的输出模式配置的。通用定时器的输出模式由 timer_oc_parameter_struct类型结构体的主要有以下几个成员: 1) outputstate 配置输出模式的状态使能或关闭输出。 2) outputnstate 本成员的参数值即为比较寄存器 CH1CV的数值,当脉冲计数器CNT与CH0CV的比较结果发生变化时,输出脉冲将发生跳变。 3) ocpolarity 有效电平的极性,把 PWM 模式中的有效电平设置为高电平或低电平。 本实验中向该成员赋值为 TIMER_OC_POLARITY_LOW(有效电平为低电平),因为在上面把输出模式配置为 PWM0 模式,向上计数,所以在 CNT< CH0CV时,通道 n 输出为低电平,否则为高电平。 4) ocnpolarity 用于比较有效电平的极性。
本实验中就是通过不断改变比较寄存器CH1CV的值,达到控制 PWM 信号的占空比呈指数曲线变化的目的。在本函数代码中,我们对该成员赋予初始为 0,而改变比较寄存器 CH0CV 值的操作是在中断服务函数中修改的。填充完输出模式初始化结构体后,调用输出模式初始化函数 timer_channel_output_config()对通道进行初始化。
以上是最基本的PWM输出调制实现呼吸灯。
笔者接下来还要讲解一下重映射的输出配置。在这里讲解的是通过重映射 TIMER2_CH2到 PB0 上,由 TIMER2_CH1输出 PWM 来控制LED的亮度。下面我们介绍通过库函数来配置该功能的步骤。
1)开启 TIMER2时钟以及复用功能时钟,配置 PA7为复用输出。 要使用 TIMER2,我们必须先开启 TIMER2的时钟,这点相信大家看了这么多代码,应该明白了。这里我们还要配置 PA7为复用输出,此时,PA7属于复用功能输出。 rcu_periph_clock_enable(RCU_AF);
其余的和前面的配置一样,就不再列出了。
2)初始化 TIMER2,设置 TIMER2的 CAR 和 PSC。
3)设置 TIMER2_CH1的 PWM 模式,使能 TIMER2的 CH1输出。
4)使能 TIMER2。 在完成以上设置了之后,我们需要使能 TIMER2。 使能 TIMER2的方法前面已经讲解过: timer_enable(TIMER2);
5)修改 TIMER2_ CH1CV来控制占空比。 最后,在经过以上设置之后, PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改 TIMER2_CH1CV则可以控制 CH1的输出占空比。继而控制LED的亮度。在库函数中,修改 TIMER2_CH1CV占空比的函数是: void timer_channel_output_pulse_value_config(uint32_ttimer_periph, uint16_t channel, uint32_t pulse)
通过以上5个步骤,我们就可以控制 TIMER2的 CH2 输出 PWM 波了。 接下来看看主函数的代码:
- /*Includes*********************************************************************/
- #include "gd32vf103.h"
- #include"gd32vf103r_led_start.h"
- #include"gd32vf103r_systick_start.h"
- #include"gd32vf103r_timx_start.h"
- /*!
- brief main function
- param[in] none
- param[out] none
- retval none
- */
- int main(void)
- {
- int16_t i = 0;
- FlagStatus breathe_flag = SET;
- /* configure the TIMER peripheral */
- timer2_init();
- while(1)
- {
- /* delay a time in milliseconds */
- delay_ms(40);
- if(SET == breathe_flag)
- {
- i = i + 10;
- }
- else
- {
- i = i - 10;
- }
- if(500 < i)
- {
- breathe_flag = RESET;
- }
- if(0 >= i)
- {
- breathe_flag = SET;
- }
- /* configure TIMER channel output pulsevalue */
- //timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_1, i);
- TIMER_CH1CV(TIMER2) = i;
- }
- }
复制代码
代码很简单,就是不断改变CH1CV的值从而控制 CH1的输出占空比。
2.2 中断方式1.生成指数曲线 PWM 数据 要实现 LED 亮度随着指数曲线变化,我们需要使用占空比呈指数曲线变化的 PWM 信号,而这样的信号由定时器经过查表产生。这个表的数据存储在程序中的数组 indexWave中。
uint8_tindexWave[] = {1,1,2,2,3,4,6,8,10,14,19,25,33,44,59,80, 107,143,191,255,255,191,143,107,80,59,44,33,25,19,14,10,8,6,4,3,2,2,1,1}; 这个表有 40 个数字,从图中可以看到这些数字呈指数上升再衰减,正好是呼吸灯的一个控制周期。数字的大小范围是 0~255,即把 LED 的亮度分为了 0~255 个等级。
假如我们把定时器的脉冲计数器 CNT 上限设置为 255,把这个表的数据一个一个地赋值到定时器的比较寄存器CH2CV中,那么在每个 PWM 周期中,当 CNT的计数值小于比较寄存器 CH2CV的值时, 就会在通道中输出低电平,点亮 LED,而随着 CCR 的值由 LED 亮度表得来,所以 LED 点亮的时间就会呈图中的曲线变化,实现呼吸灯的功能。
2.初始化 GPIO 这部分和前面的一样,没啥好说的。
3.配置定时器模式 这里也差不多,只是将分频系数设置的稍微大些,另外开启了中断。
- /*
- brief configure the TIMER peripheral
- param[in] none
- param[out] none
- retval none
- */
- void timer2_init(void)
- {
- timer_oc_parameter_struct timer_ocintpara;
- timer_parameter_struct timer_initpara;
- /* configure the GPIO ports */
- timer_gpio_init();
- rcu_periph_clock_enable(RCU_TIMER2);
- timer_deinit(TIMER2);
- /* TIMER2 configuration */
- timer_initpara.prescaler = 3999;
- timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
- timer_initpara.counterdirection = TIMER_COUNTER_UP;
- timer_initpara.period = 255;
- timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
- timer_initpara.repetitioncounter = 0;
- timer_init(TIMER2, &timer_initpara);
- /* CH0 configuration in PWM mode 0 */
- timer_ocintpara.outputstate = TIMER_CCX_ENABLE;
- timer_ocintpara.outputnstate =TIMER_CCXN_DISABLE;
- timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
- timer_ocintpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
- timer_ocintpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
- timer_ocintpara.ocnidlestate =TIMER_OCN_IDLE_STATE_LOW;
- timer_channel_output_config(TIMER2,TIMER_CH_1, &timer_ocintpara);
- /* CH0 configuration in PWM mode 0,dutycycle 25% */
- timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_1, 0);
- timer_channel_output_mode_config(TIMER2,TIMER_CH_1, TIMER_OC_MODE_PWM0);
- timer_channel_output_shadow_config(TIMER2,TIMER_CH_1, TIMER_OC_SHADOW_DISABLE);
- timer_primary_output_config(TIMER2,ENABLE);
- /* auto-reload preload enable */
- timer_auto_reload_shadow_enable(TIMER2);
- /* Timer2 interrupt setting, preemptivepriority 0, sub-priority 2 */
- eclic_global_interrupt_enable();
- eclic_set_nlbits(ECLIC_GROUP_LEVEL3_PRIO1);
- eclic_irq_enable(TIMER2_IRQn,1,0);
- /* Enable Timer2 update interrupt */
- timer_interrupt_enable(TIMER2,TIMER_INT_UP);
- /* TIMER2 enable */
- timer_enable(TIMER2);
- }
复制代码
配置好中断,下面就要编写中断服务函数。
- /*!
- brief this function handles TIMER2 exception
- param[in] none
- param[out] none
- retval none
- */
- void TIMER2_IRQHandler(void)
- {
- static uint8_t pwm_index = 0; //用于PWM查表
- static uint8_t period_cnt = 0; //用于计算周期数
- if(timer_interrupt_flag_get(TIMER2,TIMER_INT_FLAG_UP))
- {
- /* 清除TIMER2 中断标志位 */
- timer_interrupt_flag_clear(TIMER2,TIMER_INT_FLAG_UP);
- period_cnt++;
- if(period_cnt >= 10) //若输出的周期数大于10,输出下一种脉冲宽的PWM波
- {
- //根据PWM表修改定时器的比较寄存器值
- TIMER_CH1CV(TIMER2) =indexWave[pwm_index];
- pwm_index++; //标志PWM表的下一个元素
- //若PWM脉冲表已经输出完成一遍,重置PWM查表标志
- if( pwm_index >= 40)
- {
- pwm_index=0;
- }
- period_cnt=0; //重置周期计数标志
- }
- }
- }
复制代码
本中断服务函数在每次定时器更新事件发生时执行一次(即 256 个定时器时钟周期)。函数中使用了静态变量 pwm_index 和 period_cnt,它们分别用来查找 PWM 表元素和记录同样占空比的脉冲输出了多少次。
本代码的目的是每 10 次定时器中断更新一次 PWM 表中的数据到比较寄存器中,当遍历完 PWM 表的 40 个元素时,再重头开始遍历 PWM 表,周而复始,重复 LED 的呼吸过程。
整个呼吸过程的时间计算方法如下: 因为定时器的 prescaler 设置为 3999; 所以定时器的时钟频率:fTIMER= 108000000/(prescaler+1) = 27000 Hz 即定时器的时钟周期为:tTIMER= 1/fTIMER = 1/27000 s 因为定时器的 period 设置为 255; 所以定时器的中断周期为:tint= tTIMER* (period+1) =0.00948s 因为 PWM 表有 pwm_index = 40 个亮度占空比数据,同种占空比信号输出 period_cnt =10 次 所以一个呼吸周期 T = tint *40 *10= 3.792s
3、呼吸灯的实验现象将程序编译好下载到板子中,可一看到LED1像呼吸一样渐渐变明或者渐渐变暗,但是方法二明显比方法一更流畅,效果更好。
|