基于stm32f407VGT6控制WS2812的TIM1+PWM+DMA实现方式

项目中使用到了ws2812灯带,作为产品的外观显示灯,经过开发之后,整理一下,仅供大家参考。

WS2812B是一个集控制电路与发光电路于一体的智能外控LED光源。 其外型与一个5050 LED灯珠相同, 每个元件即为一个像素点。 像素点内部包含了智能数字接口数据锁存信号整形放大驱动电路, 还包含有高精度的内部振荡器和可编程定电流控制部分, 有效保证了像素点光的颜色高度一致。数据协议采用单线归零码的通讯方式, 像素点在上电复位以后, DIN端接受从控制器传输过来的数据, 首先送过来的24bit数据被第一个像素点提取后, 送到像素点内部的数据锁存器, 剩余的数据经过内部整形处理电路整形放大后通过DO端口开始转发输出给下一个级联的像素点, 每经过一个像素点的传输, 信号减少24bit。 像素点采用自动整形转发技术, 使得该像素点的级联个数不受信号传送的限制, 仅受限信号传输速度要求。高达2KHz的端口扫描频率, 在高清摄像头的捕捉下都不会出现闪烁现象,非常适合高速移动产品的使用。280μs以上的RESET时间, 出现中断也不会引起误复位, 可以支持更低频率、 价格便宜的MCU。LED具有低电压驱动、 环保节能、 亮度高、 散射角度大、 一致性好超、 低功率及超长寿命等优点。 将控制电路集成于LED上面, 电路变得更加简单, 体积小, 安装更加简便。

WS2812B主要特点:
● IC控制电路与LED点光源共用一个电源。
● 控制电路与RGB芯片集成在一个5050封装的元器件中, 构成一个完整的外控像素点。
● 内置信号整形电路, 任何一个像素点收到信号后经过波形整形再输出, 保证线路波形畸变不会累加。
● 内置上电复位和掉电复位电路。
● 每个像素点的三基色颜色可实现256级亮度显示, 完成16777216种颜色的全真色彩显示。
● 端口扫描频率2KHz/s。
● 串行级联接口, 能通过一根信号线完成数据的接收与解码。
● 当刷新速率30帧/秒时, 级联数不小于1024点。
● 数据发送速度可达800Kbps。
● 光的颜色高度一致, 性价比高。
● 电源反接不会损坏。
● 外围不需要包含电容在内的所有任何电子元器件。

首先看一下其实现原理吧,对于应用者来说,硬件电路设计只需要三根线即可(5V电源线,地线,IO数据线)。其电路模型也很简单,如下:
基于stm32f407VGT6控制WS2812的TIM1+PWM+DMA实现方式_第1张图片
对于嵌入式程序开发者来说也比较简单,仅仅控制IO数据线就可以了,但是里要控制这个神奇的灯的颜色,这就需要对其数据传输的要求有所了解了。
基于stm32f407VGT6控制WS2812的TIM1+PWM+DMA实现方式_第2张图片
在这里插入图片描述
如上图所示,当数据线传入灯带后,第一个灯珠截取第一个24位数据留做己用,而后会将其余的数据进行整形后发送给第二颗灯珠,第二颗灯珠会依次截取24位数据并对剩余数据进行整形发送,以此类推。直到最后一组数据被显示为止。每一组24位数根据数据的不同显示不同的颜色和亮度(每个像素点的三基色颜色可实现256级亮度显示, 完成16777216种颜色的全真色彩显示)。
再来看一看每个灯的24位数据,其每一位数据只能是0或1,但是其电位时序室友要个要求的,如下:
基于stm32f407VGT6控制WS2812的TIM1+PWM+DMA实现方式_第3张图片基于stm32f407VGT6控制WS2812的TIM1+PWM+DMA实现方式_第4张图片
根据其电位控制要求,我们可以通过软件实现IO控制模拟0码和1码的实现。并进行数据传输。

说完了灯珠控制原理,再来谈一谈控制平台,众所周知,stm32系列的内核速率有限,考虑到指令周期的严格性,采用IO电平反转的方式来实现灯珠码型是不可靠,不现实的。

考虑到项目中实际应用的灯珠数量是比较大的(我的项目是使用270个灯珠,约4.5米的灯带),每个灯珠24bit数据,数据量每次传输量即为 270 * 24 / 8 = 810Bytes。传送800Bytes数据对于stm32来说并不苦难,但是频繁的传送可能会导致其不可靠行增大。

所以在项目中采用了TIM1+PWM+DMA的控制方式。设计思路是这样的,根据其特性可知数据发送速率最大达800kbps,根据此速率计算其周期为T= 1.25us。通过定时器实现其周期,再利用PWM实现其0码和1码,在通过DMA将数据传送出去。

废话不多说,看代码。

首先看一下定时器的初始化:

