SPI + DMA

說一說DMA是什麼東西,DMA本身的意思是Direct Memory Access,直接存取訪問,可以看到這只是一種存取方式,或者說讀寫方式,或是直白點來說,就是直接讀取,說的太直白了,感覺這個DMA這個詞在腦子裏感覺有點SB了,就這麼一個破爛玩意兒起這個這個類似遇到DNA一樣的玩意兒。

直接存取或者直接讀取寫入什麼呢?當然是數據了,從哪裏讀,或者往哪裏寫呢?

這個問題好,後面會說到讀取的位置和寫入的位置。

今天說的這個DMA不是解釋這個讀寫方式的,其實也沒什麼讀寫方式可言,就和普通的SPI,I2C一樣,我也可以叫他們直接讀寫

有人肯定說你這個解釋錯了,好吧,是錯了!因爲沒有體現出Direct "直接"這個意思,直接的意思是不需要通過CPU,可以把數據從特定地址讀出來。

哇靠!認知有點感覺弱爆,從小到大都是被告訴CPU是數據運算的中樞神經!你一個不需要中樞神經,不需要大腦的植物人做法是怎麼實現的?!!!

話先說回來,DMA這個東西只是中存取方式,在我們的MCU中,使用DMA控制器纔可以實現直接存取訪問,也就是說使用DMA控制器纔可以實現DMA操作

DMA控制器提供了一種“硬件的方式”在外設和存儲器之間或者存儲器和存儲器之間的傳輸數據,而不需要CPU介入,從而釋放帶寬,(其實是釋放CPU,可以讓CPU去做其他活)

 

如果你做個其他傳輸總線設備的驅動比如說I2C設備,SPI設備,你就知道,有I2C 控制器,SPI控制器,那麼這裏是一個簡單的DMA控制器

DMA控制器的工作,特別是需要在軟件中實現的內容,比前面i2C,SPI ,你一定要覺得他更簡單,難的是是理解(仔細品味這句話,你就覺得這是廢話)

先貼一張我也不知道爲什麼這個時候要貼出來的圖片:(看到DMA了嗎,仔細看,看不到就算了)

可以看到DMA位於AHB總線矩陣中,AHB你就記得它是一個很重要的高速總線得了,對應有APB外設總線,它的頻率可能比AHB低一點,比較是外設嘛

 

從圖片上就可以看出,我這裏是用的一款GD32E10X的國產MCU爲例子介紹的,它的主要特徵呢,我也懶得打字了,貼個圖:

可以看出,最大傳輸數據長度是65536也就是是2的十六次方,二的十次方是1K,那麼2的十六次就是16K了,sorry 64k

64k對於一款嵌入式的MCU來說,我感覺足夠了,不要擡槓,[旺財]

通道什麼的不管,不過要說一點,不同的通道對應不同的外設地址,這句看不懂直接往下看。

然後是說的源端和目的端。其實就是讀取和寫入地址。

後面說的是傳輸模式啊,中斷這些,懶的說了,自己體會下,如果體會不出來,留個言吧。(其實留言我也不一定回覆)

 

下面一句話我看了之後感覺很經典這是SPEC上說的:

DMA傳輸分兩步:從源地址讀取數據,之後將讀取的數據存儲到目的地址。。。。這我想起了把大象放入冰箱分幾步了

這SPEC的撰寫人之前一定是說相聲的。

DMA控制器基於DMA_CHxPADDR,DMA_CHxMADDR,DMA_CHxCTL寄存器的值計算下一次操作的源/目的地址。

DMA_CHxCNT寄存器用於控制傳輸的次數。

DMA_CHxCTL寄存器的PWIDTH和MWIDTH位域決定每次發送和接收的字節數(字節、半字,字)

這些對關鍵寄存器的介紹很不錯,如果你仔細去體會它要表達的意思,你會有很多疑問,會帶領你去思考。

 

