0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

【CW32无线抄表项目】单片机SPI+DMA读写Flash(W25Q)保姆级避坑指南

CW32生态社区 来源:CW32生态社区 2026-04-02 16:35 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

在用 SPI 读写 Flash(比如 W25Q 系列)时,往往会觉得用 CPU 一个字节一个字节地收发太慢了。于是大家都会想到用 DMA(直接内存访问) 这个“搬运工”来代劳。

但是!当你满怀信心地配置好 DMA,一跑程序,往往会绝望地卡死在 while(dma_done == 0); 里面。今天,我们就用一段极简的测试代码(往 Flash 里写一个 "kunkun" 并读出来),手把手教你如何完美打通 SPI 和 DMA 的任督二脉!

核心思维预警:SPI 和 DMA 是怎么配合的?

SPI 的全双工脾气:SPI 就像一个双向传送带。你发一个字节出去,必然会同时收一个字节回来。必须有发才有收。

DMA 的搬运工角色:我们通常需要雇佣两个 DMA 搬运工。一个叫 TX(发送通道),负责把内存里的数据疯狂塞给 SPI;另一个叫 RX(接收通道),负责把 SPI 收到的数据搬回内存。

第一步:准备好你的“停车场”(内存对齐)

// 【关键】:定义真正的内存空间,并强制 4 字节对齐
__attribute__((aligned(4))) uint8_t CW_DMA_TxBuf1[256] = "kunkun"; 
__attribute__((aligned(4))) uint8_t CW_DMA_RxBuf1[256];

注意: DMA 搬运数据速度极快,但它有个小怪癖——喜欢整齐的地址。加上 attribute((aligned(4))) 就是告诉编译器:“请把这两个数组放在能被 4 整除的内存地址上”。如果不加,有时候硬件在寻址时可能会报错或者发生数据偏移。

C 语言中内存对齐(结构体)

struct MyData {
    int a;    // 4 字节
    int b;    // 4 字节
    char c;   // 1 字节
};

它的内存布局就像这样:

第 0-3 字节:放 int a,完美填满一排。

第 4-7 字节:放 int b,完美填满第二排。

第 8 字节:放 char c,它只占了第三排的第一个座位。

第 9-11 字节: CPU 是个“强迫症”,它要求下一个结构体(如果你定义一个数组的话)必须从新的一排(4 的倍数地址)开始。为了保证这种整齐,它在 char c 后面塞了 3 个字节的废话(Padding)。

所以:9 (有效)+ 3 (垫片) = 12 字节。

#pragma pack(1)
struct MyData {
    int a;
    int b;
    char c;
};
#pragma pack() // 用完记得关掉,否则会影响后面的代码
//缺点:CPU 访问 a 和 b 可能会变慢一点点,
//因为地址可能不再是 4 的倍数,CPU 甚至需要分两次读取再拼接(这叫非对齐访问)。

第二步:配置 DMA 搬运工的“打卡机”(中断配置)

void NVIC_Configuration(void){
    __disable_irq(); 
    NVIC_ClearPendingIRQ(DMACH23_IRQn);
    NVIC_SetPriority(DMACH23_IRQn, 1); // 建议设个优先级
    NVIC_EnableIRQ(DMACH23_IRQn); 
    __enable_irq();  
}

/* 定义一个全局标志位,告诉主程序:搬完了! */
volatile uint8_t g_dma_done = 0; // 全局标志位
void DMACH23_IRQHandler(void)
{
    // 检查通道 2 (RX) 是否完成(通常以 RX 完成为准,因为 RX 结束代表总线时钟已全部跑完)
    if (DMA_GetITStatus(DMA_IT_TC2))
    {
        DMA_ClearITPendingBit(DMA_IT_TC2);
        g_dma_done = 1; // 竖起旗子
    }
    // 清理通道 3 (TX) 标志位
    if (DMA_GetITStatus(DMA_IT_TC3))
    {
        DMA_ClearITPendingBit(DMA_IT_TC3);
    }

    // 错误处理
    if (DMA_GetITStatus(DMA_IT_TE2) || DMA_GetITStatus(DMA_IT_TE3))
    {
        DMA_ClearITPendingBit(DMA_IT_TE2 | DMA_IT_TE3);
        Error_Handle();
    }
}

