好的,收到您的请求。您的笔记内容非常详尽且专业,我将严格遵循您的要求,保持所有代码和图片不变,对语言进行适当润色,重点在于优化和美化整体排版,整理出一个逻辑清晰、阅读体验更佳的技术文档。
STM32 HAL 库 AD/DA 应用深度解析
本文档旨在系统性地介绍在 STM32CubeMX 环境下,从基础到高级配置和使用 ADC (模数转换器) 与 DAC (数模转换器) 的多种方案。
目录
1. ADC (模数转换)的三种实现方案
1.1 方案一:轮询法 (Polling) - 简单直接
工作原理
轮询法是最基础的 ADC 数据获取方式。其过程完全由 CPU 主动发起和等待,简单直观。
生活化类比:去信箱取信
- 启动:你走到楼下信箱 (
HAL_ADC_Start)。- 等待:你站在信箱旁一直等,直到邮递员把信投进去 (
HAL_ADC_PollForConversion)。- 读取:你打开信箱,取出信件 (
HAL_ADC_GetValue)。这种方式的缺点是,在等待期间 CPU 被完全占用,无法处理其他事务,效率较低。但对于功能验证或简单应用,不失为一种快速的实现方法。
CubeMX 配置
- 选择 ADC 引脚:在 Pinout & Configuration 视图中,选择一个支持 ADC 的引脚(如
PC0),并将其配置为ADC1_IN10。

- 配置 ADC 参数:在左侧
Analog→ADC1中,参数基本保持默认。只需根据外部电路的阻抗,选择一个合适的采样时间 (Sampling Time) 即可。

代码实现与解析
__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 的转换结果搬运到内存中。
核心思路:
- 配置 ADC 工作在连续转换模式 (Continuous Conversion Mode)。
- 配置 DMA 通道为循环模式 (Circular Mode),让它在 ADC 每次转换完成后,自动将结果搬运到内存缓冲区数组。当数组写满后,DMA 会自动回到数组开头继续写入。
- CPU 不再等待单次转换,而是在主循环或定时任务中,按需、定期地去读取 DMA 缓冲区的内容,并通常进行求平均值等处理以获得稳定读数。
CubeMX 配置
-
配置 DMA:在
ADC1的DMA Settings标签页,点击Add添加ADC1的 DMA 请求。- Mode:
Circular(循环模式)。 - Data Width:
Word。原因:STM32 的外设寄存器通常是 32 位的,使用Word(4字节) 可以保证最高效的内存对齐访问,且与我们定义的uint32_t缓冲区类型匹配。

- Mode:
-
配置 ADC 参数:
- 启用
Continuous Conversion Mode(连续转换模式)。 - 启用
DMA Continuous Requests(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 分析),此方案是最佳选择。
生活化类比:机器人采集样本
- 下达指令:你命令一个机器人 (DMA+ADC) 去采集指定数量 (
BUFFER_SIZE) 的样本 (HAL_ADC_Start_DMA)。- 自动工作:一个节拍器 (Timer) 每响一次,机器人就采集一个样本。
- 完成报告:机器人集满所有样本后,会举起一个牌子 (触发 DMA 传输完成中断,设置标志位
AdcConvEnd) 并停下工作。- 处理并重启:你 (CPU) 看到牌子后,去取走所有样本进行处理。处理完毕后,再次命令机器人开始新一轮的采集。
CubeMX 配置
-
配置定时器 (如 TIM3) 作为触发源:
- Clock Source:
Internal Clock。 - PSC & ARR: 计算并设置分频系数 (PSC) 和自动重装载值 (ARR) 以获得所需的触发频率。例如,要实现 10kHz 触发频率,在 90MHz 定时器时钟下,可设
PSC=90-1,ARR=100-1。 - Trigger Event Selection (TRGO):
Update Event。

- Clock Source:
-
修改 ADC 的 DMA 模式:
- Mode:
Normal(普通模式),因为我们每次只传输一个数据块。

- Mode:
-
修改 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 全局中断:在
NVIC Settings中勾选ADC1, ADC2 and ADC3 global interrupts。

代码实现与解析
#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 资源。
生活化类比:自动钢琴演奏
- 乐谱 (查找表):预先在内存中生成一个数组,存储一个完整周期正弦波的离散数字点。
- 节拍器 (Timer):配置一个定时器以固定频率触发 DAC。
- 自动翻页机 (DMA):配置 DMA 在每次收到节拍器信号后,自动从“乐谱”中取出下一个音符(数据点),送给演奏家。
- 演奏家 (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 配置
-
配置定时器 (如 TIM6):
- 启用并设置频率,此频率决定了采样点的输出速率。
- 重要公式:
正弦波频率 = 定时器触发频率 / 每周期采样点数 - Trigger Output (TRGO):
Update Event。

-
配置 DAC:
- 启用 DAC 通道。
- Trigger: 选择
Timer 6 Trigger Out event。

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

- Direction:
“启动自动演奏”:代码实现
/**
* @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需设置为实际的通道数。

代码修改
当启用多通道扫描后,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);
}
}