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)

  1. // 2nd 1st 3rd 2nd 4th 3rd 6th 5th
  2. 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,以此類推,是不是直觀許多呢?

  1. volatile uint8_t g_au8frameBuffer[24] = {
  2. // R G B
  3. 0x80, 0x00, 0x00, // Red
  4. 0x80, 0x20, 0x00, // Orange
  5. 0x40, 0x40, 0x00, // Yellow
  6. 0x00, 0x80, 0x00, // Green
  7. 0x00, 0x00, 0xFF, // Blue
  8. 0x00, 0x40, 0x80, // Indigo
  9. 0x80, 0x00, 0xFF, // Violet
  10. 0x80, 0x80, 0x80 // White
  11. };

讀出4-byte存入LLSI的FIFO

當然,我們可以像下面這樣讀取陣列四次,然後透過移位的方式湊成32-bit的資料,再寫入LLSI的FIFO,但要做很多運算,這顯然不是個好方法。

  1. uint32_t u32data;
  2. u32data = g_au8frameBuffer[0];
  3. u32data |= g_au8frameBuffer[1] << 8;
  4. u32data |= g_au8frameBuffer[2] << 16;
  5. u32data |= g_au8frameBuffer[3] << 24;
  6. LLSI_WRITE_DATA(LLSI0, u32data);

好的方法:運用指標直接以32-bit的格式讀取8-bit陣列

既然NUC1263的Cortex-M23是32位元的處理器,我們就要好好善用它。

C的陣列若不加索引值,可以得到陣列起始的記憶體位址。所以如果只打g_au8frameBuffer可以得到一個8-bit指標;前面加上(uint32_t *),即可將8-bit指標轉換為32-bit指標。

  1. (uint32_t *)g_au8frameBuffer

指標中儲存的是記憶體位址,要存取指標指向的記憶體位址中儲存的值,得在前面加上一個星號*來解指標(dereference),如此以來就能直接以32-bit的格式存取陣列。

  1. *((uint32_t *)g_au8frameBuffer)

向LLSI寫入指標指向的32-bit資料

傳送完畢以後,我們就要將記憶體位址指向下一組資料,所以要從陣列的起始點加上g_u32DataCount++。

  1. /* Write the first word to trigger TXTH_INT */
  2. LLSI_WRITE_DATA(LLSI0, *((uint32_t *)g_au8frameBuffer + g_u32DataCount++));

如果不好懂的話,這一行其實可以分解成這樣:

  1. LLSI_WRITE_DATA(LLSI0, *((uint32_t *)g_au8frameBuffer + g_u32DataCount));
  2. g_u32DataCount++;

接著就是在中斷服務程式把陣列剩餘的資料傳送完,一樣可以看到上面那行的蹤跡:

  1. void LLSI0_IRQHandler()
  2. {
  3. if (LLSI_GetIntFlag(LLSI0, LLSI_TXTH_INT_MASK))
  4. {
  5. while(LLSI_GET_TX_FIFO_FULL_FLAG(LLSI0) == 0) // Fill up the TX FIFO
  6. {
  7. if (g_u32DataCount == (TEST_COUNT - 1))
  8. LLSI_SET_LAST_DATA(LLSI0); // Before writing last word data to LLSI_DATA, user must write LDT = 1
  9. if (g_u32DataCount < TEST_COUNT)
  10. LLSI_WRITE_DATA(LLSI0, *((uint32_t *)g_au8frameBuffer + g_u32DataCount++));
  11. else
  12. break;
  13. }
  14.  
  15. if(g_u32DataCount >= TEST_COUNT)
  16. {
  17. LLSI_DisableInt(LLSI0, LLSI_TXTH_INT_MASK);
  18. }
  19. }
  20.  
  21. if (LLSI_GetIntFlag(LLSI0, LLSI_FEND_INT_MASK)) // FENDIF will be set to 1 when LLSI transfer last pixel data.
  22. {
  23. g_u32FrameEnd = 1;
  24. LLSI_ClearIntFlag(LLSI0, LLSI_FEND_INT_MASK);
  25. }
  26. }

