資料下載:
https://telesky.yuque.com/bdys8w/01/zr02y6vd0r7mnzcl?singleDoc#
參考倉(cāng)庫(kù):
https://gitee.com/Armink/SFUD




一、程序分析
硬件總線(xiàn)映射(引腳與時(shí)鐘的“避坑點(diǎn)”)
#define FLASH_SPIx CW_SPI2 // 注意:CW32 中 SPI1 在 APB2 總線(xiàn),而 SPI2 通常掛載在 APB1 總線(xiàn)上! #define FLASH_SPI_CLK RCC_APB1_PERIPH_SPI2 #define FLASH_SPI_APBClkENx RCC_APBPeriphClk_Enable1 // 改為 APB1 的時(shí)鐘使能 //SPIx GPIO 統(tǒng)一修改為 GPIOB 及對(duì)應(yīng)的引腳 #define FLASH_SPI_SCK_GPIO_CLK RCC_AHB_PERIPH_GPIOB #define FLASH_SPI_SCK_GPIO_PORT CW_GPIOB #define FLASH_SPI_SCK_GPIO_PIN GPIO_PIN_10 #define FLASH_SPI_MISO_GPIO_CLK RCC_AHB_PERIPH_GPIOB #define FLASH_SPI_MISO_GPIO_PORT CW_GPIOB #define FLASH_SPI_MISO_GPIO_PIN GPIO_PIN_14 #define FLASH_SPI_MOSI_GPIO_CLK RCC_AHB_PERIPH_GPIOB #define FLASH_SPI_MOSI_GPIO_PORT CW_GPIOB #define FLASH_SPI_MOSI_GPIO_PIN GPIO_PIN_15 // CS引腳修改為 PB12 #define FLASH_SPI_CS_GPIO_CLK RCC_AHB_PERIPH_GPIOB #define FLASH_SPI_CS_GPIO_PORT CW_GPIOB #define FLASH_SPI_CS_GPIO_PIN GPIO_PIN_12 //GPIO AF (引腳復(fù)用功能重映射) #define FLASH_SPI_AF_SCK PB10_AFx_SPI2SCK() #define FLASH_SPI_AF_MISO PB14_AFx_SPI2MISO() #define FLASH_SPI_AF_MOSI PB15_AFx_SPI2MOSI() //CS LOW or HIGH (片選拉低/拉高控制宏) #define FLASH_SPI_CS_LOW() PB12_SETLOW() #define FLASH_SPI_CS_HIGH() PB12_SETHIGH()
注意:CW32 中 SPI1 在 APB2 總線(xiàn),而 SPI2 通常掛載在 APB1 總線(xiàn)上!很多新手移植代碼時(shí),把 SPI1 改成 SPI2,引腳也改了,但 Flash 就是沒(méi)反應(yīng)。原因就在于沒(méi)注意單片機(jī)內(nèi)部的總線(xiàn)掛載情況,把 APB1 錯(cuò)寫(xiě)成了 APB2,導(dǎo)致時(shí)鐘根本沒(méi)開(kāi)起來(lái)。。
初始化參數(shù):用代碼還原“時(shí)序圖”
示例程序:SPI 初始化核心代碼段
/************************ SPI 參數(shù)配置 ***********************/ SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 雙線(xiàn)全雙工 (DI和DO兩根線(xiàn)同時(shí)工作) SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主機(jī)模式 (單片機(jī)當(dāng)老板,F(xiàn)lash當(dāng)員工) SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 一次發(fā) 8 個(gè) bit (一個(gè)字節(jié)) // 重點(diǎn) 1:時(shí)鐘極性與相位 (還原 Mode 3) SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // 時(shí)鐘空閑時(shí)為高電平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 在第 2 個(gè)邊沿 (上升沿) 抓取數(shù)據(jù) // 重點(diǎn) 2:片選信號(hào)軟件控制 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 放棄硬件CS,改用普通GPIO軟件控制 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 速度設(shè)置:分頻系數(shù) (可根據(jù)需要調(diào)整) // 重點(diǎn) 3:高低位順序 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 最高有效位 (MSB) 最先發(fā)送 SPI_Init(FLASH_SPIx, &SPI_InitStructure); // 把配置參數(shù)正式寫(xiě)入單片機(jī)寄存器 SPI_Cmd(FLASH_SPIx, ENABLE); // 啟動(dòng) SPI 模塊
大家還記得前面我們?cè)?W25Q64 數(shù)據(jù)手冊(cè)里看到的時(shí)序圖嗎?有一條虛線(xiàn)標(biāo)著 Mode 3,它的特點(diǎn)是:單片機(jī)不發(fā)數(shù)據(jù)時(shí),時(shí)鐘線(xiàn)(CLK)是停在高電平的。 代碼里的 SPI_CPOL = SPI_CPOL_High 就是在告訴單片機(jī):‘沒(méi)事干的時(shí)候,把時(shí)鐘線(xiàn)拉高’。
那么什么時(shí)候讀數(shù)據(jù)呢?Mode 3 規(guī)定是在時(shí)鐘的上升沿。大家想,既然空閑是高電平,那它動(dòng)起來(lái)的第一個(gè)動(dòng)作肯定是‘往下拉’(下降沿,第 1 個(gè)邊沿),然后才是‘往上拉’(上升沿,第 2 個(gè)邊沿)。所以,我們必須把相位配置成 SPI_CPHA = SPI_CPHA_2Edge。
這兩行代碼加在一起,就是標(biāo)準(zhǔn)的 SPI Mode 3!
手動(dòng)包裹每一次通訊 (重點(diǎn))
這是軟件 CS 最直觀的體現(xiàn)。 SPI_FLASH_WriteEnable 函數(shù),就像做漢堡一樣,把發(fā)送數(shù)據(jù)的動(dòng)作“夾”在拉低和拉高之間:
void SPI_FLASH_WriteEnable(void){
FLASH_SPI_CS_LOW(); // 1. 手動(dòng)拉低:老師點(diǎn)名“W25Q64,聽(tīng)好了!”
SPI_FLASH_SendByte(FLASH_CMD_WriteEnable); // 2. 發(fā)送 0x06 指令
FLASH_SPI_CS_HIGH(); // 3. 手動(dòng)拉高:指令結(jié)束,“去執(zhí)行吧!”
}
以后不管是發(fā) 1 個(gè)字節(jié),還是發(fā) 256 個(gè)字節(jié),格式永遠(yuǎn)是:先拉低 -> 中間瘋狂發(fā)數(shù)據(jù) -> 最后拉高。
二、 為什么放棄硬件 CS,非要自己用軟件寫(xiě)?
硬件 SPI 往往很“死板”。有些單片機(jī)的硬件 CS 邏輯是:每發(fā)送完一個(gè)字節(jié),它就會(huì)自動(dòng)把 CS 拉高一下,然后再拉低發(fā)下一個(gè)字節(jié)。
致命后果:回憶一下我們之前的時(shí)序圖,如果執(zhí)行 Page Program (頁(yè)寫(xiě)入) 連續(xù)寫(xiě) 256 個(gè)字節(jié),W25Q64 要求這期間 CS 必須全程保持低電平。如果硬件 SPI 中途把 CS 拉高了哪怕一微秒,F(xiàn)lash 就會(huì)認(rèn)為:“通訊被意外打斷了,剛才收到的數(shù)據(jù)全部作廢!”
軟件 CS 的優(yōu)勢(shì):只有程序員才知道一次通訊到底多長(zhǎng)。用代碼控制,哪怕發(fā) 1000 個(gè)字節(jié),只要我們不寫(xiě) FLASH_SPI_CS_HIGH(),門(mén)就永遠(yuǎn)開(kāi)著。
可能會(huì)有人以為,把 0x06 或擦除指令發(fā)給 Flash,它立刻就去干活了。錯(cuò)!
原理解密:Flash 內(nèi)部有一個(gè)指令緩存。它一直在聽(tīng),直到看到 CS 從低變高(上升沿) 的那一瞬間,它才知道:“哦,單片機(jī)的話(huà)說(shuō)完了,我現(xiàn)在立刻去執(zhí)行!”
軟件 CS 的優(yōu)勢(shì):通過(guò)軟件代碼,我們能精準(zhǔn)地確保最后一個(gè) bit 完全從引腳上發(fā)送出去了,再?gòu)娜莸貓?zhí)行 PB12_SETHIGH(),觸發(fā) Flash 內(nèi)部的高壓泵去擦寫(xiě)。硬件 CS 往往在時(shí)鐘停止的那一瞬間就立刻抬起,有時(shí)會(huì)導(dǎo)致最后一個(gè)比特的保持時(shí)間不夠。
3、SPI 的“心臟”:底層收發(fā)函數(shù)
/**
* @brief 通過(guò) SPI 發(fā)送 1 個(gè)字節(jié),同時(shí)接收 1 個(gè)字節(jié)
*/
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
/*1. 等待發(fā)送漏斗空出來(lái) (TXE: Transmit Buffer Empty)單片機(jī)往外發(fā)數(shù)據(jù)是需要時(shí)間的
發(fā)送寄存器里上一個(gè)字節(jié)還沒(méi)漏完,馬上又塞一個(gè)新字節(jié)進(jìn)去,新數(shù)據(jù)就會(huì)把老數(shù)據(jù)擠爆(覆蓋掉)。
所以我們必須死等,直到單片機(jī)說(shuō):‘報(bào)告,TXE 標(biāo)志位置位了’ 我們才能執(zhí)行下一步 SPI_SendData 進(jìn)去。
*/
while(SPI_GetFlagStatus(FLASH_SPIx, SPI_FLAG_TXE) == RESET);
// 2. 把數(shù)據(jù)倒進(jìn)發(fā)送漏斗
SPI_SendData(FLASH_SPIx, byte);
// 3. 等待接收漏斗裝滿(mǎn) (RXNE: Receive Buffer Not Empty)
/*
單片機(jī)就會(huì)觸發(fā)一個(gè)嚴(yán)重的溢出錯(cuò)誤(OVR 標(biāo)志位置位)。一旦發(fā)生這個(gè)錯(cuò)誤,SPI 硬件就會(huì)強(qiáng)行自我鎖死,
拒絕再發(fā)送或接收任何數(shù)據(jù),直到你手動(dòng)去清空錯(cuò)誤標(biāo)志
*/
while(SPI_GetFlagStatus(FLASH_SPIx, SPI_FLAG_RXNE) == RESET);
// 4. 把接收漏斗里的數(shù)據(jù)拿出來(lái)返回、
/*
SPI 最核心的物理機(jī)制了:移位寄存器(Shift Register)。
SPI 的 MOSI(發(fā))和 MISO(收)在單片機(jī)內(nèi)部其實(shí)連著同一個(gè)首尾相接的環(huán)形跑道。
你每往外擠出去 1 個(gè) bit,外面就必然會(huì)擠進(jìn)來(lái) 1 個(gè) bit。
也就是說(shuō),哪怕你只是想單純地發(fā)指令給 Flash(比如發(fā) 0x06),當(dāng)你發(fā)完這 8 個(gè) bit 的同時(shí),F(xiàn)lash 也會(huì)被迫通過(guò) MISO 給你塞回來(lái) 8 個(gè) bit 的‘垃圾數(shù)據(jù)’。
我們?nèi)绻话堰@些垃圾數(shù)據(jù)從接收漏斗(SPI_ReceiveData)里拿走清空,下次想真正收數(shù)據(jù)時(shí),系統(tǒng)就會(huì)報(bào)錯(cuò)。這就是為什么發(fā)送函數(shù)最后必須要 return 一個(gè)接收值?!? */
return SPI_ReceiveData(FLASH_SPIx);
}
4、擦與寫(xiě)操作

