stm32—ADC和DAC

ADC和DAC


在嵌入式系统中,微控制器经常需要与现实世界的模拟信号进行交互。STM32微控制器内置了模拟数字转换器(ADC)和数字模拟转换器(DAC),它们是实现这种交互的关键模块。

1. 模拟数字转换器(ADC)

ADC的作用是将连续变化的模拟电压信号转换为离散的数字值,以便微控制器进行处理。

工作原理:

STM32的ADC通常采用**逐次逼近型(SAR)**架构。其基本思想是通过一系列比较来逐渐逼近输入模拟电压的数字表示。

  • 采样(Sampling): ADC在特定时刻对模拟输入电压进行采样,将其固定在采样保持电容上。采样时间越短,对快速变化的信号捕捉能力越强。
  • 量化(Quantization) : 将采样的模拟电压值映射到有限的离散数字级别。ADC的分辨率决定了量化级别数量,例如,12位ADC可以将模拟输入转换为212=4096个不同的数字值。
  • 编码(Encoding): 将量化后的级别转换为相应的二进制数字代码。

原理图示意:

虽然STM32内部的ADC结构复杂,但其简化原理可以表示为:

c 复制代码
                   +-------------------+
模拟输入 (Vin) ----| 采样保持电路 (S/H) |----
                   +-------------------+
                             |
                             V
                   +-------------------+
                   | 比较器 (Comparator) |----
                   +-------------------+
                             |
                             V
                   +-------------------+
                   | 逐次逼近寄存器 (SAR) |----
                   +-------------------+
                             |
                             V
                   +-------------------+
                   | 控制逻辑 (Control Logic) |----
                   +-------------------+
                             |
                             V
数字输出 (Digital Out) <--------------------

主要特点:

  • 高分辨率: STM32的ADC通常提供12位甚至更高(如16位)的分辨率,这意味着可以精确地测量模拟电压。
  • 多通道: 大多数STM32芯片具有多个ADC通道,可以同时或顺序转换多个模拟输入。
  • 多种转换模式
    • 单次转换模式 (Single Conversion Mode): 每次触发只进行一次转换。
    • 连续转换模式 (Continuous Conversion Mode): 一旦启动,ADC会持续进行转换,直到停止。
    • 扫描模式 (Scan Mode): 自动按顺序转换多个通道。
    • 注入模式 (Injected Mode): 允许在常规转换序列中插入高优先级的转换,常用于实时控制。
  • 多种触发源: 转换可以由软件、定时器事件、外部中断等多种方式触发。
  • DMA支持: 可以通过DMA(直接内存访问)将ADC转换结果自动传输到内存中,减轻CPU负担,提高效率。
  • 自校准: 部分STM32系列支持ADC自校准功能,以提高测量精度。
  • 模拟看门狗: 可以设置模拟信号的阈值,当模拟输入超出范围时触发中断。
  • 过采样: 通过对多次采样结果进行平均,可以提高有效分辨率。

应用场景:

  • 传感器数据采集: 读取温度、光照、压力、湿度等模拟传感器的输出。
  • 电池电压监测: 实时监测电池电量。
  • 电源管理: 监测电源轨电压,确保系统稳定运行。
  • 电机控制: 通过电流传感器反馈进行闭环控制。
  • 音频信号处理: 简单的音频输入采样。

2. 数字模拟转换器(DAC)

DAC的作用是将微控制器产生的数字信号转换为连续变化的模拟电压信号。

工作原理:

STM32的DAC通常采用R-2R梯形网络电阻串型结构。其基本思想是根据数字输入的不同位,通过精确的电阻网络组合,产生不同大小的电流或电压,然后叠加形成最终的模拟输出。

  • 数字输入: 微控制器提供一个数字值给DAC。
  • 转换: DAC内部的电阻网络根据数字输入值产生相应的模拟电压。
  • 输出: DAC输出一个与数字输入值成比例的模拟电压。

原理图示意:

以简化的R-2R梯形网络为例:

c 复制代码
                +-------------------+
数字输入 (Dout) |                   |
(例如12位)    |                   |
D11 -----+---+ R/2R 网络           +----- 模拟输出 (Vout)
D10 -----|   |                   |
...      |   |                   |
D0  -----+---+                   |
                +-------------------+

