扫码加入

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

【CW32无线抄表项目】W25Q_CW32_DMA简介

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

以前单片机搬运数据(比如把串口收到的 100 个字节存进数组),必须由 CPU 亲自动手:读一个字节、存一个字节。搬砖的时候,CPU 没法去算水表的流量,也没法去管 4G 模块。

DMA 就是一个‘专业的搬运工’。 你只要告诉它:‘从哪搬(源)、搬到哪(目的)、搬多少(数量)’,然后 CPU 就可以去喝咖啡了,搬运工作全交给 DMA 硬件电路自动完成。等搬完了,它会敲敲门告诉 CPU:‘老板,活干完了!(中断)”。

阶段一:CW32F030—DMA框图

一共三件事:谁在干活?路怎么走?谁在发号施令?

搬运工小队:DMA 模块 (Ch1 - Ch5)

在图的最左侧,你会看到一个大方框,里面排着 Ch1 到 Ch5。这就是我们提到的 5 条独立“传送带”。 比如 Ch1 的优先级最高。如果你有 5 件事同时发生,DMA 会先紧着 Ch1 的活干。对于我们的项目,我们会占用其中两条来负责 SPI2 的收和发。

交通枢纽:Bus Matrix (总线矩阵)

图中中心位置那个灰色的、带有多个交叉点的长条,这就是总线矩阵。它是单片机里的“立交桥”。

注意图中有两条并行的总线:AHB Bus (CPU) 和 AHB Bus (DMA)。

“大家发现了吗?CPU 和 DMA 各有一条路通往总线矩阵。这意味着:当 DMA 在把数据从 SPI 搬往 SRAM 的时候,CPU 可以走另外一条路去执行别的指令,互不干扰! 这就是为什么用了 DMA 后,单片机会变快的原因——因为有两个‘人’同时在干活。”

货源地与目的地:外设与存储器

看图的最右侧,那一排方块就是数据要去的目的地。

AHB 设备:Flash(存程序)、SRAM(运行数据)、GPIO(点灯/按键)。

APB 设备:通过“AHB to APB 桥接器”连接的各种传感器接口。

避坑点“同一个桥下的设备,CPU 永远比 DMA 更有话语权。” 如果两个人都想访问同一个桥下的东西,DMA 得先给 CPU 让路。

谁在喊“开工”? (DMA Requests)

注意看图左侧那两条细细的箭头:

Software (软件触发):CPU 直接下令:“现在,立刻,把这段数据拷走!”

Hardware (硬件触发):外设(比如 SPI、UART)发信号:“老板,我这儿攒够 1 个字节了,快搬走吧!”

“对于我们的 SPI Flash 搬运,我们看的是下面的‘Hardware’箭头。SPI2 会像敲门一样给 DMA 发请求。”

阶段三:逻辑分类(查阅 8.4 传输模式)

逻辑分类——掌握 DMA 的 “搬运套路”

开关 A:DMA_TRIGy.TYPE —— 谁在喊“开工”?

软件触发 (TYPE=0)

话术:这就好比你亲自给搬运工打电话:“喂,把这叠文件复印一下。”主要用于内存到内存的数据拷贝。

硬件触发 (TYPE=1)

话术:这就好比你在流水线上装了个感应器。SPI 接收到一个字节,感应器就响一下,搬运工才动一下。

开关 B:DMA_CSRy.TRANS —— 搬运时的优先级(重点!)

BULK 模式 (大批量传输, TRANS=0)

特点霸道、一次性干完、不许插队。

比喻:就像一辆满载的渣土车冲上高速,谁也不让,直到把货卸完。

风险:如果搬运的数据量非常大(比如几千个字节),DMA 会长时间占用总线,导致 CPU 没法访问内存,你的程序看起来就像“假死”了一样。

BLOCK 模式 (块传输, TRANS=1)

特点礼貌、允许插队、每搬一个就问一下。

