SPI是我最常用的介面之一,連線管腳僅為4根;在常見的晶片間通訊方式中,速度遠優於UART、I2C等其他介面。STM32的SPI口的同步時鐘最快可到PCLK的二分之一,單個位元組或字的通訊時間都在us以下,因此大多數情況下我們會使用查詢法控制SPI口的傳輸。但對於大量且連續的通訊,再使用查詢法就顯得有些浪費CPU的時間,DMA控制SPI的讀寫顯然成為一種不錯的選擇。
為DMA控制SPI批次資料讀寫的功能,參照官方程式碼編寫的DMA控制SPI口在主/從兩種模式下,讀寫資料的的程式碼,供各位網友直接使用或批評指正。先直接上我得到結論:
1、運用STM32的SPI口的DMA的功能,能夠提升STM32與外設之間通訊的速率和實時性。
2、但在STM32的SPI的主機模式下,DMA控制器無法自動產生片選CS訊號,只能與無需同步CS訊號的外設器件通訊。為產生同步的CS訊號,只能由軟體控制SPI逐字傳送,而DMA僅用於接收SPI資料,這樣做的效率和不使用DMA時一樣。
3、主模式下,軟體控制片選CS訊號和SPI讀寫時,存在至少50%的時間空隙,降低了其SPI通訊的效率。
4、STM32的SPI主機模式下,無法只使用DMA接收,而不傳送。原因是沒有觸發SPI的DMA接收的訊號。但SPI的傳送可以是軟體控制的逐字傳送,也可以是DMA控制的連續傳送。
5、STM32的SPI若要使用DMA方式,最合適的是讓STM32工作在SPI的從模式,由外部主機(如FPGA)來控制通訊的實時性和高速性。
以下原創內容歡迎網友轉載,但請註明出處: https://www.cnblogs.com/helesheng
一、STM32做SPI主機(Master)時的DMA傳輸
STM32做SPI主機進行DMA通訊時,尤其需要注意的是:不能單獨使用SPI接收資料DMA,一定要配合SPI傳送資料,DMA接收資料通道才能收到資料。道理很簡單:STM32做主機時,如果不主動傳送資料將無法產生時鐘和片選等訊號,亦無法在傳輸完成後觸發DMA接收資料。但在使用時,這一點非常容易被忽視,從而造成DMA接收SPI資料通道DMA1CH2和DMACH4「不工作」。
圖1、STM32 DMA1各通道功能
具體來說,使用SPI口的DMA接收功能有兩種設定方法:
1、SPI口的接收和傳送各使用一個DMA通道
這樣做最符合DMA控制大量資料連續傳送和接收的設計初衷,此種情況下的SPI口和兩個DMA通道的設定分別如下:
1 RCC_APB2PeriphClockCmd( RCC_APB2Periph_SPI1, ENABLE ); 2 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;//PA5 6 7是SPI1的SCK MIOS MOSI 3 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //複用推輓輸出 4 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 5 GPIO_Init(GPIOA, &GPIO_InitStructure); 6 GPIO_SetBits(GPIOA , GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7); //將其置位 7 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //設定SPI單向或者雙向的資料模式:SPI設定為雙線雙向全雙工 8 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //設定SPI工作模式:設定為主SPI 9 SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; //設定SPI的資料大小:SPI傳送接收8位元幀結構 10 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //空閒時時鐘為低電平 11 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //資料捕獲於第1個時鐘沿 12 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //SPI_NSS_Hard; ////NSS訊號由硬體(NSS管腳)還是軟體(使用SSI位)管理:內部NSS訊號有SSI位控制 13 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; //定義波特率預分頻的值 14 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定資料傳輸從MSB位還是LSB位開始:資料傳輸從MSB位開始 15 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值計算的多項式 16 SPI_Init(SPI1, &SPI_InitStructure); //根據SPI_InitStruct中指定的引數初始化外設SPIx暫存器 17 SPI_Cmd(SPI1, ENABLE); //使能SPI1外設
1 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA傳輸 2 ///////以下設定DMA CH2用於接收SPI的DMA通道///// 3 DMA_DeInit(DMA1_Channel2); //將DMA的通道1暫存器重設為預設值 4 DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&(SPI1->DR); //DMA外設基地址 5 DMA_InitStructure.DMA_MemoryBaseAddr = (u32)spi_rx_buff; //DMA記憶體基地址 6 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //資料傳輸方向,從外設讀取資料到記憶體 7 DMA_InitStructure.DMA_BufferSize = num; //DMA通道的DMA快取的大小 8 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外設地址暫存器不變 9 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //記憶體地址暫存器遞增 10 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //資料寬度為16位元 11 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //資料寬度為16位元 12 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常模式 13 DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //DMA通道 x擁有中優先順序 14 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x沒有設定為記憶體到記憶體傳輸 15 DMA_Init(DMA1_Channel2, &DMA_InitStructure); //根據DMA_InitStruct中指定的引數初始化DMA的通道 16 ///////以下設定DMA的SPI傳送通道/////////// 17 DMA_DeInit(DMA1_Channel3); 18 DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&(SPI1->DR); //設定接收外設(0x4001300C) 地址(源地址) 19 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)spi_tx_buff; //設定 SRAM 儲存地址(源地址) 20 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //傳輸方向 記憶體-外設 21 DMA_InitStructure.DMA_BufferSize = num; //設定 SPI1 接收長度 22 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外設地址增量(不變) 23 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //記憶體地址增量(變化) 24 DMA_InitStructure.DMA_PeripheralDataSize = DMA_MemoryDataSize_HalfWord; //外設傳輸寬度(位元組) 25 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //記憶體傳輸寬度(位元組) 26 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //傳輸方式,一次傳輸完停止,不重新載入 27 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA優先順序 28 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //記憶體到記憶體方式禁止 29 DMA_Init(DMA1_Channel3, &DMA_InitStructure);
主程式中收發控制程式如下:
1 SPIx_Init();//SPI初始化 2 DMA_Config(256);//設定DMA對應的兩個通道,資料深度設為256 3 SPI_I2S_DMACmd(SPI1 , SPI_I2S_DMAReq_Rx , ENABLE); 4 SPI_I2S_DMACmd(SPI1 , SPI_I2S_DMAReq_Tx , ENABLE); 5 while(1) 6 { 7 DMA_SetCurrDataCounter(DMA1_Channel2,256);//必須在每次啟動DMA之前設定 8 DMA_SetCurrDataCounter(DMA1_Channel3,256);//必須在每次啟動DMA之前設定 9 DMA_Cmd(DMA1_Channel2, ENABLE); //使能DMA所指示的通道 10 DMA_Cmd(DMA1_Channel3, ENABLE); //使能DMA所指示的通道 11 while(1) 12 { 13 if(DMA_GetFlagStatus(DMA1_FLAG_TC2) != RESET) //判斷通道2傳輸完成 14 { 15 DMA_ClearFlag(DMA1_FLAG_TC2);//清除通道2傳輸完成標誌 16 break; 17 } 18 } 19 DMA_Cmd(DMA1_Channel2, DISABLE);//禁止DMA 20 DMA_Cmd(DMA1_Channel3, DISABLE);//禁止DMA 21 delay_ms(1); 22 }
這裡我沒有使用DMA中斷,為的是驗證程式碼的簡單易懂;在實際使用時,建議讀者使用中斷以提高資料讀寫效率。另外,程式碼中值得注意的地方有:
1、 使用DMA傳輸之前,必須使能SPI傳送和接收觸發DAM傳輸請求,官方韌體庫中的函數分別為:SPI_I2S_DMACmd(SPI1 , SPI_I2S_DMAReq_Rx , ENABLE);和SPI_I2S_DMACmd(SPI1 , SPI_I2S_DMAReq_Tx , ENABLE);
2、 每輪DMA傳輸完成後,需在次啟動一輪DMA傳輸之前,需要重新設定傳輸資料計數器:DMA_SetCurrDataCounter(DMA1_Channel2,256);和DMA_SetCurrDataCounter(DMA1_Channel3,256);
另外,我在使用上述方法的時候,忽然發現一個致命的問題:如果使用DMA控制STM32作為SPI主機輸出資料,那麼誰來產生片選訊號CS呢?後來嘗試過將NSS(PA4——SPI1或PB12——SPI2)管腳設定給SPI口,並改由硬體來控制該管腳: SPI_InitStructure.SPI_NSS = SPI_NSS_Hard;結果發現均不奏效,也就是說:在SPI主模式下使用DMA傳送,無法產生有效的片選CS訊號!這無疑是致命的缺陷!——也許是我的理解不到位,請各位知道怎麼解決這個問題的大神一定要高速我一下。(當然這一缺陷,對於無需在單次傳送位元組/半字之後給出片選CS訊號的應用——如大容量SPI介面記憶體,並不成其為問題。)
無法在用DMA控制SPI傳送時控制CS訊號,我只好退而求其次:改由軟體控制SPI傳送,並同步產生CS訊號。但這樣做已經失去了DMA接收SPI的意義,因為軟體控制SPI傳送後,通訊的速度和使用查詢法是一樣的!
2、SPI接收使用DMA控制,傳送使用軟體控制
儘管我認為傳送使用軟體控制後,DMA在接收中帶來的好處已經基本喪失,但在這裡仍然給出主程式中收發控制程式供讀者參考。
1 SPIx_Init(); 2 DMA_Config(256);//設定DMA的SPI通道,資料深度設為256 3 SPI_I2S_DMACmd(SPI1 , SPI_I2S_DMAReq_Rx , ENABLE); 4 delay_ms(300); 5 while(1) 6 { 7 while(n_interrupt != 0);//等到中斷到來 8 while(n_interrupt == 0);//等到中斷結束 9 DMA_SetCurrDataCounter(DMA1_Channel2,256);//這部必須在每次啟動DMA之前設定, 10 DMA_Cmd(DMA1_Channel2, ENABLE); //使能DMA所指示的通道 11 for(k = 0 ; k < 256 ; k++) 12 { 13 CS = 0; 14 SPIx_ReadWrite16bit(0xaa55);//只使用了DMA接收SPI資料,但接收要由軟體啟動傳送資料才能接收,此處只是隨便傳送了一個資料 15 CS = 1; 16 } 17 if(DMA_GetFlagStatus(DMA1_FLAG_TC2)!=RESET) //判斷通道2傳輸完成 18 DMA_ClearFlag(DMA1_FLAG_TC2);//清除通道2傳輸完成標誌 19 DMA_Cmd(DMA1_Channel2, DISABLE);//禁止DMA 20 //////////以下可以把資料傳輸走////////// 21 22 }
可以看到,當由軟體控制SPI傳送後,就可以由軟體產生和傳送同步的片選CS了。但這樣做與收發都採用查詢法的效率幾乎一樣了。
特別的,當採用查詢法直接控制SPI口的接收和傳送時,硬體的讀寫和軟體的指令總是存在較大時間空隙:向SPI資料暫存器SPI_DR寫入資料到SPI實際發出資料之間存在至少200ns間隔;檢測SPI狀態暫存器SPI_SR中的TXE(傳送緩衝區空位)時,TXE位的變化總是比實際傳送完成晚至少200ns。例如上面的程式碼,函數SPIx_ReadWrite16bit();通過軟體控制片選CS訊號和SPI硬體的方式通訊,下圖是它所產生的CS訊號(藍)和SCK(黃),可以發現該函數用於傳送的時間只佔了實際耗費時間的一半以下,特別是當傳送字長僅為8bits時,時間利用率真的是非常感人。
查詢法實現片選CS訊號(藍色)和SPI硬體產生的時序
對於這樣的實時性,我實在是不明白意法半導體的STM32設計師的初衷是什麼。當然,也有可能是筆者才疏學淺,如果有大神知道,煩請轉告,多謝!
二、STM32做SPI從機(Slave)時的DMA傳輸
當然用DMA讀寫SPI,更合理的方式是讓STM32的SPI工作在從機模式,只要主機給出合理的片選CS、時鐘SCK和資料MOSI/MISO訊號,作為從機的STM32就能在DMA的支援下,實現高效、實時的資料接收。下面的程式碼中,我將SPI1設定為從機模式,用DMA1CH2接收資料。
1 /////// DMA CH2設定程式碼///////// 2 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA傳輸 3 DMA_DeInit(DMA1_Channel2); //將DMA的通道1暫存器重設為預設值 4 DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&(SPI1->DR); //DMA外設基地址 5 DMA_InitStructure.DMA_MemoryBaseAddr = (u32)spi_rx_buff; //DMA記憶體基地址 6 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //資料傳輸方向,從外設讀取資料到記憶體 7 DMA_InitStructure.DMA_BufferSize = num; //DMA通道的DMA快取的大小 8 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外設地址暫存器不變 9 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //記憶體地址暫存器遞增 10 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //資料寬度為16位元 11 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //資料寬度為16位元 12 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常模式 13 DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //DMA通道 x擁有中優先順序 14 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x沒有設定為記憶體到記憶體傳輸 15 DMA_Init(DMA1_Channel2, &DMA_InitStructure); //根據DMA_InitStruct中指定的引數初始化DMA的通道 16 /////// SPI1設定程式碼///////// 17 RCC_APB2PeriphClockCmd( RCC_APB2Periph_SPI1, ENABLE ); 18 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;//PA4 PA5 6 7是SPI1的CS SCK MIOS MOSI 19 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //複用推輓輸出 20 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 21 GPIO_Init(GPIOA, &GPIO_InitStructure); 22 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //設定SPI單向或者雙向的資料模式:SPI設定為雙線雙向全雙工 23 SPI_InitStructure.SPI_Mode = SPI_Mode_Slave; //設定SPI工作模式:設定為SPI從機 24 SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; //設定SPI的資料大小:SPI傳送接收8位元幀結構 25 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //空閒時時鐘為低電平 26 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //資料捕獲於第1個時鐘沿 27 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //SPI_NSS_Hard; ////NSS訊號由硬體(NSS管腳)還是軟體(使用SSI位)管理:內部NSS訊號有SSI位控制 28 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; //定義波特率預分頻的值 29 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定資料傳輸從MSB位還是LSB位開始:資料傳輸從MSB位開始 30 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值計算的多項式 31 SPI_Init(SPI1, &SPI_InitStructure); //根據SPI_InitStruct中指定的引數初始化外設SPIx暫存器
主程式中,控制DMA和讀取緩衝中的程式如下所示。這裡為了程式碼的簡單易懂,同樣沒有使用DMA中斷,在實際使用時,建議讀者使用中斷以提高資料讀寫效率。
1 SPI_Cmd(SPI1, ENABLE); //使能SPI1外設 2 DMA_SetCurrDataCounter(DMA1_Channel2,256);//這部必須在每次啟動DMA之前設定, 3 DMA_Cmd(DMA1_Channel2, ENABLE); //使能DMA所指示的通道 4 while(DMA_GetFlagStatus(DMA1_FLAG_TC2)==RESET); //判斷通道2傳輸完成 5 DMA_ClearFlag(DMA1_FLAG_TC2);//清除通道2傳輸完成標誌 6 DMA_Cmd(DMA1_Channel2, DISABLE);//禁止DMA 7 SPI_Cmd(SPI1, DISABLE); //禁止SPI,只在開啟SPI時接收資料,防止主機不斷傳送 8 //////////以下可以把資料傳輸走////////// 9 for(i=0;i<256;i++) 10 data_repo_short[i] = spi_rx_buff[i];
下圖是我用FPGA作為SPI主機產生的讀寫時序,可以看到此時SPI可以達到很高的通訊效率。提高SCK的主頻後,通訊速度上限10Mbytes/S左右(主要受限於STM32的接收SCK頻率)。
FPGA產生的SPI主機時序,STM32做從機
三、總結
STM32的SPI介面並不完美,仍然存在各種小問題,尤其是在SPI作為主機受DMA控制傳輸大量資料時,效率並不能得到很大提升。但當STM32的SPI作為從機時,DMA控制的資料傳輸,能夠較大的提升資料常數效率。