7 ADDA模块

ADDA 模块

定位问题:ADC 和 DAC 分别解决什么问题?三种 ADC 采集方式的 CPU 占用为什么不同?正弦波为什么用查表而不是实时算?


走进 AD/DA 的奇妙世界

很多东西都可以通过电压反映——温度、光照、声音、压力,传感器把物理量变成电压,单片机要读懂这个电压,就需要 ADC。反过来,单片机要控制模拟器件,就需要 DAC 把数字值变回电压。

  • ADC(模数转换):把连续变化的模拟电压 → 离散的数字值,让单片机能处理

  • DAC(数模转换):把数字值 → 模拟电压,让单片机能驱动模拟器件


ADC 基础概念

模拟 vs. 数字

  • 模拟:像音量旋钮,可以连续变化,有无限多个可能的值

  • 数字:像计算器按键,只有有限的离散状态(0, 1, 2…)

ADC 的任务就是把模拟信号"采样"并"量化"成数字值。

分辨率

分辨率决定 ADC 能把模拟电压"切"成多少份,用"位"表示。

分辨率越高,测量越精确,但转换速度可能越慢,成本也越高。

GD32 ADC 支持 12 位 → 2¹² = 4096 个级别 → 最小分辨电压 = 3.3V / 4096 ≈ 0.8mV

参考电压 Vref

ADC 的"标尺",定义了能测的最大电压。输入电压不能超过 Vref。

 数字值 = 输入电压 / Vref × (2^分辨率 - 1)

GD32 通常 Vref = 3.3V(连接到 Vdda)。

采样率

ADC 每秒进行多少次转换,单位 SPS(Samples Per Second)。

做信号测量需要考虑——奈奎斯特采样定理:采样率必须至少是被测信号最高频率的 2 倍,否则信号失真(混叠)。


硬件连接:捕捉模拟信号

ADC引脚连接示意图

上图:ADC 引脚在开发板上的位置。

ADC引脚标注

上图:引脚编号与 ADC 通道对应关系(如 PA0 → ADC1_IN0)。

找到 ADC 输入引脚:查阅开发板原理图或引脚定义,在 CubeMX 中可配置为 ADC 输入通道。

ADC连接方式示意

上图:电位器分压接法,模拟信号源连接到 ADC 引脚的典型方式。

GND共地连接示意

上图:GND 共地连接,模拟信号源的地线必须和单片机地线接在一起。

连接模拟信号源:将信号源(电位器、传感器分压电路等)连接到选定的 ADC 引脚。

连接地线(GND):模拟信号源的地线必须与 GD32 共地,这是保证测量准确的基础。

(可选)连接 Vref:通常直接用板子的 3.3V 供电即可。

注意:输入电压绝对不能超过 0V ~ Vref(3.3V)范围。GND 必须接!


ADC 轮询法:最直接的读取方式

蓝桥杯单片机就是轮询读取,是最简单的方案。

想象去楼下信箱取信:

  1. 你走到信箱旁 → HAL_ADC_Start(&hadc1) 启动一次转换

  2. 你站那一直等,直到邮递员把信投进去 → HAL_ADC_PollForConversion 循环检查完成标志,直到超时

  3. 投完后你打开信箱取出信件 → adc_val = HAL_ADC_GetValue(&hadc1) 读取结果

等待期间 CPU 基本被"卡住"了,不能干别的事。简单但效率低,只适合简单验证场景。

代码实现

先说代码再说配置:

 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. 换算电压(可选)
         voltage = (float)adc_val * 3.3f / 4096.0f;
 ​
         my_printf(&huart1, "ADC Value: %lu, Voltage: %.2fV\n", adc_val, voltage);
     }
     else
     {
         // 转换超时处理
     }
 ​
     // 5. 若 CubeMX 配置为连续转换模式,读完需要手动停止
     // HAL_ADC_Stop(&hadc1);
 }

逻辑分解:

  1. HAL_ADC_Start:通知 ADC 硬件启动一次转换

  2. HAL_ADC_PollForConversion:不断检查 EOC(End of Conversion)标志位,阻塞函数,等待期间 CPU 卡在这里

  3. HAL_ADC_GetValue:转换完成后从数据寄存器读结果(0~4095)

  4. 电压换算:adc_val / 4096.0 × 3.3V

  5. 单次转换模式 ADC 完成后自动停止,连续模式需手动调 HAL_ADC_Stop

