NUC1263 LLSIClock開發筆記:LLSI與PDMA搭配使用
DMA是什麼?
DMA的全稱叫做Direct Memory Access,「直接記憶體存取」。它可以直接根據使用者設定的來源位址、目標位址、長度等,直接搬移記憶體中的資料,而不用CPU的介入,幫CPU分攤一些反覆搬移的事情。尤其在搬移大量資料時,或者一些CPU負載重的應用情境之下,藉由DMA的幫忙可以顯著提高系統整體的效能。
在計算機系統中,CPU之外的硬體周邊裝置也經常會映射在一些記憶體位址中,讓使用者能像讀寫記憶體般,輕易地操作它們,稱作Memory-mapped IO。因此DMA也可以搬移硬體周邊裝置的資料。
PDMA是什麼?
不過在微控制器的應用中,資料的搬移往往需要根據周邊裝置的狀態同步。DMA前面加了一個P就是Peripheral Direct Memory Access,意即可以經由硬體周邊裝置產生的事件當成是DMA的傳輸請求,直接觸發DMA傳輸。
以這次的NUC1263為例,LLSI硬體提供了深度4-word (16-byte)的FIFO buffer,是在FIFO有足夠空間時,由LLSI硬體產生信號直接向PDMA發送傳輸請求,依序將整個Frame buffer傳輸完畢,期間就不需要CPU介入該過程。
使用PDMA的好處
透過硬體產生的事件,直接將資料搬移的任務交給PDMA,而非反覆讓CPU進中斷服務程式搬移資料,把CPU的資源留給那些真正需要運算的程式。
讓我們開始看看LLSI+PDMA的程式吧!
基於上一篇文章「運用指標提高LLSI Frame Buffer的可讀性、易用性」的程式下去修改成PDMA傳輸的版本,主要留意下面這些內容。
- 要記得在SYS_Init()中開啟LLSI和PDMA的clock
- LLSI_Open()要將工作模式設為LLSI_MODE_PDMA
首先在SYS_Init()中除了LLSI之外還要記得開啟PDMA的clock。
/* Enable LLSI0 module clock */ CLK_EnableModuleClock(LLSI0_MODULE); /* Enable PDMA peripheral clock */ CLK_EnableModuleClock(PDMA_MODULE);
PDMA_Init()的部分,我使用PDMA Channel 0,傳輸寬度設定成和LLSI_DATA一致的32-bit。其中PDMA_SetTransferAddr()將Frame buffer g_au8frameBuffer[]陣列起始位址設定成來源位址,且每次傳輸完都要將位址+1 (PDMA_SAR_INC);然後目標位址就固定是LLSI_DATA。如果看懂上一篇文章「運用指標提高LLSI Frame Buffer的可讀性、易用性」應該就好理解8-bit陣列讓PDMA用32-bit傳是什麼樣的概念了。
void PDMA_Init(void)
{
/* Reset PDMA module */
SYS_ResetModule(PDMA_RST);
/* Open Channel 0 */
PDMA_Open(1 << 0);
/* Transfer count is TEST_COUNT, transfer width is 32 bits(one word) */
PDMA_SetTransferCnt(0, PDMA_WIDTH_32, TEST_COUNT);
/* Transfer type is single transfer */
PDMA_SetBurstType(0, PDMA_REQ_SINGLE, 0);
/* Set source address is g_au8frameBuffer, destination address is &LLSI0->DATA */
PDMA_SetTransferAddr(0, (uint32_t)g_au8frameBuffer, PDMA_SAR_INC, (uint32_t)&LLSI0->DATA, PDMA_DAR_FIX);
/* Request source is LLSI0 */
PDMA_SetTransferMode(0, PDMA_LLSI0, FALSE, 0);
}
LLSI_Init()中將工作模式設定為LLSI_MODE_PDMA。
LLSI_Open(LLSI0, LLSI_MODE_PDMA, LLSI_FORMAT_RGB, HCLK_CLK, 1250, 400, 850, 50000, 8, LLSI_IDLE_LOW);
中斷服務程式
使用LLSI+PDMA傳輸時,中斷唯一的用途只有通知一整個Frame已經傳送到ARGB LED上了,讓主程式可以開始更新下一個資料框的資料到Frame buffer中。所以就只要開Frame End Interrupt就好。
/* Enable Frame End Interrupt */ LLSI_EnableInt(LLSI0, LLSI_FEND_INT_MASK); NVIC_EnableIRQ(LLSI0_IRQn);
比較看看PDMA模式和軟體模式的中斷服務程式,就可以發現CPU真的少很多事!
這是LLSI PDMA模式的中斷服務程式,非常簡短,且只有每個Frame結束才會產生一次中斷:
void LLSI0_IRQHandler()
{
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);
}
}
這是LLSI軟體模式下的中斷服務程式,傳輸期間需要不斷依靠CPU搬移陣列的資料到LLSI FIFO中:
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);
}
}
主程式迴圈
一樣在主程式迴圈中等待Frame End Interrupt的設定的旗標,然後更新Frame Buffer。
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;
PDMA_SetTransferCnt(0, PDMA_WIDTH_32, TEST_COUNT);
PDMA->DSCT[0].CTL = (PDMA->DSCT[0].CTL & ~PDMA_DSCT_CTL_OPMODE_Msk) | PDMA_OP_BASIC;
CLK_SysTickLongDelay(500000);
}
}
Frame buffer更新好以後透過這兩行程式將PDMA重新啟動。
PDMA_SetTransferCnt(0, PDMA_WIDTH_32, TEST_COUNT); PDMA->DSCT[0].CTL = (PDMA->DSCT[0].CTL & ~PDMA_DSCT_CTL_OPMODE_Msk) | PDMA_OP_BASIC;
結論
在LLSIClock的例子中,只是驅動ARGB七段顯示器(七段顯示器內有8顆ARGB LED, LLSIClock 用6個七段顯示器,因此總共48個ARGB LED),PDMA的幫助可能還不是那麼明顯;試想如果拿NUC1263來設計動輒數百顆ARGB LED的電競鍵盤,PDMA就能很好地騰出CPU的資源。

留言
張貼留言