第七讲ADDA模块
本讲总览
这一讲主要解决 4 个问题:
-
ADC到底在做什么,为什么单片机明明处理数字量,却还能“读电压”。 -
CubeMX里 ADC 的几个核心参数分别是什么意思,比如分辨率、对齐方式、采样时间。 -
课堂里常见的 3 种采样思路:轮询、
DMA连续搬运、定时器触发 +DMA块处理。 -
DAC怎么配合DMA输出一条比较平滑的正弦波。
这篇笔记尽量保留课堂原代码思路,不改你上课时的写法,重点补两类内容:
-
这段配置和代码现在在干什么
-
为什么课堂上这里要这样配
复习时先抓住 6 个结论:
-
ADC是把模拟量变成数字量,DAC是把数字量再变回模拟量。 -
12位ADC一共有4096个量化等级,对应数字范围通常是0 ~ 4095。 -
参考电压
Vref很重要,它决定“满量程”对应多少电压。 -
轮询法最容易看懂,但最占
CPU。 -
DMA的价值不是“更高级”,而是“让 CPU 少管搬运,专注处理结果”。 -
DAC + DMA + 定时器的本质,是按照固定节奏把查找表里的数据一项一项送出去。
AD 和 DA 分别是什么
-
ADC:Analog to Digital Converter,模数转换,把模拟电压转成数字值。 -
DAC:Digital to Analog Converter,数模转换,把数字值转成模拟电压。
可以先这样理解:
-
传感器、电位器、麦克风这类外部世界给出来的,很多是模拟量。
-
单片机内部运算、判断、通信,更擅长处理的是数字量。
所以:
-
想“读”外面的电压,要靠
ADC -
想“输出”一个变化电压或波形,要靠
DAC
模拟 vs. 数字
想象一下调节收音机的音量旋钮和按计算器的数字键:
-
模拟 (Analog):像音量旋钮,可以在一个范围内连续变化,有无限多个可能值。
-
数字 (Digital):像计算器按键,只有有限的、离散的状态,比如
0、1、2...。
ADC 的任务,就是把连续变化的模拟信号先“采样”,再“量化”为数字值。
分辨率
分辨率决定了 ADC 能把输入电压切成多少份,也就是它能区分多细的变化。
比如:
-
8位 ADC:2^8 = 256级 -
10位 ADC:2^10 = 1024级 -
12位 ADC:2^12 = 4096级
所以一个 12 位 ADC:
-
数字结果通常是
0 ~ 4095 -
总共可以表示
4096个量化等级
如果参考电压是 3.3V,那么 1 级大约对应:
3.3 / 4096 ≈ 0.000805V ≈ 0.8mV
复习结论
-
位数越高,能分得越细。
-
课堂里最常见的是
12位 ADC。 -
分辨率高,不代表一切都更准,还要看参考电压、采样时间、噪声和硬件电路。
参考电压 Vref
参考电压可以理解成 ADC 测量时使用的“标尺上限”。
如果 Vref = 3.3V,那么 ADC 会把 0 ~ 3.3V 这一段映射成数字值。
常用关系可以记成:
ADC结果 ≈ Vin / Vref × 4095
Vin ≈ ADC结果 / 4095 × Vref
课堂代码里也常写成:
voltage = (float)adc_val * 3.3f / 4096.0f;
这类写法本质上是在做“按 4096 级近似换算”,平时做电压显示完全够用。
在很多 STM32 单片机里:
-
Vref往往和VDDA有关系 -
如果
VDDA不稳,ADC 结果也会跟着漂
为什么会漂
最核心的一句就是:
ADC结果 ≈ Vin / Vref × 4095
所以 ADC 看的是输入电压和参考电压的比值,不是只看 Vin 本身。
如果很多板子上 Vref 实际上就和 VDDA 相关,那 VDDA 一变,分母就在变:
-
Vin不变 -
Vref / VDDA变了 -
最后 ADC 数字结果就会漂
可以把它理解成:
-
Vref像尺子 -
Vin像被测长度 -
如果尺子刻度自己在变,量出来的结果当然也会变
为什么 Vref 会漂移
常见原因就记这几个:
-
电源本身有波动
-
板上有噪声和纹波
-
负载变化把
3.3V拉动了 -
温度变化让基准源或稳压器有偏移
复习结论
-
Vref由硬件参考源决定,很多时候就是VDDA。 -
Vref不稳,本质上就是 ADC 的“标尺”不稳。 -
Vref决定 ADC 的测量范围上限。 -
电源不稳、参考不稳,ADC 再高位数也不一定测得准。
采样率
采样率表示 ADC 每秒钟做多少次采样,单位常见为:
-
SPS -
Hz
根据奈奎斯特采样定理:
-
采样率至少要大于被测信号最高频率的
2倍 -
否则会失真,甚至出现混叠
比如你想采一个变化比较快的波形:
-
采样太慢,只能看到“几个点”
-
采样足够快,波形轮廓才会更接近真实情况
复习结论
-
采样率越高,越容易捕捉快速变化的模拟信号。
-
但采样率越高,数据量也越大,后续处理压力也会上来。
硬件连接先记 4 件事
在真正配 ADC 之前,硬件上先确认下面几件事:
-
模拟信号要接到真正支持
ADC的引脚上。 -
信号源的
GND必须和开发板GND共地。 -
输入电压不能超过芯片允许范围,通常不要高于
Vref/VDDA。 -
如果信号是电位器、传感器分压这类来源,先想一下它的输出阻抗高不高,这会直接影响后面采样时间怎么选。
复习结论
ADC读不准,不一定是代码问题,很多时候是接线、共地、输入范围或信号源阻抗的问题。
ADC 工作过程的直观理解
下面这些图主要是在讲一件事:
-
模拟信号本来是连续的
-
ADC 是按固定时刻去“取样”
-
每次取样后再量化成一个离散的数字值




