痞子(zi)衡(heng)嵌入式:在i.MXRTxxx下使能DMA鏈式傳(chuan)輸可達(da)到(dao)SPI從(cong)設備接收(shou)速(su)率上限50Mbps
大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家介紹的是i.MXRT下使能DMA鏈式傳輸可達到SPI從設備接收速率上限50Mbps。
最近痞子(zi)衡在幫一個 RT600 的(de)(de) AR 眼鏡客戶優化(hua) SPI 從(cong)設(she)備接收數據(ju)的(de)(de)速率,我們知道(dao) SPI 從(cong)設(she)備接收數據(ju)方法一般(ban)有(you)三種(zhong):1) 輪詢模(mo)式(shi),2) 中斷(duan)模(mo)式(shi),3) DMA 模(mo)式(shi)。前兩種(zhong)模(mo)式(shi)都會受(shou)到(dao) CPU 性能的(de)(de)限制,而 DMA 模(mo)式(shi)則(ze)可以(yi)最大程度地降低 CPU 負(fu)載,提(ti)高(gao)數據(ju)傳輸效率以(yi)及(ji)速率。
然而使用 DMA 傳(chuan)輸(shu)也(ye)會有(you)潛(qian)在問題,單次 DMA 傳(chuan)輸(shu)數(shu)據長度(du)有(you)上限(xian)(受限(xian)于 DMA 通(tong)道緩(huan)沖區(qu)長度(du)),如果在一(yi)次 DMA 傳(chuan)輸(shu)結束之后才開始手動啟動下一(yi)次 DMA 傳(chuan)輸(shu),中(zhong)間的延遲則有(you)可能導致(zhi)漏(lou)收(shou)數(shu)據,這時我(wo)們就(jiu)需(xu)要使用 DMA 鏈式傳(chuan)輸(shu)(Linked Transfer)來解決(jue)潛(qian)在漏(lou)收(shou)數(shu)據問題。這便是今(jin)天(tian)我(wo)們要討論的話(hua)題:
- Note1:本文方法主要針對 RT500/600 上的 DMA(又稱LPC_DMA),其來自于恩智浦 LPC 系列。
- Note2:RT700/RT4digits 上的 eDMA 與 LPC_DMA 完全不同,其來自于原飛思卡爾 Kinetis 系列(KL25 DMA是第一代,K60 eDMA算第二代)。
一、Flexcomm SPI速率
在討(tao)論這(zhe)個話(hua)題之前,我們先來(lai)看(kan)一下 RT500/600 上的 SPI 外(wai)(wai)設(she)本身速率。我們知(zhi)道 RT3digits 上有一個非常神(shen)奇的外(wai)(wai)設(she) Flexcomm(與之對應的是 RT4digits 上的 FlexIO),這(zhe)個外(wai)(wai)設(she)可以按照用戶需求被配置成 USART, SPI, I2C 或者 I2S 外(wai)(wai)設(she)功(gong)能(neng)之一。
RT3digits 內部一(yi)般(ban)會(hui)有多個 Flexcomm,而其本身又分為普通和專用(yong)兩(liang)種類型:普通 Flexcomm 內部結構復(fu)雜(za),因為外設功(gong)能(neng)(neng)配置靈活(huo)性(xing)而稍(shao)稍(shao)放(fang)棄(qi)了(le)(le)一(yi)點傳(chuan)輸性(xing)能(neng)(neng);專用(yong) Flexcomm 則限定用(yong)于特定外設功(gong)能(neng)(neng),放(fang)棄(qi)了(le)(le)靈活(huo)性(xing),但是傳(chuan)輸性(xing)能(neng)(neng)更高。下面是芯片數據(ju)手冊里(li)找(zhao)到的(de) SPI 速率(lv):
| 芯片系列 | 普通SPI Master/Slave:TX/RX - 25Mbps |
高速SPI Master:TX/RX - 50Mbps Slave:RX - 50Mbps, TX - 35Mbps |
|---|---|---|
| RT500 | Flexcomm 0-8, 10-12 | 專用 Flexcomm 14,16 |
| RT600 | Flexcomm 0-7 | 專用 Flexcomm 14 |
二、為什么必須要用DMA?
下圖(tu)是 Flexcomm 模塊(kuai)簡圖(tu),其內部對于(yu)收(shou)發均配置(zhi)了(le)一個深(shen)度(du)為 8 entries 的 FIFO(對于(yu) SPI frame 長(chang)度(du)硬件上(shang)能直接支持 4-16 bits,所以(yi)這里 entries 是以(yi) frame 長(chang)度(du)為單(dan)位(wei)。如果配置(zhi)為最常用的 8bits frame,那 FIFO 就能緩存(cun)(cun) 8bytes 數(shu)據),有(you)一定的數(shu)據緩存(cun)(cun)能力(li),不(bu)至于(yu)因 CPU 響應(ying)不(bu)及而立即漏(lou)數(shu)據。

