西门子笔记(3)adda

好的,收到您的请求。您的笔记内容非常详尽且专业,我将严格遵循您的要求,保持所有代码和图片不变,对语言进行适当润色,重点在于优化和美化整体排版,整理出一个逻辑清晰、阅读体验更佳的技术文档。


STM32 HAL 库 AD/DA 应用深度解析

本文档旨在系统性地介绍在 STM32CubeMX 环境下,从基础到高级配置和使用 ADC (模数转换器) 与 DAC (数模转换器) 的多种方案。

目录

  1. ADC (模数转换) 的三种实现方案
  2. DAC (数模转换) 与多通道 ADC 应用

1. ADC (模数转换)的三种实现方案

1.1 方案一:轮询法 (Polling) - 简单直接

工作原理

轮询法是最基础的 ADC 数据获取方式。其过程完全由 CPU 主动发起和等待,简单直观。

生活化类比:去信箱取信

  1. 启动:你走到楼下信箱 (HAL_ADC_Start)。
  2. 等待:你站在信箱旁一直等,直到邮递员把信投进去 (HAL_ADC_PollForConversion)。
  3. 读取:你打开信箱,取出信件 (HAL_ADC_GetValue)。

这种方式的缺点是,在等待期间 CPU 被完全占用,无法处理其他事务,效率较低。但对于功能验证或简单应用,不失为一种快速的实现方法。

CubeMX 配置

  1. 选择 ADC 引脚:在 Pinout & Configuration 视图中,选择一个支持 ADC 的引脚(如 PC0),并将其配置为 ADC1_IN10。
    选择ADC引脚
  2. 配置 ADC 参数:在左侧 Analog → ADC1 中,参数基本保持默认。只需根据外部电路的阻抗,选择一个合适的采样时间 (Sampling Time) 即可。
    配置ADC参数

代码实现与解析

__IO uint32_t adc_val; // 用于存储ADC原始值
__IO float voltage;    // 用于存储计算后的电压

/**
  * @brief  使用轮询方式读取一次ADC值
  */
void adc_read_by_polling(void) 
{
    // 1. 启动 ADC 转换
    HAL_ADC_Start(&hadc1);

    // 2. 等待转换完成 (阻塞式),超时时间设置为1000ms
    if (HAL_ADC_PollForConversion(&hadc1, 1000) == HAL_OK) 
    {
        // 3. 转换成功,读取数字结果 (12位分辨率下为 0-4095)
        adc_val = HAL_ADC_GetValue(&hadc1);

        // 4. (可选) 将数字值转换为实际电压
        //    假设 Vref = 3.3V, 分辨率 12位 (2^12 = 4096)
        voltage = (float)adc_val * 3.3f / 4096.0f; 

        my_printf(&huart1, "ADC Value: %lu, Voltage: %.2fV\n", adc_val, voltage);
    } 
    else 
    {
        // 转换超时或出错处理
        my_printf(&huart1, "ADC Poll Timeout!\n");
    }
  
    // 5. (注意)如果ADC配置为单次转换模式,转换完成后会自动停止,无需手动调用Stop。
    // HAL_ADC_Stop(&hadc1); 
}

// 在主循环或任务中调用
void adc_task()
{
	adc_read_by_polling();
}

1.2 方案二:DMA + 定时轮询 - 初步解放 CPU

工作原理

为解决轮询法效率低下的问题,我们引入 DMA (Direct Memory Access)。DMA 就像一个独立的“搬运工”,可以在无需 CPU 干预的情况下,自动将 ADC 的转换结果搬运到内存中。

核心思路:

  1. 配置 ADC 工作在连续转换模式 (Continuous Conversion Mode)。
  2. 配置 DMA 通道为循环模式 (Circular Mode),让它在 ADC 每次转换完成后,自动将结果搬运到内存缓冲区数组。当数组写满后,DMA 会自动回到数组开头继续写入。
  3. CPU 不再等待单次转换,而是在主循环或定时任务中,按需、定期地去读取 DMA 缓冲区的内容,并通常进行求平均值等处理以获得稳定读数。

CubeMX 配置

  1. 配置 DMA:在 ADC1 的 DMA Settings 标签页,点击 Add 添加 ADC1 的 DMA 请求。

    • Mode: Circular (循环模式)。
    • Data Width: Word。原因:STM32 的外设寄存器通常是 32 位的,使用 Word (4字节) 可以保证最高效的内存对齐访问,且与我们定义的 uint32_t 缓冲区类型匹配。
      配置ADC DMA
  2. 配置 ADC 参数:

    • 启用 Continuous Conversion Mode (连续转换模式)。
    • 启用 DMA Continuous Requests (DMA 连续请求)。
      启用连续转换和DMA请求

代码实现与解析

#define ADC_DMA_BUFFER_SIZE 32                  // DMA缓冲区大小
uint32_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE]; // DMA 目标缓冲区
__IO uint32_t adc_val;                        // 存储计算后的平均 ADC 值
__IO float voltage;                           // 存储计算后的电压值

