1,WS2812時序
WS2812是一個集控制電路與發光電路于一體的智能外控LED光源,每個ws2812均含有4個引腳,引腳功能如下圖:
WS2812可采用級聯的方式,將上一個WS2812的DOUT引腳連接到下一個WS2812的DIN引腳,即可實現一個引腳控制多個WS2812。一般自己設計電路時還要在電源輸入處添加一個0.1uF的小電容進行濾波。注意,雖然理論上可以連接足夠多的ws2812,但是使用時要注意驅動電壓和驅動電流是否足夠驅動這些彩燈。
由于只有一個引腳控制ws2812,所以我們只能通過控制引腳輸出高、低電平的時間,來讓WS2812知道我們想讓他顯示哪一個燈,顯示什么顏色。
想要讓1個ws2812顯示我們想要的顏色,我們需要給它發送顏色數據。每一個ws2812的顏色數據都是24位:8位綠色+8位紅色+8位藍色。
如果我們想控制1個ws2812,我們發送24位顏色數據;如果我們想控制2個ws2812,我們需要連續發送48位顏色數據;如果我們想控制n個ws2812,我們需要連續發送n*24位顏色數據。
那么ws2812如何知道我們發送的每一個位的數據是“1”還是“0”呢?
WS2812手冊中對于數據有明確的時間定義:
可以看出,當我們想要發送一個位的數據時,如果這個位是“1”,我們就控制單片機的引腳輸出高電平0.85us,然后控制引腳輸出低電平0.40us;如果這個位是“0”,則控制引腳輸出高電平0.40us,然后控制引腳輸出低電平0.85us。
而如果我們想控制1個燈顯示顏色,我們需要發送24個位的顏色數據給ws2812。如果我們想控制n個燈顯示顏色,我們從第1個燈的顏色數據開始發送,直至發送完n*24個數據。最后我們需要控制單片機引腳輸出低電平超過50us,讓彩燈顯示顏色。
2,寫出驅動程序
注意,STC15F104W只有8個引腳,我們一般采用內部晶振電路。下面的代碼都是基于12MHz的晶振頻率寫的。WS2812對于單片機的引腳時序要求比較高。我們一般調用intrins.h這個頭文件的機器周期函數,nop()函數執行一次占用一個機器周期(此處時鐘周期和機器周期相等),所以每條機器周期時間為1/12MHz=83.3us。(代碼中很多處使用宏定義,宏定義在預編譯階段程序就完成代碼替代工作,不會影響程序的執行時間,且修改方便)
下面是h文件代碼:
#ifndef __WS2812_H
#define __WS2812_H
//頭文件區
#include <STC15F2K60S2.H>
#include <intrins.h>
//用戶修改參數區
//#define WS2812_FREQUENCY
#define RGB_PIN P33 //控制彩燈引腳(需要配置為強推挽輸出)
#define WS2812_MAX 25 //彩燈最大個數
#define WS2812_NUMBERS 8 //彩燈個數
#define RED 0xff0000 //紅色
#define GREEN 0x00ff00 //綠色
#define BLUE 0x0000ff //藍色
#define BLACK 0x000000 //熄滅
#define WHITE 0xffffff //白色
#define RGB_PIN_H() RGB_PIN = 1
#define RGB_PIN_L() RGB_PIN = 0
#define delay1NOP() _nop_();
#define delay1NOP() _nop_();
#define delay2NOP() delay1NOP(); _nop_();
#define delay3NOP() delay2NOP();_nop_();
#define delay5NOP() delay3NOP();delay2NOP();
#define delay7NOP() delay5NOP();delay2NOP();
void Ws2812b_WriteByte(unsigned char byte);//發送一個字節數據(@12.000MHz,理論每個機器周期83ns,測試約為76ns)
void setLedCount(unsigned char count);//設置彩燈數目,范圍0-25.
unsigned char getLedCount();//彩燈數目查詢函數
void rgb_SetColor(unsigned char LedId, unsigned long color);//設置彩燈顏色
void rgb_SetRGB(unsigned char LedId, unsigned long red, unsigned long green, unsigned long blue);//設置彩燈顏色
void rgb_SendArray();//發送彩燈數據
#endif
接著我們對c代碼進行詳解。
首先調用h文件,然后定義一個數組,用來存放彩燈的顏色數據。這里數據存放在data空間(空間有限,只能存放25個燈左右的數據),后面有需要再優化到xdata空間。ledsCount 記錄實際我們想要控制的彩燈的數目,nbLedsBytes 用來記錄彩燈的數據個數(一個燈需要3個字節)。
#include "ws2812.h"
unsigned char LedsArray[WS2812_MAX * 3]; //定義顏色數據存儲數組
unsigned int ledsCount = WS2812_NUMBERS; //定義實際彩燈默認個數
unsigned int nbLedsBytes = WS2812_NUMBERS*3; //定義實際彩燈顏色數據個數
接著我們開始編寫彩燈的設置數目函數,查詢數目函數,以及兩個設置指定彩燈顏色的函數。
//設置彩燈數目,范圍0-25.
void setLedCount(unsigned char count)
{
ledsCount = WS2812_MAX > count ? count : WS2812_MAX;
nbLedsBytes = ledsCount*3;
}
//彩燈數目查詢函數
unsigned char getLedCount()
{
return ledsCount;
}
//設置彩燈顏色(在這里我將綠和紅色進行顛倒,這樣比較符合我們日常生活的紅綠藍的順序)
void rgb_SetColor(unsigned char LedId, unsigned long color)
{
if( LedId > ledsCount )
{
return; //to avoid overflow
}
LedsArray[LedId * 3] = (color>>8)&0xff;
LedsArray[LedId * 3 + 1] = (color>>16)&0xff;
LedsArray[LedId * 3 + 2] = (color>>0)&0xff;
}
//設置彩燈顏色
void rgb_SetRGB(unsigned char LedId, unsigned long red, unsigned long green, unsigned long blue)
{
unsigned long Color=red<<16|green<<8|blue;
rgb_SetColor(LedId,Color);
}
然后我們對彩燈數據通過引腳發送出去。注意,發送彩燈數據的過程中,請將中斷關閉,否則有可能會導致數據發送到一半被中斷打斷,導致顯示異常。Ws2812b_WriteByte()函數是這個驅動代碼的核心。
//發送彩燈數據
void rgb_SendArray()
{
unsigned int i;
bit a=EA;
//發送數據
EA=0;
for(i=0; i<nbLedsBytes; i++)
Ws2812b_WriteByte(LedsArray[ i]);
EA=a;
}
下面的Ws2812b_WriteByte函數是我基于stc15f104w調試的。我寫過不同單片機的驅動。不同單片機的每條指令花費的時間都不一樣。如果您想要采用其他系列的單片機,我們只需要修改Ws2812b_WriteByte()函數里面的內容即可。其他的函數可以不做修改。方便代碼的移植。
主要修改的地方是高電平總的拉高時間。這里之所以不對“1”和“0”的內容進行函數封裝,是為了我自己調試的方便。如果您覺得代碼不夠簡短好看,您可以將判斷條件里面的內容用宏定義進行一下封裝。
/*
//使用12.000MHz頻率,理論每個機器周期83ns,實際測試約為76ns。
//下面都是基于76ns納秒一個機器周期計算的。
//測試時發現可以支持12-20Mhz頻率。
//如果想要使用11.0592Mhz頻率,在h文件取消“#define WS2812_FREQUENCY”的注釋
//在實際測試中發現,ws2812對高電平時間較為敏感,對低電平時間不敏感
//也就是說,低電平時間可以稍微長一些也不會影響程序,但是高電平時間需要控制的比較準確才行。
*/
void Ws2812b_WriteByte(unsigned char byte)
{
#ifndef WS2812_FREQUENCY
if(byte & 0x80)
{
RGB_PIN_H();//4個機器周期(STC15F104W的拉高拉低都需要4個機器周期,其他系列暫時不知道,但是很大可能不是4個機器周期)
delay7NOP();//7個機器周期
RGB_PIN_L();//4+8(跳出這個if判斷需要5個機器周期,再進入下一個if判斷需要3個機器周期)
}
else
{
RGB_PIN_H();
delay2NOP();//4+2
RGB_PIN_L();
delay3NOP();//4+3+5(跳出這個else需要3個機器周期,進入下一個else則需要2個機器周期)
}
if(byte & 0x40)
{
RGB_PIN_H();//
delay7NOP();//4+7
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay2NOP();//4+2
RGB_PIN_L();
delay3NOP();//4+3+5
}
if(byte & 0x20)
{
RGB_PIN_H();//
delay7NOP();//4+7
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay2NOP();//4+2
RGB_PIN_L();
delay3NOP();//4+3+5
}
if(byte & 0x10)
{
RGB_PIN_H();//
delay7NOP();//4+7
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay2NOP();//4+2
RGB_PIN_L();
delay3NOP();//4+3+5
}
if(byte & 0x8)
{
RGB_PIN_H();//
delay7NOP();//4+7
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay2NOP();//4+2
RGB_PIN_L();
delay3NOP();//4+3+5
}
if(byte & 0x4)
{
RGB_PIN_H();//
delay7NOP();//4+7
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay2NOP();//4+2
RGB_PIN_L();
delay3NOP();//4+3+5
}
if(byte & 0x2)
{
RGB_PIN_H();//
delay7NOP();//4+7
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay2NOP();//4+2
RGB_PIN_L();
delay3NOP();//4+3+5
}
if(byte & 0x1)
{
RGB_PIN_H();//
delay7NOP();//4+7
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay2NOP();//4+2
RGB_PIN_L();
delay3NOP();//4+3+5
}
#else
if(byte & 0x80)
{
RGB_PIN_H();//
delay5NOP();//4+5
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay1NOP();//4+1
RGB_PIN_L();
delay2NOP();//4+2+5
}
if(byte & 0x40)
{
RGB_PIN_H();//
delay5NOP();//4+5
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay1NOP();//4+1
RGB_PIN_L();
delay2NOP();//4+2+5
}
if(byte & 0x20)
{
RGB_PIN_H();//
delay5NOP();//4+5
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay1NOP();//4+1
RGB_PIN_L();
delay2NOP();//4+2+5
}
if(byte & 0x10)
{
RGB_PIN_H();//
delay5NOP();//4+5
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay1NOP();//4+1
RGB_PIN_L();
delay2NOP();//4+2+5
}
if(byte & 0x8)
{
RGB_PIN_H();//
delay5NOP();//4+5
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay1NOP();//4+1
RGB_PIN_L();
delay2NOP();//4+2+5
}
if(byte & 0x4)
{
RGB_PIN_H();//
delay5NOP();//4+5
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay1NOP();//4+1
RGB_PIN_L();
delay2NOP();//4+2+5
}
if(byte & 0x2)
{
RGB_PIN_H();//
delay5NOP();//4+5
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay1NOP();//4+1
RGB_PIN_L();
delay2NOP();//4+2+5
}
if(byte & 0x1)
{
RGB_PIN_H();//
delay5NOP();//4+5
RGB_PIN_L();//4+8
}
else
{
RGB_PIN_H();
delay1NOP();//4+1
RGB_PIN_L();
delay2NOP();//4+2+5
}
#endif
}
main文件里面,我是用stc生成了一個300ms的延時函數。讓程序每隔300ms更換一下顏色并輸出。紅綠藍三色交替閃爍。
#include <STC15F2K60S2.H>
#include "ws2812.h"
void Delay300ms();//@12.000MHz
void main()
{
int i=0;
setLedCount(12);//可以設置0-25之間的任何數目,根據實際情況而定
rgb_SetRGB(0,0,0,0);//設置第一個燈關閉(這里作為使用示例,下面的rgb_SetColor覆蓋了這個函數的作用)
while(1)
{
for(i=0; i<getLedCount(); i++)//設置所有的燈為紅色
{
rgb_SetColor(i,RED);
}
rgb_SendArray();//發送給ws2812,顯示顏色
Delay300ms();
for(i=0; i<getLedCount(); i++)//設置所有的燈為綠色
{
rgb_SetColor(i,GREEN);
}
rgb_SendArray();//發送給ws2812,顯示顏色
Delay300ms();
for(i=0; i<getLedCount(); i++)//設置所有的燈為藍色
{
rgb_SetColor(i,BLUE);
}
rgb_SendArray();//發送給ws2812,顯示顏色
Delay300ms();
}
}
void Delay300ms()//@12.000MHz
{
unsigned char i, j, k;
_nop_();
_nop_();
i = 14;
j = 174;
k = 224;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
3,軟件調試
確保您的芯片型號選擇正確。
填寫軟件調試的晶振頻率,我這里使用16MHz(只是用來軟件計算機器周期,和實際晶振無關)。1/16MHz=62.5ns。如果采用12MHz,我計算機器周期不太方便。
點擊調試。沒有配置時默認是軟件調試模式。
點擊引腳信號的邏輯分析。
在新彈出的窗口中點擊Setup。然后輸入您的控制引腳。
使用調試控件,一邊觀察波形圖。
在程序執行RGB_PIN_H();這一句之前,我用紅色的線定位了此時的時間位置,執行這一句后,我用藍色的線查看它們之間的差值,差值為0.25us。這里我的軟件仿真使用的是16MHz晶振,每個機器周期為62.5ns。0.25us相當于4個機器周期。正如我上面的發送函數里面的機器周期備注一樣。同理可以計算出每行代碼需要花費的機器周期時間,從而準確寫出驅動。
比如,我們需要0.85us的高電平時間,也就是850ns,然后實際上我們使用的晶振是12MHz。每個機器周期的時間是理論是1/12Mhz=83.3ns。那么我們就需要850/83.3=10.2個機器周期。這里取11個機器周期。根據前面我們知道電平拉高需要用去4個機器周期,因此我們還需要讓高電平維持7個機器周期的時間,然后將電平拉低。以此原理,寫出我們的Ws2812b_WriteByte()函數的驅動。
4,硬件調試
實際硬件調試時,我寫了一個測試程序。我用軟件調試時,波形為引腳電平拉高10個機器周期,然后拉低10個機器周期。循環動作。
while(1)
{
P33=1;//4
delay6NOP();//6
P33=0;//4
delay2NOP();//2+4
}
但是實際用示波器測試時,發現高電平時間和低電平時間均為3.8格*200ns/格=760ns。所以實際上每個機器周期的時間我這里是76ns左右。距離理論值83.3ns相差不多,而且ws2812每個高電平信號都可以有150ns左右的誤差,因此這里按照理論計算和按照實際計算均可。我按理論值計算。
————————————————
版權聲明:本文為CSDN博主「DIY愛好玩家」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_38476200/article/details/115519393
|