CubeMX 里的 ADC 基础配置
先把 ADC 引脚开出来

先确认两件事:
-
你用的是哪个
ADC,比如ADC1 -
你读的是哪个通道,对应到哪个引脚
分辨率

这里决定结果有多少位。
初学阶段直接记:
-
常用:
12-bit -
返回值范围:
0 ~ 4095
右对齐

右对齐的意思不是“结果更准”,而是结果放在寄存器里的位置不同。
对 12 位 ADC 来说:
-
右对齐更常用
-
你直接读出来看数值更直观
比如测到十进制 1000,右对齐后就老老实实在低位部分,处理起来更顺手。
单次转换

单次转换很好理解:
-
你启动一次
-
它转换一次
-
读完结果结束
这类模式适合:
-
偶尔测一次
-
初学先把流程跑通
采样时间 / 转换周期
![]()
这里要区分两个概念:
-
采样时间:给内部采样电容“充电取样”的时间
-
转换时间:把采到的电压变成数字结果的时间
更长的采样时间,通常更有利于高阻输入或信号源驱动能力弱的场景,因为采样会更稳定。
所以更稳妥的理解是:
-
不是简单记成“越长越准”
-
而是“采样时间太短时,某些输入源可能采不稳”
复习结论
-
12-bit + 右对齐 + 单次转换是最适合入门先跑通的组合之一。 -
真正调不出 ADC 时,优先查:通道、引脚、触发方式、DMA、采样时间。
ADC 时钟 ADCCLK
老师课件里把这一点讲得比较细,这里顺手补上。
ADC 不是直接按主频工作的,它有自己的工作时钟。常见理解方式是:
ADCCLK = ADC时钟源 / 预分频
所以你在 CubeMX 里除了点 ADC 本身的参数,还要留意:
-
时钟源从哪来
-
分频选了多少
-
最终算出来的
ADCCLK有没有超过芯片手册限制
一个很实用的关系是:
总转换时间 ≈ 采样时间 + 固定转换时间
而固定转换时间本身又和 ADCCLK 有关。
所以:
-
ADCCLK越高,单次转换通常越快 -
但不能一味拉高,超过芯片允许上限会出问题
这里怎么记最省事
-
先查芯片手册允许的
ADC最大时钟。 -
在不超限制的前提下,选一个比较稳妥的分频值。
-
不要只盯着“快不快”,还要和采样时间一起看。
多通道扫描时,CubeMX 里还要多看 3 个点
如果你后面不只是读一个通道,而是一次要读多个通道,那么除了开 ADC 之外,还会多出几项关键配置。
1. Scan Conversion Mode
-
单通道:通常
Disabled -
多通道顺序采样:通常
Enabled
一旦开了扫描模式,就要继续配置:
-
Number Of Conversion -
每个
Rank对应哪个通道 -
每个通道各自的
Sampling Time
也就是说,多通道不是“多勾几个引脚”就完了,而是要明确:
第1个采谁 -> 第2个采谁 -> 第3个采谁
2. EOC Selection
这个在刚学时特别容易忽略。
它决定“什么时候算转换结束”:
-
End of single conversion:每转完一个通道就算一次结束 -
End of sequence conversion:整个扫描序列全部转完才算结束
如果你是:
-
单通道轮询
-
或者简单单次转换
前者更直观。
如果你是:
-
多通道扫描
-
配合
DMA一次搬整组数据
那通常更适合:
End of sequence conversion
3. Discontinuous Mode
这个模式不是最常用的,但知道它干什么就行:
-
它可以把一个扫描序列拆成几小组
-
每次触发只转其中一组
学习阶段先记:
-
没特殊需求先不要开
-
先把普通扫描模式吃透更重要
外部触发怎么理解
前面你看到的轮询法,本质上是软件自己调用:
HAL_ADC_Start(...)
这就是软件触发。
但如果想让采样更稳定、更固定,就经常会改成:
- 用定时器的某个事件来触发
ADC
老师课件里最核心的点是:
-
Timer x Trigger Out event (TRGO)是最常用的触发源 -
触发边沿通常选
Rising edge
什么时候用软件触发,什么时候用定时器触发
-
轮询法、简单调试:软件触发更直接
-
固定频率采样、配合
DMA:定时器触发更像正式方案
一句话区分
-
软件触发:什么时候采样,由代码走到哪决定
-
定时器触发:什么时候采样,由硬件定时节拍决定
所以如果你关心的是:
-
“能不能先读到值” → 软件触发足够
-
“是不是固定频率采到的” → 更应该看定时器触发
还有一个很容易踩的点:
-
如果你已经准备用定时器外部触发
-
那么
Continuous Conversion Mode往往就不能再按“纯连续转换”那套想
更实用的理解是:
- 想要“每到一个定时点只触发一次序列”,通常要让触发节奏由定时器接管
课堂里的头文件和模式切换
#include "adc_app.h"
#include "adc.h"
#include "math.h"
#include "dac.h"
#include "tim.h"
#include "usart_app.h"
#include "string.h"
#include "usart.h"
// 1 轮询
// 2 DMA连续转换
// 3 DMA TIM 多通道采集
#define ADC_MODE (1)
这段的核心意思就是:
-
同一套工程里,保留 3 种实验方式
-
通过修改
ADC_MODE,切换不同实现
这种写法很适合学习阶段:
-
好对比
-
不容易把 3 套思路完全写散
方法一:轮询
适合什么时候
-
先学流程
-
先看懂
ADC最基础的使用 -
采样频率不高,对实时性要求不强
代码
#if ADC_MODE == 1
__IO uint32_t adc_val; // 用于存储计算后的 ADC 值
__IO float voltage; // 用于存储计算后的电压值
void adc_task(void)
{
HAL_ADC_Start(&hadc1);
if (HAL_ADC_PollForConversion(&hadc1, 1000) == HAL_OK)
{
adc_val = HAL_ADC_GetValue(&hadc1);
voltage = (float)adc_val * 3.3f / 4096.0f;
my_printf(&huart1, "ADC Value: %lu, Voltage: %.2fV\n", adc_val, voltage);
}
else
{
// 转换超时或出错处理
}
}
这段流程在干什么
-
HAL_ADC_Start(&hadc1):启动一次 ADC 转换。 -
HAL_ADC_PollForConversion(...):原地等,直到转换完成。 -
HAL_ADC_GetValue(...):把结果读出来。 -
按
3.3V和12位分辨率换算成电压。 -
用串口把结果发出去。
这种方法的优缺点
-
优点:最好懂,最适合入门。
-
缺点:CPU 会一直等着 ADC,阻塞感很强。
一句话总结:
- 轮询法是“ADC 没干完,我就不走”。
方法二:ADC + DMA 连续搬运
这类方法的核心思路
不要每采一次就让 CPU 去读一次寄存器,而是:
-
ADC 不断转换
-
DMA 自动把结果搬到数组里
-
CPU 过一会儿再统一处理
这样就能明显减轻 CPU 的搬运压力。

