FireBLE驱动第一篇:呼吸灯

本帖最后由 安安 于 2015-4-8 17:33 编辑

[postbg]bg2.png[/postbg]尊重所有单片机开发的流程,我学习的第一个程序从来都是流水灯,我也从没有腻过,哈哈。但是由于FireBLE载的LED只有三个,不能再像当初51单片机那样的一排LED玩顺序亮灭和拖尾跑马灯等等了,恰好发现LED1的IO口是可以复用于PWM的,于是决定做一个前几年非常时髦的呼吸灯了。

我最早见到的呼吸灯是使用于翻盖手机上,思绪一下子回到了初中的课堂,老师来到教室第一件事情就是把手机摆放在讲台上最显眼的位置,呵呵,那时候手机是很高档的东西,不像现在这样满大街啊。那时候上课爱走神,就经常看着老师的手机的呼吸灯,一呼一吸的,很有规律,感觉很有趣,当时我还不知道那是有未读通知的一个提示,还一直在猜想为什么它有时候一呼一吸的,有些时候就不呼吸。呼吸灯确实是一个有趣而实用的发明,在我们手机调成静音并且取消震动的时候,又或者我们在把手机放在桌面上充电又有事离开而暂时错过了一个电话的时候,呼吸灯能很好的提醒我们刚才手机有过来电或者短信,这样不至于我们遗漏一些比较重要的信息。而有节奏的呼吸又使得这样的提示既不单调而又生动有趣,比起突然间的闪一下要给人更舒服的体验。

那么呼吸灯应该怎么样做呢?呼吸灯可以分为硬件实现的和软件实现的,本章介绍软件上如何实现呼吸灯,有兴趣的也可以查看相关的硬件呼吸灯的资料。呼吸灯主要靠PWM波来实现对LED电压的控制,所以实验的大致方向应该是先初始化系统,配置IO口,然后再在main函数中调节PWM参数以实现渐熄和渐亮的效果。

1.系统初始化。
首先将源码中的PWM例程拷贝出来,打开。

<font color="#000">int main (void)
{
    SystemInit();

    pwm_init(PWM_CH0);
    pwm_io_config();
        .
        .
        .

    while (1)                                /* Loop forever */
    {

    }
}</font>

例程中系统初始化中分为三个大模块,第一个模块Sub module clock setting的作用是设置时钟并且分配时钟;第二个模块的主要功能是缺省设置GPIO的状态;第三个模块看情况而定,由于本例程不带任何外设,所以不做外设的初始化。

void SystemInit(void)
{
    /*
     **************************
     * Sub module clock setting
     **************************
     */
    syscon_SetIvrefX32WithMask(QN_SYSCON, SYSCON_MASK_DVDD12_SW_EN, MASK_ENABLE);
    syscon_set_sysclk_src(CLK_XTAL, __XTAL);

#if __XTAL == XTAL_32MHz
    calibration_init(XTAL_32M);
#else
    calibration_init(XTAL_16M);
#endif
    ref_pll_calibration();<p></p>
<p style="line-height: 30px; text-indent: 2em;">    clk32k_enable(__32K_TYPE);
#if QN_32K_RCO
    rco_calibration();
#endif</p>
<p style="line-height: 30px; text-indent: 2em;">    // Disable all peripheral clock, will be enabled in the driver initilization.
    timer_clock_off(QN_TIMER0);
    timer_clock_off(QN_TIMER1);
    timer_clock_off(QN_TIMER2);
    timer_clock_off(QN_TIMER3);
    uart_clock_off(QN_UART0);
    uart_clock_off(QN_UART1);
    spi_clock_off(QN_SPI0);
    usart_reset((uint32_t) QN_SPI1);
    spi_clock_off(QN_SPI1);
    flash_clock_off();
    gpio_clock_off();
    adc_clock_off();
    dma_clock_off();
    pwm_clock_off();

    // calibration will change system clock setting
    // Configure sytem clock.
    syscon_set_sysclk_src(CLK_XTAL, __XTAL);
    syscon_set_ahb_clk(__AHB_CLK);
    syscon_set_apb_clk(__APB_CLK);
    syscon_set_ble_clk(__BLE_CLK);
    syscon_set_timer_clk(__TIMER_CLK);
    syscon_set_usart_clk((uint32_t)QN_UART0, __USART_CLK);
    syscon_set_usart_clk((uint32_t)QN_UART1, __USART_CLK);
   </p>
<p style="line-height: 30px; text-indent: 2em;">    /*
     **************************
     * IO configuration
     **************************
     */</p>
<p style="line-height: 30px; text-indent: 2em;">    SystemIOCfg();</p>
<p style="line-height: 30px; text-indent: 2em;">    /*
     **************************
     * Peripheral setting
     **************************
     */</p>
<p style="line-height: 30px; text-indent: 2em;">}</p>

