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的資源。





留言

這個網誌中的熱門文章

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

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

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