比喻:就像一个礼貌的快递员,每过一个路口(搬完一个数据块)都会停下来看看有没有救护车(高优先级的 CPU 指令或 DMA 通道)要先过。如果有,他先让路。

优点:系统响应速度快,不会让 CPU 闲着。

在我们的水表网关项目中,我们应该选择 ‘硬件触发 BLOCK 传输模式’

为什么?

因为我们需要配合 SPI2 的接收/发送信号(硬件触发)。

因为我们要跑 FreeRTOS。FreeRTOS 要求任务切换非常快,如果我们用 BULK 模式一次性搬运几百个字节,可能会导致 FreeRTOS 的高优先级任务被‘堵在路口’进不来。用 BLOCK 模式,能保证我们的 CPU 随时能处理突发的 4G 通信或无线中断。”

我们的 CW32 只有 8KB RAM,CPU 和 DMA 共享这条窄窄的内存通道。如果 DMA 用 BULK(无间隙) 模式死死占住总线,CPU 就没法从 RAM 里读取 FreeRTOS 的任务指令,你的系统就会出现微小的“卡顿”。而图中的 BLOCK 模式,利用这些微小的间隙,让 CPU 和 DMA 实现了真正的并行工作

起跑:SOFTSRC 信号的“发令枪”

硬件表现:看最上面那根线 DMA_TRIGy.SOFTSRC。当 CPU 在寄存器里把这一位写为 1 时,电平瞬间拉高。

分析:这就是所谓的“软件触发”。不需要外设(如串口、SPI)给信号,CPU 说“开始”,DMA 立即起跑。

呼吸:传输间隙(The Breather)

这是 BLOCK 模式最核心的硬件特征:

硬件表现:观察“传输间隙”和“数据传输时隙”。你会发现,DMA 并不是一鼓作气把所有数据搬完的,它每搬运一个数据(比如从 SA 搬到 DA),就会停顿一下(传输间隙)。

为什么这么设计?“这就好比一个有礼貌的搬运工。他每搬一箱货,都会在路口停半秒,看看有没有救护车(CPU 发出的紧急指令)要过。如果有,他就让路,等救护车走了再搬下一箱。这就是 BLOCK 模式比 BULK 模式‘讲理’的地方。”

关联硬件:这个“停顿”就是手册里提到的仲裁机制

算账:地址与计数的“自动加减法”

看图底部的三组波形,这是 DMA 的“核心算法”在硬件上的体现:

计数器倒计时 (CNT):每经过一个传输时隙,CNT 的值就从 3 变成 2,再变成 1。当变为 0 时,DMA 任务结束,自动停止。

地址自增 (SRCADDR / DSTADDR)

细节分析:图中显示地址从 SA变成了 SA+2,再变成 SA+4。

教学点“大家注意这个 +2!这意味着我们现在搬运的数据宽度是 16 位(半字)。如果是 8 位(字节)搬运,这里就是 +1;如果是 32 位(字)搬运,这里就是 +4。DMA 硬件会自动根据你的设置来计算下一个目的地在哪,完全不用你操心。”

信号驱动:从“发令枪”变成“敲门声”

波形观察:看第一行 硬件触发请求信号。它不再是一个长电平,而是一个个短脉冲

硬件分析:这些脉冲就是在 表 8-2 里选的那些“频道”发出来的。

比喻“比如你正在用 SPI 读 Flash。SPI 硬件每收到一个字节,就会‘敲一下门’(发一个脉冲信号)。DMA 听到敲门声,才动一下,搬一个字节。这就是所谓的硬件同步。”

一步一停:受控的传输间隙

波形观察:注意看 传输间隙。在硬件触发模式下,间隙的时间长短不由 DMA 决定,而是由外设决定

要点:如果 SPI 跑得慢,两个脉冲之间的距离就大,间隙就长;如果 SPI 跑得快,间隙就短。