跟着Demo例程往下,是PWM_CH0的初始化,主要工作是开启PWM时钟,设置PWM的中断等等。

void pwm_init(enum PWM_CH pwmch)
{
    uint32_t mask = 0, reg = 0;

    pwm_clock_on();

#if CONFIG_ENABLE_DRIVER_PWM0==TRUE
    if (pwmch == PWM_CH0) {
        mask = PWM_MASK_CH0_POL|PWM_MASK_CH0_IE|PWM_MASK_CH0_EN;
        #if CONFIG_PWM0_ENABLE_INTERRUPT==TRUE
        reg |= PWM_MASK_CH0_IE;  // enable PWM ch0 int
        NVIC_EnableIRQ(PWM0_IRQn);
        #endif
    }
#endif

#if CONFIG_ENABLE_DRIVER_PWM1==TRUE
    if (pwmch == PWM_CH1) {
        mask = PWM_MASK_CH1_POL|PWM_MASK_CH1_IE|PWM_MASK_CH1_EN;
        #if CONFIG_PWM1_ENABLE_INTERRUPT==TRUE
        reg |= PWM_MASK_CH1_IE;  // enable PWM ch1 int
        NVIC_EnableIRQ(PWM1_IRQn);
        #endif
    }
#endif

    pwm_pwm_SetCRWithMask(QN_PWM, mask, reg);  // config PWM
}

接下来就是GPIO的配置了。

配置GPIO的方法有两种,一种是syscon_SetPMCRx,一种是syscon_SetPMCRxWithMask。由注释可见,syscon_SetPMCR0中在一个32位的寄存器PMCR0中可配置GPIO[0]~GPIO[15]共16个IO的功能复用选择,对应和P0组和P1组IO口的配置。对于syscon_SetPMCRx来说,配置的方法就是简单的赋值,适合在缺省设置或者一些简单的例程中使用,因为这样的赋值会覆盖掉其他IO口的配置状态,引发潜在危机。对于syscon_SetPMCRxWithMask呢,就是比较柔和的了,利用掩码赋值可以在不影响其他IO的情况下,对自己需要更改配置的IO口进行配置。