为什么这里推荐配置成 word



先记结论:
- 课堂这套写法里,把 DMA 缓冲区定义成
uint32_t,再按word传输,是更省事也更不容易绕晕自己的配法。
原因主要有 3 个:
-
HAL_ADC_Start_DMA()这个接口本身就更偏向按uint32_t *去传地址。 -
用
uint32_t缓冲区时,内存对齐更自然,和很多例程写法一致。 -
虽然 ADC 有效数据常常只用低
12位,但“存储容器”写宽一点,代码更统一。
这种方法在 CubeMX 里通常怎么配
如果你想做的是“ADC 一直采,DMA 一直搬,CPU 定时来读缓冲区”,那配置思路通常是:
-
Continuous Conversion Mode:Enabled -
DMA Mode:Circular -
Memory Increment:开启 -
Peripheral Increment:关闭 -
Data Width:常见推荐Peripheral: Word,Memory: Word -
一般不靠中断处理数据,而是主循环或定时任务自己来读缓冲区
也就是说,这一类方法的关键词就是:
连续转换 + Circular DMA + 后台搬运 + CPU定时处理
代码
#elif ADC_MODE == 2
#define ADC_DMA_BUFFER_SIZE 32
uint32_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];
__IO uint32_t adc_val;
__IO float voltage;
void adc_dma_init(void)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_DMA_BUFFER_SIZE);
}
void adc_task(void)
{
uint32_t adc_sum = 0;
for(uint16_t i = 0; i < ADC_DMA_BUFFER_SIZE; i++)
{
adc_sum += adc_dma_buffer[i];
}
adc_val = adc_sum / ADC_DMA_BUFFER_SIZE;
voltage = ((float)adc_val * 3.3f) / 4096.0f;
my_printf(&huart1, "Average ADC: %lu, Voltage: %.2fV\n", adc_val, voltage);
}
这段代码现在在干什么
-
adc_dma_init():只负责把ADC + DMA跑起来。 -
adc_dma_buffer[]:DMA 自动往里面填采样结果。 -
adc_task():CPU 定期过来,把这一批数据求和再取平均。
这时候 CPU 的工作从“每次都去搬数据”变成了“用的时候来处理结果”。
为什么缓冲区定义成 uint32_t,不是 uint16_t
这里不是在说“ADC 结果有 32 位”,而是在说:
- 这套 DMA/HAL 配法下,直接用
uint32_t缓冲区更顺手、更统一。
如果你把缓冲区定义成 uint16_t,有时也能工作,但会带来两个问题:
-
看代码的人容易混淆“DMA 宽度”和“ADC 有效位宽”
-
某些例程里还要额外解释强转、对齐和搬运单位
所以学习阶段更建议直接记住:
uint32_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_DMA_BUFFER_SIZE);
然后处理时,按有效低位去理解它就行。
ADC_DMA_BUFFER_SIZE 为什么不是直接写 sizeof(adc_dma_buffer)
这个问题很关键。
先给结论:
-
HAL_ADC_Start_DMA()第 3 个参数要的是传输单元个数 -
sizeof(adc_dma_buffer)得到的是字节数
如果:
uint32_t adc_dma_buffer[32];
那么:
sizeof(adc_dma_buffer) == 128
因为:
-
32个元素 -
每个
uint32_t占4字节 -
总字节数就是
128
但 DMA 这里想要的是“搬 32 个数据单元”,不是“搬 128 个字节”。
所以写:
#define ADC_DMA_BUFFER_SIZE 32
或者更通用一点写成:
#define ADC_DMA_BUFFER_SIZE (sizeof(adc_dma_buffer) / sizeof(adc_dma_buffer[0]))
都可以。
在 main 里初始化

