STM32单片机学习(34) —— ADC实验: ADC规则组配合DMA实现自动化转运

文章目录

接线以及实验效果同上一篇一样。并且代码非常简单。主要要理解为什么可以实现自动化转运。

运行过程

为什么要使用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]);
	}
		
}

	

拓展的问题

如果在现实场景下,就是会偶尔出现数据覆盖的现象,应该怎么办?

分析

出现数据覆盖现象,也就是说明接收方数据来不及接收数据,那这个时候应该怎么做?

我的解决方式

  1. 丢弃数据,等待CPU处理完进行重传
  2. 设置缓冲队列或多级缓冲队列进行数据接收(保证数据完整性)
  3. 可以容许进行数据覆盖
  4. 阻塞发送,等待空闲再发送数据

具体情况

以上的解决方式是,都可以的,但具体怎么解决,需要根据现实的业务场景进行选择。看你的场景,是更注重实时性,还是数据完整性,亦或是内存的稳定。包括缓冲队列是否可以使用。如果是两台设备进行通信,可以使用缓冲队列,如果是任务间通信,数据量不大,显然就不合适了

表格

策略 行为描述 优点 缺点 适用场景
阻塞 (Blocking) 发送方等待,直到队列有空位。 数据不丢失,保证完整性。 可能导致发送方任务被长时间挂起,影响系统实时性。 金融交易、关键日志等不允许丢数据的场景。
丢弃 (Drop) 队列满时,直接丢弃新来的数据包。 发送方永不阻塞,响应最快。 会丢失最新的信息,可能导致错过关键的即时事件。 监控报警、用户点击流等更看重实时性且可容忍部分丢失的场景。
覆盖 (LATEST/Conflate) 队列满时,用新数据替换掉队列中最老的数据。 保证了消费者拿到的永远是最新状态,内存占用稳定。 中间的历史数据会丢失,无法追溯完整过程。 传感器读数、UI状态更新等只关心当前值、不关心变化过程的场景。

总结

所以,下次在设计类似功能时,你可以先问自己一个问题:

  • 业务更关心"发生过什么"(需要完整性),还是"现在是什么"(需要实时性)?

想清楚这一点,就能在这三种策略中做出最合适的选择。

相关推荐
Realdagongzai1 小时前
Linux 6.19.10 内核调度器算法详解
linux·学习·算法·spring·kernel
xxl大卡1 小时前
Redis完整详细学习笔记
redis·笔记·学习
星夜夏空991 小时前
FreeRTOS学习(1)——裸机开发与操作系统
单片机·嵌入式硬件·学习
Cat_Rocky1 小时前
CICD-Git简单学习 操作流程后续补
git·学习
weixin_550083151 小时前
基于知识图谱的python个性化学习路径推荐系统项目源码
人工智能·学习·知识图谱
rit84324992 小时前
PIC32MX + FreeRTOS + ENC28J60 + LwIP 构建通讯管理机(通信网关)程序
stm32
魔法阵维护师2 小时前
从零开发游戏需要学习的c#模块,第二十七章(远程攻击 —— 发射子弹)
学习·游戏·c#
一口吃俩胖子2 小时前
【脉宽调制DCDC功率变换学习笔记022】DCDC变换器的稳定性、奈奎斯特准则、增益裕度和相位裕度
笔记·学习
weixin_428005302 小时前
C#调用 AI学习从0开始-第1阶段(基础与工具)-第7天多轮对话记忆
人工智能·学习·c#·多轮对话·千问api调用