关键变量

  • __IO uint32_t adc_val__IOvolatile,提示编译器不要优化这个变量(中断/DMA 模式下很重要)

  • __IO float voltage:浮点电压值

  • extern ADC_HandleTypeDef hadc1:所有 ADC 操作通过这个句柄进行

轮询法的局限性:等待期间反复检查状态,无法执行其他任务。只适用于实时性要求不高的简单场景。

CubeMX 配置

CubeMX ADC基础配置界面

上图:ADC 基础参数配置界面,分辨率选 12 bits,数据对齐选 Right alignment,转换模式选 Single。

配置要点(课件补充)

ADC 模式与分辨率

  • 分辨率选 12 bits(最精确)

  • 数据对齐选 Right alignment(右对齐,读出值即为 0~4095)

    • Left alignment:数据存在高位,读出后需右移 4 位
  • 转换模式:Single(单次)适合轮询;Continuous(连续)适合 DMA

ADC 时钟配置

  • ADCCLK = ADC 时钟源 / 预分频值

  • 频率越高转换越快,但不能超过芯片手册规定的最大值

    • F1 系列最大 14MHz,F4 系列可达 36MHz

    • GD32F470 查手册确认,参数不能照抄

  • 总转换时间 = 采样时间 + N × (1/ADCCLK),N 为分辨率位数

ADC 采样时间

  • 信号源阻抗越高,需要越长的采样时间让内部电容充电

    • 低阻抗(运放输出 <1kΩ):1.5 或 7.5 cycles

    • 中阻抗(电位器 1k~50kΩ):28.5 或 55.5 cycles

    • 高阻抗(>50kΩ):71.5 或 239.5 cycles,或加前置运放跟随器

  • 采样时间过短 → 结果偏低;过长 → 降低最大采样率

然后 Generate Code:

CubeMX生成代码

上图:点击 Generate Code 后 CubeMX 生成工程代码。

运行结果

轮询法串口输出结果

上图:串口助手显示 ADC 数值和对应电压,轮询法运行结果。


ADC DMA + 定时处理:解放 CPU 的初步尝试

无脑的转化,无脑的搬运。这是适合打控制的方案(电赛测控类场景很常用)。

请出强大帮手 DMA(Direct Memory Access)。思路是:

  1. 配置 ADC 工作在连续转换模式

  2. 配置 DMA 通道,ADC 每完成一次转换后自动将结果搬运到内存缓冲区

  3. DMA 配置为循环模式(Circular Mode),缓冲区写满后自动从头覆盖

  4. 不启用中断,CPU 在自己的定时任务中定期读取缓冲区

  5. 对缓冲区中的多个采样值求平均得到更稳定的读数

代码实现(单通道)

 #define ADC_DMA_BUFFER_SIZE 32
 uint32_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE]; // 推荐 uint32_t 而非 uint16_t
 __IO uint32_t adc_val;
 __IO float    voltage;
 ​
 // 初始化(调用一次,DMA 后台持续运行)
 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);
 }

问:能写成 sizeof(adc_dma_buffer) 吗?

 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, sizeof(adc_dma_buffer)); // ❌

不行HAL_ADC_Start_DMA 第三个参数是元素个数,不是字节数。sizeof(adc_dma_buffer) 返回字节数(uint32_t[32] = 128 字节),而不是 32 个元素。应该用 ADC_DMA_BUFFER_SIZE

为什么缓冲区用 uint32_t 而不是 uint16_t

STM32/GD32 的 ADC 数据寄存器(DR)是 32 位的,即使有效数据只有 12 位。DMA 以 Word(32位)传输可以最高效地匹配硬件,保证内存对齐。如果用 uint16_t 缓冲区:DMA 每次写 32 位会覆盖相邻两个 uint16_t 元素,通常能工作但严格来说是未定义行为。推荐定义为 uint32_t,处理时取低16位

 uint32_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];
 // 处理时只用低16位
 adc_sum += (uint16_t)adc_dma_buffer[i];

