在用 SPI 讀寫(xiě) Flash(比如 W25Q 系列)時(shí),往往會(huì)覺(jué)得用 CPU 一個(gè)字節(jié)一個(gè)字節(jié)地收發(fā)太慢了。于是大家都會(huì)想到用 DMA(直接內(nèi)存訪(fǎng)問(wèn)) 這個(gè)“搬運(yùn)工”來(lái)代勞。
但是!當(dāng)你滿(mǎn)懷信心地配置好 DMA,一跑程序,往往會(huì)絕望地卡死在 while(dma_done == 0); 里面。今天,我們就用一段極簡(jiǎn)的測(cè)試代碼(往 Flash 里寫(xiě)一個(gè) "kunkun" 并讀出來(lái)),手把手教你如何完美打通 SPI 和 DMA 的任督二脈!
核心思維預(yù)警:SPI 和 DMA 是怎么配合的?
SPI 的全雙工脾氣:SPI 就像一個(gè)雙向傳送帶。你發(fā)一個(gè)字節(jié)出去,必然會(huì)同時(shí)收一個(gè)字節(jié)回來(lái)。必須有發(fā)才有收。
DMA 的搬運(yùn)工角色:我們通常需要雇傭兩個(gè) DMA 搬運(yùn)工。一個(gè)叫 TX(發(fā)送通道),負(fù)責(zé)把內(nèi)存里的數(shù)據(jù)瘋狂塞給 SPI;另一個(gè)叫 RX(接收通道),負(fù)責(zé)把 SPI 收到的數(shù)據(jù)搬回內(nèi)存。
第一步:準(zhǔn)備好你的“停車(chē)場(chǎng)”(內(nèi)存對(duì)齊)
// 【關(guān)鍵】:定義真正的內(nèi)存空間,并強(qiáng)制 4 字節(jié)對(duì)齊 __attribute__((aligned(4))) uint8_t CW_DMA_TxBuf1[256] = "kunkun"; __attribute__((aligned(4))) uint8_t CW_DMA_RxBuf1[256];
注意: DMA 搬運(yùn)數(shù)據(jù)速度極快,但它有個(gè)小怪癖——喜歡整齊的地址。加上 attribute((aligned(4))) 就是告訴編譯器:“請(qǐng)把這兩個(gè)數(shù)組放在能被 4 整除的內(nèi)存地址上”。如果不加,有時(shí)候硬件在尋址時(shí)可能會(huì)報(bào)錯(cuò)或者發(fā)生數(shù)據(jù)偏移。
C 語(yǔ)言中內(nèi)存對(duì)齊(結(jié)構(gòu)體)
struct MyData {
int a; // 4 字節(jié)
int b; // 4 字節(jié)
char c; // 1 字節(jié)
};
它的內(nèi)存布局就像這樣:
第 0-3 字節(jié):放 int a,完美填滿(mǎn)一排。
第 4-7 字節(jié):放 int b,完美填滿(mǎn)第二排。
第 8 字節(jié):放 char c,它只占了第三排的第一個(gè)座位。
第 9-11 字節(jié): CPU 是個(gè)“強(qiáng)迫癥”,它要求下一個(gè)結(jié)構(gòu)體(如果你定義一個(gè)數(shù)組的話(huà))必須從新的一排(4 的倍數(shù)地址)開(kāi)始。為了保證這種整齊,它在 char c 后面塞了 3 個(gè)字節(jié)的廢話(huà)(Padding)。
所以:9 (有效)+ 3 (墊片) = 12 字節(jié)。
#pragma pack(1)
struct MyData {
int a;
int b;
char c;
};
#pragma pack() // 用完記得關(guān)掉,否則會(huì)影響后面的代碼
//缺點(diǎn):CPU 訪(fǎng)問(wèn) a 和 b 可能會(huì)變慢一點(diǎn)點(diǎn),
//因?yàn)榈刂房赡懿辉偈?4 的倍數(shù),CPU 甚至需要分兩次讀取再拼接(這叫非對(duì)齊訪(fǎng)問(wèn))。
第二步:配置 DMA 搬運(yùn)工的“打卡機(jī)”(中斷配置)
void NVIC_Configuration(void){ __disable_irq(); NVIC_ClearPendingIRQ(DMACH23_IRQn); NVIC_SetPriority(DMACH23_IRQn, 1); // 建議設(shè)個(gè)優(yōu)先級(jí) NVIC_EnableIRQ(DMACH23_IRQn); __enable_irq(); }
/* 定義一個(gè)全局標(biāo)志位,告訴主程序:搬完了! */
volatile uint8_t g_dma_done = 0; // 全局標(biāo)志位
void DMACH23_IRQHandler(void)
{
// 檢查通道 2 (RX) 是否完成(通常以 RX 完成為準(zhǔn),因?yàn)?RX 結(jié)束代表總線(xiàn)時(shí)鐘已全部跑完)
if (DMA_GetITStatus(DMA_IT_TC2))
{
DMA_ClearITPendingBit(DMA_IT_TC2);
g_dma_done = 1; // 豎起旗子
}
// 清理通道 3 (TX) 標(biāo)志位
if (DMA_GetITStatus(DMA_IT_TC3))
{
DMA_ClearITPendingBit(DMA_IT_TC3);
}
// 錯(cuò)誤處理
if (DMA_GetITStatus(DMA_IT_TE2) || DMA_GetITStatus(DMA_IT_TE3))
{
DMA_ClearITPendingBit(DMA_IT_TE2 | DMA_IT_TE3);
Error_Handle();
}
}
注意:搬運(yùn)工(DMA)干完活總得跟老板(CPU)匯報(bào)一下吧?這段代碼就是給系統(tǒng)注冊(cè)了一個(gè)“微信提示音”。當(dāng) DMA 搬完 6 個(gè)字節(jié)的 "kunkun" 時(shí),它會(huì)觸發(fā)中斷,把我們代碼里的 g_dma_done 標(biāo)志位置為 1,這樣我們的 while 死循環(huán)就能沖過(guò)去了。
第三步:重頭戲!初始化 SPI 和 DMA
這段 SPI2_DMA_Init 初始化代碼里,藏著幾個(gè)最容易讓人抓狂的致命地雷,我們已經(jīng)全部掃清了:
void SPI2_DMA_Init(void){
// ... (變量聲明省略) ...// 避坑 1:一定要給外設(shè)通電!
__RCC_SPI2_CLK_ENABLE();
RCC_AHBPeriphClk_Enable(RCC_AHB_PERIPH_DMA, ENABLE);
如果不打開(kāi) SPI 的時(shí)鐘,SPI 就等于沒(méi)插電,你后面寫(xiě)的所有寄存器配置都會(huì)像扔進(jìn)黑洞一樣毫無(wú)反應(yīng)。
// 避坑 2:找對(duì)收發(fā)貨的“物理地址”// 【RX 接收通道配置】
DMA_InitStruct.DMA_SrcAddress = (uint32_t)&CW_SPI2->DR; // 收貨地:SPI 的數(shù)據(jù)寄存器
DMA_InitStruct.DMA_DstAddress = (uint32_t)CW_DMA_RxBuf1; // 卸貨地:我們的內(nèi)存數(shù)組
DMA_InitStruct.DMA_DstInc = DMA_DstAddress_Increase; // 卸貨時(shí)地址要遞增,依次排好排滿(mǎn)
DMA_InitStruct.HardTrigSource = 33; // 告訴搬運(yùn)工,聽(tīng) SPI2_RX 的哨聲// 【TX 發(fā)送通道配置】
DMA_InitStruct.DMA_SrcAddress = (uint32_t)CW_DMA_TxBuf1; // 收貨地:我們的 "kunkun" 數(shù)組
DMA_InitStruct.DMA_SrcInc = DMA_SrcAddress_Increase; // 拿貨時(shí)挨個(gè)字母拿
DMA_InitStruct.DMA_DstAddress = (uint32_t)&CW_SPI2->DR; // 卸貨地:SPI 的數(shù)據(jù)寄存器
DMA_InitStruct.HardTrigSource = 37; // 告訴搬運(yùn)工,聽(tīng) SPI2_TX 的哨聲




找到“店名”(觸發(fā)源編號(hào) Index)
看你第一張圖:
001000:這是二進(jìn)制的 8。手冊(cè)規(guī)定,這是 SPI2 接收店的“店號(hào)”。
001001:這是二進(jìn)制的 9。手冊(cè)規(guī)定,這是 SPI2 發(fā)送店的“店號(hào)”。
找到“打卡方式”(位域分配)
看你第二張圖(DMA 觸發(fā)寄存器位域描述):
第 0 位 (TYPE):設(shè)置為 1 才能開(kāi)啟“硬件觸發(fā)模式”。如果是 0,搬運(yùn)工就不聽(tīng) SPI 的哨聲了。
第 5 ~ 2 位 (HARDSRC):手冊(cè)規(guī)定,這 4 位是用來(lái)填“店號(hào)”的。
現(xiàn)場(chǎng)算賬(公式推導(dǎo))
因?yàn)椤暗晏?hào)”要填在從 第 2 位 開(kāi)始的地方,所以我們需要把店號(hào) 左移 2 位(相當(dāng)于乘以 4),然后把 第 0 位 設(shè)為 1。
對(duì)于 SPI2_RX (接收):
店號(hào):8(二進(jìn)制 1000)。
填位:把 1000 往左挪兩位,變成 1000xx。
加上開(kāi)關(guān):最后一位(TYPE)填 1,變成 100001。
轉(zhuǎn)換:二進(jìn)制 100001 就是十進(jìn)制的 33!
$$8 times 4 + 1 = 33$$
對(duì)于 SPI2_TX (發(fā)送):
店號(hào):9(二進(jìn)制 1001)。
填位:把 1001 往左挪兩位,變成 1001xx。
加上開(kāi)關(guān):最后一位(TYPE)填 1,變成 100101。
轉(zhuǎn)換:二進(jìn)制 100101 就是十進(jìn)制的 37!
$$9 times 4 + 1 = 37$$
| 信號(hào)名稱(chēng) | 原始編號(hào) (Index) | 寄存器填法 (二進(jìn)制) | 最終數(shù)值 |
| SPI2_RX | 8 (1000) | 10 0001 | 33 |
| SPI2_TX | 9 (1001) | 10 0101 | 37 |
很多朋友喜歡自己手算地址,比如寫(xiě)個(gè) 0x4000380C。一旦算錯(cuò)哪怕一個(gè)字節(jié),DMA 就會(huì)把數(shù)據(jù)搬到錯(cuò)誤的地方導(dǎo)致崩潰。用 &CW_SPI2->DR 讓編譯器去抓取絕對(duì)正確的地址,最穩(wěn)妥!
// 避坑 3:安全地?fù)軇?dòng)開(kāi)關(guān)// 先關(guān)閉 SPI (SPE=0),確保寄存器可寫(xiě),防止被硬件鎖死
CW_SPI2->CR1 &= ~(uint32_t)(1 < 6);
CW_SPI2-?>CR1 |= (uint32_t)(0x03 < 16); // 告訴 SPI:允許你呼叫 DMA!
CW_SPI2-?>CR1 |= (uint32_t)(1 < 6); // 重新開(kāi)啟 SPI (SPE=1)
}
SPE=0(熄火):你必須先按下停止鍵,讓機(jī)器停下來(lái)。否則,為了安全,機(jī)器的換擋桿(寄存器)是鎖死拔不動(dòng)的。
設(shè)置 DMA(換擋):機(jī)器停穩(wěn)后,你才能把檔位撥到“全自動(dòng)模式(DMA模式)”。
SPE=1(重新啟動(dòng)):接好線(xiàn)、換好擋后,再次合上電源。這時(shí)候,機(jī)器就會(huì)按照你設(shè)定的“全自動(dòng)模式”狂奔了。
如果你跳過(guò)第一步直接改,表面上代碼寫(xiě)進(jìn)去了,但實(shí)際上機(jī)器內(nèi)部的檔位根本沒(méi)動(dòng),這就是為什么很多人程序卡死在 while 里的“靈異”原因。
第四步:寫(xiě)入數(shù)據(jù),千萬(wàn)別忘了“清腸胃”!
看 W25Q_DMA_Write_Kunkun 這個(gè)寫(xiě)函數(shù),注意中間那段極其特殊的代碼:
// 1. CPU 手動(dòng)發(fā)送指令和地址 (比如 0x02, 還有 24位地址)// ... 省略 ...//
-
cpu
+關(guān)注
關(guān)注
68文章
11308瀏覽量
225573 -
智能水表
+關(guān)注
關(guān)注
4文章
217瀏覽量
24383 -
CW32
+關(guān)注
關(guān)注
1文章
323瀏覽量
1925
發(fā)布評(píng)論請(qǐng)先 登錄
【CW32無(wú)線(xiàn)抄表項(xiàng)目】W25Q+CW32程序示例
CW32單片機(jī)如何讓生活更便捷
單片機(jī)匯編讀寫(xiě)SPI FLASH的詳細(xì)資料說(shuō)明
STC8單片機(jī)硬件SPI通信例程W25Q16
單片機(jī)學(xué)習(xí)筆記————STM32使用SPI讀寫(xiě)串行Flash(二)
STM32入門(mén)開(kāi)發(fā): 介紹SPI總線(xiàn)、讀寫(xiě)W25Q64(FLASH)(硬件+模擬時(shí)序)
STM32單片機(jī)基礎(chǔ)18——使用硬件QSPI讀寫(xiě)SPI Flash(W25Q64)
stm32 cubemx usb spi flash w25q128 u盤(pán)調(diào)試筆記
STM32 SPI讀寫(xiě)W25Q64(三)
基于CW32單片機(jī)做的軟硬件開(kāi)源項(xiàng)目
CW32單片機(jī)在智能馬桶的應(yīng)用介紹
【CW32無(wú)線(xiàn)抄表項(xiàng)目】W25Q_CW32_DMA簡(jiǎn)介
【CW32無(wú)線(xiàn)抄表項(xiàng)目】單片機(jī)SPI+DMA讀寫(xiě)Flash(W25Q)保姆級(jí)避坑指南
評(píng)論