对 FreeRTOS 的意义:由于存在这些间隙,CPU 始终有机会插手总线。即使你在搬运 500 个节点的大数据,系统也不会因为 DMA 占线而导致任务切换卡顿。

经典的 外设 -> 内存 地址配置

观察最下面的 SA(源地址)和 DA(目的地址)波形,这是教科书级的配置案例:

源地址 (SA) 自动自增

现象:图中 DMA_SRCADDRy 随传输进程从 SA 累加。

原理:我们要把 RAM 数组里准备好的数据包按顺序取出来,所以地址必须自增。

目的地址 (DA) 保持不变

现象:图中 DMA_DSTADDRy 始终固定在 DA。

原理:数据被搬运到了外设的发送寄存器(如串口发送口)。外设寄存器地址是固定的,这就是所谓的“单点取货口”。

软件触发:通常用于“内存 -> 内存” (M2M)

对应的就是图 8-2。

场景:把数组 A 的数据拷贝到数组 B。

逻辑:源头是内存(自增),目的地也是内存(自增)。

结果:你看到 SA和 DA 都在同步增长(SA+1, DA+1...)。

硬件触发:通常用于“内存 -> 外设” (M2P) 或 “外设 ->内存” (P2M)

对应你看到的图 8-5(发送场景)。

场景:把 RAM 里的数据包通过串口发出去。

逻辑:源头是内存(自增),目的地是串口发送寄存器(固定)。

结果:你看到 SA 在增长,而 DA 像根横线一样死死不动。

三、 如果换成“硬件触发读取”,DA 也会加!

想象一下:如果你现在用 DMA 接收串口数据(P2M),情况就反过来了:

源地址 (SA):串口接收寄存器  固定(守株待兔)。

目的地址 (DA):RAM 接收数组  自增(按顺序排队)。

结论:在硬件触发的接收模式下,DA 是会自增的。所以,地址动不动,完全取决于你搬运的目标是“一个点”还是“一排位置”。

阶段四:进阶技能(重点看 8.6 DMA 中断)

大家想象一下,DMA 在后台帮我们搬运 512 字节的水表数据。CPU 不可能一直盯着它看(轮询),那样太累了。 我们希望:DMA 搬完了,或者搬运出错了,能主动‘敲敲 CPU 的门’(触发中断),告诉我们结果。这就是手册 8.6 节讲的内容。

标准动作:中断的“三步走” (对照表 8-4)

很多初学者写完中断函数发现只进去了一次,程序就死机了。这是因为没搞清楚表 8-4 里的 “SOP(标准作业程序)”

第一步:开门 (使能)

在初始化代码里,你需要把开关拨到 1

想让它搬完就报信?设置 TCIE = 1(Transfer Complete Interrupt Enable)。

想让它出错就报警?设置 TEIE = 1(Transfer Error Interrupt Enable)。

第二步:进屋 (中断服务程序)

当信号触发,CPU 会自动跳进 DMA_IRQHandler(中断函数)。

第三步:打扫卫生 (清除标志位) —— 极其关键!

看表  最右边一列:“设置 DMA_ICR.TCy 为 0”

“这就好比敲门。DMA 把门敲响了,如果你进屋处理完事情,不把那个门铃关掉(清标志位),CPU 就会认为门一直在响,从而陷入死循环,500 节点程序就卡死在这儿了!”

FreeRTOS 实战:如何让 8KB RAM 飞起来?

在网关项目中,不需要在中断函数里写复杂的逻辑。用 “二值信号量 (Binary Semaphore)”

实战逻辑:

任务 A (读水表):配置好 DMA,开启中断,然后调用 xSemaphoreTake(xSem_DMA, portMAX_DELAY)。此时任务 A 进入阻塞(睡眠)状态,完全不占 CPU。

DMA 搬运:硬件自动搬运,CPU 在跑别的任务(比如 4G 通讯)。

DMA 中断:搬完了,进入 DMA_IRQHandler