主要特点:

  • 高分辨率: 通常提供12位分辨率,可以产生212=4096个不同的模拟输出电压级别。
  • 多通道: 部分STM32芯片具有多个DAC通道,可以同时输出多个模拟电压。
  • 多种波形生成
    • 软件模式: 通过软件直接写入数据寄存器来更新输出电压。
    • 波形生成器: 内置三角波和噪声波形生成器,可以自动生成特定波形。
  • DMA支持: 可以通过DMA自动将数据传输到DAC数据寄存器,实现连续波形生成,减轻CPU负担。
  • 外部触发: 可以通过定时器、外部中断等触发DAC的转换。
  • 输出缓冲: 部分DAC具有输出缓冲器,可以提高输出驱动能力和稳定性。

应用场景:

  • 波形生成: 生成正弦波、方波、三角波、锯齿波等各种模拟波形,用于信号发生器、测试设备等。
  • 模拟电压输出: 提供可编程的参考电压或控制电压,例如用于PWM调光、电机速度控制等。
  • 音频播放: 简单的音频信号输出(通过DAC和外部滤波)。
  • 可编程增益放大器 (PGA) 控制: 通过DAC输出控制PGA的增益。
  • 传感器校准: 提供精确的模拟电压来校准外部传感器。

1. STM32 ADC实际代码流程示例

假设我们要使用ADC1的通道0(PA0引脚)来读取一个模拟电压值,并将其通过UART打印出来。

所需硬件:

  • STM32开发板(例如STM32F407VGT6)
  • 连接在PA0上的模拟信号源(例如电位器)
  • USB转串口模块(用于UART调试输出)

代码流程概述:

  1. CubeMX配置:
    • 启用ADC1,选择Channel 0。
    • 配置ADC的时钟、分辨率、采样时间等参数。
    • 启用UART(例如USART2)用于打印结果。
    • 生成代码。
  2. HAL库代码实现:
    • 包含头文件。
    • 定义ADC句柄和变量。
    • main函数中,执行ADC和UART的初始化(CubeMX生成的)。
    • 启动ADC转换。
    • 获取转换结果。
    • 将结果通过UART发送。

代码示例:

c 复制代码
#include "main.h" // 包含CubeMX生成的头文件
#include "string.h" // 用于字符串操作,例如sprintf

// ADC句柄声明 (通常由CubeMX生成在main.h中,或自行声明)
extern ADC_HandleTypeDef hadc1; // 假设CubeMX已生成此句柄
extern UART_HandleTypeDef huart2; // 假设CubeMX已生成此UART句柄

void MX_ADC1_Init(void); // ADC初始化函数声明 (CubeMX生成)
void MX_USART2_UART_Init(void); // UART初始化函数声明 (CubeMX生成)

int main(void)
{
  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the System clock */
  SystemClock_Config(); // 系统时钟配置 (CubeMX生成)

  /* Initialize all configured peripherals */
  MX_GPIO_Init();      // GPIO初始化 (CubeMX生成)
  MX_ADC1_Init();      // ADC1初始化 (CubeMX生成)
  MX_USART2_UART_Init(); // USART2初始化 (CubeMX生成)

  uint32_t adc_raw_value;
  float voltage;
  char uart_buf[50];

  while (1)
  {
    /* 1. 启动ADC转换 */
    HAL_ADC_Start(&hadc1);

    /* 2. 等待ADC转换完成 */
    // 超时时间设置为100ms,可以根据实际情况调整
    if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
    {
      /* 3. 获取ADC转换结果 */
      adc_raw_value = HAL_ADC_GetValue(&hadc1);

      /* 4. 将原始ADC值转换为实际电压 (假设ADC参考电压为3.3V,12位分辨率) */
      // Vout = (ADC_Value / Max_ADC_Value) * Vref
      // 对于12位ADC,Max_ADC_Value = 4095
      voltage = (float)adc_raw_value / 4095.0f * 3.3f;

      /* 5. 打印结果到UART */
      sprintf(uart_buf, "ADC Raw: %lu, Voltage: %.3fV\r\n", adc_raw_value, voltage);
      HAL_UART_Transmit(&huart2, (uint8_t*)uart_buf, strlen(uart_buf), HAL_MAX_DELAY);
    }

    /* 6. 停止ADC (可选,如果是非连续模式) */
    HAL_ADC_Stop(&hadc1);

    /* 延时,以便观察结果,避免UART输出过快 */
    HAL_Delay(500); // 每500ms读取一次
  }
}

