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

news2026/4/3 14:32:04
资料下载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 个空位count10但你手里有 300 个字节要写。 这个函数的逻辑是先调用PageWrite把手里的 10 个字节塞进当前的空位把这一页填满。此时地址自动对齐到了下一页的开头。手里还剩 290 个字节。算一下刚好能填满 1 个整页256字节。于是用while循环再调用一次PageWrite写入 256 字节。最后剩下一条 34 字节的尾巴NumOfSingle34再调用一次PageWrite收尾。有了这个总监把关我们在应用层只需要无脑调用BufferWrite想写多少写多少再也不用管什么 256 字节的物理边界了”示例假设你现在身处第一页的末尾还剩 10 个空位count10但你手里有 300 个字节要写。 这个函数的逻辑是先调用PageWrite把手里的 10 个字节塞进当前的空位把这一页填满。此时地址自动对齐到了下一页的开头。手里还剩 290 个字节。算一下刚好能填满 1 个整页256字节。于是用while循环再调用一次PageWrite写入 256 字节。最后剩下一条 34 字节的尾巴NumOfSingle34再调用一次PageWrite收尾。有了这个总监把关我们在应用层只需要无脑调用BufferWrite想写多少写多少再也不用管什么 256 字节的物理边界了”大家发现没有读取函数并没有像写入那样去算什么页边界、满不满的问题。 为什么因为 Flash 的物理结构对‘读操作’没有设限只要你不拉高 CS 引脚内部的地址指针就会自动加 1。哪怕你直接让NumByteToRead 83886088MB它也会顺畅地把整颗芯片从头到尾给你扫一遍。这就是‘扫描仪’的威力。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换成我们在时序图章节讲过的0x0BFast Read极速读取。唯一的区别是发完 24 位地址后代码里要多发一个字节的0xFF8个 Dummy Clocks给 Flash 留出反应时间然后才能进入while循环去吸取真实数据。二、SDK分析与移植1.SDK分析原工程中没有下列程序需要自己找一个地方加进去/* 1. 针对 AC6 的禁用半主机指令 */ __asm(.global __use_no_semihosting\n\t); /* 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_semihosting\n\t);这句话编译可能完全通过零警告但一下载到板子上就会出现“灵异事件”插着仿真器调试代码跑得好好的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 \0 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(\r\n尝试写入的数据为: %s\r\n, Flash_TxBuffer); // // // 读数据 // SPI_FLASH_BufferRead(Flash_RxBuffer, FLASH_ReadAddress, BufferSize); // printf(\r\n实际读出的数据为: %s\r\n, Flash_RxBuffer); // // // 检查 // TransferStatus Buffercmp(Flash_TxBuffer, Flash_RxBuffer, BufferSize); // if(TransferStatus FLASH_PASSED) // { // printf(\r\nFLASH Success! kunkun 验证通过\r\n); // } // else // { // printf(\r\nFLASH Error 1! 数据不一致\r\n); // } //} 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(\r\n[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(\r\n尝试向 0x1000 写入数据: %s, Flash_TxBuffer2); SPI_FLASH_BufferRead(Flash_RxBuffer2, addr2, 7); printf(\r\n从 0x1000 实际读出数据: %s, Flash_RxBuffer2); if(Buffercmp(Flash_TxBuffer2, Flash_RxBuffer2, 7) FLASH_PASSED) { printf(\r\n[Sector 1] zhiyin 验证通过两个区域均正常\r\n); TransferStatus FLASH_PASSED; } else { printf(\r\n[Sector 1] zhiyin 失败请检查地址 0x1000 处的写入。); TransferStatus FLASH_FAILED; } } else { printf(\r\n[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 引脚说明W25Q64VCC3.3V电源W25Q64GNDGND电源地W25Q64/CSPB12软件片选 (CS)W25Q64CLKPB10SPI2 时钟 (SCK)W25Q64DO (IO1)PB14SPI2 数据输出 (MISO)W25Q64DI (IO0)PB15SPI2 数据输入 (MOSI)其他PA08 / PA09悬空串口引脚不接线代码可保留以防报错方案二开发调试模式带串口打印printf原工程就是如此可以通过串口打印出来信息。此模式下你可以通过电脑串口助手查看 Flash 的 ID 识别情况和程序运行状态。连接设备设备引脚CW32F030 引脚说明W25Q64VCC3.3V电源严禁接 5VW25Q64GNDGND电源地W25Q64/CSPB12软件片选 (CS)W25Q64CLKPB10SPI2 时钟 (SCK)W25Q64DO (IO1)PB14SPI2 数据输出 (MISO)W25Q64DI (IO0)PB15SPI2 数据输入 (MOSI)USB转TTLRXDPA08单片机发送 (TX)接模块接收USB转TTLTXDPA09单片机接收 (RX)接模块发送USB转TTLGNDGND共地通讯基础

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2479133.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…