DMA_CHxCNT寄存器的CNT位域必須在CHEN位置位前被配置,其控制傳輸的次數。在傳輸過程中,CNT位域的值表示還有多少次數據傳輸將被執行。

這句話可以品味一下,一旦你設置了讀取的地址,然後設置CNT(也就是讀取的次數),那麼它就會讀取你設置的次數。

但是它必須在CHEN爲被置位前設置。如果將DMA_CHxCTL寄存器的CHEN位清零,可以停止DMA傳輸。

 

地址生成

存儲器和外設都獨立的支持兩種地址生成算法:固定模式和增量模式。寄存器DMA_CHxCTL的PNAGA和MNAGA位用來設置存儲器和外設的地址生成算法。

在固定模式中,地址一直固定爲初始化的基地址(DAM_CHxPADDR,DMA_CHxMADDR).

在增量模式中,下一次傳輸數據的地址是當前地址加1(或者2,4),這個值取決於數據傳輸寬度。

 

循環模式

循環模式用來處理連續的外設請求(如ADC掃描模式)。將DMA_CHxCTL寄存器的CMEN位置位可以使能循環模式。

在循環模式中,當每次DMA傳輸完成後,CNT值會被重新載入,且傳輸完成標誌位會被置1.DMA會一直響應外設的請求,知道通道使能位(DMA_CHxCTL寄存器的CHEN位)被清0.

 

存儲器到存儲器模式

將DMA_CHxCTL寄存器的M2M位置位可以使能存儲器到存儲器模式。在此模式下,DMA通道傳輸數據時不依賴外設的請求信號。一旦DMA_CHxCTL寄存器的CHEN位被置1,DMA通道就離家開始傳輸數據,直到DMA_CHXCNT寄存器達到0,DMA通道纔會停止。

 

通道配置

要啓動一次新的DMA數據傳輸,建議遵循以下步驟進行操作:

1:讀取CHEN位,判斷通道是否使能。如果爲1(通道已經使能),清零改位。當CHEN爲0時,請按照下列步驟配置DMA,啓動新的傳輸。

2:配置DMA_CHxCTL寄存器的M2M以及DIR位,選擇傳輸模式

3:配置DMA_CHxCTL寄存器的CMEN位,注意這裏是CMEN不是CHEN,選擇是否使能循環模式。

4:配置DMA_CHxCTL寄存器的PRIO位,選擇該通道的軟件優先級。

5:通過DMA_CHxCTL寄存器配置存儲器和外設的傳輸寬度以及存儲器和外設地址生成算法。這裏的傳輸寬度是否對速度有大的影響?可以測下一個字的寬度和一個字節的寬度的速度差異。生成算法,讀取肯定是固定算法了,接收是增量算法,因爲做SPI讀取的時候只能通過SPI的SPI_DATA寄存器讀取數據,寫入是寫入內存中的連續區域,要按照讀取寬度寫入對應的地址,注意這裏的地址偏移是對應的傳輸寬度。

6:通過DMA_CHxCTL寄存器配置傳輸完成中斷,半傳輸完成中斷,傳輸錯誤中斷的使能位,中斷都可以配置起來看看,看下傳輸完成的中斷是否有被調用到,半傳輸完成中斷是否是傳輸了一半給出的中斷,傳輸錯誤中斷是什麼樣子的

7:通過DMA_CHxPADDR寄存器配置外設基地址。SPI Flash的話外設的基地址就是SPI_DATA這個數據寄存器,這個寄存器是32位的,這個可以考慮最大的位數傳輸

8:通過DMA_CHxMADDR寄存器配置存儲器基地址。

9:通過DMA_CHxCNT寄存器配置設計及傳輸總量。

10:將DMA_CHxCTL寄存器的CHEN位置1,使能DMA通道。

 

中斷:

每個DMA通道都有一個專用的中斷。中斷事件有三種類型:傳輸完成,半傳輸完成和傳輸錯誤。每一箇中斷事件在DMA_INTF寄存器中有專用的標誌位,在DMA_INTC寄存器中有專用的清除位,在DMA_CHxCTL寄存器中有專用的使能位。