注意:搬运工(DMA)干完活总得跟老板(CPU)汇报一下吧?这段代码就是给系统注册了一个“微信提示音”。当 DMA 搬完 6 个字节的 "kunkun" 时,它会触发中断,把我们代码里的 g_dma_done 标志位置为 1,这样我们的 while 死循环就能冲过去了。

第三步:重头戏!初始化 SPI 和 DMA

这段 SPI2_DMA_Init 初始化代码里,藏着几个最容易让人抓狂的致命地雷,我们已经全部扫清了:

void SPI2_DMA_Init(void){
    // ... (变量声明省略) ...//  避坑 1:一定要给外设通电!
    __RCC_SPI2_CLK_ENABLE();
    RCC_AHBPeriphClk_Enable(RCC_AHB_PERIPH_DMA, ENABLE);

如果不打开 SPI 的时钟,SPI 就等于没插电,你后面写的所有寄存器配置都会像扔进黑洞一样毫无反应。

  //  避坑 2:找对收发货的“物理地址”// 【RX 接收通道配置】
    DMA_InitStruct.DMA_SrcAddress = (uint32_t)&CW_SPI2->DR; // 收货地:SPI 的数据寄存器
    DMA_InitStruct.DMA_DstAddress = (uint32_t)CW_DMA_RxBuf1;   // 卸货地:我们的内存数组
    DMA_InitStruct.DMA_DstInc = DMA_DstAddress_Increase;       // 卸货时地址要递增,依次排好排满
    DMA_InitStruct.HardTrigSource = 33; // 告诉搬运工,听 SPI2_RX 的哨声// 【TX 发送通道配置】
    DMA_InitStruct.DMA_SrcAddress = (uint32_t)CW_DMA_TxBuf1;   // 收货地:我们的 "kunkun" 数组
    DMA_InitStruct.DMA_SrcInc = DMA_SrcAddress_Increase;       // 拿货时挨个字母拿
    DMA_InitStruct.DMA_DstAddress = (uint32_t)&CW_SPI2->DR; // 卸货地:SPI 的数据寄存器
    DMA_InitStruct.HardTrigSource = 37; // 告诉搬运工,听 SPI2_TX 的哨声

wKgZO2nLz0SAPLKVAAC8kmHbP0g098.jpg

图片

图片

图片

找到“店名”(触发源编号 Index)

看你第一张图:

001000:这是二进制的 8。手册规定,这是 SPI2 接收店的“店号”。

001001:这是二进制的 9。手册规定,这是 SPI2 发送店的“店号”。

找到“打卡方式”(位域分配)

看你第二张图(DMA 触发寄存器位域描述):

第 0 位 (TYPE):设置为 1 才能开启“硬件触发模式”。如果是 0,搬运工就不听 SPI 的哨声了。

第 5 ~ 2 位 (HARDSRC):手册规定,这 4 位是用来填“店号”的。

现场算账(公式推导)

因为“店号”要填在从 第 2 位 开始的地方,所以我们需要把店号 左移 2 位(相当于乘以 4),然后把 第 0 位 设为 1。

对于 SPI2_RX (接收):

店号:8(二进制 1000)。

填位:把 1000 往左挪两位,变成 1000xx。

加上开关:最后一位(TYPE)填 1,变成 100001。

转换:二进制 100001 就是十进制的 33!

$$8 times 4 + 1 = 33$$

对于 SPI2_TX (发送):

店号:9(二进制 1001)。

填位:把 1001 往左挪两位,变成 1001xx。

加上开关:最后一位(TYPE)填 1,变成 100101。

转换:二进制 100101 就是十进制的 37!

$$9 times 4 + 1 = 37$$

信号名称 原始编号 (Index) 寄存器填法 (二进制) 最终数值
SPI2_RX 8 (1000) 10 0001 33
SPI2_TX 9 (1001) 10 0101 37

很多朋友喜欢自己手算地址,比如写个 0x4000380C。一旦算错哪怕一个字节,DMA 就会把数据搬到错误的地方导致崩溃。用 &CW_SPI2->DR 让编译器去抓取绝对正确的地址,最稳妥!

 //  避坑 3:安全地拨动开关// 先关闭 SPI (SPE=0),确保寄存器可写,防止被硬件锁死
    CW_SPI2->CR1 &= ~(uint32_t)(1 < < 6);       
    CW_SPI2- >CR1 |= (uint32_t)(0x03 < < 16);    // 告诉 SPI:允许你呼叫 DMA!
    CW_SPI2- >CR1 |= (uint32_t)(1 < < 6);        // 重新开启 SPI (SPE=1)
}