// 注意:MX_ADC1_Init(), MX_USART2_UART_Init(), SystemClock_Config(), MX_GPIO_Init()
// 这些函数通常由STM32CubeMX自动生成,并放在main.c或其他独立的.c文件中。
// 它们会配置ADC和UART的寄存器,包括时钟、引脚、模式等。

/* 示例:MX_ADC1_Init() 结构概要 (CubeMX生成的一部分) */
/*
void MX_ADC1_Init(void)
{
  ADC_ChannelConfTypeDef sConfig = {0};

  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_DIV4; // 时钟预分频
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;     // 12位分辨率
  hadc1.Init.ScanConvMode = DISABLE;              // 单通道,不扫描
  hadc1.Init.ContinuousConvMode = DISABLE;        // 单次转换模式
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; // 软件触发
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.NbrOfConversion = 1;
  hadc1.Init.DMAContinuousRequests = DISABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }

  // 配置ADC通道
  sConfig.Channel = ADC_CHANNEL_0;          // 选择通道0
  sConfig.Rank = 1;                         // 序列中的第一个
  sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES; // 采样时间
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
}
*/

ADC DMA示例 (更高效):

如果需要连续、高速地采集数据,使用DMA是非常高效的方式,可以减轻CPU的负担。

c 复制代码
#include "main.h"
#include "string.h"

extern ADC_HandleTypeDef hadc1;
extern UART_HandleTypeDef huart2;

// DMA缓冲区,用于存储ADC转换结果
#define ADC_BUF_SIZE 10 // 假设我们采集10个样本
uint16_t adc_dma_buffer[ADC_BUF_SIZE];

void MX_ADC1_Init(void);
void MX_USART2_UART_Init(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_ADC1_Init(); // 确保在CubeMX中配置ADC的DMA请求
  MX_USART2_UART_Init();

  char uart_buf[100];

  /* 启动ADC DMA传输 */
  // 一旦启动,ADC会持续转换并将结果通过DMA存入adc_dma_buffer
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_BUF_SIZE);

  while (1)
  {
    /* 在这里,你可以执行其他任务,ADC转换在后台通过DMA进行 */

    /* 例如,周期性地检查DMA缓冲区数据 */
    // 注意:如果DMA模式是Circular,数据会不断更新
    // 如果是Normal,需要在HAL_ADC_ConvCpltCallback中重新启动DMA

    // 简单地打印第一个值作为示例
    sprintf(uart_buf, "ADC DMA Raw (first sample): %u\r\n", adc_dma_buffer[0]);
    HAL_UART_Transmit(&huart2, (uint8_t*)uart_buf, strlen(uart_buf), HAL_MAX_DELAY);

    HAL_Delay(100); // 延时
  }
}

// ADC转换完成回调函数 (在stm32f4xx_it.c中实现)
// 当ADC的DMA传输完成一半或全部完成时,会调用此函数
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
  if(hadc->Instance == ADC1)
  {
    // 可以在这里处理ADC转换完成的数据
    // 例如,对adc_dma_buffer中的所有数据进行处理
    // 如果DMA模式是Normal,你需要在这里再次启动DMA传输:
    // HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_BUF_SIZE);
  }
}

// ADC半转换完成回调函数 (仅在DMA模式下)
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
  if(hadc->Instance == ADC1)
  {
    // 可以在这里处理ADC转换到一半的数据 (例如,处理前半部分的缓冲区)
  }
}

2. STM32 DAC实际代码流程示例

假设我们要使用DAC1的通道1(PA4引脚)来输出一个简单的三角波。

所需硬件:

  • STM32开发板
  • 示波器(用于观察DAC输出波形)

代码流程概述:

  1. CubeMX配置:
    • 启用DAC1,选择Channel 1。
    • 配置DAC的时钟。
    • 选择DAC触发源(例如,TIM6)。
    • 启用TIM6,配置其周期以控制DAC的更新速率。
    • 生成代码。
  2. HAL库代码实现:
    • 包含头文件。
    • 定义DAC句柄和定时器句柄。
    • 定义三角波数据数组。
    • main函数中,执行DAC和定时器的初始化。
    • 启动DAC波形生成。

代码示例:

c 复制代码
#include "main.h"
#include "math.h" // 用于生成正弦波,这里我们生成三角波,也可以手动定义

// DAC句柄和定时器句柄 (CubeMX生成)
extern DAC_HandleTypeDef hdac; // 假设DAC1通道1
extern TIM_HandleTypeDef htim6; // 假设使用TIM6作为DAC触发源