/** * @brief 扇區(qū)擦除 4KB * * @param SectorAddr :待擦除的扇區(qū)地址 */ void SPI_FLASH_SectorErase(uint32_t SectorAddr) { //發(fā)送 寫(xiě)使能 指令 SPI_FLASH_WriteEnable(); //等待寫(xiě)入完成 // SPI_FLASH_WaitForWriteEnd(); FLASH_SPI_CS_LOW(); //發(fā)送 扇區(qū)擦除 指令 SPI_FLASH_SendByte(FLASH_CMD_SectorErase); //發(fā)送 待擦除扇區(qū)地址 SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16); // 發(fā)送高 8 位地址 SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8); // 發(fā)送中 8 位地址 SPI_FLASH_SendByte(SectorAddr & 0xFF); // 發(fā)送低 8 位地址 FLASH_SPI_CS_HIGH(); //等待擦除完成 SPI_FLASH_WaitForWriteEnd(); }
傳入的 SectorAddr 最好是 4096 的整數(shù)倍(比如 0x000000, 0x001000)。如果你傳了個(gè)中間地址,F(xiàn)lash 還是會(huì)暴力地把包含這個(gè)地址的整個(gè) 4KB 扇區(qū)全部抹掉!

這段代碼極其簡(jiǎn)單,就是個(gè) while 循環(huán),把傳進(jìn)來(lái)的數(shù)組數(shù)據(jù)一個(gè)一個(gè)發(fā)出去。 但它有一個(gè)致命的物理限制——它絕對(duì)不能跨頁(yè)! 如果你在這一頁(yè)的第 250 個(gè)字節(jié)處開(kāi)始寫(xiě),準(zhǔn)備寫(xiě) 10 個(gè)字節(jié)。當(dāng)寫(xiě)到第 256 個(gè)字節(jié)(本頁(yè)結(jié)尾)時(shí),F(xiàn)lash 不會(huì)自動(dòng)翻頁(yè)!它會(huì)像打字機(jī)卡殼一樣,強(qiáng)行把打字頭拽回本頁(yè)的第 1 個(gè)字節(jié),把你之前好端端的數(shù)據(jù)給覆蓋掉。這就是著名的‘頁(yè)卷回(Page Wrap)’災(zāi)難?!?/p>
如果沒(méi)有大容量的 RAM 做緩存,就全靠這個(gè)函數(shù)來(lái)智能切分?jǐn)?shù)據(jù),安全跨頁(yè)。