SPE=0(熄火):你必须先按下停止键,让机器停下来。否则,为了安全,机器的换挡杆(寄存器)是锁死拔不动的。

设置 DMA(换挡):机器停稳后,你才能把档位拨到“全自动模式(DMA模式)”。

SPE=1(重新启动):接好线、换好挡后,再次合上电源。这时候,机器就会按照你设定的“全自动模式”狂奔了。

如果你跳过第一步直接改,表面上代码写进去了,但实际上机器内部的档位根本没动,这就是为什么很多人程序卡死在 while 里的“灵异”原因。

第四步:写入数据,千万别忘了“清肠胃”!

看 W25Q_DMA_Write_Kunkun 这个写函数,注意中间那段极其特殊的代码:

 // 1. CPU 手动发送指令和地址 (比如 0x02, 还有 24位地址)// ... 省略 ...// 

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    11320

    浏览量

    225834
  • 智能水表
    +关注

    关注

    4

    文章

    218

    浏览量

    24392
  • CW32
    +关注

    关注

    1

    文章

    323

    浏览量

    1955
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

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

    /Armink/SFUD 一、程序分析 硬件总线映射(引脚与时钟的“点”)   #define FLASH_SPIx CW_SPI2// 注意:
    的头像 发表于 03-31 21:29 4913次阅读
    【<b class='flag-5'>CW32</b><b class='flag-5'>无线</b><b class='flag-5'>抄</b><b class='flag-5'>表项目</b>】<b class='flag-5'>W25Q+CW</b>32程序示例

    CW32单片机如何让生活更便捷

    智能调节电量,根据电量情况进行节能策略的调整。例如,降低加热功率或减少冲洗时间等,以此提高电能的使用效率并节约能源。 CW32单片机是一种非常强大的工具,它适用于对FLASH、RAM、GPIO等资源需求
    发表于 12-11 06:11

    STM32F10x_SPI (硬件接口 + 软件模拟)读写Flash25Q16)

    STM32F10x_SPI(硬件接口 + 软件模拟)读写Flash25Q16)
    的头像 发表于 03-25 13:59 1.3w次阅读
    STM32F10x_<b class='flag-5'>SPI</b> (硬件接口 + 软件模拟)<b class='flag-5'>读写</b><b class='flag-5'>Flash</b>(<b class='flag-5'>25Q</b>16)

    单片机汇编读写SPI FLASH的详细资料说明

    本文档的主要内容详细介绍的是单片机汇编读写SPI FLASH的详细资料说明。
    发表于 08-14 10:45 20次下载

    STC8单片机硬件SPI通信例程W25Q16

    。 本篇讲的是使用硬件SPI单片机W25Q16进行通信,模拟SPI通信将会在下一篇讲。使用W25Q16的步骤如下: 1.配置
    发表于 11-18 13:36 71次下载
    STC8<b class='flag-5'>单片机</b>硬件<b class='flag-5'>SPI</b>通信例程<b class='flag-5'>W25Q</b>16

    单片机学习笔记————STM32使用SPI读写串行Flash(二)

    第一步:STM32与Flash的硬件连接单片机型号:STM32F103ZET6Flash型号:W25Q64第二步:配置相关的宏/**************************
    发表于 11-30 17:21 12次下载
    <b class='flag-5'>单片机</b>学习笔记————STM32使用<b class='flag-5'>SPI</b><b class='flag-5'>读写</b>串行<b class='flag-5'>Flash</b>(二)

    STM32入门开发: 介绍SPI总线、读写W25Q64(FLASH)(硬件+模拟时序)

    时序,本文示例代码里同时采用模拟时序和硬件时序两种方式读写W25Q64。模拟时序更加方便移植到其他单片机,更加方便学习理解SPI时序,通用性更高,不分MCU;硬件时序效率更高,每个MC
    发表于 12-02 09:06 41次下载
    STM32入门开发: 介绍<b class='flag-5'>SPI</b>总线、<b class='flag-5'>读写</b><b class='flag-5'>W25Q</b>64(<b class='flag-5'>FLASH</b>)(硬件+模拟时序)

    STM32单片机基础18——使用硬件QSPI读写SPI FlashW25Q64)

    本篇详细的记录了如何使用STM32CubeMX配置STM32L431RCT6的硬件QSPI外设与 SPI Flash 通信(W25Q64)。1. 准备工作硬件准备开发板首先需要准备一个开发板,这里我
    发表于 12-02 10:21 23次下载
    STM32<b class='flag-5'>单片机</b>基础18——使用硬件QSPI<b class='flag-5'>读写</b><b class='flag-5'>SPI</b> <b class='flag-5'>Flash</b>(<b class='flag-5'>W25Q</b>64)

    stm32 cubemx usb spi flash w25q128 u盘调试笔记

    基本代码确定使用需求 USB SPIusb以下配置保持默认配置即可,切记不要胡乱修改参数。spi调试spi flash我使用的flashw25q
    发表于 12-14 18:52 34次下载
    stm32 cubemx usb <b class='flag-5'>spi</b> <b class='flag-5'>flash</b> <b class='flag-5'>w25q</b>128 u盘调试笔记

    STM32 SPI读写W25Q64(三)

    GPIO口模拟SPI读写W25Q64的基本内容已经跟大家介绍完了,今天跟大家介绍下如何通过串口接收文件并保存到W25Q64中。
    发表于 07-22 11:11 3188次阅读
    STM32 <b class='flag-5'>SPI</b><b class='flag-5'>读写</b><b class='flag-5'>W25Q</b>64(三)

    CW32单片机低电压检测器的使用介绍

    CW32单片机低电压检测器的使用介绍
    的头像 发表于 09-18 10:56 2204次阅读
    <b class='flag-5'>CW32</b><b class='flag-5'>单片机</b>低电压检测器的使用介绍

    CW32单片机I2C接口读写EEPROM芯片介绍

    CW32单片机I2C接口读写EEPROM芯片介绍
    的头像 发表于 11-09 17:42 3104次阅读
    <b class='flag-5'>CW32</b><b class='flag-5'>单片机</b>I2C接口<b class='flag-5'>读写</b>EEPROM芯片介绍

    基于CW32单片机做的软硬件开源项目

    今天就再给大家分享一个基于CW32单片机做的软硬件开源项目,其中包括RTOS、GUI、蓝牙、电源管理等众多常用功能。
    的头像 发表于 10-19 10:17 2357次阅读
    基于<b class='flag-5'>CW32</b><b class='flag-5'>单片机</b>做的软硬件开源<b class='flag-5'>项目</b>

    CW32单片机在智能马桶的应用介绍

    和调节。本文将介绍CW32单片机在智能马桶的详细应用。图:CW32的智能马桶控制板CW32单片机在智能马桶的应用介绍1.温度感应与控制智能马
    的头像 发表于 12-20 10:09 1596次阅读
    <b class='flag-5'>CW32</b><b class='flag-5'>单片机</b>在智能马桶的应用介绍

    CW32无线表项目W25Q_CW32_DMA简介

    以前单片机搬运数据(比如把串口收到的 100 个字节存进数组),必须由 CPU 亲自动手:读一个字节、存一个字节。搬砖的时候,CPU 没法去算水表的流量,也没法去管 4G 模块。 DMA 就是一个
    的头像 发表于 03-31 21:41 816次阅读
    【<b class='flag-5'>CW32</b><b class='flag-5'>无线</b><b class='flag-5'>抄</b><b class='flag-5'>表项目</b>】<b class='flag-5'>W25Q_CW32_DMA</b>简介