更新資料時一樣是等待Frame End Interrupt後再進行。本範例是將彩虹顏色分別點亮七段顯示器的每一顆LED,每次更新是把顏色會往後移動一顆,也就是每一段會輪流顯示一種顏色。我用了一個雙層迴圈,裡面那一層一次移動1個byte,由於1顆ARGB LED共3色(3個byte),所以再靠外面那層重複三次,才能完整移動1顆ARGB LED的色彩。

  1. if (g_u32FrameEnd == 1) // Wait for FEND_INT
  2. {
  3. for (int i=0; i<3 for="" i="" int="" j="" u8tmp="g_au8frameBuffer[23];">0; j--)
  4. g_au8frameBuffer[j] = g_au8frameBuffer[j-1];
  5. g_au8frameBuffer[0] = u8Tmp;
  6. }
  7.  
  8. g_u32FrameEnd = 0;
  9. g_u32DataCount = 0;
  10.  
  11. LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK);
  12. CLK_SysTickLongDelay(500000);
  13. }


成果影片

最後盡可能簡短提一下技術細節:為什麼用指標可以從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韌體開發更加直觀!本篇點到為止,剩下期待讀者們分享您們運用的方式囉!

完整程式碼:
  1. #include <stdio.h>
  2. #include "NuMicro.h"
  3.  
  4. #define HCLK_CLK 72000000
  5. #define TEST_COUNT 6
  6.  
  7. // 2nd 1st
  8. // 32-bit color format is 0xRRBBGGRR for the ELSS-4X6RGBWA/T2/S290-C
  9. volatile uint8_t g_au8frameBuffer[24] = {
  10. 0x80, 0x00, 0x00, // Red
  11. 0x80, 0x20, 0x00, // Orange
  12. 0x40, 0x40, 0x00, // Yellow
  13. 0x00, 0x80, 0x00, // Green
  14. 0x00, 0x00, 0xFF, // Blue
  15. 0x00, 0x40, 0x80, // Indigo
  16. 0x80, 0x00, 0xFF, // Violet
  17. 0x80, 0x80, 0x80 // White
  18. };
  19. volatile uint32_t g_u32DataCount = 0;
  20. volatile uint32_t g_u32FrameEnd = 0;
  21.  
  22. void LLSI0_IRQHandler()
  23. {
  24. if (LLSI_GetIntFlag(LLSI0, LLSI_TXTH_INT_MASK))
  25. {
  26. while(LLSI_GET_TX_FIFO_FULL_FLAG(LLSI0) == 0) // Fill up the TX FIFO
  27. {
  28. if (g_u32DataCount == (TEST_COUNT - 1))
  29. LLSI_SET_LAST_DATA(LLSI0); // Before writing last word data to LLSI_DATA, user must write LDT = 1
  30. if (g_u32DataCount < TEST_COUNT)
  31. LLSI_WRITE_DATA(LLSI0, *((uint32_t *)g_au8frameBuffer + g_u32DataCount++));
  32. else
  33. break;
  34. }
  35.  
  36. if(g_u32DataCount >= TEST_COUNT)
  37. {
  38. LLSI_DisableInt(LLSI0, LLSI_TXTH_INT_MASK);
  39. }
  40. }
  41.  
  42. if (LLSI_GetIntFlag(LLSI0, LLSI_FEND_INT_MASK)) // FENDIF will be set to 1 when LLSI transfer last pixel data.
  43. {
  44. g_u32FrameEnd = 1;
  45. LLSI_ClearIntFlag(LLSI0, LLSI_FEND_INT_MASK);
  46. }
  47. }
  48.  
  49. void SYS_Init(void)
  50. {
  51. /*---------------------------------------------------------------------------------------------------------*/
  52. /* Init System Clock */
  53. /*---------------------------------------------------------------------------------------------------------*/
  54. /* Enable HIRC clock */
  55. CLK_EnableXtalRC(CLK_PWRCTL_HIRCEN_Msk);
  56.  
  57. /* Wait for HIRC clock ready */
  58. CLK_WaitClockReady(CLK_STATUS_HIRCSTB_Msk);
  59.  
  60. /* Set core clock to HCLK_CLK Hz */
  61. CLK_SetCoreClock(HCLK_CLK);
  62.  
  63. /* Enable UART0 module clock */
  64. CLK_EnableModuleClock(UART0_MODULE);
  65.  
  66. /* Select UART0 module clock source as HIRC/2 and UART0 module clock divider as 1 */
  67. CLK_SetModuleClock(UART0_MODULE, CLK_CLKSEL1_UART0SEL_HIRC_DIV2, CLK_CLKDIV0_UART0(1));
  68.  
  69. /* Enable LLSI0 module clock */
  70. CLK_EnableModuleClock(LLSI0_MODULE);
  71.  
  72. /*---------------------------------------------------------------------------------------------------------*/
  73. /* Init I/O Multi-function */
  74. /*---------------------------------------------------------------------------------------------------------*/
  75. /* Set PB multi-function pins for UART0 RXD and TXD */
  76. SYS->GPB_MFPH = (SYS->GPB_MFPH & (~(UART0_RXD_PB12_Msk | UART0_TXD_PB13_Msk))) | UART0_RXD_PB12 | UART0_TXD_PB13;
  77.  
  78. /* Set PC multi-function pins for LLSI0 */
  79. SET_LLSI0_OUT_PC5();
  80. }
  81.  
  82. void UART0_Init()
  83. {
  84. /*---------------------------------------------------------------------------------------------------------*/
  85. /* Init UART */
  86. /*---------------------------------------------------------------------------------------------------------*/
  87. /* Reset UART0 module */
  88. SYS_ResetModule(UART0_RST);
  89.  
  90. /* Configure UART0 and set UART0 Baudrate */
  91. UART_Open(UART0, 115200);
  92. }
  93.  
  94. void LLSI_Init(void)
  95. {
  96. /*---------------------------------------------------------------------------------------------------------*/
  97. /* Init LLSI */
  98. /*---------------------------------------------------------------------------------------------------------*/
  99. /* Configure as software mode, RGB output format, 8 pixels in a frame and idle output low */
  100. /* Set clock divider. LLSI clock rate = 72MHz */
  101. /* Set data transfer period. T_Period = 1250ns */
  102. /* Set duty period. T_T0H = 400ns; T_T1H = 850ns */
  103. /* Set reset command period. T_ResetPeriod = 50000ns */
  104. LLSI_Open(LLSI0, LLSI_MODE_SW, LLSI_FORMAT_RGB, HCLK_CLK, 1250, 400, 850, 50000, 8, LLSI_IDLE_LOW);
  105.  
  106. /* Set TX FIFO threshold */
  107. LLSI_SetFIFO(LLSI0, 2);
  108.  
  109. /* Enable reset command function */
  110. LLSI_ENABLE_RESET_COMMAND(LLSI0);
  111.  
  112. NVIC_EnableIRQ(LLSI0_IRQn);
  113. }
  114.  
  115. void main(void)
  116. {
  117. uint8_t u8Tmp;
  118. /* Unlock protected registers */
  119. SYS_UnlockReg();
  120.  
  121. /* Init System, IP clock and multi-function I/O. */
  122. SYS_Init();
  123.  
  124. /* Lock protected registers */
  125. SYS_LockReg();
  126.  
  127. /* Init UART0 for printf */
  128. UART_Open(UART0, 115200);
  129.  
  130. printf("\n\nCPU @ %d Hz\n", SystemCoreClock);
  131.  
  132. /* Init LLSI */
  133. LLSI_Init();
  134.  
  135. /* Enable Transmit FIFO Threshold Interrupt and Frame End Interrupt */
  136. LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK | LLSI_FEND_INT_MASK);
  137.  
  138. /* Write the first word to trigger TXTH_INT */
  139. LLSI_WRITE_DATA(LLSI0, *((uint32_t *)g_au8frameBuffer + g_u32DataCount++));
  140.  
  141. while(1)
  142. {
  143. if (g_u32FrameEnd == 1) // Wait for FEND_INT
  144. {
  145. for (int i=0; i<3; i++)
  146. {
  147. u8Tmp = g_au8frameBuffer[23];
  148.  
  149. for (int j=23; j>0; j--)
  150. g_au8frameBuffer[j] = g_au8frameBuffer[j-1];
  151. g_au8frameBuffer[0] = u8Tmp;
  152. }
  153.  
  154. g_u32FrameEnd = 0;
  155. g_u32DataCount = 0;
  156.  
  157. LLSI_EnableInt(LLSI0, LLSI_TXTH_INT_MASK);
  158. CLK_SysTickLongDelay(500000);
  159. }
  160. }
  161. }
  162.  


留言

這個網誌中的熱門文章

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

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

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