/**
 *
 * @param *SYSCON SYSCON Pointer
 * @param value The value to set to PMCR0 register
 *
 * @brief  outputs specified value to PMCR0 register
 *
-            00(Default)     01              10              11
- [1:0]      GPIO[0](I/O)    UART0_TXD(O)    SPI0_DAT(I/O)   RTCI(I)           P0_0
- [3:2]      GPIO[1](I/O)    NC              SPI0_CS0(I/O)   UART0_CTSn(I)     P0_1
- [5:4]      GPIO[2](I/O)    I2C_SDA(I/O)    SPI0_CLK(I/O)   UART0_RTSn(O)     P0_2
- [7:6]      GPIO[3](I/O)    RADIO_EN(O)     CLKOUT0(O)      TIMER0_eclk(I/O)  P0_3
- [9:8]      GPIO[4](I/O)    NC              CLKOUT1(O)      RTCI(I)           P0_4
- [11:10]    GPIO[5](I/O)    I2C_SCL(I/O)    ADCT(I)         ACMP1_O(O)        P0_5
- [13:12]    SW_DAT(I/O)     GPIO[6](I/O)    AIN2(AI)        ACMP1-(AI)        P0_6
- [15:14]    SW_CLK(I)       GPIO[7](I/O)    AIN3(AI)        ACMP1+(AI)        P0_7
- [17:16]    GPIO[8](I/O)    SPI1_DIN(I)     UART1_RXD(I)    TIMER2_eclk(I/O)  P1_0
- [19:18]    GPIO[9](I/O)    SPI1_DAT(I/O)   UART1_TXD(O)    TIMER1_0(I/O)     P1_1
- [21:20]    GPIO[10](I/O)   SPI1_CS0(I/O)   UART1_CTSn(I)   ADCT(I)           P1_2
- [23:22]    GPIO[11](I/O)   SPI1_CLK(I/O)   UART1_RTSn(O)   CLKOUT1(O)        P1_3
- [25:24]    GPIO[12](I/O)   RDYN(O)         NC              TIMER1_3(I/O)     P1_4
- [27:26]    GPIO[13](I/O)   RADIO_EN(O)     PWM1(O)         TIMER1_2(I/O)     P1_5
- [29:28]    GPIO[14](I/O)   SPI0_CS1_O(O)   PWM0(O)         TIMER0_3(I/O)     P1_6
- [31:30]    GPIO[15](I/O)   UART0_RXD(I)    SPI0_DIN(I)     TIMER0_o(O)       P1_7
*/
 __STATIC_INLINE void syscon_SetPMCR0(QN_SYSCON_TypeDef *SYSCON, uint32_t value)
 {
       SYSCON->PMCR0 = value;
 }

 __STATIC_INLINE void syscon_SetPMCR0WithMask(QN_SYSCON_TypeDef *SYSCON, uint32_t mask, uint32_t value)
 {
       uint32_t reg;

       reg = SYSCON->PMCR0;
       reg &= ~mask;
       reg |= (mask&value);
       SYSCON->PMCR0 = reg;
 }

配置完GPIO后,接下来就是PWM的配置了。其中第二项的分频系数最大为0x3ff,因此在8M的晶振下,如果使用默认定时器时钟,是不能做到比较低频率的PWM波的,只能做到毫秒级。当然如果有需求可以增加定时器时钟分频或者更换时钟源来达到秒级的PWM频率。由于呼吸灯中PWM频率越高控制越细腻,所以我们无须修改时钟,以1KHz的PWM波进行调制即可。经过分析,第三项参数为PWM波的周期,第四项为脉冲宽度。如果第四项大于第三项的话,将会倒置PWM波的输出电平。

uint8_t pwm_config(enum PWM_CH pwmch, uint16_t pscal, uint8_t periodcount, uint8_t pulsecount)

最后是PWM通道的输出选通。

pwm_enable(PWM_CH0, MASK_ENABLE);

有了以上的了解,我们就可以对呼吸灯进行设计了。首先是系统初始化和PWM的IO配置。

int main (void)
{
    SystemInit();

    pwm_init(PWM_CH0);
    pwm_io_config();

    while (1)                                /* Loop forever */
    {
            led_breath();
    }
}

接下来在while循环中不断的改变呼吸灯的占空比,实现呼吸效果。实现呼吸效果,首先要明白LED灯的电压与电流的关系。由于LED的特性,电流与电压的的关系如下图所示,由于LED灯的电流决定了LED的亮度,所以,我们需要找出LED的开启电压。

为了找出开启电压,我 决定用一直递增pwm的脉宽,直至LED点亮,然后记录当前脉宽,这样的话我需要一定的串口调试,于是我还需要加入串口的内容。加入串口头文件,初始化串口0,并且配置uart的IO。之前提到PWM的GPIO配置会影响到其他IO口的配置,所以,我们还需要把例程的配置方法改过来。

串口初始化

  uart_init(QN_UART0,USARTx_CLK(0),UART_9600);
  uart_tx_enable(QN_UART0, MASK_ENABLE);
  uart_rx_enable(QN_UART0, MASK_ENABLE);
  uart_io_config();

pwm通道IO配置

