2016年3月4日 星期五

怎麼用 { Arduino + 單晶片 } 控制 APA102 做流星燈

網頁最後修改時間:2016/03/04

這篇網頁主要說明如何使用單晶片 (以AT89S52作範例,其他型號也可以 ) 控制 APA102 RGB LED  做流星燈。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
有購買商品的使用者,網頁中所需相關資料已放置於雲端硬碟,請自行下載使用!
其餘的使用者,請自行依照提供之連結下載相關資料,程式碼複製貼上使用!
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*

*********************************************************************************
相關零件可至露天賣場訂購:
*********************************************************************************

APA102 資料輸入格式說明:

APA102 的驅動很簡單,只要將需要的 APA102 燈串中的每個燈的 RGB 色彩做指定 (起頭 + B1, G1, R1 + B2, G2, R2 + ... + Bn, Gn, Rn + 結尾 = 資料格式 (Data Foramt) ),通過兩隻資料輸入接腳 (<CI>, <DI>)  依 "資料格式" 的定義再以 bit 方式做輸入:
<DI> 是每個 bit 的輸入;<CI> 是每個 bit 輸入之後需要 MCU 給個脈衝訊號,才能將 <DI> 每個 bit 輸入的訊號推入到 APA102 晶片的暫存器內。
APA102 資料輸入格式
這種控制方式類似 74HC595 晶片的控制,但是少了 <RCLK> 接腳觸發讓資料一並輸出。

APA102 使用的方法,就是請微控制器要開始傳送每一顆 APA102 RGB 資料前 ( 對於每一顆可顯示 RGB 顏色的 LED 也可稱為像素 (pixel) ),先傳送 32 0 表示開始。

接著,傳送每個像素的亮度等級以及 RGB 顏色。亮度等級以 8-bit 表示,但是只有後面 5 個 bit 可以使用,也就是只有 32 種亮度等級可以調整;RGB 顏色的傳送共 24 個 bit,但是傳送的順序是以 B >>> G >>> R 的順序傳送。

每一個像素的資料傳輸完了之後,最後一個步驟就是連續輸出 32 1,讓所有像素輸出混色結果。

這部分的單晶片程式碼片段如下
// APA102 Data Format
// 
//  "start frame"   |   LED 1 ... LED n | end of frame
//   32-bit 0       | 32-bit ... 32-bit | 32-bit 1
//
//  LED frame 32 bits
//  111 |   Brightness  |   BLUE    |   GREEN   |   RED
//          5-bit 亮度    |   8-bit   |   8-bit   |   8-bit
//

void ledshow( CRGB *leds )
{
    int i, j;

    // start frame
    Do = 0;
    for( i = 0; i < 32; i++)
        oneclock();
    
    // LEDs frame
    for( i = 0; i < NUM_LEDS; i++ )
    {
        for( j = 0; j < 8; j++ )    // 5-bit brightness, 使用最高亮度 11111
        {   Do = 1; oneclock(); }
        for( j = 0; j < 8; j++ )    // BLUE
        {   Do = (( leds[i].b << j ) & 0x80 ) ? 1: 0; oneclock(); }
        for( j = 0; j < 8; j++ )    // GREEN
        {   Do = (( leds[i].g << j ) & 0x80 ) ? 1: 0; oneclock(); }
        for( j = 0; j < 8; j++ )    // RED
        {   Do = (( leds[i].r << j ) & 0x80 ) ? 1: 0; oneclock(); }
    }
    
    // end of frame 
    Do = 1;
    for( i = 0; i < 32; i++ )
        oneclock();
}

整個 APA102 的色彩控制重點就在上面,而如何產生自己需要的顏色有很多方法可以實現像是 RGB 或是使用 HSV。由於我們要做流星燈,因此 RGB 顏色每一顆都是一樣的,只有亮度需要做調整。最簡單的解決方法,就是由 RGB 產生顏色,再轉換為 HSV 格式,調整其中的亮度值為固定漸距,如此就可以產生相同顏色但是不同亮度的顏色值,這也是流星燈產生的原理。

