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); } 3>
成果影片
最後盡可能簡短提一下技術細節:為什麼用指標可以從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); } } }
留言
張貼留言