西门子笔记(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 参数:在左侧 AnalogADC1 中,参数基本保持默认。只需根据外部电路的阻抗,选择一个合适的采样时间 (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:在 ADC1DMA 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 ModeDMA 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: PeripheralMemory 均设为 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 配置

  • ADC1Parameter Settings 中,启用 Scan Conversion Mode
  • Rank 配置区,配置多个通道的转换顺序。例如,将 IN10 配置为 Rank 1IN5 配置为 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);
    }
}