基於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數據線)。其電路模型也很簡單,如下:
在這裏插入圖片描述
對於嵌入式程序開發者來說也比較簡單,僅僅控制IO數據線就可以了,但是裏要控制這個神奇的燈的顏色,這就需要對其數據傳輸的要求有所瞭解了。
在這裏插入圖片描述
在這裏插入圖片描述
如上圖所示,當數據線傳入燈帶後,第一個燈珠截取第一個24位數據留做己用,而後會將其餘的數據進行整形後發送給第二顆燈珠,第二顆燈珠會依次截取24位數據並對剩餘數據進行整形發送,以此類推。直到最後一組數據被顯示爲止。每一組24位數根據數據的不同顯示不同的顏色和亮度(每個像素點的三基色顏色可實現256級亮度顯示, 完成16777216種顏色的全真色彩顯示)。
再來看一看每個燈的24位數據,其每一位數據只能是0或1,但是其電位時序室友要個要求的,如下:
在這裏插入圖片描述在這裏插入圖片描述
根據其電位控制要求,我們可以通過軟件實現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模塊可知。
在這裏插入圖片描述
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的強大之處還是很多的,需要再接再厲。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章