/**
  * @brief  初始化并启动ADC的DMA传输
  */
void adc_dma_init(void)
{
    // 启动 ADC 并使能 DMA 传输
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_DMA_BUFFER_SIZE);
}

/**
  * @brief  ADC数据处理任务,在主循环或定时器中定期调用
  */
void adc_task(void)
{
    uint32_t adc_sum = 0; //用于将储存求和的数据

    // 1. 计算 DMA 缓冲区中所有采样值的总和
    for(uint16_t i = 0; i < ADC_DMA_BUFFER_SIZE; i++)
    {
        adc_sum += adc_dma_buffer[i];
    }

    // 2. 计算平均 ADC 值
    adc_val = adc_sum / ADC_DMA_BUFFER_SIZE; 

    // 3. (可选) 将平均数字值转换为实际电压值
    voltage = ((float)adc_val * 3.3f) / 4096.0f;

    my_printf(&huart1, "Average ADC: %lu, Voltage: %.2fV\n", adc_val, voltage);
}

1.3 方案三:DMA + 定时器触发 + 中断 - 精准数据块采集

工作原理

当我们需要以固定的频率进行 ADC 采样,并在采集完一批数据块后进行集中处理时(如 FFT 分析),此方案是最佳选择。

生活化类比:机器人采集样本

  1. 下达指令:你命令一个机器人 (DMA+ADC) 去采集指定数量 (BUFFER_SIZE) 的样本 (HAL_ADC_Start_DMA)。
  2. 自动工作:一个节拍器 (Timer) 每响一次,机器人就采集一个样本。
  3. 完成报告:机器人集满所有样本后,会举起一个牌子 (触发 DMA 传输完成中断,设置标志位 AdcConvEnd) 并停下工作。
  4. 处理并重启:你 (CPU) 看到牌子后,去取走所有样本进行处理。处理完毕后,再次命令机器人开始新一轮的采集。

CubeMX 配置

  1. 配置定时器 (如 TIM3) 作为触发源:

    • Clock Source: Internal Clock。
    • PSC & ARR: 计算并设置分频系数 (PSC) 和自动重装载值 (ARR) 以获得所需的触发频率。例如,要实现 10kHz 触发频率,在 90MHz 定时器时钟下,可设 PSC=90-1, ARR=100-1。
    • Trigger Event Selection (TRGO): Update Event。
      配置定时器触发源
  2. 修改 ADC 的 DMA 模式:

    • Mode: Normal (普通模式),因为我们每次只传输一个数据块。
      修改DMA为Normal模式
  3. 修改 ADC 参数:

    • 禁用 Continuous Conversion Mode 和 DMA Continuous Requests。
    • External Trigger Conversion Source: Timer 3 Trigger Out event (选择定时器触发)。
    • External Trigger Conversion Edge: Trigger detection on the rising edge (上升沿触发)。
      配置ADC外部触发
  4. 使能 ADC 全局中断:在 NVIC Settings 中勾选 ADC1, ADC2 and ADC3 global interrupts。
    使能ADC全局中断

代码实现与解析

#define BUFFER_SIZE 1000
__IO uint32_t adc_val_dma_buffer[BUFFER_SIZE]; // DMA 目标缓冲区
__IO float voltage;                            // 存储计算后的电压
__IO uint8_t AdcConvEnd = 0;                   // ADC 转换完成标志

/**
  * @brief  初始化并启动由定时器触发的ADC DMA传输
  */
void adc_tim_dma_init(void)
{
    // 启动定时器作为触发源
    HAL_TIM_Base_Start(&htim3); 
  
    // 启动 ADC 的 DMA 传输
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_dma_buffer, BUFFER_SIZE);

    // 显式禁用 DMA 半传输中断
    __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
}

/**
  * @brief  ADC 转换完成回调函数 (由 DMA TC 中断触发)
  * @param  hadc ADC句柄
  */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if (hadc->Instance == ADC1)
    {
        // 停止ADC和DMA,防止在处理数据时被新数据覆盖
        HAL_ADC_Stop_DMA(hadc);
        // 设置转换完成标志,通知主循环数据已准备好
        AdcConvEnd = 1;
    }
}

/**
  * @brief  ADC数据块处理任务
  */
void adc_task(void)
{
    if (AdcConvEnd)
    {
        // 1. 处理数据块
        for(uint16_t i = 0; i < BUFFER_SIZE; i++)
        {
            voltage = ((float)adc_val_dma_buffer[i]) * 3.3f / 4096.0f;
            my_printf(&huart1, "{tim_adc}%.2f\n", voltage);	
        }
    
        // 2. 清除标志位
        AdcConvEnd = 0;

        // 3. 重新启动下一次采集
        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_dma_buffer, BUFFER_SIZE);
        __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
    }
}

2. DAC (数模转换) 与多通道 ADC 应用

2.1 DAC + DMA 输出正弦波:硬件自动化波形生成

工作原理

DAC (Digital-to-Analog Converter) 的任务是将数字值转换为模拟电压。通过结合定时器和DMA,可以实现高效、精准的任意波形输出,而几乎不占用 CPU 资源。

