天天看點

《嵌入式 – GD32開發實戰指南》第9章 呼吸燈

開發環境:

MDK:Keil 5.30

開發闆:GD32F207I-EVAL

MCU:GD32F207IK

9.1呼吸燈的工作原理

呼吸燈,就是指燈光裝置的亮度随着時間由暗到亮逐漸增強,再由亮到暗逐漸衰減,很有節奏感地一起一伏,就像是在呼吸一樣,因而被廣泛應用于手機、電腦等電子裝置的訓示燈中。

要使用數字器件控制燈光的強弱,我們很自然就想到 PWM(脈沖寬度調制)技術。假如以LED 作為燈光裝置,且由控制器輸出的 PWM 信号可以直接驅動 LED,PWM 信号中的低電平可點亮 LED 燈。當 LED 以較高的頻率進行開關(亮滅)切換時,由于視覺暫留效應,人眼是看不到 LED 燈的閃爍現象的,反映到人眼中能感覺到的是亮度的差别。即以一定的時間長度為周期,LED 燈亮的平均時間越長,亮度就越高,反之越暗。是以,我們可以使用高頻率的 PWM 信号,通過調制信号的占空比,控制 LED 燈的亮度。

那麼具體我們應該控制 LED 燈以怎樣的亮度曲線變化能夠達到最好的效果呢?亮度随着時間逐漸變強再衰減,可以用兩種常見的數學函數表示,分别是半個周期的正弦函數與指數上升曲線及其對稱得到的下降曲線。

《嵌入式 – GD32開發實戰指南》第9章 呼吸燈

相對來說,使用下凹函數曲線燈光處于暗的狀态更長,是以指數函數的曲線更符合我們呼吸燈的亮度變化要求。

9.2呼吸燈實作

9.2.1簡單方式

筆者先用最簡單的方式來實作,也就是定時改變比較寄存器的值。

1.初始化 GPIO

下面分析具體的定時器配置代碼。本實驗使用 PB0 作為定時器 PWM 輸出通道,先對它進行初始化。作 PWM 輸出通道的引腳需要被配置為複用推挽輸出模式。

/*
    brief      configure PWM GPIO
    param[in]  none
    param[out] none
    retval     none
*/
static void timer_gpio_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOB);
    rcu_periph_clock_enable(RCU_AF);

    /* Configure PB0 (TIMER2 CH2) as alternate function */
    gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
}      

2.配置定時器模式

在timer2_init()函數中,完成了呼吸燈所需要的定時器 PWM 輸出模式配置。

/*
    brief      configure the TIMER peripheral
    param[in]  none
    param[out] none
    retval     none
  */
void timer2_init(void)
{
    /* TIMER1 configuration: generate PWM signals with different duty cycles:
       TIMER1CLK = SystemCoreClock / 120 = 1MHz */
    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         = 119;
    timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
    timer_initpara.counterdirection  = TIMER_COUNTER_UP;
    timer_initpara.period            = 250;
    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_2, &timer_ocintpara);

    /* CH0 configuration in PWM mode 0,duty cycle 25% */
    timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_2, 0);
    timer_channel_output_mode_config(TIMER2, TIMER_CH_2, TIMER_OC_MODE_PWM0);
    timer_channel_output_shadow_config(TIMER2, TIMER_CH_2, TIMER_OC_SHADOW_DISABLE);

    /* auto-reload preload enable */
    timer_auto_reload_shadow_enable(TIMER2);
    /* TIMER2 enable */
    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 成員指派為 119,即對時鐘 120 分頻,是以定時器的時鐘周期 T 為 120/120000000。

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

本成員的參數值即為比較寄存器 CH0CV的數值,當脈沖計數器CNT與CH0CV的比較結果發生變化時,輸出脈沖将發生跳變。

3) ocpolarity

有效電平的極性,把 PWM 模式中的有效電平設定為高電平或低電平。

本實驗中向該成員指派為 TIMER_OC_POLARITY_LOW (有效電平為低電平),因為在上面把輸出模式配置為 PWM0 模式,向上計數,是以在 CNT< CH0CV 時,通道 n 輸出為低電平,否則為高電平。

4) ocnpolarity

用于比較有效電平的極性。

本實驗中就是通過不斷改變比較寄存器CH0CV的值,達到控制 PWM 信号的占空比呈指數曲線變化的目的。在本函數代碼中,我們對該成員賦予初始為 0,而改變比較寄存器 CH0CV 值的操作是在中斷服務函數中修改的。填充完輸出模式初始化結構體後,調用輸出模式初始化函數 timer_channel_output_config()對通道進行初始化。

以上是最基本的PWM輸出調制實作呼吸燈。

筆者接下來還要講解一下重映射的輸出配置。在這裡講解的是通過重映射 TIMER2_CH2到 PB0 上,由 TIMER2_CH2 輸出 PWM 來控制LED的亮度。下面我們介紹通過庫函數來配置該功能的步驟。

1)開啟 TIMER2時鐘以及複用功能時鐘,配置 PB0為複用輸出。

要使用 TIMER2,我們必須先開啟 TIMER2的時鐘,這點相信大家看了這麼多代碼,應該明白了。這裡我們還要配置 PB0為複用輸出,這是因為TIMER2_CH2 通道将重映射到 PB0上,此時,PB0屬于複用功能輸出。在此隻列出庫函數設定 AFIO 時鐘的方法。