释放信号量:在中断里清除标志位,执行 xSemaphoreGiveFromISR(xSem_DMA)

唤醒:任务 A 瞬间醒来,处理数据。

初始状态xSemaphoreCreateBinary 创建出来的信号量默认就是 0。所以任务执行到 xSemaphoreTake 时肯定会卡住,这正是我们想要的——让它等中断。

FromISR 的重要性:在中断里严禁使用xSemaphoreGive。必须带 FromISR,否则会导致系统内核崩溃。

portYIELD_FROM_ISR:这一行非常关键。如果没有它,虽然信号量给了,但系统可能要等到下一个“时钟滴答(Tick)”才会切换任务。加上它,任务 A 就能**“秒醒”**。

#include "FreeRTOS.h"#include "task.h"#include "semphr.h"#include "cw32f030_dma.h"#include "cw32f030_uart.h"// 1. 定义信号量句柄SemaphoreHandle_t xSem_DMA_Complete = NULL;// 模拟数据缓冲区uint8_t MeterDataBuffer[64];// ==========================================// 任务 A:水表数据处理任务// ==========================================void Task_WaterMeter(void *pvParameters) {    // 创建二值信号量(初始为 0,即“没票”)    xSem_DMA_Complete = xSemaphoreCreateBinary();    for(;;) {        // --- 第一步:配置并启动 DMA ---        // 假设这里配置 DMA 从 SPI (PAN3031) 搬运数据到 MeterDataBuffer        // DMA_Cmd(DMA_CH1, ENABLE);         // --- 第二步:阻塞等待信号量 ---        // portMAX_DELAY 表示一直等,直到 DMA 中断把信号量交出来        // 此时任务进入“阻塞态”,CPU 会自动去跑 4G 任务,完全不浪费电力        if(xSemaphoreTake(xSem_DMA_Complete, portMAX_DELAY) == pdPASS) {
            // --- 第五步:任务唤醒,处理数据 ---            // 当代码运行到这里,说明 DMA 已经搬完了!            printf("DMA 搬运完成,开始解析水表数据...n");            // 处理 MeterDataBuffer...        }    }}// ==========================================// DMA 中断服务函数 (硬件自动触发)// ==========================================void DMA_CH1_IRQHandler(void) {    BaseType_t xHigherPriorityTaskWoken = pdFALSE;    // --- 第三步:检查并清除标志位 ---    if (DMA_GetITStatus(DMA_IT_TC1)) {        DMA_ClearITPendingBit(DMA_IT_TC1); // 必须手动清零,否则会死循环        // --- 第四步:释放信号量唤醒任务 ---        // 注意:中断里必须使用 FromISR 后缀的函数        xSemaphoreGiveFromISR(xSem_DMA_Complete, &xHigherPriorityTaskWoken);        /*当你在中断里调用 xSemaphoreGiveFromISR 时,        FreeRTOS 内核会做一件重要的事情:检查被唤醒的任务(任务 A)的优先级。        它的逻辑非常简单:“如果标志位是 TRUE,就手动触发一次任务调度(Context Switch)。”
        如果没有这一行代码,会发生什么?        中断结束,CPU 返回刚才被中断的任务继续跑。        任务 A 虽然醒了(处于就绪态),但必须等到下一个“系统时钟滴答(Tick)”或者当前任务主动放弃         CPU 时,内核才会发现任务 A 优先级更高并进行切换。        这会带来 毫秒级 的延迟,对于高速通讯来说可能导致数据溢出
        // 如果唤醒的任务优先级更高,立即进行一次“任务切换”        // 这样任务 A 能在中断结束的一瞬间就开始干活,没有延迟*/
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);    }}// ==========================================// 任务 B:4G 通讯任务 (模拟 CPU 忙碌)// ==========================================void Task_4G_Comm(void *pvParameters) {    for(;;) {        // 这里的逻辑在任务 A 睡觉时依然在运行        // 比如不停地查询 4G 模块状态、心跳包等        printf("4G 任务正在运行,不影响水表读取...n");        vTaskDelay(pdMS_TO_TICKS(1000));    }}int main(void) {    // 基础硬件初始化...
    // 创建任务    xTaskCreate(Task_WaterMeter, "Meter", 256, NULL, 3, NULL); // 优先级高    xTaskCreate(Task_4G_Comm,     "4G",    256, NULL, 2, NULL); // 优先级中    vTaskStartScheduler(); // 启动调度器    while(1);}
