STM32CubeDAC及DMA配置

发布于:2025-06-03 ⋅ 阅读:(24) ⋅ 点赞:(0)


下面是笔者遇到的问题,为解决以下问题才有了此篇文章

请添加图片描述

/*生成正弦波查找表*/
/**
*	@param	buffer查找表
*	@param	samples查找表尺寸
*	@param	amplitude峰值(相对于中心值)
*	@param	offset偏移
* @retval none
*/
void generate_sin_table(uint16_t* buffer,uint16_t samples,uint16_t amplitude,float offset)
{
	float angle_step = 2 * 3.14159 / samples;
	for (uint16_t i = 0;i < samples;i++)
	{
		buffer[i] = (uint16_t )((sinf (i * angle_step ) + 1.0f) * 0.5f * (float)amplitude );
	}
	
}

#define samples 2000
#define amplitude 4095
uint16_t buffer[samples];
void dac_sin_init(void)
{
	generate_sin_table (buffer,samples,amplitude,0);
	HAL_TIM_Base_Start(&htim6); 
	HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_1,(uint32_t*)buffer,samples,DAC_ALIGN_12B_R);
	
}#define BUFFER_SIZE 1000        
uint32_t dac_val_buffer[BUFFER_SIZE / 2]; 
__IO uint32_t adc_val_buffer[BUFFER_SIZE]; 

__IO uint8_t AdcConvEnd = 0;             

void adc_tim_dma_init(void)
{

    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);//在最后一次转换后(单 ADC 模式)使能 ADC DMA 请求,并使能 ADC 转换

    __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);

    HAL_TIM_Base_Start(&htim3); 
	
}


void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    // 检查是否是我们关心的 ADC (hadc1) 的中断
    if (hadc->Instance == ADC1) // 或 if(hadc == &hadc1)
    {
        HAL_ADC_Stop_DMA(hadc);

        // 设置转换完成标志,通知后台任务数据已准备好
        AdcConvEnd = 1;
        
        // 处理数据: 从原始 ADC 缓冲区提取数据到 dac_val_buffer
        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            dac_val_buffer[i] = adc_val_buffer[i * 2 + 1];
        }

        // 保存ADC数据到文件系统
        FRESULT res = save_adc_data_to_file(dac_val_buffer, BUFFER_SIZE / 2);
        if (res != FR_OK)
        {
            my_printf(&huart1, "Failed to save ADC data! Error: %d\r\n", res);
        }

        // 打印部分数据用于调试 (示例)
        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            my_printf(&huart1, "{dac}%d\r\n", (int)dac_val_buffer[i]);
        }

        // 清除数据缓冲区 (可选)
        memset(dac_val_buffer, 0, sizeof(uint32_t) * (BUFFER_SIZE / 2));

        // 清除转换完成标志,准备下一次采集
        AdcConvEnd = 0;

        // 重新启动 ADC 和 DMA 采集,采集下一组数据
        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);
        // 再次禁止半传输中断 (如果 Start_DMA 重新启用了它)
        __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
    }
}

void adc_task(void)
{
    if (AdcConvEnd)
    {

        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            dac_val_buffer[i] = adc_val_buffer[i * 2 + 1];
        }

        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
						my_printf (&huart1 ,"{dac}%d\r\n",(int)dac_val_buffer[i]);
        }

        memset(dac_val_buffer, 0, sizeof(uint32_t) * (BUFFER_SIZE / 2));

        AdcConvEnd = 0;

        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);
        __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
				
    }
}

我使用查表法用DA生成正弦波,输入到AD通道2,将AD的输出用串口打印出来,结果打印的是上图的样子,问题在哪

一,问题1

在这里插入图片描述