如果坚持用 uint16_t 缓冲区,则需在 CubeMX 中将 DMA 内存宽度改为 Half Word(16位)

CubeMX 配置

CubeMX DMA模式ADC配置

上图:ADC DMA Settings 配置界面,添加 DMA 请求并设置参数。

ADC DMA 配置要点(课件补充)

  • 在 ADC 的 DMA Settings 标签页点 Add 添加 DMA 请求

  • Mode:Circular(连续采样核心配置,自动循环)

  • Increment Address:Memory 勾选,Peripheral 不勾选

  • Data Width:Peripheral: Word,Memory: Word(推荐)

  • DMA 配置与触发方式、连续转换模式、NVIC 设置紧密关联,需协同配置

记得回到之前配置图片所示(DMA 模式和轮询法的 CubeMX 配置有区别):

DMA模式与轮询法配置区别

上图:与轮询法配置对比,DMA 模式需要额外启用 DMA 通道。

关键变量说明

  • adc_dma_buffer:DMA 持续写入 ADC 结果的内存区域,CPU 定期读这里

  • adc_val:对缓冲区求平均后的值

  • voltage:换算后的电压

优缺点:相比轮询极大降低 CPU 占用,无需处理中断,实现简单。但数据处理不是实时的,读取时缓冲区中混合了不同时刻的采样值——拿来求均值没问题,但不知道这批数据对应哪个时间窗口,不能做频率分析。

运行结果

DMA轮询法运行结果

上图:Mode 2 串口输出平均 ADC 值和电压,数据相对稳定。


ADC 精准采样:定时器触发 + DMA + 块处理

信号和电源题就用这种方案

轮询法效率低,DMA+定时处理法数据处理不够实时。当需要以固定频率采样并对一批数据块集中处理时,结合定时器、DMA 和中断。

32 里的定时器不单单做定时器使用,它还可以作为 ADC/DAC 的触发源(TRGO 信号)。

思路:

  1. 定时器以固定频率产生触发信号(TRGO),启动 ADC 转换序列

  2. DMA在每次 ADC 转换完成后,将结果从 ADC 数据寄存器搬到内存缓冲区,设置为 Normal 模式(采满 BUFFER_SIZE 个点后停止)

  3. DMA 采满后触发 TC 中断(Transfer Complete)

  4. 中断回调里:停止 DMA,设置标志位 AdcConvEnd = 1

  5. 后台任务检测到标志位后:处理数据 → 清除标志 → 重启 DMA

类比:你命令机器人(DMA+ADC)去收集 1000 个样本。收集完毕后机器人举起牌子(设置 Flag)并停下来。你(CPU)看到牌子后走过去取走所有样本进行处理,处理完再命令机器人开始新一轮。

这种方式允许在采集间隙处理数据,但处理和重启 DMA 期间可能丢失连续信号,适合对数据块进行分析处理而非严格连续实时场景。

定时器基础与配置

定时器时钟源

通常选内部时钟(Internal Clock),频率与 APB 总线时钟有关(如 APB1 是 72MHz,TIM3 时钟基频通常也是 72MHz)。

预分频器(PSC)

 Counter Clock = Timer Clock / (PSC + 1)

为什么是 PSC+1:计数器从 0 数到 PSC,共数了 PSC+1 次才产生一个计数周期,所以实际分频比是 PSC+1。

例:Timer Clock = 72MHz,PSC = 71 → Counter Clock = 72MHz / 72 = 1MHz(每 1µs 计一次数)

自动重装载寄存器(ARR)

 触发频率 = Counter Clock / (ARR + 1)

为什么是 ARR+1:计数器从 0 数到 ARR,到达 ARR 时产生更新事件(UEV),随即重新从 0 开始,共数了 ARR+1 次。

例:Counter Clock = 1MHz,想每 10ms 触发一次(100Hz):

 100 = 1,000,000 / (ARR + 1)  →  ARR = 9999

通过组合 PSC 和 ARR 可以精确配置出所需触发频率。课件里有对应的计算器工具。

触发输出(TRGO)

