開箱實作NUC1263 MCU!探索新唐獨家LLSI ARGB LED控制介面!

連假前看到新唐在論壇介紹NUC1263這顆MCU,除了CPU是ARMv8的Cortex-M23之外,還有一個新唐專利硬體周邊LLSI,加上億光同學塞了他們新開發的神奇產品給我,腦袋裡面馬上冒出漂亮的應用,於是就上新唐線上商店下單一片NuMaker-NUC1263SD開發板和5顆晶片,沒想到隔天就到了~ 

新唐NUC1263系列產品帶給您炫彩的ARGB燈效控制 -LLSI介面簡易設計,方便好用-

不過作品還在設計當中,敬請期待。先來用現成的Adafruit NeoPixel Ring試玩一下NUC1263這顆MCU和它的LLSI吧!一樣會在GitHub和留言區提供完整測試程式碼喔!(看到專案名稱,知道我又要做什麼了吧XD)

https://github.com/danchouzhou/LLSIClock/blob/main/firmware/LLSI_RGBW/main.c

先幫讀者整理一下這顆NUC1263重點特色

  • Arm Cortex-M23 72 MHz,ARMv8-M架構的CPU
  • 內建64 KB Flash和20 KB SRAM
  • 工作電壓2.5 V ~ 5.5 V

除了常見的UART、SPI、I2C、ADC、PWM之外,NUC1263還有I3C、USB和4個DAC,其中I3C還可以從獨立VDDIO電源接腳,輸入I3C匯流排上的電壓,讓I3C不用外接level shifter,最低就能支援0.95 V的電壓準位。不過最大的特色還是全新推出的專利獨家功能LLSI。

第一次聽到LLSI?

LLSI全稱是LED Light Strip Interface,其實就是專門為可定址LED (ARGB LED, addressable RGB LED) 設計的控制介面,常見的型號例如WS2812B、SK6812,或者是Adafruit推出的NeoPixel系列,這種介面的好處就是只要一條信號線從第一顆LED的DIN輸入,第一顆的DOUT串到第二顆的DIN,以此類推就可以控制整串的全彩LED。

由於只有一條信號線,ARGB LED的控制介面是利用信號high low時長不同,來代表資料的0和1。新唐將這種控制時序設計成硬體,取了酷酷的名字LLSI並整合進MCU裡面,成為首度推出有硬體控制ARGB LED能力的MCU公司,這也是新唐MCU才有的獨家功能。下圖節錄自NUC1263的手冊:

RGB動態燈光效果的市場趨勢

隨著RGB動態燈光效果的需求越來越受歡迎,已經可以在電競鍵盤、滑鼠、主機板、顯示卡、機殼、風扇、電源、散熱器、耳麥等數不清的電競周邊產品看見RGB動態光效華麗的應用,現在就連Windows 11的設定中,還直接內建調整燈光效果的選項。甚至是在一些電視背面、燈飾也陸續出現RGB LED,來營造各式各樣的居家氛圍。

控制ARGB LED

以往用MCU控制ARGB LED的方式,是去改變GPIO的輸出狀態,期間以NOP指令或是timer delay,以軟體方式刻出ARGB LED的時序。為了達到時序精度要求,在傳輸的過程中CPU不得做其它事情,還得把中斷關閉來保證資料能完整傳輸出去。例如之前porting到新唐MCU的NeoPixel Library即是如此,沒有辦法的辦法。

RGB動態光效越來越受歡迎之外,LED數量、花樣也是越變越多、越來越複雜,軟體刻時序的方式恐怕不再能滿足人們的胃口,韌體設計更是令工程師們頭疼。

世界首創、獨家功能、最新專利!LLSI的優勢

幸好新唐推出了LLSI,使用LLSI時只要將每顆LED色彩資料填入buffer內,硬體會自動幫我們把資料按照ARGB LED的時序丟出去,這就跟一些串列通訊介面的使用邏輯一樣。

NUC1263的LLSI提供了深度16-byte的FIFO buffer,且支援閾值中斷、DMA傳輸,這樣的設計騰出了CPU資源,讓CPU去做原本該做的工作,以及去算那些花花綠綠的燈效;內建的64 KB / 20 KB的記憶體,即使LED數量一多,記憶體還是很夠用!

開箱實作

介紹完LLSI了,接著讓我們深入韌體技術的部份吧!無論您習慣的是Keil MDK、IAR或是新唐自家的NuEclipse開發環境,都可以參考NuMaker-NUC1263SD的手冊來將開發環境設定起來。