我们一个点一个点来检查吧,首先是你说的DAC 输出和 ADC 采样之间没有同步,我想知道是否同步,先检查DAC输出吧,void generate_sin_table(uint16_t* buffer,uint16_t samples,uint16_t amplitude,float offset),这个函数生成的表格没有问题,再看void dac_sin_init(void) ,这个函数中HAL_TIM_Base_Start(&htim6); 和HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_1,(uint32_t*)buffer,samples,DAC_ALIGN_12B_R);的作用我不太了解,可以详细讲讲吗?我是使用定时器6作为DAC输出的触发信号?还是使用定时器6作为DMA的运输信号,我不懂啊,下面是我STM32CubeMX生成的定时器6的代码void MX_TIM6_Init(void)
{

/* USER CODE BEGIN TIM6_Init 0 */

/* USER CODE END TIM6_Init 0 */

TIM_MasterConfigTypeDef sMasterConfig = {0};

/* USER CODE BEGIN TIM6_Init 1 */

/* USER CODE END TIM6_Init 1 /
htim6.Instance = TIM6;
htim6.Init.Prescaler = 180-1;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 100-1;
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
/
USER CODE BEGIN TIM6_Init 2 */

/* USER CODE END TIM6_Init 2 */

}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{

if(tim_baseHandle->Instance==TIM3)
{
/* USER CODE BEGIN TIM3_MspInit 0 */

/* USER CODE END TIM3_MspInit 0 /
/
TIM3 clock enable /
__HAL_RCC_TIM3_CLK_ENABLE();
/
USER CODE BEGIN TIM3_MspInit 1 */

/* USER CODE END TIM3_MspInit 1 /
}
else if(tim_baseHandle->Instance==TIM6)
{
/
USER CODE BEGIN TIM6_MspInit 0 */

/* USER CODE END TIM6_MspInit 0 /
/
TIM6 clock enable /
__HAL_RCC_TIM6_CLK_ENABLE();
/
USER CODE BEGIN TIM6_MspInit 1 */

/* USER CODE END TIM6_MspInit 1 */
}
}

