资料下载:
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 往往在时钟停止的那一瞬间就立刻抬起,有时会导致最后一个比特的保持时间不够。
3、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);
}
4、擦与写操作

/** * @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); // 擦除第一个 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 验证通过!正在挑战 Sector 1...");
// --- 步骤 3:测试第二个扇区 (4-8KB) ---
// 5-8KB 的数据属于第二个 4KB 扇区,起始地址为 0x1000
uint32_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;
}
}
#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 | 共地(通讯基础) |


审核编辑 黄宇
-
无线抄表
+关注
关注
0文章
40浏览量
17435 -
CW32
+关注
关注
1文章
323浏览量
1955
发布评论请先 登录
CW32 MCU有哪些系列?
cw32和stm32的区别
cw32和gd32的区别
应用笔记-CW32 自举程序中使用的 ISP 协议
【CW32无线抄表项目】示例通信程序讲解
【CW32无线抄表项目】W25Q_CW32_DMA简介
【CW32无线抄表项目】W25Q+CW32程序示例
评论