将定时器的更新事件(UEV)输出为 TRGO 信号,触发 ADC 或 DAC 外设转换。CubeMX 中设置 Trigger Output 为 “Update Event”。

ADC 时钟配置补充(课件扩充内容)

ADC 有独立的时钟配置:

 ADCCLK = ADC 时钟源 / 预分频值
 转换时间 = 采样时间 + N × (1/ADCCLK)

ADCCLK 越高转换越快,但不能超过芯片手册的 fADC_max。GD32F470 参数查手册,不能直接用 STM32 F4 系列参数。

ADC 外部触发配置(课件补充)

  • Software Trigger:软件调用 HAL_ADC_Start() 手动触发(轮询/简单中断用)

  • Timer x Trigger Out event:定时器 TRGO 触发(精确固定频率采样用)

  • 使用定时器触发时:Continuous Conversion Mode 必须设为 Disabled,否则外部触发只在第一次生效

  • 触发边沿:定时器触发通常选 Rising edge

ADC 与 DMA 配置

CubeMX 配置步骤

CubeMX Mode3 ADC外部触发配置

上图:ADC 外部触发配置,选择定时器 TRGO 作为触发源,边沿选上升沿。

CubeMX Mode3 DMA Normal模式配置

上图:DMA 配置为 Normal 模式(非 Circular),采满后停止触发 TC 中断。

CubeMX Mode3 NVIC中断配置

上图:NVIC 配置,启用 ADC 全局中断和 DMA 通道中断。

CubeMX Mode3 ADC通道配置

上图:ADC 配置为多通道扫描,Rank1 和 Rank2 分别对应两个采集通道。

CubeMX Mode3 完整配置截图

上图:Mode 3 完整 CubeMX 配置概览。

ADC 扫描模式设置(课件补充)

  • Scan Conversion Mode:多通道扫描时设为 Enabled,单通道设为 Disabled

  • Number Of Conversion:扫描通道数(本工程配了 2 个通道)

  • EOC Selection:配合 DMA 读整个序列时选 End of sequence conversion

  • 多通道配置后,DMA 缓冲区中数据交错存储[ch1, ch2, ch1, ch2, ...]

ADC 中断配置(课件补充)

  • 轮询法:无需启用中断

  • DMA + 定时处理(Mode 2):通常无需启用中断

  • TIM + DMA + 中断(Mode 3):需启用 ADC 全局中断 + DMA 通道中断

    • HAL_ADC_ConvCpltCallback:由 DMA TC(全传输完成)中断触发,在此处设标志位

    • 回调内只做快速操作,耗时处理放主循环

代码实现

参考工程代码(ADC_MODE == 3):

 #define BUFFER_SIZE 1000
 ​
 extern DMA_HandleTypeDef hdma_adc1;
 ​
 uint32_t      dac_val_buffer[BUFFER_SIZE / 2];
 __IO uint32_t adc_val_buffer[BUFFER_SIZE];
 __IO uint8_t  AdcConvEnd = 0;
 ​
 void adc_tim_dma_init(void)
 {
     HAL_TIM_Base_Start(&htim3);                                         // 启动触发定时器
     HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_val_buffer, BUFFER_SIZE); // 启动 DMA 采集
     __HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT);                       // 禁用半传输中断
 }
 ​
 // DMA 全传输完成回调(中断里自动触发)
 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
 {
     UNUSED(hadc);
     if(hadc == &hadc1)
     {
         HAL_ADC_Stop_DMA(hadc);  // 停止采集(Normal 模式也会自动停,这里显式停)
         AdcConvEnd = 1;          // 通知后台任务
     }
 }
 ​
 void adc_task(void)
 {
     // 一次转换 3 + 12.5 = 15.5 个 ADC 时钟周期
     // 一次转换 15.5 / 22.5 ≈ 0.68µs
     // 1000 个点 ≈ 0.68ms,但定时器触发间隔更长,实际约 10ms
     if(AdcConvEnd)
     {
         // 多通道交错数据,取通道2(奇数索引)
         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;
     }
 }

i*2+1 的原因:ADC 配了 2 个通道扫描,DMA 数据交错存储:

 adc_val_buffer[0] → 通道1(如外部输入)
 adc_val_buffer[1] → 通道2(如 DA 回采)← i*2+1 取的是这个
 adc_val_buffer[2] → 通道1
 adc_val_buffer[3] → 通道2
 ...