void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
{

if(tim_baseHandle->Instance==TIM3)
{
/* USER CODE BEGIN TIM3_MspDeInit 0 */

/* USER CODE END TIM3_MspDeInit 0 /
/
Peripheral clock disable /
__HAL_RCC_TIM3_CLK_DISABLE();
/
USER CODE BEGIN TIM3_MspDeInit 1 */

/* USER CODE END TIM3_MspDeInit 1 /
}
else if(tim_baseHandle->Instance==TIM6)
{
/
USER CODE BEGIN TIM6_MspDeInit 0 */

/* USER CODE END TIM6_MspDeInit 0 /
/
Peripheral clock disable /
__HAL_RCC_TIM6_CLK_DISABLE();
/
USER CODE BEGIN TIM6_MspDeInit 1 */

/* USER CODE END TIM6_MspDeInit 1 /
}
}上图是我的DAC的DMA配置,我也不太懂啊,除了正弦波初始化函数中的定时器初始化函数我不理解,还有这个 HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_1,(uint32_t
)buffer,samples,DAC_ALIGN_12B_R);
开启DAC的DMA的函数我也不懂啊?上面是我对与你提出的第一点的疑问,一定要详细讲讲啊

二,解决1

1,宏观思路+CubeMX配置

大致理解一下:
TIM6 在到达自动重装值时会产生一个触发信号(TRGO_Update)

这个触发信号我在CubeMX定时器那一节讲过,就是定时器的引脚(非IO引脚)连接到定时器和其他外设,定时器时间到了之后(其实可以选择如下1图),会将此信号传输给外设,去启动其他外设(包括定时器)如下2图
在这里插入图片描述
当上图定时器的Trigger Source选择为ITRx时就代表选择其他定时器作为触发源
在这里插入图片描述

这个触发信号既告诉 DAC 去从它的数据寄存器里“拿数值”输出到模拟引脚,同时又告诉 DMA 把你事先准备好的查表数据“搬运”到 DAC 的数据寄存器,即使我的DMA没有配置定时器的触发源(好像也无法配置):
在这里插入图片描述
解释一下上面的配置:
Memory to Peripheral:告诉 DMA 从内存搬数据到外设(DAC 数据寄存器)
HalfWord即半字即两个字节:我们的表格成员都是uin16_t即半字,所以 DMA 每次搬运半字到 DAC数据寄存器
Circular:首先我们要知道DMA搬运的是我们生成的表格buffer(数组,有采样点个成员)
当 DMA 完成一轮“搬运(samples即采样点 个 half‐word即buffer表格的一个成员)”之后,不会自动停止,而是重新从 buffer[0] 再开始循环搬运
除此之外我们还有开启中断:
这样在DMA传输完成之后会触发中断
在这里插入图片描述
因为我们用的是 Circular 模式,往往只在完成一整圈(samples 个点)后才能触发 ConvCpltCallback

2,HAL_TIM_Base_Start(&htim6) 的作用

在 MX_TIM6_Init() 里,CubeMX 已经调用了 HAL_TIM_Base_Init(&htim6),把 TIM6 的 Prescaler、Period、MasterConfig 都写好了寄存器,但此时 TIM6 还处于“停止”状态

1,作用1:使能TIM6的时钟并让它开始计数

如何 让TIM6开始计数呢?
答案就是调用 HAL_TIM_Base_Start(&htim6) ,然后HAL 会执行:

__HAL_TIM_ENABLE(&htim6);     // 打开定时器的计数功能
__HAL_TIM_CLEAR_IT(&htim6, TIM_IT_UPDATE); // 清除 Update 事件标志(以免刚使能就直接触发一次中断)

2,作用2:当 TIM6 溢出时,会产生一个 TRGO 更新输出

严格来说,这不是HAL_TIM_Base_Start(&htim6) ,这个函数的主要主要就是让定时器开始计数,只是在我们这个情景中,我们将TIM6的溢出事件作为TRGO产生的条件
在这里插入图片描述
而定时器6的TRGO同时又作为DAC转换的触发信号(作为其他外设的触发信号)
在这里插入图片描述

对比HAL_TIM_Base_Start(&htim6) 与MX_TIM6_Init()

3,HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)buffer, samples, DAC_ALIGN_12B_R) 的作用

1,开启 DAC 外设本身

这个函数会开启DAC转换,
同时配置DAC相关的配置:时钟,触发源,输出对其模式等信息

让 DAC1_CH1 准备好:一旦检测到一个边沿上升(来自 TIM6),就把它的数据保持寄存器(DHR12R1)中的数值“送到”转换器,然后输出到模拟引脚(如果通道 1 对应的是 PA4,就输出到 PA4)

2,配置并使能 DMA 通道

就是我们在CubeMX中配置的东西:
在这里插入图片描述

设置 DMA 通道的方向:Memory → Peripheral
把 buffer 中的一个 uint16_t(16 位)按半字搬到 DAC 的 DHR12R1 寄存器
设置 Data Width:Peripheral = HalfWord,Memory = HalfWord
Memory Increment Enable:就是我们勾选的Memory
每搬一次数据后,DMA 会把内存地址自动自增 2 字节,指向 buffer[i+1],这样下一次再搬就拿下一个表值
模式 = Circular(循环模式):
当“搬运计数器”从 0 一直搬到 samples–1 一共搬完 samples 次后,DMA 不会自动停止,而是把剩余寄存器重新归零,并把内存地址回到 buffer[0],再次从头开始搬数据。
使能 DMA Stream:
HAL 会调用 __HAL_DMA_ENABLE(&hdma_dac1_stream5) 让 DMA 准备相应DAC的DMA请求(定时器的TRGO触发DAC转换时,DAC会产生DMA请求,请求DMA将内存中查找表的成员运输到DAC的数据寄存器)

3,建立“触发→DAC→DMA” 的链路

因为我们在 CubeMX 里把 DAC Trigger Source 选为了 TIM6_TRGO,那么 DAC 会在每次检测到 TIM6 的 TRGO 上升沿时,自己发出一个 DMA 请求信号(叫做 DAC_DMACON 请求),DMA接收到请求,DMA 从 buffer[CurrentIndex] 把一个 uint16_t 送到 DAC_DHR12R1 寄存器→DAC 立刻把写入的 DHR12R1 值转换成模拟电压输出到 PA4

我们的定时器触发DAC转换,DAC产生DMA请求,DMA再响应请求去搬运数据,整个过程的速度是怎么样的呢?为了解决DAC输出与ADC采集的时序问题,这个思考是有必要的

首先我们的定时器总线的频率是90MHz:
在这里插入图片描述
TIM6的预分频系数是90:也就是实际到达定时器6的时钟频率是1MHz,CNT每过1us记一次数
在这里插入图片描述
ARR的数值为100-1,也就是说CNT加100次就归0,也就是每过100us产生溢出事件,产生一个TRGO信号触发一次DAC转换(转换一个查找表的成员),产生一次DMA请求,DMA运输一次一个查找表成员到DAC数据寄存器
总结就是:如果我的查找表有2000个成员,就需要2000*100us = 200ms才能完全转化一个查找表周期即产生一个一周期的正弦波,也就是说正弦波频率为5Hz

三,附问题1

在这里插入图片描述

HalfWord/半字:因为 DAC 是 12 位对齐,我们把每个查表值放在 uint16_t(2 字节)里,所以 DMA 每次搬运一个半字到 DAC,这个12位对齐是什么意思?和HalfWord有什么关系?你说要开启知道,上图中有两个中断代表什么意思啊?还有你说触发信号既告诉 DAC 去从它的数据寄存器里“拿数值”输出到模拟引脚,同时又告诉 DMA 把你事先准备好的查表数据“搬运”到 DAC 的数据寄存器,但是我没有在DMA中配置定时器触发DMA运输啊?还有定时器6的TRGO产生触发脉冲,告诉 DAC “现在请更新一次数据寄存器并输出电压”,让 DMA 去搬运 table 下一个数据,这里更新数据寄存器是更新所有的查找表成员,输出一整个周期的电压,DMA搬运所有的查找表成员到数据寄存器吗?还是仅仅只是搬运一个成员呢?还有你说HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)buffer, samples, DAC_ALIGN_12B_R)可以配置DAC的输出对齐模式为 12bit Right (DAC_ALIGN_12B_R),什么是输出对齐模式啊?还有你帮我看一下我对整个过程的时序理解对吗:我们的定时器总线的频率是90MHz,TIM6的预分频系数是90:也就是实际到达定时器6的时钟频率是1MHz,CNT每过1us记一次数,ARR的数值为100-1,也就是说CNT加100次就归0,也就是每过100us产生溢出事件,产生一个TRGO信号触发一次DAC转换(转换一个查找表的成员),产生一次DMA请求,DMA运输一次一个查找表成员到DAC数据寄存器,如果我的查找表有2000个成员,就需要2000*100us = 200ms才能完全转化一个查找表周期即产生一个一周期的正弦波,帮我看一下这个时序理解对吗?还有你说的在 MX_TIM6_Init() 之后、HAL_TIM_Base_Start(&htim6) 之前,把一个 GPIO(如 PA5) 配置成“在 TIM6 Update 中断里翻转一次”。我可以在 CubeMX 里打开 TIM6 的中断使能,然后在 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 里判断 if(htim->Instance == TIM6) HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5),将void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)写在MX_TIM6_Init() 之后、HAL_TIM_Base_Start(&htim6) 之前吗,或者 你能不能具体一点 把一个 GPIO(如 PA5) 配置成“在 TIM6 Update 中断里翻转一次”写在什么位置?当 buffer[0…samples−1] 都搬完后,DMA 会在不通知 CPU 的情况下把地址重新归 0,从头再搬。(因为 Circular),为什么在Circular模式下,DMA在转化完一个周期之后,不会通过CPU同意,就继续从0开始搬运数据,如果是Normal会怎么样?

四,附解决1

1,什么是“12 位对齐”

STM32 的原生 DAC 外设是一个 12 位的数模转换器。它内部有一个“数据保持寄存器”(Data Holding Register)
这个寄存器是 16 位宽度的寄存器(2 Byte),但是 DAC 硬件只会读取其中的低 12 位来做真正的数模转换,寄存器高 4 位可以不用,DAC 只关心低 12 位。
ST官方在HAL库中提供了三种对齐方式,就是我们的查找表buffer写入到DAC的数据寄存器时,怎么和数据寄存器对齐:
1,DAC_ALIGN_12B_R
就是把一个 12 位数据(0~4095)写入到数据寄存器的低12位
2,DAC_ALIGN_12B_L
把一个 12 位数据放到高12位,这个其实也是有应用场景的:

场景 为什么适合用 DAC_ALIGN_12B_L
高精度 PWM 调制 某些情况下你可能用 DAC 模拟出一个 PWM 结果,比如将数据和时间做某种线性映射后给 DAC,左对齐数据更适合后端滤波器处理。
直接操作 16 位数据流时方便 比如你从 SD 卡读到的是一组 [uint16_t] 音频数据,每个样本都在高 12 位有效,低 4 位是 padding,为了节省计算,就直接 DAC_ALIGN_12B_L 写进去

3,DAC_ALIGN_8B_R
把一个 8 位数据放到高8位
我们在哪选择对齐模式呢,CubeMX好像没有,其实是在下面的函数中
HAL_DAC_Start_DMA(&hdac, …, DAC_ALIGN_12B_R),
HAL_DAC_Start_DMA() 或 HAL_DAC_SetValue()

2,为什么Peripheral 使用Halfword?

在这里插入图片描述
这里有Peripheral和Memory,它两有什么区别呢?

项目 意思
Peripheral 是 DMA 的“目标”或“源”外设,比如 DAC、ADC、USART、SPI 等。这里指 DAC 的 DHR12R1
Memory 是你自己定义的缓冲区,比如 uint16_t buffer[samples]; 这样的数组,就是 Memory 端。

Peripheral Data Width其实就是指外设DMA搬运的数据宽度,我们的查找表是16字节的(其实有效数据只有12位即0~4095),而DAC的数据寄存器刚好是16位(有效数据也是12位),所以使用DMA传输时必须设置为HalfWord

3,DMA1 Stream5 全局中断(DMA1_Stream5_Global_IRQn)

在这里插入图片描述

什么是DMA1_Stream5 ?
就是一条DMA运输的线路:查找表Memory→DACPeripheral

如上图,此中断是默认打开的
在 CubeMX 里,一旦你启用了某个 DMA Stream(比如 DAC1 → DMA1_Stream5),CubeMX 会自动默认打开中断功能(即使你没主动去 NVIC 里勾选中断)。因为:
HAL 驱动层默认认为“你可能会在传输完成时希望做处理”

既然是默认打开的,我们如何手动关闭呢?
可以在HAL_DAC_Start_DMA() 调用之后加上下面两句,彻底屏蔽 DMA 打中断

__HAL_DMA_DISABLE_IT(&hdma_dac1, DMA_IT_TC); // 关闭 Transfer Complete 中断
__HAL_DMA_DISABLE_IT(&hdma_dac1, DMA_IT_HT); // 关闭 Half Transfer 中断

如果打开此中断,我们的DMA在传输完一整个buffer或传输一半个buffer之后,就会触发中断:进入 void DMA1_Stream5_IRQHandler(void) 中断处理函数 → 调用 HAL_DMA_IRQHandler(&hdma_dac1) → 最终如果是“传输完成”(Transfer Complete),就会调用绑定在这一路的 HAL_DAC_ConvCpltCallback()

打开此中断之后, Circular 模式和 Normal会有什么区别呢?

我们可以在CubeMX里修改这两个模式,也可以在MX_DMA_Init() 里把 hdma_dac1_stream5.Init.Mode = DMA_CIRCULAR 改成 Normal

在使用 Circular 模式 时,如果传输完成,DMA会产生中断,然后从buffer[0]开始搬运数据
如果使用Normal,传输完成之后,DMA会产生一次中断,然后不在传输数据

4, TIM6 全局中断 与 “DAC1/DAC2 underrun error interrupts”

首先解释什么是TIM6 global interrupt?
如果我们在 CubeMX “NVIC Settings” 中给 TIM6_DAC_IRQn 勾选“Enabled”,就表示我们的TIM6产生更新事件后,能够调用回调 void TIM6_DAC_IRQHandler(void) → HAL_TIM_IRQHandler(&htim6) → 最终能触发 HAL_TIM_PeriodElapsedCallback(),如果不勾选,我们的TIM6只会计数,在发生更新事件后,CPU 不会进入中断处理,也就是说,TIM6的作用只是当作DAC的触发源

那什么又是DAC1 and DAC2 underrun error interrupts呢?
即“闪存下溢 (underrun) 错误中断”
什么意思?
就是如果某次 TIM6_TRGO 来得太快、DMA 还没把新数据搬来,那么 DAC 寄存器里的数据可能已经用完(相当于“没给 DAC 新数据,DAC 只能输出一个未知值”),硬件就会认为“发生了下溢 (underrun error),下溢发生时就会触发 TIM6_DAC_IRQn 中断,进入 HAL_DAC_IRQHandler(),你可以在 HAL_DAC_ErrorCallback() 里捕获并处理
如果我们不勾选DAC1 and DAC2 underrun error interrupts,出现 underrun 只是硬件标志会置位,但不会打断 CPU

问题2

请添加图片描述
ADC DMA 缓冲区提取奇偶下标,其实我们使用了两个ADC通道,按配置顺序:第一个的通道10,PC0引脚,第二个是通道5,PA5引脚,上面是我的ADC配置,这个adc_val_buffer应该是用来存储这两个通道的模拟数据的,通道1第一个数据在adc_val_buffer[0],第二个数据在adc_val_buffer[2],第三个数据在adc_val_buffer[4],通道2的第一个数据在adc_val_buffer[1],第二个数据在adc_val_buffer[3],这个理解有错误吗?还有__IO uint32_t adc_val_buffer[BUFFER_SIZE];
__IO uint8_t AdcConvEnd = 0;这里的__IO有什么用?还有这个函数void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)的回调机制是怎么样的?这个回调机制和DAC的回调机制都要详细讲讲啊???接下来就是DAC和ADC的转换时序问题了,你看我的理解有错误吗:我的DAC使用的查找表是buffer[2000],就是有2000个uin16_t的采样点,而ADC收集数据的区域是uint32_t adc_val_buffer[BUFFER_SIZE],有1000个uin32_t数据,同时这个区域要交叉存放两个通道的数据,使用只能存放500个DAC的数据,我确定虽然ADC和DAC的定时器不是同一个,但是这两个定时器的触发频率的一样的都是100us触发一次,产生一次TRGO驱动ADC和DAC转换,我想ADC转换完毕一轮数据需要1000*100us的时间,虽然通道1是PC0接收外界滑动变阻器的模拟电压,但是转换滑变的模拟信号也需要定时器触发,而且在ADC进行通道1的转换时,比如正在转换滑变数据到adc_val_buffer[4],就是第五次转换,这时DAC正在将buffer[4]的数据转换为模拟信号,转换完成之后交给ADC,ADC是在什么时候接收数据呢?或者说ADC的adc_val_buffer为DAC分的500个位置,DAC的buffer的哪些成员抢到了呢?

解决2

1,__IO 是什么?

__IO 是 CMSIS 定义的一个修饰符(实际是宏),表示

#define __IO volatile

它的作用是提醒编译器:这个变量的值随时可能被硬件修改或DMA修改
如果不提醒编译器,编译器不能察觉到变量volatile在中断中变化,加上__IO,编译器每次都要从内存真正读取它的值

2,HAL_ADC_ConvCpltCallback() 的回调机制

我们知道DMA的半满中断和全满中断是默认打开的:如下图
在这里插入图片描述
但是我们可以手动关闭: __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);就是关闭半满中断,我写在了下面的初始化代码里面,但是没有关闭全满中断,因为我想在DMA搬运完成后,触发中断,方便我在中断里做事,说具体一点就是在HAL_ADC_ConvCpltCallback() 里面做事,

void adc_tim_dma_init(void)
{

    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);//在最后一次转换后(单 ADC 模式)使能 ADC DMA 请求,并使能 ADC 转换

    __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);

    HAL_TIM_Base_Start(&htim3); 
	
}

具体流程:
DMA全满事件到来时会进入全满中断:进入中断函数 DMAx_Streamx_IRQHandler()
该函数内部调用:

HAL_DMA_IRQHandler(&hdma_adc1);

进而触发 HAL 注册在 ADC 上的 DMA 完成回调(相当于将DMA的全满事件绑定到了ADC上):

static void ADC_DMAConvCplt(DMA_HandleTypeDef *hdma)

这个回调最终会调用你用户定义的:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)

3,回顾HAL_DAC_ConvCpltCallback() 的回调机制

4,ADC 与 DAC 同步采样机制分析

五,附问题2

请添加图片描述
请添加图片描述

当 DMA 把你设定数量的采样数据(即 adc_val_buffer[BUFFER_SIZE])全部搬完(CNDTR = 0)这是什么意思啊,我是嵌入式小白,DMA将ADC要的数据adc_val_buffer搬运完,是指从哪里搬运到哪里呢?上面是我ADC的DMA的配置,你之前说DMA的全满中断和半满中断是默认打开的,上图中,DMA中断也确实关不掉,下面是我的adc初始化函数void adc_tim_dma_init(void)
{

HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);//在最后一次转换后(单 ADC 模式)使能 ADC DMA 请求,并使能 ADC 转换

__HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);

HAL_TIM_Base_Start(&htim3); 

}这里关闭了半满中断,所以全满中断是没有关闭的,所以DMA搬运完成后会进入中断,但是我还是不知道DMA搬运的是什么啊? HAL_ADC_Start_DMA(&hadc1, (uint32_t )adc_val_buffer, BUFFER_SIZE);这个函数一定要好好讲讲啊?项目中 使用的是 Circular 模式,DMA 不会停止,也通常不会需要用到这个回调函数(因为不需要 CPU 管),但是我好像没有关闭DMA半满和全满中断啊?你看void dac_sin_init(void)
{
generate_sin_table (buffer,samples,amplitude,0);
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_1,(uint32_t
)buffer,samples,DAC_ALIGN_12B_R);

}我记得你说要在HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_1,(uint32_t*)buffer,samples,DAC_ALIGN_12B_R);后面写上__HAL_DMA_DISABLE_IT(&hdma_dac1, DMA_IT_TC); // 关闭 Transfer Complete 中断
__HAL_DMA_DISABLE_IT(&hdma_dac1, DMA_IT_HT); // 关闭 Half Transfer 中断,才算关闭,而且我确定我的CubeMX的DMA stream global interrupt是勾选上的,而且不能取消勾选啊?你说DAC 查表输出:uint16_t buffer[2000],这好像不对吧,buffer是查找表,存储数字量,DMA将它交给DAC转化成模拟量?ADC 采样缓存:uint32_t adc_val_buffer[1000],这个adc_val_buffer我也不理解是干什么的啊?下面是我ADC的代码,你在理解理解吧:

#define BUFFER_SIZE 1000        
uint32_t dac_val_buffer[BUFFER_SIZE / 2]; 
__IO uint32_t adc_val_buffer[BUFFER_SIZE]; 

__IO uint8_t AdcConvEnd = 0;             

void adc_tim_dma_init(void)
{

    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);//在最后一次转换后(单 ADC 模式)使能 ADC DMA 请求,并使能 ADC 转换

    __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);

    HAL_TIM_Base_Start(&htim3); 
	
}


void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    // 检查是否是我们关心的 ADC (hadc1) 的中断
    if (hadc->Instance == ADC1) // 或 if(hadc == &hadc1)
    {
        HAL_ADC_Stop_DMA(hadc);

        // 设置转换完成标志,通知后台任务数据已准备好
        AdcConvEnd = 1;
        
        // 处理数据: 从原始 ADC 缓冲区提取数据到 dac_val_buffer
        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            dac_val_buffer[i] = adc_val_buffer[i * 2 + 1];
        }

        // 保存ADC数据到文件系统
        FRESULT res = save_adc_data_to_file(dac_val_buffer, BUFFER_SIZE / 2);
        if (res != FR_OK)
        {
            my_printf(&huart1, "Failed to save ADC data! Error: %d\r\n", res);
        }

        // 打印部分数据用于调试 (示例)
        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            my_printf(&huart1, "{dac}%d\r\n", (int)dac_val_buffer[i]);
        }

        // 清除数据缓冲区 (可选)
        memset(dac_val_buffer, 0, sizeof(uint32_t) * (BUFFER_SIZE / 2));

        // 清除转换完成标志,准备下一次采集
        AdcConvEnd = 0;

        // 重新启动 ADC 和 DMA 采集,采集下一组数据
        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);
        // 再次禁止半传输中断 (如果 Start_DMA 重新启用了它)
        __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
    }
}

void adc_task(void)
{
    if (AdcConvEnd)
    {

        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            dac_val_buffer[i] = adc_val_buffer[i * 2 + 1];
        }

        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
						my_printf (&huart1 ,"{dac}%d\r\n",(int)dac_val_buffer[i]);
        }

        memset(dac_val_buffer, 0, sizeof(uint32_t) * (BUFFER_SIZE / 2));

        AdcConvEnd = 0;

        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);
        __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
				
    }
}

请添加图片描述

HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);这个函数的作用能不能再详细讲讲?我的理解是它会开启ADC转换,然后转换完adc_val_buffer的所以成员后,ADC发起DMA请求,DMA将adc_val_buffer所有成员都搬运到adc_val_buffer?这个思路有问题啊?adc_val_buffer到adc_val_buffer?不对ADC转换的不是adc_val_buffer,而是通道1和通道2的数据,但是我没有看见这2个通道的数据在哪啊? TIM3 每 100μs 触发一次 TRGO → ADC 开始两路转换(通道10 + 通道5),你的意思是说100us,ADC会将每个通道的数据都转换一个吗,也就是说1000个数据,我只进行500次转换?“DMA always stores ADC results into 32-bit aligned memory, even if ADC resolution is only 12-bit or 10-bit”意思是直接内存访问(DMA)总是将模数转换器(ADC)的结果存储到 32 位对齐的内存中,即使 ADC 分辨率仅为 12 位或 10 位。
也就是说,我们的内存即adc_val_buffer必须是32位的,但是ADC的ADC_DR寄存器应该是16位的吧,那为什么上图中Peripheral可以选择32位的Word?

六,附解决2

1,在ADC转换中DMA“搬运”是什么?

HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);

作用就是让DMA 从 ADC 数据寄存器里自动取出转换结果 → 填到你提供的数组 adc_val_buffer[] 里,方向是外设ADC到内存
详细一定就是:
ADC1 经过触发信号(TIM3 TRGO)启动一次转换
转换完成后,ADC 的结果(比如12位数值)会暂存在 ADC_DR寄存器 中
DMA2_Stream0 被 ADC 自动唤醒(DAC也会驱动DMA):把 ADC_DR 的值拷贝 → 放到 adc_val_buffer[i] 位置中

参数 含义
&hadc1 要操作的 ADC 外设(ADC1)
(uint32_t*)adc_val_buffer DMA 目标内存地址,ADC 转换结果存在哪里(必须是32位对齐)
BUFFER_SIZE 总共要搬多少个“数值”(不是字节,是单位个数)
那么为什么 (uint32_t *)adc_val_buffer必须是uin32_t,不能是uin16_t呢?
虽然 ADC 是 12 位的(结果 0~4095),HAL 必须把结果放进一个 32bit 的 uint32_t(而不是 uint16_t),因为STM32 HAL 的规定:

“DMA always stores ADC results into 32-bit aligned memory, even if ADC resolution is only 12-bit or 10-bit”意思是直接内存访问(DMA)总是将模数转换器(ADC)的结果存储到 32 位对齐的内存中,即使 ADC 分辨率仅为 12 位或 10 位。
也就是说,我们的内存即adc_val_buffer必须是32位的,但是ADC的ADC_DR寄存器只有16位,DMA 是可以从 16 位外设读数据并扩展成 32 位搬运

2,HAL_ADC_Start_DMA() 这个函数到底做了什么?

HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);

1,配置 DMA 传输参数
2,启动 DMA ,让DMA监听ADC的DMA请求
3,启动 ADC,让ADC等待定时器的TRGO

3,流程

步骤 内容
1 TIM3 每 100μs 触发一次 TRGO
2 ADC 扫描 Rank 1(通道10)、Rank 2(通道5),各转换一次,共两个结果
3 每转换完一个通道,DMA 自动把 ADC_DR 的结果搬运进 adc_val_buffer[i]
4 你设了 BUFFER_SIZE = 1000,也就是说一共会存入 1000 个数(500次触发 * 2个通道)

注意:所以每次 TIM3 触发,
ADC 会:连续转换两个通道
DMA 会搬两次数据到数组中
也就是说adc_val_buffer[1000]只需要被DMA写500次就能写满

4,ADC 结果只有 12 位,DMA Peripheral 和 Memory 都设置为 Word(32位)

DMA搬运的起点是ADC的数据寄存器,但这个寄存器只有16位,而且这16位中只有12位数据有效,终点就是adc_val_buffer[BUFFER_SIZE],是32位的(HAL库 的要求)

虽然两者不对应,但是只有我们将Peripheral设置成32位,DMA 就可以从 16 位外设读数据并扩展成 32 位搬运

七,问题3

我在回调函数(DMA搬运完一轮数据时触发)里面把ADC的1000个转换结果中的通道2的结果抽出来,再打印出来,然后再开启DMA运输,这样的做法我觉得对串口打印出的波形完整性影响不大啊?为什么你之前说可能出现“ADC 再次采集时机”和“你还在打印上一次波形”的逻辑冲突,造成波形显示不稳定?然后你还提出这样的建议:建议把 “DMA 采集好再打包成 buffer → 再一次性批量写到文件或存储→最后才串口打印” 的思路分离:
在 ConvCpltCallback 里只做“拷贝/标记/存储”逻辑,不要一边采一边打 printf。
在主循环或专门的任务里(adc_task())检查 AdcConvEnd = 1,再做一次性批量的“串口打印”或者“保存到 SD 卡”,这样可以避免干扰采样时序。我是嵌入式小白,你的想法我看不懂啊?给出你我目前的回调函数代码

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    // 检查是否是我们关心的 ADC (hadc1) 的中断
    if (hadc->Instance  = ADC1) // 或 if(hadc == &hadc1)
    {
        HAL_ADC_Stop_DMA(hadc);

        // 设置转换完成标志,通知后台任务数据已准备好
        AdcConvEnd = 1;
        
        // 处理数据: 从原始 ADC 缓冲区提取数据到 dac_val_buffer
        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            dac_val_buffer[i] = adc_val_buffer[i * 2 + 1];
        }

        // 保存ADC数据到文件系统
   //     FRESULT res = save_adc_data_to_file(dac_val_buffer, BUFFER_SIZE / 2);
//        if (res != FR_OK)
//        {
//            my_printf(&huart1, "Failed to save ADC data! Error: %d\r\n", res);
//        }

        // 打印部分数据用于调试 (示例)
        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            my_printf(&huart1, "{dac}%d\r\n", (int)dac_val_buffer[i]);
        }

        // 清除数据缓冲区 (可选)
    //   memset(dac_val_buffer, 0, sizeof(uint32_t) * (BUFFER_SIZE / 2));

        // 清除转换完成标志,准备下一次采集
        AdcConvEnd = 0;

//        // 重新启动 ADC 和 DMA 采集,采集下一组数据
//        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);
//        // 再次禁止半传输中断 (如果 Start_DMA 重新启用了它)
//        __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
    }
}

这个 AdcConvEnd = 0和1我也不明白是什么意思啊?

七,解决3

1,串口打印时间过长?

用的是 UART 115200 波特率,发送一个字符大约要 87μs,而每条消息 {dac}1234\r\n 差不多是 10个字符,那你每条消息发送时间 ≈ 1ms!
打印 500 条数据 ≈ 500ms!
ADC 采样速度是多少?是:100μs 采一次
所以 500 次只需要 50ms

所以?
所以我们在打印数据时(使用500ms,此时还停止了ADC采集,采集完一周期使用50ms),这样相当于,有10个需要我们采集的周期我们忽略了

DAC在串口打印时还在输出模拟信号到ADC的通道2,但是现在ADC处于停止状态,接收不到模拟信号,这和DMA没有关系,因为DAC的输出与ADC的采集是通过IO引脚物理连接实现的,不是通过DMA,对于DAC,DMA只是负责将查找表搬运到DAC数据寄存器,对于ADC,DMA只是负责将ADC转换结果搬运到内存dac_val_buffer里面

2, AdcConvEnd = 1 / 0 是干嘛的

含义
1 表示 DMA 数据采集好了,可以处理
0 表示数据还没采好,不要乱动

网站公告

今日签到

点亮在社区的每一天
去签到