/**
* @brief 寫(xiě)入不定量數(shù)據(jù)
*
* @param pBuffer :待寫(xiě)入數(shù)據(jù)的指針
* @param WriteAddr :寫(xiě)入地址
* @param NumByteToWrite :寫(xiě)入數(shù)據(jù)長(zhǎng)度
* @note
* -需要先擦除
*/
void SPI_FLASH_BufferWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
Addr = WriteAddr % SPI_FLASH_PageSize;
count = SPI_FLASH_PageSize - Addr;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
if(Addr == 0) /* WriteAddr 剛好按頁(yè)對(duì)齊 */
{
if(NumOfPage == 0) /* NumByteToWrite < SPI_FLASH_PageSize */
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
else /* NumByteToWrite >= SPI_FLASH_PageSize */
{
while(NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
if(NumOfSingle != 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
else /* WriteAddr 與 SPI_FLASH_PageSize 不對(duì)齊 */
{
if(NumOfPage == 0) /* NumByteToWrite < SPI_FLASH_PageSize */
{
if(NumOfSingle > count) /*!< (NumByteToWrite + WriteAddr) > SPI_FLASH_PageSize */
{
temp = NumOfSingle - count;
//寫(xiě)完當(dāng)前頁(yè)
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
//寫(xiě)剩余數(shù)據(jù)
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
}
else
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
}
else /* NumByteToWrite >= SPI_FLASH_PageSize */
{
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
//先寫(xiě)完當(dāng)前頁(yè),以后地址將對(duì)齊
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
//WriteAddr 剛好按頁(yè)對(duì)齊
while(NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
if(NumOfSingle != 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
算法邏輯解剖:四個(gè)關(guān)鍵變量:
Addr = WriteAddr % SPI_FLASH_PageSize;
翻譯:算一下你要寫(xiě)的起始位置,在當(dāng)前頁(yè)的偏移量是多少(也就是打字機(jī)現(xiàn)在處于這一頁(yè)的第幾格)。如果 Addr == 0,說(shuō)明剛好在一頁(yè)的開(kāi)頭(完美對(duì)齊)。
count = SPI_FLASH_PageSize - Addr;
翻譯:算一下當(dāng)前這一頁(yè),還剩下多少空位可以寫(xiě)。
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
翻譯:算一下你給的數(shù)據(jù)總長(zhǎng),能填滿(mǎn)幾個(gè)完整的整頁(yè)。
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
翻譯:算一下填滿(mǎn)整頁(yè)后,最后還剩下的一條“小尾巴”是幾個(gè)字節(jié)。
這個(gè)函數(shù)的本質(zhì)就是‘填坑與翻頁(yè)’。 假設(shè)你現(xiàn)在身處第一頁(yè)的末尾,還剩 10 個(gè)空位(count=10),但你手里有 300 個(gè)字節(jié)要寫(xiě)。 這個(gè)函數(shù)的邏輯是:
先調(diào)用 PageWrite,把手里的 10 個(gè)字節(jié)塞進(jìn)當(dāng)前的空位,把這一頁(yè)填滿(mǎn)。
此時(shí)地址自動(dòng)對(duì)齊到了下一頁(yè)的開(kāi)頭。
手里還剩 290 個(gè)字節(jié)。算一下,剛好能填滿(mǎn) 1 個(gè)整頁(yè)(256字節(jié))。于是用 while 循環(huán)再調(diào)用一次 PageWrite 寫(xiě)入 256 字節(jié)。
最后剩下一條 34 字節(jié)的尾巴(NumOfSingle=34),再調(diào)用一次 PageWrite 收尾。 有了這個(gè)總監(jiān)把關(guān),我們?cè)趹?yīng)用層只需要無(wú)腦調(diào)用 BufferWrite,想寫(xiě)多少寫(xiě)多少,再也不用管什么 256 字節(jié)的物理邊界了!”
示例
假設(shè)你現(xiàn)在身處第一頁(yè)的末尾,還剩 10 個(gè)空位(count=10),但你手里有 300 個(gè)字節(jié)要寫(xiě)。 這個(gè)函數(shù)的邏輯是:
先調(diào)用 PageWrite,把手里的 10 個(gè)字節(jié)塞進(jìn)當(dāng)前的空位,把這一頁(yè)填滿(mǎn)。
此時(shí)地址自動(dòng)對(duì)齊到了下一頁(yè)的開(kāi)頭。
手里還剩 290 個(gè)字節(jié)。算一下,剛好能填滿(mǎn) 1 個(gè)整頁(yè)(256字節(jié))。于是用 while 循環(huán)再調(diào)用一次 PageWrite 寫(xiě)入 256 字節(jié)。
最后剩下一條 34 字節(jié)的尾巴(NumOfSingle=34),再調(diào)用一次 PageWrite 收尾。 有了這個(gè)總監(jiān)把關(guān),我們?cè)趹?yīng)用層只需要無(wú)腦調(diào)用 BufferWrite,想寫(xiě)多少寫(xiě)多少,再也不用管什么 256 字節(jié)的物理邊界了!”
大家發(fā)現(xiàn)沒(méi)有,讀取函數(shù)并沒(méi)有像寫(xiě)入那樣去算什么頁(yè)邊界、滿(mǎn)不滿(mǎn)的問(wèn)題。 為什么?因?yàn)?Flash 的物理結(jié)構(gòu)對(duì)‘讀操作’沒(méi)有設(shè)限!只要你不拉高 CS 引腳,內(nèi)部的地址指針就會(huì)自動(dòng)加 1。哪怕你直接讓 NumByteToRead = 8388608(8MB),它也會(huì)順暢地把整顆芯片從頭到尾給你掃一遍。這就是‘掃描儀’的威力。

void SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
// 動(dòng)作 1:拉下開(kāi)關(guān),告訴 Flash 準(zhǔn)備干活
FLASH_SPI_CS_LOW();
// 動(dòng)作 2:發(fā)送“普通讀”指令 (0x03)
SPI_FLASH_SendByte(FLASH_CMD_ReadData);
// 動(dòng)作 3:發(fā)送 24 位起始地址 (從哪里開(kāi)始讀?)
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16); // 高 8 位
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8); // 中 8 位
SPI_FLASH_SendByte(ReadAddr & 0xFF); // 低 8 位
// 動(dòng)作 4:開(kāi)啟“吸塵器”模式,瘋狂吸取數(shù)據(jù)
while(NumByteToRead--)
{
*pBuffer = SPI_FLASH_ReadByte(); // 內(nèi)部在發(fā) 0xFF 啞字節(jié)換取數(shù)據(jù)
pBuffer++; // 指針后移,準(zhǔn)備存下一個(gè)字節(jié)
}
// 動(dòng)作 5:完工,拉高 CS 結(jié)束通訊
FLASH_SPI_CS_HIGH();
}
大家仔細(xì)對(duì)比一下我們上一節(jié)講的 BufferWrite,寫(xiě)數(shù)據(jù)的時(shí)候,代碼長(zhǎng)達(dá)幾十行,要算偏移量、算剩余空間,一旦跨越 256 字節(jié)的頁(yè)邊界就得重新發(fā)地址。
但是你們看讀數(shù)據(jù)的代碼,居然只有一個(gè)簡(jiǎn)單的 while 循環(huán)! 它根本不管 256 字節(jié)的界限,想讀多少就讀多少(NumByteToRead 甚至可以填幾萬(wàn))。這是為什么呢?
這就是 Flash 的物理魅力!寫(xiě)數(shù)據(jù)像用老式打字機(jī),打到紙的邊緣(256字節(jié))就會(huì)卡死,必須手動(dòng)換行(重新發(fā)地址)。而讀數(shù)據(jù)就像拉開(kāi)一幅無(wú)盡的卷軸,只要你一開(kāi)始告訴它一個(gè)起始地址(動(dòng)作 3),并且只要 CS 引腳一直保持低電平,F(xiàn)lash 內(nèi)部的地址指針就會(huì)自動(dòng) +1、跨頁(yè)、跨扇區(qū)、跨塊,暢通無(wú)阻!
我們現(xiàn)在用的指令是 0x03(普通讀取)。在 W25Q64 的手冊(cè)里,普通讀取的時(shí)鐘頻率是有上限的(通常在 33MHz 甚至更低)。 如果你的 CW32 單片機(jī)跑得飛快,把 SPI 時(shí)鐘設(shè)置到了 48MHz 極限狂飆,用這個(gè) 0x03 指令讀出來(lái)的數(shù)據(jù)可能會(huì)錯(cuò)位或者全是亂碼!
解決辦法: 把 0x03 換成我們?cè)跁r(shí)序圖章節(jié)講過(guò)的 0x0B(Fast Read,極速讀取)。唯一的區(qū)別是,發(fā)完 24 位地址后,代碼里要多發(fā)一個(gè)字節(jié)的 0xFF(8個(gè) Dummy Clocks)給 Flash 留出反應(yīng)時(shí)間,然后才能進(jìn)入 while 循環(huán)去吸取真實(shí)數(shù)據(jù)。
二、SDK分析與移植
1.SDK分析


