所以这就是为什么我一直重重复复强调。理论和实践必须相辅相成!缺一不可!
就比如题主说的这个问题,在计组的那堆书里,基本每一本都有讲到DMA方式,几乎每一本都在说DMA数据传输可以独立于CPU来完成,节约了CPU运算资源,提升了整体效率。
但书上没写提升效率大多数情况仅限传输大量需长时等待的数据的情况下!比如你计划传输一大块AD采样数据,比如你想往片外传输一块GRAM,比如你想用SPI从FLASH上读一块sector。
但在微控制领域,使用频率更多的是与外设交互控制数据,这就需要发送一系列命令帧,这类命令帧一般都很短每个可能都只有几个到十几个字节的大小,且要求连续,比如你操作一个外设,可能需要发送以下几段命令帧:
1.使能并初始化,等待几毫秒
2.检查设备的一系列参数,看看符不符合运行条件,可能又是几个毫秒
3.发送一些列配置参数,这段可能会分好几步,每一步可能也是几毫秒
4.操作外设,发送命令
5.取得返回数据。
可能整套流程走下来,也就个十几几十毫秒的长度,后面的命令必须在前面的命令执行完之后,如果你想充分利用这部分的性能,你可能需要写一个状态机来在这方面做状态迁移切换,代码直接大了一倍,还容易写出bug,绝大多数情况,这几十甚至百来毫秒对功能整体并不会有什么影响,所以直接按过程来写,是最简单也是最不容易出错的方式。
那你会问了,那为什么还要用DMA方式来写?
1.最重要当然是写起来简单啊,通讯协议的参数配置一下,拉个寄存器就可以发送了,整个代码量写起来就那么一点,底层还有官方硬件给你背书不容易错,相当于有包给你调,干嘛不用。
2.次要原因是功耗也会稍微低那么一点点,DMA部分有专门的硬件支撑,会稍微能省那么一丢丢的电。
3.部分的协议速度更快。
你看,书上不会告诉你是不是?
毕竟很多要是书上写实际用DMA的最常见目的是因为代码写起来比较偷懒,多影响装逼啊!
================================================
虽然每次我都强调,理论和实践必须相辅相成,但还是很多纸上谈兵并无比坚定认为这样做ok的,所以我每次我还得搬出个实例出来,解释一下题中:"轮询来等dma结束,还有更暴力的直接延时一个固定值"到底是写代码水平不行,还是看代码的"too young",我们以工控行业中非常常见的一种控制协议MODBUS来举这个栗子
一般情况下,我们主要重点关注读和写两种主要交互协议,不论是读还是写,都是一个双向交互的过程,例如,上面是读过程,下面是写过程,都是一个一问一答模式的交互
一般情况下来说,如果不是用MODBUS连续读写一大块的数据,即使是MODBUS ASCII的数据交互也不会太长,因此,一般的伪代码流程如下
1.发送R/W命令
2.*检查数据是否发送完成 (轮询等待发送结束)
3.延迟等待一段时间(可能和步骤2合并,因为通讯协议确定和设备指令执行时间确定后,这个时间基本可以算出来)
4.读取返回结果
我相信上面的这种做法,也正是题目中提到的"轮询或延迟"方法,这是实际项目中非常常见的代码,当然,走不走DMA实现方式都类似。
好了,如果我们以DMA的方式走MODBUS.这个时候,理论党就跳出来了,你这个写法不科学,没有充分利用DMA的优势,延迟等待时间可以去执行其它的任务,我们可以用中断的方式来判断是否命令发完了。
好,这个时候呢,我们听理论党一回,拿中断方式来实现这个过程,那么这个代码逻辑这么写呢?这个时候,本来简单的问题直接上升到程序整体架构的问题,因为发送命令的动作完成后,你无法判断命令是否发送完成,你必须退出这个任务流程,把CPU让给其它任务使用,同时用一个中断来将自己的"发送任务"切换到下一个状态,也就是说,如果你不上OS,很可能你需要写一个状态机来完成这个任务。
不过没关系,我们仍然以裸机嵌入式项目中最常见的大循环+状态机的方式来完成这个任务,伪代码如下
//主循环
while(1)
{
....
其它任务的代码
...
MODBUS任务(待命)
...
其它任务的代码
...
}
我们将这个DMA完成一次MODBUS命令交互的过程,称作MODBUS发送任务,那么,我们要完成这个任务,要怎么做?
设定好DMA参数,发送R/W命令,这个时候,我们要把任务做一个切换,既然是一个FSM方式,那么,当前状态应该是等待数据发送完成,因此,代码变成下面这样
DMA中断完成函数()
{
if(MODBUS任务是等待发送完成)
切换MODBUS任务到等待设备执行
}
//主循环
while(1)
{
....
其它任务的代码
...
MODBUS任务(等待发送完成)
...
其它任务的代码
...
}
因此当数据发送完成以后,程序触发中断,把MODBUS任务切换到下一步等待设备执行,因为命令发过去后,得等待设备执行一段时间,再用DMA收数据
DMA发送完成函数()
{
if(MODBUS任务是等待发送完成)
切换MODBUS任务到等待设备执行
}
//主循环
while(1)
{
....
其它任务的代码
...
MODBUS任务(等待命令执行完成)
...
其它任务的代码
...
}
等待后,就是接收了,这个时候,我们再开一个中断函数
DMA发送完成函数()
{
if(MODBUS任务是等待发送完成)
切换MODBUS任务到等待设备执行
}
DMA接收中断函数()
{
切换MODBUS任务到完成,根据返回结果,可以是成功/收到数据/失败
}
//主循环
while(1)
{
....
其它任务的代码
...
MODBUS任务(接收数据)
...
其它任务的代码
...
}
当然,上面的情况是一切顺利的情况,如果底层设备无响应,你还必须设置一个超时时间,如果超时了,必须将MODBUS任务设置为失败
DMA发送完成函数()
{
if(MODBUS任务是等待发送完成)
切换MODBUS任务到等待设备执行
}
DMA接收中断函数()
{
切换MODBUS任务到完成,根据返回结果,可以是成功/收到数据/失败
}
//主循环
while(1)
{
....
其它任务的代码
...
MODBUS任务(接收数据.超时验证)
...
其它任务的代码
...
}
到这里,只是完成了一条MODBUS命令的交互,是不是看了就头大,一个简单的操作,就涉及到至少5个状态(待命,数据发送中,执行等待中,接受数据,成功/失败)的切换,如果我们直接用轮询或等待方式来写,整个代码体系就简单多了
发送命令帧()
{
设置好寄存器参数,开始DMA发送;
while(发送标志未完成);
等待执行时间;
while(接收数据或未超时);
取得返回参数
根据返回参数返回函数,比如一个bool值,0表示成功,1表示失败,2表示超时
}
并且在实际的应用中,MODBUS的控制命令大多都需要若干条命令来完成,例如,你想将机械杆抬起这一个简单的动作,可以就涉及到
- 先用MODBUS读取当机械状态,判断满不满足机械运行条件,比如设备必须运行正常
- 再用MODBUS读取当机械杆的状态,以判断是否可以执行抬起这个动作
- 再读取当前的一系列控制参数,计算最终的抬起距离
- 发送抬起动作的命令
- 轮询判断动作是否已经完成
如果你用过程化的代码来写,你可以将上面的过程一步一步的写下来,如果你将这个发送的过程使用中断或状态机的异步方式来写,你差不多就可以想象你整个项目架构会复杂到一个什么地步了.其中涉及到的状态迁移,我光想想就觉得头大,毫不夸张的说,你为了利用一些微不足道的性能,将整个项目的复杂度和后期维护难度,推上了几个量级。
最后,说个题外话轮询方式一定比中断方式更有效率么?这个还真不一定,如果你使用状态机的方式进行轮询(如下所示),其性能利用和中断的方式并没有什么区别,并不是说DMA必须要用中断才能利用DMA的性能优势.实际上这种轮询的写法,反而更简单,可读写和后期维护性更强!
if(当前处于发送状态)
{
if(检查标志寄存器是否已经发送完成)
切换到下一个状态
else
return;
}
最后总结一下:
在这类情况利用DMA的优势,仍然是我之前提到的,另外评论中有一个增加整个控制系统的鲁棒性,因为DMA挂掉了,你仍然可以对其重置,这个我觉得也是一个非常到位的原因。
至于中断什么的,这都八竿子打哪去了?
最后来个经验之谈,如果你不明确清楚自己在干什么,这个功能怎么做,慎用异步执行,它很可能会放大整个项目的复杂度与稳定性!!!工程问题不仅仅只是性能利用问题!!!!
你可能又会问了,那有没有什么方式能简单实现功能,又能充分利用其优势呢?,
这个还真有,一个是利用OS,完成这种调度问题.
另一种是利用类似于脚本VM,由虚拟机完成调度.
本质上类似于包装了状态机,简化了代码,以上