特性 二值信号量 (Binary) 计数型信号量 (Counting)
数值范围 0 或 1 0 到 最大值 (用户设定)
核心功能 同步 (Synchronization) 资源管理 (Resource Management)
通俗理解 “一个通知”:要么来了,要么没来 “一堆门票”:还剩几张,被领了几张
典型关系 1 对 1 1 对 多(或 N 次事件)

二值信号量:最强“同步闹钟”

正如 DMA 实战逻辑里写的,它最大的作用是“等一个信号”。

适用场合:单路硬件外设完成、单一事件触发。

网关项目举例: 你的 4G 模块发送。任务 A 调用 AT+CIPSEND 后就 xSemaphoreTake 睡死。当串口 DMA 真正搬运完最后一个字符并收到 4G 模块的 SEND OK 后,中断给出一个信号量。任务 A 瞬间醒来确认结果。

特点:任务只关心“发没发完”,不关心发了多少次。

计数型信号量:高效“资源管家”

场景:假设你的网关有 3 个串口(UART1, 2, 3) 都可以用来上报数据,但你只想同时开启 2 个以节省功耗。

用法:创建一个最大值为 2 的计数信号量。

任务 1 想发数据:Take 掉一个(剩 1)。

任务 2 想发数据:Take 掉一个(剩 0)。

任务 3 想发数据:发现没票了,在门口排队(阻塞)。

任务 1 发完:Give 回一张票(剩 1),任务 3 立即拿票进场。

还有一个隐藏选手:互斥量 (Mutex)

互斥量 (Mutex):带有“优先级继承”机制。

适用场合:保护共享资源(比如 500 个节点共用的那个 WaterPulseCount 全局大变量)。

区别:二值信号量通常由“外设给,任务拿”;互斥量则是“任务 A 拿,任务 A 释放”。

阶段五:实战落地(查阅 8.7 & 8.8 寄存器列表)

一、 核心配置:决定“从哪搬、搬多少”

这三个寄存器是所有 DMA 传输的基石,少一个都跑不起来。

寄存器名称 教学重点 为什么重要
DMA_SRCADDRy 源地址:货在哪里? 告诉 DMA 数据的起始物理地址。
DMA_DSTADDRy 目的地址:往哪搬? 告诉 DMA 数据存放在哪里。
DMA_CNTy 传输数量:搬多少次? 重点提醒:它是个“倒计时器”,每搬一个,值减 1,减到 0 就停。

 

二、DMA_CSRy (控制及状态寄存器) —— 全场最核心

这是 DMA 通道的“大脑”。重点介绍其中的这几个位:

EN (位 0):总开关。配完所有参数,最后再点火。

SIZE (位 7:6):你是搬 8 位的字节、还是 32 位的字?(对应你的水表数据结构)。

SINC / DSTINC (位 4 / 5):地址要不要自增?(回忆我们之前分析的:外设不加,内存加)。

TRANS (位 3):选择 BLOCK 还是 BULK 模式。

三、DMA_TRIGy (触发源控制寄存器) —— 全场最核心

TYPE (位 0):你是要“软件点火”还是“硬件敲门”?

HARDSRC (位 7:2):也就是那张 表 8-2 里的频道号。比如要用 SPI2 搬运,这里就得填入对应的编码。

张表非常规律,从位 0 到位 17,其实就是 5 组相同的信号。每一路通道(Channel)都配有两个“灯”:

