項目中使用到了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的強大之處還是很多的,需要再接再厲。