生活化类比:自动钢琴演奏

  1. 乐谱 (查找表):预先在内存中生成一个数组,存储一个完整周期正弦波的离散数字点。
  2. 节拍器 (Timer):配置一个定时器以固定频率触发 DAC。
  3. 自动翻页机 (DMA):配置 DMA 在每次收到节拍器信号后,自动从“乐谱”中取出下一个音符(数据点),送给演奏家。
  4. 演奏家 (DAC):将接收到的数字音符转换为实际的模拟声波(电压)。

整个过程一旦启动,便由硬件全自动循环执行。

“绘制乐谱”:生成正弦波查找表

#define SINE_SAMPLES 100     // 一个周期内的采样点数
#define DAC_MAX_VALUE 4095   // 12 位 DAC 的最大值

uint16_t SineWave[SINE_SAMPLES]; // 存储正弦波数据的数组

/**
 * @brief 生成正弦波查找表
 * @param buffer: 存储波形数据的缓冲区指针
 * @param samples: 一个周期内的采样点数
 * @param amplitude: 正弦波的峰值幅度 (相对于中心值)
 */
void Generate_Sine_Wave(uint16_t* buffer, uint32_t samples, uint16_t amplitude)
{
    float step = 2.0f * 3.14159f / samples; 
    for(uint32_t i = 0; i < samples; i++)
    {
        float sine_value = sinf(i * step);
        // 将 (-1.0 ~ 1.0) 的正弦值映射到 DAC 的 (0 ~ 4095) 输出范围
        buffer[i] = (uint16_t)((sine_value * amplitude) + (DAC_MAX_VALUE / 2.0f));
    }
}

CubeMX 配置

  1. 配置定时器 (如 TIM6):

    • 启用并设置频率,此频率决定了采样点的输出速率。
    • 重要公式:正弦波频率 = 定时器触发频率 / 每周期采样点数
    • Trigger Output (TRGO): Update Event。
      配置DAC触发定时器
  2. 配置 DAC:

    • 启用 DAC 通道。
    • Trigger: 选择 Timer 6 Trigger Out event。
      配置DAC参数
  3. 配置 DAC 的 DMA:

    • Direction: Memory to Peripheral。
    • Mode: Circular。
    • Data Width: Peripheral 和 Memory 均设为 Half Word (16-bit)。
      配置DAC DMA

“启动自动演奏”:代码实现

/**
  * @brief  初始化并启动DAC DMA正弦波输出
  */
void dac_sin_init(void)
{
    // 1. 生成正弦波查找表数据
    Generate_Sine_Wave(SineWave, SINE_SAMPLES, DAC_MAX_VALUE / 2);
  
    // 2. 启动触发 DAC 的定时器
    HAL_TIM_Base_Start(&htim6);
  
    // 3. 启动 DAC 通道并通过 DMA 输出
    HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)SineWave, SINE_SAMPLES, DAC_ALIGN_12B_R);
}

2.2 ADC 多通道扫描采集

工作原理

当需要采集多个模拟通道时,可以启用 ADC 的扫描模式 (Scan Conversion Mode)。在此模式下,ADC 会按照预先配置的顺序 (Rank 1, Rank 2, …),自动地、依次地对多个通道进行转换。

CubeMX 配置

  • 在 ADC1 的 Parameter Settings 中,启用 Scan Conversion Mode。
  • 在 Rank 配置区,配置多个通道的转换顺序。例如,将 IN10 配置为 Rank 1,IN5 配置为 Rank 2。
  • Number Of Conversion 需设置为实际的通道数。
    配置ADC多通道扫描

代码修改

当启用多通道扫描后,DMA 缓冲区中会交错存放各个通道的数据。例如,[通道1数据, 通道2数据, 通道1数据, 通道2数据, ...]。因此,数据处理逻辑需要相应修改。

以下代码示例演示了如何从双通道交错数据中,仅提取第二个通道 (IN5) 的数据进行处理。

// 假设 BUFFER_SIZE 是 DMA 传输的总点数 (两个通道的总和)
// adc_val_dma_buffer 中存放的是 [IN10_1, IN5_1, IN10_2, IN5_2, ...]
uint32_t channel5_buffer[BUFFER_SIZE / 2]; // 创建一个专用缓冲区

void adc_task(void)
{
    if (AdcConvEnd)
    {
        // 1. 从交错数据中提取第二个通道的数据
        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            // adc_val_dma_buffer[i * 2] 是通道10的数据
            // adc_val_dma_buffer[i * 2 + 1] 是通道5的数据
            channel5_buffer[i] = adc_val_dma_buffer[i * 2 + 1];
        }

        // 2. 处理提取出的通道5数据
        for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
        {
            my_printf(&huart1, "{dac}%d\n", (int)channel5_buffer[i]);
        }
  
        AdcConvEnd = 0;

        // 3. 重新启动下一次多通道扫描采集
        HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_dma_buffer, BUFFER_SIZE);
        __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
    }
}

2 个赞