單晶片完整程式碼:

沒有雲端硬碟連結的使用者,若要直接使用下面程式碼,請使用 Keil C 打開一個新專案檔,進行下面一些簡單設置:

  • 晶片選擇:ATEML AT89S52 ( 或是自己使用的 51 晶片 )
  • 振盪器頻率:預設使用 12 MHz ( 若使用其他頻率的使用者,請記得確認一下程式碼中延遲副程式 ( delay 開頭的 ),可能需要再做調整才能符合真實的延遲時間。
  • 記得輸出為 HEX 要打勾
單晶片與 APA102 的連接很簡單,包括電源只需要 4 條線,都列在程式開頭說明處。

使用單晶片最小系統開發套件驅動  APA102 燈串
程式在編譯之前,請先設定好 APA102 RGB LED 燈的數目 (NUM_LEDS) 和流星的長度 (NUM_METEORS)。

編譯之後會在專案檔儲存的相同目錄產生 .hex 的燒錄檔。

/******************************************************************************
* APA102_LEDMeteorLight.c
*
*   流星燈展示!
*
*   接線:
*       AT89S52         APA102
*         P3.6           CO
*         P3.7           DO
*         VCC            5V         ; 接 5V 電源或是 USB 電源
*         GND            GND
*
*   編譯環境:
*       Keil uVision V4.72.9.0
*
*------------------------------------------------------------------------------
*   NUM_LEDS:APA102 燈的數目
*   NUM_METEORS:流星在燈串裡的長度設定
*------------------------------------------------------------------------------
*
*   Result: Work !!!
*
******************************************************************************/
#include <reg52.h>
#include <intrins.h>

// definitions
#define OSC_FREQ        (12000000UL)    // cpu freq. 12MHz
#define OSC_PER_INST    (12)

#define NUM_LEDS    10
// 流星長度
#define NUM_METEORS    8
// 流星亮度間隔 ( HSV )
#define  INCREASE_BRIGHTNESS  ( 256 / NUM_METEORS )

typedef unsigned char uint8_t;
typedef unsigned int  uint16_t;
typedef unsigned long uint32_t;

typedef struct HSV {
    uint16_t h; // hue
    uint8_t s;  // saturation
    uint8_t v;  // value
} CHSV;

typedef struct RGB {
    uint8_t r;  // red
    uint8_t g;  // green
    uint8_t b;  // blue
} CRGB;

// SPI 接腳設定
//  只需要設定APA102 連接的 CO 與 DO 與 AT89S52 連接的接腳 

sbit Co = P3^6;
sbit Do = P3^7;

CRGB leds[NUM_LEDS];
CRGB meteors[NUM_METEORS];
/*-----------------------------------------
 Delay
-----------------------------------------*/
void delay_5us()
{
    _nop_(); _nop_(); _nop_();      // 5us
}

void delay_5ms()
{
    unsigned int x, y;
    for( x = 0; x < 5; x++)
        for( y = 0; y < 122; y++ );
} 

void delay_seconds(uint8_t seconds)
{
    uint8_t i;
    for(i = 0 ;i < seconds * 50; i++ )
        delay_5ms();
}

static void oneclock()
{
    Co = 1;
    //delay_5us();
    Co = 0;
    //delay_5us();
}

// APA102 Data Format
// 
//  "start frame"   |   LED 1 ... LED n | end of frame
//   32-bit 0       | 32-bit ... 32-bit | 32-bit 1
//
//  LED frame 32 bits
//  111 |   Brightness  |   BLUE    |   GREEN   |   RED
//          5-bit 亮度    |   8-bit   |   8-bit   |   8-bit
//

void ledshow( CRGB *leds )
{
    int i, j;
    
    // start frame
    Do = 0;
    for( i = 0; i < 32; i++)
        oneclock();
    
    // LEDs frame
    for( i = 0; i < NUM_LEDS; i++ )
    {
        for( j = 0; j < 8; j++ )    // 5-bit brightness, 使用最高亮度 11111
        {   Do = 1; oneclock(); }
        for( j = 0; j < 8; j++ )    // BLUE
        {   Do = (( leds[i].b << j ) & 0x80 ) ? 1: 0; oneclock(); }
        for( j = 0; j < 8; j++ )    // GREEN
        {   Do = (( leds[i].g << j ) & 0x80 ) ? 1: 0; oneclock(); }
        for( j = 0; j < 8; j++ )    // RED
        {   Do = (( leds[i].r << j ) & 0x80 ) ? 1: 0; oneclock(); }
    }   
    
    // end of frame 
    Do = 1;
    for( i = 0; i < 32; i++ )
        oneclock();
}

//
// HSV ( 0-360 度;0-100%, 1-100% ) 轉換為 ( 0-255, 0-255, 0-255 )
// 例如:粉紅色
// HSV ( 320 度, 100%, 100% ) = ( 228, 255, 255 ) = RGB ( 255, 255, 0 )
//
static void hsv2rgb( const CHSV *hsv, CRGB *rgb )
{
    uint8_t region, remainder, p, q, t;
    
    if (hsv->s == 0)
    {
        rgb->r = hsv->v;
        rgb->g = hsv->v;
        rgb->b = hsv->v;
        return;
    }
    
    region = hsv->h / 43;
    remainder = (hsv->h - (region * 43)) * 6; 

    p = (hsv->v * (255 - hsv->s)) >> 8;
    q = (hsv->v * (255 - ((hsv->s * remainder) >> 8))) >> 8;
    t = (hsv->v * (255 - ((hsv->s * (255 - remainder)) >> 8))) >> 8;

    switch (region)
    {
        case 0:
            rgb->r = hsv->v; rgb->g = t; rgb->b = p;
            break;
        case 1:
            rgb->r = q; rgb->g = hsv->v; rgb->b = p;
            break;
        case 2:
            rgb->r = p; rgb->g = hsv->v; rgb->b = t;
            break;
        case 3:
            rgb->r = p; rgb->g = q; rgb->b = hsv->v;
            break;
        case 4:
            rgb->r = t; rgb->g = p; rgb->b = hsv->v;
            break;
        default:
            rgb->r = hsv->v; rgb->g = p; rgb->b = q;
            break;
    }
}

void setHSV( CRGB *rgb, uint16_t h, uint8_t s, uint8_t v )
{
    CHSV hsv;
    hsv.h = h;
    hsv.s = s;
    hsv.v = v;
    hsv2rgb( &hsv, rgb);
}

void setRGB( CRGB *rgb, uint8_t r, uint8_t g, uint8_t b )
{
    rgb->r = r;
    rgb->g = g;
    rgb->b = b;
}

void main()
{
    uint16_t bg;
    int i, j;
    
    int ea, eb;   // 流星現在在燈串的位置;ea: 低位元組,eb: 高位元組
    int ma, mb;   // 流星陣列複製的起始位置;ma: 低位元組,mb: 高位元組

    // 接腳初始化狀態
    Co = 0;
    Do = 0;

    while(1)
    {   
        j = 0;
        // 下面顯示出整串流星的長度以及亮度變化,由 0 .. 4 是滅到亮
        for( i = INCREASE_BRIGHTNESS - 1; i < 256; i += INCREASE_BRIGHTNESS )
        {
            setHSV( &meteors[j++], 228, 255, i );
        }
        /*===================================================================================
        *  流星由近端到遠端移動
    ------------------------------------------------------------------------------------*/
        for( i = 0; i < ( NUM_LEDS + NUM_METEORS); i++ )    // ea: 流星起頭的位置
        {
            // 設定 leds 全部為 black
            for( ea = 0; ea < NUM_LEDS; ea++ )
                setRGB( &leds[ea], 0, 0, 0 );

            // 找到複製 meteors 到 leds 的起頭與結束位置
            if( i < NUM_METEORS ) // 整個流星還未完全進入到燈串當中
            {
                ma = 0;
                mb = i;
                ea = NUM_METEORS - i - 1;
                eb = NUM_METEORS - 1;
            }
            else if( i >= NUM_LEDS )    // 流星頭部已經開始超出燈串
            {
                ma = i - NUM_METEORS + 1;
                mb = NUM_LEDS;
                ea = 0;
                eb = NUM_LEDS + NUM_METEORS - 1 - i;
            }
            else    // 流星在燈串中
            {
                ma = i - NUM_METEORS + 1;
                mb = i + 1;
                ea = 0;
                eb = NUM_METEORS;
            }

            for( ma; ma < mb; ma++ )    // 複製流星的起頭位置
            {
                leds[ma].r = meteors[ea].r;
                leds[ma].g = meteors[ea].g;
                leds[ma].b = meteors[ea++].b;
            }
            ledshow( leds );
            delay_5ms();    delay_5ms();
            delay_5ms();    delay_5ms();
            delay_5ms();    delay_5ms();
        }
        delay_seconds(2);
        /*===================================================================================*/

        /*=====================================================================================
        *  流星由遠端到近端移動
        -------------------------------------------------------------------------------------*/

    for( i = ( NUM_LEDS + NUM_METEORS - 1 ); i >=0 ; i-- )    // ea: 流星起頭的位置
        {
            // 設定 leds 全部為 black
            for( ea = 0; ea < NUM_LEDS; ea++ )
                setRGB( &leds[ea], 0, 0, 0 );

            // 找到複製 meteors 到 leds 的起頭與結束位置
            if( i < NUM_METEORS ) // 整個流星還未完全進入到燈串當中
            {
                ma = 0;
                mb = i;
                ea = i;
                eb = 0;
            }
            else if( i >= NUM_LEDS )    // 流星頭部已經開始超出燈串
            {
                ma = i - NUM_METEORS;
                mb = NUM_LEDS;
                ea = NUM_METEORS - 1;          
                eb = i - NUM_LEDS;
            }
            else    // 流星在燈串中
            {
                ma = i - NUM_METEORS;
                mb = i;
                ea = NUM_METEORS - 1;
                eb = 0;
            }

            for( ma; ma < mb; ma++ )    // 複製流星的起頭位置
            {               
                leds[ma].r = meteors[ea].r;     // eb++ [滅到亮],倒退
                leds[ma].g = meteors[ea].g;
                leds[ma].b = meteors[ea--].b;
            }

            ledshow( leds );
            delay_5ms();    delay_5ms();
            delay_5ms();    delay_5ms();
            delay_5ms();    delay_5ms();
        }

        delay_seconds(2);
    }   
}


Arduino 程式碼:

Arduino 程式碼中流星燈跑動的方式跟單晶片的一樣,但 LED 控制使用 FastLED 3.1 函式庫 ( 也可以直接將上面單晶片的程式碼移植 )。基本上,當更換不同的 RGB LED ( 無論是單線式或是雙線式通訊 ) 時,幾乎只要修改使用的 RGB LED 晶片名稱就可以直接使用,非常的方便所以使用!

重要參數的設定跟單晶片一樣,需設定 APA102 的 RGB LED 數量和流星燈的數目。

另外多出的幾個參數設定:

  • STAY_TIME:設定流星燈移動的速度。使用者可以自己調整式當的速度值。
  • DEBUG:預設式開啟此功能,可以知道流星燈移動的過程,不需要就 comment 掉。

接線的部分,除了電源接 DC 5V,DATA_PIN (<DI>) 接到 Arduino <D3>,CLOCK_PIN (<CI>) 接到 Arduino <D2>

除非要加入其他功能,要不然就是上面那幾個參數值的修改。修改之後編譯後上傳就會看到 APA102 的流星燈。

#include "FastLED.h"

/*=====================================================================================
*  重要常數宣告,依實際連接方式作變更
---------------------------------------------------------------------------------------------------------------------------------------------------------*/
// 燈串裡 APA102 的數目
#define NUM_LEDS  10

// 在燈串裡的流星長度
#define NUM_METEORS  8

// 流星亮度間隔 ( HSV )
#define  INCREASE_BRIGHTNESS  ( 256 / NUM_METEORS )

// 流星每一次停留時間 ( ms)
#define STAY_TIME  45

// APA102 通訊資料接腳
#define DATA_PIN  3
// APA102 時脈接腳
#define CLOCK_PIN 2

//  DEBUG
#define DEBUG

/*====================================================================================*/

// Define the array of leds
CRGB leds[NUM_LEDS];
CRGB meteors[NUM_METEORS];

void setup() {

    // sanity check delay - allows reprogramming if accidently blowing power w/leds
    delay(2000);  
      
    FastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, RGB>(leds, NUM_LEDS);

    Serial.begin(9600);
      
    #ifdef DEBUG
        Serial.print(F("NUM_LEDS")); Serial.print("\t");
        Serial.print(F("NUM_METEORS")); Serial.print("\t");
        Serial.print(F("INCREASE_BRIGHTNESS")); Serial.print("\t");
        Serial.print(F("STAY_TIME"));
        Serial.println("");  
        Serial.print(NUM_LEDS); Serial.print("\t");  
        Serial.print(NUM_METEORS); Serial.print("\t");
        Serial.print(INCREASE_BRIGHTNESS); Serial.print("\t");
        Serial.print(STAY_TIME);
        Serial.print(INCREASE_BRIGHTNESS);
        Serial.println("");
    #endif
  
    /*--
       對於 APA102 來說,setRGB 的參數要變成像下面這樣,顏色才會正常顯示
          setRGB( BLUE, GREEN, RED )
    --*/
    int i = 0, j = 0;
    // 下面顯示出整串流星的長度以及亮度變化,由 0 .. 7 是滅到亮
    for( i = INCREASE_BRIGHTNESS - 1; i < 256; i += INCREASE_BRIGHTNESS )
    {
        #ifdef DEBUG
            Serial.print( i );
            Serial.print("\t");
            Serial.println( j );
        #endif
        
        meteors[j++].setHSV( 228, 255, i );
    }
       
    /*下面顯示出整串流星的長度以及亮度變化,由 7... 0 是滅到亮
    for( i = 255; i >= 0; i -= INCREASE_BRIGHTNESS )
    {
      Serial.print( i );
      Serial.print("\t");
      Serial.println( j );
      meteors[j++].setHSV( 255, 255, i );
    }*/
    
}

void loop() {
  int i, ea, eb;   // 流星現在在燈串的位置;ea: 低位元組,eb: 高位元組
  int ma, mb;         // 流星陣列複製的起始位置;ma: 低位元組,mb: 高位元組

/*===================================================================================
*  流星由近端到遠端移動
-----------------------------------------------------------------------------------------------------------------------------------------------------*/
  for( i = 0; i < ( NUM_LEDS + NUM_METEORS); i++ )    // ea: 流星起頭的位置
  {
    // 設定 leds 全部為 black
    for( ea = 0; ea < NUM_LEDS; ea++ )
        leds[ea].setRGB( 0, 0 ,0 );

    // 找到複製 meteors 到 leds 的起頭與結束位置
    if( i < NUM_METEORS ) // 整個流星還未完全進入到燈串當中
    {
      ma = 0;
      mb = i;
      ea = NUM_METEORS - i - 1;
      eb = NUM_METEORS - 1;
    }
    else if( i >= NUM_LEDS )    // 流星頭部已經開始超出燈串
    {
      ma = i - NUM_METEORS + 1;
      mb = NUM_LEDS;
      ea = 0;
      eb = NUM_LEDS + NUM_METEORS - 1 - i;
    }
    else    // 流星在燈串中
    {
      ma = i - NUM_METEORS + 1;
      mb = i + 1;
      ea = 0;
      eb = NUM_METEORS;
    }
    
    #ifdef DEBUG
      Serial.print(F("i")); Serial.print(i); Serial.print("\t");
      Serial.print(F("ma")); Serial.print(ma); Serial.print("\t");
      Serial.print(F("mb")); Serial.print(mb); Serial.print("\t");
      Serial.print(F("ea")); Serial.print(ea); Serial.print("\t");
      Serial.print(F("eb")); Serial.print(eb); Serial.println("");
    #endif
    
    for( ma; ma < mb; ma++ )    // 複製流星的起頭位置
    {
      leds[ma] = meteors[ea++];
    }
    
    FastLED.show();
    delay(STAY_TIME);    // default 45 ms
  } 
  delay(1500);
  /*===================================================================================*/

  /*===================================================================================
  *  流星由遠端到近端移動
  -----------------------------------------------------------------------------------------------------------------------------------------------------*/
  for( i = ( NUM_LEDS + NUM_METEORS - 1 ); i >=0 ; i-- )    // ea: 流星起頭的位置
  {
    // 設定 leds 全部為 black
    for( ea = 0; ea < NUM_LEDS; ea++ )
        leds[ea].setRGB( 0, 0, 0 );
        
    // 找到複製 meteors 到 leds 的起頭與結束位置
    if( i < NUM_METEORS ) // 整個流星還未完全進入到燈串當中
    {
      ma = 0;
      mb = i;
      ea = i;
      eb = 0;
    }
    else if( i >= NUM_LEDS )    // 流星頭部已經開始超出燈串
    {
      ma = i - NUM_METEORS;
      mb = NUM_LEDS;
      ea = NUM_METEORS - 1;          
      eb = i - NUM_LEDS;
    }
    else    // 流星在燈串中
    {
      ma = i - NUM_METEORS;
      mb = i;
      ea = NUM_METEORS - 1;
      eb = 0;
    }
    
    #ifdef DEBUG
      Serial.print(F("i")); Serial.print(i); Serial.print("\t");
      Serial.print(F("ma")); Serial.print(ma); Serial.print("\t");
      Serial.print(F("mb")); Serial.print(mb); Serial.print("\t");
      Serial.print(F("ea")); Serial.print(ea); Serial.print("\t");
      Serial.print(F("eb")); Serial.print(eb); Serial.println("");
    #endif
    
    for( ma; ma < mb; ma++ )    // 複製流星的起頭位置
    {
      leds[ma] = meteors[ea--];    // eb++ [滅到亮],倒退
    }
    
    FastLED.show();
    delay(STAY_TIME);
  } 
  delay(1000);
  /*====================


結論:

相對於 WS2812B 單線式通訊的 RGB LED 控制,APA102 使用雙線式通訊相對比較在單晶片中實現,而且因為控制的資料格式明確,所以實作起來也相對輕鬆!

對於不是使用 Arduino 的使用者,若是要使用程式碼去實現 APA102 燈串的控制,只要將上面單晶片的程式碼主要部分轉換到您自己的晶片支援的語法格式,很快就可以達到跟上面或更好的結果!


<<部落格相關網頁文章>>

2 則留言:

  1. Hello sir I like you project gret job ,
    But 89s52 vs ws 2811 interface simpel plz. I request

    回覆刪除
  2. Hi,
    I think you can check Arduino library "FastLED" for information you want. I didn't do it before.

    回覆刪除

留言屬名為"Unknown"或"不明"的用戶,大多這樣的留言都會直接被刪除掉,不會得到任何回覆!

發問問題,請描述清楚你(妳)的問題,別人回答前不會想去 "猜" 問題是什麼?

不知道怎麼發問,請看 [公告] 部落格提問須知 - 如何問問題 !