https://www.nuvoton.com/export/resource-files/en-us--UM_NuMaker-NUC1263SD_EN_Rev1.00.pdf

用LLSI也能控制RGBW「四色」可定址LED

從NUC1263的手冊中可以發現,LLSI是為RGB三色的可定址LED所設計的,一次以3個bytes為單位送出資料,支援RGB和GRB兩種不同的排列順序。能控制一般的ARGB不稀奇,今天就來挑戰看看能不能用LLSI控制ARGB的變種型號,四色的ARGB”W”,來帶過實作的環節! 

為了測試NUC1263的LLSI,從箱子裡挖出一直珍藏著捨不得用的Adafruit NeoPixel Ring - 24 x RGBW Natural White - ~4500K,都還記得是當初在美國念書,從當地的商店Micro Center花二十幾塊美金購買的。它除了常見的RGB三色之外額外多了一個白光,能比RGB調出來的白色有更好的演色性 (CRI)。

用LLSI分別點亮RGBW其中一個顏色

要調色、做效果首先就是要知道一顆LED中的R、G、B、W排列順序以及要怎麼單獨控制,所以就先從單一個顏色開始點亮吧!

LLSI的使用方式很簡單,只要這三行就能啟動LLSI硬體,我使用NUC1263的PC.5來當LLSI0的輸出。由於這次使用的是四色LED,並非LLSI原先設計的三色LED,因此數量的欄位要自己換算一下:

LLSI_PCNT = 24 / 3 x 4 = 32

CLK_EnableModuleClock(LLSI0_MODULE); // Enable LLSI0 module clock
// T_Period = 1250ns; T_T0H = 400ns; T_T1H = 850ns; T_ResetPeriod = 50000ns; LED count = 32
LLSI_Open(LLSI0, LLSI_MODE_SW, LLSI_FORMAT_RGB, HCLK_CLK, 1250, 400, 850, 50000, 0, LLSI_IDLE_LOW);
SET_LLSI0_OUT_PC5(); // Set PC.5 multi-function pins for LLSI0

下面則是分別點亮四顆LED其中的R、G、B、W,每個顏色占用8-bit,測試後可得知排列順序為0xWWBBRRGG。不過最後為什麼還多送了一筆0x00000000呢?正如剛剛提到「LLSI是為RGB三色的可定址LED所設計的,一次以3個bytes為單位送出資料」,四個RGBW一共16-byte無法整除3,LLSI會認為資料尚未備齊,不會將最後一顆的白光給送出去。所以才要填入第五筆資料「騙」它,將最後一顆的資料送出去。

// 0xWWBBRRGG
LLSI_WRITE_DATA(LLSI0, 0x000000FF);
LLSI_WRITE_DATA(LLSI0, 0x0000FF00);
LLSI_WRITE_DATA(LLSI0, 0x00FF0000);
LLSI_WRITE_DATA(LLSI0, 0xFF000000);
LLSI_WRITE_DATA(LLSI0, 0x00000000);

用LLSI製作跑馬燈效果

幸好這次用的NeoPixel Ring是24顆RGBW,是3和4的公倍數,在LLSI_Open()時將LED數量填入32,製作RGBW跑馬燈效果就不用額外去處理。要玩效果,程式的架構就不再是一顆一顆點了,而是變成這樣:

  1. 開一個陣列g_au32frameBuffer[]當成”影格”
  2. 開啟閾值中斷,當LLSI FIFO中的內容低過設定的數量,通知CPU將接下來的資料補充進去
  3. 開啟”影格”結束中斷,當所有LED的資料都傳送完畢產生中斷,將旗標g_u32FrameEnd設為’1’,並更新圖案樣式g_u32PatternToggle
  4. 於主程式中判斷旗標,當前一”幀”傳送完畢,即可運算新的圖案更新至”影格中”

從這段範例就可以明顯感受到使用LLSI控制ARGB LED的便利性了,將圖案填入g_au32frameBuffer之後,由LLSI硬體自動產生時序,也會在FIFO低於下限時硬體自動產生中斷,通知CPU補充彈匣!保留了許多CPU資源。

透過下面三行程式碼,可以開啟上述兩種中斷功能:

/* Set TX FIFO threshold */
LLSI_SetFIFO(LLSI0, 2);
NVIC_EnableIRQ(LLSI0_IRQn);
/* Enable Transmit FIFO Threshold Interrupt and Frame End Interrupt */
LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK | LLSI_FEND_INT_MASK);

中斷服務程式:

void LLSI0_IRQHandler()
{
    if (LLSI_GetIntFlag(LLSI0, LLSI_TXTH_INT_MASK))
    {
        while(LLSI_GET_TX_FIFO_FULL_FLAG(LLSI0) == 0) // Fill up the TX FIFO
        {
            if (g_u32DataCount == (TEST_COUNT - 1))
                LLSI_SET_LAST_DATA(LLSI0); // Before writing last word data to LLSI_DATA, user must write LDT = 1
            
            if (g_u32DataCount < TEST_COUNT)
                LLSI_WRITE_DATA(LLSI0, g_au32frameBuffer[g_u32DataCount++]);
            else
                break;
        }

        if(g_u32DataCount >= TEST_COUNT)
        {
            LLSI_DisableInt(LLSI0, LLSI_TXTH_INT_MASK);
        }
    }

    if (LLSI_GetIntFlag(LLSI0, LLSI_FEND_INT_MASK)) // FENDIF will be set to 1 when LLSI transfer last pixel data.
    {
        g_u32FrameEnd = 1;
        g_u32PatternToggle++;
        LLSI_ClearIntFlag(LLSI0, LLSI_FEND_INT_MASK);
    }
}

主程式更新圖案的算法:

if (g_u32FrameEnd == 1)  // Wait for FEND_INT
{
    u32Tmp = g_u32PatternToggle % 4;
    
    /* Generate a new frame */
    for (int i=0; i<TEST_COUNT; i++)
    {
        if (i % 4 == u32Tmp)
            g_au32frameBuffer[i] = 0xFF000000 >> (u32Tmp * 8);
        else
            g_au32frameBuffer[i] = 0x00000000;
    }

    g_u32FrameEnd = 0;
    g_u32DataCount = 0;

    LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK);
    
    CLK_SysTickDelay(50000);
}

成果展示影片

文章最後邀請讀者加入新唐論壇,除了能一起參與技術交流之外,還能領到新唐原廠線上商店的9折優惠券,想玩玩看最新的LLSI獨家專利技術嗎?敬請把握機會了!

https://forum.nuvoton.com/zh/programs-events/seminars-events/14354

完整程式碼:

#include <stdio.h>
#include "NuMicro.h"

#define HCLK_CLK    72000000
#define TEST_COUNT  24

// Color format is 0xWWBBRRGG for the Adafruit NeoPixel Ring 24 x RGBW (2862)
volatile uint32_t g_au32frameBuffer[24] = {
    0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 
    0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 
    0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 
    0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000
};
volatile uint32_t g_u32PatternToggle = 0;
volatile uint32_t g_u32DataCount = 0;
volatile uint32_t g_u32FrameEnd = 0;

void LLSI0_IRQHandler()
{
    if (LLSI_GetIntFlag(LLSI0, LLSI_TXTH_INT_MASK))
    {
        while(LLSI_GET_TX_FIFO_FULL_FLAG(LLSI0) == 0) // Fill up the TX FIFO
        {
            if (g_u32DataCount == (TEST_COUNT - 1))
                LLSI_SET_LAST_DATA(LLSI0); // Before writing last word data to LLSI_DATA, user must write LDT = 1
            
            if (g_u32DataCount < TEST_COUNT)
                LLSI_WRITE_DATA(LLSI0, g_au32frameBuffer[g_u32DataCount++]);
            else
                break;
        }

        if(g_u32DataCount >= TEST_COUNT)
        {
            LLSI_DisableInt(LLSI0, LLSI_TXTH_INT_MASK);
        }
    }

    if (LLSI_GetIntFlag(LLSI0, LLSI_FEND_INT_MASK)) // FENDIF will be set to 1 when LLSI transfer last pixel data.
    {
        g_u32FrameEnd = 1;
        g_u32PatternToggle++;
        LLSI_ClearIntFlag(LLSI0, LLSI_FEND_INT_MASK);
    }
}