复习时记一句:
-
MX_ADC1_Init()是把外设配置好 -
adc_dma_init()才是真正把 DMA 采样跑起来
方法三:定时器触发 + DMA + 块处理
前面两种方法各有优劣:
-
轮询法:简单,但效率低
-
DMA 连续法:省 CPU,但数据处理时机不够可控
如果你想要的是:
-
按固定频率采样
-
采一整块数据后再统一处理
那就可以上:
-
定时器触发
-
DMA 搬运
-
中断通知“这一块采完了”
这就是更像“正式采集系统”的思路。







这一套流程怎么理解
可以按下面这条链去记:
定时器到点 -> 触发 ADC -> ADC 结果进 DMA缓冲区 -> 一块数据采满 -> 中断置标志 -> 主循环处理
这一套在 CubeMX 里和方法二最大的区别
这点非常值得单独记一下。
方法二更像:
-
ADC持续不断地转 -
DMA Circular一直覆盖缓冲区 -
CPU自己找时机去读
方法三更像:
-
定时器决定采样节奏
-
ADC按触发去执行一次次采样 / 一次次序列 -
DMA先搬满一整块 -
搬满后触发中断,通知 CPU “这一块好了”
所以在配置上常见会变成:
-
External Trigger Source:选某个Timer x TRGO -
External Trigger Edge:常用Rising edge -
Continuous Conversion Mode:通常Disabled -
DMA Mode:常用Normal -
多通道时
Scan Mode:Enabled -
多通道配合
DMA时EOC Selection:更适合End of sequence conversion -
NVIC:启用对应的DMA中断,必要时启用ADC global interrupt
一句话区分:
-
方法二重在“持续搬运”
-
方法三重在“整块采集、整块处理”
这段代码要特别看懂什么
void adc_task(void)
{
// 一次数据转换 3 + 12.5 =15.5个ADC时钟周期
// 一次数据转换 15.5 / 22.5 = 0.68us
// 1000个数据转换需要 1000 * 10 = 10000us = 10ms
// while(!AdcConvEnd);
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));
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val_buffer, BUFFER_SIZE);
__HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);
AdcConvEnd = 0;
}
}
这段代码里最容易忽略的一点是:
dac_val_buffer[i] = adc_val_buffer[i * 2 + 1];
这通常说明当前 DMA 缓冲区里存的是多通道交错数据。
比如如果你开了两个通道:
ch1, ch2, ch1, ch2, ch1, ch2 ...
那么:
-
i * 2可能取的是第一个通道 -
i * 2 + 1可能取的是第二个通道
所以这里本质上是在:
-
从交错排列的 ADC 原始数据里
-
把其中一个通道单独抽出来
__HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT); 是什么
这行是在关闭 DMA 的半传输中断。
意思就是:
-
我不想采到一半就被打断处理
-
我只关心“整块缓冲区采满”的时刻
这对于“按块处理”很常见。
老师课件里还专门提到:
-
HT是半传输中断 -
TC是全传输完成中断
如果你后面想做更高实时性的“乒乓缓冲”处理,才会开始认真用:
-
HAL_ADC_ConvHalfCpltCallback(...) -
HAL_ADC_ConvCpltCallback(...)
而你当前这套课堂代码,更接近“先关掉半传输,只在整块完成后处理”。
这种方法的特点
-
优点:采样节奏更稳定,适合固定频率采集。
-
优点:更适合后续做滤波、波形分析、批量串口输出。
-
缺点:理解门槛更高,要同时想清楚定时器、ADC、DMA、中断这 4 个东西怎么配合。
一句话总结:
- 这套方案更像真正项目里会用的采集框架。
触发定时器怎么配,先抓 3 个参数
不管你是定时器触发 ADC,还是定时器触发 DAC,核心都绕不开这 3 个参数:
-
PSC:预分频 -
ARR:自动重装载值 -
TRGO:触发输出事件选择
一个最常见的关系是:
Counter Clock = Timer Clock / (PSC + 1)
Update Event Frequency = Counter Clock / (ARR + 1)
然后再把:
TRGO = Update Event
送给 ADC 或 DAC 当触发源。
复习结论
-
你想要多高的采样频率或波形更新频率,本质上就是在定
PSC和ARR。 -
TRGO如果没配对,ADC/DAC外部触发那边就算选了定时器也不会按预期工作。
DAC + DMA 输出正弦波
前面一直在讲“把模拟量读进来”,这里换个方向,讲“把数字量送出去变成模拟波形”。
先补 4 个 CubeMX 里的配置理解点:
1. 输出缓冲 Output Buffer
一般先记结论:
- 大多数时候建议
Enable
因为它的作用主要是:
-
提高驱动能力
-
降低输出阻抗
-
让输出电压更稳
除非你有很特殊的功耗或负载要求,不然先开着更省心。
2. 触发源 Trigger
如果你只是想手动给一个固定电压:
- 可以走软件触发
如果你想让 DAC 稳定、连续地按节奏吐出波形:
- 通常选某个
Timer x Trigger Out event
这也是你文里 TIM6 + DAC + DMA 那套方案真正成立的关键。
3. 内建波形和自定义波形不要混着想
有些 STM32 的 DAC 自带:
-
噪声波
-
三角波
但如果你现在要输出的是:
-
正弦波
-
任意波形
那重点就不是开内建波形,而是:
-
准备查找表
-
让
DMA去搬
也就是说:
-
内建波形适合简单演示
-
查找表 + DMA 适合你现在这类正弦波输出
4. DAC DMA 为什么常用 Half Word
这里和 ADC DMA 不一样,老师课件里给的建议非常明确:
-
ADC DMA常见推荐Word -
DAC DMA如果是12位输出查找表,常见推荐Half Word
原因很简单:
-
你的波形表通常是
uint16_t -
DAC的12位数据寄存器写入也正好适合16位宽度去搬
所以对 DAC + DMA 这套来说,更顺手的理解是:
Memory: Half Word
Peripheral: Half Word
再配合:
-
Memory Increment:开启 -
Peripheral Increment:关闭 -
Mode:循环输出波形时常用Circular
核心思路是:
-
先在内存里准备一张正弦波查找表
-
定时器按固定频率触发
-
DMA每次把一个点送到DAC -
所有点连起来,就形成了一条平滑波形