文章開頭(tou)說(shuo)了 SPI 從設備接收數(shu)據方法有三種,我們來一一具體分析:
- 輪詢方式: CPU 每隔一段時間就來讀一次 SPI RX FIFO 狀態寄存器,一旦有數據就立刻取走,這種方法能夠達到速率上限 50Mbps,但是代價是 275/300MHz 的 CPU 需要付出相當大的負載地在這輪詢 SPI RX FIFO 寄存器狀態,這對于應用程序設計太不友好,稍稍不慎就會漏數據,顯得匆匆忙忙,一般不會這么用。
- 中斷方式: 預先設置一下 SPI RX FIFO 的 level trigger point(1-8 entries),當 RX FIFO 中數據達到這個水平時就觸發中斷,在 ISR 里把數據取走。這種方法可以降低 CPU 負載,但是由于 Cortex-M33 中斷延遲較大,再加上 ISR 代碼執行時間消耗導致可能達不到速率上限 50Mbps(理想情況下需要 FIFO 觸發設 4 entries,然后一次 ISR 讀取 4 個 entries 數據),ISR 加點代碼都需要謹慎,顯得小心翼翼,因此也不太推薦。
- DMA方式: 利用 DMA 來自動搬運 SPI RX FIFO 中的數據到指定 Buffer 中(最長 1024 個 SPI frame),完全不需要 CPU 參與,這種方式可以最大程度地降低 CPU 負載,并且可以輕松達到 50Mbps 的速率上限,此時才算是游刃有余,唯一需要注意的是,單次 DMA 傳輸長度有上限,需要使用 DMA 鏈式傳輸來避免數據漏收。
三、LPC_DMA功能介紹
看起來(lai)這種情(qing)況下 DMA 是必須(xu)要用了(le),那我們(men)(men)就(jiu)來(lai)簡(jian)單了(le)解一(yi)下 LPC_DMA 功能,下圖是其原理框圖。首先(xian)我們(men)(men)要知(zhi)道一(yi)個 DMA 會包含多個 channel(RT600 上(shang)是 33 個,RT500上(shang)是 37 個),各 channel 均(jun)可以獨立工(gong)作(zuo)(當然也可以合作(zuo))。對于每個 channel 獨立工(gong)作(zuo),我們(men)(men)需要重點(dian)了(le)解四個最基本的(de)概念:
- src/dest data: DMA 本質上是數據搬運,從源地址到目的地址,源/目的地址既可以是一般內存,也可以是外設寄存器,因此從類型上就分為:內存->內存、內存->外設、外設->內存、外設->外設(這種類型一般需要特殊設計,RT3digits 不直接支持)。
- XFER Count: 一次 DMA 傳輸搬運的數據量,是可配置的,RT500/600 里上限均是 1024 個 units(每個 unit 長度 8/16/32bits 可配)。
- DMA requests: 指 DMA 數據搬運涉及外設寄存器時,源/目的地址對應哪種外設,這里每個 channel 設計是定死的,比如 RT600 上 DMA0 channel 26 僅能接收 Flexcomm 14 SPI 的 RX 請求。
- DMA triggers: 指觸發 DMA 開始工作的條件,每個 channel 相同,除了最基本的軟件直接觸發之外,均可以配置不同硬件觸發條件(RT600 上是 25 個,RT500 上是 27 個),這些硬件觸發條件包含各種外設中斷、其他 DMA channel trigger output 等。

