扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

【CW32无线抄表项目】W25Q+CW32程序示例

03/23 10:21
350
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

资料下载:

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);  // 擦除第一个 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;    }}
#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

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

以开放、共享、互助为理念,致力于构建武汉芯源半导体CW32系列MCU生态社区。无论是嵌入式MCU小自还是想要攻破技术难题的工程师,亦或是需求解决方案的产品经理都可在CW32生态社区汲取营养、共同成长。