原工程中沒(méi)有下列程序,需要自己找一個(gè)地方加進(jìn)去

/* 1. 針對(duì) AC6 的禁用半主機(jī)指令 */ __asm(".global __use_no_semihostingnt"); /* 2. 定義標(biāo)準(zhǔn)庫(kù)需要的支持函數(shù) */ #include /* 這里的 __FILE 結(jié)構(gòu)體在 AC6 下通常不需要手動(dòng)定義,MicroLIB 會(huì)處理 */ /* 但為了徹底重定向 printf,我們需要實(shí)現(xiàn)底層輸出函數(shù) */ // 如果你沒(méi)在其他地方定義 fputc,請(qǐng)加上這段: int fputc(int ch, FILE *f) { // 假設(shè)你使用的是 UART1,發(fā)送寄存器為 TDR // 這里的具體寄存器名根據(jù) CW32 庫(kù)文件決定,通常是 CW_UART1->TDR 或類(lèi)似 USART_SendData_8bit(CW_UART1, (uint8_t)ch); while (USART_GetFlagStatus(CW_UART1, USART_FLAG_TXE) == RESET); return ch; } /* 3. 定義半主機(jī)依賴(lài)的底層存根函數(shù) */ void _sys_exit(int x) { x = x; while (1); // 報(bào)錯(cuò)后死循環(huán) } void _ttywrch(int ch) { ch = ch; }
這段代碼是嵌入式開(kāi)發(fā)里非常經(jīng)典的 “printf串口重定向與半主機(jī)模式(Semihosting)禁用” 模板。特別是當(dāng)你從舊版的 Keil AC5 編譯器升級(jí)到最新的 AC6 編譯器 時(shí),這段代碼是必須要有的“護(hù)身符”。
如果在代碼里調(diào)用了 printf(),但不加這段程序
這段代碼是嵌入式開(kāi)發(fā)里非常經(jīng)典的 “printf串口重定向與半主機(jī)模式(Semihosting)禁用” 模板。特別是當(dāng)你從舊版的 Keil AC5 編譯器升級(jí)到最新的 AC6 編譯器 時(shí),這段代碼是必須要有的“護(hù)身符”。
如果在代碼里調(diào)用了 printf(),但不加這段程序,你會(huì)遇到兩種極其折磨人的報(bào)錯(cuò)現(xiàn)象:
現(xiàn)象一:編譯直接報(bào)錯(cuò)(Linker Error)
如果你不加 _sys_exit 和 _ttywrch 這幾個(gè)存根函數(shù),同時(shí)又在代碼里用了標(biāo)準(zhǔn) C 庫(kù)函數(shù)(沒(méi)勾選 MicroLIB 的情況下),點(diǎn)擊編譯時(shí),Keil 的 Build Output 窗口會(huì)爆出紅色的底層鏈接錯(cuò)誤:
常見(jiàn)報(bào)錯(cuò)長(zhǎng)這樣:
Error: L6218E: Undefined symbol _sys_exit (referred from ...)
Error: L6218E: Undefined symbol _ttywrch (referred from ...)
Error: L6218E: Undefined symbol __aeabi_assert ...
為什么報(bào)錯(cuò)? C 語(yǔ)言的標(biāo)準(zhǔn)庫(kù)原本是給電腦(Windows/Linux)設(shè)計(jì)的,當(dāng)程序出錯(cuò)或者結(jié)束時(shí),它會(huì)默認(rèn)去調(diào)用操作系統(tǒng)的退出函數(shù)(exit)或終端輸出函數(shù)(ttywrch)。但我們的 CW32 單片機(jī)里根本沒(méi)有操作系統(tǒng)!編譯器找不到這些底層函數(shù),就會(huì)報(bào)“未定義符號(hào)”的錯(cuò)誤。代碼里寫(xiě)死這幾個(gè)空函數(shù),就是為了騙過(guò)編譯器:“行了,退出函數(shù)我給你準(zhǔn)備好了,你別報(bào)錯(cuò)了?!?/p>
現(xiàn)象二:運(yùn)行時(shí)“拔線(xiàn)死機(jī)”(The Silent Killer)
這是最坑、最容易讓崩潰的現(xiàn)象。如果你沒(méi)加 __asm(".global __use_no_semihostingnt"); 這句話(huà),編譯可能完全通過(guò),零警告,但一下載到板子上就會(huì)出現(xiàn)“靈異事件”:
插著仿真器調(diào)試: 代碼跑得好好的,printf 的數(shù)據(jù)能在 Keil 的 Debug 窗口里打印出來(lái)。
拔掉仿真器,插充電寶獨(dú)立供電: 板子死機(jī)了!程序卡死在啟動(dòng)階段,LED 也不閃了,所有任務(wù)罷工。
為什么死機(jī)?(半主機(jī)模式的坑) 半主機(jī)模式(Semihosting)是一種調(diào)試機(jī)制。它會(huì)讓單片機(jī)的 printf 試圖通過(guò) JTAG/SWD 仿真器的數(shù)據(jù)線(xiàn),把字符傳給電腦屏幕。 如果沒(méi)禁用半主機(jī)模式,每次執(zhí)行 printf 時(shí),單片機(jī)內(nèi)部會(huì)觸發(fā)一條特殊的硬件斷點(diǎn)指令(BKPT)來(lái)呼叫電腦。當(dāng)你拔掉仿真器獨(dú)立運(yùn)行時(shí),單片機(jī)喊破喉嚨也沒(méi)人理它,它就會(huì)一直卡在這個(gè)斷點(diǎn)指令上,導(dǎo)致整個(gè)系統(tǒng)徹底死機(jī)。
現(xiàn)象三:printf 變成“啞巴”
如果不加 fputc 這個(gè)函數(shù):
現(xiàn)象: 編譯可以通過(guò),程序也不會(huì)死機(jī),但是你的電腦串口助手里收不到任何數(shù)據(jù)。
為什么?printf 只負(fù)責(zé)把你要發(fā)送的變量轉(zhuǎn)換成字符格式(比如把數(shù)字 123 變成字符 '1', '2', '3'),但它不知道這些字符要從單片機(jī)的哪個(gè)引腳扔出去。 fputc 就是 printf 和 CW32 硬件之間的**“水管接頭”**。你在 fputc 里寫(xiě)了 USART_SendData_8bit(CW_UART1, ch),printf 才知道:“哦!原來(lái)我要把字符塞進(jìn) UART1 的發(fā)送寄存器里啊。”
2.示例程序