了解(jie)了 DMA 基(ji)本概念,我(wo)們還需要再進一步了解(jie)下 DMA 數(shu)據傳輸的幾種方式(這里均針對單(dan) channel 而言(yan)):
- Single buffer: 這是最基礎的單次 DMA 傳輸方式(一般用于內存到內存),并且源/目的地址均是線性不間隔增長。
- Linked transfers: 這是將多次 DMA 傳輸串起來(數量不限,僅受限于內存容量,即只要你有足夠的 Buffer),一次傳輸結束自動轉到下一次傳輸。這里包含了一個非常常用的場景:當僅串 2 次 DMA 傳輸時(利用雙 Buffer 循環工作),這叫乒乓傳輸(Ping-pong Transfer)。
- Interleaved transfers: 這是一種特殊的 DMA 傳輸方式,可建立在 Linked transfers 基礎之上,但是源/目標地址增長可按一定步長,適用于預處理音頻/圖像數據場合(比如二維圖像數據里僅提取每行/列數據,多通道音頻數據里僅提取單通道數據)。
有了上面(mian)的(de)(de)鋪墊,現在我(wo)們(men)很自然地想到(dao)可(ke)以把多個(ge)(ge) DMA channel 串起來(lai)一(yi)起工(gong)作,channel A 完成觸發 channel B 繼續(xu)工(gong)作,那就(jiu)是所(suo)謂的(de)(de) Channel chaining 模(mo)式。此外(wai),雖(sui)然原則(ze)上多個(ge)(ge) channel 可(ke)以并行工(gong)作,但是如果涉及到(dao)總線帶寬限制或者內存訪問沖(chong)突,我(wo)們(men)還可(ke)以為其設置(zhi)響應(ying)優(you)先(xian)級(ji)(ji),RT500/600 上一(yi)共支持 8 檔(dang)優(you)先(xian)級(ji)(ji)(每次仲裁時始(shi)(shi)終(zhong)從最高優(you)先(xian)級(ji)(ji) channel 開(kai)始(shi)(shi)檢(jian)查,并選擇第一(yi)個(ge)(ge)處于激活(huo)狀(zhuang)態的(de)(de) channel 進行服務)。
四、使能SPI DMA鏈式傳輸方法
關于 DMA 本身的(de)鏈式傳輸(shu)示例可直接參考 \SDK_25_09_00_EVK-MIMXRT685\boards\evkmimxrt685\driver_examples\dma\linked_transfer 例程,這個例程設計非(fei)常簡單(dan)清晰(xi),代碼過程一(yi)目了然,實現的(de)功能(neng)就是將 s_srcBuffer1 和 s_srcBuffer2 數據往 s_destBuffer 里搬,如果 DMA_Callback 里不做任何處理,那么將會一(yi)直循環搬移。
#include "fsl_dma.h"
static dma_handle_t s_DMA_Handle;
SDK_ALIGN(dma_descriptor_t s_dma_table[2], 16U);
SDK_ALIGN(uint32_t s_srcBuffer1[4], sizeof(uint32_t));
SDK_ALIGN(uint32_t s_srcBuffer2[4], sizeof(uint32_t));
SDK_ALIGN(uint32_t s_destBuffer[8], sizeof(uint32_t));
// 一次 DMA 傳輸結束用戶回調(對應一個 DMA 傳輸描述符里的工作)
void DMA_Callback(dma_handle_t *handle, void *param, bool transferDone, uint32_t tcds)
{
// Do someting
}
// 初始化 DMA0 通道 0
DMA_Init(DMA0);
DMA_CreateHandle(&s_DMA_Handle, DMA0, 0);
DMA_EnableChannel(DMA0, 0);
DMA_SetCallback(&s_DMA_Handle, DMA_Callback, NULL);
// DMA 傳輸屬性配置(uint大小為4bytes,源和目標地址均按1個unit自增,一次傳輸16bytes,使能reload特性和INTB)
uint32_t xferCfg = DMA_SetChannelXferConfig(true, false, false, true, 4U, kDMA_AddressInterleave1xWidth, kDMA_AddressInterleave1xWidth, 16U);
// 初始化兩個 DMA 傳輸描述符,并且將其互相鏈接
DMA_SetupDescriptor(&(s_dma_table[0]), xferCfg, s_srcBuffer1, &s_destBuffer[0], &(s_dma_table[1]));
DMA_SetupDescriptor(&(s_dma_table[1]), xferCfg, s_srcBuffer2, &s_destBuffer[4], &(s_dma_table[0]));
// 將第一個 DMA 傳輸描述符賦給 DMA0 通道 0
DMA_SubmitChannelDescriptor(&s_DMA_Handle, &(s_dma_table[0]));
// 軟件觸發 DMA0 通道 0 開始工作
DMA_StartTransfer(&s_DMA_Handle);
但是很遺憾的(de)是 SDK 中并沒有現成(cheng)的(de) SPI RX DMA 鏈式傳輸(shu)例程(cheng),在僅有的(de) \SDK_25_09_00_EVK-MIMXRT685\boards\evkmimxrt685\driver_examples\spi\dma_b2b_transfer\slave\ 例程(cheng)里,它(ta)也僅是啟(qi)動(dong)單次(ci)傳輸(shu),查(cha)看(kan)其代(dai)碼,我(wo)們發現根(gen)本原(yuan)因是 fsl_spi_dma.c 驅(qu)動(dong)(V2.2.2)里從設計上就不支持(chi)鏈式傳輸(shu)。
SPI_SlaveTransferDMA() -> SPI_MasterTransferDMA() ->
SPI_TransferSetupRxContextDMA(handle, xfer);
SPI_EnableRxDMA(base, true);
SPI_TransferSubmitNextRxDMA(base, handle); // 問題出在這個函數設計上
handle->rxInProgress = true;
DMA_StartTransfer(handle->rxHandle);
在 SPI_TransferSubmitNextRxDMA() 函數(shu)里,其默認使用內部 s_dma_descriptor_table 描述符,每次(ci)(ci)只提(ti)交單次(ci)(ci) DMA 傳(chuan)輸,禁止了 reload 功能,這(zhe)個(ge)函數(shu)顯然(ran)是(shi)為了被(bei)多次(ci)(ci)調用去(qu)連續(xu)順序(xu)數(shu)據傳(chuan)輸而設計的(de),因此要想(xiang)實現 DMA 鏈式傳(chuan)輸,我(wo)們必須改造這(zhe)個(ge)函數(shu)。