中斷其實還是比較容易理解的。畢竟我們在嵌入式開發中中斷很常見。

這裏如果你要使用中斷,

nvic_irq_enable(DMA0_Channel3_IRQn,0,0);
dma_interrupt_enable(DMA0, DMA_CH3, DMA_INT_FTF);

 

然後在gd32e10x_it.c中實現中斷handler就可以了,

void DMA0_Channel3_IRQHandler(void)
{
    if(dma_interrupt_flag_get(DMA0, DMA_CH3, DMA_INT_FLAG_FTF)){     
        dma_interrupt_flag_clear(DMA0, DMA_CH3, DMA_INT_FLAG_G);
    }
}

注意一點你使用哪個channel就實現哪個channle的中斷。或者你要有在中斷代碼寫完之後重新檢查的習慣。

 

DMA 請求映射

多個外設請求被映射到同一個DMA通道。這些請求信號在經過邏輯或後進入DMA。通過配置對應外設的寄存器,每個外設的請求均可以獨立的開啓或者關閉。用戶必須確保同一時間,在同一個通道上僅有一個外設的請求被開啓。

這裏截圖是爲了說明,你要用哪個外設的DMA功能,或者說DMA控制器要去寫入或者讀取哪個外設,你要找的可以用在這個外設的DMA以及channel.

比如我現在要用DMA去讀取SPI1的數據,你就需要用DMA0 CH3這個通道,因爲可以看到到只有DMA0 通道3支持SPI1 R。

下面有個例子,是SPI + DMA的操作,之前是純SPI操作,現在是通過SPI+DMA的方式去讀取spi接口的nor flash的操作

用的還是GD flash

之前用純SPI讀取flash數據的接口是:

/*!
    \brief      read a block of data from the flash
    \param[in]  pbuffer: pointer to the buffer that receives the data read from the flash
    \param[in]  read_addr: flash's internal address to read from
    \param[in]  num_byte_to_read: number of bytes to read from the flash
    \param[out] none
    \retval     none
*/
spiflash_ret spiflash_buffer_read(uint8_t* pbuffer, uint32_t read_addr, uint16_t num_byte_to_read)
{
    spiflash_ret ret = spiflash_ret_success;
    /* select the flash: chip slect low */
    SPI_FLASH_CS_LOW();

    /* send "read from memory " instruction */
    spi_flash_send_byte(READ);

    /* send read_addr high nibble address byte to read from */
    spi_flash_send_byte((read_addr & 0xFF0000) >> 16);
    /* send read_addr medium nibble address byte to read from */
    spi_flash_send_byte((read_addr& 0xFF00) >> 8);
    /* send read_addr low nibble address byte to read from */
    spi_flash_send_byte(read_addr & 0xFF);

    /* while there is data to be read */
    while(num_byte_to_read--){
        /* read a byte from the flash */
        *pbuffer = spi_flash_send_byte(DUMMY_BYTE);
        /* point to the next location where the byte read will be saved */
        pbuffer++;
    }

    /* deselect the flash: chip select high */
    SPI_FLASH_CS_HIGH();
    
    return ret;
}

可以看到在讀取命令和地址發送之後,有一個while循環,就是這個while循環來讀取數據的,可以看出這樣一個while循環似乎花費了太多的時間,

但是做技術可以不是隻是看的,你可以用這種方式讀取比如說1M的數據量看看,看看耗時是多少,再用後面的SPI + DMA 的方式去讀取做過對比:

下面這個函數是利用這個上面的函數做的一個修改,當然主要修改的是while循環部分,我們前面還是利用純SPI去發送讀取命令和發送地址,

在接收數據的時候我們來看下。

如果我們要用SPI+DMA的方式來讀取,我們不妨看下SEPC,看下SPI介紹中對於DMA有沒有介紹。

上面這個截圖就是SPI接口的描述中對於DMA的介紹