void MX_DAC_Init(void);      // DAC初始化函数声明
void MX_TIM6_Init(void);     // TIM6初始化函数声明

// 定义三角波数据 (例如,12位分辨率DAC,0-4095)
// 这里为了简化,我们定义一个简单的上升和下降序列
#define TRIANGLE_WAVE_SIZE 20
uint16_t TriangleWave[TRIANGLE_WAVE_SIZE] = {
    0, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, // 上升
    1800, 1600, 1400, 1200, 1000, 800, 600, 400, 200, 0   // 下降
};

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DAC_Init();   // 确保在CubeMX中配置DAC使用TIM6触发和DMA
  MX_TIM6_Init();

  /* 启动定时器 */
  HAL_TIM_Base_Start(&htim6);

  /* 启动DAC,通过DMA传输三角波数据 */
  // HAL_DAC_Start_DMA(DAC句柄, DAC通道, 数据缓冲区, 缓冲区大小, 数据对齐方式)
  // DAC_CHANNEL_1对应PA4
  // HAL_DAC_Start_DMA 会将TIM6的更新事件作为触发源,每次触发传输一个数据到DAC
  if (HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)TriangleWave, TRIANGLE_WAVE_SIZE, DAC_ALIGN_12B_R) != HAL_OK)
  {
    Error_Handler();
  }

  while (1)
  {
    // DAC和定时器在后台自动运行,生成三角波
    // 你可以在这里执行其他任务
    HAL_Delay(10);
  }
}

// 注意:MX_DAC_Init() 和 MX_TIM6_Init() 也会由CubeMX自动生成。

/* 示例:MX_DAC_Init() 结构概要 (CubeMX生成的一部分) */
/*
void MX_DAC_Init(void)
{
  DAC_ChannelConfTypeDef sConfig = {0};

  hdac.Instance = DAC;
  if (HAL_DAC_Init(&hdac) != HAL_OK)
  {
    Error_Handler();
  }

  sConfig.DAC_Trigger = DAC_TRIGGER_T6_TRGO; // 定时器6触发
  sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE; // 使能输出缓冲
  if (HAL_DAC_ConfigChannel(&hdac, &sConfig, DAC_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
}
*/

/* 示例:MX_TIM6_Init() 结构概要 (CubeMX生成的一部分) */
/*
void MX_TIM6_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  htim6.Instance = TIM6;
  htim6.Init.Prescaler = 0; // 根据系统时钟和期望频率设置
  htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim6.Init.Period = 100 - 1; // 周期,例如100个计数触发一次
  htim6.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  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();
  }
}
*/

DAC软件模式示例:

如果不需要复杂的波形,或者只需要输出一个固定的模拟电压,可以直接通过软件写入。

c 复制代码
#include "main.h"

extern DAC_HandleTypeDef hdac;

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DAC_Init();

  /* 启动DAC (非DMA模式) */
  if (HAL_DAC_Start(&hdac, DAC_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }

  uint16_t dac_value = 0; // 0-4095 对应 0V-3.3V (假设Vref=3.3V)

  while (1)
  {
    /* 设置DAC输出电压 */
    // 这里我们简单地让DAC输出电压在0V到3.3V之间循环变化
    HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value);

    dac_value += 100; // 每次增加100
    if (dac_value > 4095)
    {
      dac_value = 0;
    }

    HAL_Delay(50); // 延时50ms
  }
}

总结:

  • CubeMX是STM32开发中非常重要的工具,它可以大大简化外设的初始化配置,并自动生成大量的HAL库代码。在实际项目中,强烈推荐首先使用CubeMX进行配置。
  • ADC和DAC的使用核心在于:
    • ADC: 启动转换 (HAL_ADC_StartHAL_ADC_Start_DMA),等待转换完成 (HAL_ADC_PollForConversion 或利用回调函数),获取结果 (HAL_ADC_GetValue)。
    • DAC: 启动输出 (HAL_DAC_StartHAL_DAC_Start_DMA),通过软件或DMA提供数据 (HAL_DAC_SetValue 或 DMA传输)。
  • DMA在处理连续数据流(如ADC采集和DAC波形生成)时非常关键,它可以显著提高系统效率,减少CPU的干预。
  • 回调函数 (HAL_ADC_ConvCpltCallback, HAL_DAC_ConvCpltCallback 等) 是HAL库中处理中断和DMA完成事件的重要机制,可以方便地在数据准备就绪时执行自定义逻辑。