以前单片机搬运数据(比如把串口收到的 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
166