#include "flashhoufun.h"
#include "cw32_eval_spi_flash.h"
uint8_t Flash_TxBuffer[] = "kunkun";
uint8_t Flash_RxBuffer[BufferSize];
uint8_t Flash_TxBuffer2[] = "zhiyin";
uint8_t Flash_RxBuffer2[7]; // zhiyin 長(zhǎng)度為 6 + ''
uint8_t DeviceID = 0;
uint16_t ManufactDeviceID = 0;
uint32_t JedecID = 0;
uint8_t UniqueID[8];
// 使用新名字
volatile FlashTestStatus TransferStatus = FLASH_FAILED;
// 替換返回類(lèi)型和內(nèi)部比較的宏
FlashTestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength)
{
while(BufferLength--)
{
if(*pBuffer1 != *pBuffer2)
{
return FLASH_FAILED;
}
pBuffer1++;
pBuffer2++;
}
return FLASH_PASSED;
}
//void flash_fun(void)
//{
// DeviceID = SPI_FLASH_DeviceID();
// ManufactDeviceID = SPI_FLASH_ManufactDeviceID();
// JedecID = SPI_FLASH_JedecID();
// SPI_FLASH_UniqueID(UniqueID);
//
// // 擦除扇區(qū) 4KB
// SPI_FLASH_SectorErase(FLASH_SectorToEraseAddress);
//
// // 寫(xiě)數(shù)據(jù)
// SPI_FLASH_BufferWrite(Flash_TxBuffer, FLASH_WriteAddress, BufferSize);
// printf("rn嘗試寫(xiě)入的數(shù)據(jù)為: %srn", Flash_TxBuffer);
//
// // 讀數(shù)據(jù)
// SPI_FLASH_BufferRead(Flash_RxBuffer, FLASH_ReadAddress, BufferSize);
// printf("rn實(shí)際讀出的數(shù)據(jù)為: %srn", Flash_RxBuffer);
//
// // 檢查
// TransferStatus = Buffercmp(Flash_TxBuffer, Flash_RxBuffer, BufferSize);
// if(TransferStatus == FLASH_PASSED)
// {
// printf("rnFLASH Success! kunkun 驗(yàn)證通過(guò)!rn");
// }
// else
// {
// printf("rnFLASH Error 1! 數(shù)據(jù)不一致!rn");
// }
//}
void flash_fun(void)
{
// --- 步驟 1:讀取 ID 確認(rèn)通信 (保持不變) ---
DeviceID = SPI_FLASH_DeviceID();
ManufactDeviceID = SPI_FLASH_ManufactDeviceID();
JedecID = SPI_FLASH_JedecID();
SPI_FLASH_UniqueID(UniqueID);
// --- 步驟 2:測(cè)試第一個(gè)扇區(qū) (0-4KB) ---
uint32_t addr1 = 0x0000;
SPI_FLASH_SectorErase(addr1); // 擦除第一個(gè) 4KB
SPI_FLASH_BufferWrite(Flash_TxBuffer, addr1, BufferSize);
SPI_FLASH_BufferRead(Flash_RxBuffer, addr1, BufferSize);
if(Buffercmp(Flash_TxBuffer, Flash_RxBuffer, BufferSize) == FLASH_PASSED)
{
printf("rn[Sector 0] kunkun 驗(yàn)證通過(guò)!正在挑戰(zhàn) Sector 1...");
// --- 步驟 3:測(cè)試第二個(gè)扇區(qū) (4-8KB) ---
// 5-8KB 的數(shù)據(jù)屬于第二個(gè) 4KB 扇區(qū),起始地址為 0x1000
uint32_t addr2 = 0x1000;
SPI_FLASH_SectorErase(addr2); // 擦除第二個(gè) 4KB 扇區(qū)
SPI_FLASH_BufferWrite(Flash_TxBuffer2, addr2, 7);
printf("rn嘗試向 0x1000 寫(xiě)入數(shù)據(jù): %s", Flash_TxBuffer2);
SPI_FLASH_BufferRead(Flash_RxBuffer2, addr2, 7);
printf("rn從 0x1000 實(shí)際讀出數(shù)據(jù): %s", Flash_RxBuffer2);
if(Buffercmp(Flash_TxBuffer2, Flash_RxBuffer2, 7) == FLASH_PASSED)
{
printf("rn[Sector 1] zhiyin 驗(yàn)證通過(guò)!兩個(gè)區(qū)域均正常!rn");
TransferStatus = FLASH_PASSED;
}
else
{
printf("rn[Sector 1] zhiyin 失敗,請(qǐng)檢查地址 0x1000 處的寫(xiě)入。");
TransferStatus = FLASH_FAILED;
}
}
else
{
printf("rn[Sector 0] kunkun 驗(yàn)證失敗,請(qǐng)檢查底層驅(qū)動(dòng)。");
TransferStatus = FLASH_FAILED;
}
}
#include "main.h"
#include "cw32f030_gpio.h"
#include "cw32f030_rcc.h"
#include "init.h"
#include "buffer.h"
#include "fun.h"
#include "radio.h"
#include "delay.h"
#include "flashhoufun.h"
#include "cw32_eval_spi_flash.h"
// 全局中斷標(biāo)志 (fun.c 也要用)
volatile uint8_t g_bIrqTriggered = 0;
void System_Init_Config(void);
int32_t main(void)
{
System_Init_Config();
SPI_FLASH_Init();
flash_fun();
while (1)
{
}
}
void System_Init_Config(void)
{
RCC_Configuration();
GPIO_Configuration();
SPI_Configuration();
EXTI_Configuration();
ADC_Configuration();
}
3.實(shí)物與效果展示
注意:W25Q64 是 3.3V 器件,嚴(yán)禁接 5V
方案一:獨(dú)立運(yùn)行模式(無(wú)串口打?。?/strong>
當(dāng)你完成調(diào)試,準(zhǔn)備將網(wǎng)關(guān)部署到 500 個(gè)節(jié)點(diǎn)的現(xiàn)場(chǎng)時(shí),可以撤掉串口模塊以精簡(jiǎn)電路。
| 連接設(shè)備 | 設(shè)備引腳 | CW32F030 引腳 | 說(shuō)明 |
| W25Q64 | VCC | 3.3V | 電源 |
| W25Q64 | GND | GND | 電源地 |
| W25Q64 | /CS | PB12 | 軟件片選 (CS) |
| W25Q64 | CLK | PB10 | SPI2 時(shí)鐘 (SCK) |
| W25Q64 | DO (IO1) | PB14 | SPI2 數(shù)據(jù)輸出 (MISO) |
| W25Q64 | DI (IO0) | PB15 | SPI2 數(shù)據(jù)輸入 (MOSI) |
| 其他 | PA08 / PA09 | 懸空 | 串口引腳不接線(xiàn),代碼可保留以防報(bào)錯(cuò) |


方案二:開(kāi)發(fā)調(diào)試模式(帶串口打印 printf)
原工程就是如此,可以通過(guò)串口打印出來(lái)信息。
此模式下,你可以通過(guò)電腦串口助手查看 Flash 的 ID 識(shí)別情況和程序運(yùn)行狀態(tài)。
| 連接設(shè)備 | 設(shè)備引腳 | CW32F030 引腳 | 說(shuō)明 |
| W25Q64 | VCC | 3.3V | 電源(嚴(yán)禁接 5V) |
| W25Q64 | GND | GND | 電源地 |
| W25Q64 | /CS | PB12 | 軟件片選 (CS) |
| W25Q64 | CLK | PB10 | SPI2 時(shí)鐘 (SCK) |
| W25Q64 | DO (IO1) | PB14 | SPI2 數(shù)據(jù)輸出 (MISO) |
| W25Q64 | DI (IO0) | PB15 | SPI2 數(shù)據(jù)輸入 (MOSI) |
| USB轉(zhuǎn)TTL | RXD | PA08 | 單片機(jī)發(fā)送 (TX),接模塊接收 |
| USB轉(zhuǎn)TTL | TXD | PA09 | 單片機(jī)接收 (RX),接模塊發(fā)送 |
| USB轉(zhuǎn)TTL | GND | GND | 共地(通訊基礎(chǔ)) |


審核編輯 黃宇
-
無(wú)線(xiàn)抄表
+關(guān)注
關(guān)注
0文章
40瀏覽量
17435 -
CW32
+關(guān)注
關(guān)注
1文章
323瀏覽量
1935
發(fā)布評(píng)論請(qǐng)先 登錄
【CW32無(wú)線(xiàn)抄表項(xiàng)目】主控及射頻芯片介紹
CW32量產(chǎn)燒錄工具
CW32 MCU有哪些系列?
cw32和stm32的區(qū)別
cw32和gd32的區(qū)別
應(yīng)用筆記-CW32 自舉程序中使用的 ISP 協(xié)議
【CW32無(wú)線(xiàn)抄表項(xiàng)目】示例通信程序講解
【CW32無(wú)線(xiàn)抄表項(xiàng)目】W25Q_CW32_DMA簡(jiǎn)介
【CW32無(wú)線(xiàn)抄表項(xiàng)目】W25Q+CW32程序示例
評(píng)論