這主要是說如果你要用DMA來傳輸SPI的TX 和 RX數據,要做一下使能SPI模式的DMA動作,下面在代碼中有說明:

static void dma0_ch3_init(void)
{
	    /* enable DMA clock */
    rcu_periph_clock_enable(RCU_DMA0);
	
	nvic_irq_enable(DMA0_Channel3_IRQn,0,0);
	dma0_ch3_test_config();
}

這裏在SPI的初始化同時會調用DMA CH3的初始化,時鐘,和DMA0 CH3 IRQ,

void dma0_ch3_test_config(void)
{
	memset(g_destbuf,0x00,sizeof(g_destbuf));
    dma_parameter_struct dma_init_struct;
    /* initialize DMA channel 3 */
    dma_deinit(DMA0, DMA_CH3);//先要deinit一下
    dma_struct_para_init(&dma_init_struct);//將這個結構體中的數據全部初始化爲0,
    
    dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY;//這裏我們只是做個測試,從外設(SPI)讀取數據到內存
    dma_init_struct.memory_addr = (uint32_t)g_destbuf;//這個是個數組,也就是前面說的內存

    dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;//在內存中需要採用自動增長的方式
    dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;//每次讀取8bit,當然SPI 的DATA寄存器是32位的寄存器你可以讀取32位
    dma_init_struct.number = TRANSFER_NUM;//這個就是你這次DAM傳輸需要傳輸的字節數
    dma_init_struct.periph_addr = (uint32_t)&SPI_DATA(SPI1);//這裏就是外設的地址,DMA讀取數據都是從外設讀取的,這裏相對於DMA來說,SPI就是外設,是ARM內核的外設
    dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;//SPI的Data寄存器是數據的唯一讀入地址,SPI的數據在傳輸的時候都會不斷的寫入到這個寄存器,所以我們讀取數據都只讀一個寄存器,地址並不會增加改變
    dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;//外設的讀取位數也是8位
    dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;//優先級高,這個在多個外設都要用到DMA的時候會用到
    dma_init(DMA0, DMA_CH3, &dma_init_struct);//把我們的這些設置都寫入到DMA的對應寄存器中
    /* DMA channel 0 mode configuration */
    dma_circulation_disable(DMA0, DMA_CH3);//不採用循環模式,讀完就結束
    dma_memory_to_memory_disable(DMA0, DMA_CH3);//不是從memory到memory的讀取,所以disable
    /* DMA channel 0 interrupt configuration */
    dma_interrupt_enable(DMA0, DMA_CH3, DMA_INT_FTF);//enable 中斷,如果你需要用到中斷的話
    /* enable DMA transfer */
    //dma_channel_enable(DMA0, DMA_CH3);//以爲你這裏只是初始化,先不要enable DMA,只有在真正用的時候單獨調用DMA,就可以傳輸數據了。
}

好了,初始化結束了,看下我們的讀取操作,這裏是採用DMA讀取數據後在後面直接把數據打印出來

