NUC1263 LLSIClock開發筆記:運用指標提高LLSI Frame Buffer的可讀性、易用性
LLSI設計時大概是考量到系統整體效能,資料是以32-bit的寬度寫入LLSI FIFO的。因此最直接的方式就是將frame buffer宣告成32-bit的陣列,這個方式拿來點四色”ARGBW” LED時倒是沒什麼問題。但是當我試著用LLSI點亮三色ARGB七段顯示器的時候就有發現,1個顏色8-bit,1顆ARGB LED 3個顏色總共24-bit,資料的排列順序會就變成下面這樣 …
(第一行的註解是指第幾顆LED)
- // 2nd 1st 3rd 2nd 4th 3rd 6th 5th
- uint32_t g_au32frameBuffer[6] = {0xRRBBGGRR, 0xGGRRBBGG, 0xBBGGRRBB, 0xRRBBGGRR ...}
光用看的是不是就很頭大? 再想到之後要去個別調整每一顆ARGB LED的色彩、亮度,頭就更大了。這也讓我開始思考,有沒有辦法將陣列宣告成8-bit,然後以32-bit的格式來讀取8-bit陣列,寫入LLSI的FIFO。
完整程式碼一樣可以看留言區或是GitHub
https://github.com/danchouzhou/LLSIClock/blob/main/firmware/LLSI_RGB/main.c
從32-bit陣列轉變成8-bit陣列
陣列如果變成這樣讓g_au8frameBuffer[0]~[2],儲存第1顆ARGB LED的R, G, B;g_au8frameBuffer[3]~[5],儲存第2顆ARGB LED的R, G, B,以此類推,是不是直觀許多呢?
- volatile uint8_t g_au8frameBuffer[24] = {
- // R G B
- 0x80, 0x00, 0x00, // Red
- 0x80, 0x20, 0x00, // Orange
- 0x40, 0x40, 0x00, // Yellow
- 0x00, 0x80, 0x00, // Green
- 0x00, 0x00, 0xFF, // Blue
- 0x00, 0x40, 0x80, // Indigo
- 0x80, 0x00, 0xFF, // Violet
- 0x80, 0x80, 0x80 // White
- };
讀出4-byte存入LLSI的FIFO
當然,我們可以像下面這樣讀取陣列四次,然後透過移位的方式湊成32-bit的資料,再寫入LLSI的FIFO,但要做很多運算,這顯然不是個好方法。
- uint32_t u32data;
- u32data = g_au8frameBuffer[0];
- u32data |= g_au8frameBuffer[1] << 8;
- u32data |= g_au8frameBuffer[2] << 16;
- u32data |= g_au8frameBuffer[3] << 24;
- LLSI_WRITE_DATA(LLSI0, u32data);
好的方法:運用指標直接以32-bit的格式讀取8-bit陣列
既然NUC1263的Cortex-M23是32位元的處理器,我們就要好好善用它。
C的陣列若不加索引值,可以得到陣列起始的記憶體位址。所以如果只打g_au8frameBuffer可以得到一個8-bit指標;前面加上(uint32_t *),即可將8-bit指標轉換為32-bit指標。
- (uint32_t *)g_au8frameBuffer
指標中儲存的是記憶體位址,要存取指標指向的記憶體位址中儲存的值,得在前面加上一個星號*來解指標(dereference),如此以來就能直接以32-bit的格式存取陣列。
- *((uint32_t *)g_au8frameBuffer)
向LLSI寫入指標指向的32-bit資料
傳送完畢以後,我們就要將記憶體位址指向下一組資料,所以要從陣列的起始點加上g_u32DataCount++。
- /* Write the first word to trigger TXTH_INT */
- LLSI_WRITE_DATA(LLSI0, *((uint32_t *)g_au8frameBuffer + g_u32DataCount++));
如果不好懂的話,這一行其實可以分解成這樣:
- LLSI_WRITE_DATA(LLSI0, *((uint32_t *)g_au8frameBuffer + g_u32DataCount));
- g_u32DataCount++;
接著就是在中斷服務程式把陣列剩餘的資料傳送完,一樣可以看到上面那行的蹤跡:
- 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, *((uint32_t *)g_au8frameBuffer + 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;
- LLSI_ClearIntFlag(LLSI0, LLSI_FEND_INT_MASK);
- }
- }
更新資料時一樣是等待Frame End Interrupt後再進行。本範例是將彩虹顏色分別點亮七段顯示器的每一顆LED,每次更新是把顏色會往後移動一顆,也就是每一段會輪流顯示一種顏色。我用了一個雙層迴圈,裡面那一層一次移動1個byte,由於1顆ARGB LED共3色(3個byte),所以再靠外面那層重複三次,才能完整移動1顆ARGB LED的色彩。
- if (g_u32FrameEnd == 1) // Wait for FEND_INT
- {
- for (int i=0; i<3 for="" i="" int="" j="" u8tmp="g_au8frameBuffer[23];">0; j--)
- g_au8frameBuffer[j] = g_au8frameBuffer[j-1];
- g_au8frameBuffer[0] = u8Tmp;
- }
- g_u32FrameEnd = 0;
- g_u32DataCount = 0;
- LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK);
- CLK_SysTickLongDelay(500000);
- }
成果影片
最後盡可能簡短提一下技術細節:為什麼用指標可以從8-bit陣列讀出32-bit的資料?
這要從資料是如何儲存在記憶體中,以及陣列是如何儲存在記憶體講起了。
端序
記憶體通常以位元組(byte)為單位組成,每一個位址(Address)會對應一個byte的資料,事實上C的指標就是在紀錄這邊所提的記憶體位址。
那麼uint32_t的變數是如何儲存在記憶體當中的? 這就要了解「端序 (Endianness)」。
https://en.wikipedia.org/wiki/Endianness
所謂「端序」就是指記憶體的起始位址要從高位元組或是從低位元組開始存放,請參考維基百科上由Aeroid發表容易理解的圖片。不過注意喔,每一個位元組的MSB, LSB的順序則是不改變的。
Arm的處理器IP可以選擇big-endian (BE) 或是little-endian (LE),不過現今主流的習慣是little-endian,可以參考以下文獻。
“ARM cores support both modes, but are most commonly used in, and typically default to little-endian mode.”
https://developer.arm.com/documentation/den0013/latest/Porting/Endianness
新唐的MCU也不例外,採用的是little-endian data format,這在手冊中也有註明。(程式記憶體、資料記憶體、中斷向量表通常都會採用相同的little-endian格式)
陣列
在C語言中,陣列在記憶體中會被存放在一塊連續的記憶體區域中,也就是宣告一個陣列後,陣列中的每一個元素會依序排列在連續的記憶體位址,且每一個元素佔用相同大小的記憶體空間。
指標
在Address Bus為32-bit的系統中,無論將指標宣告為uint8_t *、uint16_t *或是uint32_t *,實際上指標儲存的都是一個32-bit的位址,差別在於操作指標時要以幾個byte為單位,例如指標+1要加幾個位址,或是dereference要讀取的位元組數量。本篇範例是32-bit指標(uint32_t *)g_au8frameBuffer + g_u32DataCount,g_u32DataCount如果是1,實際上位址會是+4(因為32-bit資料由4-byte組成),且dereference一次會讀取4-byte。
所以8-bit陣列若以32-bit指標來存取,綜合前述「little-endian data format」、「陣列在記憶體中會被存放在一塊連續的記憶體區域中」,dereference後的排列順序就會變成這樣,是不是就恢復LLSI所需的32-bit格式了呢?
備註:如果是8051,雖然它是8-bit處理器,不過Address Bus為16-bit,指標儲存的就會是個16-bit的位址。
結論
了解陣列在記憶體中的儲存方式並善用指標,甚至可以用二維陣列或是C的結構,來定義ARGB LED的資料結構,使LLSI韌體開發更加直觀!本篇點到為止,剩下期待讀者們分享您們運用的方式囉!
- #include <stdio.h>
- #include "NuMicro.h"
- #define HCLK_CLK 72000000
- #define TEST_COUNT 6
- // 2nd 1st
- // 32-bit color format is 0xRRBBGGRR for the ELSS-4X6RGBWA/T2/S290-C
- volatile uint8_t g_au8frameBuffer[24] = {
- 0x80, 0x00, 0x00, // Red
- 0x80, 0x20, 0x00, // Orange
- 0x40, 0x40, 0x00, // Yellow
- 0x00, 0x80, 0x00, // Green
- 0x00, 0x00, 0xFF, // Blue
- 0x00, 0x40, 0x80, // Indigo
- 0x80, 0x00, 0xFF, // Violet
- 0x80, 0x80, 0x80 // White
- };
- 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, *((uint32_t *)g_au8frameBuffer + 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;
- 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, 8 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, 8, 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)
- {
- uint8_t u8Tmp;
- /* 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);
- /* Write the first word to trigger TXTH_INT */
- LLSI_WRITE_DATA(LLSI0, *((uint32_t *)g_au8frameBuffer + g_u32DataCount++));
- while(1)
- {
- if (g_u32FrameEnd == 1) // Wait for FEND_INT
- {
- for (int i=0; i<3; i++)
- {
- u8Tmp = g_au8frameBuffer[23];
- for (int j=23; j>0; j--)
- g_au8frameBuffer[j] = g_au8frameBuffer[j-1];
- g_au8frameBuffer[0] = u8Tmp;
- }
- g_u32FrameEnd = 0;
- g_u32DataCount = 0;
- LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK);
- CLK_SysTickLongDelay(500000);
- }
- }
- }
留言
張貼留言