调度器可以改成 10 了,之前 100 是因为什么?

之前做 UART、GPIO 等任务时,没有 ADC 采集需求,100ms 任务周期足够。现在 1000 个点每次约 0.68ms 采完,需要更快处理频率,改成 10ms 更合适。

注意事项

  • DMA 必须设 Normal 模式,采满停止才能触发 TC 中断

  • adc_task 处理时间必须小于数据块采集间隔,否则会延迟下一次采集

  • 处理和重启 DMA 期间 ADC 不在采集,数据会不连续——不适合需要无缝数据流的场景


DAC 基础概念

DAC 把单片机的数字值转换为模拟电压,就像"乐谱演奏家",把数字乐谱变成实际声波。

分辨率

12 位 DAC 将数字值(0~4095)映射到输出电压的 4096 个等级,点数越多输出越平滑。

参考电压 Vref

 输出电压 = Vref × 数字值 / 2^分辨率

对于 12 位 DAC,Vref = 3.3V:

  • 数字值 0 → 输出 ≈ 0V

  • 数字值 4095 → 输出 ≈ 3.3V

  • 数字值 2048 → 输出 ≈ 3.3 × 2048/4096 = 1.65V

建立时间

DAC 将数字值变成电压需要一定时间稳定,这限制了最高输出频率。

输出缓冲

DAC 内置运放缓冲器,开启后:

  • 提高驱动能力:可直接驱动小阻抗负载而电压不跌落

  • 降低输出阻抗:输出更稳定

CubeMX 中通常选 Enable


DAC + DMA 输出正弦波

作业内容包含这一部分

思路(查表法)

实时调 sinf() 是浮点运算,CPU 越忙频率越难保证,时序抖动大。查表法

  1. 提前算好一个周期 100 个点的正弦值存进数组(查找表 LUT)

  2. 定时器以固定频率产生 TRGO 信号(“节拍器”)

  3. DAC 每次接收到触发就从查找表取下一个点输出

  4. DMA 配置为 Circular 模式,自动循环喂数据

  5. 一旦启动,CPU 完全不需要干预,硬件自动循环输出

类比:你把一首歌的乐谱(查找表)交给自动翻页机(DMA),设置一个节拍器(Timer)控制演奏机器人(DAC)。节拍器每响一次,翻页机就把下一个音符喂给机器人,机器人立刻演奏。整个过程自动化,你只需启动它们。

频率控制

 正弦波频率 = 定时器触发频率 / 每周期采样点数

想输出 1kHz,查找表 100 个点:

 定时器触发频率 = 1kHz × 100 = 100kHz(每 10µs 触发一次)

改频率的两种方法:改定时器频率,或改查找表点数

代码实现

 #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); // sinf 比 sin 更快(单精度)
 ​
         // 映射到 DAC 范围(0~4095):
         // 1. sine_value(-1~1) × amplitude → (-amp ~ +amp)
         // 2. 加上中心值(4095/2≈2047.5) → 整体上移到 0~4095
         buffer[i] = (uint16_t)(sine_value * amplitude + DAC_MAX_VALUE / 2.0f);
 ​
         // 钳位:浮点误差可能略超边界
         if(buffer[i] > DAC_MAX_VALUE) buffer[i] = DAC_MAX_VALUE;
     }
 }
 ​
 // 初始化(调用一次,硬件自动循环输出)
 void dac_sin_init(void)
 {
     // 生成最大幅度的正弦波(0~4095)
     Generate_Sine_Wave(SineWave, SINE_SAMPLES, DAC_MAX_VALUE / 2, 0.0f);
 ​
     HAL_TIM_Base_Start(&htim6);  // 启动节拍定时器
 ​
     // 启动 DAC DMA 循环输出
     HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1,
                       (uint32_t*)SineWave, SINE_SAMPLES,
                       DAC_ALIGN_12B_R);
 }
 // 无需后台任务,硬件自动循环

CubeMX 配置

DAC输出缓冲和触发配置