生成正弦波查找表
#define SINE_SAMPLES 100
#define DAC_MAX_VALUE 4095
uint16_t SineWave[SINE_SAMPLES];
void Generate_Sine_Wave(uint16_t* buffer, uint32_t samples, uint16_t amplitude, float phase_shift)
{
float step = 2.0f * 3.14159f / samples;
for(uint32_t i = 0; i < samples; i++)
{
float sine_value = sinf(i * step + phase_shift);
buffer[i] = (uint16_t)((sine_value * amplitude) + (DAC_MAX_VALUE / 2.0f));
if (buffer[i] > DAC_MAX_VALUE) buffer[i] = DAC_MAX_VALUE;
}
}
这段函数做的事非常单纯:
-
sinf(...)先算出标准正弦值 -
再把范围从
-1 ~ 1映射到0 ~ 4095 -
最后把每个点存进
SineWave[]
也就是说:
-
SineWave[]不是随便的数组 -
它本质上就是一张“波形样本表”
初始化
void dac_sin_init(void)
{
Generate_Sine_Wave(SineWave, SINE_SAMPLES, DAC_MAX_VALUE / 2, 0.0f);
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t *)SineWave, SINE_SAMPLES, DAC_ALIGN_12B_R);
}
这 3 行分别在干什么
-
Generate_Sine_Wave(...)
先把正弦波数据准备好。 -
HAL_TIM_Base_Start(&htim6)
启动一个“定时节拍器”。 -
HAL_DAC_Start_DMA(...)
让 DMA 按节拍把查找表数据不断送给 DAC。
输出频率怎么理解
一个很好记的近似关系:
输出波形频率 ≈ 定时器触发频率 / SINE_SAMPLES
比如:
-
定时器每秒触发
10000次 -
一张表有
100个点
那么输出频率大约就是:
10000 / 100 = 100Hz
所以你后面想改波形频率,通常会从两个地方下手:
-
改定时器触发频率
-
改一个周期内的采样点数
复习结论
-
DAC自己不会“凭空画波”,它只是按你给的数据输出电压。 -
查找表越密、定时器节拍越稳,输出波形看起来越平滑。
三种方法怎么选
-
方法一:轮询
最适合先学会、先跑通、偶尔测一次。 -
方法二:ADC + DMA 连续搬运
最适合一直采、减少CPU搬运、做平均值或持续监测。 -
方法三:定时器触发 + DMA + 块处理
最适合固定频率采样、整块分析、波形采集和更正式的项目方案。
如果只想记最顺手的判断:
-
先学会:用轮询
-
先实用:用连续
DMA -
先专业:用定时器触发 +
DMA
本讲最后串起来
这一讲可以按下面这条主线记:
-
ADC负责把外部模拟量采进来 -
DMA负责减少 CPU 搬运压力 -
定时器负责给采样或输出提供稳定节奏 -
DAC负责把数字表格重新变成模拟波形
如果只想记最实用的学习顺序,可以按这个顺序复习:
-
先吃透
ADC的单次轮询 -
再看
ADC + DMA为什么省 CPU -
再理解“定时器触发 + DMA + 块处理”为什么更像正式方案
-
最后把
DAC + DMA当成“反过来播放数组里的波形”
这样 ADDA 这一讲就不只是“会点按钮配 CubeMX”,而是真的知道每一层在干什么。