资料下载:
https://telesky.yuque.com/bdys8w/01/zr02y6vd0r7mnzcl?singleDoc#
参考仓库:
https://gitee.com/Armink/SFUD
一、程序分析
硬件总线映射(引脚与时钟的“避坑点”)
#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 统一修改为 GPIOB 及对应的引脚#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 (引脚复用功能重映射)#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 就是没反应。原因就在于没注意单片机内部的总线挂载情况,把 APB1 错写成了 APB2,导致时钟根本没开起来。。
初始化参数:用代码还原“时序图”
示例程序:SPI 初始化核心代码段
/************************ SPI 参数配置 ***********************/SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工 (DI和DO两根线同时工作)SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式 (单片机当老板,Flash当员工)SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 一次发 8 个 bit (一个字节)// 重点 1:时钟极性与相位 (还原 Mode 3)SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // 时钟空闲时为高电平SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 在第 2 个边沿 (上升沿) 抓取数据// 重点 2:片选信号软件控制SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 放弃硬件CS,改用普通GPIO软件控制SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 速度设置:分频系数 (可根据需要调整)// 重点 3:高低位顺序SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 最高有效位 (MSB) 最先发送SPI_Init(FLASH_SPIx, &SPI_InitStructure); // 把配置参数正式写入单片机寄存器SPI_Cmd(FLASH_SPIx, ENABLE); // 启动 SPI 模块
大家还记得前面我们在 W25Q64 数据手册里看到的时序图吗?有一条虚线标着 Mode 3,它的特点是:单片机不发数据时,时钟线(CLK)是停在高电平的。 代码里的 SPI_CPOL = SPI_CPOL_High 就是在告诉单片机:‘没事干的时候,把时钟线拉高’。
那么什么时候读数据呢?Mode 3 规定是在时钟的上升沿。大家想,既然空闲是高电平,那它动起来的第一个动作肯定是‘往下拉’(下降沿,第 1 个边沿),然后才是‘往上拉’(上升沿,第 2 个边沿)。所以,我们必须把相位配置成 SPI_CPHA = SPI_CPHA_2Edge。
这两行代码加在一起,就是标准的 SPI Mode 3!
手动包裹每一次通讯 (重点)
这是软件 CS 最直观的体现。 SPI_FLASH_WriteEnable 函数,就像做汉堡一样,把发送数据的动作“夹”在拉低和拉高之间:
void SPI_FLASH_WriteEnable(void){FLASH_SPI_CS_LOW(); // 1. 手动拉低:老师点名“W25Q64,听好了!”SPI_FLASH_SendByte(FLASH_CMD_WriteEnable); // 2. 发送 0x06 指令FLASH_SPI_CS_HIGH(); // 3. 手动拉高:指令结束,“去执行吧!”}
以后不管是发 1 个字节,还是发 256 个字节,格式永远是:先拉低 -> 中间疯狂发数据 -> 最后拉高。
二、 为什么放弃硬件 CS,非要自己用软件写?
硬件 SPI 往往很“死板”。有些单片机的硬件 CS 逻辑是:每发送完一个字节,它就会自动把 CS 拉高一下,然后再拉低发下一个字节。
致命后果:回忆一下我们之前的时序图,如果执行 Page Program (页写入) 连续写 256 个字节,W25Q64 要求这期间 CS 必须全程保持低电平。如果硬件 SPI 中途把 CS 拉高了哪怕一微秒,Flash 就会认为:“通讯被意外打断了,刚才收到的数据全部作废!”
软件 CS 的优势:只有程序员才知道一次通讯到底多长。用代码控制,哪怕发 1000 个字节,只要我们不写 FLASH_SPI_CS_HIGH(),门就永远开着。
可能会有人以为,把 0x06 或擦除指令发给 Flash,它立刻就去干活了。错!
原理解密:Flash 内部有一个指令缓存。它一直在听,直到看到 CS 从低变高(上升沿) 的那一瞬间,它才知道:“哦,单片机的话说完了,我现在立刻去执行!”
软件 CS 的优势:通过软件代码,我们能精准地确保最后一个 bit 完全从引脚上发送出去了,再从容地执行 PB12_SETHIGH(),触发 Flash 内部的高压泵去擦写。硬件 CS 往往在时钟停止的那一瞬间就立刻抬起,有时会导致最后一个比特的保持时间不够。
SPI 的“心脏”:底层收发函数
/*** @brief 通过 SPI 发送 1 个字节,同时接收 1 个字节*/uint8_t SPI_FLASH_SendByte(uint8_t byte){/*1. 等待发送漏斗空出来 (TXE: Transmit Buffer Empty)单片机往外发数据是需要时间的发送寄存器里上一个字节还没漏完,马上又塞一个新字节进去,新数据就会把老数据挤爆(覆盖掉)。所以我们必须死等,直到单片机说:‘报告,TXE 标志位置位了’ 我们才能执行下一步 SPI_SendData 进去。*/while(SPI_GetFlagStatus(FLASH_SPIx, SPI_FLAG_TXE) == RESET);// 2. 把数据倒进发送漏斗SPI_SendData(FLASH_SPIx, byte);// 3. 等待接收漏斗装满 (RXNE: Receive Buffer Not Empty)/*单片机就会触发一个严重的溢出错误(OVR 标志位置位)。一旦发生这个错误,SPI 硬件就会强行自我锁死,拒绝再发送或接收任何数据,直到你手动去清空错误标志*/while(SPI_GetFlagStatus(FLASH_SPIx, SPI_FLAG_RXNE) == RESET);// 4. 把接收漏斗里的数据拿出来返回、/*SPI 最核心的物理机制了:移位寄存器(Shift Register)。SPI 的 MOSI(发)和 MISO(收)在单片机内部其实连着同一个首尾相接的环形跑道。你每往外挤出去 1 个 bit,外面就必然会挤进来 1 个 bit。也就是说,哪怕你只是想单纯地发指令给 Flash(比如发 0x06),当你发完这 8 个 bit 的同时,Flash 也会被迫通过 MISO 给你塞回来 8 个 bit 的‘垃圾数据’。我们如果不把这些垃圾数据从接收漏斗(SPI_ReceiveData)里拿走清空,下次想真正收数据时,系统就会报错。这就是为什么发送函数最后必须要 return 一个接收值。”*/return SPI_ReceiveData(FLASH_SPIx);}
擦与写操作
/*** @brief 扇区擦除 4KB** @param SectorAddr :待擦除的扇区地址*/void SPI_FLASH_SectorErase(uint32_t SectorAddr){//发送 写使能 指令SPI_FLASH_WriteEnable();//等待写入完成// SPI_FLASH_WaitForWriteEnd();FLASH_SPI_CS_LOW();//发送 扇区擦除 指令SPI_FLASH_SendByte(FLASH_CMD_SectorErase);//发送 待擦除扇区地址SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16); // 发送高 8 位地址SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8); // 发送中 8 位地址SPI_FLASH_SendByte(SectorAddr & 0xFF); // 发送低 8 位地址FLASH_SPI_CS_HIGH();//等待擦除完成SPI_FLASH_WaitForWriteEnd();}
传入的 SectorAddr 最好是 4096 的整数倍(比如 0x000000, 0x001000)。如果你传了个中间地址,Flash 还是会暴力地把包含这个地址的整个 4KB 扇区全部抹掉!
这段代码极其简单,就是个 while 循环,把传进来的数组数据一个一个发出去。 但它有一个致命的物理限制——它绝对不能跨页! 如果你在这一页的第 250 个字节处开始写,准备写 10 个字节。当写到第 256 个字节(本页结尾)时,Flash 不会自动翻页!它会像打字机卡壳一样,强行把打字头拽回本页的第 1 个字节,把你之前好端端的数据给覆盖掉。这就是著名的‘页卷回(Page Wrap)’灾难。”
如果没有大容量的 RAM 做缓存,就全靠这个函数来智能切分数据,安全跨页。
/*** @brief 写入不定量数据** @param pBuffer :待写入数据的指针* @param WriteAddr :写入地址* @param NumByteToWrite :写入数据长度* @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;//写完当前页SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);WriteAddr += count;pBuffer += count;//写剩余数据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;//先写完当前页,以后地址将对齐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);}}}}
算法逻辑解剖:四个关键变量:
Addr = WriteAddr % SPI_FLASH_PageSize;
翻译:算一下你要写的起始位置,在当前页的偏移量是多少(也就是打字机现在处于这一页的第几格)。如果 Addr == 0,说明刚好在一页的开头(完美对齐)。
count = SPI_FLASH_PageSize - Addr;
翻译:算一下当前这一页,还剩下多少空位可以写。
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
翻译:算一下你给的数据总长,能填满几个完整的整页。
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
翻译:算一下填满整页后,最后还剩下的一条“小尾巴”是几个字节。
这个函数的本质就是‘填坑与翻页’。 假设你现在身处第一页的末尾,还剩 10 个空位(count=10),但你手里有 300 个字节要写。 这个函数的逻辑是:
先调用 PageWrite,把手里的 10 个字节塞进当前的空位,把这一页填满。
此时地址自动对齐到了下一页的开头。
手里还剩 290 个字节。算一下,刚好能填满 1 个整页(256字节)。于是用 while 循环再调用一次 PageWrite 写入 256 字节。
最后剩下一条 34 字节的尾巴(NumOfSingle=34),再调用一次 PageWrite 收尾。 有了这个总监把关,我们在应用层只需要无脑调用 BufferWrite,想写多少写多少,再也不用管什么 256 字节的物理边界了!”
示例
假设你现在身处第一页的末尾,还剩 10 个空位(count=10),但你手里有 300 个字节要写。 这个函数的逻辑是:
先调用 PageWrite,把手里的 10 个字节塞进当前的空位,把这一页填满。
此时地址自动对齐到了下一页的开头。
手里还剩 290 个字节。算一下,刚好能填满 1 个整页(256字节)。于是用 while 循环再调用一次 PageWrite 写入 256 字节。
最后剩下一条 34 字节的尾巴(NumOfSingle=34),再调用一次 PageWrite 收尾。 有了这个总监把关,我们在应用层只需要无脑调用 BufferWrite,想写多少写多少,再也不用管什么 256 字节的物理边界了!”
大家发现没有,读取函数并没有像写入那样去算什么页边界、满不满的问题。 为什么?因为 Flash 的物理结构对‘读操作’没有设限!只要你不拉高 CS 引脚,内部的地址指针就会自动加 1。哪怕你直接让 NumByteToRead = 8388608(8MB),它也会顺畅地把整颗芯片从头到尾给你扫一遍。这就是‘扫描仪’的威力。
void SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead){// 动作 1:拉下开关,告诉 Flash 准备干活FLASH_SPI_CS_LOW();// 动作 2:发送“普通读”指令 (0x03)SPI_FLASH_SendByte(FLASH_CMD_ReadData);// 动作 3:发送 24 位起始地址 (从哪里开始读?)SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16); // 高 8 位SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8); // 中 8 位SPI_FLASH_SendByte(ReadAddr & 0xFF); // 低 8 位// 动作 4:开启“吸尘器”模式,疯狂吸取数据while(NumByteToRead--){*pBuffer = SPI_FLASH_ReadByte(); // 内部在发 0xFF 哑字节换取数据pBuffer++; // 指针后移,准备存下一个字节}// 动作 5:完工,拉高 CS 结束通讯FLASH_SPI_CS_HIGH();}
大家仔细对比一下我们上一节讲的 BufferWrite,写数据的时候,代码长达几十行,要算偏移量、算剩余空间,一旦跨越 256 字节的页边界就得重新发地址。
但是你们看读数据的代码,居然只有一个简单的 while 循环! 它根本不管 256 字节的界限,想读多少就读多少(NumByteToRead 甚至可以填几万)。这是为什么呢?
这就是 Flash 的物理魅力!写数据像用老式打字机,打到纸的边缘(256字节)就会卡死,必须手动换行(重新发地址)。而读数据就像拉开一幅无尽的卷轴,只要你一开始告诉它一个起始地址(动作 3),并且只要 CS 引脚一直保持低电平,Flash 内部的地址指针就会自动 +1、跨页、跨扇区、跨块,畅通无阻!
我们现在用的指令是 0x03(普通读取)。在 W25Q64 的手册里,普通读取的时钟频率是有上限的(通常在 33MHz 甚至更低)。 如果你的 CW32 单片机跑得飞快,把 SPI 时钟设置到了 48MHz 极限狂飙,用这个 0x03 指令读出来的数据可能会错位或者全是乱码!
解决办法: 把 0x03 换成我们在时序图章节讲过的 0x0B(Fast Read,极速读取)。唯一的区别是,发完 24 位地址后,代码里要多发一个字节的 0xFF(8个 Dummy Clocks)给 Flash 留出反应时间,然后才能进入 while 循环去吸取真实数据。
二SDK分析与移植
1.SDK分析
原工程中没有下列程序,需要自己找一个地方加进去
/* 1. 针对 AC6 的禁用半主机指令 */__asm(".global __use_no_semihostingnt");/* 2. 定义标准库需要的支持函数 */#include <stdio.h>/* 这里的 __FILE 结构体在 AC6 下通常不需要手动定义,MicroLIB 会处理 *//* 但为了彻底重定向 printf,我们需要实现底层输出函数 */// 如果你没在其他地方定义 fputc,请加上这段:int fputc(int ch, FILE *f) {// 假设你使用的是 UART1,发送寄存器为 TDR// 这里的具体寄存器名根据 CW32 库文件决定,通常是 CW_UART1->TDR 或类似USART_SendData_8bit(CW_UART1, (uint8_t)ch);while (USART_GetFlagStatus(CW_UART1, USART_FLAG_TXE) == RESET);return ch;}/* 3. 定义半主机依赖的底层存根函数 */void _sys_exit(int x) {x = x;while (1); // 报错后死循环}void _ttywrch(int ch) {ch = ch;}
这段代码是嵌入式开发里非常经典的 “printf串口重定向与半主机模式(Semihosting)禁用” 模板。特别是当你从旧版的 Keil AC5 编译器升级到最新的 AC6 编译器 时,这段代码是必须要有的“护身符”。
如果在代码里调用了 printf(),但不加这段程序
这段代码是嵌入式开发里非常经典的 “printf串口重定向与半主机模式(Semihosting)禁用” 模板。特别是当你从旧版的 Keil AC5 编译器升级到最新的 AC6 编译器 时,这段代码是必须要有的“护身符”。
如果在代码里调用了 printf(),但不加这段程序,你会遇到两种极其折磨人的报错现象:
现象一:编译直接报错(Linker Error)
如果你不加 _sys_exit 和 _ttywrch 这几个存根函数,同时又在代码里用了标准 C 库函数(没勾选 MicroLIB 的情况下),点击编译时,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 语言的标准库原本是给电脑(Windows/Linux)设计的,当程序出错或者结束时,它会默认去调用操作系统的退出函数(exit)或终端输出函数(ttywrch)。但我们的 CW32 单片机里根本没有操作系统!编译器找不到这些底层函数,就会报“未定义符号”的错误。代码里写死这几个空函数,就是为了骗过编译器:“行了,退出函数我给你准备好了,你别报错了。”
现象二:运行时“拔线死机”(The Silent Killer)
这是最坑、最容易让崩溃的现象。如果你没加 __asm(".global __use_no_semihostingnt"); 这句话,编译可能完全通过,零警告,但一下载到板子上就会出现“灵异事件”:
插着仿真器调试: 代码跑得好好的,printf 的数据能在 Keil 的 Debug 窗口里打印出来。
拔掉仿真器,插充电宝独立供电: 板子死机了!程序卡死在启动阶段,LED 也不闪了,所有任务罢工。
为什么死机?(半主机模式的坑) 半主机模式(Semihosting)是一种调试机制。它会让单片机的 printf 试图通过 JTAG/SWD 仿真器的数据线,把字符传给电脑屏幕。 如果没禁用半主机模式,每次执行 printf 时,单片机内部会触发一条特殊的硬件断点指令(BKPT)来呼叫电脑。当你拔掉仿真器独立运行时,单片机喊破喉咙也没人理它,它就会一直卡在这个断点指令上,导致整个系统彻底死机。
现象三:printf 变成“哑巴”
如果不加 fputc 这个函数:
现象: 编译可以通过,程序也不会死机,但是你的电脑串口助手里收不到任何数据。
为什么?printf 只负责把你要发送的变量转换成字符格式(比如把数字 123 变成字符 '1', '2', '3'),但它不知道这些字符要从单片机的哪个引脚扔出去。 fputc 就是 printf 和 CW32 硬件之间的**“水管接头”**。你在 fputc 里写了 USART_SendData_8bit(CW_UART1, ch),printf 才知道:“哦!原来我要把字符塞进 UART1 的发送寄存器里啊。”
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;// 替换返回类型和内部比较的宏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);//// // 擦除扇区 4KB// SPI_FLASH_SectorErase(FLASH_SectorToEraseAddress);//// // 写数据// SPI_FLASH_BufferWrite(Flash_TxBuffer, FLASH_WriteAddress, BufferSize);// printf("rn尝试写入的数据为: %srn", Flash_TxBuffer);//// // 读数据// SPI_FLASH_BufferRead(Flash_RxBuffer, FLASH_ReadAddress, BufferSize);// printf("rn实际读出的数据为: %srn", Flash_RxBuffer);//// // 检查// TransferStatus = Buffercmp(Flash_TxBuffer, Flash_RxBuffer, BufferSize);// if(TransferStatus == FLASH_PASSED)// {// printf("rnFLASH Success! kunkun 验证通过!rn");// }// else// {// printf("rnFLASH Error 1! 数据不一致!rn");// }//}void flash_fun(void){// --- 步骤 1:读取 ID 确认通信 (保持不变) ---DeviceID = SPI_FLASH_DeviceID();ManufactDeviceID = SPI_FLASH_ManufactDeviceID();JedecID = SPI_FLASH_JedecID();SPI_FLASH_UniqueID(UniqueID);// --- 步骤 2:测试第一个扇区 (0-4KB) ---uint32_t addr1 = 0x0000;SPI_FLASH_SectorErase(addr1); // 擦除第一个 4KBSPI_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 验证通过!正在挑战 Sector 1...");// --- 步骤 3:测试第二个扇区 (4-8KB) ---// 5-8KB 的数据属于第二个 4KB 扇区,起始地址为 0x1000uint32_t addr2 = 0x1000;SPI_FLASH_SectorErase(addr2); // 擦除第二个 4KB 扇区SPI_FLASH_BufferWrite(Flash_TxBuffer2, addr2, 7);printf("rn尝试向 0x1000 写入数据: %s", Flash_TxBuffer2);SPI_FLASH_BufferRead(Flash_RxBuffer2, addr2, 7);printf("rn从 0x1000 实际读出数据: %s", Flash_RxBuffer2);if(Buffercmp(Flash_TxBuffer2, Flash_RxBuffer2, 7) == FLASH_PASSED){printf("rn[Sector 1] zhiyin 验证通过!两个区域均正常!rn");TransferStatus = FLASH_PASSED;}else{printf("rn[Sector 1] zhiyin 失败,请检查地址 0x1000 处的写入。");TransferStatus = FLASH_FAILED;}}else{printf("rn[Sector 0] kunkun 验证失败,请检查底层驱动。");TransferStatus = FLASH_FAILED;}}
#ifndef __FflashhoufunLASHHOUFUN_H#define __FLASHHOUFUN_H#include "cw32f030.h"#include <stdio.h>#define BufferSize 7 // "kunkun" + ''#define FLASH_WriteAddress 0x00000#define FLASH_ReadAddress FLASH_WriteAddress#define FLASH_SectorToEraseAddress FLASH_WriteAddress// 我这里换个专属名字,防止重定义冲突typedef enum {FLASH_FAILED = 0, FLASH_PASSED = !FLASH_FAILED} FlashTestStatus;extern uint8_t Flash_TxBuffer[];extern uint8_t Flash_RxBuffer[];extern uint8_t DeviceID;extern uint16_t ManufactDeviceID;extern uint32_t JedecID;extern uint8_t UniqueID[8];// 使用新的专属类型extern volatile FlashTestStatus TransferStatus;void flash_fun(void);// 函数声明也要换成新的返回类型FlashTestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength);#endif /* __FLASHHOUFUN_H */
#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"// 全局中断标志 (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 器件,严禁接 5V
方案一:独立运行模式(无串口打印)
当你完成调试,准备将网关部署到 500 个节点的现场时,可以撤掉串口模块以精简电路。
| 连接设备 | 设备引脚 | CW32F030 引脚 | 说明 |
| W25Q64 | VCC | 3.3V | 电源 |
| W25Q64 | GND | GND | 电源地 |
| W25Q64 | /CS | PB12 | 软件片选 (CS) |
| W25Q64 | CLK | PB10 | SPI2 时钟 (SCK) |
| W25Q64 | DO (IO1) | PB14 | SPI2 数据输出 (MISO) |
| W25Q64 | DI (IO0) | PB15 | SPI2 数据输入 (MOSI) |
| 其他 | PA08 / PA09 | 悬空 | 串口引脚不接线,代码可保留以防报错 |
方案二:开发调试模式(带串口打印 printf)
原工程就是如此,可以通过串口打印出来信息。
此模式下,你可以通过电脑串口助手查看 Flash 的 ID 识别情况和程序运行状态。
| 连接设备 | 设备引脚 | CW32F030 引脚 | 说明 |
| W25Q64 | VCC | 3.3V | 电源(严禁接 5V) |
| W25Q64 | GND | GND | 电源地 |
| W25Q64 | /CS | PB12 | 软件片选 (CS) |
| W25Q64 | CLK | PB10 | SPI2 时钟 (SCK) |
| W25Q64 | DO (IO1) | PB14 | SPI2 数据输出 (MISO) |
| W25Q64 | DI (IO0) | PB15 | SPI2 数据输入 (MOSI) |
| USB转TTL | RXD | PA08 | 单片机发送 (TX),接模块接收 |
| USB转TTL | TXD | PA09 | 单片机接收 (RX),接模块发送 |
| USB转TTL | GND | GND | 共地(通讯基础) |
扫码加入QQ群3群| 610403240
350