文章目录
接线以及实验效果同上一篇一样。并且代码非常简单。主要要理解为什么可以实现自动化转运。
运行过程
为什么要使用DMA
ADC通道外接传感器,使用连续触发扫描模式,则ADC的DR寄存器就会源源不断的接收外部数据。但是我们已经了解规则组不同于注入组,规则组只有一个寄存器,当有数据输入时要及时取走,不然就会覆盖
在这里拓展一个问题,如果在现实场景下,就是会偶尔出现数据覆盖的现象,应该怎么办?这是一个开放性的题,我的想法会放在末尾。
如果使用CPU进行搬运的话,很可能处理不及时,所以需要用DMA来缓解CPU的压力。因为我还没有写有关DMA的文章,我先不过多讲述,按照408操作系统中对DMA的介绍就足够,知道是干什么的就可以了。
为什么需要 DMA?(没有 DMA 会怎样)
在没有 DMA 的传统模式(PIO模式)下,数据的搬运全靠 CPU 亲力亲为。比如串口收到一个字节的数据,CPU 就需要中断当前的工作,把这个字节从外设寄存器"搬"到内存里,然后再回去做原来的事。
如果数据量很大(比如传输高清视频、大文件读写),CPU 就会把大量时间浪费在这些琐碎的"搬砖"工作上,导致系统卡顿、效率低下。
DMA 是如何工作的?
引入 DMA 后,CPU 就可以从繁重的搬运工作中解放出来。DMA 的基本工作流程如下:
CPU 下达指令:CPU 只需要告诉 DMA 控制器三个核心信息:数据从哪里来(源地址)、要搬到哪里去(目标地址)、一共要搬多少(数据长度)。
DMA 独立搬运:DMA 控制器接管总线,开始自动搬运数据。在此期间,CPU 可以完全不受影响地去处理其他复杂的计算或逻辑任务。
完成后汇报:当所有数据搬运完毕后,DMA 会通过一个中断告诉 CPU:"老板,活干完了!"
DMA 的核心优势
解放 CPU:CPU 不再被数据搬运占用,可以专心处理核心业务,极大降低了 CPU 负载。
提升效率:数据传输和 CPU 计算可以并行进行,显著提高了系统的整体吞吐量和响应速度。
实时性强:在处理音频流、传感器高频采样等对时间要求苛刻的场景中,DMA 能保证数据不丢失、处理更及时。
如何实现的自动化
DMA可以自己去目标寄存器和内存中搬运数据,而ADC使用连续触发扫描模式,则是使得ADC可以不断地获取传感器的参数。这就就使得他们达成一种很好的流水线工作模式。
传感器采集外部信号 ------> ADC采集信后存入寄存器中 ------> DMA检测到寄存器中有数据进行数据搬运 ------> 数据寄存器为空又可以接收下一个传感器的数据
所以在代码的实现中,我们起始仅仅需要完成初始化的工作就可以了。
代码实现
c
void Init_DMA(uint16_t *addr, uint8_t buffersize){
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitTypeDef DMA_InitStruct;
// DMA转运数据的长度/次数
DMA_InitStruct.DMA_BufferSize = buffersize;
// 外设寄存器是源地址
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC ;
// ADC1外设到内存
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable ;
// 内存相关设置
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)addr;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
// 进行自增
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
// 循环模式
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
// 设置外设相关
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
// ADC规则通道为16位,所以使用半字转运
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
// 寄存器不自增
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
// 设置优先级
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
DMA_Cmd(DMA1_Channel1, ENABLE);
}
void init_ADC(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
ADC_InitTypeDef ADC_InitStruct;
// 右对齐
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
// 使用软件触发
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
// 独立模式
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
// 使用四个规则组通道
ADC_InitStruct.ADC_NbrOfChannel = 4;
// 扫描模式
ADC_InitStruct.ADC_ScanConvMode = ENABLE;
// 连续模式
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;
// 初始化
ADC_Init(ADC1, &ADC_InitStruct);
ADC_Cmd(ADC1, ENABLE);
// 规则组通道设置
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
// 一些校验工作
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1) == SET);
// 保证软件触发
ADC_ExternalTrigConvCmd(ADC1, DISABLE);
// 连续触发,只需要开启一次
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
// 最后启用DMA转运
ADC_DMACmd(ADC1, ENABLE);
}
uint16_t arr[4];
char str[4][7];
// 转换成电压显示
void Switch_Volt(uint16_t *arr, char str[][7], uint8_t len ){
for(int i = 0; i < len; i++){
sprintf(str[i], " %3.2fV", ((float)arr[i] / 4096.0 * 3.3));
}
}
int main(){
OLED_Init();
Init_DMA(arr, 4);
init_ADC();
OLED_ShowString(1, 1, "PA0:");
OLED_ShowString(2, 1, "PA1:");
OLED_ShowString(3, 1, "PA2:");
OLED_ShowString(4, 1, "PA3:");
while(1){
OLED_ShowUnsignedNum(1, 5, arr[0], 4);
OLED_ShowUnsignedNum(2, 5, arr[1], 4);
OLED_ShowUnsignedNum(3, 5, arr[2], 4);
OLED_ShowUnsignedNum(4, 5, arr[3], 4);
Switch_Volt(arr, str, 4);
OLED_ShowString(1, 9, str[0]);
OLED_ShowString(2, 9, str[1]);
OLED_ShowString(3, 9, str[2]);
OLED_ShowString(4, 9, str[3]);
}
}
拓展的问题
如果在现实场景下,就是会偶尔出现数据覆盖的现象,应该怎么办?
分析
出现数据覆盖现象,也就是说明接收方数据来不及接收数据,那这个时候应该怎么做?
我的解决方式
- 丢弃数据,等待CPU处理完进行重传
- 设置缓冲队列或多级缓冲队列进行数据接收(保证数据完整性)
- 可以容许进行数据覆盖
- 阻塞发送,等待空闲再发送数据
具体情况
以上的解决方式是,都可以的,但具体怎么解决,需要根据现实的业务场景进行选择。看你的场景,是更注重实时性,还是数据完整性,亦或是内存的稳定。包括缓冲队列是否可以使用。如果是两台设备进行通信,可以使用缓冲队列,如果是任务间通信,数据量不大,显然就不合适了
表格
| 策略 | 行为描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 阻塞 (Blocking) | 发送方等待,直到队列有空位。 | 数据不丢失,保证完整性。 | 可能导致发送方任务被长时间挂起,影响系统实时性。 | 金融交易、关键日志等不允许丢数据的场景。 |
| 丢弃 (Drop) | 队列满时,直接丢弃新来的数据包。 | 发送方永不阻塞,响应最快。 | 会丢失最新的信息,可能导致错过关键的即时事件。 | 监控报警、用户点击流等更看重实时性且可容忍部分丢失的场景。 |
| 覆盖 (LATEST/Conflate) | 队列满时,用新数据替换掉队列中最老的数据。 | 保证了消费者拿到的永远是最新状态,内存占用稳定。 | 中间的历史数据会丢失,无法追溯完整过程。 | 传感器读数、UI状态更新等只关心当前值、不关心变化过程的场景。 |
总结
所以,下次在设计类似功能时,你可以先问自己一个问题:
- 业务更关心"发生过什么"(需要完整性),还是"现在是什么"(需要实时性)?
想清楚这一点,就能在这三种策略中做出最合适的选择。