void SYS_Init(void)
{
    /*---------------------------------------------------------------------------------------------------------*/
    /* Init System Clock                                                                                       */
    /*---------------------------------------------------------------------------------------------------------*/
    /* Enable HIRC clock */
    CLK_EnableXtalRC(CLK_PWRCTL_HIRCEN_Msk);

    /* Wait for HIRC clock ready */
    CLK_WaitClockReady(CLK_STATUS_HIRCSTB_Msk);

    /* Set core clock to HCLK_CLK Hz */
    CLK_SetCoreClock(HCLK_CLK);

    /* Enable UART0 module clock */
    CLK_EnableModuleClock(UART0_MODULE);

    /* Select UART0 module clock source as HIRC/2 and UART0 module clock divider as 1 */
    CLK_SetModuleClock(UART0_MODULE, CLK_CLKSEL1_UART0SEL_HIRC_DIV2, CLK_CLKDIV0_UART0(1));

    /* Enable LLSI0 module clock */
    CLK_EnableModuleClock(LLSI0_MODULE);

    /*---------------------------------------------------------------------------------------------------------*/
    /* Init I/O Multi-function                                                                                 */
    /*---------------------------------------------------------------------------------------------------------*/
    /* Set PB multi-function pins for UART0 RXD and TXD */
    SYS->GPB_MFPH = (SYS->GPB_MFPH & (~(UART0_RXD_PB12_Msk | UART0_TXD_PB13_Msk))) | UART0_RXD_PB12 | UART0_TXD_PB13;

    /* Set PC multi-function pins for LLSI0 */
    SET_LLSI0_OUT_PC5();
}

void UART0_Init()
{
    /*---------------------------------------------------------------------------------------------------------*/
    /* Init UART                                                                                               */
    /*---------------------------------------------------------------------------------------------------------*/
    /* Reset UART0 module */
    SYS_ResetModule(UART0_RST);

    /* Configure UART0 and set UART0 Baudrate */
    UART_Open(UART0, 115200);
}

void LLSI_Init(void)
{
    /*---------------------------------------------------------------------------------------------------------*/
    /* Init LLSI                                                                                               */
    /*---------------------------------------------------------------------------------------------------------*/
    /* Configure as software mode, RGB output format, 6 pixels in a frame and idle output low */
    /* Set clock divider. LLSI clock rate = 72MHz */
    /* Set data transfer period. T_Period = 1250ns */
    /* Set duty period. T_T0H = 400ns; T_T1H = 850ns */
    /* Set reset command period. T_ResetPeriod = 50000ns */
    LLSI_Open(LLSI0, LLSI_MODE_SW, LLSI_FORMAT_RGB, HCLK_CLK, 1250, 400, 850, 50000, 32, LLSI_IDLE_LOW);

    /* Set TX FIFO threshold */
    LLSI_SetFIFO(LLSI0, 2);

    /* Enable reset command function */
    LLSI_ENABLE_RESET_COMMAND(LLSI0);

    NVIC_EnableIRQ(LLSI0_IRQn);
}

void main(void)
{
    uint32_t u32Tmp;
    
    /* Unlock protected registers */
    SYS_UnlockReg();

    /* Init System, IP clock and multi-function I/O. */
    SYS_Init();

    /* Lock protected registers */
    SYS_LockReg();

    /* Init UART0 for printf */
    UART_Open(UART0, 115200);

    printf("\n\nCPU @ %d Hz\n", SystemCoreClock);

    /* Init LLSI */
    LLSI_Init();

    /* Enable Transmit FIFO Threshold Interrupt and Frame End Interrupt */
    LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK | LLSI_FEND_INT_MASK);

    LLSI_WRITE_DATA(LLSI0, g_au32frameBuffer[g_u32DataCount++]); // Write the first word to trigger TXTH_INT

    while(1)
    {
        if (g_u32FrameEnd == 1)  // Wait for FEND_INT
        {
            u32Tmp = g_u32PatternToggle % 4;
            
            /* Generate a new frame */
            for (int i=0; i<TEST_COUNT; i++)
            {
                if (i % 4 == u32Tmp)
                    g_au32frameBuffer[i] = 0xFF000000 >> (u32Tmp * 8);
                else
                    g_au32frameBuffer[i] = 0x00000000;
            }

            g_u32FrameEnd = 0;
            g_u32DataCount = 0;

            LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK);
            
            CLK_SysTickDelay(50000);
        }
    }
}


留言

這個網誌中的熱門文章

無法被取代的指針型三用電表(一):前言

關於新唐科技NuMicro ISP的介紹和使用方式

科風 BNT-1000AP 黑武士系列不斷電系統開箱拆解、簡易評測及經驗分享