上图:DAC 配置界面,Output Buffer 选 Enable,Trigger 选对应定时器的 TRGO。

DAC DMA配置界面

上图:DAC DMA Settings,Direction 为 Memory to Peripheral,Mode 为 Circular。

DAC DMA数据宽度配置

上图:DMA 数据宽度配置,外设和内存均选 Half Word(匹配 uint16_t 的查找表)。

定时器TIM6配置用于DAC触发

上图:TIM6 配置,PSC 和 ARR 决定触发频率,TRGO 设为 Update Event。

DAC定时器完整配置

上图:定时器和 DAC 联动配置完整截图,确认 TRGO 与 DAC Trigger 匹配。

DAC 输出与触发(课件补充)

  • Output Buffer:推荐 Enable(提高驱动能力)

  • Trigger:手动输出固定电压选 None;DMA 波形输出选定时器 TRGO

DAC DMA 配置(课件补充)

  • Direction:Memory to Peripheral(内存→外设,与 ADC 方向相反)

  • Mode:Circular(循环读取查找表)

  • Data Width:Peripheral: Half Word,Memory: Half Word(匹配 uint16_t SineWave[]

    • 注意:DAC 用 Half Word,ADC 用 Word,两者不同
  • Memory 地址递增,Peripheral 地址固定

  • 纯 DAC 输出通常不需要启用中断

DAC 内建波形(课件补充,了解即可)

  • 部分 STM32/GD32 支持硬件直接生成噪声波(LFSR 伪随机)和三角波

  • 需要定时器触发,不能同时用 DMA 自定义波形

  • 需要复杂波形(正弦、任意波)时,禁用内建波形,用 DMA 模式

多通道采集配置

多通道采集CubeMX配置

上图:ADC 配置两个通道,Rank1 和 Rank2 分别接 DA 回采和外部输入。

DA 回采结构:把 DA 输出引脚(PA4)和 AD 输入引脚(PA5)用导线短接,AD 采集自己 DA 发出的信号。这样可以验证波形频率、幅度,做信号分析。

记得把 PA4 和 PA5 短接了。

Mode 3 的完整 adc_task(含 DA 回采)

 void adc_task(void)
 {
     // 一次转换 3 + 12.5 = 15.5 个 ADC 时钟周期
     // 一次转换 15.5 / 22.5 ≈ 0.68µs
     // 1000 个点 ≈ 0.68ms
     if(AdcConvEnd)
     {
         for(uint16_t i = 0; i < BUFFER_SIZE / 2; i++)
             dac_val_buffer[i] = adc_val_buffer[i * 2 + 1]; // 取通道2(DA回采)
 ​
         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;
     }
 }

运行结果与问题

串口打印波形数据(不连续)

上图:串口助手显示采集到的正弦波数据,可以看到波形不连续。

为什么数据不是连续的?

因为:

 my_printf(&huart1, "{dac}%d\r\n", (int)dac_val_buffer[i]);

每次打印都需要时间(UART 串行发送),500 个点连续打印耗时远超采集时间,所以串口看到的是一段一段的数据,不是平滑连续的波形。这是串口打印本身的限制,不是采集的问题

如果需要看连续波形,应该:用示波器直接看 PA4(DAC 输出引脚),或采集足够多周期的数据后用上位机绘图。

只要能打出两个周期以上的数据,就可以分析频率、幅度等特性。

问:改成 %.1f 打印浮点数会发生什么?

 my_printf(&huart1, "{dac}%.1f\r\n", (float)dac_val_buffer[i]); // ❌

浮点格式化比较消耗栈,容易卡死,不建议打印浮点数。 浮点 printf 会调用软件浮点库,栈消耗大,加上串口本来就慢,容易导致调度器超时或栈溢出。用整数打印,需要时在上位机处理换算。


深入 HAL 库 API

ADC 相关

函数 用途
HAL_ADC_Start 启动一次 ADC 转换(轮询/中断用)
HAL_ADC_Stop 停止转换(连续模式需要)
HAL_ADC_PollForConversion 阻塞等待转换完成,超时返回 TIMEOUT
HAL_ADC_GetValue 读取转换结果(0~4095)
HAL_ADC_Start_DMA DMA 模式启动,第三参数是元素个数不是字节数
HAL_ADC_Stop_DMA 停止 DMA 模式采集
HAL_ADC_ConvCpltCallback DMA TC 中断或转换完成回调,用户重写此函数
__HAL_DMA_DISABLE_IT 宏,禁用指定 DMA 中断(如 DMA_IT_HT 半传输中断)

HAL_ADC_Start_DMA 参数说明:

  • pDatauint32_t* 类型(即使缓冲区是 uint16_t 也需要强转)

  • Length:元素个数,不是字节数

HAL_ADC_ConvCpltCallback

  • 非 DMA 模式:由 ADC EOC 中断触发

  • DMA 模式:由 DMA **TC(全传输完成)**中断触发,这是 DMA 模式处理整块数据的主要入口

__HAL_DMA_DISABLE_IT(&hdma_adc1, DMA_IT_HT):禁用半传输中断,只用全传输中断,防止意外触发回调。

DAC 相关

函数 用途
HAL_DAC_Start 启动指定 DAC 通道(非 DMA 模式)
HAL_DAC_Stop 停止指定 DAC 通道
HAL_DAC_SetValue 手动设置 DAC 输出值(hdac, Channel, Alignment, Data)
HAL_DAC_Start_DMA DMA 模式启动 DAC,循环输出查找表
HAL_DAC_Stop_DMA 停止 DMA 模式 DAC 输出

HAL_DAC_Start_DMA 参数说明:

  • Alignment:数据对齐方式,常用 DAC_ALIGN_12B_R(12位右对齐)

  • pData:同样需要 uint32_t* 强转(即使是 uint16_t 查找表)

TIM 相关

函数 用途
HAL_TIM_Base_Start 启动定时器,开始产生 TRGO 信号

一旦定时器启动并以配置频率产生 UEV(更新事件),TRGO 信号就会触发 ADC 采集或 DAC 输出。


作业

 1. DA 可以发送正弦波、方波、三角波
 2. 按键可以控制波形的周期
 3. 旋钮可以控制波形的峰峰值
 4. 串口最少可以打印出两个周期的波形(可以启动/暂停)
 5. 可以通过串口查询指令进行参数查询:
    - 波形类型(通过 DA 发送的模式变量读取,或通过 AD 数据分析)
    - 频率(不能从定时器直接读出,必须通过 AD 采集数据计算)
    - 峰峰值(不能从定时器直接读出,必须通过 AD 采集数据计算)
 6. 可以通过串口控制指令进行模式切换和参数设置(波形、周期、峰峰值,指令集自定)

这节课对后续比赛/工程的价值

  • PT100 采样板用的就是 ADC + DMA,这节学完能看懂采样板的软件驱动逻辑

  • TIM + DMA + 中断是工业数据采集的标准模式,后续做频率测量、FFT 分析都要用到

  • DAC 正弦波 + ADC 回采是信号验证的经典手段,比赛里可能直接考这个结构

  • 查表法是嵌入式节省 CPU 的通用思路,不只适用于正弦波

  • Mode 2(DMA + 定时处理)适合电赛测控类(慢变化信号、只需均值)

  • Mode 3(块处理)适合分析类(需要对一批数据做整体处理)


速查卡

关键点 一句话
ADC 轮询 Start → PollForConversion(阻塞)→ GetValue
ADC DMA Circular 一次启动,DMA 持续循环填充,CPU 定期取均值,适合测控
ADC TIM+DMA Normal 采满停,TC 中断设标志,任务处理后重启,适合信号分析
sizeof 陷阱 HAL_ADC_Start_DMA 第三参数是元素个数,不是字节数
多通道交错 i*2 取通道1,i*2+1 取通道2
DAC 正弦波 查表 + 定时器触发 + DMA Circular,CPU 启动一次不用管
正弦波频率 = 定时器触发频率 / 每周期点数
DA 回采 PA4(DAC)短接 PA5(ADC),自测信号
不用浮点打印 float printf 耗栈容易卡死,用整数打印
PSC/ARR 公式 Counter Clock = TimerClk/(PSC+1),触发频率 = CC/(ARR+1)

更正timer6配置