rcu_periph_clock_enable(RCU_AF);      

其餘的和前面的配置一樣,就不再列出了。

2)初始化 TIMER2,設定 TIMER2的 CAR 和 PSC。

3)設定 TIMER2_CH2 的 PWM 模式,使能 TIMER2的 CH2 輸出。

4)使能 TIMER2。

在完成以上設定了之後,我們需要使能 TIMER2。 使能 TIMER2的方法前面已經講解過:

timer_enable(TIMER2);      

5)修改 TIMER2_ CH0CV來控制占空比。

最後,在經過以上設定之後, PWM 其實已經開始輸出了,隻是其占空比和頻率都是固定的,而我們通過修改 TIMER2_CH2CV則可以控制 CH2 的輸出占空比。繼而控制LED的亮度。在庫函數中,修改 TIMER2_CH2CV占空比的函數是:

void timer_channel_output_pulse_value_config(uint32_t timer_periph, uint16_t channel, uint32_t pulse)      

通過以上5個步驟,我們就可以控制 TIMER2的 CH2 輸出 PWM 波了。

接下來看看主函數的代碼:

/* Includes*********************************************************************/
#include "gd32f2_systick.h"
#include "gd32f2_timx.h"

/*
    brief      main function
    param[in]  none
    param[out] none
    retval     none
*/
int main(void)
{
    uint16_t i = 0;
    FlagStatus breathe_flag = SET;

    //systick init
    sysTick_init();

    /* configure the TIMER peripheral */
    timer2_init();

    while(1)
    {
        /* delay a time in milliseconds */
        delay_ms(5);
        if(SET == breathe_flag) 
                {
            i++;
        }
        else
        {
            i--;
        }
        if(250 < i)
        {
            breathe_flag = RESET;
        }
        if(0 >= i)
        {
            breathe_flag = SET;
        }
        /* configure TIMER channel output pulse value */
        //timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_2, i);
        TIMER_CH2CV(TIMER2) = (uint32_t)i;
    }
}      

代碼很簡單,就是不斷改變CH2CV的值進而控制 CH2 的輸出占空比。

9.2.2中斷方式

1.生成指數曲線 PWM 資料

要實作 LED 亮度随着指數曲線變化,我們需要使用占空比呈指數曲線變化的 PWM 信号,而這樣的信号由定時器經過查表産生。這個表的資料存儲在程式中的數組 indexWave中。

uint8_t indexWave[] = {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 點亮的時間就會呈圖中的曲線變化,實作呼吸燈的功能。

這個表的資料是使用 matlab 軟體生成的。該代碼運作後會生成一個“index_wave.c”的檔案,使用者把該檔案中的資料複制到工程中的數組中即可。

%本代碼用于産生呼吸燈使用的指數函數資料
clear;

x = [0 : 8/19 : 8];       %設定序列 ,指數上升
up = 2.^x ;               %求上升指數序列  
up = uint8(up);           %化為8位資料

y = [8: -8/19 :0];       %設定序列 ,指數下降
down = 2.^y ;            %求下降指數序列
down = uint8(down);      %化為8位資料

line = [[0:8/19:8],[8:8/19:16]]         %拼接序列
val = [up , down]                       %拼接輸出序列

dlmwrite('index_wave.c',val);       %輸出到檔案index_wave.c
plot(line,val,'.');                 %顯示波形圖      

2.初始化 GPIO

這部分和前面的一樣,沒啥好說的。

3.配置定時器模式

這裡也差不多,隻是将分頻系數設定的稍微大些,另外開啟了中斷。

/*
    brief      configure the TIMER peripheral
    param[in]  none
    param[out] none
    retval     none
  */
void timer2_init(void)
{
    /* TIMER1 configuration: generate PWM signals with different duty cycles:
       TIMER1CLK = SystemCoreClock / 120 = 1MHz */
    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(TIMER7);

    /* TIMER1 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_2, &timer_ocintpara);

    /* CH0 configuration in PWM mode 0,duty cycle 25% */
    timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_2, 0);
    timer_channel_output_mode_config(TIMER2, TIMER_CH_2, TIMER_OC_MODE_PWM0);
    timer_channel_output_shadow_config(TIMER2, TIMER_CH_2, TIMER_OC_SHADOW_DISABLE);

    /* auto-reload preload enable */
    timer_auto_reload_shadow_enable(TIMER2);
    
    /* Timer2 interrupt setting, preemptive priority 0, sub-priority 2 */
    nvic_irq_enable(TIMER2_IRQn, 0, 2); 
    
    /* 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))
  {
    /* 清除TIMER5 中斷标志位 */
    timer_interrupt_flag_clear(TIMER2, TIMER_INT_FLAG_UP);
    
    period_cnt++;
    if(period_cnt >= 10)  //若輸出的周期數大于10,輸出下一種脈沖寬的PWM波
    {
        //根據PWM表修改定時器的比較寄存器值
        TIMER_CH2CV(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;

是以定時器的時鐘頻率:

即定時器的時鐘周期為:

因為定時器的 period 設定為 255;

是以定時器的中斷周期為:

因為 PWM 表有 pwm_index = 40 個亮度占空比資料,同種占空比信号輸出 period_cnt =10 次

是以一個呼吸周期

9.3呼吸燈的實驗現象