/*!
    \brief      read a block of data from the flash
    \param[in]  pbuffer: pointer to the buffer that receives the data read from the flash
    \param[in]  read_addr: flash's internal address to read from
    \param[in]  num_byte_to_read: number of bytes to read from the flash
    \param[out] none
    \retval     none
*/
spiflash_ret spiflash_dma_read(uint8_t* pbuffer, uint32_t read_addr, uint16_t num_byte_to_read)
{
    spiflash_ret ret = spiflash_ret_success;
    /* select the flash: chip slect low */
    SPI_FLASH_CS_LOW();

    /* send "read from memory " instruction */
    spi_flash_send_byte(READ);

    /* send read_addr high nibble address byte to read from */
    spi_flash_send_byte((read_addr & 0xFF0000) >> 16);
    /* send read_addr medium nibble address byte to read from */
    spi_flash_send_byte((read_addr& 0xFF00) >> 8);
    /* send read_addr low nibble address byte to read from */
    spi_flash_send_byte(read_addr & 0xFF);

	spi_parameter_struct spi_init_struct;
	    /* SPI1 parameter config */
    spi_init_struct.trans_mode           = SPI_TRANSMODE_RECEIVEONLY;
    spi_init_struct.device_mode          = SPI_MASTER;
    spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;
    spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
    spi_init_struct.nss                  = SPI_NSS_SOFT;
    spi_init_struct.prescale             = SPI_PSC_8;
    spi_init_struct.endian               = SPI_ENDIAN_MSB;
    spi_init(SPI1, &spi_init_struct);
	dma_channel_enable(DMA0, DMA_CH3);
	while(g_dmacomplete_flag == 0);

    /* deselect the flash: chip select high */
    SPI_FLASH_CS_HIGH();
	uint8_t i = 0;
	for(i = 0; i < 8; i++)
		printf("[%d] = %d\r\n",i,g_destbuf[i]);
    printf("g_destbuf = %s strlen(g_destbuf) = %d\r\n",g_destbuf,strlen(g_destbuf));
    return ret;
}

主要是下面這一部分:

spi_parameter_struct spi_init_struct;
	    /* SPI1 parameter config */
    spi_init_struct.trans_mode           = SPI_TRANSMODE_RECEIVEONLY;
    spi_init_struct.device_mode          = SPI_MASTER;
    spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;
    spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
    spi_init_struct.nss                  = SPI_NSS_SOFT;
    spi_init_struct.prescale             = SPI_PSC_8;
    spi_init_struct.endian               = SPI_ENDIAN_MSB;
    spi_init(SPI1, &spi_init_struct);
	dma_channel_enable(DMA0, DMA_CH3);
	while(g_dmacomplete_flag == 0);

 

我們看到之前的函數在讀取的時候都會發送0xFF,然後才能獲取數據,這裏我看了爲網友的介紹

在這裏可以把SPI trans mode設置爲RECEIVE ONLY模式,這樣就不用發送0xff,可以直接用DMA來讀取了。

真的感謝這位網友,他的博客地址https://blog.csdn.net/chenwei2002/article/details/49722373

因爲他用的STM32的,在設置SPI trans mode的時候我設置錯了,是用了

把這個函數當成了設置receive only的設置了

/*!
    \brief      configure SPI bidirectional transfer direction
    \param[in]  spi_periph: SPIx(x=0,1,2)
    \param[in]  transfer_direction: SPI transfer direction
                only one parameter can be selected which is shown as below:
      \arg        SPI_BIDIRECTIONAL_TRANSMIT: SPI work in transmit-only mode
      \arg        SPI_BIDIRECTIONAL_RECEIVE: SPI work in receive-only mode
    \param[out] none
    \retval     none
*/
void spi_bidirectional_transfer_config(uint32_t spi_periph, uint32_t transfer_direction)
{
    if(SPI_BIDIRECTIONAL_TRANSMIT == transfer_direction){
        /* set the transmit-only mode */
        SPI_CTL0(spi_periph) |= (uint32_t)SPI_BIDIRECTIONAL_TRANSMIT;
    }else{
        /* set the receive-only mode */
        SPI_CTL0(spi_periph) &= SPI_BIDIRECTIONAL_RECEIVE;
    }
}

其實不是他,設置爲Receive only是

spi_init_struct.trans_mode           = SPI_TRANSMODE_RECEIVEONLY;

 

,然後寫入對應的SPI寄存器

至於while(g_dmacomplete_flag == 0);這句話,我是在中斷中對這個變量設置爲了1

所以這裏是一直等待DMA操作完成纔去將片選拉高,否則會出現讀取錯誤的問題

 

總的來說,介紹的例子比之前講的理論淺薄很多,其實例子在實現過程中,或者說我自己在摸索過程中遇到了比想象更多的問題。

好在現在可以通過DMA來實現數據傳輸了。

後面我會利用SPI+DMA 和純SPI來做下對比,對比速度可以提升多少

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章