具體改造(zao)過(guo)程(cheng),痞子(zi)衡就不一(yi)一(yi)贅述了,大家直接看下面代碼吧,只(zhi)是改造(zao)過(guo)程(cheng)中還是有一(yi)些坑需要注意的,這(zhe)些都是痞子(zi)衡調試時的血淚教(jiao)訓。
//github.com/JayHeng/perf-rt600-fcspi-dma-linked-transfer/blob/main/devices/MIMXRT685S/drivers/fsl_spi_dma.c
坑1:鏈式傳輸時 DMA_SubmitTransfer() 函數里不能判斷 DMA_ChannelIsActive(),否則初始化提交第二個 DMA 傳輸描述符時會直接返回 busy
坑2:鏈式傳輸時 SPI_TransferRxHandlerDMA() 中斷處理要重新設計,不要根據 rxInProgress、rxRemainingBytes 狀態來決定是否調用用戶回調函數
坑3:鏈式傳輸時 spiHandle->state 狀態不要在中斷里改變成 kSPI_Idle 狀態,否則 SPI_MasterTransferGetCountDMA() 函數會失效
至此,i.MXRT下(xia)使能DMA鏈(lian)式傳輸可達到SPI從設備接(jie)收速率上限50Mbps痞子衡便(bian)介紹完畢(bi)了,掌聲在哪里~~~
歡迎訂閱
文章會同時發布到我的 博客園主頁、、、 平臺上。
微信搜索"痞子衡嵌入式"或者掃描下(xia)面(mian)二維碼,就可以(yi)在手機上第(di)一(yi)時間看了(le)哦。

最后歡迎(ying)關(guan)注(zhu)痞(pi)(pi)子衡(heng)個(ge)人微(wei)信公眾(zhong)號【痞(pi)(pi)子衡(heng)嵌入(ru)(ru)式(shi)】,一個(ge)專注(zhu)嵌入(ru)(ru)式(shi)技術的公眾(zhong)號,跟著痞(pi)(pi)子衡(heng)一起玩轉嵌入(ru)(ru)式(shi)。
衡(heng)杰(痞(pi)子衡(heng)),目(mu)前(qian)就職于(yu)恩(en)智浦(NXP)半導體(ti)MCU系(xi)統(tong)應(ying)用部門,擔(dan)任高級(ji)嵌(qian)入式系(xi)統(tong)應(ying)用工程師。
專欄內所有文章的轉載請注明出處://www.xtjzw.net/henjay724/
與痞子衡進一步交流或咨詢業務合作請發郵件至 hengjie1989@foxmail.com
可(ke)以(yi)關注痞(pi)子衡(heng)的Github主頁(ye) ,有很多好玩的嵌入式項目。
關于專欄文章有任何疑問請直接在博客下面留言,痞子衡會及時回復免費(劃重點)答疑。
痞子衡郵箱已被私信擠爆,技術問題不推薦私信,堅持私信請先掃碼付款(5元起步)再發。