資料下載:
https://telesky.yuque.com/bdys8w/01/zr02y6vd0r7mnzcl?singleDoc#
參考倉庫:
https://gitee.com/Armink/SFUD




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

/** * @brief 扇區(qū)擦除 4KB * * @param SectorAddr :待擦除的扇區(qū)地址 */ void SPI_FLASH_SectorErase(uint32_t SectorAddr) { //發(fā)送 寫使能 指令 SPI_FLASH_WriteEnable(); //等待寫入完成 // 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)。如果你傳了個中間地址,F(xiàn)lash 還是會暴力地把包含這個地址的整個 4KB 扇區(qū)全部抹掉!

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

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

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


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

/* 1. 針對 AC6 的禁用半主機(jī)指令 */ __asm(".global __use_no_semihostingnt"); /* 2. 定義標(biāo)準(zhǔn)庫需要的支持函數(shù) */ #include /* 這里的 __FILE 結(jié)構(gòu)體在 AC6 下通常不需要手動定義,MicroLIB 會處理 */ /* 但為了徹底重定向 printf,我們需要實現(xiàn)底層輸出函數(shù) */ // 如果你沒在其他地方定義 fputc,請加上這段: int fputc(int ch, FILE *f) { // 假設(shè)你使用的是 UART1,發(fā)送寄存器為 TDR // 這里的具體寄存器名根據(jù) CW32 庫文件決定,通常是 CW_UART1->TDR 或類似 USART_SendData_8bit(CW_UART1, (uint8_t)ch); while (USART_GetFlagStatus(CW_UART1, USART_FLAG_TXE) == RESET); return ch; } /* 3. 定義半主機(jī)依賴的底層存根函數(shù) */ void _sys_exit(int x) { x = x; while (1); // 報錯后死循環(huán) } void _ttywrch(int ch) { ch = ch; }
這段代碼是嵌入式開發(fā)里非常經(jīng)典的 “printf串口重定向與半主機(jī)模式(Semihosting)禁用” 模板。特別是當(dāng)你從舊版的 Keil AC5 編譯器升級到最新的 AC6 編譯器 時,這段代碼是必須要有的“護(hù)身符”。
如果在代碼里調(diào)用了 printf(),但不加這段程序
這段代碼是嵌入式開發(fā)里非常經(jīng)典的 “printf串口重定向與半主機(jī)模式(Semihosting)禁用” 模板。特別是當(dāng)你從舊版的 Keil AC5 編譯器升級到最新的 AC6 編譯器 時,這段代碼是必須要有的“護(hù)身符”。
如果在代碼里調(diào)用了 printf(),但不加這段程序,你會遇到兩種極其折磨人的報錯現(xiàn)象:
現(xiàn)象一:編譯直接報錯(Linker Error)
如果你不加 _sys_exit 和 _ttywrch 這幾個存根函數(shù),同時又在代碼里用了標(biāo)準(zhǔn) C 庫函數(shù)(沒勾選 MicroLIB 的情況下),點(diǎn)擊編譯時,Keil 的 Build Output 窗口會爆出紅色的底層鏈接錯誤:
常見報錯長這樣:
Error: L6218E: Undefined symbol _sys_exit (referred from ...)
Error: L6218E: Undefined symbol _ttywrch (referred from ...)
Error: L6218E: Undefined symbol __aeabi_assert ...
為什么報錯? C 語言的標(biāo)準(zhǔn)庫原本是給電腦(Windows/Linux)設(shè)計的,當(dāng)程序出錯或者結(jié)束時,它會默認(rèn)去調(diào)用操作系統(tǒng)的退出函數(shù)(exit)或終端輸出函數(shù)(ttywrch)。但我們的 CW32 單片機(jī)里根本沒有操作系統(tǒng)!編譯器找不到這些底層函數(shù),就會報“未定義符號”的錯誤。代碼里寫死這幾個空函數(shù),就是為了騙過編譯器:“行了,退出函數(shù)我給你準(zhǔn)備好了,你別報錯了?!?/p>
現(xiàn)象二:運(yùn)行時“拔線死機(jī)”(The Silent Killer)
這是最坑、最容易讓崩潰的現(xiàn)象。如果你沒加 __asm(".global __use_no_semihostingnt"); 這句話,編譯可能完全通過,零警告,但一下載到板子上就會出現(xiàn)“靈異事件”:
插著仿真器調(diào)試: 代碼跑得好好的,printf 的數(shù)據(jù)能在 Keil 的 Debug 窗口里打印出來。
拔掉仿真器,插充電寶獨(dú)立供電: 板子死機(jī)了!程序卡死在啟動階段,LED 也不閃了,所有任務(wù)罷工。
為什么死機(jī)?(半主機(jī)模式的坑) 半主機(jī)模式(Semihosting)是一種調(diào)試機(jī)制。它會讓單片機(jī)的 printf 試圖通過 JTAG/SWD 仿真器的數(shù)據(jù)線,把字符傳給電腦屏幕。 如果沒禁用半主機(jī)模式,每次執(zhí)行 printf 時,單片機(jī)內(nèi)部會觸發(fā)一條特殊的硬件斷點(diǎn)指令(BKPT)來呼叫電腦。當(dāng)你拔掉仿真器獨(dú)立運(yùn)行時,單片機(jī)喊破喉嚨也沒人理它,它就會一直卡在這個斷點(diǎn)指令上,導(dǎo)致整個系統(tǒng)徹底死機(jī)。
現(xiàn)象三:printf 變成“啞巴”
如果不加 fputc 這個函數(shù):
現(xiàn)象: 編譯可以通過,程序也不會死機(jī),但是你的電腦串口助手里收不到任何數(shù)據(jù)。
為什么?printf 只負(fù)責(zé)把你要發(fā)送的變量轉(zhuǎn)換成字符格式(比如把數(shù)字 123 變成字符 '1', '2', '3'),但它不知道這些字符要從單片機(jī)的哪個引腳扔出去。 fputc 就是 printf 和 CW32 硬件之間的**“水管接頭”**。你在 fputc 里寫了 USART_SendData_8bit(CW_UART1, ch),printf 才知道:“哦!原來我要把字符塞進(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 長度為 6 + ''
uint8_t DeviceID = 0;
uint16_t ManufactDeviceID = 0;
uint32_t JedecID = 0;
uint8_t UniqueID[8];
// 使用新名字
volatile FlashTestStatus TransferStatus = FLASH_FAILED;
// 替換返回類型和內(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);
//
// // 寫數(shù)據(jù)
// SPI_FLASH_BufferWrite(Flash_TxBuffer, FLASH_WriteAddress, BufferSize);
// printf("rn嘗試寫入的數(shù)據(jù)為: %srn", Flash_TxBuffer);
//
// // 讀數(shù)據(jù)
// SPI_FLASH_BufferRead(Flash_RxBuffer, FLASH_ReadAddress, BufferSize);
// printf("rn實際讀出的數(shù)據(jù)為: %srn", Flash_RxBuffer);
//
// // 檢查
// TransferStatus = Buffercmp(Flash_TxBuffer, Flash_RxBuffer, BufferSize);
// if(TransferStatus == FLASH_PASSED)
// {
// printf("rnFLASH Success! kunkun 驗證通過!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:測試第一個扇區(qū) (0-4KB) ---
uint32_t addr1 = 0x0000;
SPI_FLASH_SectorErase(addr1); // 擦除第一個 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 驗證通過!正在挑戰(zhàn) Sector 1...");
// --- 步驟 3:測試第二個扇區(qū) (4-8KB) ---
// 5-8KB 的數(shù)據(jù)屬于第二個 4KB 扇區(qū),起始地址為 0x1000
uint32_t addr2 = 0x1000;
SPI_FLASH_SectorErase(addr2); // 擦除第二個 4KB 扇區(qū)
SPI_FLASH_BufferWrite(Flash_TxBuffer2, addr2, 7);
printf("rn嘗試向 0x1000 寫入數(shù)據(jù): %s", Flash_TxBuffer2);
SPI_FLASH_BufferRead(Flash_RxBuffer2, addr2, 7);
printf("rn從 0x1000 實際讀出數(shù)據(jù): %s", Flash_RxBuffer2);
if(Buffercmp(Flash_TxBuffer2, Flash_RxBuffer2, 7) == FLASH_PASSED)
{
printf("rn[Sector 1] zhiyin 驗證通過!兩個區(qū)域均正常!rn");
TransferStatus = FLASH_PASSED;
}
else
{
printf("rn[Sector 1] zhiyin 失敗,請檢查地址 0x1000 處的寫入。");
TransferStatus = FLASH_FAILED;
}
}
else
{
printf("rn[Sector 0] kunkun 驗證失敗,請檢查底層驅(qū)動。");
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.實物與效果展示
注意:W25Q64 是 3.3V 器件,嚴(yán)禁接 5V
方案一:獨(dú)立運(yùn)行模式(無串口打?。?/strong>
當(dāng)你完成調(diào)試,準(zhǔn)備將網(wǎng)關(guān)部署到 500 個節(jié)點(diǎn)的現(xiàn)場時,可以撤掉串口模塊以精簡電路。
| 連接設(shè)備 | 設(shè)備引腳 | CW32F030 引腳 | 說明 |
| W25Q64 | VCC | 3.3V | 電源 |
| W25Q64 | GND | GND | 電源地 |
| W25Q64 | /CS | PB12 | 軟件片選 (CS) |
| W25Q64 | CLK | PB10 | SPI2 時鐘 (SCK) |
| W25Q64 | DO (IO1) | PB14 | SPI2 數(shù)據(jù)輸出 (MISO) |
| W25Q64 | DI (IO0) | PB15 | SPI2 數(shù)據(jù)輸入 (MOSI) |
| 其他 | PA08 / PA09 | 懸空 | 串口引腳不接線,代碼可保留以防報錯 |


方案二:開發(fā)調(diào)試模式(帶串口打印 printf)
原工程就是如此,可以通過串口打印出來信息。
此模式下,你可以通過電腦串口助手查看 Flash 的 ID 識別情況和程序運(yùn)行狀態(tài)。
| 連接設(shè)備 | 設(shè)備引腳 | CW32F030 引腳 | 說明 |
| W25Q64 | VCC | 3.3V | 電源(嚴(yán)禁接 5V) |
| W25Q64 | GND | GND | 電源地 |
| W25Q64 | /CS | PB12 | 軟件片選 (CS) |
| W25Q64 | CLK | PB10 | SPI2 時鐘 (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ǔ)) |


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