void time1andPwmInit(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);

    /* GPIO remap to TIM1*/
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource9, GPIO_AF_TIM1);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
    GPIO_Init(GPIOE, &GPIO_InitStructure);
    GPIO_ResetBits(GPIOC, GPIO_Pin_13);


    /* timer period : T =(arr + 1) * (PSC + 1) / Tck.   arr: period value PSC:prescaler value  Tck: system clock */
    TIM_TimeBaseStructure.TIM_Period = 210 - 1;                     /* T = (TIM_Period + 1)*(0+1)/168M  = 800kHz*/
    TIM_TimeBaseStructure.TIM_Prescaler = 0;                        /* 0 */
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;         /* 0 */
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);

    /* PWM1 Mode configuration: Channel1 */
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0;                                  /* 1 ~ TIM_TimeBaseStructure.TIM_Period */
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Disable;    //TIM_OutputNState_Enable;
    TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High;
    TIM_OCInitStructure.TIM_OCIdleState = TIM_OCNIdleState_Set;
    TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
    TIM_OC1Init(TIM1, &TIM_OCInitStructure);
    TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);

    TIM_Cmd(TIM1, ENABLE);
    TIM_CtrlPWMOutputs(TIM1,ENABLE);
}

在我的工程中,我采用的是stm32f407VGT6,其内核时钟为168M,可根据时钟更改210这个值。采用的IO数据端口为E9作为TIM1_CH1输出,将其配置为PWM输出。

再来看一下DMA模块初始化:

#define TIM1_CCR1_Address   (TIM1_BASE + 0x34)    //0x40001034

void dmaInit(void)
{
    DMA_InitTypeDef DMA_InitStructure;

    /* DMA clock enable */
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

    DMA_DeInit(DMA2_Stream6);

    /* DMA2 Stream6 Config for PWM1 by TIM1_CH1*/
    DMA_InitStructure.DMA_Channel = DMA_Channel_0;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)TIM1_CCR1_Address;
    DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)g_ledDataBuffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
    DMA_InitStructure.DMA_BufferSize = 42;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
    DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
    DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;

    DMA_Init(DMA2_Stream6, &DMA_InitStructure);

    /* TIM1 DMA Request enable */
    TIM_DMACmd(TIM1, TIM_DMA_CC1, ENABLE);

}

参考数据手册有关DMA模块可知。
基于stm32f407VGT6控制WS2812的TIM1+PWM+DMA实现方式_第5张图片
TIM1_CH1外设只能使用DMA2_Stream6_Ch0和DMA2_Stream1_Ch6,本项目采用前者。对于此配置,最重要的是就是以下三点:
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)TIM1_CCR1_Address; //外设基地址
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)g_ledDataBuffer; //内存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; //传输方式:内存到映射

根据TIM1的PWM输出原理,可知我们此时要传输的外设地址为TIM1_CCR1寄存器地址。DMA内存地址即是我们传输数据的首地址。

最后看一看数据发送模块的代码:

#define TIMING_ONE          (143)
#define TIMING_ZERO         (67)

uint8_t rgbRed[][3] = {{0xff, 0x00, 0x00}};

void ledSingleShow(uint8_t (*color)[3], uint16_t len)
{
    uint8_t i = 0;
    uint16_t memaddr = 0;
    uint16_t buffersize = 0;

    buffersize = (len * 24) + 1;       // number of bytes needed is #LEDs * 24 bytes + 42 trailing bytes
    memaddr = 0;                        // reset buffer memory index

    while (len)
    {
        /*  green data */
        for(i = 0; i < 8; i++)
        {
            g_ledDataBuffer[memaddr] = ((color[0][1] << i) & 0x0080) ? TIMING_ONE : TIMING_ZERO;
            memaddr++;
        }

        /*  red data */
        for(i = 0; i < 8; i++)
        {   
            g_ledDataBuffer[memaddr] = ((color[0][0] << i) & 0x0080) ? TIMING_ONE : TIMING_ZERO;
            memaddr++;
        }

        /*  blue data */
        for(i = 0; i < 8; i++)
        {
            g_ledDataBuffer[memaddr] = ((color[0][2] << i) & 0x0080) ? TIMING_ONE : TIMING_ZERO;
            memaddr++;
        }

        len--;
    }

    DMA_SetCurrDataCounter(DMA2_Stream6, buffersize);
    TIM_Cmd(TIM1, ENABLE);
    TIM_DMACmd(TIM1, TIM_DMA_CC1, ENABLE);
    DMA_Cmd(DMA2_Stream6, ENABLE);
    while(!DMA_GetFlagStatus(DMA2_Stream6, DMA_FLAG_TCIF6));
    DMA_Cmd(DMA2_Stream6, DISABLE);
    DMA_ClearFlag(DMA2_Stream6, DMA_FLAG_TCIF6);
    TIM_Cmd(TIM1, DISABLE);

}

上述代码实际包含两部分功能,数据组成和DMA传送。
根据传入参数格式可知,参数为灯珠颜色的数组,根据其数组各元素定义,重新定义DMA要发送的内存数据。而后将DMA发送功能打开。 其实此时我们只是将不同的占空比系数发送给了TIM1->CCR1寄存器,TIM1的周期为1.25us,再此周期中,通过占空比,会实现led灯珠的0码和1码。从而实现led显示。

#define TIMING_ONE (143)
#define TIMING_ZERO (67)
至于这两个宏定义,大家根据led灯的时序波形图计算记得得出。

经过过这个小东西,发现是stm32的强大之处还是很多的,需要再接再厉。

你可能感兴趣的:(stm32)