灯 A:TC (Transfer Complete) —— 传输完成标志

位地址:位 0, 4, 8, 12, 16(分别对应通道 1, 2, 3, 4, 5)。

含义:这是“绿灯”。当 TC = 1 时,表示这一批次的数据(你在 CNT 寄存器里设定的数量)已经全部安全送达目的地。

实战逻辑:在你的 500 节点项目中,当读取 Flash 的 DMA 完成时,你会看到这个位变 1。

灯 B:TE (Transfer Error) —— 传输错误标志

位地址:位 1, 5, 9, 13, 17(分别对应通道 1, 2, 3, 4, 5)。

含义:这是“红灯”。正常情况下它应该是 0。如果变成 1,说明出事了!

报错原因:通常是地址指错了(越界)或者总线访问冲突。如果看到这个位亮了,就要去检查 SRCADDR 和 DSTADDR 有没有写对。

通道编号 成功看这里 (TC) 失败看这里 (TE)
通道 1 位 0 位 1
通道 2 位 4 位 5
通道 3 位 8 位 9
通道 4 位 12 位 13
通道 5 位 16 位 17

大家看,这寄存器就像一排指示灯。如果你用的是通道 2(CH2)来传串口数据,你就盯着第 4 位看。一旦第 4 位变成了 1,就说明你的数据已经躺在内存里等着你处理了!

注意

DMA_ISR 是“只读”的

如果你在中断服务程序里发现了 TC1 = 1,处理完业务后,你必须去 DMA_ICR(清除寄存器) 对应的位写一个 0。

后果警告:如果你不跑去 DMA_ICR 清除这个标志,DMA_ISR 里的那个 1 就会一直亮着。单片机会认为“怎么还没处理完?”,然后疯狂地重复进入中断,直到你的程序死机

为什么我们要学 DMA_ISR?因为在 FreeRTOS 里,我们不希望 CPU 浪费时间去等。我们让任务去‘睡觉’(挂起),等 DMA 搬完砖,硬件会自动点亮 DMA_ISR 里的 TC 灯,触发中断。中断里看一眼这个灯,确定没问题了,再发个信号量把任务叫醒。这种‘事毕报信’的机制,才是单片机在 8KB RAM 下还能高效运行的秘密

在中断服务函数(ISR)中干的事:

动作:快进快出。

职责 1:清除硬件中断标志位(灭灯)。

职责 2:释放信号量或发送任务通知(报信)。

职责 3:如果唤醒的任务优先级更高,触发任务切换(上下文切换)。

在任务(Task)中干的事:

动作:等待信号量(睡觉)。

职责 1:拿到信号量后,处理真正耗时的业务逻辑(如解析数据包、存入 Flash、计算校验和)。

职责 2:处理完后重新配置下一轮的 DMA。

四、DMA_ICR中断标志清除寄存器 (Interrupt Clear Register)

寄存器全称:中断标志清除寄存器 (Interrupt Clear Register)

它的核心作用:当 CPU 处理完 DMA 的中断任务后,必须通过这个寄存器告诉硬件:“我已经知道了,你可以把报告撤下去了。”

物理联系:它和 DMA_ISR 是一一对应的。ISR 负责告状,ICR 负责撤诉。

权限密码:为什么是 R1W0

这张表里有一个非常独特的权限说明:R1W0

官方解释

R1 (Read 1):无论当前中断是否发生,你用代码去读这个寄存器,它吐给你的数据全是 1(即 0xFFFFFFFF)。

W0: 清除通道标志W1: 无功能

大白话翻译:这是一个“写 0 触发”的机关。

如果你想清除通道 1 的完成标志(TC1),你得往位 0 写一个数字 0

如果你往这里写 1,硬件会当没看见,什么都不会发生。

提醒:这和很多单片机(如 STM32)“写 1 清零”的习惯正好相反。

扫码加入QQ群3群| 610403240

相关推荐

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

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