void pwm_io_config(void)
{
    // pin mux
    syscon_SetPMCR0WithMask(QN_SYSCON,
                P07_MASK_PIN_CTRL
               | P06_MASK_PIN_CTRL
               ,P07_SW_CLK_PIN_CTRL
                             | P06_SW_DAT_PIN_CTRL
                             );
    syscon_SetPMCR1WithMask(QN_SYSCON,
                P27_MASK_PIN_CTRL
               | P26_MASK_PIN_CTRL
               ,P27_PWM0_PIN_CTRL         //P2.7 pwm0
                             | P26_PWM1_PIN_CTRL         //P2.6 pwm1
                             );
    // pin pull ( 00 : High-Z,  01 : Pull-down,  10 : Pull-up,  11 : Reserved )
    syscon_SetPPCR0(QN_SYSCON, 0xAAAA5AAA);
    syscon_SetPPCR1(QN_SYSCON, 0x2AAAAAAA);
}
void pwm_io_dis_config(void)
{
    syscon_SetPMCR1WithMask(QN_SYSCON,P27_MASK_PIN_CTRL
               | P26_GPIO_22_PIN_CTRL
               , P27_GPIO_23_PIN_CTRL         //P2.7 GPIO
                             | P26_GPIO_22_PIN_CTRL         //P2.6 GPIO
                             );
    gpio_set_direction_field(GPIO_P27|GPIO_P26, GPIO_INPUT);
}

串口IO配置

void        uart_io_config(void)
{
  syscon_SetPMCR0WithMask(QN_SYSCON,        P00_MASK_PIN_CTRL
                   | P17_GPIO_15_PIN_CTRL,
                    P00_UART0_TXD_PIN_CTRL
                   |        P17_UART0_RXD_PIN_CTRL
  );
}
void        uart_io_disconfig(void)
{
  syscon_SetPMCR0WithMask(QN_SYSCON,        P00_MASK_PIN_CTRL
                   | P17_GPIO_15_PIN_CTRL,
                    P00_GPIO_0_PIN_CTRL
                   |        P17_GPIO_15_PIN_CTRL
  );
}

寻找LED开启电压

    while (1)                                /* Loop forever */
    {
    //P2.7 will output pwm wave with period for 1000us and pulse for 400us
    pulse_w += 25;
    i_to_array(pulse_w,pulse_w_a);
    uart_printf(QN_UART0,(uint8_t *)pulse_w_a);
    pwm_config(PWM_CH0, PWM_PSCAL_DIV, PWM_COUNT_US(1000, PWM_PSCAL_DIV), PWM_COUNT_US(pulse_w, PWM_PSCAL_DIV));
    pwm_enable(PWM_CH0, MASK_ENABLE);
    if (pulse_w == 1000)
    {
      pulse_w = 0;
      pulse_w_a[1] = 0x30;
      pulse_w_a[2] = 0x30;
      pulse_w_a[3] = 0x30;
      pulse_w_a[4] = 0x30;
    }
    delay(1000000);
    }

编译,出错

led_keil_error.png

没理由啊,我明明有加入头文件和.c文件,却提示找不到uart_init和uart_printf,仔细一看,左侧的driver目录下这样显示

uart.c和pwm.c显示不一样,有红色的提示,难道例程中虽然加入了这一项,但是出现了错误?于是删除再重新添加uart.c,编译成功!果然是这个问题!

经过测试,LED只要97.5%的脉宽就能点亮了,尽管亮度很低。看来在图中的VA指的是LED的正常工作电压,我们需要做的就是利用PWM将LED两端的电压控制在VB和最低亮度之间。实际中,从最低亮度到VA点是非常陡峭的,我们需要密集采样,而VA到VB点大致是呈线性变化的,我们可以平均取样。采样非常自由,我做了30个点的采样,以1000us的脉冲周期进行调制,则数组为:

static uint16_t pwm_breath[] = {997,995,993,990,985,980,970,950,900,800,700,600,500,400,100,50,100,400,500,600,700,800,900,950,970,980,985,990,993,995};

在主循环中不断的对pwm进行占空比调制,实现呼吸灯:

    while (1)                                /* Loop forever */
    {
    //P2.7 will output pwm wave with period for 1000us and pulse for 400u
    for        (uint8_t i = 0;i < pwm_array_lenth;i++)
   {
    pwm_config(PWM_CH0, PWM_PSCAL_DIV, PWM_COUNT_US(1000, PWM_PSCAL_DIV), PWM_COUNT_US(pwm_breath[i], PWM_PSCAL_DIV));
    pwm_enable(PWM_CH0, MASK_ENABLE);
    delay(100000);
   }
    }

实际效果如下:

呼吸灯.gif

:lol